From 3664a02ae1a5ca368d6e9cb3a8e016177d528238 Mon Sep 17 00:00:00 2001 From: Tor Andersson Date: Wed, 23 Oct 2024 23:58:09 +0200 Subject: annexation, expeditionary corps, set-aside markers --- play.js | 26 ++++ rules.js | 445 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++------ 2 files changed, 433 insertions(+), 38 deletions(-) diff --git a/play.js b/play.js index 5073ee0..578156b 100644 --- a/play.js +++ b/play.js @@ -51,6 +51,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" ] @@ -957,10 +960,15 @@ function layout_retro(s, pow) { function layout_victory_pool(pow, max, x, y) { let n = view.vp[pow] + let m = 0 if (pow === P_PRUSSIA) { + m = view.vp[SET_ASIDE_PRUSSIA] + n += m if (view.flags & F_SILESIA_ANNEXED) ++n } if (pow === P_FRANCE) { + m = view.vp[SET_ASIDE_FRANCE] + n += m if (view.flags & F_ITALY_FRANCE) ++n if (view.flags & F_EMPEROR_FRANCE) ++n } @@ -971,12 +979,25 @@ function layout_victory_pool(pow, max, x, y) { for (let i = 0; i < view.victory.length; i += 2) if (view.victory[i+1] === pow) ++n + for (let i = 0; i < max - n; ++i) { let e = ui.victory[pow][used_victory[pow]++] e.style.left = (x - 16 + (i%5) * 20) + "px" e.style.top = (y - 16 + (i/5|0) * 20) + "px" ui.markers_element.appendChild(e) } + + for (let i = 0; i < m; ++i) { + let e = ui.victory[pow][used_victory[pow]++] + if (pow === P_FRANCE) { + e.style.left = (x - 16 + i * 20) + "px" + e.style.top = (1635 - 16) + "px" + } else { + e.style.left = (x + 80 - 16 - i * 20) + "px" + e.style.top = (19 - 16) + "px" + } + ui.markers_element.appendChild(e) + } } function layout_elector(s, pow) { @@ -1207,6 +1228,11 @@ function on_update() { for (let pow of all_powers) action_button_with_argument("power", pow, power_name[pow]) + action_button("reduce", "Reduce") + action_button("offer", "Offer") + action_button("accept", "Accept") + action_button("deny", "Deny") + action_button("re_enter", "Re-enter") action_button("force_march", "Force march") 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 -- cgit v1.2.3