summaryrefslogtreecommitdiff
path: root/rules.js
diff options
context:
space:
mode:
authorTor Andersson <tor@ccxvii.net>2024-10-23 23:58:09 +0200
committerTor Andersson <tor@ccxvii.net>2024-10-23 23:58:09 +0200
commit3664a02ae1a5ca368d6e9cb3a8e016177d528238 (patch)
treeff2a52de3fb259e6881cce952752f9017395bc75 /rules.js
parentd1a4e47292f347556a6506c2b20f6adf8e69d619 (diff)
downloadmaria-3664a02ae1a5ca368d6e9cb3a8e016177d528238.tar.gz
annexation, expeditionary corps, set-aside markers
Diffstat (limited to 'rules.js')
-rw-r--r--rules.js445
1 files changed, 407 insertions, 38 deletions
diff --git a/rules.js b/rules.js
index d1c1e75..960a50a 100644
--- a/rules.js
+++ b/rules.js
@@ -4,8 +4,11 @@
/* TODO
+check inactive prompts
check push_undo/clear_undo for political phase and political changes
+add undo steps and pauses for saxony neutral return pieces
+
show who controls which power in player list
set-aside victory marker areas
@@ -81,6 +84,9 @@ const P_AUSTRIA = 3
const P_BAVARIA = 4
const P_SAXONY = 5
+const SET_ASIDE_FRANCE = 4
+const SET_ASIDE_PRUSSIA = 5
+
const power_name = [ "France", "Prussia", "Pragmatic Army", "Austria", "Bavaria", "Saxony" ]
const power_class = [ "france", "prussia", "pragmatic", "austria", "bavaria", "saxony" ]
@@ -92,6 +98,8 @@ const F_SILESIA_ANNEXED = 16
const F_IMPERIAL_ELECTION = 32 // imperial election card revealed!
const F_WAR_OF_JENKINS_EAR = 64 // -1 France TC for one turn
const F_SAXONY_TC_ONCE = 128 // only draw TCs once per turn
+const F_FRANCE_REDUCED = 256
+const F_PRUSSIA_NEUTRAL = 512
const SPADES = 0
const CLUBS = 1
@@ -128,6 +136,15 @@ const all_power_trains = [
[ 29 ],
]
+const all_power_pieces = [
+ [ ...all_power_generals[0], ...all_power_trains[0] ],
+ [ ...all_power_generals[1], ...all_power_trains[1] ],
+ [ ...all_power_generals[2], ...all_power_trains[2] ],
+ [ ...all_power_generals[3], ...all_power_trains[3] ],
+ [ ...all_power_generals[4], ...all_power_trains[4] ],
+ [ ...all_power_generals[5], ...all_power_trains[5] ],
+]
+
const last_piece = 29
const all_hussars = [ 30, 31 ]
@@ -299,6 +316,10 @@ const COSEL = find_city("Cosel")
const MUNCHEN = find_city("München")
const DRESDEN = find_city("Dresden")
+const WOLDENBURG = find_city("Woldenburg")
+const OMANS = find_city("Omans")
+const STEINAMANGER = find_city("Steinamanger")
+
const ENGLAND = find_city("England")
const EAST_PRUSSIA = find_city("East Prussia")
const AUSTRIAN_ITALY_BOX = data.sectors.ItalyA[0]
@@ -694,6 +715,7 @@ all_home_country_fortresses[P_PRAGMATIC] = set_intersect(all_fortresses, data.co
all_home_country_fortresses[P_AUSTRIA] = set_intersect(all_fortresses, data.country.Austria)
all_home_country_fortresses[P_BAVARIA] = set_intersect(all_fortresses, data.country.Bavaria)
all_home_country_fortresses[P_SAXONY] = set_intersect(all_fortresses, data.country.Saxony)
+const all_silesian_fortresses = set_intersect(all_fortresses, data.country.Silesia)
const all_home_country_major_fortresses = []
all_home_country_major_fortresses[P_FRANCE] = set_intersect(data.type.major_fortress, data.country.France)
@@ -703,13 +725,22 @@ all_home_country_major_fortresses[P_AUSTRIA] = set_intersect(data.type.major_for
all_home_country_major_fortresses[P_BAVARIA] = set_intersect(data.type.major_fortress, data.country.Bavaria)
all_home_country_major_fortresses[P_SAXONY] = set_intersect(data.type.major_fortress, data.country.Saxony)
+const all_core_austria_cities = data.country.Austria.filter(is_bohemia_space)
+const all_core_austria_fortresses = all_home_country_fortresses[P_AUSTRIA].filter(is_bohemia_space)
+
+const all_prussian_and_silesian_fortresses = set_union(
+ all_home_country_fortresses[P_PRUSSIA],
+ all_silesian_fortresses
+)
+
+const all_prussian_and_silesian_cities = set_union(data.country.Prussia, data.country.Silesia)
+const all_prussian_and_silesian_and_polish_cities = set_union(data.country.Prussia, data.country.Silesia, data.country.Poland)
+
const all_arenberg_major_fortresses = set_union(
all_home_country_major_fortresses[P_PRAGMATIC],
all_home_country_major_fortresses[P_AUSTRIA]
)
-const all_silesian_fortresses = set_intersect(all_fortresses, data.country.Silesia)
-
const protect_range = []
for (let s of all_fortresses)
make_protect_range(protect_range[s] = [], s, s, 3)
@@ -724,10 +755,12 @@ function make_protect_range(result, start, here, range) {
}
function is_home_country(s) {
- // TODO: Silesia
switch (game.power) {
case P_FRANCE: return set_has(data.country.France, s)
- case P_PRUSSIA: return set_has(data.country.Prussia, s)
+ case P_PRUSSIA:
+ if (has_prussia_annexed_silesia())
+ return set_has(all_prussian_and_silesian_cities, s)
+ return set_has(data.country.Prussia, s)
case P_PRAGMATIC: return set_has(data.country.Netherlands, s)
case P_AUSTRIA: return set_has(data.country.Austria, s)
case P_BAVARIA: return set_has(data.country.Bavaria, s)
@@ -736,10 +769,12 @@ function is_home_country(s) {
}
function is_home_country_for_return(s) {
- // TODO: Silesia
switch (game.power) {
case P_FRANCE: return set_has(data.country.France, s) || set_has(data.country.Bavaria, s)
- case P_PRUSSIA: return set_has(data.country.Prussia, s)
+ case P_PRUSSIA:
+ if (has_prussia_annexed_silesia())
+ return set_has(all_prussian_and_silesian_cities, s)
+ return set_has(data.country.Prussia, s)
case P_PRAGMATIC: return set_has(data.country.Netherlands, s)
case P_AUSTRIA: return set_has(data.country.Austria, s)
case P_BAVARIA: return set_has(data.country.Bavaria, s)
@@ -797,6 +832,11 @@ function set_control_of_fortress(s, pow) {
return
}
+ if (pow === P_FRANCE && game.vp[SET_ASIDE_FRANCE] > 0) {
+ if (set_has(all_core_austria_fortresses, s))
+ return_set_aside_markers(P_FRANCE, SET_ASIDE_FRANCE)
+ }
+
if (is_enemy_home_country(s) || is_friendly_minor_home_country(s) || set_has(all_silesian_fortresses, s))
map_set(game.victory, s, pow)
else
@@ -955,11 +995,11 @@ function count_used_troops() {
return current
}
-function has_general_of_power(to, power) {
- for (let p of all_power_generals[power])
- if (game.pos[p] === to)
- return true
- return false
+function find_general_of_power(s, pow) {
+ for (let p of all_power_generals[pow])
+ if (game.pos[p] === s)
+ return p
+ return -1
}
function has_any_piece(to) {
@@ -1128,9 +1168,30 @@ function goto_action_stage() {
function end_action_stage() {
clear_undo()
+ set_active_to_current_action_stage()
+
if (check_instant_victory())
return
+ if (game.power === P_PRUSSIA && has_prussia_conquered_silesia()) {
+ game.state = "offer_peace"
+ return
+ }
+
+ if (
+ game.power === P_FRANCE &&
+ !(game.flags & F_FRANCE_REDUCED) &&
+ count_french_vp_markers_in_core_austria() > 0 &&
+ france_has_no_generals_in_core_austria()
+ ) {
+ game.state = "france_reduces_military_objectives"
+ return
+ }
+
+ end_action_stage_2()
+}
+
+function end_action_stage_2() {
if (++game.stage === 4)
goto_end_turn()
else
@@ -1822,11 +1883,9 @@ function can_train_move_anywhere(p) {
function can_general_move_anywhere(p) {
let from = game.pos[p]
- console.log("MOVE CHEK", p, piece_abbr[p])
for (let to of data.cities.adjacent[from])
if (can_move_general_to(p, from, to))
return true
- console.log(" fail")
return false
}
@@ -1836,10 +1895,7 @@ states.movement = {
let done_generals = true
let done_trains = true
-console.log("MOVE", game.moved, all_controlled_generals(game.power))
-
for (let p of all_controlled_generals(game.power)) {
- console.log(">P", piece_abbr[p])
if (!set_has(game.moved, p) && is_piece_on_map(p)) {
if (can_general_move_anywhere(p)) {
gen_action_piece(p)
@@ -2010,6 +2066,11 @@ function move_general_to(to, is_force_march) {
}
}
+ // return set-aside prussian victory markers when leaving prussia
+ if (pow === P_PRUSSIA && game.vp[SET_ASIDE_PRUSSIA] > 0)
+ if (!set_has(all_prussian_and_silesian_cities, to))
+ return_set_aside_markers(P_PRUSSIA, SET_ASIDE_PRUSSIA)
+
return stop
}
@@ -2640,9 +2701,17 @@ states.recruit = {
},
}
+function enter_piece_at(p, s) {
+ if (is_general(game.selected))
+ enter_general_at(game.selected, s)
+ else
+ enter_train_at(game.selected, s)
+}
+
function enter_general_at(p, s) {
game.pos[p] = s
- game.troops[p] = 1
+ if (game.troops[p] < 1)
+ game.troops[p] = 1
// remove hussars
for (let p of all_hussars) {
@@ -2659,6 +2728,11 @@ function enter_general_at(p, s) {
game.pos[p] = ELIMINATED
}
}
+
+ // return set-aside prussian victory markers when leaving prussia
+ if (piece_power[p] === P_PRUSSIA && game.vp[SET_ASIDE_PRUSSIA] > 0)
+ if (!set_has(all_prussian_and_silesian_cities, to))
+ return_set_aside_markers(P_PRUSSIA, SET_ASIDE_PRUSSIA)
}
function enter_train_at(p, s) {
@@ -2832,6 +2906,7 @@ states.combat_target = {
game.defender = game.pos[p]
+ // TODO: map_filter
for (let i = 0; i < game.combat.length; i += 2) {
if (game.combat[i] === game.attacker && game.combat[i+1] === game.defender) {
array_remove_pair(game.combat, i)
@@ -3404,6 +3479,11 @@ function goto_retroactive_conquest() {
const power_victory_target = [ 11, 13, 8, 8 ]
+function return_set_aside_markers(pow, ix) {
+ log(power_name[pow] + " returned " + game.vp[ix] + " victory markers to pool.")
+ game.vp[ix] = 0
+}
+
function elector_majority() {
let elector_france = 0
let elector_pragmatic = 0
@@ -3423,9 +3503,11 @@ function elector_majority() {
function count_vp_markers(pow) {
let n = game.vp[pow]
if (pow === P_PRUSSIA) {
+ n += game.vp[SET_ASIDE_PRUSSIA]
if (game.flags & F_SILESIA_ANNEXED) ++n
}
if (pow === P_FRANCE) {
+ n += game.vp[SET_ASIDE_FRANCE]
if (game.flags & F_ITALY_FRANCE) ++n
if (game.flags & F_EMPEROR_FRANCE) ++n
if (elector_majority() === P_FRANCE) ++n
@@ -3924,7 +4006,9 @@ states.political_troops_place = {
}
function goto_mannheim_to_french_control() {
- throw "TODO"
+ log("Mannheim to French control.")
+ map_set(game.elector, MANNHEIM, P_FRANCE)
+ next_execute_political_card()
}
function goto_pragmatic_general_to_england() {
@@ -4002,13 +4086,16 @@ function goto_adjust_political_tracks() {
}
function check_expeditionary_corps(power, space, active) {
- if (active && !has_general_of_power(space, power)) {
+ let p = find_general_of_power(space, power)
+ if (active && p < 0) {
set_active_to_power(power)
+ game.selected = space
game.state = "send_expeditionary_corps_off_map"
return 1
}
- if (!active && has_general_of_power(space, power)) {
+ if (!active && p >= 0) {
set_active_to_power(power)
+ game.selected = p
game.state = "bring_expeditionary_corps_on_map"
return 1
}
@@ -4038,6 +4125,51 @@ function goto_saxony_defection() {
end_adjust_political_tracks()
}
+/* EXPEDITIONARY CORPS */
+
+states.bring_expeditionary_corps_on_map = {
+ inactive: "bring expeditionary corps onto map",
+ prompt() {
+ prompt("Bring expeditionary corps onto map.")
+ view.selected = game.selected
+ let entry = -1
+ if (game.power === P_PRUSSIA)
+ entry = WOLDENBURG
+ if (game.power === P_FRANCE)
+ entry = OMANS
+ if (game.power === P_AUSTRIA)
+ entry = STEINAMANGER
+ if (can_move_piece_to(game.selected, ELIMINATED, entry))
+ gen_action_space(entry)
+ else
+ for (let s of search_nearest_city(entry))
+ gen_action_space(s)
+ },
+ space(s) {
+ log("P" + game.selected + " to S" + s + ".")
+ enter_piece_at(game.selected, s)
+ game.selected = -1
+ goto_expeditionary_corps()
+ },
+}
+
+states.send_expeditionary_corps_off_map = {
+ inactive: "send expeditionary corps off map",
+ prompt() {
+ prompt("Send expeditionary corps off map.")
+ for (let p of all_power_generals[game.power])
+ if (is_piece_on_map_or_eliminated(p))
+ gen_action_piece(p)
+ },
+ piece(p) {
+ if (game.troops[p] === 0)
+ throw "TODO - pay for 2 troops minimum"
+ game.pos[p] = game.selected
+ game.selected = -1
+ goto_expeditionary_corps()
+ },
+}
+
/* SAXONY'S DEFECTION */
function search_nearest_city(p) {
@@ -4150,10 +4282,7 @@ states.saxony_return_foreign_who = {
inactive: "return foreign pieces from Saxony",
prompt() {
prompt("Return pieces to the nearest city in their home country.")
- for (let p of all_power_generals[game.power])
- if (set_has(data.country.Saxony, game.pos[p]))
- gen_action_piece(p)
- for (let p of all_power_trains[game.power])
+ for (let p of all_power_pieces[game.power])
if (set_has(data.country.Saxony, game.pos[p]))
gen_action_piece(p)
},
@@ -4173,10 +4302,7 @@ states.saxony_return_foreign_where = {
},
space(s) {
log(">P" + game.selected + " to S" + s)
- if (is_general(game.selected))
- enter_general_at(game.selected, s)
- else
- enter_train_at(game.selected, s)
+ enter_piece_at(game.selected, s)
game.selected = -1
goto_saxony_return_foreign_pieces()
},
@@ -4187,7 +4313,7 @@ states.saxony_return_home = {
prompt() {
prompt("Return pieces to their set-up cities.")
let done = true
- for (let p of [ SAXONY_GENERAL, SAXONY_TRAIN ]) {
+ for (let p of all_power_pieces[P_SAXONY]) {
if (is_piece_on_map(p) && game.pos[p] !== setup_piece_position[p]) {
gen_action_piece(p)
done = false
@@ -4221,7 +4347,8 @@ function goto_saxony_becomes_austrian_ally() {
return
}
- if (has_general_of_power(game.pos[SAXONY_GENERAL], P_PRUSSIA))
+ // if stacked with prussian general
+ if (find_general_of_power(game.pos[SAXONY_GENERAL], P_PRUSSIA) >= 0)
game.state = "saxony_move_general"
else
end_saxony_neutral()
@@ -4244,17 +4371,259 @@ states.saxony_move_general = {
function end_saxony_neutral() {
game.selected = -1
- if (game.stage === 100)
+
+ // Silesia annexed!
+ if (game.stage > 100) {
+ game.stage -= 100
+ goto_annex_silesia_return_austrian_pieces()
+ return
+ }
+
+ // Political Card event shift
+ if (game.stage === 100) {
end_adjust_political_tracks()
- else
- next_combat()
+ return
+ }
+
+ // Battle Victory shift
+ next_combat()
}
-/* NEUTRALITY */
+/* PRUSSIA ANNEXES SILESIA */
function is_prussia_neutral() {
- // TODO
- return false
+ return game.flags & F_PRUSSIA_NEUTRAL
+}
+
+function has_prussia_annexed_silesia() {
+ return !!(game.flags & F_SILESIA_ANNEXED)
+}
+
+function has_prussia_conquered_silesia() {
+ if (has_prussia_annexed_silesia())
+ return false
+ for (let s of all_silesian_fortresses) {
+ let pow = map_get(game.victory, s)
+ if (pow !== P_PRUSSIA)
+ return false
+ }
+ return true
+}
+
+states.offer_peace = {
+ inactive: "offer peace",
+ prompt() {
+ prompt("Annex Silesia and offer temporary peace with Austria?")
+ view.actions.offer = 1
+ view.actions.pass = 1
+ },
+ offer() {
+ set_active_to_power(P_AUSTRIA)
+ game.state = "accept_peace"
+ },
+ pass() {
+ end_action_stage_2()
+ },
+}
+
+states.accept_peace = {
+ inactive: "accept peace",
+ prompt() {
+ prompt("Accept Prussia's offer of temporary peace to annex Silesia?")
+ view.actions.accept = 1
+ view.actions.deny = 1
+ },
+ accept() {
+ goto_annex_silesia()
+ },
+ deny() {
+ end_action_stage_2()
+ },
+}
+
+function goto_annex_silesia() {
+ log("Silesia Annexed")
+
+ game.flags |= F_SILESIA_ANNEXED
+ game.flags |= F_PRUSSIA_NEUTRAL
+
+ // remove all austrian markers in prussia
+ // set aside half prussian markers in prussia
+ let n = 0
+ for (let s of all_prussian_and_silesian_fortresses) {
+ let pow = map_get(game.victory, s, -1)
+ if (pow === P_AUSTRIA) {
+ map_delete(game.victory, s)
+ }
+ if (pow === P_PRUSSIA) {
+ map_delete(game.victory, s)
+ ++n
+ }
+ }
+ n = (n + 1) >> 1
+ log("Removed all Austrian victory markers.")
+ log("Set aside " + n + " Prussian victory markers.")
+ game.vp[SET_ASIDE_PRUSSIA] = n
+
+ if (is_saxony_prussian()) {
+ log("Saxony shifted to neutral.")
+ game.stage += 100
+ game.saxony = 3
+ goto_saxony_becomes_neutral()
+ return
+ }
+
+ goto_annex_silesia_return_austrian_pieces()
+
+ // return austrian pieces in prussia or Poland
+ // return prussian pieces outside prussia
+ // enter second supply train
+ // prussia is neutral until actions stage after next
+
+ // return markers if prussian piece leaves prussia
+}
+
+function goto_annex_silesia_return_austrian_pieces() {
+ set_active_to_power(P_AUSTRIA)
+ if (power_has_any_piece_in_list(P_AUSTRIA, all_prussian_and_silesian_and_polish_cities))
+ game.state = "silesia_return_austrian_who"
+ else
+ goto_annex_silesia_return_prussian_pieces()
+}
+
+states.silesia_return_austrian_who = {
+ inactive: "return Austrian pieces from Prussia and Poland",
+ prompt() {
+ prompt("Return pieces to the nearest city in their home country.")
+ for (let p of all_power_pieces[P_AUSTRIA])
+ if (set_has(all_prussian_and_silesian_and_polish_cities, game.pos[p]))
+ gen_action_piece(p)
+ },
+ piece(p) {
+ game.selected = p
+ game.state = "silesia_return_austrian_where"
+ },
+}
+
+states.silesia_return_austrian_where = {
+ inactive: "return Austrian pieces from Prussia and Poland",
+ prompt() {
+ prompt("Return pieces to the nearest city in their home country.")
+ view.selected = game.selected
+ for (let s of search_nearest_home_city(game.selected))
+ gen_action_space(s)
+ },
+ space(s) {
+ log(">P" + game.selected + " to S" + s)
+ enter_piece_at(game.selected, s)
+ game.selected = -1
+ goto_annex_silesia_return_austrian_pieces()
+ },
+}
+
+function goto_annex_silesia_return_prussian_pieces() {
+ set_active_to_power(P_PRUSSIA)
+ game.state = "silesia_return_prussian_who"
+}
+
+states.silesia_return_prussian_who = {
+ inactive: "return Austrian pieces from Prussia and Poland",
+ prompt() {
+ prompt("Return pieces to the nearest city in their home country.")
+ let done = true
+ for (let p of all_power_pieces[P_PRUSSIA]) {
+ if (is_piece_on_map(p) && !set_has(all_prussian_and_silesian_cities, game.pos[p])) {
+ gen_action_piece(p)
+ done = false
+ }
+ }
+ if (done)
+ view.actions.next = 1
+ },
+ piece(p) {
+ push_undo()
+ game.selected = p
+ game.state = "silesia_return_prussian_where"
+ },
+ next() {
+ clear_undo()
+ end_action_stage_2()
+ },
+}
+
+states.silesia_return_prussian_where = {
+ inactive: "return Austrian pieces from Prussia and Poland",
+ prompt() {
+ prompt("Return pieces to the nearest city in their home country.")
+ view.selected = game.selected
+ for (let s of search_nearest_home_city(game.selected))
+ gen_action_space(s)
+ },
+ space(s) {
+ log(">P" + game.selected + " to S" + s)
+ enter_piece_at(game.selected, s)
+ game.selected = -1
+ game.state = "silesia_return_prussian_who"
+ },
+}
+
+/* FRANCE REDUCES MILITARY OBJECTIVES */
+
+function count_french_vp_markers_in_core_austria() {
+ let n = 0
+ map_for_each(game.victory, (s, pow) => {
+ if (pow === P_FRANCE && set_has(all_core_austria_cities, s))
+ ++n
+ })
+ return n
+}
+
+function remove_french_vp_markers_in_core_austria() {
+ // TODO: map_filter
+ for (let i = 0; i < game.victory.length; i += 2) {
+ if (game.victory[i+1] === P_FRANCE) {
+ if (set_has(all_core_austria_fortresses, game.victory[i])) {
+ array_remove_pair(game.victory, i)
+ i -= 2
+ }
+ }
+ }
+}
+
+function france_has_no_generals_in_core_austria() {
+ for (let p of all_power_generals[P_FRANCE]) {
+ let s = game.pos[p]
+ if (is_bohemia_space(s) && set_has(data.country.Austria, s))
+ return false
+ }
+ return true
+}
+
+states.france_reduces_military_objectives = {
+ inactive: "reduce military objectives",
+ prompt() {
+ prompt("Reduce military objectives?")
+ view.actions.reduce = 1
+ view.actions.pass = 1
+ },
+ reduce() {
+ game.flags |= F_FRANCE_REDUCED
+ let n = 0
+ for (let s of all_core_austria_fortresses) {
+ let pow = map_get(game.victory, s, -1)
+ if (pow === P_FRANCE) {
+ map_delete(game.victory, s)
+ ++n
+ }
+ }
+ n = (n + 1) >> 1
+ log("France set aside " + n + " victory markers.")
+ game.vp[SET_ASIDE_FRANCE] = n
+ end_action_stage_2()
+ },
+ pass() {
+ end_action_stage_2()
+ },
}
/* SETUP */
@@ -4367,7 +4736,7 @@ const setup_piece_position = [
function make_political_deck() {
let deck41 = [ 0, 1, 2, 3, 4, 5 ]
- let deck42 = [ 6, 7, 8, 9, 10, 11, 25 ]
+ let deck42 = [ 6, 7, 8, 9, 10, 11, 24 ]
let deck43 = [ 12, 13, 14, 15, 16, 17 ]
let deck44 = [ 18, 19, 20, 21, 22, 23 ]
shuffle_bigint(deck41)
@@ -4417,7 +4786,7 @@ exports.setup = function (seed, _scenario, _options) {
turn: 0,
stage: 0,
- vp: [ 0, 0, 0, 0 ], // battle victory points
+ vp: [ 0, 0, 0, 0, 0, 0 ], // battle victory points, set-aside VP
saxony: 2, // political track
russia: 6, // political track
italy: 5, // political track