summaryrefslogtreecommitdiff
path: root/rules.js
diff options
context:
space:
mode:
authorTor Andersson <tor@ccxvii.net>2023-05-24 21:01:14 +0200
committerTor Andersson <tor@ccxvii.net>2023-05-24 21:06:17 +0200
commit1f96c7a95ed557712c259fa09bab9e4a2e3f17d7 (patch)
tree8068a4f310de878a8687cbcb4dea64a61d8321e0 /rules.js
parent0c673db43e6a6872ffc3ecf4db42b168ad375c70 (diff)
downloadred-flag-over-paris-1f96c7a95ed557712c259fa09bab9e4a2e3f17d7.tar.gz
Zoot zoot!
# Conflicts: # tools/boxes.svg # tools/genboxes.py
Diffstat (limited to 'rules.js')
-rw-r--r--rules.js1605
1 files changed, 1287 insertions, 318 deletions
diff --git a/rules.js b/rules.js
index 51d2753..55cd221 100644
--- a/rules.js
+++ b/rules.js
@@ -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
+ }
+ }
}