summaryrefslogtreecommitdiff
path: root/rules.js
diff options
context:
space:
mode:
Diffstat (limited to 'rules.js')
-rw-r--r--rules.js3509
1 files changed, 3509 insertions, 0 deletions
diff --git a/rules.js b/rules.js
new file mode 100644
index 0000000..09f5a11
--- /dev/null
+++ b/rules.js
@@ -0,0 +1,3509 @@
+"use strict";
+
+// TODO: campaign messed up who is who after battle
+// TODO: retreat with 0 CU after battle
+
+exports.scenarios = [
+ "Historical"
+];
+
+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 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 CAMPAIGN_CARDS = [ 67, 68, 69, 70 ];
+const DECLARATION_OF_INDEPENDENCE = 99;
+const BARON_VON_STEUBEN = 86;
+const WAR_ENDS_1779 = 71;
+const BENJAMIN_FRANKLIN = 101;
+
+const ENEMY = { "American": BRITISH, "British": AMERICAN };
+
+const default_options = {};
+
+let states = {};
+let events = {};
+
+let game;
+
+function random(n) {
+ return Math.floor(((game.seed = game.seed * 48271 % 0x7fffffff) / 0x7fffffff) * n);
+}
+
+function log(s) {
+ game.log.push(s);
+}
+
+function clear_undo() {
+ game.undo = [];
+}
+
+function push_undo() {
+ game.undo.push(JSON.stringify(game, (k,v) => {
+ if (k === 'undo') return undefined;
+ if (k === 'log') return v.length;
+ return v;
+ }));
+}
+
+function pop_undo() {
+ let undo = game.undo;
+ let save_log = game.log;
+ Object.assign(game, JSON.parse(undo.pop()));
+ game.undo = undo;
+ save_log.length = game.log;
+ game.log = save_log;
+}
+
+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",
+ french_alliance: 0,
+ french_alliance_triggered: false,
+ european_war: false,
+ french_navy: FRENCH_REINFORCEMENTS,
+ regulars: true,
+ war_ends: 0,
+ played_british_reinforcements: 0,
+ played_american_reinforcements: [],
+
+ pc: {},
+ generals: {},
+ moved: {},
+ cu: [],
+ control: {},
+
+ deck: create_deck(),
+ discard: [],
+ reshuffle: false,
+ a_hand: [],
+ b_hand: [],
+ a_queue: 0,
+ b_queue: 0,
+
+ log: [],
+ undo: [],
+
+ prompt: null,
+ actions: [],
+ }
+
+ function spawn_unit(owner, location, pc, cu, name) {
+ 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,
+ });
+ }
+ }
+
+ function british(place, pc, cu, ld) { spawn_unit(BRITISH, place, pc, cu, ld); }
+ function american(place, pc, cu, ld) { spawn_unit(AMERICAN, place, pc, cu, ld); }
+ function french(place, pc, cu, ld) { 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);
+
+ american("Lexington Concord", true, 5, "Washington");
+ american("Newport", false, 2, "Greene");
+ american("Charleston", true, 2);
+ american("Philadelphia", true);
+
+ british(BRITISH_REINFORCEMENTS, false, 0, "Burgoyne");
+ british(BRITISH_REINFORCEMENTS, false, 0, "Clinton");
+ british(BRITISH_REINFORCEMENTS, false, 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");
+
+ french(FRENCH_REINFORCEMENTS, false, 5, "Rochambeau");
+
+ goto_committees_of_correspondence();
+}
+
+/* GAME STATE */
+
+function create_deck() {
+ let deck = [];
+ for (let i = 1; i <= 110; ++i) {
+ // No DoI or Baron von Steuben first year.
+ if (i == DECLARATION_OF_INDEPENDENCE || i == BARON_VON_STEUBEN)
+ continue;
+ deck.push(i);
+ }
+ return deck;
+}
+
+function reshuffle_deck() {
+ game.log.push("The deck is reshuffled.");
+ game.reshuffle = false;
+ game.deck = game.deck.concat(game.discard);
+ game.discard = [];
+}
+
+function roll_d6() {
+ return random(6) + 1;
+}
+
+function deal_card() {
+ if (game.deck.length == 0)
+ reshuffle_deck();
+ let i = random(game.deck.length);
+ let c = game.deck[i];
+ game.deck.splice(i, 1);
+ return c;
+}
+
+function last_discard() {
+ if (game.discard.length > 0)
+ return game.discard[game.discard.length-1];
+ return null;
+}
+
+function active_hand() {
+ return (game.active == AMERICAN) ? game.a_hand : game.b_hand;
+}
+
+function play_card(c, reason) {
+ if (reason)
+ game.log.push(game.active[0] + " plays [" + c + ": " + CARDS[c].title + "] " + reason);
+ else
+ game.log.push(game.active[0] + " plays [" + c + ": " + CARDS[c].title + "]");
+ if (CARDS[c].reshuffle == 'if_played')
+ game.reshuffle = true;
+ remove_from_array(active_hand(), c);
+ game.last_played = c;
+ if (!CARDS[c].once)
+ game.discard.push(c);
+ else
+ game.log.push("Card " + c + " removed from game.");
+}
+
+function discard_card_from_hand(hand, c) {
+ remove_from_array(hand, c);
+ game.discard.push(c);
+ if (CARDS[c].reshuffle == 'if_discarded')
+ game.reshuffle = true;
+ game.log.push(game.active[0] + " discards [" + c + ": " + CARDS[c].title + "]");
+}
+
+function discard_card(c, reason) {
+ game.last_played = c;
+ discard_card_from_hand(active_hand(), c);
+ if (reason)
+ game.log.push(game.active[0] + " discards [" + c + ": " + CARDS[c].title + "] " + reason);
+ else
+ game.log.push(game.active[0] + " discards [" + c + ": " + CARDS[c].title + "]");
+}
+
+function can_exchange_for_discard(c) {
+ if (game.did_discard_event) {
+ if (game.active == BRITISH)
+ return true;
+ return CARDS[c].count > 1;
+ }
+ return false;
+}
+
+function can_play_event(c) {
+ let card = CARDS[c];
+ switch (card.when) {
+ case 'before_french_alliance': return !game.french_alliance_triggered;
+ case 'after_french_alliance': return game.french_alliance_triggered;
+ case 'european_war_in_effect': return game.european_war;
+ }
+ return true;
+}
+
+function can_play_reinforcements() {
+ if (game.active == BRITISH) {
+ 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)
+ ++n;
+ return n > 0;
+ }
+ return false;
+ }
+ if (game.active == AMERICAN)
+ return game.played_american_reinforcements.length < 2;
+ return false;
+}
+
+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;
+}
+
+function is_fortified_port(where) {
+ return SPACES[where].type == 'fortified-port';
+}
+
+function is_continental_congress_dispersed() {
+ return game.congress == CONTINENTAL_CONGRESS_DISPERSED;
+}
+
+function is_winter_quarter_space(where) {
+ let colony = SPACES[where].colony;
+ if (colony == 'GA' || colony == 'SC' || colony == 'NC')
+ return true; // south of winter attrition line
+ let type = SPACES[where].type;
+ if (type == 'winter-quarters' || type == 'fortified-port')
+ return true;
+ return false;
+}
+
+function allowed_to_place_american_pc() {
+ if (is_continental_congress_dispersed())
+ return false;
+ if (game.pennsylvania_and_new_jersey_line_mutinies)
+ return false;
+ return true;
+}
+
+function is_british_militia(space) {
+ return game.control[SPACES[space].colony] == BRITISH;
+}
+
+function is_american_militia(space) {
+ return game.control[SPACES[space].colony] == AMERICAN;
+}
+
+function is_american_winter_offensive() {
+ if (game.who == WASHINGTON && game.a_hand.length == 0)
+ return true;
+ return false;
+}
+
+/* PC */
+
+function has_no_pc(space) {
+ return game.pc[space] != BRITISH && game.pc[space] != AMERICAN;
+}
+
+function has_british_pc(space) {
+ return game.pc[space] == BRITISH;
+}
+
+function has_american_pc(space) {
+ return game.pc[space] == AMERICAN;
+}
+
+function has_enemy_pc(space) {
+ if (game.active == BRITISH)
+ return has_american_pc(space);
+ else
+ return has_british_pc(space);
+}
+
+function has_no_pc(space) {
+ return game.pc[space] != BRITISH && game.pc[space] != AMERICAN;
+}
+
+function is_adjacent_to_british_pc(a) {
+ for (let b of SPACES[a].exits)
+ if (has_british_pc(b))
+ return true;
+ if (SPACES[a].port) {
+ for (let b in SPACES) {
+ if (SPACES[b].port)
+ if (has_british_pc(b))
+ return true;
+ }
+ }
+ return false;
+}
+
+function is_adjacent_to_american_pc(a) {
+ for (let b of SPACES[a].exits)
+ if (has_american_pc(b))
+ return true;
+ return false;
+}
+
+function place_british_pc(space) {
+ game.log.push("B places PC in " + space);
+ if (game.british_pc_space_list)
+ remove_from_array(game.british_pc_space_list, space);
+ game.pc[space] = BRITISH;
+}
+
+function place_american_pc(space) {
+ game.log.push("A places PC in " + space);
+ game.pc[space] = AMERICAN;
+}
+
+function remove_pc(space) {
+ if (game.active == BRITISH)
+ game.log.push("B removes PC in " + space);
+ else
+ game.log.push("A removes PC in " + space);
+ game.pc[space] = undefined;
+}
+
+function flip_pc(space) {
+ if (game.active == BRITISH)
+ game.log.push("B flips PC in " + space);
+ else
+ game.log.push("A flips PC in " + space);
+ game.pc[space] = ENEMY[game.pc[space]];
+}
+
+function update_colony_control() {
+ for (let c in COLONIES) {
+ let control = 0;
+ for (let space of COLONIES[c]) {
+ if (game.pc[space] == BRITISH)
+ --control;
+ else if (game.pc[space] == AMERICAN)
+ ++control;
+ }
+ if (control < 0)
+ game.control[c] = BRITISH;
+ else if (control > 0)
+ game.control[c] = AMERICAN;
+ else
+ game.control[c] = undefined;
+ }
+}
+
+/* CU */
+
+function find_cu(owner, space) {
+ for (let i = 0; i < game.cu.length; ++i) {
+ let cu = game.cu[i];
+ if (cu.location == space && cu.owner == owner)
+ return cu;
+ }
+ return null;
+}
+
+function find_british_cu(space) {
+ return find_cu(BRITISH, space);
+}
+
+function find_american_cu(space) {
+ return find_cu(AMERICAN, space);
+}
+
+function find_french_cu(space) {
+ return find_cu(FRENCH, space);
+}
+
+function has_british_cu(space) {
+ return find_british_cu(space) != null;
+}
+
+function has_no_british_cu(space) {
+ return !has_british_cu(space);
+}
+
+function has_american_or_french_cu(space) {
+ return find_american_cu(space) != null || find_french_cu(space) != null;
+}
+
+function has_american_cu(space) {
+ return find_american_cu(space) != null;
+}
+
+function has_french_cu(space) {
+ return find_french_cu(space) != null;
+}
+
+function has_enemy_cu(where) {
+ if (game.active == BRITISH)
+ return has_american_or_french_cu(where);
+ else
+ return has_british_cu(where);
+}
+
+function count_cu(owner, space) {
+ let cu = find_cu(owner, space);
+ return cu ? cu.count : 0;
+}
+
+function count_british_cu(where) {
+ let cu = find_british_cu(where);
+ return cu ? cu.count : 0;
+}
+
+function count_american_cu(where) {
+ let cu = find_american_cu(where);
+ return cu ? cu.count : 0;
+}
+
+function count_french_cu(where) {
+ let cu = find_french_cu(where);
+ return cu ? cu.count : 0;
+}
+
+function count_unmoved_british_cu(where) {
+ let cu = find_british_cu(where);
+ return cu ? cu.count - cu.moved : 0;
+}
+
+function count_unmoved_american_cu(where) {
+ let cu = find_american_cu(where);
+ return cu ? cu.count - cu.moved : 0;
+}
+
+function count_unmoved_french_cu(where) {
+ let cu = find_french_cu(where);
+ return cu ? cu.count - cu.moved : 0;
+}
+
+function mark_moved_cu(owner, space, moved) {
+ if (moved > 0)
+ find_cu(owner, space).moved += moved;
+}
+
+function count_american_and_french_cu(where) {
+ return count_american_cu(where) + count_french_cu(where);
+}
+
+function count_enemy_cu(where) {
+ if (game.active == BRITISH)
+ return count_american_and_french_cu(where);
+ else
+ return count_british_cu(where);
+}
+
+function spawn_cu(owner, where, count) {
+ game.cu.push({owner:owner, location:where, count:count, moved:0});
+}
+
+function remove_cu(owner, where, count) {
+ if (count == 0)
+ return;
+ let cu = find_cu(owner, where);
+ if (count >= cu.count) {
+ let i = game.cu.indexOf(cu);
+ remove_from_array(game.cu, cu);
+ } else {
+ cu.count -= count;
+ }
+}
+
+function place_cu(owner, where, count) {
+ let cu = find_cu(owner, where);
+ if (!cu)
+ spawn_cu(owner, where, count);
+ else
+ cu.count += count;
+}
+
+function place_british_cu(where, count) {
+ place_cu(BRITISH, where, count);
+}
+
+function place_american_cu(where, count) {
+ place_cu(AMERICAN, where, count);
+}
+
+function place_french_cu(where, count) {
+ place_cu(FRENCH, where, count);
+}
+
+function move_cu(owner, from, to, count) {
+ if (count == 0)
+ return;
+ let from_cu = find_cu(owner, from);
+ if (count < from_cu.count) {
+ from_cu.count -= count;
+ place_cu(owner, to, count);
+ } else {
+ let to_cu = find_cu(owner, to);
+ if (to_cu) {
+ remove_cu(owner, from, from_cu.count);
+ to_cu.count += count;
+ } else {
+ from_cu.location = to;
+ }
+ }
+}
+
+function move_british_cu(from, to, count) {
+ move_cu(BRITISH, from, to, count);
+}
+
+/* GENERALS */
+
+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;
+}
+
+function find_british_general(where) {
+ for (let general of BRITISH_GENERALS)
+ if (game.generals[general].location == where)
+ return general;
+ return null;
+}
+
+function find_american_or_french_general(where) {
+ for (let general of AMERICAN_GENERALS)
+ if (game.generals[general].location == where)
+ return general;
+ return null;
+}
+
+function has_british_general(where) {
+ return find_british_general(where) != null;
+}
+
+function has_american_or_french_general(where) {
+ return find_american_or_french_general(where) != null;
+}
+
+function has_enemy_general(where) {
+ if (game.active == BRITISH)
+ return has_american_or_french_general(where);
+ else
+ return has_british_general(where);
+}
+
+function count_friendly_generals(where) {
+ let list;
+ if (game.active == BRITISH)
+ list = BRITISH_GENERALS;
+ else
+ list = AMERICAN_GENERALS;
+ let count = 0;
+ for (let g of list)
+ if (location_of_general(g) == where)
+ ++count;
+ return count;
+}
+
+function can_activate_general(c) {
+ if (game.active == BRITISH)
+ return can_activate_british_general(c);
+ else
+ return can_activate_american_general(c);
+}
+
+function can_activate_british_general(c) {
+ let ops = CARDS[c].count + game.b_queue;
+ for (let g of BRITISH_GENERALS)
+ if (is_general_on_map(g) && GENERALS[g].strategy <= ops)
+ return true;
+ return false;
+}
+
+function can_activate_american_general(c) {
+ let ops = CARDS[c].count + game.a_queue;
+ for (let g of AMERICAN_GENERALS)
+ if (is_general_on_map(g) && GENERALS[g].strategy <= ops)
+ return true;
+ return false;
+}
+
+function move_general(who, where) {
+ game.generals[who].location = where;
+}
+
+function capture_washington() {
+ game.generals[WASHINGTON].location = null;
+
+ if (!game.french_alliance_triggered) {
+ game.french_alliance -= 3;
+ if (game.french_alliance < 0)
+ game.french_alliance = 0;
+ }
+
+ goto_george_washington_captured();
+}
+
+function capture_british_general(where) {
+ let g = find_british_general(where);
+ game.log.push(g + " is captured!");
+ move_general(g, CAPTURED_GENERALS);
+}
+
+function capture_american_or_french_general(where) {
+ let g = find_american_or_french_general(where);
+ game.log.push(g + " is captured!");
+ if (g == WASHINGTON)
+ capture_washington();
+ else
+ move_general(g, CAPTURED_GENERALS);
+}
+
+function capture_enemy_general(where) {
+ if (game.active == BRITISH)
+ capture_american_or_french_general(where);
+ else
+ capture_british_general(where);
+}
+
+function remove_benedict_arnold() {
+ if (game.generals[ARNOLD].location) {
+ game.log.push("Arnold is removed from the game!");
+ game.generals[ARNOLD].location = null;
+ }
+}
+
+/* ARMIES */
+
+function has_british_army(where) {
+ return has_british_general(where) && has_british_cu(where);
+}
+
+function has_american_army(where) {
+ return has_american_or_french_general(where) && has_american_or_french_cu(where);
+}
+
+function has_no_british_playing_piece(where) {
+ if (has_british_pc(where))
+ return false;
+ if (has_british_general(where))
+ return false;
+ if (has_british_cu(where))
+ return false;
+ return true;
+}
+
+function has_no_american_unit(where) {
+ if (has_american_or_french_general(where))
+ return false;
+ if (has_american_or_french_cu(where))
+ return false;
+ if (game.congress == where)
+ return false;
+ return true;
+}
+
+function place_british_reinforcements(who, count, where) {
+ let already_there = find_british_general(where);
+ if (who && already_there) {
+ move_general(already_there, BRITISH_REINFORCEMENTS);
+ }
+ if (who) {
+ game.log.push("B reinforces " + where + " with " + who);
+ move_general(who, where);
+ }
+ if (count > 0) {
+ game.log.push("B reinforces " + where + " with " + count + " CU");
+ move_british_cu(BRITISH_REINFORCEMENTS, where, count);
+ }
+}
+
+function place_american_reinforcements(who, count, where) {
+ let already_there = find_american_or_french_general(where);
+ if (who && already_there) {
+ // Never replace Washington
+ if (already_there == WASHINGTON)
+ who = null;
+ else
+ move_general(already_there, AMERICAN_REINFORCEMENTS);
+ }
+ if (who) {
+ game.log.push("A reinforces " + where + " with " + who);
+ move_general(who, where);
+ }
+ game.log.push("A reinforces " + where + " with " + count + " CU");
+ place_american_cu(where, count);
+}
+
+function place_french_reinforcements(who, where) {
+ let already_there = find_american_or_french_general(where);
+ if (who && already_there) {
+ // Never replace Washington
+ if (already_there == WASHINGTON)
+ who = null;
+ else
+ move_general(already_there, AMERICAN_REINFORCEMENTS);
+ }
+ if (who) {
+ game.log.push("A reinforces " + where + " with " + who);
+ move_general(who, where);
+ }
+ game.log.push("A reinforces " + where + " with the French CU");
+ move_cu(FRENCH, FRENCH_REINFORCEMENTS, where, count_french_cu(FRENCH_REINFORCEMENTS));
+ 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)
+ game.carry_british = 5;
+ game.carry_american = 0;
+ game.carry_french = 0;
+}
+
+function pickup_max_american_cu(where) {
+ game.carry_british = 0;
+ game.carry_french = count_unmoved_french_cu(where);
+ game.carry_american = count_unmoved_american_cu(where);
+ if (game.carry_french > 5)
+ game.carry_french = 5;
+ if (game.carry_american + game.carry_french > 5)
+ game.carry_american = 5 - game.carry_french;
+}
+
+function move_army(who, from, to) {
+ game.count -= movement_cost(from, to);
+ if (game.mobility && has_enemy_cu(to)) {
+ game.mobility = false;
+ game.count -= 1;
+ }
+ if (game.carry_british > 0)
+ move_cu(BRITISH, from, to, game.carry_british);
+ if (game.carry_american > 0)
+ move_cu(AMERICAN, from, to, game.carry_american);
+ if (game.carry_french > 0)
+ move_cu(FRENCH, from, to, game.carry_french);
+ move_general(who, to);
+}
+
+function intercept_army(who, from, to) {
+ if (game.carry_british > 0)
+ move_cu(BRITISH, from, to, game.carry_british);
+ if (game.carry_american > 0)
+ move_cu(AMERICAN, from, to, game.carry_american);
+ if (game.carry_french > 0)
+ move_cu(FRENCH, from, to, game.carry_french);
+ move_general(who, to);
+}
+
+function overrun(where) {
+ game.log.push(game.active[0] + " overruns CU in " + where);
+ let cu;
+ if (game.active == BRITISH)
+ cu = find_american_cu(where) || find_french_cu(where);
+ else
+ cu = find_british_cu(where);
+ remove_cu(cu.owner, where, 1);
+}
+
+function retreat_american_army(from, to) {
+ let g = find_american_or_french_general(from);
+ if (g)
+ move_general(g, to);
+ move_cu(AMERICAN, from, to, count_american_cu(from));
+ move_cu(FRENCH, from, to, count_french_cu(from));
+}
+
+function retreat_british_army(from, to) {
+ let g = find_british_general(from);
+ if (g)
+ move_general(g, to);
+ move_cu(BRITISH, from, to, count_british_cu(from));
+}
+
+function surrender_american_army(where) {
+ let g = find_american_or_french_general(where);
+ if (g)
+ capture_american_or_french_general(where);
+ remove_cu(AMERICAN, where, count_american_cu(where));
+ remove_cu(FRENCH, where, count_french_cu(where));
+}
+
+function surrender_british_army(where) {
+ let g = find_british_general(where);
+ if (g)
+ capture_british_general(g);
+ game.british_losses += count_british_cu(where);
+ remove_cu(BRITISH, where, count_british_cu(where));
+}
+
+function disperse_continental_congress(where) {
+ game.log.push("Contintental Congress dispersed!");
+ game.congress = CONTINENTAL_CONGRESS_DISPERSED;
+ game.congress_was_dispersed = true;
+}
+
+/* MOVE GENERATORS */
+
+function gen_action(action, argument) {
+ if (!game.actions)
+ game.actions = {}
+ if (argument != undefined) {
+ if (!(action in game.actions))
+ game.actions[action] = [ argument ];
+ else
+ game.actions[action].push(argument);
+ } else {
+ game.actions[action] = 1;
+ }
+}
+
+function gen_action_undo() {
+ if (!game.actions)
+ game.actions = {}
+ if (game.undo && game.undo.length > 0)
+ game.actions.undo = 1;
+ else
+ game.actions.undo = 0;
+}
+
+function gen_pass() {
+ gen_action('pass');
+}
+
+function gen_remove_british_pc_from(list_of_colonies) {
+ for (let colony of list_of_colonies) {
+ for (let space of COLONIES[colony]) {
+ if (has_british_pc(space) && has_no_british_cu(space)) {
+ gen_action('remove_pc', space);
+ }
+ }
+ }
+}
+
+function gen_remove_american_pc() {
+ for (let space in SPACES) {
+ if (has_american_pc(space) && has_no_american_unit(space)) {
+ gen_action('remove_pc', space);
+ }
+ }
+}
+
+function gen_remove_american_pc_from(list_of_colonies) {
+ for (let colony of list_of_colonies) {
+ for (let space of COLONIES[colony]) {
+ if (has_american_pc(space) && has_no_american_unit(space)) {
+ gen_action('remove_pc', space);
+ }
+ }
+ }
+}
+
+function gen_remove_american_pc_from_non_port(list_of_colonies) {
+ for (let colony of list_of_colonies) {
+ for (let space of COLONIES[colony]) {
+ if (!SPACES[space].port) {
+ if (has_american_pc(space) && has_no_american_unit(space)) {
+ gen_action('remove_pc', space);
+ }
+ }
+ }
+ }
+}
+
+function gen_remove_american_pc_within_two_spaces_of_a_british_general() {
+ 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;
+ }
+ }
+ }
+ }
+ for (let space in 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) {
+ if (has_no_pc(space) && has_no_british_playing_piece(space)) {
+ gen_action('place_american_pc', space);
+ }
+ }
+}
+
+function gen_place_american_pc_in(list_of_colonies) {
+ for (let colony of list_of_colonies) {
+ for (let space of COLONIES[colony]) {
+ if (has_no_pc(space) && has_no_british_playing_piece(space)) {
+ gen_action('place_american_pc', space);
+ }
+ }
+ }
+}
+
+/* SETUP PHASE */
+
+function goto_committees_of_correspondence() {
+ log(".h2.american Committes of Correspondence");
+ log("");
+ game.active = AMERICAN;
+ game.state = 'committees_of_correspondence';
+ game.coc = THE_13_COLONIES.slice();
+}
+
+states.committees_of_correspondence = {
+ prompt: function (current) {
+ game.prompt = "Committees of Correspondence: Place 1 PC marker in each of the 13 colonies. " + game.coc.length + " left.";
+ if (game.coc.length > 0)
+ gen_place_american_pc_in(game.coc);
+ else
+ gen_pass();
+ },
+ place_american_pc: function (space) {
+ push_undo();
+ let colony = SPACES[space].colony;
+ remove_from_array(game.coc, colony);
+ place_american_pc(space);
+ },
+ pass: function () {
+ clear_undo();
+ goto_for_the_king();
+ }
+}
+
+function goto_for_the_king() {
+ log("");
+ log(".h2.british For the King");
+ log("");
+ delete game.coc;
+ game.active = BRITISH;
+ game.state = 'for_the_king';
+ game.count = 3;
+ gen_british_pc_ops_start();
+}
+
+states.for_the_king = {
+ prompt: function (current) {
+ game.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) {
+ push_undo();
+ place_british_pc(space);
+ --game.count;
+ },
+ pass: function () {
+ clear_undo();
+ gen_british_pc_ops_end();
+ goto_start_year();
+ }
+}
+
+/* REINFORCEMENTS AND START OF STRATEGY PHASE */
+
+function automatic_victory() {
+ let n_american = 0;
+ let n_british = 0;
+ for (let space in SPACES) {
+ n_american += count_french_cu(space) + count_american_cu(space);
+ if (SPACES[space].colony != "CA")
+ n_british += count_british_cu(space);
+ }
+ if (n_american == 0) {
+ game.victory = "British Automatic Victory!";
+ game.active = "None";
+ game.result = BRITISH;
+ game.state = 'game_over';
+ game.log.push(game.victory);
+ return true;
+ }
+ if (n_british == 0) {
+ game.victory = "American Automatic Victory!";
+ game.active = "None";
+ game.result = AMERICAN;
+ game.state = 'game_over';
+ game.log.push(game.victory);
+ return true;
+ }
+ return false;
+}
+
+function goto_start_year() {
+ log("");
+ log(".h1 Year " + game.year);
+ log("");
+
+ // Prisoner exchange
+ for (let g of BRITISH_GENERALS)
+ if (game.generals[g].location == CAPTURED_GENERALS)
+ move_general(g, BRITISH_REINFORCEMENTS);
+ for (let g of AMERICAN_GENERALS)
+ if (game.generals[g].location == CAPTURED_GENERALS)
+ move_general(g, AMERICAN_REINFORCEMENTS);
+
+ switch (game.year) {
+ case 1775: place_british_cu(BRITISH_REINFORCEMENTS, 3); break;
+ case 1776: place_british_cu(BRITISH_REINFORCEMENTS, 8); break;
+ case 1777: place_british_cu(BRITISH_REINFORCEMENTS, 1); break;
+ case 1778: place_british_cu(BRITISH_REINFORCEMENTS, 8); break;
+ case 1779: place_british_cu(BRITISH_REINFORCEMENTS, 1); break;
+ case 1780: place_british_cu(BRITISH_REINFORCEMENTS, 5); break;
+ case 1781: place_british_cu(BRITISH_REINFORCEMENTS, 1); break;
+ case 1782: place_british_cu(BRITISH_REINFORCEMENTS, 1); break;
+ case 1783: place_british_cu(BRITISH_REINFORCEMENTS, 1); break;
+ }
+
+ if (game.year == 1776) {
+ game.deck.push(DECLARATION_OF_INDEPENDENCE);
+ game.deck.push(BARON_VON_STEUBEN);
+ }
+
+ if (game.reshuffle)
+ reshuffle_deck();
+
+ game.a_hand = [];
+ game.b_hand = [];
+ for (let i = 0; i < 7; ++i) {
+ game.a_hand.push(deal_card());
+ game.b_hand.push(deal_card());
+ }
+
+ game.a_queue = 0;
+ game.b_queue = 0;
+ game.did_discard_event = false;
+
+ // TODO: save the played card numbers instead (rule 6.1B clarification)
+ game.played_british_reinforcements = 0;
+ game.played_american_reinforcements = [];
+ game.active = BRITISH;
+ game.state = 'british_declare_first';
+}
+
+states.british_declare_first = {
+ prompt: function (current) {
+ game.prompt = "Declare yourself as the first player by playing a campaign card?";
+ gen_pass();
+ for (let c of CAMPAIGN_CARDS) {
+ if (game.b_hand.includes(c)) {
+ gen_action('card_campaign', c);
+ }
+ }
+ },
+ card_campaign: function (c) {
+ delete game.congress_was_dispersed;
+ game.log.push("B goes first by playing a campaign card");
+ game.active = BRITISH;
+ goto_campaign(c);
+ },
+ pass: function () {
+ if (game.congress_was_dispersed)
+ game.active = BRITISH;
+ else
+ game.active = AMERICAN;
+ game.state = 'choose_first_player';
+ delete game.congress_was_dispersed;
+ },
+}
+
+states.choose_first_player = {
+ prompt: function (current) {
+ game.prompt = "Choose who will play the first strategy card.";
+ gen_action('american_first');
+ gen_action('british_first');
+ },
+ american_first: function (c) {
+ game.log.push("A goes first");
+ goto_strategy_phase(AMERICAN);
+ },
+ british_first: function (c) {
+ game.log.push("B goes first");
+ goto_strategy_phase(BRITISH);
+ },
+}
+
+/* STRATEGY PHASE */
+
+function goto_strategy_phase(new_active) {
+ game.active = new_active;
+ game.state = 'strategy_phase';
+ log("");
+ if (game.active === AMERICAN)
+ log(".h2.american American Turn");
+ else
+ log(".h2.british British Turn");
+ log("");
+}
+
+states.strategy_phase = {
+ prompt: function (current) {
+ game.prompt = "Play a strategy card.";
+ gen_strategy_plays(active_hand());
+ },
+ card_campaign: function (c) {
+ game.did_discard_event = false;
+ clear_queue();
+ goto_campaign(c);
+ },
+ card_play_event: function (c) {
+ push_undo();
+ game.did_discard_event = false;
+ clear_queue();
+ do_event(c);
+ },
+ card_discard_event: function (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) {
+ 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) {
+ push_undo();
+ game.did_discard_event = false;
+ clear_queue();
+ goto_ops_reinforcements(c);
+ },
+ card_ops_queue: function (c) {
+ game.did_discard_event = false;
+ play_card(c, "to queue");
+ if (game.active == BRITISH)
+ game.b_queue += CARDS[c].count;
+ else
+ game.a_queue += CARDS[c].count;
+ end_strategy_card();
+ },
+ card_ops_general: function (c) {
+ push_undo();
+ game.did_discard_event = false;
+ goto_ops_general(c);
+ },
+ exchange_for_discard: function (c) {
+ game.did_discard_event = false;
+ let d = game.discard.pop();
+ discard_card(c);
+ active_hand().push(d);
+ game.log.push(game.active[0] + " picks up " + d + ": " + CARDS[d].title);
+ },
+}
+
+function end_strategy_card() {
+ clear_undo();
+
+ if (automatic_victory())
+ return;
+
+ if (game.campaign) {
+ if (--game.campaign > 0) {
+ game.count = 3; // can activate any general!
+ game.state = 'ops_general_who';
+ return;
+ } else {
+ delete game.landing_party;
+ delete game.campaign;
+ }
+ }
+
+ if (!game.french_alliance_triggered && game.french_alliance == 9) {
+ game.log.push("The French sign an alliance with the Americans!");
+ game.french_alliance_triggered = true;
+ if (game.french_navy == FRENCH_REINFORCEMENTS) {
+ game.save_active = game.active;
+ game.active = AMERICAN;
+ game.state = 'place_french_navy_trigger';
+ return;
+ }
+ }
+
+ game.moved = {};
+ for (let cu of game.cu)
+ cu.moved = 0;
+
+ goto_strategy_phase(ENEMY[game.active]);
+
+ let hand = active_hand();
+ if (hand.length == 0) {
+ game.active = ENEMY[game.active];
+ hand = active_hand();
+ if (hand.length == 0)
+ return goto_winter_attrition_phase();
+ }
+}
+
+function clear_queue() {
+ if (game.active == BRITISH)
+ game.b_queue = 0;
+ else
+ game.a_queue = 0;
+}
+
+function gen_strategy_plays(hand) {
+ for (let c of hand) {
+ let card = CARDS[c];
+ switch (card.type) {
+ case 'mandatory-event':
+ gen_action('card_play_event', c);
+ break;
+ case 'campaign':
+ gen_action('card_campaign', c);
+ break;
+ case 'ops':
+ if (can_exchange_for_discard(c))
+ gen_action('exchange_for_discard', c);
+ if (can_activate_general(c))
+ gen_action('card_ops_general', c);
+ gen_action('card_ops_pc', c);
+ if (can_play_reinforcements())
+ gen_action('card_ops_reinforcements', c);
+ if (card.count < 3)
+ gen_action('card_ops_queue', c);
+ break;
+ case 'british-event':
+ case 'british-event-or-battle':
+ if (game.active == BRITISH)
+ if (can_play_event(c))
+ gen_action('card_play_event', c);
+ gen_action('card_discard_event', c);
+ break;
+ case 'american-event':
+ if (game.active == AMERICAN)
+ if (can_play_event(c))
+ gen_action('card_play_event', c);
+ gen_action('card_discard_event', c);
+ break;
+ case 'british-battle':
+ case 'american-battle':
+ gen_action('card_discard_event', c);
+ break;
+ }
+ }
+}
+
+/* DISCARD EVENT CARD FOR PC ACTION */
+
+states.discard_event_pc_action = {
+ prompt: function (current) {
+ game.prompt = "Place, flip, or remove PC marker.";
+ gen_pass();
+ if (game.active == BRITISH)
+ gen_british_discard_event_pc_action();
+ else
+ gen_american_discard_event_pc_action();
+ },
+ place_british_pc: function (space) {
+ place_british_pc(space);
+ end_strategy_card();
+ },
+ place_american_pc: function (space) {
+ place_american_pc(space);
+ end_strategy_card();
+ },
+ remove_pc: function (space) {
+ remove_pc(space);
+ end_strategy_card();
+ },
+ flip_pc: function (space) {
+ flip_pc(space);
+ end_strategy_card();
+ },
+ pass: function () {
+ end_strategy_card();
+ },
+}
+
+function gen_british_discard_event_pc_action() {
+ for (let space in SPACES) {
+ if (is_adjacent_to_british_pc(space)) {
+ if (has_no_pc(space) && has_no_american_unit(space))
+ gen_action('place_british_pc', space);
+ else if (has_american_pc(space) && has_british_army(space))
+ gen_action('flip_pc', space);
+ else if (has_american_pc(space) && has_no_american_unit(space))
+ gen_action('remove_pc', space);
+ }
+ }
+}
+
+function gen_american_discard_event_pc_action() {
+ for (let space in SPACES) {
+ if (is_adjacent_to_american_pc(space)) {
+ if (has_no_pc(space) && has_no_british_cu(space)) {
+ if (allowed_to_place_american_pc())
+ gen_action('place_american_pc', space);
+ }
+ else if (has_british_pc(space) && has_american_or_french_general(space)) {
+ gen_action('flip_pc', space);
+ }
+ else if (has_british_pc(space) && has_no_british_cu(space)) {
+ gen_action('remove_pc', space);
+ }
+ }
+ }
+}
+
+/* PLAY OPS CARD FOR PC ACTIONS */
+
+function goto_ops_pc(count) {
+ game.count = count;
+ game.state = 'ops_pc';
+ if (game.active == BRITISH)
+ gen_british_pc_ops_start();
+}
+
+states.ops_pc = {
+ prompt: function (current) {
+ game.prompt = "Place or flip PC markers. " + game.count + " left.";
+ gen_pass();
+ if (game.count > 0) {
+ if (game.active == BRITISH)
+ gen_british_pc_ops();
+ else
+ gen_american_pc_ops()
+ }
+ },
+ place_british_pc: function (space) {
+ push_undo();
+ place_british_pc(space);
+ --game.count;
+ },
+ place_american_pc: function (space) {
+ push_undo();
+ place_american_pc(space);
+ --game.count;
+ },
+ flip_pc: function (space) {
+ push_undo();
+ flip_pc(space);
+ --game.count;
+ },
+ pass: function () {
+ if (game.active == BRITISH)
+ gen_british_pc_ops_end();
+ end_strategy_card();
+ },
+}
+
+function gen_british_pc_ops_start() {
+ game.british_pc_space_list = [];
+ for (let space in 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);
+ }
+ }
+}
+
+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) {
+ if (has_british_army(space)) {
+ if (has_no_pc(space))
+ gen_action('place_british_pc', space);
+ else if (has_american_pc(space))
+ gen_action('flip_pc', space);
+ }
+ }
+}
+
+function gen_british_pc_ops_end(space) {
+ delete game.british_pc_space_list;
+}
+
+function gen_american_pc_ops() {
+ for (let space in SPACES) {
+ if (has_no_pc(space) && has_no_british_cu(space)) {
+ if (allowed_to_place_american_pc())
+ gen_action('place_american_pc', space);
+ }
+ else if (has_british_pc(space) && has_american_or_french_general(space)) {
+ gen_action('flip_pc', space);
+ }
+ }
+}
+
+/* PLAY OPS CARD FOR REINFORCEMENTS */
+
+function goto_ops_reinforcements(c) {
+ let count = CARDS[c].count;
+ play_card(c, "for reinforcements");
+ if (game.active == BRITISH) {
+ game.played_british_reinforcements = count;
+ game.count = count_british_cu(BRITISH_REINFORCEMENTS);
+ game.state = 'ops_british_reinforcements_who';
+ } else {
+ game.played_american_reinforcements.push(count);
+ game.count = count;
+ game.state = 'ops_american_reinforcements_who';
+ }
+}
+
+states.ops_british_reinforcements_who = {
+ prompt: function (current) {
+ game.prompt = "Reinforcements: choose an available general or pass to bring only CU."
+ game.prompt += " Carrying " + game.count + " British CU.";
+ gen_pass();
+ gen_british_reinforcements_who();
+ },
+ drop_british_cu: function () {
+ --game.count;
+ },
+ pickup_british_cu: function () {
+ ++game.count;
+ },
+ select_general: function (g) {
+ push_undo();
+ game.state = 'ops_british_reinforcements_where';
+ game.who = g;
+ },
+ pass: function () {
+ push_undo();
+ game.state = 'ops_british_reinforcements_where';
+ game.who = null;
+ },
+}
+
+states.ops_british_reinforcements_where = {
+ prompt: function (current) {
+ game.prompt = "Reinforcements: choose a port space.";
+ game.prompt += " Carrying " + game.count + " British CU.";
+ gen_british_reinforcements_where();
+ },
+ drop_british_cu: function () {
+ --game.count;
+ },
+ pickup_british_cu: function () {
+ ++game.count;
+ },
+ place_reinforcements: function (space) {
+ place_british_reinforcements(game.who, game.count, space);
+ end_strategy_card();
+ game.who = null;
+ },
+}
+
+states.ops_american_reinforcements_who = {
+ prompt: function (current) {
+ game.prompt = "Reinforcements: choose an available general or pass to bring only CU.";
+ gen_pass();
+ gen_american_reinforcements_who();
+ },
+ select_general: function (g) {
+ push_undo();
+ game.state = 'ops_american_reinforcements_where';
+ game.who = g;
+ },
+ pass: function () {
+ push_undo();
+ game.state = 'ops_american_reinforcements_where';
+ game.who = null;
+ },
+}
+
+states.ops_american_reinforcements_where = {
+ prompt: function (current) {
+ game.prompt = "Reinforcements: choose a space.";
+ gen_american_reinforcements_where(game.who);
+ },
+ place_reinforcements: function (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;
+ },
+}
+
+function gen_british_reinforcements_who() {
+ for (let g of BRITISH_GENERALS) {
+ let general = game.generals[g];
+ if (general.location == BRITISH_REINFORCEMENTS) {
+ gen_action('select_general', g);
+ }
+ }
+ if (game.count > 0)
+ gen_action('drop_british_cu');
+ if (game.count < count_british_cu(BRITISH_REINFORCEMENTS))
+ gen_action('pickup_british_cu');
+}
+
+function gen_british_reinforcements_where() {
+ for (let space in SPACES) {
+ if (is_non_blockaded_port(space))
+ if (!has_american_or_french_cu(space) && !has_american_pc(space))
+ gen_action('place_reinforcements', space);
+ }
+ if (game.count > 0)
+ gen_action('drop_british_cu');
+ if (game.count < count_british_cu(BRITISH_REINFORCEMENTS))
+ gen_action('pickup_british_cu');
+}
+
+function gen_american_reinforcements_who() {
+ for (let g of AMERICAN_GENERALS) {
+ let general = game.generals[g];
+ if (general.location == AMERICAN_REINFORCEMENTS) {
+ gen_action('select_general', g);
+ }
+ }
+}
+
+function gen_american_reinforcements_where(general) {
+ for (let space in SPACES) {
+ if (!has_british_cu(space) && !has_british_pc(space)) {
+ if (general == ROCHAMBEAU) {
+ if (SPACES[space].port)
+ gen_action('place_reinforcements', space);
+ } else {
+ gen_action('place_reinforcements', space);
+ }
+ }
+ }
+}
+
+/* PLAY OPS CARD TO MOVE A GENERAL */
+
+function goto_ops_general(c) {
+ play_card(c, " to activate a general");
+ if (game.active == BRITISH) {
+ game.count = CARDS[c].count + game.b_queue;
+ game.b_queue = 0;
+ } else {
+ game.count = CARDS[c].count + game.a_queue;
+ game.a_queue = 0;
+ }
+ game.state = 'ops_general_who';
+}
+
+states.ops_general_who = {
+ prompt: function (current) {
+ if (game.campaign && game.landing_party)
+ game.prompt = "Campaign: Activate a general or use a landing party. " + game.campaign + " left.";
+ else if (game.campaign)
+ game.prompt = "Campaign: Activate a general. " + game.campaign + " left.";
+ else
+ game.prompt = "Activate a general with strategy rating " + game.count + " or lower.";
+ if (game.landing_party)
+ gen_landing_party();
+ gen_activate_general();
+ gen_pass();
+ },
+ place_british_pc: function (where) {
+ game.landing_party = 0;
+ place_british_pc(where);
+ end_strategy_card();
+ },
+ flip_pc: function (where) {
+ game.landing_party = 0;
+ flip_pc(where);
+ end_strategy_card();
+ },
+ select_general: function (g) {
+ push_undo();
+ goto_ops_general_move(g, false);
+ },
+ pass: function () {
+ if (game.campaign > 0)
+ game.campaign = 0;
+ end_strategy_card();
+ }
+}
+
+function gen_landing_party() {
+ for (let space in 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);
+ if (has_no_pc(space) && has_no_american_unit(space) && has_no_british_playing_piece(space))
+ gen_action('place_british_pc', space);
+ }
+ }
+}
+
+function gen_activate_general() {
+ if (game.active == BRITISH)
+ return gen_activate_british_general();
+ else
+ return gen_activate_american_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])
+ 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])
+ gen_action('select_general', g);
+}
+
+function goto_remove_general(where) {
+ game.state = 'remove_general';
+ game.where = where;
+}
+
+states.remove_general = {
+ prompt: function (current) {
+ game.prompt = "Remove a general to the reinforcements box.";
+ gen_remove_general();
+ },
+ select_general: function (g) {
+ if (game.active == BRITISH)
+ move_general(g, BRITISH_REINFORCEMENTS);
+ else
+ move_general(g, AMERICAN_REINFORCEMENTS);
+ end_strategy_card();
+ },
+}
+
+function goto_remove_general_after_intercept() {
+ game.state = 'remove_general_after_intercept';
+}
+
+states.remove_general_after_intercept = {
+ prompt: function (current) {
+ game.prompt = "Remove a general to the reinforcements box.";
+ gen_remove_general();
+ },
+ select_general: function (g) {
+ if (game.active == BRITISH)
+ move_general(g, BRITISH_REINFORCEMENTS);
+ else
+ move_general(g, AMERICAN_REINFORCEMENTS);
+ end_intercept();
+ },
+}
+
+function goto_remove_general_after_retreat(where) {
+ game.state = 'remove_general_after_retreat';
+ game.where = where;
+}
+
+states.remove_general_after_retreat = {
+ prompt: function (current) {
+ game.prompt = "Remove a general to the reinforcements box.";
+ gen_remove_general();
+ },
+ select_general: function (g) {
+ if (game.active == BRITISH)
+ move_general(g, BRITISH_REINFORCEMENTS);
+ else
+ move_general(g, AMERICAN_REINFORCEMENTS);
+ end_battle();
+ },
+}
+
+function gen_remove_general() {
+ if (game.active == BRITISH)
+ return gen_remove_british_general();
+ else
+ return gen_remove_american_general();
+}
+
+function gen_remove_british_general() {
+ for (let g of BRITISH_GENERALS)
+ if (location_of_general(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)
+ gen_action('select_general', g);
+}
+
+function goto_ops_general_move(g, marblehead) {
+ game.state = 'ops_general_move';
+ game.who = g;
+ if (marblehead) {
+ game.mobility = false;
+ game.count = 6;
+ } else {
+ if (game.active == BRITISH) {
+ game.mobility = false;
+ game.count = 4;
+ } else {
+ game.mobility = true;
+ game.count = 5;
+ }
+ }
+ let where = location_of_general(g);
+ if (game.active == BRITISH)
+ pickup_max_british_cu(where);
+ else
+ pickup_max_american_cu(where);
+}
+
+states.ops_general_move = {
+ prompt: function (current) {
+ game.prompt = "Move " + game.who + " with ";
+ if (game.carry_british > 0) {
+ game.prompt += game.carry_british + " British CU.";
+ } else if (game.carry_french + game.carry_american > 0) {
+ if (game.carry_french > 0) {
+ if (game.carry_american > 0) {
+ game.prompt += game.carry_french + " French CU and ";
+ game.prompt += game.carry_american + " American CU.";
+ } else {
+ game.prompt += game.carry_french + " French CU.";
+ }
+ } else {
+ game.prompt += game.carry_american + " American CU.";
+ }
+ } else {
+ game.prompt += game.carry_american + " no CU.";
+ }
+ if (game.count == 1)
+ game.prompt += " " + game.count + " move left.";
+ else if (game.count > 1)
+ game.prompt += " " + game.count + " moves left.";
+
+ // Cannot stop on enemy general
+ if (!has_enemy_general(location_of_general(game.who)))
+ gen_pass();
+
+ gen_carry_cu();
+ gen_move_general();
+ },
+
+ pickup_british_cu: function () { ++game.carry_british; },
+ pickup_american_cu: function () { ++game.carry_american; },
+ pickup_french_cu: function () { ++game.carry_french; },
+
+ drop_british_cu: function () {
+ push_undo();
+ --game.carry_british;
+ if (game.moved[game.who])
+ mark_moved_cu(BRITISH, location_of_general(game.who), 1);
+ },
+ drop_american_cu: function () {
+ push_undo();
+ --game.carry_american;
+ if (game.moved[game.who])
+ mark_moved_cu(AMERICAN, location_of_general(game.who), 1);
+ },
+ drop_french_cu: function () {
+ push_undo();
+ --game.carry_french;
+ if (game.moved[game.who])
+ mark_moved_cu(FRENCH, location_of_general(game.who), 1);
+ },
+
+ move: function (to) {
+ push_undo();
+
+ game.moved[game.who] = 1;
+ 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);
+ if (has_american_pc(to) && cu > 0 && !is_sea_move && !has_british_cu(to))
+ intercept = can_intercept_to(to);
+ }
+
+ move_army(game.who, from, to);
+
+ if (cu > 0) {
+ if (has_enemy_general(to) && !has_enemy_cu(to)) {
+ capture_enemy_general(to);
+ }
+ if (game.active == BRITISH && game.congress == to && !has_enemy_cu(to)) {
+ disperse_continental_congress(to);
+ }
+ if (cu >= 4 && count_enemy_cu(to) == 1 && !has_enemy_general(to)) {
+ overrun(to);
+ }
+ }
+
+ if (intercept)
+ goto_intercept(from, to);
+ else
+ resume_moving(from, to);
+ },
+ pass: function () {
+ clear_undo();
+ let where = location_of_general(game.who);
+ end_move();
+ if (count_friendly_generals(where) > 1)
+ goto_remove_general(where);
+ else
+ end_strategy_card();
+ },
+}
+
+function resume_moving(from, to) {
+ if (has_enemy_cu(to)) {
+ end_move();
+ goto_start_battle(from, to);
+ }
+}
+
+function can_intercept_to(to) {
+ for (let space of SPACES[to].exits) {
+ if (has_american_army(space)) {
+ let g = find_american_or_french_general(space);
+ if (g && !game.moved[g])
+ return true;
+ }
+ }
+ return false;
+}
+
+function gen_intercept() {
+ for (let space of SPACES[game.where].exits) {
+ if (has_american_army(space)) {
+ let g = find_american_or_french_general(space);
+ if (g && !game.moved[g])
+ gen_action('select_general', g);
+ else if (g)
+ console.log(g, "already moved");
+ }
+ }
+}
+
+function goto_intercept(from, where) {
+ clear_undo();
+ game.save_who = game.who;
+ game.who = null;
+ game.from = from;
+ game.where = where;
+ game.active = AMERICAN;
+ game.state = 'intercept';
+}
+
+states.intercept = {
+ prompt: function (current) {
+ game.prompt = "Intercept " + game.save_who + " in " + game.where + "?";
+ gen_pass();
+ gen_intercept();
+ },
+ select_general: function (g) {
+ // TODO: roll for intercept!
+ game.moved[g] = 1;
+ let die = roll_d6();
+ if (die <= GENERALS[g].agility) {
+ game.log.push(g + " intercepts (" + die + " <= " + GENERALS[g].agility + ")");
+ game.did_intercept = 1;
+
+ let save_carry_british = game.carry_british;
+ let save_carry_american = game.carry_american;
+ let save_carry_french = game.carry_french;
+
+ pickup_max_american_cu(location_of_general(g));
+ intercept_army(g, location_of_general(g), game.where);
+
+ game.carry_british = save_carry_british;
+ game.carry_american = save_carry_american;
+ game.carry_french = save_carry_french;
+
+ if (count_friendly_generals(game.where) > 1)
+ goto_remove_general_after_intercept();
+ else
+ end_intercept();
+ } else {
+ game.log.push(g + " fails to intercept (" + die + " > " + GENERALS[g].agility + ")");
+ if (!can_intercept_to(game.where))
+ end_intercept();
+ }
+ },
+ pass: function () {
+ end_intercept();
+ },
+}
+
+function end_intercept() {
+ game.active = BRITISH;
+ game.state = 'ops_general_move';
+ game.who = game.save_who;
+ delete game.save_who;
+ resume_moving(game.from, game.where);
+ delete game.from;
+}
+
+function end_move() {
+ let where = location_of_general(game.who);
+ if (game.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;
+ delete game.mobility;
+ delete game.carry_british;
+ delete game.carry_american;
+ delete game.carry_french;
+}
+
+function path_type(from, to) {
+ return PATH_TYPE[PATH_INDEX[from][to]];
+}
+
+function path_name(from, to) {
+ return PATH_NAME[PATH_INDEX[from][to]];
+}
+
+function gen_carry_cu() {
+ let where = location_of_general(game.who);
+ if (game.active == BRITISH) {
+ if (game.carry_british > 0)
+ gen_action('drop_british_cu');
+ if (game.carry_british < 5 && game.carry_british < count_unmoved_british_cu(where))
+ gen_action('pickup_british_cu');
+ } else {
+ let carry_total = game.carry_french + game.carry_american;
+ if (game.carry_french > 0)
+ gen_action('drop_french_cu');
+ if (game.carry_american > 0)
+ gen_action('drop_american_cu');
+ if (carry_total < 5 && game.carry_french < count_unmoved_french_cu(where))
+ gen_action('pickup_french_cu');
+ if (carry_total < 5 && game.carry_american < count_unmoved_american_cu(where))
+ gen_action('pickup_american_cu');
+ }
+}
+
+function movement_cost(from, to) {
+ switch (path_type(from, to)) {
+ case undefined: return 4; /* must be a sea connection if no direct path */
+ case 'wilderness': return 3;
+ default: return 1;
+ }
+}
+
+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) {
+ let mp = 1;
+ if (path_type(from, to) == 'wilderness') {
+ if (path_name(from, to) == FALMOUTH_QUEBEC)
+ if (game.who != ARNOLD)
+ continue;
+ mp = 3;
+ }
+
+ if (alone) {
+ if (has_enemy_cu(to))
+ continue;
+ if (has_enemy_pc(to))
+ continue;
+ // TODO: more robust check for not stopping (or allow undo in case he gets stuck)
+ if (has_enemy_general(to) && game.count - mp == 0)
+ continue;
+ }
+
+ if (game.mobility && has_enemy_cu(to)) {
+ if (game.count - mp >= 1)
+ gen_action('move', to);
+ } else {
+ if (game.count - mp >= 0)
+ gen_action('move', to);
+ }
+ }
+ if (game.active == BRITISH && game.count == 4) {
+ if (is_non_blockaded_port(from)) {
+ for (let to in SPACES) {
+ if (to != from) {
+ if (is_non_blockaded_port(to)) {
+ if (!has_american_pc(to) && !has_american_or_french_cu(to)) {
+ // TODO: duplicate action if can move by normal road
+ gen_action('move', to);
+ }
+ }
+ }
+ }
+ }
+ }
+}
+
+/* CAMPAIGN */
+
+function goto_campaign(c) {
+ play_card(c);
+ game.state = 'campaign';
+ game.campaign = CARDS[c].count;
+ game.landing_party = game.active == BRITISH ? 1 : 0;
+ game.count = 3; // can activate any general!
+ game.state = 'ops_general_who';
+}
+
+/* EVENTS */
+
+events.the_war_ends = function (c, card) {
+ game.log.push(game.active[0] + " plays " + c + ": " + CARDS[c].title);
+ game.log.push("The war will end in " + card.year);
+ game.last_played = c;
+ remove_from_array(active_hand(), c);
+ if (game.war_ends)
+ game.discard.push(WAR_ENDS_1779 + game.war_ends - 1779);
+ game.war_ends = card.year;
+ end_strategy_card();
+}
+
+events.remove_random_british_card = function (c, card) {
+ play_card(c);
+ remove_random_card(game.b_hand);
+}
+
+events.remove_random_american_card = function (c, card) {
+ play_card(c);
+ remove_random_card(game.a_hand);
+}
+
+function remove_random_card(hand) {
+ if (hand.length > 0) {
+ let i = random(hand.length);
+ let c = hand[i];
+ discard_card_from_hand(hand, c);
+ if (CARDS[c].type == 'mandatory-event')
+ do_event(c);
+ else
+ end_strategy_card();
+ }
+}
+
+function advance_french_alliance(count) {
+ if (game.french_alliance < 9) {
+ game.french_alliance += count;
+ if (game.french_alliance > 9)
+ game.french_alliance = 9;
+ game.log.push("French alliance advances to " + count);
+ }
+}
+
+function lose_regular_advantage() {
+ if (game.regulars) {
+ game.log.push("The British Regulars Advantage is lost!");
+ game.regulars = false;
+ advance_french_alliance(2);
+ }
+}
+
+events.baron_von_steuben_trains_the_continental_army = function (c, card) {
+ play_card(c);
+ if (is_general_on_map(WASHINGTON)) {
+ let where = location_of_general(WASHINGTON);
+ game.log.push("A places 2 CU with Washington in " + where);
+ place_american_cu(where, 2);
+ lose_regular_advantage();
+ }
+ end_strategy_card();
+}
+
+events.advance_french_alliance = function (c, card) {
+ play_card(c);
+ advance_french_alliance(card.count);
+ end_strategy_card();
+}
+
+events.remove_french_navy = function (c, card) {
+ play_card(c);
+ game.french_navy = TURN_TRACK[game.year+1];
+ end_strategy_card();
+}
+
+events.remove_british_pc_from = function (c, card) {
+ play_card(c);
+ game.count = card.count;
+ game.where = card.where;
+ game.state = 'remove_british_pc_from';
+}
+
+states.remove_british_pc_from = {
+ prompt: function (current) {
+ game.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);
+ if (--game.count == 0) {
+ delete game.where;
+ end_strategy_card();
+ }
+ },
+ pass: function () {
+ delete game.where;
+ end_strategy_card();
+ },
+}
+
+events.remove_american_pc = function (c, card) {
+ play_card(c);
+ game.count = card.count;
+ game.state = 'remove_american_pc';
+}
+
+states.remove_american_pc = {
+ prompt: function (current) {
+ game.prompt = "Remove American PC markers. " + game.count + " left.";
+ gen_pass();
+ gen_remove_american_pc();
+ },
+ remove_pc: function (where) {
+ remove_pc(where);
+ if (--game.count == 0) {
+ end_strategy_card();
+ }
+ },
+ pass: function () {
+ end_strategy_card();
+ },
+}
+
+events.remove_american_pc_from = function (c, card) {
+ play_card(c);
+ game.count = card.count;
+ game.where = card.where;
+ game.state = 'remove_american_pc_from';
+}
+
+states.remove_american_pc_from = {
+ prompt: function (current) {
+ game.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);
+ if (--game.count == 0) {
+ delete game.where;
+ end_strategy_card();
+ }
+ },
+ pass: function () {
+ delete game.where;
+ end_strategy_card();
+ },
+}
+
+events.remove_american_pc_from_non_port = function (c, card) {
+ play_card(c);
+ game.count = card.count;
+ game.where = card.where;
+ game.state = 'remove_american_pc_from_non_port';
+}
+
+states.remove_american_pc_from_non_port = {
+ prompt: function (current) {
+ game.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);
+ if (--game.count == 0) {
+ delete game.where;
+ end_strategy_card();
+ }
+ },
+ pass: function () {
+ delete game.where;
+ end_strategy_card();
+ },
+}
+
+events.remove_american_pc_within_two_spaces_of_a_british_general = function (c, card) {
+ play_card(c);
+ game.count = card.count;
+ game.state = 'remove_american_pc_within_two_spaces_of_a_british_general';
+}
+
+states.remove_american_pc_within_two_spaces_of_a_british_general = {
+ prompt: function (current) {
+ game.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);
+ if (--game.count == 0) {
+ delete game.where;
+ end_strategy_card();
+ }
+ },
+ pass: function () {
+ delete game.where;
+ end_strategy_card();
+ },
+}
+
+events.place_american_pc = function (c, card) {
+ play_card(c);
+ game.count = card.count;
+ game.state = 'place_american_pc';
+}
+
+states.place_american_pc = {
+ prompt: function (current) {
+ game.prompt = "Place American PC markers. " + game.count + " left.";
+ gen_pass();
+ gen_place_american_pc();
+ },
+ place_american_pc: function (where) {
+ place_american_pc(where);
+ if (--game.count == 0) {
+ end_strategy_card();
+ }
+ },
+ pass: function () {
+ end_strategy_card();
+ },
+}
+
+events.place_american_pc_in = function (c, card) {
+ play_card(c);
+ game.count = card.count;
+ game.where = card.where;
+ game.state = 'place_american_pc_in';
+}
+
+states.place_american_pc_in = {
+ prompt: function (current) {
+ game.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);
+ if (--game.count == 0) {
+ delete game.where;
+ end_strategy_card();
+ }
+ },
+ pass: function () {
+ delete game.where;
+ end_strategy_card();
+ },
+}
+
+events.lord_sandwich_coastal_raids = function (c, card) {
+ play_card(c);
+ game.state = 'lord_sandwich_coastal_raids';
+ game.count = 2;
+ game.where = null;
+}
+
+states.lord_sandwich_coastal_raids = {
+ prompt: function (current) {
+ game.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);
+ end_strategy_card();
+ },
+ remove_pc: function (where) {
+ game.where = where;
+ remove_pc(where);
+ if (--game.count == 0)
+ end_strategy_card();
+ },
+ pass: function () {
+ end_strategy_card();
+ },
+}
+
+function gen_lord_sandwich_coastal_raids(first_removed) {
+ for (let space in SPACES) {
+ if (SPACES[space].port)
+ if (has_american_pc(space) && has_no_american_unit(space))
+ gen_action('remove_pc', space);
+ if (space == first_removed)
+ gen_action('place_british_pc', space);
+ }
+}
+
+events.remove_american_cu = function (c, card) {
+ play_card(c);
+ game.state = 'remove_american_cu';
+}
+
+states.remove_american_cu = {
+ prompt: function (current) {
+ game.prompt = "Remove one American CU from any space.";
+ gen_pass();
+ gen_remove_american_cu();
+ },
+ remove_cu: function (where) {
+ let cu = find_american_cu(where) || find_french_cu(where);
+ remove_cu(cu.owner, where, 1);
+ end_strategy_card();
+ },
+ pass: function () {
+ end_strategy_card();
+ },
+}
+
+function gen_remove_american_cu() {
+ for (let space in SPACES) {
+ if (has_american_or_french_cu(space))
+ gen_action('remove_cu', space);
+ }
+}
+
+function gen_remove_british_cu() {
+ for (let space in SPACES) {
+ if (has_british_cu(space))
+ gen_action('remove_cu', space);
+ }
+}
+
+events.pennsylvania_and_new_jersey_line_mutinies = function (c, card) {
+ play_card(c);
+ game.pennsylvania_and_new_jersey_line_mutinies = true;
+ game.state = 'pennsylvania_and_new_jersey_line_mutinies';
+ game.count = 2;
+ game.where = null;
+}
+
+states.pennsylvania_and_new_jersey_line_mutinies = {
+ prompt: function (current) {
+ game.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) {
+ 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 () {
+ end_strategy_card();
+ },
+}
+
+function gen_pennsylvania_and_new_jersey_line_mutinies(first_removed) {
+ for (let space in SPACES) {
+ if (has_american_or_french_cu(space))
+ if (space != first_removed)
+ gen_action('remove_cu', space);
+ }
+}
+
+events.john_glovers_marblehead_regiment = function (c, card) {
+ play_card(c);
+ game.state = 'john_glovers_marblehead_regiment_who';
+ game.count = 3; // strategy rating for gen_activate_general
+}
+
+states.john_glovers_marblehead_regiment_who = {
+ prompt: function (current) {
+ game.prompt = "Activate an American general.";
+ gen_activate_general();
+ },
+ select_general: function (g) {
+ goto_ops_general_move(g, true);
+ },
+}
+
+events.declaration_of_independence = function (c, card) {
+ play_card(c);
+ game.last_active = game.active;
+ game.active = AMERICAN;
+ game.doi = THE_13_COLONIES.slice();
+ game.state = 'declaration_of_independence';
+}
+
+states.declaration_of_independence = {
+ prompt: function (current) {
+ game.prompt = "Declaration of Independence: Place 1 PC marker in each of the 13 colonies. ";
+ game.prompt += game.doi.length + " left.";
+ gen_pass();
+ gen_place_american_pc_in(game.doi);
+ },
+ place_american_pc: function (space) {
+ let colony = SPACES[space].colony;
+ remove_from_array(game.doi, colony);
+ place_american_pc(space);
+ if (game.doi.length == 0)
+ end_declaration_of_independence();
+ },
+ pass: function () {
+ end_declaration_of_independence();
+ }
+}
+
+function end_declaration_of_independence() {
+ game.active = game.last_active;
+ delete game.last_active;
+ delete game.doi;
+ end_strategy_card();
+}
+
+function goto_george_washington_captured() {
+ /* Save all the state we clobber during the interrupt. */
+ game.last_state = game.state;
+ game.last_active = game.active;
+ game.last_count = game.count;
+
+ game.state = 'george_washington_captured';
+ game.active = BRITISH;
+ game.count = 5;
+}
+
+states.george_washington_captured = {
+ prompt: function (current) {
+ game.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);
+ if (--game.count == 0) {
+ end_george_washington_captured();
+ }
+ },
+ pass: function () {
+ end_george_washington_captured();
+ },
+}
+
+function end_george_washington_captured() {
+ /* Restore previous state. */
+ game.state = game.last_state;
+ game.count = game.last_count;
+ game.active = game.last_active;
+ delete game.last_state;
+ delete game.last_active;
+ delete game.last_count;
+}
+
+function do_event(c) {
+ let card = CARDS[c];
+ if (card.event in events)
+ events[card.event](c, card);
+ else
+ throw new Error("Event not implemented yet: " + card.event)
+}
+
+/* BATTLE */
+
+function can_retreat_before_battle(where) {
+ if (game.did_intercept)
+ return false;
+ // can't retreat if attempted (successful or not) interception!
+ let g = find_american_or_french_general(where);
+ if (g && !game.moved[g])
+ return true;
+ return false;
+}
+
+function goto_start_battle(from, where) {
+ clear_undo();
+ game.attacker = game.active;
+ game.attack_from = from;
+ game.british_losses = 0;
+ game.where = where;
+ game.a_bonus = 0;
+ game.b_bonus = 0;
+ if (game.active == BRITISH && can_retreat_before_battle(where))
+ goto_retreat_before_battle();
+ else
+ goto_play_attacker_battle_card();
+}
+
+function goto_retreat_before_battle() {
+ game.active = AMERICAN;
+ game.who = find_american_or_french_general(game.where);
+ game.state = 'retreat_before_battle';
+}
+
+states.retreat_before_battle = {
+ prompt: function (current) {
+ game.prompt = "Attempt retreat before battle?";
+ gen_pass();
+ gen_defender_retreat();
+ },
+ move: function (to) {
+ let agility = GENERALS[game.who].agility;
+ if (GENERALS[game.who].bonus)
+ agility += 2;
+ let roll = roll_d6();
+ if (roll <= agility) {
+ game.log.push("A successfully retreats before battle: " + roll + " <= " + agility);
+ pickup_max_american_cu(game.where);
+ move_army(game.who, game.where, to);
+ goto_remove_general_after_retreat_before_battle(to);
+ } else {
+ game.log.push("A fails to retreat before battle: " + roll + " > " + agility);
+ end_retreat_before_battle();
+ }
+ },
+ pass: function () {
+ end_retreat_before_battle();
+ },
+}
+
+function goto_remove_general_after_retreat_before_battle(to) {
+ if (count_friendly_generals(to) > 1) {
+ game.state = 'remove_general_after_retreat_before_battle';
+ game.save_where = game.where;
+ game.where = to;
+ } else {
+ end_remove_general_after_retreat_before_battle();
+ }
+}
+
+states.remove_general_after_retreat_before_battle = {
+ prompt: function (current) {
+ game.prompt = "Remove a general to the reinforcements box.";
+ gen_remove_general();
+ },
+ select_general: function (g) {
+ if (game.active == BRITISH)
+ move_general(g, BRITISH_REINFORCEMENTS);
+ else
+ move_general(g, AMERICAN_REINFORCEMENTS);
+ game.where = game.save_where;
+ delete game.save_where;
+ end_remove_general_after_retreat_before_battle();
+ },
+}
+
+
+function end_remove_general_after_retreat_before_battle() {
+ let b_cu = count_british_cu(game.where);
+ let a_cu = count_american_and_french_cu(game.where);
+ if (a_cu == 0) {
+ end_battle();
+ } else if (b_cu >= 4 && a_cu == 1) {
+ overrun(game.where);
+ end_battle();
+ } else {
+ end_retreat_before_battle();
+ }
+}
+
+function can_defender_retreat(to) {
+ if (to == game.attack_from)
+ return false;
+ if (has_enemy_pc(to))
+ return false;
+ if (has_enemy_cu(to))
+ return false;
+ return true;
+}
+
+function can_attacker_retreat() {
+ let to = game.attack_from;
+ if (has_enemy_pc(to))
+ return false;
+ if (has_enemy_cu(to))
+ return false;
+ return true;
+}
+
+function gen_defender_retreat() {
+ let from = game.where;
+ for (let to of SPACES[from].exits) {
+ if (can_defender_retreat(to))
+ gen_action('move', to);
+ }
+ if (game.active == BRITISH) {
+ let can_sea_retreat = false;
+ if (is_non_blockaded_port(from)) {
+ if (is_fortified_port(from)) {
+ if (has_british_pc(from) && is_non_blockaded_port(from))
+ can_sea_retreat = true;
+ } else {
+ can_sea_retreat = true;
+ }
+ }
+ if (can_sea_retreat) {
+ for (let to in SPACES) {
+ if (to != from && is_non_blockaded_port(to)) {
+ if (!has_american_pc(to) && !has_american_or_french_cu(to)) {
+ gen_action('move', to);
+ }
+ }
+ }
+ }
+ }
+}
+
+function gen_attacker_retreat() {
+ if (can_attacker_retreat())
+ gen_action('move', game.attack_from);
+}
+
+function end_retreat_before_battle() {
+ game.who = null;
+ goto_play_attacker_battle_card();
+}
+
+function goto_play_attacker_battle_card() {
+ game.active = game.attacker;
+ game.state = 'play_attacker_battle_card';
+}
+
+states.play_attacker_battle_card = {
+ prompt: function (current) {
+ game.prompt = "Attack: Play or discard event for DRM.";
+ gen_pass();
+ gen_battle_card();
+ },
+ card_battle_play: function (c) {
+ play_card(c, "for +2 DRM");
+ if (game.active == BRITISH) {
+ if (CARDS[c].event == 'remove_benedict_arnold')
+ remove_benedict_arnold();
+ game.b_draw_after_battle = true;
+ game.b_bonus += 2;
+ } else {
+ game.a_draw_after_battle = true;
+ game.a_bonus += 2;
+ }
+ goto_play_defender_battle_card();
+ },
+ card_battle_discard: function (c) {
+ discard_card(c, "for +1 DRM");
+ if (game.active == BRITISH) {
+ game.b_draw_after_battle = true;
+ game.b_bonus += 1;
+ } else {
+ game.a_draw_after_battle = true;
+ game.a_bonus += 1;
+ }
+ goto_play_defender_battle_card();
+ },
+ pass: function () {
+ goto_play_defender_battle_card();
+ },
+}
+
+function goto_play_defender_battle_card() {
+ game.state = 'play_defender_battle_card';
+ game.active = ENEMY[game.attacker];
+}
+
+states.play_defender_battle_card = {
+ prompt: function (current) {
+ game.prompt = "Defend: Play or discard event for DRM.";
+ gen_pass();
+ gen_battle_card();
+ },
+ card_battle_play: function (c) {
+ play_card(c, "for +2 DRM");
+ if (game.active == BRITISH) {
+ if (CARDS[c].event == 'remove_benedict_arnold')
+ remove_benedict_arnold();
+ game.b_draw_after_battle = true;
+ game.b_bonus += 2;
+ } else {
+ game.a_draw_after_battle = true;
+ game.a_bonus += 2;
+ }
+ resolve_battle();
+ },
+ card_battle_discard: function (c) {
+ discard_card(c, "for +1 DRM");
+ if (game.active == BRITISH) {
+ game.b_draw_after_battle = true;
+ game.b_bonus += 1;
+ } else {
+ game.a_draw_after_battle = true;
+ game.a_bonus += 1;
+ }
+ resolve_battle();
+ },
+ pass: function () {
+ resolve_battle();
+ },
+}
+
+function gen_battle_card() {
+ for (let c of active_hand()) {
+ let card = CARDS[c];
+ if (game.active == BRITISH) {
+ switch (card.type) {
+ case 'british-battle':
+ case 'british-event-or-battle':
+ gen_action('card_battle_play', c);
+ break;
+ case 'british-event':
+ case 'american-event':
+ case 'american-battle':
+ gen_action('card_battle_discard', c);
+ break;
+ }
+ } else {
+ switch (card.type) {
+ case 'british-battle':
+ case 'british-event-or-battle':
+ case 'british-event':
+ case 'american-event':
+ gen_action('card_battle_discard', c);
+ break;
+ case 'american-battle':
+ gen_action('card_battle_play', c);
+ break;
+ }
+ }
+ }
+}
+
+function roll_loser_combat_losses(log) {
+ let roll = roll_d6();
+ let losses = 0;
+ log.push("Loser Combat Loss Roll: " + roll);
+ switch (roll) {
+ case 1: case 2: case 3: losses = 1; break;
+ case 4: case 5: losses = 2; break;
+ case 6: losses = 3; break;
+ }
+ return losses;
+}
+
+function roll_winner_combat_losses(log, losing_general) {
+ let agility = losing_general ? GENERALS[losing_general].agility : 0;
+ let roll = roll_d6();
+ log.push("Enemy Agility Rating: " + agility);
+ log.push("Winner Combat Loss Roll: " + roll);
+ let losses = 0;
+ switch (agility) {
+ case 0: losses = (roll == 1) ? 1 : 0; break;
+ case 1: losses = (roll <= 2) ? 1 : 0; break;
+ case 2: losses = (roll <= 3) ? 1 : 0; break;
+ case 3: losses = (roll <= 4) ? 1 : 0; break;
+ }
+ return losses;
+}
+
+function apply_british_combat_losses(max) {
+ let cu = find_british_cu(game.where);
+ if (cu.count > max) {
+ cu.count -= max;
+ return max;
+ }
+ remove_from_array(game.cu, cu);
+ return cu.count;
+}
+
+function apply_american_combat_losses(max) {
+ let cu = find_american_cu(game.where);
+ if (cu) {
+ if (cu.count > max) {
+ cu.count -= max;
+ return max;
+ }
+ remove_from_array(game.cu, cu);
+ return cu.count;
+ }
+ return 0;
+}
+
+function apply_french_combat_losses(max) {
+ let cu = find_french_cu(game.where);
+ if (cu) {
+ if (cu.count > max) {
+ cu.count -= max;
+ return max;
+ }
+ remove_from_array(game.cu, cu);
+ return cu.count;
+ }
+ return 0;
+}
+
+function apply_american_and_french_combat_losses(max) {
+ let n = apply_american_combat_losses(max);
+ if (n < max)
+ n += apply_french_combat_losses(max - n)
+ return n;
+}
+
+function resolve_battle() {
+ let a_log = [];
+ let b_log = [];
+
+ game.active = ENEMY[game.active];
+ let b_g = find_british_general(game.where);
+ let b_cu = count_british_cu(game.where);
+ let a_g = find_american_or_french_general(game.where);
+ let a_cu = count_american_and_french_cu(game.where);
+ let b_br = 0;
+ let a_br = 0;
+
+ if (b_g)
+ b_log.push("General: " + b_g);
+ if (a_g)
+ a_log.push("General: " + a_g);
+
+ if (b_g) {
+ let roll = roll_d6();
+ if (roll <= 3)
+ b_br = Math.min(Math.floor(GENERALS[b_g].battle / 2), b_cu);
+ else
+ b_br = Math.min(GENERALS[b_g].battle, b_cu);
+ b_log.push("Actual Battle Rating Roll: " + roll);
+ }
+
+ if (a_g) {
+ let roll = roll_d6();
+ if (roll <= 3)
+ a_br = Math.min(Math.floor(GENERALS[a_g].battle / 2), a_cu);
+ else
+ a_br = Math.min(GENERALS[a_g].battle, a_cu);
+ a_log.push("Actual Battle Rating Roll: " + roll);
+ }
+
+ b_log.push("+" + b_cu + " CU");
+ a_log.push("+" + a_cu + " CU");
+ b_log.push("+" + b_br + " Actual Battle Rating");
+ a_log.push("+" + a_br + " Actual Battle Rating");
+
+ let b_drm = b_cu + b_br + game.b_bonus;
+ if (game.regulars) {
+ b_log.push("+1 British Regulars' Advantage");
+ b_drm += 1;
+ }
+ if (is_non_blockaded_port(game.where)) {
+ if (is_fortified_port(game.where)) {
+ if (has_british_pc(game.where)) {
+ b_log.push("+1 Royal Navy Support");
+ b_drm += 1;
+ }
+ } else {
+ b_log.push("+1 Royal Navy Support");
+ b_drm += 1;
+ }
+ }
+ if (is_british_militia(game.where)) {
+ b_log.push("+1 Militia");
+ b_drm += 1;
+ }
+ if (game.b_bonus == 2)
+ b_log.push("+2 Battle Card");
+ else if (game.b_bonus == 1)
+ b_log.push("+1 Discard of an Event Card");
+
+ let a_drm = a_cu + a_br + game.a_bonus;
+ if (is_american_militia(game.where)) {
+ a_log.push("+1 Militia");
+ a_drm += 1;
+ }
+ if (is_american_winter_offensive()) {
+ a_log.push("+2 American Winter Offensive");
+ a_drm += 2;
+ }
+ if (game.a_bonus == 2)
+ a_log.push("+2 Battle Card");
+ else if (game.a_bonus == 1)
+ a_log.push("+1 Discard of an Event Card");
+ if (game.did_intercept) {
+ a_log.push("+1 Interception");
+ a_drm += 1;
+ }
+
+ let b_roll = roll_d6();
+ let a_roll = roll_d6();
+
+ b_log.push("Battle Roll: " + b_roll);
+ b_log.push("Battle Total: " + (b_roll + b_drm));
+ a_log.push("Battle Roll: " + a_roll);
+ a_log.push("Battle Total: " + (a_roll + a_drm));
+
+ let victor;
+ if (game.active == BRITISH)
+ victor = (b_roll + b_drm) >= (a_roll + a_drm) ? BRITISH : AMERICAN;
+ else
+ victor = (b_roll + b_drm) >= (a_roll + a_drm) ? BRITISH : AMERICAN;
+
+ let a_lost_cu, b_lost_cu;
+ if (victor == BRITISH) {
+ b_lost_cu = roll_winner_combat_losses(b_log, a_g);
+ a_lost_cu = roll_loser_combat_losses(a_log);
+ } else {
+ b_lost_cu = roll_loser_combat_losses(b_log);
+ a_lost_cu = roll_winner_combat_losses(a_log, b_g);
+ }
+
+ game.british_losses = apply_british_combat_losses(b_lost_cu);
+ let american_losses = apply_american_and_french_combat_losses(a_lost_cu);
+
+ b_log.push("Losses: " + game.british_losses + " CU");
+ a_log.push("Losses: " + american_losses + " CU");
+
+ // Special case: winning general with no CU on enemy PC is captured
+ if (victor == BRITISH) {
+ if (b_g && count_british_cu(game.where) == 0 && has_american_pc(game.where))
+ capture_british_general(game.where);
+ } else {
+ if (a_g && count_american_and_french_cu(game.where) == 0 && has_british_pc(game.where))
+ capture_american_or_french_general(game.where);
+ }
+
+ game.log.push("BRITISH BATTLE REPORT:\n" + b_log.join("\n"));
+ game.log.push("AMERICAN BATTLE REPORT:\n" + a_log.join("\n"));
+ game.log.push(victor + " victory in " + game.where + "!");
+
+ if (victor == AMERICAN)
+ advance_french_alliance(1);
+
+ goto_retreat_after_battle(victor);
+}
+
+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)
+ return end_battle();
+ } else {
+ game.who = find_british_general(game.where);
+ if (game.who == null && count_british_cu(game.where) == 0)
+ return end_battle();
+ }
+ game.active = ENEMY[victor];
+ game.state = 'retreat_after_battle';
+}
+
+states.retreat_after_battle = {
+ prompt: function (current) {
+ game.prompt = "Retreat after battle.";
+ gen_action('surrender');
+ if (game.active == game.attacker)
+ gen_attacker_retreat();
+ else
+ gen_defender_retreat();
+ },
+ move: function (to) {
+ game.log.push(game.active[0] + " retreats to " + to);
+ if (game.active == BRITISH)
+ retreat_british_army(game.where, to);
+ else
+ retreat_american_army(game.where, to);
+ if (count_friendly_generals(to) > 1)
+ goto_remove_general_after_retreat(to);
+ else
+ end_battle();
+ },
+ surrender: function () {
+ // End battle here, so if Washington is captured we can handle the interrupt state.
+ let active = game.active;
+ let where = game.where;
+ end_battle();
+
+ game.log.push(active[0] + " surrenders");
+ if (active == BRITISH)
+ surrender_british_army(where);
+ else
+ surrender_american_army(where);
+ },
+}
+
+function end_battle() {
+ game.active = game.attacker;
+
+ if (game.british_losses >= 3)
+ lose_regular_advantage();
+
+ // TODO: delay until end of campaign
+ if (game.b_draw_after_battle)
+ game.b_hand.push(deal_card());
+ if (game.a_draw_after_battle)
+ game.a_hand.push(deal_card());
+
+ delete game.did_intercept;
+ delete game.b_bonus;
+ delete game.a_bonus;
+ delete game.b_draw_after_battle;
+ delete game.a_draw_after_battle;
+ delete game.attack_from;
+ delete game.british_losses;
+ delete game.attacker;
+ game.where = null;
+ game.who = null;
+ end_strategy_card();
+}
+
+/* END TURN PHASES */
+
+function apply_single_winter_attrition(owner, space) {
+ let die = roll_d6();
+ game.log.push(owner[0] + " attrition roll " + die + " in " + space);
+ if (die <= 3) {
+ game.log.push(owner[0] + " lose 1 CU in " + space);
+ remove_cu(owner, space, 1);
+ }
+}
+
+function apply_winter_attrition(owner, space, n) {
+ let half = Math.floor(n / 2);
+ game.log.push(owner[0] + " lose " + half + " CU in " + space);
+ remove_cu(owner, space, half);
+}
+
+function goto_winter_attrition_phase() {
+ game.log.push("");
+ game.log.push("Winter Attrition");
+
+ for (let space in 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;
+
+ if (n_british == 1 && !wq)
+ apply_single_winter_attrition(BRITISH, space, has_british_general(space));
+ if (n_british > 1 && !wq)
+ apply_winter_attrition(BRITISH, space, n_british);
+
+ if (n_american == 0 && n_french == 1 && !wq)
+ apply_single_winter_attrition(FRENCH, space, has_american_or_french_general(space));
+ if (n_american == 0 && n_french > 1 && !wq)
+ apply_winter_attrition(FRENCH, space, n_french);
+
+ if (n_american == 1 && n_french == 0)
+ apply_single_winter_attrition(AMERICAN, space, has_american_or_french_general(space));
+ if (n_american > 1 && n_french == 0) {
+ let n = n_american;
+ if (has_washington && wq)
+ n = Math.max(0, n - 5);
+ apply_winter_attrition(AMERICAN, space, n);
+ }
+
+ if (n_american > 0 && n_french > 0) {
+ let n = n_american + n_french;
+ if (has_washington && wq)
+ n = Math.max(0, n - 5);
+ let half = Math.floor(n / 2);
+
+ // TODO: let player choose (but why would he ever choose the french?)
+ let lose_american = Math.min(half, n_american);
+ game.log.push(owner[0] + " lose " + lose_american + " American CU in " + space);
+ remove_cu(AMERICAN, space, n_american);
+ half -= lose_american;
+ n_american -= lose_american;
+
+ if (half > 0) {
+ game.log.push(owner[0] + " lose " + half + " French CU in " + space);
+ remove_cu(FRENCH, space, half);
+ }
+ }
+ }
+
+ if (automatic_victory())
+ return;
+
+ goto_french_naval_phase();
+}
+
+function goto_french_naval_phase() {
+ if (game.french_navy != FRENCH_REINFORCEMENTS) {
+ game.active = AMERICAN;
+ game.state = 'place_french_navy';
+ } else {
+ goto_political_control_phase();
+ }
+}
+
+function gen_place_french_navy() {
+ gen_action('place_navy', "Sea1");
+ gen_action('place_navy', "Sea2");
+ gen_action('place_navy', "Sea3");
+ gen_action('place_navy', "Sea4");
+ gen_action('place_navy', "Sea5");
+ gen_action('place_navy', "Sea6");
+ gen_action('place_navy', "Sea7");
+}
+
+states.place_french_navy_trigger = {
+ prompt: function (current) {
+ game.prompt = "Place the French Navy in a blockade zone.";
+ gen_place_french_navy();
+ },
+ place_navy: function (zone) {
+ game.log.push("A places French Navy.");
+ game.french_navy = zone;
+ game.active = game.save_active;
+ delete game.save_active;
+ end_strategy_card()
+ },
+}
+
+states.place_french_navy = {
+ prompt: function (current) {
+ game.prompt = "Place the French Navy in a blockade zone.";
+ gen_place_french_navy();
+ },
+ place_navy: function (zone) {
+ game.log.push("A places French Navy.");
+ game.french_navy = zone;
+ goto_political_control_phase();
+ },
+}
+
+function place_pc_markers_segment() {
+ for (let space in SPACES) {
+ if (has_american_army(space)) {
+ if (has_no_pc(space))
+ place_american_pc(space);
+ else if (has_british_pc(space))
+ flip_pc(space);
+ }
+ if (has_british_army(space)) {
+ if (has_no_pc(space))
+ place_british_pc(space);
+ else if (has_american_pc(space))
+ flip_pc(space);
+ }
+ }
+}
+
+function is_american_pc_root(space) {
+ if (has_no_pc(space) && has_no_british_cu(space))
+ return true;
+ if (game.congress == space)
+ return true;
+ if (has_american_pc(space)) {
+ if (has_american_or_french_cu(space))
+ return true;
+ if (has_american_or_french_general(space))
+ return true;
+ }
+ return false;
+}
+
+function is_british_pc_root(space) {
+ if (has_no_pc(space) && !has_american_or_french_cu(space) && !has_american_or_french_general(space))
+ return true;
+ if (has_british_pc(space)) {
+ if (is_port(space))
+ return true;
+ if (has_british_cu(space))
+ return true;
+ }
+ return false;
+}
+
+function is_american_pc_path(space) {
+ if (has_american_pc(space))
+ return !has_british_army(space);
+ return false;
+}
+
+function is_british_pc_path(space) {
+ if (has_british_pc(space))
+ return !has_american_army(space);
+ return false;
+}
+
+function spread_american_path(seen, from) {
+ for (let to of SPACES[from].exits) {
+ if (to in seen)
+ continue;
+ if (is_american_pc_path(to)) {
+ seen[to] = 1;
+ spread_american_path(seen, to);
+ }
+ }
+}
+
+function spread_british_path(seen, from) {
+ for (let to of SPACES[from].exits) {
+ if (to in seen)
+ continue;
+ if (is_british_pc_path(to)) {
+ seen[to] = 1;
+ spread_british_path(seen, to);
+ }
+ }
+}
+
+function remove_isolated_american_pc_segment() {
+ game.log.push("Removing isolated American PC");
+ let seen = {};
+ for (let space in SPACES) {
+ if (is_american_pc_root(space)) {
+ seen[space] = 1;
+ spread_american_path(seen, space);
+ }
+ }
+ for (let space in SPACES)
+ if (has_american_pc(space) && !seen[space])
+ remove_pc(space);
+}
+
+function remove_isolated_british_pc_segment() {
+ game.log.push("Removing isolated British PC");
+ let seen = {};
+ for (let space in SPACES) {
+ if (is_british_pc_root(space)) {
+ seen[space] = 1;
+ spread_british_path(seen, space);
+ }
+ }
+ for (let space in SPACES)
+ if (has_british_pc(space) && !seen[space])
+ remove_pc(space);
+}
+
+function gen_return_continental_congress() {
+ let n = 0;
+ for (let space in SPACES) {
+ if (SPACES[space].colony != 'CA') {
+ if (has_american_pc(space) && has_no_british_playing_piece(space)) {
+ gen_action('place_continental_congress', space);
+ ++n;
+ }
+ }
+ }
+ return n;
+}
+
+function goto_political_control_phase() {
+ if (game.congress == CONTINENTAL_CONGRESS_DISPERSED) {
+ game.active = AMERICAN;
+ game.state = 'return_continental_congress';
+ } else {
+ goto_political_control_phase_2();
+ }
+}
+
+states.return_continental_congress = {
+ prompt: function () {
+ game.prompt = "Return Continental Congress to a space in the 13 colonies.";
+ if (gen_place_continental_congress() == 0)
+ gen_pass();
+ },
+ place_continental_congress: function (where) {
+ game.congess = where;
+ goto_political_control_phase_2();
+ },
+ pass: function () {
+ goto_political_control_phase_2();
+ }
+}
+
+function goto_political_control_phase_2() {
+ place_pc_markers_segment();
+ remove_isolated_american_pc_segment();
+ remove_isolated_british_pc_segment();
+ goto_end_phase();
+}
+
+states.european_war = {
+ prompt: function () {
+ game.prompt = "European War: Remove 2 British CU from any spaces. " + game.count + " left.";
+ gen_pass();
+ gen_remove_british_cu();
+ },
+ remove_cu: function (where) {
+ let cu = find_british_cu(where);
+ remove_cu(BRITISH, where, 1);
+ if (--game.count == 0)
+ goto_end_phase();
+ },
+ pass: function () {
+ goto_end_phase();
+ }
+}
+
+function norths_government_falls() {
+ update_colony_control();
+
+ let n_american = 0;
+ for (let c in COLONIES)
+ if (game.control[c] == AMERICAN)
+ ++n_american;
+
+ if (n_american >= 7)
+ game.result = AMERICAN;
+ else
+ game.result = BRITISH;
+
+ game.victory = "North's Government Falls: " + game.result + " Victory!";
+ game.active = "None";
+ game.state = 'game_over';
+
+ game.log.push(game.victory);
+}
+
+function goto_end_phase() {
+ if (game.french_alliance_triggered && !game.european_war) {
+ game.european_war = true;
+ game.count = 2;
+ game.active = AMERICAN;
+ game.state = 'european_war';
+ game.reshuffle = true;
+ return;
+ }
+
+ if ((game.war_ends && game.year >= game.war_ends) || game.year == 1783)
+ return norths_government_falls();
+
+ delete game.pennsylvania_and_new_jersey_line_mutinies;
+ game.a_queue = game.b_queue = 0;
+ game.year += 1;
+ goto_start_year();
+}
+
+states.game_over = {
+ prompt: function () {
+ game.prompt = game.victory;
+ }
+}
+
+/* CLIENT/SERVER COMMS */
+
+exports.ready = function (scenario, options, players) {
+ return players.length === 2;
+}
+
+exports.setup = function (seed, scenario, players) {
+ setup_game(seed);
+ return game;
+}
+
+exports.action = function (state, current, action, arg) {
+ game = state;
+ // TODO: check against action list
+ if (current == game.active) {
+ let S = states[game.state];
+ if (action in S) {
+ S[action](arg);
+ } else {
+ if (action === 'undo' && game.undo && game.undo.length > 0)
+ pop_undo();
+ else
+ throw new Error("Invalid action: " + action);
+ }
+ }
+ return game;
+}
+
+function list_actions(current) {
+ game.actions = {}
+ game.prompt = "";
+ states[game.state].prompt(current);
+}
+
+exports.view = function(state, current) {
+ game = state;
+
+ list_actions(current);
+
+ update_colony_control();
+
+ let view = {
+ active: state.active,
+ year: state.year,
+ war_ends: state.war_ends,
+ played_british_reinforcements: state.played_british_reinforcements,
+ played_american_reinforcements: state.played_american_reinforcements,
+ congress: state.congress,
+ european_war: state.european_war,
+ french_alliance: state.french_alliance,
+ french_navy: state.french_navy,
+ regulars: state.regulars,
+ generals: state.generals,
+ cu: state.cu,
+ pc: state.pc,
+ control: state.control,
+ a_cards: state.a_hand.length,
+ b_cards: state.b_hand.length,
+ a_queue: state.a_queue,
+ b_queue: state.b_queue,
+ last_played: state.last_played,
+ who: state.who,
+ log: state.log,
+ }
+
+ if (state.pennsylvania_and_new_jersey_line_mutinies)
+ view.pennsylvania_and_new_jersey_line_mutinies = true;
+
+ if (current == AMERICAN)
+ view.hand = state.a_hand;
+ else if (current == BRITISH)
+ view.hand = state.b_hand;
+ else
+ view.hand = [];
+
+ if (current == state.active) {
+ gen_action_undo();
+ view.prompt = state.prompt;
+ view.actions = state.actions;
+ } else {
+ view.prompt = "Waiting for " + game.active + " player \u2014 " + game.prompt;
+ }
+
+ return view;
+}