summaryrefslogtreecommitdiff
path: root/rules.js
diff options
context:
space:
mode:
authorTor Andersson <tor@ccxvii.net>2021-05-01 00:48:35 +0200
committerTor Andersson <tor@ccxvii.net>2022-11-16 19:47:32 +0100
commit38ffd7b596c5888b36588e4490c4d4bc35e7be62 (patch)
tree756e3ce290178b279026e744da0bcf7d793a42b8 /rules.js
downloadhammer-of-the-scots-38ffd7b596c5888b36588e4490c4d4bc35e7be62.tar.gz
hammer: Import Hammer of the Scots.
Diffstat (limited to 'rules.js')
-rw-r--r--rules.js2825
1 files changed, 2825 insertions, 0 deletions
diff --git a/rules.js b/rules.js
new file mode 100644
index 0000000..3514f95
--- /dev/null
+++ b/rules.js
@@ -0,0 +1,2825 @@
+"use strict";
+
+exports.scenarios = [
+ "Braveheart",
+ "The Bruce",
+ "Campaign",
+];
+
+// TODO: delay retreats to end of battle initiative step so they are hidden
+// TODO: prohibit moving back to last location?
+// TODO: undo builds
+
+const { CARDS, BLOCKS, AREAS, BORDERS } = require('./data');
+
+const ENEMY = { Scotland: "England", England: "Scotland" }
+const OBSERVER = "Observer";
+const BOTH = "Both";
+const ENGLAND = "England";
+const SCOTLAND = "Scotland";
+const E_BAG = "E. Bag";
+const S_BAG = "S. Bag";
+const EDWARD = "Edward";
+const KING = "King";
+const MORAY = "Moray";
+const E_BRUCE = "Bruce/E";
+const S_BRUCE = "Bruce/S";
+const E_COMYN = "Comyn/E";
+const S_COMYN = "Comyn/S";
+const WALLACE = "Wallace";
+const NORSE = "Norse";
+const FRENCH_KNIGHTS = "French Knights";
+
+// serif cirled numbers
+const DIE_HIT = [ 0, '\u2776', '\u2777', '\u2778', '\u2779', '\u277A', '\u277B' ];
+const DIE_MISS = [ 0, '\u2460', '\u2461', '\u2462', '\u2463', '\u2464', '\u2465' ];
+
+const ATTACK_MARK = " *";
+const RESERVE_MARK = "";
+
+let states = {};
+
+let game = null;
+
+function log(...args) {
+ let s = Array.from(args).join(" ");
+ game.log.push(s);
+}
+
+function print_turn_log_no_count(text) {
+ function print_move(last) {
+ return "\n" + last.join(" \u2192 ");
+ }
+ if (game.turn_log.length > 0) {
+ game.turn_log.sort();
+ for (let entry of game.turn_log)
+ text += print_move(entry);
+ } else {
+ text += "\nnothing.";
+ }
+ log(text);
+ delete game.turn_log;
+}
+
+function print_turn_log(verb) {
+ function print_move(last) {
+ return "\n" + n + " " + last.join(" \u2192 ");
+ }
+ let text = game.active + " " + verb + ":";
+ game.turn_log.sort();
+ let last = game.turn_log[0];
+ let n = 0;
+ for (let entry of game.turn_log) {
+ if (entry.toString() != last.toString()) {
+ text += print_move(last);
+ n = 0;
+ }
+ ++n;
+ last = entry;
+ }
+ if (n > 0)
+ text += print_move(last);
+ else
+ text += "\nnothing.";
+ log(text);
+ delete game.turn_log;
+}
+
+function is_active_player(current) {
+ return (current == game.active) || (game.active == BOTH && current != OBSERVER);
+}
+
+function is_inactive_player(current) {
+ return current == OBSERVER || (game.active != current && game.active != BOTH);
+}
+
+function remove_from_array(array, item) {
+ let i = array.indexOf(item);
+ if (i >= 0)
+ array.splice(i, 1);
+}
+
+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 log = game.log;
+ Object.assign(game, JSON.parse(undo.pop()));
+ game.undo = undo;
+ log.length = game.log;
+ game.log = log;
+}
+
+function gen_action_undo(view) {
+ if (!view.actions)
+ view.actions = {}
+ if (game.undo && game.undo.length > 0)
+ view.actions.undo = 1;
+ else
+ view.actions.undo = 0;
+}
+
+function gen_action(view, action, argument) {
+ if (!view.actions)
+ view.actions = {}
+ if (argument != undefined) {
+ if (!(action in view.actions))
+ view.actions[action] = [ argument ];
+ else
+ view.actions[action].push(argument);
+ } else {
+ view.actions[action] = 1;
+ }
+}
+
+function roll_d6() {
+ return Math.floor(Math.random() * 6) + 1;
+}
+
+function shuffle_deck() {
+ let deck = [];
+ for (let c = 1; c <= 25; ++c)
+ deck.push(c);
+ return deck;
+}
+
+function deal_cards(deck, n) {
+ let hand = [];
+ for (let i = 0; i < n; ++i) {
+ let k = Math.floor(Math.random() * deck.length);
+ hand.push(deck[k]);
+ deck.splice(k, 1);
+ }
+ return hand;
+}
+
+function block_name(who) {
+ if (who == EDWARD)
+ return game.edward == 1 ? "Edward I" : "Edward II";
+ if (who == KING)
+ return "Scottish King";
+ return BLOCKS[who].name;
+}
+
+function block_owner(who) {
+ return BLOCKS[who].owner;
+}
+
+function block_type(who) {
+ return BLOCKS[who].type;
+}
+
+function block_move(who) {
+ return BLOCKS[who].move;
+}
+
+function block_max_steps(who) {
+ return BLOCKS[who].steps;
+}
+
+function block_is_mortal(who) {
+ return BLOCKS[who].mortal;
+}
+
+function block_initiative(who) {
+ return BLOCKS[who].combat[0];
+}
+
+function block_printed_fire_power(who) {
+ return BLOCKS[who].combat[1] | 0;
+}
+
+function block_fire_power(who, where) {
+ let area = AREAS[where];
+ let combat = block_printed_fire_power(who);
+ if (is_defender(who)) {
+ if (block_type(who) == 'nobles' && area.home == block_name(who))
+ ++combat;
+ else if (who == MORAY && where == "Moray")
+ ++combat;
+ }
+ return combat;
+}
+
+function is_coastal_area(where) {
+ return AREAS[where].coastal;
+}
+
+function is_cathedral_area(where) {
+ return AREAS[where].cathedral;
+}
+
+function is_friendly_coastal_area(where) {
+ return is_coastal_area(where) && is_friendly_area(where);
+}
+
+function is_in_friendly_coastal_area(who) {
+ let where = game.location[who];
+ if (where && where != E_BAG && where != S_BAG)
+ return is_friendly_coastal_area(where);
+ return false;
+}
+
+function is_on_map(who) {
+ let where = game.location[who];
+ if (where && where != E_BAG && where != S_BAG)
+ return true;
+ return false;
+}
+
+function count_blocks_in_area(where) {
+ let count = 0;
+ for (let b in BLOCKS)
+ if (game.location[b] == where)
+ ++count;
+ return count;
+}
+
+function castle_limit(where) {
+ if (game.active == SCOTLAND && is_cathedral_area(where))
+ return AREAS[where].limit + 1;
+ return AREAS[where].limit;
+}
+
+function is_within_castle_limit(where) {
+ return count_blocks_in_area(where) <= castle_limit(where);
+}
+
+function is_under_castle_limit(where) {
+ return count_blocks_in_area(where) < castle_limit(where);
+}
+
+function count_english_nobles() {
+ let count = 0;
+ for (let b in BLOCKS)
+ if (block_owner(b) == ENGLAND && block_type(b) == 'nobles')
+ if (is_on_map(b))
+ ++count;
+ return count;
+}
+
+function count_scottish_nobles() {
+ let count = 0;
+ for (let b in BLOCKS)
+ if (block_owner(b) == SCOTLAND && block_type(b) == 'nobles')
+ if (is_on_map(b))
+ ++count;
+ if (is_on_map(MORAY))
+ ++count;
+ return count;
+}
+
+function find_noble(owner, name) {
+ if (name in BLOCKS)
+ return name;
+ return name + "/" + owner[0];
+}
+
+function border_id(a, b) {
+ return (a < b) ? a + "/" + b : b + "/" + a;
+}
+
+function border_was_last_used_by_enemy(from, to) {
+ return game.last_used[border_id(from, to)] == ENEMY[game.active];
+}
+
+function border_type(a, b) {
+ return BORDERS[border_id(a,b)];
+}
+
+function border_limit(a, b) {
+ return game.border_limit[border_id(a,b)] || 0;
+}
+
+function reset_border_limits() {
+ game.border_limit = {};
+}
+
+function count_friendly(where) {
+ let p = game.active;
+ let count = 0;
+ for (let b in BLOCKS)
+ if (game.location[b] == where && block_owner(b) == p)
+ ++count;
+ return count;
+}
+
+function count_enemy(where) {
+ let p = ENEMY[game.active];
+ let count = 0;
+ for (let b in BLOCKS)
+ if (game.location[b] == where && block_owner(b) == p)
+ ++count;
+ return count;
+}
+
+function is_friendly_area(where) { return count_friendly(where) > 0 && count_enemy(where) == 0; }
+function is_enemy_area(where) { return count_friendly(where) == 0 && count_enemy(where) > 0; }
+function is_neutral_area(where) { return count_friendly(where) == 0 && count_enemy(where) == 0; }
+function is_contested_area(where) { return count_friendly(where) > 0 && count_enemy(where) > 0; }
+function is_friendly_or_neutral_area(where) { return is_friendly_area(where) || is_neutral_area(where); }
+
+function have_contested_areas() {
+ for (let where in AREAS)
+ if (is_contested_area(where))
+ return true;
+ return false;
+}
+
+function count_pinning(where) {
+ return count_enemy(where);
+}
+
+function count_pinned(where) {
+ let count = 0;
+ for (let b in BLOCKS)
+ if (game.location[b] == where && block_owner(b) == game.active)
+ if (!game.reserves.includes(b))
+ ++count;
+ return count;
+}
+
+function is_pinned(from) {
+ if (game.active == game.p2) {
+ if (count_pinned(from) <= count_pinning(from))
+ return true;
+ }
+ return false;
+}
+
+function can_block_use_border(who, from, to) {
+ if (border_type(from, to) == 'major')
+ return border_limit(from, to) < 6;
+ return border_limit(from, to) < 2;
+}
+
+function can_block_move_to(who, from, to) {
+ // No group moves across Anglo-Scottish border
+ if (from == ENGLAND || to == ENGLAND)
+ if (game.moves == 0)
+ return false;
+ if (game.active == SCOTLAND && game.truce == SCOTLAND && to == ENGLAND)
+ return false;
+ if (can_block_use_border(who, from, to)) {
+ if (count_pinning(from) > 0) {
+ if (border_was_last_used_by_enemy(from, to))
+ return false;
+ }
+ if (game.truce == game.active && is_enemy_area(to))
+ return false;
+ return true;
+ }
+ return false;
+}
+
+function can_block_move(who) {
+ if (who == NORSE)
+ return false;
+ if (block_owner(who) == game.active && !game.moved[who]) {
+ let from = game.location[who];
+ if (from) {
+ if (is_pinned(from))
+ return false;
+ for (let to of AREAS[from].exits)
+ if (can_block_move_to(who, from, to))
+ return true;
+ }
+ }
+ return false;
+}
+
+function can_block_continue(who, from, here) {
+ if (here == ENGLAND)
+ return false;
+ if (is_contested_area(here))
+ return false;
+ if (border_type(from, here) == 'minor')
+ return false;
+ if (game.distance >= block_move(who))
+ return false;
+ for (let to of AREAS[here].exits)
+ if (to != game.last_from && can_block_move_to(who, here, to))
+ return true;
+ return false;
+}
+
+function can_block_retreat_to(who, to) {
+ if (is_friendly_area(to) || is_neutral_area(to)) {
+ let from = game.location[who];
+ if (block_owner(who) == ENGLAND && from == ENGLAND)
+ return false;
+ if (block_owner(who) == SCOTLAND && to == ENGLAND)
+ return false;
+ if (can_block_use_border(who, from, to)) {
+ if (border_was_last_used_by_enemy(from, to))
+ return false;
+ return true;
+ }
+ }
+ return false;
+}
+
+function can_block_retreat(who) {
+ if (who == NORSE)
+ return true;
+ if (block_owner(who) == game.active) {
+ let from = game.location[who];
+ for (let to of AREAS[from].exits)
+ if (can_block_retreat_to(who, to))
+ return true;
+ }
+ return false;
+}
+
+function can_block_regroup_to(who, to) {
+ if (is_friendly_area(to) || is_neutral_area(to)) {
+ let from = game.location[who];
+ if (block_owner(who) == ENGLAND && from == ENGLAND)
+ return false;
+ if (block_owner(who) == SCOTLAND && to == ENGLAND)
+ return false;
+ if (can_block_use_border(who, from, to))
+ return true;
+ }
+ return false;
+}
+
+function can_block_regroup(who) {
+ if (block_owner(who) == game.active) {
+ let from = game.location[who];
+ for (let to of AREAS[from].exits)
+ if (can_block_regroup_to(who, to))
+ return true;
+ }
+ return false;
+}
+
+function is_battle_reserve(b) {
+ return game.reserves.includes(b);
+}
+
+function is_attacker(b) {
+ if (game.location[b] == game.where && block_owner(b) == game.attacker[game.where])
+ return !game.reserves.includes(b);
+ return false;
+}
+
+function is_defender(b) {
+ if (game.location[b] == game.where && block_owner(b) != game.attacker[game.where])
+ return !game.reserves.includes(b);
+ return false;
+}
+
+function swap_blocks(old) {
+ let bo = ENEMY[block_owner(old)];
+ let b = find_noble(bo, block_name(old));
+ game.location[b] = game.location[old];
+ game.steps[b] = game.steps[old];
+ game.location[old] = null;
+ game.steps[old] = block_max_steps(old);
+ return b;
+}
+
+function disband(who) {
+ game.location[who] = block_owner(who) == ENGLAND ? E_BAG : S_BAG;
+ game.steps[who] = block_max_steps(who);
+}
+
+function eliminate_block(who, reason) {
+ if (block_type(who) == 'nobles') {
+ if (reason == 'retreat') {
+ game.turn_log.push([game.location[who], "Captured"]);
+ } else if (reason == 'combat') {
+ game.flash = block_name(who) + " is captured.";
+ log(block_name(who) + " is captured.");
+ } else {
+ log(block_name(who) + " is captured.");
+ }
+ } else {
+ if (reason == 'retreat') {
+ game.turn_log.push([game.location[who], "Eliminated"]);
+ } else if (reason == 'combat') {
+ game.flash = block_name(who) + " is eliminated.";
+ log(block_name(who) + " is eliminated.");
+ } else {
+ if (block_owner(who) == ENGLAND)
+ log("English block is eliminated.");
+ else
+ log("Scottish block is eliminated.");
+ }
+ }
+
+ // TODO: clean up and check all combinations
+ if (who == EDWARD) {
+ if (reason == 'combat' || reason == 'retreat') {
+ if (game.edward == 1) {
+ game.edward = 2;
+ disband(who);
+ } else {
+ game.location[who] = null;
+ if (reason == 'combat') {
+ game.victory = "Scotland wins because king Edward II has died in battle!";
+ game.result = SCOTLAND;
+ }
+ }
+ } else {
+ disband(who);
+ }
+ } else if (who == KING) {
+ game.location[who] = null;
+ if (reason == 'combat') {
+ game.victory = "England wins because the Scottish king has died in battle!";
+ game.result = ENGLAND
+ }
+ } else if (block_is_mortal(who) && (reason == 'combat' || reason == 'retreat')) {
+ game.location[who] = null;
+ } else if (block_type(who) == 'nobles') {
+ who = swap_blocks(who);
+ game.steps[who] = 1; // flip at strength 1 if eliminated
+ if (reason == 'combat' || reason == 'retreat')
+ game.reserves.push(who);
+ } else {
+ disband(who);
+ }
+}
+
+function reduce_block(who, reason) {
+ if (game.steps[who] == 1) {
+ eliminate_block(who, reason);
+ } else {
+ --game.steps[who];
+ }
+}
+
+function count_attackers() {
+ let count = 0;
+ for (let b in BLOCKS)
+ if (is_attacker(b))
+ ++count;
+ return count;
+}
+
+function count_defenders() {
+ let count = 0;
+ for (let b in BLOCKS)
+ if (is_defender(b))
+ ++count;
+ return count;
+}
+
+const CELTIC_BLOCKS = [
+ "Ulster Infantry",
+ "Wales Archers",
+ "Wales Infantry",
+];
+
+function celtic_unity_roll(who) {
+ let die = roll_d6();
+ if (die >= 5) {
+ log(who + " roll " + DIE_MISS[die] + " for Celtic unity and return to the draw pool.");
+ disband(who);
+ } else {
+ log(who + " roll " + DIE_HIT[die] + " for Celtic unity \u2013 no effect.");
+ }
+}
+
+// SETUP
+
+function reset_blocks() {
+ for (let b in BLOCKS) {
+ game.steps[b] = block_max_steps(b);
+ if (block_type(b) == 'nobles')
+ game.location[b] = null;
+ else if (block_owner(b) == ENGLAND)
+ game.location[b] = E_BAG;
+ else
+ game.location[b] = S_BAG;
+ }
+}
+
+function deploy_noble(owner, area, name) {
+ if (name in BLOCKS) {
+ game.location[name] = area;
+ } else {
+ let friend = find_noble(owner, name);
+ let enemy = find_noble(ENEMY[owner], name);
+ game.location[friend] = area;
+ game.location[enemy] = null;
+ }
+}
+
+function deploy_block(area, block) {
+ game.location[block] = area;
+}
+
+function draw_from_bag(bag, exclude_list) {
+ let list = [];
+ for (let b in BLOCKS) {
+ if (exclude_list && exclude_list.includes(b))
+ continue;
+ if (game.location[b] == bag)
+ list.push(b);
+ }
+ return list[Math.floor(Math.random() * list.length)];
+}
+
+function deploy_english(count) {
+ let list = [];
+ for (let b in BLOCKS)
+ if (game.location[b] == E_BAG)
+ list.push(b);
+ for (let i = 0; i < count; ++i) {
+ let x = Math.floor(Math.random() * list.length);
+ let b = list[x];
+ list.splice(x,1);
+ game.location[b] = ENGLAND;
+ game.steps[b] = block_max_steps(b);
+ }
+}
+
+function deploy_off_map(block) {
+ game.location[block] = null;
+}
+
+function setup_braveheart() {
+ reset_blocks();
+
+ deploy_noble("England", "Badenoch", "Comyn");
+ deploy_noble("England", "Angus", "Angus");
+ deploy_noble("England", "Argyll", "Argyll");
+ deploy_noble("England", "Mar", "Mar");
+ deploy_noble("England", "Lennox", "Lennox");
+ deploy_noble("England", "Buchan", "Buchan");
+ deploy_noble("England", "Ross", "Ross");
+ deploy_noble("England", "Atholl", "Atholl");
+ deploy_noble("England", "Dunbar", "Dunbar");
+ deploy_noble("England", "Mentieth", "Mentieth");
+ deploy_noble("England", "Lanark", "Steward");
+
+ deploy_block("Lothian", "Cumbria Infantry");
+ deploy_block("Mentieth", "Northumber Infantry");
+
+ deploy_english(4);
+
+ deploy_noble("Scotland", "Annan", "Bruce");
+ deploy_noble("Scotland", "Galloway", "Galloway");
+
+ deploy_block("Fife", "Wallace");
+ deploy_block("Fife", "Douglas");
+ deploy_block("Fife", "Barclay");
+ deploy_block("Moray", "Moray");
+ deploy_block("Moray", "Fraser");
+ deploy_block("Strathspey", "Grant");
+
+ deploy_off_map("King");
+ deploy_off_map("French Knights");
+
+ game.scottish_king = false;
+ game.edward = 1;
+ game.year = 1297;
+ game.end_year = 1305;
+}
+
+function setup_the_bruce() {
+ reset_blocks();
+
+ deploy_noble("England", "Badenoch", "Comyn");
+ deploy_noble("England", "Angus", "Angus");
+ deploy_noble("England", "Argyll", "Argyll");
+ deploy_noble("England", "Buchan", "Buchan");
+ deploy_noble("England", "Galloway", "Galloway");
+ deploy_noble("England", "Ross", "Ross");
+ deploy_noble("England", "Mentieth", "Mentieth");
+ deploy_noble("England", "Lanark", "Steward");
+
+ deploy_block("Moray", "Cumbria Infantry");
+ deploy_block("Mentieth", "Northumber Infantry");
+ deploy_block("Lothian", "Durham Infantry");
+ deploy_block("Lanark", "Westmor Infantry");
+
+ deploy_english(6);
+
+ deploy_noble("Scotland", "Dunbar", "Dunbar");
+ deploy_noble("Scotland", "Lennox", "Lennox");
+ deploy_noble("Scotland", "Atholl", "Atholl");
+ deploy_noble("Scotland", "Mar", "Mar");
+ deploy_noble("Scotland", "Carrick", "Bruce");
+
+ deploy_block("Fife", "King");
+ deploy_block("Fife", "Douglas");
+ deploy_block("Fife", "Barclay");
+ deploy_block("Lennox", "Campbell");
+ deploy_block("Carrick", "Lindsay");
+
+ deploy_off_map("Moray");
+ deploy_off_map("Wallace");
+ deploy_off_map("French Knights");
+
+ game.scottish_king = true;
+ game.edward = 1;
+ game.year = 1306;
+ game.end_year = 1314;
+}
+
+function setup_campaign() {
+ setup_braveheart();
+ game.end_year = 1400; /* no limit */
+}
+
+// GAME TURN
+
+function start_year() {
+ log("");
+ log("Start Year " + game.year + ".");
+
+ // Deal new cards
+ let deck = shuffle_deck();
+ game.e_hand = deal_cards(deck, 5);
+ game.s_hand = deal_cards(deck, 5);
+
+ start_game_turn();
+}
+
+function start_game_turn() {
+ let turn = 6 - game.e_hand.length;
+ log("");
+ log("Start Turn " + turn + " of Year " + game.year + ".");
+
+ // Reset movement and attack tracking state
+ game.truce = false;
+ reset_border_limits();
+ game.last_used = {};
+ game.attacker = {};
+ game.reserves = [];
+ game.moved = {};
+
+ goto_card_phase();
+}
+
+function end_game_turn() {
+ if (count_english_nobles() == 0) {
+ game.victory = "Scotland wins by controlling all the nobles!";
+ game.result = SCOTLAND;
+ }
+ if (count_scottish_nobles() == 0) {
+ game.victory = "England wins by controlling all the nobles!";
+ game.result = ENGLAND;
+ }
+ if (game.victory)
+ return goto_game_over();
+
+ if (game.e_hand.length > 0)
+ start_game_turn()
+ else
+ goto_winter_turn();
+}
+
+// CARD PHASE
+
+function goto_card_phase() {
+ game.e_card = 0;
+ game.s_card = 0;
+ game.show_cards = false;
+ game.state = 'play_card';
+ game.active = BOTH;
+}
+
+function resume_play_card() {
+ if (game.s_card > 0 && game.e_card > 0)
+ reveal_cards();
+ else if (game.s_card > 0)
+ game.active = ENGLAND;
+ else if (game.e_card > 0)
+ game.active = SCOTLAND;
+ else
+ game.active = BOTH;
+}
+
+states.play_card = {
+ prompt: function (view, current) {
+ if (current == OBSERVER)
+ return view.prompt = "Waiting for players to play a card.";
+ if (current == ENGLAND) {
+ if (game.e_card) {
+ view.prompt = "Waiting for Scotland to play a card.";
+ gen_action(view, 'undo');
+ } else {
+ view.prompt = "Play a card.";
+ for (let c of game.e_hand)
+ gen_action(view, 'play', c);
+ }
+ }
+ if (current == SCOTLAND) {
+ if (game.s_card) {
+ view.prompt = "Waiting for England to play a card.";
+ gen_action(view, 'undo');
+ } else {
+ view.prompt = "Play a card.";
+ for (let c of game.s_hand)
+ gen_action(view, 'play', c);
+ }
+ }
+ },
+ play: function (card, current) {
+ if (current == ENGLAND) {
+ remove_from_array(game.e_hand, card);
+ game.e_card = card;
+ }
+ if (current == SCOTLAND) {
+ remove_from_array(game.s_hand, card);
+ game.s_card = card;
+ }
+ resume_play_card();
+ },
+ undo: function (_, current) {
+ if (current == ENGLAND) {
+ game.e_hand.push(game.e_card);
+ game.e_card = 0;
+ }
+ if (current == SCOTLAND) {
+ game.s_hand.push(game.s_card);
+ game.s_card = 0;
+ }
+ resume_play_card();
+ }
+}
+
+function reveal_cards() {
+ log("England plays " + CARDS[game.e_card].name + ".");
+ log("Scotland plays " + CARDS[game.s_card].name + ".");
+ game.show_cards = true;
+
+ let ec = CARDS[game.e_card];
+ let sc = CARDS[game.s_card];
+
+ if (ec.event && sc.event) {
+ log("Two events played at the same time. The year will end after this turn.");
+ game.e_hand.length = 0;
+ game.s_hand.length = 0;
+ }
+
+ if (ec.event) {
+ game.p1 = ENGLAND;
+ game.p2 = SCOTLAND;
+ } else if (sc.event) {
+ game.p1 = SCOTLAND;
+ game.p2 = ENGLAND;
+ } else if (sc.moves > ec.moves) {
+ game.p1 = SCOTLAND;
+ game.p2 = ENGLAND;
+ } else {
+ game.p1 = ENGLAND;
+ game.p2 = SCOTLAND;
+ }
+
+ game.active = game.p1;
+ start_player_turn();
+}
+
+function start_player_turn() {
+ log("");
+ log("Start " + game.active + " turn.");
+ reset_border_limits();
+ let ec = CARDS[game.e_card];
+ let sc = CARDS[game.s_card];
+ if (game.active == ENGLAND && ec.event)
+ goto_event(ec.event);
+ else if (game.active == SCOTLAND && sc.event)
+ goto_event(sc.event);
+ else if (game.active == ENGLAND)
+ goto_move_phase(ec.moves);
+ else if (game.active == SCOTLAND)
+ goto_move_phase(sc.moves);
+}
+
+function end_player_turn() {
+ game.moves = 0;
+ game.activated = null;
+ game.main_origin = null;
+ game.main_border = null;
+
+ if (game.active == game.p2) {
+ goto_battle_phase();
+ } else {
+ game.active = game.p2;
+ start_player_turn();
+ }
+}
+
+// CORONATION
+
+function can_crown_bruce() {
+ return game.location[WALLACE] == null && game.location[S_BRUCE] == "Fife";
+}
+
+function can_crown_comyn() {
+ return game.location[WALLACE] == null && game.location[S_COMYN] == "Fife";
+}
+
+function can_crown_balliol() {
+ return game.year >= 1301 && is_on_map(FRENCH_KNIGHTS);
+}
+
+function goto_event(event) {
+ if (game.active == SCOTLAND && !game.scottish_king &&
+ (can_crown_bruce() || can_crown_comyn() || can_crown_balliol())) {
+ game.state = 'coronation_event';
+ game.event = event;
+ } else {
+ goto_event_card(event);
+ }
+}
+
+states.coronation_event = {
+ prompt: function (view, current) {
+ if (is_inactive_player(current))
+ return view.prompt = "Waiting for " + game.active + " to crown a king.";
+ view.prompt = "Play event or crown a king?";
+ gen_action(view, 'play_event');
+ if (can_crown_bruce())
+ gen_action(view, 'crown_bruce');
+ if (can_crown_comyn())
+ gen_action(view, 'crown_comyn');
+ if (can_crown_balliol())
+ gen_action(view, 'return_of_the_king');
+ },
+ crown_bruce: function () {
+ log("Bruce is crowned King!");
+ game.scottish_king = true;
+ game.location[KING] = "Fife";
+ game.steps[KING] = block_max_steps(KING);
+ defect_comyn_nobles();
+ },
+ crown_comyn: function () {
+ log("Comyn is crowned King!");
+ game.scottish_king = true;
+ game.location[KING] = "Fife";
+ game.steps[KING] = block_max_steps(KING);
+ defect_bruce_nobles();
+ },
+ return_of_the_king: function () {
+ log("Return of the King!");
+ game.scottish_king = true;
+ game.location[KING] = game.location[FRENCH_KNIGHTS];
+ game.steps[KING] = block_max_steps(KING);
+ defect_bruce_nobles();
+ },
+ play_event: function () {
+ let event = game.event;
+ delete game.event;
+ goto_event_card(event);
+ },
+}
+
+function defect_bruce_nobles() {
+ defect_nobles([ "Bruce", "Mar", "Lennox", "Atholl", "Dunbar", "Mentieth", "Steward" ]);
+}
+
+function defect_comyn_nobles() {
+ defect_nobles([ "Comyn", "Angus", "Argyll", "Buchan", "Galloway", "Ross" ]);
+}
+
+function defect_nobles(list) {
+ for (let name of list) {
+ let who = find_noble(game.active, name);
+ if (is_on_map(who)) {
+ let where = game.location[who];
+ log(name + " defects.");
+ who = swap_blocks(who);
+ if (is_contested_area(where))
+ game.attacker[where] = block_owner(who);
+ }
+ }
+ resume_coronation();
+}
+
+function resume_coronation() {
+ if (have_contested_areas()) {
+ game.active = game.p1;
+ game.state = 'coronation_battles';
+ } else {
+ game.active = SCOTLAND;
+ end_player_turn();
+ }
+}
+
+states.coronation_battles = {
+ prompt: function (view, current) {
+ if (is_inactive_player(current))
+ return view.prompt = "Waiting for " + game.active + " to choose a battle.";
+ view.prompt = "Coronation: Choose the next battle to fight!";
+ for (let where in AREAS)
+ if (is_contested_area(where))
+ gen_action(view, 'area', where);
+ },
+ area: function (where) {
+ start_battle(where, 'coronation');
+ },
+}
+
+// EVENTS
+
+function goto_event_card(event) {
+ switch (event) {
+ case 'herald': goto_herald(); break;
+ case 'pillage': goto_pillage(); break;
+ case 'sea_move': goto_sea_move(); break;
+ case 'truce': goto_truce(); break;
+ case 'victuals': goto_victuals(); break;
+ }
+}
+
+function goto_truce() {
+ log("Truce is in effect!");
+ game.truce = ENEMY[game.active];
+ end_player_turn();
+}
+
+function goto_herald() {
+ game.state = 'herald';
+}
+
+function is_enemy_noble(who) {
+ return is_on_map(who) && block_type(who) == 'nobles' && block_owner(who) == ENEMY[game.active];
+}
+
+states.herald = {
+ prompt: function (view, current) {
+ if (is_inactive_player(current))
+ return view.prompt = "Waiting for " + game.active + " to choose a noble.";
+ view.prompt = "Herald: Name an enemy noble to try to convert to your side.";
+ gen_action(view, 'pass');
+ for (let b in BLOCKS)
+ if (is_enemy_noble(b))
+ gen_action(view, 'noble', block_name(b));
+ },
+ noble: function (name) {
+ let who = find_noble(ENEMY[game.active], name);
+ let die = roll_d6();
+ if (die <= 4) {
+ log("Herald roll " + DIE_HIT[die] + " converts " + name + ".");
+ let where = game.location[who];
+ who = swap_blocks(who);
+ if (is_contested_area(where)) {
+ game.attacker[where] = game.active;
+ start_battle(where, 'herald');
+ return;
+ }
+ } else {
+ log("Herald roll " + DIE_MISS[die] + " fails to convert " + name + ".");
+ }
+ end_player_turn();
+ },
+ pass: function () {
+ end_player_turn();
+ },
+}
+
+function goto_victuals() {
+ game.victuals = 3;
+ game.where = null;
+ game.state = 'victuals';
+ game.turn_log = [];
+ clear_undo();
+}
+
+states.victuals = {
+ prompt: function (view, current) {
+ if (is_inactive_player(current))
+ return view.prompt = "Waiting for " + game.active + " to build.";
+ view.prompt = "Victuals: Distribute three steps among friendly blocks in one group.";
+ gen_action_undo(view);
+ gen_action(view, 'end_builds');
+ if (game.victuals > 0) {
+ for (let b in BLOCKS) {
+ if (is_on_map(b) && block_owner(b) == game.active)
+ if (game.steps[b] < block_max_steps(b))
+ if (!game.where || game.location[b] == game.where)
+ gen_action(view, 'block', b);
+ }
+ }
+ },
+ block: function (who) {
+ push_undo();
+ game.where = game.location[who];
+ game.turn_log.push([game.where]);
+ ++game.steps[who];
+ --game.victuals;
+ },
+ end_builds: function () {
+ print_turn_log("victuals");
+ clear_undo();
+ delete game.victuals;
+ game.where = null;
+ end_player_turn();
+ },
+ undo: pop_undo
+}
+
+function goto_pillage() {
+ game.state = 'pillage';
+ game.turn_log = [];
+}
+
+states.pillage = {
+ prompt: function (view, current) {
+ if (is_inactive_player(current))
+ return view.prompt = "Waiting for " + game.active + " to pillage.";
+ view.prompt = "Pillage: Pillage one enemy group adjacent to a friendly group.";
+ gen_action(view, 'pass');
+ for (let from in AREAS) {
+ if (is_friendly_area(from)) {
+ for (let to of AREAS[from].exits)
+ if (is_contested_area(to) || is_enemy_area(to))
+ gen_action(view, 'area', to);
+ }
+ }
+ },
+ area: function (where) {
+ game.where = where;
+ game.pillage = 2;
+ game.active = ENEMY[game.active];
+ game.state = 'pillage_hits';
+ },
+ pass: function () {
+ end_player_turn();
+ },
+}
+
+function pillage_victims() {
+ function is_candidate(b) {
+ return block_owner(b) == game.active && game.location[b] == game.where;
+ }
+ let max = 0;
+ for (let b in BLOCKS)
+ if (is_candidate(b) && game.steps[b] > max)
+ max = game.steps[b];
+ let list = [];
+ for (let b in BLOCKS)
+ if (is_candidate(b) && game.steps[b] == max)
+ list.push(b);
+ return list;
+}
+
+states.pillage_hits = {
+ prompt: function (view, current) {
+ if (is_inactive_player(current))
+ return view.prompt = "Waiting for " + game.active + " to apply pillage hits.";
+ view.prompt = "Pillage: Apply two hits in " + game.where + ".";
+ for (let b of pillage_victims())
+ gen_action(view, 'block', b);
+ },
+ block: function (who) {
+ --game.pillage;
+ reduce_block(who, 'pillage');
+ if (game.pillage == 0 || pillage_victims().length == 0) {
+ game.active = ENEMY[game.active];
+ game.state = 'pillage_builds';
+ game.pillage = 2 - game.pillage;
+ game.from = game.where;
+ game.where = null;
+ }
+ },
+}
+
+states.pillage_builds = {
+ prompt: function (view, current) {
+ if (is_inactive_player(current))
+ return view.prompt = "Waiting for " + game.active + " to apply pillage builds.";
+ view.prompt = "Pillage: Add pillaged steps to friendly blocks in the pillaging group.";
+ gen_action_undo(view);
+ gen_action(view, 'end_pillage');
+ if (game.pillage > 0) {
+ if (game.where) {
+ for (let b in BLOCKS)
+ if (block_owner(b) == game.active && game.location[b] == game.where)
+ if (game.steps[b] < block_max_steps(b))
+ gen_action(view, 'block', b);
+ } else {
+ for (let to of AREAS[game.from].exits)
+ for (let b in BLOCKS)
+ if (block_owner(b) == game.active && game.location[b] == to)
+ if (game.steps[b] < block_max_steps(b))
+ gen_action(view, 'block', b);
+ }
+ }
+ },
+ block: function (who) {
+ push_undo();
+ game.where = game.location[who];
+ game.turn_log.push([game.from, game.where]);
+ ++game.steps[who];
+ --game.pillage;
+ // TODO: auto-end pillage builds?
+ // if (game.pillage == 0) end_pillage(game.from);
+ },
+ end_pillage: function () {
+ while (game.pillage > 0) {
+ --game.pillage;
+ game.turn_log.push([game.from]);
+ }
+ end_pillage(game.from);
+ },
+ undo: pop_undo
+}
+
+function end_pillage(where) {
+ print_turn_log("pillages");
+ game.from = null;
+ game.where = null;
+ delete game.pillage;
+ if (is_contested_area(where)) {
+ game.attacker[where] = ENEMY[game.active];
+ start_battle(where, 'pillage');
+ } else {
+ end_player_turn();
+ }
+}
+
+function goto_sea_move() {
+ game.moves = 2;
+ game.from = null;
+ game.where = null;
+ game.state = 'sea_move';
+ game.turn_log = [];
+ clear_undo();
+}
+
+states.sea_move = {
+ prompt: function (view, current) {
+ if (is_inactive_player(current))
+ return view.prompt = "Waiting for " + game.active + " to sea move.";
+ view.prompt = "Sea Move: Move one or two blocks from one coastal area to one other friendly coastal area.";
+ gen_action_undo(view);
+ gen_action(view, 'end_move_phase');
+ if (game.moves > 0) {
+ for (let b in BLOCKS) {
+ if (b == NORSE)
+ continue;
+ if (is_in_friendly_coastal_area(b) && block_owner(b) == game.active)
+ if (!game.from || game.location[b] == game.from)
+ gen_action(view, 'block', b);
+ }
+ }
+ },
+ block: function (who) {
+ push_undo();
+ game.who = who;
+ game.state = 'sea_move_to';
+ },
+ end_move_phase: function () {
+ print_turn_log("sea moves");
+ clear_undo();
+ game.moves = 0;
+ game.from = null;
+ game.where = null;
+ end_player_turn();
+ },
+ undo: pop_undo
+}
+
+states.sea_move_to = {
+ prompt: function (view, current) {
+ if (is_inactive_player(current))
+ return view.prompt = "Waiting for " + game.active + " to sea move.";
+ view.prompt = "Sea Move: Move one or two blocks from one coastal area to one other friendly coastal area.";
+ gen_action_undo(view);
+ gen_action(view, 'block', game.who);
+ if (game.where) {
+ gen_action(view, 'area', game.where);
+ } else {
+ let from = game.location[game.who];
+ for (let to in AREAS)
+ if (to != from && is_friendly_coastal_area(to))
+ gen_action(view, 'area', to);
+ }
+ },
+ area: function (to) {
+ if (!game.from)
+ game.from = game.location[game.who];
+ game.turn_log.push([game.from, to]);
+ game.location[game.who] = to
+ game.moved[game.who] = true;
+ game.where = to;
+ game.who = null;
+ --game.moves;
+ game.state = 'sea_move';
+ },
+ block: pop_undo,
+ undo: pop_undo
+}
+
+// MOVE PHASE
+
+function goto_move_phase(moves) {
+ game.state = 'move_who';
+ game.moves = moves;
+ game.activated = [];
+ game.main_origin = {};
+ game.main_border = {};
+ game.turn_log = [];
+ clear_undo();
+}
+
+states.move_who = {
+ prompt: function (view, current) {
+ if (is_inactive_player(current))
+ return view.prompt = "Waiting for " + game.active + " to move.";
+ view.prompt = "Choose an army to move. " + game.moves + "MP left.";
+ gen_action_undo(view);
+ gen_action(view, 'end_move_phase');
+ for (let b in BLOCKS) {
+ if (b == NORSE && game.active == SCOTLAND && is_on_map(NORSE)) {
+ if (!game.moved[b] && game.moves > 0 && !is_pinned(game.location[NORSE]))
+ gen_action(view, 'block', NORSE);
+ }
+ if (can_block_move(b)) {
+ if (game.moves == 0) {
+ let from = game.location[b];
+ if (game.activated.includes(from))
+ gen_action(view, 'block', b);
+ } else {
+ gen_action(view, 'block', b);
+ }
+ }
+ }
+ },
+ block: function (who) {
+ push_undo();
+ game.who = who;
+ game.state = 'move_where';
+ game.origin = game.location[who];
+ game.last_from = null;
+ game.distance = 0;
+ },
+ end_move_phase: function () {
+ clear_undo();
+ game.moves = 0;
+ print_turn_log("moves");
+ end_player_turn();
+ },
+ undo: pop_undo
+}
+
+function move_block(who, from, to) {
+ game.location[who] = to;
+ game.border_limit[border_id(from, to)] = border_limit(from, to) + 1;
+ game.distance ++;
+ if (is_contested_area(to)) {
+ game.last_used[border_id(from, to)] = game.active;
+ if (!game.attacker[to]) {
+ game.attacker[to] = game.active;
+ game.main_border[to] = from;
+ game.main_origin[to] = game.origin;
+ return ATTACK_MARK;
+ } else {
+ if (game.attacker[to] != game.active || game.main_border[to] != from || game.main_origin[to] != game.origin) {
+ game.reserves.push(who);
+ return RESERVE_MARK;
+ } else {
+ return ATTACK_MARK;
+ }
+ }
+ }
+ return false;
+}
+
+states.move_where = {
+ prompt: function (view, current) {
+ if (is_inactive_player(current))
+ return view.prompt = "Waiting for " + game.active + " to move.";
+ view.prompt = "Move " + block_name(game.who) + "."
+ gen_action_undo(view);
+ gen_action(view, 'block', game.who);
+ let from = game.location[game.who];
+ if (game.who == NORSE) {
+ for (let to in AREAS)
+ if (to != from && to != ENGLAND && is_coastal_area(to))
+ if (game.truce != game.active || !is_enemy_area(to))
+ gen_action(view, 'area', to);
+ } else {
+ if (game.distance > 0)
+ gen_action(view, 'area', from);
+ for (let to of AREAS[from].exits) {
+ if (to != game.last_from && can_block_move_to(game.who, from, to))
+ gen_action(view, 'area', to);
+ }
+ }
+ },
+ block: function (who) {
+ if (game.distance == 0)
+ pop_undo();
+ else
+ end_move();
+ },
+ area: function (to) {
+ let from = game.location[game.who];
+ if (to == from) {
+ end_move();
+ return;
+ }
+ if (game.who == NORSE) {
+ log("The Norse move by sea.");
+ game.location[game.who] = to;
+ game.moved[game.who] = true;
+ if (is_contested_area(to)) {
+ if (!game.attacker[to]) {
+ game.turn_log.push([from, to + ATTACK_MARK + " (Norse)"]);
+ game.attacker[to] = game.active;
+ } else {
+ game.turn_log.push([from, to + RESERVE_MARK + " (Norse)"]);
+ game.reserves.push(game.who);
+ }
+ } else {
+ game.turn_log.push([from, to + " (Norse)"]);
+ }
+ --game.moves;
+ game.who = null;
+ game.state = 'move_who';
+ } else {
+ if (game.distance == 0)
+ game.move_buf = [ from ];
+ let mark = move_block(game.who, from, to);
+ if (mark)
+ game.move_buf.push(to + mark);
+ else
+ game.move_buf.push(to);
+ game.last_from = from;
+ if (!can_block_continue(game.who, from, to))
+ end_move();
+ }
+ },
+ undo: pop_undo
+}
+
+function end_move() {
+ if (game.distance > 0) {
+ let to = game.location[game.who];
+ if (game.origin == ENGLAND || to == ENGLAND) {
+ log(game.active + " crosses the Anglo-Scottish border.");
+ game.moves --;
+ } else if (!game.activated.includes(game.origin)) {
+ log(game.active + " activates " + game.origin + ".");
+ game.activated.push(game.origin);
+ game.moves --;
+ }
+ game.moved[game.who] = true;
+ game.turn_log.push(game.move_buf);
+ }
+ delete game.move_buf;
+ game.who = null;
+ game.distance = 0;
+ game.origin = null;
+ game.last_from = null;
+ game.state = 'move_who';
+}
+
+// BATTLE PHASE
+
+function goto_battle_phase() {
+ if (have_contested_areas()) {
+ game.active = game.p1;
+ game.state = 'battle_phase';
+ } else {
+ goto_border_raids();
+ }
+}
+
+states.battle_phase = {
+ prompt: function (view, current) {
+ if (is_inactive_player(current))
+ return view.prompt = "Waiting for " + game.active + " to choose a battle.";
+ view.prompt = "Choose the next battle to fight!";
+ for (let where in AREAS)
+ if (is_contested_area(where))
+ gen_action(view, 'area', where);
+ },
+ area: function (where) {
+ start_battle(where, 'battle');
+ },
+}
+
+function start_battle(where, reason) {
+ game.battle_active = game.active;
+ game.battle_reason = reason;
+ game.flash = "";
+ log("");
+ if (reason != 'battle')
+ log("Defection battle in " + where + ".");
+ else
+ log("Battle in " + where + ".");
+ game.where = where;
+ game.battle_round = 0;
+ game.state = 'battle_round';
+ start_battle_round();
+}
+
+function resume_battle() {
+ if (game.victory)
+ return goto_game_over();
+ game.state = 'battle_round';
+ pump_battle_round();
+}
+
+function end_battle() {
+ game.flash = "";
+ game.battle_round = 0;
+ reset_border_limits();
+ game.moved = {};
+
+ game.active = game.attacker[game.where];
+ if (is_contested_area(game.where))
+ log("~ " + ENEMY[game.active] + " wins the battle ~");
+ else if (is_enemy_area(game.where))
+ log("~ " + ENEMY[game.active] + " wins the battle ~");
+ else
+ log("~ " + game.active + " wins the battle ~");
+
+ goto_retreat();
+}
+
+function bring_on_reserves() {
+ for (let b in BLOCKS)
+ if (game.location[b] == game.where)
+ remove_from_array(game.reserves, b);
+}
+
+function start_battle_round() {
+ if (++game.battle_round <= 3) {
+ log("~ Battle round " + game.battle_round + " ~");
+
+ reset_border_limits();
+ game.moved = {};
+
+ if (game.battle_round == 1) {
+ for (let b of CELTIC_BLOCKS)
+ if (game.location[b] == game.where && !is_battle_reserve(b))
+ celtic_unity_roll(b);
+ }
+ if (game.battle_round == 2) {
+ if (count_defenders() == 0) {
+ log("Defending main force was eliminated. The attacker is now the defender.");
+ game.attacker[game.where] = ENEMY[game.attacker[game.where]];
+ } else if (count_attackers() == 0) {
+ log("Attacking main force was eliminated.");
+ }
+ for (let b of CELTIC_BLOCKS)
+ if (game.location[b] == game.where && is_battle_reserve(b))
+ celtic_unity_roll(b);
+ bring_on_reserves();
+ }
+ if (game.battle_round == 3) {
+ bring_on_reserves();
+ }
+
+ pump_battle_round();
+ } else {
+ end_battle();
+ }
+}
+
+function pump_battle_round() {
+ function filter_battle_blocks(ci, is_candidate) {
+ let output = null;
+ for (let b in BLOCKS) {
+ if (is_candidate(b) && !game.moved[b]) {
+ if (block_initiative(b) == ci) {
+ if (!output)
+ output = [];
+ output.push(b);
+ }
+ }
+ }
+ return output;
+ }
+
+ function battle_step(active, initiative, candidate) {
+ game.battle_list = filter_battle_blocks(initiative, candidate);
+ if (game.battle_list) {
+ game.active = active;
+ return true;
+ }
+ return false;
+ }
+
+ if (is_friendly_area(game.where) || is_enemy_area(game.where)) {
+ end_battle();
+ } else if (count_attackers() == 0 || count_defenders() == 0) {
+ start_battle_round();
+ } else {
+ let attacker = game.attacker[game.where];
+ let defender = ENEMY[attacker];
+
+ if (battle_step(defender, 'A', is_defender)) return;
+ if (battle_step(attacker, 'A', is_attacker)) return;
+ if (battle_step(defender, 'B', is_defender)) return;
+ if (battle_step(attacker, 'B', is_attacker)) return;
+ if (battle_step(defender, 'C', is_defender)) return;
+ if (battle_step(attacker, 'C', is_attacker)) return;
+
+ start_battle_round();
+ }
+}
+
+function pass_with_block(b) {
+ game.flash = block_name(b) + " passes.";
+ log(block_name(b) + " passes.");
+ game.moved[b] = true;
+ resume_battle();
+}
+
+function retreat_with_block(b) {
+ game.who = b;
+ game.state = 'retreat_in_battle';
+}
+
+function fire_with_block(b) {
+ game.moved[b] = true;
+ game.hits = 0;
+ let steps = game.steps[b];
+ let fire = block_fire_power(b, game.where);
+ let printed_fire = block_printed_fire_power(b);
+ let rolls = [];
+ for (let i = 0; i < steps; ++i) {
+ let die = roll_d6();
+ if (die <= fire) {
+ rolls.push(DIE_HIT[die]);
+ ++game.hits;
+ } else {
+ rolls.push(DIE_MISS[die]);
+ }
+ }
+ game.flash = block_name(b) + " " + BLOCKS[b].combat;
+ if (fire > printed_fire)
+ game.flash += "+" + (fire - printed_fire);
+ game.flash += "\nfires " + rolls.join(" ") + "\n";
+ if (game.hits == 0)
+ game.flash += "and misses.";
+ else if (game.hits == 1)
+ game.flash += "and scores 1 hit.";
+ else
+ game.flash += "and scores " + game.hits + " hits.";
+ log(game.flash);
+ if (game.hits > 0) {
+ game.active = ENEMY[game.active];
+ goto_battle_hits();
+ } else {
+ resume_battle();
+ }
+}
+
+states.battle_round = {
+ show_battle: true,
+ prompt: function (view, current) {
+ if (is_inactive_player(current))
+ return view.prompt = "Waiting for " + game.active + " to choose a combat action.";
+ view.prompt = "Fire, retreat, or pass with an army.";
+ for (let b of game.battle_list) {
+ gen_action(view, 'block', b);
+ gen_action(view, 'battle_fire', b);
+ gen_action(view, 'battle_pass', b);
+ if (can_block_retreat(b))
+ gen_action(view, 'battle_retreat', b);
+ }
+ },
+ block: function (who) {
+ fire_with_block(who);
+ },
+ battle_fire: function (who) {
+ fire_with_block(who);
+ },
+ battle_retreat: function (who) {
+ retreat_with_block(who);
+ },
+ battle_pass: function (who) {
+ pass_with_block(who);
+ }
+}
+
+function goto_battle_hits() {
+ game.battle_list = list_victims(game.active);
+ if (game.battle_list.length == 0)
+ resume_battle();
+ else
+ game.state = 'battle_hits';
+}
+
+function apply_hit(who) {
+ game.flash = block_name(who) + " takes a hit.";
+ reduce_block(who, 'combat');
+ game.hits--;
+ if (game.victory)
+ goto_game_over();
+ else if (game.hits == 0)
+ resume_battle();
+ else {
+ game.battle_list = list_victims(game.active);
+ if (game.battle_list.length == 0)
+ resume_battle();
+ else
+ game.flash += " " + game.hits + (game.hits == 1 ? " hit left." : " hits left.");
+ }
+}
+
+function list_victims(p) {
+ let is_candidate = (p == game.attacker[game.where]) ? is_attacker : is_defender;
+ let max = 0;
+ for (let b in BLOCKS)
+ if (is_candidate(b) && game.steps[b] > max)
+ max = game.steps[b];
+ let list = [];
+ for (let b in BLOCKS)
+ if (is_candidate(b) && game.steps[b] == max)
+ list.push(b);
+ return list;
+}
+
+states.battle_hits = {
+ show_battle: true,
+ prompt: function (view, current) {
+ if (is_inactive_player(current))
+ return view.prompt = "Waiting for " + game.active + " to assign hits.";
+ view.prompt = "Assign " + game.hits + (game.hits != 1 ? " hits" : " hit") + " to your armies.";
+ for (let b of game.battle_list) {
+ gen_action(view, 'block', b);
+ gen_action(view, 'battle_hit', b);
+ }
+ },
+ block: function (who) {
+ apply_hit(who);
+ },
+ battle_hit: function (who) {
+ apply_hit(who);
+ },
+}
+
+function goto_retreat() {
+ game.active = game.attacker[game.where];
+ if (is_contested_area(game.where)) {
+ game.state = 'retreat';
+ game.turn_log = [];
+ clear_undo();
+ } else {
+ goto_regroup();
+ }
+}
+
+states.retreat = {
+ prompt: function (view, current) {
+ if (is_inactive_player(current))
+ return view.prompt = "Waiting for " + game.active + " to retreat.";
+ view.prompt = "Retreat: Choose an army to move.";
+ gen_action_undo(view);
+ if (!is_contested_area(game.where))
+ gen_action(view, 'end_retreat');
+ for (let b in BLOCKS)
+ if (game.location[b] == game.where && can_block_retreat(b))
+ gen_action(view, 'block', b);
+ },
+ end_retreat: function () {
+ for (let b in BLOCKS)
+ if (game.location[b] == game.where && block_owner(b) == game.active)
+ eliminate_block(b, 'retreat');
+ print_turn_log("retreats");
+ clear_undo();
+ goto_regroup();
+ },
+ block: function (who) {
+ push_undo();
+ game.who = who;
+ game.state = 'retreat_to';
+ },
+ undo: pop_undo
+}
+
+states.retreat_to = {
+ prompt: function (view, current) {
+ if (is_inactive_player(current))
+ return view.prompt = "Waiting for " + game.active + " to retreat.";
+ gen_action_undo(view);
+ gen_action(view, 'block', game.who);
+ let can_retreat = false;
+ if (game.who == NORSE) {
+ view.prompt = "Retreat: Move the army to a friendly coastal area.";
+ for (let to in AREAS) {
+ if (to != game.where && to != ENGLAND && is_friendly_coastal_area(to)) {
+ gen_action(view, 'area', to);
+ can_retreat = true;
+ }
+ }
+ } else {
+ view.prompt = "Retreat: Move the army to a friendly or neutral area.";
+ for (let to of AREAS[game.where].exits) {
+ if (can_block_retreat_to(game.who, to)) {
+ gen_action(view, 'area', to);
+ can_retreat = true;
+ }
+ }
+ }
+ if (!can_retreat)
+ gen_action(view, 'eliminate');
+ },
+ area: function (to) {
+ let from = game.where;
+ if (game.who == NORSE) {
+ game.turn_log.push([from, to + " (Norse)"]);
+ game.location[game.who] = to;
+ } else {
+ game.turn_log.push([from, to]);
+ move_block(game.who, game.where, to);
+ }
+ game.who = null;
+ game.state = 'retreat';
+ },
+ eliminate: function () {
+ eliminate_block(game.who, 'retreat');
+ game.state = 'retreat';
+ },
+ block: pop_undo,
+ undo: pop_undo
+}
+
+states.retreat_in_battle = {
+ prompt: function (view, current) {
+ if (is_inactive_player(current))
+ return view.prompt = "Waiting for " + game.active + " to retreat.";
+ gen_action(view, 'undo');
+ if (game.who == NORSE) {
+ view.prompt = "Retreat: Move the army to a friendly coastal area.";
+ for (let to in AREAS)
+ if (to != game.where && to != ENGLAND && is_friendly_coastal_area(to))
+ gen_action(view, 'area', to);
+ } else {
+ view.prompt = "Retreat: Move the army to a friendly or neutral area.";
+ for (let to of AREAS[game.where].exits)
+ if (can_block_retreat_to(game.who, to))
+ gen_action(view, 'area', to);
+ }
+ },
+ area: function (to) {
+ if (game.who == NORSE) {
+ game.flash = "Norse retreat to " + to + ".";
+ log("The Norse retreat to " + to + ".");
+ game.location[game.who] = to;
+ } else {
+ game.flash = block_name(game.who) + " retreats.";
+ log(game.active + " retreats to " + to + ".");
+ move_block(game.who, game.where, to);
+ }
+ game.who = null;
+ resume_battle();
+ },
+ undo: function () {
+ game.who = null;
+ resume_battle();
+ }
+}
+
+function goto_regroup() {
+ game.active = game.attacker[game.where];
+ if (is_enemy_area(game.where))
+ game.active = ENEMY[game.active];
+ game.state = 'regroup';
+ game.turn_log = [];
+ clear_undo();
+}
+
+states.regroup = {
+ prompt: function (view, current) {
+ if (is_inactive_player(current))
+ return view.prompt = "Waiting for " + game.active + " to regroup.";
+ view.prompt = "Regroup: Choose an army to move.";
+ gen_action_undo(view);
+ gen_action(view, 'end_regroup');
+ for (let b in BLOCKS)
+ if (game.location[b] == game.where && can_block_regroup(b))
+ gen_action(view, 'block', b);
+ },
+ block: function (who) {
+ push_undo();
+ game.who = who;
+ game.state = 'regroup_to';
+ },
+ end_regroup: function () {
+ print_turn_log("regroups");
+ game.where = null;
+ clear_undo();
+ game.active = game.battle_active;
+ delete game.battle_active;
+ if (game.battle_reason == 'herald') {
+ delete game.battle_reason;
+ end_player_turn();
+ } else if (game.battle_reason == 'pillage') {
+ delete game.battle_reason;
+ end_player_turn();
+ } else if (game.battle_reason == 'coronation') {
+ delete game.battle_reason;
+ resume_coronation();
+ } else {
+ delete game.battle_reason;
+ goto_battle_phase();
+ }
+ },
+ undo: pop_undo
+}
+
+states.regroup_to = {
+ prompt: function (view, current) {
+ if (is_inactive_player(current))
+ return view.prompt = "Waiting for " + game.active + " to regroup.";
+ view.prompt = "Regroup: Move the army to a friendly or neutral area.";
+ gen_action_undo(view);
+ gen_action(view, 'block', game.who);
+ if (game.who == NORSE) {
+ for (let to in AREAS)
+ if (to != game.where && to != ENGLAND && is_friendly_coastal_area(to))
+ gen_action(view, 'area', to);
+ } else {
+ for (let to of AREAS[game.where].exits)
+ if (can_block_regroup_to(game.who, to))
+ gen_action(view, 'area', to);
+ }
+ },
+ area: function (to) {
+ let from = game.where;
+ if (game.who == NORSE) {
+ game.turn_log.push([from, to + " (Norse)"]);
+ game.location[game.who] = to;
+ } else {
+ game.turn_log.push([from, to]);
+ move_block(game.who, game.where, to);
+ }
+ game.who = null;
+ game.state = 'regroup';
+ },
+ block: pop_undo,
+ undo: pop_undo
+}
+
+// BORDER RAIDS
+
+function count_non_noble_english_blocks() {
+ let count = 0;
+ for (let b in BLOCKS)
+ if (block_owner(b) == ENGLAND && block_type(b) != 'nobles')
+ if (game.location[b])
+ ++count;
+ return count;
+}
+
+function goto_border_raids() {
+ game.active = ENGLAND;
+ if (is_enemy_area(ENGLAND)) {
+ log("Scotland border raids.");
+ if (count_non_noble_english_blocks() > 0) {
+ game.state = 'border_raids';
+ } else {
+ log("England has no non-noble blocks in play.");
+ }
+ } else {
+ end_game_turn();
+ }
+}
+
+states.border_raids = {
+ prompt: function (view, current) {
+ if (is_inactive_player(current))
+ return view.prompt = "Waiting for England to choose a border raid victim.";
+ view.prompt = "Border Raids: Eliminate a non-Noble block.";
+ for (let b in BLOCKS)
+ if (block_owner(b) == ENGLAND && block_type(b) != 'nobles')
+ if (game.location[b])
+ gen_action(view, 'block', b);
+ },
+ block: function (who) {
+ eliminate_block(who, 'border_raids')
+ end_game_turn();
+ },
+}
+
+// WINTERING
+
+function goto_winter_turn() {
+ game.moved = {};
+ log("");
+ log("Start Wintering.");
+ english_nobles_go_home();
+}
+
+function is_bruce(who) {
+ return who == E_BRUCE || who == S_BRUCE;
+}
+
+function is_comyn(who) {
+ return who == E_COMYN || who == S_COMYN;
+}
+
+function find_noble_home(who) {
+ for (let where in AREAS)
+ if (AREAS[where].home == block_name(who))
+ return where;
+ return null;
+}
+
+function go_home_to(who, home) {
+ let name = block_name(who);
+ let from = game.location[who];
+ if (from != home) {
+ game.location[who] = home;
+ if (is_contested_area(home)) {
+ game.turn_log.push([name, home + " \u2727"]);
+ who = swap_blocks(who);
+ } else {
+ game.turn_log.push([name, home]);
+ }
+ }
+}
+
+function go_home(who) {
+ go_home_to(who, find_noble_home(who));
+}
+
+function english_nobles_go_home() {
+ game.turn_log = [];
+ game.active = ENGLAND;
+ for (let b in BLOCKS) {
+ if (block_owner(b) == ENGLAND && block_type(b) == 'nobles' && game.location[b])
+ if (!is_bruce(b) && !is_comyn(b))
+ go_home(b);
+ }
+
+ game.going_home = ENGLAND;
+ game.bruce_home = false;
+ game.comyn_home = false;
+ goto_e_bruce();
+}
+
+function scottish_nobles_go_home() {
+ game.turn_log = [];
+ game.active = SCOTLAND;
+ for (let b in BLOCKS) {
+ if (block_owner(b) == SCOTLAND && block_type(b) == 'nobles' && game.location[b])
+ if (!is_bruce(b) && !is_comyn(b))
+ go_home(b);
+ }
+ game.going_home = SCOTLAND;
+ goto_s_bruce();
+}
+
+function goto_e_bruce() {
+ game.who = E_BRUCE;
+ if (game.location[E_BRUCE] && !game.bruce_home)
+ send_bruce_home();
+ else
+ end_bruce();
+}
+
+function goto_s_bruce() {
+ game.who = S_BRUCE;
+ if (game.location[S_BRUCE] && !game.bruce_home)
+ send_bruce_home();
+ else
+ end_bruce();
+}
+
+function send_bruce_home() {
+ game.bruce_home = true;
+ let annan = is_friendly_or_neutral_area("Annan");
+ let carrick = is_friendly_or_neutral_area("Carrick");
+ if (annan && !carrick) {
+ go_home_to(game.who, "Annan");
+ game.who = null;
+ return end_bruce();
+ }
+ if (carrick && !annan) {
+ go_home_to(game.who, "Carrick");
+ game.who = null;
+ return end_bruce();
+ }
+ if (!annan && !carrick) {
+ game.active = ENEMY[game.active];
+ game.who = swap_blocks(game.who);
+ }
+ game.state = 'bruce';
+}
+
+states.bruce = {
+ prompt: function (view, current) {
+ if (is_inactive_player(current))
+ return view.prompt = "Waiting for " + game.active + " to move Bruce to one of his home areas.";
+ view.prompt = "Nobles go Home: Move Bruce to one of his home areas.";
+ gen_action(view, 'area', "Annan");
+ gen_action(view, 'area', "Carrick");
+ },
+ area: function (to) {
+ go_home_to(game.who, to);
+ game.who = null;
+ end_bruce();
+ },
+}
+
+function end_bruce() {
+ game.who = null;
+ game.active = game.going_home;
+ if (game.going_home == ENGLAND)
+ goto_e_comyn();
+ else
+ goto_s_comyn();
+}
+
+function goto_e_comyn() {
+ game.who = E_COMYN;
+ if (game.location[E_COMYN] && !game.comyn_home)
+ send_comyn_home();
+ else
+ end_comyn();
+}
+
+function goto_s_comyn() {
+ game.who = S_COMYN;
+ if (game.location[S_COMYN] && !game.comyn_home)
+ send_comyn_home();
+ else
+ end_comyn();
+}
+
+function send_comyn_home() {
+ game.comyn_home = true;
+ let badenoch = is_friendly_or_neutral_area("Badenoch");
+ let lochaber = is_friendly_or_neutral_area("Lochaber");
+ if (badenoch && !lochaber) {
+ go_home_to(game.who, "Badenoch");
+ game.who = null;
+ return end_comyn();
+ }
+ if (lochaber && !badenoch) {
+ go_home_to(game.who, "Lochaber");
+ game.who = null;
+ return end_comyn();
+ }
+ if (!lochaber && !badenoch) {
+ game.active = ENEMY[game.active];
+ game.who = swap_blocks(game.who);
+ }
+ game.state = 'comyn';
+}
+
+states.comyn = {
+ prompt: function (view, current) {
+ if (is_inactive_player(current))
+ return view.prompt = "Waiting for " + game.active + " to move Comyn to one of his home areas.";
+ view.prompt = "Nobles go Home: Move Comyn to one of his home areas.";
+ gen_action(view, 'area', "Badenoch");
+ gen_action(view, 'area', "Lochaber");
+ },
+ area: function (to) {
+ go_home_to(game.who, to);
+ game.who = null;
+ end_comyn();
+ },
+}
+
+function end_comyn() {
+ game.who = null;
+ game.active = game.going_home;
+ if (game.active == ENGLAND) {
+ print_turn_log_no_count("English nobles go home:");
+ scottish_nobles_go_home();
+ } else {
+ goto_moray();
+ }
+}
+
+function goto_moray() {
+ delete game.going_home;
+ delete game.bruce_home;
+ delete game.comyn_home;
+
+ if (is_on_map(MORAY)) {
+ game.state = 'moray';
+ game.active = SCOTLAND;
+ game.who = MORAY;
+ } else {
+ goto_scottish_king();
+ }
+}
+
+states.moray = {
+ prompt: function (view, current) {
+ if (is_inactive_player(current))
+ return view.prompt = "Waiting for Scotland to move Moray.";
+ view.prompt = "Nobles go Home: Move Moray to his home area, remain where he is, or disband.";
+ gen_action(view, 'disband');
+ if (is_within_castle_limit(game.location[MORAY]))
+ gen_action(view, 'area', game.location[MORAY]);
+ if (is_friendly_or_neutral_area("Moray"))
+ gen_action(view, 'area', "Moray");
+ },
+ disband: function () {
+ game.turn_log.push(["Moray", "Pool"]);
+ disband(MORAY);
+ game.who = null;
+ goto_scottish_king();
+ },
+ area: function (to) {
+ let from = game.location[MORAY];
+ if (to != from)
+ game.turn_log.push(["Moray", to]);
+ game.location[MORAY] = to;
+ game.who = null;
+ goto_scottish_king();
+ },
+}
+
+function goto_scottish_king() {
+ print_turn_log_no_count("Scottish nobles go home:");
+
+ if (is_on_map(KING)) {
+ game.state = 'scottish_king';
+ game.active = SCOTLAND;
+ game.who = KING;
+ } else {
+ goto_edward_wintering();
+ }
+}
+
+states.scottish_king = {
+ prompt: function (view, current) {
+ if (is_inactive_player(current))
+ return view.prompt = "Waiting for Scotland to move the King.";
+ view.prompt = "Scottish King: Move the King to a cathedral, remain where he is, or disband.";
+ gen_action(view, 'disband');
+ if (is_within_castle_limit(game.location[KING]))
+ gen_action(view, 'area', game.location[KING]);
+ for (let where in AREAS) {
+ if (is_cathedral_area(where))
+ if (is_friendly_or_neutral_area(where))
+ gen_action(view, 'area', where);
+ }
+ },
+ disband: function () {
+ log("Scottish King disbands.");
+ disband(KING);
+ game.who = null;
+ goto_edward_wintering();
+ },
+ area: function (to) {
+ if (game.location[KING] != to) {
+ log("Scottish King moves to " + to + ".");
+ game.location[KING] = to;
+ }
+ game.who = null;
+ goto_edward_wintering();
+ },
+}
+
+function is_in_scotland(who) {
+ return is_on_map(who) && game.location[who] != ENGLAND;
+}
+
+function goto_edward_wintering() {
+ if (game.edward == 1 && game.year != 1306 && is_in_scotland(EDWARD) && !game.wintered_last_year) {
+ game.active = ENGLAND;
+ game.who = EDWARD;
+ game.state = 'edward_wintering';
+ return;
+ }
+
+ if (game.edward == 1 && game.year == 1306) {
+ log("Edward I dies.");
+ game.edward = 2;
+ }
+
+ if (is_on_map(EDWARD)) {
+ log("Edward disbands.");
+ disband(EDWARD);
+ }
+
+ game.wintered_last_year = false;
+ goto_english_disbanding();
+}
+
+states.edward_wintering = {
+ prompt: function (view, current) {
+ if (is_inactive_player(current))
+ return view.prompt = "Waiting for England to winter in Scotland or disband.";
+ view.prompt = "Edward Wintering: Winter in Scotland or disband.";
+ gen_action(view, 'disband');
+ gen_action(view, 'area', game.location[EDWARD]);
+ },
+ disband: function () {
+ log("Edward disbands.");
+ disband(EDWARD);
+ game.who = null;
+ game.wintered_last_year = false;
+ goto_english_disbanding();
+ },
+ area: function (to) {
+ log("Edward winters in " + to + ".");
+ game.who = null;
+ game.wintered_last_year = true;
+ goto_english_disbanding();
+ },
+}
+
+function goto_english_disbanding() {
+ game.active = ENGLAND;
+ game.turn_log = [];
+ let ask = false;
+ for (let b in BLOCKS) {
+ let where = game.location[b];
+
+ // All blocks in England must disband.
+ if (where == ENGLAND) {
+ game.turn_log.push([ENGLAND]);
+ disband(b);
+ }
+
+ if (block_owner(b) == ENGLAND && is_on_map(b)) {
+ // Knights, Archers, & Hobelars must disband except when wintering with Edward.
+ let type = block_type(b);
+ if (type == 'knights' || type == 'archers' || type == 'hobelars') {
+ if (where == game.location[EDWARD]) {
+ ask = true;
+ } else {
+ game.turn_log.push([where]);
+ disband(b);
+ }
+ }
+
+ // Infantry may remain in Scotland subject to Castle Limits or wintering with Edward.
+ if (type == 'infantry') {
+ ask = true;
+ }
+ }
+ }
+ if (ask) {
+ game.state = 'english_disbanding';
+ clear_undo();
+ } else {
+ print_turn_log("disbands");
+ goto_wallace();
+ }
+}
+
+states.english_disbanding = {
+ prompt: function (view, current) {
+ if (is_inactive_player(current))
+ return view.prompt = "Waiting for England to disband.";
+
+ gen_action_undo(view);
+
+ // Mandatory disbanding
+ let okay_to_end = true;
+ for (let b in BLOCKS) {
+ if (block_owner(b) == ENGLAND && is_on_map(b)) {
+ let where = game.location[b];
+ let type = block_type(b);
+ if (type == 'infantry') {
+ if (!is_within_castle_limit(where) && where != game.location[EDWARD]) {
+ okay_to_end = false;
+ gen_action(view, 'block', b);
+ }
+ }
+ }
+ }
+
+ if (!okay_to_end)
+ {
+ view.prompt = "English Disbanding: Disband units in excess of castle limits.";
+ }
+ else
+ {
+ // Voluntary disbanding
+ view.prompt = "English Disbanding: You may disband units to the pool.";
+ gen_action(view, 'end_disbanding');
+ for (let b in BLOCKS) {
+ if (block_owner(b) == ENGLAND && is_on_map(b)) {
+ let type = block_type(b);
+ if (type == 'knights' || type == 'archers' || type == 'hobelars')
+ gen_action(view, 'block', b);
+ if (type == 'infantry')
+ gen_action(view, 'block', b);
+ }
+ }
+ }
+ },
+ block: function (who) {
+ push_undo();
+ game.turn_log.push([game.location[who]]);
+ disband(who);
+ },
+ end_disbanding: function () {
+ print_turn_log("disbands");
+ clear_undo();
+ goto_wallace();
+ },
+ undo: pop_undo
+}
+
+function goto_wallace() {
+ game.active = SCOTLAND;
+ if (is_on_map(WALLACE) && is_friendly_or_neutral_area("Selkirk")) {
+ game.state = 'wallace';
+ game.who = WALLACE;
+ } else {
+ goto_scottish_disbanding();
+ }
+}
+
+states.wallace = {
+ prompt: function (view, current) {
+ if (is_inactive_player(current))
+ return view.prompt = "Waiting for Scotland to move Wallace.";
+ view.prompt = "Scottish Disbanding: Move Wallace to Selkirk and gain 2 steps or remain where he is.";
+ gen_action(view, 'area', game.location[WALLACE]);
+ gen_action(view, 'area', "Selkirk");
+ },
+ area: function (to) {
+ if (to == "Selkirk") {
+ if (to != game.location[WALLACE])
+ log("Wallace goes home to " + to + ".");
+ game.steps[WALLACE] = Math.min(block_max_steps(WALLACE), game.steps[WALLACE] + 2);
+ log("Wallace gains 2 steps.");
+ }
+ game.location[WALLACE] = to;
+ game.who = null;
+ goto_scottish_disbanding();
+ },
+}
+
+function goto_scottish_disbanding() {
+ game.active = SCOTLAND;
+ game.turn_log = [];
+ let ask = false;
+ for (let b in BLOCKS) {
+ if (block_owner(b) == SCOTLAND && is_on_map(b)) {
+ let type = block_type(b);
+ if (type != 'nobles')
+ ask = true;
+ }
+ }
+ if (ask) {
+ game.state = 'scottish_disbanding';
+ clear_undo();
+ } else {
+ print_turn_log("disbands");
+ goto_scottish_builds();
+ }
+}
+
+states.scottish_disbanding = {
+ prompt: function (view, current) {
+ if (is_inactive_player(current))
+ return view.prompt = "Waiting for Scotland to disband.";
+
+ gen_action_undo(view);
+
+ // Mandatory disbanding
+ let okay_to_end = true;
+ for (let b in BLOCKS) {
+ if (block_owner(b) == SCOTLAND && is_on_map(b)) {
+ let where = game.location[b];
+ if (b == WALLACE && where == "Selkirk")
+ continue;
+ let type = block_type(b);
+ if (type != 'nobles') {
+ if (!is_within_castle_limit(where)) {
+ okay_to_end = false;
+ gen_action(view, 'block', b);
+ }
+ }
+ }
+ }
+
+ if (!okay_to_end) {
+ view.prompt = "Scottish Disbanding: Disband units in excess of castle limits.";
+ } else {
+ // Voluntary disbanding
+ view.prompt = "Scottish Disbanding: You may disband units to the pool.";
+ gen_action(view, 'end_disbanding');
+ for (let b in BLOCKS) {
+ if (block_owner(b) == SCOTLAND && is_on_map(b)) {
+ let type = block_type(b);
+ if (type != 'nobles')
+ gen_action(view, 'block', b);
+ }
+ }
+ }
+ },
+ block: function (who) {
+ push_undo();
+ game.turn_log.push([game.location[who]]);
+ disband(who);
+ },
+ end_disbanding: function () {
+ print_turn_log("disbands");
+ clear_undo();
+ goto_scottish_builds();
+ },
+ undo: pop_undo
+}
+
+function goto_scottish_builds() {
+ game.active = SCOTLAND;
+
+ if (!game.french_knights && count_scottish_nobles() >= 8) {
+ log("French knights added to pool.");
+ game.french_knights = true;
+ game.location[FRENCH_KNIGHTS] = S_BAG;
+ game.steps[FRENCH_KNIGHTS] = block_max_steps(FRENCH_KNIGHTS);
+ }
+
+ game.rp = {};
+ for (let where in AREAS) {
+ if (is_friendly_area(where)) {
+ game.rp[where] = castle_limit(where);
+ }
+ }
+ game.state = 'scottish_builds';
+ game.turn_log = [];
+ clear_undo();
+}
+
+states.scottish_builds = {
+ prompt: function (view, current) {
+ if (is_inactive_player(current))
+ return view.prompt = "Waiting for Scotland to build.";
+ view.prompt = "Scottish Builds: Deploy or reinforce armies.";
+ gen_action_undo(view);
+ gen_action(view, 'end_builds');
+ for (let where in game.rp) {
+ let rp = game.rp[where];
+ if (rp > 0) {
+ for (let b in BLOCKS)
+ if (game.location[b] == where && game.steps[b] < block_max_steps(b))
+ gen_action(view, 'block', b);
+ if (is_under_castle_limit(where))
+ gen_action(view, 'area', where);
+ }
+ }
+ },
+ area: function (where) {
+ let who;
+ if (where == "Lanark" || where == "Badenoch")
+ who = draw_from_bag(S_BAG, [ NORSE, FRENCH_KNIGHTS ]);
+ else
+ who = draw_from_bag(S_BAG);
+ if (who) {
+ clear_undo(); // no undo after drawing from the bag!
+ game.turn_log.push([where]);
+ game.location[who] = where;
+ game.steps[who] = 1;
+ --game.rp[where];
+ }
+ },
+ block: function (who) {
+ push_undo();
+ let where = game.location[who];
+ game.turn_log.push([where]);
+ --game.rp[where];
+ ++game.steps[who];
+ },
+ end_builds: function () {
+ print_turn_log("builds");
+ game.rp = null;
+ clear_undo();
+ goto_english_builds();
+ },
+ undo: pop_undo
+}
+
+function goto_english_builds() {
+ game.active = ENGLAND;
+ game.rp = {};
+ for (let where in AREAS)
+ if (is_friendly_area(where))
+ game.rp[where] = castle_limit(where);
+ game.state = 'english_builds';
+ game.turn_log = [];
+}
+
+states.english_builds = {
+ prompt: function (view, current) {
+ if (is_inactive_player(current))
+ return view.prompt = "Waiting for England to build.";
+ view.prompt = "English Builds: Deploy or reinforce armies.";
+ gen_action_undo(view);
+ gen_action(view, 'end_builds');
+ for (let where in game.rp) {
+ let rp = game.rp[where];
+ if (rp > 0) {
+ for (let b in BLOCKS) {
+ if (game.location[b] == where && game.steps[b] < block_max_steps(b)) {
+ let type = block_type(b);
+ if (type == 'nobles' || type == 'infantry')
+ gen_action(view, 'block', b);
+ }
+ }
+ }
+ }
+ },
+ block: function (who) {
+ push_undo();
+ let where = game.location[who];
+ game.turn_log.push([where]);
+ --game.rp[where];
+ ++game.steps[who];
+ },
+ end_builds: function () {
+ print_turn_log("builds");
+ game.rp = null;
+ goto_english_feudal_levy();
+ },
+ undo: pop_undo
+}
+
+function goto_english_feudal_levy() {
+ if (!is_on_map(EDWARD)) {
+ let count = Math.ceil(count_blocks_in_area(E_BAG) / 2);
+ log("English feudal levy:\n" + count + " England");
+ deploy_english(count);
+ }
+ end_winter_turn();
+}
+
+function end_winter_turn() {
+ if (++game.year > game.end_year)
+ goto_game_over();
+ else
+ start_year();
+}
+
+function goto_game_over() {
+ if (!game.victory) {
+ let e = count_english_nobles();
+ let s = count_scottish_nobles();
+ if (e > s) {
+ game.victory = "England wins by controlling the most nobles!";
+ game.result = ENGLAND;
+ } else if (s > e) {
+ game.victory = "Scotland wins by controlling the most nobles!";
+ game.result = SCOTLAND;
+ } else {
+ game.log("Tied for majority of nobles.");
+ if (is_on_map(WALLACE)) {
+ game.victory = "Tied for control of nobles. Scotland wins because Wallace is on the map!";
+ game.result = SCOTLAND;
+ } else {
+ game.victory = "Tied for control of nobles. England wins because Wallace is dead or in the draw pool!";
+ game.result = ENGLAND;
+ }
+ }
+ }
+ log(game.victory);
+ game.active = "None";
+ game.state = 'game_over';
+}
+
+states.game_over = {
+ prompt: function (view, current) {
+ view.prompt = game.victory;
+ }
+}
+
+function make_battle_view() {
+ let battle = {
+ EA: [], EB: [], EC: [], ER: [],
+ SA: [], SB: [], SC: [], SR: [],
+ flash: game.flash
+ };
+
+ battle.title = game.attacker[game.where] + " attacks " + game.where;
+ battle.title += " \u2014 round " + game.battle_round + " of 3";
+
+ function fill_cell(cell, owner, fn) {
+ for (let b in BLOCKS)
+ if (game.location[b] == game.where & block_owner(b) == owner && fn(b))
+ cell.push([b, game.steps[b], game.moved[b]?1:0])
+ }
+
+ fill_cell(battle.ER, ENGLAND, b => is_battle_reserve(b));
+ fill_cell(battle.EA, ENGLAND, b => !is_battle_reserve(b) && block_initiative(b) == 'A');
+ fill_cell(battle.EB, ENGLAND, b => !is_battle_reserve(b) && block_initiative(b) == 'B');
+ fill_cell(battle.EC, ENGLAND, b => !is_battle_reserve(b) && block_initiative(b) == 'C');
+ fill_cell(battle.SR, SCOTLAND, b => is_battle_reserve(b));
+ fill_cell(battle.SA, SCOTLAND, b => !is_battle_reserve(b) && block_initiative(b) == 'A');
+ fill_cell(battle.SB, SCOTLAND, b => !is_battle_reserve(b) && block_initiative(b) == 'B');
+ fill_cell(battle.SC, SCOTLAND, b => !is_battle_reserve(b) && block_initiative(b) == 'C');
+
+ return battle;
+}
+
+exports.setup = function (scenario, players) {
+ if (players.length != 2)
+ throw new Error("Invalid player count: " + players.length);
+ game = {
+ attacker: {},
+ border_limit: {},
+ last_used: {},
+ location: {},
+ log: [],
+ main_border: {},
+ main_origin: {},
+ moved: {},
+ moves: 0,
+ prompt: null,
+ reserves: [],
+ show_cards: false,
+ steps: {},
+ who: null,
+ where: null,
+ }
+ if (scenario == "The Bruce")
+ setup_the_bruce();
+ else if (scenario == "Braveheart")
+ setup_braveheart();
+ else if (scenario == "Campaign")
+ setup_campaign();
+ else
+ throw new Error("Unknown scenario:", scenario);
+ start_year();
+ return game;
+}
+
+exports.action = function (state, current, action, arg) {
+ game = state;
+ // TODO: check current, action and argument against action list
+ if (true) {
+ let S = states[game.state];
+ if (action in S)
+ S[action](arg, current);
+ else
+ throw new Error("Invalid action: " + action);
+ }
+ return state;
+}
+
+exports.resign = function (state, current) {
+ game = state;
+ if (game.state != 'game_over') {
+ log("");
+ log(current + " resigned.");
+ game.active = "None";
+ game.state = 'game_over';
+ game.victory = current + " resigned.";
+ game.result = ENEMY[current];
+ }
+}
+
+exports.view = function(state, current) {
+ game = state;
+
+ let view = {
+ log: game.log,
+ year: game.year,
+ edward: game.edward,
+ e_vp: count_english_nobles(),
+ s_vp: count_scottish_nobles(),
+ e_card: (game.show_cards || current == ENGLAND) ? game.e_card : 0,
+ s_card: (game.show_cards || current == SCOTLAND) ? game.s_card : 0,
+ hand: (current == ENGLAND) ? game.e_hand : (current == SCOTLAND) ? game.s_hand : [],
+ who: (game.active == current) ? game.who : null,
+ where: game.where,
+ known: {},
+ secret: { Scotland: {}, England: {} },
+ battle: null,
+ active: game.active,
+ prompt: null,
+ actions: null,
+ };
+
+ states[game.state].prompt(view, current);
+
+ if (states[game.state].show_battle)
+ view.battle = make_battle_view();
+
+ for (let b in BLOCKS) {
+ let a = game.location[b];
+ if (current == block_owner(b) || game.state == 'game_over') {
+ if (a)
+ view.known[b] = [a, game.steps[b], game.moved[b] ? 1 : 0];
+ } else {
+ if (a) {
+ let list = view.secret[block_owner(b)];
+ if (!(a in list))
+ list[a] = [0, 0];
+ list[a][0]++;
+ if (game.moved[b])
+ list[a][1]++;
+ }
+ }
+ }
+
+ return view;
+}