"use strict" // TODO: capture washington is messy // TODO: campaign messed up who is who after battle // TODO: retreat with 0 CU after battle // campaign // event /* 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 NH = 1 const NY = 2 const MA = 3 const CT = 4 const RI = 5 const PA = 6 const NJ = 7 const MD = 8 const DE = 9 const VA = 10 const NC = 11 const SC = 12 const GA = 13 const 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 NC"] const CAPTURED_GENERALS = data.space_index["Captured Generals"] const CONTINENTAL_CONGRESS_DISPERSED = data.space_index["Continental Congress Dispersed"] const BRITISH_REINFORCEMENTS = data.space_index["British 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 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 WAR_ENDS_1779 = 71 // const BENJAMIN_FRANKLIN = 101 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 ENEMY = { [P_AMERICA]: P_BRITAIN, [P_BRITAIN]: P_AMERICA } var states = {} var events = {} var game var view /* 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, 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 the deck.") clear_flag(F_RESHUFFLE) // Reconstitute deck, minus removed cards, cards in hand, war_ends, and reinforcement cards. // Rule 6.1B clarification. 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, reason) { if (reason) log(game.active[0] + " played #" + c + " " + reason) else log(game.active[0] + " played #" + c) if (CARDS[c].reshuffle === "if_played") set_flag(F_RESHUFFLE) set_delete(active_hand(), c) game.last_played = c if (CARDS[c].once) { log("Removed card " + c + ".") 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) logp("discarded #" + c) } function discard_card(c, reason) { game.last_played = c discard_card_from_hand(active_hand(), c) if (reason) logp("discarded #" + c + " " + reason) else logp("discarded #" + c) } function can_exchange_for_discard(c) { if (game.did_discard_event) { if (game.active === P_BRITAIN) return true return CARDS[c].count > 1 } return false } function can_play_event(c) { let card = CARDS[c] switch (card.when) { case "before_french_alliance": return !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) 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 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) { logp("placed PC in " + 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) { logp("placed PC in " + space) set_space_pc(space, PC_AMERICAN) } function remove_pc(space) { if (game.active === P_BRITAIN) logp("removed PC in " + space) else logp("removed PC in " + space) set_space_pc(space, PC_NONE) } function flip_pc(space) { if (game.active === P_BRITAIN) logp("flipped PC in " + space) else logp("flipped PC in " + 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 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_american_cu(space) { return count_american_cu(space) > 0 } function has_french_cu(space) { return 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 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 } goto_george_washington_captured() } function capture_british_general(where) { let g = find_british_general(where) log(g + " was captured!") move_general(g, CAPTURED_GENERALS) } function capture_american_or_french_general(where) { let g = find_american_or_french_general(where) log(g + " was captured!") if (g === WASHINGTON) capture_washington() else move_general(g, CAPTURED_GENERALS) } function capture_enemy_general(where) { if (game.active === P_BRITAIN) capture_american_or_french_general(where) else capture_british_general(where) } function remove_benedict_arnold() { if (!is_general_at_location(ARNOLD, NOWHERE)) { log("Removed Arnold from the game!") set_general_location(ARNOLD, NOWHERE) } } /* ARMIES */ function has_british_army(where) { return has_british_general(where) && has_british_cu(where) } function has_american_army(where) { return has_american_or_french_general(where) && has_american_or_french_cu(where) } function has_no_british_playing_piece(where) { if (has_british_pc(where)) return false if (has_british_general(where)) return false if (has_british_cu(where)) return false return true } function has_no_american_unit(where) { if (has_american_or_french_general(where)) return false if (has_american_or_french_cu(where)) return false if (game.congress === where) return false return true } function place_british_reinforcements(who, count, where) { let already_there = find_british_general(where) if (who !== NOBODY && already_there !== NOBODY) { move_general(already_there, BRITISH_REINFORCEMENTS) } if (who !== NOBODY) { logp("reinforced " + where + " with " + who) move_general(who, where) } if (count > 0) { logp("reinforced " + where + " with " + count + " CU") move_british_cu(BRITISH_REINFORCEMENTS, where, count) if (has_enemy_general(where)) capture_enemy_general(where) if (game.active === P_BRITAIN && 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) { logp("reinforced " + where + " with " + who) move_general(who, where) } logp("reinforced " + where + " with " + count + " CU") place_american_cu(where, count) if (has_enemy_general(where)) capture_enemy_general(where) } function place_french_reinforcements(who, where) { let already_there = find_american_or_french_general(where) if (who !== NOBODY && already_there !== NOBODY) { // Never replace Washington if (already_there === WASHINGTON) who = NOBODY else move_general(already_there, AMERICAN_REINFORCEMENTS) } if (who !== NOBODY) { logp("reinforced " + where + " with " + who) move_general(who, where) } logp("reinforced " + where + " with the French CU") move_french_cu(FRENCH_REINFORCEMENTS, where, count_french_cu(FRENCH_REINFORCEMENTS)) move_french_cu(AMERICAN_REINFORCEMENTS, where, count_french_cu(AMERICAN_REINFORCEMENTS)) } function pickup_max_british_cu(move, where) { move.carry_british = count_unmoved_british_cu(where) if (move.carry_british > 5) move.carry_british = 5 move.carry_american = 0 move.carry_french = 0 } function pickup_max_american_cu(move, where) { move.carry_british = 0 move.carry_french = count_unmoved_french_cu(where) move.carry_american = count_unmoved_american_cu(where) 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(who, from, to) { game.move.count -= movement_cost(from, to) if (game.move.mobility && has_enemy_cu(to)) { game.move.mobility = false game.move.count -= 1 } if (game.move.carry_british > 0) move_british_cu(from, to, game.move.carry_british) if (game.move.carry_american > 0) move_american_cu(from, to, game.move.carry_american) if (game.move.carry_french > 0) move_french_cu(from, to, game.move.carry_french) move_general(who, to) } function intercept_army(who, from, to) { if (game.intercept.carry_british > 0) move_british_cu(from, to, game.intercept.carry_british) if (game.intercept.carry_american > 0) move_american_cu(from, to, game.intercept.carry_american) if (game.intercept.carry_french > 0) move_french_cu(from, to, game.intercept.carry_french) move_general(who, to) } function overrun(where) { logp("overran CU in " + where) if (game.active === P_BRITAIN) { if (count_american_cu(where) > 0) remove_american_cu(where, 1) else remove_french_cu(where, 1) } else { remove_british_cu(where, 1) } } function retreat_american_army(from, to) { let g = find_american_or_french_general(from) if (g !== NOBODY) move_general(g, to) move_american_cu(from, to, count_american_cu(from)) move_french_cu(from, to, count_french_cu(from)) } function retreat_british_army(from, to) { let g = find_british_general(from) if (g !== NOBODY) move_general(g, to) move_british_cu(from, to, count_british_cu(from)) } 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("Contintental Congress dispersed!") game.congress = CONTINENTAL_CONGRESS_DISPERSED set_flag(F_CONGRESS_WAS_DISPERSED) } /* MOVE GENERATORS */ function gen_remove_british_pc_from(list_of_colonies) { for (let colony of list_of_colonies) { for (let space of COLONIES[colony]) { if (has_british_pc(space) && has_no_british_cu(space)) { gen_action_space(space) } } } } function gen_remove_american_pc() { for (let space of all_spaces) { if (has_american_pc(space) && has_no_american_unit(space)) { gen_action_space(space) } } } function gen_remove_american_pc_from(list_of_colonies) { for (let colony of list_of_colonies) { for (let space of COLONIES[colony]) { if (has_american_pc(space) && has_no_american_unit(space)) { gen_action_space(space) } } } } function gen_remove_american_pc_from_non_port(list_of_colonies) { for (let colony of list_of_colonies) { for (let space of COLONIES[colony]) { if (!is_port(space)) { if (has_american_pc(space) && has_no_american_unit(space)) { gen_action_space(space) } } } } } function gen_remove_american_pc_within_two_spaces_of_a_british_general() { let candidates = [] for (let g of BRITISH_GENERALS) { let a = location_of_general(g) if (is_map_space(a)) { set_add(candidates, a) for (let b of SPACES[a].adjacent) { set_add(candidates, b) for (let c of SPACES[b].adjacent) { set_add(candidates, c) } } } } for (let space of candidates) if (has_american_pc(space) && has_no_american_unit(space)) gen_action_space(space) } function gen_place_american_pc() { for (let space of all_spaces) { if (has_no_pc(space) && has_no_british_playing_piece(space)) { gen_action_space(space) } } } function gen_place_american_pc_in(list_of_colonies) { for (let colony of list_of_colonies) { for (let space of COLONIES[colony]) { if (has_no_pc(space) && has_no_british_playing_piece(space)) { gen_action_space(space) } } } } /* SETUP PHASE */ function goto_committees_of_correspondence() { log("=a Committes of Correspondence") logbr() 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() { view.prompt = "Committees of Correspondence: Place 1 PC marker in each of the 13 colonies." if (game.colonies.length > 0) { view.prompt += " " + game.colonies.length + " left." gen_place_american_pc_in(game.colonies) } else { view.prompt += " 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() { logbr() log("=b For the King") logbr() 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() { view.prompt = "For the King: Place 3 PC markers." if (game.count > 0) gen_british_pc_ops() else view.actions.next = 1 }, 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() { logbr() log("=t Year " + game.year) logbr() // Prisoner exchange // TODO: manual? 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 deck.") game.deck.push(DECLARATION_OF_INDEPENDENCE) game.deck.push(BARON_VON_STEUBEN) shuffle(game.deck) } if (has_flag(F_RESHUFFLE)) { log("Re-shuffled deck.") 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 = { 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_campaign", c) } } }, card_campaign(c) { clear_flag(F_CONGRESS_WAS_DISPERSED) logp("went first by playing a campaign card") game.active = P_BRITAIN goto_campaign(c) }, pass() { if (has_flag(F_CONGRESS_WAS_DISPERSED)) game.active = P_BRITAIN else game.active = P_AMERICA game.state = "choose_first_player" clear_flag(F_CONGRESS_WAS_DISPERSED) }, } states.choose_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() { logp("went first") goto_strategy_phase(P_AMERICA) }, britain_first() { logp("went first") goto_strategy_phase(P_BRITAIN) }, } /* STRATEGY PHASE */ function goto_strategy_phase(new_active) { game.active = new_active game.state = "strategy_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()) { let card = CARDS[c] switch (card.type) { case "mandatory-event": gen_action_card("card_play_event", c) break case "campaign": gen_action_card("card_campaign", c) break case "ops": if (can_exchange_for_discard(c)) gen_action_card("exchange_for_discard", c) if (can_activate_general(c)) gen_action_card("card_ops_general", c) gen_action_card("card_ops_pc", c) if (can_play_reinforcements()) gen_action_card("card_ops_reinforcements", c) if (card.count < 3) gen_action_card("card_ops_queue", c) break case "british-event": case "british-event-or-battle": if (game.active === P_BRITAIN) if (can_play_event(c)) gen_action_card("card_play_event", c) gen_action_card("card_discard_event", c) break case "american-event": if (game.active === P_AMERICA) if (can_play_event(c)) gen_action_card("card_play_event", c) gen_action_card("card_discard_event", c) break case "british-battle": case "american-battle": gen_action_card("card_discard_event", c) break } } }, card_campaign(c) { game.did_discard_event = 0 clear_queue() goto_campaign(c) }, card_play_event(c) { push_undo() game.did_discard_event = 0 clear_queue() do_event(c) }, card_discard_event(c) { push_undo() game.did_discard_event = c clear_queue() discard_card(c, "PC action") game.state = "discard_event_pc_action" }, card_ops_pc(c) { push_undo() game.did_discard_event = 0 clear_queue() play_card(c, "for PC") goto_ops_pc(CARDS[c].count) }, card_ops_reinforcements(c) { push_undo() game.did_discard_event = 0 clear_queue() goto_ops_reinforcements(c) }, card_ops_queue(c) { game.did_discard_event = 0 play_card(c, "to queue") if (game.active === P_BRITAIN) game.b_queue += CARDS[c].count else game.a_queue += CARDS[c].count end_strategy_card() }, card_ops_general(c) { push_undo() game.did_discard_event = 0 goto_ops_general(c) }, exchange_for_discard(c) { let d = game.did_discard_event = 0 game.did_discard_event = 0 discard_card(c, "exchange") set_add(active_hand(), d) logp("picked up up #" + d) }, } function end_strategy_card() { if (automatic_victory()) { clear_undo() return } if (game.campaign) { if (--game.campaign > 0) { game.count = 3 // can activate any general! game.state = "ops_general_who" return } else { clear_flag(F_LANDING_PARTY) delete game.campaign } } game.state = "end_strategy_card" } states.end_strategy_card = { prompt() { view.prompt = "Done." }, next() { clear_undo() next_strategy_card() }, } function next_strategy_card() { 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() goto_strategy_phase(ENEMY[game.active]) let hand = active_hand() if (hand.length === 0) { game.active = ENEMY[game.active] hand = active_hand() if (hand.length === 0) return goto_winter_attrition_phase() } } function clear_queue() { if (game.active === P_BRITAIN) game.b_queue = 0 else game.a_queue = 0 } /* DISCARD EVENT CARD FOR PC ACTION */ states.discard_event_pc_action = { prompt() { view.prompt = "Place, flip, or remove PC marker." view.actions.pass = 1 if (game.active === P_BRITAIN) gen_british_discard_event_pc_action() else gen_american_discard_event_pc_action() }, space(s) { 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_british_army(s)) flip_pc(s) else remove_pc(s) } end_strategy_card() }, pass() { end_strategy_card() }, } 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 = { prompt() { view.prompt = "Place or flip PC markers. " + game.count + " left." view.actions.pass = 1 if (game.count > 0) { if (game.active === P_BRITAIN) gen_british_pc_ops() else gen_american_pc_ops() } }, 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) } --game.count }, pass() { if (game.active === P_BRITAIN) gen_british_pc_ops_end() end_strategy_card() }, } 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 goto_ops_reinforcements(c) { let count = CARDS[c].count play_card(c, "for reinforcements") 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 = { prompt() { view.prompt = "Reinforcements: choose an available general or pass to bring only CU." view.prompt += " Carrying " + game.count + " British CU." view.move = { to: BRITISH_REINFORCEMENTS, who: NOBODY, carry_british: game.count } view.actions.pass = 1 gen_british_reinforcements_who() }, drop_british_cu() { --game.count }, pickup_british_cu() { ++game.count }, general(g) { push_undo() game.state = "ops_british_reinforcements_where" game.who = g }, pass() { push_undo() game.state = "ops_british_reinforcements_where" delete game.who }, } states.ops_british_reinforcements_where = { prompt() { view.prompt = "Reinforcements: choose a port space." view.prompt += " Carrying " + game.count + " British CU." view.move = { to: BRITISH_REINFORCEMENTS, who: game.who, carry_british: game.count } gen_british_reinforcements_where() }, drop_british_cu() { --game.count }, pickup_british_cu() { ++game.count }, space(space) { place_british_reinforcements(game.who, game.count, space) end_strategy_card() delete game.who }, } states.ops_american_reinforcements_who = { prompt() { view.prompt = "Reinforcements: choose an available general or pass to bring only CU." view.move = { to: AMERICAN_REINFORCEMENTS, who: NOBODY, carry_american: game.count } view.actions.pass = 1 gen_american_reinforcements_who() }, general(g) { push_undo() game.state = "ops_american_reinforcements_where" game.who = g }, pass() { push_undo() game.state = "ops_american_reinforcements_where" delete game.who }, } states.ops_american_reinforcements_where = { prompt() { view.prompt = "Reinforcements: choose a space." view.move = { to: AMERICAN_REINFORCEMENTS, who: game.who, carry_american: game.count } gen_american_reinforcements_where(game.who) }, space(space) { if (game.who === ROCHAMBEAU) place_french_reinforcements(game.who, space) else place_american_reinforcements(game.who, game.count, space) end_strategy_card() delete game.who }, } function gen_british_reinforcements_who() { for (let g of BRITISH_GENERALS) { if (is_general_at_location(g, BRITISH_REINFORCEMENTS)) { gen_action_general(g) } } if (game.count > 0) gen_action("drop_british_cu") if (game.count < count_british_cu(BRITISH_REINFORCEMENTS)) gen_action("pickup_british_cu") } function gen_british_reinforcements_where() { for (let space of all_spaces) { if (is_non_blockaded_port(space)) if (!has_american_or_french_cu(space) && !has_american_pc(space)) gen_action_space(space) } if (game.count > 0) gen_action("drop_british_cu") if (game.count < count_british_cu(BRITISH_REINFORCEMENTS)) gen_action("pickup_british_cu") } function gen_american_reinforcements_who() { for (let g of AMERICAN_GENERALS) { 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)) { if (general === ROCHAMBEAU) { if (is_port(space)) gen_action_space(space) } else { gen_action_space(space) } } } } /* PLAY OPS CARD TO MOVE A GENERAL */ function goto_ops_general(c) { play_card(c, "to activate a general") 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 = { prompt() { if (game.campaign && !has_flag(F_LANDING_PARTY)) view.prompt = "Campaign: Activate a general or use a landing party. " + game.campaign + " left." else if (game.campaign) view.prompt = "Campaign: Activate a general. " + game.campaign + " left." else view.prompt = "Activate a general with strategy rating " + game.count + " or lower." if (has_flag(F_LANDING_PARTY)) gen_landing_party() gen_activate_general() view.actions.pass = 1 }, place_british_pc(where) { clear_flag(F_LANDING_PARTY) place_british_pc(where) end_strategy_card() }, flip_pc(where) { clear_flag(F_LANDING_PARTY) flip_pc(where) end_strategy_card() }, general(g) { push_undo() goto_ops_general_move(g, false) }, pass() { if (game.campaign > 0) game.campaign = 0 end_strategy_card() }, } 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("flip_pc", space) if (has_no_pc(space) && has_no_american_unit(space) && has_no_british_playing_piece(space)) gen_action("place_british_pc", space) } } } function gen_activate_general() { if (game.active === 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_remove_general(where) { game.state = "remove_general" game.where = where } states.remove_general = { prompt() { view.prompt = "Remove a general to the reinforcements box." gen_remove_general(game.where) }, general(g) { if (game.active === P_BRITAIN) move_general(g, BRITISH_REINFORCEMENTS) else move_general(g, AMERICAN_REINFORCEMENTS) end_strategy_card() }, } function goto_remove_general_after_intercept() { game.state = "remove_general_after_intercept" } states.remove_general_after_intercept = { prompt() { view.prompt = "Remove a general to the reinforcements box." gen_remove_general(game.move.to) }, general(g) { if (game.active === P_BRITAIN) move_general(g, BRITISH_REINFORCEMENTS) else move_general(g, AMERICAN_REINFORCEMENTS) end_intercept() }, } function goto_remove_general_after_retreat(where) { game.state = "remove_general_after_retreat" game.where = where } states.remove_general_after_retreat = { prompt() { view.prompt = "Remove a general to the reinforcements box." gen_remove_general(game.where) }, general(g) { if (game.active === P_BRITAIN) move_general(g, BRITISH_REINFORCEMENTS) else move_general(g, AMERICAN_REINFORCEMENTS) end_battle() }, } function gen_remove_general(where) { if (game.active === P_BRITAIN) return gen_remove_british_general(where) else return gen_remove_american_general(where) } function gen_remove_british_general(where) { for (let g of BRITISH_GENERALS) if (is_general_at_location(g, where)) gen_action_general(g) } function gen_remove_american_general(where) { for (let g of AMERICAN_GENERALS) if (g !== WASHINGTON) if (is_general_at_location(g, where)) 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, where) else pickup_max_american_cu(game.move, where) } states.ops_general_move = { prompt() { view.prompt = "Move " + game.move.who + " with " if (game.move.carry_british > 0) { view.prompt += game.move.carry_british + " 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 + " French CU." } } else { view.prompt += game.move.carry_american + " American CU." } } else { view.prompt += game.move.carry_american + " no CU." } if (game.count === 1) view.prompt += " " + game.move.count + " move left." else if (game.count > 1) view.prompt += " " + game.move.count + " moves left." // Cannot stop on enemy general if (!has_enemy_general(location_of_general(game.move.who))) view.actions.pass = 1 gen_carry_cu() gen_move_general() }, pickup_british_cu() { ++game.move.carry_british }, pickup_american_cu() { ++game.move.carry_american }, pickup_french_cu() { ++game.move.carry_french }, drop_british_cu() { push_undo() --game.move.carry_british if (has_general_moved(game.move.who)) 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)) 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)) mark_moved_french_cu(location_of_general(game.move.who), 1) }, space(to) { push_undo() set_general_moved(game.move.who) let from = location_of_general(game.move.who) let cu = game.move.carry_british + game.move.carry_american + game.move.carry_french let 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)) intercept = can_intercept_to(to) } game.move.from = from game.move.to = to move_army(game.move.who, from, to) if (cu > 0) { if (has_enemy_general(to) && !has_enemy_cu(to)) { capture_enemy_general(to) } if (game.active === P_BRITAIN && game.congress === to && !has_enemy_cu(to)) { disperse_continental_congress() } if (cu >= 4 && count_enemy_cu(to) === 1 && !has_enemy_general(to)) { overrun(to) } } if (intercept) goto_intercept() else resume_moving() }, pass() { clear_undo() let where = location_of_general(game.move.who) end_move() if (count_friendly_generals(where) > 1) goto_remove_general(where) else end_strategy_card() }, } function resume_moving() { if (has_enemy_cu(game.move.to)) { goto_start_battle() } } function can_intercept_to(to) { for (let space of SPACES[to].adjacent) { if (has_american_army(space)) { let g = find_american_or_french_general(space) if (g && !has_general_moved(g)) return true } } return false } function gen_intercept() { for (let space of SPACES[game.move.to].adjacent) { if (has_american_army(space)) { let g = find_american_or_french_general(space) if (g && !has_general_moved(g)) gen_action_general(g) } } } function goto_intercept() { clear_undo() game.active = P_AMERICA game.state = "intercept" } states.intercept = { prompt() { view.prompt = "Intercept " + game.move.who + " in " + game.move.to + "?" view.actions.pass = 1 gen_intercept() }, general(g) { set_general_moved(g) let die = roll_d6() if (die <= GENERALS[g].agility) { log(g + " intercepted (" + die + " <= " + GENERALS[g].agility + ")") game.move.did_intercept = 1 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, location_of_general(g)) intercept_army(g, location_of_general(g), game.move.to) if (count_friendly_generals(game.move.to) > 1) goto_remove_general_after_intercept() else end_intercept() } else { log(g + " failed to intercept (" + die + " > " + GENERALS[g].agility + ")") delete game.intercept if (!can_intercept_to(game.move.to)) end_intercept() } }, pass() { end_intercept() }, } function end_intercept() { game.active = P_BRITAIN game.state = "ops_general_move" delete game.intercept resume_moving() } function end_move() { 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 } function path_type(from, to) { if (set_has(SPACES[from].path, to)) return "path" if (set_has(SPACES[from].wilderness, to)) return "wilderness" return "sea" } function gen_carry_cu() { let where = location_of_general(game.move.who) if (game.active === P_BRITAIN) { if (game.move.carry_british > 0) gen_action("drop_british_cu") if (game.move.carry_british < 5 && game.move.carry_british < count_unmoved_british_cu(where)) gen_action("pickup_british_cu") } else { let carry_total = game.move.carry_french + game.move.carry_american if (game.move.carry_french > 0) gen_action("drop_french_cu") if (game.move.carry_american > 0) gen_action("drop_american_cu") if (carry_total < 5 && game.move.carry_french < count_unmoved_french_cu(where)) gen_action("pickup_french_cu") if (carry_total < 5 && game.move.carry_american < count_unmoved_american_cu(where)) gen_action("pickup_american_cu") } } 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 SPACES[from].adjacent) { let mp = 1 if (path_type(from, to) === "wilderness") { if ((from === QUEBEC && to === FALMOUTH) || (to === QUEBEC && from === FALMOUTH)) if (game.move.who !== ARNOLD) continue mp = 3 } if (alone) { if (has_enemy_cu(to)) continue if (has_enemy_pc(to)) continue // TODO: more robust check for not stopping (or allow undo in case he gets stuck) if (has_enemy_general(to) && game.count - mp === 0) continue } if (game.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.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 // TODO: duplicate action if can move by normal road gen_action_space(to) } } } } } } } /* CAMPAIGN */ function goto_campaign(c) { play_card(c) game.state = "campaign" game.campaign = CARDS[c].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 */ events.the_war_ends = function (c, card) { logp("played #" + c) log("The war will end in " + card.year) game.last_played = c set_delete(active_hand(), c) game.war_ends = c end_strategy_card() } events.remove_random_british_card = function (c, card) { play_card(c) remove_random_card(game.b_hand) } events.remove_random_american_card = function (c, card) { play_card(c) remove_random_card(game.a_hand) } function remove_random_card(hand) { if (hand.length > 0) { let i = random(hand.length) let c = hand[i] discard_card_from_hand(hand, c) if (CARDS[c].type === "mandatory-event") do_event(c) else end_strategy_card() } else end_strategy_card() } function advance_french_alliance(count) { if (game.french_alliance < 9) { game.french_alliance += count if (game.french_alliance > 9) game.french_alliance = 9 log("French alliance advanced to " + count) } } function lose_regular_advantage() { if (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 (c, card) { play_card(c) if (is_general_on_map(WASHINGTON)) { let where = location_of_general(WASHINGTON) logp("placed 2 CU with Washington in " + where) place_american_cu(where, 2) lose_regular_advantage() } end_strategy_card() } events.advance_french_alliance = function (c, card) { play_card(c) advance_french_alliance(card.count) end_strategy_card() } events.remove_french_navy = function (c, card) { play_card(c) game.french_navy = game.year + 1 end_strategy_card() } events.remove_british_pc_from = function (c, card) { play_card(c) game.count = card.count game.where = card.where game.state = "remove_british_pc_from" } states.remove_british_pc_from = { prompt() { view.prompt = "Remove British PC markers from " + game.where.join(", ") + ". " + game.count + " left." view.actions.pass = 1 gen_remove_british_pc_from(game.where) }, space(where) { remove_pc(where) if (--game.count === 0) { delete game.where end_strategy_card() } }, pass() { delete game.where end_strategy_card() }, } events.remove_american_pc = function (c, card) { play_card(c) game.count = card.count game.state = "remove_american_pc" } states.remove_american_pc = { prompt() { view.prompt = "Remove American PC markers. " + game.count + " left." view.actions.pass = 1 gen_remove_american_pc() }, space(where) { remove_pc(where) if (--game.count === 0) { end_strategy_card() } }, pass() { end_strategy_card() }, } events.remove_american_pc_from = function (c, card) { play_card(c) game.count = card.count game.where = card.where game.state = "remove_american_pc_from" } states.remove_american_pc_from = { prompt() { view.prompt = "Remove American PC markers from " + game.where.join(", ") + ". " + game.count + " left." view.actions.pass = 1 gen_remove_american_pc_from(game.where) }, space(where) { remove_pc(where) if (--game.count === 0) { delete game.where end_strategy_card() } }, pass() { delete game.where end_strategy_card() }, } events.remove_american_pc_from_non_port = function (c, card) { play_card(c) game.count = card.count game.where = card.where game.state = "remove_american_pc_from_non_port" } states.remove_american_pc_from_non_port = { prompt() { view.prompt = "Remove American PC markers from non-Port space in " + game.where.join(", ") + ". " + game.count + " left." view.actions.pass = 1 gen_remove_american_pc_from_non_port(game.where) }, space(where) { remove_pc(where) if (--game.count === 0) { delete game.where end_strategy_card() } }, pass() { delete game.where end_strategy_card() }, } events.remove_american_pc_within_two_spaces_of_a_british_general = function (c, card) { play_card(c) game.count = card.count game.state = "remove_american_pc_within_two_spaces_of_a_british_general" } states.remove_american_pc_within_two_spaces_of_a_british_general = { prompt() { view.prompt = "Remove American PC markers within two spaces of a British general. " + game.count + " left." view.actions.pass = 1 gen_remove_american_pc_within_two_spaces_of_a_british_general() }, space(where) { remove_pc(where) if (--game.count === 0) { delete game.where end_strategy_card() } }, pass() { delete game.where end_strategy_card() }, } events.place_american_pc = function (c, card) { play_card(c) game.count = card.count game.state = "place_american_pc" } states.place_american_pc = { prompt() { view.prompt = "Place American PC markers. " + game.count + " left." view.actions.pass = 1 gen_place_american_pc() }, place_american_pc(where) { place_american_pc(where) if (--game.count === 0) { end_strategy_card() } }, pass() { end_strategy_card() }, } events.place_american_pc_in = function (c, card) { play_card(c) game.count = card.count game.where = card.where game.state = "place_american_pc_in" } states.place_american_pc_in = { prompt() { view.prompt = "Place American PC markers in " + game.where.join(", ") + ". " + game.count + " left." view.actions.pass = 1 gen_place_american_pc_in(game.where) }, place_american_pc(where) { place_american_pc(where) if (--game.count === 0) { delete game.where end_strategy_card() } }, pass() { delete game.where end_strategy_card() }, } events.lord_sandwich_coastal_raids = function (c, card) { play_card(c) game.state = "lord_sandwich_coastal_raids" game.count = 2 delete game.where } states.lord_sandwich_coastal_raids = { prompt() { view.prompt = "Remove two or flip one American PC in a port space." view.actions.pass = 1 gen_lord_sandwich_coastal_raids(game.where) }, space(where) { if (has_american_pc(space)) { game.where = where remove_pc(where) if (--game.count === 0) end_strategy_card() } else { place_british_pc(where) } end_strategy_card() }, pass() { end_strategy_card() }, } 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 (c, card) { play_card(c) game.state = "remove_american_cu" } states.remove_american_cu = { prompt() { view.prompt = "Remove one American CU from any space." view.actions.pass = 1 gen_remove_american_cu() }, remove_cu(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() }, } function gen_remove_american_cu() { for (let space of all_spaces) { if (has_american_or_french_cu(space)) gen_action("remove_cu", space) } } events.remove_british_cu = function (c, card) { play_card(c) game.state = "remove_british_cu" game.count = card.count } states.remove_british_cu = { prompt() { view.prompt = "Remove " + game.count + " British CU from any space." view.actions.pass = 1 gen_remove_british_cu() }, remove_cu(where) { remove_british_cu(where, 1) if (--game.count === 0) end_strategy_card() }, pass() { end_strategy_card() }, } function gen_remove_british_cu() { for (let space of all_spaces) { if (has_british_cu(space)) gen_action("remove_cu", space) } } events.pennsylvania_and_new_jersey_line_mutinies = function (c, card) { play_card(c) 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 = { prompt() { view.prompt = "Remove two American CUs from the map, one each from two different spaces." view.actions.pass = 1 gen_pennsylvania_and_new_jersey_line_mutinies(game.where) }, remove_cu(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() }, } 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("remove_cu", space) } } events.john_glovers_marblehead_regiment = function (c, card) { play_card(c) game.state = "john_glovers_marblehead_regiment_who" game.count = 3 // strategy rating for gen_activate_general } states.john_glovers_marblehead_regiment_who = { prompt() { view.prompt = "Activate an American general." gen_activate_general() }, general(g) { goto_ops_general_move(g, true) }, } events.declaration_of_independence = function (c, card) { play_card(c) game.save = game.active game.active = P_AMERICA game.colonies = THE_13_COLONIES.slice() game.state = "declaration_of_independence" } states.declaration_of_independence = { prompt() { view.prompt = "Declaration of Independence: Place 1 PC marker in each of the 13 colonies. " view.prompt += game.colonies.length + " left." view.actions.pass = 1 gen_place_american_pc_in(game.colonies) }, place_american_pc(space) { let colony = SPACES[space].colony set_delete(game.colonies, colony) place_american_pc(space) if (game.colonies.length === 0) end_declaration_of_independence() }, pass() { end_declaration_of_independence() }, } function end_declaration_of_independence() { game.active = game.save delete game.save delete game.colonies end_strategy_card() } function goto_george_washington_captured() { // TODO: this is not robust enough! figure out exactly where we are called. /* Save all the state we clobber during the interrupt. */ game.save = { state: game.state, active: game.active, count: game.count } game.state = "george_washington_captured" game.active = P_BRITAIN game.count = 5 } states.george_washington_captured = { prompt() { view.prompt = "George Washington is captured! Remove American PC markers. " + game.count + " left." view.actions.pass = 1 gen_remove_american_pc() }, space(where) { remove_pc(where) if (--game.count === 0) { end_george_washington_captured() } }, pass() { end_george_washington_captured() }, } function end_george_washington_captured() { /* Restore previous state. */ game.state = game.save.state game.count = game.save.count game.active = game.save.active delete game.save } function do_event(c) { let card = CARDS[c] if (card.event in events) events[card.event](c, card) else throw new Error("Event not implemented yet: " + card.event) } /* BATTLE */ function can_retreat_before_battle() { if (game.move.did_intercept) return false // can't retreat if attempted (successful or not) interception! let g = find_american_or_french_general(game.move.to) if (g && !has_general_moved(g)) return true return false } function goto_start_battle() { clear_undo() game.combat = { attacker: game.active, a_bonus: 0, b_bonus: 0, b_draw_after_battle: false, a_draw_after_battle: false, british_losses: 0, } if (game.active === P_BRITAIN && can_retreat_before_battle()) goto_retreat_before_battle() else goto_play_attacker_battle_card() } function goto_retreat_before_battle() { game.active = P_AMERICA game.state = "retreat_before_battle" } states.retreat_before_battle = { prompt() { view.prompt = "Attempt retreat before battle?" view.actions.pass = 1 gen_defender_retreat() }, space(to) { let who = find_american_or_french_general(game.move.to) let agility = GENERALS[who].agility if (GENERALS[who].bonus) agility += 2 let roll = roll_d6() if (roll <= agility) { logp("successfully retreated before battle: " + roll + " <= " + agility) pickup_max_american_cu(game.move.to) move_army(who, game.move.to, to) goto_remove_general_after_retreat_before_battle(to) } else { logp("failed to retreat before battle: " + roll + " > " + agility) end_retreat_before_battle() } }, pass() { end_retreat_before_battle() }, } 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_remove_general_after_retreat_before_battle() } } states.remove_general_after_retreat_before_battle = { prompt() { view.prompt = "Remove a general to the reinforcements box." gen_remove_general(game.where) }, general(g) { if (game.active === P_BRITAIN) move_general(g, BRITISH_REINFORCEMENTS) else move_general(g, AMERICAN_REINFORCEMENTS) end_remove_general_after_retreat_before_battle() }, } function end_remove_general_after_retreat_before_battle() { let b_cu = count_british_cu(game.move.to) let a_cu = count_american_and_french_cu(game.move.to) if (a_cu === 0) { end_battle() } else if (b_cu >= 4 && a_cu === 1) { overrun(game.move.to) end_battle() } else { end_retreat_before_battle() } } function can_defender_retreat(to) { if (to === game.move.from) return false if (has_enemy_pc(to)) return false if (has_enemy_cu(to)) return false return true } function can_attacker_retreat() { let to = game.move.from if (has_enemy_pc(to)) return false if (has_enemy_cu(to)) return false return true } function gen_defender_retreat() { let from = game.move.to for (let to of SPACES[from].adjacent) { if (can_defender_retreat(to)) gen_action_space(to) } if (game.active === P_BRITAIN) { let can_sea_retreat = false if (is_non_blockaded_port(from)) { if (is_fortified_port(from)) { if (has_british_pc(from) && is_non_blockaded_port(from)) can_sea_retreat = true } else { can_sea_retreat = true } } if (can_sea_retreat) { for (let to of all_spaces) { if (to !== from && is_non_blockaded_port(to)) { if (!has_american_pc(to) && !has_american_or_french_cu(to)) { gen_action_space(to) } } } } } } function gen_attacker_retreat() { if (can_attacker_retreat()) gen_action_space(game.move.from) } function end_retreat_before_battle() { goto_play_attacker_battle_card() } function goto_play_attacker_battle_card() { game.active = game.combat.attacker game.state = "play_attacker_battle_card" } states.play_attacker_battle_card = { prompt() { view.prompt = "Attack: Play or discard event for DRM." view.actions.pass = 1 gen_battle_card() }, card_battle_play(c) { play_card(c, "for +2 DRM") if (game.active === P_BRITAIN) { if (CARDS[c].event === "remove_benedict_arnold") remove_benedict_arnold() game.combat.b_draw_after_battle = true game.combat.b_bonus += 2 } else { game.combat.a_draw_after_battle = true game.combat.a_bonus += 2 } goto_play_defender_battle_card() }, card_battle_discard(c) { discard_card(c, "for +1 DRM") if (game.active === P_BRITAIN) { game.combat.b_draw_after_battle = true game.combat.b_bonus += 1 } else { game.combat.a_draw_after_battle = true game.combat.a_bonus += 1 } goto_play_defender_battle_card() }, pass() { goto_play_defender_battle_card() }, } function goto_play_defender_battle_card() { game.state = "play_defender_battle_card" game.active = ENEMY[game.combat.attacker] } states.play_defender_battle_card = { prompt() { view.prompt = "Defend: Play or discard event for DRM." view.actions.pass = 1 gen_battle_card() }, card_battle_play(c) { play_card(c, "for +2 DRM") if (game.active === P_BRITAIN) { if (CARDS[c].event === "remove_benedict_arnold") remove_benedict_arnold() game.combat.b_draw_after_battle = true game.combat.b_bonus += 2 } else { game.combat.a_draw_after_battle = true game.combat.a_bonus += 2 } resolve_battle() }, card_battle_discard(c) { discard_card(c, "for +1 DRM") if (game.active === P_BRITAIN) { game.combat.b_draw_after_battle = true game.combat.b_bonus += 1 } else { game.combat.a_draw_after_battle = true game.combat.a_bonus += 1 } resolve_battle() }, pass() { resolve_battle() }, } function gen_battle_card() { for (let c of active_hand()) { let card = CARDS[c] if (game.active === P_BRITAIN) { switch (card.type) { case "british-battle": case "british-event-or-battle": gen_action_card("card_battle_play", c) break case "british-event": case "american-event": case "american-battle": gen_action_card("card_battle_discard", c) break } } else { switch (card.type) { case "british-battle": case "british-event-or-battle": case "british-event": case "american-event": gen_action_card("card_battle_discard", c) break case "american-battle": gen_action_card("card_battle_play", c) break } } } } function roll_loser_combat_losses(log) { let roll = roll_d6() let losses = 0 log.push("Loser Combat Loss Roll: " + roll) switch (roll) { case 1: case 2: case 3: losses = 1 break case 4: case 5: losses = 2 break case 6: losses = 3 break } return losses } function roll_winner_combat_losses(log, losing_general) { let agility = losing_general !== NOBODY ? GENERALS[losing_general].agility : 0 let roll = roll_d6() log.push("Enemy Agility Rating: " + agility) log.push("Winner Combat Loss Roll: " + roll) let losses = 0 switch (agility) { case 0: losses = roll === 1 ? 1 : 0 break case 1: losses = roll <= 2 ? 1 : 0 break case 2: losses = roll <= 3 ? 1 : 0 break case 3: losses = roll <= 4 ? 1 : 0 break } return losses } function apply_british_combat_losses(max) { let n = Math.min(count_british_cu(game.move.to), max) remove_british_cu(game.move.to, 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) 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) 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 = [] 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 if (b_g !== NOBODY) b_log.push("General: " + b_g) if (a_g !== NOBODY) a_log.push("General: " + a_g) 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("Actual Battle Rating Roll: " + roll) } 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) a_log.push("Actual Battle Rating Roll: " + roll) } b_log.push("+" + b_cu + " CU") a_log.push("+" + a_cu + " CU") b_log.push("+" + b_br + " Actual Battle Rating") a_log.push("+" + a_br + " Actual Battle Rating") let b_drm = b_cu + b_br + game.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 } let b_roll = roll_d6() let a_roll = roll_d6() b_log.push("Battle Roll: " + b_roll) b_log.push("Battle Total: " + (b_roll + b_drm)) a_log.push("Battle Roll: " + a_roll) a_log.push("Battle Total: " + (a_roll + a_drm)) let victor if (game.active === P_BRITAIN) victor = b_roll + b_drm >= a_roll + a_drm ? P_BRITAIN : P_AMERICA else victor = b_roll + b_drm >= a_roll + a_drm ? P_BRITAIN : P_AMERICA let a_lost_cu, b_lost_cu if (victor === P_BRITAIN) { b_lost_cu = roll_winner_combat_losses(b_log, a_g) a_lost_cu = roll_loser_combat_losses(a_log) } else { b_lost_cu = roll_loser_combat_losses(b_log) a_lost_cu = roll_winner_combat_losses(a_log, b_g) } game.combat.british_losses = apply_british_combat_losses(b_lost_cu) let american_losses = apply_american_and_french_combat_losses(a_lost_cu) b_log.push("Losses: " + game.combat.british_losses + " CU") a_log.push("Losses: " + american_losses + " 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("BRITISH BATTLE REPORT:\n" + b_log.join("\n")) log("AMERICAN BATTLE REPORT:\n" + a_log.join("\n")) log(victor + " victory in " + game.move.to + "!") if (victor === P_AMERICA) advance_french_alliance(1) goto_retreat_after_battle(victor) } function goto_retreat_after_battle(victor) { if (victor === P_BRITAIN) { let who = find_american_or_french_general(game.move.to) if (who === NOBODY && count_american_and_french_cu(game.move.to) === 0) return end_battle() } else { let who = find_british_general(game.move.to) if (who === NOBODY && count_british_cu(game.move.to) === 0) return end_battle() } game.active = ENEMY[victor] game.state = "retreat_after_battle" } states.retreat_after_battle = { prompt() { view.prompt = "Retreat after battle." gen_action("surrender") if (game.active === game.combat.attacker) gen_attacker_retreat() else gen_defender_retreat() }, space(to) { logp("retreated to " + to) if (game.active === P_BRITAIN) retreat_british_army(game.move.to, to) else retreat_american_army(game.move.to, to) if (count_friendly_generals(to) > 1) goto_remove_general_after_retreat(to) else end_battle() }, surrender() { // End battle here, so if Washington is captured we can handle the interrupt state. let active = game.active // TODO: ugly clean this up end_battle() logp("surrendered") if (active === P_BRITAIN) surrender_british_army(game.move.to) else surrender_american_army(game.move.to) }, } function end_battle() { game.active = game.combat.attacker if (game.active === P_BRITAIN && game.congress === game.move.to) disperse_continental_congress() if (game.combat.british_losses >= 3) lose_regular_advantage() // TODO: delay until end of campaign if (game.combat.b_draw_after_battle) set_add(game.b_hand, deal_card()) if (game.combat.a_draw_after_battle) set_add(game.a_hand, deal_card()) delete game.combat end_move() end_strategy_card() } /* WINTER ATTRITION PHASE */ function apply_single_winter_attrition(owner, space) { let die = roll_d6() log(owner[0] + " attrition roll " + die + " in " + space) if (die <= 3) { log(owner[0] + " lost 1 CU in " + space) remove_cu(owner, space, 1) } } function apply_winter_attrition(owner, space, n) { let half = Math.floor(n / 2) log(owner[0] + " lost " + half + " CU in " + space) remove_cu(owner, space, half) } function goto_winter_attrition_phase() { logbr() log("Winter Attrition") for (let space of all_spaces) { let wq = is_winter_quarter_space(space) let n_british = count_british_cu(space) let n_american = count_american_cu(space) let n_french = count_french_cu(space) let has_washington = is_general_at_location(WASHINGTON, space) if (n_british === 1 && !wq) apply_single_winter_attrition(BRITISH, space, has_british_general(space)) if (n_british > 1 && !wq) apply_winter_attrition(BRITISH, space, n_british) if (n_american === 0 && n_french === 1 && !wq) apply_single_winter_attrition(FRENCH, space, has_american_or_french_general(space)) if (n_american === 0 && n_french > 1 && !wq) apply_winter_attrition(FRENCH, space, n_french) if (n_american === 1 && n_french === 0) apply_single_winter_attrition(AMERICAN, space, has_american_or_french_general(space)) if (n_american > 1 && n_french === 0) { let n = n_american if (has_washington && wq) n = Math.max(0, n - 5) apply_winter_attrition(AMERICAN, space, n) } if (n_american > 0 && n_french > 0) { let n = n_american + n_french if (has_washington && wq) n = Math.max(0, n - 5) let half = Math.floor(n / 2) // TODO: let player choose (but why would he ever choose the french?) let lose_american = Math.min(half, n_american) log(owner[0] + " lost " + lose_american + " American CU in " + space) remove_american_cu(space, n_american) half -= lose_american n_american -= lose_american if (half > 0) { log(owner[0] + " lost " + half + " French CU in " + space) remove_french_cu(space, half) } } } if (automatic_victory()) return goto_french_naval_phase() } /* FRENCH NAVAL PHASE */ function goto_french_naval_phase() { if (game.french_navy !== -1) { game.save = game.active 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 ] } states.place_french_navy_trigger = { prompt() { view.prompt = "Place the French Navy in a blockade zone." gen_place_french_navy() }, sea(zone) { logp("placed French Navy.") game.french_navy = zone goto_place_rochambeau() }, } function can_place_rochambeau() { for (let space in SPACES) if (is_port(space)) 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 { move_general(ROCHAMBEAU, AMERICAN_REINFORCEMENTS) move_cu(FRENCH, FRENCH_REINFORCEMENTS, AMERICAN_REINFORCEMENTS, 5) end_place_rochambeau() } } states.place_rochambeau = { prompt() { view.prompt = "Place Rochambeau in a port." let can_place = false for (let space in SPACES) { if (is_port(space)) { if (!has_british_cu(space) && !has_british_pc(space)) { gen_action_space(space) can_place = true } } } }, space(space) { logp("placed Rochambeau .") move_general(ROCHAMBEAU, space) move_cu(FRENCH, FRENCH_REINFORCEMENTS, space, 5) end_place_rochambeau() }, } function end_place_rochambeau() { game.active = game.save delete game.save next_strategy_card() } states.place_french_navy = { prompt() { view.prompt = "Place the French Navy in a blockade zone." gen_place_french_navy() }, sea(zone) { logp("placed French Navy.") game.french_navy = zone goto_political_control_phase() }, } /* POLITICAL CONTROL PHASE: RETURN CONGRESS */ function goto_political_control_phase() { if (game.congress === CONTINENTAL_CONGRESS_DISPERSED) { game.active = P_AMERICA game.state = "return_continental_congress" } else { goto_political_control_phase_2() } } function gen_place_continental_congress() { let n = 0 for (let space of all_spaces) { if (SPACES[space].colony !== "CA") { if (has_american_pc(space) && has_no_british_playing_piece(space)) { gen_action("place_continental_congress", space) ++n } } } return n } states.return_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 }, place_continental_congress(where) { game.congress = where goto_political_control_phase_2() }, pass() { goto_political_control_phase_2() }, } /* POLITICAL CONTROL PHASE: PLACE PC MARKERS */ function goto_political_control_phase_2() { // TODO: manually place and remove place_pc_markers_segment() remove_isolated_american_pc_segment() remove_isolated_british_pc_segment() goto_end_phase() } function place_pc_markers_segment() { for (let space of all_spaces) { if (has_american_army(space)) { if (has_no_pc(space)) place_american_pc(space) else if (has_british_pc(space)) flip_pc(space) } if (has_british_army(space)) { if (has_no_pc(space)) place_british_pc(space) else if (has_american_pc(space)) flip_pc(space) } } } /* POLITICAL CONTROL PHASE: REMOVE ISOLATED PC MARKERS */ 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 remove_isolated_american_pc_segment() { log("Removed isolated American PC") let seen = [] for (let space of all_spaces) { if (is_american_pc_root(space)) { set_add(seen, space) spread_american_path(seen, space) } } for (let space of all_spaces) if (has_american_pc(space) && !set_has(seen, space)) remove_pc(space) } function remove_isolated_british_pc_segment() { log("Removed isolated British PC") let seen = [] for (let space of all_spaces) { if (is_british_pc_root(space)) { set_add(seen, space) spread_british_path(seen, space) } } for (let space of all_spaces) if (has_british_pc(space) && !set_has(seen, space)) remove_pc(space) } /* END PHASE */ function goto_end_phase() { if (has_flag(F_FRENCH_ALLIANCE_TRIGGERED) && !has_flag(F_EUROPEAN_WAR)) { set_flag(F_EUROPEAN_WAR) game.count = 2 game.active = P_AMERICA 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 = { prompt() { view.prompt = "European War: Remove 2 British CU from any spaces. " + game.count + " left." view.actions.pass = 1 gen_remove_british_cu() }, remove_cu(where) { remove_british_cu(where, 1) if (--game.count === 0) goto_end_phase() }, pass() { 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 !== "CA") 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.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.victory) return true } return false } function norths_government_falls() { let n_american = 0 for (let c = 0; c <= 13; ++c) if (get_colony_control(c) === PC_AMERICAN) ++n_american if (n_american >= 7) game.result = P_AMERICA else game.result = P_BRITAIN game.victory = "North's Government Falls: " + game.result + " 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_sea(s) { gen_action("sea", s) } 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) } exports.scenarios = [ "Standard" ] exports.roles = [ P_BRITAIN, P_AMERICA ] exports.setup = function (seed, scenario, options) { setup_game(seed) return game } exports.action = function (state, current, action, arg) { game = state //Object.seal(game) // don't allow adding properties! // TODO: check against action list if (current === game.active) { let S = states[game.state] if (action in S) { S[action](arg) } else { if (action === "undo" && game.undo && game.undo.length > 0) pop_undo() else throw new Error("Invalid action: " + action) } } return game } exports.view = function (state, current) { 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.last_played, move: game.move } if (current === P_AMERICA) view.hand = game.a_hand else if (current === P_BRITAIN) view.hand = game.b_hand else view.hand = [] if (current === game.active) { view.actions = {} states[game.state].prompt() if (game.undo && game.undo.length > 0) view.actions.undo = 1 else view.actions.undo = 0 } 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 logp(s) { game.log.push(game.active + " " + s) } function logbr() { if (game.log.length > 0 && game.log[game.log.length - 1] !== "") game.log.push("") } function clear_undo() { if (game.undo) { game.undo.length = 0 } } function push_undo() { if (game.undo) { let copy = {} for (let k in game) { let v = game[k] if (k === "undo") continue else if (k === "log") v = v.length else if (typeof v === "object" && v !== null) v = object_copy(v) copy[k] = v } game.undo.push(copy) } } function pop_undo() { if (game.undo) { let save_log = game.log let save_undo = game.undo game = save_undo.pop() save_log.length = game.log game.log = save_log game.undo = save_undo } } function random(range) { // An MLCG using integer arithmetic with doubles. // https://www.ams.org/journals/mcom/1999-68-225/S0025-5718-99-00996-5/S0025-5718-99-00996-5.pdf // m = 2**35 − 31 return (game.seed = game.seed * 200105 % 34359738337) % range } function random_bigint(range) { // Largest MLCG that will fit its state in a double. // Uses BigInt for arithmetic, so is an order of magnitude slower. // https://www.ams.org/journals/mcom/1999-68-225/S0025-5718-99-00996-5/S0025-5718-99-00996-5.pdf // m = 2**53 - 111 return (game.seed = Number(BigInt(game.seed) * 5667072534355537n % 9007199254740881n)) % range } function shuffle(list) { // Fisher-Yates shuffle for (let i = list.length - 1; i > 0; --i) { let j = random(i + 1) let tmp = list[j] list[j] = list[i] list[i] = tmp } } function shuffle_bigint(list) { // Fisher-Yates shuffle for (let i = list.length - 1; i > 0; --i) { let j = random_bigint(i + 1) let tmp = list[j] list[j] = list[i] list[i] = tmp } } // Fast deep copy for objects without cycles function object_copy(original) { if (Array.isArray(original)) { let n = original.length let copy = new Array(n) for (let i = 0; i < n; ++i) { let v = original[i] if (typeof v === "object" && v !== null) copy[i] = object_copy(v) else copy[i] = v } return copy } else { let copy = {} for (let i in original) { let v = original[i] if (typeof v === "object" && v !== null) copy[i] = object_copy(v) else copy[i] = v } return copy } } // Array remove and insert (faster than splice) function array_remove(array, index) { let n = array.length for (let i = index + 1; i < n; ++i) array[i - 1] = array[i] array.length = n - 1 } function array_remove_item(array, item) { let n = array.length for (let i = 0; i < n; ++i) if (array[i] === item) return array_remove(array, i) } function array_insert(array, index, item) { for (let i = array.length; i > index; --i) array[i] = array[i - 1] array[index] = item } function array_remove_pair(array, index) { let n = array.length for (let i = index + 2; i < n; ++i) array[i - 2] = array[i] array.length = n - 2 } function array_insert_pair(array, index, key, value) { for (let i = array.length; i > index; i -= 2) { array[i] = array[i-2] array[i+1] = array[i-1] } array[index] = key array[index+1] = value } // Set as plain sorted array function set_clear(set) { set.length = 0 } function set_has(set, item) { let a = 0 let b = set.length - 1 while (a <= b) { let m = (a + b) >> 1 let x = set[m] if (item < x) b = m - 1 else if (item > x) a = m + 1 else return true } return false } function set_add(set, item) { let a = 0 let b = set.length - 1 while (a <= b) { let m = (a + b) >> 1 let x = set[m] if (item < x) b = m - 1 else if (item > x) a = m + 1 else return } array_insert(set, a, item) } function set_delete(set, item) { let a = 0 let b = set.length - 1 while (a <= b) { let m = (a + b) >> 1 let x = set[m] if (item < x) b = m - 1 else if (item > x) a = m + 1 else { array_remove(set, m) return } } } function set_toggle(set, item) { let a = 0 let b = set.length - 1 while (a <= b) { let m = (a + b) >> 1 let x = set[m] if (item < x) b = m - 1 else if (item > x) a = m + 1 else { array_remove(set, m) return } } array_insert(set, a, item) } // Map as plain sorted array of key/value pairs function map_clear(map) { map.length = 0 } function map_has(map, key) { let a = 0 let b = (map.length >> 1) - 1 while (a <= b) { let m = (a + b) >> 1 let x = map[m<<1] if (key < x) b = m - 1 else if (key > x) a = m + 1 else return true } return false } function map_get(map, key, missing) { let a = 0 let b = (map.length >> 1) - 1 while (a <= b) { let m = (a + b) >> 1 let x = map[m<<1] if (key < x) b = m - 1 else if (key > x) a = m + 1 else return map[(m<<1)+1] } return missing } function map_set(map, key, value) { let a = 0 let b = (map.length >> 1) - 1 while (a <= b) { let m = (a + b) >> 1 let x = map[m<<1] if (key < x) b = m - 1 else if (key > x) a = m + 1 else { map[(m<<1)+1] = value return } } array_insert_pair(map, a<<1, key, value) } function map_delete(map, key) { let a = 0 let b = (map.length >> 1) - 1 while (a <= b) { let m = (a + b) >> 1 let x = map[m<<1] if (key < x) b = m - 1 else if (key > x) a = m + 1 else { array_remove_pair(map, m<<1) return } } }