"use strict" // TODO: campaign messed up who is who after battle // TODO: retreat with 0 CU after battle exports.scenarios = [ "Historical" ] exports.roles = [ "American", "British" ] const CARDS = require("./cards") const DATA = require("./data") const SPACES = DATA.SPACES const COLONIES = DATA.COLONIES const GENERALS = DATA.GENERALS const BLOCKADE = DATA.BLOCKADE const PATH_INDEX = DATA.PATH_INDEX const PATH_NAME = DATA.PATH_NAME const PATH_TYPE = DATA.PATH_TYPE const 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 let view function random(n) { return (game.seed = (game.seed * 200105) % 34359738337) % n } function logbr() { if (game.log.length > 0 && game.log[game.log.length - 1] !== "") game.log.push("") } function log(s) { game.log.push(s) } function logp(s) { game.log.push(game.active[0] + " " + s) } function object_copy(original) { if (Array.isArray(original)) { let n = original.length let copy = new Array(n) for (let i = 0; i < n; ++i) { let v = original[i] if (typeof v === "object" && v !== null) copy[i] = object_copy(v) else copy[i] = v } return copy } else { let copy = {} for (let i in original) { let v = original[i] if (typeof v === "object" && v !== null) copy[i] = object_copy(v) else copy[i] = v } return copy } } function clear_undo() { if (game.undo) { game.undo.length = 0 } } function push_undo() { if (game.undo) { let copy = {} for (let k in game) { let v = game[k] if (k === "undo") continue else if (k === "log") v = v.length else if (typeof v === "object" && v !== null) v = object_copy(v) copy[k] = v } game.undo.push(copy) } } function pop_undo() { if (game.undo) { let save_log = game.log let save_undo = game.undo game = save_undo.pop() save_log.length = game.log game.log = save_log game.undo = save_undo } } function remove_from_array(array, item) { let i = array.indexOf(item) if (i >= 0) array.splice(i, 1) } function setup_game(seed) { game = { seed: seed, year: 1775, congress: "Philadelphia", 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() { log("Reshuffled the deck.") 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) log(game.active[0] + " played #" + c + " " + reason) else log(game.active[0] + " played #" + c) 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 log("Removed card " + c + ".") } 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 logp("discarded #" + c) } function discard_card(c, reason) { game.last_played = c discard_card_from_hand(active_hand(), c) if (reason) logp("discarded #" + c + " " + reason) else logp("discarded #" + c) } 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 } switch (card.event) { case "john_glovers_marblehead_regiment": return has_general_on_map() } 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 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) { logp("placed 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) { logp("placed PC in " + space) game.pc[space] = AMERICAN } function remove_pc(space) { if (game.active === BRITISH) logp("removed PC in " + space) else logp("removed PC in " + space) game.pc[space] = undefined } function flip_pc(space) { if (game.active === BRITISH) logp("flipped PC in " + space) else logp("flipped 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 has_general_on_map() { if (game.active === BRITISH) return has_british_general_on_map() else return has_american_general_on_map() } function has_british_general_on_map() { for (let g of BRITISH_GENERALS) if (is_general_on_map(g)) return true return false } function has_american_general_on_map() { for (let g of AMERICAN_GENERALS) if (is_general_on_map(g)) return true return false } 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) log(g + " was captured!") move_general(g, CAPTURED_GENERALS) } function capture_american_or_french_general(where) { let g = find_american_or_french_general(where) log(g + " was 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) { log("Removed Arnold 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) { logp("reinforced " + where + " with " + who) move_general(who, where) } if (count > 0) { logp("reinforced " + where + " with " + count + " CU") move_british_cu(BRITISH_REINFORCEMENTS, where, count) if (has_enemy_general(where)) capture_enemy_general(where) if (game.active === BRITISH && game.congress === where) disperse_continental_congress(where) } } 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) { logp("reinforced " + where + " with " + who) move_general(who, where) } logp("reinforced " + where + " with " + count + " CU") place_american_cu(where, count) if (has_enemy_general(where)) capture_enemy_general(where) } 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) { logp("reinforced " + where + " with " + who) move_general(who, where) } logp("reinforced " + 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) { logp("overran 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(where) game.british_losses += count_british_cu(where) remove_cu(BRITISH, where, count_british_cu(where)) } function disperse_continental_congress(where) { log("Contintental Congress dispersed!") game.congress = CONTINENTAL_CONGRESS_DISPERSED game.congress_was_dispersed = true } /* MOVE GENERATORS */ function gen_action(action, argument) { if (!view.actions) view.actions = {} if (argument !== undefined) { if (!(action in view.actions)) view.actions[action] = [ argument ] else view.actions[action].push(argument) } else { view.actions[action] = 1 } } function gen_action_undo() { if (!view.actions) view.actions = {} if (game.undo && game.undo.length > 0) view.actions.undo = 1 else view.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") logbr() game.active = AMERICAN game.state = "committees_of_correspondence" game.coc = THE_13_COLONIES.slice() } states.committees_of_correspondence = { inactive: "Committees of Correspondence", prompt: function (current) { view.prompt = "Committees of Correspondence: Place 1 PC marker in each of the 13 colonies. " + game.coc.length + " left." if (game.coc.length > 0) 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() { logbr() log(".h2.british For the King") logbr() delete game.coc game.active = BRITISH game.state = "for_the_king" game.count = 3 gen_british_pc_ops_start() } states.for_the_king = { inactive: "For the King", prompt: function (current) { view.prompt = "For the King: Place 3 PC markers. " + game.count + " left." if (game.count > 0) gen_british_pc_ops() else gen_pass() }, place_british_pc: function (space) { 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" log(game.victory) return true } if (n_british === 0) { game.victory = "American Automatic Victory!" game.active = "None" game.result = AMERICAN game.state = "game_over" log(game.victory) return true } return false } function goto_start_year() { logbr() log(".h1 Year " + game.year) logbr() // 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) { view.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 logp("went 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) { view.prompt = "Choose who will play the first strategy card." gen_action("american_first") gen_action("british_first") }, american_first: function (c) { logp("went first") goto_strategy_phase(AMERICAN) }, british_first: function (c) { logp("went first") goto_strategy_phase(BRITISH) }, } /* STRATEGY PHASE */ function goto_strategy_phase(new_active) { game.active = new_active game.state = "strategy_phase" logbr() if (game.active === AMERICAN) log(".h2.american American Turn") else log(".h2.british British Turn") logbr() } states.strategy_phase = { inactive: "strategy phase", prompt: function (current) { view.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) logp("picked up up #" + d) }, } 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) { log("The French signed 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) { view.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) { view.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) { view.prompt = "Reinforcements: choose an available general or pass to bring only CU." view.prompt += " Carrying " + game.count + " British CU." gen_pass() gen_british_reinforcements_who() }, drop_british_cu: function () { --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) { view.prompt = "Reinforcements: choose a port space." view.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) { view.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) { view.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) view.prompt = "Campaign: Activate a general or use a landing party. " + game.campaign + " left." else if (game.campaign) view.prompt = "Campaign: Activate a general. " + game.campaign + " left." else view.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) { view.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) { view.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) { view.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) { view.prompt = "Move " + game.who + " with " if (game.carry_british > 0) { view.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) { view.prompt += game.carry_french + " French CU and " view.prompt += game.carry_american + " American CU." } else { view.prompt += game.carry_french + " French CU." } } else { view.prompt += game.carry_american + " American CU." } } else { view.prompt += game.carry_american + " no CU." } if (game.count === 1) view.prompt += " " + game.count + " move left." else if (game.count > 1) view.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) } } } 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) { view.prompt = "Intercept " + game.save_who + " in " + game.where + "?" gen_pass() gen_intercept() }, select_general: function (g) { game.moved[g] = 1 let die = roll_d6() if (die <= GENERALS[g].agility) { log(g + " intercepted (" + 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 { log(g + " failed 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)) { // don't leave alone if (alone && has_enemy_general(to)) continue // 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) { logp("played #" + c) log("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() } 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 log("French alliance advanced to " + count) } } function lose_regular_advantage() { if (game.regulars) { log("British Regulars Advantage 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) logp("placed 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) { view.prompt = "Remove British PC markers from " + game.where.join(", ") + ". " + game.count + " left." gen_pass() gen_remove_british_pc_from(game.where) }, remove_pc: function (where) { remove_pc(where) 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) { view.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) { view.prompt = "Remove American PC markers from " + game.where.join(", ") + ". " + game.count + " left." gen_pass() gen_remove_american_pc_from(game.where) }, remove_pc: function (where) { remove_pc(where) 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) { view.prompt = "Remove American PC markers from non-Port space in " + game.where.join(", ") + ". " + game.count + " left." gen_pass() gen_remove_american_pc_from_non_port(game.where) }, remove_pc: function (where) { remove_pc(where) 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) { view.prompt = "Remove American PC markers within two spaces of a British general. " + game.count + " left." gen_pass() gen_remove_american_pc_within_two_spaces_of_a_british_general() }, remove_pc: function (where) { remove_pc(where) 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) { view.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) { view.prompt = "Place American PC markers in " + game.where.join(", ") + ". " + game.count + " left." gen_pass() gen_place_american_pc_in(game.where) }, place_american_pc: function (where) { place_american_pc(where) 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) { view.prompt = "Remove two or flip one American PC in a port space." gen_pass() gen_lord_sandwich_coastal_raids(game.where) }, place_british_pc: function (where) { place_british_pc(where) 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) { view.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) } } events.remove_british_cu = function (c, card) { play_card(c) game.state = "remove_british_cu" game.count = card.count } states.remove_british_cu = { prompt: function (current) { view.prompt = "Remove " + game.count + " British CU from any space." gen_pass() gen_remove_british_cu() }, remove_cu: function (where) { let cu = find_british_cu(where) remove_cu(cu.owner, where, 1) if (--game.count === 0) end_strategy_card() }, pass: function () { end_strategy_card() }, } 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) { view.prompt = "Remove two American CUs from the map, one each from two different spaces." gen_pass() gen_pennsylvania_and_new_jersey_line_mutinies(game.where) }, remove_cu: function (where) { 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) { view.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) { view.prompt = "Declaration of Independence: Place 1 PC marker in each of the 13 colonies. " view.prompt += game.doi.length + " left." gen_pass() gen_place_american_pc_in(game.doi) }, place_american_pc: function (space) { 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) { view.prompt = "George Washington is captured! Remove American PC markers. " + game.count + " left." gen_pass() gen_remove_american_pc() }, remove_pc: function (where) { remove_pc(where) 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) { view.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) { logp("successfully retreated 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 { logp("failed 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) { view.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) { view.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) { view.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) } log("BRITISH BATTLE REPORT:\n" + b_log.join("\n")) log("AMERICAN BATTLE REPORT:\n" + a_log.join("\n")) log(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) { view.prompt = "Retreat after battle." gen_action("surrender") if (game.active === game.attacker) gen_attacker_retreat() else gen_defender_retreat() }, move: function (to) { logp("retreated 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() logp("surrendered") if (active === BRITISH) surrender_british_army(where) else surrender_american_army(where) }, } function end_battle() { game.active = game.attacker if (game.active === BRITISH && game.congress === game.where) disperse_continental_congress(game.where) 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() log(owner[0] + " attrition roll " + die + " in " + space) if (die <= 3) { log(owner[0] + " lost 1 CU in " + space) remove_cu(owner, space, 1) } } function apply_winter_attrition(owner, space, n) { let half = Math.floor(n / 2) log(owner[0] + " lost " + half + " CU in " + space) remove_cu(owner, space, half) } function goto_winter_attrition_phase() { logbr() log("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) log(owner[0] + " lost " + lose_american + " American CU in " + space) remove_cu(AMERICAN, space, n_american) half -= lose_american n_american -= lose_american if (half > 0) { log(owner[0] + " lost " + 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) { view.prompt = "Place the French Navy in a blockade zone." gen_place_french_navy() }, place_navy: function (zone) { logp("placed 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) { view.prompt = "Place the French Navy in a blockade zone." gen_place_french_navy() }, place_navy: function (zone) { logp("placed 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() { log("Removed 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() { log("Removed 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_place_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 () { view.prompt = "Return Continental Congress to a space in the 13 colonies." if (gen_place_continental_congress() === 0) gen_pass() }, place_continental_congress: function (where) { game.congress = 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 () { view.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" log(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 () { view.prompt = game.victory }, } /* CLIENT/SERVER COMMS */ exports.setup = function (seed, scenario, options) { 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) } } update_colony_control() return game } function list_actions(current) { view.actions = {} states[game.state].prompt(current) } exports.view = function (state, current) { game = state 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) { list_actions(current) gen_action_undo() } else { let inactive = states[game.state].inactive if (typeof inactive !== "string") inactive = game.state view.prompt = "Waiting for " + game.active + " player \u2014 " + inactive + "." } return view }