diff options
author | Tor Andersson <tor@ccxvii.net> | 2023-05-24 21:01:14 +0200 |
---|---|---|
committer | Tor Andersson <tor@ccxvii.net> | 2023-05-24 21:06:17 +0200 |
commit | 1f96c7a95ed557712c259fa09bab9e4a2e3f17d7 (patch) | |
tree | 8068a4f310de878a8687cbcb4dea64a61d8321e0 /rules.js | |
parent | 0c673db43e6a6872ffc3ecf4db42b168ad375c70 (diff) | |
download | red-flag-over-paris-1f96c7a95ed557712c259fa09bab9e4a2e3f17d7.tar.gz |
Zoot zoot!
# Conflicts:
# tools/boxes.svg
# tools/genboxes.py
Diffstat (limited to 'rules.js')
-rw-r--r-- | rules.js | 1605 |
1 files changed, 1287 insertions, 318 deletions
@@ -1,272 +1,612 @@ "use strict" -const data = require("./data") - -const RED_CUBE_POOL = 0 -const RED_CRISIS_TRACK = [1, 2, 3, 4] -const RED_BONUS_CUBES = [ 5, 6, 7 ] -const BLUE_CUBE_POOL = 8 -const BLUE_CRISIS_TRACK = [9, 10, 11, 12] -const BLUE_BONUS_CUBES = [ 13, 14, 15 ] -const S_ROYALISTS = 16 -const S_NATIONAL_ASSEMBLY = 17 -const S_REPUBLICANS = 18 -const S_CATHOLIC_CHURCH = 19 -const S_PRESS = 20 -const S_SOCIAL_MOVEMENTS = 21 -const S_MONT_VALERIEN = 22 -const S_BUTTE_MONTMARTRE = 23 -const S_FORT_D_ISSY = 24 -const S_BUTTE_AUX_CAILLES = 25 -const S_PERE_LACHAISE = 26 -const S_CHATEAU_DE_VINCENNES = 27 -const S_PRUSSIAN_OCCUPIED_TERRITORY = 28 -const S_VERSAILLES_HQ = 29 - -var game, view - -var states = {} +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 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 S_NATIONAL_ASSEMBLY = 0 +const S_ROYALISTS = 1 +const S_REPUBLICANS = 2 + +const S_PRESS = 3 +const S_CATHOLIC_CHURCH = 4 +const S_SOCIAL_MOVEMENTS = 5 + +const S_MONT_VALERIEN = 6 +const S_FORT_D_ISSY = 7 +const S_CHATEAU_DE_VINCENNES = 8 + +const S_BUTTE_MONTMARTRE = 9 +const S_BUTTE_AUX_CAILLES = 10 +const S_PERE_LACHAISE = 11 + +const S_PRUSSIAN_OCCUPIED_TERRITORY = 12 +const S_VERSAILLES_HQ = 13 + +const first_space = S_NATIONAL_ASSEMBLY +const last_space = S_PERE_LACHAISE +const space_count = last_space + 1 + +const POLITICAL = [ + S_NATIONAL_ASSEMBLY, + S_ROYALISTS, + S_REPUBLICANS, + S_PRESS, + S_CATHOLIC_CHURCH, + S_SOCIAL_MOVEMENTS, +] + +const MILITARY = [ + S_MONT_VALERIEN, + S_FORT_D_ISSY, + S_CHATEAU_DE_VINCENNES, + S_BUTTE_MONTMARTRE, + S_BUTTE_AUX_CAILLES, + S_PERE_LACHAISE, +] + +function is_political_space(s) { + return ( + s === S_NATIONAL_ASSEMBLY || + s === S_ROYALISTS || + s === S_REPUBLICANS || + s === S_PRESS || + s === S_CATHOLIC_CHURCH || + s === S_SOCIAL_MOVEMENTS + ) +} -exports.roles = [ "Commune", "Versailles" ] +function is_military_space(s) { + return ( + s === S_MONT_VALERIEN || + s === S_FORT_D_ISSY || + s === S_CHATEAU_DE_VINCENNES || + s === S_BUTTE_MONTMARTRE || + s === S_BUTTE_AUX_CAILLES || + s === S_PERE_LACHAISE + ) +} -exports.action = function (state, current, action, arg) { - game = state - if (action in states[game.state]) { - states[game.state][action](arg, current) - } else { - if (action === "undo" && game.undo && game.undo.length > 0) - pop_undo() - else - throw new Error("Invalid action: " + action) - } - return game +const DIM_INSTITUTIONAL = [ S_NATIONAL_ASSEMBLY, S_ROYALISTS, S_REPUBLICANS ] +const DIM_PUBLIC_OPINION = [ S_PRESS, S_CATHOLIC_CHURCH, S_SOCIAL_MOVEMENTS ] + +const DIM_FORTS = [ S_MONT_VALERIEN, S_FORT_D_ISSY, S_CHATEAU_DE_VINCENNES ] +const DIM_PARIS = [ S_BUTTE_MONTMARTRE, S_BUTTE_AUX_CAILLES, S_PERE_LACHAISE ] + +const ADJACENT_TO = [ + [ S_ROYALISTS, S_REPUBLICANS ], + [ S_PRESS, S_CATHOLIC_CHURCH ], + [ S_PRESS, S_SOCIAL_MOVEMENTS ], + [ S_ROYALISTS, S_REPUBLICANS, S_CATHOLIC_CHURCH, S_SOCIAL_MOVEMENTS ], + [ S_ROYALISTS, S_PRESS ], + [ S_REPUBLICANS, S_PRESS ], + [ S_BUTTE_MONTMARTRE, S_VERSAILLES_HQ ], + [ S_CHATEAU_DE_VINCENNES, S_BUTTE_AUX_CAILLES, S_VERSAILLES_HQ ], + [ S_FORT_D_ISSY, S_PERE_LACHAISE, S_PRUSSIAN_OCCUPIED_TERRITORY ], + [ S_MONT_VALERIEN, S_BUTTE_AUX_CAILLES, S_PERE_LACHAISE ], + [ S_FORT_D_ISSY, S_BUTTE_MONTMARTRE, S_PERE_LACHAISE ], + [ S_CHATEAU_DE_VINCENNES, S_BUTTE_MONTMARTRE, S_BUTTE_AUX_CAILLES, S_PRUSSIAN_OCCUPIED_TERRITORY ], + [ S_CHATEAU_DE_VINCENNES, S_PERE_LACHAISE ], + [ S_MONT_VALERIEN, S_FORT_D_ISSY ], +] + +const ADJACENT_FROM = [ + [ ], + [ S_NATIONAL_ASSEMBLY, S_PRESS, S_CATHOLIC_CHURCH ], + [ S_NATIONAL_ASSEMBLY, S_PRESS, S_SOCIAL_MOVEMENTS ], + [ S_ROYALISTS, S_REPUBLICANS, S_CATHOLIC_CHURCH, S_SOCIAL_MOVEMENTS ], + [ S_ROYALISTS, S_PRESS ], + [ S_REPUBLICANS, S_PRESS ], + [ S_BUTTE_MONTMARTRE, S_VERSAILLES_HQ ], + [ S_CHATEAU_DE_VINCENNES, S_BUTTE_AUX_CAILLES, S_VERSAILLES_HQ ], + [ S_FORT_D_ISSY, S_PERE_LACHAISE, S_PRUSSIAN_OCCUPIED_TERRITORY ], + [ S_MONT_VALERIEN, S_BUTTE_AUX_CAILLES, S_PERE_LACHAISE ], + [ S_FORT_D_ISSY, S_BUTTE_MONTMARTRE, S_PERE_LACHAISE ], + [ S_CHATEAU_DE_VINCENNES, S_BUTTE_MONTMARTRE, S_BUTTE_AUX_CAILLES, S_PRUSSIAN_OCCUPIED_TERRITORY ], + [ S_CHATEAU_DE_VINCENNES, S_PERE_LACHAISE ], + [ S_MONT_VALERIEN, S_FORT_D_ISSY ], +] + +// === GAME STATE === + +function is_objective_card(c) { + return c >= 42 && c <= 53 } -exports.resign = function (state, current) { - game = state - if (game.state !== "game_over") { - log_br() - log(`${current} resigned.`) - game.state = "game_over" - game.active = null - game.state = "game_over" - game.result = (current === "Commune" ? "Versailles" : "Commune") - game.victory = current + " resigned." - } - return game +function is_strategy_card(c) { + return !is_objective_card(c) && c !== 17 && c !== 34 } -exports.is_checkpoint = function (a, b) { - return a.round !== b.round +function is_commune_card(c) { + return c >= 18 && c <= 34 } -exports.view = function(state, current) { - game = state +function is_versailles_card(c) { + return c >= 1 && c <= 17 +} - view = { - log: game.log, - prompt: null, - actions: null, +function is_neutral_card(c) { + return c >= 35 && c <= 41 +} - round: game.round, - initiative: game.initiative, - political_vp: game.political_vp, - military_vp: game.military_vp, +function enemy_player() { + if (game.active === COMMUNE) + return VERSAILLES + return COMMUNE +} - red_hand: game.red_hand.length, - blue_hand: game.blue_hand.length, - red_momentum: game.red_momentum, - blue_momentum: game.blue_momentum, +function player_hand(current) { + if (current === COMMUNE) + return game.red_hand + return game.blue_hand +} - discs: game.discs, - cubes: game.cubes, +function is_space(s) { + return s >= first_space && s <= last_space +} - active_card: game.active_card, - hand: 0, - objective: 0 +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 } - if (current === "Commune") { - view.hand = game.red_hand - view.objective = game.red_objective + for (let p = first_versailles_cube; p <= last_versailles_cube; ++p) { + let s = game.pieces[p] + if (is_space(s)) + v_count[s] += 1 } - if (current === "Versailles") { - view.hand = game.blue_hand - view.objective = game.blue_objective + + for (let p = first_commune_disc; p <= last_commune_disc; ++p) { + let s = game.pieces[p] + if (is_space(s)) + c_count[s] += 1 } - if (game.state === "game_over") { - view.prompt = game.victory - } else if (current === "Observer" || (game.active !== current && 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(current) - 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 - } + 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 << (S_PERE_LACHAISE)) + game.presence |= (1 << (S_SOCIAL_MOVEMENTS)) + game.presence |= (1 << (S_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 } - return view } -exports.setup = function (seed, scenario, options) { - game = { - seed: seed, - log: [], - undo: [], - active: "Both", - state: "choose_objective_card", +function is_present(side, s) { + if (side === COMMUNE) + return game.presence & (1 << (s)) + return game.presence & (1 << (s + space_count)) +} - round: 1, - initiative: null, - political_vp: 0, - military_vp: 0, - red_momentum: 0, - blue_momentum: 0, +function is_commune_control(s) { + if (s === S_VERSAILLES_HQ) + return false + if (s === S_PRUSSIAN_OCCUPIED_TERRITORY) + return false + return game.control & (1 << (s)) +} - strategy_deck: [], - objective_deck: [], +function is_versailles_control(s) { + if (s === S_VERSAILLES_HQ) + return true + if (s === S_PRUSSIAN_OCCUPIED_TERRITORY) + return game.blue_momentum === 3 + return game.control & (1 << (s + space_count)) +} - red_hand: [ 34 ], - red_objective: 0, - blue_hand: [ 17 ], - blue_objective: 0, +function is_control(side, s) { + if (side === COMMUNE) + return is_commune_control(s) + return is_versailles_control(s) +} - cubes: [ - // red 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], - S_PRESS, - S_SOCIAL_MOVEMENTS, - S_PERE_LACHAISE, +function is_adjacent_to_control(side, here) { + for (let s of ADJACENT_TO[here]) + if (is_control(side, s)) + return true + return false +} - // blue 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], - BLUE_CUBE_POOL, - BLUE_CUBE_POOL, - BLUE_CUBE_POOL, - BLUE_CUBE_POOL, - BLUE_CUBE_POOL, - BLUE_CUBE_POOL, - S_ROYALISTS, - S_PRESS, - ], +function is_control_dimension(side, dim) { + for (let s of dim) + if (!is_control(side, s)) + return false + return true +} - discs: [ 0, 0, 0, 0 ], +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 +} - active_card: 0, - count: 0, - } +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 +} - log_h1("Red Flag Over Paris") - log_h1("Round 1") +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 +} - for (let i = 1; i <= 41; ++i) - if (i !== 17 && i !== 34) - game.strategy_deck.push(i) +function count_commune_pieces(s) { + return count_commune_cubes(s) + count_commune_discs(s) +} - for (let i = 42; i <= 53; ++i) - game.objective_deck.push(i) +function count_versailles_pieces(s) { + return count_versailles_cubes(s) + count_versailles_discs(s) +} - shuffle(game.strategy_deck) - shuffle(game.objective_deck) +function count_enemy_pieces(s) { + if (game.active === COMMUNE) + return count_versailles_pieces(s) + return count_commune_pieces(s) +} - for (let i = 0; i < 4; ++i) { - game.red_hand.push(game.strategy_deck.pop()) - game.blue_hand.push(game.strategy_deck.pop()) +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)) { + game.pieces[p] = RED_CUBE_POOL[0] // TODO... + } else if (is_versailles_cube(p)) { + game.pieces[p] = BLUE_CUBE_POOL + } else { + game.pieces[p] = -1 } +} - for (let i = 0; i < 2; ++i) { - game.red_hand.push(game.objective_deck.pop()) - game.blue_hand.push(game.objective_deck.pop()) +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 game +function for_each_enemy_cube(s, f) { + if (game.active === COMMUNE) + for_each_versailles_cube(s, f) + else + for_each_commune_cube(s, f) } -// === GAME STATES === +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 is_objective_card(c) { - return c >= 42 && c <= 53 +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 is_strategy_card(c) { - return !is_objective_card(c) && c !== 17 && c !== 34 +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 enemy_player() { - if (game.active === "Commune") - return "Versailles" - return "Commune" +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 player_hand(current) { - if (current === "Commune") - return game.red_hand - return game.blue_hand +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 can_play_event(c) { - let side = data.cards[c].side - if (side === game.active || side === "Neutral") - return true - return false +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 can_advance_momentum() { - if (game.active === "Commune") - return game.red_momentum < 3 - return game.blue_momentum < 3 +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) } +// === OBJECTIVE PHASE === + states.choose_objective_card = { inactive: "choose an objective card", prompt(current) { view.prompt = "Choose an Objective card." - for (let c of player_hand(current)) { - if (is_objective_card(c)) { - gen_action("card", c) - } - } + for (let c of player_hand(current)) + if (is_objective_card(c)) + gen_action_card(c) }, card(c, current) { - if (current === "Commune") { + if (current === COMMUNE) { game.red_objective = c game.red_hand = game.red_hand.filter(c => !is_objective_card(c)) } else { @@ -276,16 +616,31 @@ states.choose_objective_card = { if (game.red_objective > 0 && game.blue_objective > 0) goto_initiative_phase() else if (game.red_objective > 0) - game.active = "Versailles" + game.active = VERSAILLES else if (game.blue_objective > 0) - game.active = "Commune" + game.active = COMMUNE else game.active = "Both" }, } +// === INITIATIVE PHASE === + +function commune_political_vp() { + return game.political_vp +} + +function versailles_political_vp() { + return -game.political_vp +} + function goto_initiative_phase() { - game.active = "Commune" + 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" } @@ -298,18 +653,20 @@ states.initiative_phase = { }, commune() { log("Initiative: Commune") - game.initiative = "Commune" + game.initiative = COMMUNE game.active = game.initiative goto_strategy_phase() }, versailles() { log("Initiative: Versailles") - game.initiative = "Versailles" + game.initiative = VERSAILLES game.active = game.initiative goto_strategy_phase() }, } +// === STRATEGY PHASE === + function goto_strategy_phase() { clear_undo() log_h2(game.active) @@ -325,40 +682,61 @@ states.strategy_phase = { inactive: "play a card", prompt() { view.prompt = "Play a card." - for (let c of player_hand(game.active)) { - console.log(game.active, "hand", c) + let hand = player_hand(game.active) + let n_strategy = 0 + for (let c of hand) + if (is_strategy_card(c)) + n_strategy += 1 + for (let c of hand) { if (is_strategy_card(c)) { if (can_play_event(c)) gen_action("card_event", c) - gen_action("card_ops", c) - if (game.active_card > 0 && can_play_event(game.active_card)) + gen_action("card_ops_political", c) + gen_action("card_ops_military", c) + if (game.discard > 0 && can_play_event(game.discard)) gen_action("card_use_discarded", c) if (can_advance_momentum()) gen_action("card_advance_momentum", c) } + if (c === 17 || c === 34) { + if (n_strategy > 0) { + gen_action("card_ops_political", c) + gen_action("card_ops_military", c) + } + } } }, card_event(c) { push_undo() log(`Played #${c} for event.`) array_remove_item(player_hand(game.active), c) - game.active_card = c + game.discard = c goto_play_event() }, - card_ops(c) { + card_ops_political(c) { + push_undo() + if (c === 17 || c === 34) + return goto_final_crisis_discard(c, POLITICAL) + log(`Played #${c} for ${card_ops[c]} Political ops.`) + array_remove_item(player_hand(game.active), c) + game.discard = c + goto_operations(card_ops[c], POLITICAL) + }, + card_ops_military(c) { push_undo() - log(`Played #${c} for ${data.cards[c].ops} ops.`) + if (c === 17 || c === 34) + return goto_final_crisis_discard(c, MILITARY) + log(`Played #${c} for ${card_ops[c]} Military ops.`) array_remove_item(player_hand(game.active), c) - game.active_card = c - game.count = data.cards[c].ops - game.state = "operations" + game.discard = c + goto_operations(card_ops[c], MILITARY) }, card_advance_momentum(c) { push_undo() log(`Played #${c} to advance momentum.`) array_remove_item(player_hand(game.active), c) - game.active_card = c - if (game.active === "Commune") + game.discard = c + if (game.active === COMMUNE) game.red_momentum += 1 else game.blue_momentum += 1 @@ -367,14 +745,265 @@ states.strategy_phase = { }, card_use_discarded(c) { push_undo() - log(`Discarded #${c} to play #${game.active_card}.`) - let old_c = game.active_card + log(`Discarded #${c} to play #${game.discard}.`) + let old_c = game.discard array_remove_item(player_hand(game.active), c) - game.active_card = c + game.discard = c goto_play_event(old_c) }, } +// === OPERATIONS === + +function goto_operations(count, spaces) { + game.count = count + game.spaces = spaces + goto_operations_remove() +} + +function goto_final_crisis_discard(c, spaces) { + game.state = "discard_final_crisis" + game.count = 4 + game.spaces = spaces + array_remove_item(player_hand(game.active), c) +} + +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(game.active) + for (let c of hand) + if (is_strategy_card(c)) + gen_action("card", c) + }, + card(c) { + push_undo() + log(`Discarded #${c} to play Final Crisis card for ops.`) + array_remove_item(player_hand(game.active), 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(game.active, s) || is_adjacent_to_control(game.active, 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(game.active, next)) + str += 1 + if (is_present(game.active, s)) + str += 1 + if (is_control(game.active, 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(game.active, s) || is_adjacent_to_control(game.active, s)) { + let d = has_enemy_disc(s) + if (!d || game.count >= 2) + return true + } + return false +} + +states.operations_place = { + prompt() { + view.prompt = "Operations: Place cubes into Political spaces." + 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() +} + +// === EVENTS === + function goto_play_event(c) { switch (c) { // TODO @@ -382,16 +1011,299 @@ function goto_play_event(c) { resume_strategy_phase() } -// === COMMON LIBRARY === -function gen_action(action, argument) { - if (argument !== undefined) { - if (!(action in view.actions)) - view.actions[action] = [] - set_add(view.actions[action], argument) +// === SETUP === + +exports.setup = function (seed, scenario, options) { + game = { + seed: seed, + 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_hand: [ 34 ], + red_objective: 0, + + blue_hand: [ 17 ], + blue_objective: 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], + S_PRESS, + S_SOCIAL_MOVEMENTS, + S_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], + BLUE_CUBE_POOL, + BLUE_CUBE_POOL, + BLUE_CUBE_POOL, + BLUE_CUBE_POOL, + BLUE_CUBE_POOL, + BLUE_CUBE_POOL, + S_ROYALISTS, + S_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) + + for (let i = 0; i < 4; ++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_hand.push(game.objective_deck.pop()) + game.blue_hand.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, + objective: 0 + } + + if (player === COMMUNE) { + view.hand = game.red_hand + view.objective = game.red_objective + } + if (player === VERSAILLES) { + view.hand = game.blue_hand + 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[action] = 1 + 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) { @@ -401,7 +1313,16 @@ function random(range) { 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] @@ -410,29 +1331,82 @@ function shuffle(list) { } } -// remove item at index (faster than splice) +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 - return array } -// insert item at index (faster than splice) function array_insert(array, index, item) { for (let i = array.length; i > index; --i) array[i] = array[i - 1] array[index] = item - return array } -function array_remove_item(array, item) { - let i = array.indexOf(item) - if (i >= 0) - array_remove(array, i) +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 } @@ -464,9 +1438,9 @@ function set_add(set, item) { else if (item > x) a = m + 1 else - return set + return } - return array_insert(set, a, item) + array_insert(set, a, item) } function set_delete(set, item) { @@ -479,10 +1453,11 @@ function set_delete(set, item) { b = m - 1 else if (item > x) a = m + 1 - else - return array_remove(set, m) + else { + array_remove(set, m) + return + } } - return set } function set_toggle(set, item) { @@ -495,89 +1470,83 @@ function set_toggle(set, item) { b = m - 1 else if (item > x) a = m + 1 - else - return array_remove(set, m) - } - return array_insert(set, a, item) -} - - -// 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 + else { + array_remove(set, m) + return } - 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_insert(set, a, item) } -function clear_undo() { - if (game.undo.length > 0) - game.undo = [] -} +// Map as plain sorted array of key/value pairs -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 log(msg) { - game.log.push(msg) +function map_clear(map) { + map.length = 0 } -function log_br() { - if (game.log.length > 0 && game.log[game.log.length-1] !== "") - game.log.push("") +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 logi(msg) { - game.log.push(">" + msg) +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 log_h1(msg) { - log_br() - log(".h1 " + msg) - log_br() +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 log_h2(msg) { - log_br() - log(".h2 " + msg) - log_br() +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 + } + } } |