summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorTor Andersson <tor@ccxvii.net>2021-07-06 18:01:07 +0200
committerTor Andersson <tor@ccxvii.net>2023-02-18 11:54:52 +0100
commit836d13c6e0eb96ab10d810f4016eda6fc06efdd2 (patch)
treecee8d5996327b2da1a27ecafc70d7f11f1df1a6c
parentb96672ea3a3de9a3ad8436e333ac074f73a02e64 (diff)
downloadwilderness-war-836d13c6e0eb96ab10d810f4016eda6fc06efdd2.tar.gz
Start Wilderness War rules.
-rw-r--r--rules.js4503
1 files changed, 4503 insertions, 0 deletions
diff --git a/rules.js b/rules.js
new file mode 100644
index 0000000..805e075
--- /dev/null
+++ b/rules.js
@@ -0,0 +1,4503 @@
+"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 (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 && has_enemy_stockade(where)) {
+ 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;
+}