summaryrefslogtreecommitdiff
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
parent0c673db43e6a6872ffc3ecf4db42b168ad375c70 (diff)
downloadred-flag-over-paris-1f96c7a95ed557712c259fa09bab9e4a2e3f17d7.tar.gz
Zoot zoot!
# Conflicts: # tools/boxes.svg # tools/genboxes.py
-rw-r--r--data.js97
-rw-r--r--play.html7
-rw-r--r--play.js256
-rw-r--r--rules.js1605
4 files changed, 1479 insertions, 486 deletions
diff --git a/data.js b/data.js
deleted file mode 100644
index 499b190..0000000
--- a/data.js
+++ /dev/null
@@ -1,97 +0,0 @@
-"use strict"
-
-const data = {
-
- cards: [
- null,
- { side: "Versailles", ops: 1, name: "Jules Decatel" },
- { side: "Versailles", ops: 1, name: "The Murder of Vincenzini" },
- { side: "Versailles", ops: 1, name: "Brassardiers" },
- { side: "Versailles", ops: 1, name: "Jules Ferry" },
- { side: "Versailles", ops: 1, name: "Le Figaro" },
- { side: "Versailles", ops: 1, name: "Général Louis Valentin" },
- { side: "Versailles", ops: 1, name: "Général Espivent" },
- { side: "Versailles", ops: 1, name: "Les Amis de l'Ordre" },
- { side: "Versailles", ops: 2, name: "Socialist Newspaper Ban" },
- { side: "Versailles", ops: 2, name: "Fortification of Mont-Valérien" },
- { side: "Versailles", ops: 2, name: "Adolphe Thiers" },
- { side: "Versailles", ops: 2, name: "Otto von Bismarck" },
- { side: "Versailles", ops: 2, name: "Général Ernest de Cissey" },
- { side: "Versailles", ops: 2, name: "Colonel de Lochner" },
- { side: "Versailles", ops: 2, name: "Jules Favre" },
- { side: "Versailles", ops: 2, name: "Hostage Decree" },
- { side: "Versailles", ops: 4, name: "Maréchal Macmahon" },
- { side: "Commune", ops: 1, name: "Paule Minck" },
- { side: "Commune", ops: 1, name: "Walery Wroblewski" },
- { side: "Commune", ops: 1, name: "Banque de France" },
- { side: "Commune", ops: 1, name: "Le Réveil" },
- { side: "Commune", ops: 1, name: "Execution of Generals" },
- { side: "Commune", ops: 1, name: "Les Cantinières" },
- { side: "Commune", ops: 1, name: "Eugène Protot" },
- { side: "Commune", ops: 1, name: "Paul Cluseret" },
- { side: "Commune", ops: 2, name: "Gaston Crémieux" },
- { side: "Commune", ops: 2, name: "Luise Michel" },
- { side: "Commune", ops: 2, name: "Jaroslav Dombrowski" },
- { side: "Commune", ops: 2, name: "Raoul Rigault" },
- { side: "Commune", ops: 2, name: "Karl Marx" },
- { side: "Commune", ops: 2, name: "Blanquists" },
- { side: "Commune", ops: 2, name: "Général Lullier" },
- { side: "Commune", ops: 2, name: "Jules Vallès" },
- { side: "Commune", ops: 4, name: "Charles Delescluze" },
- { side: "Neutral", ops: 3, name: "Conciliation" },
- { side: "Neutral", ops: 3, name: "Georges Clemenceau" },
- { side: "Neutral", ops: 3, name: "Archbishop Georges Darboy" },
- { side: "Neutral", ops: 3, name: "Victor Hugo" },
- { side: "Neutral", ops: 3, name: "Léon Gambetta" },
- { side: "Neutral", ops: 3, name: "Elihu Washburne" },
- { side: "Neutral", ops: 3, name: "Freemason Parade" },
- { side: "Objective", ops: 0, name: "Paris Cannons", region: "Butte Montmartre" },
- { side: "Objective", ops: 0, name: "Aux Barricades!", region: "Buttes-Aux-Cailles" },
- { side: "Objective", ops: 0, name: "Commune's Stronghold", region: "Père Lachaise" },
- { side: "Objective", ops: 0, name: "Fighting in Issy Village", region: "Fort d'Issy" },
- { side: "Objective", ops: 0, name: "Battle of Mont-Valérien", region: "Mont-Valérien" },
- { side: "Objective", ops: 0, name: "Raid on Château de Vincennes", region: "Château de Vincennes" },
- { side: "Objective", ops: 0, name: "Revolution in the Press", region: "Press" },
- { side: "Objective", ops: 0, name: "Pius IX", region: "Catholic Church" },
- { side: "Objective", ops: 0, name: "Socialist International", region: "Social Movements" },
- { side: "Objective", ops: 0, name: "Royalists Dissension", region: "Royalists" },
- { side: "Objective", ops: 0, name: "Rise of Republicanism", region: "Republicans" },
- { side: "Objective", ops: 0, name: "Legitimacy", region: "National Assembly" },
- ],
-
- space_names: [
- "Red Cube Pool",
- "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",
- "Royalists",
- "National Assembly",
- "Republicans",
- "Catholic Church",
- "Press",
- "Social Movements",
- "Mont-Valérien",
- "Butte Montmartre",
- "Fort d'Issy",
- "Butte-Aux-Cailles",
- "Père Lachaise",
- "Château de Vincennes",
- "Prussian Occupied Territory",
- "Versailles HQ",
- ],
-}
-
-if (typeof module !== 'undefined')
- module.exports = data
diff --git a/play.html b/play.html
index fce9c2c..1a2151b 100644
--- a/play.html
+++ b/play.html
@@ -9,7 +9,6 @@
<link rel="stylesheet" href="/fonts/fonts.css">
<link rel="stylesheet" href="/common/play.css">
<script defer src="/common/play.js"></script>
-<script defer src="data.js"></script>
<script defer src="play.js"></script>
<style>
@@ -109,7 +108,6 @@ body.Versailles header.your_turn { background-color: skyblue; }
.piece {
position: absolute;
- pointer-events: none;
background-size: cover;
background-repeat: no-repeat;
filter: drop-shadow(0px 2px 3px #0008);
@@ -328,7 +326,8 @@ body.Versailles header.your_turn { background-color: skyblue; }
<div id="popup" onmouseleave="hide_popup_menu()">
<div id="menu_card_event" class="always" onclick="on_card_event()">Play event</div>
-<div id="menu_card_ops" class="always" onclick="on_card_ops()">Operations</div>
+<div id="menu_card_ops_political" class="always" onclick="on_card_ops_political()">Political operations</div>
+<div id="menu_card_ops_military" class="always" onclick="on_card_ops_military()">Military operations</div>
<div id="menu_card_use_discarded" class="always" onclick="on_card_use_discarded()">Use discarded event</div>
<div id="menu_card_advance_momentum" class="always" onclick="on_card_advance_momentum()">Advance momentum</div>
</div>
@@ -369,7 +368,7 @@ body.Versailles header.your_turn { background-color: skyblue; }
<div class="role_user">-</div>
</div>
</div>
- <div class="card_info"><div id="active_card" class="card card_strategy_back"></div></div>
+ <div class="card_info"><div id="discarded_card" class="card card_strategy_back"></div></div>
</div>
<div id="log"></div>
</aside>
diff --git a/play.js b/play.js
index c3abff4..f2691b2 100644
--- a/play.js
+++ b/play.js
@@ -1,6 +1,7 @@
"use strict"
-const space_count = data.space_names.length
+// TODO: layout cubes in spaces in two groups (max 4 each space)
+// TODO: layout cubes on tracks/pools as one space
let layout = []
let space_layout_cube = []
@@ -18,40 +19,142 @@ let ui = {
round_marker: document.getElementById("round_marker"),
}
+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 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 space_count = space_names.length
+
// :r !python3 tools/genboxes.py
const boxes = {
- "Royalists": [318,1711,506,505],
- "Republicans": [1960,1712,504,504],
- "Catholic Church": [318,3146,505,505],
- "Social Movements": [1960,3146,506,505],
- "Fort d'Issy": [3374,3172,506,506],
- "Butte-Aux-Cailles": [4153,2642,505,505],
- "Père Lachaise": [4824,2364,506,505],
- "Château de Vincennes": [5369,2599,504,507],
- "Press": [1176,2908,447,448],
- "National Assembly": [1168,1984,447,447],
- "Butte Montmartre": [4208,1842,447,444],
- "Mont-Valérien": [2868,2028,447,448],
- "Versailles HQ": [2646,3398,404,380],
- "Prussian Occupied Territory": [5190,1413,718,346],
- "Red Crisis Track Start": [3928,244,361,448],
- "Red Crisis Track Escalation": [4289,244,334,448],
- "Red Crisis Track Tension": [4623,244,332,448],
- "Red Crisis Track Final Crisis": [4955,244,332,448],
- "Blue Crisis Track Start": [1728,244,354,446],
- "Blue Crisis Track Escalation": [1394,244,334,446],
- "Blue Crisis Track Tension": [1061,244,333,446],
- "Blue Crisis Track Final Crisis": [728,244,333,446],
- "Blue Objective Card": [479,3983,1088,218],
- "Red Objective Card": [4454,3984,1088,218],
- "Red Cube Pool": [4498,790,791,219],
- "Blue Cube Pool": [720,790,791,219],
- "Red Bonus Cubes 1": [4289,89,334,155],
- "Red Bonus Cubes 2": [4623,89,332,155],
- "Red Bonus Cubes 3": [4955,89,332,155],
- "Blue Bonus Cubes 1": [1394,89,334,155],
- "Blue Bonus Cubes 2": [1061,89,333,155],
- "Blue Bonus Cubes 3": [728,89,333,155],
+ "Royalists": [80,428,126,126],
+ "Republicans": [490,428,126,126],
+ "Catholic Church": [80,786,126,126],
+ "Social Movements": [490,786,126,126],
+ "Fort D'Issy": [844,793,126,126],
+ "Butte-Aux-Cailles": [1038,660,126,126],
+ "Père Lachaise": [1206,591,126,126],
+ "Château de Vincennes": [1342,650,126,127],
+ "Press": [294,727,112,112],
+ "National Assembly": [292,496,112,112],
+ "Butte Montmartre": [1052,460,112,111],
+ "Mont-Valérien": [717,507,112,112],
+ "Versailles HQ": [662,850,101,95],
+ "Prussian Occupied Territory": [1298,353,180,86],
+ "Red Crisis Track Start": [982,61,90,112],
+ "Red Crisis Track Escalation": [1072,61,84,112],
+ "Red Crisis Track Tension": [1156,61,83,112],
+ "Red Crisis Track Final Crisis": [1239,61,83,112],
+ "Blue Crisis Track Start": [432,61,88,112],
+ "Blue Crisis Track Escalation": [348,61,84,112],
+ "Blue Crisis Track Tension": [265,61,83,112],
+ "Blue Crisis Track Final Crisis": [182,61,83,112],
+ "Blue Objective Card": [120,996,272,54],
+ "Red Objective Card": [1114,996,272,54],
+ "Blue Cube Pool": [180,198,198,55],
+ "Red Bonus Cubes 1": [1072,22,84,39],
+ "Red Bonus Cubes 2": [1156,22,83,39],
+ "Red Bonus Cubes 3": [1239,22,83,39],
+ "Blue Bonus Cubes 1": [348,22,84,39],
+ "Blue Bonus Cubes 2": [265,22,83,39],
+ "Blue Bonus Cubes 3": [182,22,83,39],
+ "Red Cube Pool 1": [787,330,115,39],
+ "Red Cube Pool 2": [902,330,136,39],
+ "Red Cube Pool 3": [1038,330,157,40],
+ "Prussian Collaboration 1": [600,330,115,39],
+ "Prussian Collaboration 2": [463,330,136,39],
+ "Prussian Collaboration 3": [306,330,157,39],
}
function is_card_action(action, card) {
@@ -60,14 +163,8 @@ function is_card_action(action, card) {
return false
}
-function is_cube_action(i) {
- if (view.actions && view.actions.cube && view.actions.cube.includes(i))
- return true
- return false
-}
-
-function is_disc_action(i) {
- if (view.actions && view.actions.disc && view.actions.disc.includes(i))
+function is_piece_action(i) {
+ if (view.actions && view.actions.piece && view.actions.piece.includes(i))
return true
return false
}
@@ -95,14 +192,14 @@ function on_click_space(evt) {
function on_click_cube(evt) {
if (evt.button === 0) {
- if (send_action('cube', evt.target.my_cube))
+ if (send_action('piece', evt.target.my_cube))
evt.stopPropagation()
}
}
function on_click_disc(evt) {
if (evt.button === 0) {
- if (send_action('disc', evt.target.my_disc))
+ if (send_action('piece', evt.target.my_disc))
evt.stopPropagation()
}
}
@@ -134,13 +231,14 @@ function build_user_interface() {
elt.className = "piece disc red"
else
elt.className = "piece disc blue"
- elt.my_disc = i
+ elt.my_disc = i + 36
elt.addEventListener("mousedown", on_click_disc)
+ document.getElementById("pieces").appendChild(elt)
}
for (let i = 0; i < space_count; ++i) {
- let name = data.space_names[i]
- let r = boxes[name]
+ let name = space_names[i]
+ let [x, y, w, h] = boxes[name]
elt = ui.spaces[i] = document.createElement("div")
elt.className = "space"
elt.my_space = i
@@ -149,10 +247,6 @@ function build_user_interface() {
elt.addEventListener("mouseenter", on_focus_space)
elt.addEventListener("mouseleave", on_blur)
let bw = 8
- let x = Math.round(r[0] / 4)
- let y = Math.round(r[1] / 4)
- let w = Math.round(r[2] / 4)
- let h = Math.round(r[3] / 4)
elt.style.top = y + "px"
elt.style.left = x + "px"
elt.style.width = (w - bw * 2) + "px"
@@ -185,12 +279,14 @@ function layout_cubes(list, xorig, yorig) {
}
function layout_disc(s, disc) {
- if (s > 0)
+ if (s > 0) {
disc.classList.remove("hide")
+ disc.style.left = (space_layout_disc[s].x - 50 - 12) + "px"
+ disc.style.top = (space_layout_disc[s].y - 20 - 8) + "px"
+ disc.style.zIndex = 51;
+ }
else
disc.classList.add("hide")
- disc.style.left = (space_layout_disc[s].x - 50 - 12) + "px"
- disc.style.top = (space_layout_disc[s].y - 20 - 8) + "px"
}
function on_focus_card_tip(card_number) {
@@ -203,7 +299,7 @@ function on_blur_card_tip() {
function sub_card_name(match, p1, offset, string) {
let c = p1 | 0
- let n = data.cards[c].name
+ let n = card_names[c]
return `<span class="tip" onmouseenter="on_focus_card_tip(${c})" onmouseleave="on_blur_card_tip()">${n}</span>`
}
@@ -240,10 +336,10 @@ function on_log(text) {
}
function on_update() {
- if (view.active_card)
- document.getElementById("active_card").className = `card card_${view.active_card}`
+ if (view.discard)
+ document.getElementById("discarded_card").className = `card card_${view.discard}`
else
- document.getElementById("active_card").className = `card card_strategy_back`
+ document.getElementById("discarded_card").className = `card card_strategy_back`
if (view.initiative === "Commune")
document.getElementById("commune_info").textContent = "\u2756"
@@ -267,28 +363,42 @@ function on_update() {
for (let c of view.hand)
document.getElementById("hand").appendChild(ui.cards[c])
- for (let i = 0; i < space_count; ++i)
+ for (let i = 0; i < space_names.length; ++i)
layout[i] = []
for (let i = 0; i < 36; ++i) {
- layout[view.cubes[i]].push(ui.cubes[i])
- ui.cubes[i].classList.toggle("action", is_cube_action(i))
+ if (view.pieces[i] >= 0) {
+ layout[view.pieces[i]].push(ui.cubes[i])
+ ui.cubes[i].classList.remove("hide");
+ ui.cubes[i].classList.toggle("action", is_piece_action(i))
+ }
+ else {
+ ui.cubes[i].classList.add("hide");
+ }
}
for (let i = 0; i < space_count; ++i) {
layout_cubes(layout[i], space_layout_cube[i].x, space_layout_cube[i].y)
ui.spaces[i].classList.toggle("action", is_space_action(i))
}
for (let i = 0; i < 4; ++i) {
- layout_disc(view.discs[i], ui.discs[i])
- ui.discs[i].classList.toggle("action", is_cube_action(i))
+ layout_disc(view.pieces[36+i], ui.discs[i])
+ ui.discs[i].classList.toggle("action", is_piece_action(36+i))
}
for (let i = 1; i < ui.cards.length; ++i) {
ui.cards[i].classList.toggle("action", is_card_action('card', i))
}
- action_button("commune", "Commune")
- action_button("versailles", "Versailles")
- action_button("undo", "Undo")
+ action_button("commune", "Commune");
+ action_button("versailles", "Versailles");
+
+ action_button("spend", "Spend");
+ action_button("draw", "Draw");
+
+ action_button("end_remove", "End Remove");
+ action_button("end_turn", "End Turn");
+
+ action_button("done", "Done");
+ action_button("undo", "Undo");
}
/* CARD ACTION MENU */
@@ -327,6 +437,16 @@ function on_card_ops() {
hide_popup_menu()
}
+function on_card_ops_political() {
+ if (send_action('card_ops_political', current_popup_card))
+ hide_popup_menu()
+}
+
+function on_card_ops_military() {
+ if (send_action('card_ops_military', current_popup_card))
+ hide_popup_menu()
+}
+
function on_card_use_discarded() {
if (send_action('card_use_discarded', current_popup_card))
hide_popup_menu()
@@ -347,8 +467,10 @@ function on_click_card(evt) {
let menu = []
if (is_card_action('card_event', card))
menu.push('card_event')
- if (is_card_action('card_ops', card))
- menu.push('card_ops')
+ if (is_card_action('card_ops_political', card))
+ menu.push('card_ops_political')
+ if (is_card_action('card_ops_military', card))
+ menu.push('card_ops_military')
if (is_card_action('card_use_discarded', card))
menu.push('card_use_discarded')
if (is_card_action('card_advance_momentum', card))
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
+ }
+ }
}