summaryrefslogtreecommitdiff
path: root/rules.js
diff options
context:
space:
mode:
Diffstat (limited to 'rules.js')
-rw-r--r--rules.js1248
1 files changed, 783 insertions, 465 deletions
diff --git a/rules.js b/rules.js
index 02dc94b..f7fc222 100644
--- a/rules.js
+++ b/rules.js
@@ -7,47 +7,77 @@ exports.scenarios = [ "Historical" ]
exports.roles = [ "American", "British" ]
-const CARDS = require("./cards")
-const DATA = require("./data")
-const SPACES = DATA.SPACES
-const COLONIES = DATA.COLONIES
-const GENERALS = DATA.GENERALS
-const BLOCKADE = DATA.BLOCKADE
-const PATH_INDEX = DATA.PATH_INDEX
-const PATH_NAME = DATA.PATH_NAME
-const PATH_TYPE = DATA.PATH_TYPE
+const data = require("./data")
+
+const CARDS = data.cards
+const SPACES = data.spaces
+const COLONIES = data.colony_spaces
+const GENERALS = data.generals
+
+const Canada = 0
+const NH = 1
+const NY = 2
+const MA = 3
+const CT = 4
+const RI = 5
+const PA = 6
+const NJ = 7
+const MD = 8
+const DE = 9
+const VA = 10
+const NC = 11
+const SC = 12
+const GA = 13
const BRITISH = "British"
const AMERICAN = "American"
const FRENCH = "French"
-const BRITISH_GENERALS = [ "Burgoyne", "Carleton", "Clinton", "Cornwallis", "Howe" ]
-const AMERICAN_GENERALS = [ "Arnold", "Gates", "Greene", "Lafayette", "Lee", "Lincoln", "Washington", "Rochambeau" ]
-const WASHINGTON = "Washington"
-const ROCHAMBEAU = "Rochambeau"
-const ARNOLD = "Arnold"
-
-const CAPTURED_GENERALS = "Captured Generals"
-const CONTINENTAL_CONGRESS_DISPERSED = "Continental Congress Dispersed"
-const BRITISH_REINFORCEMENTS = "British Leader Reinforcements"
-const AMERICAN_REINFORCEMENTS = "American Leader Reinforcements"
-const FRENCH_REINFORCEMENTS = "French Reinforcements"
-const TURN_TRACK = {
- 1775: "Game Turn 1775",
- 1776: "Game Turn 1776",
- 1777: "Game Turn 1777",
- 1778: "Game Turn 1778",
- 1779: "Game Turn 1779",
- 1780: "Game Turn 1780",
- 1781: "Game Turn 1781",
- 1782: "Game Turn 1782",
- 1783: "Game Turn 1783",
-}
-
-const FALMOUTH_QUEBEC = "Falmouth/Quebec"
-
-const THE_13_COLONIES = [ "NH", "NY", "MA", "CT", "RI", "PA", "NJ", "MD", "DE", "VA", "NC", "SC", "GA" ]
-const SOUTH_OF_WINTER_ATTRITION_LINE = [ "NC", "SC", "GA" ]
+const PC_NONE = 0
+const PC_BRITISH = 1
+const PC_AMERICAN = 2
+
+const AMERICAN_GENERALS = [ 0, 1, 2, 3, 4, 5, 6, 7 ]
+const BRITISH_GENERALS = [ 8, 9, 10, 11, 12 ]
+
+const NOBODY = -1
+const ARNOLD = data.general_index["Arnold"]
+const BURGOYNE = data.general_index["Burgoyne"]
+const CLINTON = data.general_index["Clinton"]
+const CORNWALLIS = data.general_index["Cornwallis"]
+const GATES = data.general_index["Gates"]
+const GREENE = data.general_index["Greene"]
+const LAFAYETTE = data.general_index["Lafayette"]
+const LEE = data.general_index["Lee"]
+const LINCOLN = data.general_index["Lincoln"]
+const ROCHAMBEAU = data.general_index["Rochambeau"]
+const WASHINGTON = data.general_index["Washington"]
+const CARLETON = data.general_index["Carleton"]
+const HOWE = data.general_index["Howe"]
+
+const NOWHERE = -1
+const BOSTON = data.space_index["Boston"]
+const CHARLESTON = data.space_index["Charleston"]
+const FORT_DETROIT = data.space_index["Fort Detroit"]
+const GILBERT_TOWN = data.space_index["Gilbert Town"]
+const LEXINGTON_CONCORD = data.space_index["Lexington Concord"]
+const MONTREAL = data.space_index["Montreal"]
+const NEWPORT = data.space_index["Newport"]
+const NINETY_SIX = data.space_index["Ninety Six"]
+const NORFOLK = data.space_index["Norfolk"]
+const PHILADELPHIA = data.space_index["Philadelphia"]
+const QUEBEC = data.space_index["Quebec"]
+const WILMINGTON_NC = data.space_index["Wilmington NC"]
+
+const CAPTURED_GENERALS = data.space_index["Captured Generals"]
+const CONTINENTAL_CONGRESS_DISPERSED = data.space_index["Continental Congress Dispersed"]
+const BRITISH_REINFORCEMENTS = data.space_index["British Leader Reinforcements"]
+const AMERICAN_REINFORCEMENTS = data.space_index["American Leader Reinforcements"]
+const FRENCH_REINFORCEMENTS = data.space_index["French Reinforcements"]
+const ELIMINATED = data.spaces.length
+
+const THE_13_COLONIES = [ 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13 ]
+const SOUTH_OF_WINTER_ATTRITION_LINE = [ 11, 12, 13 ]
const CAMPAIGN_CARDS = [ 67, 68, 69, 70 ]
const DECLARATION_OF_INDEPENDENCE = 99
@@ -55,9 +85,10 @@ const BARON_VON_STEUBEN = 86
const WAR_ENDS_1779 = 71
const BENJAMIN_FRANKLIN = 101
-const ENEMY = { American: BRITISH, British: AMERICAN }
+const space_count = 66
+const all_spaces = new Array(space_count).fill(0).map((_,i)=>i)
-const default_options = {}
+const ENEMY = { American: BRITISH, British: AMERICAN }
let states = {}
let events = {}
@@ -65,107 +96,28 @@ let events = {}
let game
let view
-function random(n) {
- return (game.seed = (game.seed * 200105) % 34359738337) % n
-}
-
-function logbr() {
- if (game.log.length > 0 && game.log[game.log.length - 1] !== "")
- game.log.push("")
-}
-
-function log(s) {
- game.log.push(s)
-}
-
-function logp(s) {
- game.log.push(game.active[0] + " " + s)
-}
-
-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
- }
-}
-
-function clear_undo() {
- if (game.undo) {
- game.undo.length = 0
- }
-}
-
-function push_undo() {
- if (game.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() {
- if (game.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 remove_from_array(array, item) {
- let i = array.indexOf(item)
- if (i >= 0)
- array.splice(i, 1)
-}
-
function setup_game(seed) {
game = {
seed: seed,
year: 1775,
- congress: "Philadelphia",
+ congress: data.space_index["Philadelphia"],
french_alliance: 0,
french_alliance_triggered: false,
european_war: false,
- french_navy: FRENCH_REINFORCEMENTS,
+ french_navy: -1,
regulars: true,
war_ends: 0,
played_british_reinforcements: 0,
played_american_reinforcements: [],
- pc: {},
- generals: {},
- moved: {},
+ pc: new Array(space_count).fill(PC_NONE),
+ generals: new Array(general_count).fill(NOWHERE),
+ moved: [],
cu: [],
- control: {},
+
+ // TODO: compute on the fly
+ control: [],
+
deck: create_deck(),
discard: [],
@@ -182,22 +134,13 @@ function setup_game(seed) {
actions: [],
}
- function spawn_unit(owner, location, pc, cu, name) {
+ function spawn_unit(owner, location, pc, cu, general=NOBODY) {
if (pc)
- game.pc[location] = owner
- if (name) {
- game.generals[name] = {
- location: location,
- }
- }
- if (cu > 0) {
- game.cu.push({
- owner: owner,
- location: location,
- count: cu,
- moved: 0,
- })
- }
+ set_space_pc(location, pc)
+ if (general !== NOBODY)
+ set_general_location(general, location)
+ if (cu > 0)
+ spawn_cu(owner, location, count)
}
function british(place, pc, cu, ld) {
@@ -210,31 +153,31 @@ function setup_game(seed) {
spawn_unit(FRENCH, place, pc, cu, ld)
}
- british("Quebec", true, 2, "Carleton")
- british("Montreal", true)
- british("Fort Detroit", true, 1)
- british("Boston", true, 5, "Howe")
- british("Norfolk", true)
- british("Gilbert Town", true)
- british("Wilmington NC", true)
- british("Ninety Six", true)
+ british(QUEBEC, PC_BRITISH, 2, CARLETON)
+ british(MONTREAL, PC_BRITISH)
+ british(FORT_DETROIT, PC_BRITISH, 1)
+ british(BOSTON, PC_BRITISH, 5, HOWE)
+ british(NORFOLK, PC_BRITISH)
+ british(GILBERT_TOWN, PC_BRITISH)
+ british(WILMINGTON_NC, PC_BRITISH)
+ british(NINETY_SIX, PC_BRITISH)
- american("Lexington Concord", true, 5, "Washington")
- american("Newport", false, 2, "Greene")
- american("Charleston", true, 2)
- american("Philadelphia", true)
+ american(LEXINGTON_CONCORD, PC_AMERICAN, 5, WASHINGTON)
+ american(NEWPORT, PC_NONE, 2, GREENE)
+ american(CHARLESTON, PC_AMERICAN, 2)
+ american(PHILADELPHIA, PC_AMERICAN)
- british(BRITISH_REINFORCEMENTS, false, 0, "Burgoyne")
- british(BRITISH_REINFORCEMENTS, false, 0, "Clinton")
- british(BRITISH_REINFORCEMENTS, false, 0, "Cornwallis")
+ british(BRITISH_REINFORCEMENTS, PC_NONE, 0, BURGOYNE)
+ british(BRITISH_REINFORCEMENTS, PC_NONE, 0, CLINTON)
+ british(BRITISH_REINFORCEMENTS, PC_NONE, 0, CORNWALLIS)
- american(AMERICAN_REINFORCEMENTS, false, 0, "Arnold")
- american(AMERICAN_REINFORCEMENTS, false, 0, "Lincoln")
- american(AMERICAN_REINFORCEMENTS, false, 0, "Gates")
- american(AMERICAN_REINFORCEMENTS, false, 0, "Lee")
- american(AMERICAN_REINFORCEMENTS, false, 0, "Lafayette")
+ american(AMERICAN_REINFORCEMENTS, PC_NONE, 0, ARNOLD)
+ american(AMERICAN_REINFORCEMENTS, PC_NONE, 0, LINCOLN)
+ american(AMERICAN_REINFORCEMENTS, PC_NONE, 0, GATES)
+ american(AMERICAN_REINFORCEMENTS, PC_NONE, 0, LEE)
+ american(AMERICAN_REINFORCEMENTS, PC_NONE, 0, LAFAYETTE)
- french(FRENCH_REINFORCEMENTS, false, 5, "Rochambeau")
+ french(FRENCH_REINFORCEMENTS, PC_NONE, 5, ROCHAMBEAU)
goto_committees_of_correspondence()
}
@@ -275,7 +218,7 @@ function deal_card() {
function last_discard() {
if (game.discard.length > 0)
return game.discard[game.discard.length - 1]
- return null
+ return 0
}
function active_hand() {
@@ -289,7 +232,7 @@ function play_card(c, reason) {
log(game.active[0] + " played #" + c)
if (CARDS[c].reshuffle === "if_played")
game.reshuffle = true
- remove_from_array(active_hand(), c)
+ array_remove_item(active_hand(), c)
game.last_played = c
if (!CARDS[c].once)
game.discard.push(c)
@@ -298,7 +241,7 @@ function play_card(c, reason) {
}
function discard_card_from_hand(hand, c) {
- remove_from_array(hand, c)
+ array_remove_item(hand, c)
game.discard.push(c)
if (CARDS[c].reshuffle === "if_discarded")
game.reshuffle = true
@@ -345,7 +288,7 @@ function can_play_reinforcements() {
if (game.played_british_reinforcements === 0) {
let n = count_british_cu(BRITISH_REINFORCEMENTS)
for (let g of BRITISH_GENERALS)
- if (game.generals[g].location === BRITISH_REINFORCEMENTS)
+ if (is_general_at_location(g, BRITISH_REINFORCEMENTS))
++n
return n > 0
}
@@ -356,14 +299,17 @@ function can_play_reinforcements() {
return false
}
+function is_map_space(s) {
+ return s >= 0 && s <= 65
+}
+
function is_port(where) {
return SPACES[where].port
}
function is_non_blockaded_port(where) {
- if (SPACES[where].port && BLOCKADE[where] !== game.french_navy)
- return true
- return false
+ let port = SPACES[where].port
+ return (port > 0 && port !== game.french_navy)
}
function is_fortified_port(where) {
@@ -376,7 +322,7 @@ function is_continental_congress_dispersed() {
function is_winter_quarter_space(where) {
let colony = SPACES[where].colony
- if (colony === "GA" || colony === "SC" || colony === "NC")
+ if (set_has(SOUTH_OF_WINTER_ATTRITION_LINE, colony))
return true // south of winter attrition line
let type = SPACES[where].type
if (type === "winter-quarters" || type === "fortified-port")
@@ -393,11 +339,11 @@ function allowed_to_place_american_pc() {
}
function is_british_militia(space) {
- return game.control[SPACES[space].colony] === BRITISH
+ return game.control[SPACES[space].colony] === PC_BRITISH
}
function is_american_militia(space) {
- return game.control[SPACES[space].colony] === AMERICAN
+ return game.control[SPACES[space].colony] === PC_AMERICAN
}
function is_american_winter_offensive() {
@@ -408,16 +354,20 @@ function is_american_winter_offensive() {
/* PC */
+function set_space_pc(space, pc) {
+ game.pc[space] = pc
+}
+
function has_no_pc(space) {
- return game.pc[space] !== BRITISH && game.pc[space] !== AMERICAN
+ return game.pc[space] === PC_NONE
}
function has_british_pc(space) {
- return game.pc[space] === BRITISH
+ return game.pc[space] === PC_BRITISH
}
function has_american_pc(space) {
- return game.pc[space] === AMERICAN
+ return game.pc[space] === PC_AMERICAN
}
function has_enemy_pc(space) {
@@ -428,11 +378,11 @@ function has_enemy_pc(space) {
}
function is_adjacent_to_british_pc(a) {
- for (let b of SPACES[a].exits)
+ for (let b of SPACES[a].adjacent)
if (has_british_pc(b))
return true
if (SPACES[a].port) {
- for (let b in SPACES) {
+ for (let b of all_spaces) {
if (SPACES[b].port)
if (has_british_pc(b))
return true
@@ -442,7 +392,7 @@ function is_adjacent_to_british_pc(a) {
}
function is_adjacent_to_american_pc(a) {
- for (let b of SPACES[a].exits)
+ for (let b of SPACES[a].adjacent)
if (has_american_pc(b))
return true
return false
@@ -451,13 +401,13 @@ function is_adjacent_to_american_pc(a) {
function place_british_pc(space) {
logp("placed PC in " + space)
if (game.british_pc_space_list)
- remove_from_array(game.british_pc_space_list, space)
- game.pc[space] = BRITISH
+ array_remove_item(game.british_pc_space_list, space)
+ set_space_pc(space, PC_BRITISH)
}
function place_american_pc(space) {
logp("placed PC in " + space)
- game.pc[space] = AMERICAN
+ set_space_pc(space, PC_AMERICAN)
}
function remove_pc(space) {
@@ -465,7 +415,7 @@ function remove_pc(space) {
logp("removed PC in " + space)
else
logp("removed PC in " + space)
- game.pc[space] = undefined
+ set_space_pc(space, PC_NONE)
}
function flip_pc(space) {
@@ -473,24 +423,27 @@ function flip_pc(space) {
logp("flipped PC in " + space)
else
logp("flipped PC in " + space)
- game.pc[space] = ENEMY[game.pc[space]]
+ if (has_british_pc(space))
+ set_space_pc(space, PC_AMERICAN)
+ else
+ set_space_pc(space, PC_BRITISH)
}
function update_colony_control() {
- for (let c in COLONIES) {
+ for (let c = 0; c <= 13; ++c) {
let control = 0
for (let space of COLONIES[c]) {
- if (game.pc[space] === BRITISH)
+ if (has_british_pc(space))
--control
- else if (game.pc[space] === AMERICAN)
+ else if (has_american_pc(space))
++control
}
if (control < 0)
- game.control[c] = BRITISH
+ game.control[c] = PC_BRITISH
else if (control > 0)
- game.control[c] = AMERICAN
+ game.control[c] = PC_AMERICAN
else
- game.control[c] = undefined
+ game.control[c] = PC_NONE
}
}
@@ -505,6 +458,11 @@ function find_cu(owner, space) {
return null
}
+function reset_moved_cu() {
+ for (let cu of game.cu)
+ cu.moved = 0
+}
+
function find_british_cu(space) {
return find_cu(BRITISH, space)
}
@@ -605,7 +563,7 @@ function remove_cu(owner, where, count) {
let cu = find_cu(owner, where)
if (count >= cu.count) {
let i = game.cu.indexOf(cu)
- remove_from_array(game.cu, cu)
+ array_remove_item(game.cu, cu)
} else {
cu.count -= count
}
@@ -655,38 +613,54 @@ function move_british_cu(from, to, count) {
/* GENERALS */
+function location_of_general(g) {
+ return game.generals[g]
+}
+
+function set_general_location(g, s) {
+ game.generals[g] = s
+}
+
+function is_general_at_location(g, s) {
+ return location_of_general(g) === s
+}
+
function is_general_on_map(g) {
- switch (game.generals[g].location) {
- case null: /* killed */
- case CAPTURED_GENERALS:
- case BRITISH_REINFORCEMENTS:
- case AMERICAN_REINFORCEMENTS:
- case FRENCH_REINFORCEMENTS:
- return false
- }
- return true
+ return is_map_space(location_of_general(g))
+}
+
+function has_general_moved(g) {
+ return set_has(game.moved, g)
+}
+
+function set_general_moved(g) {
+ set_add(game.moved, g)
+}
+
+function reset_moved_generals() {
+ set_clear(game.moved)
}
function find_british_general(where) {
for (let general of BRITISH_GENERALS)
- if (game.generals[general].location === where)
+ if (is_general_at_location(general, where))
return general
- return null
+ return NOBODY
}
function find_american_or_french_general(where) {
for (let general of AMERICAN_GENERALS)
- if (game.generals[general].location === where)
+ if (is_general_at_location(general, where))
return general
- return null
+ return NOBODY
}
function has_british_general(where) {
- return find_british_general(where) !== null
+ return find_british_general(where) !== NOBODY
}
function has_american_or_french_general(where) {
- return find_american_or_french_general(where) !== null
+ return find_american_or_french_general(where) !== NOBODY
}
function has_enemy_general(where) {
@@ -704,7 +678,7 @@ function count_friendly_generals(where) {
list = AMERICAN_GENERALS
let count = 0
for (let g of list)
- if (location_of_general(g) === where)
+ if (is_general_at_location(g, where))
++count
return count
}
@@ -754,11 +728,11 @@ function can_activate_american_general(c) {
}
function move_general(who, where) {
- game.generals[who].location = where
+ set_general_location(who, where)
}
function capture_washington() {
- game.generals[WASHINGTON].location = null
+ set_general_location(WASHINGTON, ELIMINATED)
if (!game.french_alliance_triggered) {
game.french_alliance -= 3
@@ -792,9 +766,9 @@ function capture_enemy_general(where) {
}
function remove_benedict_arnold() {
- if (game.generals[ARNOLD].location) {
+ if (!is_general_at_location(ARNOLD, ELIMINATED)) {
log("Removed Arnold from the game!")
- game.generals[ARNOLD].location = null
+ set_general_location(ARNOLD, ELIMINATED)
}
}
@@ -830,10 +804,10 @@ function has_no_american_unit(where) {
function place_british_reinforcements(who, count, where) {
let already_there = find_british_general(where)
- if (who && already_there) {
+ if (who !== NOBODY && already_there !== NOBODY) {
move_general(already_there, BRITISH_REINFORCEMENTS)
}
- if (who) {
+ if (who !== NOBODY) {
logp("reinforced " + where + " with " + who)
move_general(who, where)
}
@@ -848,15 +822,16 @@ function place_british_reinforcements(who, count, where) {
}
function place_american_reinforcements(who, count, where) {
+console.log("PLACE AM", who, count, where)
let already_there = find_american_or_french_general(where)
- if (who && already_there) {
+ if (who !== NOBODY && already_there !== NOBODY) {
// Never replace Washington
if (already_there === WASHINGTON)
- who = null
+ who = NOBODY
else
move_general(already_there, AMERICAN_REINFORCEMENTS)
}
- if (who) {
+ if (who !== NOBODY) {
logp("reinforced " + where + " with " + who)
move_general(who, where)
}
@@ -868,14 +843,14 @@ function place_american_reinforcements(who, count, where) {
function place_french_reinforcements(who, where) {
let already_there = find_american_or_french_general(where)
- if (who && already_there) {
+ if (who !== NOBODY && already_there !== NOBODY) {
// Never replace Washington
if (already_there === WASHINGTON)
- who = null
+ who = NOBODY
else
move_general(already_there, AMERICAN_REINFORCEMENTS)
}
- if (who) {
+ if (who !== NOBODY) {
logp("reinforced " + where + " with " + who)
move_general(who, where)
}
@@ -884,10 +859,6 @@ function place_french_reinforcements(who, where) {
move_cu(FRENCH, AMERICAN_REINFORCEMENTS, where, count_french_cu(AMERICAN_REINFORCEMENTS))
}
-function location_of_general(g) {
- return game.generals[g].location
-}
-
function pickup_max_british_cu(where) {
game.carry_british = count_unmoved_british_cu(where)
if (game.carry_british > 5)
@@ -1017,7 +988,7 @@ function gen_remove_british_pc_from(list_of_colonies) {
}
function gen_remove_american_pc() {
- for (let space in SPACES) {
+ for (let space of all_spaces) {
if (has_american_pc(space) && has_no_american_unit(space)) {
gen_action("remove_pc", space)
}
@@ -1047,26 +1018,26 @@ function gen_remove_american_pc_from_non_port(list_of_colonies) {
}
function gen_remove_american_pc_within_two_spaces_of_a_british_general() {
- let candidates = {}
+ let candidates = []
for (let g of BRITISH_GENERALS) {
- let a = game.generals[g].location
- if (a in SPACES) {
- candidates[a] = true
- for (let b of SPACES[a].exits) {
- candidates[b] = true
- for (let c of SPACES[b].exits) {
- candidates[c] = true
+ let a = location_of_general(g)
+ if (is_map_space(a)) {
+ set_add(candidates, a)
+ for (let b of SPACES[a].adjacent) {
+ set_add(candidates, b)
+ for (let c of SPACES[b].adjacent) {
+ set_add(candidates, c)
}
}
}
}
- for (let space in candidates)
+ for (let space of candidates)
if (has_american_pc(space) && has_no_american_unit(space))
gen_action("remove_pc", space)
}
function gen_place_american_pc() {
- for (let space in SPACES) {
+ for (let space of all_spaces) {
if (has_no_pc(space) && has_no_british_playing_piece(space)) {
gen_action("place_american_pc", space)
}
@@ -1095,7 +1066,7 @@ function goto_committees_of_correspondence() {
states.committees_of_correspondence = {
inactive: "Committees of Correspondence",
- prompt: function (current) {
+ prompt(current) {
view.prompt =
"Committees of Correspondence: Place 1 PC marker in each of the 13 colonies. " + game.coc.length + " left."
if (game.coc.length > 0)
@@ -1103,13 +1074,13 @@ states.committees_of_correspondence = {
else
gen_pass()
},
- place_american_pc: function (space) {
+ place_american_pc(space) {
push_undo()
let colony = SPACES[space].colony
- remove_from_array(game.coc, colony)
+ array_remove_item(game.coc, colony)
place_american_pc(space)
},
- pass: function () {
+ pass() {
clear_undo()
goto_for_the_king()
},
@@ -1128,19 +1099,19 @@ function goto_for_the_king() {
states.for_the_king = {
inactive: "For the King",
- prompt: function (current) {
+ prompt(current) {
view.prompt = "For the King: Place 3 PC markers. " + game.count + " left."
if (game.count > 0)
gen_british_pc_ops()
else
gen_pass()
},
- place_british_pc: function (space) {
+ place_british_pc(space) {
push_undo()
place_british_pc(space)
--game.count
},
- pass: function () {
+ pass() {
clear_undo()
gen_british_pc_ops_end()
goto_start_year()
@@ -1152,7 +1123,7 @@ states.for_the_king = {
function automatic_victory() {
let n_american = 0
let n_british = 0
- for (let space in SPACES) {
+ for (let space of all_spaces) {
n_american += count_french_cu(space) + count_american_cu(space)
if (SPACES[space].colony !== "CA")
n_british += count_british_cu(space)
@@ -1183,10 +1154,10 @@ function goto_start_year() {
// Prisoner exchange
for (let g of BRITISH_GENERALS)
- if (game.generals[g].location === CAPTURED_GENERALS)
+ if (is_general_at_location(g, CAPTURED_GENERALS))
move_general(g, BRITISH_REINFORCEMENTS)
for (let g of AMERICAN_GENERALS)
- if (game.generals[g].location === CAPTURED_GENERALS)
+ if (is_general_at_location(g, CAPTURED_GENERALS))
move_general(g, AMERICAN_REINFORCEMENTS)
switch (game.year) {
@@ -1246,7 +1217,7 @@ function goto_start_year() {
}
states.british_declare_first = {
- prompt: function (current) {
+ prompt(current) {
view.prompt = "Declare yourself as the first player by playing a campaign card?"
gen_pass()
for (let c of CAMPAIGN_CARDS) {
@@ -1255,13 +1226,13 @@ states.british_declare_first = {
}
}
},
- card_campaign: function (c) {
+ card_campaign(c) {
delete game.congress_was_dispersed
logp("went first by playing a campaign card")
game.active = BRITISH
goto_campaign(c)
},
- pass: function () {
+ pass() {
if (game.congress_was_dispersed)
game.active = BRITISH
else
@@ -1272,16 +1243,16 @@ states.british_declare_first = {
}
states.choose_first_player = {
- prompt: function (current) {
+ prompt(current) {
view.prompt = "Choose who will play the first strategy card."
gen_action("american_first")
gen_action("british_first")
},
- american_first: function (c) {
+ american_first(c) {
logp("went first")
goto_strategy_phase(AMERICAN)
},
- british_first: function (c) {
+ british_first(c) {
logp("went first")
goto_strategy_phase(BRITISH)
},
@@ -1302,42 +1273,42 @@ function goto_strategy_phase(new_active) {
states.strategy_phase = {
inactive: "strategy phase",
- prompt: function (current) {
+ prompt(current) {
view.prompt = "Play a strategy card."
gen_strategy_plays(active_hand())
},
- card_campaign: function (c) {
+ card_campaign(c) {
game.did_discard_event = false
clear_queue()
goto_campaign(c)
},
- card_play_event: function (c) {
+ card_play_event(c) {
push_undo()
game.did_discard_event = false
clear_queue()
do_event(c)
},
- card_discard_event: function (c) {
+ card_discard_event(c) {
push_undo()
game.did_discard_event = true
clear_queue()
discard_card(c, "PC action")
game.state = "discard_event_pc_action"
},
- card_ops_pc: function (c) {
+ card_ops_pc(c) {
push_undo()
game.did_discard_event = false
clear_queue()
play_card(c, "for PC")
goto_ops_pc(CARDS[c].count)
},
- card_ops_reinforcements: function (c) {
+ card_ops_reinforcements(c) {
push_undo()
game.did_discard_event = false
clear_queue()
goto_ops_reinforcements(c)
},
- card_ops_queue: function (c) {
+ card_ops_queue(c) {
game.did_discard_event = false
play_card(c, "to queue")
if (game.active === BRITISH)
@@ -1346,12 +1317,12 @@ states.strategy_phase = {
game.a_queue += CARDS[c].count
end_strategy_card()
},
- card_ops_general: function (c) {
+ card_ops_general(c) {
push_undo()
game.did_discard_event = false
goto_ops_general(c)
},
- exchange_for_discard: function (c) {
+ exchange_for_discard(c) {
game.did_discard_event = false
let d = game.discard.pop()
discard_card(c)
@@ -1380,7 +1351,7 @@ function end_strategy_card() {
if (!game.french_alliance_triggered && game.french_alliance === 9) {
log("The French signed an alliance with the Americans!")
game.french_alliance_triggered = true
- if (game.french_navy === FRENCH_REINFORCEMENTS) {
+ if (game.french_navy === -1) {
game.save_active = game.active
game.active = AMERICAN
game.state = "place_french_navy_trigger"
@@ -1388,9 +1359,8 @@ function end_strategy_card() {
}
}
- game.moved = {}
- for (let cu of game.cu)
- cu.moved = 0
+ reset_moved_generals()
+ reset_moved_cu()
goto_strategy_phase(ENEMY[game.active])
@@ -1401,6 +1371,7 @@ function end_strategy_card() {
if (hand.length === 0)
return goto_winter_attrition_phase()
}
+
}
function clear_queue() {
@@ -1455,7 +1426,7 @@ function gen_strategy_plays(hand) {
/* DISCARD EVENT CARD FOR PC ACTION */
states.discard_event_pc_action = {
- prompt: function (current) {
+ prompt(current) {
view.prompt = "Place, flip, or remove PC marker."
gen_pass()
if (game.active === BRITISH)
@@ -1463,29 +1434,29 @@ states.discard_event_pc_action = {
else
gen_american_discard_event_pc_action()
},
- place_british_pc: function (space) {
+ place_british_pc(space) {
place_british_pc(space)
end_strategy_card()
},
- place_american_pc: function (space) {
+ place_american_pc(space) {
place_american_pc(space)
end_strategy_card()
},
- remove_pc: function (space) {
+ remove_pc(space) {
remove_pc(space)
end_strategy_card()
},
- flip_pc: function (space) {
+ flip_pc(space) {
flip_pc(space)
end_strategy_card()
},
- pass: function () {
+ pass() {
end_strategy_card()
},
}
function gen_british_discard_event_pc_action() {
- for (let space in SPACES) {
+ for (let space of all_spaces) {
if (is_adjacent_to_british_pc(space)) {
if (has_no_pc(space) && has_no_american_unit(space))
gen_action("place_british_pc", space)
@@ -1498,7 +1469,7 @@ function gen_british_discard_event_pc_action() {
}
function gen_american_discard_event_pc_action() {
- for (let space in SPACES) {
+ for (let space of all_spaces) {
if (is_adjacent_to_american_pc(space)) {
if (has_no_pc(space) && has_no_british_cu(space)) {
if (allowed_to_place_american_pc())
@@ -1522,7 +1493,7 @@ function goto_ops_pc(count) {
}
states.ops_pc = {
- prompt: function (current) {
+ prompt(current) {
view.prompt = "Place or flip PC markers. " + game.count + " left."
gen_pass()
if (game.count > 0) {
@@ -1532,22 +1503,22 @@ states.ops_pc = {
gen_american_pc_ops()
}
},
- place_british_pc: function (space) {
+ place_british_pc(space) {
push_undo()
place_british_pc(space)
--game.count
},
- place_american_pc: function (space) {
+ place_american_pc(space) {
push_undo()
place_american_pc(space)
--game.count
},
- flip_pc: function (space) {
+ flip_pc(space) {
push_undo()
flip_pc(space)
--game.count
},
- pass: function () {
+ pass() {
if (game.active === BRITISH)
gen_british_pc_ops_end()
end_strategy_card()
@@ -1556,7 +1527,7 @@ states.ops_pc = {
function gen_british_pc_ops_start() {
game.british_pc_space_list = []
- for (let space in SPACES) {
+ for (let space of all_spaces) {
if (has_no_pc(space) && has_no_american_unit(space)) {
if (is_adjacent_to_british_pc(space))
game.british_pc_space_list.push(space)
@@ -1567,7 +1538,7 @@ function gen_british_pc_ops_start() {
function gen_british_pc_ops() {
for (let space of game.british_pc_space_list)
gen_action("place_british_pc", space)
- for (let space in SPACES) {
+ for (let space of all_spaces) {
if (has_british_army(space)) {
if (has_no_pc(space))
gen_action("place_british_pc", space)
@@ -1582,7 +1553,7 @@ function gen_british_pc_ops_end(space) {
}
function gen_american_pc_ops() {
- for (let space in SPACES) {
+ for (let space of all_spaces) {
if (has_no_pc(space) && has_no_british_cu(space)) {
if (allowed_to_place_american_pc())
gen_action("place_american_pc", space)
@@ -1609,86 +1580,85 @@ function goto_ops_reinforcements(c) {
}
states.ops_british_reinforcements_who = {
- prompt: function (current) {
+ prompt(current) {
view.prompt = "Reinforcements: choose an available general or pass to bring only CU."
view.prompt += " Carrying " + game.count + " British CU."
gen_pass()
gen_british_reinforcements_who()
},
- drop_british_cu: function () {
+ drop_british_cu() {
--game.count
},
- pickup_british_cu: function () {
+ pickup_british_cu() {
++game.count
},
- select_general: function (g) {
+ select_general(g) {
push_undo()
game.state = "ops_british_reinforcements_where"
game.who = g
},
- pass: function () {
+ pass() {
push_undo()
game.state = "ops_british_reinforcements_where"
- game.who = null
+ game.who = NOBODY
},
}
states.ops_british_reinforcements_where = {
- prompt: function (current) {
+ prompt(current) {
view.prompt = "Reinforcements: choose a port space."
view.prompt += " Carrying " + game.count + " British CU."
gen_british_reinforcements_where()
},
- drop_british_cu: function () {
+ drop_british_cu() {
--game.count
},
- pickup_british_cu: function () {
+ pickup_british_cu() {
++game.count
},
- place_reinforcements: function (space) {
+ place_reinforcements(space) {
place_british_reinforcements(game.who, game.count, space)
end_strategy_card()
- game.who = null
+ game.who = NOBODY
},
}
states.ops_american_reinforcements_who = {
- prompt: function (current) {
+ prompt(current) {
view.prompt = "Reinforcements: choose an available general or pass to bring only CU."
gen_pass()
gen_american_reinforcements_who()
},
- select_general: function (g) {
+ select_general(g) {
push_undo()
game.state = "ops_american_reinforcements_where"
game.who = g
},
- pass: function () {
+ pass() {
push_undo()
game.state = "ops_american_reinforcements_where"
- game.who = null
+ game.who = NOBODY
},
}
states.ops_american_reinforcements_where = {
- prompt: function (current) {
+ prompt(current) {
view.prompt = "Reinforcements: choose a space."
gen_american_reinforcements_where(game.who)
},
- place_reinforcements: function (space) {
+ place_reinforcements(space) {
if (game.who === ROCHAMBEAU)
place_french_reinforcements(game.who, space)
else
place_american_reinforcements(game.who, game.count, space)
end_strategy_card()
- game.who = null
+ game.who = NOBODY
},
}
function gen_british_reinforcements_who() {
for (let g of BRITISH_GENERALS) {
- let general = game.generals[g]
- if (general.location === BRITISH_REINFORCEMENTS) {
+ if (is_general_at_location(g, BRITISH_REINFORCEMENTS)) {
gen_action("select_general", g)
}
}
@@ -1699,7 +1669,7 @@ function gen_british_reinforcements_who() {
}
function gen_british_reinforcements_where() {
- for (let space in SPACES) {
+ for (let space of all_spaces) {
if (is_non_blockaded_port(space))
if (!has_american_or_french_cu(space) && !has_american_pc(space))
gen_action("place_reinforcements", space)
@@ -1712,15 +1682,14 @@ function gen_british_reinforcements_where() {
function gen_american_reinforcements_who() {
for (let g of AMERICAN_GENERALS) {
- let general = game.generals[g]
- if (general.location === AMERICAN_REINFORCEMENTS) {
+ if (is_general_at_location(g, AMERICAN_REINFORCEMENTS)) {
gen_action("select_general", g)
}
}
}
function gen_american_reinforcements_where(general) {
- for (let space in SPACES) {
+ for (let space of all_spaces) {
if (!has_british_cu(space) && !has_british_pc(space)) {
if (general === ROCHAMBEAU) {
if (SPACES[space].port)
@@ -1747,7 +1716,7 @@ function goto_ops_general(c) {
}
states.ops_general_who = {
- prompt: function (current) {
+ prompt(current) {
if (game.campaign && game.landing_party)
view.prompt = "Campaign: Activate a general or use a landing party. " + game.campaign + " left."
else if (game.campaign)
@@ -1759,21 +1728,21 @@ states.ops_general_who = {
gen_activate_general()
gen_pass()
},
- place_british_pc: function (where) {
+ place_british_pc(where) {
game.landing_party = 0
place_british_pc(where)
end_strategy_card()
},
- flip_pc: function (where) {
+ flip_pc(where) {
game.landing_party = 0
flip_pc(where)
end_strategy_card()
},
- select_general: function (g) {
+ select_general(g) {
push_undo()
goto_ops_general_move(g, false)
},
- pass: function () {
+ pass() {
if (game.campaign > 0)
game.campaign = 0
end_strategy_card()
@@ -1781,7 +1750,7 @@ states.ops_general_who = {
}
function gen_landing_party() {
- for (let space in SPACES) {
+ for (let space of all_spaces) {
if (!is_fortified_port(space) && is_non_blockaded_port(space)) {
if (has_american_pc(space) && has_no_american_unit(space))
gen_action("flip_pc", space)
@@ -1800,13 +1769,13 @@ function gen_activate_general() {
function gen_activate_british_general() {
for (let g of BRITISH_GENERALS)
- if (is_general_on_map(g) && GENERALS[g].strategy <= game.count && !game.moved[g])
+ if (is_general_on_map(g) && GENERALS[g].strategy <= game.count && !has_general_moved(g))
gen_action("select_general", g)
}
function gen_activate_american_general() {
for (let g of AMERICAN_GENERALS)
- if (is_general_on_map(g) && GENERALS[g].strategy <= game.count && !game.moved[g])
+ if (is_general_on_map(g) && GENERALS[g].strategy <= game.count && !has_general_moved(g))
gen_action("select_general", g)
}
@@ -1816,11 +1785,11 @@ function goto_remove_general(where) {
}
states.remove_general = {
- prompt: function (current) {
+ prompt(current) {
view.prompt = "Remove a general to the reinforcements box."
gen_remove_general()
},
- select_general: function (g) {
+ select_general(g) {
if (game.active === BRITISH)
move_general(g, BRITISH_REINFORCEMENTS)
else
@@ -1834,11 +1803,11 @@ function goto_remove_general_after_intercept() {
}
states.remove_general_after_intercept = {
- prompt: function (current) {
+ prompt(current) {
view.prompt = "Remove a general to the reinforcements box."
gen_remove_general()
},
- select_general: function (g) {
+ select_general(g) {
if (game.active === BRITISH)
move_general(g, BRITISH_REINFORCEMENTS)
else
@@ -1853,11 +1822,11 @@ function goto_remove_general_after_retreat(where) {
}
states.remove_general_after_retreat = {
- prompt: function (current) {
+ prompt(current) {
view.prompt = "Remove a general to the reinforcements box."
gen_remove_general()
},
- select_general: function (g) {
+ select_general(g) {
if (game.active === BRITISH)
move_general(g, BRITISH_REINFORCEMENTS)
else
@@ -1875,14 +1844,14 @@ function gen_remove_general() {
function gen_remove_british_general() {
for (let g of BRITISH_GENERALS)
- if (location_of_general(g) === game.where)
+ if (is_general_at_location(g, game.where))
gen_action("select_general", g)
}
function gen_remove_american_general() {
for (let g of AMERICAN_GENERALS)
if (g !== WASHINGTON)
- if (location_of_general(g) === game.where)
+ if (is_general_at_location(g, game.where))
gen_action("select_general", g)
}
@@ -1909,7 +1878,7 @@ function goto_ops_general_move(g, marblehead) {
}
states.ops_general_move = {
- prompt: function (current) {
+ prompt(current) {
view.prompt = "Move " + game.who + " with "
if (game.carry_british > 0) {
view.prompt += game.carry_british + " British CU."
@@ -1940,45 +1909,45 @@ states.ops_general_move = {
gen_move_general()
},
- pickup_british_cu: function () {
+ pickup_british_cu() {
++game.carry_british
},
- pickup_american_cu: function () {
+ pickup_american_cu() {
++game.carry_american
},
- pickup_french_cu: function () {
+ pickup_french_cu() {
++game.carry_french
},
- drop_british_cu: function () {
+ drop_british_cu() {
push_undo()
--game.carry_british
- if (game.moved[game.who])
+ if (has_general_moved(game.who))
mark_moved_cu(BRITISH, location_of_general(game.who), 1)
},
- drop_american_cu: function () {
+ drop_american_cu() {
push_undo()
--game.carry_american
- if (game.moved[game.who])
+ if (has_general_moved(game.who))
mark_moved_cu(AMERICAN, location_of_general(game.who), 1)
},
- drop_french_cu: function () {
+ drop_french_cu() {
push_undo()
--game.carry_french
- if (game.moved[game.who])
+ if (has_general_moved(game.who))
mark_moved_cu(FRENCH, location_of_general(game.who), 1)
},
- move: function (to) {
+ move(to) {
push_undo()
- game.moved[game.who] = 1
+ set_general_moved(game.who)
let from = location_of_general(game.who)
let cu = game.carry_british + game.carry_american + game.carry_french
let intercept = false
if (game.active === BRITISH) {
- let is_sea_move = path_type(from, to) === undefined
+ let is_sea_move = path_type(from, to) === "sea"
if (has_american_pc(to) && cu > 0 && !is_sea_move && !has_british_cu(to))
intercept = can_intercept_to(to)
}
@@ -2002,7 +1971,7 @@ states.ops_general_move = {
else
resume_moving(from, to)
},
- pass: function () {
+ pass() {
clear_undo()
let where = location_of_general(game.who)
end_move()
@@ -2021,10 +1990,10 @@ function resume_moving(from, to) {
}
function can_intercept_to(to) {
- for (let space of SPACES[to].exits) {
+ for (let space of SPACES[to].adjacent) {
if (has_american_army(space)) {
let g = find_american_or_french_general(space)
- if (g && !game.moved[g])
+ if (g && !has_general_moved(g))
return true
}
}
@@ -2032,10 +2001,10 @@ function can_intercept_to(to) {
}
function gen_intercept() {
- for (let space of SPACES[game.where].exits) {
+ for (let space of SPACES[game.where].adjacent) {
if (has_american_army(space)) {
let g = find_american_or_french_general(space)
- if (g && !game.moved[g])
+ if (g && !has_general_moved(g))
gen_action("select_general", g)
}
}
@@ -2044,7 +2013,7 @@ function gen_intercept() {
function goto_intercept(from, where) {
clear_undo()
game.save_who = game.who
- game.who = null
+ game.who = NOBODY
game.from = from
game.where = where
game.active = AMERICAN
@@ -2052,13 +2021,13 @@ function goto_intercept(from, where) {
}
states.intercept = {
- prompt: function (current) {
+ prompt(current) {
view.prompt = "Intercept " + game.save_who + " in " + game.where + "?"
gen_pass()
gen_intercept()
},
- select_general: function (g) {
- game.moved[g] = 1
+ select_general(g) {
+ set_general_moved(g)
let die = roll_d6()
if (die <= GENERALS[g].agility) {
log(g + " intercepted (" + die + " <= " + GENERALS[g].agility + ")")
@@ -2085,7 +2054,7 @@ states.intercept = {
end_intercept()
}
},
- pass: function () {
+ pass() {
end_intercept()
},
}
@@ -2101,12 +2070,12 @@ function end_intercept() {
function end_move() {
let where = location_of_general(game.who)
- if (game.moved[game.who]) {
+ if (has_general_moved(game.who)) {
mark_moved_cu(BRITISH, where, game.carry_british)
mark_moved_cu(AMERICAN, where, game.carry_american)
mark_moved_cu(FRENCH, where, game.carry_french)
}
- game.who = null
+ game.who = NOBODY
delete game.mobility
delete game.carry_british
delete game.carry_american
@@ -2114,11 +2083,11 @@ function end_move() {
}
function path_type(from, to) {
- return PATH_TYPE[PATH_INDEX[from][to]]
-}
-
-function path_name(from, to) {
- return PATH_NAME[PATH_INDEX[from][to]]
+ if (set_has(SPACES[from].path, to))
+ return "path"
+ if (set_has(SPACES[from].wilderness, to))
+ return "wilderness"
+ return "sea"
}
function gen_carry_cu() {
@@ -2143,22 +2112,24 @@ function gen_carry_cu() {
function movement_cost(from, to) {
switch (path_type(from, to)) {
- case undefined:
+ case "sea":
return 4 /* must be a sea connection if no direct path */
case "wilderness":
return 3
- default:
+ case "path":
return 1
+ default:
+ throw "IMPOSSIBLE"
}
}
function gen_move_general() {
let from = location_of_general(game.who)
let alone = game.carry_british + game.carry_american + game.carry_french === 0
- for (let to of SPACES[from].exits) {
+ for (let to of SPACES[from].adjacent) {
let mp = 1
if (path_type(from, to) === "wilderness") {
- if (path_name(from, to) === FALMOUTH_QUEBEC)
+ if ((from === QUEBEC && to === FALMOUTH) || (to === QUEBEC && from === FALMOUTH))
if (game.who !== ARNOLD)
continue
mp = 3
@@ -2184,7 +2155,7 @@ function gen_move_general() {
}
if (game.active === BRITISH && game.count === 4) {
if (is_non_blockaded_port(from)) {
- for (let to in SPACES) {
+ for (let to of all_spaces) {
if (to !== from) {
if (is_non_blockaded_port(to)) {
if (!has_american_pc(to) && !has_american_or_french_cu(to)) {
@@ -2218,7 +2189,7 @@ events.the_war_ends = function (c, card) {
logp("played #" + c)
log("The war will end in " + card.year)
game.last_played = c
- remove_from_array(active_hand(), c)
+ array_remove_item(active_hand(), c)
if (game.war_ends)
game.discard.push(WAR_ENDS_1779 + game.war_ends - 1779)
game.war_ends = card.year
@@ -2284,7 +2255,7 @@ events.advance_french_alliance = function (c, card) {
events.remove_french_navy = function (c, card) {
play_card(c)
- game.french_navy = TURN_TRACK[game.year + 1]
+ game.french_navy = game.year + 1
end_strategy_card()
}
@@ -2296,19 +2267,19 @@ events.remove_british_pc_from = function (c, card) {
}
states.remove_british_pc_from = {
- prompt: function (current) {
+ prompt(current) {
view.prompt = "Remove British PC markers from " + game.where.join(", ") + ". " + game.count + " left."
gen_pass()
gen_remove_british_pc_from(game.where)
},
- remove_pc: function (where) {
+ remove_pc(where) {
remove_pc(where)
if (--game.count === 0) {
delete game.where
end_strategy_card()
}
},
- pass: function () {
+ pass() {
delete game.where
end_strategy_card()
},
@@ -2321,18 +2292,18 @@ events.remove_american_pc = function (c, card) {
}
states.remove_american_pc = {
- prompt: function (current) {
+ prompt(current) {
view.prompt = "Remove American PC markers. " + game.count + " left."
gen_pass()
gen_remove_american_pc()
},
- remove_pc: function (where) {
+ remove_pc(where) {
remove_pc(where)
if (--game.count === 0) {
end_strategy_card()
}
},
- pass: function () {
+ pass() {
end_strategy_card()
},
}
@@ -2345,19 +2316,19 @@ events.remove_american_pc_from = function (c, card) {
}
states.remove_american_pc_from = {
- prompt: function (current) {
+ prompt(current) {
view.prompt = "Remove American PC markers from " + game.where.join(", ") + ". " + game.count + " left."
gen_pass()
gen_remove_american_pc_from(game.where)
},
- remove_pc: function (where) {
+ remove_pc(where) {
remove_pc(where)
if (--game.count === 0) {
delete game.where
end_strategy_card()
}
},
- pass: function () {
+ pass() {
delete game.where
end_strategy_card()
},
@@ -2371,20 +2342,20 @@ events.remove_american_pc_from_non_port = function (c, card) {
}
states.remove_american_pc_from_non_port = {
- prompt: function (current) {
+ prompt(current) {
view.prompt =
"Remove American PC markers from non-Port space in " + game.where.join(", ") + ". " + game.count + " left."
gen_pass()
gen_remove_american_pc_from_non_port(game.where)
},
- remove_pc: function (where) {
+ remove_pc(where) {
remove_pc(where)
if (--game.count === 0) {
delete game.where
end_strategy_card()
}
},
- pass: function () {
+ pass() {
delete game.where
end_strategy_card()
},
@@ -2397,19 +2368,19 @@ events.remove_american_pc_within_two_spaces_of_a_british_general = function (c,
}
states.remove_american_pc_within_two_spaces_of_a_british_general = {
- prompt: function (current) {
+ prompt(current) {
view.prompt = "Remove American PC markers within two spaces of a British general. " + game.count + " left."
gen_pass()
gen_remove_american_pc_within_two_spaces_of_a_british_general()
},
- remove_pc: function (where) {
+ remove_pc(where) {
remove_pc(where)
if (--game.count === 0) {
delete game.where
end_strategy_card()
}
},
- pass: function () {
+ pass() {
delete game.where
end_strategy_card()
},
@@ -2422,18 +2393,18 @@ events.place_american_pc = function (c, card) {
}
states.place_american_pc = {
- prompt: function (current) {
+ prompt(current) {
view.prompt = "Place American PC markers. " + game.count + " left."
gen_pass()
gen_place_american_pc()
},
- place_american_pc: function (where) {
+ place_american_pc(where) {
place_american_pc(where)
if (--game.count === 0) {
end_strategy_card()
}
},
- pass: function () {
+ pass() {
end_strategy_card()
},
}
@@ -2446,19 +2417,19 @@ events.place_american_pc_in = function (c, card) {
}
states.place_american_pc_in = {
- prompt: function (current) {
+ prompt(current) {
view.prompt = "Place American PC markers in " + game.where.join(", ") + ". " + game.count + " left."
gen_pass()
gen_place_american_pc_in(game.where)
},
- place_american_pc: function (where) {
+ place_american_pc(where) {
place_american_pc(where)
if (--game.count === 0) {
delete game.where
end_strategy_card()
}
},
- pass: function () {
+ pass() {
delete game.where
end_strategy_card()
},
@@ -2468,32 +2439,32 @@ events.lord_sandwich_coastal_raids = function (c, card) {
play_card(c)
game.state = "lord_sandwich_coastal_raids"
game.count = 2
- game.where = null
+ game.where = NOWHERE
}
states.lord_sandwich_coastal_raids = {
- prompt: function (current) {
+ prompt(current) {
view.prompt = "Remove two or flip one American PC in a port space."
gen_pass()
gen_lord_sandwich_coastal_raids(game.where)
},
- place_british_pc: function (where) {
+ place_british_pc(where) {
place_british_pc(where)
end_strategy_card()
},
- remove_pc: function (where) {
+ remove_pc(where) {
game.where = where
remove_pc(where)
if (--game.count === 0)
end_strategy_card()
},
- pass: function () {
+ pass() {
end_strategy_card()
},
}
function gen_lord_sandwich_coastal_raids(first_removed) {
- for (let space in SPACES) {
+ for (let space of all_spaces) {
if (SPACES[space].port)
if (has_american_pc(space) && has_no_american_unit(space))
gen_action("remove_pc", space)
@@ -2508,23 +2479,23 @@ events.remove_american_cu = function (c, card) {
}
states.remove_american_cu = {
- prompt: function (current) {
+ prompt(current) {
view.prompt = "Remove one American CU from any space."
gen_pass()
gen_remove_american_cu()
},
- remove_cu: function (where) {
+ remove_cu(where) {
let cu = find_american_cu(where) || find_french_cu(where)
remove_cu(cu.owner, where, 1)
end_strategy_card()
},
- pass: function () {
+ pass() {
end_strategy_card()
},
}
function gen_remove_american_cu() {
- for (let space in SPACES) {
+ for (let space of all_spaces) {
if (has_american_or_french_cu(space))
gen_action("remove_cu", space)
}
@@ -2537,24 +2508,24 @@ events.remove_british_cu = function (c, card) {
}
states.remove_british_cu = {
- prompt: function (current) {
+ prompt(current) {
view.prompt = "Remove " + game.count + " British CU from any space."
gen_pass()
gen_remove_british_cu()
},
- remove_cu: function (where) {
+ remove_cu(where) {
let cu = find_british_cu(where)
remove_cu(cu.owner, where, 1)
if (--game.count === 0)
end_strategy_card()
},
- pass: function () {
+ pass() {
end_strategy_card()
},
}
function gen_remove_british_cu() {
- for (let space in SPACES) {
+ for (let space of all_spaces) {
if (has_british_cu(space))
gen_action("remove_cu", space)
}
@@ -2565,29 +2536,29 @@ events.pennsylvania_and_new_jersey_line_mutinies = function (c, card) {
game.pennsylvania_and_new_jersey_line_mutinies = true
game.state = "pennsylvania_and_new_jersey_line_mutinies"
game.count = 2
- game.where = null
+ game.where = NOWHERE
}
states.pennsylvania_and_new_jersey_line_mutinies = {
- prompt: function (current) {
+ prompt(current) {
view.prompt = "Remove two American CUs from the map, one each from two different spaces."
gen_pass()
gen_pennsylvania_and_new_jersey_line_mutinies(game.where)
},
- remove_cu: function (where) {
+ remove_cu(where) {
let cu = find_american_cu(where) || find_french_cu(where)
remove_cu(cu.owner, where, 1)
game.where = where
if (--game.count === 0)
end_strategy_card()
},
- pass: function () {
+ pass() {
end_strategy_card()
},
}
function gen_pennsylvania_and_new_jersey_line_mutinies(first_removed) {
- for (let space in SPACES) {
+ for (let space of all_spaces) {
if (has_american_or_french_cu(space))
if (space !== first_removed)
gen_action("remove_cu", space)
@@ -2601,11 +2572,11 @@ events.john_glovers_marblehead_regiment = function (c, card) {
}
states.john_glovers_marblehead_regiment_who = {
- prompt: function (current) {
+ prompt(current) {
view.prompt = "Activate an American general."
gen_activate_general()
},
- select_general: function (g) {
+ select_general(g) {
goto_ops_general_move(g, true)
},
}
@@ -2619,20 +2590,20 @@ events.declaration_of_independence = function (c, card) {
}
states.declaration_of_independence = {
- prompt: function (current) {
+ prompt(current) {
view.prompt = "Declaration of Independence: Place 1 PC marker in each of the 13 colonies. "
view.prompt += game.doi.length + " left."
gen_pass()
gen_place_american_pc_in(game.doi)
},
- place_american_pc: function (space) {
+ place_american_pc(space) {
let colony = SPACES[space].colony
- remove_from_array(game.doi, colony)
+ array_remove_item(game.doi, colony)
place_american_pc(space)
if (game.doi.length === 0)
end_declaration_of_independence()
},
- pass: function () {
+ pass() {
end_declaration_of_independence()
},
}
@@ -2656,18 +2627,18 @@ function goto_george_washington_captured() {
}
states.george_washington_captured = {
- prompt: function (current) {
+ prompt(current) {
view.prompt = "George Washington is captured! Remove American PC markers. " + game.count + " left."
gen_pass()
gen_remove_american_pc()
},
- remove_pc: function (where) {
+ remove_pc(where) {
remove_pc(where)
if (--game.count === 0) {
end_george_washington_captured()
}
},
- pass: function () {
+ pass() {
end_george_washington_captured()
},
}
@@ -2697,7 +2668,7 @@ function can_retreat_before_battle(where) {
return false
// can't retreat if attempted (successful or not) interception!
let g = find_american_or_french_general(where)
- if (g && !game.moved[g])
+ if (g && !has_general_moved(g))
return true
return false
}
@@ -2723,12 +2694,12 @@ function goto_retreat_before_battle() {
}
states.retreat_before_battle = {
- prompt: function (current) {
+ prompt(current) {
view.prompt = "Attempt retreat before battle?"
gen_pass()
gen_defender_retreat()
},
- move: function (to) {
+ move(to) {
let agility = GENERALS[game.who].agility
if (GENERALS[game.who].bonus)
agility += 2
@@ -2743,7 +2714,7 @@ states.retreat_before_battle = {
end_retreat_before_battle()
}
},
- pass: function () {
+ pass() {
end_retreat_before_battle()
},
}
@@ -2759,11 +2730,11 @@ function goto_remove_general_after_retreat_before_battle(to) {
}
states.remove_general_after_retreat_before_battle = {
- prompt: function (current) {
+ prompt(current) {
view.prompt = "Remove a general to the reinforcements box."
gen_remove_general()
},
- select_general: function (g) {
+ select_general(g) {
if (game.active === BRITISH)
move_general(g, BRITISH_REINFORCEMENTS)
else
@@ -2808,7 +2779,7 @@ function can_attacker_retreat() {
function gen_defender_retreat() {
let from = game.where
- for (let to of SPACES[from].exits) {
+ for (let to of SPACES[from].adjacent) {
if (can_defender_retreat(to))
gen_action("move", to)
}
@@ -2823,7 +2794,7 @@ function gen_defender_retreat() {
}
}
if (can_sea_retreat) {
- for (let to in SPACES) {
+ for (let to of all_spaces) {
if (to !== from && is_non_blockaded_port(to)) {
if (!has_american_pc(to) && !has_american_or_french_cu(to)) {
gen_action("move", to)
@@ -2840,7 +2811,7 @@ function gen_attacker_retreat() {
}
function end_retreat_before_battle() {
- game.who = null
+ game.who = NOBODY
goto_play_attacker_battle_card()
}
@@ -2850,12 +2821,12 @@ function goto_play_attacker_battle_card() {
}
states.play_attacker_battle_card = {
- prompt: function (current) {
+ prompt(current) {
view.prompt = "Attack: Play or discard event for DRM."
gen_pass()
gen_battle_card()
},
- card_battle_play: function (c) {
+ card_battle_play(c) {
play_card(c, "for +2 DRM")
if (game.active === BRITISH) {
if (CARDS[c].event === "remove_benedict_arnold")
@@ -2868,7 +2839,7 @@ states.play_attacker_battle_card = {
}
goto_play_defender_battle_card()
},
- card_battle_discard: function (c) {
+ card_battle_discard(c) {
discard_card(c, "for +1 DRM")
if (game.active === BRITISH) {
game.b_draw_after_battle = true
@@ -2879,7 +2850,7 @@ states.play_attacker_battle_card = {
}
goto_play_defender_battle_card()
},
- pass: function () {
+ pass() {
goto_play_defender_battle_card()
},
}
@@ -2890,12 +2861,12 @@ function goto_play_defender_battle_card() {
}
states.play_defender_battle_card = {
- prompt: function (current) {
+ prompt(current) {
view.prompt = "Defend: Play or discard event for DRM."
gen_pass()
gen_battle_card()
},
- card_battle_play: function (c) {
+ card_battle_play(c) {
play_card(c, "for +2 DRM")
if (game.active === BRITISH) {
if (CARDS[c].event === "remove_benedict_arnold")
@@ -2908,7 +2879,7 @@ states.play_defender_battle_card = {
}
resolve_battle()
},
- card_battle_discard: function (c) {
+ card_battle_discard(c) {
discard_card(c, "for +1 DRM")
if (game.active === BRITISH) {
game.b_draw_after_battle = true
@@ -2919,7 +2890,7 @@ states.play_defender_battle_card = {
}
resolve_battle()
},
- pass: function () {
+ pass() {
resolve_battle()
},
}
@@ -3005,7 +2976,7 @@ function apply_british_combat_losses(max) {
cu.count -= max
return max
}
- remove_from_array(game.cu, cu)
+ array_remove_item(game.cu, cu)
return cu.count
}
@@ -3016,7 +2987,7 @@ function apply_american_combat_losses(max) {
cu.count -= max
return max
}
- remove_from_array(game.cu, cu)
+ array_remove_item(game.cu, cu)
return cu.count
}
return 0
@@ -3029,7 +3000,7 @@ function apply_french_combat_losses(max) {
cu.count -= max
return max
}
- remove_from_array(game.cu, cu)
+ array_remove_item(game.cu, cu)
return cu.count
}
return 0
@@ -3176,11 +3147,11 @@ function resolve_battle() {
function goto_retreat_after_battle(victor) {
if (victor === BRITISH) {
game.who = find_american_or_french_general(game.where)
- if (game.who === null && count_american_and_french_cu(game.where) === 0)
+ if (game.who === NOBODY && count_american_and_french_cu(game.where) === 0)
return end_battle()
} else {
game.who = find_british_general(game.where)
- if (game.who === null && count_british_cu(game.where) === 0)
+ if (game.who === NOBODY && count_british_cu(game.where) === 0)
return end_battle()
}
game.active = ENEMY[victor]
@@ -3188,7 +3159,7 @@ function goto_retreat_after_battle(victor) {
}
states.retreat_after_battle = {
- prompt: function (current) {
+ prompt(current) {
view.prompt = "Retreat after battle."
gen_action("surrender")
if (game.active === game.attacker)
@@ -3196,7 +3167,7 @@ states.retreat_after_battle = {
else
gen_defender_retreat()
},
- move: function (to) {
+ move(to) {
logp("retreated to " + to)
if (game.active === BRITISH)
retreat_british_army(game.where, to)
@@ -3207,7 +3178,7 @@ states.retreat_after_battle = {
else
end_battle()
},
- surrender: function () {
+ surrender() {
// End battle here, so if Washington is captured we can handle the interrupt state.
let active = game.active
let where = game.where
@@ -3244,8 +3215,8 @@ function end_battle() {
delete game.attack_from
delete game.british_losses
delete game.attacker
- game.where = null
- game.who = null
+ game.where = NOWHERE
+ game.who = NOBODY
end_strategy_card()
}
@@ -3270,12 +3241,12 @@ function goto_winter_attrition_phase() {
logbr()
log("Winter Attrition")
- for (let space in SPACES) {
+ for (let space of all_spaces) {
let wq = is_winter_quarter_space(space)
let n_british = count_british_cu(space)
let n_american = count_american_cu(space)
let n_french = count_french_cu(space)
- let has_washington = game.generals[WASHINGTON].location === space
+ let has_washington = is_general_at_location(WASHINGTON, space)
if (n_british === 1 && !wq)
apply_single_winter_attrition(BRITISH, space, has_british_general(space))
@@ -3323,7 +3294,7 @@ function goto_winter_attrition_phase() {
}
function goto_french_naval_phase() {
- if (game.french_navy !== FRENCH_REINFORCEMENTS) {
+ if (game.french_navy !== -1) {
game.active = AMERICAN
game.state = "place_french_navy"
} else {
@@ -3342,11 +3313,11 @@ function gen_place_french_navy() {
}
states.place_french_navy_trigger = {
- prompt: function (current) {
+ prompt(current) {
view.prompt = "Place the French Navy in a blockade zone."
gen_place_french_navy()
},
- place_navy: function (zone) {
+ place_navy(zone) {
logp("placed French Navy.")
game.french_navy = zone
game.active = game.save_active
@@ -3356,11 +3327,11 @@ states.place_french_navy_trigger = {
}
states.place_french_navy = {
- prompt: function (current) {
+ prompt(current) {
view.prompt = "Place the French Navy in a blockade zone."
gen_place_french_navy()
},
- place_navy: function (zone) {
+ place_navy(zone) {
logp("placed French Navy.")
game.french_navy = zone
goto_political_control_phase()
@@ -3368,7 +3339,7 @@ states.place_french_navy = {
}
function place_pc_markers_segment() {
- for (let space in SPACES) {
+ for (let space of all_spaces) {
if (has_american_army(space)) {
if (has_no_pc(space))
place_american_pc(space)
@@ -3423,22 +3394,24 @@ function is_british_pc_path(space) {
}
function spread_american_path(seen, from) {
- for (let to of SPACES[from].exits) {
- if (to in seen)
+ // TODO: BFS
+ for (let to of SPACES[from].adjacent) {
+ if (set_has(seen, to))
continue
if (is_american_pc_path(to)) {
- seen[to] = 1
+ set_add(seen, to)
spread_american_path(seen, to)
}
}
}
function spread_british_path(seen, from) {
- for (let to of SPACES[from].exits) {
- if (to in seen)
+ // TODO: BFS
+ for (let to of SPACES[from].adjacent) {
+ if (set_has(seen, to))
continue
if (is_british_pc_path(to)) {
- seen[to] = 1
+ set_add(seen, to)
spread_british_path(seen, to)
}
}
@@ -3446,35 +3419,35 @@ function spread_british_path(seen, from) {
function remove_isolated_american_pc_segment() {
log("Removed isolated American PC")
- let seen = {}
- for (let space in SPACES) {
+ let seen = []
+ for (let space of all_spaces) {
if (is_american_pc_root(space)) {
- seen[space] = 1
+ set_add(seen, space)
spread_american_path(seen, space)
}
}
- for (let space in SPACES)
- if (has_american_pc(space) && !seen[space])
+ for (let space of all_spaces)
+ if (has_american_pc(space) && !set_has(seen, space))
remove_pc(space)
}
function remove_isolated_british_pc_segment() {
log("Removed isolated British PC")
- let seen = {}
- for (let space in SPACES) {
+ let seen = []
+ for (let space of all_spaces) {
if (is_british_pc_root(space)) {
- seen[space] = 1
+ set_add(seen, space)
spread_british_path(seen, space)
}
}
- for (let space in SPACES)
- if (has_british_pc(space) && !seen[space])
+ for (let space of all_spaces)
+ if (has_british_pc(space) && !set_has(seen, space))
remove_pc(space)
}
function gen_place_continental_congress() {
let n = 0
- for (let space in SPACES) {
+ for (let space of all_spaces) {
if (SPACES[space].colony !== "CA") {
if (has_american_pc(space) && has_no_british_playing_piece(space)) {
gen_action("place_continental_congress", space)
@@ -3495,21 +3468,22 @@ function goto_political_control_phase() {
}
states.return_continental_congress = {
- prompt: function () {
+ prompt() {
view.prompt = "Return Continental Congress to a space in the 13 colonies."
if (gen_place_continental_congress() === 0)
gen_pass()
},
- place_continental_congress: function (where) {
+ place_continental_congress(where) {
game.congress = where
goto_political_control_phase_2()
},
- pass: function () {
+ pass() {
goto_political_control_phase_2()
},
}
function goto_political_control_phase_2() {
+ // TODO: manually place and remove
place_pc_markers_segment()
remove_isolated_american_pc_segment()
remove_isolated_british_pc_segment()
@@ -3517,18 +3491,18 @@ function goto_political_control_phase_2() {
}
states.european_war = {
- prompt: function () {
+ prompt() {
view.prompt = "European War: Remove 2 British CU from any spaces. " + game.count + " left."
gen_pass()
gen_remove_british_cu()
},
- remove_cu: function (where) {
+ remove_cu(where) {
let cu = find_british_cu(where)
remove_cu(BRITISH, where, 1)
if (--game.count === 0)
goto_end_phase()
},
- pass: function () {
+ pass() {
goto_end_phase()
},
}
@@ -3537,8 +3511,8 @@ function norths_government_falls() {
update_colony_control()
let n_american = 0
- for (let c in COLONIES)
- if (game.control[c] === AMERICAN)
+ for (let c = 0; c <= 13; ++C)
+ if (game.control[c] === PC_AMERICAN)
++n_american
if (n_american >= 7)
@@ -3573,7 +3547,7 @@ function goto_end_phase() {
}
states.game_over = {
- prompt: function () {
+ prompt() {
view.prompt = game.victory
},
}
@@ -3659,3 +3633,347 @@ exports.view = function (state, current) {
return view
}
+
+
+// === COMMON LIBRARY ===
+
+function log(s) {
+ game.log.push(s)
+}
+
+function logp(s) {
+ game.log.push(game.active + " " + s)
+}
+
+function logbr(s) {
+ if (game.log.length > 0 && game.log[game.log.length - 1] !== "")
+ game.log.push("")
+}
+
+function clear_undo() {
+ if (game.undo) {
+ game.undo.length = 0
+ }
+}
+
+function push_undo() {
+ if (game.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() {
+ if (game.undo) {
+ let save_log = game.log
+ let save_undo = game.undo
+ game = save_undo.pop()
+ save_log.length = game.log
+ game.log = save_log
+ game.undo = save_undo
+ }
+}
+
+function random(range) {
+ // An MLCG using integer arithmetic with doubles.
+ // https://www.ams.org/journals/mcom/1999-68-225/S0025-5718-99-00996-5/S0025-5718-99-00996-5.pdf
+ // m = 2**35 − 31
+ return (game.seed = game.seed * 200105 % 34359738337) % range
+}
+
+function random_bigint(range) {
+ // Largest MLCG that will fit its state in a double.
+ // Uses BigInt for arithmetic, so is an order of magnitude slower.
+ // https://www.ams.org/journals/mcom/1999-68-225/S0025-5718-99-00996-5/S0025-5718-99-00996-5.pdf
+ // m = 2**53 - 111
+ return (game.seed = Number(BigInt(game.seed) * 5667072534355537n % 9007199254740881n)) % range
+}
+
+function shuffle(list) {
+ // Fisher-Yates shuffle
+ for (let i = list.length - 1; i > 0; --i) {
+ let j = random(i + 1)
+ let tmp = list[j]
+ list[j] = list[i]
+ list[i] = tmp
+ }
+}
+
+function shuffle_bigint(list) {
+ // Fisher-Yates shuffle
+ for (let i = list.length - 1; i > 0; --i) {
+ let j = random_bigint(i + 1)
+ let tmp = list[j]
+ list[j] = list[i]
+ list[i] = tmp
+ }
+}
+
+// Fast deep copy for objects without cycles
+function object_copy(original) {
+ if (Array.isArray(original)) {
+ let n = original.length
+ let copy = new Array(n)
+ for (let i = 0; i < n; ++i) {
+ let v = original[i]
+ if (typeof v === "object" && v !== null)
+ copy[i] = object_copy(v)
+ else
+ copy[i] = v
+ }
+ return copy
+ } else {
+ let copy = {}
+ for (let i in original) {
+ let v = original[i]
+ if (typeof v === "object" && v !== null)
+ copy[i] = object_copy(v)
+ else
+ copy[i] = v
+ }
+ return copy
+ }
+}
+
+// Array remove and insert (faster than splice)
+
+function array_remove(array, index) {
+ let n = array.length
+ for (let i = index + 1; i < n; ++i)
+ array[i - 1] = array[i]
+ array.length = n - 1
+}
+
+function array_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_insert(array, index, item) {
+ for (let i = array.length; i > index; --i)
+ array[i] = array[i - 1]
+ array[index] = item
+}
+
+function array_remove_pair(array, index) {
+ let n = array.length
+ for (let i = index + 2; i < n; ++i)
+ array[i - 2] = array[i]
+ array.length = n - 2
+}
+
+function array_insert_pair(array, index, key, value) {
+ for (let i = array.length; i > index; i -= 2) {
+ array[i] = array[i-2]
+ array[i+1] = array[i-1]
+ }
+ array[index] = key
+ array[index+1] = value
+}
+
+// Set as plain sorted array
+
+function set_clear(set) {
+ set.length = 0
+}
+
+function set_has(set, item) {
+ let a = 0
+ let b = set.length - 1
+ while (a <= b) {
+ let m = (a + b) >> 1
+ let x = set[m]
+ if (item < x)
+ b = m - 1
+ else if (item > x)
+ a = m + 1
+ else
+ return true
+ }
+ return false
+}
+
+function set_add(set, item) {
+ let a = 0
+ let b = set.length - 1
+ while (a <= b) {
+ let m = (a + b) >> 1
+ let x = set[m]
+ if (item < x)
+ b = m - 1
+ else if (item > x)
+ a = m + 1
+ else
+ return
+ }
+ array_insert(set, a, item)
+}
+
+function set_delete(set, item) {
+ let a = 0
+ let b = set.length - 1
+ while (a <= b) {
+ let m = (a + b) >> 1
+ let x = set[m]
+ if (item < x)
+ b = m - 1
+ else if (item > x)
+ a = m + 1
+ else {
+ array_remove(set, m)
+ return
+ }
+ }
+}
+
+function set_toggle(set, item) {
+ let a = 0
+ let b = set.length - 1
+ while (a <= b) {
+ let m = (a + b) >> 1
+ let x = set[m]
+ if (item < x)
+ b = m - 1
+ else if (item > x)
+ a = m + 1
+ else {
+ array_remove(set, m)
+ return
+ }
+ }
+ array_insert(set, a, item)
+}
+
+// Map as plain sorted array of key/value pairs
+
+function map_clear(map) {
+ map.length = 0
+}
+
+function map_has(map, key) {
+ let a = 0
+ let b = (map.length >> 1) - 1
+ while (a <= b) {
+ let m = (a + b) >> 1
+ let x = map[m<<1]
+ if (key < x)
+ b = m - 1
+ else if (key > x)
+ a = m + 1
+ else
+ return true
+ }
+ return false
+}
+
+function map_get(map, key, missing) {
+ let a = 0
+ let b = (map.length >> 1) - 1
+ while (a <= b) {
+ let m = (a + b) >> 1
+ let x = map[m<<1]
+ if (key < x)
+ b = m - 1
+ else if (key > x)
+ a = m + 1
+ else
+ return map[(m<<1)+1]
+ }
+ return missing
+}
+
+function map_set(map, key, value) {
+ let a = 0
+ let b = (map.length >> 1) - 1
+ while (a <= b) {
+ let m = (a + b) >> 1
+ let x = map[m<<1]
+ if (key < x)
+ b = m - 1
+ else if (key > x)
+ a = m + 1
+ else {
+ map[(m<<1)+1] = value
+ return
+ }
+ }
+ array_insert_pair(map, a<<1, key, value)
+}
+
+function map_delete(map, 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 {
+ array_remove_pair(map, m<<1)
+ return
+ }
+ }
+}
+
+function object_diff(a, b) {
+ if (a === b)
+ return false
+ if (a !== null && b !== null && typeof a === "object" && typeof b === "object") {
+ if (Array.isArray(a)) {
+ if (!Array.isArray(b))
+ return true
+ let a_length = a.length
+ if (b.length !== a_length)
+ return true
+ for (let i = 0; i < a_length; ++i)
+ if (object_diff(a[i], b[i]))
+ return true
+ return false
+ }
+ for (let key in a)
+ if (object_diff(a[key], b[key]))
+ return true
+ for (let key in b)
+ if (!(key in a))
+ return true
+ return false
+ }
+ return true
+}
+
+// same as Object.groupBy
+function object_group_by(items, callback) {
+ let groups = {}
+ if (typeof callback === "function") {
+ for (let item of items) {
+ let key = callback(item)
+ if (key in groups)
+ groups[key].push(item)
+ else
+ groups[key] = [ item ]
+ }
+ } else {
+ for (let item of items) {
+ let key = item[callback]
+ if (key in groups)
+ groups[key].push(item)
+ else
+ groups[key] = [ item ]
+ }
+ }
+ return groups
+}