"use strict" const DEBUG = 1 const R_LOUIS_XV = "Louis XV" const R_FREDERICK = "Frederick" const R_MARIA_THERESA = "Maria Theresa" const R_PLAYER_A = "Player A" const R_PLAYER_B = "Player B" const ROLES_3P = [ R_MARIA_THERESA, R_FREDERICK, R_LOUIS_XV ] const ROLES_2P = [ R_PLAYER_A, R_PLAYER_B ] exports.scenarios = [ "Advanced", "Advanced 2P", "Introductory", "Introductory 2P", ] exports.roles = function (scenario, _options) { switch (scenario) { case "Advanced": case "Introductory": return ROLES_3P case "Advanced 2P": case "Introductory 2P": return ROLES_2P } } const data = require("./data.js") const political_cards = require("./cards.js") var game var view var states = {} /* DATA (SHARED) */ const deck_name = [ "red", "green", "blue", "yellow" ] const suit_name = [ "\u2660", "\u2663", "\u2665", "\u2666", "R" ] const suit_class = [ "spades", "clubs", "hearts", "diamonds", "reserve" ] const suit_letter = [ "S", "C", "H", "D", "R" ] const P_FRANCE = 0 const P_PRUSSIA = 1 const P_PRAGMATIC = 2 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", "france bavaria", "prussia saxony", "austria pragmatic", "austria pragmatic saxony", ] const turn_name = [ "Setup", "Turn 1", "Turn 2", "Turn 3", "Winter 1741", "Turn 4", "Turn 5", "Turn 6", "Winter 1742", "Turn 7", "Turn 8", "Turn 9", "Winter 1743", "Turn 10", "Turn 11", "Turn 12", "Winter 1744", ] const DI_TURN = 0 const DI_A_POWER = 1 const DI_B_POWER = 2 const DI_A_PROMISE = 3 const DI_B_PROMISE = 4 const DI_A_PHASE = 5 const DI_B_PHASE = 6 const F_EMPEROR_FRANCE = 1 const F_EMPEROR_AUSTRIA = 2 const F_ITALY_FRANCE = 4 const F_ITALY_AUSTRIA = 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 F_PRUSSIA_NEUTRAL_2 = 65536 const F_MOVE_FLANDERS = 1024 const F_MOVE_DISPUTE = 2048 const F_INTRODUCTORY = 4096 const F_TWO_PLAYER = 8192 const F_PLAYER_A_PICK = 16384 // picked PC card const F_PLAYER_B_PICK = 32768 // promise validation phases const V_POLITICS = 1 const V_HUSSARS = 2 const V_MOVEMENT = 4 const V_RETREAT = 8 const SPADES = 0 const CLUBS = 1 const HEARTS = 2 const DIAMONDS = 3 const RESERVE = 4 const IMPERIAL_ELECTION = 24 const ELIMINATED = data.cities.name.length const ARENBERG = 17 const BAVARIAN_GENERAL = 18 const SAXONY_GENERAL = 19 const PRUSSIAN_TRAIN_2 = 23 const all_powers = [ 0, 1, 2, 3, 4, 5 ] const all_major_powers = [ 0, 1, 2, 3 ] const all_minor_powers = [ 4, 5 ] const all_power_generals = [ [ 0, 1, 2, 3, 4 ], [ 5, 6, 7, 8 ], [ 9, 10, 11 ], [ 12, 13, 14, 15, 16, 17 ], [ 18 ], [ 19 ], ] const all_power_trains = [ [ 20, 21 ], [ 22, 23 ], [ 24 ], [ 25, 26, 27 ], [ 28 ], [ 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 ] const piece_power = [ P_FRANCE, P_FRANCE, P_FRANCE, P_FRANCE, P_FRANCE, P_PRUSSIA, P_PRUSSIA, P_PRUSSIA, P_PRUSSIA, P_PRAGMATIC, P_PRAGMATIC, P_PRAGMATIC, P_AUSTRIA, P_AUSTRIA, P_AUSTRIA, P_AUSTRIA, P_AUSTRIA, P_AUSTRIA, P_BAVARIA, P_SAXONY, P_FRANCE, P_FRANCE, P_PRUSSIA, P_PRUSSIA, P_PRAGMATIC, P_AUSTRIA, P_AUSTRIA, P_AUSTRIA, P_BAVARIA, P_SAXONY, P_AUSTRIA, P_AUSTRIA ] const piece_rank = [ 1, 2, 3, 4, 5, 1, 2, 3, 4, 1, 2, 3, 1, 2, 3, 4, 5, 6, 1, 1, ] const piece_abbr = [ "F1", "F2", "F3", "F4", "F5", "P1", "P2", "P3", "P4", "PA1", "PA2", "PA3", "A1", "A2", "A3", "A4", "A5", "A6", "B1", "S1", ] const piece_name = [ "Moritz", "Belle-Isle", "Broglie", "Maillebois", "Noailles", "Friedrich", "Schwerin", "Leopold", "Dessauer", "George II", "Cumberland", "Earl of Stair", "Karl", "Traun", "Khevenhüller", "Batthyány", "Neipperg", "Arenberg", "Törring", "Rutowski", "supply train", "supply train", "supply train", "supply train", "supply train", "supply train", "supply train", "supply train", "supply train", "supply train", "hussar", "hussar", ] const piece_log_name = [ "Moritz", "Belle-Isle", "Broglie", "Maillebois", "Noailles", "Friedrich", "Schwerin", "Leopold", "Dessauer", "George II", "Cumberland", "Earl of Stair", "Karl", "Traun", "Khevenhüller", "Batthyány", "Neipperg", "Arenberg", "Törring", "Rutowski", "French ST", "French ST", "Prussian ST", "Prussian ST", "Pragmatic Army ST", "Austrian ST", "Austrian ST", "Austrian ST", "Bavarian ST", "Saxon ST", "Hussars", "Hussars", ] function to_deck(c) { return c >> 7 } function to_suit(c) { return (c >> 4) & 7 } function to_value(c) { if (to_suit(c) === RESERVE) return 8 return c & 15 } /* DATA (LISTS) */ const all_france_generals = all_power_generals[P_FRANCE] const all_france_trains = all_power_trains[P_FRANCE] const all_france_pieces = all_power_pieces[P_FRANCE] const all_prussia_generals = all_power_generals[P_PRUSSIA] const all_prussia_trains = all_power_trains[P_PRUSSIA] const all_prussia_pieces = all_power_pieces[P_PRUSSIA] const all_pragmatic_generals = all_power_generals[P_PRAGMATIC] const all_pragmatic_trains = all_power_trains[P_PRAGMATIC] const all_pragmatic_pieces = all_power_pieces[P_PRAGMATIC] const all_austria_generals = all_power_generals[P_AUSTRIA] const all_austria_trains = all_power_trains[P_AUSTRIA] const all_austria_pieces = all_power_pieces[P_AUSTRIA] const all_bavaria_generals = all_power_generals[P_BAVARIA] const all_bavaria_trains = all_power_trains[P_BAVARIA] const all_bavaria_pieces = all_power_pieces[P_BAVARIA] const all_saxony_generals = all_power_generals[P_SAXONY] const all_saxony_trains = all_power_trains[P_SAXONY] const all_saxony_pieces = all_power_pieces[P_SAXONY] const all_powers_france = [ P_FRANCE ] const all_powers_prussia = [ P_PRUSSIA ] const all_powers_france_prussia = [ P_FRANCE, P_PRUSSIA ] const all_france_prussia_generals = [ all_france_generals, all_prussia_generals ].flat() const all_france_prussia_trains = [ all_france_trains, all_prussia_trains ].flat() const all_france_prussia_pieces = [ all_france_pieces, all_prussia_pieces ].flat() const all_powers_pragmatic = [ P_PRAGMATIC ] const all_powers_france_pragmatic = [ P_FRANCE, P_PRAGMATIC ] const all_france_pragmatic_generals = [ all_france_generals, all_pragmatic_generals ].flat() const all_france_pragmatic_trains = [ all_france_trains, all_pragmatic_trains ].flat() const all_france_pragmatic_pieces = [ all_france_pieces, all_pragmatic_pieces ].flat() const all_powers_prussia_pragmatic = [ P_PRUSSIA, P_PRAGMATIC ] const all_prussia_pragmatic_generals = [ all_prussia_generals, all_pragmatic_generals ].flat() const all_prussia_pragmatic_trains = [ all_prussia_trains, all_pragmatic_trains ].flat() const all_prussia_pragmatic_pieces = [ all_prussia_pieces, all_pragmatic_pieces ].flat() const all_powers_france_prussia_pragmatic = [ P_FRANCE, P_PRUSSIA, P_PRAGMATIC ] const all_france_prussia_pragmatic_generals = [ all_france_generals, all_prussia_generals, all_pragmatic_generals ].flat() const all_france_prussia_pragmatic_trains = [ all_france_trains, all_prussia_trains, all_pragmatic_trains ].flat() const all_france_prussia_pragmatic_pieces = [ all_france_pieces, all_prussia_pieces, all_pragmatic_pieces ].flat() const all_powers_austria = [ P_AUSTRIA ] const all_powers_france_austria = [ P_FRANCE, P_AUSTRIA ] const all_france_austria_generals = [ all_france_generals, all_austria_generals ].flat() const all_france_austria_trains = [ all_france_trains, all_austria_trains ].flat() const all_france_austria_pieces = [ all_france_pieces, all_austria_pieces ].flat() const all_powers_prussia_austria = [ P_PRUSSIA, P_AUSTRIA ] const all_prussia_austria_generals = [ all_prussia_generals, all_austria_generals ].flat() const all_prussia_austria_trains = [ all_prussia_trains, all_austria_trains ].flat() const all_prussia_austria_pieces = [ all_prussia_pieces, all_austria_pieces ].flat() const all_powers_france_prussia_austria = [ P_FRANCE, P_PRUSSIA, P_AUSTRIA ] const all_france_prussia_austria_generals = [ all_france_generals, all_prussia_generals, all_austria_generals ].flat() const all_france_prussia_austria_trains = [ all_france_trains, all_prussia_trains, all_austria_trains ].flat() const all_france_prussia_austria_pieces = [ all_france_pieces, all_prussia_pieces, all_austria_pieces ].flat() const all_powers_pragmatic_austria = [ P_PRAGMATIC, P_AUSTRIA ] const all_pragmatic_austria_generals = [ all_pragmatic_generals, all_austria_generals ].flat() const all_pragmatic_austria_trains = [ all_pragmatic_trains, all_austria_trains ].flat() const all_pragmatic_austria_pieces = [ all_pragmatic_pieces, all_austria_pieces ].flat() const all_powers_france_pragmatic_austria = [ P_FRANCE, P_PRAGMATIC, P_AUSTRIA ] const all_france_pragmatic_austria_generals = [ all_france_generals, all_pragmatic_generals, all_austria_generals ].flat() const all_france_pragmatic_austria_trains = [ all_france_trains, all_pragmatic_trains, all_austria_trains ].flat() const all_france_pragmatic_austria_pieces = [ all_france_pieces, all_pragmatic_pieces, all_austria_pieces ].flat() const all_powers_prussia_pragmatic_austria = [ P_PRUSSIA, P_PRAGMATIC, P_AUSTRIA ] const all_prussia_pragmatic_austria_generals = [ all_prussia_generals, all_pragmatic_generals, all_austria_generals ].flat() const all_prussia_pragmatic_austria_trains = [ all_prussia_trains, all_pragmatic_trains, all_austria_trains ].flat() const all_prussia_pragmatic_austria_pieces = [ all_prussia_pieces, all_pragmatic_pieces, all_austria_pieces ].flat() const all_powers_france_prussia_pragmatic_austria = [ P_FRANCE, P_PRUSSIA, P_PRAGMATIC, P_AUSTRIA ] const all_france_prussia_pragmatic_austria_generals = [ all_france_generals, all_prussia_generals, all_pragmatic_generals, all_austria_generals ].flat() const all_france_prussia_pragmatic_austria_trains = [ all_france_trains, all_prussia_trains, all_pragmatic_trains, all_austria_trains ].flat() const all_france_prussia_pragmatic_austria_pieces = [ all_france_pieces, all_prussia_pieces, all_pragmatic_pieces, all_austria_pieces ].flat() const all_powers_bavaria = [ P_BAVARIA ] const all_powers_france_bavaria = [ P_FRANCE, P_BAVARIA ] const all_france_bavaria_generals = [ all_france_generals, all_bavaria_generals ].flat() const all_france_bavaria_trains = [ all_france_trains, all_bavaria_trains ].flat() const all_france_bavaria_pieces = [ all_france_pieces, all_bavaria_pieces ].flat() const all_powers_prussia_bavaria = [ P_PRUSSIA, P_BAVARIA ] const all_prussia_bavaria_generals = [ all_prussia_generals, all_bavaria_generals ].flat() const all_prussia_bavaria_trains = [ all_prussia_trains, all_bavaria_trains ].flat() const all_prussia_bavaria_pieces = [ all_prussia_pieces, all_bavaria_pieces ].flat() const all_powers_france_prussia_bavaria = [ P_FRANCE, P_PRUSSIA, P_BAVARIA ] const all_france_prussia_bavaria_generals = [ all_france_generals, all_prussia_generals, all_bavaria_generals ].flat() const all_france_prussia_bavaria_trains = [ all_france_trains, all_prussia_trains, all_bavaria_trains ].flat() const all_france_prussia_bavaria_pieces = [ all_france_pieces, all_prussia_pieces, all_bavaria_pieces ].flat() const all_powers_pragmatic_bavaria = [ P_PRAGMATIC, P_BAVARIA ] const all_pragmatic_bavaria_generals = [ all_pragmatic_generals, all_bavaria_generals ].flat() const all_pragmatic_bavaria_trains = [ all_pragmatic_trains, all_bavaria_trains ].flat() const all_pragmatic_bavaria_pieces = [ all_pragmatic_pieces, all_bavaria_pieces ].flat() const all_powers_france_pragmatic_bavaria = [ P_FRANCE, P_PRAGMATIC, P_BAVARIA ] const all_france_pragmatic_bavaria_generals = [ all_france_generals, all_pragmatic_generals, all_bavaria_generals ].flat() const all_france_pragmatic_bavaria_trains = [ all_france_trains, all_pragmatic_trains, all_bavaria_trains ].flat() const all_france_pragmatic_bavaria_pieces = [ all_france_pieces, all_pragmatic_pieces, all_bavaria_pieces ].flat() const all_powers_prussia_pragmatic_bavaria = [ P_PRUSSIA, P_PRAGMATIC, P_BAVARIA ] const all_prussia_pragmatic_bavaria_generals = [ all_prussia_generals, all_pragmatic_generals, all_bavaria_generals ].flat() const all_prussia_pragmatic_bavaria_trains = [ all_prussia_trains, all_pragmatic_trains, all_bavaria_trains ].flat() const all_prussia_pragmatic_bavaria_pieces = [ all_prussia_pieces, all_pragmatic_pieces, all_bavaria_pieces ].flat() const all_powers_france_prussia_pragmatic_bavaria = [ P_FRANCE, P_PRUSSIA, P_PRAGMATIC, P_BAVARIA ] const all_france_prussia_pragmatic_bavaria_generals = [ all_france_generals, all_prussia_generals, all_pragmatic_generals, all_bavaria_generals ].flat() const all_france_prussia_pragmatic_bavaria_trains = [ all_france_trains, all_prussia_trains, all_pragmatic_trains, all_bavaria_trains ].flat() const all_france_prussia_pragmatic_bavaria_pieces = [ all_france_pieces, all_prussia_pieces, all_pragmatic_pieces, all_bavaria_pieces ].flat() const all_powers_austria_bavaria = [ P_AUSTRIA, P_BAVARIA ] const all_austria_bavaria_generals = [ all_austria_generals, all_bavaria_generals ].flat() const all_austria_bavaria_trains = [ all_austria_trains, all_bavaria_trains ].flat() const all_austria_bavaria_pieces = [ all_austria_pieces, all_bavaria_pieces ].flat() const all_powers_france_austria_bavaria = [ P_FRANCE, P_AUSTRIA, P_BAVARIA ] const all_france_austria_bavaria_generals = [ all_france_generals, all_austria_generals, all_bavaria_generals ].flat() const all_france_austria_bavaria_trains = [ all_france_trains, all_austria_trains, all_bavaria_trains ].flat() const all_france_austria_bavaria_pieces = [ all_france_pieces, all_austria_pieces, all_bavaria_pieces ].flat() const all_powers_prussia_austria_bavaria = [ P_PRUSSIA, P_AUSTRIA, P_BAVARIA ] const all_prussia_austria_bavaria_generals = [ all_prussia_generals, all_austria_generals, all_bavaria_generals ].flat() const all_prussia_austria_bavaria_trains = [ all_prussia_trains, all_austria_trains, all_bavaria_trains ].flat() const all_prussia_austria_bavaria_pieces = [ all_prussia_pieces, all_austria_pieces, all_bavaria_pieces ].flat() const all_powers_france_prussia_austria_bavaria = [ P_FRANCE, P_PRUSSIA, P_AUSTRIA, P_BAVARIA ] const all_france_prussia_austria_bavaria_generals = [ all_france_generals, all_prussia_generals, all_austria_generals, all_bavaria_generals ].flat() const all_france_prussia_austria_bavaria_trains = [ all_france_trains, all_prussia_trains, all_austria_trains, all_bavaria_trains ].flat() const all_france_prussia_austria_bavaria_pieces = [ all_france_pieces, all_prussia_pieces, all_austria_pieces, all_bavaria_pieces ].flat() const all_powers_pragmatic_austria_bavaria = [ P_PRAGMATIC, P_AUSTRIA, P_BAVARIA ] const all_pragmatic_austria_bavaria_generals = [ all_pragmatic_generals, all_austria_generals, all_bavaria_generals ].flat() const all_pragmatic_austria_bavaria_trains = [ all_pragmatic_trains, all_austria_trains, all_bavaria_trains ].flat() const all_pragmatic_austria_bavaria_pieces = [ all_pragmatic_pieces, all_austria_pieces, all_bavaria_pieces ].flat() const all_powers_france_pragmatic_austria_bavaria = [ P_FRANCE, P_PRAGMATIC, P_AUSTRIA, P_BAVARIA ] const all_france_pragmatic_austria_bavaria_generals = [ all_france_generals, all_pragmatic_generals, all_austria_generals, all_bavaria_generals ].flat() const all_france_pragmatic_austria_bavaria_trains = [ all_france_trains, all_pragmatic_trains, all_austria_trains, all_bavaria_trains ].flat() const all_france_pragmatic_austria_bavaria_pieces = [ all_france_pieces, all_pragmatic_pieces, all_austria_pieces, all_bavaria_pieces ].flat() const all_powers_prussia_pragmatic_austria_bavaria = [ P_PRUSSIA, P_PRAGMATIC, P_AUSTRIA, P_BAVARIA ] const all_prussia_pragmatic_austria_bavaria_generals = [ all_prussia_generals, all_pragmatic_generals, all_austria_generals, all_bavaria_generals ].flat() const all_prussia_pragmatic_austria_bavaria_trains = [ all_prussia_trains, all_pragmatic_trains, all_austria_trains, all_bavaria_trains ].flat() const all_prussia_pragmatic_austria_bavaria_pieces = [ all_prussia_pieces, all_pragmatic_pieces, all_austria_pieces, all_bavaria_pieces ].flat() const all_powers_france_prussia_pragmatic_austria_bavaria = [ P_FRANCE, P_PRUSSIA, P_PRAGMATIC, P_AUSTRIA, P_BAVARIA ] const all_france_prussia_pragmatic_austria_bavaria_generals = [ all_france_generals, all_prussia_generals, all_pragmatic_generals, all_austria_generals, all_bavaria_generals ].flat() const all_france_prussia_pragmatic_austria_bavaria_trains = [ all_france_trains, all_prussia_trains, all_pragmatic_trains, all_austria_trains, all_bavaria_trains ].flat() const all_france_prussia_pragmatic_austria_bavaria_pieces = [ all_france_pieces, all_prussia_pieces, all_pragmatic_pieces, all_austria_pieces, all_bavaria_pieces ].flat() const all_powers_saxony = [ P_SAXONY ] const all_powers_france_saxony = [ P_FRANCE, P_SAXONY ] const all_france_saxony_generals = [ all_france_generals, all_saxony_generals ].flat() const all_france_saxony_trains = [ all_france_trains, all_saxony_trains ].flat() const all_france_saxony_pieces = [ all_france_pieces, all_saxony_pieces ].flat() const all_powers_prussia_saxony = [ P_PRUSSIA, P_SAXONY ] const all_prussia_saxony_generals = [ all_prussia_generals, all_saxony_generals ].flat() const all_prussia_saxony_trains = [ all_prussia_trains, all_saxony_trains ].flat() const all_prussia_saxony_pieces = [ all_prussia_pieces, all_saxony_pieces ].flat() const all_powers_france_prussia_saxony = [ P_FRANCE, P_PRUSSIA, P_SAXONY ] const all_france_prussia_saxony_generals = [ all_france_generals, all_prussia_generals, all_saxony_generals ].flat() const all_france_prussia_saxony_trains = [ all_france_trains, all_prussia_trains, all_saxony_trains ].flat() const all_france_prussia_saxony_pieces = [ all_france_pieces, all_prussia_pieces, all_saxony_pieces ].flat() const all_powers_pragmatic_saxony = [ P_PRAGMATIC, P_SAXONY ] const all_pragmatic_saxony_generals = [ all_pragmatic_generals, all_saxony_generals ].flat() const all_pragmatic_saxony_trains = [ all_pragmatic_trains, all_saxony_trains ].flat() const all_pragmatic_saxony_pieces = [ all_pragmatic_pieces, all_saxony_pieces ].flat() const all_powers_france_pragmatic_saxony = [ P_FRANCE, P_PRAGMATIC, P_SAXONY ] const all_france_pragmatic_saxony_generals = [ all_france_generals, all_pragmatic_generals, all_saxony_generals ].flat() const all_france_pragmatic_saxony_trains = [ all_france_trains, all_pragmatic_trains, all_saxony_trains ].flat() const all_france_pragmatic_saxony_pieces = [ all_france_pieces, all_pragmatic_pieces, all_saxony_pieces ].flat() const all_powers_prussia_pragmatic_saxony = [ P_PRUSSIA, P_PRAGMATIC, P_SAXONY ] const all_prussia_pragmatic_saxony_generals = [ all_prussia_generals, all_pragmatic_generals, all_saxony_generals ].flat() const all_prussia_pragmatic_saxony_trains = [ all_prussia_trains, all_pragmatic_trains, all_saxony_trains ].flat() const all_prussia_pragmatic_saxony_pieces = [ all_prussia_pieces, all_pragmatic_pieces, all_saxony_pieces ].flat() const all_powers_france_prussia_pragmatic_saxony = [ P_FRANCE, P_PRUSSIA, P_PRAGMATIC, P_SAXONY ] const all_france_prussia_pragmatic_saxony_generals = [ all_france_generals, all_prussia_generals, all_pragmatic_generals, all_saxony_generals ].flat() const all_france_prussia_pragmatic_saxony_trains = [ all_france_trains, all_prussia_trains, all_pragmatic_trains, all_saxony_trains ].flat() const all_france_prussia_pragmatic_saxony_pieces = [ all_france_pieces, all_prussia_pieces, all_pragmatic_pieces, all_saxony_pieces ].flat() const all_powers_austria_saxony = [ P_AUSTRIA, P_SAXONY ] const all_austria_saxony_generals = [ all_austria_generals, all_saxony_generals ].flat() const all_austria_saxony_trains = [ all_austria_trains, all_saxony_trains ].flat() const all_austria_saxony_pieces = [ all_austria_pieces, all_saxony_pieces ].flat() const all_powers_france_austria_saxony = [ P_FRANCE, P_AUSTRIA, P_SAXONY ] const all_france_austria_saxony_generals = [ all_france_generals, all_austria_generals, all_saxony_generals ].flat() const all_france_austria_saxony_trains = [ all_france_trains, all_austria_trains, all_saxony_trains ].flat() const all_france_austria_saxony_pieces = [ all_france_pieces, all_austria_pieces, all_saxony_pieces ].flat() const all_powers_prussia_austria_saxony = [ P_PRUSSIA, P_AUSTRIA, P_SAXONY ] const all_prussia_austria_saxony_generals = [ all_prussia_generals, all_austria_generals, all_saxony_generals ].flat() const all_prussia_austria_saxony_trains = [ all_prussia_trains, all_austria_trains, all_saxony_trains ].flat() const all_prussia_austria_saxony_pieces = [ all_prussia_pieces, all_austria_pieces, all_saxony_pieces ].flat() const all_powers_france_prussia_austria_saxony = [ P_FRANCE, P_PRUSSIA, P_AUSTRIA, P_SAXONY ] const all_france_prussia_austria_saxony_generals = [ all_france_generals, all_prussia_generals, all_austria_generals, all_saxony_generals ].flat() const all_france_prussia_austria_saxony_trains = [ all_france_trains, all_prussia_trains, all_austria_trains, all_saxony_trains ].flat() const all_france_prussia_austria_saxony_pieces = [ all_france_pieces, all_prussia_pieces, all_austria_pieces, all_saxony_pieces ].flat() const all_powers_pragmatic_austria_saxony = [ P_PRAGMATIC, P_AUSTRIA, P_SAXONY ] const all_pragmatic_austria_saxony_generals = [ all_pragmatic_generals, all_austria_generals, all_saxony_generals ].flat() const all_pragmatic_austria_saxony_trains = [ all_pragmatic_trains, all_austria_trains, all_saxony_trains ].flat() const all_pragmatic_austria_saxony_pieces = [ all_pragmatic_pieces, all_austria_pieces, all_saxony_pieces ].flat() const all_powers_france_pragmatic_austria_saxony = [ P_FRANCE, P_PRAGMATIC, P_AUSTRIA, P_SAXONY ] const all_france_pragmatic_austria_saxony_generals = [ all_france_generals, all_pragmatic_generals, all_austria_generals, all_saxony_generals ].flat() const all_france_pragmatic_austria_saxony_trains = [ all_france_trains, all_pragmatic_trains, all_austria_trains, all_saxony_trains ].flat() const all_france_pragmatic_austria_saxony_pieces = [ all_france_pieces, all_pragmatic_pieces, all_austria_pieces, all_saxony_pieces ].flat() const all_powers_prussia_pragmatic_austria_saxony = [ P_PRUSSIA, P_PRAGMATIC, P_AUSTRIA, P_SAXONY ] const all_prussia_pragmatic_austria_saxony_generals = [ all_prussia_generals, all_pragmatic_generals, all_austria_generals, all_saxony_generals ].flat() const all_prussia_pragmatic_austria_saxony_trains = [ all_prussia_trains, all_pragmatic_trains, all_austria_trains, all_saxony_trains ].flat() const all_prussia_pragmatic_austria_saxony_pieces = [ all_prussia_pieces, all_pragmatic_pieces, all_austria_pieces, all_saxony_pieces ].flat() const all_powers_france_prussia_pragmatic_austria_saxony = [ P_FRANCE, P_PRUSSIA, P_PRAGMATIC, P_AUSTRIA, P_SAXONY ] const all_france_prussia_pragmatic_austria_saxony_generals = [ all_france_generals, all_prussia_generals, all_pragmatic_generals, all_austria_generals, all_saxony_generals ].flat() const all_france_prussia_pragmatic_austria_saxony_trains = [ all_france_trains, all_prussia_trains, all_pragmatic_trains, all_austria_trains, all_saxony_trains ].flat() const all_france_prussia_pragmatic_austria_saxony_pieces = [ all_france_pieces, all_prussia_pieces, all_pragmatic_pieces, all_austria_pieces, all_saxony_pieces ].flat() const all_powers_bavaria_saxony = [ P_BAVARIA, P_SAXONY ] const all_bavaria_saxony_generals = [ all_bavaria_generals, all_saxony_generals ].flat() const all_bavaria_saxony_trains = [ all_bavaria_trains, all_saxony_trains ].flat() const all_bavaria_saxony_pieces = [ all_bavaria_pieces, all_saxony_pieces ].flat() const all_powers_france_bavaria_saxony = [ P_FRANCE, P_BAVARIA, P_SAXONY ] const all_france_bavaria_saxony_generals = [ all_france_generals, all_bavaria_generals, all_saxony_generals ].flat() const all_france_bavaria_saxony_trains = [ all_france_trains, all_bavaria_trains, all_saxony_trains ].flat() const all_france_bavaria_saxony_pieces = [ all_france_pieces, all_bavaria_pieces, all_saxony_pieces ].flat() const all_powers_prussia_bavaria_saxony = [ P_PRUSSIA, P_BAVARIA, P_SAXONY ] const all_prussia_bavaria_saxony_generals = [ all_prussia_generals, all_bavaria_generals, all_saxony_generals ].flat() const all_prussia_bavaria_saxony_trains = [ all_prussia_trains, all_bavaria_trains, all_saxony_trains ].flat() const all_prussia_bavaria_saxony_pieces = [ all_prussia_pieces, all_bavaria_pieces, all_saxony_pieces ].flat() const all_powers_france_prussia_bavaria_saxony = [ P_FRANCE, P_PRUSSIA, P_BAVARIA, P_SAXONY ] const all_france_prussia_bavaria_saxony_generals = [ all_france_generals, all_prussia_generals, all_bavaria_generals, all_saxony_generals ].flat() const all_france_prussia_bavaria_saxony_trains = [ all_france_trains, all_prussia_trains, all_bavaria_trains, all_saxony_trains ].flat() const all_france_prussia_bavaria_saxony_pieces = [ all_france_pieces, all_prussia_pieces, all_bavaria_pieces, all_saxony_pieces ].flat() const all_powers_pragmatic_bavaria_saxony = [ P_PRAGMATIC, P_BAVARIA, P_SAXONY ] const all_pragmatic_bavaria_saxony_generals = [ all_pragmatic_generals, all_bavaria_generals, all_saxony_generals ].flat() const all_pragmatic_bavaria_saxony_trains = [ all_pragmatic_trains, all_bavaria_trains, all_saxony_trains ].flat() const all_pragmatic_bavaria_saxony_pieces = [ all_pragmatic_pieces, all_bavaria_pieces, all_saxony_pieces ].flat() const all_powers_france_pragmatic_bavaria_saxony = [ P_FRANCE, P_PRAGMATIC, P_BAVARIA, P_SAXONY ] const all_france_pragmatic_bavaria_saxony_generals = [ all_france_generals, all_pragmatic_generals, all_bavaria_generals, all_saxony_generals ].flat() const all_france_pragmatic_bavaria_saxony_trains = [ all_france_trains, all_pragmatic_trains, all_bavaria_trains, all_saxony_trains ].flat() const all_france_pragmatic_bavaria_saxony_pieces = [ all_france_pieces, all_pragmatic_pieces, all_bavaria_pieces, all_saxony_pieces ].flat() const all_powers_prussia_pragmatic_bavaria_saxony = [ P_PRUSSIA, P_PRAGMATIC, P_BAVARIA, P_SAXONY ] const all_prussia_pragmatic_bavaria_saxony_generals = [ all_prussia_generals, all_pragmatic_generals, all_bavaria_generals, all_saxony_generals ].flat() const all_prussia_pragmatic_bavaria_saxony_trains = [ all_prussia_trains, all_pragmatic_trains, all_bavaria_trains, all_saxony_trains ].flat() const all_prussia_pragmatic_bavaria_saxony_pieces = [ all_prussia_pieces, all_pragmatic_pieces, all_bavaria_pieces, all_saxony_pieces ].flat() const all_powers_france_prussia_pragmatic_bavaria_saxony = [ P_FRANCE, P_PRUSSIA, P_PRAGMATIC, P_BAVARIA, P_SAXONY ] const all_france_prussia_pragmatic_bavaria_saxony_generals = [ all_france_generals, all_prussia_generals, all_pragmatic_generals, all_bavaria_generals, all_saxony_generals ].flat() const all_france_prussia_pragmatic_bavaria_saxony_trains = [ all_france_trains, all_prussia_trains, all_pragmatic_trains, all_bavaria_trains, all_saxony_trains ].flat() const all_france_prussia_pragmatic_bavaria_saxony_pieces = [ all_france_pieces, all_prussia_pieces, all_pragmatic_pieces, all_bavaria_pieces, all_saxony_pieces ].flat() const all_powers_austria_bavaria_saxony = [ P_AUSTRIA, P_BAVARIA, P_SAXONY ] const all_austria_bavaria_saxony_generals = [ all_austria_generals, all_bavaria_generals, all_saxony_generals ].flat() const all_austria_bavaria_saxony_trains = [ all_austria_trains, all_bavaria_trains, all_saxony_trains ].flat() const all_austria_bavaria_saxony_pieces = [ all_austria_pieces, all_bavaria_pieces, all_saxony_pieces ].flat() const all_powers_france_austria_bavaria_saxony = [ P_FRANCE, P_AUSTRIA, P_BAVARIA, P_SAXONY ] const all_france_austria_bavaria_saxony_generals = [ all_france_generals, all_austria_generals, all_bavaria_generals, all_saxony_generals ].flat() const all_france_austria_bavaria_saxony_trains = [ all_france_trains, all_austria_trains, all_bavaria_trains, all_saxony_trains ].flat() const all_france_austria_bavaria_saxony_pieces = [ all_france_pieces, all_austria_pieces, all_bavaria_pieces, all_saxony_pieces ].flat() const all_powers_prussia_austria_bavaria_saxony = [ P_PRUSSIA, P_AUSTRIA, P_BAVARIA, P_SAXONY ] const all_prussia_austria_bavaria_saxony_generals = [ all_prussia_generals, all_austria_generals, all_bavaria_generals, all_saxony_generals ].flat() const all_prussia_austria_bavaria_saxony_trains = [ all_prussia_trains, all_austria_trains, all_bavaria_trains, all_saxony_trains ].flat() const all_prussia_austria_bavaria_saxony_pieces = [ all_prussia_pieces, all_austria_pieces, all_bavaria_pieces, all_saxony_pieces ].flat() const all_powers_france_prussia_austria_bavaria_saxony = [ P_FRANCE, P_PRUSSIA, P_AUSTRIA, P_BAVARIA, P_SAXONY ] const all_france_prussia_austria_bavaria_saxony_generals = [ all_france_generals, all_prussia_generals, all_austria_generals, all_bavaria_generals, all_saxony_generals ].flat() const all_france_prussia_austria_bavaria_saxony_trains = [ all_france_trains, all_prussia_trains, all_austria_trains, all_bavaria_trains, all_saxony_trains ].flat() const all_france_prussia_austria_bavaria_saxony_pieces = [ all_france_pieces, all_prussia_pieces, all_austria_pieces, all_bavaria_pieces, all_saxony_pieces ].flat() const all_powers_pragmatic_austria_bavaria_saxony = [ P_PRAGMATIC, P_AUSTRIA, P_BAVARIA, P_SAXONY ] const all_pragmatic_austria_bavaria_saxony_generals = [ all_pragmatic_generals, all_austria_generals, all_bavaria_generals, all_saxony_generals ].flat() const all_pragmatic_austria_bavaria_saxony_trains = [ all_pragmatic_trains, all_austria_trains, all_bavaria_trains, all_saxony_trains ].flat() const all_pragmatic_austria_bavaria_saxony_pieces = [ all_pragmatic_pieces, all_austria_pieces, all_bavaria_pieces, all_saxony_pieces ].flat() const all_powers_france_pragmatic_austria_bavaria_saxony = [ P_FRANCE, P_PRAGMATIC, P_AUSTRIA, P_BAVARIA, P_SAXONY ] const all_france_pragmatic_austria_bavaria_saxony_generals = [ all_france_generals, all_pragmatic_generals, all_austria_generals, all_bavaria_generals, all_saxony_generals ].flat() const all_france_pragmatic_austria_bavaria_saxony_trains = [ all_france_trains, all_pragmatic_trains, all_austria_trains, all_bavaria_trains, all_saxony_trains ].flat() const all_france_pragmatic_austria_bavaria_saxony_pieces = [ all_france_pieces, all_pragmatic_pieces, all_austria_pieces, all_bavaria_pieces, all_saxony_pieces ].flat() const all_powers_prussia_pragmatic_austria_bavaria_saxony = [ P_PRUSSIA, P_PRAGMATIC, P_AUSTRIA, P_BAVARIA, P_SAXONY ] const all_prussia_pragmatic_austria_bavaria_saxony_generals = [ all_prussia_generals, all_pragmatic_generals, all_austria_generals, all_bavaria_generals, all_saxony_generals ].flat() const all_prussia_pragmatic_austria_bavaria_saxony_trains = [ all_prussia_trains, all_pragmatic_trains, all_austria_trains, all_bavaria_trains, all_saxony_trains ].flat() const all_prussia_pragmatic_austria_bavaria_saxony_pieces = [ all_prussia_pieces, all_pragmatic_pieces, all_austria_pieces, all_bavaria_pieces, all_saxony_pieces ].flat() /* DATA */ function is_major_power(pow) { return pow < 4 } function is_bohemia_space(s) { return s >= 0 && s <= 401 } function is_flanders_space(s) { return s >= 402 && s <= 618 } function is_map_space(s) { return s >= 0 && s <= 618 } function is_piece_on_map(p) { return is_map_space(game.pos[p]) } function is_piece_eliminated(p) { return game.pos[p] === ELIMINATED } function is_piece_on_map_or_eliminated(p) { let s = game.pos[p] return is_map_space(s) || s === ELIMINATED } function find_city(city) { let n = data.cities.name.length let x = -1 for (let c = 0; c < n; ++c) { if (data.cities.name[c] === city) { if (x < 0) x = c else throw "TWO CITIES: " + city } } if (x < 0) throw "CITY NOT FOUND: " + city return x } const TRIER = find_city("Trier") const MAINZ = find_city("Mainz") const KOLN = find_city("Köln") const MANNHEIM = find_city("Mannheim") const LIEGNITZ = find_city("Liegnitz") const GLOGAU = find_city("Glogau") const BRESLAU = find_city("Breslau") const BRIEG = find_city("Brieg") const GLATZ = find_city("Glatz") const NEISSE = find_city("Neisse") 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 GRONINGEN = find_city("Groningen") const ENGLAND = find_city("England") const EAST_PRUSSIA = find_city("East Prussia") const AUSTRIAN_ITALY_BOX = data.sectors.ItalyA[0] const FRENCH_ITALY_BOX = data.sectors.ItalyF[0] const all_electoral_colleges = [ // find_city("England"), find_city("Prag"), find_city("Mainz"), find_city("Trier"), find_city("Köln"), find_city("Mannheim"), find_city("München"), find_city("Dresden"), find_city("Berlin"), ] const all_pieces = [ ...all_power_generals.flat(), ...all_power_trains.flat() ] const all_trains = [ ...all_power_trains.flat() ] const all_generals = [ ...all_power_generals.flat() ] const all_generals_by_rank = all_generals.slice().sort((a,b)=>piece_rank[a]-piece_rank[b]) const all_powers_none = [] function coop_major_power(pow) { if (pow === P_BAVARIA) return P_FRANCE if (pow === P_SAXONY) { if (is_saxony_prussian()) return P_PRUSSIA if (is_saxony_austrian()) return P_AUSTRIA } return pow } function coop_minor_power(pow) { if (pow === P_FRANCE) return P_BAVARIA if (pow === P_PRUSSIA && is_saxony_prussian()) return P_SAXONY if (pow === P_AUSTRIA && is_saxony_austrian()) return P_SAXONY return pow } function is_hostile_to_austria() { switch (game.power) { case P_FRANCE: case P_BAVARIA: case P_PRUSSIA: return true case P_SAXONY: return is_saxony_prussian() case P_PRAGMATIC: case P_AUSTRIA: return false } } // TODO: simplify all these lists and stuff function all_controlled_powers(pow) { switch (coop_major_power(pow)) { case P_FRANCE: return all_powers_france_bavaria case P_PRUSSIA: if (is_saxony_prussian()) return all_powers_prussia_saxony return all_powers_prussia case P_PRAGMATIC: return all_powers_pragmatic case P_AUSTRIA: if (is_two_player() && !is_intro()) { if (is_saxony_austrian()) return all_powers_pragmatic_austria_saxony return all_powers_pragmatic_austria } if (is_saxony_austrian()) return all_powers_austria_saxony return all_powers_austria } } function all_controlled_pieces(pow) { switch (coop_major_power(pow)) { case P_FRANCE: return all_france_bavaria_pieces case P_PRUSSIA: if (is_saxony_prussian()) return all_prussia_saxony_pieces return all_power_pieces[P_PRUSSIA] case P_PRAGMATIC: return all_power_pieces[P_PRAGMATIC] case P_AUSTRIA: if (is_two_player() && !is_intro()) { if (is_saxony_austrian()) return all_pragmatic_austria_saxony_pieces return all_pragmatic_austria_pieces } if (is_saxony_austrian()) return all_austria_saxony_pieces return all_power_pieces[P_AUSTRIA] } } function all_controlled_generals(pow) { switch (coop_major_power(pow)) { case P_FRANCE: return all_france_bavaria_generals case P_PRUSSIA: if (is_saxony_prussian()) return all_prussia_saxony_generals return all_power_generals[P_PRUSSIA] case P_PRAGMATIC: return all_power_generals[P_PRAGMATIC] case P_AUSTRIA: if (is_two_player() && !is_intro()) { if (is_saxony_austrian()) return all_pragmatic_austria_saxony_generals return all_pragmatic_austria_generals } if (is_saxony_austrian()) return all_austria_saxony_generals return all_power_generals[P_AUSTRIA] } } function all_controlled_trains(pow) { switch (coop_major_power(pow)) { case P_FRANCE: return all_france_bavaria_trains case P_PRUSSIA: if (is_saxony_prussian()) return all_prussia_saxony_trains return all_power_trains[P_PRUSSIA] case P_PRAGMATIC: return all_power_trains[P_PRAGMATIC] case P_AUSTRIA: if (is_two_player() && !is_intro()) { if (is_saxony_austrian()) return all_pragmatic_austria_saxony_trains return all_pragmatic_austria_trains } if (is_saxony_austrian()) return all_austria_saxony_trains return all_power_trains[P_AUSTRIA] } } function all_friendly_minor_powers(pow) { switch (pow) { case P_FRANCE: case P_BAVARIA: case P_PRUSSIA: if (is_saxony_prussian()) return all_powers_bavaria_saxony return all_powers_bavaria case P_SAXONY: if (is_saxony_prussian()) return all_powers_bavaria_saxony return all_powers_saxony case P_PRAGMATIC: case P_AUSTRIA: if (is_saxony_austrian()) return all_powers_saxony return all_powers_none } } function all_enemy_powers(pow) { switch (pow) { case P_FRANCE: case P_BAVARIA: case P_PRUSSIA: if (is_saxony_prussian()) return all_powers_pragmatic_austria return all_powers_pragmatic_austria_saxony case P_SAXONY: if (is_saxony_prussian()) return all_powers_pragmatic_austria return all_powers_france_prussia_bavaria case P_PRAGMATIC: case P_AUSTRIA: if (is_saxony_austrian()) return all_powers_france_prussia_bavaria return all_powers_france_prussia_bavaria_saxony } } function is_allied_power(a, b) { return !all_enemy_powers(a).includes(b) } function is_controlled_power(major, other) { return player_from_power(major) === player_from_power(other) } function all_non_coop_powers(pow) { switch (pow) { case P_FRANCE: case P_BAVARIA: return all_powers_prussia_pragmatic_austria_saxony case P_PRUSSIA: return all_powers_france_pragmatic_austria_bavaria case P_SAXONY: if (is_saxony_prussian()) return all_powers_france_pragmatic_austria_bavaria return all_powers_france_prussia_pragmatic_bavaria case P_PRAGMATIC: case P_AUSTRIA: if (is_saxony_austrian()) return all_powers_france_prussia_bavaria return all_powers_france_prussia_bavaria_saxony } } function all_coop_generals(pow) { switch (coop_major_power(pow)) { case P_FRANCE: return all_france_bavaria_generals case P_PRUSSIA: if (is_saxony_prussian()) return all_prussia_saxony_generals return all_power_generals[P_PRUSSIA] case P_PRAGMATIC: case P_AUSTRIA: if (is_saxony_austrian()) return all_pragmatic_austria_saxony_generals return all_pragmatic_austria_generals } } function all_france_allied_generals() { if (is_saxony_prussian()) return all_france_prussia_bavaria_saxony_generals return all_france_prussia_bavaria_generals } function all_france_allied_trains() { if (is_saxony_prussian()) return all_france_prussia_bavaria_saxony_trains return all_france_prussia_bavaria_trains } function all_austria_allied_generals() { if (is_saxony_austrian()) return all_pragmatic_austria_saxony_generals return all_pragmatic_austria_generals } function all_austria_allied_trains() { if (is_saxony_austrian()) return all_pragmatic_austria_saxony_trains return all_pragmatic_austria_trains } function all_allied_trains(pow) { switch (coop_major_power(pow)) { case P_FRANCE: case P_PRUSSIA: return all_france_allied_trains() case P_AUSTRIA: case P_PRAGMATIC: return all_austria_allied_trains() } } function all_enemy_trains(pow) { switch (coop_major_power(pow)) { case P_FRANCE: case P_PRUSSIA: return all_austria_allied_trains() case P_AUSTRIA: case P_PRAGMATIC: return all_france_allied_trains() } } function all_enemy_generals(pow) { switch (coop_major_power(pow)) { case P_FRANCE: case P_PRUSSIA: return all_austria_allied_generals() case P_AUSTRIA: case P_PRAGMATIC: return all_france_allied_generals() } } function is_general(p) { return p < 20 } function is_supply_train(p) { return p >= 20 && p < 30 } function is_hussar(p) { return p >= 30 && p < 32 } function format_card_prompt(c) { if (is_reserve(c)) return "8R" return to_value(c) + suit_name[to_suit(c)] } function format_card(c) { return (to_deck(c)+1) + "^" + format_card_prompt(c) } function format_card_list(list) { return list.map(format_card).join(", ") } function format_reserve(c, v) { return (to_deck(c)+1) + "^" + v + "R" } function is_reserve(c) { return to_suit(c) === RESERVE } function format_card_list_prompt(list) { if (list.length > 0) return list.map(format_card_prompt).join(", ") return "nothing" } function format_selected() { if (Array.isArray(game.selected)) { if (game.selected.length === 0) return "nobody" return game.selected.map(p => piece_name[p]).join(" and ") } else { if (game.selected < 0) return "nobody" return piece_name[game.selected] } } function log_move_path() { if (game.move_path.length > 1) log("@" + game.selected + ";" + game.move_path.join(",")) } /* OBJECTIVES */ const all_fortresses = [] set_add_all(all_fortresses, data.type.major_fortress) set_add_all(all_fortresses, data.type.minor_fortress) const all_home_country_fortresses = [] all_home_country_fortresses[P_FRANCE] = set_intersect(all_fortresses, data.country.France) all_home_country_fortresses[P_PRUSSIA] = set_intersect(all_fortresses, data.country.Prussia) all_home_country_fortresses[P_PRAGMATIC] = set_intersect(all_fortresses, data.country.Netherlands) 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) all_home_country_major_fortresses[P_PRUSSIA] = set_intersect(data.type.major_fortress, data.country.Prussia) all_home_country_major_fortresses[P_PRAGMATIC] = set_intersect(data.type.major_fortress, data.country.Netherlands) all_home_country_major_fortresses[P_AUSTRIA] = set_intersect(data.type.major_fortress, data.country.Austria) 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_silesian_major_fortresses = set_intersect(data.type.major_fortress, data.country.Silesia) 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_core_austria_and_silesia_fortresses = set_union( all_core_austria_fortresses, all_silesian_fortresses ) const all_prussian_and_silesian_fortresses = set_union( all_home_country_fortresses[P_PRUSSIA], all_silesian_fortresses ) const all_prussian_and_silesian_major_fortresses = set_union( all_home_country_major_fortresses[P_PRUSSIA], all_silesian_major_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 protect_range = [] for (let s of all_fortresses) make_protect_range(protect_range[s] = [], s, s, 3) function make_protect_range(result, start, here, range) { for (let next of data.cities.adjacent[here]) { set_add(result, next) if (range > 1) make_protect_range(result, start, next, range - 1) } } function is_home_country(s) { switch (game.power) { case P_FRANCE: return set_has(data.country.France, 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) case P_SAXONY: return set_has(data.country.Saxony, s) } } function is_home_country_for_return(s) { switch (game.power) { case P_FRANCE: return set_has(data.country.France, s) || set_has(data.country.Bavaria, 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) case P_SAXONY: return set_has(data.country.Saxony, s) } } function is_enemy_home_country(s) { for (let other of all_enemy_powers(game.power)) if (set_has(all_home_country_fortresses[other], s)) return true return false } function is_friendly_minor_home_country(s) { for (let other of all_friendly_minor_powers(game.power)) if (set_has(all_home_country_fortresses[other], s)) return true return false } function get_home_power(s) { for (let pow of all_powers) if (set_has(all_home_country_fortresses[pow], s)) return pow return -1 } function is_enemy_controlled_fortress(s) { for (let other of all_enemy_powers(game.power)) if (is_power_controlled_fortress(other, s)) return true return false } function is_friendly_controlled_fortress(s) { return !is_enemy_controlled_fortress(s) } function is_power_controlled_fortress(pow, s) { let owner = map_get(game.elector, s, -1) if (owner < 0) owner = map_get(game.victory, s, -1) if (owner < 0) return set_has(all_home_country_fortresses[pow], s) return owner === pow } function set_control_of_fortress(s, pow) { if (s === TRIER || s === MAINZ || s === KOLN || s === MANNHEIM) { if (pow === P_AUSTRIA || pow === P_PRAGMATIC) map_set(game.elector, s, P_AUSTRIA) else map_set(game.elector, s, P_FRANCE) return } if (pow === P_FRANCE && game.vp[SET_ASIDE_FRANCE] > 0) { if (is_bohemia_space(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 map_delete(game.victory, s) } function is_space_protected_by_piece(s, p) { return set_has(protect_range[s], game.pos[p]) } function is_protected_from_conquest(s) { let owner = map_get(game.elector, s, -1) if (owner < 0) owner = map_get(game.victory, s, -1) if (owner < 0) owner = get_home_power(s) for (let p of all_coop_generals(owner)) if (is_space_protected_by_piece(s, p)) return true return false } /* STATE */ function is_intro() { return game.flags & F_INTRODUCTORY } function is_two_player() { return game.flags & F_TWO_PLAYER } const TC_PER_TURN_TABLE = [ 5, 3, 3, 5, 1, 1 ] function tc_per_turn_base() { if (game.power === P_FRANCE && is_intro()) return 3 return TC_PER_TURN_TABLE[game.power] } function tc_per_turn_modifier() { let n = 0 // Saxony track if (game.power === P_SAXONY) { if (game.saxony > 4) ++n } // Russia track if (game.power === P_PRUSSIA) { if (game.russia <= 2) ++n if (game.russia >= 8) --n if (game.russia >= 9) --n } // Italy track if (game.power === P_AUSTRIA) { if (game.italy <= 3) --n } if (game.power === P_FRANCE) { if (game.italy >= 7) --n } // Jacobite Rising if (game.power === P_PRAGMATIC) { for (let p of all_power_generals[P_PRAGMATIC]) if (game.pos[p] === ENGLAND) --n } // War of Jenkins' Ear if (game.power === P_FRANCE) { if (game.flags & F_WAR_OF_JENKINS_EAR) --n } return n } function count_subsidies(pow) { let n = 0 if (game.contracts[pow]) for (let other of all_powers) if (map_get(game.contracts[pow], other, 0) > 0) ++n return n } function list_subsidies(pow) { let result = [] if (game.contracts[pow]) for (let other of all_powers) if (map_get(game.contracts[pow], other, 0) > 0) result.push(power_name[other]) return result } function player_from_power(pow) { if (is_two_player()) { switch (pow) { case P_FRANCE: return R_PLAYER_A case P_PRUSSIA: return R_PLAYER_A case P_PRAGMATIC: return R_PLAYER_B case P_AUSTRIA: return R_PLAYER_B case P_SAXONY: if (is_saxony_prussian()) return R_PLAYER_A else return R_PLAYER_B case P_BAVARIA: return R_PLAYER_A } } switch (pow) { case P_FRANCE: return R_LOUIS_XV case P_PRUSSIA: return R_FREDERICK case P_PRAGMATIC: return R_FREDERICK case P_AUSTRIA: return R_MARIA_THERESA case P_SAXONY: if (is_saxony_prussian()) return R_FREDERICK else return R_MARIA_THERESA case P_BAVARIA: return R_LOUIS_XV } } function set_active_to_power_keep_undo(new_power) { game.power = new_power game.active = player_from_power(new_power) } function set_active_to_power(new_power) { let old_active = player_from_power(game.power) let new_active = player_from_power(new_power) if (old_active !== new_active) clear_undo() game.power = new_power game.active = new_active } function get_top_piece(s) { for (let p of all_hussars) if (game.pos[p] === s) return p for (let p of all_trains) if (game.pos[p] === s) return p return get_supreme_commander(s) } function get_supreme_commander(s) { for (let p of all_generals) if ((game.supreme & (1<= a && s <= b) return true return false } function get_space_suit(s) { if (s >= ELIMINATED) return SPADES if (is_space_suit(s, data.suit.spades)) return SPADES if (is_space_suit(s, data.suit.clubs)) return CLUBS if (is_space_suit(s, data.suit.hearts)) return HEARTS if (is_space_suit(s, data.suit.diamonds)) return DIAMONDS throw "IMPOSSIBLE" } function count_used_troops() { let current = 0 for (let p of all_power_generals[game.power]) current += game.troops[p] return current } 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) { for (let p = 0; p <= last_piece; ++p) if (game.pos[p] === to) return true return false } function has_any_hussar(to) { for (let p of all_hussars) if (game.pos[p] === to) return true return false } function has_friendly_supply_train(to) { for (let p of all_allied_trains(game.power)) if (game.pos[p] === to) return true return false } function has_enemy_piece(to) { for (let p of all_enemy_generals(game.power)) if (game.pos[p] === to) return true for (let p of all_enemy_trains(game.power)) if (game.pos[p] === to) return true return false } function has_non_cooperative_general(to) { for (let other of all_non_coop_powers(game.power)) for (let p of all_power_generals[other]) if (game.pos[p] === to) return true return false } function count_generals(to) { let n = 0 for (let p of all_generals) if (game.pos[p] === to) ++n return n } function select_stack(s) { let list = [] for (let p of all_generals_by_rank) if ((game.supreme & (1<P" + p + " eliminated") else log("P" + p + " eliminated.") game.supreme &= ~(1 << p) game.pos[p] = ELIMINATED game.troops[p] = 0 set_in_supply(p) } /* SEQUENCE OF PLAY */ function goto_start_turn() { game.turn += 1 log("# " + turn_name[game.turn]) game.selected = -1 // clear per turn flags game.flags &= ~F_SAXONY_TC_ONCE game.flags &= ~F_WAR_OF_JENKINS_EAR if (game.turn % 4 === 0) { goto_winter_turn() } else { if (is_intro()) start_sequence_of_play() else goto_politics() } // remove expired deals game.deals = game.deals.filter(deal => deal[DI_TURN] >= game.turn) } function goto_end_turn() { if (game.flags & F_IMPERIAL_ELECTION) { goto_imperial_election() return } goto_start_turn() } const advanced_sequence_of_play = [ { power: P_AUSTRIA, action: goto_place_hussars }, { power: P_FRANCE, action: start_action_stage }, { power: P_FRANCE, action: goto_tactical_cards }, { power: P_BAVARIA, action: goto_tactical_cards }, { power: P_FRANCE, action: goto_supply }, { power: P_BAVARIA, action: goto_supply }, { power: P_FRANCE, action: goto_movement }, { power: P_FRANCE, action: validate_end_movement }, { power: P_FRANCE, action: goto_combat }, { power: P_FRANCE, action: end_action_stage }, { power: P_FRANCE, action: goto_france_reduces_military_objectives }, { power: P_PRUSSIA, action: start_action_stage }, { power: P_PRUSSIA, action: goto_prussia_ends_neutrality }, { power: P_PRUSSIA, action: goto_tactical_cards }, { power: P_SAXONY, action: goto_tactical_cards, condition: is_saxony_prussian }, { power: P_PRUSSIA, action: goto_supply }, { power: P_SAXONY, action: goto_supply, condition: is_saxony_prussian }, { power: P_PRUSSIA, action: goto_movement }, { power: P_PRUSSIA, action: validate_end_movement }, { power: P_PRUSSIA, action: goto_combat }, { power: P_PRUSSIA, action: end_action_stage }, { power: P_PRUSSIA, action: goto_prussia_annexes_silesia }, { power: P_AUSTRIA, action: start_action_stage }, { power: P_AUSTRIA, action: goto_tactical_cards }, { power: P_SAXONY, action: goto_tactical_cards, condition: is_saxony_austrian }, { power: P_AUSTRIA, action: goto_supply }, { power: P_SAXONY, action: goto_supply, condition: is_saxony_austrian }, { power: P_PRAGMATIC, action: goto_tactical_cards }, { power: P_PRAGMATIC, action: goto_supply }, // alternate moves on flanders starting with pragmatic army { power: P_PRAGMATIC, action: goto_movement }, { power: P_PRAGMATIC, action: validate_end_movement }, { power: P_AUSTRIA, action: goto_combat }, { power: P_PRAGMATIC, action: goto_combat }, { power: P_AUSTRIA, action: end_action_stage }, { power: P_FRANCE, action: goto_end_turn }, ] const two_player_sequence_of_play = [ { power: P_AUSTRIA, action: goto_place_hussars }, { power: P_FRANCE, action: start_action_stage }, { power: P_FRANCE, action: goto_tactical_cards }, { power: P_BAVARIA, action: goto_tactical_cards }, { power: P_FRANCE, action: goto_supply }, { power: P_BAVARIA, action: goto_supply }, { power: P_FRANCE, action: goto_movement }, { power: P_FRANCE, action: end_movement }, { power: P_FRANCE, action: goto_combat }, { power: P_FRANCE, action: end_action_stage }, { power: P_FRANCE, action: goto_france_reduces_military_objectives }, { power: P_PRUSSIA, action: start_action_stage }, { power: P_PRUSSIA, action: goto_prussia_ends_neutrality }, { power: P_PRUSSIA, action: goto_tactical_cards }, { power: P_SAXONY, action: goto_tactical_cards, condition: is_saxony_prussian }, { power: P_PRUSSIA, action: goto_supply }, { power: P_SAXONY, action: goto_supply, condition: is_saxony_prussian }, { power: P_PRUSSIA, action: goto_movement }, { power: P_PRUSSIA, action: end_movement }, { power: P_PRUSSIA, action: goto_combat }, { power: P_PRUSSIA, action: end_action_stage }, { power: P_PRUSSIA, action: goto_prussia_annexes_silesia }, { power: P_AUSTRIA, action: start_action_stage }, { power: P_AUSTRIA, action: goto_tactical_cards }, { power: P_SAXONY, action: goto_tactical_cards, condition: is_saxony_austrian }, { power: P_PRAGMATIC, action: goto_tactical_cards }, { power: P_AUSTRIA, action: goto_supply }, { power: P_SAXONY, action: goto_supply, condition: is_saxony_austrian }, { power: P_PRAGMATIC, action: goto_supply }, // austria and pragmatic share movement (like with minor power) { power: P_AUSTRIA, action: goto_movement }, { power: P_AUSTRIA, action: end_movement }, // austria and pragmatic share combat (like with minor power) { power: P_AUSTRIA, action: goto_combat }, { power: P_AUSTRIA, action: end_action_stage }, { power: P_FRANCE, action: goto_end_turn }, ] const intro_sequence_of_play = [ { power: P_AUSTRIA, action: goto_place_hussars }, { power: P_FRANCE, action: start_action_stage }, { power: P_FRANCE, action: goto_tactical_cards }, { power: P_BAVARIA, action: goto_tactical_cards }, { power: P_FRANCE, action: goto_supply }, { power: P_BAVARIA, action: goto_supply }, { power: P_FRANCE, action: goto_movement }, { power: P_FRANCE, action: end_movement }, { power: P_FRANCE, action: goto_combat }, { power: P_FRANCE, action: end_action_stage }, { power: P_PRUSSIA, action: start_action_stage }, { power: P_PRUSSIA, action: goto_tactical_cards }, { power: P_SAXONY, action: goto_tactical_cards }, { power: P_PRUSSIA, action: goto_supply }, { power: P_SAXONY, action: goto_supply }, { power: P_PRUSSIA, action: goto_movement }, { power: P_PRUSSIA, action: end_movement }, { power: P_PRUSSIA, action: goto_combat }, { power: P_PRUSSIA, action: end_action_stage }, { power: P_AUSTRIA, action: start_action_stage }, { power: P_AUSTRIA, action: goto_tactical_cards }, { power: P_AUSTRIA, action: goto_supply }, { power: P_AUSTRIA, action: goto_movement }, { power: P_AUSTRIA, action: end_movement }, { power: P_AUSTRIA, action: goto_combat }, { power: P_AUSTRIA, action: end_action_stage }, { power: P_FRANCE, action: goto_end_turn }, ] function start_sequence_of_play() { game.stage = -1 next_sequence_of_play() } function current_sequence_of_play() { if (is_intro()) return intro_sequence_of_play[game.stage] if (is_two_player()) return two_player_sequence_of_play[game.stage] return advanced_sequence_of_play[game.stage] } function set_active_to_current_sequence_of_play() { set_active_to_power(current_sequence_of_play().power) } function next_sequence_of_play() { ++game.stage let row = current_sequence_of_play() set_active_to_power(row.power) if (row.condition && !row.condition()) next_sequence_of_play() else row.action() } function start_action_stage() { switch (game.power) { case P_FRANCE: log("=6 France and Bavaria") break case P_PRUSSIA: if (is_saxony_prussian()) log("=7 Prussia and Saxony") else log("=1 Prussia") break case P_AUSTRIA: if (is_saxony_prussian()) log("=8 Austria and the Pragmatic Army") else log("=9 Austria, Pragmatic, Saxony") break } next_sequence_of_play() } function end_action_stage() { if (!check_instant_victory()) next_sequence_of_play() } /* AUSTRIA PLACES ITS HUSSARS */ function goto_place_hussars() { log("=" + P_AUSTRIA + " Hussars") set_active_to_power(P_AUSTRIA) game.state = "place_hussars" set_clear(game.moved) save_checkpoint() } function end_place_hussars() { clear_checkpoint() set_clear(game.moved) for (let p of all_hussars) log("P" + p + " at S" + game.pos[p] + ".") next_sequence_of_play() } states.place_hussars = { inactive: "place hussars", prompt() { prompt("Place the hussars.") for (let p of all_hussars) if (!set_has(game.moved, p)) gen_action_piece(p) if (!has_any_hussar(ELIMINATED)) view.actions.end_place_hussars = 1 }, piece(p) { push_undo() set_add(game.moved, p) game.selected = p game.state = "place_hussars_where" }, end_place_hussars() { game.proposal = game.undo[0] clear_undo() goto_validate_hussars() }, } states.place_hussars_where = { inactive: "place hussars", prompt() { prompt("Place the hussar in a city.") view.selected = game.selected for (let p of all_power_generals[P_AUSTRIA]) if (is_piece_on_map(p)) for (let x of search_hussar_bfs(game.pos[p])) gen_action_space(x) }, space(to) { game.state = "place_hussars" game.pos[game.selected] = to game.selected = -1 }, } function search_hussar_bfs(from) { let seen = [ from ] let queue = [ from << 4 ] while (queue.length > 0) { let item = queue.shift() let here = item >> 4 let dist = (item & 15) + 1 for (let next of data.cities.adjacent[here]) { if (set_has(seen, next)) continue if (is_bohemia_space(next) && !has_any_piece(next) && !has_any_hussar(next)) set_add(seen, next) if (dist < 4) queue.push((next << 4) | dist) } } set_delete(seen, from) return seen } /* TACTICAL CARDS */ function for_each_card_in_hand(f) { for (let c of game.hand1[game.power]) f(c) for (let c of game.hand2[game.power]) // subsidies and returned cards f(c) } function gen_cards_in_hand() { for_each_card_in_hand(gen_action_card) } function remove_card_in_hand(c) { set_delete(game.hand1[game.power], c) set_delete(game.hand2[game.power], c) } function count_cards_in_hand() { return game.hand1[game.power].length + game.hand2[game.power].length > 0 } function find_largest_discard(u) { for (let i = 0; i < 4; ++i) if (u[i] <= u[0] && u[i] <= u[1] && u[i] <= u[2] && u[i] <= u[3]) return i throw "OUT OF CARDS" } function count_used_cards() { let held = [ 0, 0, 0, 0 ] // count cards in hands for (let pow of all_powers) { for (let c of game.hand1[pow]) held[to_deck(c)]++ for (let c of game.hand2[pow]) held[to_deck(c)]++ } // count cards in political display for (let pow of all_major_powers) { for (let c of game.face_up[pow]) held[to_deck(c)]++ for (let c of game.face_down[pow]) held[to_deck(c)]++ } // count cards currently being held if (game.draw) for (let c of game.draw) held[to_deck(c)]++ // count cards remaining in deck for (let c of game.deck) held[to_deck(c)]++ return held } function next_tactics_deck() { let held = count_used_cards() // find next unused deck for (let i = 1; i < 5; ++i) { if (held[i] === 0) { game.deck = make_tactics_deck(i) shuffle_bigint(game.deck) log("Shuffled " + deck_name[i] + ".") return } } // find two largest discard piles let a = find_largest_discard(held) if (held[a] === 38) return held[a] = 100 let b = find_largest_discard(held) if (held[b] === 38) return log("Shuffled " + deck_name[a] + " and " + deck_name[b] + ".") game.deck = [ make_tactics_discard(a), make_tactics_discard(b) ].flat() shuffle_bigint(game.deck) } function total_discard_list() { let discard = count_used_cards() for (let i = 0; i < 4; ++i) discard[i] = Math.ceil((38 - discard[i]) / 5) return discard } function draw_tc(draw, n) { while (n > 0) { if (game.deck.length === 0) { next_tactics_deck() if (game.deck.length === 0) { log("The cards ran out!") break } } set_add(draw, game.deck.pop()) --n } } function give_subsidy(other) { if (other === P_BAVARIA && is_enemy_controlled_fortress(MUNCHEN)) { log(">1 to Bavaria lost\nS" + MUNCHEN + " is enemy controlled") return } if (other === P_SAXONY && is_enemy_controlled_fortress(DRESDEN)) { log(">1 to Saxony lost\nS" + DRESDEN + " is enemy controlled") return } log(">1 to " + power_name[other]) draw_tc(game.hand2[other], 1) } function goto_tactical_cards() { if (game.power === P_SAXONY) { if (game.flags & F_SAXONY_TC_ONCE) { next_sequence_of_play() return } game.flags |= F_SAXONY_TC_ONCE } if (is_major_power(game.power)) game.state = "tactical_cards_draw" else draw_tactical_cards() } function draw_tactical_cards() { clear_undo() // reveal random cards game.draw = [] if (game.power === P_BAVARIA && is_enemy_controlled_fortress(MUNCHEN)) { log("Bavaria TC draw lost\nS" + MUNCHEN + " is enemy controlled.") } else if (game.power === P_SAXONY && is_enemy_controlled_fortress(DRESDEN)) { log("Saxony TC draw lost\nS" + DRESDEN + " is enemy controlled.") } else { let base = tc_per_turn_base() let mod = tc_per_turn_modifier() let sub = count_subsidies(game.power) // Too many subsidies to hand out! // NOTE: This can only happen with Prussia. // If giving subsidies to Saxony, Bavaria, and/or France // while at the -1 or -2 TC on the Russia track. while (base + mod - sub < 0) { // Cancel cooperative subsidy first. if (map_get(game.contracts[P_PRUSSIA], P_SAXONY, 0) > 0) { log("Canceled subsidy to Saxony (forced).") map_delete(game.contracts[P_PRUSSIA], P_SAXONY) continue } // Then the shortest of the Bavarian or France subsidy. let n_france = map_get(game.contracts[P_PRUSSIA], P_FRANCE, 0) let n_bavaria = map_get(game.contracts[P_PRUSSIA], P_BAVARIA, 0) if (n_france > 0 && n_bavaria > 0) { // Cancel the shortest remaining subsidy. if (n_france > n_bavaria) n_france = 0 else n_bavaria = 0 } if (n_france > 0) { log("Canceled subsidy to France (forced).") map_delete(game.contracts[P_PRUSSIA], P_FRANCE) continue } if (n_bavaria > 0) { log("Canceled subsidy to Bavaria (forced).") map_delete(game.contracts[P_PRUSSIA], P_BAVARIA) continue } break // impossible } if (mod < 0) log(`${power_name[game.power]} ${base} TC (-${-mod} political).`) else if (mod > 0) log(`${power_name[game.power]} ${base} TC (+${mod} political).`) else log(`${power_name[game.power]} ${base} TC.`) if (sub > 0) { log(">" + (base + mod - sub) + " to " + power_name[game.power]) draw_tc(game.draw, base + mod - sub) for (let other of all_powers) { let contract = map_get(game.contracts[game.power], other, 0) if (contract > 0) { give_subsidy(other) if (contract > 1) map_set(game.contracts[game.power], other, contract - 1) else map_delete(game.contracts[game.power], other) } } } else { draw_tc(game.draw, base + mod) } } game.state = "tactical_cards_show" } function end_tactical_cards() { for (let c of game.hand2[game.power]) set_add(game.hand1[game.power], c) game.hand2[game.power] = [] for (let c of game.draw) set_add(game.hand1[game.power], c) delete game.draw next_sequence_of_play() } states.tactical_cards_draw = { inactive: "draw tactical cards", prompt() { let base = tc_per_turn_base() let mod = tc_per_turn_modifier() let sub = count_subsidies(game.power) let str = "Draw " + (base + mod) + " TC." if (sub > 0) str += " Give " + sub + " TC to " + list_subsidies(game.power).join(" and ") + "." prompt(str) if (base + mod - sub >= 0) view.actions.draw = 1 else view.actions.draw = 0 // else cancel contracts! // TODO: force france to cancel one subsidy if prussia cannot draw cards }, draw() { draw_tactical_cards() }, } states.tactical_cards_show = { inactive: "draw tactical cards", prompt() { view.draw = game.draw prompt("Draw " + format_card_list_prompt(game.draw) + ".") view.actions.next = 1 }, next() { end_tactical_cards() }, } /* PAYMENT */ function sum_card_values(list) { let n = 0 for (let c of list) n += to_value(c) return n } function find_largest_card(list) { for (let v = 10; v >= 2; --v) { for (let c of list) if (to_value(c) === v) return c } throw "NO CARDS FOUND IN LIST" } function spend_card_value(pool, used, amount) { if (game.count > 0) { if (amount < game.count) { game.count -= amount amount = 0 } else { amount -= game.count game.count = 0 } } while (amount > 0) { let c = find_largest_card(pool) let v = to_value(c) set_delete(pool, c) set_add(used, c) if (v > amount) { game.count = v - amount amount = 0 } else { amount -= v } } } /* SUPPLY */ function is_out_of_supply(p) { return (game.oos & (1 << p)) !== 0 } function set_out_of_supply(p) { return game.oos |= (1 << p) } function set_in_supply(p) { return game.oos &= ~(1 << p) } function search_supply_path_avoid_hussars(who) { let pow = piece_power[who] let from = game.pos[who] let trains = all_power_trains[pow] if (is_home_country(from)) return 1 let seen = [ from ] let queue = [ from << 4 ] while (queue.length > 0) { let item = queue.shift() let here = item >> 4 let dist = (item & 15) + 1 for (let next of data.cities.adjacent[here]) { for (let p of trains) if (game.pos[p] === next) return dist if (has_any_hussar(next)) continue if (set_has(seen, next)) continue if (has_enemy_piece(next)) continue if (is_forbidden_neutral_space(pow, next)) continue set_add(seen, next) if (dist < 6) queue.push((next << 4) | dist) } } return 0 } function search_supply_path(who) { let pow = piece_power[who] let from = game.pos[who] let trains = all_power_trains[pow] if (who === ARENBERG) { if (set_has(data.country.Netherlands, from)) return 1 trains = all_pragmatic_austria_trains } if (is_home_country(from)) return 1 let seen = [ from ] let queue = [ from << 4 ] while (queue.length > 0) { let item = queue.shift() let here = item >> 4 let dist = (item & 15) + 1 for (let next of data.cities.adjacent[here]) { for (let p of trains) if (game.pos[p] === next) return dist if (set_has(seen, next)) continue if (has_enemy_piece(next)) continue if (is_forbidden_neutral_space(pow, next)) continue set_add(seen, next) if (dist < 6) queue.push((next << 4) | dist) } } return 0 } function goto_supply() { game.count = 0 // to track saxony shift to oos game.supply = { hussars: [], restore: [], suffer: [], } for (let p of all_power_generals[game.power]) { if (!is_piece_on_map(p)) continue if (is_hostile_to_austria()) { let d = search_supply_path_avoid_hussars(p) if (d > 0) { if (is_out_of_supply(p)) set_add(game.supply.restore, p) } else { d = search_supply_path(p) if (d > 0) map_set(game.supply.hussars, p, d) else set_add(game.supply.suffer, p) } } else { let d = search_supply_path(p) if (d > 0) { if (is_out_of_supply(p)) set_add(game.supply.restore, p) } else { set_add(game.supply.suffer, p) } } } if (game.supply.hussars.length + game.supply.restore.length + game.supply.suffer.length === 0) end_supply() else resume_supply() } function resume_supply() { set_active_to_current_sequence_of_play() if (game.supply.hussars.length > 0) goto_supply_hussars() else if (game.supply.restore.length > 0) goto_supply_restore() else if (game.supply.suffer.length > 0) goto_supply_suffer() else end_supply() } function goto_supply_hussars() { log_br() log("Hussars") set_active_to_power(piece_power[game.supply.hussars[0]]) game.state = "supply_hussars" game.supply.pool = [] game.supply.used = [] game.count = 0 } function goto_supply_restore() { log_br() log("In supply") game.state = "supply_restore" } function goto_supply_suffer() { log_br() log("Out of supply") game.state = "supply_suffer" game.count = 0 // number of lost VP } states.supply_hussars = { inactive: "supply", prompt() { let paid = game.count + sum_card_values(game.supply.pool) view.selected = [] let can_pay = false let debt = 0 map_for_each(game.supply.hussars, (p, d) => { if (piece_power[p] === game.power) { if (d <= paid) { can_pay = true gen_action_piece(p) } else { view.selected.push(p) } debt += d } }) let str if (debt > 0) str = `Pay ${debt} for tracing supply through hussars` else str = "Hussar payment done" if (paid > 1) str += " \u2014 " + paid + " points." else if (paid === 1) str += " \u2014 1 point." else str += "." if (paid < debt) gen_cards_in_hand() prompt(str) view.draw = game.supply.pool if (debt === 0 || (!can_pay && count_cards_in_hand() === 0)) view.actions.next = 1 }, piece(p) { push_undo() let cost = map_get(game.supply.hussars, p) spend_card_value(game.supply.pool, game.supply.used, cost) map_delete(game.supply.hussars, p) if (is_out_of_supply(p)) set_add(game.supply.restore, p) log(">P" + p + " paid " + cost) }, card(c) { push_undo() remove_card_in_hand(c) set_add(game.supply.pool, c) }, next() { push_undo() if (game.supply.used.length > 0) log(">with " + game.supply.used.map(format_card).join(", ")) else log(">paid nothing") // move generals not paid for to out of supply list map_for_each(game.supply.hussars, (p, _) => { if (piece_power[p] === game.power) set_add(game.supply.suffer, p) }) for (let p of game.supply.suffer) map_delete(game.supply.hussars, p) // put back into hand unused cards for (let c of game.supply.pool) set_add(game.hand2[game.power], c) delete game.supply.pool delete game.supply.used game.count = 0 resume_supply() }, } states.supply_restore = { inactive: "supply", prompt() { prompt("Restore supply to generals with a supply path.") for (let p of game.supply.restore) gen_action_piece(p) }, piece(p) { log(`>P${p} at S${game.pos[p]}`) set_delete(game.supply.restore, p) set_in_supply(p) if (game.supply.restore.length === 0) resume_supply() }, } function remove_one_troop(p) { if (game.troops[p] === 1) { for (let x of all_power_generals[game.power]) { if (game.pos[x] === game.pos[p] && game.troops[x] > 1) { game.troops[x] -- return } } } if (game.troops[p] > 0) game.troops[p] -- } states.supply_suffer = { inactive: "supply", prompt() { prompt("Flip and remove troops from generals without a supply path.") for (let p of game.supply.suffer) gen_action_piece(p) }, piece(p) { let s = game.pos[p] set_delete(game.supply.suffer, p) if (is_out_of_supply(p)) { log(`>P${p} at S${s} (-2)`) remove_one_troop(p) remove_one_troop(p) } else { log(`>P${p} at S${s} (-1)`) set_out_of_supply(p) remove_one_troop(p) } if (game.troops[p] <= 0) { eliminate_general(p, true) lose_vp(coop_major_power(game.power), 1) if (is_bohemia_space(s)) game.count += 1 // for saxony shift } if (game.supply.suffer.length === 0) resume_supply() }, } states.supply_done = { inactive: "supply", prompt() { prompt("Supply done.") view.actions.end_supply = 1 }, end_supply() { end_supply() }, } function end_supply() { if (game.count > 0 && should_saxony_shift_after_supply(coop_major_power(game.power))) { set_active_to_power(P_AUSTRIA) game.state = "saxony_shift_supply" return } delete game.supply next_sequence_of_play() } /* TRANSFER TROOPS */ function find_unstacked_general() { let here = game.pos[game.selected] for (let p of all_power_generals[game.power]) if (game.pos[p] === here && game.selected !== p) return p return -1 } function count_stacked_take() { return 8 - game.troops[game.selected] } function count_unstacked_take() { let p = find_unstacked_general() if (p < 0) return 0 return 8 - game.troops[p] } function count_stacked_give() { return game.troops[game.selected] - 1 } function count_unstacked_give() { let p = find_unstacked_general() if (p < 0) return 0 return game.troops[p] - 1 } function take_troops(total) { game.troops[game.selected] += total game.troops[find_unstacked_general()] -= total } function give_troops(total) { game.troops[game.selected] -= total game.troops[find_unstacked_general()] += total } /* MOVEMENT */ function goto_movement() { set_clear(game.moved) if (is_intro()) { for (let p of all_pieces) if (is_flanders_space(game.pos[p])) set_add(game.moved, p) } log_br() game.move_re_entered = 0 game.move_conq = [] game.state = "movement" save_checkpoint() } function has_unmoved_piece(pow) { for (let p of all_controlled_pieces(pow)) if (!set_has(game.moved, p) && is_piece_on_map(p) && can_piece_move_anywhere(p)) return true return false } function has_moved_piece_on_flanders_map(pow) { for (let p of all_power_pieces[pow]) if (is_flanders_space(game.pos[p]) && set_has(game.moved, p)) return true return false } function has_unmoved_piece_on_flanders_map(pow) { for (let p of all_power_pieces[pow]) if (is_flanders_space(game.pos[p]) && !set_has(game.moved, p) && can_piece_move_anywhere(p)) return true return false } function resume_movement() { if (is_two_player() && game.power === P_PRAGMATIC) game.power = P_AUSTRIA set_active_to_power(coop_major_power(game.power)) game.selected = -1 game.state = "movement" // alternate on flanders map (TODO: when dispute only) if (is_alternating_move()) game.state = "movement_flanders_next" } function resume_movement_after_flanders_stacking() { flush_alternate_move() set_active_to_power(coop_major_power(game.power)) game.selected = -1 // keep moving with new power game.state = "movement" } function is_forbidden_neutral_space(pow, to) { if (is_saxony_neutral()) { if (pow === P_SAXONY) { if (!set_has(data.country.Saxony, to)) return true } else { if (set_has(data.country.Saxony, to)) return true } } if (is_prussia_neutral()) { if (pow === P_PRUSSIA) { if (!set_has(all_prussian_and_silesian_cities, to)) return true } else { if (set_has(all_prussian_and_silesian_cities, to)) return true } } return false } function can_move_piece_to(p, from, to) { if (is_general(p)) return can_move_general_to(p, from, to) return can_move_train_to(p, from, to) } function can_train_move_anywhere(p) { let from = game.pos[p] for (let to of data.cities.adjacent[from]) if (can_move_train_to(p, from, to)) return true return false } function can_general_move_anywhere(p) { let from = game.pos[p] for (let to of data.cities.adjacent[from]) if (can_move_general_to(p, from, to)) return true return false } function can_piece_move_anywhere(p) { if (is_general(p)) return can_general_move_anywhere(p) else return can_train_move_anywhere(p) } function may_dispute_flanders_movement() { if (game.flags & F_MOVE_DISPUTE) return false if (game.power === P_PRAGMATIC && has_moved_piece_on_flanders_map(P_AUSTRIA)) return true if (game.power === P_AUSTRIA) return true return false } states.movement_flanders_next = { inactive: "move", prompt() { prompt("Alternate moves on Flanders map.") if (game.power === P_PRAGMATIC) gen_action_power(P_AUSTRIA) else gen_action_power(P_PRAGMATIC) }, power(pow) { flush_alternate_move() set_active_to_power(pow) game.state = "movement" }, } states.movement = { inactive: "move", dont_snap() { // don't snapshot during alternating move phase if (!is_two_player() && !is_intro() && (game.power === P_PRAGMATIC || game.power === P_AUSTRIA)) return true return false }, prompt() { let done_generals = true let done_trains = true for (let p of all_controlled_generals(game.power)) { if (!set_has(game.moved, p) && is_piece_on_map(p)) { if (can_general_move_anywhere(p)) { gen_action_piece(p) done_generals = false } } } for (let p of all_controlled_trains(game.power)) { if ((game.move_re_entered & (1 << piece_power[p])) === 0) { if (can_train_re_enter(p)) view.actions.re_enter = 1 } if (!set_has(game.moved, p)) { if (is_piece_on_map(p)) { if (can_train_move_anywhere(p)) { gen_action_piece(p) done_trains = false } } } } if (game.power === P_AUSTRIA && has_unmoved_piece_on_flanders_map(game.power)) prompt("Move your pieces on the Flanders map.") else if (done_trains && done_generals) prompt("Movement done.") else if (done_generals && !done_trains) prompt("Move your supply trains.") else if (!done_generals && done_trains) prompt("Move your generals.") else prompt("Move your generals and supply trains.") // austria/pragmatic shared movement phase if (!is_two_player() && !is_intro() && (game.power === P_PRAGMATIC || game.power === P_AUSTRIA)) { if (has_unmoved_piece_on_flanders_map(P_AUSTRIA) || has_unmoved_piece_on_flanders_map(P_PRAGMATIC)) { // pieces left to move on flanders if (!(game.flags & F_MOVE_DISPUTE)) { if (game.power === P_PRAGMATIC && has_unmoved_piece_on_flanders_map(P_AUSTRIA)) gen_action_power(P_AUSTRIA) if (game.power === P_AUSTRIA && has_unmoved_piece_on_flanders_map(P_PRAGMATIC)) gen_action_power(P_PRAGMATIC) if (may_dispute_flanders_movement()) view.actions.dispute = 1 else view.actions.dispute = 0 } if (game.restart) { if (has_moved_piece_on_flanders_map(P_PRAGMATIC) || has_moved_piece_on_flanders_map(P_AUSTRIA)) view.actions.restart = 1 else view.actions.restart = 0 } } else { // only bohemian pieces left if (!has_unmoved_piece(P_PRAGMATIC) && !has_unmoved_piece(P_AUSTRIA)) view.actions.end_movement = 1 else if (game.power === P_PRAGMATIC && has_unmoved_piece(P_AUSTRIA)) gen_action_power(P_AUSTRIA) else if (game.power === P_AUSTRIA && has_unmoved_piece(P_PRAGMATIC)) gen_action_power(P_PRAGMATIC) else view.actions.confirm_end_movement = DEBUG } } else { if (!has_unmoved_piece(game.power)) view.actions.end_movement = 1 else view.actions.confirm_end_movement = DEBUG } }, re_enter() { push_undo() goto_re_enter_train() }, piece(p) { push_undo() set_active_to_power(piece_power[p]) game.selected = p game.count = 0 let here = game.pos[p] if (is_flanders_space(here)) game.flags |= F_MOVE_FLANDERS else game.flags &= ~F_MOVE_FLANDERS if (data.cities.main_roads[here].length > 0) game.main = 1 else game.main = 0 game.move_path = [ here ] if (is_supply_train(p)) game.state = "move_supply_train" else { game.supreme &= ~(1 << p) game.state = "move_general" } }, confirm_end_movement() { this.end_movement() }, end_movement() { push_undo() next_sequence_of_play() }, power(pow) { // cooperative on flanders map set_active_to_power(pow) }, restart() { let dispute = game.flags & F_MOVE_DISPUTE restore_checkpoint() game.state = "restart_flanders_movement" game.flags |= dispute // preserve disputed mode }, dispute() { restore_checkpoint() game.state = "dispute_flanders_movement" game.flags |= F_MOVE_DISPUTE }, } states.restart_flanders_movement = { dont_snap: true, inactive: "move", prompt() { prompt("Reset moves on Flanders map due to request.") view.actions.resume = 1 view.actions.undo = 0 }, resume() { game.state = "movement" }, } states.dispute_flanders_movement = { dont_snap: true, inactive: "move", prompt() { prompt("Alternating moves on Flanders map due to dispute.") view.actions.resume = 1 view.actions.undo = 0 }, resume() { log("Disputed Flanders movement.") game.state = "movement" }, } function end_movement() { clear_checkpoint() game.flags &= ~F_MOVE_DISPUTE game.flags &= ~F_MOVE_FLANDERS if (game.moved.length === 0) log("Nothing moved.") set_clear(game.moved) log_conquest(game.move_conq) delete game.move_conq delete game.move_re_entered next_sequence_of_play() } function format_move(max) { let n = max - game.count if (game.main) return ` up to ${n} cities (${n+1} on main roads).` return ` up to ${n} cities.` } function can_move_train_to(p, from, to) { if (is_forbidden_neutral_space(piece_power[p], to)) return false if (is_illegal_cross_map_move(from, to)) return false return !has_any_piece(to) } function is_illegal_cross_map_move(from, to) { if (is_intro() && is_flanders_space(to)) return true return ( game.power !== P_FRANCE && game.power !== P_AUSTRIA && ( (is_flanders_space(from) && is_bohemia_space(to)) || (is_flanders_space(to) && is_bohemia_space(from)) ) ) } function can_move_general_to(p, from, to) { if (is_forbidden_neutral_space(piece_power[p], to)) return false if (is_illegal_cross_map_move(from, to)) return false if (has_friendly_supply_train(to)) return false if (has_non_cooperative_general(to)) return false if (count_generals(to) >= 2) return false return true } function move_general_to(to, is_force_march) { let pow = game.power let who = game.selected let from = game.pos[who] let stop = false game.pos[game.selected] = to // Cannot conquer if force marching. // Cannot conquer if out of supply. if (!is_force_march && !is_out_of_supply(who)) { if (is_enemy_controlled_fortress(from)) { if (is_protected_from_conquest(from)) { // first one to place has prio (austria/prag on flanders) if (!map_has(game.retro, from)) map_set(game.retro, from, coop_major_power(game.power)) } else { set_add(game.move_conq, from) set_control_of_fortress(from, coop_major_power(game.power)) } } } // eliminate supply train for (let p of all_enemy_trains(pow)) { if (game.pos[p] === to) { if (!game.move_elim) game.move_elim = [] set_add(game.move_elim, p) game.pos[p] = ELIMINATED // NOTE: eliminating a supply train does not stop movement! } } // uniting stacks: stop moving for (let p of all_coop_generals(pow)) { if (game.pos[p] === to && game.selected !== p) { stop = true } } // remove hussars for (let p of all_hussars) { if (game.pos[p] === to) { if (!game.move_elim) game.move_elim = [] set_add(game.move_elim, p) game.pos[p] = ELIMINATED } } // 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 } states.move_supply_train = { inactive: "move", prompt() { prompt("Move supply train" + format_move(2)) view.selected = game.selected let who = game.selected let here = game.pos[who] if (game.count < 2 + game.main) for (let next of data.cities.main_roads[here]) if (can_move_train_to(who, here, next)) gen_action_space_or_piece(next) if (game.count < 2) for (let next of data.cities.roads[here]) if (can_move_train_to(who, here, next)) gen_action_space_or_piece(next) if (game.count > 0) gen_action_piece(who) view.actions.stop = 1 }, piece(p) { if (p === game.selected) this.stop() else this.space(game.pos[p]) }, stop() { end_move_piece() }, space(to) { let who = game.selected let from = game.pos[who] game.move_path.push(to) if (!set_has(data.cities.main_roads[from], to)) game.main = 0 // remove hussars for (let p of all_hussars) { if (game.pos[p] === to) { if (!game.move_elim) game.move_elim = [] set_add(game.move_elim, p) game.pos[p] = ELIMINATED } } game.pos[who] = to if (++game.count === 2 + game.main) end_move_piece() }, } function can_force_march(who, here) { for (let next of data.cities.main_roads[here]) if (can_move_general_to(who, here, next)) return true return false } states.move_general = { inactive: "move", prompt() { prompt("Move " + format_selected() + format_move(3)) view.selected = game.selected let who = game.selected let here = game.pos[who] if (game.count === 0) { if (can_force_march(who, here)) view.actions.force_march = 1 let s_take = count_stacked_take() let s_give = count_stacked_give() let u_take = count_unstacked_take() let u_give = count_unstacked_give() if (s_take > 0 && u_give > 0) view.actions.take = 1 if (s_give > 0 && u_take > 0) view.actions.give = 1 view.actions.stop = 1 } else { gen_action_piece(who) view.actions.stop = 1 } if (game.count < 3 + game.main) for (let next of data.cities.main_roads[here]) if (can_move_general_to(who, here, next)) gen_action_space_or_piece(next) if (game.count < 3) for (let next of data.cities.roads[here]) if (can_move_general_to(who, here, next)) gen_action_space_or_piece(next) }, take() { game.state = "move_take" }, give() { game.state = "move_give" }, piece(p) { if (p === game.selected) this.stop() else this.space(game.pos[p]) }, stop() { end_move_piece() }, space(to) { let who = game.selected let from = game.pos[who] game.move_path.push(to) if (!set_has(data.cities.main_roads[from], to)) game.main = 0 if (move_general_to(to, false) || ++game.count === 3 + game.main) end_move_piece() }, force_march() { game.state = "force_march" } } function is_adjacent_to_enemy_piece(here) { for (let next of data.cities.adjacent[here]) if (has_enemy_piece(next)) return true return false } function search_force_march(p, came_from, start, range) { let seen = [ start ] let queue = [ start << 4 ] while (queue.length > 0) { let item = queue.shift() let here = item >> 4 let dist = (item & 15) + 1 for (let next of data.cities.main_roads[here]) { if (set_has(seen, next)) continue if (is_enemy_controlled_fortress(next)) continue if (has_enemy_piece(next)) continue if (is_adjacent_to_enemy_piece(next)) continue if (!can_move_general_to(p, here, next)) continue if (came_from) map_set(came_from, next, here) set_add(seen, next) if (dist < range && !has_any_piece(next)) queue.push((next << 4) | dist) } } set_delete(seen, start) return seen } states.force_march = { inactive: "move", prompt() { prompt(`Force March ${format_selected()} up to ${8-game.count} cities.`) view.selected = game.selected let here = game.pos[game.selected] if (game.count < 8) for (let s of search_force_march(game.selected, null, here, 8 - game.count)) gen_action_space_or_piece(s) if (game.count > 0) { gen_action_piece(game.selected) view.actions.stop = 1 } }, piece(p) { if (p === game.selected) this.stop() else this.space(game.pos[p]) }, space(to) { let here = game.pos[game.selected] let came_from = [] let stop = false search_force_march(game.selected, came_from, here, 8 - game.count) let path = [] while (to !== here) { path.unshift(to) to = map_get(came_from, to) } for (let s of path) { game.move_path.push(s) stop = move_general_to(s, true) ++game.count } if (game.count === 8 || stop) end_move_piece() }, stop() { end_move_piece() }, } states.move_take = { inactive: "move", prompt() { prompt("Transfer troops to " + format_selected() + ".") view.selected = game.selected let take = count_stacked_take() let give = count_unstacked_give() let n = Math.min(take, give) view.actions.value = [] for (let i = 1; i <= n; ++i) view.actions.value.push(i) }, value(v) { take_troops(v) game.state = "move_general" }, } states.move_give = { inactive: "move", prompt() { prompt("Transfer troops from " + format_selected() + ".") view.selected = game.selected let take = count_unstacked_take() let give = count_stacked_give() let n = Math.min(take, give) view.actions.value = [] for (let i = 1; i <= n; ++i) view.actions.value.push(i) }, value(v) { give_troops(v) game.state = "move_general" }, } function is_flanders_stack_move() { if (!is_two_player()) { let here = game.pos[game.selected] if (is_flanders_space(here)) { if (game.power === P_PRAGMATIC) { if (find_general_of_power(here, P_AUSTRIA) >= 0) return true } if (game.power === P_AUSTRIA) { if (find_general_of_power(here, P_PRAGMATIC) >= 0) return true } } } return false } function is_alternating_move() { if (!is_two_player() && !is_intro() && (game.flags & F_MOVE_DISPUTE) && (game.flags & F_MOVE_FLANDERS)) { if (game.power === P_PRAGMATIC) { if (has_unmoved_piece_on_flanders_map(P_AUSTRIA)) return true } if (game.power === P_AUSTRIA) { if (has_unmoved_piece_on_flanders_map(P_PRAGMATIC)) return true } } return false } function flush_alternate_move() { log_conquest(game.move_conq) game.move_conq = [] log_br() } function end_move_piece() { let here = game.pos[game.selected] set_add(game.moved, game.selected) log_move_path() if (game.move_elim) { for (let p of game.move_elim) { if (is_hussar(p)) log("P" + p + " removed.") else log("P" + p + " eliminated.") } delete game.move_elim } delete game.move_path // uniting stacks: flag all as moved let supreme = false if (is_general(game.selected)) { for (let p of all_coop_generals(game.power)) { if (game.pos[p] === here && p !== game.selected) { if (piece_rank[p] === piece_rank[game.selected]) supreme = true set_add(game.moved, p) } } } if (supreme) game.state = "move_supreme" else if (is_flanders_stack_move()) goto_confirm_flanders_stack() else resume_movement() } states.move_supreme = { inactive: "move", prompt() { prompt("Choose supreme commander for mixed stack.") let here = game.pos[game.selected] for (let p of all_coop_generals(game.power)) { if (game.pos[p] === here) { gen_action("supreme", p) gen_action_piece(p) } } }, supreme(p) { let here = game.pos[game.selected] for (let p of all_coop_generals(game.power)) if (game.pos[p] === here) game.supreme &= ~(1< 1) game.state = "re_enter_train_power" else game.state = "re_enter_train" game.recruit = { pool: [], used: [], pieces: [], } game.count = 0 } function can_train_re_enter(p) { return ( (is_piece_on_map_or_eliminated(p)) && !set_has(game.moved, p) && has_re_entry_space_for_supply_train(piece_power[p]) ) } function has_re_entry_space_for_supply_train(pow) { if (coop_minor_power(pow) !== pow) return can_re_enter_train_at_power_fortress(pow) || can_re_enter_train_at_power_fortress(coop_minor_power(pow)) else return can_re_enter_train_at_power_fortress(pow) } function gen_re_enter_train_at_power_fortress(pow) { for (let s of all_home_country_major_fortresses[pow]) if (is_friendly_controlled_fortress(s) && !has_any_piece(s)) gen_action_space(s) } function can_re_enter_train_at_power_fortress(pow) { for (let s of all_home_country_major_fortresses[pow]) if (is_friendly_controlled_fortress(s) && !has_any_piece(s)) return true return false } states.re_enter_train_power = { inactive: "move", prompt() { prompt("Re-enter supply train from which power?") for (let pow of all_controlled_powers(game.power)) { if (game.move_re_entered & (1 << pow)) continue for (let p of all_power_trains[pow]) { if (can_train_re_enter(p)) { gen_action_power(pow) break } } } }, power(pow) { game.move_re_entered |= (1 << pow) set_active_to_power(pow) game.state = "re_enter_train" }, } states.re_enter_train = { inactive: "move", prompt() { let str let paid = game.count + sum_card_values(game.recruit.pool) let av_trains = 0 for (let p of all_power_trains[game.power]) { if (can_train_re_enter(p)) { if (paid >= 4) gen_action_piece(p) av_trains += 1 } } if (paid / 4 < av_trains) gen_cards_in_hand() if (game.recruit.used.length > 0) view.actions.next = 1 if (av_trains > 0) { str = "Re-enter supply trains for 4 each" if (paid > 1) str += " \u2014 " + paid + " points." else if (paid === 1) str += " \u2014 1 point." else str += "." } else { str = "Re-enter supply trains done." } prompt(str) view.draw = game.recruit.pool }, piece(p) { push_undo() spend_card_value(game.recruit.pool, game.recruit.used, 4) set_add(game.moved, p) map_set(game.recruit.pieces, p, game.pos[p]) game.state = "re_enter_train_where" game.selected = p }, card(c) { push_undo() remove_card_in_hand(c) set_add(game.recruit.pool, c) }, next() { push_undo() end_re_enter_train() }, } states.re_enter_train_where = { inactive: "move", prompt() { prompt("Re-enter supply train at a major fortress.") view.selected = game.selected view.draw = game.recruit.pool gen_re_enter_train_at_power_fortress(game.power) if (coop_minor_power(game.power) !== game.power) gen_re_enter_train_at_power_fortress(coop_minor_power(game.power)) }, space(to) { enter_train_at(game.selected, to) game.selected = -1 game.state = "re_enter_train" }, } function end_re_enter_train() { if (game.recruit.used.length > 0) { log_br() log(power_name[game.power] + " spent " + game.recruit.used.map(format_card).join(", ") + ".") map_for_each(game.recruit.pieces, (p, s) => { if (s !== ELIMINATED) log("Re-entered P" + p + " from S" + s + " at S" + game.pos[p] + ".") else log("Re-entered P" + p + " at S" + game.pos[p] + ".") }) log_br() } // put back into hand unused cards for (let c of game.recruit.pool) set_add(game.hand2[game.power], c) delete game.recruit set_active_to_power(coop_major_power(game.power)) game.state = "movement" } /* WINTER RECRUITMENT */ // TODO: swap saxony/austria order when not prussian-aligned? const POWER_FROM_WINTER_STAGE = [ P_FRANCE, P_BAVARIA, P_PRUSSIA, P_SAXONY, P_AUSTRIA, P_PRAGMATIC, ] function set_active_to_current_winter_stage() { set_active_to_power(POWER_FROM_WINTER_STAGE[game.stage]) } function goto_winter_turn() { // record winter scores for (let pow of all_major_powers) game.score[pow].push(count_victory_markers_in_pool(pow)) if (is_intro()) { if (game.turn === 10) { goto_game_over(player_from_power(P_AUSTRIA), "Austria survived 9 turns!") return } } if (game.turn === 16) { log("Winter Scores") let best_score = 1000 let best_power = -1 for (let pow of all_major_powers) { log(">" + power_name[pow] + " " + game.score[pow].join(", ")) let total = 0 for (let n of game.score[pow]) total += n if (total <= best_score) { best_score = total best_power = pow } } goto_game_over(player_from_power(best_power), power_name[best_power] + " won!") return } game.stage = 0 goto_winter_stage() } function goto_winter_stage() { set_active_to_current_winter_stage() if (is_intro() && game.power === P_PRAGMATIC) { end_winter_stage() return } log("=" + game.power) goto_recruit() } function end_winter_stage() { if (++game.stage === 6) goto_end_turn() else goto_winter_stage() } function goto_recruit() { game.count = 0 if (!can_recruit_anything()) { end_recruit() return } game.recruit = { pool: [], used: [], pieces: [], troops: 0, } game.state = "recruit" } function all_re_entry_cities_for_general(p) { if (p === ARENBERG) return all_arenberg_major_fortresses return all_home_country_major_fortresses[game.power] } function has_re_entry_space_for_general(p) { for (let s of all_re_entry_cities_for_general(p)) if (can_re_enter_general_at_city(s)) return true return false } function can_re_enter_general_at_city(to) { if (is_enemy_controlled_fortress(to)) return false if (has_friendly_supply_train(to)) return false if (has_non_cooperative_general(to)) return false if (count_generals(to) >= 2) return false return true } function can_recruit_anything() { for (let p of all_power_generals[game.power]) { // can re-enter generals if (is_piece_eliminated(p) && has_re_entry_space_for_general(p)) return true // can recruit troops? if (is_piece_on_map(p) && game.troops[p] < 8) return true } return false } states.recruit = { inactive: "recruit", prompt() { let av_general = 0 let av_troops = 0 for (let p of all_power_generals[game.power]) { if (is_intro() && is_flanders_space(game.pos[p])) continue if (is_piece_on_map(p)) av_troops += 8 - game.troops[p] else if (is_piece_eliminated(p) && has_re_entry_space_for_general(p)) { av_general += 1 av_troops += 8 } } let str if (av_general > 0 && av_troops > 0) str = `Re-enter generals and recruit up to ${av_troops} troops for 4 each` else if (av_troops > 0) str = `Recruit up to ${av_troops} troops for 4 each` else str = "Nothing to recruit" let paid = game.count + sum_card_values(game.recruit.pool) if (paid > 1) str += " \u2014 " + paid + " points." else if (paid === 1) str += " \u2014 1 point." else str += "." prompt(str) view.draw = game.recruit.pool if (av_troops > 0) { if (paid / 4 < av_troops) gen_cards_in_hand() if (paid >= 4) { for (let p of all_power_generals[game.power]) { if (is_intro() && is_flanders_space(game.pos[p])) continue if (game.troops[p] > 0 && game.troops[p] < 8) gen_action_piece(p) else if (is_piece_eliminated(p) && has_re_entry_space_for_general(p)) gen_action_piece(p) } } } if (paid < 4 || av_troops === 0) view.actions.end_recruit = 1 }, card(c) { push_undo() remove_card_in_hand(c) set_add(game.recruit.pool, c) }, piece(p) { push_undo() spend_card_value(game.recruit.pool, game.recruit.used, 4) if (is_piece_eliminated(p)) { game.selected = p game.state = "re_enter_general_where" } else { game.recruit.troops += 1 game.troops[p] += 1 } }, end_recruit() { push_undo() end_recruit() }, } function remove_hussars_at(s) { for (let p of all_hussars) { if (game.pos[p] === s) { log("P" + p + " removed.") game.pos[p] = ELIMINATED } } } function enter_piece_at(p, s) { if (is_general(p)) enter_general_at(p, s) else enter_train_at(p, s) } function enter_general_at(p, s) { game.pos[p] = s if (game.troops[p] < 1) game.troops[p] = 1 remove_hussars_at(s) // remove enemy supply trains for (let p of all_enemy_trains(game.power)) { if (game.pos[p] === s) { log("P" + p + " eliminated.") 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, s)) return_set_aside_markers(P_PRUSSIA, SET_ASIDE_PRUSSIA) } function enter_train_at(p, s) { game.pos[p] = s remove_hussars_at(s) } states.re_enter_general_where = { inactive: "recruit", prompt() { prompt("Re-enter " + format_selected() + ".") view.selected = game.selected for (let s of all_re_entry_cities_for_general(game.selected)) if (can_re_enter_general_at_city(s)) gen_action_space(s) }, space(s) { let p = game.selected set_add(game.recruit.pieces, p) enter_general_at(p, s) game.recruit.troops += 1 game.selected = -1 game.state = "recruit" }, } states.re_enter_general_from_political_card = { inactive: "execute political card", prompt() { prompt("Re-enter " + format_selected() + ".") view.selected = game.selected for (let s of all_re_entry_cities_for_general(game.selected)) if (can_re_enter_general_at_city(s)) gen_action_space(s) }, space(s) { let p = game.selected log("P" + p + " re-entered at S" + s + ".") enter_general_at(p, s) game.selected = -1 if (--game.count > 0) game.state = "political_troops_place" else next_execute_political_card() }, } function end_recruit() { if (game.recruit) { if (game.recruit.used.length > 0) { log_br() log("Recruited " + game.recruit.troops + " troops with " + game.recruit.used.map(format_card).join(", ") + ".") for (let p of game.recruit.pieces) log("Re-entered P" + p + " at S" + game.pos[p] + ".") } else { log("Recruited nothing.") } // put back into hand unused cards for (let c of game.recruit.pool) set_add(game.hand2[game.power], c) delete game.recruit } else { log("Recruited nothing.") } if (game.special_saxony_recruit) { delete game.special_saxony_recruit end_saxony_neutral() return } end_winter_stage() } /* COMBAT (CHOOSE TARGETS) */ function goto_combat() { let from = [] let to = [] for (let p of all_controlled_generals(game.power)) { if (piece_power[p] === P_PRUSSIA && is_prussia_neutral()) continue if (piece_power[p] === P_SAXONY && is_saxony_neutral()) continue if (is_piece_on_map(p)) set_add(from, game.pos[p]) } for (let p of all_enemy_generals(game.power)) { if (piece_power[p] === P_PRUSSIA && is_prussia_neutral()) continue if (piece_power[p] === P_SAXONY && is_saxony_neutral()) continue if (is_piece_on_map(p)) set_add(to, game.pos[p]) } game.combat = [] for (let a of from) { for (let b of to) { if (set_has(data.cities.adjacent[a], b)) { game.combat.push(a) game.combat.push(b) } } } if (game.combat.length > 0) game.state = "combat" else goto_retroactive_conquest() } function next_combat() { set_active_to_current_sequence_of_play() game.count = 0 delete game.attacker delete game.defender if (game.combat.length > 0) game.state = "combat" else goto_retroactive_conquest() } states.combat = { inactive: "attack", prompt() { prompt("Resolve your attacks.") for (let i = 0; i < game.combat.length; i += 2) gen_action_supreme_commander(game.combat[i]) }, piece(p) { push_undo() game.attacker = game.pos[p] game.state = "combat_target" }, } states.combat_target = { inactive: "attack", prompt() { prompt("Choose enemy stack to attack.") for (let i = 0; i < game.combat.length; i += 2) if (game.combat[i] === game.attacker) gen_action_supreme_commander(game.combat[i+1]) }, piece(p) { clear_undo() // reveal hidden information game.defender = game.pos[p] game.combat = map_filter(game.combat, (atk, def) => atk !== game.attacker || def !== game.defender) goto_resolve_combat() }, } function goto_resolve_combat() { let a_troops = 0 let d_troops = 0 for (let p of all_generals) { if (game.pos[p] === game.attacker) a_troops += game.troops[p] if (game.pos[p] === game.defender) d_troops += game.troops[p] } log_br() game.count = a_troops - d_troops let a = get_supreme_commander(game.attacker) let a_top = game.troops[a] let d = get_supreme_commander(game.defender) let d_top = game.troops[d] log("!") log(`>P${a} at S${game.attacker}`) log(`>P${d} at S${game.defender}`) if (is_mixed_stack(game.attacker) && is_mixed_stack(game.defender)) log(`>Troops (${a_top}+${a_troops-a_top}) - (${d_top}+${d_troops-d_top}) = ${game.count}`) else if (is_mixed_stack(game.attacker)) log(`>Troops (${a_top}+${a_troops-a_top}) - ${d_troops} = ${game.count}`) else if (is_mixed_stack(game.defender)) log(`>Troops ${a_troops} - (${d_top}+${d_troops-d_top}) = ${game.count}`) else log(`>Troops ${a_troops} - ${d_troops} = ${game.count}`) if (game.count <= 0) { set_active_attacker() game.state = "combat_attack" } else { set_active_defender() game.state = "combat_defend" } } function end_resolve_combat() { if (game.count === 0) { log(">Tied") next_combat() } else if (game.count > 0) { game.selected = select_stack(game.defender) goto_retreat() } else { game.selected = select_stack(game.attacker) goto_retreat() } } /* COMBAT (CARD PLAY) */ function format_combat_stack(s) { let p = get_supreme_commander(s) return suit_name[get_space_suit(s)] + " " + piece_name[p] } function signed_number(v) { if (v > 0) return "+" + v if (v < 0) return "-" + (-v) return "0" } function format_combat(value) { let a = format_combat_stack(game.attacker) let d = format_combat_stack(game.defender) let s = signed_number(value) let p = power_name[game.power] return `${a} vs ${d}. ${p} is at ${s}.` } function inactive_attack() { return "Waiting for " + format_combat(game.count) } function inactive_defend() { return "Waiting for " + format_combat(-game.count) } function prompt_combat(value, extra = null) { let text = format_combat(value) if (extra) text += " " + extra view.prompt = text } function set_active_attacker() { set_active_to_power(get_stack_power(game.attacker)) } function set_active_defender() { set_active_to_power(get_stack_power(game.defender)) } function resume_combat_attack() { if (game.count === 0) game.state = "combat_attack_swap" else if (game.count > 0) game.state = "combat_attack_swap" else game.state = "combat_attack" } function resume_combat_defend() { if (game.count === 0) game.state = "combat_defend_swap" else if (game.count < 0) game.state = "combat_defend_swap" else game.state = "combat_defend" } function gen_play_card(suit) { let score = Math.abs(game.count) let has_suit = false for_each_card_in_hand(c => { let c_suit = to_suit(c) if (c_suit === suit) { has_suit = true gen_action_card(c) } else if (c_suit === RESERVE) { gen_action_card(c) } }) // cannot pass if at 0 (and can play) if (score === 0 && has_suit) view.actions.pass = 0 else view.actions.pass = 1 } function gen_play_reserve() { view.draw = [ game.reserve ] view.actions.value = [ 1, 2, 3, 4, 5, 6, 7, 8 ] } function play_card(c, sign) { let prefix = (sign < 0 ? ">>" : ">") + power_name[game.power] if (sign < 0) game.count -= to_value(c) else game.count += to_value(c) let score = signed_number(sign * game.count) log(`${prefix} ${format_card(c)} = ${score}`) } function play_reserve(v, sign) { let c = game.reserve delete game.reserve let prefix = (sign < 0 ? ">>" : ">") + power_name[game.power] if (sign < 0) game.count -= v else game.count += v let score = signed_number(sign * game.count) log(`${prefix} ${format_reserve(c, v)} = ${score}`) } function play_combat_card(c, sign, resume, next_state) { push_undo() remove_card_in_hand(c) if (is_reserve(c)) { game.state = next_state game.reserve = c } else { play_card(c, sign) resume() } } states.combat_attack = { inactive: inactive_attack, prompt() { prompt_combat(game.count) gen_play_card(get_space_suit(game.attacker)) }, card(c) { play_combat_card(c, +1, resume_combat_attack, "combat_attack_reserve") }, pass() { end_resolve_combat() }, } states.combat_defend = { inactive: inactive_defend, prompt() { prompt_combat(-game.count) gen_play_card(get_space_suit(game.defender)) }, card(c) { play_combat_card(c, -1, resume_combat_defend, "combat_defend_reserve") }, pass() { end_resolve_combat() }, } states.combat_attack_reserve = { inactive: inactive_attack, prompt() { prompt_combat(game.count, "Choose value.") view.draw = [ game.reserve ] gen_play_reserve() }, value(v) { play_reserve(v, +1) resume_combat_attack() }, } states.combat_defend_reserve = { inactive: inactive_defend, prompt() { prompt_combat(-game.count, "Choose value.") view.draw = [ game.reserve ] gen_play_reserve() }, value(v) { play_reserve(v, -1) resume_combat_defend() }, } states.combat_attack_swap = { inactive: inactive_attack, prompt() { prompt_combat(game.count) view.actions.next = 1 }, next() { set_active_defender() game.state = "combat_defend" }, } states.combat_defend_swap = { inactive: inactive_defend, prompt() { prompt_combat(-game.count) view.actions.next = 1 }, next() { set_active_attacker() game.state = "combat_attack" }, } /* RETREAT */ function get_winner() { return game.count > 0 ? game.attacker : game.defender } function get_loser() { return game.count < 0 ? game.attacker : game.defender } function set_active_winner() { if (game.count > 0) set_active_attacker() else set_active_defender() } function remove_stack_from_combat(s) { for (let i = game.combat.length - 2; i >= 0; i -= 2) if (game.combat[i] === s || game.combat[i + 1] === s) array_remove_pair(game.combat, i) } function goto_retreat() { let hits = Math.abs(game.count) let lost = [ 0, 0, 0, 0, 0, 0 ] // per power! let loser = get_loser() // no more fighting for the loser remove_stack_from_combat(loser) // apply hits for (let i = game.selected.length - 1; i >= 0 && hits > 0; --i) { let p = game.selected[i] while (game.troops[p] > 1 && hits > 0) { lost[piece_power[p]]++ --game.troops[p] --hits } } for (let i = game.selected.length - 1; i >= 0 && hits > 0; --i) { let p = game.selected[i] while (game.troops[p] > 0 && hits > 0) { lost[piece_power[p]]++ --game.troops[p] --hits } } for (let pow of all_powers) if (lost[pow] > 0) log(power_name[pow] + " lost " + (lost[pow]) + " troops.") // track VP gained and lost if (game.count > 0) { game.winner_power = coop_major_power(get_stack_power(game.attacker)) game.loser_power = coop_major_power(get_stack_power(game.defender)) } else { game.winner_power = coop_major_power(get_stack_power(game.defender)) game.loser_power = coop_major_power(get_stack_power(game.attacker)) } game.lost_generals = 0 game.lost_troops = 0 for (let pow of all_powers) game.lost_troops += lost[pow] resume_retreat() } function resume_retreat() { // eliminate generals with no more hits for (let p of game.selected) { if (game.troops[p] === 0) { game.lost_generals += 1 game.state = "retreat_eliminate_hits" return } } // retreat remaining generals if (game.selected.length > 0) { game.retreat = search_retreat(get_loser(), get_winner(), Math.abs(game.count)) if (game.retreat.length > 0) { // victor chooses retreat destination set_active_winner() game.state = "retreat" } else { delete game.retreat game.state = "retreat_eliminate_trapped" } return } // no retreat if generals wiped out finish_combat() } states.retreat_eliminate_hits = { inactive: "retreat", prompt() { prompt("Eliminate generals without troops.") // remove eliminated generals for (let p of game.selected) if (game.troops[p] === 0) gen_action_piece(p) }, piece(p) { eliminate_general(p, false) set_delete(game.selected, p) resume_retreat() }, } states.retreat_eliminate_trapped = { inactive: "retreat", prompt() { prompt("Eliminate " + format_selected() + " without a retreat path.") // special case - bavarian + french mixed stack - eliminate bavarian first in case // french can retreat to flanders. if (game.selected.includes(BAVARIAN_GENERAL) && game.selected.length > 1) gen_action_piece(BAVARIAN_GENERAL) else for (let p of game.selected) gen_action_piece(p) }, piece(p) { log("Trapped.") game.lost_generals += 1 game.lost_troops += game.troops[p] eliminate_general(p, false) array_remove_item(game.selected, p) resume_retreat() }, } // search distances from winner within retreat range function search_retreat_distance(from, range) { let seen = [ from, 0 ] let queue = [ from << 4 ] while (queue.length > 0) { let item = queue.shift() let here = item >> 4 let dist = (item & 15) + 1 for (let next of data.cities.adjacent[here]) { if (map_has(seen, next)) continue if (dist <= range) { map_set(seen, next, dist) queue.push((next << 4) | dist) } } } return seen } function is_illegal_cross_map_retreat(from, to) { if ((is_flanders_space(from) && is_bohemia_space(to)) || (is_flanders_space(to) && is_bohemia_space(from))) { for (let p of game.selected) { let pow = piece_power[p] if (pow !== P_FRANCE && pow !== P_AUSTRIA) return true } } return false } // search all possible retreat paths of given length function search_retreat_possible_dfs(result, seen, here, range) { for (let next of data.cities.adjacent[here]) { if (seen.includes(next)) continue if (has_any_piece(next)) continue if (is_illegal_cross_map_retreat(here, next)) continue if (range === 1) { map_set(result, next, seen.slice(1)) } else { seen.push(next) search_retreat_possible_dfs(result, seen, next, range - 1) seen.pop() } } } function search_retreat_possible(from, range) { let result = [] search_retreat_possible_dfs(result, [from], from, range) return result } function search_retreat(loser, winner, range) { let distance = search_retreat_distance(winner, range + 1) let possible = search_retreat_possible(loser, range) let max = 0 map_for_each(possible, (s, _) => { let d = map_get(distance, s, -1) if (d > max) max = d }) let result = [] map_for_each(possible, (s, path) => { if (map_get(distance, s, -1) === max) map_set(result, s, path) }) return result } states.retreat = { inactive: "retreat defeated general", prompt() { prompt("Retreat " + format_selected() + " " + Math.abs(game.count) + " cities.") view.selected = game.selected map_for_each(game.retreat, (s, _) => { gen_action_space(s) }) }, space(to) { push_undo() log("Retreated to S" + to + ".") let path = map_get(game.retreat, to, 0) for (let s of path) remove_hussars_at(s) remove_hussars_at(to) for (let p of game.selected) game.pos[p] = to delete game.retreat game.state = "retreat_done" }, } states.retreat_done = { inactive: "retreat defeated general", prompt() { prompt("Retreat done.") view.actions.next = 1 }, next() { goto_validate_retreat() }, } function gain_vp(pow, n) { let old_vp = game.vp[pow] game.vp[pow] = Math.min(2, game.vp[pow] + n) if (game.vp[pow] > old_vp) log(`${power_name[pow]} +${game.vp[pow] - old_vp} VP.`) } function lose_vp(pow, n) { let old_vp = game.vp[pow] game.vp[pow] = Math.max(0, game.vp[pow] - n) if (game.vp[pow] < old_vp) log(`${power_name[pow]} -${old_vp - game.vp[pow]} VP.`) } function finish_combat() { clear_undo() game.selected = -1 if (is_intro()) { delete game.lost_generals delete game.lost_troops next_combat() return } let n = game.lost_troops / 3 | 0 if (n === 0 && game.lost_generals > 0) n = 1 if (n > 0) { gain_vp(game.winner_power, n) lose_vp(game.loser_power, n) } delete game.lost_generals delete game.lost_troops if (should_shift_saxony_after_battle(n)) { game.count = n set_active_to_power(P_AUSTRIA) game.state = "saxony_shift_battle" return } next_combat() } /* RETRO-ACTIVE CONQUEST */ function get_fortress_control_power(s) { for (let pow of all_major_powers) if (is_power_controlled_fortress(pow, s)) return pow return -1 } function log_conquest(conq) { if (conq.length > 0) { let groups = map_group_by(conq, get_fortress_control_power) map_for_each(groups, (pow, list) => { log_br() log(power_name[pow] + " Conquered") for (let s of list) log(">S" + s) }) } } function goto_retroactive_conquest() { delete game.combat let conq = [] map_for_each(game.retro, function (s, pow) { if (pow === game.power && is_enemy_controlled_fortress(s)) { if (!is_protected_from_conquest(s)) { set_control_of_fortress(s, pow) conq.push(s) } } }) game.retro = map_filter(game.retro, (_,pow) => pow !== game.power) log_conquest(conq) next_sequence_of_play() } /* VICTORY */ 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 for (let i = 0; i < 8; i += 2) { if (game.elector[i+1] === P_FRANCE) elector_france ++ else elector_pragmatic ++ } if (elector_france >= 3) return P_FRANCE if (elector_pragmatic >= 3) return P_PRAGMATIC return -1 } function count_victory_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 } if (pow === P_AUSTRIA) { if (game.flags & F_ITALY_AUSTRIA) ++n if (game.flags & F_EMPEROR_AUSTRIA) ++n } if (pow === P_PRAGMATIC) { if (elector_majority() === P_PRAGMATIC) ++n } for (let i = 0; i < game.victory.length; i += 2) if (game.victory[i+1] === pow) ++n return n } function count_victory_markers_in_pool(pow) { return Math.max(0, power_victory_target[pow] - count_victory_markers(pow)) } function count_victory_markers_in_country(pow, country) { let n = 0 map_for_each(game.victory, (vspace, vpow) => { if (vpow === pow && set_has(country, vspace)) ++n }) return n } function check_instant_victory_intro() { let win_france = count_victory_markers_in_country(P_FRANCE, all_core_austria_fortresses) >= 9 let win_prussia = count_victory_markers_in_country(P_PRUSSIA, all_core_austria_and_silesia_fortresses) >= 12 if (is_two_player()) { if (win_france && win_prussia) { goto_game_over(R_PLAYER_A, "France controls 9 fortresses and Prussia controls 12!") return true } } else { if (win_france) { goto_game_over(player_from_power(P_FRANCE), "France controls 9 fortresses in Austria!") return true } if (win_prussia) { goto_game_over(player_from_power(P_PRUSSIA), "Prussia controls 12 fortresses in Austria and Silesia!") return true } } return false } function check_instant_victory() { if (is_intro()) return check_instant_victory_intro() let margin = [ count_victory_markers(P_FRANCE) - 11, count_victory_markers(P_PRUSSIA) - 13, count_victory_markers(P_PRAGMATIC) - 8, count_victory_markers(P_AUSTRIA) - 8, ] let best = 0 for (let pow = 1; pow < 4; ++pow) if (margin[pow] >= margin[best]) best = pow if (margin[best] >= 0) { goto_game_over(player_from_power(best), power_name[best] + " instant victory!") return true } return false } /* POLITICS */ const POWER_FROM_POLITICAL_STAGE = [ P_PRUSSIA, P_FRANCE, P_PRAGMATIC, P_AUSTRIA, ] const INFLUENCE_ORDER = [ P_AUSTRIA, P_PRAGMATIC, P_FRANCE, P_PRUSSIA, ] function goto_politics() { clear_undo() // reveal random cards game.political = [] game.stage = 0 game.save_saxony = game.saxony // 25.1 Return face-down (previously placed) TCs to the players for (let pow of all_major_powers) { for (let c of game.face_down[pow]) set_add(game.hand2[pow], c) game.face_down[pow] = [] } // 25.2 Reveal 2 Political Cards while (game.political.length < 2) { let pc = game.pol_deck.pop() log("C" + pc) log("%" + pc) log("") if (pc === IMPERIAL_ELECTION) game.flags |= F_IMPERIAL_ELECTION else game.political.push(pc) } // 25.3 Determine the political trump suit if (game.winner_power >= 0) { set_active_to_power(game.winner_power) game.state = "determine_trump_suit" } else { for (;;) { let list = [] draw_tc(list, 1) log("Trump " + format_card(list[0]) + ".") if (!is_reserve(list[0])) { game.trump = to_suit(list[0]) break } } log_br() goto_place_tc_on_display() } } states.determine_trump_suit = { inactive: "determine the political trump suit", prompt() { prompt("Determine the political trump suit.") view.actions.suit = [ 0, 1, 2, 3 ] }, suit(s) { push_undo() game.trump = s log(power_name[game.power] + " chose " + suit_name[game.trump] + " as trump.") log_br() game.state = "determine_trump_suit_done" } } states.determine_trump_suit_done = { inactive: "determine the political trump suit", prompt() { prompt("Political trump suit is " + suit_name[game.trump] + ".") view.actions.next = 1 }, next() { goto_place_tc_on_display() } } // 25.4 Place TCs on the political display function goto_place_tc_on_display() { log("Place TC on political display") next_place_tc_on_display() } function next_place_tc_on_display() { set_active_to_power(POWER_FROM_POLITICAL_STAGE[game.stage]) game.state = "place_tc_on_display" } states.place_tc_on_display = { inactive: "place TC on political display", prompt() { prompt(`Place TC on political display (${suit_name[game.trump]} is trump).`) gen_cards_in_hand() view.actions.pass = 1 }, card(c) { push_undo() log(">" + power_name[game.power] + " placed TC") remove_card_in_hand(c) set_add(game.face_down[game.power], c) game.state = "place_tc_on_display_done" }, pass() { log(">" + power_name[game.power] + " passed") end_place_tc_on_display() }, } states.place_tc_on_display_done = { inactive: "place TC on political display", prompt() { prompt(`Place TC on political display (${suit_name[game.trump]} is trump) done.`) view.actions.next = 1 }, next() { end_place_tc_on_display() }, } function end_place_tc_on_display() { if (++game.stage === 4) goto_determine_order_of_influence() else next_place_tc_on_display() } // 25.5 Determine order of influence function goto_determine_order_of_influence() { log_br() log("Order of influence") // Turn cards face-up (and turn bluff cards face down again) for (let pow of all_major_powers) { for (let i = 0; i < game.face_down[pow].length;) { let c = game.face_down[pow][i] if (is_trump_card(c)) { array_remove(game.face_down[pow], i) set_add(game.face_up[pow], c) } else { ++i } } } // Reveal let list = POWER_FROM_POLITICAL_STAGE.slice() list.reverse() list.sort((a,b) => count_influence(b) - count_influence(a)) for (let pow of list) { if (game.face_down[pow].length > 0) log(">" + power_name[pow] + " " + format_card_list(game.face_down[pow]) + " (bluff)") if (game.face_up[pow].length > 0) log(">" + power_name[pow] + " " + format_card_list(game.face_up[pow])) } log_br() game.stage = 0 goto_select_political_card() } function count_influence(pow) { let n = 0 for (let c of game.face_up[pow]) if (is_reserve(c)) n += 16 else n += to_value(c) return n } function most_influence() { let p_most = -1 let n_most = 0 for (let pow of INFLUENCE_ORDER) { if (game.stage & (1 << pow)) continue let n = count_influence(pow) if (n > n_most) { n_most = n p_most = pow } } return p_most } // 25.6 Select Political Cards function goto_select_political_card() { if (game.political.length > 0) { let pow = most_influence() if (pow < 0) { end_politics() } else { game.stage |= 1 << pow set_active_to_power(pow) log("=" + game.power) if (is_two_player()) { if ((game.flags & F_PLAYER_A_PICK) && (game.power === P_FRANCE || game.power === P_PRUSSIA)) { game.state = "must_save_tc" return } if ((game.flags & F_PLAYER_B_PICK) && (game.power === P_PRAGMATIC || game.power === P_AUSTRIA)) { game.state = "must_save_tc" return } } game.state = "select_political_card" save_checkpoint() } } else { end_politics() } } states.must_save_tc = { inactive: "select a political card", prompt() { prompt(`Save your TC.`) view.actions.save = 1 }, save() { log(power_name[game.power] + " saved TC.") goto_select_political_card() }, } states.select_political_card = { inactive: "select a political card", prompt() { prompt(`Select a political card or save your TC.`) for (let pc of game.political) if (set_has(political_cards[pc].powers, game.power)) gen_action_political(pc) view.actions.save = 1 }, political(pc) { push_undo() log("C" + pc) // face-up TCs to discard game.face_up[game.power] = [] if (is_two_player()) { if (game.power === P_FRANCE || game.power === P_PRUSSIA) game.flags |= F_PLAYER_A_PICK if (game.power === P_PRAGMATIC || game.power === P_AUSTRIA) game.flags |= F_PLAYER_B_PICK } game.pc = pc game.pcx = -1 game.state = "political_card_discard_or_execute" }, save() { log(power_name[game.power] + " saved TC.") goto_validate_politics() }, } states.political_card_discard_or_execute = { inactive: "select a political card", prompt() { prompt(`Execute or discard "${political_cards[game.pc].title}".`) view.pc = game.pc view.actions.execute = 1 view.actions.discard = 1 }, execute() { push_undo() log(power_name[game.power] + " executed card.") next_execute_political_card() }, discard() { push_undo() log("Discarded with no effect.") game.state = "political_card_done" }, } function end_politics() { delete game.political game.trump = -1 if (is_two_player()) { game.flags &= ~F_PLAYER_A_PICK game.flags &= ~F_PLAYER_B_PICK } // did not take a political turn; flip all cards face-down for (let pow of all_major_powers) { if (!(game.stage & (1 << pow))) { for (let c of game.face_up[pow]) set_add(game.face_down[pow], c) game.face_up[pow] = [] } } goto_adjust_political_tracks() } function end_adjust_political_tracks() { if (check_instant_victory()) return start_sequence_of_play() } /* POLITICAL CARDS */ const event_shift = { "Italy +1": { track: "italy", amount: [ 1 ] }, "Italy +2": { track: "italy", amount: [ 2 ] }, "Italy -1 or +1": { track: "italy", amount: [ -1, 1 ] }, "Italy -1 or +2": { track: "italy", amount: [ -1, 2 ] }, "Italy -1": { track: "italy", amount: [ -1 ] }, "Italy -2 or +1": { track: "italy", amount: [ -2, 1 ] }, "Italy -2": { track: "italy", amount: [ -2 ] }, "Russia +1": { track: "russia", amount: [ 1 ] }, "Russia -1 or +1": { track: "russia", amount: [ -1, 1 ] }, "Russia -1 or +2": { track: "russia", amount: [ -1, 2 ] }, "Russia -1": { track: "russia", amount: [ -1 ] }, "Russia -2": { track: "russia", amount: [ -2 ] }, "Saxony +1": { track: "saxony", amount: [ 1 ] }, "Saxony +4": { track: "saxony", amount: [ 4 ] }, "Saxony -1 if allied with Prussia": { track: "saxony", amount: [ -1 ], condition: () => game.saxony < 3, }, } const event_troops = { "Your major power +3 troops": { tcs: 0, troops: 3, power: () => [ game.power ] }, "France +1 TC and +3 troops": { tcs: 1, troops: 3, power: () => [ P_FRANCE ] }, "Pragmatic +1 TC and +3 troops": { tcs: 1, troops: 3, power: () => [ P_PRAGMATIC ] }, "Pragmatic or France +1 TC and +2 troops": { tcs: 1, troops: 2, power: () => [ P_PRAGMATIC, P_FRANCE ] }, "France or Bavaria +2 troops": { tcs: 0, troops: 2, power: () => [ P_FRANCE, P_BAVARIA ] }, "Saxony +2 troops": { tcs: 0, troops: 2, power: () => [ P_SAXONY ] }, } const event_misc = { "Mannheim to French control": goto_mannheim_to_french_control, "Pragmatic general to England": goto_pragmatic_general_to_england, "France -1 TC this turn": goto_war_of_jenkins_ear, } function current_political_effect() { return political_cards[game.pc].effects[game.pcx] } function next_execute_political_card() { if (++game.pcx === political_cards[game.pc].effects.length) { game.state = "political_card_done" return } let fx = current_political_effect() if (fx in event_shift) game.state = "political_shift" else if (fx in event_troops) game.state = "political_troop_power" else if (fx in event_misc) event_misc[fx]() } states.political_card_done = { inactive: "execute political card", prompt() { prompt("Political card done.") view.pc = game.pc view.actions.end_political_card = 1 }, end_political_card() { push_undo() end_execute_political_card() }, } function end_execute_political_card() { // Ugly hack to restore the political card executor by peeking at the checkpoint state! set_active_to_power(game.restart.power) array_remove_item(game.political, game.pc) delete game.pc delete game.pcx goto_validate_politics() } const TRACK_NAME = { saxony: "Saxony marker", russia: "Russia marker", italy: "Italy marker" } states.political_shift = { inactive: "execute political card", prompt() { let info = event_shift[current_political_effect()] prompt("Shift " + TRACK_NAME[info.track] + ".") if (info.condition === undefined || info.condition()) { if (info.track === "saxony") view.actions.shift_saxony = info.amount if (info.track === "russia") view.actions.shift_russia = info.amount if (info.track === "italy") view.actions.shift_italy = info.amount } view.actions.ignore = 1 view.pc = game.pc }, shift_italy(n) { this.shift(n) }, shift_russia(n) { this.shift(n) }, shift_saxony(n) { this.shift(n) }, shift(n) { push_undo() let info = event_shift[current_political_effect()] game[info.track] += n if (n < 0) log(TRACK_NAME[info.track] + " " + (-n) + " left.") else log(TRACK_NAME[info.track] + " " + (n) + " right.") next_execute_political_card() }, ignore() { push_undo() let info = event_shift[current_political_effect()] log("Did not shift " + TRACK_NAME[info.track] + ".") next_execute_political_card() }, } states.political_troop_power = { inactive: "execute political card", prompt() { let info = event_troops[current_political_effect()] let powers = info.power().map(pow => power_name[pow]).join(" or ") if (info.tcs > 0) prompt(powers + " receives 1 TC and " + info.troops + " troops.") else prompt(powers + " receives " + info.troops + " troops.") view.pc = game.pc view.actions.power = info.power() view.actions.ignore = 1 }, power(pow) { // TODO: delay until after validation let info = event_troops[current_political_effect()] set_active_to_power(pow) if (info.tcs > 0) { clear_undo() // reveal random cards log(power_name[pow] + " TC.") draw_tc(game.draw = [], info.tcs, game.power) game.state = "political_troops_draw" } else { goto_political_troops_place() } }, ignore() { let info = event_troops[current_political_effect()] if (info.tcs > 0) log("Did not receive TC and troops.") else log("Did not receive troops.") next_execute_political_card() }, } states.political_troops_draw = { inactive: "execute political card", prompt() { prompt("Draw " + format_card_list_prompt(game.draw) + ".") view.draw = game.draw view.actions.next = 1 }, next() { push_undo() let info = event_troops[current_political_effect()] if (info.tcs > 0) { for (let c of game.draw) set_add(game.hand1[game.power], c) delete game.draw } goto_political_troops_place() }, } function can_add_troops() { for (let p of all_power_generals[game.power]) { // can re-enter generals if (is_piece_eliminated(p) && has_re_entry_space_for_general(p)) return true // can recruit troops? (even to off-map boxes) if (!is_piece_eliminated(p) && game.troops[p] < 8) return true } return false } function goto_political_troops_place() { let info = event_troops[current_political_effect()] game.count = info.troops if (can_add_troops()) { log(power_name[game.power] + " " + game.count + " troops.") game.state = "political_troops_place" } else { log(power_name[game.power] + " cannot receive troops.") next_execute_political_card() } } states.political_troops_place = { inactive: "execute political card", prompt() { if (game.count > 1) prompt("Recieve " + game.count + " troops.") else if (game.count === 1) prompt("Recieve 1 troop.") if (game.count > 0) { for (let p of all_power_generals[game.power]) { if (!is_piece_eliminated(p) && game.troops[p] < 8) gen_action_piece(p) if (is_piece_eliminated(p) && has_re_entry_space_for_general(p)) gen_action_piece(p) } } }, piece(p) { push_undo() if (game.pos[p] === ELIMINATED) { game.selected = p game.state = "re_enter_general_from_political_card" } else { game.troops[p] += 1 if (--game.count === 0 || !can_add_troops()) next_execute_political_card() } }, } function goto_mannheim_to_french_control() { log("Mannheim to French control.") map_set(game.elector, MANNHEIM, P_FRANCE) next_execute_political_card() } function goto_war_of_jenkins_ear() { game.state = "war_of_jenkins_ear" } states.war_of_jenkins_ear = { inactive: "execute political card", prompt() { prompt("France -1 TC this turn.") view.actions.accept = 1 view.actions.ignore = 1 }, accept() { push_undo() log("France -1 TC this turn.") game.flags |= F_WAR_OF_JENKINS_EAR next_execute_political_card() }, ignore() { push_undo() next_execute_political_card() }, } function goto_pragmatic_general_to_england() { log("Pragmatic Army -1 TC until game end.") set_active_to_power(P_PRAGMATIC) game.selected = ENGLAND game.state = "send_expeditionary_corps" } /* POLITICAL TRACKS */ function is_saxony_neutral() { return game.saxony > 2 && game.saxony < 5 } function is_saxony_prussian() { return game.saxony <= 2 } function is_saxony_austrian() { // Austrian control, but neutral return game.saxony >= 3 } function is_saxony_austrian_ally() { return game.saxony === 5 } function should_shift_saxony_after_battle(n) { if (is_intro()) return false if (n > 0 && game.saxony < 5) { if (game.loser_power === P_PRUSSIA) return true if (is_two_player() && game.loser_power === P_FRANCE && is_bohemia_space(game.attacker)) return true } return false } function should_saxony_shift_after_supply(pow) { if (is_intro()) return false if (game.saxony < 5) { if (pow === P_PRUSSIA) return true if (is_two_player() && pow === P_FRANCE) return true } return false } states.saxony_shift_battle = { inactive: "shift Saxony marker", prompt() { prompt(`Shift Saxony marker ${game.count} to the right.`) view.actions.shift_saxony = [ 1 ] view.actions.ignore = 1 }, shift_saxony(_) { log("Saxony marker " + game.count + " right.") let save_saxony = game.saxony game.saxony = Math.max(1, Math.min(5, game.saxony + game.count)) game.count = 0 // Saxony defection if (save_saxony < 3 && is_saxony_neutral()) goto_saxony_becomes_neutral("combat") else if (save_saxony < 5 && is_saxony_austrian_ally()) goto_saxony_becomes_austrian_ally("combat") else next_combat() }, ignore() { log("Did not shift Saxony marker.") game.count = 0 next_combat() }, } states.saxony_shift_supply = { inactive: "shift Saxony marker", prompt() { prompt(`Shift Saxony marker up to ${game.count} to the right.`) view.actions.shift_saxony = [] for (let i = 1; i <= game.count; ++i) view.actions.shift_saxony.push(i) view.actions.ignore = 1 }, shift_saxony(n) { log("Saxony marker " + n + " right.") let save_saxony = game.saxony game.saxony = Math.max(1, Math.min(5, game.saxony + n)) game.count = 0 // Saxony defection if (save_saxony < 3 && is_saxony_neutral()) goto_saxony_becomes_neutral("supply") else if (save_saxony < 5 && is_saxony_austrian_ally()) goto_saxony_becomes_austrian_ally("supply") else resume_supply() }, ignore() { log("Did not shift Saxony marker.") game.count = 0 resume_supply() }, } function goto_adjust_political_tracks() { // clamp final values to track game.saxony = Math.max(1, Math.min(5, game.saxony)) game.russia = Math.max(1, Math.min(9, game.russia)) game.italy = Math.max(1, Math.min(9, game.italy)) // TODO: log italy vp change if (game.italy <= 2) game.flags |= F_ITALY_FRANCE if (game.italy <= 5) game.flags &= ~F_ITALY_AUSTRIA if (game.italy >= 5) game.flags &= ~F_ITALY_FRANCE if (game.italy >= 8) game.flags |= F_ITALY_AUSTRIA // Expeditionary corps goto_expeditionary_corps() } function check_expeditionary_corps(power, active) { let p = find_off_map_piece(power) if (active && p < 0) { set_active_to_power(power) game.state = "send_expeditionary_corps" return 1 } if (!active && p >= 0) { set_active_to_power(power) goto_return_expeditionary_corps() return 1 } return 0 } function goto_expeditionary_corps() { if (check_expeditionary_corps(P_PRUSSIA, game.russia >= 5)) return if (check_expeditionary_corps(P_FRANCE, game.italy >= 8)) return if (check_expeditionary_corps(P_AUSTRIA, game.italy <= 2)) return goto_saxony_defection() } function goto_saxony_defection() { // Saxony defection let save_saxony = game.save_saxony delete game.save_saxony if (save_saxony < 3 && is_saxony_neutral()) goto_saxony_becomes_neutral("political") else if (save_saxony < 5 && is_saxony_austrian_ally()) goto_saxony_becomes_austrian_ally("political") else end_adjust_political_tracks() } /* EXPEDITIONARY CORPS */ function off_map_box(pow) { if (pow === P_PRUSSIA) return EAST_PRUSSIA if (pow === P_FRANCE) return FRENCH_ITALY_BOX if (pow === P_AUSTRIA) return AUSTRIAN_ITALY_BOX if (pow === P_PRAGMATIC) return ENGLAND return -1 } function off_map_entry_space(pow) { if (pow === P_PRUSSIA) return WOLDENBURG if (pow === P_FRANCE) return OMANS if (pow === P_AUSTRIA) return STEINAMANGER if (pow === P_PRAGMATIC) return GRONINGEN return -1 } function find_off_map_piece(pow) { return find_general_of_power(off_map_box(pow), pow) } function goto_return_expeditionary_corps() { game.state = "return_expeditionary_corps" if (can_move_piece_to(find_off_map_piece(game.power), ELIMINATED, off_map_entry_space(game.power))) game.state = "return_expeditionary_corps" else game.state = "return_expeditionary_corps_push" } states.return_expeditionary_corps_push = { inactive: "return expeditionary corps", prompt() { prompt("Move blocking pieces out of the way.") let s = off_map_entry_space(game.power) for (let p of all_pieces) if (game.pos[p] === s) gen_action_piece(p) }, piece(p) { push_undo() set_active_to_power(piece_power[p]) game.selected = p game.state = "return_expeditionary_corps_push_to" }, } states.return_expeditionary_corps_push_to = { inactive: "return expeditionary corps", prompt() { prompt("Move blocking pieces out of the way.") view.selected = game.selected for (let s of search_nearest_city(game.selected, game.pos[game.selected])) gen_action_space(s) }, space(to) { let who = game.selected let from = game.pos[who] log("P" + who + " from S" + from + " to S" + to + ".") enter_piece_at(who, to) game.selected = -1 if (has_any_piece(from)) game.state = "return_expeditionary_corps_push" else goto_expeditionary_corps() }, } states.return_expeditionary_corps = { inactive: "return expeditionary corps", prompt() { prompt("Return expeditionary corps.") view.selected = find_off_map_piece(game.power) gen_action_space(off_map_entry_space(game.power)) }, space(to) { let who = find_off_map_piece(game.power) let from = game.pos[who] log("P" + who + " from S" + from + " to S" + to + ".") enter_piece_at(who, to) goto_expeditionary_corps() }, } states.send_expeditionary_corps = { inactive: "send expeditionary corps off map", prompt() { prompt("Send expeditionary corps to off map box.") for (let p of all_power_generals[game.power]) if (is_piece_on_map_or_eliminated(p)) gen_action_piece(p) }, piece(p) { push_undo() game.selected = p // transfer troops to other stacked general if possible let take = count_unstacked_take() let give = count_stacked_give() let xfer = Math.min(take, give) if (xfer > 0) give_troops(xfer) let s = off_map_box(game.power) log(`Expeditionary corps P${p} from S${game.pos[p]} to S${s}.`) game.pos[p] = s if (game.troops[p] === 0) { game.recruit = { pool: [], used: [], } game.state = "recruit_for_expeditionary_corps" } else { game.state = "send_expeditionary_corps_done" } }, } states.send_expeditionary_corps_done = { inactive: "send expeditionary corps off map", prompt() { prompt("Send expeditionary corps to off map box done.") view.actions.next = 1 }, next() { game.selected = -1 if (game.power === P_PRAGMATIC) next_execute_political_card() else goto_expeditionary_corps() }, } states.recruit_for_expeditionary_corps = { inactive: "send expeditionary corps off map", prompt() { prompt("Recruit 2 troops for expeditionary corps at a price of 8 TC-points.") view.draw = game.recruit.pool if (sum_card_values(game.recruit.pool) >= 8 || count_cards_in_hand() === 0) view.actions.next = 1 else gen_cards_in_hand() }, card(c) { push_undo() remove_card_in_hand(c) set_add(game.recruit.pool, c) }, next() { push_undo() if (sum_card_values(game.recruit.pool) >= 8) { spend_card_value(game.recruit.pool, game.recruit.used, 8) } else { game.recruit.used = game.recruit.pool game.recruit.pool = [] } log(power_name[game.power] + " spent " + game.recruit.used.map(format_card).join(", ") + ".") // put back into hand unused cards for (let c of game.recruit.pool) set_add(game.hand2[game.power], c) delete game.recruit game.troops[game.selected] = 2 game.selected = -1 if (game.power === P_PRAGMATIC) next_execute_political_card() else goto_expeditionary_corps() }, } /* SAXONY'S DEFECTION */ function search_nearest_city(p, from) { let result = [] let min_dist = 1000 let seen = [ from ] let queue = [ from << 4 ] while (queue.length > 0) { let item = queue.shift() let here = item >> 4 let dist = (item & 15) + 1 for (let next of data.cities.adjacent[here]) { if (set_has(seen, next)) continue set_add(seen, next) if (can_move_piece_to(p, from, next)) { if (dist <= min_dist) { min_dist = dist set_add(result, next) } } if (dist < min_dist) queue.push((next << 4) | dist) } } return result } function search_nearest_home_city(p) { let from = game.pos[p] let result = [] let min_dist = 1000 let seen = [ from ] let queue = [ from << 4 ] while (queue.length > 0) { let item = queue.shift() let here = item >> 4 let dist = (item & 15) + 1 for (let next of data.cities.adjacent[here]) { if (set_has(seen, next)) continue set_add(seen, next) if (is_home_country_for_return(next)) { if (can_move_piece_to(p, from, next)) { if (dist <= min_dist) { min_dist = dist set_add(result, next) } } } if (dist < min_dist) queue.push((next << 4) | dist) } } return result } function power_has_any_piece_in_list(pow, list) { for (let p of all_power_generals[pow]) if (set_has(list, game.pos[p])) return true for (let p of all_power_trains[pow]) if (set_has(list, game.pos[p])) return true } function goto_saxony_becomes_neutral(reason) { game.saxony_reason = reason set_active_to_power(P_SAXONY) log_br() log("Saxony becomes neutral!") // invalidate contracts with former allies map_delete(game.contracts[P_FRANCE], P_SAXONY) map_delete(game.contracts[P_PRUSSIA], P_SAXONY) // Return all victory markers log("Removed victory markers.") for (let s of all_home_country_fortresses[P_SAXONY]) { let pow = map_get(game.victory, s, -1) if (pow >= 0) { map_delete(game.victory, s) } } goto_saxony_return_foreign_pieces() } function goto_saxony_return_foreign_pieces() { for (let pow of all_powers) { if (pow === P_SAXONY) continue if (power_has_any_piece_in_list(pow, data.country.Saxony)) { set_active_to_power(pow) game.state = "saxony_return_foreign_who" return } } goto_saxony_return_home() } states.saxony_return_foreign_who = { inactive: "return foreign pieces from Saxony", prompt() { prompt("Return pieces to the nearest city in their home country.") let done = true for (let p of all_power_pieces[game.power]) { if (set_has(data.country.Saxony, game.pos[p])) { gen_action_piece(p) done = false } } if (done) view.actions.next = 1 }, piece(p) { push_undo() game.selected = p game.state = "saxony_return_foreign_where" }, next() { goto_saxony_return_foreign_pieces() }, } states.saxony_return_foreign_where = { inactive: "return foreign pieces from Saxony", 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 = "saxony_return_foreign_who" }, } function goto_saxony_return_home() { set_active_to_power(P_SAXONY) game.state = "saxony_return_home" } states.saxony_return_home = { inactive: "become neutral", prompt() { prompt("Return pieces to their set-up cities.") let done = true 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 } } if (done) view.actions.next = 1 }, piece(p) { let s = setup_piece_position[p] game.pos[p] = s log(">P" + p + " to S" + s) }, next() { end_saxony_neutral() }, } function goto_saxony_becomes_austrian_ally(reason) { game.saxony_reason = reason set_active_to_power(P_SAXONY) log_br() log("Saxony becomes Austrian ally!") // invalidate contracts with former allies map_delete(game.contracts[P_FRANCE], P_SAXONY) map_delete(game.contracts[P_PRUSSIA], P_SAXONY) game.selected = SAXONY_GENERAL if (game.pos[SAXONY_GENERAL] === ELIMINATED) { game.special_saxony_recruit = 1 goto_recruit() return } // 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() } states.saxony_move_general = { inactive: "re-enter general", prompt() { prompt("Move " + format_selected() + " to the nearest empty city.") view.selected = game.selected for (let s of search_nearest_city(game.selected, game.pos[game.selected])) gen_action_space(s) }, space(s) { log(">P" + game.selected + " to S" + s) enter_piece_at(game.selected, s) end_saxony_neutral() }, } function end_saxony_neutral() { let reason = game.saxony_reason delete game.saxony_reason game.selected = -1 // Silesia annexed! if (reason === "silesia") { goto_annex_silesia_return_austrian_pieces() return } // Political Card event shift if (reason === "political") { end_adjust_political_tracks() return } // Out of Supply shift if (reason === "supply") { end_supply() return } // Battle Victory shift if (reason === "combat") { next_combat() return } throw "IMPOSSIBLE" } /* PRUSSIA ANNEXES SILESIA */ function goto_prussia_ends_neutrality() { if (game.flags & F_PRUSSIA_NEUTRAL_2) { game.flags &= ~F_PRUSSIA_NEUTRAL_2 } else if (game.flags & F_PRUSSIA_NEUTRAL) { game.flags &= ~F_PRUSSIA_NEUTRAL log("Prussian Neutrality ended.") } next_sequence_of_play() } function goto_prussia_annexes_silesia() { if (has_prussia_conquered_silesia()) game.state = "offer_peace" else next_sequence_of_play() } function is_prussia_neutral() { 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 peace with Austria?") view.actions.peace = 1 view.actions.pass = 1 }, peace() { log("Annexion of Silesia proposed.") set_active_to_power(P_AUSTRIA) game.state = "accept_peace" }, pass() { log("Annexion of Silesia passed.") next_sequence_of_play() }, } states.accept_peace = { inactive: "accept peace", prompt() { prompt("Accept Prussia's offer of peace for annexion of Silesia?") view.actions.accept = 1 view.actions.reject = 1 }, accept() { log("Austria accepts peace.") goto_annex_silesia() }, reject() { log("Austria refuses peace.") next_sequence_of_play() }, } function goto_annex_silesia() { log_br() log("=1 Annexion of Silesia") game.flags |= F_SILESIA_ANNEXED game.flags |= F_PRUSSIA_NEUTRAL game.flags |= F_PRUSSIA_NEUTRAL_2 // remove all austrian markers in prussia 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) } } log("Removed all Austrian victory markers in Prussia.") // set aside half prussian markers in prussia let n = 0 for (let s of all_core_austria_fortresses) { let pow = map_get(game.victory, s, -1) if (pow === P_PRUSSIA) { map_delete(game.victory, s) ++n } } log("Removed " + n + " Prussian victory markers in Austria.") n = (n + 1) >> 1 log("Set aside " + n + " Prussian victory markers.") game.vp[SET_ASIDE_PRUSSIA] = n if (is_saxony_prussian()) { log("Saxony shifted to neutral.") game.saxony = 3 goto_saxony_becomes_neutral("silesia") return } goto_annex_silesia_return_austrian_pieces() } 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.") let done = true 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) done = false } } if (done) view.actions.next = 1 }, piece(p) { push_undo() game.selected = p game.state = "silesia_return_austrian_where" }, next() { goto_annex_silesia_return_prussian_pieces() }, } 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 game.state = "silesia_return_austrian_who" }, } function goto_annex_silesia_return_prussian_pieces() { set_active_to_power(P_PRUSSIA) 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])) { game.state = "silesia_return_prussian_who" return } } game.state = "silesia_enter_prussian_train" } 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.") 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) }, piece(p) { push_undo() game.selected = p game.state = "silesia_return_prussian_where" }, } 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 goto_annex_silesia_return_prussian_pieces() }, } states.silesia_enter_prussian_train = { inactive: "enter Prussian supply train", prompt() { prompt("Enter second supply train.") view.selected = PRUSSIAN_TRAIN_2 let possible = false for (let s of all_prussian_and_silesian_major_fortresses) { if (is_friendly_controlled_fortress(s) && !has_any_piece(s)) { gen_action_space(s) possible = true } } if (!possible) view.actions.pass = 1 }, space(s) { let p = PRUSSIAN_TRAIN_2 log(">P" + p + " to S" + s) enter_piece_at(p, s) game.state = "silesia_done" }, pass() { let p = PRUSSIAN_TRAIN_2 let s = ELIMINATED log(">P" + p + " to S" + s) game.pos[p] = s next_sequence_of_play() }, } states.silesia_done = { inactive: "annex Silesia", prompt() { prompt("Silesian annexation done.") view.actions.next = 1 }, next() { next_sequence_of_play() } } /* FRANCE REDUCES MILITARY OBJECTIVES */ function goto_france_reduces_military_objectives() { if ( !(game.flags & F_FRANCE_REDUCED) && count_french_victory_markers_in_core_austria() > 0 && france_has_no_generals_in_core_austria() ) game.state = "france_reduces_military_objectives" else next_sequence_of_play() } function count_french_victory_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 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() { push_undo() 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_br() log("France reduces military objectives.") log("Set aside " + n + " victory markers.") game.vp[SET_ASIDE_FRANCE] = n game.state = "france_reduces_military_objectives_done" }, pass() { next_sequence_of_play() }, } states.france_reduces_military_objectives_done = { inactive: "reduce military objectives", prompt() { prompt("Set aside " + game.vp[SET_ASIDE_FRANCE] + " victory markers.") view.actions.next = 1 }, next() { next_sequence_of_play() }, } /* IMPERIAL ELECTION */ const POWER_FROM_IMPERIAL_ELECTION_STAGE = [ P_AUSTRIA, P_FRANCE, P_PRAGMATIC, P_PRUSSIA ] function goto_imperial_election() { log("# Imperial Election") game.count = 0 game.state = "imperial_election" game.stage = 0 set_active_to_power(P_AUSTRIA) log(power_name[game.power]) } function next_imperial_election() { if (game.power === P_FRANCE) { set_active_to_power(P_BAVARIA) } else if (coop_major_power(P_SAXONY) === game.power) { set_active_to_power(P_SAXONY) } else { if (++game.stage === 4) { end_imperial_election() return } set_active_to_power(POWER_FROM_IMPERIAL_ELECTION_STAGE[game.stage]) } log(power_name[game.power]) } states.imperial_election = { inactive: "vote in the imperial election", prompt() { let n = 0 if (game.power === P_PRAGMATIC) ++n for (let s of all_electoral_colleges) if (is_power_controlled_fortress(game.power, s)) ++n if (n === 0) prompt("Cast no votes in the Imperial Election.") else if (n === 1) prompt("Cast your vote in the Imperial Election.") else prompt("Cast " + n + " votes in the Imperial Election.") if (n === 0) view.actions.pass = 1 else { if (game.power === P_AUSTRIA || game.power === P_PRAGMATIC) view.actions.power = [ P_AUSTRIA ] else view.actions.power = [ P_BAVARIA, P_AUSTRIA ] } }, power(pow) { if (game.power === P_PRAGMATIC) { log(">Hannover for Austria") ++game.count } for (let s of all_electoral_colleges) { if (is_power_controlled_fortress(game.power, s)) { log(">S" + s + " for " + power_name[pow]) if (pow === P_AUSTRIA) ++game.count } } next_imperial_election() }, pass() { log(">No votes") next_imperial_election() } } function end_imperial_election() { if (game.count >= 5) { log("Francis Stephen of Lorraine is Emperor.") game.flags |= F_EMPEROR_AUSTRIA } else { log("Charles Albert of Bavaria is Emperor.") game.flags |= F_EMPEROR_FRANCE } game.flags &= ~F_IMPERIAL_ELECTION goto_start_turn() } /* SUBSIDY CONTRACTS - CREATE */ function undo_subsidy_wizard() { game.state = game.proposal.save_state delete game.proposal } function may_propose_subsidy_from(pow) { if (is_two_player()) { if (pow === P_PRAGMATIC || pow === P_AUSTRIA) return is_saxony_austrian_ally() } return true } function create_subsidy() { let from = game.proposal.from let to = game.proposal.to let n = game.proposal.n map_set(game.contracts[from], to, n) } function goto_propose_subsidy(save_power) { game.proposal = { save_power, save_state: game.state, from: -1, to: -1, n: 0 } game.state = "propose_subsidy_from" } states.propose_subsidy_from = { dont_snap: true, inactive: "create subsidy contract", prompt() { prompt("Subsidy contract from which major power?") for (let pow of all_major_powers) if (may_propose_subsidy_from(pow)) gen_action_power(pow) view.actions.undo = 1 }, power(from) { game.proposal.from = from game.state = "propose_subsidy_to" }, undo() { undo_subsidy_wizard() }, } states.propose_subsidy_to = { dont_snap: true, inactive: "create subsidy contract", prompt() { let from = game.proposal.from prompt(`Subsidy contract from ${power_name[from]} to who?`) for (let to of (is_two_player() ? all_minor_powers : all_powers)) { if (from !== to && is_allied_power(from, to)) { if (from === P_FRANCE && to === P_BAVARIA && game.turn <= 3) continue // may not modify first 3-turn contract if (to === P_SAXONY && is_saxony_neutral()) continue gen_action_power(to) } } view.actions.undo = 1 }, power(to) { game.proposal.to = to game.state = "propose_subsidy_length" }, undo() { undo_subsidy_wizard() }, } states.propose_subsidy_length = { dont_snap: true, inactive: "create subsidy contract", prompt() { let from = game.proposal.from let to = game.proposal.to prompt(`Subsidy contract from ${power_name[from]} to ${power_name[to]} for how many turns?`) view.actions.value = [ 1, 2, 3, 4, 5, 6 ] view.actions.undo = 1 }, value(n) { game.proposal.n = n let ctl_from = is_controlled_power(game.power, game.proposal.from) let ctl_to = is_controlled_power(game.power, game.proposal.to) if (ctl_from && ctl_to) { create_subsidy() end_propose_subsidy() } else { set_active_to_power_keep_undo(game.proposal.from) game.state = "propose_subsidy_approve_from" } }, undo() { undo_subsidy_wizard() }, } states.propose_subsidy_approve_from = { dont_snap: true, inactive: "approve subsidy contract", prompt() { let from = game.proposal.from let to = game.proposal.to let n = game.proposal.n prompt(`Subsidy contract from ${power_name[from]} to ${power_name[to]} for ${n} turns?`) view.actions.accept = 1 view.actions.reject = 1 }, accept() { set_active_to_power_keep_undo(game.proposal.to) game.state = "propose_subsidy_approve_to" }, reject() { goto_reject_propose_subsidy() }, } states.propose_subsidy_approve_to = { dont_snap: true, inactive: "approve subsidy contract", prompt() { let from = game.proposal.from let to = game.proposal.to let n = game.proposal.n prompt(`Subsidy contract from ${power_name[from]} to ${power_name[to]} for ${n} turns?`) view.actions.accept = 1 view.actions.reject = 1 }, accept() { create_subsidy() end_propose_subsidy() }, reject() { goto_reject_propose_subsidy() }, } function goto_reject_propose_subsidy() { if (game.power === game.proposal.to) set_active_to_power_keep_undo(game.proposal.from) else set_active_to_power_keep_undo(game.proposal.save_power) game.state = "reject_propose_subsidy" } function next_reject_propose_subsidy() { if (game.power !== game.proposal.save_power) set_active_to_power_keep_undo(game.proposal.save_power) else end_propose_subsidy() } states.reject_propose_subsidy = { dont_snap: true, inactive: "approve subsidy contract", prompt() { let from = game.proposal.from let to = game.proposal.to let n = game.proposal.n prompt(`Subsidy contract from ${power_name[from]} to ${power_name[to]} for ${n} turns was NOT accepted.`) view.actions.resume = 1 view.actions.undo = 0 }, resume() { next_reject_propose_subsidy() }, } function end_propose_subsidy() { set_active_to_power_keep_undo(game.proposal.save_power) game.state = game.proposal.save_state delete game.proposal } /* SUBSIDY CONTRACTS - CANCEL */ function may_cancel_subsidy() { let result = false for (let from of all_major_powers) { map_for_each(game.contracts[from], (to, _n) => { // cannot cancel initial contract! if (from === P_FRANCE && to === P_BAVARIA && game.turn < 3) return result = true }) } return result } function cancel_subsidy() { let from = game.proposal.from let to = game.proposal.to map_delete(game.contracts[from], to) } function goto_cancel_subsidy(save_power) { game.proposal = { save_power, save_state: game.state, from: -1, to: -1 } game.state = "cancel_subsidy_from" } states.cancel_subsidy_from = { dont_snap: true, inactive: "cancel subsidy contract", prompt() { prompt("Cancel which subsidy contract?") for (let from of all_major_powers) { map_for_each(game.contracts[from], (to, _n) => { // cannot cancel initial contract! if (from === P_FRANCE && to === P_BAVARIA && game.turn < 3) return gen_action_power(from) }) } view.actions.undo = 1 }, power(from) { game.proposal.from = from game.state = "cancel_subsidy_to" }, undo() { undo_subsidy_wizard() }, } states.cancel_subsidy_to = { dont_snap: true, inactive: "cancel subsidy contract", prompt() { let from = game.proposal.from prompt(`Cancel subsidy contract from ${power_name[from]} to who?`) map_for_each(game.contracts[from], (to, _n) => { // cannot cancel initial contract! if (from === P_FRANCE && to === P_BAVARIA && game.turn < 3) return gen_action_power(to) }) view.actions.undo = 1 }, power(to) { game.proposal.to = to let ctl_from = is_controlled_power(game.power, game.proposal.from) let ctl_to = is_controlled_power(game.power, game.proposal.to) if (ctl_from && ctl_to) { cancel_subsidy() end_cancel_subsidy() } else { set_active_to_power_keep_undo(game.proposal.from) game.state = "cancel_subsidy_approve_from" } }, undo() { undo_subsidy_wizard() }, } states.cancel_subsidy_approve_from = { dont_snap: true, inactive: "cancel subsidy contract", prompt() { let from = game.proposal.from let to = game.proposal.to prompt(`Cancel subsidy contract from ${power_name[from]} to ${power_name[to]}?`) view.actions.accept = 1 // TODO: cannot refuse if minor power fortress is occupied! view.actions.reject = 1 }, accept() { set_active_to_power_keep_undo(game.proposal.to) game.state = "cancel_subsidy_approve_to" }, reject() { goto_reject_cancel_subsidy() }, } states.cancel_subsidy_approve_to = { dont_snap: true, inactive: "cancel subsidy contract", prompt() { let from = game.proposal.from let to = game.proposal.to prompt(`Cancel subsidy contract from ${power_name[from]} to ${power_name[to]}?`) view.actions.accept = 1 // TODO: cannot refuse if minor power fortress is occupied! view.actions.reject = 1 }, accept() { cancel_subsidy() end_cancel_subsidy() }, reject() { goto_reject_cancel_subsidy() }, } function goto_reject_cancel_subsidy() { if (game.power === game.proposal.to) set_active_to_power_keep_undo(game.proposal.from) else set_active_to_power_keep_undo(game.proposal.save_power) game.state = "reject_cancel_subsidy" } function next_reject_cancel_subsidy() { if (game.power !== game.proposal.save_power) set_active_to_power_keep_undo(game.proposal.save_power) else end_cancel_subsidy() } states.reject_cancel_subsidy = { dont_snap: true, inactive: "cancel subsidy contract", prompt() { let from = game.proposal.from let to = game.proposal.to prompt(`Subsidy from ${power_name[from]} to ${power_name[to]} was NOT canceled.`) view.actions.resume = 1 view.actions.undo = 0 }, resume() { next_reject_cancel_subsidy() }, } function end_cancel_subsidy() { set_active_to_power_keep_undo(game.proposal.save_power) game.state = game.proposal.save_state delete game.proposal } /* NEGOTIATION - DEALS */ function goto_propose_deal(save_power, deal) { game.proposal = { save_power, save_state: game.state, deal } let from = game.proposal.deal[DI_A_POWER] set_active_to_power_keep_undo(from) game.state = "accept_deal_from" } states.accept_deal_from = { dont_snap: true, inactive: "accept deal", prompt() { let from = game.proposal.deal[DI_A_POWER] let to = game.proposal.deal[DI_B_POWER] prompt(`Accept deal between ${power_name[from]} and ${power_name[to]}?`) view.actions.accept = 1 view.actions.reject = 1 view.actions.undo = 0 }, accept() { let to = game.proposal.deal[DI_B_POWER] set_active_to_power_keep_undo(to) game.state = "accept_deal_to" }, reject() { goto_reject_deal() }, } states.accept_deal_to = { dont_snap: true, inactive: "accept deal", prompt() { let from = game.proposal.deal[DI_A_POWER] let to = game.proposal.deal[DI_B_POWER] prompt(`Accept deal between ${power_name[from]} and ${power_name[to]}?`) view.actions.accept = 1 view.actions.reject = 1 view.actions.undo = 0 }, accept() { game.deals.push(game.proposal.deal) end_accept_deal() }, reject() { goto_reject_deal() }, } function goto_reject_deal() { let from = game.proposal.deal[DI_A_POWER] let to = game.proposal.deal[DI_B_POWER] if (game.power === to) set_active_to_power_keep_undo(from) else set_active_to_power_keep_undo(game.proposal.save_power) game.state = "reject_deal" } function next_reject_deal() { if (game.power !== game.proposal.save_power) set_active_to_power_keep_undo(game.proposal.save_power) else end_accept_deal() } states.reject_deal = { dont_snap: true, inactive: "accept deal", prompt() { let from = game.proposal.deal[DI_A_POWER] let to = game.proposal.deal[DI_B_POWER] prompt(`Deal between ${power_name[from]} and ${power_name[to]} was NOT accepted.`) view.actions.resume = 1 view.actions.undo = 0 }, resume() { next_reject_cancel_subsidy() }, } function end_accept_deal() { set_active_to_power_keep_undo(game.proposal.save_power) game.state = game.proposal.save_state delete game.proposal } /* NEGOTIATION - PING PLAYER */ function goto_ping() { game.proposal = { save_power: game.power, save_state: game.state } game.state = "ping" } states.ping = { dont_snap: true, inactive: "ping", prompt() { prompt("Ping which power to respond to chat?") for (let pow of all_powers) if (!is_controlled_power(game.power, pow)) gen_action_power(pow) view.actions.undo = 1 }, power(pow) { set_active_to_power_keep_undo(pow) game.state = "pong" }, undo() { states.pong.resume() }, } states.pong = { dont_snap: true, inactive: "respond", prompt() { prompt(power_name[game.proposal.save_power] + " has requested your response in chat.") view.actions.propose_deal = 1 view.actions.ping = 1 view.actions.resume = 1 view.actions.undo = 0 }, ping() { game.state = "ping" }, resume() { set_active_to_power_keep_undo(game.proposal.save_power) game.state = game.proposal.save_state delete game.proposal }, propose_deal(arg) { let save_power = game.proposal.save_power game.state = game.proposal.save_state delete game.proposal goto_propose_deal(save_power, arg) }, } /* VALIDATE PROMISES */ function should_validate_promise(me, you, phase) { for (let deal of game.deals) { if (deal[DI_A_POWER] === me && deal[DI_B_POWER] === you && (deal[DI_A_PHASE] & phase)) return true if (deal[DI_B_POWER] === me && deal[DI_A_POWER] === you && (deal[DI_B_PHASE] & phase)) return true } return false } function goto_validate_promise(phase, resume) { game.proposal = 0 for (let other of all_powers) if (should_validate_promise(game.power, other, phase)) game.proposal |= (1 << other) resume() } function resume_validate_promise(state, end) { if (game.proposal) { for (let other of all_powers) { if (game.proposal & (1 << other)) { game.proposal &= ~(1 << other) game.state = state set_active_to_power_keep_undo(other) return } } } delete game.proposal end() } /* VALIDATE: POLITICS */ function goto_validate_politics() { goto_validate_promise(V_POLITICS, resume_validate_politics) } function resume_validate_politics() { resume_validate_promise("validate_politics", end_validate_politics) } function end_validate_politics() { clear_checkpoint() goto_select_political_card() } states.validate_politics = { dont_snap: true, inactive: "validate promise", prompt() { let other = current_sequence_of_play().power prompt("Did " + power_name[other] + " keep their politics promise?") view.actions.yes = 1 view.actions.no = 1 }, yes() { resume_validate_politics() }, no() { let other = game.power restore_checkpoint() game.proposal = other game.state = "validate_politics_fail" }, } states.validate_politics_fail = { dont_snap: true, inactive: "select a political card", prompt() { prompt("Politics promise to " + power_name[game.proposal] + " was not kept.") view.actions.resume = 1 view.actions.undo = 0 }, resume() { game.state = "select_political_card" delete game.proposal }, } /* VALIDATE: HUSSARS */ function goto_validate_hussars() { game.proposal = 0 for (let other of all_powers) if (should_validate_promise(game.power, other, V_HUSSARS)) game.proposal |= (1 << other) resume_validate_hussars() } function resume_validate_hussars() { resume_validate_promise("validate_hussars", end_place_hussars) } states.validate_hussars = { dont_snap: true, inactive: "validate promise", prompt() { let other = current_sequence_of_play().power prompt("Did " + power_name[other] + " keep their hussar promise?") view.actions.yes = 1 view.actions.no = 1 }, yes() { resume_validate_hussars() }, no() { let other = game.power restore_checkpoint() game.proposal = other game.state = "validate_hussars_fail" }, } states.validate_hussars_fail = { dont_snap: true, inactive: "place hussars", prompt() { prompt("Promise to " + power_name[game.proposal] + " was not kept.") view.actions.resume = 1 view.actions.undo = 0 }, resume() { game.state = "place_hussars" delete game.proposal }, } /* VALIDATE: MOVEMENT */ function should_validate_movement(power) { for (let other of all_powers) if (should_validate_promise(power, other, V_MOVEMENT)) game.proposal |= (1 << other) } function validate_end_movement() { game.proposal = 0 should_validate_movement(game.power) if (game.power === P_PRAGMATIC) should_validate_movement(P_AUSTRIA) resume_validate_movement() } function resume_validate_movement() { resume_validate_promise("validate_movement", end_movement) } states.validate_movement = { dont_snap: true, inactive: "validate promise", prompt() { let other = current_sequence_of_play().power prompt("Did " + power_name[other] + " keep their movement promise?") view.actions.yes = 1 view.actions.no = 1 }, yes() { resume_validate_movement() }, no() { let other = game.power restore_checkpoint() game.proposal = other game.state = "validate_movement_fail" }, } states.validate_movement_fail = { dont_snap: true, inactive: "move", prompt() { prompt("Promise to " + power_name[game.proposal] + " was not kept. Movement phase restarted.") view.actions.resume = 1 view.actions.undo = 0 }, resume() { game.state = "movement" delete game.proposal }, } /* VALIDATE: RETREAT */ function goto_validate_retreat() { goto_validate_promise(V_RETREAT, resume_validate_retreat) } function resume_validate_retreat() { resume_validate_promise("validate_retreat", finish_combat) } states.validate_retreat = { dont_snap: true, inactive: "validate promise", prompt() { let other = current_sequence_of_play().power prompt("Did " + power_name[other] + " keep their retreat promise?") view.actions.yes = 1 view.actions.no = 1 }, yes() { resume_validate_retreat() }, no() { let other = game.power pop_undo() game.proposal = other game.state = "validate_retreat_fail" }, } states.validate_retreat_fail = { dont_snap: true, inactive: "retreat defeated general", prompt() { prompt("Retreat promise to " + power_name[game.proposal] + " was not kept.") view.actions.resume = 1 view.actions.undo = 0 }, resume() { game.state = "retreat" delete game.proposal }, } /* SETUP */ const POWER_FROM_SETUP_STAGE = [ P_FRANCE, P_PRUSSIA, P_PRAGMATIC, P_AUSTRIA, ] const INTRO_POWER_FROM_SETUP_STAGE = [ P_PRUSSIA, P_AUSTRIA, ] function set_active_setup_power() { if (is_intro()) set_active_to_power(INTRO_POWER_FROM_SETUP_STAGE[game.stage]) else set_active_to_power(POWER_FROM_SETUP_STAGE[game.stage]) } const setup_initial_tcs = [ 2, 9, 3, 5, 5, 3 ] const setup_total_troops = [ 26, 16+6, 14, 28, 5, 5 ] const setup_min_troops = [ 7, 6, 5, 1, 1, 1, 1, 4, 6, 1, 1, 1, 1, 1, 6, 2, 1, 4, 5, 5, ] const setup_max_troops = [ 8, 8, 8, 8, 8, 8, 8, 8, 6, 8, 8, 8, 8, 8, 8, 8, 8, 8, 5, 5, ] const setup_troops = [ 7, 6, 5, 0, 0, 0, 0, 4, 6, 0, 0, 0, 0, 0, 6, 2, 0, 4, 5, 5, ] const setup_piece_position = [ // - GENERALS - // F find_city("Beaune"), find_city("Schwandorf"), find_city("Ergoldsbach"), find_city("Créspy-en-V."), find_city("Sarreguemines"), // P find_city("Steinau"), find_city("Steinau"), find_city("Sprottau"), find_city("East Prussia"), // PA find_city("Delfzijl"), find_city("Delfzijl"), find_city("Dordrecht"), // A find_city("Austerlitz"), find_city("Steinamanger"), find_city("Stuhlweißenburg"), find_city("Stuhlweißenburg"), find_city("Trübau"), find_city("Malmedy"), // B find_city("Ergoldsbach"), // S find_city("Radeberg"), // - TRAINS - // F find_city("Bar-le-Duc"), find_city("Regensburg"), // P find_city("Grünberg"), find_city("Silesia Victory"), // PA find_city("Tilburg"), // A find_city("Hlinsko"), find_city("Bruck"), find_city("Geel"), // B find_city("Falkenstein"), // S find_city("Meißen"), // Hussars ELIMINATED, ELIMINATED ] function make_political_deck() { let deck41 = [ 0, 1, 2, 3, 4, 5 ] 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) shuffle_bigint(deck42) shuffle_bigint(deck43) shuffle_bigint(deck44) return [ deck44, deck43, deck42, deck41 ].flat() } function make_tactics_deck(n) { let deck = [] for (let suit = 0; suit <= 3; ++suit) for (let value = 2; value <= 10; ++value) deck.push((n << 7) | (suit << 4) | value) deck.push((n << 7) | (RESERVE << 4) | 2) deck.push((n << 7) | (RESERVE << 4) | 3) return deck } function make_tactics_discard(n) { return make_tactics_deck(n).filter(c => { if (game.draw && set_has(game.draw, c)) return false for (let pow of all_powers) { if (set_has(game.hand1[pow], c)) return false if (set_has(game.hand2[pow], c)) return false } for (let pow of all_major_powers) { if (set_has(game.face_up[pow], c)) return false if (set_has(game.face_down[pow], c)) return false } return true }) } exports.setup = function (seed, scenario, _options) { game = { seed: seed, undo: [], log: [], state: "setup", active: R_LOUIS_XV, power: P_FRANCE, turn: 0, stage: 0, score: [ [], [], [], [] ], // winter scores 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 flags: 0, // emperor vp, italy vp, silesia annexed, etc // for tracking VP gains/losses and political trump suit winner_power: -1, loser_power: -1, trump: SPADES, deck: null, hand1: [ [], [], [], [], [], [] ], hand2: [ [], [], [], [], [], [] ], deals: [], // [ power, promise, turn ] tuples contracts: [ [ P_BAVARIA, 3 ], [], [], [] ], // face-up (saved) TCs face_up: [ [], [], [], [] ], // face-down (placed) TCs face_down: [ [], [], [], [] ], pos: setup_piece_position.slice(), oos: 0, supreme: 0, troops: setup_troops.slice(), victory: [], elector: [], moved: [], retro: [], selected: -1, count: 0, } if (scenario.startsWith("Introductory")) { log("# Introductory") game.flags |= F_INTRODUCTORY game.troops[ARENBERG] = 4 game.troops[0] = 7 game.troops[1] = 8 game.troops[2] = 8 game.troops[3] = 2 game.troops[4] = 1 } else { log("# Advanced") } if (scenario.endsWith("2P")) { game.flags |= F_TWO_PLAYER } if (!is_intro()) game.pol_deck = make_political_deck() game.deck = make_tactics_deck(0) shuffle_bigint(game.deck) map_set(game.elector, TRIER, P_AUSTRIA) map_set(game.elector, MAINZ, P_AUSTRIA) map_set(game.elector, KOLN, P_FRANCE) map_set(game.elector, MANNHEIM, P_FRANCE) map_set(game.victory, LIEGNITZ, P_PRUSSIA) map_set(game.victory, GLOGAU, P_PRUSSIA) map_set(game.victory, BRESLAU, P_AUSTRIA) map_set(game.victory, BRIEG, P_AUSTRIA) map_set(game.victory, GLATZ, P_AUSTRIA) map_set(game.victory, NEISSE, P_AUSTRIA) map_set(game.victory, COSEL, P_AUSTRIA) // Deal initial cards for (let pow of all_powers) { if (is_intro() && pow === P_PRAGMATIC) continue for (let i = 0; i < setup_initial_tcs[pow]; ++i) set_add(game.hand1[pow], game.deck.pop()) } log("# 1741") goto_setup() return game } states.setup = { inactive: "setup troops", prompt() { let n_troops = setup_total_troops[game.power] - count_used_troops() if (n_troops === 0) { prompt("Setup done.") view.actions.end_setup = 1 } else { let n_stacks = 0 for (let p of all_power_generals[game.power]) { if (!set_has(game.moved, p)) { gen_action_piece(p) n_stacks ++ } } if (n_stacks > 1) prompt("Add " + n_troops + " troops to " + n_stacks + " generals.") else if (n_troops > 1) prompt("Add " + n_troops + " troops to last general.") else prompt("Add 1 troop to last general.") } }, piece(p) { push_undo() game.selected = p game.state = "setup_general" }, end_setup() { end_setup() }, } function count_unsetup_min() { let n = 0 for (let p of all_power_generals[game.power]) if (game.selected !== p && !set_has(game.moved, p)) n += setup_min_troops[p] - game.troops[p] return n } function count_unsetup_max() { let n = 0 for (let p of all_power_generals[game.power]) if (game.selected !== p && !set_has(game.moved, p)) n += setup_max_troops[p] - game.troops[p] return n } states.setup_general = { inactive: "setup troops", prompt() { prompt("Allocate troops to " + format_selected() + ".") view.selected = game.selected let who = game.selected let n_self_min = setup_min_troops[who] let n_self_max = setup_max_troops[who] let n_troops = setup_total_troops[game.power] - count_used_troops() + game.troops[who] let n_leave_min = count_unsetup_min() let n_leave_max = count_unsetup_max() // leave at least 1 for each remaining general let take_max = Math.min(n_self_max, n_troops - n_leave_min) // leave no more than 8 for each remaining general let take_min = Math.max(n_self_min, n_troops - n_leave_max) view.actions.value = [] for (let i = take_min; i <= take_max; ++i) view.actions.value.push(i) }, value(v) { set_add(game.moved, game.selected) game.troops[game.selected] = v game.selected = -1 game.state = "setup" }, } function goto_setup() { game.stage = 0 set_active_setup_power() for (let p of all_generals) if (setup_min_troops[p] === setup_max_troops[p]) set_add(game.moved, p) game.state = "setup" } function end_setup() { let end = is_intro() ? 2 : 4 if (++game.stage === end) { set_clear(game.moved) goto_start_turn() } else { set_active_setup_power() } } /* VIEW */ function mask_pol_deck() { if (game.pol_deck.length > 0) return political_cards[game.pol_deck[game.pol_deck.length - 1]].year - 1740 return 0 } function mask_troops(player) { let view_troops = [] for (let pow of all_powers) { if (player_from_power(pow) === player) { for (let p of all_power_generals[pow]) view_troops.push(game.troops[p]) } else { for (let p of all_power_generals[pow]) { let s = game.pos[p] if (game.attacker === s || game.defender === s) view_troops.push(game.troops[p]) else view_troops.push(0) } } } return view_troops } function mask_hand1(player) { let view_hand = [] for (let pow of all_powers) { if (player_from_power(pow) === player) view_hand[pow] = game.hand1[pow] else view_hand[pow] = game.hand1[pow].concat(game.hand2[pow]).map(c => c & ~127) } return view_hand } function mask_hand2(player) { let view_hand = [] for (let pow of all_powers) { if (player_from_power(pow) === player) view_hand[pow] = game.hand2[pow] else view_hand[pow] = [] } return view_hand } function mask_face_down() { return game.face_down.map(list => list.map(c => c & ~127)) } function is_trump_card(c) { return (game.trump >= 0) && (is_reserve(c) || to_suit(c) === game.trump) } function total_troops_list() { let list = [] for (let pow of all_powers) { let n = 0 for (let p of all_power_generals[pow]) n += game.troops[p] list[pow] = n } return list } exports.view = function (state, player) { game = state view = { prompt: null, actions: null, log: game.log, turn: game.turn, vp: game.vp, saxony: game.saxony, russia: game.russia, italy: game.italy, flags: game.flags, victory: game.victory, elector: game.elector, pos: game.pos, oos: game.oos, supreme: game.supreme, troops: mask_troops(player), hand1: mask_hand1(player), hand2: mask_hand2(player), pt: total_troops_list(), discard: total_discard_list(), deals: game.deals, contracts: game.contracts, face_up: game.face_up, face_down: mask_face_down(), power: game.power, retro: game.retro, } if (game.attacker !== undefined && game.defender !== undefined) { view.attacker = game.attacker view.defender = game.defender } if (!is_intro()) view.pol_deck = mask_pol_deck() if (game.proposal && game.proposal.deal) view.proposed_deal = game.proposal.deal if (game.political) view.political = game.political if (game.state === "game_over") { view.prompt = game.victory_reason view.troops = game.troops view.hand1 = game.hand1 view.hand2 = game.hand2 } else if (game.active !== player) { let inactive = states[game.state].inactive || game.state if (typeof inactive === "function") view.prompt = inactive() else view.prompt = `Waiting for ${power_name[game.power]} to ${inactive}.` } else { view.actions = {} if (states[game.state]) states[game.state].prompt() else view.prompt = "Unknown state: " + game.state if (view.actions.undo === undefined) { if (game.undo && game.undo.length > 0) view.actions.undo = 1 else view.actions.undo = 0 } // negotiation actions for active player if (game.proposal === undefined && !is_intro() && view.turn > 0) { if (may_cancel_subsidy()) view.actions.cancel_subsidy = 1 if (!is_intro()) view.actions.propose_subsidy = 1 if (!is_two_player() && !is_intro()) view.actions.propose_deal = 1 view.actions.ping = 1 } } return view } exports.action = function (state, _player, action, arg) { game = state let S = states[game.state] if (S && action in S) { S[action](arg) } else { if (action === "undo" && game.undo && game.undo.length > 0) { pop_undo() } else if (action === "propose_subsidy") { goto_propose_subsidy(game.power) } else if (action === "cancel_subsidy") { goto_cancel_subsidy(game.power) } else if (action === "propose_deal") { goto_propose_deal(game.power, arg) } else if (action === "ping") { goto_ping() } else throw new Error("Invalid action: " + action) } return game } exports.dont_snap = function (state) { let dont_snap = states[state.state].dont_snap if (typeof dont_snap === "function") { game = state return dont_snap() } return !!dont_snap } /* COMMON FRAMEWORK */ function goto_game_over(result, victory_reason) { log("# The End") game.active = "None" game.state = "game_over" game.result = result game.victory_reason = victory_reason log(game.victory_reason) return true } function prompt(str) { view.prompt = power_name[game.power] + ": " + str } function gen_action(action, argument) { if (view.actions[action] === undefined) view.actions[action] = [ argument ] else set_add(view.actions[action], argument) } function gen_action_power(p) { gen_action("power", p) } function gen_action_piece(p) { gen_action("piece", p) } function gen_action_space(s) { gen_action("space", s) } function gen_action_supreme_commander(s) { let p = get_supreme_commander(s) if (p >= 0) gen_action_piece(p) } function gen_action_space_or_piece(s) { let p = get_top_piece(s) if (p >= 0) gen_action_piece(p) else gen_action_space(s) } function gen_action_card(c) { gen_action("card", c) } function gen_action_political(c) { gen_action("political", c) } function log(msg) { game.log.push(msg) } function log_br() { if (game.log.length > 0 && game.log[game.log.length - 1] !== "") game.log.push("") } /* COMMON LIBRARY */ function save_checkpoint() { clear_undo() push_undo() game.restart = game.undo.pop() } function restore_checkpoint() { game.undo = [ game.restart ] delete game.restart pop_undo() push_undo() game.restart = game.undo.pop() } function clear_checkpoint() { delete game.restart } function clear_undo() { game.undo = [] } function push_undo() { if (game.undo) { let copy = {} for (let k in game) { let v = game[k] if (k === "subsidies") continue if (k === "deals") continue if (k === "restart") continue if (k === "undo") continue else if (k === "log") v = v.length else if (typeof v === "object" && v !== null) v = object_copy(v) copy[k] = v } game.undo.push(copy) } } function pop_undo() { if (game.undo) { let save_log = game.log let save_undo = game.undo let save_subsidies = game.subsidies let save_deals = game.deals let save_restart = game.restart game = save_undo.pop() save_log.length = game.log game.log = save_log game.undo = save_undo game.subsidies = save_subsidies game.deals = save_deals game.restart = save_restart } } function random_bigint(range) { // Largest MLCG that will fit its state in a double. // Uses BigInt for arithmetic, so is an order of magnitude slower. // https://www.ams.org/journals/mcom/1999-68-225/S0025-5718-99-00996-5/S0025-5718-99-00996-5.pdf // m = 2**53 - 111 return (game.seed = Number(BigInt(game.seed) * 5667072534355537n % 9007199254740881n)) % range } function shuffle_bigint(list) { // Fisher-Yates shuffle for (let i = list.length - 1; i > 0; --i) { let j = random_bigint(i + 1) let tmp = list[j] list[j] = list[i] list[i] = tmp } } // Fast deep copy for objects without cycles function object_copy(original) { if (Array.isArray(original)) { let n = original.length let copy = new Array(n) for (let i = 0; i < n; ++i) { let v = original[i] if (typeof v === "object" && v !== null) copy[i] = object_copy(v) else copy[i] = v } return copy } else { let copy = {} for (let i in original) { let v = original[i] if (typeof v === "object" && v !== null) copy[i] = object_copy(v) else copy[i] = v } return copy } } // Array remove and insert (faster than splice) function array_remove(array, index) { let n = array.length for (let i = index + 1; i < n; ++i) array[i - 1] = array[i] array.length = n - 1 } function array_remove_item(array, item) { let n = array.length for (let i = 0; i < n; ++i) if (array[i] === item) return array_remove(array, i) } function array_insert(array, index, item) { for (let i = array.length; i > index; --i) array[i] = array[i - 1] array[index] = item } function array_remove_pair(array, index) { let n = array.length for (let i = index + 2; i < n; ++i) array[i - 2] = array[i] array.length = n - 2 } function array_insert_pair(array, index, key, value) { for (let i = array.length; i > index; i -= 2) { array[i] = array[i-2] array[i+1] = array[i-1] } array[index] = key array[index+1] = value } // Set as plain sorted array function set_clear(set) { set.length = 0 } function set_has(set, item) { let a = 0 let b = set.length - 1 while (a <= b) { let m = (a + b) >> 1 let x = set[m] if (item < x) b = m - 1 else if (item > x) a = m + 1 else return true } return false } function set_add(set, item) { let a = 0 let b = set.length - 1 while (a <= b) { let m = (a + b) >> 1 let x = set[m] if (item < x) b = m - 1 else if (item > x) a = m + 1 else return } array_insert(set, a, item) } function set_delete(set, item) { let a = 0 let b = set.length - 1 while (a <= b) { let m = (a + b) >> 1 let x = set[m] if (item < x) b = m - 1 else if (item > x) a = m + 1 else { array_remove(set, m) return } } } function set_add_all(set, other) { for (let item of other) set_add(set, item) } function set_union(one, two) { let set = [] for (let item of one) set_add(set, item) for (let item of two) set_add(set, item) return set } function set_intersect(one, two) { let set = [] for (let item of one) if (set_has(two, item)) set_add(set, item) return set } // Map as plain sorted array of key/value pairs function map_clear(set) { set.length = 0 } function map_has(map, key) { let a = 0 let b = (map.length >> 1) - 1 while (a <= b) { let m = (a + b) >> 1 let x = map[m<<1] if (key < x) b = m - 1 else if (key > x) a = m + 1 else return true } return false } function map_get(map, key, missing) { let a = 0 let b = (map.length >> 1) - 1 while (a <= b) { let m = (a + b) >> 1 let x = map[m<<1] if (key < x) b = m - 1 else if (key > x) a = m + 1 else return map[(m<<1)+1] } return missing } function map_set(map, key, value) { let a = 0 let b = (map.length >> 1) - 1 while (a <= b) { let m = (a + b) >> 1 let x = map[m<<1] if (key < x) b = m - 1 else if (key > x) a = m + 1 else { map[(m<<1)+1] = value return } } array_insert_pair(map, a<<1, key, value) } function map_delete(map, key) { let a = 0 let b = (map.length >> 1) - 1 while (a <= b) { let m = (a + b) >> 1 let x = map[m<<1] if (key < x) b = m - 1 else if (key > x) a = m + 1 else { array_remove_pair(map, m<<1) return } } } function map_for_each(map, f) { for (let i = 0; i < map.length; i += 2) f(map[i], map[i+1]) } function map_filter(map, f) { let out = [] for (let i = 0; i < map.length; i += 2) if (f(map[i], map[i+1])) out.push(map[i], map[i+1]) return out } function map_group_by(items, callback) { let groups = [] if (typeof callback === "function") { for (let item of items) { let key = callback(item) let arr = map_get(groups, key) if (arr) arr.push(item) else map_set(groups, key, [ item ]) } } else { for (let item of items) { let key = item[callback] let arr = map_get(groups, key) if (arr) arr.push(item) else map_set(groups, key, [ item ]) } } return groups }