"use strict" // TODO: clean up push/clear_undo // TODO: clean up prompts // TODO: event prompts // TODO: clean up logging const COMMUNE = "Commune" const VERSAILLES = "Versailles" var game, view, states = {} exports.scenarios = [ "Standard" ] 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 crisis_names = [ "Escalation", "Tension", "Final Crisis" ] 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", "Louise 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 OUT_OF_PLAY = -1 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 ANY = [ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11 ] 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, ] 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 PIVOTAL = [ NATIONAL_ASSEMBLY, PRESS, MONT_VALERIEN, BUTTE_MONTMARTRE ] function is_same_list(a, b) { if (a === b) return true if (a.length !== b.length) return false for (let i = 0; i < a.length; ++i) if (a[i] !== b[i]) return false return true } function space_list_name(list) { if (list.length === 1) return space_names[list[0]] if (list.length === 12) return "any space" if (is_same_list(list, POLITICAL)) return "Political" if (is_same_list(list, MILITARY)) return "Military" if (is_same_list(list, INSTITUTIONAL)) return "Institutional" if (is_same_list(list, PUBLIC_OPINION)) return "Public Opinion" if (is_same_list(list, FORTS)) return "Forts" if (is_same_list(list, PARIS)) return "Paris" if (is_same_list(list, PIVOTAL)) return "Pivotal" return list.map(s=>space_names[s]).join(", ") } const DIMENSION_SPACES = [ INSTITUTIONAL, INSTITUTIONAL, INSTITUTIONAL, PUBLIC_OPINION, PUBLIC_OPINION, PUBLIC_OPINION, FORTS, FORTS, FORTS, PARIS, PARIS, PARIS, ] const DIMENSION_NAME = [ "Institutional", "Institutional", "Institutional", "Public Opinion", "Public Opinion", "Public Opinion", "Forts", "Forts", "Forts", "Paris", "Paris", "Paris", ] 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 ], ] 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 OBJECTIVE_SPACE = [ BUTTE_MONTMARTRE, BUTTE_AUX_CAILLES, PERE_LACHAISE, FORT_D_ISSY, MONT_VALERIEN, CHATEAU_DE_VINCENNES, PRESS, CATHOLIC_CHURCH, SOCIAL_MOVEMENTS, ROYALISTS, REPUBLICANS, NATIONAL_ASSEMBLY, ] function objective_card_space(c) { return OBJECTIVE_SPACE[c - 42] } function commune_objective_card() { return game.red_objective } function versailles_objective_card() { return game.blue_objective } function commune_objective_space() { return objective_card_space(commune_objective_card()) } function versailles_objective_space() { return objective_card_space(versailles_objective_card()) } // === GAME STATE === function discard_card(c) { array_remove_item(player_hand(), c) game.discard.push(c) } function recycle_card(c) { array_remove_item(player_hand(), c) game.strategy_deck.unshift(c) } function remove_card(c) { array_remove_item(player_hand(), c) } function add_political_vp(side, amount) { if (side === COMMUNE) { if (amount > 0) log("Commune +" + amount + " Political VP.") else log("Commune " + amount + " Political VP.") game.political_vp += amount } else { if (amount > 0) log("Versailles +" + amount + " Political VP.") else log("Versailles " + amount + " Political VP.") game.political_vp -= amount } game.political_vp = Math.min(5, Math.max(-5, game.political_vp)) } function add_military_vp(side, amount) { if (side === COMMUNE) { if (amount > 0) log("Commune +" + amount + " Military VP.") else log("Commune " + amount + " Military VP.") game.military_vp += amount } else { if (amount > 0) log("Versailles +" + amount + " Military VP.") else log("Versailles " + amount + " Military VP.") game.military_vp -= amount } game.military_vp = Math.min(5, Math.max(-5, game.military_vp)) } 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 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 && is_versailles_card(c)) return false if (game.active === VERSAILLES && is_commune_card(c)) return false // Cards 38 through 41 can only be played if you have the initiative if (c >= 38 && c <= 41) if (game.active !== game.initiative) return false // Les Amis de l'Ordre - must place 2 cubes in Paris if (c === 8) { if (count_available_versailles_cubes() < 2) return false let n = count_versailles_cubes(BUTTE_MONTMARTRE) + count_versailles_cubes(BUTTE_AUX_CAILLES) + count_versailles_cubes(PERE_LACHAISE) if (n > 12 - 2) return false } // Otto von Bismarck - must remove blue cube from National Assembly if (c === 12) if (!has_versailles_cube(NATIONAL_ASSEMBLY)) return false // Le Reveil - cannot be played if Socialist Newspaper Ban if (c === 21) if (game.snb) return false // Victor Hugo - must decrease both player momentums if (c === 38) if (game.red_momentum < 1 || game.blue_momentum < 1) return false // Freemason Parade - must remove red & blue cubes from one Military space if (c === 41) if (!can_play_freemason_parade()) return false return true } 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_commune_control_dimension(dim) { for (let s of dim) if (!is_commune_control(s)) return false return true } function is_versailles_control_dimension(dim) { for (let s of dim) if (!is_versailles_control(s)) return false return true } function count_cubes(s) { return count_commune_cubes(s) + count_versailles_cubes(s) } function for_each_cube(s, f) { for_each_commune_cube(s, f) for_each_versailles_cube(s, f) } 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_friendly_discs(s) { if (game.active === COMMUNE) return count_commune_discs(s) return count_versailles_discs(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 find_available_disc() { return find_friendly_disc(OUT_OF_PLAY) } function has_commune_cube(s) { return find_commune_cube(s) >= 0 } function has_versailles_cube(s) { return find_versailles_cube(s) >= 0 } function has_enemy_cube(s) { return find_enemy_cube(s) >= 0 } function has_friendly_cube(s) { return find_friendly_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_commune_disc(p) { return p >= first_commune_disc && p <= last_commune_disc } function is_versailles_disc(p) { return p >= first_versailles_disc && p <= last_versailles_disc } function is_disc(p) { return p >= 36 } function can_place_cube_in_any(list) { for (let s of list) if (can_place_cube(s)) return true return false } function can_replace_cube_in_any(list) { for (let s of list) if (can_replace_cube(s)) return true return false } function can_place_cube(s, removed = 0) { return find_available_cube(removed) >= 0 && count_friendly_cubes(s) < 4 } function can_place_disc(s) { return count_friendly_discs(s) < 1 } function can_replace_cube(s) { return has_enemy_cube(s) && can_place_cube(s) } function can_remove_cube(s) { return has_enemy_cube(s) } 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] = OUT_OF_PLAY } else if (is_versailles_cube(p)) { game.pieces[p] = BLUE_CUBE_POOL } else { game.pieces[p] = OUT_OF_PLAY } } function remove_piece_from_play(p) { game.pieces[p] = OUT_OF_PLAY } function move_piece(p, s) { game.pieces[p] = s } function place_cube(s, removed = 0) { game.pieces[find_available_cube(removed)] = s } function place_disc(s) { game.pieces[find_available_disc()] = s } function replace_cube(p) { let s = game.pieces[p] remove_piece(p) place_cube(s) } function find_available_cube(removed = 0) { let p = -1 if (game.active === COMMUNE) { if (removed) { p = find_commune_cube(OUT_OF_PLAY) if (p >= 0) return p } for (let i = 2; i >= 0; --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 count_available_versailles_cubes() { return ( count_versailles_cubes(BLUE_CUBE_POOL) + count_versailles_cubes(BLUE_CRISIS_TRACK[0]) + count_versailles_cubes(BLUE_CRISIS_TRACK[1]) + count_versailles_cubes(BLUE_CRISIS_TRACK[2]) + count_versailles_cubes(BLUE_CRISIS_TRACK[3]) ) } 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 } function commune_military_vp() { return game.military_vp } function versailles_military_vp() { return -game.military_vp } function where_present(list) { return list.filter(s => is_present(s)) } // === START ROUND === function start_round() { if (game.round === 3) { goto_final_crisis() return } if (count_cubes(RED_CRISIS_TRACK[3]) < 2 && count_cubes(BLUE_CRISIS_TRACK[3]) < 2) { goto_final_crisis() return } game.round += 1 log_h1("Round " + game.round) let n = 4 if (game.censorship) n = 5 for (let i = 0; i < n; ++i) { game.red_hand.push(draw_strategy_card()) game.blue_hand.push(draw_strategy_card()) } game.red_objective = [ game.objective_deck.pop(), game.objective_deck.pop() ] game.blue_objective = [ game.objective_deck.pop(), game.objective_deck.pop() ] game.active = "Both" game.state = "choose_objective_card" } // === CHOOSE OBJECTIVE CARD === states.choose_objective_card = { inactive: "choose an objective card", prompt(player) { view.prompt = "Choose an Objective card to keep." if (player === 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, player) { if (player === COMMUNE) game.red_objective = c else game.blue_objective = c if (typeof game.red_objective === "number" && typeof game.blue_objective === "number") end_choose_objective_card() else if (typeof game.red_objective === "number") game.active = VERSAILLES else if (typeof game.blue_objective === "number") 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 = "Initiative Phase: Decide first player." view.actions.commune = 1 view.actions.versailles = 1 }, commune() { log("Initiative to Commune.") game.initiative = COMMUNE end_initiative_phase() }, versailles() { log("Initiative to Versailles.") game.initiative = VERSAILLES end_initiative_phase() }, } function end_initiative_phase() { game.active = game.initiative if (game.round === 4) goto_final_crisis_events() else if (game.censorship) goto_censorship_phase() else goto_strategy_phase() } // === CENSORSHIP PHASE === function goto_censorship_phase() { game.state = "censorship_phase" } states.censorship_phase = { inactive: "discard a card", prompt() { view.prompt = "Censorship Phase: Discard a card from your hand." for (let c of player_hand()) gen_action("card", c) }, card(c) { log_br() log(game.active + " - Censorship") logi("C" + c) discard_card(c) game.active = enemy_player() if (game.active === game.initiative) goto_strategy_phase() }, } // === PLAYING STRATEGY CARDS === function goto_strategy_phase() { clear_undo() log_h2("Strategy Phase") game.state = "strategy_phase" } function end_play_card() { clear_undo() goto_crisis_breach("strategy") } function end_crisis_breach_strategy() { if (game.round === 4) resume_final_crisis() else resume_strategy_phase() } function resume_strategy_phase() { if (game.red_hand.length === 1 && game.blue_hand.length === 1) { goto_set_aside_cards() } else { clear_undo() game.active = enemy_player() game.state = "strategy_phase" } } function player_final_crisis_card() { if (game.active === COMMUNE) return game.red_final return game.blue_final } function top_discard() { let n = game.discard.length if (n > 0) return game.discard[n-1] return 0 } states.strategy_phase = { inactive: "play a card", prompt() { view.prompt = "Strategy Phase: Play a card." for (let c of player_hand()) gen_action_card(c) if (player_hand().length > 1) { let final = player_final_crisis_card() if (final > 0) gen_action_card(final) if (top_discard() > 0 && !is_neutral_card(top_discard())) if (can_play_event(top_discard())) gen_action_card(top_discard()) } }, card(c) { push_undo() game.what = c if (c === 17 || c === 34) { game.state = "play_final_discard" } else if (c === top_discard()) { game.state = "play_discard" } else { game.state = "play_card" } }, } states.play_card = { inactive: "play a card", prompt() { let c = game.what view.prompt = card_names[game.what] + ": Play for Event, Operations, or Momentum." 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 }, event() { push_undo() log_h3(game.active + " - Event") logi("C" + game.what) discard_card(game.what) goto_play_event(game.what) }, political() { push_undo() log_h3(game.active + " - Operations") logi("C" + game.what) discard_card(game.what) goto_operations(card_ops[game.what], POLITICAL) }, military() { push_undo() log_h3(game.active + " - Operations") logi("C" + game.what) discard_card(game.what) goto_operations(card_ops[game.what], MILITARY) }, momentum() { push_undo() log_h3(game.active + " - Momentum") logi("C" + game.what) if (game.censorship) recycle_card(game.what) else remove_card(game.what) if (game.active === COMMUNE) game.state = "advance_revolutionary_momentum" else game.state = "advance_prussian_collaboration" }, } states.advance_revolutionary_momentum = { inactive: "advance Revolutionary Momentum", prompt() { view.prompt = "Advance Revolutionary Momentum." view.actions.red_momentum = 1 }, red_momentum() { increase_revolutionary_momentum() }, } states.advance_prussian_collaboration = { inactive: "advance Prussian Collaboration", prompt() { view.prompt = "Advance Prussian Collaboration." view.actions.blue_momentum = 1 }, blue_momentum() { increase_prussian_collaboration() }, } states.play_discard = { inactive: "play a card", prompt() { view.prompt = card_names[game.what] + ": Discard a card." for (let c of player_hand()) gen_action_card(c) view.selected_card = game.what }, card(c) { push_undo() log_h3(game.active + " - Discarded Event") logi("C" + c) logi("C" + game.what) discard_card(c) goto_play_event(game.what) }, } states.play_final_discard = { inactive: "play a card", prompt() { view.prompt = card_names[game.what] + ": Discard a card." for (let c of player_hand()) gen_action_card(c) view.selected_card = game.what }, card(c) { push_undo() log_h3(game.active + " - Operations (FC)") logi("C" + c) logi("C" + game.what) discard_final() discard_card(c) game.state = "play_final_ops" }, } states.play_final_ops = { inactive: "play a card", prompt() { view.prompt = card_names[game.what] + ": Use up to 4 Ops." view.selected_card = game.what view.actions.political = 1 view.actions.military = 1 }, political() { push_undo() goto_operations(4, POLITICAL) }, military() { push_undo() goto_operations(4, MILITARY) }, } function discard_final() { if (game.active === COMMUNE) game.red_final = 0 else game.blue_final = 0 } // === PLAYER MOMENTUM TRACKS === function increase_revolutionary_momentum() { game.red_momentum += 1 log("Increased RM to " + game.red_momentum + ".") goto_increase_revolutionary_momentum_trigger() } function goto_increase_revolutionary_momentum_trigger() { game.momentum_active = game.active game.active = VERSAILLES if (game.red_momentum >= 2 && can_place_cube_in_any(INSTITUTIONAL)) { clear_undo() game.state = "increase_revolutionary_momentum_trigger" } else { end_increase_momentum() } } function increase_prussian_collaboration() { game.blue_momentum += 1 log("Increased BM to " + game.blue_momentum + ".") goto_increase_prussian_collaboration_add() } function goto_increase_prussian_collaboration_add() { game.state = "increase_prussian_collaboration_add" for (let i = 0; i < game.blue_momentum; ++i) if (has_versailles_cube(PRUSSIAN_COLLABORATION[i])) return goto_increase_prussian_collaboration_trigger() } function goto_increase_prussian_collaboration_trigger() { game.momentum_active = game.active game.active = COMMUNE if (game.blue_momentum >= 2 && can_place_cube_in_any(PUBLIC_OPINION)) { clear_undo() game.state = "increase_prussian_collaboration_trigger" } else { end_increase_momentum() } } function end_increase_momentum() { game.active = game.momentum_active delete game.momentum_active if (game.vm) vm_next() else end_play_card() } function decrease_revolutionary_momentum() { game.red_momentum -= 1 log("Decreased RM to " + game.red_momentum + ".") goto_decrease_revolutionary_momentum_remove() } function goto_decrease_revolutionary_momentum_remove() { game.state = "decrease_revolutionary_momentum_remove" for (let i = game.red_momentum; i < 3; ++i) if (has_commune_cube(RED_CUBE_POOL[i])) return end_decrease_momentum() } function decrease_prussian_collaboration() { game.blue_momentum -= 1 log("Decreased BM to " + game.blue_momentum + ".") end_decrease_momentum() } function end_decrease_momentum() { vm_next() } states.decrease_revolutionary_momentum_remove = { inactive: "remove cubes from Pool", prompt() { view.prompt = "Revolutionary Momentum: Remove cubes from Pool." for (let i = game.red_momentum; i < 3; ++i) for_each_commune_cube(RED_CUBE_POOL[i], gen_action_piece) }, piece(p) { log("Removed RC from play.") remove_piece_from_play(p) goto_decrease_revolutionary_momentum_remove() }, } states.increase_prussian_collaboration_add = { inactive: "add cubes to Pool", prompt() { view.prompt = "Prussian Collaboration: Add cubes to Pool." for (let i = 0; i < game.blue_momentum; ++i) for_each_versailles_cube(PRUSSIAN_COLLABORATION[i], gen_action_piece) }, piece(p) { log("Added BC to Pool.") remove_piece(p) goto_increase_prussian_collaboration_add() }, } states.increase_revolutionary_momentum_trigger = { inactive: "place a cube", prompt() { view.prompt = "Revolutionary Momentum: Place a cube in an Institutional space." for (let s of INSTITUTIONAL) if (can_place_cube(s)) gen_action_space(s) view.actions.pass = 1 }, space(s) { log("Placed BC in S" + s + ".") place_cube(s) end_increase_momentum() }, pass() { end_increase_momentum() }, } states.increase_prussian_collaboration_trigger = { inactive: "place a cube", prompt() { view.prompt = "Prussian Collaboration: Place a cube in a Public Opinion space." for (let s of PUBLIC_OPINION) if (can_place_cube(s)) gen_action_space(s) view.actions.pass = 1 }, space(s) { log("Placed RC in S" + s + ".") place_cube(s) end_increase_momentum() }, pass() { end_increase_momentum() }, } // === CRISIS TRACK & CUBE POOLS === function is_commune_first_to_breach_final() { return count_versailles_cubes(BLUE_CRISIS_TRACK[3]) === 2 } function is_versailles_first_to_breach_final() { return count_commune_cubes(RED_CRISIS_TRACK[3]) === 2 } function goto_crisis_breach(next) { game.breach_active = game.active game.breach_next = next goto_commune_crisis_breach() } function end_crisis_breach() { game.active = game.breach_active let next = game.breach_next delete game.breach_active delete game.breach_next switch (next) { case "strategy": end_crisis_breach_strategy() break case "pivotal": end_crisis_breach_pivotal() break case "objective": end_crisis_breach_objective() break } } function assess_commune_crisis_track(i, an, bn) { let a = count_commune_cubes(RED_CRISIS_TRACK[i+1]) let b = count_commune_cubes(RED_BONUS_CUBES[i]) if (a < an) { if (b === bn) { log_h3("Commune - " + crisis_names[i]) if (i === 2 && is_commune_first_to_breach_final()) add_political_vp(COMMUNE, -1) } return b > 0 } return false } function assess_versailles_crisis_track(i, an, bn) { let a = count_versailles_cubes(BLUE_CRISIS_TRACK[i+1]) let b = count_versailles_cubes(BLUE_BONUS_CUBES[i]) if (a < an) { if (b === bn) { log_h3("Versailles - " + crisis_names[i]) if (i === 2 && is_versailles_first_to_breach_final()) add_political_vp(VERSAILLES, -1) } return b > 0 } return false } function goto_commune_crisis_breach() { game.active = COMMUNE game.state = "commune_crisis_breach" if (assess_commune_crisis_track(0, 2, 2)) return if (assess_commune_crisis_track(1, 2, 2)) return if (assess_commune_crisis_track(2, 2, 2)) return end_commune_crisis_breach() } function goto_versailles_crisis_breach() { game.active = VERSAILLES game.state = "versailles_crisis_breach" if (assess_versailles_crisis_track(0, 2, 1)) return if (assess_versailles_crisis_track(1, 1, 1)) return if (assess_versailles_crisis_track(2, 2, 2)) return end_versailles_crisis_breach() } function end_commune_crisis_breach() { goto_versailles_crisis_breach() } function end_versailles_crisis_breach() { end_crisis_breach() } states.commune_crisis_breach = { inactive: "move bonus cubes", prompt() { view.prompt = "Crisis Breach: Move bonus cubes to Pool." if (count_commune_cubes(RED_CRISIS_TRACK[1]) < 2) for_each_commune_cube(RED_BONUS_CUBES[0], gen_action_piece) if (count_commune_cubes(RED_CRISIS_TRACK[2]) < 2) for_each_commune_cube(RED_BONUS_CUBES[1], gen_action_piece) if (count_commune_cubes(RED_CRISIS_TRACK[3]) < 2) { if (count_versailles_cubes(BLUE_BONUS_CUBES[2]) > 0) { view.prompt = "Crisis Breach: Remove bonus cubes." for_each_versailles_cube(BLUE_BONUS_CUBES[2], gen_action_piece) } else for_each_commune_cube(RED_BONUS_CUBES[2], gen_action_piece) } }, piece(p) { if (is_versailles_cube(p)) { remove_piece_from_play(p) log("Removed BC from bonus.") } else { remove_piece(p) if (game.pieces[p] < 0) log("Removed RC from bonus.") else log("Added RC to Pool.") goto_commune_crisis_breach() } }, } states.versailles_crisis_breach = { inactive: "move bonus cubes", prompt() { view.prompt = "Crisis Breach: Move bonus cubes to Pool." if (count_versailles_cubes(BLUE_CRISIS_TRACK[1]) < 2) for_each_versailles_cube(BLUE_BONUS_CUBES[0], gen_action_piece) if (count_versailles_cubes(BLUE_CRISIS_TRACK[2]) < 1) for_each_versailles_cube(BLUE_BONUS_CUBES[1], gen_action_piece) if (count_versailles_cubes(BLUE_CRISIS_TRACK[3]) < 2) { if (count_commune_cubes(RED_BONUS_CUBES[2]) > 0) { view.prompt = "Crisis Breach: Remove opponent's bonus cubes." for_each_commune_cube(RED_BONUS_CUBES[2], gen_action_piece) } else for_each_versailles_cube(BLUE_BONUS_CUBES[2], gen_action_piece) } }, piece(p) { if (is_commune_cube(p)) { remove_piece_from_play(p) log("Removed RC from bonus.") } else { remove_piece(p) log("Added BC to Pool.") goto_versailles_crisis_breach() } }, } // === OPERATIONS === function goto_operations(count, spaces) { game.count = count game.spaces = spaces goto_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 } function piece_abbr(p) { if (is_commune_cube(p)) return "RC" if (is_versailles_cube(p)) return "BC" if (is_commune_disc(p)) return "RD" if (is_versailles_disc(p)) return "BD" } states.operations_remove = { inactive: "use Operations Points", prompt() { let prompt = "Use " + game.count + " OP in " + space_list_name(game.spaces) + " to remove." if (game.vm) event_prompt(prompt) else view.prompt = "Operations: " + prompt 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.place = 1 }, piece(p) { push_undo() let s = game.pieces[p] let pn = piece_abbr(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) { logi("Remove " + pn + " from S" + s + ".") remove_piece(p) resume_operations_remove() } else { log("Remove " + pn + " from S" + s + ".") game.who = p game.state = "operations_remove_spend" } } else { log("Removed " + pn + " from S" + s + ".") remove_piece(p) resume_operations_remove() } }, place() { push_undo() goto_operations_place() }, } states.operations_remove_spend = { inactive: "use Operations Points", prompt() { let p = game.who let s = game.pieces[p] let str = military_strength(s) view.prompt = "Operations: Military strength " + str + " \u2014 spend extra OP?" view.selected_cube = p if (game.count > 0) view.actions.spend = 1 else view.actions.spend = 0 view.actions.pass = 1 }, spend() { logi("Spent extra OP") game.count -= 1 attempt_remove_piece(1) }, pass() { attempt_remove_piece(0) }, } states.operations_remove_draw = { inactive: "use Operations Points", prompt() { view.prompt = "Operations: Draw card for removal in Military." 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 = draw_strategy_card() let str = military_strength(s) + extra let ops = card_ops[c] logi("Drew C" + c) logi("Strength " + str + " vs " + ops) if (str >= ops) { remove_piece(p) logi("Success") } else { logi("Failure") } 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() else game.state = "operations_remove" } 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 (can_place_cube(s)) { let d = has_enemy_disc(s) if (!d || game.count >= 2) return true } } return false } states.operations_place = { inactive: "use Operations Points", prompt() { let prompt = "Use " + game.count + " OP in " + space_list_name(game.spaces) + " to place." if (game.vm) event_prompt(prompt) else view.prompt = "Operations: " + prompt for (let s of game.spaces) if (can_operations_place_space(s)) gen_action_space(s) view.actions.pass = 1 }, space(s) { push_undo() if (has_enemy_disc(s)) game.count -= 2 else game.count -= 1 if (game.active === COMMUNE) log("Placed RC in S" + s + ".") else log("Placed BC in S" + s + ".") place_cube(s) resume_operations_place() }, pass() { end_operations() }, } function resume_operations_place() { if (game.count === 0 || !can_operations_place()) goto_operations_done() } function goto_operations_done() { if (game.vm) end_operations() else game.state = "operations_done" } states.operations_done = { inactive: "use Operations Points", prompt() { view.prompt = "Operations: Done." view.actions.done = 1 }, done() { end_operations() }, } function end_operations() { if (game.vm) vm_next() else end_play_card() } // === 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() } // === PIVOTAL SPACE BONUS ACTIONS === function goto_pivotal_space_bonus_actions() { log_h2("Pivotal Bonus Actions") update_presence_and_control() game.spaces = PIVOTAL.filter(s => is_commune_control(s) || is_versailles_control(s)) resume_pivotal_space_bonus_actions() } function resume_pivotal_space_bonus_actions() { clear_undo() goto_crisis_breach("pivotal") } function end_crisis_breach_pivotal() { game.active = game.initiative if (game.spaces.length > 0) game.state = "pivotal_space_bonus_actions" else goto_crisis_dimension_scoring() } states.pivotal_space_bonus_actions = { inactive: "choose a Pivotal Space", prompt() { view.prompt = "Pivotal Space Bonus Actions: Choose a Pivotal Space." for (let s of game.spaces) gen_action_space(s) }, space(s) { goto_bonus_action(s) }, } function goto_bonus_action(s) { array_remove_item(game.spaces, s) game.where = s game.state = "bonus_action" if (is_commune_control(s)) game.active = COMMUNE else game.active = VERSAILLES log_h3(DIMENSION_NAME[s] + " - " + game.active) } states.bonus_action = { inactive: "choose a Bonus Action", prompt() { let dimension = DIMENSION_SPACES[game.where] view.prompt = "Pivotal Space Bonus Action in " + DIMENSION_NAME[game.where] + "." view.where = game.where view.actions.pass = 1 view.actions.de_escalate = 1 view.actions.spread_influence = 1 if (can_replace_cube_in_any(dimension)) view.actions.turncoat = 1 else view.actions.turncoat = 0 }, de_escalate() { push_undo() // log("De-escalated.") game.state = "de_escalate_1" }, spread_influence() { push_undo() // log("Spread Influence.") game.who = -1 game.count = 2 game.state = "spread_influence" }, turncoat() { push_undo() // log("Turncoat.") game.state = "turncoat" }, pass() { log("Passed.") resume_pivotal_space_bonus_actions() }, } states.de_escalate_1 = { inactive: "de-escalate", prompt() { let dimension = DIMENSION_SPACES[game.where] view.prompt = "De-escalate in " + DIMENSION_NAME[game.where] + ": Remove your own cube." for (let s of dimension) for_each_friendly_cube(s, gen_action_piece) }, piece(p) { push_undo() let s = game.pieces[p] log("Removed " + piece_abbr(p) + " from S" + s + ".") remove_piece(p) game.state = "de_escalate_2" }, } states.de_escalate_2 = { inactive: "de-escalate", prompt() { let dimension = DIMENSION_SPACES[game.where] view.prompt = "De-escalate in " + DIMENSION_NAME[game.where] + ": Remove your own or opponent's cube." for (let s of dimension) { for_each_enemy_cube(s, gen_action_piece) for_each_friendly_cube(s, gen_action_piece) } view.actions.skip = 1 }, piece(p) { push_undo() let s = game.pieces[p] log("Removed " + piece_abbr(p) + " from S" + s + ".") remove_piece(p) resume_pivotal_space_bonus_actions() }, skip() { push_undo() resume_pivotal_space_bonus_actions() }, } states.spread_influence = { inactive: "spread influence", prompt() { let dimension = DIMENSION_SPACES[game.where] view.prompt = "Spread Influence in " + DIMENSION_NAME[game.where] + "." view.selected_cube = game.who if (game.who < 0) { // TODO: don't move same cube twice! for (let s of dimension) for_each_friendly_cube(s, gen_action_piece) } else { for (let s of dimension) if (game.pieces[game.who] !== s) gen_action_space(s) } view.actions.done = 1 }, piece(p) { push_undo() game.who = p }, space(s) { let from = game.pieces[game.who] log("Moved " + piece_abbr(game.who) + " from S" + from + " to S" + s + ".") move_piece(game.who, s) game.who = -1 if (--game.count === 0) resume_pivotal_space_bonus_actions() }, done() { resume_pivotal_space_bonus_actions() }, } states.turncoat = { inactive: "replace a cube", prompt() { let dimension = DIMENSION_SPACES[game.where] view.prompt = "Turncoat in " + DIMENSION_NAME[game.where] + ": Replace an opponent's cube." for (let s of dimension) for_each_enemy_cube(s, gen_action_piece) }, piece(p) { let s = game.pieces[p] push_undo() log("Replaced " + piece_abbr(p) + " in S" + s + ".") replace_cube(p) resume_pivotal_space_bonus_actions() }, } // === CRISIS DIMENSION SCORING === function score_control(s, cf, vf, arg) { if (is_political_space(s)) { if (cf(arg)) { add_political_vp(COMMUNE, 1) } else if (vf(arg)) { add_political_vp(VERSAILLES, 1) } else { log("Nobody.") } } else { if (cf(arg)) { add_military_vp(COMMUNE, 1) } else if (vf(arg)) { add_military_vp(VERSAILLES, 1) } else { log("Nobody.") } } } function score_commune_control(s, cf, vf, arg) { if (is_political_space(s)) { if (cf(arg)) add_political_vp(COMMUNE, 1) else if (vf(arg)) add_political_vp(COMMUNE, -1) else log("Nobody.") } else { if (cf(arg)) add_military_vp(COMMUNE, 1) else if (vf(arg)) add_military_vp(COMMUNE, -1) else log("Nobody.") } } function score_versailles_control(s, cf, vf, arg) { if (is_political_space(s)) { if (cf(arg)) add_political_vp(VERSAILLES, -1) else if (vf(arg)) add_political_vp(VERSAILLES, 1) else log("Nobody.") } else { if (cf(arg)) add_military_vp(VERSAILLES, -1) else if (vf(arg)) add_military_vp(VERSAILLES, 1) else log("Nobody.") } } function goto_crisis_dimension_scoring() { clear_undo() log_h2("Crisis Dimension Scoring") update_presence_and_control() game.spaces = PIVOTAL.slice() game.active = game.initiative game.state = "crisis_dimension_scoring" } states.crisis_dimension_scoring = { inactive: "score Crisis Dimensions", prompt() { view.prompt = "Crisis Dimension Scoring: Choose a Dimension." for (let s of game.spaces) gen_action_space(s) }, space(s) { array_remove_item(game.spaces, s) log_h3(DIMENSION_NAME[s]) score_control(s, is_commune_control_dimension, is_versailles_control_dimension, DIMENSION_SPACES[s]) if (game.spaces.length === 0) end_crisis_dimension_scoring() }, } function end_crisis_dimension_scoring() { if (game.round === 4) goto_final_victory() else goto_objective_card_scoring() } // === OBJECTIVE CARD SCORING === function goto_objective_card_scoring() { log_h2("Objective Card Scoring") update_presence_and_control() game.reveal_objectives = 1 game.active = game.initiative game.count = 3 game.state = "objective_card_scoring" } states.objective_card_scoring = { inactive: "score Objective Cards", prompt() { view.prompt = "Objective Card Scoring: Score each Objective Card space." if (game.count & 1) gen_action_space(commune_objective_space()) if (game.count & 2) gen_action_space(versailles_objective_space()) }, space(s) { if (s === commune_objective_space()) { log_h3("S" + s) score_commune_control(s, is_commune_control, is_versailles_control, s) game.count ^= 1 } if (s === versailles_objective_space()) { log_h3("S" + s) score_versailles_control(s, is_commune_control, is_versailles_control, s) game.count ^= 2 } if (game.count === 0) goto_objective_card_remove() }, } function goto_objective_card_remove() { game.state = "objective_card_remove" if (game.red_objective && !is_commune_control(commune_objective_space())) return if (game.blue_objective && !is_versailles_control(versailles_objective_space())) return goto_objective_card_play() } states.objective_card_remove = { inactive: "remove unfulfilled Objective Cards", prompt() { view.prompt = "Objective Card Scoring: Remove each unfulfilled Objective Card." if (!is_commune_control(commune_objective_space())) gen_action_card(commune_objective_card()) if (!is_versailles_control(versailles_objective_space())) gen_action_card(versailles_objective_card()) }, card(c) { if (c === commune_objective_card()) { log_h3("Commune - Unfulfilled") logi("C" + commune_objective_card()) game.red_objective = 0 } if (c === versailles_objective_card()) { log_h3("Versailles - Unfulfilled") logi("C" + versailles_objective_card()) game.blue_objective = 0 } goto_objective_card_remove() } } function goto_objective_card_play() { clear_undo() if (commune_objective_card() || versailles_objective_card()) { game.active = game.initiative game.state = "objective_card_play" } else { delete game.reveal_objectives start_round() } } states.objective_card_play = { inactive: "play fulfilled Objective Cards", prompt() { view.prompt = "Objective Card Scoring: Play each fulfilled Objective Card." let c = commune_objective_card() if (c) gen_action_card(c) let v = versailles_objective_card() if (v) gen_action_card(v) }, card(c) { if (c === commune_objective_card()) { game.red_objective = 0 game.red_fulfilled += 1 game.active = COMMUNE log_h3("Commune - Event") logi("C" + c) goto_play_event(c) } if (c === versailles_objective_card()) { game.blue_objective = 0 game.blue_fulfilled += 1 game.active = VERSAILLES log_h3("Versailles - Event") logi("C" + c) goto_play_event(c) } }, } // === FINAL CRISIS === function goto_final_crisis() { log_h1("Final Crisis") game.red_hand = game.red_set_aside if (game.red_final) game.red_hand.push(game.red_final) game.red_final = 0 game.red_set_aside = [] game.blue_hand = game.blue_set_aside if (game.blue_final) game.blue_hand.push(game.blue_final) game.blue_final = 0 game.blue_set_aside = [] game.active = "Both" game.state = "final_crisis_discard" resume_final_crisis_discard() } function resume_final_crisis_discard() { if (game.red_hand.length > game.round && game.blue_hand.length > game.round) game.active = "Both" else if (game.red_hand.length > game.round) game.active = COMMUNE else if (game.blue_hand.length > game.round) game.active = VERSAILLES else { clear_undo() game.round = 4 goto_initiative_phase() } } states.final_crisis_discard = { inactive: "discard a card", prompt(player) { view.prompt = "Discard down to " + game.round + " cards." if (player === COMMUNE) { if (game.red_hand.length > game.round) for (let c of game.red_hand) gen_action_card(c) } if (player === VERSAILLES) { if (game.blue_hand.length > game.round) for (let c of game.blue_hand) gen_action_card(c) } }, card(c, player) { push_undo() if (player === COMMUNE) array_remove_item(game.red_hand, c) if (player === VERSAILLES) array_remove_item(game.blue_hand, c) resume_final_crisis_discard() }, } function goto_final_crisis_events() { clear_undo() game.state = "final_crisis_events" } function resume_final_crisis() { if (game.red_hand.length === 0 && game.blue_hand.length === 0) { goto_pivotal_space_bonus_actions() } else { game.state = "final_crisis_events" game.active = game.final_active game.active = enemy_player() } } states.final_crisis_events = { inactive: "play an event", prompt() { view.prompt = "Play a card." for (let c of player_hand()) gen_action_card(c) }, card(c) { game.final_active = game.active discard_card(c) if (game.active === VERSAILLES && is_commune_card(c)) { game.what = c game.active = COMMUNE game.state = "final_crisis_opponent_event" } else if (game.active === COMMUNE && is_versailles_card(c)) { game.what = c game.active = VERSAILLES game.state = "final_crisis_opponent_event" } else { log_h3(game.active + " - Event") logi("C" + c) if (can_play_event(c)) goto_play_event(c) else log("Could not play.") } }, } // TODO: - is this necessary? states.final_crisis_opponent_event = { inactive: "play an event", prompt() { view.prompt = card_names[game.what] + ": Play or ignore event." view.selected_card = game.what if (can_play_event(game.what)) view.actions.event = 1 else view.actions.event = 0 view.actions.pass = 1 }, event() { log_h3(game.active + " - Event") logi("C" + game.what) goto_play_event(game.what) }, pass() { log_h3(game.active + " - Pass") logi("C" + game.what) end_play_card() }, } function goto_final_victory() { update_presence_and_control() log_h2("Final Victory") if (game.red_momentum === 3) { log_h3("Revolutionary Momentum") add_political_vp(COMMUNE, 1) } if (game.blue_momentum === 3) { log_h3("Prussian Collaboration") add_military_vp(VERSAILLES, 1) } log_br() if (versailles_military_vp() > commune_military_vp()) return goto_game_over(VERSAILLES, "Versailles won with Military VP!") if (commune_political_vp() > versailles_political_vp()) return goto_game_over(COMMUNE, "Commune won with Political VP!") let v = 0 let c = 0 log_h3("Tiebreaker") if (versailles_military_vp() + versailles_political_vp() > commune_military_vp() + commune_political_vp()) { log("Combined VP: Versailles.") ++v } if (commune_military_vp() + commune_political_vp() > versailles_military_vp() + versailles_political_vp()) { log("Combined VP: Commune.") ++c } if (game.blue_fulfilled > game.red_fulfilled) { log("Objectives fulfilled: Versailles.") ++v } if (game.red_fulfilled > game.blue_fulfilled) { log("Objectives fulfilled: Commune.") ++c } let nv = 0 let nc = 0 for (let s of PIVOTAL) { if (is_commune_control(s)) nc++ if (is_versailles_control(s)) nv++ } if (nv > nc) { log("Pivotal Spaces: Versailles.") v++ } if (nc > nv) { log("Pivotal Spaces: Commune.") c++ } if (game.initiative === VERSAILLES) { log("Final Initiative: Versailles.") v++ } else { log("Final Initiative: Commune.") c++ } log_br() if (v > c) return goto_game_over(VERSAILLES, "Versailles won with " + v + " vs " + c + " conditions!") if (c > v) return goto_game_over(COMMUNE, "Commune won with " + c + " vs " + v + " conditions!") return goto_game_over(COMMUNE, "Commune won final tie breaker!") } // === EVENTS === function goto_play_event(c) { update_presence_and_control() goto_vm(c) } function end_event() { if (is_objective_card(game.vm.fp)) { game.vm = null goto_crisis_breach("objective") } else { game.vm = null end_play_card() } } function end_crisis_breach_objective() { goto_objective_card_play() } function goto_vm(proc) { game.state = "vm" game.vm = { prompt: 0, fp: proc, ip: 0, } vm_exec() } function event_prompt(str) { if (typeof str === "undefined") str = CODE[game.vm.fp][game.vm.prompt][1] if (typeof str === "function") str = str() view.prompt = card_names[game.vm.fp] + ": " + str } function vm_inst(a) { return CODE[game.vm.fp][game.vm.ip][a] } function vm_operand(a) { let x = CODE[game.vm.fp][game.vm.ip][a] if (a > 0 && typeof x === "function") return x() return x } function vm_exec() { vm_inst(0)() } function vm_next() { game.vm.ip ++ vm_exec() } function vm_asm() { vm_operand(1) vm_next() } function vm_prompt() { game.vm.prompt = game.vm.ip vm_next() } function vm_goto() { game.state = vm_operand(1) } function vm_return() { game.state = "vm_return" } states.vm_return = { inactive: "finish playing the event", prompt() { event_prompt("Done.") // TODO: or end_event ? view.actions.done = 1 }, done() { end_event() }, } function vm_if() { if (!vm_operand(1)) { let balance = 1 while (balance > 0) { ++game.vm.ip switch (vm_operand(0)) { case vm_if: ++balance break case vm_endif: --balance break case vm_else: if (balance === 1) --balance break } if (game.vm.ip < 0 || game.vm.ip > CODE[game.vm.fp].length) throw "ERROR" } } vm_next() } function vm_else() { vm_jump(vm_endif, vm_if, 1, 1) } function vm_jump(op, nop, dir, step) { let balance = 1 while (balance > 0) { game.vm.ip += dir if (vm_inst(0) === op) --balance if (vm_inst(0) === nop) ++balance if (game.vm.ip < 0 || game.vm.ip > CODE[game.vm.fp].length) throw "ERROR" } game.vm.ip += step vm_exec() } function vm_endif() { vm_next() } function vm_endswitch() { vm_next() } function vm_switch() { game.vm.choice = null game.state = "vm_switch" } function vm_case() { if (game.vm.choice === vm_operand(1)) { vm_next() } else { do ++game.vm.ip while (vm_inst(0) !== vm_case && vm_inst(0) !== vm_endswitch) vm_exec() } } function vm_player() { // TODO: pause for undo? game.active = vm_operand(1) vm_next() } function vm_ops() { goto_operations(vm_operand(1), vm_operand_spaces(2)) } function vm_increase_revolutionary_momentum() { if (game.red_momentum < 3) game.state = "vm_increase_revolutionary_momentum" else vm_next() } function vm_increase_prussian_collaboration() { if (game.blue_momentum < 3) game.state = "vm_increase_prussian_collaboration" else vm_next() } function vm_decrease_revolutionary_momentum() { if (game.red_momentum > 0) game.state = "vm_decrease_revolutionary_momentum" else vm_next() } function vm_decrease_prussian_collaboration() { if (game.blue_momentum > 0) game.state = "vm_decrease_prussian_collaboration" else vm_next() } function vm_operand_spaces(x) { let s = vm_operand(x) if (typeof s === "number") return [ s ] return s } function vm_remove_up_to() { game.vm.upto = 1 game.vm.count = vm_operand(1) game.vm.spaces = vm_operand_spaces(2) goto_vm_remove() } function vm_remove() { game.vm.count = vm_operand(1) game.vm.spaces = vm_operand_spaces(2) goto_vm_remove() } function vm_remove_own() { game.vm.spaces = vm_operand_spaces(1) game.state = "vm_remove_own" } function vm_replace_up_to() { game.vm.upto = 1 game.vm.count = vm_operand(1) game.vm.spaces = vm_operand_spaces(2) game.state = "vm_replace" goto_vm_replace() } function vm_replace_different() { game.vm.count = vm_operand(1) game.vm.spaces = vm_operand_spaces(2).slice() // make a copy for safe mutation game.state = "vm_replace_different" } function vm_replace() { game.vm.count = vm_operand(1) game.vm.spaces = vm_operand_spaces(2) goto_vm_replace() } function vm_place_removed_up_to() { game.vm.removed = 1 game.vm.upto = 1 game.vm.count = vm_operand(1) game.vm.spaces = vm_operand_spaces(2) goto_vm_place() } function vm_place_up_to() { game.vm.upto = 1 game.vm.count = vm_operand(1) game.vm.spaces = vm_operand_spaces(2) goto_vm_place() } function vm_place() { game.vm.count = vm_operand(1) game.vm.spaces = vm_operand_spaces(2) goto_vm_place() } function vm_may_place_disc() { game.vm.upto = 1 game.vm.count = 1 game.vm.spaces = vm_operand_spaces(1) game.state = "vm_place_disc" goto_vm_place_disc() } function vm_place_disc() { game.vm.count = 1 game.vm.spaces = vm_operand_spaces(1) goto_vm_place_disc() } function vm_move_up_to() { game.vm.count = vm_operand(1) game.vm.a = vm_operand_spaces(2) game.vm.b = vm_operand_spaces(3) goto_vm_move() } // === EVENT STATES === states.vm_switch = { inactive: "choose an event option", prompt() { event_prompt() for (let choice of vm_operand(1)) view.actions[choice] = 1 }, place() { push_undo() game.vm.choice = "place" vm_next() }, replace() { push_undo() game.vm.choice = "replace" vm_next() }, remove() { push_undo() game.vm.choice = "remove" vm_next() }, momentum() { push_undo() game.vm.choice = "momentum" vm_next() }, ops() { push_undo() game.vm.choice = "ops" vm_next() }, political() { push_undo() game.vm.choice = "political" vm_next() }, military() { push_undo() game.vm.choice = "military" vm_next() }, public_opinion() { push_undo() game.vm.choice = "public_opinion" vm_next() }, paris() { push_undo() game.vm.choice = "paris" vm_next() }, } states.vm_increase_revolutionary_momentum = { inactive: "increase Revolutionary Momentum", prompt() { event_prompt("Increase Revolutionary Momentum.") view.actions.red_momentum = 1 }, red_momentum() { push_undo() increase_revolutionary_momentum() }, } states.vm_increase_prussian_collaboration = { inactive: "increase Prussian Collaboration", prompt() { event_prompt("Increase Prussian Collaboration.") view.actions.blue_momentum = 1 }, blue_momentum() { push_undo() increase_prussian_collaboration() }, } states.vm_decrease_revolutionary_momentum = { inactive: "decrease Revolutionary Momentum", prompt() { event_prompt("Decrease Revolutionary Momentum.") view.actions.red_momentum = 1 }, red_momentum() { push_undo() decrease_revolutionary_momentum() }, } states.vm_decrease_prussian_collaboration = { inactive: "decrease Prussian Collaboration", prompt() { event_prompt("Decrease Prussian Collaboration.") view.actions.blue_momentum = 1 }, blue_momentum() { push_undo() decrease_prussian_collaboration() }, } function can_vm_place() { for (let s of game.vm.spaces) if (can_place_cube(s, game.vm.removed)) return true return false } function goto_vm_place() { if (can_vm_place(game.vm.removed)) game.state = "vm_place" else vm_next() } states.vm_place = { inactive: "place a cube", prompt() { event_prompt() if (game.vm.upto) view.actions.skip = 1 for (let s of game.vm.spaces) if (can_place_cube(s, game.vm.removed)) gen_action_space(s) }, space(s) { push_undo() place_cube(s, game.vm.removed) if (game.active === COMMUNE) log("Placed RC in S" + s + ".") else log("Placed BC in S" + s + ".") if (--game.vm.count === 0 || !can_vm_place(game.vm.removed)) vm_next() }, skip() { push_undo() vm_next() }, } function can_vm_place_disc() { for (let s of game.vm.spaces) if (can_place_disc(s)) return true return false } function goto_vm_place_disc() { if (can_vm_place_disc()) { if (find_available_disc() < 0) game.state = "vm_move_disc" else game.state = "vm_place_disc" } else { vm_next() } } states.vm_move_disc = { inactive: "place a disc", prompt() { event_prompt("Remove a disc to place it elsewhere.") if (game.vm.upto) view.actions.skip = 1 if (game.active === COMMUNE) for (let p = first_commune_disc; p <= last_commune_disc; ++p) gen_action_piece(p) else for (let p = first_versailles_disc; p <= last_versailles_disc; ++p) gen_action_piece(p) }, piece(p) { push_undo() if (game.active === COMMUNE) log("Moved RD from S" + s + ".") else log("Moved BD from S" + s + ".") remove_piece(p) game.state = "vm_place_disc" }, skip() { push_undo() vm_next() }, } states.vm_place_disc = { inactive: "place a disc", prompt() { event_prompt() if (game.vm.upto) view.actions.skip = 1 for (let s of game.vm.spaces) if (can_place_disc(s)) gen_action_space(s) }, space(s) { push_undo() if (game.active === COMMUNE) log("Placed RD in S" + s + ".") else log("Placed BD in S" + s + ".") place_disc(s) vm_next() }, skip() { push_undo() vm_next() }, } function can_vm_replace() { for (let s of game.vm.spaces) if (can_replace_cube(s)) return true return false } function goto_vm_replace() { if (can_vm_replace()) game.state = "vm_replace" else vm_next() } states.vm_replace = { inactive: "replace a cube", prompt() { event_prompt() if (game.vm.upto) view.actions.skip = 1 for (let s of game.vm.spaces) if (can_replace_cube(s)) for_each_enemy_cube(s, gen_action_piece) }, piece(p) { push_undo() let s = game.pieces[p] replace_cube(p) log("Replaced " + piece_abbr(p) + " in S" + s + ".") if (--game.vm.count === 0 || !can_vm_replace()) vm_next() }, skip() { push_undo() vm_next() }, } function can_vm_remove() { for (let s of game.vm.spaces) if (can_remove_cube(s)) return true return false } function goto_vm_remove() { if (can_vm_remove()) game.state = "vm_remove" else vm_next() } states.vm_remove = { inactive: "remove a cube", prompt() { event_prompt() if (game.vm.upto) view.actions.skip = 1 for (let s of game.vm.spaces) if (can_remove_cube(s)) for_each_enemy_cube(s, gen_action_piece) }, piece(p) { push_undo() let s = game.pieces[p] if (game.active === COMMUNE) log("Removed BC from S" + s + ".") else log("Removed RC from S" + s + ".") remove_piece(p) if (--game.vm.count === 0 || !can_vm_remove()) vm_next() }, skip() { push_undo() vm_next() }, } states.vm_remove_own = { inactive: "remove a cube", prompt() { event_prompt() for (let s of game.vm.spaces) for_each_friendly_cube(s, gen_action_piece) }, piece(p) { push_undo() let s = game.pieces[p] if (game.active === COMMUNE) log("Removed RC from S" + s + ".") else log("Removed BC from S" + s + ".") remove_piece(p) vm_next() }, } function can_vm_move() { let from = false let to = false for (let s of game.vm.a) if (!game.vm.b.includes(s) && has_friendly_cube(s)) from = true for (let s of game.vm.b) if (count_friendly_cubes(s) < 4) to = true return from && to } function goto_vm_move() { game.who = -1 if (can_vm_move()) game.state = "vm_move" else vm_next() } states.vm_move = { inactive: "move a cube", prompt() { event_prompt() view.actions.skip = 1 if (game.who < 0) { for (let s of game.vm.a) if (!game.vm.b.includes(s)) for_each_friendly_cube(s, gen_action_piece) } else { game.selected_cube = game.who for (let s of game.vm.b) if (count_friendly_cubes(s) < 4) gen_action_space(s) } }, piece(p) { push_undo() game.who = p }, space(s) { let from = game.pieces[game.who] move_piece(game.who, s) log("Moved " + piece_abbr(game.who) + " from S" + from + " to S" + s + ".") game.who = -1 if (--game.vm.count === 0 || !can_vm_move()) vm_next() }, skip() { push_undo() vm_next() }, } // === COMPLICATED EVENT STATES === states.reveal_commune_objective = { inactive: "look at Commune player's Objective Card", prompt() { event_prompt("Revealing Commune player's Objective Card.") view.red_objective = game.red_objective view.actions.done = 1 }, done() { vm_next() }, } states.reveal_commune_hand = { inactive: "look at Commune player's hand", prompt() { event_prompt("Revealing Commune player's hand.") view.hand = game.red_hand view.actions.done = 1 }, done() { vm_next() }, } states.general_louis_valentin = { inactive: "remove a cube", prompt() { event_prompt("Remove 1 from up to 2 different Paris spaces where present.") view.actions.skip = 1 for (let s of game.vm.spaces) if (can_remove_cube(s)) for_each_enemy_cube(s, gen_action_piece) }, piece(p) { push_undo() let s = game.pieces[p] array_remove_item(game.vm.spaces, s) log("Removed RC from S" + s + ".") remove_piece(p) if (--game.vm.count === 0 || !can_vm_remove()) vm_next() }, skip() { vm_next() }, } states.pius_ix = { inactive: "replace a cube", prompt() { event_prompt("Replace 1 in 2 different Political spaces.") for (let s of game.vm.spaces) if (can_replace_cube(s)) for_each_enemy_cube(s, gen_action_piece) }, piece(p) { push_undo() let s = game.pieces[p] array_remove_item(game.vm.spaces, s) replace_cube(p) log("Replaced " + piece_abbr(p) + " in S" + s + ".") if (--game.vm.count === 0 || !can_vm_replace()) vm_next() }, } function init_karl_marx() { clear_undo() game.vm.cards = [ draw_strategy_card(), draw_strategy_card(), draw_strategy_card(), ] } states.karl_marx_discard = { inactive: "discard a card", prompt() { event_prompt("Discard 2 cards, then play or discard the last card.") view.hand = game.vm.cards for (let c of game.vm.cards) gen_action_card(c) }, card(c) { push_undo() log("Discarded C" + c + ".") array_remove_item(game.vm.cards, c) game.discard.push(c) if (game.vm.cards.length === 1) game.state = "karl_marx_play" }, } states.karl_marx_play = { inactive: "play or discard a card", prompt() { event_prompt("Play or discard the last card.") view.hand = game.vm.cards if (can_play_event(game.vm.cards[0])) view.actions.event = 1 else view.actions.event = 0 gen_action_card(game.vm.cards[0]) }, event() { let c = game.vm.cards[0] push_undo() log("Played C" + c + ".") game.discard.push(c) goto_play_event(c) }, card(c) { push_undo() log("Discarded C" + c + ".") game.discard.push(c) vm_next() }, } function can_play_freemason_parade() { for (let s of MILITARY) if (has_commune_cube(s) && has_versailles_cube(s)) return true return false } states.freemason_parade = { inactive: "remove a cube", prompt() { event_prompt("Remove a cube from both factions in a single Military space.") for (let s of MILITARY) if (has_commune_cube(s) && has_versailles_cube(s)) gen_action_space(s) }, space(s) { game.vm.freemason_parade = s vm_next() }, } // === SETUP === exports.setup = function (seed, scenario, options) { game = { seed: seed, log: [], undo: [], active: null, state: null, round: 0, initiative: null, political_vp: 0, military_vp: 0, red_momentum: 0, blue_momentum: 0, strategy_deck: [], objective_deck: [], discard: [], red_final: 34, red_hand: [], red_set_aside: [], red_objective: [], red_fulfilled: 0, blue_final: 17, blue_hand: [], blue_set_aside: [], blue_objective: [], blue_fulfilled: 0, 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 OUT_OF_PLAY, OUT_OF_PLAY, // Versailles discs OUT_OF_PLAY, OUT_OF_PLAY, ], count: 0, spaces: null, who: -1, } log_h1("Red Flag Over Paris") if (options.censorship) { log("Censorship Phase Variant.") game.censorship = 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) start_round() return game } function draw_strategy_card() { if (game.strategy_deck.length === 0) { log("Reshuffled.") game.strategy_deck = game.discard game.discard = [] shuffle(game.strategy_deck) } return game.strategy_deck.pop() } // === 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, red_final: game.red_final, blue_final: game.blue_final, red_objective: 0, blue_objective: 0, pieces: game.pieces, discard: top_discard(), hand: 0, final: 0, set_aside: 0, } if (game.censorship) view.censorship = 1 if (typeof game.red_objective === "object") view.red_objective = 2 else if (game.red_objective > 0) view.red_objective = 1 if (typeof game.blue_objective === "object") view.blue_objective = 2 else if (game.blue_objective > 0) view.blue_objective = 1 if (player === COMMUNE) { view.hand = game.red_hand view.set_aside = game.red_set_aside view.red_objective = game.red_objective } if (player === VERSAILLES) { view.hand = game.blue_hand view.set_aside = game.blue_set_aside view.blue_objective = game.blue_objective } if (game.reveal_objectives) { view.red_objective = game.red_objective view.blue_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 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 (player === COMMUNE) goto_game_over(VERSAILLES, "Commune resigned.") if (player === VERSAILLES) goto_game_over(COMMUNE, "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] = [] view.actions[action].push(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() } function log_h3(msg) { log_br() log(".h3 " + msg) } function log_sep() { log(".hr") } // === 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 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 } } // 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 } // === GENERATED EVENT CODE === const CODE = [] CODE[1] = [ // Jules Ducatel [ vm_asm, ()=>clear_undo() ], [ vm_if, ()=>(game.round < 4) ], [ vm_asm, ()=>log("Commune Objective:") ], [ vm_asm, ()=>logi("C" + game.red_objective) ], [ vm_goto, "reveal_commune_objective" ], [ vm_else ], [ vm_asm, ()=>log("Commune hand:") ], [ vm_asm, ()=>{ for (let c of game.red_hand) logi("C" + c) } ], [ vm_goto, "reveal_commune_hand" ], [ vm_endif ], [ vm_ops, 1, MILITARY ], [ vm_return ], ] CODE[2] = [ // The Murder of Vincenzini [ vm_increase_revolutionary_momentum ], [ vm_prompt, "Remove up to 3 from Political." ], [ vm_remove_up_to, 3, POLITICAL ], [ vm_return ], ] CODE[3] = [ // Brassardiers [ vm_prompt, "Replace 1 in Military where present." ], [ vm_replace, 1, ()=>(where_present(MILITARY)) ], [ vm_return ], ] CODE[4] = [ // Jules Ferry [ vm_prompt, "Place up to 2 in Republicans or replace 1 in National Assembly." ], [ vm_switch, ["place","replace"] ], [ vm_case, "place" ], [ vm_prompt, "Place up to 2 in Republicans." ], [ vm_place_up_to, 2, REPUBLICANS ], [ vm_case, "replace" ], [ vm_prompt, "Replace 1 in National Assembly." ], [ vm_replace, 1, NATIONAL_ASSEMBLY ], [ vm_endswitch ], [ vm_return ], ] CODE[5] = [ // Le Figaro [ vm_prompt, "Place up to 2 in Press or replace 1 in Public Opinion." ], [ vm_switch, ["place","replace"] ], [ vm_case, "place" ], [ vm_prompt, "Place up to 2 in Press." ], [ vm_place_up_to, 2, PRESS ], [ vm_case, "replace" ], [ vm_prompt, "Replace 1 in Public Opinion." ], [ vm_replace, 1, PUBLIC_OPINION ], [ vm_endswitch ], [ vm_return ], ] CODE[6] = [ // Général Louis Valentin [ vm_asm, ()=>game.vm.count = 2 ], [ vm_asm, ()=>game.vm.spaces = where_present(PARIS) ], [ vm_if, ()=>(can_vm_remove()) ], [ vm_goto, "general_louis_valentin" ], [ vm_endif ], [ vm_return ], ] CODE[7] = [ // Général Espivent [ vm_prompt, "Remove up to 2 from Social Movements." ], [ vm_remove_up_to, 2, SOCIAL_MOVEMENTS ], [ vm_return ], ] CODE[8] = [ // Les Amis de l'Ordre [ vm_prompt, "Place 2 in Paris." ], [ vm_place, 2, PARIS ], [ vm_return ], ] CODE[9] = [ // Socialist Newspaper Ban [ vm_increase_revolutionary_momentum ], [ vm_prompt, "Remove up to 2 from Press." ], [ vm_remove_up_to, 2, PRESS ], [ vm_asm, ()=>game.snb=1 ], [ vm_return ], ] CODE[10] = [ // Fortification of Mont-Valérien [ vm_prompt, "Place up to 1 in Mont-Valérien." ], [ vm_place_up_to, 1, MONT_VALERIEN ], [ vm_asm, ()=>update_presence_and_control() ], [ vm_if, ()=>(is_control(MONT_VALERIEN)) ], [ vm_prompt, "You may place a Fortification in Mont-Valérien." ], [ vm_may_place_disc, MONT_VALERIEN ], [ vm_endif ], [ vm_return ], ] CODE[11] = [ // Adolphe Thiers [ vm_increase_prussian_collaboration ], [ vm_prompt, "Place up to 2 in Royalists." ], [ vm_place_up_to, 2, ROYALISTS ], [ vm_return ], ] CODE[12] = [ // Otto von Bismarck [ vm_prompt, "Remove 1 from National Assembly." ], [ vm_remove_own, NATIONAL_ASSEMBLY ], [ vm_increase_prussian_collaboration ], [ vm_ops, 2, MILITARY ], [ vm_return ], ] CODE[13] = [ // Général Ernest de Cissey [ vm_ops, 4, FORT_D_ISSY ], [ vm_return ], ] CODE[14] = [ // Colonel de Lochner [ vm_if, ()=>(is_versailles_control(MONT_VALERIEN)) ], [ vm_ops, 3, BUTTE_MONTMARTRE ], [ vm_else ], [ vm_ops, 3, MONT_VALERIEN ], [ vm_endif ], [ vm_return ], ] CODE[15] = [ // Jules Favre [ vm_increase_prussian_collaboration ], [ vm_prompt, "Place up to 1 in Republicans" ], [ vm_place_up_to, 1, REPUBLICANS ], [ vm_return ], ] CODE[16] = [ // Hostage Decree [ vm_prompt, "Remove all from Catholic Church." ], [ vm_remove, 20, CATHOLIC_CHURCH ], [ vm_return ], ] CODE[17] = [ // Maréchal Macmahon [ vm_ops, 4, PARIS ], [ vm_return ], ] CODE[18] = [ // Paule Minck [ vm_prompt, "Replace 1 in Military where present." ], [ vm_replace, 1, ()=>(where_present(MILITARY)) ], [ vm_return ], ] CODE[19] = [ // Walery Wroblewski [ vm_prompt, "Place one Barricade in Paris where present." ], [ vm_place_disc, ()=>(where_present(PARIS)) ], [ vm_return ], ] CODE[20] = [ // Banque de France [ vm_decrease_revolutionary_momentum ], [ vm_prompt, "Place up to 1 in Paris." ], [ vm_place_up_to, 1, PARIS ], [ vm_prompt, "You may place a Barricade in Paris." ], [ vm_may_place_disc, PARIS ], [ vm_return ], ] CODE[21] = [ // Le Réveil [ vm_increase_revolutionary_momentum ], [ vm_prompt, "Place up to 1 in Press." ], [ vm_place_up_to, 1, PRESS ], [ vm_return ], ] CODE[22] = [ // Execution of Generals [ vm_increase_prussian_collaboration ], [ vm_prompt, "Replace up to 2 in Paris where present." ], [ vm_replace_up_to, 2, ()=>(where_present(PARIS)) ], [ vm_return ], ] CODE[23] = [ // Les Cantinières [ vm_prompt, "Place up to 2 in Military where present (use removed cubes)." ], [ vm_place_removed_up_to, 2, ()=>(where_present(MILITARY)) ], [ vm_return ], ] CODE[24] = [ // Eugène Protot [ vm_ops, 3, INSTITUTIONAL ], [ vm_return ], ] CODE[25] = [ // Paul Cluseret [ vm_ops, 2, PARIS ], [ vm_return ], ] CODE[26] = [ // Gaston Crémieux [ vm_increase_revolutionary_momentum ], [ vm_place_up_to, 2, SOCIAL_MOVEMENTS ], [ vm_return ], ] CODE[27] = [ // Louise Michel [ vm_increase_revolutionary_momentum ], [ vm_prompt, "Use 1 OP in Public Opinion or Paris." ], [ vm_switch, ["public_opinion","paris"] ], [ vm_case, "public_opinion" ], [ vm_ops, 1, PUBLIC_OPINION ], [ vm_case, "paris" ], [ vm_ops, 1, PARIS ], [ vm_endswitch ], [ vm_return ], ] CODE[28] = [ // Jaroslav Dombrowski [ vm_prompt, "Place up to 1 in Paris." ], [ vm_place_up_to, 1, PARIS ], [ vm_prompt, "You may place a Barricade in Paris." ], [ vm_may_place_disc, PARIS ], [ vm_return ], ] CODE[29] = [ // Raoul Rigault [ vm_increase_revolutionary_momentum ], [ vm_prompt, "Move up to 2 from Catholic Church to Paris." ], [ vm_move_up_to, 2, CATHOLIC_CHURCH, PARIS ], [ vm_return ], ] CODE[30] = [ // Karl Marx [ vm_asm, ()=>init_karl_marx() ], [ vm_goto, "karl_marx_discard" ], [ vm_return ], ] CODE[31] = [ // Blanquists [ vm_increase_revolutionary_momentum ], [ vm_ops, 2, MILITARY ], [ vm_return ], ] CODE[32] = [ // Général Lullier [ vm_ops, 4, CHATEAU_DE_VINCENNES ], [ vm_return ], ] CODE[33] = [ // Jules Vallès [ vm_increase_revolutionary_momentum ], [ vm_ops, 2, PUBLIC_OPINION ], [ vm_return ], ] CODE[34] = [ // Charles Delescluze [ vm_ops, 3, MILITARY ], [ vm_ops, 1, POLITICAL ], [ vm_return ], ] CODE[35] = [ // Conciliation [ vm_prompt, "Move up to 2 from Public Opinion or Paris." ], [ vm_switch, ["public_opinion","paris"] ], [ vm_case, "public_opinion" ], [ vm_prompt, "Move up to 2 from Public Opinion to Paris." ], [ vm_move_up_to, 2, PUBLIC_OPINION, PARIS ], [ vm_case, "paris" ], [ vm_prompt, "Move up to 2 from Paris to Public Opinion." ], [ vm_move_up_to, 2, PARIS, PUBLIC_OPINION ], [ vm_endswitch ], [ vm_return ], ] CODE[36] = [ // Georges Clemenceau [ vm_prompt, "Move up to 3 from Paris to Institutional." ], [ vm_move_up_to, 3, PARIS, INSTITUTIONAL ], [ vm_return ], ] CODE[37] = [ // Archbishop Georges Darboy [ vm_prompt, "Move up to 4 from any spaces to Catholic Church." ], [ vm_move_up_to, 4, ANY, CATHOLIC_CHURCH ], [ vm_return ], ] CODE[38] = [ // Victor Hugo [ vm_decrease_revolutionary_momentum ], [ vm_decrease_prussian_collaboration ], [ vm_ops, 4, PUBLIC_OPINION ], [ vm_return ], ] CODE[39] = [ // Léon Gambetta [ vm_prompt, "Replace up to 1 in Institutional." ], [ vm_replace_up_to, 1, INSTITUTIONAL ], [ vm_if, ()=>(game.active === COMMUNE) ], [ vm_decrease_prussian_collaboration ], [ vm_else ], [ vm_decrease_revolutionary_momentum ], [ vm_endif ], [ vm_return ], ] CODE[40] = [ // Elihu Washburne [ vm_if, ()=>(game.active === COMMUNE) ], [ vm_increase_revolutionary_momentum ], [ vm_else ], [ vm_increase_prussian_collaboration ], [ vm_endif ], [ vm_ops, 2, INSTITUTIONAL ], [ vm_return ], ] CODE[41] = [ // Freemason Parade [ vm_goto, "freemason_parade" ], [ vm_prompt, "Remove a cube from both factions." ], [ vm_remove_own, ()=>(game.vm.freemason_parade) ], [ vm_remove, 1, ()=>(game.vm.freemason_parade) ], [ vm_prompt, "Place up to 2 or replace 1 in Institutional." ], [ vm_switch, ["place","replace"] ], [ vm_case, "place" ], [ vm_prompt, "Place up to 2 in Institutional." ], [ vm_place_up_to, 2, INSTITUTIONAL ], [ vm_case, "replace" ], [ vm_prompt, "Replace 1 in Institutional." ], [ vm_replace, 1, INSTITUTIONAL ], [ vm_endswitch ], [ vm_return ], ] CODE[42] = [ // Paris Cannons [ vm_if, ()=>(can_advance_momentum()) ], [ vm_prompt, "Use 3 OP in Paris or increase your Player Momentum." ], [ vm_switch, ["momentum","ops"] ], [ vm_case, "momentum" ], [ vm_if, ()=>(game.active === COMMUNE) ], [ vm_increase_revolutionary_momentum ], [ vm_else ], [ vm_increase_prussian_collaboration ], [ vm_endif ], [ vm_case, "ops" ], [ vm_ops, 3, PARIS ], [ vm_endswitch ], [ vm_else ], [ vm_ops, 3, PARIS ], [ vm_endif ], [ vm_return ], ] CODE[43] = [ // Aux Barricades! [ vm_ops, 2, PARIS ], [ vm_player, COMMUNE ], [ vm_prompt, "You may place a Barricade in Butte-aux-Cailles." ], [ vm_may_place_disc, BUTTE_AUX_CAILLES ], [ vm_return ], ] CODE[44] = [ // Commune's Stronghold [ vm_prompt, "Use 2 OP in Military or remove 1 from any space." ], [ vm_switch, ["ops","remove"] ], [ vm_case, "ops" ], [ vm_ops, 2, MILITARY ], [ vm_case, "remove" ], [ vm_prompt, "Remove 1 from any space." ], [ vm_remove, 1, ANY ], [ vm_endswitch ], [ vm_return ], ] CODE[45] = [ // Fighting in Issy Village [ vm_ops, 2, FORTS ], [ vm_player, VERSAILLES ], [ vm_prompt, "You may place a Fortification in Fort d'Issy." ], [ vm_may_place_disc, FORT_D_ISSY ], [ vm_return ], ] CODE[46] = [ // Battle of Mont-Valérien [ vm_ops, 3, FORTS ], [ vm_if, ()=>(game.active === COMMUNE) ], [ vm_decrease_prussian_collaboration ], [ vm_else ], [ vm_increase_prussian_collaboration ], [ vm_endif ], [ vm_return ], ] CODE[47] = [ // Raid on Château de Vincennes [ vm_prompt, "Use 2 OP in Military or remove 1 from any space." ], [ vm_switch, ["ops","remove"] ], [ vm_case, "ops" ], [ vm_ops, 2, MILITARY ], [ vm_case, "remove" ], [ vm_remove, 1, ANY ], [ vm_endswitch ], [ vm_return ], ] CODE[48] = [ // Revolution in the Press [ vm_prompt, "Use 3 OP in Public Opinion or replace 1 in any space." ], [ vm_switch, ["ops","replace"] ], [ vm_case, "ops" ], [ vm_ops, 3, PUBLIC_OPINION ], [ vm_case, "replace" ], [ vm_replace, 1, ANY ], [ vm_endswitch ], [ vm_return ], ] CODE[49] = [ // Pius IX [ vm_prompt, "Replace in 2 different Political or remove 1 from any space." ], [ vm_switch, ["replace","remove"] ], [ vm_case, "replace" ], [ vm_asm, ()=>game.vm.count = 2 ], [ vm_asm, ()=>game.vm.spaces = POLITICAL.slice() ], [ vm_if, ()=>(can_vm_replace()) ], [ vm_goto, "pius_ix" ], [ vm_endif ], [ vm_endif ], [ vm_case, "remove" ], [ vm_remove, 1, ANY ], [ vm_endswitch ], [ vm_return ], ] CODE[50] = [ // Socialist International [ vm_ops, 2, PUBLIC_OPINION ], [ vm_if, ()=>(game.active === COMMUNE) ], [ vm_increase_revolutionary_momentum ], [ vm_else ], [ vm_decrease_revolutionary_momentum ], [ vm_endif ], [ vm_return ], ] CODE[51] = [ // Royalists Dissension [ vm_prompt, "Use 2 OP in Institutional or remove 1 from any space." ], [ vm_switch, ["ops","remove"] ], [ vm_case, "ops" ], [ vm_ops, 2, INSTITUTIONAL ], [ vm_case, "remove" ], [ vm_remove, 1, ANY ], [ vm_endswitch ], [ vm_return ], ] CODE[52] = [ // Rise of Republicanism [ vm_prompt, "Use 3 OP in Institutional or replace 1 in Military." ], [ vm_switch, ["ops","replace"] ], [ vm_case, "ops" ], [ vm_ops, 3, INSTITUTIONAL ], [ vm_case, "replace" ], [ vm_replace, 1, MILITARY ], [ vm_endswitch ], [ vm_return ], ] CODE[53] = [ // Legitimacy [ vm_ops, 3, POLITICAL ], [ vm_if, ()=>(game.active === COMMUNE) ], [ vm_increase_revolutionary_momentum ], [ vm_else ], [ vm_increase_prussian_collaboration ], [ vm_endif ], [ vm_return ], ]