"use strict" /* DATA */ const data = require("./data.js") const CARDS = data.cards const SPACES = data.spaces const COLONIES = data.colonies const GENERALS = data.generals const Canada = 0 const P_BRITAIN = "Britain" const P_AMERICA = "America" const PC_NONE = 0 const PC_BRITISH = 1 const PC_AMERICAN = 2 const CU_BRITISH_SHIFT = 2 const CU_AMERICAN_SHIFT = 8 const CU_FRENCH_SHIFT = 14 const CU_BRITISH_MASK = 63 << CU_BRITISH_SHIFT const CU_AMERICAN_MASK = 63 << CU_AMERICAN_SHIFT const CU_FRENCH_MASK = 7 << CU_FRENCH_SHIFT const AMERICAN_GENERALS = [ 0, 1, 2, 3, 4, 5, 6, 7 ] const BRITISH_GENERALS = [ 8, 9, 10, 11, 12 ] const NOBODY = -1 const ARNOLD = data.general_index["Arnold"] const BURGOYNE = data.general_index["Burgoyne"] const CLINTON = data.general_index["Clinton"] const CORNWALLIS = data.general_index["Cornwallis"] const GATES = data.general_index["Gates"] const GREENE = data.general_index["Greene"] const LAFAYETTE = data.general_index["Lafayette"] const LEE = data.general_index["Lee"] const LINCOLN = data.general_index["Lincoln"] const ROCHAMBEAU = data.general_index["Rochambeau"] const WASHINGTON = data.general_index["Washington"] const CARLETON = data.general_index["Carleton"] const HOWE = data.general_index["Howe"] const BOSTON = data.space_index["Boston"] const CHARLESTON = data.space_index["Charleston"] const FALMOUTH = data.space_index["Falmouth"] const FORT_DETROIT = data.space_index["Fort Detroit"] const GILBERT_TOWN = data.space_index["Gilbert Town"] const LEXINGTON_CONCORD = data.space_index["Lexington Concord"] const MONTREAL = data.space_index["Montreal"] const NEWPORT = data.space_index["Newport"] const NINETY_SIX = data.space_index["Ninety Six"] const NORFOLK = data.space_index["Norfolk"] const PHILADELPHIA = data.space_index["Philadelphia"] const QUEBEC = data.space_index["Quebec"] const WILMINGTON_NC = data.space_index["Wilmington"] const CONCORD = data.space_index["Concord"] const CAPTURED_GENERALS = data.space_index["Captured Generals"] const CONTINENTAL_CONGRESS_DISPERSED = data.space_index["Continental Congress Dispersed"] const BRITISH_REINFORCEMENTS = data.space_index["British Reinforcement Box"] const AMERICAN_REINFORCEMENTS = data.space_index["American Leader Reinforcements"] const FRENCH_REINFORCEMENTS = data.space_index["French Reinforcements"] const NOWHERE = data.spaces.length const ALL_COLONIES = [ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13 ] const THE_13_COLONIES = [ 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13 ] const SOUTH_OF_WINTER_ATTRITION_LINE = [ 11, 12, 13 ] const CAMPAIGN_CARDS = [ 67, 68, 69, 70 ] const DECLARATION_OF_INDEPENDENCE = 99 const BARON_VON_STEUBEN = 86 const F_RESHUFFLE = 1 const F_REGULARS = 2 const F_EUROPEAN_WAR = 4 const F_FRENCH_ALLIANCE_TRIGGERED = 8 const F_MUTINIES = 16 const F_CONGRESS_WAS_DISPERSED = 32 const F_LANDING_PARTY = 64 const general_count = data.generals.length const space_count = 66 const all_spaces = new Array(space_count).fill(0).map((_,i)=>i) const all_port_spaces = all_spaces.filter(s => is_port(s)) const ENEMY = { [P_AMERICA]: P_BRITAIN, [P_BRITAIN]: P_AMERICA } var states = {} var events = {} var game var view function card_name(c) { return "\u201c" + CARDS[c].title + "\u201d" } function colony_name(x) { return data.colony_name[x] } function space_name(s) { return SPACES[s].name } function general_name(s) { return GENERALS[s].name } const arnold_quebec_adjacent = [ MONTREAL, FALMOUTH ] const arnold_falmouth_adjacent = [ QUEBEC, CONCORD, BOSTON ] function get_adjacent_for_move(s, who) { if (who === ARNOLD) { if (s === QUEBEC) return arnold_quebec_adjacent if (s === FALMOUTH) return arnold_falmouth_adjacent } return SPACES[s].adjacent } /* SETUP */ function setup_game(seed) { game = { seed: seed, log: [], undo: [], active: P_AMERICA, state: null, // tracks year: 1775, flags: F_REGULARS, // played cards war_ends: 0, reinforcements: [0,0,0], // pieces and markers french_alliance: 0, congress: PHILADELPHIA, french_navy: -1, loca: new Array(general_count).fill(NOWHERE), cupc: new Array(space_count + 2).fill(PC_NONE), // hands & card deck a_hand: [], b_hand: [], a_queue: 0, b_queue: 0, a_draw: 0, b_draw: 0, card: 0, last_played: 0, did_discard_event: 0, deck: null, removed: [], mv: [], mvcu: [], count: 0, } set_space_pc(QUEBEC, PC_BRITISH) set_space_pc(MONTREAL, PC_BRITISH) set_space_pc(FORT_DETROIT, PC_BRITISH) set_space_pc(BOSTON, PC_BRITISH) set_space_pc(NORFOLK, PC_BRITISH) set_space_pc(GILBERT_TOWN, PC_BRITISH) set_space_pc(WILMINGTON_NC, PC_BRITISH) set_space_pc(NINETY_SIX, PC_BRITISH) set_general_location(CARLETON, QUEBEC) set_general_location(HOWE, BOSTON) place_british_cu(QUEBEC, 2) place_british_cu(BOSTON, 5) place_british_cu(FORT_DETROIT, 1) set_space_pc(LEXINGTON_CONCORD, PC_AMERICAN) set_space_pc(CHARLESTON, PC_AMERICAN) set_space_pc(PHILADELPHIA, PC_AMERICAN) set_general_location(WASHINGTON, LEXINGTON_CONCORD) set_general_location(GREENE, NEWPORT) place_american_cu(LEXINGTON_CONCORD, 5) place_american_cu(NEWPORT, 2) place_american_cu(CHARLESTON, 2) set_general_location(BURGOYNE, BRITISH_REINFORCEMENTS) set_general_location(CLINTON, BRITISH_REINFORCEMENTS) set_general_location(CORNWALLIS, BRITISH_REINFORCEMENTS) set_general_location(ARNOLD, AMERICAN_REINFORCEMENTS) set_general_location(LINCOLN, AMERICAN_REINFORCEMENTS) set_general_location(GATES, AMERICAN_REINFORCEMENTS) set_general_location(LEE, AMERICAN_REINFORCEMENTS) set_general_location(LAFAYETTE, AMERICAN_REINFORCEMENTS) set_general_location(ROCHAMBEAU, FRENCH_REINFORCEMENTS) place_french_cu(FRENCH_REINFORCEMENTS, 5) create_deck() goto_committees_of_correspondence() } /* GAME STATE */ function has_flag(f) { return (game.flags & f) !== 0 } function set_flag(f) { return game.flags |= f } function clear_flag(f) { return game.flags &= ~f } function create_deck() { game.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 game.deck.push(i) } shuffle(game.deck) } function reshuffle_deck() { log("Reshuffled.") clear_flag(F_RESHUFFLE) // Reconstitute deck, minus removed cards, cards in hand, war_ends, and reinforcement cards. // Rule 6.1B clarification. game.deck = [] for (let c = 1; c <= 110; ++c) { if (game.a_hand.includes(c)) continue if (game.b_hand.includes(c)) continue if (game.reinforcements.includes(c)) continue if (set_has(game.removed, c)) continue if (game.war_ends === c) continue game.deck.push(c) } shuffle(game.deck) } function roll_d6() { return random(6) + 1 } function deal_card() { if (game.deck.length === 0) reshuffle_deck() return game.deck.pop() } function active_hand() { return game.active === P_AMERICA ? game.a_hand : game.b_hand } function play_card(c) { if (CARDS[c].reshuffle === "if_played") { set_flag(F_RESHUFFLE) } set_delete(active_hand(), c) if (CARDS[c].once) { log("Removed card.") set_add(game.removed, c) } } function discard_card_from_hand(hand, c) { set_delete(hand, c) if (CARDS[c].reshuffle === "if_discarded") set_flag(F_RESHUFFLE) } function discard_card(c) { discard_card_from_hand(active_hand(), c) } function can_exchange_for_discard(c) { if (game.did_discard_event) { let card = CARDS[c] if (card.type === "ops") { if (game.active === P_BRITAIN) return CARDS[c].count > 0 return CARDS[c].count > 1 } } return false } function can_play_event(c) { let card = CARDS[c] switch (card.when) { case "before_french_alliance": return !has_flag(F_FRENCH_ALLIANCE_TRIGGERED) case "after_french_alliance": return has_flag(F_FRENCH_ALLIANCE_TRIGGERED) case "european_war_in_effect": return has_flag(F_EUROPEAN_WAR) } switch (card.event) { case "john_glovers_marblehead_regiment": return has_general_on_map() } return true } function can_play_reinforcements() { if (game.active === P_BRITAIN) { if (game.reinforcements[0] === 0) { let n = count_british_cu(BRITISH_REINFORCEMENTS) for (let g of BRITISH_GENERALS) if (is_general_at_location(g, BRITISH_REINFORCEMENTS)) ++n return n > 0 } return false } if (game.active === P_AMERICA) return game.reinforcements[1] === 0 || game.reinforcements[2] === 0 return false } function is_map_space(s) { return s >= 0 && s <= 65 } function is_port(where) { return SPACES[where].port >= 0 } function is_non_blockaded_port(where) { let port = SPACES[where].port return (port >= 0 && port !== game.french_navy) } 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 (set_has(SOUTH_OF_WINTER_ATTRITION_LINE, colony)) return true // south of winter attrition line let type = SPACES[where].type if (type === "winter_quarters" || type === "fortified_port") return true return false } function allowed_to_place_american_pc() { if (is_continental_congress_dispersed()) return false if (has_flag(F_MUTINIES)) return false return true } function is_british_militia(space) { return get_colony_control(SPACES[space].colony) === PC_BRITISH } function is_american_militia(space) { return get_colony_control(SPACES[space].colony) === PC_AMERICAN } function is_american_winter_offensive() { if (game.move.who === WASHINGTON && game.a_hand.length === 0 && game.b_hand.length === 0 && !game.combat.a_bonus && !game.combat.b_bonus) return true return false } /* PC */ function set_space_pc(space, pc) { game.cupc[space] &= ~3 game.cupc[space] |= pc } function get_space_pc(space) { return game.cupc[space] & 3 } function has_no_pc(space) { return get_space_pc(space) === PC_NONE } function has_british_pc(space) { return get_space_pc(space) === PC_BRITISH } function has_american_pc(space) { return get_space_pc(space) === PC_AMERICAN } function has_enemy_pc(space) { if (game.active === P_BRITAIN) return has_american_pc(space) else return has_british_pc(space) } function has_friendly_pc(space) { if (game.active !== P_BRITAIN) return has_american_pc(space) else return has_british_pc(space) } function is_adjacent_to_british_pc(a) { for (let b of SPACES[a].adjacent) if (has_british_pc(b)) return true if (is_port(a)) { for (let b of all_spaces) { if (is_port(b)) if (has_british_pc(b)) return true } } return false } function is_adjacent_to_american_pc(a) { for (let b of SPACES[a].adjacent) if (has_american_pc(b)) return true return false } function place_british_pc(space) { log("Placed PC at S" + space + ".") if (game.british_pc_space_list) set_delete(game.british_pc_space_list, space) set_space_pc(space, PC_BRITISH) } function place_american_pc(space) { log("Placed PC at S" + space + ".") set_space_pc(space, PC_AMERICAN) } function remove_pc(space) { log("Removed PC at S" + space + ".") set_space_pc(space, PC_NONE) } function flip_pc(space) { log("Flipped PC at S" + space + ".") if (has_british_pc(space)) set_space_pc(space, PC_AMERICAN) else set_space_pc(space, PC_BRITISH) } function get_colony_control(c) { let control = 0 if (c === Canada) { if (get_space_pc(QUEBEC) === PC_BRITISH && get_space_pc(MONTREAL) === PC_BRITISH) return PC_BRITISH if (get_space_pc(QUEBEC) === PC_AMERICAN && get_space_pc(MONTREAL) === PC_AMERICAN) return PC_AMERICAN return PC_NONE } for (let space of COLONIES[c]) { let pc = get_space_pc(space) if (pc === PC_BRITISH) --control else if (pc === PC_AMERICAN) ++control } if (control < 0) return PC_BRITISH if (control > 0) return PC_AMERICAN return PC_NONE } /* CU */ function reset_moved_cu() { map_clear(game.mvcu) } function count_british_cu(s) { return (game.cupc[s] & CU_BRITISH_MASK) >>> CU_BRITISH_SHIFT } function count_american_cu(s) { return (game.cupc[s] & CU_AMERICAN_MASK) >>> CU_AMERICAN_SHIFT } function count_french_cu(s) { return (game.cupc[s] & CU_FRENCH_MASK) >>> CU_FRENCH_SHIFT } function set_british_cu(s, n) { game.cupc[s] &= ~CU_BRITISH_MASK game.cupc[s] |= n << CU_BRITISH_SHIFT } function set_american_cu(s, n) { game.cupc[s] &= ~CU_AMERICAN_MASK game.cupc[s] |= n << CU_AMERICAN_SHIFT } function set_french_cu(s, n) { game.cupc[s] &= ~CU_FRENCH_MASK game.cupc[s] |= n << CU_FRENCH_SHIFT } function has_british_cu(space) { return count_british_cu(space) > 0 } function has_no_british_cu(space) { return count_british_cu(space) === 0 } function has_american_or_french_cu(space) { return count_american_cu(space) > 0 || count_french_cu(space) > 0 } function has_enemy_cu(where) { if (game.active === P_BRITAIN) return has_american_or_french_cu(where) else return has_british_cu(where) } function has_friendly_cu(where) { if (game.active !== P_BRITAIN) return has_american_or_french_cu(where) else return has_british_cu(where) } function count_unmoved_british_cu(where) { return count_british_cu(where) - map_get(game.mvcu, where * 3 + 0, 0) } function count_unmoved_american_cu(where) { return count_american_cu(where) - map_get(game.mvcu, where * 3 + 1, 0) } function count_unmoved_french_cu(where) { return count_french_cu(where) - map_get(game.mvcu, where * 3 + 2, 0) } function mark_moved_cu_imp(offset, where, moved) { if (moved > 0) { let old = map_get(game.mvcu, where * 3 + offset, 0) map_set(game.mvcu, where * 3 + offset, old + moved) } } function mark_moved_british_cu(space, moved) { mark_moved_cu_imp(0, space, moved) } function mark_moved_american_cu(space, moved) { mark_moved_cu_imp(1, space, moved) } function mark_moved_french_cu(space, moved) { mark_moved_cu_imp(2, space, 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 === P_BRITAIN) return count_american_and_french_cu(where) else return count_british_cu(where) } function remove_british_cu(where, count) { set_british_cu(where, count_british_cu(where) - count) } function remove_american_cu(where, count) { set_american_cu(where, count_american_cu(where) - count) } function remove_french_cu(where, count) { set_french_cu(where, count_french_cu(where) - count) } function place_british_cu(where, count) { set_british_cu(where, count_british_cu(where) + count) } function place_american_cu(where, count) { set_american_cu(where, count_american_cu(where) + count) } function place_french_cu(where, count) { set_french_cu(where, count_french_cu(where) + count) } function move_british_cu(from, to, count) { set_british_cu(from, count_british_cu(from) - count) set_british_cu(to, count_british_cu(to) + count) } function move_american_cu(from, to, count) { set_american_cu(from, count_american_cu(from) - count) set_american_cu(to, count_american_cu(to) + count) } function move_french_cu(from, to, count) { set_french_cu(from, count_french_cu(from) - count) set_french_cu(to, count_french_cu(to) + count) } /* GENERALS */ function location_of_general(g) { return game.loca[g] } function set_general_location(g, s) { game.loca[g] = s } function is_general_at_location(g, s) { return location_of_general(g) === s } function is_general_on_map(g) { return is_map_space(location_of_general(g)) } function has_general_moved(g) { return set_has(game.mv, g) } function set_general_moved(g) { set_add(game.mv, g) } function reset_moved_generals() { set_clear(game.mv) } function find_british_general(where) { for (let general of BRITISH_GENERALS) if (is_general_at_location(general, where)) return general return NOBODY } function find_american_or_french_general(where) { for (let general of AMERICAN_GENERALS) if (is_general_at_location(general, where)) return general return NOBODY } function has_british_general(where) { return find_british_general(where) !== NOBODY } function has_american_or_french_general(where) { return find_american_or_french_general(where) !== NOBODY } function has_enemy_general(where) { if (game.active === P_BRITAIN) return has_american_or_french_general(where) else return has_british_general(where) } function count_friendly_generals(where) { let list if (game.active === P_BRITAIN) list = BRITISH_GENERALS else list = AMERICAN_GENERALS let count = 0 for (let g of list) if (is_general_at_location(g, where)) ++count return count } function has_general_on_map() { if (game.active === P_BRITAIN) 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 === P_BRITAIN) 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) { set_general_location(who, where) } function capture_washington() { set_general_location(WASHINGTON, NOWHERE) if (!has_flag(F_FRENCH_ALLIANCE_TRIGGERED)) { game.french_alliance -= 3 if (game.french_alliance < 0) game.french_alliance = 0 } game.washington_captured = 1 } function capture_british_general(where) { let g = find_british_general(where) log("Captured G" + g + ".") move_general(g, CAPTURED_GENERALS) } function capture_american_or_french_general(where) { let g = find_american_or_french_general(where) log("Captured G" + g + ".") if (g === WASHINGTON) capture_washington() else move_general(g, CAPTURED_GENERALS) } function capture_enemy_general(where) { if (game.active === P_BRITAIN) capture_american_or_french_general(where) else capture_british_general(where) } /* 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 pickup_max_british_cu(move) { move.carry_british = count_unmoved_british_cu(move.from) if (move.carry_british > 5) move.carry_british = 5 move.carry_american = 0 move.carry_french = 0 } function pickup_max_american_cu(move) { move.carry_british = 0 move.carry_french = count_unmoved_french_cu(move.from) move.carry_american = count_unmoved_american_cu(move.from) if (move.carry_french > 5) move.carry_french = 5 if (move.carry_american + move.carry_french > 5) move.carry_american = 5 - move.carry_french } function move_army(army) { if (army.carry_british > 0) move_british_cu(army.from, army.to, army.carry_british) if (army.carry_american > 0) move_american_cu(army.from, army.to, army.carry_american) if (army.carry_french > 0) move_french_cu(army.from, army.to, army.carry_french) move_general(army.who, army.to) } function overrun(where) { log("Overrun.") if (game.active === P_BRITAIN) { if (count_american_cu(where) > 0) remove_american_cu(where, 1) else remove_french_cu(where, 1) } else { advance_french_alliance(1) remove_british_cu(where, 1) } } function log_retreat(g, cu, to) { if (g !== NOBODY) { if (cu > 0) log("Retreated G" + g + " with " + cu + " CU to S" + to + ".") else log("Retreated G" + g + " to S" + to + ".") } else { log("Retreated " + cu + " CU to S" + to + ".") } } function retreat_american_army(from, to) { let g = find_american_or_french_general(from) if (g !== NOBODY) move_general(g, to) let a_cu = count_american_cu(from) let f_cu = count_french_cu(from) move_american_cu(from, to, a_cu) move_french_cu(from, to, f_cu) log_retreat(g, a_cu + f_cu, to) } function retreat_british_army(from, to) { let g = find_british_general(from) if (g !== NOBODY) move_general(g, to) let b_cu = count_british_cu(from) move_british_cu(from, to, b_cu) log_retreat(g, b_cu, to) } function surrender_american_army(where) { let g = find_american_or_french_general(where) if (g !== NOBODY) capture_american_or_french_general(where) remove_american_cu(where, count_american_cu(where)) remove_french_cu(where, count_french_cu(where)) } function surrender_british_army(where) { let g = find_british_general(where) if (g !== NOBODY) capture_british_general(where) game.combat.british_losses += count_british_cu(where) remove_british_cu(where, count_british_cu(where)) } function disperse_continental_congress() { log("Dispersed Contintental Congress.") game.congress = CONTINENTAL_CONGRESS_DISPERSED set_flag(F_CONGRESS_WAS_DISPERSED) } /* PLACE/REMOVE PC IN COLONY */ function gen_place_american_pc_in_colony(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_space(space) } function filter_place_american_pc_in_colony(list_of_colonies) { let result = [] for (let colony of list_of_colonies) { for (let space of COLONIES[colony]) { if (has_no_pc(space) && has_no_british_playing_piece(space)) { result.push(colony) break } } } return result } function gen_remove_american_pc_in_colony(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_space(space) } function filter_remove_american_pc_in_colony(list_of_colonies) { let result = [] for (let colony of list_of_colonies) { for (let space of COLONIES[colony]) { if (has_american_pc(space) && has_no_american_unit(space)) { result.push(colony) break } } } return result } /* REMOVE GENERAL (STACKING) */ function prompt_remove_general(where) { view.prompt = "Remove a general to the reinforcements box." if (game.active === P_BRITAIN) { for (let g of BRITISH_GENERALS) if (is_general_at_location(g, where)) gen_action_general(g) } else { for (let g of AMERICAN_GENERALS) if (g !== WASHINGTON) if (is_general_at_location(g, where)) gen_action_general(g) } } function action_remove_general(g) { if (game.active === P_BRITAIN) move_general(g, BRITISH_REINFORCEMENTS) else move_general(g, AMERICAN_REINFORCEMENTS) } function goto_remove_general_after_move(where) { game.state = "remove_general_after_move" game.where = where } states.remove_general_after_move = { inactive: "to remove a general", prompt() { prompt_remove_general(game.where) }, general(g) { push_undo() action_remove_general(g) delete game.where end_strategy_card() }, } function goto_remove_general_after_intercept() { game.state = "remove_general_after_intercept" } states.remove_general_after_intercept = { inactive: "to remove a general", prompt() { prompt_remove_general(game.move.to) }, general(g) { push_undo() action_remove_general(g) game.state = "remove_general_after_intercept_confirm" }, } states.remove_general_after_intercept_confirm = { inactive: "to remove a general", prompt() { view.prompt = "Intercept done." view.actions.next = 1 }, next() { clear_undo() end_intercept() }, } function goto_remove_general_after_retreat_before_battle(where) { if (count_friendly_generals(where) > 1) { game.state = "remove_general_after_retreat_before_battle" game.where = where } else { end_retreat_before_battle() } } states.remove_general_after_retreat_before_battle = { inactive: "to remove a general", prompt() { prompt_remove_general(game.where) }, general(g) { push_undo() action_remove_general(g) delete game.where game.state = "remove_general_after_retreat_before_battle_confirm" }, } states.remove_general_after_retreat_before_battle_confirm = { inactive: "to remove a general", prompt() { view.prompt = "Retreat done." view.actions.next = 1 }, next() { clear_undo() end_retreat_before_battle() }, } function goto_remove_general_after_retreat(where) { game.state = "remove_general_after_retreat" game.where = where } states.remove_general_after_retreat = { inactive: "to remove a general", prompt() { prompt_remove_general(game.where) }, general(g) { push_undo() action_remove_general(g) if (g === game.move.who) game.move.who = NOBODY delete game.where game.state = "retreat_after_battle_confirm" }, } /* SETUP PHASE */ function goto_committees_of_correspondence() { log("=a Committees of Correspondence") game.active = P_AMERICA game.state = "committees_of_correspondence" game.colonies = THE_13_COLONIES.slice() } states.committees_of_correspondence = { inactive: "to place PC markers", prompt() { if (game.colonies.length > 0) { view.prompt = "Committees of Correspondence: Place a PC marker in " if (game.colonies.length <= 3) view.prompt += game.colonies.map(colony_name).join(" and ") + "." else view.prompt += "each of the 13 colonies." gen_place_american_pc_in_colony(game.colonies) } else { view.prompt = "Committees of Correspondence: Done." view.actions.next = 1 } }, space(s) { push_undo() let colony = SPACES[s].colony set_delete(game.colonies, colony) place_american_pc(s) }, next() { clear_undo() delete game.colonies goto_for_the_king() }, } function goto_for_the_king() { log("=b For the King") game.active = P_BRITAIN game.state = "for_the_king" game.count = 3 gen_british_pc_ops_start() } states.for_the_king = { inactive: "to place PC markers", prompt() { if (game.count > 0) { view.prompt = "For the King: Place 3 PC markers." gen_british_pc_ops() } else { view.actions.next = 1 view.prompt = "For the King: Done." } }, space(s) { push_undo() place_british_pc(s) --game.count }, next() { clear_undo() gen_british_pc_ops_end() goto_start_year() }, } /* REINFORCEMENTS AND START OF STRATEGY PHASE */ function goto_start_year() { log("=t Year " + game.year) // Prisoner exchange for (let g of BRITISH_GENERALS) if (is_general_at_location(g, CAPTURED_GENERALS)) move_general(g, BRITISH_REINFORCEMENTS) for (let g of AMERICAN_GENERALS) if (is_general_at_location(g, 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) { log("Added C" + DECLARATION_OF_INDEPENDENCE + ".") log("Added C" + BARON_VON_STEUBEN + ".") log("Shuffled.") game.deck.push(DECLARATION_OF_INDEPENDENCE) game.deck.push(BARON_VON_STEUBEN) shuffle(game.deck) } if (has_flag(F_RESHUFFLE)) { reshuffle_deck() } game.a_queue = 0 game.b_queue = 0 game.did_discard_event = 0 game.reinforcements = [0,0,0] game.active = P_BRITAIN game.state = "british_declare_first" game.a_hand = [] game.b_hand = [] for (let i = 0; i < 7; ++i) { set_add(game.a_hand, deal_card()) set_add(game.b_hand, deal_card()) } } states.british_declare_first = { inactive: "to declare first player", prompt() { view.prompt = "Declare yourself as the first player by playing a campaign card?" view.actions.pass = 1 for (let c of CAMPAIGN_CARDS) { if (game.b_hand.includes(c)) { gen_action_card("card", c) } } }, card(c) { push_undo() clear_flag(F_CONGRESS_WAS_DISPERSED) log("Britain declared first player.") log("=b Britain") game.card = c game.state = "strategy_phase_event" }, pass() { if (has_flag(F_CONGRESS_WAS_DISPERSED)) game.active = P_BRITAIN else game.active = P_AMERICA clear_flag(F_CONGRESS_WAS_DISPERSED) game.state = "choose_first_player" }, } states.choose_first_player = { inactive: "to declare first player", prompt() { view.prompt = "Choose who will play the first strategy card." view.actions.america_first = 1 view.actions.britain_first = 1 }, america_first() { if (game.active !== P_AMERICA) log("Britain chose America to play first.") goto_strategy_phase(P_AMERICA) }, britain_first() { if (game.active !== P_BRITAIN) log("America chose Britain to play first.") goto_strategy_phase(P_BRITAIN) }, } /* STRATEGY PHASE */ function goto_strategy_phase(new_active) { game.active = new_active game.state = "strategy_phase" 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() } if (game.active === P_AMERICA) log("=a America") else log("=b Britain") } states.strategy_phase = { inactive: "to play a strategy card", prompt() { view.prompt = "Play a strategy card." for (let c of active_hand()) { gen_action_card("card", c) if (can_exchange_for_discard(c)) view.actions.exchange = 1 } }, card(c) { push_undo() let card = CARDS[c] game.card = c switch (card.type) { case "ops": game.state = "strategy_phase_ops" break default: game.state = "strategy_phase_event" break } }, exchange() { push_undo() let d = game.did_discard_event set_add(active_hand(), d) game.state = "exchange" }, } states.exchange = { inactive: "to play a strategy card", prompt() { view.prompt = "Exchange an OPS card for " + card_name(game.did_discard_event) + "." view.selected_card = game.did_discard_event for (let c of active_hand()) if (can_exchange_for_discard(c)) gen_action_card("card", c) }, card(c) { let x = game.did_discard_event set_delete(active_hand(), c) game.did_discard_event = 0 log("Exchanged C" + c + " for C" + x + ".") game.state = "strategy_phase" }, } states.strategy_phase_ops = { inactive: "to play a strategy card", prompt() { let c = game.card view.selected_card = game.card view.prompt = "Use " + card_name(c) + "." if (can_activate_general(c)) view.actions.activate = 1 else view.actions.activate = 0 if (can_play_reinforcements()) view.actions.reinforce = 1 else view.actions.reinforce = 0 if (game.active === P_AMERICA && game.a_queue + CARDS[c].count < 3) view.actions.queue = 1 if (game.active === P_BRITAIN && game.b_queue + CARDS[c].count < 3) view.actions.queue = 1 view.actions.pc_action = 1 }, pc_action() { let c = game.card game.did_discard_event = 0 clear_queue() log("Played C" + c + ".") play_card(c) goto_ops_pc(CARDS[c].count) }, activate() { let c = game.card game.did_discard_event = 0 log("Played C" + c + ".") play_card(c) goto_ops_general(c) }, reinforce() { let c = game.card game.did_discard_event = 0 clear_queue() log("Played C" + c + ".") play_card(c) goto_ops_reinforcements(c) }, queue() { let c = game.card game.did_discard_event = 0 log("Played C" + c + ".") play_card(c) log("Operations Queue.") if (game.active === P_BRITAIN) game.b_queue += CARDS[c].count else game.a_queue += CARDS[c].count end_strategy_card() }, } states.strategy_phase_event = { inactive: "to play a strategy card", prompt() { let c = game.card view.selected_card = game.card view.prompt = "Use " + card_name(c) + "." view.actions.event = 0 view.actions.pc_action = 0 let card = CARDS[c] switch (card.type) { case "mandatory-event": view.actions.event = 1 view.actions.pc_action = 0 break case "campaign": view.actions.event = 1 view.actions.pc_action = 1 break case "british-event": case "british-event-or-battle": if (game.active === P_BRITAIN) { if (can_play_event(c)) view.actions.event = 1 else view.actions.event = 0 } view.actions.pc_action = 1 break case "american-event": if (game.active === P_AMERICA) { if (can_play_event(c)) view.actions.event = 1 else view.actions.event = 0 } view.actions.pc_action = 1 break case "british-battle": case "american-battle": view.actions.pc_action = 1 break } }, event() { let c = game.card game.did_discard_event = 0 clear_queue() do_event(c) }, pc_action() { let c = game.card game.did_discard_event = c clear_queue() log("Discarded C" + c + ".") discard_card(c) game.state = "discard_event_pc_action" }, } function end_strategy_card() { if (automatic_victory()) { clear_undo() return false } if (game.washington_captured) { goto_george_washington_captured() return false } if (game.campaign) { if (--game.campaign > 0) { game.count = 3 // can activate any general! game.state = "ops_general_who" return false } else { clear_flag(F_LANDING_PARTY) delete game.campaign } } game.state = "end_strategy_card" return true } states.end_strategy_card = { inactive: "to finish the card play", prompt() { view.prompt = "Card play done." view.actions.next = 1 }, next() { clear_undo() next_strategy_card() }, } function next_strategy_card() { clear_undo() if (!has_flag(F_FRENCH_ALLIANCE_TRIGGERED) && game.french_alliance === 9) { log("The French signed an alliance with the Americans!") set_flag(F_FRENCH_ALLIANCE_TRIGGERED) if (game.french_navy === -1) { game.save = game.active game.active = P_AMERICA game.state = "place_french_navy_trigger" return } } reset_moved_generals() reset_moved_cu() // Tournament rule for DoI and Franklin events if (game.card > 0 && CARDS[game.card].tournament && game.active === P_BRITAIN) { log_br() log("Tournament Rule!") log("British replacement card.") set_add(game.b_hand, game.replacement_card = deal_card()) game.state = "draw_replacement_card_tournament" return } game.card = 0 if (game.active === P_BRITAIN) { if (draw_british_replacement()) { game.state = "draw_replacement_card" return } if (draw_american_replacement()) { game.active = P_AMERICA game.state = "draw_replacement_card_opponent" return } } if (game.active === P_AMERICA) { if (draw_american_replacement()) { game.state = "draw_replacement_card" return } if (draw_british_replacement()) { game.active = P_BRITAIN game.state = "draw_replacement_card_opponent" return } } goto_strategy_phase(ENEMY[game.active]) } function end_strategy_card_now() { if (end_strategy_card()) next_strategy_card() } function clear_queue() { if (game.active === P_BRITAIN) game.b_queue = 0 else game.a_queue = 0 } /* REPLACEMENT CARDS AFTER BATTLE EVENTS */ function draw_british_replacement() { if (game.b_draw > 0) { log("British replacement card.") set_add(game.b_hand, game.replacement_card = deal_card()) game.b_draw-- return true } return false } function draw_american_replacement() { if (game.a_draw > 0) { log("American replacement card.") set_add(game.a_hand, game.replacement_card = deal_card()) game.a_draw-- return true } return false } states.draw_replacement_card = { inactive: "to draw a replacement card", prompt() { view.prompt = "You drew " + card_name(game.replacement_card) + "." view.selected_card = game.replacement_card view.actions.next = 1 }, next() { clear_undo() delete game.replacement_card next_strategy_card() }, } states.draw_replacement_card_opponent = { inactive: "to draw a replacement card", prompt() { view.prompt = "You drew " + card_name(game.replacement_card) + "." view.selected_card = game.replacement_card view.actions.next = 1 }, next() { clear_undo() delete game.replacement_card game.active = ENEMY[game.active] next_strategy_card() }, } states.draw_replacement_card_tournament = { inactive: "to draw a replacement card", prompt() { view.prompt = "You drew " + card_name(game.replacement_card) + "." view.selected_card = game.replacement_card view.actions.next = 1 }, next() { clear_undo() delete game.replacement_card goto_strategy_phase(game.active) }, } /* DISCARD EVENT CARD FOR PC ACTION */ states.discard_event_pc_action = { inactive: "to take a PC action", prompt() { view.prompt = "Place, flip, or remove PC marker." if (game.active === P_BRITAIN) gen_british_discard_event_pc_action() else gen_american_discard_event_pc_action() view.actions.confirm_pass = 1 }, space(s) { push_undo() if (game.active === P_BRITAIN) { if (has_no_pc(s)) place_british_pc(s) else if (has_british_army(s)) flip_pc(s) else remove_pc(s) } else { if (has_no_pc(s)) place_american_pc(s) else if (has_american_army(s)) flip_pc(s) else remove_pc(s) } end_strategy_card() }, pass() { end_strategy_card_now() }, } function gen_british_discard_event_pc_action() { for (let space of all_spaces) { if (is_adjacent_to_british_pc(space)) { if (has_no_pc(space) && has_no_american_unit(space)) gen_action_space(space) else if (has_american_pc(space) && has_british_army(space)) gen_action_space(space) else if (has_american_pc(space) && has_no_american_unit(space)) gen_action_space(space) } } } function gen_american_discard_event_pc_action() { for (let space of all_spaces) { if (is_adjacent_to_american_pc(space)) { if (has_no_pc(space) && has_no_british_cu(space)) { if (allowed_to_place_american_pc()) gen_action_space(space) } else if (has_british_pc(space) && has_american_or_french_general(space)) { gen_action_space(space) } else if (has_british_pc(space) && has_no_british_cu(space)) { gen_action_space(space) } } } } /* PLAY OPS CARD FOR PC ACTIONS */ function goto_ops_pc(count) { game.count = count game.state = "ops_pc" if (game.active === P_BRITAIN) gen_british_pc_ops_start() } states.ops_pc = { inactive: "to take a PC action", prompt() { view.prompt = "Place or flip PC markers. " + game.count + " left." if (game.count > 0) { if (game.active === P_BRITAIN) gen_british_pc_ops() else gen_american_pc_ops() } view.actions.confirm_pass = 1 }, space(s) { push_undo() if (game.active === P_BRITAIN) { if (has_american_pc(s)) flip_pc(s) else place_british_pc(s) } else { if (has_british_pc(s)) flip_pc(s) else place_american_pc(s) } if (--game.count === 0) { end_ops_pc() end_strategy_card() } }, pass() { end_ops_pc() end_strategy_card_now() }, } function end_ops_pc() { if (game.active === P_BRITAIN) gen_british_pc_ops_end() } function gen_british_pc_ops_start() { game.british_pc_space_list = [] for (let space of all_spaces) { if (has_no_pc(space) && has_no_american_unit(space)) { if (is_adjacent_to_british_pc(space)) set_add(game.british_pc_space_list, space) } } } function gen_british_pc_ops() { for (let space of game.british_pc_space_list) gen_action_space(space) for (let space of all_spaces) { if (has_british_army(space)) { if (has_no_pc(space)) gen_action_space(space) else if (has_american_pc(space)) gen_action_space(space) } } } function gen_british_pc_ops_end() { delete game.british_pc_space_list } function gen_american_pc_ops() { for (let space of all_spaces) { if (has_no_pc(space) && has_no_british_cu(space)) { if (allowed_to_place_american_pc()) gen_action_space(space) } else if (has_british_pc(space) && has_american_or_french_general(space)) { gen_action_space(space) } } } /* PLAY OPS CARD FOR REINFORCEMENTS */ function place_british_reinforcements(who, count, where) { let already_there = find_british_general(where) if (who !== NOBODY && already_there !== NOBODY) { move_general(already_there, BRITISH_REINFORCEMENTS) } if (who !== NOBODY) { log(`Reinforced ${count} CU and G${who} at S${where}.`) move_general(who, where) } else { log(`Reinforced ${count} CU at S${where}.`) } if (count > 0) { move_british_cu(BRITISH_REINFORCEMENTS, where, count) if (has_enemy_general(where)) capture_enemy_general(where) if (game.congress === where) disperse_continental_congress() } } function place_american_reinforcements(who, count, where) { let already_there = find_american_or_french_general(where) if (who !== NOBODY && already_there !== NOBODY) { // Never replace Washington if (already_there === WASHINGTON) who = NOBODY else move_general(already_there, AMERICAN_REINFORCEMENTS) } if (who !== NOBODY) { log(`Reinforced ${count} CU and G${who} at S${where}.`) move_general(who, where) } else { log(`Reinforced ${count} CU at S${where}.`) } 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 !== NOBODY && already_there !== NOBODY) { // Never replace Washington if (already_there === WASHINGTON) who = NOBODY else move_general(already_there, AMERICAN_REINFORCEMENTS) } if (who !== NOBODY) { log(`Reinforced 5 French CU and G${who} at S${where}.`) move_general(who, where) } else { log(`Reinforced 5 French CU at S${where}.`) } move_french_cu(FRENCH_REINFORCEMENTS, where, count_french_cu(FRENCH_REINFORCEMENTS)) move_french_cu(AMERICAN_REINFORCEMENTS, where, count_french_cu(AMERICAN_REINFORCEMENTS)) } function goto_ops_reinforcements(c) { let count = CARDS[c].count if (game.active === P_BRITAIN) { game.reinforcements[0] = c game.count = count_british_cu(BRITISH_REINFORCEMENTS) game.state = "ops_british_reinforcements_who" } else { if (game.reinforcements[1] === 0) game.reinforcements[1] = c else game.reinforcements[2] = c game.count = count game.state = "ops_american_reinforcements_who" } } states.ops_british_reinforcements_who = { inactive: "to reinforce", prompt() { view.prompt = "Reinforcements: Choose an available general." view.prompt += " " + game.count + " British CU." view.move = { from: BRITISH_REINFORCEMENTS, to: BRITISH_REINFORCEMENTS, who: NOBODY, carry_british: game.count } view.actions.no_general = 1 gen_british_reinforcements_who() gen_british_reinforcements_cu() }, drop_british_cu() { push_undo() --game.count }, pickup_british_cu() { push_undo() ++game.count }, general(g) { push_undo() game.state = "ops_british_reinforcements_where" game.who = g }, no_general() { push_undo() game.state = "ops_british_reinforcements_where" game.who = NOBODY }, } states.ops_british_reinforcements_where = { inactive: "to reinforce", prompt() { view.prompt = "Reinforcements: Choose a port space." view.prompt += " " + game.count + " British CU." view.move = { from: BRITISH_REINFORCEMENTS, to: BRITISH_REINFORCEMENTS, who: game.who, carry_british: game.count } view.selected_general = game.who gen_british_reinforcements_where() gen_british_reinforcements_cu() }, drop_british_cu() { push_undo() --game.count }, pickup_british_cu() { push_undo() ++game.count }, space(space) { push_undo() place_british_reinforcements(game.who, game.count, space) delete game.who // capture george washington happens in end_strategy_card end_strategy_card() }, } states.ops_american_reinforcements_who = { inactive: "to reinforce", prompt() { view.prompt = "Reinforcements: Choose an available general." view.move = { from: AMERICAN_REINFORCEMENTS, to: AMERICAN_REINFORCEMENTS, who: NOBODY, carry_american: game.count } view.actions.no_general = 1 gen_american_reinforcements_who() }, general(g) { push_undo() game.state = "ops_american_reinforcements_where" game.who = g }, no_general() { push_undo() game.state = "ops_american_reinforcements_where" game.who = NOBODY }, } states.ops_american_reinforcements_where = { inactive: "to reinforce", prompt() { view.prompt = "Reinforcements: Choose a space." view.selected_general = game.who if (game.who === ROCHAMBEAU && location_of_general(ROCHAMBEAU) === FRENCH_REINFORCEMENTS) { view.move = { from: FRENCH_REINFORCEMENTS, to: FRENCH_REINFORCEMENTS, who: game.who, carry_american: 0, carry_french: 5 } gen_french_reinforcements_where(game.who) } else { view.move = { from: AMERICAN_REINFORCEMENTS, to: AMERICAN_REINFORCEMENTS, who: game.who, carry_american: game.count } gen_american_reinforcements_where(game.who) } }, space(space) { if (game.who === ROCHAMBEAU && location_of_general(ROCHAMBEAU) === FRENCH_REINFORCEMENTS) place_french_reinforcements(game.who, space) else place_american_reinforcements(game.who, game.count, space) delete game.who end_strategy_card() }, } function gen_british_reinforcements_cu() { if (game.count > 0) view.actions.drop_british_cu = 1 else view.actions.drop_british_cu = 0 if (game.count < count_british_cu(BRITISH_REINFORCEMENTS)) view.actions.pickup_british_cu = 1 else view.actions.pickup_british_cu = 0 } function gen_british_reinforcements_who() { for (let g of BRITISH_GENERALS) { if (is_general_at_location(g, BRITISH_REINFORCEMENTS)) { gen_action_general(g) } } } function gen_british_reinforcements_where() { for (let space of all_spaces) { if (is_non_blockaded_port(space)) if (!has_american_or_french_cu(space) && !has_american_pc(space)) gen_action_space(space) } } function gen_american_reinforcements_who() { for (let g of AMERICAN_GENERALS) { if (is_general_at_location(g, AMERICAN_REINFORCEMENTS)) { gen_action_general(g) } } } function gen_american_reinforcements_where(general) { for (let space of all_spaces) { if (!has_british_cu(space) && !has_british_pc(space)) { gen_action_space(space) } } } function gen_french_reinforcements_where(general) { for (let space of all_port_spaces) { if (!has_british_cu(space) && !has_british_pc(space)) { gen_action_space(space) } } } /* PLAY OPS CARD TO MOVE A GENERAL */ function goto_ops_general(c) { if (game.active === P_BRITAIN) { 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 = { inactive: "to move", prompt() { let land = can_use_landing_party() if (land) 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 (land) view.actions.landing_party = 1 gen_activate_general() view.actions.confirm_pass = 1 }, landing_party() { push_undo() game.state = "landing_party" }, general(g) { push_undo() goto_ops_general_move(g, false) }, pass() { push_undo() if (game.campaign > 0) game.campaign = 0 end_strategy_card_now() }, } states.landing_party = { inactive: "to move", prompt() { view.prompt = "Campaign: Flip or place a PC in a port." gen_landing_party() }, space(where) { log("Landing Party.") clear_flag(F_LANDING_PARTY) if (has_american_pc()) flip_pc(where) else place_british_pc(where) // uses one campaign activation end_strategy_card() }, } function can_use_landing_party() { if (game.campaign && has_flag(F_LANDING_PARTY)) { for (let space of all_spaces) { if (!is_fortified_port(space) && is_non_blockaded_port(space)) { if (has_american_pc(space) && has_no_american_unit(space)) return true if (has_no_pc(space) && has_no_american_unit(space) && has_no_british_playing_piece(space)) return true } } } return false } function gen_landing_party() { for (let space of all_spaces) { if (!is_fortified_port(space) && is_non_blockaded_port(space)) { if (has_american_pc(space) && has_no_american_unit(space)) gen_action_space(space) if (has_no_pc(space) && has_no_american_unit(space) && has_no_british_playing_piece(space)) gen_action_space(space) } } } function gen_activate_general() { if (game.active === P_BRITAIN) 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 && !has_general_moved(g)) gen_action_general(g) } function gen_activate_american_general() { for (let g of AMERICAN_GENERALS) if (is_general_on_map(g) && GENERALS[g].strategy <= game.count && !has_general_moved(g)) gen_action_general(g) } function goto_ops_general_move(g, marblehead) { game.state = "ops_general_move" let where = location_of_general(g) game.move = { who: g, from: where, to: where, count: 0, mobility: false, carry_british: 0, carry_american: 0, carry_french: 0 } if (marblehead) { game.move.mobility = false game.move.count = 6 } else { if (game.active === P_BRITAIN) { game.move.mobility = false game.move.count = 4 } else { game.move.mobility = true game.move.count = 5 } } if (game.active === P_BRITAIN) pickup_max_british_cu(game.move) else pickup_max_american_cu(game.move) log(`Moved G${g}\nfrom S${where}`) } function format_move_prompt() { view.prompt = "Move " + data.generals[game.move.who].name + " with " if (game.move.carry_british > 0) { view.prompt += game.move.carry_british + " CU." } else if (game.move.carry_french + game.move.carry_american > 0) { if (game.move.carry_french > 0) { if (game.move.carry_american > 0) { view.prompt += game.move.carry_french + " French CU and " view.prompt += game.move.carry_american + " American CU." } else { view.prompt += game.move.carry_french + " CU." } } else { view.prompt += game.move.carry_american + " CU." } } else { view.prompt += game.move.carry_american + " no CU." } } states.ops_general_move = { inactive: "to move", prompt() { format_move_prompt() view.prompt += " " + game.move.count + " MP left." view.selected_general = view.move.who // Cannot stop on enemy general if (!has_enemy_general(location_of_general(game.move.who))) view.actions.stop = 1 if (view.move.count > 0) { gen_carry_cu() gen_move_general() } }, pickup_british_cu() { push_undo() if (has_general_moved(game.move.who)) log(">>picked up CU") ++game.move.carry_british }, pickup_american_cu() { push_undo() if (has_general_moved(game.move.who)) log(">>picked up CU") ++game.move.carry_american }, pickup_french_cu() { push_undo() if (has_general_moved(game.move.who)) log(">>picked up CU") ++game.move.carry_french }, drop_british_cu() { push_undo() --game.move.carry_british if (has_general_moved(game.move.who)) { log(">>dropped CU") mark_moved_british_cu(location_of_general(game.move.who), 1) } }, drop_american_cu() { push_undo() --game.move.carry_american if (has_general_moved(game.move.who)) { log(">>dropped CU") mark_moved_american_cu(location_of_general(game.move.who), 1) } }, drop_french_cu() { push_undo() --game.move.carry_french if (has_general_moved(game.move.who)) { log(">>dropped CU") mark_moved_french_cu(location_of_general(game.move.who), 1) } }, space(to) { push_undo() let from = location_of_general(game.move.who) let cu = game.move.carry_british + game.move.carry_american + game.move.carry_french if (!has_general_moved(game.move.who)) { log(">>with " + cu + " CU") } set_general_moved(game.move.who) let may_intercept = false if (game.active === P_BRITAIN) { let is_sea_move = path_type(from, to) === "sea" if (has_american_pc(to) && cu > 0 && !is_sea_move && !has_british_cu(to)) may_intercept = can_intercept_to(to) } game.move.from = from game.move.to = to game.move.count -= movement_cost(from, to) if (game.move.mobility && has_enemy_cu(to)) { game.move.mobility = false game.move.count -= 1 } log(">to S" + game.move.to) move_army(game.move) if (may_intercept) game.state = "confirm_move_intercept" else { disperse_and_overrun_after_move() if (game.active === P_BRITAIN && has_enemy_cu(game.move.to)) game.state = "confirm_move_battle" else if (game.active === P_AMERICA && has_enemy_cu(game.move.to)) goto_start_battle() else resume_moving() } }, stop() { push_undo() end_move(true) }, } states.confirm_move_intercept = { inactive: "to move", prompt() { format_move_prompt() view.prompt += " You may be intercepted." view.selected_general = view.move.who view.actions.next = 1 }, next() { goto_intercept() }, } states.confirm_move_battle = { inactive: "to move", prompt() { format_move_prompt() view.prompt += " Approach battle?" view.selected_general = view.move.who view.actions.next = 1 }, next() { clear_undo() goto_start_battle() }, } function disperse_and_overrun_after_move() { let cu = game.move.carry_british + game.move.carry_american + game.move.carry_french let to = game.move.to if (cu > 0) { let egen = has_enemy_general(to) let ecu = count_enemy_cu(to) if (egen && ecu === 0) capture_enemy_general(to) if (cu >= 4 && ecu === 1 && !egen) overrun(to) if (game.active === P_BRITAIN && game.congress === to && count_enemy_cu(to) === 0) disperse_continental_congress() } } function resume_moving() { game.move.from = game.move.to game.state = "ops_general_move" if (game.move.count === 0) end_move(false) else if (game.washington_captured) goto_george_washington_captured_during_move() } function end_move(stop) { let where = location_of_general(game.move.who) if (has_general_moved(game.move.who)) { mark_moved_british_cu(where, game.move.carry_british) mark_moved_american_cu(where, game.move.carry_american) mark_moved_french_cu(where, game.move.carry_french) } delete game.move if (is_map_space(where) && count_friendly_generals(where) > 1) goto_remove_general_after_move(where) else { if (stop) end_strategy_card_now() else end_strategy_card() } } function path_type(from, to) { if ((from === QUEBEC && to === FALMOUTH) || (to === QUEBEC && from === FALMOUTH)) return (game.active === P_BRITAIN) ? "sea" : "wilderness" if (set_has(SPACES[from].path, to)) return "path" if (set_has(SPACES[from].wilderness, to)) return "wilderness" return "sea" } function gen_carry_cu() { let where = location_of_general(game.move.who) if (game.active === P_BRITAIN) { view.actions.drop_british_cu = 0 view.actions.pickup_british_cu = 0 if (game.move.carry_british > 0) view.actions.drop_british_cu = 1 if (game.move.carry_british < 5 && game.move.carry_british < count_unmoved_british_cu(where)) view.actions.pickup_british_cu = 1 } else { if (game.move.carry_french > 0 || count_french_cu(where) > 0) { view.actions.drop_french_cu = 0 view.actions.pickup_french_cu = 0 } if (game.move.carry_american > 0 || count_american_cu(where) > 0) { view.actions.drop_american_cu = 0 view.actions.pickup_american_cu = 0 } let carry_total = game.move.carry_french + game.move.carry_american if (game.move.carry_french > 0) view.actions.drop_french_cu = 1 if (game.move.carry_american > 0) view.actions.drop_american_cu = 1 if (carry_total < 5 && game.move.carry_french < count_unmoved_french_cu(where)) view.actions.pickup_french_cu = 1 if (carry_total < 5 && game.move.carry_american < count_unmoved_american_cu(where)) view.actions.pickup_american_cu = 1 } } function movement_cost(from, to) { switch (path_type(from, to)) { case "sea": return 4 /* must be a sea connection if no direct path */ case "wilderness": return 3 case "path": return 1 default: throw "IMPOSSIBLE" } } function gen_move_general() { let from = location_of_general(game.move.who) let alone = game.move.carry_british + game.move.carry_american + game.move.carry_french === 0 for (let to of get_adjacent_for_move(from, game.move.who)) { let mp = 1 if (path_type(from, to) === "wilderness") 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 player gets stuck) if (has_enemy_general(to) && game.count - mp === 0) continue } if (game.move.mobility && has_enemy_cu(to)) { if (game.move.count - mp >= 1) gen_action_space(to) } else { if (game.move.count - mp >= 0) gen_action_space(to) } } if (game.active === P_BRITAIN && game.move.count === 4) { if (is_non_blockaded_port(from)) { for (let to of all_spaces) { if (to !== from) { if (is_non_blockaded_port(to)) { if (!has_american_pc(to) && !has_american_or_french_cu(to)) { // don't leave alone if (alone && has_enemy_general(to)) continue gen_action_space(to) } } } } } } } /* INTERCEPT */ function can_intercept_to(to) { if (to === QUEBEC && find_american_or_french_general(FALMOUTH) === ARNOLD && has_american_or_french_cu(FALMOUTH)) return true if (to === FALMOUTH && find_american_or_french_general(QUEBEC) === ARNOLD && has_american_or_french_cu(QUEBEC)) return true for (let from of SPACES[to].adjacent) { if (has_american_army(from)) { let g = find_american_or_french_general(from) if (g !== NOBODY && !has_general_moved(g)) return true } } return false } function gen_intercept(to) { if (!has_general_moved(ARNOLD)) { if (to === QUEBEC && find_american_or_french_general(FALMOUTH) === ARNOLD && has_american_or_french_cu(FALMOUTH)) gen_action_general(ARNOLD) if (to === FALMOUTH && find_american_or_french_general(QUEBEC) === ARNOLD && has_american_or_french_cu(QUEBEC)) gen_action_general(ARNOLD) } for (let from of SPACES[game.move.to].adjacent) { if (has_american_army(from)) { let g = find_american_or_french_general(from) if (g !== NOBODY && !has_general_moved(g)) gen_action_general(g) } } } function goto_intercept() { clear_undo() game.active = P_AMERICA game.state = "intercept_who" } states.intercept_who = { inactive: "to intercept", prompt() { view.prompt = "Intercept " + general_name(game.move.who) + " at " + space_name(game.move.to) + "?" view.actions.pass = 1 gen_intercept(game.move.to) }, general(g) { push_undo() set_general_moved(g) game.intercept = { who: g, from: location_of_general(g), to: game.move.to, carry_british: 0, carry_american: 0, carry_french: 0, } pickup_max_american_cu(game.intercept) game.state = "intercept_roll" }, pass() { end_intercept() }, } states.intercept_roll = { inactive: "to intercept", prompt() { view.prompt = "Intercept " + general_name(game.move.who) + " at " + space_name(game.move.to) + " with " + general_name(game.intercept.who) + "?" view.selected_general = game.intercept.who view.react = game.intercept view.actions.roll = 1 }, roll() { clear_undo() let who = game.intercept.who let die = roll_d6() let agility = GENERALS[who].agility if (die <= agility) { log(`Intercept with G${who}\nD${die} \xd7 A${agility} from S${game.intercept.from}`) game.move.did_intercept = 1 move_army(game.intercept) if (count_friendly_generals(game.move.to) > 1) goto_remove_general_after_intercept() else end_intercept() } else { log(`Intercept with G${who}\nD${die} \xd7 A${agility} failed`) delete game.intercept if (can_intercept_to(game.move.to)) game.state = "intercept_who" else end_intercept() } }, } function end_intercept() { game.active = P_BRITAIN delete game.intercept disperse_and_overrun_after_move() if (has_enemy_cu(game.move.to)) goto_start_battle() else resume_moving() } /* RETREAT BEFORE BATTLE */ function goto_start_battle() { game.combat = { attacker: game.active, a_bonus: 0, b_bonus: 0, british_losses: 0, } if (game.active === P_BRITAIN && can_retreat_before_battle()) goto_retreat_before_battle() else goto_play_attacker_battle_card() } function can_retreat_before_battle() { if (game.move.did_intercept) return false let here = game.move.to // can't retreat if attempted (successful or not) interception! let g = find_american_or_french_general(here) if (g === NOBODY || has_general_moved(g)) return false if (g === ARNOLD) { if (here === FALMOUTH) if (can_retreat_before_battle_to(g, here, QUEBEC)) return true if (here === QUEBEC) if (can_retreat_before_battle_to(g, here, FALMOUTH)) return true } for (let to of SPACES[here].adjacent) if (can_retreat_before_battle_to(g, here, to)) return true return false } function can_retreat_before_battle_to(g, from, to) { if (to === game.move.from) return false if (has_friendly_pc(to)) return false if (has_friendly_cu(to)) return false return true } function goto_retreat_before_battle() { game.active = P_AMERICA game.state = "retreat_before_battle" } states.retreat_before_battle = { inactive: "to retreat before battle", prompt() { view.prompt = "Attempt retreat before battle?" view.selected_general = find_american_or_french_general(game.move.to) view.actions.pass = 1 gen_defender_retreat() }, space(to) { push_undo() game.retreat = { from: game.move.to, to, who: find_american_or_french_general(game.move.to), carry_american: 0 } pickup_max_american_cu(game.retreat) game.state = "retreat_before_battle_roll" }, pass() { clear_undo() end_retreat_before_battle() }, } states.retreat_before_battle_roll = { inactive: "to retreat before battle", prompt() { view.prompt = "Attempt retreat before battle?" view.selected_general = find_american_or_french_general(game.move.to) view.react = game.retreat view.actions.roll = 1 // TODO: choose which CU to retreat with if mix of french/american }, roll() { let who = game.retreat.who let to = game.retreat.to let agility = GENERALS[who].agility let bonus = GENERALS[who].bonus ? 2 : 0 let roll = roll_d6() if (roll <= agility + bonus) { if (bonus) log(`Retreat with G${who}\nD${roll} \xd7 A${agility}+2R to S${to}`) else log(`Retreat with G${who}\nD${roll} \xd7 A${agility} to S${to}`) move_army(game.retreat) if (has_enemy_general(to)) capture_enemy_general(to) delete game.retreat goto_remove_general_after_retreat_before_battle(to) } else { if (bonus) log(`Retreat with G${who}\nD${roll} \xd7 A${agility}+2R failed`) else log(`Retreat with G${who}\nD${roll} \xd7 A${agility} failed`) delete game.retreat end_retreat_before_battle() } }, } function end_retreat_before_battle() { game.active = P_BRITAIN disperse_and_overrun_after_move() if (has_enemy_cu(game.move.to)) goto_play_attacker_battle_card() else end_battle() } /* BATTLE CARDS */ function is_trigger_remove_benedict_arnold(c) { return ( game.active === P_BRITAIN && CARDS[c].event === "remove_benedict_arnold" && !is_general_at_location(ARNOLD, NOWHERE) ) } function remove_benedict_arnold() { log("Removed Arnold from the game!") set_general_location(ARNOLD, NOWHERE) } states.remove_benedict_arnold_attacker = { inactive: "to remove Arnold", prompt() { view.prompt = "Remove Benedict Arnold from the game!" gen_action_general(ARNOLD) }, general(_) { remove_benedict_arnold() game.state = "play_attacker_battle_card_confirm" } } states.remove_benedict_arnold_defender = { inactive: "to remove Arnold", prompt() { view.prompt = "Remove Benedict Arnold from the game!" gen_action_general(ARNOLD) }, general(_) { remove_benedict_arnold() game.state = "play_defender_battle_card_confirm" } } function goto_play_attacker_battle_card() { log("=! Battle at S" + game.move.to) game.active = game.combat.attacker game.state = "play_attacker_battle_card" } states.play_attacker_battle_card = { inactive: "to play a battle card", prompt() { view.prompt = "Attack: Play or discard event for DRM." view.actions.pass = 1 gen_battle_card() }, card(c) { play_battle_card(this, c) }, card_battle_play(c) { push_undo() log(`${game.active} played C${c} for +2 DRM.`) play_card(c) if (game.active === P_BRITAIN) { game.b_draw += 1 game.combat.b_bonus += 2 } else { game.a_draw += 1 game.combat.a_bonus += 2 } if (is_trigger_remove_benedict_arnold(c)) game.state = "remove_benedict_arnold_attacker" else game.state = "play_attacker_battle_card_confirm" }, card_battle_discard(c) { push_undo() log(`${game.active} discarded C${c} for +1 DRM.`) discard_card(c) if (game.active === P_BRITAIN) { game.combat.b_bonus += 1 } else { game.combat.a_bonus += 1 } game.state = "play_attacker_battle_card_confirm" }, pass() { clear_undo() goto_play_defender_battle_card() }, } states.play_attacker_battle_card_confirm = { inactive: "to play a battle card", prompt() { let drm = (game.active === P_BRITAIN) ? game.combat.b_bonus : game.combat.a_bonus view.prompt = `Attack: +${drm} DRM.` if (drm > 1) view.prompt = "Attack: Played card for +2 DRM." else view.prompt = "Attack: Discarded card for +1 DRM." view.actions.next = 1 }, next() { // Check if Arnold's removal loads to overrun! let to = game.move.to let cu = game.move.carry_british + game.move.carry_american + game.move.carry_french if (cu >= 4 && count_enemy_cu(to) === 1 && !has_enemy_general(to)) { overrun(to) if (game.active === P_BRITAIN && game.congress === to) disperse_continental_congress() delete game.combat resume_moving() return } clear_undo() goto_play_defender_battle_card() }, } function goto_play_defender_battle_card() { game.state = "play_defender_battle_card" game.active = ENEMY[game.combat.attacker] /* // TODO - skip or not (opponent doesn't know he's been attacked) if (active_hand().length === 0) resolve_battle() */ } states.play_defender_battle_card = { inactive: "to play a battle card", prompt() { view.prompt = "Defend: Play or discard event for DRM." view.actions.pass = 1 gen_battle_card() }, card(c) { play_battle_card(this, c) }, card_battle_play(c) { push_undo() log(`${game.active} played C${c} for +2 DRM.`) play_card(c) if (game.active === P_BRITAIN) { game.b_draw += 1 game.combat.b_bonus += 2 } else { game.a_draw += 1 game.combat.a_bonus += 2 } if (is_trigger_remove_benedict_arnold(c)) game.state = "remove_benedict_arnold_defender" else game.state = "play_defender_battle_card_confirm" }, card_battle_discard(c) { push_undo() log(`${game.active} discarded C${c} for +1 DRM.`) discard_card(c) if (game.active === P_BRITAIN) { game.combat.b_bonus += 1 } else { game.combat.a_bonus += 1 } game.state = "play_defender_battle_card_confirm" }, pass() { clear_undo() resolve_battle() }, } states.play_defender_battle_card_confirm = { inactive: "to play a battle card", prompt() { let drm = (game.active === P_BRITAIN) ? game.combat.b_bonus : game.combat.a_bonus if (drm > 1) view.prompt = "Defend: Played card for +2 DRM." else view.prompt = "Defend: Discarded card for +1 DRM." view.actions.next = 1 }, next() { clear_undo() resolve_battle() }, } function play_battle_card(st, c) { let card = CARDS[c] if (game.active === P_BRITAIN) { switch (card.type) { case "british-battle": case "british-event-or-battle": st.card_battle_play(c) break case "british-event": case "american-event": case "american-battle": st.card_battle_discard(c) break } } else { switch (card.type) { case "british-battle": case "british-event-or-battle": case "british-event": case "american-event": st.card_battle_discard(c) break case "american-battle": st.card_battle_play(c) break } } } function gen_battle_card() { for (let c of active_hand()) { let card = CARDS[c] switch (card.type) { case "british-battle": case "british-event-or-battle": case "british-event": case "american-event": case "american-battle": gen_action_card("card", c) break } } } /* RESOLVE BATTLE */ function roll_loser_combat_losses(side, max_cu) { let roll = roll_d6() let lost = 0 switch (roll) { case 1: case 2: case 3: lost = 1 break case 4: case 5: lost = 2 break case 6: lost = 3 break } lost = Math.min(lost, max_cu) log(`${side} LOSSES:\n-${lost} CU ( D${roll} )`) return lost } function roll_winner_combat_losses(side, losing_general) { let agility = losing_general !== NOBODY ? GENERALS[losing_general].agility : 0 let roll = roll_d6() let lost = 0 switch (agility) { case 0: lost = roll === 1 ? 1 : 0 break case 1: lost = roll <= 2 ? 1 : 0 break case 2: lost = roll <= 3 ? 1 : 0 break case 3: lost = roll <= 4 ? 1 : 0 break } if (losing_general === NOBODY) log(`${side} LOSSES:\n-${lost} CU ( D${roll} \xd7 Nobody)`) else log(`${side} LOSSES:\n-${lost} CU ( D${roll} \xd7 A${agility} G${losing_general} )`) return lost } function apply_british_combat_losses(max) { let n = Math.min(count_british_cu(game.move.to), max) remove_british_cu(game.move.to, n) if (game.combat.attacker === P_BRITAIN) game.move.carry_british -= n return n } function apply_american_combat_losses(max) { let n = Math.min(count_american_cu(game.move.to), max) remove_american_cu(game.move.to, n) if (game.combat.attacker === P_AMERICA) game.move.carry_american -= n return n } function apply_french_combat_losses(max) { let n = Math.min(count_french_cu(game.move.to), max) remove_french_cu(game.move.to, n) if (game.combat.attacker === P_AMERICA) game.move.carry_french -= n return n } 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 = [] let r_log = [] game.active = ENEMY[game.active] let b_g = find_british_general(game.move.to) let b_cu = count_british_cu(game.move.to) let a_g = find_american_or_french_general(game.move.to) let a_cu = count_american_and_french_cu(game.move.to) let b_br = 0 let a_br = 0 let b_roll = roll_d6() let a_roll = roll_d6() b_log.push("D" + b_roll + " Roll") a_log.push("D" + a_roll + " Roll") b_log.push("+" + b_cu + " CU") a_log.push("+" + a_cu + " CU") if (b_g !== NOBODY) { 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(`+${b_br} G${b_g} ( D${roll} \xd7 BB${GENERALS[b_g].battle} )`) } if (a_g !== NOBODY) { 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) if (a_g === ROCHAMBEAU) a_log.push(`+${a_br} G${a_g} ( D${roll} \xd7 FB${GENERALS[a_g].battle} )`) else a_log.push(`+${a_br} G${a_g} ( D${roll} \xd7 AB${GENERALS[a_g].battle} )`) } let b_drm = b_cu + b_br + game.combat.b_bonus if (has_flag(F_REGULARS)) { b_log.push("+1 British Regulars' Advantage") b_drm += 1 } if (is_non_blockaded_port(game.move.to)) { if (is_fortified_port(game.move.to)) { if (has_british_pc(game.move.to)) { 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.move.to)) { b_log.push("+1 Militia") b_drm += 1 } if (game.combat.b_bonus === 2) b_log.push("+2 Battle Card") else if (game.combat.b_bonus === 1) b_log.push("+1 Discard of an Event Card") let a_drm = a_cu + a_br + game.combat.a_bonus if (is_american_militia(game.move.to)) { 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.combat.a_bonus === 2) a_log.push("+2 Battle Card") else if (game.combat.a_bonus === 1) a_log.push("+1 Discard of an Event Card") if (game.move.did_intercept) { a_log.push("+1 Interception") a_drm += 1 } b_log.push("Total: " + (b_roll + b_drm)) a_log.push("Total: " + (a_roll + a_drm)) if (game.combat.attacker === P_BRITAIN) { log("BRITISH ATTACK:\n" + b_log.join("\n")) log("AMERICAN DEFENSE:\n" + a_log.join("\n")) } else { log("AMERICAN ATTACK:\n" + a_log.join("\n")) log("BRITISH DEFENSE:\n" + b_log.join("\n")) } let victor if (game.active === P_BRITAIN) victor = b_roll + b_drm >= a_roll + a_drm ? P_BRITAIN : P_AMERICA else victor = a_roll + a_drm >= b_roll + b_drm ? P_AMERICA : P_BRITAIN let a_lost_cu, b_lost_cu if (victor === P_BRITAIN) { log("RESULT:\nBritish victory!") b_lost_cu = roll_winner_combat_losses("BRITISH", a_g) a_lost_cu = roll_loser_combat_losses("AMERICAN", a_cu) } else { log("RESULT:\nAmerican victory!") a_lost_cu = roll_winner_combat_losses("AMERICAN", b_g) b_lost_cu = roll_loser_combat_losses("BRITISH", b_cu) } game.combat.british_losses = apply_british_combat_losses(b_lost_cu) apply_american_and_french_combat_losses(a_lost_cu) // Special case: winning general with no CU on enemy PC is captured if (victor === P_BRITAIN) { if (b_g && count_british_cu(game.move.to) === 0 && has_american_pc(game.move.to)) capture_british_general(game.move.to) } else { if (a_g && count_american_and_french_cu(game.move.to) === 0 && has_british_pc(game.move.to)) capture_american_or_french_general(game.move.to) } log(r_log.join("\n")) if (victor === P_AMERICA) advance_french_alliance(1) goto_retreat_after_battle(victor) } /* RETREAT AFTER BATTLE */ function can_defender_retreat(g, from, to, is_lone) { if (to === game.move.from) return false if (has_enemy_pc(to)) return false if (has_enemy_cu(to)) return false if (is_lone && has_enemy_general(to)) return false return true } function can_attacker_retreat() { let g = game.move.who let from = game.move.to let to = game.move.from if (from === FALMOUTH && to == QUEBEC && g !== ARNOLD) return false if (from === QUEBEC && to == FALMOUTH && g !== ARNOLD) return false if (has_enemy_pc(to)) return false if (has_enemy_cu(to)) return false return true } function is_general_without_cu(s) { if (game.active === P_AMERICA) return has_american_or_french_general(s) && !has_american_or_french_cu(s) else return has_british_general(s) && !has_british_cu(s) } function is_not_fortified_port_without_british_pc(s) { if (is_fortified_port(s)) return has_british_pc(s) return true } function gen_defender_retreat() { let here = game.move.to let is_lone = is_general_without_cu(here) let can_retreat = false let g if (game.active === P_BRITAIN) g = find_british_general(here) else g = find_american_or_french_general(here) view.selected_general = g if (g === ARNOLD) { if (here === FALMOUTH) { if (can_defender_retreat(g, here, QUEBEC, is_lone)) { gen_action_space(QUEBEC) can_retreat = true } } if (here === QUEBEC) { if (can_defender_retreat(g, here, FALMOUTH, is_lone)) { gen_action_space(FALMOUTH) can_retreat = true } } } for (let to of SPACES[here].adjacent) { if (can_defender_retreat(g, here, to, is_lone)) { gen_action_space(to) can_retreat = true } } if (game.active === P_BRITAIN) { if (is_non_blockaded_port(here) && is_not_fortified_port_without_british_pc(here)) { for (let to of all_port_spaces) { if (to !== game.move.from && is_non_blockaded_port(to)) { if (!has_american_pc(to) && !has_american_or_french_cu(to)) { gen_action_space(to) can_retreat = true } } } } } if (!can_retreat) view.actions.surrender = 1 } function gen_attacker_retreat() { view.selected_general = game.move.who if (can_attacker_retreat()) gen_action_space(game.move.from) else view.actions.surrender = 1 } function goto_retreat_after_battle(victor) { let from = game.move.to if (victor === P_BRITAIN) { let who = find_american_or_french_general(from) if (who === NOBODY && count_american_and_french_cu(from) === 0) return end_battle() } else { let who = find_british_general(from) if (who === NOBODY && count_british_cu(from) === 0) return end_battle() } game.active = ENEMY[victor] game.state = "retreat_after_battle" } states.retreat_after_battle = { inactive: "to retreat after battle", prompt() { view.prompt = "Retreat after battle." if (game.active === game.combat.attacker) gen_attacker_retreat() else gen_defender_retreat() }, space(to) { push_undo() if (game.active === P_BRITAIN) retreat_british_army(game.move.to, to) else retreat_american_army(game.move.to, to) if (game.active === P_BRITAIN && game.congress === to) disperse_continental_congress() if (has_enemy_general(to)) capture_enemy_general(to) if (game.active === game.combat.attacker) game.move.to = to if (count_friendly_generals(to) > 1) goto_remove_general_after_retreat(to) else game.state = "retreat_after_battle_confirm" }, surrender() { log("Surrendered.") if (game.active === P_BRITAIN) surrender_british_army(game.move.to) else surrender_american_army(game.move.to) end_battle() }, } states.retreat_after_battle_confirm = { inactive: "to retreat after battle", prompt() { view.prompt = "Retreat after battle: Done." view.actions.next = 1 }, next() { clear_undo() end_battle() }, } /* END BATTLE */ function end_battle() { game.active = game.combat.attacker if (game.active === P_BRITAIN && game.congress === game.move.to && count_british_cu(game.move.to) > 0) disperse_continental_congress() if (game.combat.british_losses >= 3) lose_regular_advantage() delete game.combat end_move(true) } /* EVENTS */ events.campaign = function (card) { game.state = "campaign" game.campaign = card.count if (game.active === P_BRITAIN) set_flag(F_LANDING_PARTY) else clear_flag(F_LANDING_PARTY) game.count = 3 // can activate any general! game.state = "ops_general_who" } events.the_war_ends = function (card) { game.war_ends = CARDS.indexOf(card) end_strategy_card() } events.remove_random_british_card = function () { remove_random_card(game.b_hand) } events.remove_random_american_card = function () { remove_random_card(game.a_hand) } function remove_random_card(hand) { clear_undo() if (hand.length > 0) { let i = random(hand.length) let c = hand[i] log(ENEMY[game.active] + " discarded C" + c + ".") 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 " + game.french_alliance + ".") } } function lose_regular_advantage() { if (has_flag(F_REGULARS)) { log("British Regulars Advantage lost!") clear_flag(F_REGULARS) advance_french_alliance(2) } } events.baron_von_steuben_trains_the_continental_army = function () { if (is_general_on_map(WASHINGTON)) { game.state = "baron_von_steuben_trains_the_continental_army" } else { lose_regular_advantage() end_strategy_card() } } states.baron_von_steuben_trains_the_continental_army = { inactive: "to execute event", prompt() { view.prompt = "Baron von Steuben: Place 2 CU with Washington." gen_action_general(WASHINGTON) }, general(_) { let where = location_of_general(WASHINGTON) log("Placed 2 CU and Washington at S" + where + ".") place_american_cu(where, 2) lose_regular_advantage() end_strategy_card() }, } events.advance_french_alliance = function (card) { // game.state = "advance_french_alliance" advance_french_alliance(card.count) end_strategy_card() } events.remove_french_navy = function () { // NOTE: Technically it should be game.year but this way we avoid stacking with the turn marker. game.french_navy = game.year + 1 end_strategy_card() } events.remove_british_pc_from = function (card) { game.count = card.count game.where = card.where game.state = "remove_british_pc_from" } states.remove_british_pc_from = { inactive: "to execute event", prompt() { view.prompt = "Remove British PC markers from " + game.where.map(colony_name).join(", ") + ". " + game.count + " left." for (let colony of game.where) for (let space of COLONIES[colony]) if (has_british_pc(space) && has_no_british_cu(space)) gen_action_space(space) gen_event_pass() }, space(where) { push_undo() remove_pc(where) if (--game.count === 0) { delete game.where end_strategy_card() } }, pass() { delete game.where end_strategy_card_now() }, } events.remove_american_pc = function (card) { game.count = card.count game.state = "remove_american_pc" } states.remove_american_pc = { inactive: "to execute event", prompt() { view.prompt = "Remove American PC markers. " + game.count + " left." for (let space of all_spaces) if (has_american_pc(space) && has_no_american_unit(space)) gen_action_space(space) gen_event_pass() }, space(where) { push_undo() remove_pc(where) if (--game.count === 0) end_strategy_card() }, pass() { end_strategy_card_now() }, } events.remove_american_pc_from = function (card) { game.count = card.count game.where = card.where game.state = "remove_american_pc_from" } states.remove_american_pc_from = { inactive: "to execute event", prompt() { view.prompt = "Remove American PC markers from " + game.where.map(colony_name).join(", ") + ". " + game.count + " left." for (let colony of game.where) for (let space of COLONIES[colony]) if (has_american_pc(space) && has_no_american_unit(space)) gen_action_space(space) gen_event_pass() }, space(where) { push_undo() remove_pc(where) if (--game.count === 0) { delete game.where end_strategy_card() } }, pass() { delete game.where end_strategy_card_now() }, } events.remove_american_pc_from_non_port = function (card) { game.count = card.count game.where = card.where game.state = "remove_american_pc_from_non_port" } states.remove_american_pc_from_non_port = { inactive: "to execute event", prompt() { view.prompt = "Remove American PC markers from non-Port space in " + game.where.map(colony_name).join(", ") + ". " + game.count + " left." for (let colony of game.where) { for (let space of COLONIES[colony]) { if (!is_port(space)) { if (has_american_pc(space) && has_no_american_unit(space)) { gen_action_space(space) } } } } gen_event_pass() }, space(where) { push_undo() remove_pc(where) if (--game.count === 0) { delete game.where end_strategy_card() } }, pass() { delete game.where end_strategy_card_now() }, } events.remove_american_pc_within_two_spaces_of_a_british_general = function (card) { game.count = card.count game.state = "remove_american_pc_within_two_spaces_of_a_british_general_1" } function can_remove_american_pc_within_two_spaces_of_a_british_general(g) { let a = location_of_general(g) if (!is_map_space(a)) return false if (has_american_pc(a) && has_no_american_unit(a)) return true for (let b of SPACES[a].adjacent) { if (has_american_pc(b) && has_no_american_unit(b)) return true for (let c of SPACES[b].adjacent) if (has_american_pc(c) && has_no_american_unit(c)) return true } return false } function gen_remove_american_pc_within_two_spaces_of_a_british_general(g) { let a = location_of_general(g) let candidates = [ a ] for (let b of SPACES[a].adjacent) { set_add(candidates, b) for (let c of SPACES[b].adjacent) set_add(candidates, c) } for (let space of candidates) if (has_american_pc(space) && has_no_american_unit(space)) gen_action_space(space) } states.remove_american_pc_within_two_spaces_of_a_british_general_1 = { inactive: "to execute event", prompt() { view.prompt = "Remove American PC markers within two spaces of a British general." for (let g of BRITISH_GENERALS) if (can_remove_american_pc_within_two_spaces_of_a_british_general(g)) gen_action_general(g) gen_event_pass() }, general(g) { push_undo() game.who = g game.state = "remove_american_pc_within_two_spaces_of_a_british_general_2" }, } states.remove_american_pc_within_two_spaces_of_a_british_general_2 = { inactive: "to execute event", prompt() { view.prompt = "Remove American PC markers within two spaces of " + general_name(game.who) + ". " + game.count + " left." gen_remove_american_pc_within_two_spaces_of_a_british_general(game.who) gen_event_pass() }, space(where) { push_undo() remove_pc(where) if (--game.count === 0) { delete game.who end_strategy_card() } }, pass() { delete game.who end_strategy_card_now() }, } events.place_american_pc = function (card) { game.count = card.count game.state = "place_american_pc" } states.place_american_pc = { inactive: "to execute event", prompt() { view.prompt = "Place American PC markers. " + game.count + " left." for (let space of all_spaces) if (has_no_pc(space) && has_no_british_playing_piece(space)) gen_action_space(space) gen_event_pass() }, space(where) { push_undo() place_american_pc(where) if (--game.count === 0) end_strategy_card() }, pass() { end_strategy_card_now() }, } events.place_american_pc_in = function (card) { game.where = card.where game.count = card.count game.state = "place_american_pc_in" } states.place_american_pc_in = { inactive: "to execute event", prompt() { view.prompt = "Place American PC markers in " + game.where.map(colony_name).join(", ") + ". " + game.count + " left." gen_place_american_pc_in_colony(game.where) gen_event_pass() }, space(where) { place_american_pc(where) if (--game.count === 0) { delete game.where end_strategy_card() } }, pass() { delete game.where end_strategy_card_now() }, } events.lord_sandwich_coastal_raids = function () { game.state = "lord_sandwich_coastal_raids" game.count = 2 delete game.where } states.lord_sandwich_coastal_raids = { inactive: "to execute event", prompt() { view.prompt = "Remove two or flip one American PC in a port space." gen_lord_sandwich_coastal_raids(game.where) gen_event_pass() }, space(s) { if (has_american_pc(s)) { game.where = s remove_pc(s) if (--game.count === 0) end_strategy_card() } else { place_british_pc(s) end_strategy_card() } }, pass() { end_strategy_card_now() }, } function gen_lord_sandwich_coastal_raids(first_removed) { for (let space of all_spaces) { if (is_port(space)) if (has_american_pc(space) && has_no_american_unit(space)) gen_action_space(space) if (space === first_removed) gen_action_space(space) } } events.remove_american_cu = function () { game.state = "remove_american_cu" } states.remove_american_cu = { inactive: "to execute event", prompt() { view.prompt = "Remove one American CU from any space." for (let space of all_spaces) if (has_american_or_french_cu(space)) gen_action_space(space) gen_event_pass() }, space(where) { if (count_american_cu(where) > 0) remove_american_cu(where, 1) else remove_french_cu(where, 1) end_strategy_card() }, pass() { end_strategy_card_now() }, } events.remove_british_cu = function (card) { game.state = "remove_british_cu" game.count = card.count } states.remove_british_cu = { inactive: "to execute event", prompt() { view.prompt = "Remove " + game.count + " British CU from any space." for (let space of all_spaces) if (has_british_cu(space)) gen_action_space(space) gen_event_pass() }, space(where) { push_undo() log("Removed CU from S" + where + ".") remove_british_cu(where, 1) if (--game.count === 0) end_strategy_card() }, pass() { end_strategy_card_now() }, } events.pennsylvania_and_new_jersey_line_mutinies = function () { set_flag(F_MUTINIES) game.state = "pennsylvania_and_new_jersey_line_mutinies" game.count = 2 delete game.where } states.pennsylvania_and_new_jersey_line_mutinies = { inactive: "to execute event", prompt() { view.prompt = "Remove two American CUs from the map, one each from two different spaces." gen_pennsylvania_and_new_jersey_line_mutinies(game.where) gen_event_pass() }, space(where) { log("Removed CU from S" + where + ".") if (count_american_cu(where) > 0) remove_american_cu(where, 1) else remove_french_cu(where, 1) game.where = where if (--game.count === 0) end_strategy_card() }, pass() { end_strategy_card_now() }, } function gen_pennsylvania_and_new_jersey_line_mutinies(first_removed) { for (let space of all_spaces) { if (has_american_or_french_cu(space)) if (space !== first_removed) gen_action_space(space) } } events.john_glovers_marblehead_regiment = function () { game.state = "john_glovers_marblehead_regiment_who" game.count = 3 // strategy rating for gen_activate_general } states.john_glovers_marblehead_regiment_who = { inactive: "to execute event", prompt() { view.prompt = "Activate an American general." gen_activate_general() }, general(g) { goto_ops_general_move(g, true) }, } events.declaration_of_independence = function () { game.save = game.active game.active = P_AMERICA game.state = "declaration_of_independence" game.colonies = filter_place_american_pc_in_colony(THE_13_COLONIES) } states.declaration_of_independence = { inactive: "to place PC markers", prompt() { if (game.colonies.length > 0) { view.prompt = "Declaration of Independence: Place a PC marker in " if (game.colonies.length <= 3) view.prompt += game.colonies.map(colony_name).join(" and ") + "." else view.prompt += "each of the 13 colonies." } else { view.prompt = "Declaration of Independence: Done." view.actions.next = 1 } gen_place_american_pc_in_colony(game.colonies) }, space(space) { push_undo() let colony = SPACES[space].colony set_delete(game.colonies, colony) place_american_pc(space) }, next() { clear_undo() end_declaration_of_independence() } } function end_declaration_of_independence() { game.active = game.save delete game.save delete game.colonies end_strategy_card_now() } function do_event(c) { let card = CARDS[c] log("Played C" + c + ".") play_card(c) if (card.event in events) events[card.event](card) else throw new Error("Event not implemented yet: " + card.event) } /* GEORGE WASHINGTON CAPTURED */ /* - winning general with no CU on enemy PC (no undo anyway) at end of activation - retreat after battle (no undo anyway) at end of activation - place reinforcements (no player shift) at end of activation - move (no active player change) immediately */ function init_george_washington_captured(next_state, done_state) { game.active = P_BRITAIN game.state = next_state game.colonies = filter_remove_american_pc_in_colony(ALL_COLONIES) game.count = Math.min(5, game.colonies.length) delete game.washington_captured if (game.count === 0) game.state = done_state } function goto_george_washington_captured() { game.save = game.active init_george_washington_captured("george_washington_captured", "george_washington_captured_done") } function goto_george_washington_captured_during_move() { init_george_washington_captured("george_washington_captured_during_move", "george_washington_captured_during_move_done") } function action_george_washington_captured_space(s) { push_undo() remove_pc(s) set_delete(game.colonies, SPACES[s].colony) if (--game.count === 0) { delete game.colonies return true } return false } states.george_washington_captured = { inactive: "to remove PC markers", prompt() { view.prompt = "George Washington captured! Remove American PC markers. " + game.count + " left." gen_remove_american_pc_in_colony(game.colonies) }, space(s) { if (action_george_washington_captured_space(s)) game.state = "george_washington_captured_done" }, } states.george_washington_captured_during_move = { inactive: "to remove PC markers", prompt() { view.prompt = "George Washington captured! Remove American PC markers. " + game.count + " left." gen_remove_american_pc_in_colony(game.colonies) }, space(s) { if (action_george_washington_captured_space(s)) game.state = "george_washington_captured_during_move_done" }, } states.george_washington_captured_done = { inactive: "to remove PC markers", prompt() { view.prompt = "George Washington captured! Remove American PC markers. Done." view.actions.next = 1 }, next() { clear_undo() game.active = game.save delete game.save if (game.active === P_AMERICA) { // skip end turn pause after britain removed washington at end of strategy card end_strategy_card_now() } else { end_strategy_card() } }, } states.george_washington_captured_during_move_done = { inactive: "to remove PC markers", prompt() { view.prompt = "George Washington captured! Remove American PC markers. Done." view.actions.next = 1 }, next() { push_undo() game.state = "ops_general_move" }, } /* WINTER ATTRITION PHASE */ function apply_single_winter_attrition(remove_cu, space) { let die = roll_d6() if (die <= 3) { log(`-1 CU at S${space} ( D${die} )`) remove_cu(space, 1) } else { log(`-0 CU at S${space} ( D${die} )`) } } function apply_winter_attrition(remove_cu, space, n) { let half = Math.floor(n / 2) log(`-${half} CU at S${space}.`) remove_cu(space, half) } function goto_winter_attrition_phase() { goto_american_winter_attrition() } function has_american_winter_attrition(s) { let n_american = count_american_cu(s) let n_french = count_french_cu(s) // EXCEPT: Single American CU in a space with American or French General. if (n_american === 1 && n_french === 0 && has_american_or_french_general(s)) return false // EXCEPT: Single French CU in a space with American or French General not at WQ. if (n_american === 0 && n_french === 1 && has_american_or_french_general(s)) return false // EXCEPT: Stack with up to 5 CU with Washington at WQ. if (n_american + n_french <= 5 && is_general_at_location(WASHINGTON, s) && is_winter_quarter_space(s)) return false // American (or mixed with French) suffer attrition regardless of location. if (n_american > 0) return true // French suffer attrition only outside WQ if (n_french > 0 && !is_winter_quarter_space(s)) return true return false } function has_british_winter_attrition(s) { let n_british = count_british_cu(s) // EXCEPT: Single CU in a space with General. if (n_british === 1 && has_british_general(s)) return false // British suffer attrition only outside WQ if (n_british > 0 && !is_winter_quarter_space(s)) return true return false } function goto_american_winter_attrition() { game.active = P_AMERICA game.attrition = [] for (let s of all_spaces) if (has_american_winter_attrition(s)) game.attrition.push(s) if (game.attrition.length > 0) { log("=a Winter Attrition") game.state = "american_winter_attrition" } else { end_american_winter_attrition() } } function end_american_winter_attrition() { delete game.attrition goto_british_winter_attrition() } function goto_british_winter_attrition() { game.active = P_BRITAIN game.attrition = [] for (let s of all_spaces) if (has_british_winter_attrition(s)) game.attrition.push(s) if (game.attrition.length > 0) { log("=b Winter Attrition") game.state = "british_winter_attrition" } else { end_british_winter_attrition() } } function end_british_winter_attrition() { delete game.attrition if (automatic_victory()) return goto_french_naval_phase() } states.american_winter_attrition = { inactive: "to take winter attrition", prompt() { if (game.attrition.length > 0) { view.prompt = "Winter Attrition." for (let s of game.attrition) gen_action_space(s) } else { view.prompt = "Winter Attrition: Done." view.actions.next = 1 } }, space(s) { let wq = is_winter_quarter_space(s) let n_american = count_american_cu(s) let n_french = count_french_cu(s) let has_washington = is_general_at_location(WASHINGTON, s) // single american if (n_american === 1 && n_french === 0) apply_single_winter_attrition(remove_american_cu, s) // single french else if (n_american === 0 && n_french === 1) apply_single_winter_attrition(remove_french_cu, s) // french stack else if (n_american === 0 && n_french > 1) apply_winter_attrition(remove_french_cu, s, n_french) // american stack else if (n_american > 0 && n_french === 0) { if (wq && has_washington) { if (n_american === 6) apply_single_winter_attrition(remove_american_cu, s) else apply_winter_attrition(remove_american_cu, s, n_american - 5) } else { apply_winter_attrition(remove_american_cu, s, n_american) } } // mixed stack else if (n_american > 0 && n_french > 0) { let n_total = n_american + n_french if (wq && has_washington) n_total = Math.max(0, n_total - 5) // TODO: let player choose (but why would they ever choose French?) if (n_total === 1) { apply_single_winter_attrition(remove_american_cu, s, n_american) } else { let half = Math.floor(n_total / 2) let lose_american = Math.min(half, n_american) log(lose_american + " American CU at " + s) remove_american_cu(s, lose_american) half -= lose_american if (half > 0) { log(half + " French CU at " + s) remove_french_cu(s, half) } } } set_delete(game.attrition, s) }, next() { end_american_winter_attrition() }, } states.british_winter_attrition = { inactive: "to take winter attrition", prompt() { if (game.attrition.length > 0) { view.prompt = "Winter Attrition." for (let s of game.attrition) gen_action_space(s) } else { view.prompt = "Winter Attrition: Done." view.actions.next = 1 } }, space(s) { let n_british = count_british_cu(s) if (n_british === 1) apply_single_winter_attrition(remove_british_cu, s) else if (n_british > 1) apply_winter_attrition(remove_british_cu, s, n_british) set_delete(game.attrition, s) }, next() { end_british_winter_attrition() } } /* FRENCH NAVAL PHASE */ function goto_french_naval_phase() { if (game.french_navy !== -1) { log("=a French Naval Phase") game.active = P_AMERICA game.state = "place_french_navy" } else { goto_political_control_phase() } } function gen_place_french_navy() { view.actions.sea = [ 0, 1, 2, 3, 4, 5, 6 ] } const ZONE_NAME = [ "Canada", "MA/RI", "CT/NY", "PA/DE", "MD/VA", "NC/SC", "GA", ] states.place_french_navy_trigger = { inactive: "to place French navy", prompt() { view.prompt = "Place the French Navy in a blockade zone." gen_place_french_navy() }, sea(zone) { push_undo() log("French Navy at " + ZONE_NAME[zone] + ".") game.french_navy = zone goto_place_rochambeau() }, } function can_place_rochambeau() { for (let space of all_port_spaces) if (!has_british_cu(space) && !has_british_pc(space)) return true return false } function goto_place_rochambeau() { if (can_place_rochambeau()) { game.state = "place_rochambeau" } else { end_place_rochambeau() } } states.place_rochambeau = { inactive: "to place Rochambeau", prompt() { view.prompt = "Place Rochambeau in a port." view.move = { who: ROCHAMBEAU, from: FRENCH_REINFORCEMENTS, to: FRENCH_REINFORCEMENTS, carry_american: 0, carry_french: 5 } for (let space of all_port_spaces) { if (!has_british_cu(space) && !has_british_pc(space)) { gen_action_space(space) } } }, space(to) { push_undo() log("Placed 5 CU and Rochambeau at S" + to + ".") move_general(ROCHAMBEAU, to) move_french_cu(FRENCH_REINFORCEMENTS, to, 5) if (has_enemy_general(to)) capture_enemy_general(to) if (count_friendly_generals(to) > 1) game.state = "remove_general_rochambeau" else game.state = "end_place_rochambeau" }, } states.remove_general_rochambeau = { inactive: "to remove a general", prompt() { let where = location_of_general(ROCHAMBEAU) prompt_remove_general(where) view.move = { who: ROCHAMBEAU, from: where, to: where, carry_american: 0, carry_french: 5 } }, general(g) { push_undo() action_remove_general(g) game.state = "end_place_rochambeau" }, } states.end_place_rochambeau = { inactive: "to place Rochambeau", prompt() { let where = location_of_general(ROCHAMBEAU) view.move = { who: ROCHAMBEAU, from: where, to: where, carry_american: 0, carry_french: 5 } view.prompt = "Done." view.actions.next = 1 }, next() { end_place_rochambeau() }, } function end_place_rochambeau() { game.active = game.save delete game.save next_strategy_card() } states.place_french_navy = { inactive: "to place French navy", prompt() { view.prompt = "Place the French Navy in a blockade zone." gen_place_french_navy() }, sea(zone) { push_undo() log("French Navy at " + ZONE_NAME[zone] + ".") game.french_navy = zone game.state = "place_french_navy_confirm" }, } states.place_french_navy_confirm = { inactive: "to place French navy", prompt() { view.prompt = "Place the French Navy: Done." view.actions.next = 1 }, next() { clear_undo() goto_political_control_phase() }, } /* POLITICAL CONTROL PHASE: RETURN CONGRESS */ function goto_political_control_phase() { game.active = P_AMERICA if (game.congress === CONTINENTAL_CONGRESS_DISPERSED) { log("=a Continental Congress") game.state = "return_continental_congress" } else { goto_place_pc_markers_segment() } } function gen_place_continental_congress() { let n = 0 for (let space of all_spaces) { if (SPACES[space].colony !== Canada) { if (has_american_pc(space) && has_no_british_playing_piece(space)) { gen_action_space(space) ++n } } } return n } states.return_continental_congress = { inactive: "to return the continental congress", prompt() { view.prompt = "Return Continental Congress to a space in the 13 colonies." if (gen_place_continental_congress() === 0) view.actions.pass = 1 }, space(where) { push_undo() log("Returned Continental Congress to S" + where + ".") game.congress = where game.state = "return_continental_congress_confirm" }, pass() { clear_undo() goto_place_pc_markers_segment(false) }, } states.return_continental_congress_confirm = { inactive: "to return the continental congress", prompt() { view.prompt = "Return Continental Congress: Done." view.actions.next = 1 }, next() { clear_undo() goto_place_pc_markers_segment(false) }, } /* POLITICAL CONTROL PHASE: PLACE PC MARKERS */ function has_american_place_pc_markers_segment() { for (let space of all_spaces) if (has_american_army(space)) if (has_no_pc(space) || has_british_pc(space)) return true return false } function has_british_place_pc_markers_segment() { for (let space of all_spaces) if (has_british_army(space)) if (has_no_pc(space) || has_american_pc(space)) return true return false } function goto_place_pc_markers_segment() { if (has_american_place_pc_markers_segment()) { log("=a Place PC Markers") game.active = P_AMERICA game.state = "place_american_pc_markers_segment" } else if (has_british_place_pc_markers_segment()) { log("=b Place PC Markers") game.active = P_BRITAIN game.state = "place_british_pc_markers_segment" } else { goto_remove_isolated_pc_marker_segment() } } function resume_place_pc_markers_segment() { if (has_american_place_pc_markers_segment()) { game.active = P_AMERICA game.state = "place_american_pc_markers_segment" } else if (has_british_place_pc_markers_segment()) { log("=b Place PC Markers") game.active = P_BRITAIN game.state = "place_british_pc_markers_segment" } else { goto_remove_isolated_pc_marker_segment() } } states.place_american_pc_markers_segment = { inactive: "to place PC markers", prompt() { let done = true for (let space of all_spaces) { if (has_american_army(space)) { if (has_no_pc(space) || has_british_pc(space)) { done = false gen_action_space(space) } } } if (done) { view.prompt = "Place American PC markers: Done." view.actions.next = 1 } else { view.prompt = "Place American PC markers." view.actions.next = 0 } }, space(s) { if (has_no_pc(s)) place_american_pc(s) else if (has_british_pc(s)) flip_pc(s) }, next() { resume_place_pc_markers_segment() }, } states.place_british_pc_markers_segment = { inactive: "to place PC markers", prompt() { let done = true for (let space of all_spaces) { if (has_british_army(space)) { if (has_no_pc(space) || has_american_pc(space)) { done = false gen_action_space(space) } } } if (done) { view.prompt = "Place British PC markers: Done." view.actions.next = 1 } else { view.prompt = "Place British PC markers." view.actions.next = 0 } }, space(s) { if (has_no_pc(s)) place_british_pc(s) else if (has_american_pc(s)) flip_pc(s) }, next() { resume_place_pc_markers_segment() }, } /* POLITICAL CONTROL PHASE: REMOVE ISOLATED PC MARKERS */ function goto_remove_isolated_pc_marker_segment() { goto_remove_isolated_american_pc_segment() } 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) { // TODO: BFS for (let to of SPACES[from].adjacent) { if (set_has(seen, to)) continue if (is_american_pc_path(to)) { set_add(seen, to) spread_american_path(seen, to) } } } function spread_british_path(seen, from) { // TODO: BFS for (let to of SPACES[from].adjacent) { if (set_has(seen, to)) continue if (is_british_pc_path(to)) { set_add(seen, to) spread_british_path(seen, to) } } } function goto_remove_isolated_american_pc_segment() { game.active = P_AMERICA let seen = [] for (let space of all_spaces) { if (is_american_pc_root(space)) { set_add(seen, space) spread_american_path(seen, space) } } game.isolated = [] for (let space of all_spaces) if (has_american_pc(space) && !set_has(seen, space)) set_add(game.isolated, space) if (game.isolated.length > 0) { log("=a Remove Isolated PC Markers") game.active = P_AMERICA game.state = "remove_isolated_pc_segment" } else { end_remove_isolated_american_pc_segment() } } function goto_remove_isolated_british_pc_segment() { let seen = [] for (let space of all_spaces) { if (is_british_pc_root(space)) { set_add(seen, space) spread_british_path(seen, space) } } game.isolated = [] for (let space of all_spaces) if (has_british_pc(space) && !set_has(seen, space)) set_add(game.isolated, space) if (game.isolated.length > 0) { log("=b Remove Isolated PC Markers") game.active = P_BRITAIN game.state = "remove_isolated_pc_segment" } else { end_remove_isolated_british_pc_segment() } } states.remove_isolated_pc_segment = { inactive: "to remove isolated PC markers", prompt() { if (game.isolated.length > 0) { view.prompt = "Remove isolated PC markers. " + game.isolated.length + " left." for (let s of game.isolated) gen_action_space(s) } else { view.prompt = "Remove isolated PC markers. Done." view.actions.next = 1 } }, space(s) { set_delete(game.isolated, s) remove_pc(s) }, next() { if (game.active === P_AMERICA) end_remove_isolated_american_pc_segment() else end_remove_isolated_british_pc_segment() }, } function end_remove_isolated_american_pc_segment() { delete game.isolated goto_remove_isolated_british_pc_segment() } function end_remove_isolated_british_pc_segment() { delete game.isolated goto_end_phase() } /* END PHASE */ function goto_end_phase() { if (has_flag(F_FRENCH_ALLIANCE_TRIGGERED) && !has_flag(F_EUROPEAN_WAR)) { log("=b European War") set_flag(F_EUROPEAN_WAR) game.count = 2 game.active = P_BRITAIN game.state = "european_war" set_flag(F_RESHUFFLE) return } if ((game.war_ends && game.year >= CARDS[game.war_ends].year) || game.year === 1783) { norths_government_falls() return } clear_flag(F_MUTINIES) game.a_queue = game.b_queue = 0 game.year += 1 goto_start_year() } /* END PHASE: EUROPEAN WAR */ states.european_war = { inactive: "to remove 2 British CUs", prompt() { view.prompt = "European War: Remove 2 British CU from any spaces. " + game.count + " left." if (game.count > 0) for (let space of all_spaces) if (has_british_cu(space)) gen_action_space(space) gen_event_pass() }, space(where) { push_undo() log("Removed CU from S" + where + ".") remove_british_cu(where, 1) if (--game.count === 0) game.state = "european_war_confirm" }, pass() { clear_undo() goto_end_phase() }, } states.european_war_confirm = { inactive: "to remove 2 British CUs", prompt() { view.prompt = "European War: Done." view.actions.next = 1 }, next() { clear_undo() goto_end_phase() }, } /* VICTORY */ function automatic_victory() { let n_american = 0 let n_british = 0 for (let space of all_spaces) { n_american += count_french_cu(space) + count_american_cu(space) if (SPACES[space].colony !== Canada) n_british += count_british_cu(space) } if (n_american === 0) { game.victory = "British Automatic Victory!" game.active = "None" game.result = P_BRITAIN game.state = "game_over" log("=! Game Over") log(game.victory) return true } if (n_british === 0) { game.victory = "American Automatic Victory!" game.active = "None" game.result = P_AMERICA game.state = "game_over" log("=! Game Over") log(game.victory) return true } return false } function norths_government_falls() { let n_american = 0 let n_british = 0 for (let c = 0; c <= 13; ++c) { if (get_colony_control(c) === PC_AMERICAN) ++n_american if (get_colony_control(c) === PC_BRITISH) ++n_british } if (n_british >= 6) { game.result = P_BRITAIN } else { if (n_american >= 7) game.result = P_AMERICA else game.result = P_BRITAIN } log("=! Game Over") if (game.result === P_AMERICA) game.victory = "North's Government Falls:\nAmerican victory!" else game.victory = "North's Government Falls:\nBritish victory!" game.active = "None" game.state = "game_over" log(game.victory) } states.game_over = { prompt() { view.prompt = game.victory }, } /* CLIENT/SERVER COMMS */ function gen_action(action, argument) { if (!(action in view.actions)) view.actions[action] = [] set_add(view.actions[action], argument) } function gen_action_space(s) { gen_action("space", s) } function gen_action_general(g) { gen_action("general", g) } function gen_action_card(action, c) { gen_action(action, c) } function gen_event_pass() { if (!view.actions.space && !view.actions.general) view.actions.pass = 1 // TODO: else confirm_pass (if enacting events are optional) } exports.scenarios = [ "Standard" ] exports.roles = [ P_BRITAIN, P_AMERICA ] exports.setup = function (seed, _scenario, _options) { setup_game(seed) return game } exports.action = function (state, player, action, arg) { game = state if (player === game.active) { let S = states[game.state] if (action === "confirm_pass") action = "pass" 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 } exports.view = function (state, player) { game = state view = { prompt: null, log: game.log, year: game.year, flags: game.flags, war_ends: game.war_ends, reinforcements: game.reinforcements, french_alliance: game.french_alliance, congress: game.congress, french_navy: game.french_navy, loca: game.loca, cupc: game.cupc, a_cards: game.a_hand.length, b_cards: game.b_hand.length, a_queue: game.a_queue, b_queue: game.b_queue, last_played: game.card ? game.card : game.did_discard_event, move: game.move, } if (player === P_AMERICA) view.hand = game.a_hand else if (player === P_BRITAIN) view.hand = game.b_hand else view.hand = [] if (game.active === player) { view.actions = {} states[game.state].prompt() if (game.undo && game.undo.length > 0) view.actions.undo = 1 else view.actions.undo = 0 } else if (game.active === "None") { view.prompt = game.victory } else { let inactive = states[game.state].inactive if (typeof inactive !== "string") inactive = game.state view.prompt = "Waiting for " + game.active + " " + inactive + "." } return view } /* COMMON LIBRARY */ function log(s) { game.log.push(s) } function log_br() { if (game.log.length > 0 && game.log[game.log.length - 1] !== "") game.log.push("") } function clear_undo() { if (game.undo) { game.undo.length = 0 } } function push_undo() { if (game.undo) { let copy = {} for (let k in game) { let v = game[k] if (k === "undo") continue else if (k === "log") v = v.length else if (typeof v === "object" && v !== null) v = object_copy(v) copy[k] = v } game.undo.push(copy) } } function pop_undo() { if (game.undo) { let save_log = game.log let save_undo = game.undo game = save_undo.pop() save_log.length = game.log game.log = save_log game.undo = save_undo } } function random(range) { // An MLCG using integer arithmetic with doubles. // https://www.ams.org/journals/mcom/1999-68-225/S0025-5718-99-00996-5/S0025-5718-99-00996-5.pdf // m = 2**35 − 31 return (game.seed = game.seed * 200105 % 34359738337) % range } function shuffle(list) { // Fisher-Yates shuffle for (let i = list.length - 1; i > 0; --i) { let j = random(i + 1) let tmp = list[j] list[j] = list[i] list[i] = tmp } } // Fast deep copy for objects without cycles function object_copy(original) { if (Array.isArray(original)) { let n = original.length let copy = new Array(n) for (let i = 0; i < n; ++i) { let v = original[i] if (typeof v === "object" && v !== null) copy[i] = object_copy(v) else copy[i] = v } return copy } else { let copy = {} for (let i in original) { let v = original[i] if (typeof v === "object" && v !== null) copy[i] = object_copy(v) else copy[i] = v } return copy } } // Array remove and insert (faster than splice) function array_remove(array, index) { let n = array.length for (let i = index + 1; i < n; ++i) array[i - 1] = array[i] array.length = n - 1 } function array_insert(array, index, item) { for (let i = array.length; i > index; --i) array[i] = array[i - 1] array[index] = item } function array_insert_pair(array, index, key, value) { for (let i = array.length; i > index; i -= 2) { array[i] = array[i-2] array[i+1] = array[i-1] } array[index] = key array[index+1] = value } // Set as plain sorted array function set_clear(set) { set.length = 0 } function set_has(set, item) { let a = 0 let b = set.length - 1 while (a <= b) { let m = (a + b) >> 1 let x = set[m] if (item < x) b = m - 1 else if (item > x) a = m + 1 else return true } return false } function set_add(set, item) { let a = 0 let b = set.length - 1 while (a <= b) { let m = (a + b) >> 1 let x = set[m] if (item < x) b = m - 1 else if (item > x) a = m + 1 else return } array_insert(set, a, item) } function set_delete(set, item) { let a = 0 let b = set.length - 1 while (a <= b) { let m = (a + b) >> 1 let x = set[m] if (item < x) b = m - 1 else if (item > x) a = m + 1 else { array_remove(set, m) return } } } // Map as plain sorted array of key/value pairs function map_clear(map) { map.length = 0 } function map_get(map, key, missing) { let a = 0 let b = (map.length >> 1) - 1 while (a <= b) { let m = (a + b) >> 1 let x = map[m<<1] if (key < x) b = m - 1 else if (key > x) a = m + 1 else return map[(m<<1)+1] } return missing } function map_set(map, key, value) { let a = 0 let b = (map.length >> 1) - 1 while (a <= b) { let m = (a + b) >> 1 let x = map[m<<1] if (key < x) b = m - 1 else if (key > x) a = m + 1 else { map[(m<<1)+1] = value return } } array_insert_pair(map, a<<1, key, value) }