"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; }