"use strict"; // INTERRUPT CARDS: // Card 13 - Blockhouses (before enemy rolls on raid table) // Card 14 - Foul Weather (when enemy is about to move) // Card 15 - Lake Schooner (when enemy moves into friendly fortification along lake etc) // TODO: show british leader pool // TODO: show discard/removed card list in UI // TODO: re-evaluate fortress ownership and VP when pieces move or are eliminated // TODO: rename node/space -> location/space or raw_space/space or box/space? // TODO: replace piece[p].type lookups with index range checks const { spaces, pieces, cards } = require('./data'); const BRITAIN = 'Britain'; const FRANCE = 'France'; // 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; } } // 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 DEBUG() { console.log("WW("+game.state+","+game.active+"):\n\t" + JSON.stringify(game, (k,v) => (k!=='undo'&&k!=='log')?v:undefined)); } function random(n) { return ((game.seed = game.seed * 69621 % 0x7fffffff) / 0x7fffffff) * n | 0; } function roll_d6() { return random(6) + 1; } 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_active(new_active) { console.log("ACTIVE =", game.state, 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; } function set_enemy_active(new_state) { game.state = new_state; set_active(enemy()); } // LISTS const EARLY = 0; const LATE = 1; const RELUCTANT = 0; const SUPPORTIVE = 1; const ENTHUSIASTIC = 2; function find_space(name) { 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 exists_unused_unit(name) { for (let i = 0; i < pieces.length; ++i) if (pieces[i].name === name && game.pieces.location[i] === 0) return true; return 0; } 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 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 = { northern_indians: [ "Kahnawake", "Lac des Deux Montagnes", "Mississauga", "St-François", ].map(name => spaces.findIndex(space => space.name === name)), western_indians: [ "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 HALIFAX = find_space('Halifax'); const LOUISBOURG = find_space('Louisbourg'); 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'); // 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 = game.France.hand_size - game.France.hand.length; let bn = game.Britain.hand_size - game.Britain.hand.length; log("Dealt " + fn + " cards to France."); log("Dealt " + bn + " cards to Britain."); while (fn > 0 && bn > 0) { if (fn > 0) { game.France.hand.push(deal_card()); --fn; } if (bn > 0) { game.Britain.hand.push(deal_card()); --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); return p; } return 0; } // ITERATORS 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_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_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].name === tribe; } function indian_home_settlement(p) { return pieces[p].settlement || 0; } function is_regulars_unit(p) { return pieces[p].type === 'regulars'; } 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_piece_activated(p) { return game.pieces.activated.includes(p); } 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? return piece_node(p) > 0; } function is_unit_unused(p) { // TODO: permanently eliminated return piece_node(p) === 0; } 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 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_friendly_fortress(space) { return is_fortress(space) && is_friendly_controlled_space(space); } function has_enemy_fortress(space) { return is_fortress(space) && is_enemy_controlled_space(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_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 is_friendly_controlled_space(space) { if (is_space_unbesieged(space) && !has_enemy_units(space)) { if (is_originally_enemy(space)) { if (has_friendly_units(space)) return true; if (has_friendly_amphib(space)) return true; } else { // TODO: neutral spaces? // return !is_originally_friendly(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)) return true; if (has_enemy_amphib(space)) return true; } else { // TODO: neutral spaces? // return !is_originally_enemy(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_rangers(space) { if (game.active === BRITAIN) for (let p = first_british_piece; p <= last_british_piece; ++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_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_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) === 0; } // 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)) 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 award_vp(n) { if (game.active === FRANCE) { log(`France gains ${n} VP.`); game.tracks.vp += n; } else { log(`Britain gains ${n} VP.`); 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_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 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); if (is_regulars_unit(p) || is_coureurs_unit(p)) game.pieces.location[p] = 0; // TODO: permanently eliminated else game.pieces.location[p] = 0; } function eliminate_indian_tribe(tribe) { for (let p = first_enemy_unit; p <= last_enemy_unit; ++p) if (is_indian_tribe(p, tribe) && is_piece_unbesieged(p)) eliminate_piece(p); } function move_piece_to(who, to) { game.pieces.location[who] = to; } 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(space) { log("captures fortress"); award_vp(3); } 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); } // 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); 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 => { // TODO: unbesieged 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 => { // TODO: unbesieged gen_action_piece(p); }); } else if (has_br_indians) { if (is_piece_in_space(JOHNSON, from)) { // TODO: unbesieged gen_action_piece(JOHNSON); } } } else { for_each_friendly_piece_in_space(from, p => { // TODO: unbesieged 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) { max_cost *= 2; // use odd numbers for paths with one land connection 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 } // 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); 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)) 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 } // 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")); setup_leader("Amherst", "Amherst"); setup_leader("Forbes", "Forbes"); setup_leader("Wolfe", "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_bay) set_active(BRITAIN); else set_active(FRANCE); deal_cards(); start_action_phase(); } function start_action_phase() { game.state = 'action_phase'; log(""); log(`.h2 ${game.active}`); log(""); } function end_action_phase() { console.log("END ACTION PHASE"); clear_undo(); game.pieces.activated.length = 0; game.count = 0; // TODO: skip if next player has no cards or passed // TODO: end season set_active(enemy()); start_action_phase(); } 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(); 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(`${game.active} plays ${card_name(card)}.`); remove_from_array(player.hand, card); game.cards.current = card; if (cards[card].special === 'remove') game.cards.removed.push(card); else game.cards.discarded.push(card); } function discard_card(card, reason) { log(`${game.active} discards ${card_name(card)}${reason}.`); remove_from_array(player.hand, card); game.cards.current = card; if (cards[card].special === 'remove') game.cards.removed.push(card); else game.cards.discarded.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(); }, play_event(card) { push_undo(); player.did_construct = 0; play_card(card); events[cards[card].event].play(); }, 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() { // TODO: can you build fortifications, pass, then build next turn? log(game.active + " pass."); game.passed = game.active; end_action_phase(); }, } // ACTIVATION function goto_activate_individually(card) { push_undo(); player.did_construct = 0; discard_card(card, " to activate auxiliaries and leaders"); game.count = cards[card].activation; game.state = 'activate_individually'; } 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; } 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.pieces.activated.length === 0) gen_action_piece(p); } } } } }, piece(piece) { push_undo(); log(`Activate ${piece_name(piece)}.`); isolate_piece_from_force(piece); game.pieces.activated.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() { goto_pick_move(); }, } states.activate_force = { prompt() { gen_action_pass(); if (game.count > 0) { 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) gen_action_piece(p); } } else { view.prompt = "Activate a Force \u2014 done."; } }, piece(leader) { push_undo(); log(`Activate force led by ${piece_name(leader)}.`); game.pieces.activated.push(leader); game.count = 0; goto_pick_force(); }, pass() { end_action_phase(); }, } function end_activation() { clear_undo(); // TODO: goto_pick_force for campaign event goto_pick_move(); } function goto_pick_force() { if (game.pieces.activated.length === 0) { end_action_phase(); } else if (game.pieces.activated.length === 1) { let leader = game.pieces.activated.pop(); game.force = { leader: leader, selected: leader, reason: 'move', }; game.state = 'define_force'; } else { // TODO: for campaign event game.state = 'pick_force'; } } function goto_pick_move() { if (game.pieces.activated.length === 0) end_action_phase(); else if (game.pieces.activated.length === 1) goto_move_piece(game.pieces.activated[0]) else { game.state = 'pick_move'; } } states.pick_move = { prompt() { view.prompt = "Select an activated force, leader, or unit to move." gen_action_next(); game.pieces.activated.forEach(gen_action_piece); }, piece(piece) { push_undo(); goto_move_piece(piece); }, next() { end_action_phase(); }, } // DEFINE FORCE (for various actions) states.define_force = { prompt() { DEBUG(); 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(); // Short-cut to Siege/Assault if activated force is highest commanding leader in space. if (game.force.reason === 'move' && has_besieged_enemy_fortifications(space)) { let commanding = find_friendly_commanding_leader_in_space(space); if (main_leader === commanding && has_friendly_supplied_drilled_troops(space)) { // TODO: gen_action_space(space); if (is_assault_possible(space)) gen_action('assault'); else gen_action('siege'); } } // select any leader in the map space for_each_friendly_leader_in_space(space, p => { if (p !== selected && !is_piece_inside(p)) { 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 && !is_piece_inside(p)) { 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 && !is_piece_inside(p)) gen_action_piece(p); }); // pick up units for_each_friendly_unit_in_node(space, p => { if (!is_piece_inside(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 { if (cap > 0) gen_action_piece(p); } } }); // drop off units for_each_friendly_unit_in_node(leader_box(selected), p => { if (!is_piece_inside(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); }, siege() { push_undo(); let where = piece_space(game.force.leader); delete game.force; goto_resolve_siege(where); }, assault() { push_undo(); let where = piece_space(game.force.leader); delete game.force; goto_assault(where); }, next() { push_undo(); let main_leader = game.force.leader; let reason = game.force.reason; delete game.force; 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); } }, } // MOVE function goto_move_piece(who) { log(`Move ${piece_name(who)}.`); remove_from_array(game.pieces.activated, who); let from = piece_space(who); console.log("GOTO_MOVE_PIECE", who); game.state = 'move'; game.move = { moving: who, intercepting: null, intercepted: [], did_attempt_intercept: 0, avoiding: null, avoided: [], start_space: from, start_cost: 0, 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) } resume_move(); } function may_naval_move(who) { if (is_leader(who) && count_pieces_in_force(who) > 0) return cards[game.cards.current].activation === 3; return true; } function resume_move() { if (game.move.type === null) { game.move.cost = {}; game.move.path = {}; throw Error("WHAT IS THIS"); } let who = moving_piece(); const is_lone_ax = is_lone_auxiliary(who); const is_lone_ld = is_lone_leader(who); console.log("RESUME_MOVE_UNIT is_lone_ax=" + is_lone_ax + " is_lone_ld=" + is_lone_ld); 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, 9); break; case 'land': search_land_move(who, piece_space(who), game.move.start_cost, movement_allowance(who)); break; case 'naval': if (may_naval_move(who)) search_naval_move(who, piece_space(who), game.move.start_cost); break; } } 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 (!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'); } } gen_action_next(); 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); }); } }, 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]; // remember where we came from so we can retreat after battle game.raid.from[to] = game.move.path[to]; // TODO: except space moved into, if it is guarded! if (force_has_drilled_troops(who)) remove_enemy_forts_uc_in_path(game.move.path, to); move_piece_to(who, to); 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(); }, next() { // TODO end_move(); }, } function remove_siege_marker(where) { delete game.sieges[where]; } function place_siege_marker(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 goto_battle(where, false, false); } else { end_move_step(false); } } function end_move_step(final) { 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)) { 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) } } end_move(); } else if (final) { end_move(); } else { 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()) game.state = 'define_force_lone_ax'; // TODO 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_d6(); 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 has_unbesieged_enemy_units_that_did_not_intercept(where) { // TODO return has_unbesieged_enemy_units(where); } 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(piece) { return game.move.intercepted.includes(piece); } function did_piece_avoid_battle(piece) { return game.move.avoided.includes(piece); } 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_d6(); 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) { let where = game.battle.where; if (game.battle.breaking_siege) { 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 { if (game.battle.attacker === 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 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 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 ]], [ 28, [ 3, 4, 5, 5, 6, 6, 7, 8 ]], ] function combat_result(str, die) { 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]) return COMBAT_RESULT_TABLE[i][1][die]; 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_events(); } 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_events(); game.state = 'militia_in_battle'; } else { goto_battle_events(); } } 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_events(); }, } function goto_battle_events() { set_active(game.battle.attacker); // TODO: attacker then defender play events // TODO: attacker MAY participate besieged units if breaking the siege goto_battle_roll(); } function goto_battle_roll() { // TODO: modifiers let atk_str = attacker_combat_strength(); let atk_mod = 0; game.battle.atk_roll = roll_d6(); game.battle.atk_result = combat_result(atk_str, game.battle.atk_roll + atk_mod); log("ATTACKER", "str="+atk_str, "roll="+game.battle.atk_roll, "+", atk_mod, "=", game.battle.atk_result); // TODO: modifiers let def_str = defender_combat_strength(); let def_mod = 0; game.battle.def_roll = roll_d6(); game.battle.def_result = combat_result(def_str, game.battle.def_roll + def_mod); log("DEFENDER", "str="+def_str, "roll="+game.battle.def_roll, "+", def_mod, "=", game.battle.def_result); // Next state sequence: // atk step losses // atk leader checks // def step losses // def leader checks // determine winner 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.active === game.battle.attacker) goto_def_step_losses(); else goto_determine_winner(); } // 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_roll === 1 || game.battle.def_roll === 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_roll === 1 || game.battle.atk_roll === 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]); if (game.battle.leader_check.length === 0) gen_action_next(); }, piece(piece) { let die = roll_d6(); 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); }, next() { 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 = 'leader_check'; } else { game.raid.leader_check = 0; 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]); if (game.raid.leader_check.length === 0) gen_action_next(); }, piece(piece) { let die = roll_d6(); 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); }, next() { 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; // 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); // Raid battle vs militia if (game.raid && game.raid.where > 0) { if (victor === game.battle.attacker) { log("ATTACKER WON RAID BATTLE VS MILITIA"); resolve_raid(); } else { log("DEFENDER WON RAID BATTLE VS MILITIA"); retreat_attacker(game.raid.where, game.raid.from[game.raid.where] | 0); goto_pick_raid(); } 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) { console.log("POSSIBLE 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); // 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; end_retreat(); } } function eliminate_enemy_pieces_inside(where) { for (let p = first_enemy_piece; p <= last_enemy_piece; ++p) if (is_piece_in_space(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); if (has_enemy_fortress(where)) { capture_enemy_fortress(where); } if (has_enemy_fort(where)) { capture_enemy_fort(where); } } else { log("DEFENDER WON ASSAULT"); } 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); // TODO: manual user to click on retreat location 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); } }); // TODO: lift siege if attack on enemy fort repulsed } 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)); if (has_unbesieged_enemy_units(to)) return false; if (has_unbesieged_enemy_fortifications(to)) return false; if (moving_piece_came_from(to) === battle_space()) 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; // TODO: retreat from amphib to british controlled port let can_retreat = false; for_each_exit(from, to => { if (can_defender_retreat_from_to(p, from, to)) can_retreat = true; }); return can_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(); // TODO: eliminate non-retreated units 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; // TODO: retreat from amphib to british controlled port 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); } // SIEGE const SIEGE_TABLE = [ 0, 0, 0, 1, 1, 1, 2, 2 ]; function goto_resolve_siege(space) { // TODO: Coehorns clear_undo(); 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_d6(); let msg = `Roll ${die}`; let drm_att_ld = leader_tactics(att_leader); let drm = drm_att_ld; msg += `\n+${drm_att_ld} besieger's leader`; if (def_leader) { let drm_def_ld = leader_tactics(def_leader); drm += drm_def_ld; msg += `\n-${drm_def_ld} defender's leader`; } if (space == LOUISBOURG) { msg += `\n-1 for Louisbourg`; drm -= 1; } let result = SIEGE_TABLE[clamp(die + drm, 0, 7)]; msg += `\n= ${result}`; log(msg); if (result > 0) { let level = change_siege_marker(space, result); log("Siege level is " + level); } goto_assault_possible(space); } 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() { 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() { clear_undo(); if (game.raid.list.length > 0) { game.state = 'pick_raid'; } else { delete game.raid; 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)); resolve_raid(); } else { set_active(enemy()); game.state = 'militia_against_raid'; game.count = 1; } } else { resolve_raid(); } } 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 resolve_raid(); }, } const RAID_TABLE = { stockade: [ 2, 1, 1, 0, 2, 1, 0, 0 ], cultivated: [ 2, 0, 0, 0, 1, 1, 0, 0 ], }; function resolve_raid() { let where = game.raid.where; let x_stockade = has_enemy_stockade(where); let x_allied = has_enemy_allied_settlement(where); let column = 'cultivated'; if (x_stockade || x_allied || (game.events.blockhouses === game.active)) column = 'stockade'; let d = roll_d6(); let drm = 0; let mods = []; let commander = find_friendly_commanding_leader_in_space(where); if (commander) { console.log(`${piece_name(commander)} leads the raid`); let t = leader_tactics(commander); drm += t; mods.push(` +${t} tactics rating`); } if (has_friendly_rangers(where)) { drm += 1; mods.push(" +1 for rangers"); } if (enemy_department_has_at_least_n_militia(where, 2)) { drm -= 1; mods.push(" -1 for militia in department"); } log(`Raid ${space_name(where)} roll ${d}${mods.join(",")} = ${d+drm} on column vs. ${column}.`); let result = clamp(d + drm, 0, 7); let success = result >= 5; let losses = RAID_TABLE[column][result]; if (success) { log(`Result: 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: Failure with ${losses} losses.`); } game.raid.step_loss = losses; // 10.32: leader check if (d === 1 || (d === 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; 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(); } } // 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 find_unused_friendly_militia() { for (let p = first_friendly_unit; p <= last_friendly_unit; ++p) if (is_militia_unit(p) && is_unit_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(); log(`Places militia in ${space_name(s)}.`); let p = find_unused_friendly_militia(); move_piece_to(p, s); game.count -= 2; }, piece(p) { push_undo(); let s = piece_space(p); log(`Restores militia in ${space_name(s)}.`); set_unit_reduced(p, 0); game.count -= 1; }, next() { end_action_phase(); }, } function find_unused_rangers() { for (let p = first_friendly_unit; p <= last_friendly_unit; ++p) if (is_rangers_unit(p) && is_unit_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_rangers()) { 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) && is_unit_reduced(p) && is_piece_on_map(p)) { if (is_piece_unbesieged(p)) { can_place = true; gen_action_piece(p); } } } } if (game.count === 0 || !can_place) gen_action_next(); }, space(s) { push_undo(); log(`Places rangers in ${space_name(s)}.`); let p = find_unused_rangers(); move_piece_to(p, s); game.count -= 2; }, piece(p) { push_undo(); let s = piece_space(p); log(`Restores rangers in ${space_name(s)}.`); set_unit_reduced(p, 0); game.count -= 1; }, next() { end_action_phase(); }, } function find_unused_light_infantry() { for (let p = first_friendly_unit; p <= last_friendly_unit; ++p) if (is_light_infantry_unit(p) && is_unit_unused(p)) return p; return 0; } function place_two_light_infantry(s) { let p = find_unused_light_infantry(); if (p) move_piece_to(p, s); p = find_unused_light_infantry(); if (p) move_piece_to(p, s); } events.light_infantry = { play() { clear_undo(); // drawing leader from pool reveals information game.state = 'light_infantry'; game.leader = draw_leader_from_pool(); game.count = 1; if (game.leader) { move_piece_to(game.leader, leader_box(game.leader)); place_two_light_infantry(leader_box(game.leader)); } } } states.light_infantry = { prompt() { if (game.leader) { view.prompt = `Place 2 Light Infantry units and ${piece_name(game.leader)} at a fortress.`; view.who = game.leader; } else { view.prompt = `Place 2 Light Infantry units at a fortress.`; } 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) { log(`Places 2 Light Infantry and ${piece_name(game.leader)} in ${space_name(s)}.`); move_piece_to(game.leader, s); } else { log(`Places 2 Light Infantry in ${space_name(s)}.`); place_two_light_infantry(s); } game.count = 0; }, next() { delete game.leader; 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("offmap", "Dieskau"); setup_leader("offmap", "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("offmap", "Braddock"); setup_leader("offmap", "Shirley"); game.events.pitt = 1; game.France.hand_size = 9; game.Britain.hand_size = 9; 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("offmap", "Amherst"); setup_leader("offmap", "Forbes"); setup_leader("offmap", "Wolfe"); game.France.hand_size = 8; game.Britain.hand_size = 8; 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: [], activated: [], pool: [], }, sieges: {}, France: { hand: [], hand_size: 0, held: 0, did_construct: 0, allied: [], stockades: [], forts_uc: [], forts: [], raids: [], }, Britain: { hand: [], hand_size: 0, held: 0, did_construct: 0, allied: [], stockades: [], forts_uc: [], forts: [], 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 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, 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.move) view.move = game.move; if (game.force) view.force = game.force; 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; }