"use strict" // TODO: don't allow activating force in Halifax/captured Louisbourg without a 3-value card (nothing to do but sail) // WONTFIX // TODO: select leader for defense instead of automatically picking the best // TODO: remove old 7 command leader(s) immediately as they're drawn, before placing reinforcements const { spaces, pieces, cards } = require("./data") const BRITAIN = 'Britain' const FRANCE = 'France' // CARDS const first_amphib_card = 17 const last_amphib_card = 20 const SURRENDER = 6 const MASSACRE = 7 const COEHORNS = 8 const FIELDWORKS_1 = 9 const FIELDWORKS_2 = 10 const AMBUSH_1 = 11 const AMBUSH_2 = 12 const BLOCKHOUSES = 13 const FOUL_WEATHER = 14 const LAKE_SCHOONER = 15 const GEORGE_CROGHAN = 16 const LOUISBOURG_SQUADRONS = 21 const WILLIAM_PITT = 67 const DIPLOMATIC_REVOLUTION = 69 // PIECE RANGES const first_piece = 1 const last_piece = 151 const first_british_piece = 1 const last_british_piece = 86 const first_british_leader = 1 const last_british_leader = 13 const first_british_unit = 14 const last_british_unit = 86 const first_french_piece = 87 const last_french_piece = 151 const first_french_leader = 87 const last_french_leader = 96 const first_french_unit = 97 const last_french_unit = 151 const first_british_militia = 83 const last_british_militia = 86 const first_french_militia = 148 const last_french_militia = 151 const first_french_regular = 134 const last_french_regular = 147 const first_coureurs = 119 const last_coureurs = 126 const first_british_regular = 56 const last_british_regular = 72 const first_highland = 77 const last_highland = 82 const first_royal_american = 73 const last_royal_american = 76 const first_light_infantry = 26 const last_light_infantry = 31 const first_southern_provincial = 50 const last_southern_provincial = 55 const first_northern_provincial = 32 const last_northern_provincial = 49 const first_ranger = 23 const last_ranger = 25 const first_cherokee = 14 const last_cherokee = 15 const first_mohawk = 21 const last_mohawk = 22 const first_orange_indian = 113 const last_orange_indian = 118 function is_leader(p) { return (p >= 1 && p <= 13) || (p >= 87 && p <= 96) } function is_unit(p) { return (p >= 14 && p <= 86) || (p >= 97 && p <= 151) } function is_auxiliary(p) { return (p >= 14 && p <= 25) || (p >= 97 && p <= 126) } function is_drilled_troops(p) { return (p >= 26 && p <= 82) || (p >= 127 && p <= 147) } function is_militia(p) { return (p >= 83 && p <= 86) || (p >= 148 && p <= 151) } function is_regular(p) { return (p >= 56 && p <= 82) || (p >= 127 && p <= 147) } function is_light_infantry(p) { return (p >= 26 && p <= 31) } function is_provincial(p) { return (p >= 32 && p <= 55) } function is_southern_provincial(p) { return (p >= 50 && p <= 55) } function is_northern_provincial(p) { return (p >= 32 && p <= 49) } function is_marine_detachment(p) { return (p >= 127 && p <= 133) } function is_coureurs(p) { return (p >= 119 && p <= 126) } function is_ranger(p) { return (p >= 23 && p <= 25) } function is_indian(p) { return (p >= 14 && p <= 22) || (p >= 97 && p <= 118) } function is_french_indian(p) { return (p >= 97 && p <= 118) } function is_british_indian(p) { return (p >= 14 && p <= 22) } function is_blue_indian(p) { return (p >= 101 && p <= 107) } function is_orange_indian(p) { return (p >= 113 && p <= 118) } function is_blue_orange_indian(p) { return (p >= 97 && p <= 100) } function is_gray_indian(p) { return (p >= 16 && p <= 20) || (p >= 108 && p <= 112) } function is_cherokee(p) { return (p >= 14 && p <= 15) } function is_mohawk(p) { return (p >= 21 && p <= 22) } function is_british_iroquois_or_mohawk(p) { return (p >= 16 && p <= 22) } const AMHERST = 1 const BRADDOCK = 2 const ABERCROMBY = 3 const LOUDOUN = 4 const WOLFE = 5 const FORBES = 6 const SHIRLEY = 7 const MURRAY = 8 const MONCKTON = 9 const WEBB = 10 const BRADSTREET = 11 const DUNBAR = 12 const JOHNSON = 13 const MONTCALM = 87 const DIESKAU = 88 const LEVIS = 89 const VAUDREUIL = 90 const DRUCOUR = 91 const RIGAUD = 92 const VILLIERS = 93 const BOUGAINVILLE = 94 const BEAUJEU = 95 const DUMAS = 96 // SPACE RANGES const first_space = 1 const last_space = 141 const first_leader_box = 145 const last_leader_box = 167 const first_northern_department = 1 const last_northern_department = 21 const first_southern_department = 22 const last_southern_department = 40 const first_st_lawrence_department = 41 const last_st_lawrence_department = 52 function is_leader_box(s) { return (s >= 145 && s <= 167) } function is_fortress(s) { return (s >= 1 && s <= 4) || (s >= 22 && s <= 24) || (s >= 41 && s <= 42) || (s >= 139 && s <= 140) } function is_port(s) { return (s >= 1 && s <= 3) || (s >= 22 && s <= 24) || (s === 41) || (s >= 139 && s <= 140) } function is_st_lawrence_department(s) { return (s >= 41 && s <= 52) } function is_southern_department(s) { return (s >= 22 && s <= 40) } function is_northern_department(s) { return (s >= 1 && s <= 21) } function is_originally_french(s) { return (s >= 41 && s <= 52) || (s === 140) } function is_originally_british(s) { return (s >= 1 && s <= 40) || (s === 139) } function is_wilderness_or_mountain(s) { return (s >= 53 && s <= 138) } function is_wilderness(s) { return (s >= 53 && s <= 119) } function is_mountain(s) { return (s >= 120 && s <= 138) } function is_cultivated(s) { return (s >= 1 && s <= 52) } const ALBANY = 4 const ALEXANDRIA = 22 const BAIE_ST_PAUL = 43 const BALTIMORE = 23 const BOSTON = 1 const CANAJOHARIE = 56 const CAYUGA = 60 const HALIFAX = 139 const KAHNAWAKE = 45 const KARAGHIYADIRHA = 76 const KITTANING = 77 const LAC_DES_DEUX_MONTAGNES = 46 const LOGSTOWN = 81 const LOUISBOURG = 140 const MINGO_TOWN = 83 const MISSISSAUGA = 84 const MONTREAL = 42 const NEW_HAVEN = 2 const NEW_YORK = 3 const NIAGARA = 86 const OHIO_FORKS = 88 const ONEIDA_CARRY_EAST = 89 const ONEIDA_CARRY_WEST = 90 const ONEIDA_CASTLE = 91 const ONONDAGA = 92 const OSWEGO = 96 const PAYS_D_EN_HAUT = 141 const PHILADELPHIA = 24 const QUEBEC = 41 const RIVIERE_OUELLE = 47 const SHAWIANGTO = 102 const ST_FRANCOIS = 49 const ILE_D_ORLEANS = 52 const NORTHERN_COLONIAL_MILITIAS = 142 const SOUTHERN_COLONIAL_MILITIAS = 143 const ST_LAWRENCE_CANADIAN_MILITIAS = 144 // Patch up leader/box associations. const box_from_leader = [] const leader_from_box = [] for (let p = 0; p <= last_piece; ++p) box_from_leader[p] = 0 for (let s = first_leader_box; s <= last_leader_box; ++s) { let p = pieces.findIndex(piece => piece.name === spaces[s].name) box_from_leader[p] = s leader_from_box[s-first_leader_box] = p } // Patch up space exits. for (let s = first_space; s <= last_space; ++s) { let ss = spaces[s] ss.exits = ss.land.concat(ss.river).concat(ss.lakeshore) ss.exits_with_type = [] ss.land.forEach(n => ss.exits_with_type.push([n,'land'])) ss.river.forEach(n => ss.exits_with_type.push([n,'river'])) ss.lakeshore.forEach(n => ss.exits_with_type.push([n,'lakeshore'])) } // Make non-breaking names. spaces.forEach(ss => ss.nb_name = ss.name.replace(/ /g, '\xa0')) pieces.forEach(pp => { if (pp.desc) pp.nb_desc = pp.desc.replace(/ /g, '\xa0') if (pp.rdesc) pp.nb_rdesc = pp.rdesc.replace(/ /g, '\xa0') }) let game let view = null let states = {} let events = {} let player // aliased to game.french/british per-player state let enemy_player // aliased to game.french/british per-player state let supply_cache // cleared when setting active player and loading game state // These looping indices are updated with update_active_aliases() let first_enemy_leader let first_enemy_piece let first_enemy_unit let last_enemy_leader let last_enemy_piece let last_enemy_unit let first_friendly_leader let first_friendly_piece let first_friendly_unit let last_friendly_leader let last_friendly_piece let last_friendly_unit function abs(x) { return x < 0 ? -x : x } function random(n) { clear_undo() if (game.rng === 1) return ((game.seed = game.seed * 69621 % 0x7fffffff) / 0x7fffffff) * n | 0 return (game.seed = game.seed * 200105 % 34359738337) % n } function roll_die(reason) { let die = random(6) + 1 if (reason) log(`Rolled ${die} ${reason}.`) else log(`Rolled ${die}.`) return die } function modify(die, drm, why) { if (drm >= 0) log(`+${drm} ${why}.`) else if (drm < 0) log(`${drm} ${why}.`) return die + drm } function clamp(x, min, max) { return Math.min(Math.max(x, min), max) } function remove_from_array(array, item) { let i = array.indexOf(item) if (i >= 0) array_remove(array, i) } function logbr() { if (game.log.length > 0 && game.log[game.log.length-1] !== "") game.log.push("") } function log(msg) { game.log.push(msg) } function push_summary(summary, p) { let s = piece_space(p) if (!(s in summary)) summary[s] = [] summary[s].push(piece_name(p)) } function print_plain_summary(verb, list) { if (game.summary) { if (game.summary[list].length > 0) log(verb + "\n" + game.summary[list].sort((a,b)=>a-b).map(piece_name).join(",\n") + ".") delete game.summary[list] } } function print_summary(summary, verb) { for (let s in summary) log(verb + "%" + s + "\n" + summary[s].join(",\n") + ".") } function flush_summary() { if (game.summary) { print_summary(game.summary.placed, "Placed at ") print_summary(game.summary.restored, "Restored at ") print_summary(game.summary.reduced, "Reduced at ") print_summary(game.summary.eliminated, "Eliminated at ") game.summary.placed = {} game.summary.restored = {} game.summary.reduced = {} game.summary.eliminated = {} } } function init_retreat_summary() { if (game.summary) game.summary.retreat = {} } function push_retreat_summary(p, s) { if (game.summary) { if (!(s in game.summary.retreat)) game.summary.retreat[s] = [] game.summary.retreat[s].push(p) } else { // log(piece_name(p) + " retreated " + s + ".") log(piece_name(p) + " " + s + ".") } } function flush_retreat_summary() { if (game.summary) { for (let s in game.summary.retreat) log("Retreated " + s + "\n" + game.summary.retreat[s].map(piece_name).join(",\n") + ".") delete game.summary.retreat } } function init_go_home_summary() { if (game.summary) game.summary.go_home = {} } function push_go_home_summary(p, s) { if (game.summary) { if (!(s in game.summary.go_home)) game.summary.go_home[s] = [] game.summary.go_home[s].push(log_piece_name_and_place(p)) } else { // log(log_piece_name_and_place(p) + " went home to %" + s + ".") log(log_piece_name_and_place(p) + " home to %" + s + ".") } } function flush_go_home_summary() { if (game.summary) { print_summary(game.summary.go_home, "Went home to ") delete game.summary.go_home } } function enemy() { return game.active === FRANCE ? BRITAIN : FRANCE } function set_active_enemy() { clear_undo() game.active = (game.active === FRANCE) ? BRITAIN : FRANCE update_active_aliases() } function set_active(new_active) { if (new_active !== game.active) clear_undo() game.active = new_active update_active_aliases() } function update_active_aliases() { supply_cache = null if (game.active === BRITAIN) { player = game.british enemy_player = game.french first_friendly_piece = first_british_piece last_friendly_piece = last_british_piece first_friendly_leader = first_british_leader last_friendly_leader = last_british_leader first_friendly_unit = first_british_unit last_friendly_unit = last_british_unit first_enemy_piece = first_french_piece last_enemy_piece = last_french_piece first_enemy_leader = first_french_leader last_enemy_leader = last_french_leader first_enemy_unit = first_french_unit last_enemy_unit = last_french_unit } else { player = game.french enemy_player = game.british first_friendly_piece = first_french_piece last_friendly_piece = last_french_piece first_friendly_leader = first_french_leader last_friendly_leader = last_french_leader first_friendly_unit = first_french_unit last_friendly_unit = last_french_unit first_enemy_piece = first_british_piece last_enemy_piece = last_british_piece first_enemy_leader = first_british_leader last_enemy_leader = last_british_leader first_enemy_unit = first_british_unit last_enemy_unit = last_british_unit } } // LISTS const EARLY = 0 const LATE = 1 const RELUCTANT = 0 const SUPPORTIVE = 1 const ENTHUSIASTIC = 2 function find_space(name) { if (name === 'eliminated') return 0 let ix = spaces.findIndex(node => node.name === name) if (ix < 0) throw new Error("cannot find space " + name) return ix } function find_unused_piece(name) { for (let i = 0; i <= last_piece; ++i) if (pieces[i].name === name && game.location[i] === 0) return i throw new Error("cannot find unit " + name) } function find_unused_provincial(dept) { if (dept === 'northern') { for (let p = first_northern_provincial; p <= last_northern_provincial; ++p) if (is_piece_unused(p)) return p } else { for (let p = first_southern_provincial; p <= last_southern_provincial; ++p) if (is_piece_unused(p)) return p } return 0 } function find_unused_friendly_militia() { if (game.active === FRANCE) { for (let p = first_french_militia; p <= last_french_militia; ++p) if (is_piece_unused(p)) return p } else { for (let p = first_british_militia; p <= last_british_militia; ++p) if (is_piece_unused(p)) return p } return 0 } function find_unused_coureurs() { for (let p = first_coureurs; p <= last_coureurs; ++p) if (is_piece_unused(p)) return p return 0 } function find_unused_ranger() { for (let p = first_ranger; p <= last_ranger; ++p) if (is_piece_unused(p)) return p return 0 } function find_unused_british_regular() { for (let p = first_british_regular; p <= last_british_regular; ++p) if (is_piece_unused(p)) return p return 0 } function find_unused_highland() { for (let p = first_highland; p <= last_highland; ++p) if (is_piece_unused(p)) return p return 0 } function find_unused_royal_american() { for (let p = first_royal_american; p <= last_royal_american; ++p) if (is_piece_unused(p)) return p return 0 } function find_unused_light_infantry() { for (let p = first_light_infantry; p <= last_light_infantry; ++p) if (is_piece_unused(p)) return p return 0 } const ports = [ ALEXANDRIA, BALTIMORE, BOSTON, HALIFAX, LOUISBOURG, NEW_HAVEN, NEW_YORK, PHILADELPHIA, QUEBEC, ] const fortresses = [ ALBANY, ALEXANDRIA, BALTIMORE, BOSTON, HALIFAX, LOUISBOURG, MONTREAL, NEW_HAVEN, NEW_YORK, PHILADELPHIA, QUEBEC, ] const originally_french_fortresses = [ LOUISBOURG, MONTREAL, QUEBEC, ] originally_french_fortresses.sort((a,b)=>a-b) const originally_british_fortresses = [ ALBANY, ALEXANDRIA, BALTIMORE, BOSTON, HALIFAX, NEW_HAVEN, NEW_YORK, PHILADELPHIA, ] originally_british_fortresses.sort((a,b)=>a-b) const originally_british_fortresses_and_all_ports = [ ALBANY, ALEXANDRIA, BALTIMORE, BOSTON, HALIFAX, LOUISBOURG, NEW_HAVEN, NEW_YORK, PHILADELPHIA, QUEBEC, ] function is_friendly_indian(p) { if (game.active === FRANCE) return is_french_indian(p) return is_british_indian(p) } const indians = { spaces_from_color: {}, pieces_from_color: {}, pieces_from_space: {}, space_from_piece: {}, tribe_from_space: {}, } function define_indian(color, space, tribe) { if (!indians.pieces_from_color[color]) indians.pieces_from_color[color] = [] if (!indians.spaces_from_color[color]) indians.spaces_from_color[color] = [] if (space) set_add(indians.spaces_from_color[color], space) if (!indians.pieces_from_space[space]) indians.pieces_from_space[space] = [] if (space === PAYS_D_EN_HAUT) indians.tribe_from_space[space] = "Pays d'en Haut" else indians.tribe_from_space[space] = tribe for (let p = 1; p <= last_piece; ++p) { if (is_indian(p) && pieces[p].name === tribe) { set_add(indians.pieces_from_color[color], p) set_add(indians.pieces_from_space[space], p) indians.space_from_piece[p] = space } } } define_indian("cherokee", 0, "Cherokee") define_indian("mohawk", CANAJOHARIE, "Mohawk") define_indian("blue", ST_FRANCOIS, "Abenaki") define_indian("blue", LAC_DES_DEUX_MONTAGNES, "Algonquin") define_indian("blue", KAHNAWAKE, "Caughnawaga") define_indian("blue", MISSISSAUGA, "Mississauga") define_indian("orange", KITTANING, "Delaware") define_indian("orange", MINGO_TOWN, "Mingo") define_indian("orange", LOGSTOWN, "Shawnee") define_indian("blue-orange", PAYS_D_EN_HAUT, "Huron") define_indian("blue-orange", PAYS_D_EN_HAUT, "Ojibwa") define_indian("blue-orange", PAYS_D_EN_HAUT, "Ottawa") define_indian("blue-orange", PAYS_D_EN_HAUT, "Potawatomi") define_indian("gray", CAYUGA, "Cayuga") define_indian("gray", ONEIDA_CASTLE, "Oneida") define_indian("gray", ONONDAGA, "Onondaga") define_indian("gray", KARAGHIYADIRHA, "Seneca") define_indian("gray", SHAWIANGTO, "Tuscarora") const within_two_of_canajoharie = [ CANAJOHARIE ] for_each_exit(CANAJOHARIE, one => { set_add(within_two_of_canajoharie, one) for_each_exit(one, two => { set_add(within_two_of_canajoharie, two) }) }) const within_two_of_gray_settlement = [] indians.spaces_from_color.gray.forEach(zero => { set_add(within_two_of_gray_settlement, zero) }) indians.spaces_from_color.gray.forEach(zero => { for_each_exit(zero, one => { set_add(within_two_of_gray_settlement, one) for_each_exit(one, two => { set_add(within_two_of_gray_settlement, two) }) }) }) const in_or_adjacent_to_ohio_forks = [ OHIO_FORKS ] for_each_exit(OHIO_FORKS, one => { set_add(in_or_adjacent_to_ohio_forks, one) }) // CARD DECK function reshuffle_deck() { game.last_card = 0 game.log.push("Deck reshuffled.") game.deck = game.deck.concat(game.discard) game.discard = [] } function deal_card() { if (game.deck.length === 0) reshuffle_deck() let i = random(game.deck.length) let c = game.deck[i] game.deck.splice(i, 1) return c } function deal_cards() { let fn = 8 if (game.events.diplo) fn = 9 if (game.events.quiberon) fn = 7 let bn = 8 if (game.events.pitt) bn = 9 if (game.discard.includes(SURRENDER)) { reshuffle_deck() } if (game.options.pitt_dip_rev) { if (game.events.pitt && !game.events.diplo && game.discard.includes(DIPLOMATIC_REVOLUTION)) { log(`France received Diplomatic Revolution from discard.`) remove_from_array(game.discard, DIPLOMATIC_REVOLUTION) game.french.hand.push(DIPLOMATIC_REVOLUTION) } if (!game.events.pitt && game.events.diplo && game.discard.includes(WILLIAM_PITT)) { log(`Britain received William Pitt from discard.`) remove_from_array(game.discard, WILLIAM_PITT) game.british.hand.push(WILLIAM_PITT) } } fn = fn - game.french.hand.length bn = bn - game.british.hand.length log("Dealt " + fn + " cards to France.") log("Dealt " + bn + " cards to Britain.") while (fn > 0 || bn > 0) { if (fn > 0) { game.french.hand.push(deal_card()) --fn } if (bn > 0) { game.british.hand.push(deal_card()) --bn } } } function draw_leader_from_pool() { if (game.british.pool.length > 0) { let i = random(game.british.pool.length) let p = game.british.pool[i] log(`Drew ${piece_name(p)} from pool.`) // 5.55 If both on-map 7 leaders are besieged, return the third to the pool without substitution. if (is_seven_command_leader(p)) { let n = 0 if (is_piece_on_map(ABERCROMBY) && is_piece_inside(ABERCROMBY)) ++n if (is_piece_on_map(AMHERST) && is_piece_inside(AMHERST)) ++n if (is_piece_on_map(BRADDOCK) && is_piece_inside(BRADDOCK)) ++n if (is_piece_on_map(LOUDOUN) && is_piece_inside(LOUDOUN)) ++n if (n >= 2) { log(`Returned ${piece_name(p)} to pool.`) return 0 } } game.british.pool.splice(i, 1) game.location[p] = box_from_leader[p] return p } return 0 } function is_card_available(c) { return !game.discard.includes(c) && !game.removed.includes(c) } function is_enemy_card_available(c) { return enemy_player.hand.length > 0 && is_card_available(c) } function is_friendly_card_available(c) { return player.hand.length > 0 && is_card_available(c) } function get_player_hand(role) { if (role === FRANCE) return game.french.hand return game.british.hand } function is_card_available_for_attacker(c) { return get_player_hand(game.battle.attacker).length > 0 && is_card_available(c) } function is_card_available_for_defender(c) { return get_player_hand(game.battle.defender).length > 0 && is_card_available(c) } // ITERATORS function for_each_siege(fn) { for (let sid in game.sieges) fn(sid|0, game.sieges[sid]) } function for_each_exit_with_type(s, fn) { for (let [n, t] of spaces[s].exits_with_type) fn(n, t) } function for_each_exit(s, fn) { for (let n of spaces[s].exits) fn(n) } function for_each_friendly_piece_in_node(node, fn) { for (let p = first_friendly_piece; p <= last_friendly_piece; ++p) { if (is_piece_in_node(p, node)) fn(p) } } function for_each_unbesieged_friendly_piece_in_space(s, fn) { for (let p = first_friendly_piece; p <= last_friendly_piece; ++p) { if (is_piece_unbesieged_in_space(p, s)) fn(p) } } function for_each_friendly_leader_in_node(node, fn) { for (let p = first_friendly_leader; p <= last_friendly_leader; ++p) { if (is_piece_in_node(p, node)) fn(p) } } function for_each_friendly_unit_in_node(node, fn) { for (let p = first_friendly_unit; p <= last_friendly_unit; ++p) { if (is_piece_in_node(p, node)) fn(p) } } function for_each_friendly_piece_in_space(s, fn) { for (let p = first_friendly_piece; p <= last_friendly_piece; ++p) { if (is_piece_in_space(p, s)) fn(p) } } function for_each_friendly_leader_in_space(s, fn) { for (let p = first_friendly_leader; p <= last_friendly_leader; ++p) { if (is_piece_in_space(p, s)) fn(p) } } function for_each_unit_in_space(s, fn) { for (let p = first_french_unit; p <= last_french_unit; ++p) if (is_piece_in_space(p, s)) fn(p) for (let p = first_british_unit; p <= last_british_unit; ++p) if (is_piece_in_space(p, s)) fn(p) } function for_each_friendly_unit_in_space(s, fn) { for (let p = first_friendly_unit; p <= last_friendly_unit; ++p) { if (is_piece_in_space(p, s)) fn(p) } } function for_each_unbesieged_enemy_in_space(s, fn) { for (let p = first_enemy_unit; p <= last_enemy_unit; ++p) { if (is_piece_unbesieged_in_space(p, s)) fn(p) } } function for_each_piece_in_force(force, fn) { for (let p = 1; p <= last_piece; ++p) if (is_piece_in_force(p, force)) fn(p) } function for_each_leader_in_force(force, fn) { for (let p = 1; p <= last_piece; ++p) if (is_leader(p) && is_piece_in_force(p, force)) fn(p) } function for_each_unit_in_force(force, fn) { for (let p = 1; p <= last_piece; ++p) if (!is_leader(p) && is_piece_in_force(p, force)) fn(p) } function for_each_british_controlled_port(fn) { for (let i = 0; i < ports.length; ++i) if (is_british_controlled_space(ports[i])) fn(ports[i]) } function for_each_british_controlled_port_and_amphib(fn) { for (let i = 0; i < ports.length; ++i) if (is_british_controlled_space(ports[i])) fn(ports[i]) game.amphib.forEach(fn) } function list_auxiliary_units_in_force(force) { let list = [] for_each_unit_in_force(force, p => { if (is_auxiliary(p)) list.push(p) }) return list } // STATIC PROPERTIES function department_militia(s) { if (is_st_lawrence_department(s)) return ST_LAWRENCE_CANADIAN_MILITIAS if (is_northern_department(s)) return NORTHERN_COLONIAL_MILITIAS if (is_southern_department(s)) return SOUTHERN_COLONIAL_MILITIAS return 0 } function space_name(s) { return spaces[s].nb_name } function is_lake_connection(from, to) { let exits = spaces[from].lakeshore for (let i = 0; i < exits.length; ++i) if (exits[i] === to) return true return false } function has_amphibious_arrow(s) { return s === HALIFAX || s === LOUISBOURG } function is_originally_friendly(s) { if (game.active === FRANCE) return is_originally_french(s) return is_originally_british(s) } function is_originally_enemy(s) { if (game.active === BRITAIN) return is_originally_french(s) return is_originally_british(s) } function piece_name(p) { if (is_unit_reduced(p)) return pieces[p].nb_rdesc return pieces[p].nb_desc } function log_piece_name_and_place(p) { // return piece_name(p) + " at %" + piece_space(p) return piece_name(p) + " (%" + piece_space(p) + ")" } function piece_name_and_place(p) { // return piece_name(p) + " at %" + space_name(piece_space(p)) return piece_name(p) + " (" + space_name(piece_space(p)) + ")" } function piece_movement(p) { return pieces[p].movement } function leader_box(p) { return box_from_leader[p] } function leader_initiative(p) { return pieces[p].initiative } function leader_command(p) { return pieces[p].command } function force_command(force) { let n = 0 for_each_leader_in_force(force, p => { n += leader_command(p) }) return n } function leader_tactics(p) { return pieces[p].tactics } // DYNAMIC PROPERTIES function piece_node(p) { return abs(game.location[p]) } function piece_space_and_inside(p) { let where = abs(game.location[p]) if (is_leader_box(where)) return game.location[leader_from_box[where-first_leader_box]] return game.location[p] } function piece_space(p) { let where = abs(game.location[p]) if (is_leader_box(where)) return abs(game.location[leader_from_box[where-first_leader_box]]) return where } // is piece commanded by a leader (or self) function is_piece_in_force(p, force) { if (p === force) return true if (is_leader(force)) return piece_node(p) === leader_box(force) return false } function count_non_british_iroquois_and_mohawk_units_in_force(leader) { let n = 0 for_each_friendly_unit_in_node(leader_box(leader), p => { if (!is_british_iroquois_or_mohawk(p)) ++n }) return n } function count_pieces_in_force(force) { let n = 0 for_each_piece_in_force(force, () => { ++n }) return n } function count_units_in_force(force) { let n = 0 for_each_unit_in_force(force, () => { ++n }) return n } function count_friendly_units_inside(where) { let n = 0 for_each_friendly_unit_in_space(where, p => { if (is_piece_inside(p)) ++n }) return n } function count_friendly_units_in_space(where) { let n = 0 for_each_friendly_unit_in_space(where, () => { ++n }) return n } function count_unbesieged_enemy_units_in_space(where) { let n = 0 for_each_unbesieged_enemy_in_space(where, () => { ++n }) return n } function unit_strength(p) { if (is_unit_reduced(p)) return pieces[p].reduced_strength return pieces[p].strength } function is_unit_reduced(p) { return set_has(game.reduced, p) } function set_unit_reduced(p, v) { if (v) { set_add(game.reduced, p) } else { set_delete(game.reduced, p) } } function is_one_step_marine_detachment(p) { return is_marine_detachment(p) && game.options.one_step_md } function can_reduce_unit(p) { if (is_one_step_marine_detachment(p)) return false return !is_unit_reduced(p) } function is_piece_inside(p) { return game.location[p] < 0 } function is_piece_unbesieged(p) { return game.location[p] > 0 } function set_piece_inside(p) { if (game.location[p] > 0) game.location[p] = -game.location[p] } function set_piece_outside(p) { if (game.location[p] < 0) game.location[p] = -game.location[p] } function is_piece_on_map(p) { return game.location[p] !== 0 } function is_piece_unused(p) { return game.location[p] === 0 } function is_piece_in_node(p, node) { return piece_node(p) === node } function is_piece_in_space(p, s) { return piece_space(p) === s } function is_piece_unbesieged_in_space(p, s) { return piece_space_and_inside(p) === s } function is_piece_besieged_in_space(p, s) { return game.location[p] === -s } function has_amphib(s) { return set_has(game.amphib, s) } function has_friendly_amphib(s) { return game.active === BRITAIN && set_has(game.amphib, s) } function has_enemy_amphib(s) { return game.active === FRANCE && set_has(game.amphib, s) } function has_fieldworks(s) { return set_has(game.fieldworks, s) } function place_fieldworks(s) { log(`Placed fieldworks at %${s}.`) set_add(game.fieldworks, s) } function remove_fieldworks(s) { if (set_has(game.fieldworks, s)) { // log(`Fieldworks (%${s}) removed.`) log(`Removed fieldworks at %${s}.`) set_delete(game.fieldworks, s) } } function place_friendly_raided_marker(s) { log(`Placed raided marker at %${s}.`) player.raids.push(s) player.raids.sort((a,b)=>a-b) } function has_friendly_raided_marker(s) { return set_has(player.raids, s) } function has_enemy_raided_marker(s) { return set_has(enemy_player.raids, s) } function is_space_besieged(s) { return s in game.sieges } function is_space_unbesieged(s) { return !is_space_besieged(s) } function has_enemy_allied_settlement(s) { return set_has(enemy_player.allied, s) } function has_friendly_allied_settlement(s) { return set_has(player.allied, s) } function has_enemy_stockade(s) { return set_has(enemy_player.stockades, s) } function has_friendly_stockade(s) { return set_has(player.stockades, s) } function has_enemy_fortress(s) { return set_has(enemy_player.fortresses, s) } function has_friendly_fortress(s) { return set_has(player.fortresses, s) } function has_enemy_fort(s) { return set_has(enemy_player.forts, s) } function has_friendly_fort(s) { return set_has(player.forts, s) } function has_friendly_fort_uc(s) { return set_has(player.forts_uc, s) } function has_enemy_fort_or_fortress(s) { return has_enemy_fort(s) || has_enemy_fortress(s) } function has_enemy_fortifications(s) { return has_enemy_stockade(s) || has_enemy_fort(s) || has_enemy_fortress(s) } function has_friendly_fort_or_fortress(s) { return has_friendly_fort(s) || has_friendly_fortress(s) } function has_friendly_fortifications(s) { return has_friendly_stockade(s) || has_friendly_fort(s) || has_friendly_fortress(s) } function has_unbesieged_friendly_fortifications(s) { return is_space_unbesieged(s) && has_friendly_fortifications(s) } function has_unbesieged_friendly_fortress(s) { return is_space_unbesieged(s) && has_friendly_fortress(s) } function has_friendly_pieces(s) { for (let p = first_friendly_piece; p <= last_friendly_piece; ++p) if (is_piece_in_space(p, s)) return true return false } function has_friendly_units(s) { for (let p = first_friendly_unit; p <= last_friendly_unit; ++p) if (is_piece_in_space(p, s)) return true return false } function has_enemy_units(s) { for (let p = first_enemy_unit; p <= last_enemy_unit; ++p) if (is_piece_in_space(p, s)) return true return false } function has_french_units(s) { for (let p = first_french_unit; p <= last_french_unit; ++p) if (is_piece_in_space(p, s)) return true return false } function has_british_units(s) { for (let p = first_british_unit; p <= last_british_unit; ++p) if (is_piece_in_space(p, s)) return true return false } function has_french_drilled_troops(s) { for (let p = first_french_unit; p <= last_french_unit; ++p) if (is_piece_in_space(p, s)) if (is_drilled_troops(p)) return true return false } function has_british_drilled_troops(s) { for (let p = first_british_unit; p <= last_british_unit; ++p) if (is_piece_in_space(p, s)) if (is_drilled_troops(p)) return true return false } function is_french_controlled_space(s) { if (game.active === FRANCE) return is_friendly_controlled_space(s) return is_enemy_controlled_space(s) } function has_french_stockade(s) { return set_has(game.french.stockades, s) } function has_british_stockade(s) { return set_has(game.british.stockades, s) } function has_french_fort(s) { return set_has(game.french.forts, s) } function has_british_fort(s) { return set_has(game.british.forts, s) } function is_french_fortress(s) { return set_has(game.french.fortresses, s) } function is_british_fortress(s) { return set_has(game.british.fortresses, s) } function has_french_fortifications(s) { return has_french_stockade(s) || has_french_fort(s) || is_french_fortress(s) } function has_british_fortifications(s) { return has_british_stockade(s) || has_british_fort(s) || is_british_fortress(s) } function has_unbesieged_french_fortification(s) { return is_space_unbesieged(s) && has_french_fortifications(s) } function count_units_in_space(s) { let n = 0 for (let p = first_french_unit; p <= last_french_unit; ++p) if (is_piece_in_space(p, s)) ++n for (let p = first_british_unit; p <= last_british_unit; ++p) if (is_piece_in_space(p, s)) ++n return n } function has_unbesieged_friendly_leader(s) { for (let p = first_friendly_leader; p <= last_friendly_leader; ++p) if (is_piece_unbesieged_in_space(p, s)) return true return false } function has_unbesieged_enemy_leader(s) { for (let p = first_enemy_leader; p <= last_enemy_leader; ++p) if (is_piece_unbesieged_in_space(p, s)) return true return false } function has_unbesieged_enemy_units(s) { for (let p = first_enemy_unit; p <= last_enemy_unit; ++p) if (is_piece_unbesieged_in_space(p, s)) return true return false } function has_unbesieged_enemy_pieces(s) { for (let p = first_enemy_piece; p <= last_enemy_piece; ++p) if (is_piece_unbesieged_in_space(p, s)) return true return false } function has_unbesieged_enemy_units_that_did_not_intercept(s) { for (let p = first_enemy_unit; p <= last_enemy_unit; ++p) if (is_piece_unbesieged_in_space(p, s) && !did_piece_intercept(p)) return true return false } function is_friendly_controlled_space(s) { if (is_space_unbesieged(s) && !has_enemy_units(s)) { if (is_originally_enemy(s)) { if (has_friendly_units(s) || has_friendly_stockade(s) || has_friendly_fort(s)) return true if (has_friendly_amphib(s)) return true } else if (is_originally_friendly(s)) { return !has_enemy_amphib(s) } else { if (has_friendly_units(s) || has_friendly_stockade(s) || has_friendly_fort(s)) return true } } return false } function is_enemy_controlled_space(s) { if (is_space_unbesieged(s) && !has_friendly_units(s)) { if (is_originally_friendly(s)) { if (has_enemy_units(s) || has_enemy_stockade(s) || has_enemy_fort(s)) return true if (has_enemy_amphib(s)) return true } else if (is_originally_enemy(s)) { return !has_friendly_amphib(s) } else { if (has_enemy_units(s) || has_enemy_stockade(s) || has_enemy_fort(s)) return true } } return false } function is_british_controlled_space(s) { if (game.active === BRITAIN) return is_friendly_controlled_space(s) return is_enemy_controlled_space(s) } function has_friendly_supplied_drilled_troops(s) { for (let p = first_friendly_unit; p <= last_friendly_unit; ++p) if (is_drilled_troops(p) && is_piece_in_space(p, s) && is_in_supply(s)) return true return false } function has_friendly_drilled_troops(s) { for (let p = first_friendly_unit; p <= last_friendly_unit; ++p) if (is_drilled_troops(p) && is_piece_in_space(p, s)) return true return false } function has_enemy_drilled_troops(s) { for (let p = first_enemy_unit; p <= last_enemy_unit; ++p) if (is_drilled_troops(p) && is_piece_in_space(p, s)) return true return false } function has_friendly_regulars(s) { for (let p = first_friendly_unit; p <= last_friendly_unit; ++p) if (is_regular(p) && is_piece_in_space(p, s)) return true return false } function has_friendly_rangers(s) { if (game.active === BRITAIN) for (let p = first_british_unit; p <= last_british_unit; ++p) if (is_ranger(p) && is_piece_in_space(p, s)) return true return false } function has_any_indians(s) { for (let p = first_french_unit; p <= last_french_unit; ++p) if (is_indian(p) && is_piece_in_space(p, s)) return true for (let p = first_british_unit; p <= last_british_unit; ++p) if (is_indian(p) && is_piece_in_space(p, s)) return true return false } function has_friendly_indians(s) { for (let p = first_friendly_unit; p <= last_friendly_unit; ++p) if (is_indian(p) && is_piece_in_space(p, s)) return true return false } function has_unbesieged_enemy_auxiliary(s) { for (let p = first_enemy_unit; p <= last_enemy_unit; ++p) if (is_auxiliary(p) && is_piece_unbesieged_in_space(p, s)) return true return false } function has_unbesieged_enemy_fortifications(s) { return is_space_unbesieged(s) && has_enemy_fortifications(s) } function has_besieged_enemy_fortifications(s) { return is_space_besieged(s) && has_enemy_fortifications(s) } function has_unbesieged_enemy_fort_or_fortress(s) { return is_space_unbesieged(s) && has_enemy_fort_or_fortress(s) } function has_non_moving_unbesieged_friendly_units(s) { let force = moving_piece() for (let p = first_friendly_unit; p <= last_friendly_unit; ++p) { if (is_piece_unbesieged_in_space(p, s)) { if (!is_piece_in_force(p, force)) return true } } return false } function has_unbesieged_friendly_units(s) { for (let p = first_friendly_unit; p <= last_friendly_unit; ++p) if (is_piece_unbesieged_in_space(p, s)) return true return false } function has_besieged_friendly_units(s) { for (let p = first_friendly_unit; p <= last_friendly_unit; ++p) if (is_piece_besieged_in_space(p, s)) return true return false } function count_militia_in_department(box) { let n = 0 if (box === ST_LAWRENCE_CANADIAN_MILITIAS) { for (let p = first_french_militia; p <= last_french_militia; ++p) { if (piece_node(p) === box) ++n } } else { for (let p = first_british_militia; p <= last_british_militia; ++p) { if (piece_node(p) === box) ++n } } return n } function enemy_department_has_at_least_n_militia(where, n) { let box = department_militia(where) if (box) { if (game.active === BRITAIN && box === ST_LAWRENCE_CANADIAN_MILITIAS) return count_militia_in_department(box) >= n if (game.active === FRANCE && (box === NORTHERN_COLONIAL_MILITIAS || box === SOUTHERN_COLONIAL_MILITIAS)) return count_militia_in_department(box) >= n } return false } // Is a leader moving alone without a force. function is_lone_leader(who) { return is_leader(who) && count_pieces_in_force(who) === 1 } // Is a single auxiliary unit (with or without leaders) function is_lone_auxiliary(who) { if (is_leader(who)) { let only_ax = true let ax_count = 0 for_each_unit_in_force(who, p => { if (is_auxiliary(p)) ++ax_count else only_ax = false }) return only_ax && ax_count === 1 } return is_auxiliary(who) } function force_has_drilled_troops(who) { if (is_leader(who)) { let has_dt = false for_each_unit_in_force(who, p => { if (is_drilled_troops(p)) has_dt = true }) return has_dt } return is_drilled_troops(who) } function force_has_supplied_drilled_troops(who) { if (force_has_drilled_troops(who)) return is_in_supply(piece_space(who)) return false } function force_has_auxiliary(who) { if (is_leader(who)) { let has_ax = false for_each_unit_in_force(who, p => { if (is_auxiliary(p)) has_ax = true }) return has_ax } return is_auxiliary(who) } function force_has_only_auxiliary_units(who) { if (is_leader(who)) { let only_ax = true for_each_unit_in_force(who, p => { if (!is_auxiliary(p)) only_ax = false }) return only_ax } return is_auxiliary(who) } function is_raid_space(s) { if (has_friendly_fort(s) || has_enemy_fort(s)) return false if (has_friendly_fortress(s) || has_enemy_fortress(s)) return false if (has_friendly_stockade(s)) return false if (has_friendly_drilled_troops(s)) return false if (is_originally_enemy(s)) return true if (has_enemy_stockade(s)) return true if (has_enemy_allied_settlement(s)) return true return false } function movement_allowance(who) { let m = piece_movement(who) for_each_unit_in_force(who, p => { let pm = piece_movement(p) if (pm < m) m = pm }) return m } function moving_piece() { return game.move.moving } function moving_piece_space() { return game.move.where } function intercepting_piece() { return game.move.intercepting } function avoiding_piece() { return game.move.avoiding } function moving_piece_came_from() { return game.move.came_from } function find_friendly_commanding_leader_in_space(s) { let commander = 0 for (let p = first_friendly_leader; p <= last_friendly_leader; ++p) if (is_piece_in_space(p, s)) if (!commander || leader_command(p) > leader_command(commander)) commander = p return commander } function find_enemy_commanding_leader_in_space(s) { let commander = 0 for (let p = first_enemy_leader; p <= last_enemy_leader; ++p) if (is_piece_in_space(p, s)) if (!commander || leader_command(p) > leader_command(commander)) commander = p return commander } // GAME STATE CHANGE HELPERS function log_vp(n) { if (game.active === FRANCE) { if (n < 0) log(`France lost ${-n} VP.`) else log(`France gained ${n} VP.`) } else { if (n < 0) log(`Britain gained ${-n} VP.`) else log(`Britain lost ${n} VP.`) } } function award_vp(n) { if (game.active === FRANCE) { log_vp(n) game.vp += n } else { log_vp(-n) game.vp -= n } } function award_french_vp(n) { log_vp(n) game.vp += n } function award_british_vp(n) { log_vp(-n) game.vp -= n } function remove_friendly_stockade(s) { set_delete(player.stockades, s) } function remove_friendly_fort_uc(s) { set_delete(player.forts_uc, s) } function remove_friendly_fort(s) { set_delete(player.forts, s) } function remove_enemy_fort_uc(s) { set_delete(enemy_player.forts_uc, s) } function place_friendly_fort(s) { remove_friendly_stockade(s) remove_friendly_fort_uc(s) set_add(player.forts, s) } function place_friendly_fort_uc(s) { set_add(player.forts_uc, s) } // Isolate piece from any forces it may be involved in. function unstack_force(ldr) { if (is_leader(ldr)) { let box = leader_box(ldr) let s = piece_space_and_inside(ldr) for (let p = 1; p <= last_piece; ++p) { if (piece_node(p) === box) game.location[p] = s } } } function unstack_piece_from_force(p) { game.location[p] = piece_space_and_inside(p) } function restore_unit(p) { set_unit_reduced(p, 0) if (game.summary && game.summary.restored) push_summary(game.summary.restored, p) else // log(`Restored ${log_piece_name_and_place(p)}.`) log(`${log_piece_name_and_place(p)} restored.`) } function reduce_unit(p, verbose=true) { if (is_unit_reduced(p) || is_one_step_marine_detachment(p)) { eliminate_piece(p, verbose) return true } if (game.summary && game.summary.reduced) push_summary(game.summary.reduced, p) else if (verbose) // log(`Reduced ${log_piece_name_and_place(p)}.`) log(`${log_piece_name_and_place(p)} reduced.`) else // log(`Reduced ${piece_name(p)}.`) log(`${piece_name(p)} reduced.`) set_unit_reduced(p, 1) return false } function eliminate_piece(p, verbose=true) { if (game.summary && game.summary.eliminated) push_summary(game.summary.eliminated, p) else if (verbose) // log(`Eliminated ${log_piece_name_and_place(p)}.`) log(`${log_piece_name_and_place(p)} eliminated.`) else // log(`Eliminated ${piece_name(p)}.`) log(`${piece_name(p)} eliminated.`) unstack_force(p) set_unit_reduced(p, 0) game.location[p] = 0 if (is_indian(p)) { let home = indians.space_from_piece[p] if (home) { if (is_indian_tribe_eliminated(home)) { log(`Removed ${indians.tribe_from_space[home]} allied marker.`) if (is_british_indian(p)) set_delete(game.british.allied, home) else set_delete(game.french.allied, home) } } } } function eliminate_indian_tribe(s) { for (let p of indians.pieces_from_space[s]) if (is_piece_unbesieged(p)) eliminate_piece(p) } function is_indian_tribe_eliminated(s) { for (let p of indians.pieces_from_space[s]) if (is_piece_on_map(p)) return false return true } function move_piece_to(who, to) { game.location[who] = to } function is_seven_command_leader(who) { return who === ABERCROMBY || who === AMHERST || who === BRADDOCK || who === LOUDOUN } function place_piece(who, to) { game.location[who] = to if (game.summary && game.summary.placed) push_summary(game.summary.placed, who) else // log(`Placed ${log_piece_name_and_place(who)}.`) log(`${log_piece_name_and_place(who)} placed.`) // remember last placed 7-command leader(s) if (is_seven_command_leader(who)) { if (count_7_command_leaders_in_play() >= 2) { if (game.seven) game.seven.push(who) else game.seven = [ who ] } } if (is_indian(who)) { let home = indians.space_from_piece[who] if (home) { if (is_british_indian(who)) { if (!set_has(game.british.allied, home)) { log(`Placed ${indians.tribe_from_space[home]} allied marker.`) set_add(game.british.allied, home) } } else { if (!set_has(game.french.allied, home)) { log(`Placed ${indians.tribe_from_space[home]} allied marker.`) set_add(game.french.allied, home) } } } } } function capture_enemy_fortress(s) { log(`Captured fortress at %${s}.`) set_delete(enemy_player.fortresses, s) set_add(player.fortresses, s) award_vp(3) } function recapture_french_fortress(s) { log(`France recaptured fortress at %${s}.`) set_all_pieces_outside(s) set_delete(game.british.fortresses, s) set_add(game.french.fortresses, s) delete game.sieges[s] award_french_vp(3) } function recapture_british_fortress(s) { log(`Britain recaptured fortress at %${s}.`) set_all_pieces_outside(s) set_delete(game.french.fortresses, s) set_add(game.british.fortresses, s) delete game.sieges[s] award_british_vp(3) } function capture_enemy_fort_intact(s) { log(`Captured intact fort at %${s}.`) set_delete(enemy_player.forts, s) set_add(player.forts, s) award_vp(2) } function capture_enemy_fort(s) { log(`Captured fort at %${s}.`) set_delete(enemy_player.forts, s) set_add(player.forts_uc, s) award_vp(2) } function capture_enemy_stockade(s) { log(`Captured stockade at %${s}.`) set_delete(enemy_player.stockades, s) set_add(player.stockades, s) award_vp(1) } function destroy_enemy_stockade_after_battle(s) { log(`Destroyed stockade at %${s}.`) set_delete(enemy_player.stockades, s) award_vp(1) } function destroy_enemy_stockade_in_raid(s) { log(`Destroyed stockade at %${s}.`) set_delete(enemy_player.stockades, s) } function add_raid(who) { let where = piece_space(who) if (where && !game.raid.list.includes(where) && is_raid_space(where)) game.raid.list.push(where) } function is_fort_or_fortress_vacant_of_besieging_units(s) { if (has_french_fort(s) || is_french_fortress(s)) return !has_british_units(s) else return !has_french_units(s) } function set_all_pieces_outside(s) { for (let p = 1; p <= last_piece; ++p) if (is_piece_in_space(p, s)) set_piece_outside(p) } function lift_sieges_and_amphib() { // Lift sieges for_each_siege(s => { if (is_fort_or_fortress_vacant_of_besieging_units(s)) { log(`Lifted siege at %${s}.`) set_all_pieces_outside(s) delete game.sieges[s] } }) // Remove amphib for (let i = game.amphib.length-1; i >= 0; --i) { let s = game.amphib[i] if (!has_british_units(s)) { if (has_french_drilled_troops(s) || (s !== LOUISBOURG && has_unbesieged_french_fortification(s))) { log(`Removed Amphib at %${s}.`) game.amphib.splice(i, 1) } } } // Recapture abandoned enemy fortresses. for (let s of originally_french_fortresses) if (set_has(game.british.fortresses, s) && !has_british_units(s) && !has_amphib(s)) recapture_french_fortress(s) for (let s of originally_british_fortresses) if (set_has(game.french.fortresses, s) && !has_french_units(s)) recapture_british_fortress(s) // Remove forts u/c if solely occupied by enemy drilled troops for (let s of player.forts_uc) { if (has_enemy_drilled_troops(s) && !has_friendly_units(s)) { log(`Removed fort u/c at %${s}.`) remove_friendly_fort_uc(s) } } for (let s of enemy_player.forts_uc) { if (has_friendly_drilled_troops(s) && !has_enemy_units(s)) { log(`Removed fort u/c at %${s}.`) remove_enemy_fort_uc(s) } } // Check ownership of other VP locations: update_vp("niagara", NIAGARA) update_vp("ohio_forks", OHIO_FORKS) } function update_vp(name, s) { let fr = has_french_units(s) || has_french_fortifications(s) let br = has_british_units(s) || has_british_fortifications(s) if (fr && !br) { if (game[name] < 0) { log(`France captured %${s}.`) award_french_vp(1) game[name] = 1 } } else if (br && !fr) { if (game[name] > 0) { log(`Britain captured %${s}.`) award_british_vp(1) game[name] = -1 } } } // SUPPLY LINES function is_cultivated_or_fortification_or_amphib(space) { return is_cultivated(space) || has_friendly_fortifications(space) || has_friendly_amphib(space) } function search_supply_spaces_imp(queue) { let visited = new Array(spaces.length).fill(0) // console.log("======") let reached = [] for (let s of queue) { set_add(reached, s) visited[s] = 1 } while (queue.length > 0) { let here = queue.shift() // console.log("SUPPLY", space_name(here)) for_each_exit_with_type(here, (next, type) => { if (visited[next]) return if (has_unbesieged_enemy_units(next) || has_unbesieged_enemy_fortifications(next)) return // continue if (is_cultivated_or_fortification_or_amphib(here)) { // came from cultivated/fortification, // may continue by water or another fortification // may stop at anything if (type !== 'land' || is_cultivated_or_fortification_or_amphib(next)) { // console.log(" ", space_name(next), "(from cultivated, continue)") set_add(reached, next) visited[next] = 1 queue.push(next) } else { // console.log(" ", space_name(next), "(from cultivated, stop)") // allow re-visits in case we come by water a longer way set_add(reached, next) } } else { // in wilderness, arrived by water, must continue by water if (type !== 'land') { // console.log(" ", space_name(next), "(from river)") visited[next] = 1 set_add(reached, next) queue.push(next) } } }) } // console.log("====\nSUPPLY", reached.map(space_name).join("\nSUPPLY ")) return reached } function search_supply_spaces() { if (game.active === FRANCE) { let list = originally_french_fortresses.filter(is_friendly_controlled_space) supply_cache = search_supply_spaces_imp(list) } else { let list = originally_british_fortresses_and_all_ports.filter(is_friendly_controlled_space) for (let s of game.amphib) if (!list.includes(s)) list.push(s) supply_cache = search_supply_spaces_imp(list) } } function is_in_supply(from) { if (game.active === BRITAIN && has_amphib(from)) return true if (!supply_cache) search_supply_spaces() if (set_has(supply_cache, from)) return true return false } function query_supply() { let reply = {} set_active(BRITAIN) search_supply_spaces() reply.british = supply_cache set_active(FRANCE) search_supply_spaces() reply.french = supply_cache return reply } // CLOSEST PATH SEARCH function find_closest_friendly_unbesieged_fortification(start) { let queue = [] let seen = {} let stop = 1000 let result = [] queue.push([start, 0]) while (queue.length > 0) { let [ here, dist ] = queue.shift() if (dist > stop) break if (has_unbesieged_friendly_fortifications(here)) { stop = dist result.push(here) } if (dist < stop) { for_each_exit(here, next => { if (!(next in seen)) queue.push([next, dist+1]) seen[next] = 1 }) } } return result } // SEQUENCE OF PLAY function place_amherst_forbes_and_wolfe_in_pool(is_event) { if (is_event && game.year >= 1759) return log("Placed Amherst, Forbes, and Wolfe into the British leader pool.") game.british.pool.push(AMHERST) game.british.pool.push(FORBES) game.british.pool.push(WOLFE) } function start_year() { game.season = EARLY start_season() } function start_season() { switch (game.season) { case EARLY: logbr() log(`.h1 Early Season of ${game.year}`) logbr() break case LATE: logbr() log(`.h1 Late Season of ${game.year}`) logbr() break } if (game.year === 1759 && game.season === EARLY && !game.events.pitt) place_amherst_forbes_and_wolfe_in_pool(false) delete game.british.pass_fw delete game.british.pass_bh delete game.french.pass_fw delete game.french.pass_bh deal_cards() if (game.options.regulars_from_discard && game.year >= 1757) { let found = false for (let c of game.discard) { if (cards[c].event === 'british_regulars' || cards[c].event === 'highlanders') { found = true break } } if (found) { set_active(BRITAIN) game.state = 'discard_to_draw_regulars' return } } start_action_phase() } function start_action_phase() { if (game.events.quiberon) set_active(BRITAIN) else set_active(FRANCE) resume_action_phase() } function end_season() { if (game.british.hand.length > 0) game.british.held = 1 else game.british.held = 0 if (game.french.hand.length > 0) game.french.held = 1 else game.french.held = 0 delete game.events.french_regulars delete game.events.british_regulars delete player.passed delete enemy_player.passed if (game.season === EARLY) { game.season = LATE start_season() } else { end_late_season() } } function end_late_season() { logbr() log(".h2 End Late Season") logbr() delete game.events.no_amphib delete game.events.blockhouses goto_indians_and_leaders_go_home() } function resume_action_phase() { game.state = 'action_phase' game.phasing = game.active logbr() log(`.h2 ${game.active}`) logbr() } function end_action_phase() { flush_summary() lift_sieges_and_amphib() game.count = 0 if (game.british.pass_fw === 1) delete game.british.pass_fw if (game.french.pass_fw === 1) delete game.french.pass_fw if (!enemy_player.passed && enemy_player.hand.length > 0) { set_active_enemy() resume_action_phase() return } if (!player.passed && player.hand.length > 0) { resume_action_phase() return } game.phasing = null end_season() } function can_play_event(card) { let symbol = cards[card].symbol if (game.active === FRANCE && symbol === 'red') return false if (game.active === BRITAIN && symbol === 'blue') return false let event = events[cards[card].event] if (event !== undefined) { if (event.can_play) return event.can_play(card) return true } return false } function gen_card_menu(card) { if (can_play_event(card)) gen_action('play_event', card) gen_action('activate_force', card) gen_action('activate_individually', card) if (!player.did_construct) { gen_action('construct_stockades', card) gen_action('construct_forts', card) } gen_action('discard', card) } function card_name(card) { return `#${card} ${cards[card].name} [${cards[card].activation}]` } function play_card(card) { log(`${game.active} played\n${card_name(card)}.`) remove_from_array(player.hand, card) game.last_card = card if (cards[card].special === 'remove') game.removed.push(card) else game.discard.push(card) } function discard_card(card, reason) { if (reason) log(`${game.active} discarded\n${card_name(card)}\n${reason}.`) else log(`${game.active} discarded\n${card_name(card)}.`) remove_from_array(player.hand, card) game.last_card = card game.discard.push(card) } function remove_card(card) { remove_from_array(game.discard, card) game.removed.push(card) } states.action_phase = { prompt() { view.prompt = "Action Phase: Play a card." for (let i = 0; i < player.hand.length; ++i) gen_card_menu(player.hand[i]) if (player.hand.length === 1 && !player.held) gen_action_pass() }, play_event(card) { push_undo() player.did_construct = 0 logbr() play_card(card) events[cards[card].event].play(card) }, activate_force(card) { logbr() goto_activate_force(card) }, activate_individually(card) { logbr() goto_activate_individually(card) }, construct_stockades(card) { logbr() goto_construct_stockades(card) }, construct_forts(card) { logbr() goto_construct_forts(card) }, discard(card) { logbr() player.did_construct = 0 discard_card(card) end_action_phase() }, pass() { logbr() log(game.active + " passed.") player.passed = 1 end_action_phase() }, } // ACTIVATION function goto_activate_individually(card) { push_undo() player.did_construct = 0 discard_card(card, "to activate units individually") game.state = 'activate_individually' game.activation_value = 0 game.count = cards[card].activation game.activation = [] } function goto_activate_force(card) { push_undo() player.did_construct = 0 discard_card(card, "to activate a force") game.state = 'activate_force' game.activation_value = cards[card].activation } events.campaign = { play() { game.state = 'select_campaign_1' game.activation_value = 3 game.activation = [] } } states.activate_individually = { prompt() { view.prompt = `Activate units and/or leaders individually${format_remain(game.count)}.` gen_action_next() if (game.count >= 1) { for (let p = first_friendly_leader; p <= last_friendly_leader; ++p) { if (is_piece_on_map(p) && !game.activation.includes(p)) { gen_action_piece(p) } } } if (game.count > 0) { for (let p = first_friendly_unit; p <= last_friendly_unit; ++p) { if (is_piece_on_map(p) && !game.activation.includes(p)) { if (game.count >= 0.5) { if (is_indian(p)) gen_action_piece(p) } if (game.count >= 1) { if (is_ranger(p)) gen_action_piece(p) if (is_coureurs(p)) gen_action_piece(p) if (is_drilled_troops(p)) if (game.activation.length === 0) gen_action_piece(p) } } } } }, piece(p) { push_undo() game.activation.push(p) if (is_drilled_troops(p)) game.count = 0 else if (is_indian(p)) game.count -= 0.5 else game.count -= 1.0 }, next() { push_undo() goto_pick_first_move() }, } function can_activate_force(who) { let where = game.location[who] // If at Halifax or Louisbourg ... if (where === HALIFAX || where === LOUISBOURG) { // must be able to Naval move if (game.activation_value === 3 && !(game.active === FRANCE && game.events.no_fr_naval)) return true // or Siege/Assault if (can_siege_or_assault_if_activated(who, where)) return true // otherwise it's a do-nothing action return false } return true } states.activate_force = { prompt() { view.prompt = "Activate a Force." for (let p = first_friendly_leader; p <= last_friendly_leader; ++p) if (is_piece_on_map(p) && leader_initiative(p) <= game.activation_value) if (can_activate_force(p)) gen_action_piece(p) }, piece(p) { push_undo() game.force = { commander: p, reason: 'move', } game.state = 'designate_force' }, } states.select_campaign_1 = { inactive: "campaign", prompt() { view.prompt = "Campaign: Select the first leader." for (let p = first_friendly_leader; p <= last_friendly_leader; ++p) { if (is_piece_on_map(p)) if (!game.activation.includes(p)) if (can_activate_force(p)) gen_action_piece(p) } }, piece(p) { push_undo() game.force = { commander: p, reason: 'campaign_1', } game.state = 'designate_force' }, } states.select_campaign_2 = { inactive: "campaign", prompt() { view.prompt = "Campaign: Select the second leader." for (let p = first_friendly_leader; p <= last_friendly_leader; ++p) { if (is_piece_on_map(p) && !is_piece_in_force(p, game.activation[0])) if (!game.activation.includes(p)) if (can_activate_force(p)) gen_action_piece(p) } }, piece(p) { push_undo() game.force = { commander: p, reason: 'campaign_2', } game.state = 'designate_force' }, } function goto_pick_first_move() { if (game.activation.length > 1) { logbr() log("Selected\n" + game.activation.map(log_piece_name_and_place).join(",\n") + ".") game.state = 'pick_move' } else if (game.activation.length > 0) { goto_move_piece(game.activation.pop()) } else { delete game.activation_value delete game.activation end_action_phase() } } function goto_pick_next_move() { if (game.activation && game.activation.length > 0) { game.state = 'pick_move' } else { delete game.activation_value delete game.activation end_action_phase() } } states.pick_move = { prompt() { view.prompt = "Pick the next activated force, leader, or unit to move." game.activation.forEach(gen_action_piece) }, piece(p) { remove_from_array(game.activation, p) goto_move_piece(p) }, } function end_activation() { // Clear event flags delete game.events.coehorns delete game.events.ambush delete game.events.foul_weather delete game.events.george_croghan lift_sieges_and_amphib() goto_pick_next_move() } // DEFINE FORCE (for various actions) function force_has_british_iroquois_and_mohawk_units(commander) { let has_br_indians = false for_each_friendly_unit_in_node(leader_box(commander), p => { if (is_british_iroquois_or_mohawk(p)) has_br_indians = true gen_action_piece(p) }) return has_br_indians } function can_drop_off_leader(commander, subordinate) { if (subordinate === JOHNSON) if (force_has_british_iroquois_and_mohawk_units(commander)) return false return count_non_british_iroquois_and_mohawk_units_in_force(commander) <= force_command(commander) - leader_command(subordinate) } const designate_force_reason_prompt = { 'campaign_1': "for first campaign", 'campaign_2': "for second campaign", 'move': "to move", 'intercept': "to intercept", 'avoid': "to avoid battle", } states.designate_force = { inactive() { inactive_prompt("designate force " + designate_force_reason_prompt[game.force.reason], game.force.commander, 0) }, prompt() { let commander = game.force.commander let where = piece_space(commander) // 5.534 Johnson commands British Iroquois and Mohawk units for free let cmd_use = count_non_british_iroquois_and_mohawk_units_in_force(commander) let cmd_cap = force_command(commander) view.prompt = `Designate force ${designate_force_reason_prompt[game.force.reason]} with ${piece_name(commander)} from ${space_name(where)} (${cmd_use}/${cmd_cap}).` view.who = commander let can_pick_up = false // pick up sub-commanders for_each_friendly_leader_in_node(where, p => { if (game.force.reason === 'avoid' && is_piece_inside(p)) return // continue if (game.activation && game.activation.includes(p)) return // continue if (p !== commander && leader_command(p) <= leader_command(commander)) { can_pick_up = true gen_action_piece(p) } }) // pick up units for_each_friendly_unit_in_node(where, p => { if (game.force.reason === 'avoid' && (is_piece_inside(p) || did_piece_intercept(p))) return // continue if (is_british_iroquois_or_mohawk(p)) { // 5.534 Only Johnson can command British Iroquois and Mohawk (and for free) if (is_piece_in_force(JOHNSON, commander)) { can_pick_up = true gen_action_piece(p) } } else { if (cmd_use < cmd_cap) { can_pick_up = true gen_action_piece(p) } } }) // drop off sub-commanders for_each_friendly_leader_in_node(leader_box(commander), p => { if (can_drop_off_leader(commander, p)) gen_action_piece(p) }) // drop off units for_each_friendly_unit_in_node(leader_box(commander), p => { gen_action_piece(p) }) if (can_pick_up) gen_action('pick_up_all') switch (game.force.reason) { case 'campaign_1': case 'campaign_2': case 'move': // Campaign and normal activations can activate leaders without forces. gen_action_next() break case 'intercept': // Must be a force to proceed (leader + at least one unit) if (count_units_in_force(commander) > 0) gen_action('intercept') break case 'avoid': // Must be a force to proceed (leader + at least one unit) if (count_units_in_force(commander) > 0) gen_action('avoid') break } }, pick_up_all() { push_undo() let commander = game.force.commander let where = piece_space(commander) let box = leader_box(commander) // pick up all sub-commanders for_each_friendly_leader_in_node(where, p => { if (game.force.reason === 'avoid' && is_piece_inside(p)) return // continue if (game.activation && game.activation.includes(p)) return // continue if (p !== commander && leader_command(p) <= leader_command(commander)) move_piece_to(p, box) }) // pick up as many units as possible for_each_friendly_unit_in_node(where, p => { if (game.force.reason === 'avoid' && is_piece_inside(p)) return // continue if (is_british_iroquois_or_mohawk(p)) { // 5.534 Only Johnson can command British Iroquois and Mohawk (and for free) if (is_piece_in_force(JOHNSON, commander)) move_piece_to(p, box) } else { if (count_non_british_iroquois_and_mohawk_units_in_force(commander) < force_command(commander)) move_piece_to(p, box) } }) }, piece(p) { push_undo() let commander = game.force.commander let where = piece_space(commander) if (piece_node(p) === leader_box(commander)) move_piece_to(p, where) else move_piece_to(p, leader_box(commander)) }, next() { push_undo() if (game.force.reason === 'move' && count_units_in_force(game.force.commander) === 0) game.state = 'confirm_designate_force' else end_designate_force() }, intercept() { attempt_intercept() }, avoid() { attempt_avoid_battle() }, } states.confirm_designate_force = { inactive() { inactive_prompt("designate force " + designate_force_reason_prompt[game.force.reason], game.force.commander, 0) }, prompt() { view.prompt = `You have not picked up any units \u2014 are you sure you want to continue?` view.who = game.force.commander gen_action('next') }, next() { end_designate_force() } } function end_designate_force() { let commander = game.force.commander let reason = game.force.reason delete game.force switch (reason) { case 'campaign_1': game.activation.push(commander) game.state = 'select_campaign_2' break case 'campaign_2': game.activation.push(commander) goto_pick_first_move() break case 'move': goto_move_piece(commander) break default: throw Error("unknown reason state: " + game.reason) } } // TODO: merge with designate_force using reason=intercept_lone_ax states.designate_force_lone_ax = { inactive() { inactive_prompt("designate lone auxiliary force to intercept", game.force.commander, 0) }, prompt() { let commander = game.force.commander let where = piece_space(commander) let n = count_units_in_force(commander) view.prompt = `Designate lone auxiliary force to intercept with ${piece_name(commander)} from ${space_name(where)}.` view.who = commander // pick up sub-commanders for_each_friendly_leader_in_node(where, p => { if (p !== commander && leader_command(p) <= leader_command(commander)) gen_action_piece(p) }) // pick up units (max 1 auxiliary) if (n === 0) { for_each_friendly_unit_in_node(where, p => { if (is_auxiliary(p)) { if (is_british_iroquois_or_mohawk(p)) { // 5.534 Only Johnson can command British Iroquois and Mohawk (and for free) if (is_piece_in_force(JOHNSON, commander)) gen_action_piece(p) } else { gen_action_piece(p) } } }) } // drop off sub-commanders for_each_friendly_leader_in_node(leader_box(commander), p => { if (!(p === JOHNSON && force_has_british_iroquois_and_mohawk_units(commander))) gen_action_piece(p) }) // drop off units for_each_friendly_unit_in_node(leader_box(commander), p => { gen_action_piece(p) }) if (n === 1) gen_action('intercept') }, piece(p) { push_undo() let commander = game.force.commander let where = piece_space(commander) if (piece_node(p) === leader_box(commander)) move_piece_to(p, where) else move_piece_to(p, leader_box(commander)) }, intercept() { push_undo() attempt_intercept() }, } // MOVE function describe_force(force, verbose) { if (is_leader(force) && count_pieces_in_force(force) > 1) { let desc = verbose ? log_piece_name_and_place(force) : piece_name(force) for_each_piece_in_force(force, p => { if (p !== force) desc += ",\n" + piece_name(p) }) return desc } else { return verbose ? log_piece_name_and_place(force) : piece_name(force) } } function goto_move_piece(who) { logbr() log(`Activated\n${describe_force(who, true)}.`) let from = piece_space(who) game.state = 'move' game.move = { where: from, came_from: 0, type: (from === HALIFAX || from === LOUISBOURG) ? 'naval' : 'boat-or-land', used: -1, did_carry: 0, infiltrated: 0, moving: who, intercepting: 0, avoiding: 0, intercepted: [], did_attempt_intercept: 0, } game.raid = { where: 0, battle: 0, from: {}, aux: list_auxiliary_units_in_force(who) } start_move() } function start_move() { if (can_moving_force_siege_or_assault()) { game.state = 'siege_or_move' } else if (is_piece_inside(moving_piece())) { goto_break_siege() } else { resume_move() } } states.siege_or_move = { inactive() { inactive_prompt("siege or move", moving_piece(), 0) }, prompt() { let where = moving_piece_space() view.who = moving_piece() view.where = where if (is_assault_possible(where)) { if (player.hand.includes(SURRENDER)) { view.prompt = `You may assault at ${space_name(where)}, play "Surrender!", or move.` gen_action('play_event', SURRENDER) } else { view.prompt = `You may assault at ${space_name(where)} or move.` } gen_action('assault') } else { view.prompt = `You may siege at ${space_name(where)} or move.` gen_action('siege') } gen_action('move') }, siege() { push_undo() goto_siege(moving_piece_space()) }, assault() { push_undo() goto_assault(moving_piece_space()) }, play_event(c) { push_undo() game.siege_where = moving_piece_space() play_card(c) goto_surrender() }, move() { push_undo() resume_move() }, } function goto_break_siege() { let here = moving_piece_space() game.move.came_from = here goto_avoid_battle() } function piece_can_naval_move_from(who, from) { if (game.events.foul_weather) return false if (game.active === FRANCE && game.events.no_fr_naval) return false if (is_leader(who) && count_pieces_in_force(who) > 1) if (game.activation_value < 3) return false if (game.active === FRANCE) { if (from === LOUISBOURG || from === QUEBEC) return is_friendly_controlled_space(from) return false } if (game.active === BRITAIN) { if (has_amphib(from)) return true if (is_port(from)) return is_friendly_controlled_space(from) return false } return false } function max_land_movement_cost() { return game.events.foul_weather ? 2 : movement_allowance(moving_piece()) } function max_movement_cost(type) { switch (type) { case 'boat-or-land': case 'boat': return game.events.foul_weather ? 2 : 9 case 'land': return max_land_movement_cost() case 'naval': return 9 } } function resume_move() { let who = moving_piece() let where = moving_piece_space() // First retreat enemy lone leaders with our units. // Can happen if defender retreats into space(s) with lone enemy leaders. for (let p = first_enemy_leader; p <= last_enemy_leader; ++p) { let s = piece_space(p) let enemy_units = has_unbesieged_enemy_units(s) let enemy_leader = has_unbesieged_enemy_leader(s) let friendly_units = has_unbesieged_friendly_units(s) if (enemy_leader && !enemy_units && friendly_units) { return goto_retreat_lone_leader(s, 'move') } } // Then retreat our lone leaders with enemy units. // Can happen if enemy intercepted into moving piece and // lone friendly leaders remain after battle. for (let p = first_friendly_leader; p <= last_friendly_leader; ++p) { let s = piece_space(p) let enemy_units = has_unbesieged_enemy_units(s) let friendly_units = has_unbesieged_friendly_units(s) let friendly_leader = has_unbesieged_friendly_leader(s) if (friendly_leader && !friendly_units && enemy_units) { game.state = 'retreat_lone_leader' game.retreat = { from: s, reason: 'friendly_move' } return } } // Interrupt for Foul Weather response at first opportunity to move. if (game.move.used < 0) { if (is_enemy_card_available(FOUL_WEATHER) && !enemy_player.pass_fw) { if (game.options.retroactive) { game.retro_foul_weather = object_copy(game) } else { set_active_enemy() game.state = 'foul_weather' return } } game.move.used = 0 } game.state = 'move' } function is_land_path(from, to) { return spaces[from].land.includes(to) } function has_friendly_fortifications_or_cultivated(s) { return has_friendly_fortifications(s) || is_originally_friendly(s) } function add_danger_space(s) { if (!view.danger) view.danger = [] set_add(view.danger, s) } // Check if this move may lead to intercept/battle/other loss of control that prevents undo function is_danger_move(from, to) { // Lake Schooner if (is_enemy_card_available(LAKE_SCHOONER)) if (has_enemy_fortifications(to) && is_lake_connection(from, to)) return true // Battle (and interception if infiltrating) if (has_unbesieged_enemy_units(to)) return true // Retreat Lone Leader if (has_unbesieged_enemy_leader(to)) return true // Interception if (can_be_intercepted(from, to)) return true return false } function gen_action_move(from, to) { if (is_danger_move(from, to)) add_danger_space(to) gen_action_space(to) } function gen_naval_move() { let from = moving_piece_space() if (!piece_can_naval_move_from(moving_piece(), from)) return if (game.active === BRITAIN) { for (let to of game.amphib) { if (to !== from) gen_action_move(from, to) } for (let to of ports) { if (to !== from && !set_has(game.amphib, to)) if (is_friendly_controlled_space(to)) gen_action_move(from, to) } } if (game.active === FRANCE) { if (from !== LOUISBOURG && is_friendly_controlled_space(LOUISBOURG)) gen_action_move(from, LOUISBOURG) if (from !== QUEBEC && is_friendly_controlled_space(QUEBEC)) gen_action_move(from, QUEBEC) } } function is_carry_connection(from, to) { const from_ff = has_friendly_fortifications_or_cultivated(from) const to_ff = has_friendly_fortifications_or_cultivated(to) return (from_ff && to_ff) } function can_move_by_boat_or_land(used, did_carry, from, to) { if (is_land_path(from, to)) { if (used < max_land_movement_cost()) return true if (!did_carry) return is_carry_connection(from, to) return false } return true } function can_move_by_boat(used, did_carry, from, to) { if (is_land_path(from, to)) { if (!did_carry) return is_carry_connection(from, to) return false } return true } function can_move(type, used, carry, from, to) { // console.log("CAN_INFILTRATE_MOVE", type, used, carry, space_name(from), ">", space_name(to)) switch (type) { case 'boat-or-land': return can_move_by_boat_or_land(used, carry, from, to) case 'boat': return can_move_by_boat(used, carry, from, to) case 'land': return true } return false } function is_infiltration_move(to) { if (has_unbesieged_enemy_fortifications(to)) return true if (has_unbesieged_enemy_units(to)) return true return false } function can_infiltrate_search(type, used, carry, from, to) { if (can_move(type, used, carry, from, to)) { if (!is_infiltration_move(to)) { // console.log(" EXIT", space_name(to)) return true } // Downgrade from Boat/Land to Land movement if not going by river or carries. if (type === 'boat' || type === 'boat-or-land') { if (is_land_path(from, to)) { if (!carry) { if (is_carry_connection(from, to)) carry = 1 else type = 'land' } else { type = 'land' } } if (used > max_movement_cost('land')) type = 'boat' } // See if we must stop. if (type === 'land') { const from_ff = has_friendly_fortifications_or_cultivated(from) const to_ff = has_friendly_fortifications_or_cultivated(to) // Must stop on mountains. if (!to_ff && is_mountain(to)) { // console.log(" STOP mountain", used) return false } // Must stop in the next space after passing through enemy cultivated if (used > 0 && !from_ff && is_originally_enemy(from)) { // console.log(" STOP enemy cultivated") return false } } // Continue looking. if (used + 1 < max_movement_cost(type)) { for (let next of spaces[to].exits) { if (can_infiltrate_search(type, used + 1, carry, to, next)) return true } } } return false } function can_infiltrate(from, to) { // console.log("====") let result = can_infiltrate_search(game.move.type, game.move.used, game.move.did_carry, from, to) return result } function gen_regular_move() { let who = moving_piece() let from = moving_piece_space() let is_lone_ld = is_lone_leader(who) let is_lone_ax = is_lone_auxiliary(who) let has_dt = force_has_drilled_troops(who) for_each_exit(from, to => { if (is_lone_ld) { // Lone leaders can never enter an enemy occupied space if (has_unbesieged_enemy_units(to) || has_unbesieged_enemy_fortifications(to)) return // continue } else { // Must have Drilled Troops to enter an enemy fort or fortress space. // except: Infiltration by lone auxiliary if (has_unbesieged_enemy_fort_or_fortress(to)) { if (!(has_dt || (is_lone_ax && can_infiltrate(from, to)))) return // continue } } switch (game.move.type) { case 'boat-or-land': if (can_move_by_boat_or_land(game.move.used, game.move.did_carry, from, to)) gen_action_move(from, to) break case 'boat': if (can_move_by_boat(game.move.used, game.move.did_carry, from, to)) gen_action_move(from, to) break case 'land': gen_action_move(from, to) break } }) } function stop_move() { game.move.used = 9 } function stop_land_move() { if (game.move.type === 'boat-or-land') game.move.type = 'boat' else game.move.used = 9 } function would_be_infiltration_move(who, from, to) { if (is_lone_auxiliary(who)) { if (has_enemy_stockade(to) && can_infiltrate(from, to)) return true if (has_unbesieged_enemy_fort_or_fortress(to) && can_infiltrate(from, to)) return true if (has_unbesieged_enemy_units(to) && can_infiltrate(from, to)) return true } return false } function apply_move(to) { let who = moving_piece() let from = moving_piece_space() let maybe_infiltrated = is_lone_auxiliary(who) && can_infiltrate(from, to) if (game.move.type === 'naval') game.move.used = 9 else game.move.used ++ game.move.where = to game.move.came_from = from game.raid.from[to] = from // remember where raiders came from so they can retreat after battle // Downgrade from Boat/Land to Land movement if not going by river or carries. if (game.move.type === 'boat' || game.move.type === 'boat-or-land') { if (is_land_path(from, to)) { if (!game.move.did_carry) { if (is_carry_connection(from, to)) game.move.did_carry = 1 else game.move.type = 'land' } else { game.move.type = 'land' } } if (game.move.used > max_land_movement_cost('land')) game.move.type = 'boat' } if (game.move.type === 'land' || game.move.type === 'boat-or-land') { const from_ff = has_friendly_fortifications_or_cultivated(from) const to_ff = has_friendly_fortifications_or_cultivated(to) const has_dt = force_has_drilled_troops(who) const has_ax = force_has_auxiliary(who) // Must stop on mountains. if (is_mountain(to) && !to_ff) stop_land_move() // Must stop in the next space after passing through... if (game.move.used > 1 && !from_ff) { // Drilled Troops that pass through wilderness must stop in the next space. if (has_dt && !has_ax && is_wilderness(from)) if (!game.events.george_croghan) stop_land_move() // Auxiliaries that pass through enemy cultivated must stop in the next space. if (has_ax && !has_dt && is_originally_enemy(from)) stop_land_move() } } game.move.infiltrated = 0 if (has_enemy_stockade(to)) { if (maybe_infiltrated) game.move.infiltrated = 1 else stop_move() } if (has_unbesieged_enemy_fort_or_fortress(to)) { if (maybe_infiltrated) game.move.infiltrated = 1 else stop_move() } if (has_unbesieged_enemy_units(to)) { if (maybe_infiltrated) game.move.infiltrated = 1 } if (game.move.infiltrated) log(`Infiltrated %${to}.`) else log(`Moved to %${to}.`) move_piece_to(who, to) lift_sieges_and_amphib() } states.move = { inactive() { inactive_prompt("move", moving_piece(), 0) }, prompt() { let who = moving_piece() let from = moving_piece_space() if (from) { view.prompt = `Move ${piece_name_and_place(who)}` switch (game.move.type) { case 'boat-or-land': view.prompt += " by boat or land" if (game.move.did_carry) view.prompt += " (carried)" break case 'boat': view.prompt += " by boat" if (game.move.did_carry) view.prompt += " (carried)" break case 'land': view.prompt += " by land" break case 'naval': if (game.move.used > 0) view.prompt += " by ship \u2014 done." else view.prompt += " by ship." break } if (game.move.infiltrated) view.prompt += " (infiltrating)" if (game.move.type !== 'naval') { if (game.move.used === 9) view.prompt += ` \u2014 done.` else view.prompt += ` \u2014 ${game.move.used}/${max_movement_cost(game.move.type)}.` } } else { view.prompt = `${piece_name(who)} eliminated.` } view.who = who if (game.move.used === 0) { if (game.events.foul_weather && can_moving_force_siege_or_assault()) { if (is_assault_possible(from)) gen_action('assault') else gen_action('siege') } if (game.active === BRITAIN && player.hand.includes(GEORGE_CROGHAN)) { if (force_has_drilled_troops(who)) gen_action('play_event', GEORGE_CROGHAN) } if (piece_can_naval_move_from(who, from)) { if (game.move.type !== 'naval') { gen_action('naval_move') } else { if (!game.events.no_amphib) { if (game.active === BRITAIN && has_amphibious_arrow(from)) { let has_amphib = false for (let card = first_amphib_card; card <= last_amphib_card; ++card) { if (player.hand.includes(card)) { has_amphib = true gen_action('play_event', card) } } if (has_amphib) view.prompt += ` You may play "Amphibious Landing."` } } } } } if (game.move.infiltrated) { if (!has_unbesieged_enemy_fort_or_fortress(from)) gen_action('stop') else if (game.move.used === 9) gen_action('end_move') } else { gen_action('end_move') } if (game.move.used < max_movement_cost(game.move.type)) { if (game.move.type === 'naval') gen_naval_move() else gen_regular_move() } if (game.move.used < 9 && !game.move.infiltrated) { if (is_leader(who) && count_pieces_in_force(who) > 1) gen_action('drop_off') } }, play_event(card) { push_undo() play_card(card) if (card === GEORGE_CROGHAN) { game.events.george_croghan = 1 resume_move() } else { game.state = 'amphibious_landing' } }, naval_move() { push_undo() game.move.type = 'naval' resume_move() }, space(to) { push_undo() let from = moving_piece_space() if (is_enemy_card_available(LAKE_SCHOONER)) { if (has_enemy_fortifications(to) && is_lake_connection(from, to)) { set_active_enemy() game.move.lake_schooner = to game.state = 'lake_schooner' return goto_retroactive_foul_weather() } } apply_move(to) goto_intercept() }, drop_off() { push_undo() if (game.summary) game.summary.drop_off = [] game.state = 'drop_off' }, siege() { push_undo() goto_siege(moving_piece_space()) }, assault() { push_undo() goto_assault(moving_piece_space()) }, stop() { game.move.infiltrated = 0 goto_designate_inside() }, end_move() { if (game.move.used > 0) { end_move() } else { push_undo() game.state = 'confirm_end_move' } }, } states.drop_off = { inactive() { inactive_prompt("move", moving_piece(), 0) }, prompt() { let who = moving_piece() let where = moving_piece_space() view.prompt = `Drop off subordinate units or leaders in ${space_name(where)}.` view.who = who view.where = where gen_action_next() for_each_leader_in_force(who, p => { if (p !== who && can_drop_off_leader(who, p)) gen_action_piece(p) }) for_each_unit_in_force(who, p => { gen_action_piece(p) }) }, piece(who) { push_undo() if (game.summary) game.summary.drop_off.push(who) else log(`Dropped off ${piece_name(who)}.`) move_piece_to(who, moving_piece_space()) }, next() { push_undo() print_plain_summary("Dropped off", 'drop_off') resume_move() }, } states.confirm_end_move = { inactive() { inactive_prompt("move", moving_piece(), 0) }, prompt() { view.prompt = `You have not moved yet \u2014 are you sure you want to pass?` view.who = moving_piece() gen_action('end_move') }, end_move() { end_move() } } function goto_retroactive_foul_weather() { if (game.options.retroactive && game.retro_foul_weather) { // console.log("RETRO REWIND") let state_start = game.retro_foul_weather delete game.retro_foul_weather let state_next = object_copy(game) load_game_state(state_start) set_active_enemy() game.state = 'foul_weather' game.retro_foul_weather = state_next } } states.foul_weather = { inactive() { inactive_prompt("foul weather", moving_piece(), 0) }, prompt() { let p = moving_piece() view.who = p view.where = moving_piece_space() if (player.hand.includes(FOUL_WEATHER)) { view.prompt = `${piece_name_and_place(p)} is about to move. You may play "Foul Weather".` gen_action('play_event', FOUL_WEATHER) } else { view.prompt = `${piece_name_and_place(p)} is about to move. You don't have "Foul Weather".` } gen_action('pass_fw_season') if (game.activation && game.activation.length > 0) gen_action('pass_fw_action') gen_action_pass() }, play_event(c) { if (game.options.retroactive) { // console.log("RETRO STAY") delete game.retro_foul_weather } play_card(c) game.events.foul_weather = 1 game.move.used = 0 set_active_enemy() resume_move() }, pass_fw_season() { let current = game.active states.foul_weather.pass() if (current === BRITAIN) game.british.pass_fw = 2 else game.french.pass_fw = 2 }, pass_fw_action() { let current = game.active states.foul_weather.pass() if (current === BRITAIN) game.british.pass_fw = 1 else game.french.pass_fw = 1 }, pass() { if (game.options.retroactive) { // console.log("RETRO PASS") load_game_state(game.retro_foul_weather) } else { game.move.used = 0 set_active_enemy() resume_move() } } } states.lake_schooner = { inactive() { inactive_prompt("lake schooner", moving_piece(), game.move.lake_schooner) }, prompt() { let who = moving_piece() let from = moving_piece_space() let to = game.move.lake_schooner view.who = who view.where = to if (player.hand.includes(LAKE_SCHOONER)) { view.prompt = `${piece_name(who)} is about to move from ${space_name(from)} to ${space_name(to)}. You may play "Lake Schooner".` gen_action('play_event', LAKE_SCHOONER) } else { view.prompt = `${piece_name(who)} is about to move from ${space_name(from)} to ${space_name(to)}. You don't have "Lake Schooner".` } gen_action_pass() }, play_event(c) { play_card(c) let who = moving_piece() let from = moving_piece_space() let to = game.move.lake_schooner delete game.move.lake_schooner set_active_enemy() log(`${piece_name(who)} stopped at %${from}.`) if (would_be_infiltration_move(who, from, to)) { // 6.63 eliminate if forced back into enemy-occupied space during infiltration if (has_unbesieged_enemy_units(from) || has_unbesieged_enemy_fortifications(from)) { for_each_friendly_piece_in_space(from, p => { if (is_piece_unbesieged(p)) eliminate_piece(p) }) } } stop_move() resume_move() }, pass() { let to = game.move.lake_schooner delete game.move.lake_schooner set_active_enemy() apply_move(to) goto_intercept() } } states.amphibious_landing = { inactive() { inactive_prompt("amphibious landing", moving_piece(), 0) }, prompt() { let who = moving_piece() let from = moving_piece_space() view.prompt = `Amphibious Landing: Select a destination for ${piece_name_and_place(who)}.` view.who = who if (from === HALIFAX) { gen_action_move(from, LOUISBOURG) } if (from === LOUISBOURG) { gen_action_move(from, BAIE_ST_PAUL) gen_action_move(from, RIVIERE_OUELLE) gen_action_move(from, ILE_D_ORLEANS) } }, space(to) { push_undo() set_add(game.amphib, to) apply_move(to) goto_intercept() }, } function remove_siege_marker(where) { delete game.sieges[where] } function place_siege_marker(where) { log(`Started siege at %${where}.`) game.sieges[where] = 0 } function change_siege_marker(where, amount) { return game.sieges[where] = clamp(game.sieges[where] + amount, 0, 2) } function goto_battle_check() { let where = moving_piece_space() if (has_unbesieged_enemy_units(where)) { goto_battle(where, false) } else { end_move_step(false) } } function end_move_step(final=false, overrun=false) { let did_battle = !!game.battle lift_sieges_and_amphib() let who = moving_piece() let where = moving_piece_space() delete game.battle game.move.did_attempt_intercept = 0 // reset flag for next move step if (final) stop_move() // Handle death of stack... if (!has_friendly_pieces(where)) { stop_move() return resume_move() } if (!game.move.infiltrated) { if (has_unbesieged_enemy_fortifications(where)) { if (has_enemy_fort(where) || is_fortress(where)) { place_siege_marker(where) } if (has_enemy_stockade(where)) { if (has_friendly_drilled_troops(where)) { if (did_battle) destroy_enemy_stockade_after_battle(where) else capture_enemy_stockade(where) if (can_play_massacre()) return goto_massacre('move') } } stop_move() } } if (overrun && game.move.used < 9) { logbr() log(".b Overrun") logbr() } resume_move() } function end_move() { let who = moving_piece() unstack_force(who) delete game.move game.raid.list = [] for (let i = 0; i < game.raid.aux.length; ++i) add_raid(game.raid.aux[i]) goto_pick_raid() // Pause for foul weather before any raids are resolved... goto_retroactive_foul_weather() } // INTERCEPT function can_be_intercepted(came_from, here) { let result = false let who = moving_piece() // 6.723 Leaders moving alone can NOT be intercepted if (is_lone_leader(who)) return false // 6.722 entering space with friendly units or fortifications if (has_non_moving_unbesieged_friendly_units(here)) return false if (has_unbesieged_friendly_fortifications(here)) return false // 6.721 exception: can always intercept units infiltrating same space if (game.move.infiltrated) { if (has_unbesieged_enemy_units(here)) return true } const is_lone_ax = is_lone_auxiliary(who) for_each_exit(here, from => { // 6.724 may not intercept an enemy leaving their own space if (from === came_from) return // continue // 6.721 Lone auxiliary in wilderness if (is_lone_ax && is_wilderness_or_mountain(here)) { if (has_unbesieged_enemy_auxiliary(from)) result = true } else { if (has_unbesieged_enemy_units(from)) result = true } }) return result } function gen_intercept() { let is_lone_ax = is_lone_auxiliary(moving_piece()) let to = moving_piece_space() // 6.721 exception -- can always intercept units infiltrating same space if (game.move.infiltrated) { for_each_friendly_piece_in_space(to, p => { if (is_piece_unbesieged(p)) gen_action_piece(p) }) } for_each_exit(to, from => { // 6.721 if (is_lone_ax && is_wilderness_or_mountain(to)) { let has_ax = false let has_br_indians = false for_each_friendly_unit_in_space(from, p => { if (is_piece_unbesieged(p)) { if (is_auxiliary(p)) { gen_action_piece(p) if (is_british_iroquois_or_mohawk(p)) has_br_indians = true else has_ax = true } } }) // allow leaders to accompany intercepting auxiliary unit if (has_ax) { for_each_friendly_leader_in_space(from, p => { if (is_piece_unbesieged(p)) gen_action_piece(p) }) } else if (has_br_indians) { // TODO: allow intercept with Johnson as sub-commander if (is_piece_unbesieged_in_space(JOHNSON, from)) { gen_action_piece(JOHNSON) } } } else { for_each_friendly_piece_in_space(from, p => { if (is_piece_unbesieged(p)) gen_action_piece(p) }) } }) } function goto_intercept() { // Abandoned lone leader at formerly besieged fortification with enemy units... let from = moving_piece_came_from() if (has_unbesieged_friendly_leader(from) && !has_unbesieged_friendly_units(from)) { if (has_unbesieged_enemy_units(from)) { game.state = 'retreat_lone_leader' game.retreat = { from: from, reason: 'abandon' } return } } if (can_be_intercepted(moving_piece_came_from(), moving_piece_space())) { game.move.intercepting = 0 set_active_enemy() game.state = 'intercept_who' return goto_retroactive_foul_weather() } if (game.move.infiltrated) end_move_step(false) else goto_designate_inside() } function is_moving_piece_lone_ax_in_wilderness_or_mountain() { let p = moving_piece() let s = moving_piece_space() return is_lone_auxiliary(p) && is_wilderness_or_mountain(s) } states.intercept_who = { inactive() { inactive_prompt("intercept", moving_piece(), 0) }, prompt() { let where = moving_piece_space() view.where = where if (game.move.intercepting) { view.prompt = `Intercept into ${space_name(where)} with ${piece_name(game.move.intercepting)}.` view.who = game.move.intercepting gen_action('intercept') } else { view.prompt = "You may select a force or unit to intercept into " + space_name(where) + "." gen_action_pass() gen_intercept() } }, piece(p) { push_undo() let to = moving_piece_space() let from = piece_space(p) // All units can intercept in same space (even lone ax in wilderness), but no need to designate the force. if (is_leader(p)) { game.move.intercepting = p game.force = { commander: p, reason: 'intercept', } if (is_moving_piece_lone_ax_in_wilderness_or_mountain() && from !== to) { game.state = 'designate_force_lone_ax' } else { game.state = 'designate_force' } } else { game.move.intercepting = p } }, intercept() { attempt_intercept() }, pass() { game.move.intercepting = 0 end_intercept_fail() }, } function attempt_intercept() { let who = intercepting_piece() if (is_leader(who)) { for_each_piece_in_force(who, p => { if (is_unit(p)) game.move.intercepted.push(p) }) } else { game.move.intercepted.push(who) } game.move.did_attempt_intercept = 1 let die = roll_die("to intercept with\n" + describe_force(who, true)) if (is_leader(who)) die = modify(die, leader_tactics(who), "leader tactics") if (die >= 4) { log("Intercepted!") end_intercept_success() } else { log("Failed.") end_intercept_fail() } } function end_intercept_fail() { let who = intercepting_piece() if (who) unstack_force(who) set_active_enemy() game.state = 'move' if (game.move.infiltrated) end_move_step(false) else goto_designate_inside() } function end_intercept_success() { let who = intercepting_piece() let to = moving_piece_space() move_piece_to(who, to) unstack_force(who) set_active_enemy() game.state = 'move' lift_sieges_and_amphib() goto_designate_inside() } // DECLARE INSIDE/OUTSIDE FORTIFICATION function goto_designate_inside() { let where = moving_piece_space() if (has_unbesieged_enemy_units_that_did_not_intercept(where)) { if (has_enemy_fortress(where) || has_enemy_fort(where)) { set_active_enemy() game.state = 'designate_inside' if (game.summary) game.summary.inside = [] return goto_retroactive_foul_weather() } } goto_avoid_battle() } states.designate_inside = { inactive() { inactive_prompt("designate inside", moving_piece(), 0) }, prompt() { let where = moving_piece_space() view.prompt = "You may withdraw leaders and units into the fortification." view.where = where gen_action_next() let n = count_friendly_units_inside(where) for_each_friendly_piece_in_space(where, p => { if (is_piece_unbesieged(p) && !did_piece_intercept(p)) { if (is_leader(p) || is_fortress(where) || n < 4) gen_action_piece(p) } }) }, piece(p) { push_undo() if (game.summary) { game.summary.inside.push(p) } else { if (is_fortress(moving_piece_space())) log(`${piece_name(p)} withdrew into fortress.`) else log(`${piece_name(p)} withdrew into fort.`) } set_piece_inside(p) }, next() { if (is_fortress(moving_piece_space())) print_plain_summary("Withdrew into fortress", 'inside') else print_plain_summary("Withdrew into fort", 'inside') set_active_enemy() goto_avoid_battle() }, } // AVOID BATTLE function goto_avoid_battle() { let from = moving_piece_space() if (has_unbesieged_enemy_units_that_did_not_intercept(from)) { if (!game.move.did_attempt_intercept) { if (can_enemy_avoid_battle(from)) { game.move.avoiding = 0 set_active_enemy() game.state = 'avoid_who' return goto_retroactive_foul_weather() } } } goto_battle_check() } function did_piece_intercept(p) { return game.move.intercepted.includes(p) } states.avoid_who = { inactive() { inactive_prompt("avoid battle", moving_piece(), 0) }, prompt() { let from = moving_piece_space() view.where = from if (game.move.avoiding) { view.prompt = `Avoid battle from ${space_name(from)} with ${piece_name(game.move.avoiding)}.` view.who = game.move.avoiding gen_action('avoid') } else { view.prompt = "You may select a force or unit to avoid battle from " + space_name(from) + "." gen_action_pass() for_each_friendly_piece_in_space(from, p => { if (!did_piece_intercept(p) && is_piece_unbesieged(p)) gen_action_piece(p) }) } }, piece(p) { push_undo() if (is_leader(p)) { game.move.avoiding = p game.force = { commander: p, reason: 'avoid', } game.state = 'designate_force' } else { game.move.avoiding = p } }, avoid() { attempt_avoid_battle() }, pass() { game.move.avoiding = 0 end_avoid_battle() }, } function attempt_avoid_battle() { let avoiding = avoiding_piece() let moving = moving_piece() let from = moving_piece_space() // 6.8 Exception: Auxiliary and all-Auxiliary forces automatically succeed. if (is_wilderness_or_mountain(from) && force_has_only_auxiliary_units(avoiding) && !force_has_auxiliary(moving)) { log("Auxiliaries avoided battle\n" + describe_force(avoiding, false) + ".") game.state = 'avoid_to' return } let die = roll_die("to avoid battle\n" + describe_force(avoiding, false)) if (is_leader(avoiding)) die = modify(die, leader_tactics(avoiding), "leader tactics") if (die >= 4) { game.state = 'avoid_to' } else { log("Failed.") end_avoid_battle() } } function can_enemy_avoid_battle(from) { let can_avoid = false for_each_exit(from, to => { if ((moving_piece_came_from() !== to) && !has_unbesieged_friendly_units(to) && !has_unbesieged_friendly_fortifications(to)) can_avoid = true }) // 6.811 British units in Amphib space may avoid directly to port if (game.active === FRANCE) { if (has_amphib(from)) { for_each_british_controlled_port(to => { if (to !== from) can_avoid = true }) } } return can_avoid } states.avoid_to = { inactive() { inactive_prompt("avoid battle", moving_piece(), 0) }, prompt() { let from = moving_piece_space() view.prompt = `Avoid battle from ${space_name(from)} \u2014 select where to.` view.who = avoiding_piece() view.where = from for_each_exit(from, to => { if ((moving_piece_came_from() !== to) && !has_unbesieged_enemy_units(to) && !has_unbesieged_enemy_fortifications(to)) gen_action_space(to) }) // 6.811 British units in Amphib space may avoid directly to port if (game.active === BRITAIN) { if (has_amphib(from)) { for_each_british_controlled_port(to => { if (to !== from) gen_action_space(to) }) } } }, space(to) { log(`Avoided to %${to}.`) end_avoid_battle_success(to) }, } function end_avoid_battle_success(to) { let who = avoiding_piece() move_piece_to(who, to) end_avoid_battle() } function end_avoid_battle() { let who = avoiding_piece() if (who) unstack_force(who) set_active_enemy() game.state = 'move' goto_battle_check() } // BATTLE function for_each_attacking_piece(fn) { game.battle.atk_pcs.forEach(fn) } function count_attacking_units() { let n = 0 for (let i = 0; i < game.battle.atk_pcs.length; ++i) if (is_unit(game.battle.atk_pcs[i])) ++n return n } function for_each_defending_piece(fn) { let where = game.battle.where if (game.battle.assault) { if (game.battle.defender === BRITAIN) { for (let p = first_british_piece; p <= last_british_piece; ++p) if (is_piece_in_space(p, where)) fn(p) } else { for (let p = first_french_piece; p <= last_french_piece; ++p) if (is_piece_in_space(p, where)) fn(p) } } else { if (game.battle.defender === BRITAIN) { for (let p = first_british_piece; p <= last_british_piece; ++p) if (is_piece_unbesieged_in_space(p, where)) fn(p) } else { for (let p = first_french_piece; p <= last_french_piece; ++p) if (is_piece_unbesieged_in_space(p, where)) fn(p) } } } function some_attacking_piece(fn) { let r = false for_each_attacking_piece(p => { if (fn(p)) r = true }) return r } function some_defending_piece(fn) { let r = false for_each_defending_piece(p => { if (fn(p)) r = true }) return r } function attacker_combat_strength() { let str = 0 for_each_attacking_piece(p => { if (is_unit(p)) str += unit_strength(p) }) return str } function defender_combat_strength() { let str = 0 for_each_defending_piece(p => { if (is_unit(p)) str += unit_strength(p) }) return str } const COMBAT_RESULT_TABLE = [ // S/D 0 1 2 3 4 5 6 7 [ 0, [ 0, 0, 0, 0, 0, 1, 1, 1 ]], [ 1, [ 0, 0, 0, 0, 1, 1, 1, 1 ]], [ 2, [ 0, 0, 0, 1, 1, 1, 1, 2 ]], [ 3, [ 0, 0, 1, 1, 1, 1, 2, 2 ]], [ 5, [ 0, 0, 1, 1, 2, 2, 2, 3 ]], [ 8, [ 0, 1, 2, 2, 2, 3, 3, 3 ]], [ 12, [ 1, 2, 2, 2, 3, 3, 4, 4 ]], [ 16, [ 1, 2, 3, 3, 4, 4, 4, 5 ]], [ 21, [ 2, 3, 3, 4, 4, 5, 5, 6 ]], [ 27, [ 3, 4, 4, 4, 5, 5, 6, 7 ]], [ 1000, [ 3, 4, 5, 5, 5, 6, 7, 8 ]], ] function combat_result(die, str, shift) { die = clamp(die, 0, 7) str = clamp(str, 0, 28) for (let i = 0; i < COMBAT_RESULT_TABLE.length; ++i) { if (str <= COMBAT_RESULT_TABLE[i][0]) { let k = clamp(i + shift, 0, COMBAT_RESULT_TABLE.length-1) let r = COMBAT_RESULT_TABLE[k][1][die] if (k === 0) log(`Lookup ${die} on column 0.`) else if (k === COMBAT_RESULT_TABLE.length - 1) log(`Lookup ${die} on column >= 28.`) else { let a = COMBAT_RESULT_TABLE[k-1][0] + 1 let b = COMBAT_RESULT_TABLE[k][0] if (a === b) log(`Lookup ${die} on column ${b}.`) else log(`Lookup ${die} on column ${a}-${b}.`) } return r } } return NaN } function goto_battle(where, is_assault) { logbr() if (is_assault) log(".assault %" + where) else if (game.raid.where !== where) log(".battle %" + where) logbr() game.battle = { where: where, attacker: game.active, defender: enemy(), assault: is_assault, atk_worth_vp: 0, def_worth_vp: 0, atk_pcs: [], } // Make a list of attacking pieces (for sorties and so we can unstack from the leader box) if (game.battle.assault) { game.battle.atk_commander = find_friendly_commanding_leader_in_space(game.battle.where) let where = game.battle.where if (game.battle.attacker === BRITAIN) { for (let p = first_british_piece; p <= last_british_piece; ++p) if (is_piece_in_space(p, where)) game.battle.atk_pcs.push(p) } else { for (let p = first_french_piece; p <= last_french_piece; ++p) if (is_piece_in_space(p, where)) game.battle.atk_pcs.push(p) } } else if (game.raid.where === where) { game.battle.atk_commander = find_friendly_commanding_leader_in_space(game.battle.where) for_each_friendly_piece_in_space(game.battle.where, p => { game.battle.atk_pcs.push(p) }) } else { game.battle.atk_commander = game.move.moving for_each_piece_in_force(game.move.moving, p => { game.battle.atk_pcs.push(p) }) } // 5.36 unit or leader may not be activated if it participated in combat or assault. if (game.activation) { for_each_attacking_piece(p => { if (game.activation.includes(p)) { log(`Deactivated ${log_piece_name_and_place(p)}.`) remove_from_array(game.activation, p) unstack_force(p) } }) } if (game.raid) game.raid.battle = where // No Militia take part in assaults if (!game.battle.assault) goto_battle_militia() else goto_battle_sortie() // Pause for foul weather before any battles are resolved... goto_retroactive_foul_weather() } function determine_battle_vp_worth() { if (!game.battle.assault) { let n_atk = 0 for_each_attacking_piece(p => { if (is_unit(p)) ++n_atk if (is_regular(p)) game.battle.atk_worth_vp = 1 }) if (n_atk > 4) game.battle.atk_worth_vp = 1 let n_def = 0 for_each_defending_piece(p => { if (is_unit(p)) ++n_def if (is_regular(p)) game.battle.def_worth_vp = 1 }) if (n_def > 4) game.battle.def_worth_vp = 1 } } function goto_battle_militia() { let box = department_militia(game.battle.where) if (box && count_militia_in_department(box) > 0 && !game.raid.where) { let first = 0, last = 0 switch (box) { case ST_LAWRENCE_CANADIAN_MILITIAS: set_active(FRANCE) first = first_st_lawrence_department last = last_st_lawrence_department break case NORTHERN_COLONIAL_MILITIAS: set_active(BRITAIN) first = first_northern_department last = last_northern_department break case SOUTHERN_COLONIAL_MILITIAS: set_active(BRITAIN) first = first_southern_department last = last_southern_department break } // 7.3 exception: No Militia if there are enemy raided markers. for (let s = first; s <= last; ++s) if (has_enemy_raided_marker(s)) return goto_battle_sortie() game.state = 'militia_in_battle' if (game.summary) game.summary.deploy = [] } else { goto_battle_sortie() } } states.militia_in_battle = { inactive() { inactive_prompt("deploy militia in battle", 0, game.battle.where) }, prompt() { view.prompt = `You may deploy militia at ${space_name(game.battle.where)}.` let box = department_militia(game.battle.where) view.where = game.battle.where if (game.active === FRANCE) { for (let p = first_french_militia; p <= last_french_militia; ++p) if (piece_node(p) === box) gen_action_piece(p) } else { for (let p = first_british_militia; p <= last_british_militia; ++p) if (piece_node(p) === box) gen_action_piece(p) } gen_action_next() }, piece(p) { push_undo() move_piece_to(p, game.battle.where) if (game.active === game.battle.attacker) game.battle.atk_pcs.push(p) if (game.summary) game.summary.deploy.push(p) else log(`Deployed ${piece_name(p)}.`) }, next() { print_plain_summary("Deployed", 'deploy') goto_battle_sortie() }, } function goto_battle_sortie() { set_active(game.battle.attacker) if (has_besieged_friendly_units(game.battle.where)) { game.state = 'sortie' if (game.summary) game.summary.sortie = [] } else { goto_battle_attacker_events() } } function sortie_with_piece(p) { if (game.summary) game.summary.sortie.push(p) else log(`${piece_name(p)} sortied.`) game.battle.atk_pcs.push(p) // 5.36 unit or leader may not be activated if it participated in combat or assault. unstack_piece_from_force(p) if (game.activation) remove_from_array(game.activation, p) } states.sortie = { inactive() { inactive_prompt("sortie with besieged units", 0, game.battle.where) }, prompt() { view.prompt = `You may sortie with besieged units at ${space_name(game.battle.where)}.` view.where = game.battle.where let done = true for (let p = first_friendly_unit; p <= last_friendly_unit; ++p) { if (is_piece_besieged_in_space(p, game.battle.where)) { if (!game.battle.atk_pcs.includes(p)) { gen_action_piece(p) done = false } } } if (!done) gen_action('pick_up_all') gen_action_next() }, piece(p) { push_undo() sortie_with_piece(p) }, pick_up_all() { push_undo() for (let p = first_friendly_unit; p <= last_friendly_unit; ++p) if (is_piece_besieged_in_space(p, game.battle.where)) if (!game.battle.atk_pcs.includes(p)) sortie_with_piece(p) }, next() { print_plain_summary("Sortied", 'sortie') goto_battle_attacker_events() }, } function count_auxiliary_units_in_attack() { let n = 0 for_each_attacking_piece(p => { if (is_auxiliary(p)) ++n }) return n } function count_auxiliary_units_in_defense() { let n = 0 for_each_defending_piece(p => { if (is_auxiliary(p)) ++n }) return n } function has_light_infantry_in_attack() { let n = 0 for_each_attacking_piece(p => { if (is_light_infantry(p)) ++n }) return n > 0 } function has_light_infantry_in_defense() { let n = 0 for_each_defending_piece(p => { if (is_light_infantry(p)) ++n }) return n > 0 } function can_play_ambush_in_attack() { if (!game.battle.assault && game.events.ambush !== game.battle.attacker) { let s = game.battle.where if (is_card_available_for_attacker(AMBUSH_1) || is_card_available_for_attacker(AMBUSH_2)) { let n = count_auxiliary_units_in_attack() if (is_wilderness_or_mountain(s) && n > 0) { if (has_enemy_fort(s) || has_light_infantry_in_defense(s) || count_auxiliary_units_in_defense() > n) return false return true } } } return false } function can_play_ambush_in_defense() { if (!game.battle.assault && game.events.ambush !== game.battle.defender) { let s = game.battle.where if (is_card_available_for_defender(AMBUSH_1) || is_card_available_for_defender(AMBUSH_2)) { let n = count_auxiliary_units_in_defense() if (is_wilderness_or_mountain(s) && n > 0) { if (has_enemy_fort(s) || has_light_infantry_in_attack(s) || count_auxiliary_units_in_attack() > n) return false return true } } } return false } function can_play_coehorns_in_attack() { if (is_card_available_for_attacker(COEHORNS)) return game.battle.assault && has_friendly_regulars(game.battle.where) return false } function can_play_coehorns_in_defense() { // If played during siege, this will be false during assault because the card is in discard. // There's no risk of being prompted twice. if (is_card_available_for_defender(COEHORNS)) return game.battle.assault && has_friendly_regulars(game.battle.where) return false } function can_play_fieldworks_in_attack() { if (!game.battle.assault) { if (is_card_available_for_attacker(FIELDWORKS_1) || is_card_available_for_attacker(FIELDWORKS_2)) { if (has_fieldworks(game.battle.where)) { if (game.battle.assault) return has_friendly_drilled_troops(game.battle.where) else return force_has_drilled_troops(game.move.moving) } } } return false } function can_play_fieldworks_in_defense() { if (!game.battle.assault) { if (is_card_available_for_defender(FIELDWORKS_1) || is_card_available_for_defender(FIELDWORKS_2)) { if (!has_fieldworks(game.battle.where)) { return has_friendly_drilled_troops(game.battle.where) } } } return false } function goto_battle_attacker_events() { set_active(game.battle.attacker) if (can_play_ambush_in_attack() || can_play_coehorns_in_attack() || can_play_fieldworks_in_attack()) { game.state = 'attacker_events' } else { goto_battle_defender_events() } } function goto_battle_defender_events() { set_active(game.battle.defender) if (can_play_ambush_in_defense() || can_play_coehorns_in_defense() || can_play_fieldworks_in_defense()) { game.state = 'defender_events' } else { goto_battle_roll() } } states.attacker_events = { inactive() { inactive_prompt("attacker events", 0, game.battle.where) }, prompt() { let have = [] let dont_have = [] if (can_play_ambush_in_attack()) { let x = false if (player.hand.includes(AMBUSH_1)) { gen_action('play_event', AMBUSH_1) x = true } if (player.hand.includes(AMBUSH_2)) { gen_action('play_event', AMBUSH_2) x = true } if (x) have.push('"Ambush"') else dont_have.push('"Ambush"') } if (can_play_coehorns_in_attack()) { if (player.hand.includes(COEHORNS)) { gen_action('play_event', COEHORNS) have.push('"Coehorns"') } else { dont_have.push('"Coehorns"') } } if (can_play_fieldworks_in_attack()) { let x = false if (player.hand.includes(FIELDWORKS_1)) { gen_action('play_event', FIELDWORKS_1) x = true } if (player.hand.includes(FIELDWORKS_2)) { gen_action('play_event', FIELDWORKS_2) x = true } if (x) have.push('"Fieldworks"') else dont_have.push('"Fieldworks"') } view.prompt = `Attacker at ${space_name(game.battle.where)}.` view.where = game.battle.where if (have.length > 0) view.prompt += " You may play " + have.join(" or ") + "." if (dont_have.length > 0) view.prompt += " You don't have " + dont_have.join(" or ") + "." if (have.length === 0 && dont_have.length === 0) view.prompt += " You have no more response events." gen_action_next() }, play_event(c) { push_undo() play_card(c) switch (c) { case AMBUSH_1: case AMBUSH_2: game.events.ambush = game.active break case COEHORNS: game.events.coehorns = game.active break case FIELDWORKS_1: case FIELDWORKS_2: remove_fieldworks(game.battle.where) break } }, pass() { goto_battle_defender_events() }, next() { goto_battle_defender_events() }, } states.defender_events = { inactive() { inactive_prompt("defender events", 0, game.battle.where) }, prompt() { let have = [] let dont_have = [] if (can_play_ambush_in_defense()) { let x = false if (player.hand.includes(AMBUSH_1)) { gen_action('play_event', AMBUSH_1) x = true } if (player.hand.includes(AMBUSH_2)) { gen_action('play_event', AMBUSH_2) x = true } if (x) have.push('"Ambush"') else dont_have.push('"Ambush"') } if (can_play_coehorns_in_defense()) { if (player.hand.includes(COEHORNS)) { gen_action('play_event', COEHORNS) have.push('"Coehorns"') } else { dont_have.push('"Coehorns"') } } if (can_play_fieldworks_in_defense()) { let x = false if (player.hand.includes(FIELDWORKS_1)) { gen_action('play_event', FIELDWORKS_1) x = true } if (player.hand.includes(FIELDWORKS_2)) { gen_action('play_event', FIELDWORKS_2) x = true } if (x) have.push('"Fieldworks"') else dont_have.push('"Fieldworks"') } view.prompt = `Defender at ${space_name(game.battle.where)}.` view.where = game.battle.where if (have.length > 0) view.prompt += " You may play " + have.join(" or ") + "." if (dont_have.length > 0) view.prompt += " You don't have " + dont_have.join(" or ") + "." if (have.length === 0 && dont_have.length === 0) view.prompt += " You have no more response events." gen_action_next() }, play_event(c) { push_undo() play_card(c) switch (c) { case AMBUSH_1: case AMBUSH_2: if (game.events.ambush) delete game.events.ambush else game.events.ambush = game.active break case COEHORNS: game.events.coehorns = game.active break case FIELDWORKS_1: case FIELDWORKS_2: place_fieldworks(game.battle.where) break } }, pass() { goto_battle_roll() }, next() { goto_battle_roll() }, } /* if ambush === attacker attacker fires defender step loss defender fires attacker step loss else if ambush === defender defender fires attacker step loss attacker fires defender step loss else attacker fires defender fires attacker step loss defender step loss determine winner */ function goto_battle_roll() { determine_battle_vp_worth() if (game.events.ambush === game.battle.attacker) goto_atk_fire() else if (game.events.ambush === game.battle.defender) goto_def_fire() else goto_atk_fire() } function end_atk_fire() { if (game.events.ambush) goto_def_step_losses() else goto_def_fire() } function end_def_fire() { goto_atk_step_losses() } function end_step_losses() { flush_summary() if (game.active === game.battle.attacker) goto_atk_leader_check() else goto_def_leader_check() } function end_leader_check() { flush_summary() delete game.battle.leader_check if (game.events.ambush === game.battle.attacker) { if (game.active === game.battle.defender) goto_def_fire() else goto_determine_winner() } else if (game.events.ambush === game.battle.defender) { if (game.active === game.battle.attacker) goto_atk_fire() else goto_determine_winner() } else { if (game.active === game.battle.attacker) goto_def_step_losses() else goto_determine_winner() } } // FIRE function goto_atk_fire() { set_active(game.battle.attacker) // Attacker who is wiped out by ambush does not get to fire back! if (game.events.ambush === game.battle.defender) { if (!some_attacking_piece(is_unit)) { game.battle.atk_die = 0 game.battle.atk_result = 0 return end_atk_fire() } } logbr() log(".b Attacker") let str = attacker_combat_strength() let shift = 0 if (game.events.ambush === game.battle.attacker) { log(`Strength ${str} \xd7 2 for ambush.`) str *= 2 } else { log(`Strength ${str}.`) } let die = game.battle.atk_die = roll_die() if (is_leader(game.battle.atk_commander)) { die = modify(die, leader_tactics(game.battle.atk_commander), "leader tactics") } if (game.events.coehorns === game.battle.attacker) { die = modify(die, 2, "for coehorns") } if (game.battle.assault) { log(`1 column left for assaulting`) shift -= 1 } else { if (is_wilderness_or_mountain(game.battle.where)) { let atk_has_ax = some_attacking_piece(p => is_auxiliary(p) || is_light_infantry(p)) let def_has_ax = some_defending_piece(p => is_auxiliary(p) || is_light_infantry(p)) if (!atk_has_ax && def_has_ax) die = modify(die, -1, "vs auxiliaries in wilderness") } if (is_cultivated(game.battle.where)) { let atk_has_reg = some_attacking_piece(p => is_regular(p)) let def_has_reg = some_defending_piece(p => is_regular(p)) if (!atk_has_reg && def_has_reg) die = modify(die, -1, "vs regulars in cultivated") } if (has_amphib(game.battle.where) && game.move && game.move.type === 'naval') { die = modify(die, -1, "amphibious landing") } if (has_enemy_stockade(game.battle.where)) { die = modify(die, -1, "vs stockade") } if (has_fieldworks(game.battle.where)) { // NOTE: Ignore fieldworks during assault, as they belong to the besieging forces. log(`1 column left vs fieldworks`) shift -= 1 } } game.battle.atk_result = combat_result(die, str, shift) log(`Attacker result: ${game.battle.atk_result}.`) end_atk_fire() } function goto_def_fire() { set_active(game.battle.defender) // Defender who is wiped out by ambush does not get to fire back! if (game.events.ambush === game.battle.attacker) { if (!some_defending_piece(is_unit)) { game.battle.def_die = 0 game.battle.def_result = 0 return end_def_fire() } } logbr() log(".b Defender") let str = defender_combat_strength() let shift = 0 if (game.events.ambush === game.battle.defender) { log(`Strength ${str} \xd7 2 for ambush.`) str *= 2 } else { log(`Strength ${str}.`) } let die = game.battle.def_die = roll_die() let p = find_friendly_commanding_leader_in_space(game.battle.where) if (p) { die = modify(die, leader_tactics(p), "leader tactics") } if (game.events.coehorns === game.battle.defender) { die = modify(die, 2, "for coehorns") } if (!game.battle.assault) { if (is_wilderness_or_mountain(game.battle.where)) { let atk_has_ax = some_attacking_piece(p => is_auxiliary(p) || is_light_infantry(p)) let def_has_ax = some_defending_piece(p => is_auxiliary(p) || is_light_infantry(p)) if (atk_has_ax && !def_has_ax) die = modify(die, -1, "vs auxiliaries in wilderness") } if (is_cultivated(game.battle.where)) { let atk_has_reg = some_attacking_piece(p => is_regular(p)) let def_has_reg = some_defending_piece(p => is_regular(p)) if (atk_has_reg && !def_has_reg) die = modify(die, -1, "vs regulars in cultivated") } } game.battle.def_result = combat_result(die, str, shift) log(`Defender result: ${game.battle.def_result}.`) end_def_fire() } // STEP LOSSES function goto_atk_step_losses() { set_active(game.battle.attacker) game.battle.def_caused = 0 if (game.battle.def_result > 0) { if (game.move) unstack_force(moving_piece()) game.state = 'step_losses' game.battle.step_loss = game.battle.def_result if (game.battle.assault) game.battle.dt_loss = game.battle.step_loss else game.battle.dt_loss = Math.ceil(game.battle.step_loss / 2) game.battle.units = [] for_each_attacking_piece(p => { if (is_unit(p)) game.battle.units.push(p) }) logbr() log(".b Attacker Losses") } else { end_step_losses() } } function goto_def_step_losses() { set_active(game.battle.defender) game.battle.atk_caused = 0 if (game.battle.atk_result > 0) { game.state = 'step_losses' game.battle.step_loss = game.battle.atk_result if (game.battle.assault) game.battle.dt_loss = game.battle.step_loss else game.battle.dt_loss = Math.ceil(game.battle.step_loss / 2) game.battle.units = [] for_each_defending_piece(p => { if (is_unit(p)) game.battle.units.push(p) }) // None to take! if (game.battle.units.length === 0) end_step_losses() else { logbr() log(".b Defender Losses") } } else { end_step_losses() } } states.step_losses = { inactive() { if (game.active === game.battle.defender) inactive_prompt("defender step losses", 0, game.battle.where) else inactive_prompt("attacker step losses", 0, game.battle.where) }, prompt() { let done = true if (game.battle.step_loss > 0) { if (game.battle.dt_loss > 0) { for (let i = 0; i < game.battle.units.length; ++i) { let p = game.battle.units[i] if (is_drilled_troops(p) && can_reduce_unit(p)) { done = false gen_action_piece(p) } } if (done) { for (let i = 0; i < game.battle.units.length; ++i) { let p = game.battle.units[i] if (is_drilled_troops(p)) { done = false gen_action_piece(p) } } } } if (done) { for (let i = 0; i < game.battle.units.length; ++i) { let p = game.battle.units[i] if (can_reduce_unit(p)) { done = false gen_action_piece(p) } } } if (done) { for (let i = 0; i < game.battle.units.length; ++i) { let p = game.battle.units[i] done = false gen_action_piece(p) } } } if (done) { view.prompt = `Apply step losses \u2014 done.` gen_action_next() } else { if (game.battle.dt_loss > 0) view.prompt = `Apply step losses (${game.battle.step_loss} left, ${game.battle.dt_loss} from drilled troops).` else view.prompt = `Apply step losses (${game.battle.step_loss} left).` } }, piece(p) { push_undo() --game.battle.step_loss if (game.battle.dt_loss > 0 && is_drilled_troops(p)) --game.battle.dt_loss if (reduce_unit(p, false)) { remove_from_array(game.battle.atk_pcs, p) remove_from_array(game.battle.units, p) } }, next() { if (game.active === game.battle.attacker) game.battle.def_caused = game.battle.def_result - game.battle.step_loss else game.battle.atk_caused = game.battle.atk_result - game.battle.step_loss end_step_losses() }, } function goto_raid_step_losses() { if (game.raid.step_loss > 0) { game.state = 'raid_step_losses' game.raid.units = [] for_each_friendly_unit_in_space(game.raid.where, p => { game.raid.units.push(p) }) } else { goto_raid_leader_check() } } states.raid_step_losses = { inactive() { inactive_prompt("raid step losses", 0, game.raid.where) }, prompt() { view.prompt = `Apply step losses (${game.raid.step_loss}).` let can_reduce = false if (game.raid.step_loss > 0) { for (let i = 0; i < game.raid.units.length; ++i) { let p = game.raid.units[i] if (can_reduce_unit(p)) { can_reduce = true gen_action_piece(p) } } if (!can_reduce) { for (let i = 0; i < game.raid.units.length; ++i) { let p = game.raid.units[i] can_reduce = true gen_action_piece(p) } } } if (!can_reduce) gen_action_next() }, piece(p) { push_undo() --game.raid.step_loss if (reduce_unit(p, false)) remove_from_array(game.raid.units, p) }, next() { flush_summary() goto_raid_leader_check() }, } // LEADER LOSSES function goto_atk_leader_check() { set_active(game.battle.attacker) game.battle.leader_check = [] if ((game.battle.def_result > 0) && (game.battle.def_die === 1 || game.battle.def_die === 6)) { for_each_attacking_piece(p => { if (is_leader(p)) game.battle.leader_check.push(p) }) } if (game.battle.leader_check.length > 0) { game.state = 'leader_check' } else { end_leader_check() } } function goto_def_leader_check() { set_active(game.battle.defender) game.battle.leader_check = [] if ((game.battle.atk_result > 0) && (game.battle.atk_die === 1 || game.battle.atk_die === 6)) { for_each_defending_piece(p => { if (is_leader(p)) game.battle.leader_check.push(p) }) } if (game.battle.leader_check.length > 0) { log(`Leader loss check.`) game.state = 'leader_check' } else { end_leader_check() } } states.leader_check = { inactive() { if (game.active === game.battle.defender) inactive_prompt("defender leader check", 0, game.battle.where) else inactive_prompt("attacker leader check", 0, game.battle.where) }, prompt() { view.prompt = "Roll for leader losses." for (let i = 0; i < game.battle.leader_check.length; ++i) gen_action_piece(game.battle.leader_check[i]) }, piece(p) { let die = roll_die("for " + piece_name(p)) if (die === 1) { if (game.battle) remove_from_array(game.battle.atk_pcs, p) eliminate_piece(p, false) } remove_from_array(game.battle.leader_check, p) if (game.battle.leader_check.length === 0) end_leader_check() }, } function goto_raid_leader_check() { if (game.raid.leader_check) { game.raid.leader_check = [] for_each_friendly_leader_in_space(game.raid.where, p => { game.raid.leader_check.push(p) }) if (game.raid.leader_check.length > 0) { log(`Leader loss check.`) game.state = 'raid_leader_check' } else { delete game.raid.leader_check goto_raiders_go_home() } } else { goto_raiders_go_home() } } states.raid_leader_check = { inactive() { inactive_prompt("raider leader check", 0, game.raid.where) }, prompt() { view.prompt = "Roll for leader losses." for (let i = 0; i < game.raid.leader_check.length; ++i) gen_action_piece(game.raid.leader_check[i]) }, piece(p) { let die = roll_die("for " + piece_name(p)) if (die === 1) eliminate_piece(p, false) remove_from_array(game.raid.leader_check, p) if (game.raid.leader_check.length === 0) { delete game.raid.leader_check flush_summary() goto_raiders_go_home() } }, } // WINNER/LOSER function return_militia(where) { let box = department_militia(where) if (box) { let n = 0 for (let p = 1; p <= last_piece; ++p) { if (is_militia(p) && is_piece_in_space(p, where)) { move_piece_to(p, box) ++n } } if (n > 0) { log(`${n} Militia units returned to their box.`) } } } function goto_determine_winner() { set_active(game.battle.attacker) if (game.battle.assault) determine_winner_assault() else determine_winner_battle() // Reset battle event flags that may occur again (in case of overrun) delete game.events.ambush } function determine_winner_battle() { let where = game.battle.where logbr() // 7.8: Determine winner let atk_eliminated = count_attacking_units() === 0 let def_eliminated = count_unbesieged_enemy_units_in_space(where) === 0 let victor if (atk_eliminated && def_eliminated) { log("Both sides eliminated.") if (game.battle.atk_result > game.battle.def_result) victor = game.battle.attacker else victor = game.battle.defender } else if (atk_eliminated && !def_eliminated) { log("Attacker eliminated.") victor = game.battle.defender } else if (!atk_eliminated && def_eliminated) { log("Defender eliminated.") victor = game.battle.attacker } else { if (game.battle.atk_caused > game.battle.def_caused) victor = game.battle.attacker else victor = game.battle.defender } logbr() if (victor === game.battle.attacker) log(".b Attacker Won") else log(".b Defender Won") if (victor === game.battle.attacker && game.battle.def_worth_vp) { if (victor === FRANCE) award_french_vp(1) else award_british_vp(1) } if (victor === game.battle.defender && game.battle.atk_worth_vp) { if (victor === FRANCE) award_french_vp(1) else award_british_vp(1) } return_militia(game.battle.where) if (victor === game.battle.attacker) remove_fieldworks(where) logbr() // Raid battle vs militia if (game.raid && game.raid.where > 0) { if (victor === game.battle.attacker) { goto_raid_events() } else { if (game.battle.atk_pcs.length > 0) { retreat_attacker(game.raid.where, game.raid.from[game.raid.where] | 0) } else { retreat_attacker(game.raid.where, game.raid.from[game.raid.where] | 0) end_retreat_attacker(game.raid.from[game.raid.where]) } } return } // Normal battle // 6.712 - Infiltrator must always retreat from fort/fortress even if they win if (game.move.infiltrated && has_unbesieged_enemy_fort_or_fortress(game.battle.where)) victor = game.battle.defender if (victor === game.battle.attacker) { if (def_eliminated && game.battle.def_result === 0) game.battle.overrun = 1 if (has_unbesieged_enemy_pieces(where)) { log(".b Defender Retreat") goto_retreat_defender() } else { if (game.battle.overrun) { end_move_step(false, true) } else { end_move_step(true) } } } else { /* If attacker must retreat, unbesieged defenders who withdrew inside can come out. */ if (is_space_unbesieged(where)) { for (let p = first_piece; p <= last_piece; ++p) if (is_piece_besieged_in_space(p, where)) set_piece_outside(p) } if (game.battle.atk_pcs.length > 0) { unstack_force(moving_piece()) retreat_attacker(game.battle.where, moving_piece_came_from()) } else { retreat_attacker(game.battle.where, moving_piece_came_from()) end_retreat_attacker(moving_piece_came_from()) } } } function eliminate_enemy_pieces_inside(where) { for (let p = first_enemy_piece; p <= last_enemy_piece; ++p) if (is_piece_besieged_in_space(p, where)) eliminate_piece(p, false) } function determine_winner_assault() { let where = game.battle.where let victor logbr() if (game.battle.atk_result > game.battle.def_result) victor = game.battle.attacker else victor = game.battle.defender if (victor === game.battle.attacker) { log(".b Attacker Won Assault") eliminate_enemy_pieces_inside(where) remove_siege_marker(where) if (has_enemy_fortress(where)) { capture_enemy_fortress(where) if (can_play_massacre()) return goto_massacre('assault') } if (has_enemy_fort(where)) { capture_enemy_fort(where) if (can_play_massacre()) return goto_massacre('assault') } } else { log(".b Defender Won Assault") } logbr() end_move_step(true) } // RETREAT function can_attacker_retreat_from_to(p, from, to) { if (to === 0) return false if (has_unbesieged_enemy_units(to)) return false if (has_unbesieged_enemy_fortifications(to)) return false if (force_has_drilled_troops(p)) { if (is_cultivated(to) || has_friendly_fortifications(to)) return true else return false } return true } function retreat_attacker(from, to) { set_active(game.battle.attacker) game.state = 'retreat_attacker' game.retreat = { from, to } } states.retreat_attacker = { inactive() { inactive_prompt("attacker retreat", 0, game.retreat.from) }, prompt() { let from = game.retreat.from let to = game.retreat.to if (from === to) view.prompt = `Retreat losing leaders and units back into ${space_name(to)}.` else view.prompt = `Retreat losing leaders and units from ${space_name(from)} to ${space_name(to)}.` view.where = from gen_action_space(to) }, space() { let from = game.retreat.from let to = game.retreat.to // NOTE: Besieged pieces that sortie out are 'inside' so not affected by the code below. init_retreat_summary() log(".b Attacker Retreat") for_each_friendly_piece_in_space(from, p => { if (is_piece_unbesieged(p)) { if (can_attacker_retreat_from_to(p, from, to)) { push_retreat_summary(p, "to %" + to) move_piece_to(p, to) } else { eliminate_piece(p, false) } } else { if (is_fortress(to)) push_retreat_summary(p, "into fortress") else push_retreat_summary(p, "into fort") } }) flush_retreat_summary() flush_summary() logbr() end_retreat_attacker(to) } } function end_retreat_attacker(to) { if (game.move) game.move.infiltrated = 0 // Raid battle vs militia if (game.raid && game.raid.where > 0) { // if raiders need to retreat again, they go back to this // space, unless they retreat to join other raiders if (!game.raid.from[to]) game.raid.from[to] = game.retreat.from delete game.retreat return goto_pick_raid() } // Normal battle delete game.retreat end_retreat() } function goto_retreat_defender() { set_active(game.battle.defender) game.state = 'retreat_defender' init_retreat_summary() } function can_defender_retreat_from_to(p, from, to) { if (has_unbesieged_enemy_units(to)) return false if (has_unbesieged_enemy_fortifications(to)) return false if (game.move && moving_piece_came_from() === to) if (!game.retreat || game.retreat.reason !== 'friendly_move' || game.retreat.reason !== 'abandon') return false if (force_has_drilled_troops(p)) { if (is_cultivated(to) || has_friendly_fortifications(to)) return true else return false } return true } function can_defender_retreat_inside(p, from) { if (has_friendly_fort_or_fortress(from)) { let n = count_friendly_units_inside(from) let m = count_units_in_force(p) if (is_leader(p) || is_fortress(from) || (n + m) <= 4) return true } return false } function can_defender_retreat_from(p, from) { if (is_piece_inside(p)) return false if (can_defender_retreat_inside(p, from)) return true if (game.battle.defender === BRITAIN && has_amphib(from)) return true let can_retreat = false for_each_exit(from, to => { if (can_defender_retreat_from_to(p, from, to)) can_retreat = true }) return can_retreat } function can_all_defenders_retreat_from(from) { let result = true for_each_unbesieged_friendly_piece_in_space(from, p => { if (!can_defender_retreat_from(p, from)) result = false }) return result } function can_any_defenders_retreat_from_to(from, to) { let result = false for_each_unbesieged_friendly_piece_in_space(from, p => { if (can_defender_retreat_from_to(p, from, to)) result = true }) return result } function can_any_defenders_retreat_inside(from) { let result = false for_each_unbesieged_friendly_piece_in_space(from, p => { if (can_defender_retreat_inside(p, from)) result = true }) return result } states.retreat_defender = { inactive() { inactive_prompt("defender retreat", 0, game.battle.where) }, prompt() { let from = game.battle.where view.prompt = "Retreat losing leaders and units \u2014" view.where = from let can_retreat = false for_each_friendly_piece_in_node(from, p => { if (can_defender_retreat_from(p, from)) { can_retreat = true gen_action_piece(p) } }) if (!can_retreat) { view.prompt += " done." gen_action_next() } else { view.prompt += " select piece to retreat." if (can_all_defenders_retreat_from(from)) gen_action('pick_up_all') } }, piece(piece) { push_undo() game.battle.who = piece game.state = 'retreat_defender_to' }, pick_up_all() { push_undo() game.state = 'retreat_all_defenders_to' }, next() { let from = game.battle.where for_each_friendly_piece_in_space(from, p => { if (is_piece_unbesieged(p)) eliminate_piece(p, false) }) flush_retreat_summary() flush_summary() logbr() end_retreat() }, } states.retreat_defender_to = { inactive() { inactive_prompt("defender retreat", 0, game.battle.where) }, prompt() { let from = game.battle.where let who = game.battle.who view.prompt = "Retreat losing leaders and units \u2014 select where to." view.who = who if (game.active === BRITAIN && has_amphib(from)) { for_each_british_controlled_port(to => gen_action_space(to)) } if (can_defender_retreat_inside(who, from)) gen_action_space(from) for_each_exit(from, to => { if (can_defender_retreat_from_to(who, from, to)) { gen_action_space(to) } }) }, space(to) { let from = game.battle.where let who = game.battle.who if (from === to) { if (is_fortress(to)) push_retreat_summary(who, "into fortress") else push_retreat_summary(who, "into fort") set_piece_inside(who) } else { push_retreat_summary(who, "to %" + to) move_piece_to(who, to) } game.state = 'retreat_defender' }, } states.retreat_all_defenders_to = { inactive() { inactive_prompt("defender retreat", 0, game.battle.where) }, prompt() { let from = game.battle.where view.prompt = "Retreat all losing leaders and units \u2014 select where to." if (game.active === BRITAIN && has_amphib(from)) { for_each_british_controlled_port(to => gen_action_space(to)) } if (can_any_defenders_retreat_inside(from)) gen_action_space(from) for_each_exit(from, to => { if (can_any_defenders_retreat_from_to(from, to)) { gen_action_space(to) } }) }, space(to) { push_undo() let from = game.battle.where let done = true for_each_unbesieged_friendly_piece_in_space(from, p => { if (from === to) { if (can_defender_retreat_inside(p, from)) { if (is_fortress(to)) push_retreat_summary(p, "into fortress") else push_retreat_summary(p, "into fort") set_piece_inside(p) } else { done = false } } else { if (can_defender_retreat_from_to(p, from, to)) { push_retreat_summary(p, "to %" + to) move_piece_to(p, to) } else { done = false } } }) if (!can_all_defenders_retreat_from(from)) done = true if (done) game.state = 'retreat_defender' }, next() { push_undo() game.state = 'retreat_defender' } } function end_retreat() { set_active(game.battle.attacker) if (game.battle.overrun) { end_move_step(false, true) } else { end_move_step(true) } } function goto_retreat_lone_leader(from, reason) { set_active_enemy() game.state = 'retreat_lone_leader' game.retreat = { from, reason } // Pause for foul weather if necessary goto_retroactive_foul_weather() } function pick_unbesieged_leader(s) { for (let p = first_friendly_leader; p <= last_friendly_leader; ++p) if (is_piece_unbesieged_in_space(p, s)) return p return 0 } states.retreat_lone_leader = { inactive() { inactive_prompt("retreat lone leader", game.move ? moving_piece() : 0, game.retreat.where) }, prompt() { let from = game.retreat.from let who = pick_unbesieged_leader(from) view.prompt = `Retreat lone leader ${piece_name(who)} from ${space_name(from)}.` view.who = who let can_retreat = false if (game.active === BRITAIN && has_amphib(from)) { for_each_british_controlled_port(to => { can_retreat = true gen_action_space(to) }) } if (can_defender_retreat_inside(who, from)) { can_retreat = true gen_action_space(from) } for_each_exit(from, to => { if (can_defender_retreat_from_to(who, from, to)) { // Forbid lone leader to coexist with enemy lone leader if (!has_unbesieged_enemy_leader(to)) { can_retreat = true gen_action_space(to) } } }) if (!can_retreat) gen_action('eliminate') }, eliminate() { let from = game.retreat.from let who = pick_unbesieged_leader(from) eliminate_piece(who) resume_retreat_lone_leader(from) }, space(to) { let from = game.retreat.from let who = pick_unbesieged_leader(from) if (from === to) { if (is_fortress(to)) log(`${piece_name(who)} retreated into fortress.`) else log(`${piece_name(who)} retreated into fort.`) set_piece_inside(who) } else { log(`${piece_name(who)} retreated to %${to}.`) move_piece_to(who, to) } resume_retreat_lone_leader(from) }, } function resume_retreat_lone_leader(from) { let who = pick_unbesieged_leader(from) if (!who) { flush_summary() switch (game.retreat.reason) { case 'indian_alliance': set_active_enemy() delete game.retreat game.state = 'indian_alliance' break case 'move': set_active_enemy() delete game.retreat resume_move() break case 'friendly_move': delete game.retreat resume_move() break case 'abandon': delete game.retreat goto_intercept() break } } } // SIEGE const SIEGE_TABLE = [ 0, 0, 0, 1, 1, 1, 2, 2 ] function can_siege_or_assault_if_activated(leader, where) { if (has_besieged_enemy_fortifications(where)) { let commanding = find_friendly_commanding_leader_in_space(where) if (commanding > 0) { let cmd_rank = leader_command(commanding) let ldr_rank = leader_command(leader) if (ldr_rank === cmd_rank && has_friendly_supplied_drilled_troops(where)) return true } } } function can_moving_force_siege_or_assault() { let leader = moving_piece() let where = moving_piece_space() if (has_besieged_enemy_fortifications(where)) { let commanding = find_friendly_commanding_leader_in_space(where) if (commanding > 0) { let cmd_rank = leader_command(commanding) let ldr_rank = leader_command(leader) if (ldr_rank === cmd_rank && force_has_supplied_drilled_troops(leader)) { return true } } } return false } function can_play_coehorns_in_siege(s) { return is_friendly_card_available(COEHORNS) && has_friendly_regulars(s) } function goto_siege(space) { // TODO: unstack here? game.siege_where = space if (can_play_coehorns_in_siege(game.siege_where)) game.state = 'siege_coehorns_attacker' else end_siege_coehorns_attacker() } states.siege_coehorns_attacker = { inactive() { inactive_prompt("attacker siege coehorns", 0, game.siege_where) }, prompt() { if (player.hand.includes(COEHORNS)) { view.prompt = `Siege at ${space_name(game.siege_where)}. You may play "Coehorns & Howitzers".` gen_action('play_event', COEHORNS) } else { view.prompt = `Siege at ${space_name(game.siege_where)}. You don't have "Coehorns & Howitzers".` } gen_action_pass() }, play_event(c) { play_card(c) game.events.coehorns = game.active end_siege_coehorns_attacker() }, pass() { end_siege_coehorns_attacker() } } function end_siege_coehorns_attacker() { set_active_enemy() if (can_play_coehorns_in_siege(game.siege_where)) game.state = 'siege_coehorns_defender' else end_siege_coehorns_defender() } states.siege_coehorns_defender = { inactive() { inactive_prompt("defender siege coehorns", 0, game.siege_where) }, prompt() { if (player.hand.includes(COEHORNS)) { view.prompt = `Siege at ${space_name(game.siege_where)}. You may play "Coehorns & Howitzers".` gen_action('play_event', COEHORNS) } else { view.prompt = `Siege at ${space_name(game.siege_where)}. You don't have "Coehorns & Howitzers".` } gen_action_pass() }, play_event(c) { play_card(c) game.events.coehorns = game.active end_siege_coehorns_defender() }, pass() { end_siege_coehorns_defender() } } function end_siege_coehorns_defender() { set_active_enemy() if (is_friendly_card_available(SURRENDER)) { if (game.siege_where === LOUISBOURG && game.sieges[LOUISBOURG] !== 1 && game.sieges[LOUISBOURG] !== 2) resolve_siege() else game.state = 'siege_surrender' } else { resolve_siege() } } states.siege_surrender = { inactive() { inactive_prompt("siege surrender", 0, game.siege_where) }, prompt() { if (player.hand.includes(SURRENDER)) { view.prompt = `Siege at ${space_name(game.siege_where)}. You may play "Surrender!"` gen_action('play_event', SURRENDER) } else { view.prompt = `Siege at ${space_name(game.siege_where)}. You don't have "Surrender!"` } gen_action_pass() }, play_event(c) { play_card(c) goto_surrender() }, pass() { resolve_siege() } } function goto_surrender() { for (let p = first_enemy_piece; p <= last_enemy_piece; ++p) if (piece_node(p) === game.siege_where) set_piece_outside(p) delete game.sieges[game.siege_where] if (has_enemy_fort(game.siege_where)) capture_enemy_fort_intact(game.siege_where) else capture_enemy_fortress(game.siege_where) if (can_play_massacre()) return goto_massacre('surrender') goto_surrender_place() } function goto_surrender_place() { set_active_enemy() if (has_friendly_units(game.siege_where)) { game.state = 'surrender' if (game.siege_where === LOUISBOURG) { game.surrender = find_closest_friendly_unbesieged_fortification(QUEBEC) } else { game.surrender = find_closest_friendly_unbesieged_fortification(game.siege_where) if (game.surrender.length === 0) if (has_unbesieged_friendly_fortifications(LOUISBOURG)) game.surrender.push(LOUISBOURG) } } else { end_surrender() } } states.surrender = { inactive() { inactive_prompt("surrender", 0, game.siege_where) }, prompt() { view.prompt = "Surrender! Place defenders at the closest unbesieged fortification." view.where = game.siege_where for (let i=0; i < game.surrender.length; ++i) gen_action_space(game.surrender[i]) if (game.surrender.length === 0) gen_action('eliminate') }, space(s) { for (let p = first_friendly_piece; p <= last_friendly_piece; ++p) if (piece_node(p) === game.siege_where) move_piece_to(p, s) end_surrender() }, eliminate() { for (let p = first_friendly_piece; p <= last_friendly_piece; ++p) if (piece_node(p) === game.siege_where) eliminate_piece(p) end_surrender() }, } function end_surrender() { set_active_enemy() delete game.surrender delete game.siege_where end_move_step(true) } const SIEGE_TABLE_RESULT = { 0: "No effect.", 1: "Siege +1.", 2: "Siege +2." } function resolve_siege() { let where = game.siege_where logbr() log(".siege %" + where) logbr() let att_leader = find_friendly_commanding_leader_in_space(where) let def_leader = find_enemy_commanding_leader_in_space(where) let die = roll_die("for siege") die = modify(die, leader_tactics(att_leader), "besieging leader") if (game.events.coehorns) die = modify(die, game.events.coehorns === game.active ? 2 : -2, "for coehorns") if (def_leader) die = modify(die, -leader_tactics(def_leader), "defending leader") if (where === LOUISBOURG) die = modify(die, -1, "for Louisbourg") let result = SIEGE_TABLE[clamp(die, 0, 7)] log(`Lookup ${die} on siege table.`) log(SIEGE_TABLE_RESULT[result]) if (result > 0) { let level = change_siege_marker(where, result) log("Siege level " + level + ".") } goto_assault_possible(where) } // ASSAULT function is_assault_possible(where) { let siege_level = game.sieges[where] | 0 if (has_enemy_fort(where) && siege_level >= 1) return true if (has_enemy_fortress(where) && siege_level >= 2) return true return false } function goto_assault_possible(where) { if (is_assault_possible(where)) { game.state = 'assault_possible' game.assault_possible = where } else { end_move_step(true) } } states.assault_possible = { inactive() { inactive_prompt("assault is possible", 0, game.assault_possible) }, prompt() { view.prompt = `You may assault at ${space_name(game.assault_possible)}.` gen_action_space(game.assault_possible) gen_action('assault') gen_action('pass') }, space() { let where = game.assault_possible delete game.assault_possible goto_assault(where) }, assault() { let where = game.assault_possible delete game.assault_possible goto_assault(where) }, pass() { let where = game.assault_possible delete game.assault_possible log("Did not assault %" + where) end_move_step(true) }, } function goto_assault(where) { // TODO: unstack here? goto_battle(where, true) } // RAID function goto_pick_raid() { if (game.raid.list.length > 0) { game.state = 'pick_raid' } else { delete game.raid // TODO: allow demolish before ending activation end_activation() } } states.pick_raid = { inactive: "pick raid", prompt() { view.prompt = "Pick the next raid space." for (let i=0; i < game.raid.list.length; ++i) gen_action_space(game.raid.list[i]) }, space(s) { logbr() log(".raid %" + s) logbr() game.raid.where = s remove_from_array(game.raid.list, s) goto_raid_militia() }, } function goto_raid_militia() { let where = game.raid.where if (has_enemy_stockade(where) && enemy_department_has_at_least_n_militia(where, 1)) { if (where === game.raid.battle) { goto_raid_events() } else { set_active_enemy() game.state = 'militia_against_raid' game.count = 1 } } else { goto_raid_events() } } states.militia_against_raid = { inactive() { inactive_prompt("deploy militia against raid", 0, game.raid.where) }, prompt() { view.prompt = `You may deploy one militia against the raid at ${space_name(game.raid.where)}.` view.where = game.raid.where if (game.count > 0) { let box = department_militia(game.raid.where) if (game.active === FRANCE) { for (let p = first_french_militia; p <= last_french_militia; ++p) if (piece_node(p) === box) gen_action_piece(p) } else { for (let p = first_british_militia; p <= last_british_militia; ++p) if (piece_node(p) === box) gen_action_piece(p) } } gen_action_next() }, piece(p) { push_undo() move_piece_to(p, game.raid.where) log(`Deployed ${piece_name(p)}.`) game.count -- }, next() { set_active_enemy() if (game.count === 0) goto_battle(game.raid.where, false) else goto_raid_events() }, } const RAID_TABLE = { stockade: [ 2, 1, 1, 0, 2, 1, 0, 0 ], cultivated: [ 2, 0, 0, 0, 1, 1, 0, 0 ], } function goto_raid_events() { if (is_enemy_card_available(BLOCKHOUSES) && !enemy_player.pass_bh) { set_active_enemy() game.state = 'raid_blockhouses' } else { resolve_raid() } } states.raid_blockhouses = { inactive() { inactive_prompt("raid blockhouses", 0, game.raid.where) }, prompt() { if (player.hand.includes(BLOCKHOUSES)) { view.prompt = `Raid at ${space_name(game.raid.where)}. You may play "Blockhouses".` gen_action('play_event', BLOCKHOUSES) } else { view.prompt = `Raid at ${space_name(game.raid.where)}. You don't have "Blockhouses".` } gen_action('pass_bh_season') gen_action_pass() }, play_event(c) { play_card(c) game.events.blockhouses = game.active set_active_enemy() resolve_raid() }, pass_bh_season() { player.pass_bh = 1 states.raid_blockhouses.pass() }, pass() { set_active_enemy() resolve_raid() } } function resolve_raid() { let where = game.raid.where let x_stockade = has_enemy_stockade(where) let x_allied = has_enemy_allied_settlement(where) let natural_die = roll_die("for raid") let die = natural_die let commander = find_friendly_commanding_leader_in_space(where) if (commander) die = modify(die, leader_tactics(commander), "leader") if (has_friendly_rangers(where)) die = modify(die, 1, "for rangers") if (enemy_department_has_at_least_n_militia(where, 2)) die = modify(die, -1, "for milita in dept") let column = 'cultivated' if (x_stockade || x_allied) column = 'stockade' if (game.events.blockhouses === enemy()) { column = 'stockade' log("vs enemy blockhouses") } let result = clamp(die, 0, 7) let success = result >= 5 let losses = RAID_TABLE[column][result] log(`Lookup ${die} vs ${column}.`) if (success) { if (losses === 0) log(`Success.`) else log(`Success with one step loss.`) if (x_stockade || x_allied || !has_friendly_raided_marker(where)) place_friendly_raided_marker(where) if (x_stockade) destroy_enemy_stockade_in_raid(where) if (x_allied) eliminate_indian_tribe(where) } else { if (losses === 0) log(`No effect.`) else if (losses === 1) log(`Failure with one step loss.`) else log(`Failure with two step losses.`) } game.raid.step_loss = losses // 10.32: leader check if (natural_die === 1 || (natural_die === 6 && column === 'stockade')) game.raid.leader_check = 1 else game.raid.leader_check = 0 // Next states: // raider step losses // raider leader check // raiders go home goto_raid_step_losses() } // RAIDERS GO HOME & INDIANS AND LEADERS GO HOME function can_follow_indians_home(from) { for (let p = first_friendly_leader; p <= last_friendly_leader; ++p) { if (is_piece_unbesieged_in_space(p, from)) return true } for (let p = first_friendly_unit; p <= last_friendly_unit; ++p) { if (is_coureurs(p) && is_piece_unbesieged_in_space(p, from)) return true } return false } function goto_raiders_go_home() { // Surviving raiders must go home! if (has_friendly_pieces(game.raid.where)) { game.state = 'raiders_go_home' game.go_home = { reason: 'raid', who: 0, from: 0, to: 0, follow: {}, } init_go_home_summary() } else { end_raiders_go_home() } } function end_raiders_go_home() { flush_go_home_summary() delete game.go_home goto_pick_raid() } function goto_indians_and_leaders_go_home() { set_active(FRANCE) resume_indians_and_leaders_go_home() } function resume_indians_and_leaders_go_home() { game.state = 'indians_and_leaders_go_home' game.go_home = { reason: 'late_season', who: 0, from: 0, to: 0, follow: {}, } init_go_home_summary() } function end_indians_and_leaders_go_home() { flush_go_home_summary() logbr() if (game.active === FRANCE) { set_active(BRITAIN) resume_indians_and_leaders_go_home() } else { set_active(FRANCE) delete game.go_home goto_remove_raided_markers() } } states.raiders_go_home = { inactive() { inactive_prompt("raiders go home", 0, game.raid.where) }, prompt() { let from = game.raid.where let done = true if (true) { // INDIANS FIRST for_each_friendly_unit_in_space(from, p => { if (is_indian(p)) { done = false gen_action_piece(p) } }) if (done) { for_each_friendly_piece_in_space(from, p => { if (!is_indian(p)) { done = false gen_action_piece(p) } }) } } else { // IN ANY ORDER // Possibly confusing because leaders and coureurs can // only follow indians when indians lead the way. for_each_friendly_piece_in_space(from, p => { done = false gen_action_piece(p) }) } if (done) { view.prompt = `Raiders go home from ${space_name(from)} \u2014 done.` gen_action_next() } else { view.prompt = `Raiders go home from ${space_name(from)}.` } }, piece(p) { push_undo() game.go_home.who = p game.go_home.from = game.raid.where game.state = 'go_home_to' }, next() { end_raiders_go_home() }, } states.indians_and_leaders_go_home = { inactive: "indians and leaders go home", prompt() { let done = true for (let p = first_friendly_piece; p <= last_friendly_piece; ++p) { let s = piece_space(p) if (s && is_piece_unbesieged(p) && !has_friendly_fortifications(s)) { // Indians not at their settlement if (is_indian(p)) { if (s !== indians.space_from_piece[p]) { done = false gen_action_piece(p) } } // Leaders who are left alone in the wilderness if (is_leader(p)) { if (is_wilderness_or_mountain(s) && !has_friendly_units(s)) { done = false gen_action_piece(p) } } } } if (done) { view.prompt = "Indians and leaders go home \u2014 done." gen_action_next() } else { view.prompt = "Indians and leaders go home." } }, piece(p) { push_undo() game.go_home.who = p game.go_home.from = piece_space(p) game.state = 'go_home_to' }, next() { end_indians_and_leaders_go_home() }, } states.go_home_to = { inactive() { if (game.go_home.reason === 'late_season') inactive_prompt("indians and leaders go home", 0, 0) else inactive_prompt("raiders go home", 0, game.raid.where) }, prompt() { let who = game.go_home.who let from = game.go_home.from if (game.go_home.reason === 'late_season') view.prompt = `Indians and leaders go home \u2014 ${piece_name_and_place(who)}.` else view.prompt = `Raiders go home \u2014 ${piece_name_and_place(who)}.` view.who = who let can_go_home = false if (is_indian(who)) { let home = indians.space_from_piece[who] // 10.412: Cherokee have no home settlement if (home && has_friendly_allied_settlement(home) && !has_enemy_units(home)) { can_go_home = true gen_action_space(home) } if (has_unbesieged_friendly_leader(from)) { for (let to of find_closest_friendly_unbesieged_fortification(from)) { can_go_home = true gen_action_space(to) } } else if (game.go_home.follow && game.go_home.follow[from]) { can_go_home = true game.go_home.follow[from].forEach(gen_action_space) } } else { // Leader alone in the wilderness; or leaders, rangers, and coureurs after raid. for (let to of find_closest_friendly_unbesieged_fortification(from)) { can_go_home = true gen_action_space(to) } } if (!can_go_home || is_cherokee(who)) gen_action('eliminate') }, space(to) { let who = game.go_home.who let from = game.go_home.from push_go_home_summary(who, to) move_piece_to(who, to) if (is_indian(who)) { let home = indians.space_from_piece[who] game.count = 0 if (to !== home) { if (game.go_home.follow[from]) { if (game.go_home.follow[from].includes(to)) { game.count = 0 } else { game.go_home.follow[from].push(to) game.count = 1 } } else { game.go_home.follow[from] = [ to ] game.count = 1 } } if (game.count > 0 || can_follow_indians_home(from)) { game.go_home.to = to game.state = 'go_home_with_indians' } else { end_go_home_to() } } else { // Leader alone in the wilderness; or leaders, rangers and coureurs. if (is_leader(who)) { if (game.go_home.follow[from]) { if (!game.go_home.follow[from].includes(to)) game.go_home.follow[from].push(to) } else { game.go_home.follow[from] = [ to ] } } end_go_home_to() } }, eliminate() { eliminate_piece(game.go_home.who) end_go_home_to() }, } states.go_home_with_indians = { inactive() { if (game.go_home.reason === 'late_season') inactive_prompt("indians and leaders go home", 0, 0) else inactive_prompt("raiders go home", 0, game.raid.where) }, prompt() { let from = game.go_home.from let to = game.go_home.to if (game.go_home.reason === 'late_season') view.prompt = "Indians and leaders go home \u2014 " else view.prompt = "Raiders go home \u2014 " if (game.active === FRANCE) view.prompt += "leaders and coureurs may follow to " else view.prompt += "leaders may follow to " view.prompt += space_name(to) + "." view.where = to for (let p = first_friendly_leader; p <= last_friendly_leader; ++p) { if (is_piece_unbesieged_in_space(p, from)) gen_action_piece(p) } for (let p = first_friendly_unit; p <= last_friendly_unit; ++p) { if (is_coureurs(p) && is_piece_unbesieged_in_space(p, from)) gen_action_piece(p) } if (game.count === 0) gen_action_next() }, piece(p) { push_undo() let from = game.go_home.from let to = game.go_home.to push_go_home_summary(p, to) move_piece_to(p, to) if (game.count > 0 && is_leader(p)) game.count = 0 if (!can_follow_indians_home(from)) end_go_home_to() }, next() { push_undo() end_go_home_to() }, } function end_go_home_to() { game.go_home.who = 0 game.go_home.from = 0 game.go_home.to = 0 if (game.go_home.reason === 'late_season') game.state = 'indians_and_leaders_go_home' else game.state = 'raiders_go_home' } // LATE SEASON - REMOVE RAIDED MARKERS function goto_remove_raided_markers() { if (game.french.raids.length > 0) { logbr() log(`France removed ${game.french.raids.length} raided markers.`) award_french_vp(Math.ceil(game.french.raids.length / 2)) game.french.raids = [] } if (game.british.raids.length > 0) { logbr() log(`Britain removed ${game.british.raids.length} raided markers.`) award_british_vp(Math.ceil(game.british.raids.length / 2)) game.british.raids = [] } goto_winter_attrition() } // LATE SEASON - WINTER ATTRITION function is_unit_reduced_for_winter(p) { // WBC rules: one-step MD are considered reduced for winter attrition return is_unit_reduced(p) || is_one_step_marine_detachment(p) } function goto_winter_attrition() { set_active(FRANCE) game.state = 'winter_attrition' logbr() log(".h3 French Winter Attrition") logbr() resume_winter_attrition() } function resume_winter_attrition() { let done = true game.winter_attrition = {} for (let s = first_space; s <= last_space; ++s) { if (has_friendly_drilled_troops(s)) { let safe = false if (is_originally_friendly(s)) safe = true if (has_friendly_fortress(s)) safe = true if (has_friendly_fort(s) || has_friendly_stockade(s)) if (count_friendly_units_in_space(s) <= 4) safe = true let stack = { n: 0, dt: [] } for_each_friendly_unit_in_space(s, p => { if (is_drilled_troops(p)) { if (is_piece_inside(p) || !safe) { stack.dt.push(p) if (is_unit_reduced_for_winter(p)) stack.n++ } } }) if (stack.dt.length > 0) { // Never remove the last friendly step in a space. if (stack.n !== 1 || count_friendly_units_in_space(s) !== 1) { stack.n = Math.ceil(stack.n / 2) game.winter_attrition[s] = stack done = false } } } } if (done) end_winter_attrition() } function end_winter_attrition() { flush_summary() if (game.active === FRANCE) { set_active(BRITAIN) logbr() log(".h3 British Winter Attrition") logbr() resume_winter_attrition() } else { goto_victory_check() } } states.winter_attrition = { inactive: "winter attrition", prompt() { let done = true for (let s in game.winter_attrition) { let stack = game.winter_attrition[s] for (let p of stack.dt) { if (is_unit_reduced_for_winter(p)) { if (stack.n > 0) { done = false gen_action_piece(p) } } else { if (stack.n === 0) { done = false gen_action_piece(p) } } } } if (done) { view.prompt = "Winter Attrition \u2014 done." gen_action_next() } else { view.prompt = "Winter Attrition: Reduce drilled troops not in winter quarters." } }, piece(p) { let stack = game.winter_attrition[piece_space(p)] push_undo() if (is_unit_reduced_for_winter(p)) stack.n-- reduce_unit(p, true) remove_from_array(stack.dt, p) }, next() { end_winter_attrition() } } // LATE SEASON - VICTORY CHECK function are_all_british_controlled_spaces(list) { for (let i = 0; i < list.length; ++i) { let s = list[i] if (!is_british_controlled_space(s)) return false } return true } function is_enemy_controlled_fortress_for_vp(s) { // NOTE: active must be FRANCE if (has_unbesieged_friendly_units(s)) { if (is_space_besieged(s)) { // 13.12 British control unless besieging force qualifies to roll on siege table let cmd = find_friendly_commanding_leader_in_space(s) if (cmd && has_friendly_supplied_drilled_troops(s)) return false return true } return false } return true } function are_all_enemy_controlled_fortresses_for_vp(list) { let result = true for (let i = 0; i < list.length; ++i) { let s = list[i] if (!is_enemy_controlled_fortress_for_vp(s)) { result = false } } return result } function count_british_controlled_spaces(list) { let n = 0 for (let i = 0; i < list.length; ++i) { let s = list[i] if (is_british_controlled_space(s)) ++n } return n } function goto_victory_check() { set_active(FRANCE) if (are_all_british_controlled_spaces(fortresses) && are_all_british_controlled_spaces([NIAGARA, OHIO_FORKS])) return goto_game_over(BRITAIN, "Sudden Death: The British control all fortresses, Niagara, and Ohio Forks.") if (game.vp >= 11) return goto_game_over(FRANCE, "Sudden Death: France has 11 or more VP.") if (game.vp <= -11) return goto_game_over(BRITAIN, "Sudden Death: Britain has 11 or more VP.") if (game.year === 1760 && game.vp >= 8) return goto_game_over(FRANCE, "Sudden Death: France has 8 or more VP in 1760.") if (game.year === 1761 && game.vp >= 5) return goto_game_over(FRANCE, "Sudden Death: France has 5 or more VP in 1761.") if (game.year === game.end_year) { if (game.year === 1759) { // NOTE: active is FRANCE if (are_all_enemy_controlled_fortresses_for_vp(originally_british_fortresses) && count_british_controlled_spaces([QUEBEC, MONTREAL, NIAGARA, OHIO_FORKS]) >= 2) return goto_game_over(BRITAIN, "British Victory: Britain controls all its fortresses and two of Québec, Montréal, Niagara, and Ohio Forks.") if (game.vp >= 1) return goto_game_over(FRANCE, "French Victory: France has at least 1 VP.") if (game.vp <= -1) return goto_game_over(BRITAIN, "British Victory: Britain has at least 1 VP.") } if (game.year === 1762) { if (game.vp >= 1) return goto_game_over(FRANCE, "French Victory: France has at least 1 VP.") if (game.vp <= -5) return goto_game_over(BRITAIN, "British Victory: Britain has at least 5 VP.") } return goto_game_over("Draw", "The game is a draw.") } else { game.year++ start_year() } } function goto_game_over(result, victory) { logbr() log(victory) game.state = 'game_over' game.active = 'None' game.result = result game.victory = victory } states.game_over = { inactive() { view.prompt = game.victory }, prompt() { view.prompt = game.victory } } // DEMOLITION function gen_action_demolish() { for (let s of player.forts) { if (is_space_unbesieged(s)) { gen_action('demolish_fort') break } } if (player.forts_uc.length > 0) { gen_action('demolish_fort') } if (player.stockades.length > 0) { gen_action('demolish_stockade') } for (let s of game.fieldworks) { if (is_friendly_controlled_space(s) || has_unbesieged_friendly_units(s)) { gen_action('demolish_fieldworks') break } } } function goto_demolish_fort() { push_undo() game.demolish_state = game.state game.state = 'demolish_fort' } function goto_demolish_stockade() { push_undo() game.demolish_state = game.state game.state = 'demolish_stockade' } function goto_demolish_fieldworks() { push_undo() game.demolish_state = game.state game.state = 'demolish_fieldworks' } function end_demolish() { game.state = game.demolish_state delete game.demolish_state } states.demolish_fort = { inactive: "demolish fort", prompt() { view.prompt = "Demolish an unbesieged friendly fort." for (let s of player.forts) if (is_space_unbesieged(s)) gen_action_space(s) for (let s of player.forts_uc) gen_action_space(s) }, space(s) { if (has_friendly_fort_uc(s)) { log(`Demolished fort U/C at\n%${s}.`) // log(`Fort U/C (%${s}) demolished.`) // log(`Fort U/C at %${s} demolished.`) remove_friendly_fort_uc(s) } else if (has_friendly_fort(s)) { log(`Demolished fort at\n%${s}.`) // log(`Fort (%${s}) demolished.`) // log(`Fort at %${s} demolished.`) award_vp(-1) remove_friendly_fort(s) } end_demolish() } } states.demolish_stockade = { inactive: "demolish stockade", prompt() { view.prompt = "Demolish a friendly stockade." for (let s of player.stockades) gen_action_space(s) }, space(s) { log(`Demolished stockade at\n%${s}.`) // log(`Stockade (%${s}) demolished.`) // log(`Stockade at %${s} demolished.`) // log(`Demolished stockade (%${s}).`) remove_friendly_stockade(s) end_demolish() } } states.demolish_fieldworks = { inactive: "demolish fieldworks", prompt() { view.prompt = "Demolish friendly fieldworks." for (let s of game.fieldworks) if (is_friendly_controlled_space(s) || has_unbesieged_friendly_units(s)) gen_action_space(s) }, space(s) { remove_fieldworks(s) end_demolish() } } // CONSTRUCTION function format_remain(n) { if (n === 0) return " \u2014 done" return ": " + n + " left" } function goto_construct_stockades(card) { push_undo() discard_card(card, "to construct stockades") player.did_construct = 1 game.state = 'construct_stockades' game.count = cards[card].activation } states.construct_stockades = { inactive: "construct stockades", prompt() { view.prompt = `Construct Stockades${format_remain(game.count)}.` gen_action("end_construction") if (game.count > 0) { for (let s = first_space; s <= last_space; ++s) { if (has_friendly_supplied_drilled_troops(s) || is_originally_friendly(s)) { if (has_enemy_units(s)) continue if (has_enemy_fortifications(s)) continue if (has_friendly_fortifications(s)) continue if (is_space_besieged(s)) continue if (is_fortress(s)) continue gen_action_space(s) } } } }, space(s) { push_undo() log(`Stockade at %${s}.`) // log(`Constructed stockade at %${s}.`) // log(`Stockade at %${s} constructed.`) set_add(player.stockades, s) game.count -- }, end_construction() { end_action_phase() }, pass() { this.end_construction() }, next() { this.end_construction() }, } function goto_construct_forts(card) { push_undo() discard_card(card, "to construct forts") player.did_construct = 1 game.state = 'construct_forts' game.count = cards[card].activation game.list = [] } states.construct_forts = { inactive: "construct forts", prompt() { view.prompt = `Construct Forts${format_remain(game.count)}.` gen_action("end_construction") if (game.count > 0) { for (let s = first_space; s <= last_space; ++s) { if (has_friendly_supplied_drilled_troops(s)) { if (game.list.includes(s)) continue if (has_friendly_fort(s)) continue if (is_space_besieged(s)) continue if (is_fortress(s)) continue gen_action_space(s) } } } }, space(s) { push_undo() if (has_friendly_fort_uc(s)) { // log(`Finished building fort at %${s}.`) // log(`Fort (%${s}) built.`) log(`Fort at %${s}.`) place_friendly_fort(s) } else { // log(`Started building fort at %${s}.`) // log(`Fort U/C (%${s}) built.`) log(`Fort U/C at %${s}.`) place_friendly_fort_uc(s) game.list.push(s) // don't finish it in the same action phase } game.count -- }, end_construction() { delete game.list end_action_phase() }, pass() { this.end_construction() }, next() { this.end_construction() }, } // MAX TWO 7 COMMAND LEADERS function count_7_command_leaders_in_play() { let n = 0 if (is_piece_on_map(ABERCROMBY)) ++n if (is_piece_on_map(AMHERST)) ++n if (is_piece_on_map(BRADDOCK)) ++n if (is_piece_on_map(LOUDOUN)) ++n return n } function end_british_reinforcement() { delete game.leader if (count_7_command_leaders_in_play() > 2) { game.state = 'max_two_7_command_leaders_in_play' } else { delete game.seven end_action_phase() } } states.max_two_7_command_leaders_in_play = { inactive: "remove a 7 command leader", prompt() { if (count_7_command_leaders_in_play() > 2) { view.prompt = `Remove a 7 command leader from play.` if (!game.seven.includes(ABERCROMBY)) gen_action_piece(ABERCROMBY) if (!game.seven.includes(AMHERST)) gen_action_piece(AMHERST) if (!game.seven.includes(BRADDOCK)) gen_action_piece(BRADDOCK) if (!game.seven.includes(LOUDOUN)) gen_action_piece(LOUDOUN) } else { view.prompt = `Remove a 7 command leader from play \u2014 done.` gen_action_next() } }, piece(p) { push_undo() eliminate_piece(p) remove_from_array(game.seven, p) }, next() { delete game.seven end_action_phase() } } // EVENTS function can_play_massacre() { let s = moving_piece_space() if (is_enemy_card_available(MASSACRE)) return has_friendly_indians(s) && has_friendly_drilled_troops(s) return false } function goto_massacre(reason) { set_active_enemy() game.state = 'massacre_1' game.massacre = reason return goto_retroactive_foul_weather() } function end_massacre() { let reason = game.massacre delete game.massacre set_active_enemy() switch (reason) { case 'move': resume_move() break case 'assault': end_move_step(true) break case 'surrender': goto_surrender_place() break } } states.massacre_1 = { inactive() { inactive_prompt("massacre", moving_piece(), 0) }, prompt() { if (player.hand.includes(MASSACRE)) { view.prompt = `You may play "Massacre!"` gen_action('play_event', MASSACRE) } else { view.prompt = `You don't have "Massacre!"` } gen_action_pass() }, play_event(c) { play_card(c) award_vp(1) game.state = 'massacre_2' set_active_enemy() unstack_force(moving_piece()) }, pass() { end_massacre() } } states.massacre_2 = { inactive() { inactive_prompt("massacre", moving_piece(), 0) }, prompt() { let s = moving_piece_space() let done = true for (let p = 1; p <= last_piece; ++p) { if (is_indian(p) && is_piece_in_space(p, s)) { gen_action_piece(p) done = false } } if (done) { view.prompt = `Massacre! Eliminate all indians in ${space_name(s)} \u2014 done.` gen_action_next() } else { view.prompt = `Massacre! Eliminate all indians in ${space_name(s)}.` } }, piece(p) { eliminate_piece(p, false) }, next() { set_active_enemy() end_massacre() } } function can_place_in_space(s) { if (has_enemy_units(s)) return false if (has_enemy_fortifications(s)) return false return true } function can_restore_unit(p) { if (is_piece_on_map(p) && is_piece_unbesieged(p) && is_unit_reduced(p)) { if (is_militia(p)) return true // always in militia box if (is_drilled_troops(p)) return is_in_supply(piece_space(p)) return true } return false } function count_french_raids_in_southern_department() { let n = 0 for (let i = 0; i < game.french.raids.length; ++i) { if (is_southern_department(game.french.raids[i])) ++n } return n } function count_french_raids_in_northern_department() { let n = 0 for (let i = 0; i < game.french.raids.length; ++i) { if (is_northern_department(game.french.raids[i])) ++n } return n } events.provincial_regiments_dispersed_for_frontier_duty = { can_play() { let s = Math.min(count_french_raids_in_southern_department(), count_southern_provincials()) let n = Math.min(count_french_raids_in_northern_department(), count_northern_provincials()) return (s + n) > 0 }, play() { game.state = 'provincial_regiments_dispersed_for_frontier_duty' game.frontier_duty = { southern: Math.min(count_french_raids_in_southern_department(), count_southern_provincials()), northern: Math.min(count_french_raids_in_northern_department(), count_northern_provincials()), } } } states.provincial_regiments_dispersed_for_frontier_duty = { prompt() { let done = true for (let p = first_british_unit; p <= last_british_unit; ++p) { if ((game.frontier_duty.northern > 0 && is_northern_provincial(p)) || (game.frontier_duty.southern > 0 && is_southern_provincial(p))) { done = false gen_action_piece(p) } } if (done) { view.prompt = `Provincial Regiments Dispersed \u2014 done.` gen_action_next() } else { view.prompt = `Provincial Regiments Dispersed: Eliminate ${game.frontier_duty.southern} southern and ${game.frontier_duty.northern} northern provincials.` } }, piece(p) { push_undo() if (is_southern_provincial(p)) game.frontier_duty.southern -- if (is_northern_provincial(p)) game.frontier_duty.northern -- eliminate_piece(p) }, next() { delete game.frontier_duty end_action_phase() }, } events.northern_indian_alliance = { can_play() { return is_friendly_controlled_space(MONTREAL) }, play() { let roll = roll_die() if (game.vp > 4) game.count = roll else game.count = Math.ceil(roll / 2) if (has_friendly_fort(NIAGARA)) game.alliance = [ 'blue', 'blue-orange' ] else game.alliance = [ 'blue' ] game.state = 'indian_alliance' } } events.western_indian_alliance = { can_play() { return has_friendly_fort(OHIO_FORKS) }, play() { let roll = roll_die() if (game.vp > 4) game.count = roll else game.count = Math.ceil(roll / 2) if (has_friendly_fort(NIAGARA)) game.alliance = [ 'orange', 'blue-orange' ] else game.alliance = [ 'orange' ] game.state = 'indian_alliance' } } events.iroquois_alliance = { can_play() { let ff = has_friendly_fortifications(OSWEGO) || has_friendly_fortifications(ONEIDA_CARRY_WEST) || has_friendly_fortifications(ONEIDA_CARRY_EAST) let ef = has_enemy_fortifications(OSWEGO) || has_enemy_fortifications(ONEIDA_CARRY_WEST) || has_enemy_fortifications(ONEIDA_CARRY_EAST) if (ff && !ef) { if (game.active === BRITAIN) return set_has(within_two_of_gray_settlement, piece_space(JOHNSON)) return true } return false }, play() { let roll = roll_die() game.count = roll game.alliance = [ 'gray' ] game.state = 'indian_alliance' }, } function find_friendly_unused_indian(s) { for (let p of indians.pieces_from_space[s]) if (is_friendly_indian(p) && is_piece_unused(p)) return p return 0 } states.indian_alliance = { prompt() { let done = true for (let a of game.alliance) { if (game.count >= 1) { for (let p of indians.pieces_from_color[a]) { if (is_friendly_indian(p) && is_piece_unused(p)) { let s = indians.space_from_piece[p] if (!has_enemy_allied_settlement(s) && can_place_in_space(s)) { done = false gen_action_space(s) } } } } if (game.count >= 0.5) { for (let p of indians.pieces_from_color[a]) { if (is_friendly_indian(p) && can_restore_unit(p)) { done = false gen_action_piece(p) } } } } if (done) { view.prompt = `Indian Alliance \u2014 done.` gen_action_next() } else { view.prompt = `Indian Alliance: Place or restore ${game.alliance.join(" or ")} indians (${game.count} left).` } }, space(s) { push_undo() let p = find_friendly_unused_indian(s) if (p) { place_piece(p, s) game.count -= 1.0 if (has_unbesieged_enemy_leader(s) && !has_unbesieged_enemy_units(s)) goto_retreat_lone_leader(s, 'indian_alliance') } }, piece(p) { push_undo() restore_unit(p) game.count -= 0.5 }, next() { delete game.alliance end_action_phase() }, } // Used by Mohawks and Cherokees events. function place_indian(s, first, last) { push_undo() for (let p = first; p <= last; ++p) { if (is_piece_unused(p)) { place_piece(p, s) } } game.count = 0 } function can_place_indians(first, last) { for (let p = first; p <= last; ++p) if (is_piece_unused(p)) return true return false } function can_restore_unit_range(first, last) { for (let p = first; p <= last; ++p) if (can_restore_unit(p)) return true return false } function can_place_or_restore_indians(first, last) { return can_place_indians(first, last) || can_restore_unit_range(first, last) } function goto_restore_units(name, first, last) { if (can_restore_unit_range(first, last)) { game.state = 'restore_units' game.restore = { name, first, last } } else { end_action_phase() } } states.restore_units = { prompt() { let done = true for (let p = game.restore.first; p <= game.restore.last; ++p) { if (can_restore_unit(p)) { gen_action_piece(p) done = false } } if (done) { view.prompt = `Restore all ${game.restore.name} \u2014 done.` gen_action_next() } else { view.prompt = `Restore all ${game.restore.name}.` } }, piece(p) { restore_unit(p) }, next() { end_action_phase() } } events.mohawks = { can_play() { let s = piece_space(JOHNSON) if (set_has(within_two_of_canajoharie, s)) if (is_piece_unbesieged(JOHNSON)) return can_place_or_restore_indians(first_mohawk, last_mohawk) return false }, play() { if (can_place_indians(first_mohawk, last_mohawk)) { game.state = 'mohawks' game.count = 1 } else { goto_restore_units("Mohawks", first_mohawk, last_mohawk) } }, } states.mohawks = { prompt() { let done = true if (game.count > 0) { let s = piece_space(JOHNSON) if (can_place_in_space(s)) { done = false gen_action_space(s) } } if (done) { view.prompt = "Place all Mohawks not on the map with Johnson \u2014 done." gen_action_next() } else { view.prompt = "Place all Mohawks not on the map with Johnson." } }, space(s) { place_indian(s, first_mohawk, last_mohawk) }, next() { goto_restore_units("Mohawks", first_mohawk, last_mohawk) }, } events.cherokees = { can_play() { if (game.events.cherokee_uprising) return false return can_place_or_restore_indians(first_cherokee, last_cherokee) }, play() { game.events.cherokees = 1 if (can_place_indians(first_cherokee, last_cherokee)) { game.state = 'cherokees' game.count = 1 } else { goto_restore_units("Cherokees", first_cherokee, last_cherokee) } }, } states.cherokees = { prompt() { let done = true if (game.count > 0) { for (let s = first_southern_department; s <= last_southern_department; ++s) { if (has_unbesieged_friendly_fortifications(s)) { done = false gen_action_space(s) } } } if (done) { view.prompt = "Place all Cherokees not on the map at a British fortification in the southern dept \u2014 done." gen_action_next() } else { view.prompt = "Place all Cherokees not on the map at a British fortification in the southern dept." } }, space(s) { place_indian(s, first_cherokee, last_cherokee) }, next() { goto_restore_units("Cherokees", first_cherokee, last_cherokee) }, } events.cherokee_uprising = { can_play() { if (game.events.cherokees) return true return false }, play() { delete game.events.cherokees game.events.cherokee_uprising = 1 set_active_enemy() game.state = 'cherokee_uprising' game.uprising = { regular: 2, southern: 1 } }, } states.cherokee_uprising = { prompt() { let done = true for (let p = first_british_unit; p <= last_british_unit; ++p) { if (is_piece_on_map(p) && is_piece_unbesieged(p)) { let x = false if (game.uprising.regular > 0 && is_regular(p)) x = true if (game.uprising.southern > 0 && is_southern_provincial(p)) x = true if (is_cherokee(p)) x = true if (x) { done = false gen_action_piece(p) } } } if (done) { view.prompt = `Cherokee Uprising \u2014 done.` gen_action_next() } else { view.prompt = `Cherokee Uprising: Eliminate ${game.uprising.regular} regulars, ${game.uprising.southern} southern provincials, and all Cherokee.` } }, piece(p) { push_undo() if (is_regular(p)) game.uprising.regular -- if (is_southern_provincial(p)) game.uprising.southern -- eliminate_piece(p) }, next() { delete game.uprising set_active_enemy() end_action_phase() }, } events.treaty_of_easton = { can_play() { for (let s of in_or_adjacent_to_ohio_forks) if (has_unbesieged_friendly_fortifications(s) && has_british_drilled_troops(s)) return true return false }, play() { set_active_enemy() game.state = 'treaty_of_easton' }, } states.treaty_of_easton = { prompt() { let done = true for (let p = first_orange_indian; p <= last_orange_indian; ++p) { if (is_piece_on_map(p) && is_piece_unbesieged(p)) { gen_action_piece(p) done = false } } if (done) { view.prompt = "Treaty of Easton: Eliminate all unbesieged orange indians \u2014 done." gen_action_next() } else { view.prompt = "Treaty of Easton: Eliminate all unbesieged orange indians." } }, piece(p) { eliminate_piece(p) }, next() { set_active_enemy() end_action_phase() } } events.indians_desert = { play() { game.state = 'indians_desert' game.indians_desert = 0 game.count = 2 } } states.indians_desert = { prompt() { let can_desert = false if (game.count > 0) { for (let p = first_enemy_unit; p <= last_enemy_unit; ++p) { if (is_indian(p) && is_piece_on_map(p) && is_piece_unbesieged(p)) { if (!game.indians_desert || is_piece_in_space(p, game.indians_desert)) { can_desert = true gen_action_piece(p) } } } } if (can_desert) { view.prompt = `Indians Desert: Eliminate two indians from one space (${game.count} left).` } else { view.prompt = "Indians Desert: Eliminate two indians from one space \u2014 done." gen_action_next() } }, piece(p) { push_undo() if (!game.indians_desert) game.indians_desert = piece_space(p) eliminate_piece(p) game.count -- }, next() { delete game.indians_desert end_action_phase() }, } events.louisbourg_squadrons = { can_play() { return is_friendly_controlled_space(LOUISBOURG) }, play() { game.events.no_amphib = 1 let roll = roll_die() log("No amphibious landings this year.") if (roll <= 3) { log("No French naval moves ever.") log("British may play Quiberon.") log("Card removed.") game.events.no_fr_naval = 1 remove_card(LOUISBOURG_SQUADRONS) } else { log("No effect.") } end_action_phase() } } events.governor_vaudreuil_interferes = { can_play() { let n = 0 for (let p = first_enemy_leader; p <= last_enemy_leader; ++p) { if (is_piece_unbesieged(p)) if (!game.events.no_fr_naval || piece_space(p) !== LOUISBOURG) ++n } return n >= 2 }, play() { game.state = 'governor_vaudreuil_interferes' game.count = 1 game.swap = 0 }, } states.governor_vaudreuil_interferes = { inactive: "governor Vaudreuil interferes", prompt() { if (game.count > 0) { if (game.swap) { view.prompt = `Governor Vaudreuil Interferes: Reverse location of ${piece_name(game.swap)} and another French leader.` view.who = game.swap } else { view.prompt = "Governor Vaudreuil Interferes: Reverse location of two French leaders." } for (let p = first_enemy_leader; p <= last_enemy_leader; ++p) { if (is_piece_unbesieged(p)) if (!game.events.no_fr_naval || piece_space(p) !== LOUISBOURG) if (p !== game.swap) gen_action_piece(p) } } else { view.prompt = "Governor Vaudreuil Interferes \u2014 done." gen_action_next() } }, piece(p) { if (game.swap) { push_undo() let a = game.swap delete game.swap let a_loc = piece_space(a) let p_loc = piece_space(p) move_piece_to(a, p_loc) move_piece_to(p, a_loc) log(`${piece_name(a)} moved to %${p_loc}.`) log(`${piece_name(p)} moved to %${a_loc}.`) game.count = 0 } else { push_undo() game.swap = p } }, next() { end_action_phase() } } events.small_pox = { can_play() { for (let s = first_space; s <= last_space; ++s) if (count_units_in_space(s) > 4) return true return false }, play() { game.state = 'small_pox' }, } states.small_pox = { prompt() { view.prompt = "Small Pox: Choose a space with more than 4 units." for (let s = first_space; s <= last_space; ++s) if (count_units_in_space(s) > 4) gen_action_space(s) }, space(s) { log(`Small Pox at %${s}.`) let roll = roll_die() if (count_units_in_space(s) > 8) { game.count = roll } else { game.count = Math.ceil(roll / 2) } log(`Must eliminate ${game.count} steps.`) game.state = 'small_pox_eliminate_steps' game.small_pox = s set_active_enemy() }, } states.small_pox_eliminate_steps = { inactive() { inactive_prompt("small pox", 0, game.small_pox) }, prompt() { let done = true if (game.count > 0) { for_each_friendly_unit_in_space(game.small_pox, p => { if (can_reduce_unit(p)) { done = false gen_action_piece(p) } }) if (done) { for_each_friendly_unit_in_space(game.small_pox, p => { done = false gen_action_piece(p) }) } } if (done) { view.prompt = `Small Pox at ${space_name(game.small_pox)} \u2014 done.` gen_action_next() } else { view.prompt = `Small Pox at ${space_name(game.small_pox)}: Eliminate steps \u2014 ${game.count} left.` } }, piece(p) { push_undo() game.count -- reduce_unit(p, false) }, next() { if (has_any_indians(game.small_pox)) { game.state = 'small_pox_remove_indians' } else { end_small_pox() } }, } states.small_pox_remove_indians = { inactive() { inactive_prompt("small pox", 0, game.small_pox) }, prompt() { view.prompt = `Small Pox at ${space_name(game.small_pox)}: Remove all indians.` for_each_unit_in_space(game.small_pox, p => { if (is_indian(p)) gen_action_piece(p) }) }, piece(p) { eliminate_piece(p, false) if (!has_any_indians(game.small_pox)) end_small_pox() }, } function end_small_pox() { delete game.small_pox set_active_enemy() end_action_phase() } events.courier_intercepted = { can_play() { return enemy_player.hand.length > 0 }, play() { let roll = roll_die() if (roll >= 3) { let i = random(enemy_player.hand.length) let c = enemy_player.hand[i] enemy_player.hand.splice(i, 1) player.hand.push(c) log(`Stole ${card_name(c)}.`) } else { log("No effect.") } end_action_phase() }, } events.françois_bigot = { can_play() { return enemy_player.hand.length > 0 }, play() { let i = random(enemy_player.hand.length) let c = enemy_player.hand[i] enemy_player.hand.splice(i, 1) game.discard.push(c) log(`France discarded ${card_name(c)}.`) end_action_phase() }, } const british_ministerial_crisis_cards = [ 47, 48, 54, 57, 58, 59, 60, 61, 63, 64 ] events.british_ministerial_crisis = { can_play() { return enemy_player.hand.length > 0 }, play() { let n = 0 for (let i = 0; i < enemy_player.hand.length; ++i) { let c = enemy_player.hand[i] if (set_has(british_ministerial_crisis_cards, c)) ++n } if (n > 0) { set_active_enemy() game.state = 'british_ministerial_crisis' game.count = 1 } else { log("British player has none of the listed cards in hand.") end_action_phase() } }, } states.british_ministerial_crisis = { prompt() { if (game.count > 0) { view.prompt = "British Ministerial Crisis: Discard a British Regulars, Highlanders, Light Infantry, Transports, or Victories card." for (let i = 0; i < player.hand.length; ++i) { let c = player.hand[i] if (set_has(british_ministerial_crisis_cards, c)) gen_action_discard(c) } } else { view.prompt = "British Ministerial Crisis \u2014 done." gen_action_next() } }, card(c) { push_undo() game.count = 0 discard_card(c) }, next() { set_active_enemy() end_action_phase() } } function count_southern_provincials() { let n = 0 for (let p = first_southern_provincial; p <= last_southern_provincial; ++p) if (is_piece_on_map(p)) ++n return n } function count_northern_provincials() { let n = 0 for (let p = first_northern_provincial; p <= last_northern_provincial; ++p) if (is_piece_on_map(p)) ++n return n } function count_unbesieged_southern_provincials() { let n = 0 for (let p = first_southern_provincial; p <= last_southern_provincial; ++p) if (is_piece_on_map(p) && is_piece_unbesieged(p)) ++n return n } function count_unbesieged_northern_provincials() { let n = 0 for (let p = first_northern_provincial; p <= last_northern_provincial; ++p) if (is_piece_on_map(p) && is_piece_unbesieged(p)) ++n return n } function can_restore_southern_provincial_regiments() { for (let p = first_southern_provincial; p <= last_southern_provincial; ++p) if (can_restore_unit(p)) return true return false } function can_restore_northern_provincial_regiments() { for (let p = first_northern_provincial; p <= last_northern_provincial; ++p) if (can_restore_unit(p)) return true return false } events.stingy_provincial_assembly = { can_play() { if (game.pa === ENTHUSIASTIC) return false let num_n = count_unbesieged_northern_provincials() let num_s = count_unbesieged_southern_provincials() return (num_n + num_s) > 0 }, play() { let num_n = count_unbesieged_northern_provincials() let num_s = count_unbesieged_southern_provincials() if (num_n > 0 && num_s === 0) { goto_stingy_provincial_assembly('northern') } else if (num_n === 0 && num_s > 0) { goto_stingy_provincial_assembly('southern') } else { game.state = 'stingy_provincial_assembly_department' game.count = 1 } } } states.stingy_provincial_assembly_department = { prompt() { view.prompt = "Stingy Provincial Assembly: Choose a department." gen_action('northern') gen_action('southern') }, northern() { goto_stingy_provincial_assembly('northern') }, southern() { goto_stingy_provincial_assembly('southern') }, } function goto_stingy_provincial_assembly(dept) { set_active_enemy() game.state = 'stingy_provincial_assembly' game.department = dept game.count = 1 } states.stingy_provincial_assembly = { prompt() { if (game.count > 0) { view.prompt = `Stingy Provincial Assembly: Remove a ${game.department} provincial unit.` if (game.department === 'northern') { for (let p = first_northern_provincial; p <= last_northern_provincial; ++p) if (is_piece_unbesieged(p)) gen_action_piece(p) } else { for (let p = first_southern_provincial; p <= last_southern_provincial; ++p) if (is_piece_unbesieged(p)) gen_action_piece(p) } } else { view.prompt = `Stingy Provincial Assembly \u2014 done.` gen_action_next() } }, piece(p) { push_undo() game.count = 0 eliminate_piece(p) }, next() { set_active_enemy() end_action_phase() }, } events.british_colonial_politics = { can_play() { if (game.active === FRANCE) return game.pa > 0 return game.pa < 2 }, play() { if (game.active === FRANCE) { game.pa -= 1 log(`Provincial Assemblies reduced to ${pa_name()}.`) goto_british_colonial_politics() } else { game.pa += 1 log(`Provincial Assemblies increased to ${pa_name()}.`) end_action_phase() } }, } function pa_name() { switch (game.pa) { case RELUCTANT: return "Reluctant" case SUPPORTIVE: return "Supportive" case ENTHUSIASTIC: return "Enthusiastic" } } const southern_provincial_limit = [ 2, 4, 6 ] const northern_provincial_limit = [ 6, 10, 18 ] function goto_british_colonial_politics() { if (game.pa < ENTHUSIASTIC) { let num_s = count_southern_provincials() let num_n = count_northern_provincials() let max_n = northern_provincial_limit[game.pa] let max_s = southern_provincial_limit[game.pa] if (num_s > max_s || num_n > max_n) { set_active_enemy() game.state = 'british_colonial_politics' return } } end_action_phase() } states.british_colonial_politics = { prompt() { let num_s = count_southern_provincials() let num_n = count_northern_provincials() let max_n = northern_provincial_limit[game.pa] let max_s = southern_provincial_limit[game.pa] let done = true if (num_s > max_s) { for (let p = first_southern_provincial; p <= last_southern_provincial; ++p) { if (is_piece_unbesieged(p)) { gen_action_piece(p) done = false } } } if (num_n > max_n) { for (let p = first_northern_provincial; p <= last_northern_provincial; ++p) { if (is_piece_unbesieged(p)) { gen_action_piece(p) done = false } } } if (done) { view.prompt = `British Colonial Politics \u2014 done.` gen_action_next() } else { view.prompt = `British Colonial Politics: Remove provincials over limit \u2014 ${num_s}/${max_s} southern, ${num_n}/${max_n} northern.` } }, piece(p) { push_undo() eliminate_piece(p) }, next() { set_active_enemy() end_action_phase() }, } function can_raise_southern_provincial_regiments() { let num = count_southern_provincials() let max = southern_provincial_limit[game.pa] return num < max } function can_raise_northern_provincial_regiments() { let num = count_northern_provincials() let max = northern_provincial_limit[game.pa] return num < max } events.raise_provincial_regiments = { can_play() { if (game.pa === RELUCTANT) return false if (can_raise_northern_provincial_regiments() || can_restore_northern_provincial_regiments()) return true if (can_raise_southern_provincial_regiments() || can_restore_southern_provincial_regiments()) return true return false }, play() { game.state = 'raise_provincial_regiments_where' }, } states.raise_provincial_regiments_where = { prompt() { view.prompt = "Raise Provincial Regiments in which department?" if (can_raise_northern_provincial_regiments() || can_restore_northern_provincial_regiments()) gen_action('northern') if (can_raise_southern_provincial_regiments() || can_restore_southern_provincial_regiments()) gen_action('southern') }, northern() { push_undo() let num = count_northern_provincials() let max = northern_provincial_limit[game.pa] game.state = 'raise_provincial_regiments' game.count = clamp(max - num, 0, 4) game.department = 'northern' game.did_raise = 0 if (game.count === 0) goto_restore_provincial_regiments() }, southern() { push_undo() let num = count_southern_provincials() let max = southern_provincial_limit[game.pa] game.state = 'raise_provincial_regiments' game.count = clamp(max - num, 0, 2) game.department = 'southern' game.did_raise = 0 if (game.count === 0) goto_restore_provincial_regiments() }, } states.raise_provincial_regiments = { prompt() { let done = true if (!game.did_raise) { if (game.department === 'northern' && can_restore_northern_provincial_regiments()) { done = false gen_action('restore') } if (game.department === 'southern' && can_restore_southern_provincial_regiments()) { done = false gen_action('restore') } } if (game.count > 0) { if (game.department === 'northern') { for (let s = first_northern_department; s <= last_northern_department; ++s) { if (has_unbesieged_friendly_fortifications(s)) { done = false gen_action_space(s) } } } if (game.department === 'southern') { for (let s = first_southern_department; s <= last_southern_department; ++s) { if (has_unbesieged_friendly_fortifications(s)) { done = false gen_action_space(s) } } } } if (done) { view.prompt = `Raise Provincial Regiments \u2014 done.` gen_action_next() } else { if (game.did_raise) view.prompt = `Raise Provincial Regiments in ${game.department} department (${game.count} left).` else view.prompt = `Raise Provincial Regiments in ${game.department} department (${game.count} left) or restore all to full.` } }, restore() { push_undo() goto_restore_provincial_regiments() }, space(s) { push_undo() let p = find_unused_provincial(game.department) place_piece(p, s) game.count -- game.did_raise = 1 }, next() { delete game.did_raise delete game.department end_action_phase() } } function goto_restore_provincial_regiments() { game.count = 0 delete game.did_raise if (game.department === 'northern') { delete game.department goto_restore_units("Northern Provincials", first_northern_provincial, last_northern_provincial) } else { delete game.department goto_restore_units("Southern Provincials", first_southern_provincial, last_southern_provincial) } } function is_card_removed(card) { return game.removed.includes(card) } events.quiberon_bay = { can_play() { if (is_card_removed(LOUISBOURG_SQUADRONS)) return true if (is_friendly_controlled_space(LOUISBOURG)) return true if (game.year > 1759) return true return false }, play() { game.events.quiberon = 1 delete game.events.diplo end_action_phase() }, } function is_friendly_siege(space) { if (has_friendly_fort(space)) return true if (is_fortress(space)) return has_unbesieged_enemy_units(space) return false } events.bastions_repaired = { can_play() { let result = false for_each_siege((space, level) => { if (level > 0 && is_friendly_siege(space)) result = true }) return result }, play() { game.state = 'bastions_repaired' game.count = 1 }, } states.bastions_repaired = { prompt() { if (game.count > 0) { view.prompt = "Bastions Repaired: Replace a siege 1 or siege 2 marker on the map with siege 0." for_each_siege((space, level) => { if (level > 0 && is_friendly_siege(space)) gen_action_space(space) }) } else { view.prompt = "Bastions Repaired \u2014 done." gen_action_next() } }, space(s) { push_undo() log(`Replaced siege marker at %${s} with siege 0.`) game.sieges[s] = 0 game.count = 0 }, next() { end_action_phase() }, } function is_colonial_recruit(p) { return is_coureurs(p) || is_ranger(p) || is_light_infantry(p) || is_provincial(p) } events.colonial_recruits = { can_play() { for (let p = first_friendly_unit; p <= last_friendly_unit; ++p) if (can_restore_unit(p)) return true return false }, play() { let roll = roll_die() game.state = 'colonial_recruits' game.count = roll }, } states.colonial_recruits = { prompt() { let done = true if (game.count > 0) { for (let p = first_friendly_unit; p <= last_friendly_unit; ++p) { if (is_colonial_recruit(p)) { if (can_restore_unit(p)) { done = false gen_action_piece(p) } } } } if (done) { view.prompt = `Colonial Recruits \u2014 done.` gen_action_next() } else { view.prompt = `Colonial Recruits: Restore ${game.count} reduced colonial recruits.` } }, piece(p) { push_undo() restore_unit(p) game.count -- }, next() { end_action_phase() }, } function can_restore_regular_or_light_infantry_units() { for (let p = first_friendly_unit; p <= last_friendly_unit; ++p) if (is_regular(p) || is_light_infantry(p)) if (can_restore_unit(p)) return true return false } events.troop_transports_and_local_enlistments = { can_play() { if (game.active === FRANCE) { if (game.events.quiberon) return false if (is_british_controlled_space(QUEBEC)) return false } return can_restore_regular_or_light_infantry_units() }, play() { game.state = 'restore_regular_or_light_infantry_units' if (game.active === FRANCE) game.count = 3 else game.count = 6 }, } events.victories_in_germany_release_troops_and_finances_for_new_world = { can_play() { if (game.year <= 1755) return false if (game.active === FRANCE) { if (game.events.quiberon) return false if (is_british_controlled_space(QUEBEC)) return false } return can_restore_regular_or_light_infantry_units() }, play() { game.state = 'restore_regular_or_light_infantry_units' game.count = roll_die() }, } states.restore_regular_or_light_infantry_units = { prompt() { let done = true if (game.count > 0) { for (let p = first_friendly_unit; p <= last_friendly_unit; ++p) { if (is_regular(p) || is_light_infantry(p)) { if (can_restore_unit(p)) { done = false gen_action_piece(p) } } } } if (done) { view.prompt = `Restore reduced regular or light infantry \u2014 done.` gen_action_next() } else { view.prompt = `Restore ${game.count} reduced regular or light infantry.` } }, piece(p) { push_undo() restore_unit(p) game.count -- }, next() { end_action_phase() }, } events.call_out_militias = { can_play() { if (game.active === FRANCE) { for (let p = first_french_militia; p <= last_french_militia; ++p) if (is_piece_unused(p) || is_unit_reduced(p)) return true } else { for (let p = first_british_militia; p <= last_british_militia; ++p) if (is_piece_unused(p) || is_unit_reduced(p)) return true } return false }, play() { game.state = 'call_out_militias' game.count = 2 } } states.call_out_militias = { prompt() { let done = true if (game.count === 2) { if (game.active === BRITAIN) { if (find_unused_friendly_militia()) { done = false gen_action_space(SOUTHERN_COLONIAL_MILITIAS) gen_action_space(NORTHERN_COLONIAL_MILITIAS) } } else { if (find_unused_friendly_militia()) { done = false gen_action_space(ST_LAWRENCE_CANADIAN_MILITIAS) } } } if (game.count > 0) { if (game.active === BRITAIN) { for (let p = first_british_militia; p <= last_british_militia; ++p) { if (is_piece_on_map(p) && is_unit_reduced(p)) { done = false gen_action_piece(p) } } } else { for (let p = first_french_militia; p <= last_french_militia; ++p) { if (is_piece_on_map(p) && is_unit_reduced(p)) { done = false gen_action_piece(p) } } } } if (done) { view.prompt = `Call Out Militias \u2014 done.` gen_action_next() } else { if (game.count < 2) view.prompt = `Call Out Militias: Restore another militia to full strength.` else view.prompt = `Call Out Militias: Place one militia into a militia box, or restore 2 to full strength.` } }, space(s) { push_undo() let p = find_unused_friendly_militia() place_piece(p, s) game.count -= 2 }, piece(p) { push_undo() restore_unit(p) game.count -= 1 }, next() { end_action_phase() }, } events.rangers = { play() { game.state = 'rangers' game.count = 2 } } states.rangers = { prompt() { let done = true if (game.count === 2) { if (find_unused_ranger()) { for (let s = first_space; s <= last_space; ++s) { if (has_unbesieged_friendly_fortifications(s)) { done = false gen_action_space(s) } } } } if (game.count > 0) { for (let p = first_friendly_unit; p <= last_friendly_unit; ++p) { if (is_ranger(p)) { if (can_restore_unit(p)) { done = false gen_action_piece(p) } } } } if (done) { view.prompt = `Rangers \u2014 done.` gen_action_next() } else { if (game.count < 2) view.prompt = `Rangers: Restore another ranger to full strength.` else view.prompt = `Rangers: Place a ranger at a fortification, or restore 2 to full strength.` } }, space(s) { push_undo() let p = find_unused_ranger() place_piece(p, s) game.count -= 2 }, piece(p) { push_undo() restore_unit(p) game.count -= 1 }, next() { end_action_phase() }, } events.french_regulars = { can_play() { if (game.events.french_regulars) return false if (game.events.quiberon) return false if (!has_british_units(QUEBEC) && !has_amphib(QUEBEC)) return true if (!has_british_units(LOUISBOURG) && !has_amphib(LOUISBOURG)) return true return false }, play() { game.state = 'french_regulars' if (game.events.once_french_regulars) { game.leader = 1 move_piece_to(MONTCALM, leader_box(MONTCALM)) move_piece_to(LEVIS, leader_box(LEVIS)) move_piece_to(BOUGAINVILLE, leader_box(BOUGAINVILLE)) delete game.events.once_french_regulars } game.count = 2 if (game.options.regulars_vp && game.year <= 1756) award_vp(-1) } } states.french_regulars = { prompt() { if (game.count > 0) { if (game.leader) view.prompt = `French Regulars: Place Montcalm, Lévis, Bougainville and two regulars at either Québec or Louisbourg.` else view.prompt = `French Regulars: Place two regulars at either Québec or Louisbourg.` } else { view.prompt = `French Regulars \u2014 done.` } if (game.count > 0) { if (!has_british_units(QUEBEC) && !has_amphib(QUEBEC)) gen_action_space(QUEBEC) if (!has_british_units(LOUISBOURG) && !has_amphib(LOUISBOURG)) gen_action_space(LOUISBOURG) } else { gen_action_next() } }, space(s) { push_undo() if (game.leader) { place_piece(MONTCALM, s) place_piece(LEVIS, s) place_piece(BOUGAINVILLE, s) } for (let p = first_french_regular; p <= last_french_regular && game.count > 0; ++p) { if (is_piece_unused(p)) { place_piece(p, s) --game.count } } game.count = 0 }, next() { game.events.french_regulars = 1 delete game.leader end_action_phase() }, } events.light_infantry = { play() { game.state = 'light_infantry' game.count = 2 game.leader = draw_leader_from_pool() } } states.light_infantry = { prompt() { if (game.leader) { view.prompt = `Light Infantry: Place ${piece_name(game.leader)} at any fortress.` view.who = game.leader } else { if (game.count > 0) view.prompt = `Light Infantry: Place ${game.count} light infantry at any fortresses.` else view.prompt = `Light Infantry \u2014 done.` } if (game.count > 0) { for (let s = first_space; s <= last_space; ++s) { if (has_unbesieged_friendly_fortress(s)) { gen_action_space(s) } } } if (game.count === 0) gen_action_next() }, space(s) { push_undo() if (game.leader) { place_piece(game.leader, s) game.leader = 0 } else { let p = find_unused_light_infantry() if (p) { place_piece(p, s) game.count -- } else { log("No more Light Infantry units available.") game.count = 0 } } }, next() { end_british_reinforcement() }, } function can_place_in_british_ports() { for (let i = 0; i < ports.length; ++i) if (is_british_controlled_space(ports[i])) return true return game.amphib.length > 0 } events.british_regulars = { can_play() { if (game.events.british_regulars) return false return can_place_in_british_ports() }, play() { game.state = 'british_regulars' game.count = 3 game.leader = draw_leader_from_pool() if (game.options.regulars_vp && game.year <= 1756) award_vp(-1) } } states.british_regulars = { prompt() { if (game.leader) { view.prompt = `British Regulars: Place ${piece_name(game.leader)} at any port.` view.who = game.leader } else { if (game.count > 0) view.prompt = `British Regulars: Place ${game.count} regulars at any ports.` else view.prompt = `British Regulars \u2014 done.` } if (game.count > 0) { for_each_british_controlled_port_and_amphib(s => { if (can_place_in_space(s)) gen_action_space(s) }) } else { gen_action_next() } }, space(s) { push_undo() if (game.leader) { place_piece(game.leader, s) game.leader = 0 } else { let p = find_unused_british_regular() if (p) { place_piece(p, s) game.count -- } else { game.count = 0 } } }, next() { game.events.british_regulars = 1 end_british_reinforcement() }, } events.highlanders = { can_play() { if (game.events.pitt || game.year > 1758) return can_place_in_british_ports() return false }, play(card) { game.state = 'highlanders' game.leader = [] if (card === 60) { game.count = 4 for (let i = 0; i < 2; ++i) { let p = draw_leader_from_pool() if (p) game.leader.push(p) } } else { game.count = 1 let p = draw_leader_from_pool() if (p) game.leader.push(p) } } } states.highlanders = { prompt() { if (game.leader.length > 0) { let p = game.leader[0] view.prompt = `Highlanders: Place ${piece_name(p)} at any port.` view.who = p } else { if (game.count > 0) view.prompt = `Highlanders: Place ${game.count} highlanders at any ports.` else view.prompt = `Highlanders \u2014 done.` } if (game.count > 0) { for_each_british_controlled_port_and_amphib(s => { if (can_place_in_space(s)) gen_action_space(s) }) } else { gen_action_next() } }, space(s) { push_undo() if (game.leader.length > 0) { let p = game.leader.shift() place_piece(p, s) } else { let p = find_unused_highland() if (p) { place_piece(p, s) game.count -- } else { game.count = 0 } } }, next() { end_british_reinforcement() }, } events.royal_americans = { can_play() { for (let s = first_northern_department; s <= last_northern_department; ++s) if (has_unbesieged_friendly_fortress(s)) return true for (let s = first_southern_department; s <= last_southern_department; ++s) if (has_unbesieged_friendly_fortress(s)) return true return false }, play() { game.state = 'royal_americans' game.count = 4 game.leader = draw_leader_from_pool() } } states.royal_americans = { prompt() { if (game.leader) { let p = game.leader view.prompt = `Royal Americans: Place ${piece_name(p)} at any fortress in the departments.` view.who = p } else { if (game.count > 0) view.prompt = `Royal Americans: Place ${game.count} royal americans at any fortress in the departments.` else view.prompt = `Royal Americans \u2014 done.` } if (game.count > 0) { for (let s = first_northern_department; s <= last_northern_department; ++s) if (has_unbesieged_friendly_fortress(s)) gen_action_space(s) for (let s = first_southern_department; s <= last_southern_department; ++s) if (has_unbesieged_friendly_fortress(s)) gen_action_space(s) } else { gen_action_next() } }, space(s) { push_undo() if (game.leader) { place_piece(game.leader, s) game.leader = 0 } else { let p = find_unused_royal_american() if (p) { place_piece(p, s) game.count -- } else { game.count = 0 } } }, next() { end_british_reinforcement() }, } events.acadians_expelled = { can_play() { if (game.options.acadians) return true return game.active === BRITAIN }, play() { game.state = 'acadians_expelled_place_regulars' }, } states.acadians_expelled_place_regulars = { inactive: 'Acadians expelled (place regulars)', prompt() { view.prompt = "Acadians Expelled: Place two Regulars at Halifax." gen_action_space(HALIFAX) }, space() { for (let i = 0; i < 2; ++i) { let p = find_unused_british_regular() place_piece(p, HALIFAX) } game.acadians = game.active set_active(FRANCE) game.state = 'acadians_expelled_place_coureurs' }, } states.acadians_expelled_place_coureurs = { inactive: 'Acadians expelled (place coureurs)', prompt() { view.prompt = "Acadians Expelled: Place a Coureurs unit at Québec or Louisbourg." let quebec = has_british_units(QUEBEC) || has_amphib(QUEBEC) let louisbourg = has_british_units(LOUISBOURG) || has_amphib(LOUISBOURG) if (!quebec) gen_action_space(QUEBEC) if (!louisbourg) gen_action_space(LOUISBOURG) if (quebec && louisbourg) gen_action_pass() }, space(s) { push_undo() let p = find_unused_coureurs() if (p) place_piece(p, s) game.state = 'acadians_expelled_restore_coureurs_and_militia' }, pass() { set_active(game.acadians) delete game.acadians end_action_phase() }, } states.acadians_expelled_restore_coureurs_and_militia = { inactive: 'Acadians expelled (restore coureurs and militia)', prompt() { let done = true for (let p = first_french_militia; p <= last_french_militia; ++p) { if (can_restore_unit(p)) { done = false gen_action_piece(p) } } for (let p = first_coureurs; p <= last_coureurs; ++p) { if (can_restore_unit(p)) { done = false gen_action_piece(p) } } if (done) { view.prompt = "Acadians Expelled: Restore all Coureurs and Militia \u2014 done." gen_action_next() } else { view.prompt = "Acadians Expelled: Restore all Coureurs and Militia." } }, piece(p) { restore_unit(p) }, next() { set_active(game.acadians) delete game.acadians end_action_phase() }, } const william_pitt_cards = [ 'highlanders', 'british_regulars', 'light_infantry', 'troop_transports_and_local_enlistments' ] events.william_pitt = { play() { game.events.pitt = 1 game.state = 'william_pitt' game.count = 1 place_amherst_forbes_and_wolfe_in_pool(true) } } states.william_pitt = { prompt() { if (game.count > 0) { view.prompt = "William Pitt: Draw Highlanders, British Regulars, Light Infantry or Troop Transports from discard." view.hand = game.discard for (let c of game.discard) { if (william_pitt_cards.includes(cards[c].event)) gen_action('card', c) } } else { view.prompt = "William Pitt \u2014 done." } gen_action_next() }, card(c) { push_undo() log(`Drew ${card_name(c)} from discard.`) remove_from_array(game.discard, c) player.hand.push(c) game.count = 0 }, next() { end_action_phase() } } const diplomatic_revolution_cards = [ 'french_regulars', 'troop_transports_and_local_enlistments' ] events.diplomatic_revolution = { can_play() { return !game.events.quiberon }, play() { game.events.diplo = 1 game.state = 'diplomatic_revolution' game.count = 1 } } states.diplomatic_revolution = { prompt() { if (game.count > 0) { view.prompt = "Diplomatic Revolution: Draw French Regulars or Troop Transports from discard." view.hand = game.discard for (let c of game.discard) { if (diplomatic_revolution_cards.includes(cards[c].event)) gen_action('card', c) } } else { view.prompt = "Diplomatic Revolution \u2014 done." } gen_action_next() }, card(c) { push_undo() log(`Drew ${card_name(c)} from discard.`) remove_from_array(game.discard, c) player.hand.push(c) game.count = 0 }, next() { end_action_phase() } } states.discard_to_draw_regulars = { prompt() { view.prompt = `Exchange random card with British Regulars or Highlanders from discard?` gen_action('exchange') gen_action('pass') }, exchange() { push_undo() game.state = 'draw_regulars' }, pass() { start_action_phase() }, } states.draw_regulars = { prompt() { view.prompt = `Draw one British Regulars or Highlanders from the discard.` view.hand = game.discard for (let c of game.discard) { if (cards[c].event === 'british_regulars' || cards[c].event === 'highlanders') gen_action('card', c) } }, card(c) { let x = player.hand[random(player.hand.length)] remove_from_array(player.hand, x) game.discard.push(x) remove_from_array(game.discard, c) player.hand.push(c) log(`Exchanged ${card_name(x)} for ${card_name(c)} in discard.`) start_action_phase() }, } events.intrigues_against_shirley = { can_play() { return game.vp >= 1 && is_piece_on_map(SHIRLEY) && is_piece_unbesieged(SHIRLEY) }, play() { game.state = 'intrigues_against_shirley' } } states.intrigues_against_shirley = { prompt() { view.prompt = "Intrigues Against Shirley: Eliminate Shirley." gen_action_piece(SHIRLEY) }, piece() { eliminate_piece(SHIRLEY) end_action_phase() }, } // SETUP exports.scenarios = [ "Annus Mirabilis", "Annus Mirabilis (WBC)", "Early War Campaign", "Late War Campaign", "The Full Campaign", ] exports.roles = [ FRANCE, BRITAIN, ] function setup_markers(m, list) { for (let name of list) set_add(m, find_space(name)) } function setup_leader(where, who) { who = find_unused_piece(who) where = find_space(where) game.location[who] = where } function setup_unit(where, who) { who = find_unused_piece(who) where = find_space(where) game.location[who] = where } function setup_1757(end_year, start_vp) { game.year = 1757 game.end_year = end_year game.season = EARLY game.vp = start_vp game.pa = SUPPORTIVE for (let i = 1; i <= 62; ++i) game.deck.push(i) for (let i = 63; i <= 70; ++i) game.removed.push(i) setup_markers(game.french.allied, [ "Mingo Town", "Logstown", "Pays d'en Haut", "Mississauga", ]) setup_markers(game.french.forts, [ "Ticonderoga", "Crown Point", "Niagara", "Ohio Forks", ]) setup_markers(game.french.stockades, [ "Île-aux-Noix", "St-Jean", "Oswegatchie", "Cataraqui", "Toronto", "Presqu'île", "French Creek", "Venango", ]) setup_leader("Louisbourg", "Drucour") setup_unit("Louisbourg", "Marine") setup_unit("Louisbourg", "Artois") setup_unit("Louisbourg", "Bourgogne") setup_unit("Louisbourg", "Boishébert Acadian") setup_leader("Québec", "Lévis") setup_unit("Québec", "Marine") setup_unit("Québec", "Guyenne") setup_unit("Québec", "La Reine") setup_leader("Montréal", "Montcalm") setup_leader("Montréal", "Vaudreuil") setup_unit("Montréal", "Béarn") setup_unit("Montréal", "La Sarre") setup_unit("Montréal", "Repentigny") setup_unit("Montréal", "Huron") setup_unit("Montréal", "Potawatomi") setup_unit("Montréal", "Ojibwa") setup_unit("Montréal", "Mississauga") setup_unit("Crown Point", "Marine Detachment") setup_unit("Crown Point", "Perière") setup_leader("Ticonderoga", "Rigaud") setup_leader("Ticonderoga", "Bougainville") setup_unit("Ticonderoga", "Languedoc") setup_unit("Ticonderoga", "Royal Roussillon") setup_unit("Ticonderoga", "Marin") setup_leader("Cataraqui", "Villiers") setup_unit("Cataraqui", "Marine Detachment") setup_unit("Cataraqui", "Léry") setup_unit("Niagara", "Marine Detachment") setup_unit("Niagara", "Joncaire") setup_unit("Presqu'île", "Marine Detachment") setup_unit("French Creek", "Marine Detachment") setup_unit("Venango", "Langlade") setup_leader("Ohio Forks", "Dumas") setup_unit("Ohio Forks", "Marine Detachment") setup_unit("Ohio Forks", "Marine Detachment") setup_unit("Ohio Forks", "Ligneris") setup_unit("Logstown", "Shawnee") setup_unit("Mingo Town", "Mingo") setup_leader("eliminated", "Dieskau") setup_leader("eliminated", "Beaujeu") setup_markers(game.british.forts, [ "Hudson Carry South", "Hudson Carry North", "Will's Creek", "Shamokin", ]) setup_markers(game.british.forts_uc, [ "Winchester", "Shepherd's Ferry", ]) setup_markers(game.british.stockades, [ "Schenectady", "Hoosic", "Charlestown", "Augusta", "Woodstock", "Carlisle", "Harris's Ferry", "Lancaster", "Reading", "Easton", ]) setup_unit("Winchester", "Virginia") setup_unit("Shepherd's Ferry", "Maryland") setup_unit("Carlisle", "Pennsylvania") setup_unit("Shamokin", "Pennsylvania") setup_unit("Philadelphia", "1/60th") setup_leader("New York", "Loudoun") setup_leader("New York", "Abercromby") setup_unit("New York", "22nd") setup_unit("New York", "27th") setup_unit("New York", "35th") setup_unit("New York", "2/60th") setup_unit("New York", "3/60th") setup_unit("New York", "4/60th") setup_leader("Albany", "Dunbar") setup_unit("Albany", "44th") setup_unit("Albany", "48th") setup_leader("Hudson Carry South", "Webb") setup_unit("Hudson Carry South", "Rogers") setup_unit("Hudson Carry South", "Massachusetts") setup_unit("Hudson Carry South", "Connecticut") setup_unit("Hudson Carry South", "Rhode Island") setup_unit("Hudson Carry North", "New Hampshire") setup_unit("Hudson Carry North", "New Jersey") setup_leader("Schenectady", "Johnson") setup_unit("Schenectady", "New York") setup_unit("Schenectady", "1/42nd") setup_leader("Halifax", "Monckton") setup_unit("Halifax", "40th") setup_unit("Halifax", "45th") setup_unit("Halifax", "47th") setup_unit("Southern Colonial Militias", "Colonial Militia") game.british.pool.push(find_unused_piece("Amherst")) game.british.pool.push(find_unused_piece("Bradstreet")) game.british.pool.push(find_unused_piece("Forbes")) game.british.pool.push(find_unused_piece("Murray")) game.british.pool.push(find_unused_piece("Wolfe")) setup_leader("eliminated", "Braddock") setup_leader("eliminated", "Shirley") game.events.pitt = 1 game.events.diplo = 1 } function setup_1755(end_year) { game.year = 1755 game.end_year = end_year game.season = EARLY game.vp = 0 game.pa = SUPPORTIVE for (let i = 1; i <= 70; ++i) game.deck.push(i) setup_markers(game.french.allied, [ "Pays d'en Haut", "Kahnawake", "St-François", ]) setup_markers(game.british.allied, [ "Canajoharie", ]) setup_markers(game.french.forts, [ "Crown Point", "Niagara", "Ohio Forks", ]) setup_markers(game.french.stockades, [ "Île-aux-Noix", "St-Jean", "Oswegatchie", "Cataraqui", "Toronto", "Presqu'île", "French Creek", "Venango", ]) setup_leader("Louisbourg", "Drucour") setup_unit("Louisbourg", "Marine") setup_unit("Louisbourg", "Artois") setup_unit("Louisbourg", "Bourgogne") setup_leader("Québec", "Dieskau") setup_leader("Québec", "Vaudreuil") setup_unit("Québec", "Béarn") setup_unit("Québec", "Guyenne") setup_unit("Québec", "La Reine") setup_unit("Québec", "Languedoc") setup_leader("Montréal", "Rigaud") setup_unit("Montréal", "Marine") setup_unit("Montréal", "Repentigny") setup_unit("Montréal", "Perière") setup_unit("Montréal", "Caughnawaga") setup_unit("Montréal", "Abenaki") setup_unit("Île-aux-Noix", "Marine Detachment") setup_unit("Crown Point", "Marine Detachment") setup_unit("Crown Point", "Marin") setup_leader("Cataraqui", "Villiers") setup_unit("Cataraqui", "Marine Detachment") setup_unit("Cataraqui", "Léry") setup_unit("Niagara", "Marine Detachment") setup_unit("Niagara", "Joncaire") setup_unit("Presqu'île", "Marine Detachment") setup_unit("French Creek", "Marine Detachment") setup_unit("Venango", "Langlade") setup_leader("Ohio Forks", "Beaujeu") setup_leader("Ohio Forks", "Dumas") setup_unit("Ohio Forks", "Marine Detachment") setup_unit("Ohio Forks", "Ligneris") setup_unit("Ohio Forks", "Ottawa") setup_unit("Ohio Forks", "Potawatomi") setup_markers(game.british.forts, [ "Hudson Carry South", "Will's Creek", "Oswego", ]) setup_markers(game.british.stockades, [ "Oneida Carry West", "Oneida Carry East", "Schenectady", "Hoosic", "Charlestown", ]) setup_unit("Oswego", "New York") setup_leader("Albany", "Shirley") setup_leader("Albany", "Johnson") setup_unit("Albany", "Rhode Island") setup_unit("Albany", "Connecticut") setup_unit("Albany", "New Hampshire") setup_unit("Albany", "Massachusetts") setup_unit("Albany", "Massachusetts") setup_unit("Albany", "Mohawk") setup_unit("Albany", "Mohawk") setup_leader("Halifax", "Monckton") setup_unit("Halifax", "47th") setup_leader("Alexandria", "Braddock") setup_leader("Alexandria", "Dunbar") setup_unit("Alexandria", "44th") setup_unit("Alexandria", "48th") setup_unit("Will's Creek", "Virginia") setup_unit("Will's Creek", "Maryland") game.british.pool.push(find_unused_piece("Abercromby")) game.british.pool.push(find_unused_piece("Bradstreet")) game.british.pool.push(find_unused_piece("Loudoun")) game.british.pool.push(find_unused_piece("Murray")) game.british.pool.push(find_unused_piece("Webb")) game.events.once_french_regulars = 1 } exports.setup = function (seed, scenario, options) { options = object_copy(options) load_game_state({ seed: seed, options: options, state: null, phasing: null, active: FRANCE, // Tracks, VP, and event triggers year: 1755, end_year: 1762, season: 0, pa: 0, vp: 0, niagara: 1, ohio_forks: 1, events: {}, // Cards last_card: 0, deck: [], discard: [], removed: [], // Leaders and units location: pieces.map(() => 0), reduced: [], // Markers sieges: {}, amphib: [], fieldworks: [], // Per-player state french: { hand: [], held: 0, did_construct: 0, allied: [], stockades: [], forts_uc: [], forts: [], fortresses: originally_french_fortresses.slice(), raids: [], }, british: { hand: [], held: 0, did_construct: 0, allied: [], stockades: [], forts_uc: [], forts: [], fortresses: originally_british_fortresses.slice(), raids: [], pool: [], }, // Temporary action state count: 0, // activation_value: 0, // activation: [], // move: {}, // battle: {}, // raid: {}, // go_home: {}, summary: { placed: {}, restored: {}, reduced: {}, eliminated: {}, }, undo: [], log: [], }) // Old RNG for ancient replays if (options.rng) game.rng = options.rng switch (scenario) { default: // fallthrough case "Annus Mirabilis": // Start at 2VP for balance. // See https://boardgamegeek.com/thread/1366550/article/19163465#19163465 setup_1757(1759, 2) break case "Annus Mirabilis (WBC)": setup_1757(1759, 3) game.options.one_step_md = 1 game.options.regulars_from_discard = 1 break case "Early War Campaign": setup_1755(1759) break case "Late War Campaign": setup_1757(1762, 4) break case "The Full Campaign": setup_1755(1762) break } log(".h1 " + scenario) logbr() if (game.options.retroactive) { log(`Retroactive "Foul Weather".`) } if (game.options.no_foul_weather) { log(`"Foul Weather" removed.`) remove_from_array(game.deck, FOUL_WEATHER) game.removed.push(FOUL_WEATHER) } if (game.options.pitt_dip_rev) { log(`William Pitt and Diplomatic Revolution are linked.`) } if (game.options.raid_militia) { // TODO log(`Enemy raid in a department cause a militia step loss.`) log("NOT IMPLEMENTED") } if (game.options.regulars_vp) { log(`Regulars cost 1 VP before 1757.`) } if (game.options.surrender) { // TODO log(`Surrender! playable by either side.`) log("NOT IMPLEMENTED") } if (game.options.acadians) { log(`Acadians Expelled playable by either side.`) } if (game.options.regulars_from_discard) { log(`After 1756 Britain may exchange a random card for a discarded Regulars or Highlanders.`) } if (game.options.one_step_md) { log(`Marine Detachments only have one step.`) } start_year() return game } // ACTION HANDLERS function clear_undo() { if (game.undo && game.undo.length > 0) 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 (k === "retro_foul_weather") v = 1 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 let save_retro_fw = game.retro_foul_weather game = save_undo.pop() save_log.length = game.log game.log = save_log game.undo = save_undo if (game.retro_foul_weather) game.retro_foul_weather = save_retro_fw update_active_aliases() } } function gen_action_undo() { if (!view.actions) view.actions = {} if (game.undo && game.undo.length > 0) view.actions.undo = 1 else view.actions.undo = 0 } function gen_action(action, argument) { if (!view.actions) view.actions = {} if (argument !== undefined) { if (!(action in view.actions)) { view.actions[action] = [ argument ] } else { if (!view.actions[action].includes(argument)) view.actions[action].push(argument) } } else { view.actions[action] = 1 } } function gen_action_next() { gen_action('next') } function gen_action_pass() { gen_action('pass') } function gen_action_space(s) { gen_action('space', s) } function gen_action_piece(p) { gen_action('piece', p) } function gen_action_discard(c) { gen_action('card', c) } function load_game_state(state) { game = state update_active_aliases() } exports.action = function (state, current, action, arg) { load_game_state(state) if (action in states[game.state]) states[game.state][action](arg) else if (action === 'undo' && game.undo && game.undo.length > 0) pop_undo() else if (action === 'demolish_fort' && current === game.active && current === game.phasing) goto_demolish_fort() else if (action === 'demolish_stockade' && current === game.active && current === game.phasing) goto_demolish_stockade() else if (action === 'demolish_fieldworks' && current === game.active && current === game.phasing) goto_demolish_fieldworks() else throw new Error("Invalid action: " + action) return game } exports.query = function (state, current, q) { if (q === 'supply') { load_game_state(state, current) return query_supply() } if (q === 'discard') { load_game_state(state, current) return game.discard } if (q === 'removed') { load_game_state(state, current) return game.removed } return null } function inactive_prompt(name, who, where) { view.prompt = `Waiting for ${game.active} \u2014 ${name}...` if (who) view.who = who if (where) view.where = where } exports.view = function(state, current) { load_game_state(state) if (game.state !== "game_over") { if (game.retro_foul_weather && game.state !== 'foul_weather' && current !== game.active) { load_game_state(game.retro_foul_weather) } } view = { vp: game.vp, pa: game.pa, year: game.year, season: game.season, events: game.events, location: game.location, reduced: game.reduced, sieges: game.sieges, amphib: game.amphib, fieldworks: game.fieldworks, last_card: game.last_card, deck: game.deck.length, french: { hand: game.french.hand.length, allied: game.french.allied, stockades: game.french.stockades, forts_uc: game.french.forts_uc, forts: game.french.forts, fortresses: game.french.fortresses, raids: game.french.raids, }, british: { hand: game.british.hand.length, allied: game.british.allied, stockades: game.british.stockades, forts_uc: game.british.forts_uc, forts: game.british.forts, fortresses: game.british.fortresses, raids: game.british.raids, pool: game.british.pool, }, active: game.active, prompt: null, actions: null, log: game.log, } if (game.activation) view.activation = game.activation if (game.british.held) view.british.held = 1 if (game.french.held) view.french.held = 1 if (current === FRANCE) view.hand = game.french.hand else if (current === BRITAIN) view.hand = game.british.hand else view.hand = [] if (!states[game.state]) { view.prompt = "Invalid game state: " + game.state return view } if (current === 'Observer' || game.active !== current) { let inactive = states[game.state].inactive if (typeof inactive === 'function') states[game.state].inactive() else if (typeof inactive === 'string') inactive_prompt(inactive) else inactive_prompt(game.state.replace(/_/g, " ")) } else { states[game.state].prompt() if (game.active === game.phasing) { if (game.state !== 'demolish_fort' && game.state !== 'demolish_stockade' && game.state !== 'demolish_fieldworks') gen_action_demolish() } gen_action_undo() } return view } // === COMMON LIBRARY === // remove item at index (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 return array } // insert item at index (faster than splice) function array_insert(array, index, item) { for (let i = array.length; i > index; --i) array[i] = array[i - 1] array[index] = item return array } 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 set } 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 return array_remove(set, m) } return set } // 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 } }