From 150b08878acd945100024d54634bb396bbb3c3cc Mon Sep 17 00:00:00 2001 From: Tor Andersson Date: Sat, 1 Jan 2022 01:27:29 +0100 Subject: Washington's War: DO NOT PUBLISH. --- rules.js | 3509 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 3509 insertions(+) create mode 100644 rules.js (limited to 'rules.js') diff --git a/rules.js b/rules.js new file mode 100644 index 0000000..09f5a11 --- /dev/null +++ b/rules.js @@ -0,0 +1,3509 @@ +"use strict"; + +// TODO: campaign messed up who is who after battle +// TODO: retreat with 0 CU after battle + +exports.scenarios = [ + "Historical" +]; + +const CARDS = require('./cards'); +const DATA = require('./data'); +const SPACES = DATA.SPACES; +const COLONIES = DATA.COLONIES; +const GENERALS = DATA.GENERALS; +const BLOCKADE = DATA.BLOCKADE; +const PATH_INDEX = DATA.PATH_INDEX; +const PATH_NAME = DATA.PATH_NAME; +const PATH_TYPE = DATA.PATH_TYPE; + +const BRITISH = 'British'; +const AMERICAN = 'American'; +const FRENCH = 'French'; + +const BRITISH_GENERALS = [ "Burgoyne", "Carleton", "Clinton", "Cornwallis", "Howe" ]; +const AMERICAN_GENERALS = [ "Arnold", "Gates", "Greene", "Lafayette", "Lee", "Lincoln", "Washington", "Rochambeau" ]; +const WASHINGTON = "Washington"; +const ROCHAMBEAU = "Rochambeau"; +const ARNOLD = "Arnold"; + +const CAPTURED_GENERALS = "Captured Generals"; +const CONTINENTAL_CONGRESS_DISPERSED = "Continental Congress Dispersed"; +const BRITISH_REINFORCEMENTS = "British Leader Reinforcements"; +const AMERICAN_REINFORCEMENTS = "American Leader Reinforcements"; +const FRENCH_REINFORCEMENTS = "French Reinforcements"; +const TURN_TRACK = { + 1775: "Game Turn 1775", + 1776: "Game Turn 1776", + 1777: "Game Turn 1777", + 1778: "Game Turn 1778", + 1779: "Game Turn 1779", + 1780: "Game Turn 1780", + 1781: "Game Turn 1781", + 1782: "Game Turn 1782", + 1783: "Game Turn 1783", +}; + +const FALMOUTH_QUEBEC = "Falmouth/Quebec"; + +const THE_13_COLONIES = [ 'NH', 'NY', 'MA', 'CT', 'RI', 'PA', 'NJ', 'MD', 'DE', 'VA', 'NC', 'SC', 'GA' ]; +const SOUTH_OF_WINTER_ATTRITION_LINE = [ 'NC', 'SC', 'GA' ]; + +const CAMPAIGN_CARDS = [ 67, 68, 69, 70 ]; +const DECLARATION_OF_INDEPENDENCE = 99; +const BARON_VON_STEUBEN = 86; +const WAR_ENDS_1779 = 71; +const BENJAMIN_FRANKLIN = 101; + +const ENEMY = { "American": BRITISH, "British": AMERICAN }; + +const default_options = {}; + +let states = {}; +let events = {}; + +let game; + +function random(n) { + return Math.floor(((game.seed = game.seed * 48271 % 0x7fffffff) / 0x7fffffff) * n); +} + +function log(s) { + game.log.push(s); +} + +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 remove_from_array(array, item) { + let i = array.indexOf(item); + if (i >= 0) + array.splice(i, 1); +} + +function setup_game(seed) { + game = { + seed: seed, + year: 1775, + congress: "Philadelphia", + french_alliance: 0, + french_alliance_triggered: false, + european_war: false, + french_navy: FRENCH_REINFORCEMENTS, + regulars: true, + war_ends: 0, + played_british_reinforcements: 0, + played_american_reinforcements: [], + + pc: {}, + generals: {}, + moved: {}, + cu: [], + control: {}, + + deck: create_deck(), + discard: [], + reshuffle: false, + a_hand: [], + b_hand: [], + a_queue: 0, + b_queue: 0, + + log: [], + undo: [], + + prompt: null, + actions: [], + } + + function spawn_unit(owner, location, pc, cu, name) { + if (pc) + game.pc[location] = owner; + if (name) { + game.generals[name] = { + location: location, + } + } + if (cu > 0) { + game.cu.push({ + owner: owner, + location: location, + count: cu, + moved: 0, + }); + } + } + + function british(place, pc, cu, ld) { spawn_unit(BRITISH, place, pc, cu, ld); } + function american(place, pc, cu, ld) { spawn_unit(AMERICAN, place, pc, cu, ld); } + function french(place, pc, cu, ld) { spawn_unit(FRENCH, place, pc, cu, ld); } + + british("Quebec", true, 2, "Carleton"); + british("Montreal", true); + british("Fort Detroit", true, 1); + british("Boston", true, 5, "Howe"); + british("Norfolk", true); + british("Gilbert Town", true); + british("Wilmington NC", true); + british("Ninety Six", true); + + american("Lexington Concord", true, 5, "Washington"); + american("Newport", false, 2, "Greene"); + american("Charleston", true, 2); + american("Philadelphia", true); + + british(BRITISH_REINFORCEMENTS, false, 0, "Burgoyne"); + british(BRITISH_REINFORCEMENTS, false, 0, "Clinton"); + british(BRITISH_REINFORCEMENTS, false, 0, "Cornwallis"); + + american(AMERICAN_REINFORCEMENTS, false, 0, "Arnold"); + american(AMERICAN_REINFORCEMENTS, false, 0, "Lincoln"); + american(AMERICAN_REINFORCEMENTS, false, 0, "Gates"); + american(AMERICAN_REINFORCEMENTS, false, 0, "Lee"); + american(AMERICAN_REINFORCEMENTS, false, 0, "Lafayette"); + + french(FRENCH_REINFORCEMENTS, false, 5, "Rochambeau"); + + goto_committees_of_correspondence(); +} + +/* GAME STATE */ + +function create_deck() { + let deck = []; + for (let i = 1; i <= 110; ++i) { + // No DoI or Baron von Steuben first year. + if (i == DECLARATION_OF_INDEPENDENCE || i == BARON_VON_STEUBEN) + continue; + deck.push(i); + } + return deck; +} + +function reshuffle_deck() { + game.log.push("The deck is reshuffled."); + game.reshuffle = false; + game.deck = game.deck.concat(game.discard); + game.discard = []; +} + +function roll_d6() { + return random(6) + 1; +} + +function deal_card() { + if (game.deck.length == 0) + reshuffle_deck(); + let i = random(game.deck.length); + let c = game.deck[i]; + game.deck.splice(i, 1); + return c; +} + +function last_discard() { + if (game.discard.length > 0) + return game.discard[game.discard.length-1]; + return null; +} + +function active_hand() { + return (game.active == AMERICAN) ? game.a_hand : game.b_hand; +} + +function play_card(c, reason) { + if (reason) + game.log.push(game.active[0] + " plays [" + c + ": " + CARDS[c].title + "] " + reason); + else + game.log.push(game.active[0] + " plays [" + c + ": " + CARDS[c].title + "]"); + if (CARDS[c].reshuffle == 'if_played') + game.reshuffle = true; + remove_from_array(active_hand(), c); + game.last_played = c; + if (!CARDS[c].once) + game.discard.push(c); + else + game.log.push("Card " + c + " removed from game."); +} + +function discard_card_from_hand(hand, c) { + remove_from_array(hand, c); + game.discard.push(c); + if (CARDS[c].reshuffle == 'if_discarded') + game.reshuffle = true; + game.log.push(game.active[0] + " discards [" + c + ": " + CARDS[c].title + "]"); +} + +function discard_card(c, reason) { + game.last_played = c; + discard_card_from_hand(active_hand(), c); + if (reason) + game.log.push(game.active[0] + " discards [" + c + ": " + CARDS[c].title + "] " + reason); + else + game.log.push(game.active[0] + " discards [" + c + ": " + CARDS[c].title + "]"); +} + +function can_exchange_for_discard(c) { + if (game.did_discard_event) { + if (game.active == BRITISH) + return true; + return CARDS[c].count > 1; + } + return false; +} + +function can_play_event(c) { + let card = CARDS[c]; + switch (card.when) { + case 'before_french_alliance': return !game.french_alliance_triggered; + case 'after_french_alliance': return game.french_alliance_triggered; + case 'european_war_in_effect': return game.european_war; + } + return true; +} + +function can_play_reinforcements() { + if (game.active == BRITISH) { + if (game.played_british_reinforcements == 0) { + let n = count_british_cu(BRITISH_REINFORCEMENTS); + for (let g of BRITISH_GENERALS) + if (game.generals[g].location == BRITISH_REINFORCEMENTS) + ++n; + return n > 0; + } + return false; + } + if (game.active == AMERICAN) + return game.played_american_reinforcements.length < 2; + return false; +} + +function is_port(where) { + return SPACES[where].port; +} + +function is_non_blockaded_port(where) { + if (SPACES[where].port && BLOCKADE[where] != game.french_navy) + return true; + return false; +} + +function is_fortified_port(where) { + return SPACES[where].type == 'fortified-port'; +} + +function is_continental_congress_dispersed() { + return game.congress == CONTINENTAL_CONGRESS_DISPERSED; +} + +function is_winter_quarter_space(where) { + let colony = SPACES[where].colony; + if (colony == 'GA' || colony == 'SC' || colony == 'NC') + return true; // south of winter attrition line + let type = SPACES[where].type; + if (type == 'winter-quarters' || type == 'fortified-port') + return true; + return false; +} + +function allowed_to_place_american_pc() { + if (is_continental_congress_dispersed()) + return false; + if (game.pennsylvania_and_new_jersey_line_mutinies) + return false; + return true; +} + +function is_british_militia(space) { + return game.control[SPACES[space].colony] == BRITISH; +} + +function is_american_militia(space) { + return game.control[SPACES[space].colony] == AMERICAN; +} + +function is_american_winter_offensive() { + if (game.who == WASHINGTON && game.a_hand.length == 0) + return true; + return false; +} + +/* PC */ + +function has_no_pc(space) { + return game.pc[space] != BRITISH && game.pc[space] != AMERICAN; +} + +function has_british_pc(space) { + return game.pc[space] == BRITISH; +} + +function has_american_pc(space) { + return game.pc[space] == AMERICAN; +} + +function has_enemy_pc(space) { + if (game.active == BRITISH) + return has_american_pc(space); + else + return has_british_pc(space); +} + +function has_no_pc(space) { + return game.pc[space] != BRITISH && game.pc[space] != AMERICAN; +} + +function is_adjacent_to_british_pc(a) { + for (let b of SPACES[a].exits) + if (has_british_pc(b)) + return true; + if (SPACES[a].port) { + for (let b in SPACES) { + if (SPACES[b].port) + if (has_british_pc(b)) + return true; + } + } + return false; +} + +function is_adjacent_to_american_pc(a) { + for (let b of SPACES[a].exits) + if (has_american_pc(b)) + return true; + return false; +} + +function place_british_pc(space) { + game.log.push("B places PC in " + space); + if (game.british_pc_space_list) + remove_from_array(game.british_pc_space_list, space); + game.pc[space] = BRITISH; +} + +function place_american_pc(space) { + game.log.push("A places PC in " + space); + game.pc[space] = AMERICAN; +} + +function remove_pc(space) { + if (game.active == BRITISH) + game.log.push("B removes PC in " + space); + else + game.log.push("A removes PC in " + space); + game.pc[space] = undefined; +} + +function flip_pc(space) { + if (game.active == BRITISH) + game.log.push("B flips PC in " + space); + else + game.log.push("A flips PC in " + space); + game.pc[space] = ENEMY[game.pc[space]]; +} + +function update_colony_control() { + for (let c in COLONIES) { + let control = 0; + for (let space of COLONIES[c]) { + if (game.pc[space] == BRITISH) + --control; + else if (game.pc[space] == AMERICAN) + ++control; + } + if (control < 0) + game.control[c] = BRITISH; + else if (control > 0) + game.control[c] = AMERICAN; + else + game.control[c] = undefined; + } +} + +/* CU */ + +function find_cu(owner, space) { + for (let i = 0; i < game.cu.length; ++i) { + let cu = game.cu[i]; + if (cu.location == space && cu.owner == owner) + return cu; + } + return null; +} + +function find_british_cu(space) { + return find_cu(BRITISH, space); +} + +function find_american_cu(space) { + return find_cu(AMERICAN, space); +} + +function find_french_cu(space) { + return find_cu(FRENCH, space); +} + +function has_british_cu(space) { + return find_british_cu(space) != null; +} + +function has_no_british_cu(space) { + return !has_british_cu(space); +} + +function has_american_or_french_cu(space) { + return find_american_cu(space) != null || find_french_cu(space) != null; +} + +function has_american_cu(space) { + return find_american_cu(space) != null; +} + +function has_french_cu(space) { + return find_french_cu(space) != null; +} + +function has_enemy_cu(where) { + if (game.active == BRITISH) + return has_american_or_french_cu(where); + else + return has_british_cu(where); +} + +function count_cu(owner, space) { + let cu = find_cu(owner, space); + return cu ? cu.count : 0; +} + +function count_british_cu(where) { + let cu = find_british_cu(where); + return cu ? cu.count : 0; +} + +function count_american_cu(where) { + let cu = find_american_cu(where); + return cu ? cu.count : 0; +} + +function count_french_cu(where) { + let cu = find_french_cu(where); + return cu ? cu.count : 0; +} + +function count_unmoved_british_cu(where) { + let cu = find_british_cu(where); + return cu ? cu.count - cu.moved : 0; +} + +function count_unmoved_american_cu(where) { + let cu = find_american_cu(where); + return cu ? cu.count - cu.moved : 0; +} + +function count_unmoved_french_cu(where) { + let cu = find_french_cu(where); + return cu ? cu.count - cu.moved : 0; +} + +function mark_moved_cu(owner, space, moved) { + if (moved > 0) + find_cu(owner, space).moved += moved; +} + +function count_american_and_french_cu(where) { + return count_american_cu(where) + count_french_cu(where); +} + +function count_enemy_cu(where) { + if (game.active == BRITISH) + return count_american_and_french_cu(where); + else + return count_british_cu(where); +} + +function spawn_cu(owner, where, count) { + game.cu.push({owner:owner, location:where, count:count, moved:0}); +} + +function remove_cu(owner, where, count) { + if (count == 0) + return; + let cu = find_cu(owner, where); + if (count >= cu.count) { + let i = game.cu.indexOf(cu); + remove_from_array(game.cu, cu); + } else { + cu.count -= count; + } +} + +function place_cu(owner, where, count) { + let cu = find_cu(owner, where); + if (!cu) + spawn_cu(owner, where, count); + else + cu.count += count; +} + +function place_british_cu(where, count) { + place_cu(BRITISH, where, count); +} + +function place_american_cu(where, count) { + place_cu(AMERICAN, where, count); +} + +function place_french_cu(where, count) { + place_cu(FRENCH, where, count); +} + +function move_cu(owner, from, to, count) { + if (count == 0) + return; + let from_cu = find_cu(owner, from); + if (count < from_cu.count) { + from_cu.count -= count; + place_cu(owner, to, count); + } else { + let to_cu = find_cu(owner, to); + if (to_cu) { + remove_cu(owner, from, from_cu.count); + to_cu.count += count; + } else { + from_cu.location = to; + } + } +} + +function move_british_cu(from, to, count) { + move_cu(BRITISH, from, to, count); +} + +/* GENERALS */ + +function is_general_on_map(g) { + switch (game.generals[g].location) { + case null: /* killed */ + case CAPTURED_GENERALS: + case BRITISH_REINFORCEMENTS: + case AMERICAN_REINFORCEMENTS: + case FRENCH_REINFORCEMENTS: + return false; + } + return true; +} + +function find_british_general(where) { + for (let general of BRITISH_GENERALS) + if (game.generals[general].location == where) + return general; + return null; +} + +function find_american_or_french_general(where) { + for (let general of AMERICAN_GENERALS) + if (game.generals[general].location == where) + return general; + return null; +} + +function has_british_general(where) { + return find_british_general(where) != null; +} + +function has_american_or_french_general(where) { + return find_american_or_french_general(where) != null; +} + +function has_enemy_general(where) { + if (game.active == BRITISH) + return has_american_or_french_general(where); + else + return has_british_general(where); +} + +function count_friendly_generals(where) { + let list; + if (game.active == BRITISH) + list = BRITISH_GENERALS; + else + list = AMERICAN_GENERALS; + let count = 0; + for (let g of list) + if (location_of_general(g) == where) + ++count; + return count; +} + +function can_activate_general(c) { + if (game.active == BRITISH) + return can_activate_british_general(c); + else + return can_activate_american_general(c); +} + +function can_activate_british_general(c) { + let ops = CARDS[c].count + game.b_queue; + for (let g of BRITISH_GENERALS) + if (is_general_on_map(g) && GENERALS[g].strategy <= ops) + return true; + return false; +} + +function can_activate_american_general(c) { + let ops = CARDS[c].count + game.a_queue; + for (let g of AMERICAN_GENERALS) + if (is_general_on_map(g) && GENERALS[g].strategy <= ops) + return true; + return false; +} + +function move_general(who, where) { + game.generals[who].location = where; +} + +function capture_washington() { + game.generals[WASHINGTON].location = null; + + if (!game.french_alliance_triggered) { + game.french_alliance -= 3; + if (game.french_alliance < 0) + game.french_alliance = 0; + } + + goto_george_washington_captured(); +} + +function capture_british_general(where) { + let g = find_british_general(where); + game.log.push(g + " is captured!"); + move_general(g, CAPTURED_GENERALS); +} + +function capture_american_or_french_general(where) { + let g = find_american_or_french_general(where); + game.log.push(g + " is captured!"); + if (g == WASHINGTON) + capture_washington(); + else + move_general(g, CAPTURED_GENERALS); +} + +function capture_enemy_general(where) { + if (game.active == BRITISH) + capture_american_or_french_general(where); + else + capture_british_general(where); +} + +function remove_benedict_arnold() { + if (game.generals[ARNOLD].location) { + game.log.push("Arnold is removed from the game!"); + game.generals[ARNOLD].location = null; + } +} + +/* ARMIES */ + +function has_british_army(where) { + return has_british_general(where) && has_british_cu(where); +} + +function has_american_army(where) { + return has_american_or_french_general(where) && has_american_or_french_cu(where); +} + +function has_no_british_playing_piece(where) { + if (has_british_pc(where)) + return false; + if (has_british_general(where)) + return false; + if (has_british_cu(where)) + return false; + return true; +} + +function has_no_american_unit(where) { + if (has_american_or_french_general(where)) + return false; + if (has_american_or_french_cu(where)) + return false; + if (game.congress == where) + return false; + return true; +} + +function place_british_reinforcements(who, count, where) { + let already_there = find_british_general(where); + if (who && already_there) { + move_general(already_there, BRITISH_REINFORCEMENTS); + } + if (who) { + game.log.push("B reinforces " + where + " with " + who); + move_general(who, where); + } + if (count > 0) { + game.log.push("B reinforces " + where + " with " + count + " CU"); + move_british_cu(BRITISH_REINFORCEMENTS, where, count); + } +} + +function place_american_reinforcements(who, count, where) { + let already_there = find_american_or_french_general(where); + if (who && already_there) { + // Never replace Washington + if (already_there == WASHINGTON) + who = null; + else + move_general(already_there, AMERICAN_REINFORCEMENTS); + } + if (who) { + game.log.push("A reinforces " + where + " with " + who); + move_general(who, where); + } + game.log.push("A reinforces " + where + " with " + count + " CU"); + place_american_cu(where, count); +} + +function place_french_reinforcements(who, where) { + let already_there = find_american_or_french_general(where); + if (who && already_there) { + // Never replace Washington + if (already_there == WASHINGTON) + who = null; + else + move_general(already_there, AMERICAN_REINFORCEMENTS); + } + if (who) { + game.log.push("A reinforces " + where + " with " + who); + move_general(who, where); + } + game.log.push("A reinforces " + where + " with the French CU"); + move_cu(FRENCH, FRENCH_REINFORCEMENTS, where, count_french_cu(FRENCH_REINFORCEMENTS)); + move_cu(FRENCH, AMERICAN_REINFORCEMENTS, where, count_french_cu(AMERICAN_REINFORCEMENTS)); +} + +function location_of_general(g) { + return game.generals[g].location; +} + +function pickup_max_british_cu(where) { + game.carry_british = count_unmoved_british_cu(where); + if (game.carry_british > 5) + game.carry_british = 5; + game.carry_american = 0; + game.carry_french = 0; +} + +function pickup_max_american_cu(where) { + game.carry_british = 0; + game.carry_french = count_unmoved_french_cu(where); + game.carry_american = count_unmoved_american_cu(where); + if (game.carry_french > 5) + game.carry_french = 5; + if (game.carry_american + game.carry_french > 5) + game.carry_american = 5 - game.carry_french; +} + +function move_army(who, from, to) { + game.count -= movement_cost(from, to); + if (game.mobility && has_enemy_cu(to)) { + game.mobility = false; + game.count -= 1; + } + if (game.carry_british > 0) + move_cu(BRITISH, from, to, game.carry_british); + if (game.carry_american > 0) + move_cu(AMERICAN, from, to, game.carry_american); + if (game.carry_french > 0) + move_cu(FRENCH, from, to, game.carry_french); + move_general(who, to); +} + +function intercept_army(who, from, to) { + if (game.carry_british > 0) + move_cu(BRITISH, from, to, game.carry_british); + if (game.carry_american > 0) + move_cu(AMERICAN, from, to, game.carry_american); + if (game.carry_french > 0) + move_cu(FRENCH, from, to, game.carry_french); + move_general(who, to); +} + +function overrun(where) { + game.log.push(game.active[0] + " overruns CU in " + where); + let cu; + if (game.active == BRITISH) + cu = find_american_cu(where) || find_french_cu(where); + else + cu = find_british_cu(where); + remove_cu(cu.owner, where, 1); +} + +function retreat_american_army(from, to) { + let g = find_american_or_french_general(from); + if (g) + move_general(g, to); + move_cu(AMERICAN, from, to, count_american_cu(from)); + move_cu(FRENCH, from, to, count_french_cu(from)); +} + +function retreat_british_army(from, to) { + let g = find_british_general(from); + if (g) + move_general(g, to); + move_cu(BRITISH, from, to, count_british_cu(from)); +} + +function surrender_american_army(where) { + let g = find_american_or_french_general(where); + if (g) + capture_american_or_french_general(where); + remove_cu(AMERICAN, where, count_american_cu(where)); + remove_cu(FRENCH, where, count_french_cu(where)); +} + +function surrender_british_army(where) { + let g = find_british_general(where); + if (g) + capture_british_general(g); + game.british_losses += count_british_cu(where); + remove_cu(BRITISH, where, count_british_cu(where)); +} + +function disperse_continental_congress(where) { + game.log.push("Contintental Congress dispersed!"); + game.congress = CONTINENTAL_CONGRESS_DISPERSED; + game.congress_was_dispersed = true; +} + +/* MOVE GENERATORS */ + +function gen_action(action, argument) { + if (!game.actions) + game.actions = {} + if (argument != undefined) { + if (!(action in game.actions)) + game.actions[action] = [ argument ]; + else + game.actions[action].push(argument); + } else { + game.actions[action] = 1; + } +} + +function gen_action_undo() { + if (!game.actions) + game.actions = {} + if (game.undo && game.undo.length > 0) + game.actions.undo = 1; + else + game.actions.undo = 0; +} + +function gen_pass() { + gen_action('pass'); +} + +function gen_remove_british_pc_from(list_of_colonies) { + for (let colony of list_of_colonies) { + for (let space of COLONIES[colony]) { + if (has_british_pc(space) && has_no_british_cu(space)) { + gen_action('remove_pc', space); + } + } + } +} + +function gen_remove_american_pc() { + for (let space in SPACES) { + if (has_american_pc(space) && has_no_american_unit(space)) { + gen_action('remove_pc', space); + } + } +} + +function gen_remove_american_pc_from(list_of_colonies) { + for (let colony of list_of_colonies) { + for (let space of COLONIES[colony]) { + if (has_american_pc(space) && has_no_american_unit(space)) { + gen_action('remove_pc', space); + } + } + } +} + +function gen_remove_american_pc_from_non_port(list_of_colonies) { + for (let colony of list_of_colonies) { + for (let space of COLONIES[colony]) { + if (!SPACES[space].port) { + if (has_american_pc(space) && has_no_american_unit(space)) { + gen_action('remove_pc', space); + } + } + } + } +} + +function gen_remove_american_pc_within_two_spaces_of_a_british_general() { + let candidates = {}; + for (let g of BRITISH_GENERALS) { + let a = game.generals[g].location; + if (a in SPACES) { + candidates[a] = true; + for (let b of SPACES[a].exits) { + candidates[b] = true; + for (let c of SPACES[b].exits) { + candidates[c] = true; + } + } + } + } + for (let space in candidates) + if (has_american_pc(space) && has_no_american_unit(space)) + gen_action('remove_pc', space); +} + +function gen_place_american_pc() { + for (let space in SPACES) { + if (has_no_pc(space) && has_no_british_playing_piece(space)) { + gen_action('place_american_pc', space); + } + } +} + +function gen_place_american_pc_in(list_of_colonies) { + for (let colony of list_of_colonies) { + for (let space of COLONIES[colony]) { + if (has_no_pc(space) && has_no_british_playing_piece(space)) { + gen_action('place_american_pc', space); + } + } + } +} + +/* SETUP PHASE */ + +function goto_committees_of_correspondence() { + log(".h2.american Committes of Correspondence"); + log(""); + game.active = AMERICAN; + game.state = 'committees_of_correspondence'; + game.coc = THE_13_COLONIES.slice(); +} + +states.committees_of_correspondence = { + prompt: function (current) { + game.prompt = "Committees of Correspondence: Place 1 PC marker in each of the 13 colonies. " + game.coc.length + " left."; + if (game.coc.length > 0) + gen_place_american_pc_in(game.coc); + else + gen_pass(); + }, + place_american_pc: function (space) { + push_undo(); + let colony = SPACES[space].colony; + remove_from_array(game.coc, colony); + place_american_pc(space); + }, + pass: function () { + clear_undo(); + goto_for_the_king(); + } +} + +function goto_for_the_king() { + log(""); + log(".h2.british For the King"); + log(""); + delete game.coc; + game.active = BRITISH; + game.state = 'for_the_king'; + game.count = 3; + gen_british_pc_ops_start(); +} + +states.for_the_king = { + prompt: function (current) { + game.prompt = "For the King: Place 3 PC markers. " + game.count + " left."; + if (game.count > 0) + gen_british_pc_ops(); + else + gen_pass(); + }, + place_british_pc: function (space) { + push_undo(); + place_british_pc(space); + --game.count; + }, + pass: function () { + clear_undo(); + gen_british_pc_ops_end(); + goto_start_year(); + } +} + +/* REINFORCEMENTS AND START OF STRATEGY PHASE */ + +function automatic_victory() { + let n_american = 0; + let n_british = 0; + for (let space in SPACES) { + n_american += count_french_cu(space) + count_american_cu(space); + if (SPACES[space].colony != "CA") + n_british += count_british_cu(space); + } + if (n_american == 0) { + game.victory = "British Automatic Victory!"; + game.active = "None"; + game.result = BRITISH; + game.state = 'game_over'; + game.log.push(game.victory); + return true; + } + if (n_british == 0) { + game.victory = "American Automatic Victory!"; + game.active = "None"; + game.result = AMERICAN; + game.state = 'game_over'; + game.log.push(game.victory); + return true; + } + return false; +} + +function goto_start_year() { + log(""); + log(".h1 Year " + game.year); + log(""); + + // Prisoner exchange + for (let g of BRITISH_GENERALS) + if (game.generals[g].location == CAPTURED_GENERALS) + move_general(g, BRITISH_REINFORCEMENTS); + for (let g of AMERICAN_GENERALS) + if (game.generals[g].location == CAPTURED_GENERALS) + move_general(g, AMERICAN_REINFORCEMENTS); + + switch (game.year) { + case 1775: place_british_cu(BRITISH_REINFORCEMENTS, 3); break; + case 1776: place_british_cu(BRITISH_REINFORCEMENTS, 8); break; + case 1777: place_british_cu(BRITISH_REINFORCEMENTS, 1); break; + case 1778: place_british_cu(BRITISH_REINFORCEMENTS, 8); break; + case 1779: place_british_cu(BRITISH_REINFORCEMENTS, 1); break; + case 1780: place_british_cu(BRITISH_REINFORCEMENTS, 5); break; + case 1781: place_british_cu(BRITISH_REINFORCEMENTS, 1); break; + case 1782: place_british_cu(BRITISH_REINFORCEMENTS, 1); break; + case 1783: place_british_cu(BRITISH_REINFORCEMENTS, 1); break; + } + + if (game.year == 1776) { + game.deck.push(DECLARATION_OF_INDEPENDENCE); + game.deck.push(BARON_VON_STEUBEN); + } + + if (game.reshuffle) + reshuffle_deck(); + + game.a_hand = []; + game.b_hand = []; + for (let i = 0; i < 7; ++i) { + game.a_hand.push(deal_card()); + game.b_hand.push(deal_card()); + } + + game.a_queue = 0; + game.b_queue = 0; + game.did_discard_event = false; + + // TODO: save the played card numbers instead (rule 6.1B clarification) + game.played_british_reinforcements = 0; + game.played_american_reinforcements = []; + game.active = BRITISH; + game.state = 'british_declare_first'; +} + +states.british_declare_first = { + prompt: function (current) { + game.prompt = "Declare yourself as the first player by playing a campaign card?"; + gen_pass(); + for (let c of CAMPAIGN_CARDS) { + if (game.b_hand.includes(c)) { + gen_action('card_campaign', c); + } + } + }, + card_campaign: function (c) { + delete game.congress_was_dispersed; + game.log.push("B goes first by playing a campaign card"); + game.active = BRITISH; + goto_campaign(c); + }, + pass: function () { + if (game.congress_was_dispersed) + game.active = BRITISH; + else + game.active = AMERICAN; + game.state = 'choose_first_player'; + delete game.congress_was_dispersed; + }, +} + +states.choose_first_player = { + prompt: function (current) { + game.prompt = "Choose who will play the first strategy card."; + gen_action('american_first'); + gen_action('british_first'); + }, + american_first: function (c) { + game.log.push("A goes first"); + goto_strategy_phase(AMERICAN); + }, + british_first: function (c) { + game.log.push("B goes first"); + goto_strategy_phase(BRITISH); + }, +} + +/* STRATEGY PHASE */ + +function goto_strategy_phase(new_active) { + game.active = new_active; + game.state = 'strategy_phase'; + log(""); + if (game.active === AMERICAN) + log(".h2.american American Turn"); + else + log(".h2.british British Turn"); + log(""); +} + +states.strategy_phase = { + prompt: function (current) { + game.prompt = "Play a strategy card."; + gen_strategy_plays(active_hand()); + }, + card_campaign: function (c) { + game.did_discard_event = false; + clear_queue(); + goto_campaign(c); + }, + card_play_event: function (c) { + push_undo(); + game.did_discard_event = false; + clear_queue(); + do_event(c); + }, + card_discard_event: function (c) { + push_undo(); + game.did_discard_event = true; + clear_queue(); + discard_card(c, "PC action"); + game.state = 'discard_event_pc_action'; + }, + card_ops_pc: function (c) { + push_undo(); + game.did_discard_event = false; + clear_queue(); + play_card(c, "for PC"); + goto_ops_pc(CARDS[c].count); + }, + card_ops_reinforcements: function (c) { + push_undo(); + game.did_discard_event = false; + clear_queue(); + goto_ops_reinforcements(c); + }, + card_ops_queue: function (c) { + game.did_discard_event = false; + play_card(c, "to queue"); + if (game.active == BRITISH) + game.b_queue += CARDS[c].count; + else + game.a_queue += CARDS[c].count; + end_strategy_card(); + }, + card_ops_general: function (c) { + push_undo(); + game.did_discard_event = false; + goto_ops_general(c); + }, + exchange_for_discard: function (c) { + game.did_discard_event = false; + let d = game.discard.pop(); + discard_card(c); + active_hand().push(d); + game.log.push(game.active[0] + " picks up " + d + ": " + CARDS[d].title); + }, +} + +function end_strategy_card() { + clear_undo(); + + if (automatic_victory()) + return; + + if (game.campaign) { + if (--game.campaign > 0) { + game.count = 3; // can activate any general! + game.state = 'ops_general_who'; + return; + } else { + delete game.landing_party; + delete game.campaign; + } + } + + if (!game.french_alliance_triggered && game.french_alliance == 9) { + game.log.push("The French sign an alliance with the Americans!"); + game.french_alliance_triggered = true; + if (game.french_navy == FRENCH_REINFORCEMENTS) { + game.save_active = game.active; + game.active = AMERICAN; + game.state = 'place_french_navy_trigger'; + return; + } + } + + game.moved = {}; + for (let cu of game.cu) + cu.moved = 0; + + goto_strategy_phase(ENEMY[game.active]); + + let hand = active_hand(); + if (hand.length == 0) { + game.active = ENEMY[game.active]; + hand = active_hand(); + if (hand.length == 0) + return goto_winter_attrition_phase(); + } +} + +function clear_queue() { + if (game.active == BRITISH) + game.b_queue = 0; + else + game.a_queue = 0; +} + +function gen_strategy_plays(hand) { + for (let c of hand) { + let card = CARDS[c]; + switch (card.type) { + case 'mandatory-event': + gen_action('card_play_event', c); + break; + case 'campaign': + gen_action('card_campaign', c); + break; + case 'ops': + if (can_exchange_for_discard(c)) + gen_action('exchange_for_discard', c); + if (can_activate_general(c)) + gen_action('card_ops_general', c); + gen_action('card_ops_pc', c); + if (can_play_reinforcements()) + gen_action('card_ops_reinforcements', c); + if (card.count < 3) + gen_action('card_ops_queue', c); + break; + case 'british-event': + case 'british-event-or-battle': + if (game.active == BRITISH) + if (can_play_event(c)) + gen_action('card_play_event', c); + gen_action('card_discard_event', c); + break; + case 'american-event': + if (game.active == AMERICAN) + if (can_play_event(c)) + gen_action('card_play_event', c); + gen_action('card_discard_event', c); + break; + case 'british-battle': + case 'american-battle': + gen_action('card_discard_event', c); + break; + } + } +} + +/* DISCARD EVENT CARD FOR PC ACTION */ + +states.discard_event_pc_action = { + prompt: function (current) { + game.prompt = "Place, flip, or remove PC marker."; + gen_pass(); + if (game.active == BRITISH) + gen_british_discard_event_pc_action(); + else + gen_american_discard_event_pc_action(); + }, + place_british_pc: function (space) { + place_british_pc(space); + end_strategy_card(); + }, + place_american_pc: function (space) { + place_american_pc(space); + end_strategy_card(); + }, + remove_pc: function (space) { + remove_pc(space); + end_strategy_card(); + }, + flip_pc: function (space) { + flip_pc(space); + end_strategy_card(); + }, + pass: function () { + end_strategy_card(); + }, +} + +function gen_british_discard_event_pc_action() { + for (let space in SPACES) { + if (is_adjacent_to_british_pc(space)) { + if (has_no_pc(space) && has_no_american_unit(space)) + gen_action('place_british_pc', space); + else if (has_american_pc(space) && has_british_army(space)) + gen_action('flip_pc', space); + else if (has_american_pc(space) && has_no_american_unit(space)) + gen_action('remove_pc', space); + } + } +} + +function gen_american_discard_event_pc_action() { + for (let space in SPACES) { + if (is_adjacent_to_american_pc(space)) { + if (has_no_pc(space) && has_no_british_cu(space)) { + if (allowed_to_place_american_pc()) + gen_action('place_american_pc', space); + } + else if (has_british_pc(space) && has_american_or_french_general(space)) { + gen_action('flip_pc', space); + } + else if (has_british_pc(space) && has_no_british_cu(space)) { + gen_action('remove_pc', space); + } + } + } +} + +/* PLAY OPS CARD FOR PC ACTIONS */ + +function goto_ops_pc(count) { + game.count = count; + game.state = 'ops_pc'; + if (game.active == BRITISH) + gen_british_pc_ops_start(); +} + +states.ops_pc = { + prompt: function (current) { + game.prompt = "Place or flip PC markers. " + game.count + " left."; + gen_pass(); + if (game.count > 0) { + if (game.active == BRITISH) + gen_british_pc_ops(); + else + gen_american_pc_ops() + } + }, + place_british_pc: function (space) { + push_undo(); + place_british_pc(space); + --game.count; + }, + place_american_pc: function (space) { + push_undo(); + place_american_pc(space); + --game.count; + }, + flip_pc: function (space) { + push_undo(); + flip_pc(space); + --game.count; + }, + pass: function () { + if (game.active == BRITISH) + gen_british_pc_ops_end(); + end_strategy_card(); + }, +} + +function gen_british_pc_ops_start() { + game.british_pc_space_list = []; + for (let space in SPACES) { + if (has_no_pc(space) && has_no_american_unit(space)) { + if (is_adjacent_to_british_pc(space)) + game.british_pc_space_list.push(space); + } + } +} + +function gen_british_pc_ops() { + for (let space of game.british_pc_space_list) + gen_action('place_british_pc', space); + for (let space in SPACES) { + if (has_british_army(space)) { + if (has_no_pc(space)) + gen_action('place_british_pc', space); + else if (has_american_pc(space)) + gen_action('flip_pc', space); + } + } +} + +function gen_british_pc_ops_end(space) { + delete game.british_pc_space_list; +} + +function gen_american_pc_ops() { + for (let space in SPACES) { + if (has_no_pc(space) && has_no_british_cu(space)) { + if (allowed_to_place_american_pc()) + gen_action('place_american_pc', space); + } + else if (has_british_pc(space) && has_american_or_french_general(space)) { + gen_action('flip_pc', space); + } + } +} + +/* PLAY OPS CARD FOR REINFORCEMENTS */ + +function goto_ops_reinforcements(c) { + let count = CARDS[c].count; + play_card(c, "for reinforcements"); + if (game.active == BRITISH) { + game.played_british_reinforcements = count; + game.count = count_british_cu(BRITISH_REINFORCEMENTS); + game.state = 'ops_british_reinforcements_who'; + } else { + game.played_american_reinforcements.push(count); + game.count = count; + game.state = 'ops_american_reinforcements_who'; + } +} + +states.ops_british_reinforcements_who = { + prompt: function (current) { + game.prompt = "Reinforcements: choose an available general or pass to bring only CU." + game.prompt += " Carrying " + game.count + " British CU."; + gen_pass(); + gen_british_reinforcements_who(); + }, + drop_british_cu: function () { + --game.count; + }, + pickup_british_cu: function () { + ++game.count; + }, + select_general: function (g) { + push_undo(); + game.state = 'ops_british_reinforcements_where'; + game.who = g; + }, + pass: function () { + push_undo(); + game.state = 'ops_british_reinforcements_where'; + game.who = null; + }, +} + +states.ops_british_reinforcements_where = { + prompt: function (current) { + game.prompt = "Reinforcements: choose a port space."; + game.prompt += " Carrying " + game.count + " British CU."; + gen_british_reinforcements_where(); + }, + drop_british_cu: function () { + --game.count; + }, + pickup_british_cu: function () { + ++game.count; + }, + place_reinforcements: function (space) { + place_british_reinforcements(game.who, game.count, space); + end_strategy_card(); + game.who = null; + }, +} + +states.ops_american_reinforcements_who = { + prompt: function (current) { + game.prompt = "Reinforcements: choose an available general or pass to bring only CU."; + gen_pass(); + gen_american_reinforcements_who(); + }, + select_general: function (g) { + push_undo(); + game.state = 'ops_american_reinforcements_where'; + game.who = g; + }, + pass: function () { + push_undo(); + game.state = 'ops_american_reinforcements_where'; + game.who = null; + }, +} + +states.ops_american_reinforcements_where = { + prompt: function (current) { + game.prompt = "Reinforcements: choose a space."; + gen_american_reinforcements_where(game.who); + }, + place_reinforcements: function (space) { + if (game.who == ROCHAMBEAU) + place_french_reinforcements(game.who, space); + else + place_american_reinforcements(game.who, game.count, space); + end_strategy_card(); + game.who = null; + }, +} + +function gen_british_reinforcements_who() { + for (let g of BRITISH_GENERALS) { + let general = game.generals[g]; + if (general.location == BRITISH_REINFORCEMENTS) { + gen_action('select_general', g); + } + } + if (game.count > 0) + gen_action('drop_british_cu'); + if (game.count < count_british_cu(BRITISH_REINFORCEMENTS)) + gen_action('pickup_british_cu'); +} + +function gen_british_reinforcements_where() { + for (let space in SPACES) { + if (is_non_blockaded_port(space)) + if (!has_american_or_french_cu(space) && !has_american_pc(space)) + gen_action('place_reinforcements', space); + } + if (game.count > 0) + gen_action('drop_british_cu'); + if (game.count < count_british_cu(BRITISH_REINFORCEMENTS)) + gen_action('pickup_british_cu'); +} + +function gen_american_reinforcements_who() { + for (let g of AMERICAN_GENERALS) { + let general = game.generals[g]; + if (general.location == AMERICAN_REINFORCEMENTS) { + gen_action('select_general', g); + } + } +} + +function gen_american_reinforcements_where(general) { + for (let space in SPACES) { + if (!has_british_cu(space) && !has_british_pc(space)) { + if (general == ROCHAMBEAU) { + if (SPACES[space].port) + gen_action('place_reinforcements', space); + } else { + gen_action('place_reinforcements', space); + } + } + } +} + +/* PLAY OPS CARD TO MOVE A GENERAL */ + +function goto_ops_general(c) { + play_card(c, " to activate a general"); + if (game.active == BRITISH) { + game.count = CARDS[c].count + game.b_queue; + game.b_queue = 0; + } else { + game.count = CARDS[c].count + game.a_queue; + game.a_queue = 0; + } + game.state = 'ops_general_who'; +} + +states.ops_general_who = { + prompt: function (current) { + if (game.campaign && game.landing_party) + game.prompt = "Campaign: Activate a general or use a landing party. " + game.campaign + " left."; + else if (game.campaign) + game.prompt = "Campaign: Activate a general. " + game.campaign + " left."; + else + game.prompt = "Activate a general with strategy rating " + game.count + " or lower."; + if (game.landing_party) + gen_landing_party(); + gen_activate_general(); + gen_pass(); + }, + place_british_pc: function (where) { + game.landing_party = 0; + place_british_pc(where); + end_strategy_card(); + }, + flip_pc: function (where) { + game.landing_party = 0; + flip_pc(where); + end_strategy_card(); + }, + select_general: function (g) { + push_undo(); + goto_ops_general_move(g, false); + }, + pass: function () { + if (game.campaign > 0) + game.campaign = 0; + end_strategy_card(); + } +} + +function gen_landing_party() { + for (let space in SPACES) { + if (!is_fortified_port(space) && is_non_blockaded_port(space)) { + if (has_american_pc(space) && has_no_american_unit(space)) + gen_action('flip_pc', space); + if (has_no_pc(space) && has_no_american_unit(space) && has_no_british_playing_piece(space)) + gen_action('place_british_pc', space); + } + } +} + +function gen_activate_general() { + if (game.active == BRITISH) + return gen_activate_british_general(); + else + return gen_activate_american_general(); +} + +function gen_activate_british_general() { + for (let g of BRITISH_GENERALS) + if (is_general_on_map(g) && GENERALS[g].strategy <= game.count && !game.moved[g]) + gen_action('select_general', g); +} + +function gen_activate_american_general() { + for (let g of AMERICAN_GENERALS) + if (is_general_on_map(g) && GENERALS[g].strategy <= game.count && !game.moved[g]) + gen_action('select_general', g); +} + +function goto_remove_general(where) { + game.state = 'remove_general'; + game.where = where; +} + +states.remove_general = { + prompt: function (current) { + game.prompt = "Remove a general to the reinforcements box."; + gen_remove_general(); + }, + select_general: function (g) { + if (game.active == BRITISH) + move_general(g, BRITISH_REINFORCEMENTS); + else + move_general(g, AMERICAN_REINFORCEMENTS); + end_strategy_card(); + }, +} + +function goto_remove_general_after_intercept() { + game.state = 'remove_general_after_intercept'; +} + +states.remove_general_after_intercept = { + prompt: function (current) { + game.prompt = "Remove a general to the reinforcements box."; + gen_remove_general(); + }, + select_general: function (g) { + if (game.active == BRITISH) + move_general(g, BRITISH_REINFORCEMENTS); + else + move_general(g, AMERICAN_REINFORCEMENTS); + end_intercept(); + }, +} + +function goto_remove_general_after_retreat(where) { + game.state = 'remove_general_after_retreat'; + game.where = where; +} + +states.remove_general_after_retreat = { + prompt: function (current) { + game.prompt = "Remove a general to the reinforcements box."; + gen_remove_general(); + }, + select_general: function (g) { + if (game.active == BRITISH) + move_general(g, BRITISH_REINFORCEMENTS); + else + move_general(g, AMERICAN_REINFORCEMENTS); + end_battle(); + }, +} + +function gen_remove_general() { + if (game.active == BRITISH) + return gen_remove_british_general(); + else + return gen_remove_american_general(); +} + +function gen_remove_british_general() { + for (let g of BRITISH_GENERALS) + if (location_of_general(g) == game.where) + gen_action('select_general', g); +} + +function gen_remove_american_general() { + for (let g of AMERICAN_GENERALS) + if (g != WASHINGTON) + if (location_of_general(g) == game.where) + gen_action('select_general', g); +} + +function goto_ops_general_move(g, marblehead) { + game.state = 'ops_general_move'; + game.who = g; + if (marblehead) { + game.mobility = false; + game.count = 6; + } else { + if (game.active == BRITISH) { + game.mobility = false; + game.count = 4; + } else { + game.mobility = true; + game.count = 5; + } + } + let where = location_of_general(g); + if (game.active == BRITISH) + pickup_max_british_cu(where); + else + pickup_max_american_cu(where); +} + +states.ops_general_move = { + prompt: function (current) { + game.prompt = "Move " + game.who + " with "; + if (game.carry_british > 0) { + game.prompt += game.carry_british + " British CU."; + } else if (game.carry_french + game.carry_american > 0) { + if (game.carry_french > 0) { + if (game.carry_american > 0) { + game.prompt += game.carry_french + " French CU and "; + game.prompt += game.carry_american + " American CU."; + } else { + game.prompt += game.carry_french + " French CU."; + } + } else { + game.prompt += game.carry_american + " American CU."; + } + } else { + game.prompt += game.carry_american + " no CU."; + } + if (game.count == 1) + game.prompt += " " + game.count + " move left."; + else if (game.count > 1) + game.prompt += " " + game.count + " moves left."; + + // Cannot stop on enemy general + if (!has_enemy_general(location_of_general(game.who))) + gen_pass(); + + gen_carry_cu(); + gen_move_general(); + }, + + pickup_british_cu: function () { ++game.carry_british; }, + pickup_american_cu: function () { ++game.carry_american; }, + pickup_french_cu: function () { ++game.carry_french; }, + + drop_british_cu: function () { + push_undo(); + --game.carry_british; + if (game.moved[game.who]) + mark_moved_cu(BRITISH, location_of_general(game.who), 1); + }, + drop_american_cu: function () { + push_undo(); + --game.carry_american; + if (game.moved[game.who]) + mark_moved_cu(AMERICAN, location_of_general(game.who), 1); + }, + drop_french_cu: function () { + push_undo(); + --game.carry_french; + if (game.moved[game.who]) + mark_moved_cu(FRENCH, location_of_general(game.who), 1); + }, + + move: function (to) { + push_undo(); + + game.moved[game.who] = 1; + let from = location_of_general(game.who); + let cu = game.carry_british + game.carry_american + game.carry_french; + + let intercept = false; + if (game.active == BRITISH) { + let is_sea_move = (path_type(from, to) == undefined); + if (has_american_pc(to) && cu > 0 && !is_sea_move && !has_british_cu(to)) + intercept = can_intercept_to(to); + } + + move_army(game.who, from, to); + + if (cu > 0) { + if (has_enemy_general(to) && !has_enemy_cu(to)) { + capture_enemy_general(to); + } + if (game.active == BRITISH && game.congress == to && !has_enemy_cu(to)) { + disperse_continental_congress(to); + } + if (cu >= 4 && count_enemy_cu(to) == 1 && !has_enemy_general(to)) { + overrun(to); + } + } + + if (intercept) + goto_intercept(from, to); + else + resume_moving(from, to); + }, + pass: function () { + clear_undo(); + let where = location_of_general(game.who); + end_move(); + if (count_friendly_generals(where) > 1) + goto_remove_general(where); + else + end_strategy_card(); + }, +} + +function resume_moving(from, to) { + if (has_enemy_cu(to)) { + end_move(); + goto_start_battle(from, to); + } +} + +function can_intercept_to(to) { + for (let space of SPACES[to].exits) { + if (has_american_army(space)) { + let g = find_american_or_french_general(space); + if (g && !game.moved[g]) + return true; + } + } + return false; +} + +function gen_intercept() { + for (let space of SPACES[game.where].exits) { + if (has_american_army(space)) { + let g = find_american_or_french_general(space); + if (g && !game.moved[g]) + gen_action('select_general', g); + else if (g) + console.log(g, "already moved"); + } + } +} + +function goto_intercept(from, where) { + clear_undo(); + game.save_who = game.who; + game.who = null; + game.from = from; + game.where = where; + game.active = AMERICAN; + game.state = 'intercept'; +} + +states.intercept = { + prompt: function (current) { + game.prompt = "Intercept " + game.save_who + " in " + game.where + "?"; + gen_pass(); + gen_intercept(); + }, + select_general: function (g) { + // TODO: roll for intercept! + game.moved[g] = 1; + let die = roll_d6(); + if (die <= GENERALS[g].agility) { + game.log.push(g + " intercepts (" + die + " <= " + GENERALS[g].agility + ")"); + game.did_intercept = 1; + + let save_carry_british = game.carry_british; + let save_carry_american = game.carry_american; + let save_carry_french = game.carry_french; + + pickup_max_american_cu(location_of_general(g)); + intercept_army(g, location_of_general(g), game.where); + + game.carry_british = save_carry_british; + game.carry_american = save_carry_american; + game.carry_french = save_carry_french; + + if (count_friendly_generals(game.where) > 1) + goto_remove_general_after_intercept(); + else + end_intercept(); + } else { + game.log.push(g + " fails to intercept (" + die + " > " + GENERALS[g].agility + ")"); + if (!can_intercept_to(game.where)) + end_intercept(); + } + }, + pass: function () { + end_intercept(); + }, +} + +function end_intercept() { + game.active = BRITISH; + game.state = 'ops_general_move'; + game.who = game.save_who; + delete game.save_who; + resume_moving(game.from, game.where); + delete game.from; +} + +function end_move() { + let where = location_of_general(game.who); + if (game.moved[game.who]) { + mark_moved_cu(BRITISH, where, game.carry_british); + mark_moved_cu(AMERICAN, where, game.carry_american); + mark_moved_cu(FRENCH, where, game.carry_french); + } + game.who = null; + delete game.mobility; + delete game.carry_british; + delete game.carry_american; + delete game.carry_french; +} + +function path_type(from, to) { + return PATH_TYPE[PATH_INDEX[from][to]]; +} + +function path_name(from, to) { + return PATH_NAME[PATH_INDEX[from][to]]; +} + +function gen_carry_cu() { + let where = location_of_general(game.who); + if (game.active == BRITISH) { + if (game.carry_british > 0) + gen_action('drop_british_cu'); + if (game.carry_british < 5 && game.carry_british < count_unmoved_british_cu(where)) + gen_action('pickup_british_cu'); + } else { + let carry_total = game.carry_french + game.carry_american; + if (game.carry_french > 0) + gen_action('drop_french_cu'); + if (game.carry_american > 0) + gen_action('drop_american_cu'); + if (carry_total < 5 && game.carry_french < count_unmoved_french_cu(where)) + gen_action('pickup_french_cu'); + if (carry_total < 5 && game.carry_american < count_unmoved_american_cu(where)) + gen_action('pickup_american_cu'); + } +} + +function movement_cost(from, to) { + switch (path_type(from, to)) { + case undefined: return 4; /* must be a sea connection if no direct path */ + case 'wilderness': return 3; + default: return 1; + } +} + +function gen_move_general() { + let from = location_of_general(game.who); + let alone = (game.carry_british + game.carry_american + game.carry_french) == 0; + for (let to of SPACES[from].exits) { + let mp = 1; + if (path_type(from, to) == 'wilderness') { + if (path_name(from, to) == FALMOUTH_QUEBEC) + if (game.who != ARNOLD) + continue; + mp = 3; + } + + if (alone) { + if (has_enemy_cu(to)) + continue; + if (has_enemy_pc(to)) + continue; + // TODO: more robust check for not stopping (or allow undo in case he gets stuck) + if (has_enemy_general(to) && game.count - mp == 0) + continue; + } + + if (game.mobility && has_enemy_cu(to)) { + if (game.count - mp >= 1) + gen_action('move', to); + } else { + if (game.count - mp >= 0) + gen_action('move', to); + } + } + if (game.active == BRITISH && game.count == 4) { + if (is_non_blockaded_port(from)) { + for (let to in SPACES) { + if (to != from) { + if (is_non_blockaded_port(to)) { + if (!has_american_pc(to) && !has_american_or_french_cu(to)) { + // TODO: duplicate action if can move by normal road + gen_action('move', to); + } + } + } + } + } + } +} + +/* CAMPAIGN */ + +function goto_campaign(c) { + play_card(c); + game.state = 'campaign'; + game.campaign = CARDS[c].count; + game.landing_party = game.active == BRITISH ? 1 : 0; + game.count = 3; // can activate any general! + game.state = 'ops_general_who'; +} + +/* EVENTS */ + +events.the_war_ends = function (c, card) { + game.log.push(game.active[0] + " plays " + c + ": " + CARDS[c].title); + game.log.push("The war will end in " + card.year); + game.last_played = c; + remove_from_array(active_hand(), c); + if (game.war_ends) + game.discard.push(WAR_ENDS_1779 + game.war_ends - 1779); + game.war_ends = card.year; + end_strategy_card(); +} + +events.remove_random_british_card = function (c, card) { + play_card(c); + remove_random_card(game.b_hand); +} + +events.remove_random_american_card = function (c, card) { + play_card(c); + remove_random_card(game.a_hand); +} + +function remove_random_card(hand) { + if (hand.length > 0) { + let i = random(hand.length); + let c = hand[i]; + discard_card_from_hand(hand, c); + if (CARDS[c].type == 'mandatory-event') + do_event(c); + else + end_strategy_card(); + } +} + +function advance_french_alliance(count) { + if (game.french_alliance < 9) { + game.french_alliance += count; + if (game.french_alliance > 9) + game.french_alliance = 9; + game.log.push("French alliance advances to " + count); + } +} + +function lose_regular_advantage() { + if (game.regulars) { + game.log.push("The British Regulars Advantage is lost!"); + game.regulars = false; + advance_french_alliance(2); + } +} + +events.baron_von_steuben_trains_the_continental_army = function (c, card) { + play_card(c); + if (is_general_on_map(WASHINGTON)) { + let where = location_of_general(WASHINGTON); + game.log.push("A places 2 CU with Washington in " + where); + place_american_cu(where, 2); + lose_regular_advantage(); + } + end_strategy_card(); +} + +events.advance_french_alliance = function (c, card) { + play_card(c); + advance_french_alliance(card.count); + end_strategy_card(); +} + +events.remove_french_navy = function (c, card) { + play_card(c); + game.french_navy = TURN_TRACK[game.year+1]; + end_strategy_card(); +} + +events.remove_british_pc_from = function (c, card) { + play_card(c); + game.count = card.count; + game.where = card.where; + game.state = 'remove_british_pc_from'; +} + +states.remove_british_pc_from = { + prompt: function (current) { + game.prompt = "Remove British PC markers from " + game.where.join(", ") + ". " + game.count + " left."; + gen_pass(); + gen_remove_british_pc_from(game.where); + }, + remove_pc: function (where) { + remove_pc(where); + if (--game.count == 0) { + delete game.where; + end_strategy_card(); + } + }, + pass: function () { + delete game.where; + end_strategy_card(); + }, +} + +events.remove_american_pc = function (c, card) { + play_card(c); + game.count = card.count; + game.state = 'remove_american_pc'; +} + +states.remove_american_pc = { + prompt: function (current) { + game.prompt = "Remove American PC markers. " + game.count + " left."; + gen_pass(); + gen_remove_american_pc(); + }, + remove_pc: function (where) { + remove_pc(where); + if (--game.count == 0) { + end_strategy_card(); + } + }, + pass: function () { + end_strategy_card(); + }, +} + +events.remove_american_pc_from = function (c, card) { + play_card(c); + game.count = card.count; + game.where = card.where; + game.state = 'remove_american_pc_from'; +} + +states.remove_american_pc_from = { + prompt: function (current) { + game.prompt = "Remove American PC markers from " + game.where.join(", ") + ". " + game.count + " left."; + gen_pass(); + gen_remove_american_pc_from(game.where); + }, + remove_pc: function (where) { + remove_pc(where); + if (--game.count == 0) { + delete game.where; + end_strategy_card(); + } + }, + pass: function () { + delete game.where; + end_strategy_card(); + }, +} + +events.remove_american_pc_from_non_port = function (c, card) { + play_card(c); + game.count = card.count; + game.where = card.where; + game.state = 'remove_american_pc_from_non_port'; +} + +states.remove_american_pc_from_non_port = { + prompt: function (current) { + game.prompt = "Remove American PC markers from non-Port space in " + game.where.join(", ") + ". " + game.count + " left."; + gen_pass(); + gen_remove_american_pc_from_non_port(game.where); + }, + remove_pc: function (where) { + remove_pc(where); + if (--game.count == 0) { + delete game.where; + end_strategy_card(); + } + }, + pass: function () { + delete game.where; + end_strategy_card(); + }, +} + +events.remove_american_pc_within_two_spaces_of_a_british_general = function (c, card) { + play_card(c); + game.count = card.count; + game.state = 'remove_american_pc_within_two_spaces_of_a_british_general'; +} + +states.remove_american_pc_within_two_spaces_of_a_british_general = { + prompt: function (current) { + game.prompt = "Remove American PC markers within two spaces of a British general. " + game.count + " left."; + gen_pass(); + gen_remove_american_pc_within_two_spaces_of_a_british_general(); + }, + remove_pc: function (where) { + remove_pc(where); + if (--game.count == 0) { + delete game.where; + end_strategy_card(); + } + }, + pass: function () { + delete game.where; + end_strategy_card(); + }, +} + +events.place_american_pc = function (c, card) { + play_card(c); + game.count = card.count; + game.state = 'place_american_pc'; +} + +states.place_american_pc = { + prompt: function (current) { + game.prompt = "Place American PC markers. " + game.count + " left."; + gen_pass(); + gen_place_american_pc(); + }, + place_american_pc: function (where) { + place_american_pc(where); + if (--game.count == 0) { + end_strategy_card(); + } + }, + pass: function () { + end_strategy_card(); + }, +} + +events.place_american_pc_in = function (c, card) { + play_card(c); + game.count = card.count; + game.where = card.where; + game.state = 'place_american_pc_in'; +} + +states.place_american_pc_in = { + prompt: function (current) { + game.prompt = "Place American PC markers in " + game.where.join(", ") + ". " + game.count + " left."; + gen_pass(); + gen_place_american_pc_in(game.where); + }, + place_american_pc: function (where) { + place_american_pc(where); + if (--game.count == 0) { + delete game.where; + end_strategy_card(); + } + }, + pass: function () { + delete game.where; + end_strategy_card(); + }, +} + +events.lord_sandwich_coastal_raids = function (c, card) { + play_card(c); + game.state = 'lord_sandwich_coastal_raids'; + game.count = 2; + game.where = null; +} + +states.lord_sandwich_coastal_raids = { + prompt: function (current) { + game.prompt = "Remove two or flip one American PC in a port space."; + gen_pass(); + gen_lord_sandwich_coastal_raids(game.where); + }, + place_british_pc: function (where) { + place_british_pc(where); + end_strategy_card(); + }, + remove_pc: function (where) { + game.where = where; + remove_pc(where); + if (--game.count == 0) + end_strategy_card(); + }, + pass: function () { + end_strategy_card(); + }, +} + +function gen_lord_sandwich_coastal_raids(first_removed) { + for (let space in SPACES) { + if (SPACES[space].port) + if (has_american_pc(space) && has_no_american_unit(space)) + gen_action('remove_pc', space); + if (space == first_removed) + gen_action('place_british_pc', space); + } +} + +events.remove_american_cu = function (c, card) { + play_card(c); + game.state = 'remove_american_cu'; +} + +states.remove_american_cu = { + prompt: function (current) { + game.prompt = "Remove one American CU from any space."; + gen_pass(); + gen_remove_american_cu(); + }, + remove_cu: function (where) { + let cu = find_american_cu(where) || find_french_cu(where); + remove_cu(cu.owner, where, 1); + end_strategy_card(); + }, + pass: function () { + end_strategy_card(); + }, +} + +function gen_remove_american_cu() { + for (let space in SPACES) { + if (has_american_or_french_cu(space)) + gen_action('remove_cu', space); + } +} + +function gen_remove_british_cu() { + for (let space in SPACES) { + if (has_british_cu(space)) + gen_action('remove_cu', space); + } +} + +events.pennsylvania_and_new_jersey_line_mutinies = function (c, card) { + play_card(c); + game.pennsylvania_and_new_jersey_line_mutinies = true; + game.state = 'pennsylvania_and_new_jersey_line_mutinies'; + game.count = 2; + game.where = null; +} + +states.pennsylvania_and_new_jersey_line_mutinies = { + prompt: function (current) { + game.prompt = "Remove two American CUs from the map, one each from two different spaces."; + gen_pass(); + gen_pennsylvania_and_new_jersey_line_mutinies(game.where); + }, + remove_cu: function (where) { + let cu = find_american_cu(where) || find_french_cu(where); + remove_cu(cu.owner, where, 1); + game.where = where; + if (--game.count == 0) + end_strategy_card(); + }, + pass: function () { + end_strategy_card(); + }, +} + +function gen_pennsylvania_and_new_jersey_line_mutinies(first_removed) { + for (let space in SPACES) { + if (has_american_or_french_cu(space)) + if (space != first_removed) + gen_action('remove_cu', space); + } +} + +events.john_glovers_marblehead_regiment = function (c, card) { + play_card(c); + game.state = 'john_glovers_marblehead_regiment_who'; + game.count = 3; // strategy rating for gen_activate_general +} + +states.john_glovers_marblehead_regiment_who = { + prompt: function (current) { + game.prompt = "Activate an American general."; + gen_activate_general(); + }, + select_general: function (g) { + goto_ops_general_move(g, true); + }, +} + +events.declaration_of_independence = function (c, card) { + play_card(c); + game.last_active = game.active; + game.active = AMERICAN; + game.doi = THE_13_COLONIES.slice(); + game.state = 'declaration_of_independence'; +} + +states.declaration_of_independence = { + prompt: function (current) { + game.prompt = "Declaration of Independence: Place 1 PC marker in each of the 13 colonies. "; + game.prompt += game.doi.length + " left."; + gen_pass(); + gen_place_american_pc_in(game.doi); + }, + place_american_pc: function (space) { + let colony = SPACES[space].colony; + remove_from_array(game.doi, colony); + place_american_pc(space); + if (game.doi.length == 0) + end_declaration_of_independence(); + }, + pass: function () { + end_declaration_of_independence(); + } +} + +function end_declaration_of_independence() { + game.active = game.last_active; + delete game.last_active; + delete game.doi; + end_strategy_card(); +} + +function goto_george_washington_captured() { + /* Save all the state we clobber during the interrupt. */ + game.last_state = game.state; + game.last_active = game.active; + game.last_count = game.count; + + game.state = 'george_washington_captured'; + game.active = BRITISH; + game.count = 5; +} + +states.george_washington_captured = { + prompt: function (current) { + game.prompt = "George Washington is captured! Remove American PC markers. " + game.count + " left."; + gen_pass(); + gen_remove_american_pc(); + }, + remove_pc: function (where) { + remove_pc(where); + if (--game.count == 0) { + end_george_washington_captured(); + } + }, + pass: function () { + end_george_washington_captured(); + }, +} + +function end_george_washington_captured() { + /* Restore previous state. */ + game.state = game.last_state; + game.count = game.last_count; + game.active = game.last_active; + delete game.last_state; + delete game.last_active; + delete game.last_count; +} + +function do_event(c) { + let card = CARDS[c]; + if (card.event in events) + events[card.event](c, card); + else + throw new Error("Event not implemented yet: " + card.event) +} + +/* BATTLE */ + +function can_retreat_before_battle(where) { + if (game.did_intercept) + return false; + // can't retreat if attempted (successful or not) interception! + let g = find_american_or_french_general(where); + if (g && !game.moved[g]) + return true; + return false; +} + +function goto_start_battle(from, where) { + clear_undo(); + game.attacker = game.active; + game.attack_from = from; + game.british_losses = 0; + game.where = where; + game.a_bonus = 0; + game.b_bonus = 0; + if (game.active == BRITISH && can_retreat_before_battle(where)) + goto_retreat_before_battle(); + else + goto_play_attacker_battle_card(); +} + +function goto_retreat_before_battle() { + game.active = AMERICAN; + game.who = find_american_or_french_general(game.where); + game.state = 'retreat_before_battle'; +} + +states.retreat_before_battle = { + prompt: function (current) { + game.prompt = "Attempt retreat before battle?"; + gen_pass(); + gen_defender_retreat(); + }, + move: function (to) { + let agility = GENERALS[game.who].agility; + if (GENERALS[game.who].bonus) + agility += 2; + let roll = roll_d6(); + if (roll <= agility) { + game.log.push("A successfully retreats before battle: " + roll + " <= " + agility); + pickup_max_american_cu(game.where); + move_army(game.who, game.where, to); + goto_remove_general_after_retreat_before_battle(to); + } else { + game.log.push("A fails to retreat before battle: " + roll + " > " + agility); + end_retreat_before_battle(); + } + }, + pass: function () { + end_retreat_before_battle(); + }, +} + +function goto_remove_general_after_retreat_before_battle(to) { + if (count_friendly_generals(to) > 1) { + game.state = 'remove_general_after_retreat_before_battle'; + game.save_where = game.where; + game.where = to; + } else { + end_remove_general_after_retreat_before_battle(); + } +} + +states.remove_general_after_retreat_before_battle = { + prompt: function (current) { + game.prompt = "Remove a general to the reinforcements box."; + gen_remove_general(); + }, + select_general: function (g) { + if (game.active == BRITISH) + move_general(g, BRITISH_REINFORCEMENTS); + else + move_general(g, AMERICAN_REINFORCEMENTS); + game.where = game.save_where; + delete game.save_where; + end_remove_general_after_retreat_before_battle(); + }, +} + + +function end_remove_general_after_retreat_before_battle() { + let b_cu = count_british_cu(game.where); + let a_cu = count_american_and_french_cu(game.where); + if (a_cu == 0) { + end_battle(); + } else if (b_cu >= 4 && a_cu == 1) { + overrun(game.where); + end_battle(); + } else { + end_retreat_before_battle(); + } +} + +function can_defender_retreat(to) { + if (to == game.attack_from) + return false; + if (has_enemy_pc(to)) + return false; + if (has_enemy_cu(to)) + return false; + return true; +} + +function can_attacker_retreat() { + let to = game.attack_from; + if (has_enemy_pc(to)) + return false; + if (has_enemy_cu(to)) + return false; + return true; +} + +function gen_defender_retreat() { + let from = game.where; + for (let to of SPACES[from].exits) { + if (can_defender_retreat(to)) + gen_action('move', to); + } + if (game.active == BRITISH) { + let can_sea_retreat = false; + if (is_non_blockaded_port(from)) { + if (is_fortified_port(from)) { + if (has_british_pc(from) && is_non_blockaded_port(from)) + can_sea_retreat = true; + } else { + can_sea_retreat = true; + } + } + if (can_sea_retreat) { + for (let to in SPACES) { + if (to != from && is_non_blockaded_port(to)) { + if (!has_american_pc(to) && !has_american_or_french_cu(to)) { + gen_action('move', to); + } + } + } + } + } +} + +function gen_attacker_retreat() { + if (can_attacker_retreat()) + gen_action('move', game.attack_from); +} + +function end_retreat_before_battle() { + game.who = null; + goto_play_attacker_battle_card(); +} + +function goto_play_attacker_battle_card() { + game.active = game.attacker; + game.state = 'play_attacker_battle_card'; +} + +states.play_attacker_battle_card = { + prompt: function (current) { + game.prompt = "Attack: Play or discard event for DRM."; + gen_pass(); + gen_battle_card(); + }, + card_battle_play: function (c) { + play_card(c, "for +2 DRM"); + if (game.active == BRITISH) { + if (CARDS[c].event == 'remove_benedict_arnold') + remove_benedict_arnold(); + game.b_draw_after_battle = true; + game.b_bonus += 2; + } else { + game.a_draw_after_battle = true; + game.a_bonus += 2; + } + goto_play_defender_battle_card(); + }, + card_battle_discard: function (c) { + discard_card(c, "for +1 DRM"); + if (game.active == BRITISH) { + game.b_draw_after_battle = true; + game.b_bonus += 1; + } else { + game.a_draw_after_battle = true; + game.a_bonus += 1; + } + goto_play_defender_battle_card(); + }, + pass: function () { + goto_play_defender_battle_card(); + }, +} + +function goto_play_defender_battle_card() { + game.state = 'play_defender_battle_card'; + game.active = ENEMY[game.attacker]; +} + +states.play_defender_battle_card = { + prompt: function (current) { + game.prompt = "Defend: Play or discard event for DRM."; + gen_pass(); + gen_battle_card(); + }, + card_battle_play: function (c) { + play_card(c, "for +2 DRM"); + if (game.active == BRITISH) { + if (CARDS[c].event == 'remove_benedict_arnold') + remove_benedict_arnold(); + game.b_draw_after_battle = true; + game.b_bonus += 2; + } else { + game.a_draw_after_battle = true; + game.a_bonus += 2; + } + resolve_battle(); + }, + card_battle_discard: function (c) { + discard_card(c, "for +1 DRM"); + if (game.active == BRITISH) { + game.b_draw_after_battle = true; + game.b_bonus += 1; + } else { + game.a_draw_after_battle = true; + game.a_bonus += 1; + } + resolve_battle(); + }, + pass: function () { + resolve_battle(); + }, +} + +function gen_battle_card() { + for (let c of active_hand()) { + let card = CARDS[c]; + if (game.active == BRITISH) { + switch (card.type) { + case 'british-battle': + case 'british-event-or-battle': + gen_action('card_battle_play', c); + break; + case 'british-event': + case 'american-event': + case 'american-battle': + gen_action('card_battle_discard', c); + break; + } + } else { + switch (card.type) { + case 'british-battle': + case 'british-event-or-battle': + case 'british-event': + case 'american-event': + gen_action('card_battle_discard', c); + break; + case 'american-battle': + gen_action('card_battle_play', c); + break; + } + } + } +} + +function roll_loser_combat_losses(log) { + let roll = roll_d6(); + let losses = 0; + log.push("Loser Combat Loss Roll: " + roll); + switch (roll) { + case 1: case 2: case 3: losses = 1; break; + case 4: case 5: losses = 2; break; + case 6: losses = 3; break; + } + return losses; +} + +function roll_winner_combat_losses(log, losing_general) { + let agility = losing_general ? GENERALS[losing_general].agility : 0; + let roll = roll_d6(); + log.push("Enemy Agility Rating: " + agility); + log.push("Winner Combat Loss Roll: " + roll); + let losses = 0; + switch (agility) { + case 0: losses = (roll == 1) ? 1 : 0; break; + case 1: losses = (roll <= 2) ? 1 : 0; break; + case 2: losses = (roll <= 3) ? 1 : 0; break; + case 3: losses = (roll <= 4) ? 1 : 0; break; + } + return losses; +} + +function apply_british_combat_losses(max) { + let cu = find_british_cu(game.where); + if (cu.count > max) { + cu.count -= max; + return max; + } + remove_from_array(game.cu, cu); + return cu.count; +} + +function apply_american_combat_losses(max) { + let cu = find_american_cu(game.where); + if (cu) { + if (cu.count > max) { + cu.count -= max; + return max; + } + remove_from_array(game.cu, cu); + return cu.count; + } + return 0; +} + +function apply_french_combat_losses(max) { + let cu = find_french_cu(game.where); + if (cu) { + if (cu.count > max) { + cu.count -= max; + return max; + } + remove_from_array(game.cu, cu); + return cu.count; + } + return 0; +} + +function apply_american_and_french_combat_losses(max) { + let n = apply_american_combat_losses(max); + if (n < max) + n += apply_french_combat_losses(max - n) + return n; +} + +function resolve_battle() { + let a_log = []; + let b_log = []; + + game.active = ENEMY[game.active]; + let b_g = find_british_general(game.where); + let b_cu = count_british_cu(game.where); + let a_g = find_american_or_french_general(game.where); + let a_cu = count_american_and_french_cu(game.where); + let b_br = 0; + let a_br = 0; + + if (b_g) + b_log.push("General: " + b_g); + if (a_g) + a_log.push("General: " + a_g); + + if (b_g) { + let roll = roll_d6(); + if (roll <= 3) + b_br = Math.min(Math.floor(GENERALS[b_g].battle / 2), b_cu); + else + b_br = Math.min(GENERALS[b_g].battle, b_cu); + b_log.push("Actual Battle Rating Roll: " + roll); + } + + if (a_g) { + let roll = roll_d6(); + if (roll <= 3) + a_br = Math.min(Math.floor(GENERALS[a_g].battle / 2), a_cu); + else + a_br = Math.min(GENERALS[a_g].battle, a_cu); + a_log.push("Actual Battle Rating Roll: " + roll); + } + + b_log.push("+" + b_cu + " CU"); + a_log.push("+" + a_cu + " CU"); + b_log.push("+" + b_br + " Actual Battle Rating"); + a_log.push("+" + a_br + " Actual Battle Rating"); + + let b_drm = b_cu + b_br + game.b_bonus; + if (game.regulars) { + b_log.push("+1 British Regulars' Advantage"); + b_drm += 1; + } + if (is_non_blockaded_port(game.where)) { + if (is_fortified_port(game.where)) { + if (has_british_pc(game.where)) { + b_log.push("+1 Royal Navy Support"); + b_drm += 1; + } + } else { + b_log.push("+1 Royal Navy Support"); + b_drm += 1; + } + } + if (is_british_militia(game.where)) { + b_log.push("+1 Militia"); + b_drm += 1; + } + if (game.b_bonus == 2) + b_log.push("+2 Battle Card"); + else if (game.b_bonus == 1) + b_log.push("+1 Discard of an Event Card"); + + let a_drm = a_cu + a_br + game.a_bonus; + if (is_american_militia(game.where)) { + a_log.push("+1 Militia"); + a_drm += 1; + } + if (is_american_winter_offensive()) { + a_log.push("+2 American Winter Offensive"); + a_drm += 2; + } + if (game.a_bonus == 2) + a_log.push("+2 Battle Card"); + else if (game.a_bonus == 1) + a_log.push("+1 Discard of an Event Card"); + if (game.did_intercept) { + a_log.push("+1 Interception"); + a_drm += 1; + } + + let b_roll = roll_d6(); + let a_roll = roll_d6(); + + b_log.push("Battle Roll: " + b_roll); + b_log.push("Battle Total: " + (b_roll + b_drm)); + a_log.push("Battle Roll: " + a_roll); + a_log.push("Battle Total: " + (a_roll + a_drm)); + + let victor; + if (game.active == BRITISH) + victor = (b_roll + b_drm) >= (a_roll + a_drm) ? BRITISH : AMERICAN; + else + victor = (b_roll + b_drm) >= (a_roll + a_drm) ? BRITISH : AMERICAN; + + let a_lost_cu, b_lost_cu; + if (victor == BRITISH) { + b_lost_cu = roll_winner_combat_losses(b_log, a_g); + a_lost_cu = roll_loser_combat_losses(a_log); + } else { + b_lost_cu = roll_loser_combat_losses(b_log); + a_lost_cu = roll_winner_combat_losses(a_log, b_g); + } + + game.british_losses = apply_british_combat_losses(b_lost_cu); + let american_losses = apply_american_and_french_combat_losses(a_lost_cu); + + b_log.push("Losses: " + game.british_losses + " CU"); + a_log.push("Losses: " + american_losses + " CU"); + + // Special case: winning general with no CU on enemy PC is captured + if (victor == BRITISH) { + if (b_g && count_british_cu(game.where) == 0 && has_american_pc(game.where)) + capture_british_general(game.where); + } else { + if (a_g && count_american_and_french_cu(game.where) == 0 && has_british_pc(game.where)) + capture_american_or_french_general(game.where); + } + + game.log.push("BRITISH BATTLE REPORT:\n" + b_log.join("\n")); + game.log.push("AMERICAN BATTLE REPORT:\n" + a_log.join("\n")); + game.log.push(victor + " victory in " + game.where + "!"); + + if (victor == AMERICAN) + advance_french_alliance(1); + + goto_retreat_after_battle(victor); +} + +function goto_retreat_after_battle(victor) { + if (victor == BRITISH) { + game.who = find_american_or_french_general(game.where); + if (game.who == null && count_american_and_french_cu(game.where) == 0) + return end_battle(); + } else { + game.who = find_british_general(game.where); + if (game.who == null && count_british_cu(game.where) == 0) + return end_battle(); + } + game.active = ENEMY[victor]; + game.state = 'retreat_after_battle'; +} + +states.retreat_after_battle = { + prompt: function (current) { + game.prompt = "Retreat after battle."; + gen_action('surrender'); + if (game.active == game.attacker) + gen_attacker_retreat(); + else + gen_defender_retreat(); + }, + move: function (to) { + game.log.push(game.active[0] + " retreats to " + to); + if (game.active == BRITISH) + retreat_british_army(game.where, to); + else + retreat_american_army(game.where, to); + if (count_friendly_generals(to) > 1) + goto_remove_general_after_retreat(to); + else + end_battle(); + }, + surrender: function () { + // End battle here, so if Washington is captured we can handle the interrupt state. + let active = game.active; + let where = game.where; + end_battle(); + + game.log.push(active[0] + " surrenders"); + if (active == BRITISH) + surrender_british_army(where); + else + surrender_american_army(where); + }, +} + +function end_battle() { + game.active = game.attacker; + + if (game.british_losses >= 3) + lose_regular_advantage(); + + // TODO: delay until end of campaign + if (game.b_draw_after_battle) + game.b_hand.push(deal_card()); + if (game.a_draw_after_battle) + game.a_hand.push(deal_card()); + + delete game.did_intercept; + delete game.b_bonus; + delete game.a_bonus; + delete game.b_draw_after_battle; + delete game.a_draw_after_battle; + delete game.attack_from; + delete game.british_losses; + delete game.attacker; + game.where = null; + game.who = null; + end_strategy_card(); +} + +/* END TURN PHASES */ + +function apply_single_winter_attrition(owner, space) { + let die = roll_d6(); + game.log.push(owner[0] + " attrition roll " + die + " in " + space); + if (die <= 3) { + game.log.push(owner[0] + " lose 1 CU in " + space); + remove_cu(owner, space, 1); + } +} + +function apply_winter_attrition(owner, space, n) { + let half = Math.floor(n / 2); + game.log.push(owner[0] + " lose " + half + " CU in " + space); + remove_cu(owner, space, half); +} + +function goto_winter_attrition_phase() { + game.log.push(""); + game.log.push("Winter Attrition"); + + for (let space in SPACES) { + let wq = is_winter_quarter_space(space); + let n_british = count_british_cu(space); + let n_american = count_american_cu(space); + let n_french = count_french_cu(space); + let has_washington = game.generals[WASHINGTON].location == space; + + if (n_british == 1 && !wq) + apply_single_winter_attrition(BRITISH, space, has_british_general(space)); + if (n_british > 1 && !wq) + apply_winter_attrition(BRITISH, space, n_british); + + if (n_american == 0 && n_french == 1 && !wq) + apply_single_winter_attrition(FRENCH, space, has_american_or_french_general(space)); + if (n_american == 0 && n_french > 1 && !wq) + apply_winter_attrition(FRENCH, space, n_french); + + if (n_american == 1 && n_french == 0) + apply_single_winter_attrition(AMERICAN, space, has_american_or_french_general(space)); + if (n_american > 1 && n_french == 0) { + let n = n_american; + if (has_washington && wq) + n = Math.max(0, n - 5); + apply_winter_attrition(AMERICAN, space, n); + } + + if (n_american > 0 && n_french > 0) { + let n = n_american + n_french; + if (has_washington && wq) + n = Math.max(0, n - 5); + let half = Math.floor(n / 2); + + // TODO: let player choose (but why would he ever choose the french?) + let lose_american = Math.min(half, n_american); + game.log.push(owner[0] + " lose " + lose_american + " American CU in " + space); + remove_cu(AMERICAN, space, n_american); + half -= lose_american; + n_american -= lose_american; + + if (half > 0) { + game.log.push(owner[0] + " lose " + half + " French CU in " + space); + remove_cu(FRENCH, space, half); + } + } + } + + if (automatic_victory()) + return; + + goto_french_naval_phase(); +} + +function goto_french_naval_phase() { + if (game.french_navy != FRENCH_REINFORCEMENTS) { + game.active = AMERICAN; + game.state = 'place_french_navy'; + } else { + goto_political_control_phase(); + } +} + +function gen_place_french_navy() { + gen_action('place_navy', "Sea1"); + gen_action('place_navy', "Sea2"); + gen_action('place_navy', "Sea3"); + gen_action('place_navy', "Sea4"); + gen_action('place_navy', "Sea5"); + gen_action('place_navy', "Sea6"); + gen_action('place_navy', "Sea7"); +} + +states.place_french_navy_trigger = { + prompt: function (current) { + game.prompt = "Place the French Navy in a blockade zone."; + gen_place_french_navy(); + }, + place_navy: function (zone) { + game.log.push("A places French Navy."); + game.french_navy = zone; + game.active = game.save_active; + delete game.save_active; + end_strategy_card() + }, +} + +states.place_french_navy = { + prompt: function (current) { + game.prompt = "Place the French Navy in a blockade zone."; + gen_place_french_navy(); + }, + place_navy: function (zone) { + game.log.push("A places French Navy."); + game.french_navy = zone; + goto_political_control_phase(); + }, +} + +function place_pc_markers_segment() { + for (let space in SPACES) { + if (has_american_army(space)) { + if (has_no_pc(space)) + place_american_pc(space); + else if (has_british_pc(space)) + flip_pc(space); + } + if (has_british_army(space)) { + if (has_no_pc(space)) + place_british_pc(space); + else if (has_american_pc(space)) + flip_pc(space); + } + } +} + +function is_american_pc_root(space) { + if (has_no_pc(space) && has_no_british_cu(space)) + return true; + if (game.congress == space) + return true; + if (has_american_pc(space)) { + if (has_american_or_french_cu(space)) + return true; + if (has_american_or_french_general(space)) + return true; + } + return false; +} + +function is_british_pc_root(space) { + if (has_no_pc(space) && !has_american_or_french_cu(space) && !has_american_or_french_general(space)) + return true; + if (has_british_pc(space)) { + if (is_port(space)) + return true; + if (has_british_cu(space)) + return true; + } + return false; +} + +function is_american_pc_path(space) { + if (has_american_pc(space)) + return !has_british_army(space); + return false; +} + +function is_british_pc_path(space) { + if (has_british_pc(space)) + return !has_american_army(space); + return false; +} + +function spread_american_path(seen, from) { + for (let to of SPACES[from].exits) { + if (to in seen) + continue; + if (is_american_pc_path(to)) { + seen[to] = 1; + spread_american_path(seen, to); + } + } +} + +function spread_british_path(seen, from) { + for (let to of SPACES[from].exits) { + if (to in seen) + continue; + if (is_british_pc_path(to)) { + seen[to] = 1; + spread_british_path(seen, to); + } + } +} + +function remove_isolated_american_pc_segment() { + game.log.push("Removing isolated American PC"); + let seen = {}; + for (let space in SPACES) { + if (is_american_pc_root(space)) { + seen[space] = 1; + spread_american_path(seen, space); + } + } + for (let space in SPACES) + if (has_american_pc(space) && !seen[space]) + remove_pc(space); +} + +function remove_isolated_british_pc_segment() { + game.log.push("Removing isolated British PC"); + let seen = {}; + for (let space in SPACES) { + if (is_british_pc_root(space)) { + seen[space] = 1; + spread_british_path(seen, space); + } + } + for (let space in SPACES) + if (has_british_pc(space) && !seen[space]) + remove_pc(space); +} + +function gen_return_continental_congress() { + let n = 0; + for (let space in SPACES) { + if (SPACES[space].colony != 'CA') { + if (has_american_pc(space) && has_no_british_playing_piece(space)) { + gen_action('place_continental_congress', space); + ++n; + } + } + } + return n; +} + +function goto_political_control_phase() { + if (game.congress == CONTINENTAL_CONGRESS_DISPERSED) { + game.active = AMERICAN; + game.state = 'return_continental_congress'; + } else { + goto_political_control_phase_2(); + } +} + +states.return_continental_congress = { + prompt: function () { + game.prompt = "Return Continental Congress to a space in the 13 colonies."; + if (gen_place_continental_congress() == 0) + gen_pass(); + }, + place_continental_congress: function (where) { + game.congess = where; + goto_political_control_phase_2(); + }, + pass: function () { + goto_political_control_phase_2(); + } +} + +function goto_political_control_phase_2() { + place_pc_markers_segment(); + remove_isolated_american_pc_segment(); + remove_isolated_british_pc_segment(); + goto_end_phase(); +} + +states.european_war = { + prompt: function () { + game.prompt = "European War: Remove 2 British CU from any spaces. " + game.count + " left."; + gen_pass(); + gen_remove_british_cu(); + }, + remove_cu: function (where) { + let cu = find_british_cu(where); + remove_cu(BRITISH, where, 1); + if (--game.count == 0) + goto_end_phase(); + }, + pass: function () { + goto_end_phase(); + } +} + +function norths_government_falls() { + update_colony_control(); + + let n_american = 0; + for (let c in COLONIES) + if (game.control[c] == AMERICAN) + ++n_american; + + if (n_american >= 7) + game.result = AMERICAN; + else + game.result = BRITISH; + + game.victory = "North's Government Falls: " + game.result + " Victory!"; + game.active = "None"; + game.state = 'game_over'; + + game.log.push(game.victory); +} + +function goto_end_phase() { + if (game.french_alliance_triggered && !game.european_war) { + game.european_war = true; + game.count = 2; + game.active = AMERICAN; + game.state = 'european_war'; + game.reshuffle = true; + return; + } + + if ((game.war_ends && game.year >= game.war_ends) || game.year == 1783) + return norths_government_falls(); + + delete game.pennsylvania_and_new_jersey_line_mutinies; + game.a_queue = game.b_queue = 0; + game.year += 1; + goto_start_year(); +} + +states.game_over = { + prompt: function () { + game.prompt = game.victory; + } +} + +/* CLIENT/SERVER COMMS */ + +exports.ready = function (scenario, options, players) { + return players.length === 2; +} + +exports.setup = function (seed, scenario, players) { + setup_game(seed); + return game; +} + +exports.action = function (state, current, action, arg) { + game = state; + // TODO: check against action list + if (current == game.active) { + let S = states[game.state]; + if (action in S) { + S[action](arg); + } else { + if (action === 'undo' && game.undo && game.undo.length > 0) + pop_undo(); + else + throw new Error("Invalid action: " + action); + } + } + return game; +} + +function list_actions(current) { + game.actions = {} + game.prompt = ""; + states[game.state].prompt(current); +} + +exports.view = function(state, current) { + game = state; + + list_actions(current); + + update_colony_control(); + + let view = { + active: state.active, + year: state.year, + war_ends: state.war_ends, + played_british_reinforcements: state.played_british_reinforcements, + played_american_reinforcements: state.played_american_reinforcements, + congress: state.congress, + european_war: state.european_war, + french_alliance: state.french_alliance, + french_navy: state.french_navy, + regulars: state.regulars, + generals: state.generals, + cu: state.cu, + pc: state.pc, + control: state.control, + a_cards: state.a_hand.length, + b_cards: state.b_hand.length, + a_queue: state.a_queue, + b_queue: state.b_queue, + last_played: state.last_played, + who: state.who, + log: state.log, + } + + if (state.pennsylvania_and_new_jersey_line_mutinies) + view.pennsylvania_and_new_jersey_line_mutinies = true; + + if (current == AMERICAN) + view.hand = state.a_hand; + else if (current == BRITISH) + view.hand = state.b_hand; + else + view.hand = []; + + if (current == state.active) { + gen_action_undo(); + view.prompt = state.prompt; + view.actions = state.actions; + } else { + view.prompt = "Waiting for " + game.active + " player \u2014 " + game.prompt; + } + + return view; +} -- cgit v1.2.3