"use strict"; // cleanups // TODO: rename node/space -> location/space or raw_space/space or box/space? // TODO: replace piece[p].type lookups with index range checks // TODO: move core of is_friendly/enemy to is_british/french and branch in is_friendly/enemy // TODO: make 'inside' negative location instead of separate array // TODO: abbreviate per-player (game.British.xxx) property name (game.b.xxx) // TODO: add/remove 'next' steps at end of states // TODO: show british leader pool // TODO: show badge on leader boxes if they're dead or in pool // TODO: clean up handling of dead/unused french leaders (french regulars event) // TODO: show discard/removed card list in UI // TODO: defender retreat procedure needs love // features // TODO: track 'held' // TODO: battle VP awards // TODO: end of late season // TODO: french_regulars event played - montcalm not entered or dead separate state (merge DEAD/UNUSED consts) // TODO: trace supply // TODO: infiltration // check rules/implementation: // TODO: leaders alone - retreat (reinforcement events that place units) // GRAPHICS: badge for pool/dead leaders // GRAPHICS: tooltip for leader stack and strength const { spaces, pieces, cards } = require("./data"); const BRITAIN = 'Britain'; const FRANCE = 'France'; const DEAD = 0; const UNUSED = 0; // Order of pieces: br.leaders/br.units/fr.leaders/fr.units let first_british_piece = -1; let last_british_leader = -1; let last_british_piece = -1; let first_french_piece = -1; let last_french_leader = -1; let last_french_piece = -1; let british_militia_units = []; let french_militia_units = []; // Create card event names. for (let c = 1; c < cards.length; ++c) { cards[c].event = cards[c].name .replace(/&/g, "and") .replace(/!/g, "") .replace(/ /g, "_") .toLowerCase(); } // Figure out piece indices. for (let p = 1; p < pieces.length; ++p) { if (pieces[p].faction === 'british') { if (pieces[p].type === 'militias') british_militia_units.push(p); if (first_british_piece < 0) first_british_piece = p; if (pieces[p].type === 'leader') last_british_leader = p; last_british_piece = p; } else { if (pieces[p].type === 'militias') french_militia_units.push(p); if (first_french_piece < 0) first_french_piece = p; if (pieces[p].type === 'leader') last_french_leader = p; last_french_piece = p; } } let first_british_unit = last_british_leader; let last_british_unit = last_british_piece; let first_french_unit = last_french_leader; let last_french_unit = last_french_piece; // Patch up leader/box associations. for (let s = 1; s < spaces.length; ++s) { if (spaces[s].type === 'leader-box') { let p = find_leader(spaces[s].name); spaces[s].leader = p; pieces[p].box = s; } } let game; let view = null; let states = {}; let events = {}; let player; // aliased to game[friendly()] per-player state let enemy_player; // aliased to game[enemy()] per-player state // These looping indices are updated with update_active_aliases() let first_enemy_leader; let first_enemy_piece; let first_enemy_unit; let first_friendly_leader; let first_friendly_piece; let first_friendly_unit; let last_enemy_leader; let last_enemy_piece; let last_enemy_unit; let last_friendly_leader; let last_friendly_piece; let last_friendly_unit; function random(n) { return ((game.seed = game.seed * 69621 % 0x7fffffff) / 0x7fffffff) * n | 0; } function roll_die(reason) { let die = random(6) + 1; if (reason) log(`Roll ${die} ${reason}.`); else log(`Roll ${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.splice(i, 1); } function log(...args) { let s = Array.from(args).join(" "); console.log("LOG", s); game.log.push(s); } function friendly() { return game.active; } function enemy() { return game.active === FRANCE ? BRITAIN : FRANCE; } function enemy_of(role) { return role === FRANCE ? BRITAIN : FRANCE; } function set_enemy_active(new_state) { game.state = new_state; set_active(enemy()); } function set_active(new_active) { game.active = new_active; update_active_aliases(); } function update_active_aliases() { player = game[friendly()]; enemy_player = game[enemy()]; if (game.active === BRITAIN) { first_enemy_piece = first_french_piece; last_enemy_leader = last_french_leader; last_enemy_piece = last_french_piece; first_friendly_piece = first_british_piece; last_friendly_leader = last_british_leader; last_friendly_piece = last_british_piece; } else { first_enemy_piece = first_british_piece; last_enemy_leader = last_british_leader; last_enemy_piece = last_british_piece; first_friendly_piece = first_french_piece; last_friendly_leader = last_french_leader; last_friendly_piece = last_french_piece; } first_enemy_leader = first_enemy_piece; first_friendly_leader = first_friendly_piece; first_enemy_unit = last_enemy_leader + 1; first_friendly_unit = last_friendly_leader + 1; last_enemy_unit = last_enemy_piece; last_friendly_unit = last_friendly_piece; } // LISTS const EARLY = 0; const LATE = 1; const RELUCTANT = 0; const SUPPORTIVE = 1; const ENTHUSIASTIC = 2; function find_space(name) { if (name === 'dead') return DEAD; if (name === 'unused') return UNUSED; let ix = spaces.findIndex(node => node.name === name); if (ix < 0) throw new Error("cannot find space " + name); return ix; } function find_leader(name) { let ix = pieces.findIndex(piece => piece.name === name); if (ix < 0) throw new Error("cannot find leader " + name); return ix; } function find_unused_unit(name) { for (let i = 0; i < pieces.length; ++i) if (pieces[i].name === name && game.pieces.location[i] === 0) return i; throw new Error("cannot find unit " + name); } const ports = [ "Alexandria", "Baltimore", "Boston", "Halifax", "Louisbourg", "New Haven", "New York", "Philadelphia", "Québec", ].map(name => spaces.findIndex(space => space.name === name)); const french_ports = [ "Louisbourg", "Québec", ].map(name => spaces.findIndex(space => space.name === name)); const fortresses = [ "Albany", "Alexandria", "Baltimore", "Boston", "Halifax", "Louisbourg", "Montréal", "New Haven", "New York", "Philadelphia", "Québec", ].map(name => spaces.findIndex(space => space.name === name)); const originally_french_fortresses = [ "Louisbourg", "Montréal", "Québec", ].map(name => spaces.findIndex(space => space.name === name)); const originally_british_fortresses = [ "Albany", "Alexandria", "Baltimore", "Boston", "Halifax", "New Haven", "New York", "Philadelphia", ].map(name => spaces.findIndex(space => space.name === name)); const departments = { st_lawrence: [ "Baie-St-Paul", "Bécancour", "Kahnawake", "Lac des Deux Montagnes", "Montréal", "Québec", "Rivière-Ouelle", "Sorel", "St-François", "St-Jean", "Trois-Rivières", "Île d'Orléans", ].map(name => spaces.findIndex(space => space.name === name)), northern: [ "Albany", "Boston", "Burlington", "Charlestown", "Concord", "Deerfield", "Gloucester", "Hartford", "Hoosic", "Kinderhook", "Manchester", "New Haven", "New York", "Northampton", "Peekskill", "Portsmouth", "Poughkeepsie", "Providence", "Schenectady", "Trenton", "Worcester", ].map(name => spaces.findIndex(space => space.name === name)), southern: [ "Alexandria", "Ashby's Gap", "Augusta", "Baltimore", "Carlisle", "Culpeper", "Easton", "Frederick", "Harris's Ferry", "Head of Elk", "Lancaster", "New Castle", "Philadelphia", "Reading", "Shepherd's Ferry", "Winchester", "Woodstock", "Wright's Ferry", "York", ].map(name => spaces.findIndex(space => space.name === name)), } const indian_spaces = { "pays-d-en-haut": [ find_space("Pays d'en Haut") ], northern: [ "Kahnawake", "Lac des Deux Montagnes", "Mississauga", "St-François", ].map(name => spaces.findIndex(space => space.name === name)), western: [ "Kittaning", "Logstown", "Mingo Town", ].map(name => spaces.findIndex(space => space.name === name)), mohawks: [ "Canajoharie", ].map(name => spaces.findIndex(space => space.name === name)), iroquois: [ "Cayuga", "Karaghiyadirha", "Oneida Castle", "Onondaga", "Shawiangto", ].map(name => spaces.findIndex(space => space.name === name)), } const indian_tribe = {}; function define_indian_settlement(space_name, tribe) { let space = find_space(space_name); if (space_name !== "Pays d'en Haut") indian_tribe[space] = tribe; for (let p = 1; p < pieces.length; ++p) if (pieces[p].name === tribe) pieces[p].settlement = space; } define_indian_settlement("Kahnawake", "Caughnawaga"); define_indian_settlement("Lac des Deux Montagnes", "Algonquin"); define_indian_settlement("Mississauga", "Mississauga"); define_indian_settlement("St-François", "Abenaki"); define_indian_settlement("Mingo Town", "Mingo"); define_indian_settlement("Logstown", "Shawnee"); define_indian_settlement("Kittaning", "Delaware"); define_indian_settlement("Canajoharie", "Mohawk"); define_indian_settlement("Cayuga", "Cayuga"); define_indian_settlement("Karaghiyadirha", "Seneca"); define_indian_settlement("Oneida Castle", "Oneida"); define_indian_settlement("Onondaga", "Onondaga"); define_indian_settlement("Shawiangto", "Tuscarora"); define_indian_settlement("Pays d'en Haut", "Ojibwa"); define_indian_settlement("Pays d'en Haut", "Ottawa"); define_indian_settlement("Pays d'en Haut", "Potawatomi"); define_indian_settlement("Pays d'en Haut", "Huron"); const JOHNSON = find_leader("Johnson"); const MONTCALM = find_leader("Montcalm"); const LEVIS = find_leader("Lévis"); const BOUGAINVILLE = find_leader("Bougainville"); const SHIRLEY = find_leader("Shirley"); const HALIFAX = find_space("Halifax"); const LOUISBOURG = find_space("Louisbourg"); const BAIE_ST_PAUL = find_space("Baie-St-Paul"); const RIVIERE_OUELLE = find_space("Rivière-Ouelle"); const ILE_D_ORLEANS = find_space("Île d'Orléans"); const QUEBEC = find_space("Québec"); const MONTREAL = find_space("Montréal"); const OHIO_FORKS = find_space("Ohio Forks"); const CANAJOHARIE = find_space("Canajoharie"); const NIAGARA = find_space("Niagara"); const OSWEGO = find_space("Oswego"); const ONEIDA_CARRY_WEST = find_space("Oneida Carry West"); const ONEIDA_CARRY_EAST = find_space("Oneida Carry East"); const PAYS_D_EN_HAUT = find_space("Pays d'en Haut"); const ST_LAWRENCE_CANADIAN_MILITIAS = find_space("St. Lawrence Canadian Militias"); const NORTHERN_COLONIAL_MILITIAS = find_space("Northern Colonial Militias"); const SOUTHERN_COLONIAL_MILITIAS = find_space("Southern Colonial Militias"); 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 first_amphib_card = 17; const last_amphib_card = 20; const LOUISBOURG_SQUADRONS = 21; const within_two_of_canajoharie = [ CANAJOHARIE ]; for_each_exit(CANAJOHARIE, one => { if (!within_two_of_canajoharie.includes(one)) { within_two_of_canajoharie.push(one); for_each_exit(one, two => { if (!within_two_of_canajoharie.includes(two)) { within_two_of_canajoharie.push(two); } }); } }); const within_two_of_gray_settlement = []; indian_spaces.iroquois.forEach(zero => { within_two_of_gray_settlement.push(zero); }); indian_spaces.iroquois.forEach(zero => { for_each_exit(zero, one => { if (!within_two_of_gray_settlement.includes(one)) { within_two_of_gray_settlement.push(one); for_each_exit(one, two => { if (!within_two_of_gray_settlement.includes(two)) { within_two_of_gray_settlement.push(two); } }); } }); }); const in_or_adjacent_to_ohio_forks = [ OHIO_FORKS ]; for_each_exit(OHIO_FORKS, one => { in_or_adjacent_to_ohio_forks.push(one); }); function has_amphibious_arrow(space) { return space === HALIFAX || space === LOUISBOURG; } // Map spaces except militia boxes and leader boxes. const first_space = 1; const last_space = NORTHERN_COLONIAL_MILITIAS-1; const british_iroquois_or_mohawk_names = [ "Seneca", "Cayuga", "Onondaga", "Tuscarora", "Oneida", "Mohawk" ]; const british_iroquois_or_mohawk_units = []; for (let i = 0; i < pieces.length; ++i) { let piece = pieces[i]; if (piece.faction === 'british' && piece.type === 'indians' && british_iroquois_or_mohawk_names.includes(piece.name)) british_iroquois_or_mohawk_units.push(i); } const originally_friendly_spaces = { France: departments.st_lawrence.concat([LOUISBOURG]), Britain: departments.northern.concat(departments.southern).concat([HALIFAX]), } // CARD DECK function reshuffle_deck() { game.log.push("The deck is reshuffled."); game.cards.draw_pile = game.draw_pile.concat(game.cards.discarded); game.cards.discarded = []; } function last_discard() { if (game.cards.discarded.length > 0) return game.cards.discarded[game.cards.discarded.length-1]; return null; } function deal_card() { if (game.cards.draw_pile.length === 0) reshuffle_deck(); let i = random(game.cards.draw_pile.length); let c = game.cards.draw_pile[i]; game.cards.draw_pile.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; fn = fn - game.France.hand.length; bn = bn - game.Britain.hand.length; log("Dealt " + fn + " cards to France."); log("Dealt " + bn + " cards to Britain."); while (fn > 0 || bn > 0) { let c; if (fn > 0) { game.France.hand.push(c=deal_card()); log("DEAL", c, "to France"); --fn; } if (bn > 0) { game.Britain.hand.push(c=deal_card()); log("DEAL", c, "to Britain"); --bn; } } } function draw_leader_from_pool() { if (game.pieces.pool.length > 0) { let i = random(game.pieces.pool.length); let p = game.pieces.pool[i]; game.pieces.pool.splice(i, 1); move_piece_to(p, leader_box(p)); return p; } return 0; } function is_card_available(c) { return !game.cards.discarded.includes(c) && !game.cards.removed.includes(c); } // ITERATORS function for_each_siege(fn) { for (let sid in game.sieges) fn(sid|0, game.sieges[sid]); } function for_each_exit(s, fn) { let { land, river, lakeshore } = spaces[s]; for (let i = 0; i < land.length; ++i) fn(land[i], 'land'); for (let i = 0; i < river.length; ++i) fn(river[i], 'river'); for (let i = 0; i < lakeshore.length; ++i) fn(lakeshore[i], 'lakeshore'); } 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_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(space, fn) { for (let p = first_friendly_piece; p <= last_friendly_piece; ++p) { if (is_piece_in_space(p, space)) fn(p); } } function for_each_friendly_leader_in_space(space, fn) { for (let p = first_friendly_leader; p <= last_friendly_leader; ++p) { if (is_piece_in_space(p, space)) fn(p); } } function for_each_friendly_unit_in_space(space, fn) { for (let p = first_friendly_unit; p <= last_friendly_unit; ++p) { if (is_piece_in_space(p, space)) fn(p); } } function for_each_unbesieged_enemy_in_space(space, fn) { for (let p = first_enemy_unit; p <= last_enemy_unit; ++p) { if (is_piece_unbesieged(p) && is_piece_in_space(p, space)) fn(p); } } function for_each_piece_in_force(force, fn) { for (let p = 0; p < pieces.length; ++p) if (is_piece_in_force(p, force)) fn(p); } function for_each_unit_in_force(force, fn) { for (let p = 0; p < pieces.length; ++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 list_auxiliary_units_in_force(force) { let list = []; for_each_unit_in_force(force, p => { if (is_auxiliary_unit(p)) list.push(p); }); return list; } // STATIC PROPERTIES function get_department(space) { if (departments.st_lawrence.includes(space)) return departments.st_lawrence; if (departments.northern.includes(space)) return departments.northern; if (departments.southern.includes(space)) return departments.southern; return null; } function department_militia(space) { if (departments.st_lawrence.includes(space)) return ST_LAWRENCE_CANADIAN_MILITIAS; if (departments.northern.includes(space)) return NORTHERN_COLONIAL_MILITIAS; if (departments.southern.includes(space)) return SOUTHERN_COLONIAL_MILITIAS; return 0; } function space_name(s) { return spaces[s].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 is_wilderness_or_mountain(space) { let type = spaces[space].type; return type === 'wilderness' || type === 'mountain'; } function is_wilderness(space) { return spaces[space].type === 'wilderness'; } function is_mountain(space) { return spaces[space].type === 'mountain'; } function is_cultivated(space) { return spaces[space].type === 'cultivated'; } function is_militia_box(space) { return spaces[space].type === 'militia-box'; } function is_leader_box(space) { return spaces[space].type === 'leader-box'; } function is_originally_friendly(space) { return originally_friendly_spaces[friendly()].includes(space); } function is_originally_enemy(space) { return originally_friendly_spaces[enemy()].includes(space); } function is_fortress(space) { return fortresses.includes(space); } function is_port(space) { return ports.includes(space); } function is_only_port_space(space) { return space === HALIFAX || space === LOUISBOURG; } function is_leader(p) { return pieces[p].type === 'leader'; } function is_unit(p) { return pieces[p].type !== 'leader'; } function is_british_iroquois_or_mohawk(p) { return british_iroquois_or_mohawk_units.includes(p); } function is_provincial_unit(p) { switch (pieces[p].type) { case 'northern-provincials': return true; case 'southern-provincials': return true; } return false; } function is_provincial_unit_from(p, type) { switch (pieces[p].type) { case 'northern-provincials': return type === 'northern'; case 'southern-provincials': return type === 'southern'; } return false; } function is_drilled_troops(p) { switch (pieces[p].type) { case 'regulars': return true; case 'light-infantry': return true; case 'northern-provincials': return true; case 'southern-provincials': return true; } return false; } function is_militia_unit(p) { return pieces[p].type === 'militias'; } function is_light_infantry_unit(p) { return pieces[p].type === 'light-infantry'; } function is_indian_unit(p) { return pieces[p].type === 'indians'; } function is_indian_tribe(p, tribe) { return pieces[p].type === 'indians' && pieces[p].name === tribe; } function indian_home_settlement(p) { return pieces[p].settlement || 0; } function is_regulars_unit(p) { return pieces[p].type === 'regulars'; } function is_highland_unit(p) { return pieces[p].type === 'regulars' && pieces[p].subtype === 'highland'; } function is_royal_american_unit(p) { return pieces[p].type === 'regulars' && pieces[p].subtype === 'royal'; } function is_3_4_regular_unit(p) { return pieces[p].type === 'regulars' && pieces[p].subtype === undefined; } function is_western_indian_unit(p) { return pieces[p].type === 'indians' && pieces[p].subtype === 'western'; } function is_pays_d_en_haut_indian_unit(p) { return pieces[p].type === 'indians' && pieces[p].subtype === 'pays-d-en-haut'; } function is_rangers_unit(p) { return pieces[p].type === 'rangers'; } function is_coureurs_unit(p) { return pieces[p].type === 'coureurs'; } function is_auxiliary_unit(p) { switch (pieces[p].type) { case 'indians': return true; case 'coureurs': return true; case 'rangers': return true; } return false; } function piece_name(p) { return pieces[p].name; } function piece_movement(p) { return pieces[p].movement; } function leader_box_leader(s) { return spaces[s].leader; } function leader_box(p) { return pieces[p].box; } function leader_initiative(p) { return pieces[p].initiative; } function leader_command(p) { return pieces[p].command; } function leader_tactics(p) { return pieces[p].tactics; } // DYNAMIC PROPERTIES function piece_node(p) { return game.pieces.location[p]; } function piece_space(p) { let where = piece_node(p); while (is_leader_box(where)) where = piece_node(leader_box_leader(where)); return where; } // commanding leader or self function piece_force(self) { let force = self; let where = piece_node(force); while (is_leader_box(where)) { force = leader_box_leader(where); where = piece_node(force); } return force; } // is piece commanded by a leader (or self) function is_piece_in_force(self, query) { let force = self; if (force === query) return true; let where = piece_node(force); while (is_leader_box(where)) { force = leader_box_leader(where); if (force === query) return true; where = piece_node(force); } return false; } function count_non_british_iroquois_and_mohawk_units_in_leader_box(leader) { let n = 0; for_each_friendly_piece_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, p => { ++n; }); return n; } function count_units_in_force(force) { let n = 0; for_each_unit_in_force(force, p => { ++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, p => { ++n; }); return n; } function count_unbesieged_enemy_units_in_space(where) { let n = 0; for_each_unbesieged_enemy_in_space(where, p => { ++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 game.pieces.reduced.includes(p); } function set_unit_reduced(p, v) { if (v) { if (!game.pieces.reduced.includes(p)) game.pieces.reduced.push(p); } else { remove_from_array(game.pieces.reduced, p); } } function is_piece_inside(p) { return game.pieces.inside.includes(p); } function is_piece_unbesieged(p) { return !game.pieces.inside.includes(p); } function set_piece_inside(p) { if (!game.pieces.inside.includes(p)) game.pieces.inside.push(p); } function set_piece_outside(p) { remove_from_array(game.pieces.inside, p); } function set_force_inside(force) { // TODO: do we need to flatten forces inside a fort? if (is_leader(force)) { for_each_unit_in_force(force, p => { isolate_piece_from_force(p); set_piece_inside(p); }); for_each_piece_in_force(force, p => { isolate_piece_from_force(p); set_piece_inside(p); }); } else { set_piece_inside(force); } } function is_piece_on_map(p) { // TODO: militia boxes? let s = piece_node(p); return s !== UNUSED && s !== DEAD; } function is_piece_unused(p) { return piece_node(p) === UNUSED; } function is_piece_in_node(p, node) { return piece_node(p) === node; } function is_piece_in_space(p, space) { return piece_space(p) === space; } function has_amphib(space) { return game.Britain.amphib.includes(space); } function has_friendly_amphib(space) { return game.active === BRITAIN && game.Britain.amphib.includes(space); } function has_enemy_amphib(space) { return game.active === FRANCE && game.Britain.amphib.includes(space); } function has_fieldworks(space) { return game.fieldworks.includes(space); } function place_fieldworks(s) { log(`Fieldworks placed at ${space_name(s)}.`); game.fieldworks.push(s); } function remove_fieldworks(s) { if (game.fieldworks.includes(s)) { log(`Fieldworks removed at ${space_name(s)}.`); remove_from_array(game.fieldworks, s); } } function place_friendly_raided_marker(space) { player.raids.push(space); } function has_friendly_raided_marker(space) { return player.raids.includes(space); } function has_enemy_raided_marker(space) { return enemy_player.raids.includes(space); } function is_space_besieged(space) { return space in game.sieges; } function is_space_unbesieged(space) { return !is_space_besieged(space); } function has_enemy_allied_settlement(space) { return enemy_player.allied.includes(space); } function has_friendly_allied_settlement(space) { return player.allied.includes(space); } function has_enemy_stockade(space) { return enemy_player.stockades.includes(space); } function has_friendly_stockade(space) { return player.stockades.includes(space); } function has_enemy_fortress(space) { return enemy_player.fortresses.includes(space); } function has_friendly_fortress(space) { return player.fortresses.includes(space); } function has_enemy_fort(space) { return enemy_player.forts.includes(space); } function has_friendly_fort(space) { return player.forts.includes(space); } function has_enemy_fort_uc(space) { return enemy_player.forts_uc.includes(space); } function has_friendly_fort_uc(space) { return player.forts_uc.includes(space); } function has_enemy_fort_or_fortress(space) { return has_enemy_fort(space) || has_enemy_fortress(space); } function has_enemy_fortifications(space) { return has_enemy_stockade(space) || has_enemy_fort(space) || has_enemy_fortress(space); } function has_friendly_fort_or_fortress(space) { return has_friendly_fort(space) || has_friendly_fortress(space); } function has_friendly_fortifications(space) { return has_friendly_stockade(space) || has_friendly_fort(space) || has_friendly_fortress(space); } function has_unbesieged_friendly_fortifications(space) { return is_space_unbesieged(space) && has_friendly_fortifications(space); } function has_unbesieged_friendly_fortress(space) { return is_space_unbesieged(space) && has_friendly_fortress(space); } function has_friendly_units(space) { for (let p = first_friendly_unit; p <= last_friendly_unit; ++p) if (is_piece_in_space(p, space)) return true; return false; } function has_enemy_units(space) { for (let p = first_enemy_unit; p <= last_enemy_unit; ++p) if (is_piece_in_space(p, space)) return true; return false; } function has_french_units(space) { for (let p = first_french_unit; p <= last_french_unit; ++p) if (is_piece_in_space(p, space)) return true; return false; } function has_british_units(space) { for (let p = first_british_unit; p <= last_british_unit; ++p) if (is_piece_in_space(p, space)) return true; return false; } function has_french_drilled_troops(space) { for (let p = first_french_unit; p <= last_french_unit; ++p) if (is_piece_in_space(p, space)) if (is_drilled_troops(p)) return true; return false; } function has_british_drilled_troops(space) { for (let p = first_british_unit; p <= last_british_unit; ++p) if (is_piece_in_space(p, space)) if (is_drilled_troops(p)) return true; return false; } function is_originally_british(space) { return originally_friendly_spaces.Britain.includes(space); } function is_french_controlled_space(space) { if (game.active === FRANCE) return is_friendly_controlled_space(space); return is_enemy_controlled_space(space); } function has_french_stockade(space) { return game.France.stockades.includes(space); } function has_french_fort(space) { return game.France.forts.includes(space); } function has_french_fortress(space) { return is_fortress(space) && is_french_controlled_space(space); } function has_french_fortifications(space) { return has_french_stockade(space) || has_french_fort(space) || has_french_fortress(space); } function has_unbesieged_french_fortification(space) { return is_space_unbesieged(space) && has_french_fortifications(space); } function count_enemy_units_in_space(space) { let n = 0; for (let p = first_enemy_unit; p <= last_enemy_unit; ++p) if (is_piece_in_space(p, space)) ++n; return n; } function has_unbesieged_enemy_leader(space) { for (let p = first_enemy_leader; p <= last_enemy_leader; ++p) if (is_piece_in_space(p, space) && !is_piece_inside(p)) return true; return false; } function has_unbesieged_enemy_units(space) { for (let p = first_enemy_unit; p <= last_enemy_unit; ++p) if (is_piece_in_space(p, space) && !is_piece_inside(p)) return true; return false; } function has_unbesieged_enemy_units_that_did_not_intercept(space) { for (let p = first_enemy_unit; p <= last_enemy_unit; ++p) if (is_piece_in_space(p, space) && !is_piece_inside(p) && !did_piece_intercept(p)) return true; return false; } function is_friendly_controlled_space(space) { if (is_space_unbesieged(space) && !has_enemy_units(space)) { if (is_originally_enemy(space)) { if (has_friendly_units(space) || has_friendly_stockade(space) || has_friendly_fort(space)) return true; if (has_friendly_amphib(space)) return true; } else if (is_originally_friendly(space)) { return true; } else { if (has_friendly_units(space) || has_friendly_stockade(space) || has_friendly_fort(space)) return true; } } return false; } function is_enemy_controlled_space(space) { if (is_space_unbesieged(space) && !has_friendly_units(space)) { if (is_originally_friendly(space)) { if (has_enemy_units(space) || has_enemy_stockade(space) || has_enemy_fort(space)) return true; if (has_enemy_amphib(space)) return true; } else if (is_originally_enemy(space)) { return true; } else { if (has_enemy_units(space) || has_enemy_stockade(space) || has_enemy_fort(space)) return true; } } return false; } function is_british_controlled_space(space) { if (game.active === BRITAIN) return is_friendly_controlled_space(space); return is_enemy_controlled_space(space); } function is_friendly_controlled_port(space) { return is_port(space) && is_friendly_controlled_space(space); } function has_friendly_supplied_drilled_troops(space) { for (let p = first_friendly_unit; p <= last_friendly_unit; ++p) if (is_drilled_troops(p) && is_piece_in_space(p, space) && is_in_supply(space)) return true; return false; } function has_friendly_drilled_troops(space) { for (let p = first_friendly_unit; p <= last_friendly_unit; ++p) if (is_drilled_troops(p) && is_piece_in_space(p, space)) return true; return false; } function has_friendly_regulars(space) { for (let p = first_friendly_unit; p <= last_friendly_unit; ++p) if (is_regulars_unit(p) && is_piece_in_space(p, space)) return true; return false; } function has_friendly_rangers(space) { if (game.active === BRITAIN) for (let p = first_british_unit; p <= last_british_unit; ++p) if (is_rangers_unit(p) && is_piece_in_space(p, space)) return true; return false; } function has_friendly_coureurs(space) { if (game.active === FRANCE) for (let p = first_french_piece; p <= last_french_piece; ++p) if (is_coureurs_unit(p) && is_piece_in_space(p, space)) return true; return false; } function has_friendly_indians(space) { for (let p = first_friendly_unit; p <= last_friendly_unit; ++p) if (is_indian_unit(p) && is_piece_in_space(p, space)) return true; return false; } function has_unbesieged_enemy_auxiliary(space) { for (let p = first_enemy_unit; p <= last_enemy_unit; ++p) if (is_auxiliary_unit(p) && is_piece_in_space(p, space) && !is_piece_inside(p)) return true; return false; } function has_unbesieged_friendly_auxiliary(space) { for (let p = first_friendly_unit; p <= last_friendly_unit; ++p) if (is_auxiliary_unit(p) && is_piece_in_space(p, space) && !is_piece_inside(p)) return true; return false; } function has_unbesieged_enemy_fortifications(space) { return is_space_unbesieged(space) && has_enemy_fortifications(space); } function has_besieged_enemy_fortifications(space) { return is_space_besieged(space) && has_enemy_fortifications(space); } function has_unbesieged_enemy_fort_or_fortress(space) { return is_space_unbesieged(space) && has_enemy_fort_or_fortress(space); } function has_unbesieged_friendly_units(space) { for (let p = first_friendly_unit; p <= last_friendly_unit; ++p) if (is_piece_in_space(p, space) && is_piece_unbesieged(p)) return true; return false; } function has_besieged_friendly_units(space) { for (let p = first_friendly_unit; p <= last_friendly_unit; ++p) if (is_piece_in_space(p, space) && is_piece_inside(p)) return true; return false; } function count_militia_in_department(box) { let list = (box === ST_LAWRENCE_CANADIAN_MILITIAS ? french_militia_units : british_militia_units); for (let i = 0; i < list.length; ++i) if (piece_node(list[i]) === box) return true; return false; } 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_unit(p)) ++ax_count; else only_ax = false; }); return only_ax && ax_count === 1; } return is_auxiliary_unit(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_auxiliary_unit(who) { if (is_leader(who)) { let has_ax = false; for_each_unit_in_force(who, p => { if (is_auxiliary_unit(p)) has_ax = true; }); return has_ax; } return is_auxiliary_unit(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_unit(p)) only_ax = false; }); return only_ax; } return is_auxiliary_unit(who); } function is_raid_space(space) { if (has_friendly_fort(space)) return false; if (has_friendly_fortress(space)) return false; if (has_friendly_stockade(space)) return false; if (has_friendly_drilled_troops(space)) return false; if (is_originally_enemy(space)) return true; if (has_enemy_stockade(space)) return true; if (has_enemy_allied_settlement(space)) 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 piece_space(moving_piece()); } function intercepting_piece() { return game.move.intercepting; } function avoiding_piece() { return game.move.avoiding; } function moving_piece_came_from(here) { return game.move.path[here]; } function battle_space() { return game.battle.where; } function find_friendly_commanding_leader_in_space(space) { let commander = 0; for (let p = first_friendly_leader; p <= last_friendly_leader; ++p) if (is_piece_in_space(p, space)) if (!commander || leader_command(p) > leader_command(commander)) // TODO: prefer commander with higher tactics rating if same command rating commander = p; return commander; } function find_enemy_commanding_leader_in_space(space) { let commander = 0; for (let p = first_enemy_leader; p <= last_enemy_leader; ++p) if (is_piece_in_space(p, space)) 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 loses ${-n} VP.`); else log(`France gains ${n} VP.`); } else { if (n < 0) log(`Britain gains ${-n} VP.`); else log(`Britain loses ${n} VP.`); } } function award_vp(n) { if (game.active === FRANCE) { log_vp(n); game.tracks.vp += n; } else { log_vp(-n); game.tracks.vp -= n; } } function award_french_vp(n) { log_vp(n); game.tracks.vp += n; } function award_british_vp(n) { log_vp(-n); game.tracks.vp -= n; } function remove_friendly_stockade(space) { remove_from_array(player.stockades, space); } function remove_friendly_fort_uc(space) { remove_from_array(player.forts_uc, space); } function remove_friendly_fort(space) { remove_from_array(player.forts, space); } function remove_enemy_fort_uc(space) { remove_from_array(enemy_player.forts_uc, space); } function place_friendly_fort(space) { remove_friendly_stockade(space); remove_friendly_fort_uc(space); player.forts.push(space); } function place_friendly_fort_uc(space) { player.forts_uc.push(space); } // Isolate piece from any forces it may be involved in. function isolate_piece_from_force(p) { let where = piece_space(p); if (is_leader(p)) move_pieces_from_node_to_node(leader_box(p), where); move_piece_to(p, where); } function restore_unit(p) { let s = piece_space(p); log(`${piece_name(p)} restored at ${space_name(s)}.`); set_unit_reduced(p, 0); } function reduce_unit(p) { if (is_unit_reduced(p)) { eliminate_piece(p); return true; } set_unit_reduced(p, 1); log(piece_name(p) + " is reduced."); return false; } function eliminate_piece(p) { log(piece_name(p) + " is eliminated."); isolate_piece_from_force(p); set_unit_reduced(p, 0); if (is_leader(p)) game.pieces.location[p] = DEAD; else game.pieces.location[p] = UNUSED; if (is_indian_unit(p)) { let home = indian_home_settlement(p); if (home) { let tribe = indian_tribe[home]; if (is_indian_tribe_eliminated(home)) { if (home == PAYS_D_EN_HAUT) log(`Removed settlement at ${space_name(home)}.`); else log(`Removed ${tribe} settlement at ${space_name(home)}.`); if (pieces[p].faction === 'british') remove_from_array(game.Britain.allied, home); else remove_from_array(game.France.allied, home); } } } } function eliminate_indian_tribe(tribe) { // OPTIMIZE: indian unit piece ranges for (let p = 1; p < pieces.length; ++p) if (is_indian_tribe(p, tribe) && is_piece_unbesieged(p)) eliminate_piece(p); } function is_indian_tribe_eliminated(home) { // OPTIMIZE: indian unit piece ranges if (home === PAYS_D_EN_HAUT) { for (let p = 1; p < pieces.length; ++p) if (is_pays_d_en_haut_indian_unit(p)) if (is_piece_on_map(p)) return false; } else { let tribe = indian_tribe[home]; for (let p = 1; p < pieces.length; ++p) if (is_indian_tribe(p, tribe)) if (is_piece_on_map(p)) return false; } return true; } function move_piece_to(who, to) { game.pieces.location[who] = to; } function place_piece(who, to) { log(`${piece_name(who)} placed at ${space_name(to)}.`); game.pieces.location[who] = to; if (is_indian_unit(who)) { let home = indian_home_settlement(who); if (home) { let tribe = indian_tribe[home]; if (pieces[who].faction === 'british') { if (!game.Britain.allied.includes(home)) { log(`Placed ${tribe} settlement at ${space_name(home)}.`); game.Britain.allied.push(home); } } else { if (!game.France.allied.includes(home)) { log(`Placed ${tribe} settlement at ${space_name(home)}.`); game.France.allied.push(home); } } } } } function move_pieces_from_node_to_node(from, to) { for (let p = 0; p < pieces.length; ++p) { if (piece_node(p) === from) move_piece_to(p, to); } } function capture_enemy_fortress(s) { log("captures fortress"); remove_from_array(enemy_player.fortresses, s); player.fortresses.push(s); award_vp(3); } function recapture_french_fortress(s) { log(`France recaptures fortress at ${space_name(s)}.`); remove_from_array(game.Britain.fortresses, s); game.France.fortresses.push(s); award_french_vp(3); } function recapture_british_fortress(s) { log(`Britain recaptures fortress at ${space_name(s)}.`); remove_from_array(game.France.fortresses, s); game.Britain.fortresses.push(s); award_british_vp(3); } function capture_enemy_fort_intact(s) { log(`captures enemy fort intact`); remove_from_array(enemy_player.forts, s); player.forts.push(s); award_vp(2); } function capture_enemy_fort(space) { log(`captures enemy fort`); remove_from_array(enemy_player.forts, space); player.forts_uc.push(space); award_vp(2); } function capture_enemy_stockade(space) { log(`captures enemy stockade`); remove_from_array(enemy_player.stockades, space); player.stockades.push(space); award_vp(1); } function eliminate_enemy_stockade_after_battle(space) { log(`eliminates enemy stockade`); remove_from_array(enemy_player.stockades, space); award_vp(1); } function eliminate_enemy_stockade_in_raid(space) { log(`eliminates enemy stockade`); remove_from_array(enemy_player.stockades, space); } function add_raid(who) { let where = piece_space(who); console.log("add_raid", piece_name(who), "in", space_name(where)); if (where && !game.raid.list.includes(where) && is_raid_space(where)) game.raid.list.push(where); } function is_vacant_of_besieging_units(space) { if (has_french_fort(space) || has_french_fortress(space)) return !has_british_units(space); else return !has_french_units(space); } function lift_sieges_and_amphib() { console.log("LIFT SIEGES AND AMPHIB AND RECAPTURE FORTRESSES"); for_each_siege(space => { if (is_vacant_of_besieging_units(space)) { log(`Siege in ${space_name(space)} lifted.`); for (let p = 1; p < pieces.length; ++p) if (is_piece_in_space(p, space)) set_piece_outside(p); delete game.sieges[space]; } }); let amphib = game.Britain.amphib; for (let i = amphib.length-1; i >= 0; --i) { let s = amphib[i]; if (!has_british_units(s)) { if (has_french_drilled_troops(s) || has_unbesieged_french_fortification()) { log(`Amphib removed from ${space_name(s)}.`); amphib.splice(i, 1); } } } // Recapture abandoned enemy fortresses. for (let s of originally_french_fortresses) if (game.Britain.fortresses.includes(s) && is_french_controlled_space(s)) recapture_french_fortress(s); for (let s of originally_british_fortresses) if (game.France.fortresses.includes(s) && is_british_controlled_space(s)) recapture_british_fortress(s); // Check ownership of other VP locations: update_vp("niagara", NIAGARA); update_vp("ohio_forks", OHIO_FORKS); } function update_vp(name, s) { let save = game[name]; let v = 0; if (is_french_controlled_space(s)) v = 1; else if (is_british_controlled_space(s)) v = -1; if (v !== save) award_french_vp(v - save); game[name] = v; } // PATH FINDING function is_in_supply(space) { // TODO: trace supply return true; } function list_intercept_spaces(is_lone_ld, is_lone_ax) { let intercept = {}; // 6.723 Leaders moving alone can NOT be intercepted if (is_lone_ld) return intercept; console.log("INTERCEPT SEARCH is_lone_ax", is_lone_ax, "is_lone_ld", is_lone_ld); for (let from = first_space; from <= last_space; ++from) { if (has_unbesieged_enemy_units(from)) { console.log("INTERCEPT FROM", space_name(from)); // 6.721 exception -- can always intercept units infiltrating same space // TODO: infiltration // intercept[from] = 3; for_each_exit(from, to => { // 6.722 if (has_unbesieged_friendly_units(to)) return; if (has_unbesieged_friendly_fortifications(to)) return; // 6.721 if (is_lone_ax && is_wilderness_or_mountain(to)) { if (has_unbesieged_enemy_auxiliary(from)) { console.log("INTERCEPT TO", space_name(to), "(lone ax)"); intercept[to] = 2; } } else { console.log("INTERCEPT TO", space_name(to)); intercept[to] = 1; } }); } } return intercept; } function gen_intercept(is_lone_ax, to) { if (has_unbesieged_enemy_units(to)) { // 6.721 exception -- can always intercept units infiltrating same space // TODO: infiltration /* for_each_friendly_piece_in_space(to, p => { // TODO: unbesieged 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_unit(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) { if (is_piece_in_space(JOHNSON, from)) { if (is_piece_unbesieged(JOHNSON)) gen_action_piece(JOHNSON); } } } else { for_each_friendly_piece_in_space(from, p => { if (is_piece_unbesieged(p)) gen_action_piece(p); }); } }); } } function search_naval_move(who, start_space, start_cost) { let move_cost = game.move.cost = {}; let move_path = game.move.path = {}; let candidates = (game.active === FRANCE) ? french_ports : ports; if (start_cost > 0) return; let from = piece_space(who); if (!candidates.includes(from) || !is_friendly_controlled_space(from)) return; game.move.path[from] = null; candidates.forEach(space => { if (space === from) return; if (is_friendly_controlled_space(space)) { game.move.cost[space] = 1; game.move.path[space] = from; } }) } // Use Breadth First Search to find all paths. function pq_push(queue, item, prio) { // TODO: reprioritize if item is already in the queue for (let i = 0, n = queue.length; i < n; ++i) if (queue[i].prio > prio) return queue.splice(i, 0, [prio, item]); queue.push([prio, item]) } function pq_pop(queue) { return queue.shift()[1]; } function search_boat_move(who, start_space, start_cost, max_cost) { let move_cost = game.move.cost = {}; let move_path = game.move.path = {}; if (start_cost >= max_cost) return; let queue = []; pq_push(queue, start_space, 0); move_cost[start_space] = start_cost; move_path[start_space] = null; const is_lone_ld = is_lone_leader(who); const has_dt = force_has_drilled_troops(who); while (queue.length > 0) { let current = pq_pop(queue); let c_cost = move_cost[current]; let c_ff = has_friendly_fortifications(current) || is_originally_friendly(current); for_each_exit(current, (next, connection) => { let n_ff = has_friendly_fortifications(next) || is_originally_friendly(next); let n_cost = c_cost + 2; let must_stop = false; if (connection === 'land') { if (c_ff && n_ff && (c_cost & 1) === 0) { n_cost += 1; } else { return; // Not a usable land connection } } // Must stop on entering interception space if (next in game.move.intercept) must_stop = true; // Must stop on entering enemy occupied spaces if (has_unbesieged_enemy_units(next)) { if (is_lone_ld) return; // Lone leaders can never enter an enemy occupied space // TODO: Infiltration must_stop = true; // May continue if over-run } if (has_enemy_stockade(next)) { // TODO: Infiltration n_cost = 18; // may not continue } if (has_unbesieged_enemy_fort_or_fortress(next)) { if (!has_dt) return; // Must have Drilled Troops to enter an enemy fort or fortress space. // TODO: Infiltration n_cost = 18; // may not continue } // Stop when entering a space with a lone enemy leader(s) to force a retreat if (!is_lone_ld && has_unbesieged_enemy_leader(next)) must_stop = true; // May continue after retreat // No movement points left. if (n_cost >= max_cost) must_stop = true; console.log("SEARCH BOAT MOVE", space_name(current), ">", space_name(next), c_cost, n_cost, c_ff, n_ff); if (!(next in move_cost) || (n_cost < move_cost[next])) { move_cost[next] = n_cost; move_path[next] = current; if (!must_stop) pq_push(queue, next, n_cost); } }); } } function search_land_move(who, start_space, start_cost, max_cost) { let move_cost = game.move.cost = {}; let move_path = game.move.path = {}; if (start_cost >= max_cost) return; let queue = []; pq_push(queue, start_space, 0); move_cost[start_space] = start_cost; move_path[start_space] = null; const is_lone_ld = is_lone_leader(who); const has_dt = force_has_drilled_troops(who); const has_ax = force_has_auxiliary_unit(who); while (queue.length > 0) { let current = pq_pop(queue); let c_cost = move_cost[current]; let c_ff = has_friendly_fortifications(current); for_each_exit(current, (next, connection) => { let n_ff = has_friendly_fortifications(next); let n_cost = c_cost + 1; let must_stop = false; // Must stop on mountains. if (is_mountain(next) && !n_ff) n_cost = 9; // may not continue // Must stop in the next space after passing through... if (current !== game.move.start_space && !c_ff) { // Drilled Troops that pass through wilderness must stop in the next space. if (has_dt && !has_ax && is_wilderness(current)) if (!game.events.george_croghan) n_cost = 9; // may not continue // Auxiliaries that pass through enemy cultivated must stop in the next space. if (has_ax && !has_dt && is_originally_enemy(current)) n_cost = 9; // may not continue } // Must stop on entering interception space if (next in game.move.intercept) must_stop = true; // Must stop on entering enemy occupied spaces if (has_unbesieged_enemy_units(next)) { if (is_lone_ld) return; // Lone leaders can never enter an enemy occupied space // TODO: Infiltration must_stop = true; // May continue if over-run } if (has_enemy_stockade(next)) { // TODO: Infiltration n_cost = 9; // may not continue } if (has_unbesieged_enemy_fort_or_fortress(next)) { if (!has_dt) return; // Must have Drilled Troops to enter an enemy fort or fortress space. // TODO: Infiltration n_cost = 9; // may not continue } // Stop when entering a space with a lone enemy leader(s) to force a retreat if (!is_lone_ld && has_unbesieged_enemy_leader(next)) must_stop = true; // May continue after retreat // No movement points left. if (n_cost >= max_cost) must_stop = true; console.log("SEARCH LAND MOVE", space_name(current), ">", space_name(next), c_cost, n_cost, must_stop); if (!(next in move_cost) || (n_cost < move_cost[next])) { move_cost[next] = n_cost; move_path[next] = current; if (!must_stop) pq_push(queue, next, n_cost); } }); } } 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(); console.log("CLOSEST", space_name(here), dist); if (dist > stop) break; if (has_unbesieged_friendly_fortifications(here)) { console.log(" FOUND FRIENDLY FORT"); 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; }); } } console.log("CLOSEST =>", result); return result; } // SEQUENCE OF PLAY function start_year() { if (game.tracks.year === 1759 && !game.events.pitt) { log("Placing Amherst, Forbes, and Wolfe into the British leader pool."); game.pieces.pool.push(find_leader("Amherst")); game.pieces.pool.push(find_leader("Forbes")); game.pieces.pool.push(find_leader("Wolfe")); } game.tracks.season = EARLY; start_season(); } function start_season() { switch (game.tracks.season) { case EARLY: log(""); log(`.h1 Early Season of ${game.tracks.year}`); log(""); break; case LATE: log(""); log(`.h1 Late Season of ${game.tracks.year}`); log(""); break; } if (game.events.quiberon) set_active(BRITAIN); else set_active(FRANCE); deal_cards(); start_action_phase(); } function end_season() { log(""); log(".h2 End Season"); log(""); if (game.Britain.hand.length > 0) game.Britain.held = 1; else game.Britain.held = 0; if (game.France.hand.length > 0) game.France.held = 1; else game.France.held = 0; delete game.events.french_regulars; delete game.events.british_regulars; delete player.passed; delete enemy_player.passed; game.tracks.season ++; start_season(); } function end_year() { delete game.events.no_amphib; delete game.events.blockhouses; } function start_action_phase() { game.state = 'action_phase'; log(""); log(`.h2 ${game.active}`); log(""); } function end_action_phase() { lift_sieges_and_amphib(); console.log("END ACTION PHASE"); clear_undo(); game.count = 0; if (!enemy_player.passed && enemy_player.hand.length > 0) { console.log("END ACTION PHASE - NEXT PLAYER"); set_active(enemy()); start_action_phase(); return; } if (!player.passed && player.hand.length > 0) { console.log("END ACTION PHASE - SAME PLAYER"); start_action_phase(); return; } console.log("END ACTION PHASE - END SEASON"); 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); } } function card_name(card) { return `#${card} ${cards[card].name} [${cards[card].activation}]`; } function play_card(card) { log(`Played ${card_name(card)}.`); remove_from_array(player.hand, card); game.cards.current = card; if (card === SURRENDER) game.events.surrender = 1; if (cards[card].special === 'remove') game.cards.removed.push(card); else game.cards.discarded.push(card); } function discard_card(card, reason) { log(`Discarded ${card_name(card)}${reason}.`); remove_from_array(player.hand, card); game.cards.current = card; if (card === SURRENDER) game.events.surrender = 1; game.cards.discarded.push(card); } function remove_card(card) { remove_from_array(game.cards.discarded, card); game.cards.removed.push(card); } states.action_phase = { prompt() { view.prompt = "Action Phase \u2014 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(); gen_action('demolish'); }, demolish() { push_undo(); game.state = 'demolish_before_play'; }, play_event(card) { push_undo(); player.did_construct = 0; play_card(card); events[cards[card].event].play(card); }, activate_force(card) { goto_activate_force(card); }, activate_individually(card) { goto_activate_individually(card); }, construct_stockades(card) { goto_construct_stockades(card); }, construct_forts(card) { goto_construct_forts(card); }, pass() { log(game.active + " pass."); player.passed = 1; end_action_phase(); }, } // ACTIVATION function goto_activate_individually(card) { push_undo(); player.did_construct = 0; discard_card(card, " to activate auxiliaries and leaders"); game.state = 'activate_individually'; game.count = cards[card].activation; game.activation = { type: 'individually', list: [] } } function goto_activate_force(card) { push_undo(); player.did_construct = 0; discard_card(card, " to activate a force"); game.state = 'activate_force'; game.count = cards[card].activation; game.activation = { type: 'force', list: [] } } events.campaign = { play() { game.state = 'activate_campaign'; game.count = 2; game.activation = { type: 'force', list: [] } } } states.activate_individually = { prompt() { view.prompt = `Activate units and/or leaders individually \u2014 ${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)) 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)) { if (game.count >= 0.5) { if (is_indian_unit(p)) gen_action_piece(p); } if (game.count >= 1) { if (is_rangers_unit(p)) gen_action_piece(p); if (is_coureurs_unit(p)) gen_action_piece(p); if (is_drilled_troops(p)) if (game.activation.list.length === 0) gen_action_piece(p); } } } } }, piece(piece) { push_undo(); log(`Activate ${piece_name(piece)}.`); isolate_piece_from_force(piece); game.activation.list.push(piece); if (is_drilled_troops(piece)) game.count = 0; else if (is_indian_unit(piece)) game.count -= 0.5; else game.count -= 1.0; }, next() { push_undo(); goto_pick_move(); }, } 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.count) if (!game.activation.list.includes(p)) gen_action_piece(p); gen_action_pass(); }, piece(p) { push_undo(); log(`Activate force led by ${piece_name(p)}.`); game.activation.list.push(p); game.count = 0; goto_pick_move(); }, pass() { end_action_phase(); }, } states.activate_campaign = { prompt() { if (game.count > 0) { view.prompt = "Activate two leaders and their forces."; for (let p = first_friendly_leader; p <= last_friendly_leader; ++p) { if (is_piece_on_map(p)) if (!game.activation.list.includes(p)) gen_action_piece(p); } } else { view.prompt = "Activate two leaders and their forces \u2014 done."; gen_action_next(); } }, piece(leader) { push_undo(); log(`Activate force led by ${piece_name(leader)}.`); game.activation.list.push(leader); game.count--; }, next() { push_undo(); goto_pick_move(); }, } function goto_pick_move() { if (game.activation.list.length === 0) { // TODO: click next after battles delete game.activation; end_action_phase(); } else if (game.activation.list.length === 1) { pick_move(game.activation.list.pop()); } else { game.state = 'pick_move'; } } states.pick_move = { prompt() { view.prompt = "Select an activated force, leader, or unit to move." gen_action_pass(); game.activation.list.forEach(gen_action_piece); }, piece(p) { push_undo(); remove_from_array(game.activation.list, p); pick_move(p); }, pass() { end_action_phase(); }, } function pick_move(p) { if (game.activation.type === 'force') { game.force = { leader: p, selected: p, reason: 'move', }; game.state = 'define_force'; } else { 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(); clear_undo(); goto_pick_move(); } // DEFINE FORCE (for various actions) states.define_force = { prompt() { let main_leader = game.force.leader; let selected = game.force.selected; let space = piece_space(main_leader); // 5.534 Johnson commands British Iroquois and Mohawk units for free let cap = leader_command(main_leader) - count_non_british_iroquois_and_mohawk_units_in_leader_box(selected); view.prompt = `Define the force to ${game.force.reason} with ${piece_name(main_leader)} from ${space_name(space)}.`; view.prompt += " (" + piece_name(selected) + ")"; view.who = selected; gen_action_next(); // select any leader in the map space for_each_friendly_leader_in_space(space, p => { if (p !== selected) { gen_action_space(leader_box(p)); // XXX if (p !== main_leader && leader_command(p) <= leader_command(selected)) // XXX gen_action_piece(p); } }); // pick up subordinate leaders for_each_friendly_leader_in_node(space, p => { if (p !== selected) { if (p !== main_leader && leader_command(p) <= leader_command(selected)) gen_action_piece(p); } }); // drop off subordinate leaders for_each_friendly_leader_in_node(leader_box(selected), p => { if (p !== selected) { gen_action_piece(p); } }); // pick up units for_each_friendly_unit_in_node(space, p => { // Can't move pieces twice in a campaign: if (game.activation.moved && game.activation.moved.includes(p)) return; // continue if (is_british_iroquois_or_mohawk(p)) { // 5.534 Only Johnson can command British Iroquois and Mohawk (and for free) if (selected === JOHNSON) gen_action_piece(p); } else { if (cap > 0) gen_action_piece(p); } }); // drop off units for_each_friendly_unit_in_node(leader_box(selected), p => { gen_action_piece(p); }); }, piece(piece) { push_undo(); let main_leader = game.force.leader; let selected = game.force.selected; let space = piece_space(main_leader); if (piece_node(piece) === leader_box(selected)) move_piece_to(piece, space); else move_piece_to(piece, leader_box(selected)); }, space(space) { push_undo(); game.force.selected = leader_box_leader(space); }, next() { push_undo(); let main_leader = game.force.leader; let reason = game.force.reason; delete game.force; // Remember which units have moved if we played a campaign card. if (game.activation.list.length > 0) { game.activation.moved = []; for_each_piece_in_force(main_leader, p => { game.activation.moved.push(p); }); } if (reason === 'move') { goto_move_piece(main_leader); } else if (reason === 'intercept') { attempt_intercept(); } else if (reason === 'avoid') { attempt_avoid_battle(); } else if (reason === 'retreat_defender') { game.state = 'retreat_defender'; } else { throw Error("unknown reason state: " + game.reason); } }, } states.define_force_lone_ax = { prompt() { let main_leader = game.force.leader; let selected = game.force.selected; let space = piece_space(main_leader); let n = count_units_in_force(main_leader); view.prompt = `Define lone auxiliary force to ${game.force.reason} with ${piece_name(main_leader)} from ${space_name(space)}.`; view.prompt += " (" + piece_name(selected) + ")"; view.who = selected; // pick up subordinate leaders for_each_friendly_leader_in_node(space, p => { if (p !== selected) { if (p !== main_leader && leader_command(p) <= leader_command(selected)) gen_action_piece(p); } }); // drop off subordinate leaders for_each_friendly_leader_in_node(leader_box(selected), p => { if (p !== selected) { gen_action_piece(p); } }); // pick up units (max 1 auxiliary) if (n === 0) { for_each_friendly_unit_in_node(space, p => { if (is_auxiliary_unit(p)) { if (is_british_iroquois_or_mohawk(p)) { // 5.534 Only Johnson can command British Iroquois and Mohawk (and for free) if (selected === JOHNSON) gen_action_piece(p); } else { gen_action_piece(p); } } }); } if (n === 1) { gen_action_next(); } // drop off units for_each_friendly_unit_in_node(leader_box(selected), p => { gen_action_piece(p); }); }, piece(piece) { push_undo(); let main_leader = game.force.leader; let selected = game.force.selected; let space = piece_space(main_leader); if (piece_node(piece) === leader_box(selected)) move_piece_to(piece, space); else move_piece_to(piece, leader_box(selected)); }, next() { push_undo(); let main_leader = game.force.leader; let reason = game.force.reason; delete game.force; if (reason === 'intercept') { attempt_intercept(); } else { throw Error("unknown reason state: " + game.reason); } }, } // MOVE function goto_move_piece(who) { clear_undo(); log(`Move ${piece_name(who)}.`); let from = piece_space(who); game.state = 'move'; game.move = { moving: who, intercepting: null, intercepted: [], did_attempt_intercept: 0, avoiding: null, avoided: [], start_space: from, start_cost: -1, type: is_only_port_space(from) ? 'naval' : 'land', cost: null, path: null, }; 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 = { prompt() { let where = moving_piece_space(); if (is_assault_possible(where)) { // TODO: RESPONSE - Surrender! allow siege here too to allow surrender event? 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() { goto_siege(moving_piece_space()); }, assault() { goto_assault(moving_piece_space()); }, move() { resume_move(); }, } function goto_break_siege() { console.log("BREAK SIEGE"); let here = moving_piece_space(); game.move.path = { [here]: here }; goto_avoid_battle(); } function may_naval_move(who) { if (game.events.foul_weather) return false; if (game.active === FRANCE && game.no_fr_naval) return false; if (is_leader(who) && count_pieces_in_force(who) > 1) return cards[game.cards.current].activation === 3; return true; } function max_movement_cost() { switch (game.move.type) { case 'boat': return (game.events.foul_weather ? 2 : 9) * 2; // use odd numbers for paths with one land connection case 'land': return (game.events.foul_weather ? 2 : movement_allowance(moving_piece())); case 'naval': return 1; } } function resume_move() { // Interrupt for Foul Weather response at first opportunity to move. if (game.move.start_cost < 0) { if (is_card_available(FOUL_WEATHER)) { set_active(enemy()); game.state = 'foul_weather'; return; } game.move.start_cost = 0; } game.state = 'move'; if (game.move.type === null) { game.move.cost = {}; game.move.path = {}; throw Error("WHAT IS THIS"); } console.log("RESUME_MOVE"); let max_cost = max_movement_cost(); if (game.move.start_cost < max_cost) { let who = moving_piece(); const is_lone_ax = is_lone_auxiliary(who); const is_lone_ld = is_lone_leader(who); game.move.intercept = list_intercept_spaces(is_lone_ld, is_lone_ax); switch (game.move.type) { case 'boat': search_boat_move(who, piece_space(who), game.move.start_cost, max_cost); break; case 'land': search_land_move(who, piece_space(who), game.move.start_cost, max_cost); break; case 'naval': if (may_naval_move(who)) search_naval_move(who, piece_space(who), game.move.start_cost); break; break; } } else { game.move.intercept = {}; game.move.cost = {}; game.move.path = {}; } } function print_path(path, destination, first) { function print_path_rec(prev, next) { if (path[prev] !== null) print_path_rec(path[prev], prev); else if (first) log("moves from " + space_name(prev)); log("moves to " + space_name(next)); } print_path_rec(path[destination], destination); } function remove_enemy_forts_uc_in_path(path, space) { for (;;) { if (has_enemy_fort_uc(space)) { log(`remove fort u/c in ${space_name(space)}`); remove_enemy_fort_uc(space); } let next = path[space]; if (next === null) break; space = next; } } states.move = { prompt() { let who = moving_piece(); let from = piece_space(who); view.prompt = "Move " + piece_name(who) + "."; view.who = who; switch (game.move.type) { default: view.prompt += " Select a movement type."; break; case 'boat': view.prompt += " (boat)"; break; case 'land': view.prompt += " (land)"; break; case 'naval': view.prompt += " (naval)"; break; } if (game.move.start_cost === 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 (!is_only_port_space(from)) { gen_action_x('boat_move', game.move.type !== 'boat'); gen_action_x('land_move', game.move.type !== 'land'); } if (is_port(from)) { // TODO: check valid destinations too if (may_naval_move(who)) gen_action_x('naval_move', game.move.type !== 'naval'); if (!game.events.no_amphib) { if (game.active === BRITAIN && has_amphibious_arrow(from)) { for (let card = first_amphib_card; card <= last_amphib_card; ++card) if (player.hand.includes(card)) gen_action('play_event', card); } } } } gen_action_next(); gen_action('demolish'); if (game.move.cost) { for (let space_id in game.move.cost) { space_id = space_id | 0; if (space_id !== from) gen_action_space(space_id); } } if (is_leader(who)) { for_each_piece_in_force(who, p => { if (p !== who) gen_action_piece(p); }); } }, play_event(card) { push_undo(); play_card(card); if (card === GEORGE_CROGHAN) { game.events.george_croghan = 1; resume_move(); } else { game.state = 'amphibious_landing'; } }, boat_move() { game.move.type = 'boat'; resume_move(); }, land_move() { game.move.type = 'land'; resume_move(); }, naval_move() { game.move.type = 'naval'; resume_move(); }, space(to) { push_undo(); print_path(game.move.path, to, game.move.start_cost === 0); let who = moving_piece(); let cost = game.move.cost[to]; game.move.start_cost = game.move.cost[to]; let from = game.move.path[to]; // remember where we came from so we can retreat after battle game.raid.from[to] = from; // TODO: except space moved into, if it is guarded or lake schooner happens! if (force_has_drilled_troops(who)) remove_enemy_forts_uc_in_path(game.move.path, to); move_piece_to(who, to); lift_sieges_and_amphib(); if (is_card_available(LAKE_SCHOONER)) { if (has_enemy_fortifications(to) && is_lake_connection(from, to)) { clear_undo(); set_active(enemy()); game.state = 'lake_schooner'; return; } } goto_intercept(); }, piece(who) { push_undo(); let force = moving_piece(); let where = piece_space(force); log(`drops off ${piece_name(who)}`); move_piece_to(who, where); resume_move(); }, siege() { goto_siege(moving_piece_space()); }, assault() { goto_assault(moving_piece_space()); }, demolish() { push_undo(); game.state = 'demolish_during_move'; }, next() { end_move(); }, } states.foul_weather = { prompt() { let p = moving_piece(); view.prompt = `${piece_name(p)} is about to move. You may play "Foul Weather" if available.`; view.who = p; if (player.hand.includes(FOUL_WEATHER)) gen_action('play_event', FOUL_WEATHER); gen_action_pass(); }, play_event(c) { play_card(c); game.events.foul_weather = 1; game.move.start_cost = 0; set_active(enemy()); resume_move(); }, pass() { game.move.start_cost = 0; set_active(enemy()); resume_move(); } } states.lake_schooner = { prompt() { let p = moving_piece(); let to = piece_space(p); let from = game.move.path[to]; view.prompt = `${piece_name(p)} moved from ${space_name(from)} to ${space_name(to)}. You may play "Lake Schooner" if available.`; view.who = p; view.where = from; if (player.hand.includes(LAKE_SCHOONER)) gen_action('play_event', LAKE_SCHOONER); gen_action_pass(); }, play_event(c) { play_card(c); let who = moving_piece(); let to = piece_space(who); let from = game.move.path[to]; move_piece_to(who, from); log(`${piece_name(who)} stops in ${space_name(from)}.`); game.move.start_cost = 18; // enemy stops in previously occupied space set_active(enemy()); resume_move(); }, pass() { set_active(enemy()); goto_intercept(); } } states.amphibious_landing = { prompt() { let who = moving_piece(); let from = piece_space(who); view.prompt = "Place amphibious landing marker."; view.who = who; if (from === HALIFAX) { gen_action_space(LOUISBOURG); } if (from === LOUISBOURG) { gen_action_space(BAIE_ST_PAUL); gen_action_space(RIVIERE_OUELLE); gen_action_space(ILE_D_ORLEANS); } }, space(to) { push_undo(); game.Britain.amphib.push(to); let who = moving_piece(); let from = piece_space(who); game.move.path[to] = from; game.move.start_cost = 1; move_piece_to(who, to); lift_sieges_and_amphib(); game.state = 'move'; goto_intercept(); }, } function remove_siege_marker(where) { delete game.sieges[where]; } function place_siege_marker(where) { log(`Siege begun at ${space_name(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(); console.log("BATTLE CHECK", space_name(where)); if (has_unbesieged_enemy_units(where)) { // TODO: breaking the siege (units inside join) goto_battle(where, false, false); } else { end_move_step(false); } } function end_move_step(final) { lift_sieges_and_amphib(); let who = moving_piece(); let where = moving_piece_space(); console.log("END MOVE STEP"); delete game.battle; game.move.did_attempt_intercept = 0; // reset flag for next move step if (has_unbesieged_enemy_fortifications(where)) { game.move.start_cost = 99; if (has_enemy_fort(where) || is_fortress(where)) { place_siege_marker(where); } if (has_enemy_stockade(where)) { if (force_has_drilled_troops(who)) { capture_enemy_stockade(where); if (can_play_massacre()) return goto_massacre('massacre_after_move'); } } } else if (final) { game.move.start_cost = 99; } if (!is_lone_leader(who) && has_unbesieged_enemy_leader(where)) goto_retreat_lone_leader(); else resume_move(); } states.massacre_after_move = { prompt: massacre_prompt, play_event(c) { massacre_play(c); resume_move(); }, next() { set_active(enemy()); resume_move(); } } function end_move() { let who = moving_piece(); console.log("END MOVE"); 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(); } // INTERCEPT function goto_intercept() { let where = moving_piece_space(); if (where in game.move.intercept) { clear_undo(); set_enemy_active('intercept_who'); } else { goto_declare_inside(where); } } function is_moving_piece_lone_ax_in_wilderness_or_mountain() { let p = moving_piece(); let s = piece_space(p); return is_lone_auxiliary(p) && is_wilderness_or_mountain(s); } states.intercept_who = { prompt() { let who = moving_piece(); let where = piece_space(who); let is_lone_ax = is_lone_auxiliary(who); view.prompt = "Select a force or unit to intercept into " + space_name(where) + "."; view.where = where; gen_action_pass(); gen_intercept(is_lone_ax, where); }, piece(piece) { console.log("INTERCEPT WITH", piece_name(piece)); if (is_leader(piece)) { push_undo(); game.move.intercepting = piece; game.force = { leader: piece, selected: piece, reason: 'intercept', }; if (is_moving_piece_lone_ax_in_wilderness_or_mountain()) { isolate_piece_from_force(piece); game.state = 'define_force_lone_ax'; } else { game.state = 'define_force'; } } else { game.move.intercepting = piece; attempt_intercept(); } }, pass() { log(`${game.active} decline to intercept`); end_intercept_fail(); }, } function did_attempt_intercept_to_space(space) { return game.move.intercepted_spaces.includes(space); } function attempt_intercept() { let piece = intercepting_piece(); let tactics = 0; if (is_leader(piece)) { tactics = leader_tactics(piece); for_each_piece_in_force(piece, p => { game.move.intercepted.push(p) }); } else { game.move.intercepted.push(piece); } game.move.did_attempt_intercept = 1; let roll = roll_die("to intercept"); if (roll + tactics >= 4) { if (is_leader(piece)) log(`${piece_name(piece)} attempts to intercept:\n${roll} + ${tactics} >= 4 \u2014 success!`); else log(`${piece_name(piece)} attempts to intercept:\n${roll} >= 4 \u2014 success!`); end_intercept_success(); } else { if (is_leader(piece)) log(`${piece_name(piece)} attempts to intercept:\n${roll} + ${tactics} < 4 \u2014 failure!`); else log(`${piece_name(piece)} attempts to intercept:\n${roll} < 4 \u2014 failure!`); end_intercept_fail(); } } function end_intercept_fail() { set_enemy_active('move'); goto_declare_inside(); } function end_intercept_success() { let who = intercepting_piece(); let to = moving_piece_space(); console.log("INTERCEPT SUCCESS " + piece_name(who) + " TO " + space_name(to)); move_piece_to(who, to); set_enemy_active('move'); goto_declare_inside(); } // DECLARE INSIDE/OUTSIDE FORTIFICATION function goto_declare_inside() { let where = moving_piece_space(); if (has_unbesieged_enemy_units_that_did_not_intercept(where)) { if (is_fortress(where) || has_enemy_fort(where)) { console.log("DECLARE INSIDE/OUTSIDE"); set_enemy_active('declare_inside'); return; } } goto_avoid_battle(); } states.declare_inside = { prompt() { let where = moving_piece_space(); view.prompt = "Declare which units and leaders withdraw into the fortification."; gen_action_next(); let n = count_friendly_units_inside(where); for_each_friendly_piece_in_space(where, p => { if (!is_piece_inside(p) && !did_piece_intercept(p)) { if (is_leader(p) || is_fortress(where) || n < 4) gen_action_piece(p); } }); }, piece(piece) { console.log("INSIDE WITH", piece_name(piece)); push_undo(); isolate_piece_from_force(piece); set_piece_inside(piece); }, next() { set_active(enemy()); goto_avoid_battle(); }, } // AVOID BATTLE function goto_avoid_battle() { let space = moving_piece_space(); if (has_unbesieged_enemy_units(space)) { if (!game.move.did_attempt_intercept) { if (can_enemy_avoid_battle(space)) { console.log("AVOID BATTLE " + space_name(space)); set_enemy_active('avoid_who'); return; } } } goto_battle_check(space); } function did_piece_intercept(p) { return game.move.intercepted.includes(p); } function did_piece_avoid_battle(p) { return game.move.avoided.includes(p); } states.avoid_who = { prompt() { let from = piece_space(moving_piece()); view.prompt = "Select a force or unit to avoid battle in " + space_name(from) + "."; gen_action_pass(); for_each_friendly_piece_in_space(from, p => { if (!did_piece_intercept(p) && !is_piece_inside(p)) gen_action_piece(p); }); }, piece(piece) { console.log("AVOID BATTLE WITH", piece_name(piece)); if (is_leader(piece)) { push_undo(); game.move.avoiding = piece; game.force = { leader: piece, selected: piece, reason: 'avoid', }; game.state = 'define_force'; } else { game.move.avoiding = piece; attempt_avoid_battle(); } }, pass() { log(`${game.active} decline to avoid battle`); end_avoid_battle(); }, } function attempt_avoid_battle() { let from = moving_piece_space(); let piece = avoiding_piece(); let tactics = 0; if (is_leader(piece)) { tactics = leader_tactics(piece); for_each_piece_in_force(piece, p => { game.move.avoided.push(p) }); } else { game.move.avoided.push(piece); } // 6.8 Exception: Auxiliary and all-Auxiliary forces automatically succeed. if (is_wilderness_or_mountain(from) && force_has_only_auxiliary_units(piece)) { log(`${piece_name(piece)} automatically avoids battle from ${from.type} space.`); game.state = 'avoid_to'; return; } let roll = roll_die("to avoid battle"); if (roll + tactics >= 4) { if (is_leader(piece)) log(`${piece_name(piece)} attempts to avoid battle:\n${roll} + ${tactics} >= 4 \u2014 success!`); else log(`${piece_name(piece)} attempts to avoid battle:\n${roll} >= 4 \u2014 success!`); game.state = 'avoid_to'; } else { if (is_leader(piece)) log(`${piece_name(piece)} attempts to avoid battle:\n${roll} + ${tactics} < 4 \u2014 failure!`); else log(`${piece_name(piece)} attempts to avoid battle:\n${roll} < 4 \u2014 failure!`); end_avoid_battle(); } } function can_enemy_avoid_battle(from) { let can_avoid = false; for_each_exit(from, to => { if ((moving_piece_came_from(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 = { prompt() { let from = piece_space(moving_piece()); view.prompt = "Select where to avoid battle to."; gen_action_pass(); for_each_exit(from, to => { if ((moving_piece_came_from(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) { end_avoid_battle_success(to); }, pass() { log(`${game.active} decline to avoid battle`); end_avoid_battle(); }, } function end_avoid_battle_success(to) { let who = avoiding_piece(); console.log("AVOID BATTLE SUCCESS " + piece_name(who) + " TO " + space_name(to)); move_piece_to(who, to); end_avoid_battle(); } function end_avoid_battle() { console.log("END AVOID BATTLE"); set_enemy_active('move'); goto_battle_check(); } // BATTLE function for_each_attacking_piece(fn) { if (game.battle.assault) { 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)) fn(p); } else { for (let p = first_french_piece; p <= last_french_piece; ++p) if (is_piece_in_space(p, where)) fn(p); } } else { for_each_piece_in_force(game.move.moving, fn); if (game.battle.sortie) game.battle.sortie.forEach(fn); } } 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(p) && is_piece_in_space(p, where)) fn(p); } else { for (let p = first_french_piece; p <= last_french_piece; ++p) if (is_piece_unbesieged(p) && is_piece_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_attacking_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, 6, 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: ${r}.`); else if (k === COMBAT_RESULT_TABLE.length - 1) log(`Lookup ${die} on column >= 28: ${r}.`); 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}: ${r}.`); else log(`Lookup ${die} on column ${a}-${b}: ${r}.`); } return r; } } return NaN; } function goto_battle(where, is_assault=false, is_breaking_siege=false) { clear_undo(); game.battle = { where: where, attacker: game.active, defender: enemy(), assault: is_assault, breaking_siege: is_breaking_siege, }; if (game.raid) game.raid.battle = where; log("BATTLE IN " + space_name(where)); // No Militia take part in assaults if (!game.battle.assault) goto_battle_militia(); else goto_battle_sortie(); } function goto_battle_militia() { let box = department_militia(game.battle.where); if (box && count_militia_in_department(box) > 0) { console.log("MILITIA", space_name(game.battle.where), space_name(box)); let dept = null; switch (box) { case ST_LAWRENCE_CANADIAN_MILITIAS: set_active(FRANCE); dept = departments.st_lawrence; break; case NORTHERN_COLONIAL_MILITIAS: set_active(BRITAIN); dept = departments.northern; break; case SOUTHERN_COLONIAL_MILITIAS: set_active(BRITAIN); dept = departments.southern; break; } // 7.3 exception: No Militia if there are enemy raided markers. for (let i = 0; i < dept.length; ++i) if (has_enemy_raided_marker(dept[i])) return goto_battle_sortie(); game.state = 'militia_in_battle'; } else { goto_battle_sortie(); } } states.militia_in_battle = { prompt() { view.prompt = "Determine which Militia units will participate."; let box = department_militia(game.battle.where); view.where = game.battle.where; for (let p = first_friendly_unit; p <= last_friendly_unit; ++p) if (piece_node(p) === box) gen_action_piece(p); gen_action_next(); }, piece(p) { push_undo(); log(`Deploys militia in ${space_name(game.battle.where)}.`); move_piece_to(p, game.battle.where); }, next() { clear_undo(); goto_battle_sortie(); }, } function goto_battle_sortie() { set_active(game.battle.attacker); if (has_besieged_friendly_units(game.battle.where) && has_unbesieged_friendly_units(game.battle.where)) { game.state = 'sortie'; game.battle.sortie = []; } else { goto_battle_attacker_events(); } } states.sortie = { prompt() { view.prompt = "Determine which besieged units will participate in the battle."; view.where = game.battle.where; for (let p = first_friendly_unit; p <= last_friendly_unit; ++p) if (is_piece_in_space(p, game.battle.where) && is_piece_inside(p)) if (!game.battle.sortie.includes(p)) gen_action_piece(p); gen_action_next(); }, piece(p) { push_undo(); log(`${piece_name(p)} sorties.`); game.battle.sortie.push(p); }, next() { clear_undo(); goto_battle_attacker_events(); }, } function count_auxiliary_units_in_attack() { let n = 0; for_each_attacking_piece(p => { if (is_auxiliary_unit(p)) ++n; }); return n; } function count_auxiliary_units_in_defense() { let n = 0; for_each_defending_piece(p => { if (is_auxiliary_unit(p)) ++n; }); return n; } function has_light_infantry_in_attack() { let n = 0; for_each_attacking_piece(p => { if (is_light_infantry_unit(p)) ++n; }); return n > 0; } function has_light_infantry_in_defense() { let n = 0; for_each_defending_piece(p => { if (is_light_infantry_unit(p)) ++n; }); return n > 0; } function can_play_ambush_in_attack() { let s = game.battle.where; if (is_card_available(AMBUSH_1) || is_card_available(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() { let s = game.battle.where; if (is_card_available(AMBUSH_1) || is_card_available(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(COEHORNS)) return game.battle.assault && has_friendly_regulars(game.battle.where); return false; } function can_play_fieldworks_in_attack() { if (is_card_available(FIELDWORKS_1) || is_card_available(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 (is_card_available(FIELDWORKS_1) || is_card_available(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_fieldworks_in_defense()) { game.state = 'defender_events'; } else { goto_battle_roll(); } } states.attacker_events = { prompt() { view.prompt = "Attacker may play battle response cards."; if (can_play_ambush_in_attack()) { if (player.hand.includes(AMBUSH_1)) gen_action('play_event', AMBUSH_1); if (player.hand.includes(AMBUSH_2)) gen_action('play_event', AMBUSH_2); } if (can_play_coehorns_in_attack()) { if (player.hand.includes(COEHORNS)) gen_action('play_event', COEHORNS); } if (can_play_fieldworks_in_attack()) { if (player.hand.includes(FIELDWORKS_1)) gen_action('play_event', FIELDWORKS_1); if (player.hand.includes(FIELDWORKS_2)) gen_action('play_event', FIELDWORKS_2); } 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; } }, next() { clear_undo(); goto_battle_defender_events(); }, } states.defender_events = { prompt() { view.prompt = "Defender may play battle response cards."; if (can_play_ambush_in_defense()) { if (player.hand.includes(AMBUSH_1)) gen_action('play_event', AMBUSH_1); if (player.hand.includes(AMBUSH_2)) gen_action('play_event', AMBUSH_2); } if (can_play_fieldworks_in_defense()) { if (player.hand.includes(FIELDWORKS_1)) gen_action('play_event', FIELDWORKS_1); if (player.hand.includes(FIELDWORKS_2)) gen_action('play_event', FIELDWORKS_2); } 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 FIELDWORKS_1: case FIELDWORKS_2: place_fieldworks(game.battle.where); break; } }, next() { clear_undo(); 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() { 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() { if (game.active === game.battle.attacker) goto_atk_leader_check(); else goto_def_leader_check(); } function end_leader_check() { 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); log(""); log("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("for attacker"); if (is_leader(game.move.moving)) { die = modify(die, leader_tactics(game.move.moving), "leader tactics"); } if (game.events.coehorns === game.battle.attacker) { die = modify(die, 2, "for coehorns"); } if (is_wilderness_or_mountain(game.battle.where)) { let atk_has_ax = some_attacking_piece(p => is_auxiliary_unit(p) || is_light_infantry_unit(p)); let def_has_ax = some_defending_piece(p => is_auxiliary_unit(p) || is_light_infantry_unit(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_regulars_unit(p)); let def_has_reg = some_defending_piece(p => is_regulars_unit(p)); if (!atk_has_reg && def_has_reg) die = modify(die, -1, "vs regulars in cultivated"); } if (has_amphib(game.battle.where) && 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) && !game.battle.assault) { // NOTE: Ignore fieldworks during assault, as they belong to the besieging forces. log(`1 column left vs fieldworks`); shift -= 1; } if (game.battle.assault) { log(`1 column left for assaulting`); 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); log(""); log("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("for defender"); let p = find_friendly_commanding_leader_in_space(game.battle.where); if (p) { die = modify(die, leader_tactics(p), "leader tactics"); } if (is_wilderness_or_mountain(game.battle.where)) { let atk_has_ax = some_attacking_piece(p => is_auxiliary_unit(p) || is_light_infantry_unit(p)); let def_has_ax = some_defending_piece(p => is_auxiliary_unit(p) || is_light_infantry_unit(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_regulars_unit(p)); let def_has_reg = some_defending_piece(p => is_regulars_unit(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); if (game.battle.def_result > 0) { 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); }); } else { end_step_losses(); } } function goto_def_step_losses() { set_active(game.battle.defender); 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); }); } else { end_step_losses(); } } states.step_losses = { prompt() { view.prompt = `Apply step losses (${game.battle.step_loss} total, ${game.battle.dt_loss} from drilled troops).`; let can_reduce = false; 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) && !is_unit_reduced(p)) { can_reduce = true; gen_action_piece(p); } } if (!can_reduce) { for (let i = 0; i < game.battle.units.length; ++i) { let p = game.battle.units[i]; if (is_drilled_troops(p)) { can_reduce = true; gen_action_piece(p); } } } } if (!can_reduce) { for (let i = 0; i < game.battle.units.length; ++i) { let p = game.battle.units[i]; if (!is_unit_reduced(p)) { can_reduce = true; gen_action_piece(p); } } } if (!can_reduce) { for (let i = 0; i < game.battle.units.length; ++i) { let p = game.battle.units[i]; can_reduce = true; gen_action_piece(p); } } } if (!can_reduce) gen_action_next(); }, 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)) remove_from_array(game.battle.units, p); }, next() { clear_undo(); 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 = { 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 (!is_unit_reduced(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)) remove_from_array(game.raid.units, p); }, next() { clear_undo(); 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)) { log(`${game.battle.attacker} leader loss check`); 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)) { log(`${game.battle.defender} leader loss check`); for_each_defending_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(); } states.leader_check = { 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(piece) { let die = roll_die("for leader check"); if (die === 1) { log(`${piece_name(piece)} rolls ${die} and is killed`); eliminate_piece(piece); } else { log(`${piece_name(piece)} rolls ${die} and survives`); } remove_from_array(game.battle.leader_check, piece); if (game.battle.leader_check.length === 0) end_leader_check(); }, } function goto_raid_leader_check() { if (game.raid.leader_check) { game.raid.leader_check = []; log(`${game.active} leader loss check`); for_each_friendly_leader_in_space(game.raid.where, p => { game.raid.leader_check.push(p); }); if (game.raid.leader_check.length > 0) { game.state = 'raid_leader_check'; } else { delete game.raid.leader_check; raiders_go_home(); } } else { raiders_go_home(); } } states.raid_leader_check = { 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(piece) { let die = roll_die("for leader check"); if (die === 1) { log(`${piece_name(piece)} rolls ${die} and is killed`); eliminate_piece(piece); } else { log(`${piece_name(piece)} rolls ${die} and survives`); } remove_from_array(game.raid.leader_check, piece); if (game.raid.leader_check.length === 0) { delete game.raid.leader_check; raiders_go_home(); } }, } // WINNER/LOSER function return_militia(where) { let box = department_militia(where); console.log("RETURN MILITIA", space_name(where), space_name(box)); if (box) { let n = 0; for (let p = 1; p < pieces.length; ++p) { if (is_militia_unit(p) && is_piece_in_space(p, where)) { move_piece_to(p, box); ++n; } } if (n > 0) { log(`${n} Militia units return to their box.`); } } } function goto_determine_winner() { set_active(game.battle.attacker); if (game.battle.assault) determine_winner_assault(); else determine_winner_battle(); } function determine_winner_battle() { let where = game.battle.where; log(""); // 7.8: Determine winner let atk_surv = count_friendly_units_in_space(where); let def_surv = count_unbesieged_enemy_units_in_space(where); let victor; if (atk_surv === 0 && def_surv === 0) victor = game.battle.defender; else if (def_surv === 0) victor = game.battle.attacker; else if (atk_surv === 0) victor = game.battle.defender; else if (game.battle.atk_result > game.battle.def_result) victor = game.battle.attacker; else victor = game.battle.defender; // TODO: 7.64 leaders retreat if all units lost // TODO: 7.8: Award vp return_militia(game.battle.where); if (victor === game.battle.attacker) remove_fieldworks(where); // Raid battle vs militia if (game.raid && game.raid.where > 0) { if (victor === game.battle.attacker) { log("ATTACKER WON RAID BATTLE VS MILITIA"); goto_raid_events(); } else { log("DEFENDER WON RAID BATTLE VS MILITIA"); retreat_attacker(game.raid.where, game.raid.from[game.raid.where] | 0); } return; } // TODO: Breakout // Normal battle if (victor === game.battle.attacker) { log("ATTACKER WON"); if (has_unbesieged_enemy_units(where)) { goto_retreat_defender(); } else { if (def_surv === 0 && game.battle.def_result === 0) { log("OVERRUN"); end_move_step(false); } else { end_move_step(true); } } } else { log("DEFENDER WON"); let from = game.battle.where; let to = moving_piece_came_from(game.battle.where); retreat_attacker(from, to); } } function eliminate_enemy_pieces_inside(where) { for (let p = first_enemy_piece; p <= last_enemy_piece; ++p) if (is_piece_in_space(p, where) && is_piece_inside(p)) eliminate_piece(p); } function determine_winner_assault() { let where = game.battle.where; let victor; if (game.battle.atk_result > game.battle.def_result) victor = game.battle.attacker; else victor = game.battle.defender; if (victor === game.battle.attacker) { log("ATTACKER WON ASSAULT"); eliminate_enemy_pieces_inside(where); remove_siege_marker(where); remove_fieldworks(where); if (has_enemy_fortress(where)) { capture_enemy_fortress(where); if (can_play_massacre()) return goto_massacre('massacre_after_assault'); } if (has_enemy_fort(where)) { capture_enemy_fort(where); if (can_play_massacre()) return goto_massacre('massacre_after_assault'); } } else { log("DEFENDER WON ASSAULT"); } end_activation(); } states.massacre_after_assault = { prompt: massacre_prompt, play_event(c) { massacre_play(c); end_activation(); }, next() { set_active(enemy()); end_activation(); } } // RETREAT function can_attacker_retreat_from_to(p, from, to) { console.log("RETREAT QUERY (ATTACK)", piece_name(p), space_name(from), space_name(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 = { 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; delete game.retreat; console.log("RETREAT ATTACKER", space_name(from), "to", space_name(to)); // NOTE: Besieged pieces assaulting out are already inside so not affected by the code below. // NOTE: We unstack forces here by retreating individual units before leaders! for_each_friendly_unit_in_space(from, p => { if (!is_piece_inside(p)) { if (can_attacker_retreat_from_to(p, from, to)) move_piece_to(p, to); else eliminate_piece(p); } }); for_each_friendly_leader_in_space(from, p => { if (!is_piece_inside(p)) { if (can_attacker_retreat_from_to(p, from, to)) move_piece_to(p, to); else eliminate_piece(p); } }); // 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] = from; return goto_pick_raid(); } // Normal battle end_retreat(); } } function goto_retreat_defender() { set_active(game.battle.defender); let from = battle_space(); let commander = find_friendly_commanding_leader_in_space(from); if (commander && has_friendly_units(from)) { game.force = { leader: commander, selected: commander, reason: 'retreat_defender', }; game.state = 'define_force'; } else { game.state = 'retreat_defender'; } } function can_defender_retreat_from_to(p, from, to) { console.log("RETREAT QUERY", piece_name(p), space_name(from), space_name(to), "atk came from", moving_piece_came_from(from)); if (has_unbesieged_enemy_units(to)) return false; if (has_unbesieged_enemy_fortifications(to)) return false; if (moving_piece_came_from(from) === 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 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; } // TODO: auto-select pieces to retreat? states.retreat_defender = { prompt() { let from = battle_space(); 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)) { console.log(piece_name(p) + " CAN RETREAT"); can_retreat = true; gen_action_piece(p); } else console.log(piece_name(p) + " CANNOT RETREAT"); }); if (!can_retreat) { view.prompt += " done."; gen_action_next(); } else { view.prompt += " select piece to retreat."; } }, piece(piece) { push_undo(); game.battle.who = piece; game.state = 'retreat_defender_to'; }, next() { clear_undo(); let from = battle_space(); for_each_friendly_piece_in_space(from, p => { if (!is_piece_inside(p)) eliminate_piece(p); }); end_retreat(); }, } states.retreat_defender_to = { prompt() { let from = battle_space(); let who = game.battle.who; view.prompt = "Retreat losing leaders and units \u2014 select destination."; 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 = battle_space(); let who = game.battle.who; if (from === to) { log("retreats inside fortification"); set_force_inside(who); } else { log("retreats to " + space_name(to)); move_piece_to(who, to); } game.state = 'retreat_defender'; }, } function end_retreat() { set_active(game.battle.attacker); end_move_step(true); } function goto_retreat_lone_leader() { clear_undo(); set_active(enemy()); game.state = 'retreat_lone_leader'; } function pick_unbesieged_leader(s) { for (let p = first_friendly_leader; p <= last_friendly_leader; ++p) if (is_piece_in_space(p, s) && !is_piece_inside(p)) return p; return 0; } states.retreat_lone_leader = { prompt() { let from = moving_piece_space(); 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)) { can_retreat = true; gen_action_space(to); } }); if (!can_retreat) gen_action('eliminate'); }, eliminate() { let from = moving_piece_space(); let who = pick_unbesieged_leader(from); eliminate_piece(who); resume_retreat_lone_leader(from); }, space(to) { let from = moving_piece_space(); let who = pick_unbesieged_leader(from); if (from === to) { log("retreats inside fortification"); set_force_inside(who); } else { log("retreats to " + space_name(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) { set_active(enemy()); resume_move(); } } // SIEGE const SIEGE_TABLE = [ 0, 0, 0, 1, 1, 1, 2, 2 ]; function can_moving_force_siege_or_assault() { let leader = moving_piece(); let space = piece_space(leader); if (has_besieged_enemy_fortifications(space)) { let commanding = find_friendly_commanding_leader_in_space(space); if (leader === commanding && has_friendly_supplied_drilled_troops(space)) { return true; } } return false; } function can_play_coehorns_in_siege(space) { return is_card_available(COEHORNS) && has_friendly_regulars(space); } function goto_siege(space) { clear_undo(); 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 = { prompt() { view.prompt = `Siege in ${space_name(game.siege_where)}. You may play "Coehorns & Howitzers" if available.`; if (player.hand.includes(COEHORNS)) gen_action('play_event', COEHORNS); 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 = { prompt() { view.prompt = `Siege in ${space_name(game.siege_where)}. You may play "Coehorns & Howitzers" if available.`; if (player.hand.includes(COEHORNS)) gen_action('play_event', COEHORNS); 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_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 = { prompt() { view.prompt = `Siege in ${space_name(game.siege_where)}. You may play "Surrender!" if available.`; if (player.hand.includes(SURRENDER)) gen_action('play_event', 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('massacre_after_surrender'); goto_surrender_place(); } states.massacre_after_surrender = { prompt: massacre_prompt, play_event(c) { massacre_play(c); goto_surrender_place(); }, next() { set_active(enemy()); goto_surrender_place(); } } function goto_surrender_place() { set_active(enemy()); game.state = 'surrender'; game.surrender = find_closest_friendly_unbesieged_fortification(game.siege_where); } states.surrender = { 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]); }, 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(); } } function end_surrender() { set_active(enemy()); delete game.surrender; delete game.siege_where; end_activation(); } function resolve_siege() { let space = game.siege_where; log("Resolve siege in " + space_name(space)); let att_leader = find_friendly_commanding_leader_in_space(space); let def_leader = find_enemy_commanding_leader_in_space(space); let die = roll_die("for siege"); die = modify(die, leader_tactics(att_leader), "besieging leader"); if (def_leader) die = modify(die, -leader_tactics(def_leader), "defending leader"); if (space === LOUISBOURG) die = modify(die, -1, "for Louisbourg"); let result = SIEGE_TABLE[clamp(die, 0, 7)]; log(`Result(${die}): ${result}`); if (result > 0) { let level = change_siege_marker(space, result); log("Siege level is " + level); } goto_assault_possible(space); } // ASSAULT function is_assault_possible(space) { let siege_level = game.sieges[space] | 0; if (has_enemy_fort(space) && siege_level >= 1) return true; if (has_enemy_fortress(space) && siege_level >= 2) return true; return false; } function goto_assault_possible(space) { if (is_assault_possible(space)) { game.state = 'assault_possible'; game.assault_possible = space; } else { end_activation(); } } states.assault_possible = { prompt() { view.prompt = "Assault is 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("Does not assault " + space_name(where)); end_activation(); }, } function goto_assault(where) { log("Assault " + space_name(where)); goto_battle(where, true, false); } // RAID function goto_pick_raid() { if (game.raid.list.length > 0) { clear_undo(); game.state = 'pick_raid'; } else { delete game.raid; // TODO: allow demolish before ending activation end_activation(); } } states.pick_raid = { prompt() { view.prompt = "Pick next raid space."; for (let i=0; i < game.raid.list.length; ++i) gen_action_space(game.raid.list[i]); }, space(s) { log("raids " + space_name(s)) 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)) { console.log("MILITIA AGAINST RAID", space_name(where), space_name(game.raid.battle)); if (where === game.raid.battle) { console.log("BATTLED AGAINST STOCKADE, NO MILITIA ALLOWED", space_name(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 = { prompt() { view.prompt = `You may deploy one Militia unit against the raid in ${space_name(game.raid.where)}.`; view.where = game.raid.where; if (game.count > 0) { let box = department_militia(game.raid.where); for (let p = first_friendly_unit; p <= last_friendly_unit; ++p) if (piece_node(p) === box) gen_action_piece(p); } gen_action_next(); }, piece(p) { push_undo(); log(`Deploys militia in ${space_name(game.raid.where)}.`); move_piece_to(p, game.raid.where); game.count --; }, next() { clear_undo(); set_active(enemy()); if (game.count === 0) goto_battle(game.raid.where, false, 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_card_available(BLOCKHOUSES)) { set_active(enemy()); game.state = 'raid_blockhouses'; } else { resolve_raid(); } } states.raid_blockhouses = { prompt() { view.prompt = `Raid in ${space_name(game.raid.where)}. You may play "Blockhouses" if available.`; if (player.hand.includes(BLOCKHOUSES)) gen_action('play_event', BLOCKHOUSES); gen_action_pass(); }, play_event(c) { play_card(c); game.events.blockhouses = game.active; set_active(enemy()); resolve_raid(); }, 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; // TODO: only use leader that could command the force (johnson & indians, activation limit, etc?) 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]; if (success) { log(`Result ${die} vs ${column}: Success with ${losses} losses.`); if (x_stockade || x_allied || !has_friendly_raided_marker(where)) place_friendly_raided_marker(where); if (x_stockade) eliminate_enemy_stockade_in_raid(where); if (x_allied) eliminate_indian_tribe(indian_tribe[where]); } else { log(`Result ${die} vs ${column}: Failure with ${losses} losses.`); } game.raid.step_loss = losses; // 10.32: leader check if (natural_die === 1 || (natural_die === 6 && column === 'vs_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(); } function next_raider_in_space(from) { for (let p = first_friendly_piece; p <= last_friendly_piece; ++p) { if (is_piece_in_space(p, from)) { isolate_piece_from_force(p); return p; } } return 0; } function raiders_go_home() { // RULE: raiders go home -- can go to separate locations if many available? // Leaders, coureurs and rangers go to nearest fortification // Indians may follow leader // Indians go to home settlement let from = game.raid.where; let x_leader = find_friendly_commanding_leader_in_space(from); let x_rangers = has_friendly_rangers(from); let x_coureurs = has_friendly_coureurs(from); game.go_home = { who: next_raider_in_space(from), }; if (x_leader || x_rangers || x_coureurs) { game.go_home.closest = find_closest_friendly_unbesieged_fortification(from); if (x_leader) game.go_home.leader = []; } game.state = 'raiders_go_home'; } states.raiders_go_home = { prompt() { let who = game.go_home.who; if (who) view.prompt = "Raiders go home \u2014 " + piece_name(who) + "."; else view.prompt = "Raiders go home \u2014 done."; view.who = who; if (!who) { gen_action('next'); } else if (is_indian_unit(who)) { // 10.412: Cherokee have no home settlement (home=0) let home = indian_home_settlement(who); if (home && has_friendly_allied_settlement(home) && !has_enemy_units(home)) gen_action_space(home); else gen_action('eliminate'); // 10.422: Indians stacked with a leader may accompany him if (game.go_home.leader) for (let i=0; i < game.go_home.leader.length; ++i) gen_action_space(game.go_home.leader[i]); } else { for (let i=0; i < game.go_home.closest.length; ++i) gen_action_space(game.go_home.closest[i]); } }, space(s) { push_undo(); let who = game.go_home.who; log(`${piece_name(who)} goes home to ${space_name(s)}.`); move_piece_to(who, s); if (is_leader(who) && !game.go_home.leader.includes(s)) game.go_home.leader.push(s); game.go_home.who = next_raider_in_space(game.raid.where); }, eliminate() { push_undo(); eliminate_piece(game.go_home.who); game.go_home.who = next_raider_in_space(game.raid.where); }, next() { delete game.go_home; goto_pick_raid(); } } // DEMOLITION function demolish(s) { if (has_fieldworks(s)) { remove_fieldworks(s); } else if (has_friendly_stockade(s)) { log(`Demolishes stockade at ${space_name(s)}.`); remove_friendly_stockade(s); } else if (has_friendly_fort_uc(s)) { log(`Demolishes fort U/C at ${space_name(s)}.`); remove_friendly_fort_uc(s); } else if (has_friendly_fort(s)) { log(`Demolishes fort at ${space_name(s)}.`); award_vp(-1); remove_friendly_fort(s); } } function demolish_prompt() { view.prompt = "You may demolish any friendly unbesieged fortification." for (let s of game.fieldworks) if (is_friendly_controlled_space(s) || has_unbesieged_friendly_units(s)) gen_action_space(s); for (let s of player.stockades) if (is_space_unbesieged(s)) gen_action_space(s); for (let s of player.forts_uc) if (is_space_unbesieged(s)) gen_action_space(s); for (let s of player.forts) if (is_space_unbesieged(s)) gen_action_space(s); } states.demolish_before_play = { prompt: demolish_prompt, space(s) { demolish(s); game.state = 'action_phase'; }, next() { game.state = 'action_phase'; } } states.demolish_during_move = { prompt: demolish_prompt, space(s) { demolish(s); game.state = 'move'; }, next() { game.state = 'move'; } } // CONSTRUCTION function format_remain(n) { if (n === 0) return "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 = { prompt() { view.prompt = `Construct stockades \u2014 ${format_remain(game.count)}.`; gen_action_next(); if (game.count > 0) { for (let space = first_space; space <= last_space; ++space) { if (has_friendly_supplied_drilled_troops(space) || is_originally_friendly(space)) { if (has_enemy_units(space)) continue; if (has_enemy_fortifications(space)) continue; if (has_friendly_fortifications(space)) continue; if (is_space_besieged(space)) continue; if (is_fortress(space)) continue; gen_action_space(space); } } } }, space(space) { push_undo(); log(`build a stockade in ${space_name(space)}.`); player.stockades.push(space); game.count --; }, next() { end_action_phase(); }, } 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 = { prompt() { view.prompt = `Construct forts \u2014 ${format_remain(game.count)}.`; gen_action_next(); if (game.count > 0) { for (let space = first_space; space <= last_space; ++space) { if (has_friendly_supplied_drilled_troops(space)) { if (game.list.includes(space)) continue; if (has_friendly_fort(space)) continue; if (is_space_besieged(space)) continue; if (is_fortress(space)) continue; gen_action_space(space); } } } }, space(space) { push_undo(); if (has_friendly_fort_uc(space)) { log(`finish building a fort in ${space_name(space)}.`); place_friendly_fort(space); } else { log(`start building a fort in ${space_name(space)}.`); place_friendly_fort_uc(space); game.list.push(space); // don't finish it in the same action phase } game.count --; }, next() { delete game.list; end_action_phase(); }, } // EVENTS function can_play_massacre() { let s = moving_piece_space(); if (is_card_available(MASSACRE)) return has_friendly_indians(s) && has_friendly_drilled_troops(s); return false; } function goto_massacre(st) { clear_undo(); set_active(enemy()); game.state = st; } function massacre_prompt() { view.prompt = `You may play "Massacre!".`; if (player.hand.includes(MASSACRE)) gen_action('play_event', MASSACRE); gen_action_next(); } function massacre_play(c) { // TODO: massacre state for manual elimination? play_card(c); let s = moving_piece_space(); for (let p = 1; p < pieces.length; ++p) if (is_indian_unit(p) && is_piece_in_space(p, s)) eliminate_piece(p); award_vp(1); set_active(enemy()); } 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) { // TODO: out-of-supply drilled troops return is_piece_on_map(p) && is_piece_unbesieged(p) && is_unit_reduced(p); } function count_french_raids_in_dept(dept) { let n = 0; for (let i = 0; i < game.France.raids.length; ++i) { let s = game.France.raids[i]; if (departments[dept].includes(s)) ++n; } return n; } events.provincial_regiments_dispersed_for_frontier_duty = { can_play() { let s = Math.min(count_french_raids_in_dept('southern'), count_provincial_units_from('southern')); let n = Math.min(count_french_raids_in_dept('northern'), count_provincial_units_from('northern')); return (s + n) > 0; }, play() { game.state = 'provincial_regiments_dispersed_for_frontier_duty'; game.frontier_duty = { southern: Math.min(count_french_raids_in_dept('southern'), count_provincial_units_from('southern')), northern: Math.min(count_french_raids_in_dept('northern'), count_provincial_units_from('northern')), }; } } states.provincial_regiments_dispersed_for_frontier_duty = { prompt() { view.prompt = `Eliminate ${game.frontier_duty.southern} southern and ${game.frontier_duty.northern} northern provincials.`; let can_eliminate = false; for (let p = first_british_unit; p <= last_british_unit; ++p) { if ((game.frontier_duty.northern > 0 && is_provincial_unit_from(p, 'northern')) || (game.frontier_duty.southern > 0 && is_provincial_unit_from(p, 'southern'))) { can_eliminate = true; gen_action_piece(p); } } if (!can_eliminate) gen_action_next(); }, piece(p) { push_undo(); if (is_provincial_unit_from(p, 'southern')) game.frontier_duty.southern --; if (is_provincial_unit_from(p, 'northern')) 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() { clear_undo(); // rolling die let roll = roll_die(); if (game.tracks.vp > 4) game.count = roll; else game.count = Math.ceil(roll / 2); if (has_friendly_fort(NIAGARA)) game.alliance = [ 'northern', 'pays-d-en-haut' ]; else game.alliance = [ 'northern' ]; game.state = 'indian_alliance'; } } events.western_indian_alliance = { can_play() { return is_friendly_controlled_space(MONTREAL); }, play() { clear_undo(); // rolling die let roll = roll_die(); if (game.tracks.vp > 4) game.count = roll; else game.count = Math.ceil(roll / 2); if (has_friendly_fort(NIAGARA)) game.alliance = [ 'western', 'pays-d-en-haut' ]; else game.alliance = [ 'western' ]; 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 within_two_of_gray_settlement.includes(piece_space(JOHNSON)); return true; } return false; }, play() { clear_undo(); // rolling die let roll = roll_die(); game.state = 'indian_alliance'; game.count = roll; game.alliance = [ 'iroquois' ]; }, } function find_friendly_unused_indian(home) { if (home === PAYS_D_EN_HAUT) { for (let p = first_friendly_unit; p <= last_friendly_unit; ++p) if (is_pays_d_en_haut_indian_unit(p) && is_piece_unused(p)) return p; } else { let tribe = indian_tribe[home]; for (let p = first_friendly_unit; p <= last_friendly_unit; ++p) if (pieces[p].name === tribe && is_piece_unused(p)) return p; } return 0; } function is_indian_alliance(p, alliance) { if (is_indian_unit(p)) return pieces[p].subtype === alliance; return false; } states.indian_alliance = { prompt() { view.prompt = `Place at indians at their settlements or restore to full (${game.count} left).`; let can_place = false; for (let a of game.alliance) { if (game.count >= 1) { for (let s of indian_spaces[a]) { if (!has_enemy_allied_settlement(s)) { let p = find_friendly_unused_indian(s); if (p && can_place_in_space(s)) { can_place = true; gen_action_space(s); } } } } if (game.count >= 0.5) { for (let p = first_friendly_unit; p <= last_friendly_unit; ++p) { if (is_indian_alliance(p, a)) { if (can_restore_unit(p)) { can_place = true; gen_action_piece(p); } } } } } if (!can_place) gen_action_next(); }, space(s) { push_undo(); let p = find_friendly_unused_indian(s); if (p) { place_piece(p, s); game.count -= 1.0; } }, 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_and_restore_british_indian_tribe(s, tribe) { push_undo(); // TODO: restore_mohawks/cherokee state for manual restoring? // OPTIMIZE: use mohawks piece list for (let p = first_british_unit; p <= last_british_unit; ++p) { if (is_indian_tribe(p, tribe)) { if (is_piece_unused(p)) place_piece(p, s); if (can_restore_unit(p)) restore_unit(p); } } game.count = 0; } events.mohawks = { can_play() { let s = piece_space(JOHNSON); if (within_two_of_canajoharie.includes(s)) if (is_piece_unbesieged(JOHNSON)) return true; return false; }, play() { game.state = 'mohawks'; game.count = 1; }, } states.mohawks = { prompt() { view.prompt = "Place all Mohawk units not on the map with Johnson."; let can_place = false; if (game.count > 0) { let s = piece_space(JOHNSON); if (can_place_in_space(s)) { can_place = true; gen_action_space(s); } } if (!can_place) gen_action_next(); }, space(s) { place_and_restore_british_indian_tribe(s, 'Mohawk'); }, next() { end_action_phase(); }, } events.cherokees = { can_play() { if (game.events.cherokee_uprising) return false; return true; }, play() { game.events.cherokees = 1; game.state = 'cherokees'; game.count = 1; }, } states.cherokees = { prompt() { view.prompt = "Place all Cherokee units not on the map at a British fortification in the southern dept."; let can_place = false; if (game.count > 0) { for (let i = 0; i < departments.southern.length; ++i) { let s = departments.southern[i]; if (has_unbesieged_friendly_fortifications(s)) { can_place = true; gen_action_space(s); } } } if (!can_place) gen_action_next(); }, space(s) { place_and_restore_british_indian_tribe(s, 'Cherokee'); }, next() { end_action_phase(); }, } events.cherokee_uprising = { can_play() { if (game.events.cherokees) return true; return false; }, play() { clear_undo(); 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() { view.prompt = `Eliminate ${game.uprising.regular} regular, ${game.uprising.southern} southern provincial, and all Cherokee.`; let can_eliminate = false; 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_regulars_unit(p)) x = true; if (game.uprising.southern > 0 && is_provincial_unit_from(p, 'southern')) x = true; if (is_indian_tribe(p, 'Cherokee')) x = true; if (x) { can_eliminate = true; gen_action_piece(p); } } } if (!can_eliminate) gen_action_next(); }, piece(p) { push_undo(); if (is_regulars_unit(p)) game.uprising.regular --; if (is_provincial_unit_from(p, 'southern')) 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() { // TODO: treaty_of_easton state for manual elimination? for (let p = first_french_unit; p <= last_french_unit; ++p) { if (is_indian_unit(p) && is_piece_on_map(p) && is_piece_unbesieged(p)) { if (is_western_indian_unit(p)) { eliminate_piece(p); } } } 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_unit(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 = `Eliminate up to two unbesieged Indian units from any one space (${game.count} left).`; } else { view.prompt = "Eliminate up to two unbesieged Indian units from any 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; log("French Navy operates aggressively."); log("No amphibious landings this year."); let roll = roll_die() 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.swap = 0; }, } states.governor_vaudreuil_interferes = { prompt() { view.prompt = "Choose any 2 unbesieged French leaders and reverse their locations."; if (game.swap) view.who = game.swap; 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); } }, piece(p) { if (game.swap) { isolate_piece_from_force(p); 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(p)} and ${piece_name(a)} reverse locations.`); end_action_phase(); } else { push_undo(); isolate_piece_from_force(p); game.swap = p; } }, } events.small_pox = { can_play() { for (let s = first_space; s <= last_space; ++s) if (count_enemy_units_in_space(s) > 4) return true; return false; }, play() { game.state = 'small_pox'; }, } states.small_pox = { prompt() { view.prompt = "Choose a space with more than 4 units."; for (let s = first_space; s <= last_space; ++s) if (count_enemy_units_in_space(s) > 4) gen_action_space(s); }, space(s) { clear_undo(); // rolling die log(`Small Pox in ${space_name(s)}.`); let roll = roll_die(); if (count_enemy_units_in_space(s) > 8) { game.count = roll; } else { game.count = Math.ceil(roll / 2); } log(`Must eliminate ${game.count} steps.`); clear_undo(); game.state = 'reduce_from_small_pox'; game.small_pox = s; set_active(enemy()); }, } states.reduce_from_small_pox = { prompt() { view.prompt = `Small Pox in ${space_name(game.small_pox)} \u2014 eliminate ${game.count} steps.`; if (game.count > 0) { let can_reduce = false; for_each_friendly_unit_in_space(game.small_pox, p => { if (!is_unit_reduced(p)) { can_reduce = true; gen_action_piece(p); } }); if (!can_reduce) { for_each_friendly_unit_in_space(game.small_pox, p => { if (is_unit_reduced(p)) gen_action_piece(p); }); } } else { gen_action_next(); } }, piece(p) { push_undo(); game.count --; reduce_unit(p); }, next() { log("Remove all Indians."); for_each_friendly_unit_in_space(game.small_pox, p => { if (is_indian_unit(p)) eliminate_piece(p); }); 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(`Steals ${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.cards.discarded.push(c); log(`France discards ${card_name(c)}.`); if (c === SURRENDER) game.events.surrender = 1; 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 (british_ministerial_crisis_cards.includes(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() { view.prompt = "British Ministerial Crisis \u2014 discard one of the listed cards."; if (game.count > 0) { for (let i = 0; i < player.hand.length; ++i) { let c = player.hand[i]; if (british_ministerial_crisis_cards.includes(c)) gen_action_discard(c); } } else { gen_action_next(); } }, card(c) { push_undo(); game.count = 0; discard_card(c, ""); }, next() { set_active(enemy()); end_action_phase(); } } function count_reduced_unbesieged_provincial_units_from(dept) { let n = 0; for (let p = first_british_unit; p <= last_british_unit; ++p) if (is_provincial_unit_from(p, dept) && is_piece_on_map(p)) if (is_piece_unbesieged(p) && is_unit_reduced(p)) ++n; return n; } function count_unbesieged_provincial_units_from(dept) { let n = 0; // OPTIMIZE: use provincial unit numbers for (let p = first_british_unit; p <= last_british_unit; ++p) if (is_provincial_unit_from(p, dept) && is_piece_on_map(p)) if (is_piece_unbesieged(p)) ++n; return n; } function count_provincial_units_from(dept) { let n = 0; // OPTIMIZE: use provincial unit numbers for (let p = first_british_unit; p <= last_british_unit; ++p) if (is_provincial_unit_from(p, dept) && is_piece_on_map(p)) ++n; return n; } events.stingy_provincial_assembly = { can_play() { if (game.tracks.pa === ENTHUSIASTIC) return false; let num_n = count_unbesieged_provincial_units_from('northern'); let num_s = count_unbesieged_provincial_units_from('southern'); return (num_n + num_s) > 0; }, play() { let num_n = count_unbesieged_provincial_units_from('northern'); let num_s = count_unbesieged_provincial_units_from('southern'); 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 \u2014 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) { clear_undo(); set_active(enemy()); game.state = 'stingy_provincial_assembly'; game.department = dept; game.count = 1; } states.stingy_provincial_assembly = { prompt() { view.prompt = `Stingy Provincial Assembly \u2014 remove a ${game.department} provincial unit.`; if (game.count > 0) { // OPTIMIZE: use provincial unit numbers for (let p = first_british_unit; p <= last_british_unit; ++p) if (is_provincial_unit_from(p, game.department) && is_piece_unbesieged(p)) gen_action_piece(p); } else { 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.tracks.pa > 0; return game.tracks.pa < 2; }, play() { if (game.active === FRANCE) { game.tracks.pa -= 1; log(`Provincial Assemblies reduced to ${pa_name()}.`); goto_enforce_provincial_limits(); } else { game.tracks.pa += 1; log(`Provincial Assemblies increased to ${pa_name()}.`); end_action_phase(); } }, } function pa_name() { switch (game.tracks.pa) { case RELUCTANT: return "Reluctant"; case SUPPORTIVE: return "Supportive"; case ENTHUSIASTIC: return "Enthusiastic"; } } const provincial_limit_southern = [ 2, 4, 6 ]; const provincial_limit_northern = [ 6, 10, 18 ]; function provincial_limit(dept) { if (dept === 'northern') return provincial_limit_northern[game.tracks.pa]; else return provincial_limit_southern[game.tracks.pa]; } function goto_enforce_provincial_limits() { if (game.tracks.pa < ENTHUSIASTIC) { let num_s = count_provincial_units_from('southern'); let num_n = count_provincial_units_from('northern'); let max_n = provincial_limit('northern'); let max_s = provincial_limit('southern'); if (num_s > max_s || num_n > max_n) { clear_undo(); set_active(enemy()); game.state = 'enforce_provincial_limits'; return; } } end_action_phase(); } states.enforce_provincial_limits = { prompt() { let num_s = count_provincial_units_from('southern'); let num_n = count_provincial_units_from('northern'); let max_n = provincial_limit('northern'); let max_s = provincial_limit('southern'); console.log("British Colonial Politics", num_s, max_s, num_n, max_n); let can_remove = false; if (num_s > max_s || num_n > max_n) { // OPTIMIZE: use provincial unit numbers for (let p = first_british_unit; p <= last_british_unit; ++p) { if (num_s > max_s && is_provincial_unit_from(p, 'southern') && is_piece_unbesieged(p)) { gen_action_piece(p); can_remove = true; } else if (num_n > max_n && is_provincial_unit_from(p, 'northern') && is_piece_unbesieged(p)) { gen_action_piece(p); can_remove = true; } } } if (!can_remove) { view.prompt = `Remove provincial units over limit \u2014 done.`; gen_action_next(); } else { view.prompt = `Remove provincial units over limit: ${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_provincial_regiments(dept) { let num = count_provincial_units_from(dept); let max = provincial_limit(dept); return num < max; } function can_restore_provincial_regiments(dept) { return count_reduced_unbesieged_provincial_units_from(dept) > 0; } events.raise_provincial_regiments = { can_play() { if (game.tracks.pa === RELUCTANT) return false; if (can_raise_provincial_regiments('northern') || can_restore_provincial_regiments('northern')) return true; if (can_raise_provincial_regiments('southern') || can_restore_provincial_regiments('southern')) 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_provincial_regiments('northern') || can_restore_provincial_regiments('northern')) gen_action('northern'); if (can_raise_provincial_regiments('southern') || can_restore_provincial_regiments('southern')) gen_action('southern'); }, northern() { push_undo(); let num = count_provincial_units_from('northern'); let max = provincial_limit('northern'); game.state = 'raise_provincial_regiments'; game.count = clamp(max - num, 0, 4); game.department = 'northern'; }, southern() { push_undo(); let num = count_provincial_units_from('southern'); let max = provincial_limit('southern'); game.state = 'raise_provincial_regiments'; game.count = clamp(max - num, 0, 2); game.department = 'southern'; game.did_raise = 0; }, } function find_unused_provincial(dept) { for (let p = first_friendly_unit; p <= last_friendly_unit; ++p) if (is_provincial_unit_from(p, dept) && is_piece_unused(p)) return p; return 0; } states.raise_provincial_regiments = { prompt() { let num = count_provincial_units_from(game.department); let can_raise = false; if (!game.did_raise) { if (can_restore_provincial_regiments(game.department)) { can_raise = true; gen_action('restore'); } } if (game.count > 0) { let list = departments[game.department]; for (let i = 0; i < list.length; ++i) { let s = list[i]; if (has_unbesieged_friendly_fortifications(s)) { can_raise = true; gen_action_space(s); } } } if (can_raise) { if (game.did_raise) view.prompt = `Raise Provincial regiments in ${game.department} department.`; else view.prompt = `Raise Provincial regiments in ${game.department} department or restore all to full.`; } else { view.prompt = `Raise Provincial regiments in ${game.department} department \u2014 done.`; gen_action_next(); } }, restore() { // TODO: restore_provincial_regiments state for manual restoring? push_undo(); for (let p = first_friendly_unit; p <= last_friendly_unit; ++p) { if (is_provincial_unit_from(p, game.department)) if (can_restore_unit(p)) restore_unit(p); } game.count = 0; }, 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 is_card_removed(card) { return game.cards.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() { log("Battle destroys French fleet."); game.events.quiberon = 1; delete game.events.diplo; }, } 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) => { console.log("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 = "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 = "Replace a siege 1 or siege 2 marker on the map with siege 0 - done."; gen_action_next(); } }, space(s) { push_undo(); log(`Replaces siege marker in ${space_name(s)}.`); game.sieges[s] = 0; game.count = 0; }, next() { end_action_phase(); }, } function is_colonial_recruit(p) { return is_coureurs_unit(p) || is_rangers_unit(p) || is_light_infantry_unit(p) || is_provincial_unit(p); } events.colonial_recruits = { can_play() { let n = 0; for (let p = first_friendly_unit; p <= last_friendly_unit; ++p) if (is_colonial_recruit(p) && is_piece_unbesieged(p) && is_unit_reduced(p)) ++n; return n > 0; }, play() { clear_undo(); // rolling die let roll = roll_die(); game.state = 'colonial_recruits'; game.count = roll; }, } states.colonial_recruits = { prompt() { let can_restore = false; 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)) { can_restore = true; gen_action_piece(p); } } } } if (can_restore) { view.prompt = `Restore ${game.count} reduced colonial recruits.`; } else { view.prompt = `Restore colonial recruits \u2014 done.`; gen_action_next(); } }, piece(p) { push_undo(); restore_unit(p); game.count --; }, next() { end_action_phase(); }, } function has_unbesieged_reduced_regular_or_light_infantry_units() { for (let p = first_friendly_unit; p <= last_friendly_unit; ++p) if (is_regulars_unit(p) || is_light_infantry_unit(p)) if (is_piece_unbesieged(p) && is_unit_reduced(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 has_unbesieged_reduced_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 has_unbesieged_reduced_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 can_restore = false; if (game.count > 0) { for (let p = first_friendly_unit; p <= last_friendly_unit; ++p) { if (is_regulars_unit(p) || is_light_infantry_unit(p)) { if (can_restore_unit(p)) { can_restore = true; gen_action_piece(p); } } } } if (can_restore) { view.prompt = `Restore ${game.count} reduced regular or light infantry.`; } else { view.prompt = `Restore reduced regular or light infantry \u2014 done.`; gen_action_next(); } }, piece(p) { push_undo(); restore_unit(p); game.count --; }, next() { end_action_phase(); }, } function find_unused_friendly_militia() { for (let p = first_friendly_unit; p <= last_friendly_unit; ++p) if (is_militia_unit(p) && is_piece_unused(p)) return p; return 0; } events.call_out_militias = { play() { game.state = 'call_out_militias'; game.count = 2; } } states.call_out_militias = { prompt() { view.prompt = `Place a Militia unit into a militia box, or restore 2 to full strength.`; let can_place = false; if (game.count === 2) { if (game.active === BRITAIN) { if (find_unused_friendly_militia()) { can_place = true; gen_action_space(SOUTHERN_COLONIAL_MILITIAS); gen_action_space(NORTHERN_COLONIAL_MILITIAS); } } else { if (find_unused_friendly_militia()) { can_place = true; gen_action_space(ST_LAWRENCE_CANADIAN_MILITIAS); } } } if (game.count > 0) { for (let p = first_friendly_unit; p <= last_friendly_unit; ++p) { if (is_militia_unit(p) && is_unit_reduced(p) && is_militia_box(piece_space(p))) { can_place = true; gen_action_piece(p); } } } if (game.count === 0 || !can_place) gen_action_next(); }, 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(); }, } function find_unused_ranger_unit() { for (let p = first_friendly_unit; p <= last_friendly_unit; ++p) if (is_rangers_unit(p) && is_piece_unused(p)) return p; return 0; } events.rangers = { play() { game.state = 'rangers'; game.count = 2; } } states.rangers = { prompt() { view.prompt = `Place a Rangers unit at a fortification, or restore 2 to full strength.`; let can_place = false; if (game.count === 2) { if (find_unused_ranger_unit()) { for (let s = first_space; s <= last_space; ++s) { if (has_unbesieged_friendly_fortifications(s)) { can_place = true; gen_action_space(s); } } } } if (game.count > 0) { for (let p = first_friendly_unit; p <= last_friendly_unit; ++p) { if (is_rangers_unit(p)) { if (can_restore_unit(p)) { can_place = true; gen_action_piece(p); } } } } if (game.count === 0 || !can_place) gen_action_next(); }, space(s) { push_undo(); let p = find_unused_ranger_unit(); place_piece(p, s); game.count -= 2; }, piece(p) { push_undo(); restore_unit(p); game.count -= 1; }, next() { end_action_phase(); }, } function find_unused_french_regular_unit() { for (let p = first_french_unit; p <= last_french_unit; ++p) if (is_regulars_unit(p) && is_piece_unused(p)) return p; return 0; } events.french_regulars = { can_play() { if (game.events.french_regulars) return false; if (game.events.quiberon) return false; if (!has_british_units(QUEBEC)) return true; if (!has_british_units(LOUISBOURG)) return true; return false; }, play() { game.state = 'french_regulars'; game.leader = []; if (is_piece_unused(MONTCALM)) { game.leader.push(MONTCALM); move_piece_to(MONTCALM, leader_box(MONTCALM)); } if (is_piece_unused(LEVIS)) { game.leader.push(LEVIS); move_piece_to(LEVIS, leader_box(LEVIS)); } if (is_piece_unused(BOUGAINVILLE)) { game.leader.push(BOUGAINVILLE); move_piece_to(BOUGAINVILLE, leader_box(BOUGAINVILLE)); } game.count = 2; } } states.french_regulars = { prompt() { if (game.leader.length > 0) { let p = game.leader[0]; view.prompt = `Place ${piece_name(p)} at either Québec or Louisbourg.`; view.who = p; } else { view.prompt = `Place ${game.count} Regulars at either Québec or Louisbourg.`; } if (game.count > 0) { if (!has_british_units(QUEBEC)) gen_action_space(QUEBEC); if (!has_british_units(LOUISBOURG)) gen_action_space(LOUISBOURG); } 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_french_regular_unit(); if (p) { place_piece(p, s); game.count --; } else { game.count = 0; } } }, next() { game.events.french_regulars = 1; delete game.leader; end_action_phase(); }, } function find_unused_light_infantry_unit() { for (let p = first_british_unit; p <= last_british_unit; ++p) if (is_light_infantry_unit(p) && is_piece_unused(p)) return p; return 0; } events.light_infantry = { play() { clear_undo(); // drawing leader from pool game.state = 'light_infantry'; game.count = 1; game.leader = draw_leader_from_pool(); } } states.light_infantry = { prompt() { if (game.leader) { view.prompt = `Place ${piece_name(game.leader)} at any fortress.`; view.who = game.leader; } else { view.prompt = `Place ${game.count} Light Infantry at any fortresses.`; } 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_unit(); if (p) { place_piece(p, s); game.count --; } else { log("No more Light Infantry units available."); game.count = 0; } } }, next() { delete game.leader; end_action_phase(); }, } function find_unused_3_4_regular_unit() { for (let p = first_british_unit; p <= last_british_unit; ++p) if (is_3_4_regular_unit(p) && is_piece_unused(p)) return p; return 0; } events.british_regulars = { can_play() { // TODO: check available ports if (game.events.british_regulars) return false; return true; }, play() { clear_undo(); // drawing leader from pool game.state = 'british_regulars'; game.count = 3; game.leader = draw_leader_from_pool(); } } states.british_regulars = { prompt() { if (game.leader) { view.prompt = `Place ${piece_name(game.leader)} at any port.`; view.who = game.leader; } else { view.prompt = `Place ${game.count} Regulars at any ports.`; } if (game.count > 0) { for_each_british_controlled_port(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_3_4_regular_unit(); if (p) { place_piece(p, s); game.count --; } else { game.count = 0; } } }, next() { game.events.british_regulars = 1; delete game.leader; end_action_phase(); }, } function find_unused_highland_unit() { for (let p = first_british_unit; p <= last_british_unit; ++p) if (is_highland_unit(p) && is_piece_unused(p)) return p; return 0; } events.highlanders = { can_play() { // TODO: check available ports if (game.events.pitt || game.year > 1758) return true; return false; }, play(card) { clear_undo(); // drawing leader from pool 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 = `Place ${piece_name(p)} at any port.`; view.who = p; } else { view.prompt = `Place ${game.count} Highlanders at any ports.`; } if (game.count > 0) { for_each_british_controlled_port(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_unit(); if (p) { place_piece(p, s); game.count --; } else { game.count = 0; } } }, next() { delete game.leader; end_action_phase(); }, } function find_unused_royal_american_unit() { for (let p = first_british_unit; p <= last_british_unit; ++p) if (is_royal_american_unit(p) && is_piece_unused(p)) return p; return 0; } events.royal_americans = { can_play() { // TODO: check available fortresses in northern or southern depts return true; }, play() { clear_undo(); // drawing leader from pool 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 = `Place ${piece_name(p)} at any fortress in the departments.`; view.who = p; } else { view.prompt = `Place ${game.count} Royal American units at any fortress in the departments.`; } if (game.count > 0) { // OPTIMIZE: use a list of fortresses in the departments departments.northern.forEach(s => { if (has_unbesieged_friendly_fortress(s)) gen_action_space(s); }); departments.southern.forEach(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_unit(); if (p) { place_piece(p, s); game.count --; } else { game.count = 0; } } }, next() { delete game.leader; end_action_phase(); }, } function find_unused_coureurs_unit() { for (let p = first_french_unit; p <= last_french_unit; ++p) if (is_coureurs_unit(p) && is_piece_unused(p)) return p; return 0; } events.acadians_expelled = { play() { // TODO: acadians_expelled_halifax state for manual placing? for (let i = 0; i < 2; ++i) { let p = find_unused_3_4_regular_unit(); place_piece(p, HALIFAX); } // TODO: restore_acadians_expelled state for manual restoring? for (let p = first_french_unit; p <= last_french_unit; ++p) { if (is_militia_unit(p) || is_coureurs_unit(p)) if (can_restore_unit(p)) restore_unit(p); } set_active(enemy()); game.state = 'acadians_expelled'; game.count = 1; }, } states.acadians_expelled = { prompt() { view.prompt = "Place a Coureurs unit at Québec or Louisbourg."; if (game.count > 0) { if (!has_british_units(QUEBEC)) gen_action_space(QUEBEC); if (!has_british_units(LOUISBOURG)) gen_action_space(LOUISBOURG); } else { gen_action_next(); } }, space(s) { push_undo(); let p = find_unused_coureurs_unit(); if (p) place_piece(p, s); game.count = 0; }, next() { set_active(enemy()); 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; } } states.william_pitt = { prompt() { if (game.count > 0) { view.prompt = "Draw Highlanders, British Regulars, Light Infantry or Troop Transports from Discard."; view.hand = game.cards.discarded; for (let c of game.cards.discarded) { if (william_pitt_cards.includes(cards[c].event)) gen_action('card', c); } } else { view.prompt = "Draw Highlanders, British Regulars, Light Infantry or Troop Transports from Discard - done."; } gen_action_next(); }, card(c) { push_undo(); log(`Draws ${card_name(c)} from discard.`); remove_from_array(game.cards.discarded, 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 = "Draw French Regulars or Troop Transports from Discard."; view.hand = view.cards.discard; for (let c of view.cards.discarded) { if (diplomatic_revolution_cards.includes(cards[c].event)) gen_action('card', c); } } else { view.prompt = "Draw French Regulars or Troop Transports from Discard - done."; } gen_action_next(); }, card(c) { push_undo(); log(`Draws ${card_name(c)} from discard.`); remove_from_array(game.cards.discarded, c); player.hand.push(c); game.count = 0; }, next() { end_action_phase(); } } events.intrigues_against_shirley = { can_play() { return game.tracks.vp >= 1 && is_piece_on_map(SHIRLEY) && is_piece_unbesieged(SHIRLEY); }, play() { game.state = 'intrigues_against_shirley'; } } states.intrigues_against_shirley = { prompt() { view.prompt = "Eliminate Shirley."; gen_action_piece(SHIRLEY); }, piece(p) { isolate_piece_from_force(SHIRLEY); eliminate_piece(SHIRLEY); end_action_phase(); }, } // SETUP exports.scenarios = [ "Annus Mirabilis", "Early War Campaign", "Late War Campaign", "The Full Campaign", ]; exports.roles = [ FRANCE, BRITAIN, ]; exports.ready = function (scenario, options, players) { return players.length === 2; } function setup_markers(m, list) { list.forEach(name => m.push(find_space(name))); } function setup_leader(where, who) { game.pieces.location[find_leader(who)] = find_space(where); } function setup_unit(where, who) { game.pieces.location[find_unused_unit(who)] = find_space(where); } function setup_1757(end_year) { game.tracks.year = 1757; game.tracks.end_year = end_year; game.tracks.season = EARLY; game.tracks.vp = 4; game.tracks.pa = SUPPORTIVE; // TODO: optional rule start at 2VP for balance // see https://boardgamegeek.com/thread/1366550/article/19163465#19163465 for (let i = 1; i <= 62; ++i) game.cards.draw_pile.push(i); for (let i = 63; i <= 70; ++i) game.cards.removed.push(i); setup_markers(game.France.allied, [ "Mingo Town", "Logstown", "Pays d'en Haut", "Mississauga", ]); setup_markers(game.France.forts, [ "Ticonderoga", "Crown Point", "Niagara", "Ohio Forks", ]); setup_markers(game.France.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("dead", "Dieskau"); setup_leader("dead", "Beaujeu"); setup_markers(game.Britain.forts, [ "Hudson Carry South", "Hudson Carry North", "Will's Creek", "Shamokin", ]); setup_markers(game.Britain.forts_uc, [ "Winchester", "Shepherd's Ferry", ]); setup_markers(game.Britain.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.pieces.pool.push(find_leader("Amherst")); game.pieces.pool.push(find_leader("Bradstreet")); game.pieces.pool.push(find_leader("Forbes")); game.pieces.pool.push(find_leader("Murray")); game.pieces.pool.push(find_leader("Wolfe")); setup_leader("dead", "Braddock"); setup_leader("dead", "Shirley"); game.events.pitt = 1; game.events.diplo = 1; start_year(); return game; } function setup_1755() { game.tracks.year = 1755; game.tracks.season = EARLY; game.tracks.vp = 0; game.tracks.pa = SUPPORTIVE; for (let i = 1; i <= 70; ++i) game.cards.draw_pile.push(i); setup_markers(game.France.allied, [ "Pays d'en Haut", "Kahnawake", "St-François", ]); setup_markers(game.Britain.allied, [ "Canajoharie", ]); setup_markers(game.France.forts, [ "Crown Point", "Niagara", "Ohio Forks", ]); setup_markers(game.France.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.Britain.forts, [ "Hudson Carry South", "Will's Creek", "Oswego", ]); setup_markers(game.Britain.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.pieces.pool.push(find_leader("Abercromby")); game.pieces.pool.push(find_leader("Bradstreet")); game.pieces.pool.push(find_leader("Loudoun")); game.pieces.pool.push(find_leader("Murray")); game.pieces.pool.push(find_leader("Webb")); setup_leader("unused", "Amherst"); setup_leader("unused", "Forbes"); setup_leader("unused", "Wolfe"); start_year(); return game; } exports.setup = function (seed, scenario, options) { load_game_state({ seed: seed, state: null, active: FRANCE, tracks: { year: 1755, end_year: 1762, season: 0, vp: 0, pa: 0, }, events: {}, cards: { current: 0, draw_pile: [], discarded: [], removed: [], }, pieces: { location: pieces.map(x => 0), reduced: [], inside: [], pool: [], }, sieges: {}, fieldworks: [], niagara: 1, ohio_forks: 1, France: { hand: [], held: 0, did_construct: 0, allied: [], stockades: [], forts_uc: [], forts: [], fortresses: originally_french_fortresses.slice(), raids: [], }, Britain: { hand: [], held: 0, did_construct: 0, allied: [], stockades: [], forts_uc: [], forts: [], fortresses: originally_british_fortresses.slice(), raids: [], amphib: [], }, undo: [], log: [], }); switch (scenario) { default: case "Annus Mirabilis": return setup_1757(1759); case "Early War Campaign": return setup_1755(1759); case "Late War Campaign": return setup_1757(1762); case "The Full Campaign": return setup_1755(1762); } } // ACTION HANDLERS function clear_undo() { game.undo = []; } function push_undo() { game.undo.push(JSON.stringify(game, (k,v) => { if (k === 'undo') return undefined; if (k === 'log') return v.length; return v; })); } function pop_undo() { let undo = game.undo; let save_log = game.log; Object.assign(game, JSON.parse(undo.pop())); game.undo = undo; save_log.length = game.log; game.log = save_log; } function 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_x(action, enabled) { if (!view.actions) view.actions = {} view.actions[action] = enabled ? 1 : 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_pass() { gen_action('pass'); } function gen_action_next() { gen_action('next'); } function gen_action_space(space) { gen_action('space', space); } function gen_action_piece(piece) { gen_action('piece', piece); } function gen_action_discard(card) { gen_action('card', card); } function load_game_state(state) { game = state; update_active_aliases(); } exports.resign = function (state, current) { load_game_state(state); if (game.state !== 'game_over') { log(""); log(current + " resigned."); game.active = current; game.state = 'game_over'; game.victory = current + " resigned."; game.result = enemy(current); game.active = 'None'; } return state; } exports.action = function (state, current, action, arg) { load_game_state(state); let S = states[game.state]; if (action in S) { S[action](arg); } else { if (action === 'undo' && game.undo && game.undo.length > 0) pop_undo(); else throw new Error("Invalid action: " + action); } return state; } exports.view = function(state, current) { load_game_state(state); view = { tracks: game.tracks, events: game.events, pieces: game.pieces, sieges: game.sieges, fieldworks: game.fieldworks, markers: { France: { allied: game.France.allied, stockades: game.France.stockades, forts_uc: game.France.forts_uc, forts: game.France.forts, raids: game.France.raids, }, Britain: { allied: game.Britain.allied, stockades: game.Britain.stockades, forts_uc: game.Britain.forts_uc, forts: game.Britain.forts, raids: game.Britain.raids, amphib: game.Britain.amphib, }, }, cards: { current: game.cards.current, draw_pile: game.cards.draw_pile.length, discarded: game.cards.discarded, removed: game.cards.removed, }, active: game.active, prompt: null, actions: null, log: game.log, }; if (game.activation) view.activation = game.activation; if (game.move) view.move = game.move; if (game.force) view.force = game.force; if (game.Britain.held) view.british_held = 1; if (game.France.held) view.french_held = 1; if (current === FRANCE) view.hand = game.France.hand; else if (current === BRITAIN) view.hand = game.Britain.hand; else view.hand = []; if (!states[game.state]) { view.prompt = "Invalid game state: " + game.state; return view; } if (current === 'Observer' || game.active !== current) { if (states[game.state].inactive) states[game.state].inactive(); else view.prompt = `Waiting for ${game.active} \u2014 ${game.state.replace(/_/g, " ")}...`; } else { states[game.state].prompt(); gen_action_undo(); view.prompt = game.active + ": " + view.prompt; } return view; }