"use strict" // TODO: no card menu const COMMUNE = "Commune"; const VERSAILLES = "Versailles"; var game, view, states = {} exports.scenarios = [ "Standard", "Censorship" ] exports.roles = [ COMMUNE, VERSAILLES ] const first_commune_cube = 0 const last_commune_cube = 17 const first_versailles_cube = 18 const last_versailles_cube = 35 const first_commune_disc = 36 const last_commune_disc = 37 const first_versailles_disc = 38 const last_versailles_disc = 39 const card_names = [ "Initiative", "Jules Ducatel", "The Murder of Vincenzini", "Brassardiers", "Jules Ferry", "Le Figaro", "Général Louis Valentin", "Général Espivent", "Les Amis de l'Ordre", "Socialist Newspaper Ban", "Fortification of Mont-Valérien", "Adolphe Thiers", "Otto von Bismarck", "Général Ernest de Cissey", "Colonel de Lochner", "Jules Favre", "Hostage Decree", "Maréchal Macmahon", "Paule Minck", "Walery Wroblewski", "Banque de France", "Le Réveil", "Execution of Generals", "Les Cantinières", "Eugène Protot", "Paul Cluseret", "Gaston Crémieux", "Luise Michel", "Jaroslav Dombrowski", "Raoul Rigault", "Karl Marx", "Blanquists", "Général Lullier", "Jules Vallès", "Charles Delescluze", "Conciliation", "Georges Clemenceau", "Archbishop Georges Darboy", "Victor Hugo", "Léon Gambetta", "Elihu Washburne", "Freemason Parade", "Paris Cannons", "Aux Barricades!", "Commune's Stronghold", "Fighting in Issy Village", "Battle of Mont-Valérien", "Raid on Château de Vincennes", "Revolution in the Press", "Pius IX", "Socialist International", "Royalists Dissension", "Rise of Republicanism", "Legitimacy", ] const card_ops = [ 0, // Commune 1, 1, 1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 2, 4, // Versailles 1, 1, 1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 2, 4, // Neutral 3, 3, 3, 3, 3, 3, 3, // Objective 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, ] const space_names = [ "National Assembly", "Royalists", "Republicans", "Press", "Catholic Church", "Social Movements", "Mont-Valérien", "Fort D'Issy", "Château de Vincennes", "Butte Montmartre", "Butte-Aux-Cailles", "Père Lachaise", "Prussian Occupied Territory", "Versailles HQ", "Red Cube Pool 1", "Red Cube Pool 2", "Red Cube Pool 3", "Red Crisis Track Start", "Red Crisis Track Escalation", "Red Crisis Track Tension", "Red Crisis Track Final Crisis", "Red Bonus Cubes 1", "Red Bonus Cubes 2", "Red Bonus Cubes 3", "Blue Cube Pool", "Blue Crisis Track Start", "Blue Crisis Track Escalation", "Blue Crisis Track Tension", "Blue Crisis Track Final Crisis", "Blue Bonus Cubes 1", "Blue Bonus Cubes 2", "Blue Bonus Cubes 3", "Prussian Collaboration 1", "Prussian Collaboration 2", "Prussian Collaboration 3", ] const RED_CUBE_POOL = [14, 15, 16] const RED_CRISIS_TRACK = [17, 18, 19, 20] const RED_BONUS_CUBES = [21, 22, 23] const BLUE_CUBE_POOL = 24 const BLUE_CRISIS_TRACK = [25, 26, 27, 28] const BLUE_BONUS_CUBES = [29, 30, 31] const PRUSSIAN_COLLABORATION = [32, 33, 34] const NATIONAL_ASSEMBLY = 0 const ROYALISTS = 1 const REPUBLICANS = 2 const PRESS = 3 const CATHOLIC_CHURCH = 4 const SOCIAL_MOVEMENTS = 5 const MONT_VALERIEN = 6 const FORT_D_ISSY = 7 const CHATEAU_DE_VINCENNES = 8 const BUTTE_MONTMARTRE = 9 const BUTTE_AUX_CAILLES = 10 const PERE_LACHAISE = 11 const PRUSSIAN_OCCUPIED_TERRITORY = 12 const VERSAILLES_HQ = 13 const first_space = NATIONAL_ASSEMBLY const last_space = PERE_LACHAISE const space_count = last_space + 1 const POLITICAL = [ NATIONAL_ASSEMBLY, ROYALISTS, REPUBLICANS, PRESS, CATHOLIC_CHURCH, SOCIAL_MOVEMENTS, ] const MILITARY = [ MONT_VALERIEN, FORT_D_ISSY, CHATEAU_DE_VINCENNES, BUTTE_MONTMARTRE, BUTTE_AUX_CAILLES, PERE_LACHAISE, ] function is_political_space(s) { return ( s === NATIONAL_ASSEMBLY || s === ROYALISTS || s === REPUBLICANS || s === PRESS || s === CATHOLIC_CHURCH || s === SOCIAL_MOVEMENTS ) } function is_military_space(s) { return ( s === MONT_VALERIEN || s === FORT_D_ISSY || s === CHATEAU_DE_VINCENNES || s === BUTTE_MONTMARTRE || s === BUTTE_AUX_CAILLES || s === PERE_LACHAISE ) } const INSTITUTIONAL = [ NATIONAL_ASSEMBLY, ROYALISTS, REPUBLICANS ] const PUBLIC_OPINION = [ PRESS, CATHOLIC_CHURCH, SOCIAL_MOVEMENTS ] const FORTS = [ MONT_VALERIEN, FORT_D_ISSY, CHATEAU_DE_VINCENNES ] const PARIS = [ BUTTE_MONTMARTRE, BUTTE_AUX_CAILLES, PERE_LACHAISE ] const ADJACENT_TO = [ [ ROYALISTS, REPUBLICANS ], [ PRESS, CATHOLIC_CHURCH ], [ PRESS, SOCIAL_MOVEMENTS ], [ ROYALISTS, REPUBLICANS, CATHOLIC_CHURCH, SOCIAL_MOVEMENTS ], [ ROYALISTS, PRESS ], [ REPUBLICANS, PRESS ], [ BUTTE_MONTMARTRE, VERSAILLES_HQ ], [ CHATEAU_DE_VINCENNES, BUTTE_AUX_CAILLES, VERSAILLES_HQ ], [ FORT_D_ISSY, PERE_LACHAISE, PRUSSIAN_OCCUPIED_TERRITORY ], [ MONT_VALERIEN, BUTTE_AUX_CAILLES, PERE_LACHAISE ], [ FORT_D_ISSY, BUTTE_MONTMARTRE, PERE_LACHAISE ], [ CHATEAU_DE_VINCENNES, BUTTE_MONTMARTRE, BUTTE_AUX_CAILLES, PRUSSIAN_OCCUPIED_TERRITORY ], [ CHATEAU_DE_VINCENNES, PERE_LACHAISE ], [ MONT_VALERIEN, FORT_D_ISSY ], ] const ADJACENT_FROM = [ [ ], [ NATIONAL_ASSEMBLY, PRESS, CATHOLIC_CHURCH ], [ NATIONAL_ASSEMBLY, PRESS, SOCIAL_MOVEMENTS ], [ ROYALISTS, REPUBLICANS, CATHOLIC_CHURCH, SOCIAL_MOVEMENTS ], [ ROYALISTS, PRESS ], [ REPUBLICANS, PRESS ], [ BUTTE_MONTMARTRE, VERSAILLES_HQ ], [ CHATEAU_DE_VINCENNES, BUTTE_AUX_CAILLES, VERSAILLES_HQ ], [ FORT_D_ISSY, PERE_LACHAISE, PRUSSIAN_OCCUPIED_TERRITORY ], [ MONT_VALERIEN, BUTTE_AUX_CAILLES, PERE_LACHAISE ], [ FORT_D_ISSY, BUTTE_MONTMARTRE, PERE_LACHAISE ], [ CHATEAU_DE_VINCENNES, BUTTE_MONTMARTRE, BUTTE_AUX_CAILLES, PRUSSIAN_OCCUPIED_TERRITORY ], [ CHATEAU_DE_VINCENNES, PERE_LACHAISE ], [ MONT_VALERIEN, FORT_D_ISSY ], ] // === GAME STATE === function discard_card(c) { array_remove_item(player_hand(), c) game.discard = c } function recycle_card(c) { array_remove_item(player_hand(), c) game.strategy_deck.unshift(c) } function is_objective_card(c) { return c >= 42 && c <= 53 } function is_commune_card(c) { return c >= 18 && c <= 34 } function is_versailles_card(c) { return c >= 1 && c <= 17 } function is_neutral_card(c) { return c >= 35 && c <= 41 } function enemy_player() { if (game.active === COMMUNE) return VERSAILLES return COMMUNE } function player_hand() { if (game.active === COMMUNE) return game.red_hand return game.blue_hand } function player_set_aside(current) { if (game.active === COMMUNE) return game.red_set_aside return game.blue_set_aside } function is_space(s) { return s >= first_space && s <= last_space } function can_advance_momentum() { if (game.active === COMMUNE) return game.red_momentum < 3 return game.blue_momentum < 3 } function can_play_event(c) { if (game.active === COMMUNE) return is_commune_card(c) || is_neutral_card(c) return is_versailles_card(c) || is_neutral_card(c) } var c_count = new Array(space_count).fill(0) var v_count = new Array(space_count).fill(0) function update_presence_and_control() { c_count.fill(0) v_count.fill(0) for (let p = first_commune_cube; p <= last_commune_cube; ++p) { let s = game.pieces[p] if (is_space(s)) c_count[s] += 1 } for (let p = first_versailles_cube; p <= last_versailles_cube; ++p) { let s = game.pieces[p] if (is_space(s)) v_count[s] += 1 } for (let p = first_commune_disc; p <= last_commune_disc; ++p) { let s = game.pieces[p] if (is_space(s)) c_count[s] += 1 } for (let p = first_versailles_disc; p <= last_versailles_disc; ++p) { let s = game.pieces[p] if (is_space(s)) v_count[s] += 1 } game.presence = 0 game.control = 0 // Permanent Presence game.presence |= 1 << PERE_LACHAISE game.presence |= 1 << SOCIAL_MOVEMENTS game.presence |= 1 << (ROYALISTS + space_count) for (let s = first_space; s <= last_space; ++s) { let c_bit = 1 << s let v_bit = 1 << (s + space_count) if (c_count[s] > 0) game.presence |= c_bit if (v_count[s] > 0) game.presence |= v_bit if (c_count[s] > v_count[s]) game.control |= c_bit if (v_count[s] > c_count[s]) game.control |= v_bit } } function is_present(s) { if (game.active === COMMUNE) return game.presence & (1 << (s)) return game.presence & (1 << (s + space_count)) } function is_commune_control(s) { if (s === VERSAILLES_HQ) return false if (s === PRUSSIAN_OCCUPIED_TERRITORY) return false return game.control & (1 << (s)) } function is_versailles_control(s) { if (s === VERSAILLES_HQ) return true if (s === PRUSSIAN_OCCUPIED_TERRITORY) return game.blue_momentum === 3 return game.control & (1 << (s + space_count)) } function is_control(s) { if (game.active === COMMUNE) return is_commune_control(s) return is_versailles_control(s) } function is_adjacent_to_control(here) { for (let s of ADJACENT_TO[here]) if (is_control(s)) return true return false } function is_control_dimension(dim) { for (let s of dim) if (!is_control(s)) return false return true } function count_commune_cubes(s) { let n = 0 for (let p = first_commune_cube; p <= last_commune_cube; ++p) if (game.pieces[p] === s) ++n return n } function count_versailles_cubes(s) { let n = 0 for (let p = first_versailles_cube; p <= last_versailles_cube; ++p) if (game.pieces[p] === s) ++n return n } function count_commune_discs(s) { let n = 0 for (let p = first_commune_disc; p <= last_commune_disc; ++p) if (game.pieces[p] === s) ++n return n } function count_versailles_discs(s) { let n = 0 for (let p = first_versailles_disc; p <= last_versailles_disc; ++p) if (game.pieces[p] === s) ++n return n } function count_commune_pieces(s) { return count_commune_cubes(s) + count_commune_discs(s) } function count_versailles_pieces(s) { return count_versailles_cubes(s) + count_versailles_discs(s) } function count_friendly_cubes(s) { if (game.active === COMMUNE) return count_commune_cubes(s) return count_versailles_cubes(s) } function count_enemy_pieces(s) { if (game.active === COMMUNE) return count_versailles_pieces(s) return count_commune_pieces(s) } function find_commune_cube(s) { for (let p = first_commune_cube; p <= last_commune_cube; ++p) if (game.pieces[p] === s) return p return -1 } function find_versailles_cube(s) { for (let p = first_versailles_cube; p <= last_versailles_cube; ++p) if (game.pieces[p] === s) return p return -1 } function find_commune_disc(s) { for (let p = first_commune_disc; p <= last_commune_disc; ++p) if (game.pieces[p] === s) return p return -1 } function find_versailles_disc(s) { for (let p = first_versailles_disc; p <= last_versailles_disc; ++p) if (game.pieces[p] === s) return p return -1 } function find_enemy_cube(s) { if (game.active === COMMUNE) return find_versailles_cube(s) return find_commune_cube(s) } function find_enemy_disc(s) { if (game.active === COMMUNE) return find_versailles_disc(s) return find_commune_disc(s) } function find_friendly_cube(s) { if (game.active === COMMUNE) return find_commune_cube(s) return find_versailles_cube(s) } function find_friendly_disc(s) { if (game.active === COMMUNE) return find_commune_disc(s) return find_versailles_disc(s) } function has_enemy_cube(s) { return find_enemy_cube(s) >= 0 } function has_enemy_disc(s) { return find_enemy_disc(s) >= 0 } function has_enemy_piece(s) { return has_enemy_cube(s) || has_enemy_disc(s) } function is_commune_cube(p) { return p >= first_commune_cube && p <= last_commune_cube } function is_versailles_cube(p) { return p >= first_versailles_cube && p <= last_versailles_cube } function is_disc(p) { return p >= 36 } function remove_piece(p) { if (is_commune_cube(p)) { if (game.red_momentum >= 1 && count_commune_cubes(RED_CUBE_POOL[0]) < 2) game.pieces[p] = RED_CUBE_POOL[0] else if (game.red_momentum >= 2 && count_commune_cubes(RED_CUBE_POOL[1]) < 1) game.pieces[p] = RED_CUBE_POOL[1] else if (game.red_momentum >= 3 && count_commune_cubes(RED_CUBE_POOL[2]) < 1) game.pieces[p] = RED_CUBE_POOL[2] else game.pieces[p] = -1 } else if (is_versailles_cube(p)) { game.pieces[p] = BLUE_CUBE_POOL } else { game.pieces[p] = -1 } } function remove_piece_from_play(p) { game.pieces[p] = -1 } function place_piece(p, s) { game.pieces[p] = s } function find_available_cube() { let p = -1 if (game.active === COMMUNE) { for (let i = 0; i < 3; ++i) { p = find_commune_cube(RED_CUBE_POOL[i]) if (p >= 0) return p } for (let i = 0; i < 4; ++i) { p = find_commune_cube(RED_CRISIS_TRACK[i]) if (p >= 0) return p } } else { p = find_versailles_cube(BLUE_CUBE_POOL) if (p >= 0) return p for (let i = 0; i < 4; ++i) { p = find_versailles_cube(BLUE_CRISIS_TRACK[i]) if (p >= 0) return p } } return -1 } function for_each_enemy_cube(s, f) { if (game.active === COMMUNE) for_each_versailles_cube(s, f) else for_each_commune_cube(s, f) } function for_each_friendly_cube(s, f) { if (game.active === COMMUNE) for_each_commune_cube(s, f) else for_each_versailles_cube(s, f) } function for_each_enemy_disc(s, f) { if (game.active === COMMUNE) for_each_versailles_disc(s, f) else for_each_commune_disc(s, f) } function for_each_friendly_disc(s, f) { if (game.active === COMMUNE) for_each_commune_disc(s, f) else for_each_versailles_disc(s, f) } function for_each_commune_cube(s, f) { for (let p = first_commune_cube; p <= last_commune_cube; ++p) if (game.pieces[p] === s) f(p) } function for_each_versailles_cube(s, f) { for (let p = first_versailles_cube; p <= last_versailles_cube; ++p) if (game.pieces[p] === s) f(p) } function for_each_commune_disc(s, f) { for (let p = first_commune_disc; p <= last_commune_disc; ++p) if (game.pieces[p] === s) f(p) } function for_each_versailles_disc(s, f) { for (let p = first_versailles_disc; p <= last_versailles_disc; ++p) if (game.pieces[p] === s) f(p) } function commune_political_vp() { return game.political_vp } function versailles_political_vp() { return -game.political_vp } // === CHOOSE OBJECTIVE CARD === states.choose_objective_card = { inactive: "choose an objective card", prompt(current) { view.prompt = "Choose an Objective card to keep." if (current === COMMUNE) for (let c of game.red_objective) gen_action_card(c) else for (let c of game.blue_objective) gen_action_card(c) }, card(c, current) { if (current === COMMUNE) game.red_objective = [ c ] else game.blue_objective = [ c ] if (game.red_objective.length === 1 && game.blue_objective.length === 1) end_choose_objective_card() else if (game.red_objective.length === 1) game.active = VERSAILLES else if (game.blue_objective.length === 1) game.active = COMMUNE else game.active = "Both" }, } function end_choose_objective_card() { goto_initiative_phase() } // === INITIATIVE PHASE === function goto_initiative_phase() { let c_level = commune_political_vp() - game.red_momentum let v_level = versailles_political_vp() - game.blue_momentum if (c_level >= v_level) game.active = game.initiative = COMMUNE else game.active = game.initiative = VERSAILLES game.state = "initiative_phase" } states.initiative_phase = { inactive: "decide initiative", prompt() { view.prompt = "Decide who will have the initiative." view.actions.commune = 1 view.actions.versailles = 1 }, commune() { log("Initiative: Commune") game.initiative = COMMUNE end_initiative_phase() }, versailles() { log("Initiative: Versailles") game.initiative = VERSAILLES end_initiative_phase() }, } function end_initiative_phase() { game.active = game.initiative if (game.scenario === "Censorship") goto_censorship_phase() else goto_strategy_phase() } // === CENSORSHIP PHASE === function goto_censorship_phase() { game.state = "censorship_phase" } states.censorship_phase = { inactive: "censorship phase", prompt() { view.prompt = "Discard a card from your hand." for (let c of player_hand()) gen_action("card", c) }, card(c) { log(`Discarded #${c}.`) discard_card(c) if (game.active === game.initiative) game.active = enemy_player() else goto_strategy_phase() }, } // === PLAYING STRATEGY CARDS === function goto_strategy_phase() { clear_undo() log_h2(game.active) game.state = "strategy_phase" } function resume_strategy_phase() { if (game.red_hand.length === 1 && game.blue_hand.length === 1) { goto_set_aside_cards() } else { game.active = enemy_player() goto_strategy_phase() } } function has_final_crisis_card() { if (game.active === COMMUNE) return game.red_final return game.blue_final } states.strategy_phase = { inactive: "play a card", prompt() { view.prompt = "Play a card." for (let c of player_hand()) gen_action_card(c) }, card(c) { push_undo() log(`Played #${c}.`) game.what = c game.state = "play_card" }, } states.play_card = { prompt() { let c = game.what view.selected_card = game.what view.actions.political = 1 view.actions.military = 1 if (can_play_event(c)) view.actions.event = 1 else view.actions.event = 0 if (can_advance_momentum()) { view.actions.momentum = 1 if (game.active === COMMUNE) view.actions.red_momentum = 1 else view.actions.blue_momentum = 1 } if (game.discard > 0 && can_play_event(game.discard)) gen_action_card(game.discard) let final = has_final_crisis_card() if (final > 0) gen_action_card(final) }, event() { log("Event.") discard_card(game.what) goto_play_event(game.what) }, political() { log("Ops.") discard_card(game.what) goto_operations(card_ops[game.what], POLITICAL) }, military() { log("Ops.") discard_card(game.what) goto_operations(card_ops[game.what], MILITARY) }, momentum() { log(`Momentum.`) if (game.scenario === "Censorship") recycle_card(game.what) else discard_card(game.what) if (game.active === COMMUNE) advance_revolutionary_momentum(1) else advance_prussian_collaboration(1) }, red_momentum() { this.momentum() }, blue_momentum() { this.momentum() }, card(c) { log(`Discarded for #${c}.`) if (c === game.discard) { discard_card(game.what) game.what = c goto_play_event(c) } else { discard_card(game.what) game.what = c game.state = "play_final" } }, } states.play_final = { prompt() { view.prompt = card_names[game.what] + ": Use up to 4 Operations Points." view.selected_card = game.what view.actions.political = 1 view.actions.military = 1 }, political() { discard_final() goto_operations(4, POLITICAL) }, military() { discard_final() goto_operations(4, MILITARY) }, } function discard_final() { if (game.active === COMMUNE) game.red_final = 0 else game.blue_final = 0 } // === PLAYER MOMENTUM TRACKS === function advance_revolutionary_momentum(x) { game.red_momentum += x for (let i = game.red_momentum; i < 3; ++i) for_each_commune_cube(RED_CUBE_POOL[i], p => game.pieces[p] = -1) game.active = VERSAILLES if (x > 0 && game.red_momentum >= 2) game.state = "revolutionary_momentum_trigger" else end_momentum_trigger() } function advance_prussian_collaboration(x) { game.blue_momentum += x for (let i = 0; i < game.blue_momentum; ++i) for_each_versailles_cube(PRUSSIAN_COLLABORATION[i], p => game.pieces[p] = BLUE_CUBE_POOL) game.active = COMMUNE if (x > 0 && game.blue_momentum >= 2) game.state = "prussian_collaboration_trigger" else end_momentum_trigger() } states.revolutionary_momentum_trigger = { prompt() { view.prompt = "Revolutionary Momentum: Place a cube in an Institutional space." for (let s of INSTITUTIONAL) gen_action_space(s) view.actions.skip = 1 }, space(s) { place_piece(find_available_cube(), s) end_momentum_trigger() }, skip() { end_momentum_trigger() }, } states.prussian_collaboration_trigger = { prompt() { view.prompt = "Prussian Collaboration: Place a cube in a Public Opinion space." for (let s of PUBLIC_OPINION) gen_action_space(s) view.actions.skip = 1 }, space(s) { place_piece(find_available_cube(), s) end_momentum_trigger() }, skip() { end_momentum_trigger() }, } function end_momentum_trigger() { game.active = enemy_player() resume_strategy_phase() } // === OPERATIONS === function goto_operations(count, spaces) { game.count = count game.spaces = spaces goto_operations_remove() } function goto_final_crisis_discard(c, spaces) { log("Played #" + c + ".") game.state = "discard_final_crisis" game.count = 4 game.spaces = spaces } states.discard_final_crisis = { inactive: "discard a card to play final crisis", prompt() { view.prompt = "Discard a card to play Final Crisis card for ops." let hand = player_hand() for (let c of hand) gen_action("card", c) }, card(c) { push_undo() log(`Discarded #${c} to play Final Crisis card for ops.`) array_remove_item(player_hand(), c) game.discard = c goto_operations_remove() }, } // OPERATIONS: REMOVE function goto_operations_remove() { update_presence_and_control() if (can_operations_remove()) game.state = "operations_remove" else goto_operations_place() } function can_operations_remove() { for (let s of game.spaces) if (can_operations_remove_space(s)) return true return false } function can_operations_remove_space(s) { if (is_present(s) || is_adjacent_to_control(s)) { let c = has_enemy_cube(s) let d = has_enemy_disc(s) if (c || d) { if (is_political_space(s)) return true if (is_military_space(s)) if (!d || game.count >= 2) return true } } return false } function military_strength(s) { let str = 0 for (let next of ADJACENT_FROM[s]) if (is_control(next)) str += 1 if (is_present(s)) str += 1 if (is_control(s)) str += 1 return str } states.operations_remove = { prompt() { view.prompt = "Operations: Remove opponent's pieces." for (let s of game.spaces) { if (can_operations_remove_space(s)) { if (has_enemy_cube(s)) for_each_enemy_cube(s, gen_action_piece) else for_each_enemy_disc(s, gen_action_piece) } } view.actions.end_remove = 1 }, piece(p) { push_undo() let s = game.pieces[p] if (has_enemy_disc(s)) game.count -= 2 else game.count -= 1 if (is_military_space(s)) { let str = military_strength(s) if (str >= 3) { log("Military strength " + str + ".") remove_piece(p) } else if (game.count >= 1) { log("Military strength " + str + ".") game.who = p game.state = "operations_remove_spend" } else { log("Military strength " + str + ".") game.who = p game.state = "operations_remove_draw" } } else { remove_piece(p) } resume_operations_remove() }, end_remove() { push_undo() goto_operations_place() }, } states.operations_remove_spend = { prompt() { view.prompt = "Operations: Spend extra Operations Point before drawing?" view.actions.spend = 1 view.actions.draw = 1 }, spend() { log("Spent 1 ops.") game.count -= 1 attempt_remove_piece(1) }, draw() { attempt_remove_piece(0) }, } states.operations_remove_draw = { prompt() { view.prompt = "Operations: Draw card for Military removal." view.actions.draw = 1 }, draw() { attempt_remove_piece(0) }, } function attempt_remove_piece(extra) { clear_undo() let p = game.who let s = game.pieces[p] let c = game.strategy_deck.pop() let str = military_strength(s) + extra let ops = card_ops[c] log("Military strength " + str + ".") log("Removed card #" + c + " for " + ops + " strength.") if (str >= ops) remove_piece(p) game.who = -1 resume_operations_remove() } function resume_operations_remove() { if (game.count === 0) goto_operations_done() else if (!can_operations_remove()) goto_operations_place() } // OPERATIONS: PLACE function goto_operations_place() { update_presence_and_control() if (can_operations_place()) game.state = "operations_place" else game.state = "operations_done" } function can_operations_place() { if (find_available_cube() < 0) return false for (let s of game.spaces) if (can_operations_place_space(s)) return true return false } function can_operations_place_space(s) { if (is_present(s) || is_adjacent_to_control(s)) { if (count_friendly_cubes(s) >= 4) return false let d = has_enemy_disc(s) if (!d || game.count >= 2) return true } return false } states.operations_place = { prompt() { view.prompt = "Operations: Place cubes." for (let s of game.spaces) if (can_operations_place_space(s)) gen_action_space(s) view.actions.end_turn = 1 }, space(s) { push_undo() if (has_enemy_disc(s)) game.count -= 2 else game.count -= 1 place_piece(find_available_cube(), s) resume_operations_place() }, end_turn() { end_operations() }, } function resume_operations_place() { if (game.count === 0 || !can_operations_place()) goto_operations_done() } // OPERATIONS: DONE function goto_operations_done() { game.state = "operations_done" } states.operations_done = { prompt() { view.prompt = "Operations: All done." view.actions.end_turn = 1 }, end_turn() { end_operations() }, } function end_operations() { clear_undo() resume_strategy_phase() } // === SET ASIDE CARDS === function goto_set_aside_cards() { for (let c of game.red_hand) game.red_set_aside.push(c) game.red_hand = [] for (let c of game.blue_hand) game.blue_set_aside.push(c) game.blue_hand = [] goto_pivotal_space_bonus_actions() } function goto_pivotal_space_bonus_actions() { game.state = "pivotal_space_bonus_actions" game.active = game.initiative } // === EVENTS === function goto_play_event(c) { switch (c) { // TODO } resume_strategy_phase() } // === SETUP === exports.setup = function (seed, scenario, options) { game = { seed: seed, scenario: scenario, log: [], undo: [], active: "Both", state: "choose_objective_card", round: 1, initiative: null, political_vp: 0, military_vp: 0, red_momentum: 0, blue_momentum: 0, strategy_deck: [], objective_deck: [], discard: 0, red_final: 34, red_hand: [], red_set_aside: [], red_objective: [], blue_final: 17, blue_hand: [], blue_set_aside: [], blue_objective: [], presence: 0, control: 0, pieces: [ // Commune cubes RED_CRISIS_TRACK[0], RED_CRISIS_TRACK[0], RED_CRISIS_TRACK[0], RED_CRISIS_TRACK[1], RED_CRISIS_TRACK[1], RED_CRISIS_TRACK[2], RED_CRISIS_TRACK[2], RED_CRISIS_TRACK[3], RED_CRISIS_TRACK[3], RED_BONUS_CUBES[0], RED_BONUS_CUBES[0], RED_BONUS_CUBES[1], RED_BONUS_CUBES[1], RED_BONUS_CUBES[2], RED_BONUS_CUBES[2], PRESS, SOCIAL_MOVEMENTS, PERE_LACHAISE, // Versailles cubes BLUE_CRISIS_TRACK[0], BLUE_CRISIS_TRACK[1], BLUE_CRISIS_TRACK[1], BLUE_CRISIS_TRACK[2], BLUE_CRISIS_TRACK[3], BLUE_CRISIS_TRACK[3], BLUE_BONUS_CUBES[0], BLUE_BONUS_CUBES[1], BLUE_BONUS_CUBES[2], BLUE_BONUS_CUBES[2], PRUSSIAN_COLLABORATION[0], PRUSSIAN_COLLABORATION[1], PRUSSIAN_COLLABORATION[1], PRUSSIAN_COLLABORATION[2], PRUSSIAN_COLLABORATION[2], PRUSSIAN_COLLABORATION[2], ROYALISTS, PRESS, // Commune discs -1, -1, // Versailles discs -1, -1, ], count: 0, spaces: null, who: -1, } log_h1("Red Flag Over Paris") log_h1("Round 1") for (let c = 1; c <= 41; ++c) if (c !== 17 && c !== 34) game.strategy_deck.push(c) for (let c = 42; c <= 53; ++c) game.objective_deck.push(c) shuffle(game.strategy_deck) shuffle(game.objective_deck) let n = 4 if (game.scenario === "Censorship") n = 5 for (let i = 0; i < n; ++i) { game.red_hand.push(game.strategy_deck.pop()) game.blue_hand.push(game.strategy_deck.pop()) } for (let i = 0; i < 2; ++i) { game.red_objective.push(game.objective_deck.pop()) game.blue_objective.push(game.objective_deck.pop()) } return game } // === VIEW === exports.is_checkpoint = function (a, b) { return a.round !== b.round } exports.view = function(state, player) { game = state view = { log: game.log, prompt: null, actions: null, round: game.round, initiative: game.initiative, political_vp: game.political_vp, military_vp: game.military_vp, red_hand: game.red_hand.length, blue_hand: game.blue_hand.length, red_momentum: game.red_momentum, blue_momentum: game.blue_momentum, pieces: game.pieces, discard: game.discard, hand: 0, final: 0, set_aside: 0, objective: 0 } if (player === COMMUNE) { view.hand = game.red_hand view.final = game.red_final view.set_aside = game.red_set_aside view.objective = game.red_objective } if (player === VERSAILLES) { view.hand = game.blue_hand view.final = game.blue_final view.set_aside = game.blue_set_aside view.objective = game.blue_objective } if (game.state === "game_over") { view.prompt = game.victory } else if (player === "Observer" || (game.active !== player && game.active !== "Both")) { if (states[game.state]) { let inactive = states[game.state].inactive || game.state view.prompt = `Waiting for ${game.active} to ${inactive}...` } else { view.prompt = "Unknown state: " + game.state } } else { view.actions = {} if (states[game.state]) states[game.state].prompt(player) 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 } } return view } exports.action = function (state, player, action, arg) { game = state if (states[game.state] && action in states[game.state]) { states[game.state][action](arg, player) } else { if (action === "undo" && game.undo && game.undo.length > 0) pop_undo() else throw new Error("Invalid action: " + action) } return game } // === GAME OVER === exports.resign = function (state, player) { game = state if (game.state !== "game_over") { if (current === COMMUNE) goto_game_over(VERSAILLES, "Commune resigned."); if (current === VERSAILLES) goto_game_over(COMMON, "Versailles resigned."); } return game } function goto_game_over(result, victory) { game.state = "game_over" game.active = "None" game.result = result game.victory = victory log_br() log(game.victory) } states.game_over = { get inactive() { return game.victory }, prompt() { view.prompt = game.victory }, } // === ACTIONS === function gen_action(action, argument) { if (!(action in view.actions)) view.actions[action] = [] set_add(view.actions[action], argument) } function gen_action_card(c) { gen_action("card", c) } function gen_action_piece(p) { gen_action("piece", p) } function gen_action_space(s) { gen_action("space", s) } // === LOGGING === function log(msg) { game.log.push(msg) } function log_br() { if (game.log.length > 0 && game.log[game.log.length - 1] !== "") game.log.push("") } function logi(msg) { game.log.push(">" + msg) } function log_h1(msg) { log_br() log(".h1 " + msg) log_br() } function log_h2(msg) { log_br() log(".h2 " + msg) log_br() } // === COMMON LIBRARY === function clear_undo() { if (game.undo.length > 0) game.undo = [] } function push_undo() { let copy = {} for (let k in game) { let v = game[k] 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() { let save_log = game.log let save_undo = game.undo game = save_undo.pop() save_log.length = game.log game.log = save_log game.undo = save_undo } function random(range) { // An MLCG using integer arithmetic with doubles. // https://www.ams.org/journals/mcom/1999-68-225/S0025-5718-99-00996-5/S0025-5718-99-00996-5.pdf // m = 2**35 − 31 return (game.seed = game.seed * 200105 % 34359738337) % range } 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(list) { // Fisher-Yates shuffle for (let i = list.length - 1; i > 0; --i) { let j = random(i + 1) let tmp = list[j] list[j] = list[i] list[i] = tmp } } 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_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_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_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_toggle(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 } } array_insert(set, a, item) } // Map as plain sorted array of key/value pairs function map_clear(map) { map.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, item) { let a = 0 let b = (map.length >> 1) - 1 while (a <= b) { let m = (a + b) >> 1 let x = map[m<<1] if (item < x) b = m - 1 else if (item > x) a = m + 1 else { array_remove_pair(map, m<<1) return } } }