diff options
Diffstat (limited to 'rules.js')
-rw-r--r-- | rules.js | 1248 |
1 files changed, 783 insertions, 465 deletions
@@ -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 +} |