From 38ffd7b596c5888b36588e4490c4d4bc35e7be62 Mon Sep 17 00:00:00 2001 From: Tor Andersson Date: Sat, 1 May 2021 00:48:35 +0200 Subject: hammer: Import Hammer of the Scots. --- Scottish_Thistle_(Heraldry).svg | 1 + about.html | 8 + cards/card_1.jpg | Bin 0 -> 55793 bytes cards/card_1.png | Bin 0 -> 493108 bytes cards/card_2.jpg | Bin 0 -> 58652 bytes cards/card_2.png | Bin 0 -> 511264 bytes cards/card_3.jpg | Bin 0 -> 59809 bytes cards/card_3.png | Bin 0 -> 518804 bytes cards/card_back.svg | 90 ++ cards/card_herald.jpg | Bin 0 -> 69451 bytes cards/card_herald.png | Bin 0 -> 551725 bytes cards/card_pillage.jpg | Bin 0 -> 69529 bytes cards/card_pillage.png | Bin 0 -> 551547 bytes cards/card_sea_move.jpg | Bin 0 -> 66550 bytes cards/card_sea_move.png | Bin 0 -> 543169 bytes cards/card_truce.jpg | Bin 0 -> 67505 bytes cards/card_truce.png | Bin 0 -> 546330 bytes cards/card_victuals.jpg | Bin 0 -> 63486 bytes cards/card_victuals.png | Bin 0 -> 531693 bytes cover.1x.jpg | Bin 0 -> 32485 bytes cover.2x.jpg | Bin 0 -> 118138 bytes cover.jpg | Bin 0 -> 388706 bytes data.js | 314 +++++ info/blocks.html | 181 +++ info/cards.html | 42 + info/notes.html | 25 + info/rules.html | 18 + info/rules1.jpg | Bin 0 -> 255499 bytes info/rules2.jpg | Bin 0 -> 224607 bytes info/rules3.jpg | Bin 0 -> 250771 bytes info/rules4.jpg | Bin 0 -> 268494 bytes info/rules5.jpg | Bin 0 -> 250278 bytes info/rules6.jpg | Bin 0 -> 247756 bytes info/rules7.jpg | Bin 0 -> 231085 bytes info/rules8.jpg | Bin 0 -> 196200 bytes map150.png | Bin 0 -> 15049229 bytes map75.png | Bin 0 -> 4429961 bytes new_blocks_england.png | Bin 0 -> 216858 bytes new_blocks_scotland.png | Bin 0 -> 237072 bytes old_blocks_england.png | Bin 0 -> 100252 bytes old_blocks_scotland.png | Bin 0 -> 129085 bytes play.html | 888 ++++++++++++ rules.js | 2825 +++++++++++++++++++++++++++++++++++++++ thumbnail.jpg | Bin 0 -> 18115 bytes turn_marker.jpg | Bin 0 -> 14589 bytes ui.js | 740 ++++++++++ 46 files changed, 5132 insertions(+) create mode 100644 Scottish_Thistle_(Heraldry).svg create mode 100644 about.html create mode 100644 cards/card_1.jpg create mode 100644 cards/card_1.png create mode 100644 cards/card_2.jpg create mode 100644 cards/card_2.png create mode 100644 cards/card_3.jpg create mode 100644 cards/card_3.png create mode 100644 cards/card_back.svg create mode 100644 cards/card_herald.jpg create mode 100644 cards/card_herald.png create mode 100644 cards/card_pillage.jpg create mode 100644 cards/card_pillage.png create mode 100644 cards/card_sea_move.jpg create mode 100644 cards/card_sea_move.png create mode 100644 cards/card_truce.jpg create mode 100644 cards/card_truce.png create mode 100644 cards/card_victuals.jpg create mode 100644 cards/card_victuals.png create mode 100644 cover.1x.jpg create mode 100644 cover.2x.jpg create mode 100644 cover.jpg create mode 100644 data.js create mode 100644 info/blocks.html create mode 100644 info/cards.html create mode 100644 info/notes.html create mode 100644 info/rules.html create mode 100644 info/rules1.jpg create mode 100644 info/rules2.jpg create mode 100644 info/rules3.jpg create mode 100644 info/rules4.jpg create mode 100644 info/rules5.jpg create mode 100644 info/rules6.jpg create mode 100644 info/rules7.jpg create mode 100644 info/rules8.jpg create mode 100644 map150.png create mode 100644 map75.png create mode 100644 new_blocks_england.png create mode 100644 new_blocks_scotland.png create mode 100644 old_blocks_england.png create mode 100644 old_blocks_scotland.png create mode 100644 play.html create mode 100644 rules.js create mode 100644 thumbnail.jpg create mode 100644 turn_marker.jpg create mode 100644 ui.js diff --git a/Scottish_Thistle_(Heraldry).svg b/Scottish_Thistle_(Heraldry).svg new file mode 100644 index 0000000..f97cdfc --- /dev/null +++ b/Scottish_Thistle_(Heraldry).svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/about.html b/about.html new file mode 100644 index 0000000..fa6f3ed --- /dev/null +++ b/about.html @@ -0,0 +1,8 @@ +

+Hammer of the Scots brings the rebellion of the Braveheart, William Wallace, to life. As the English player, you seek to pacify Scotland by controlling all the important noble lords. The Scottish player also seeks the allegiance of nobles to support a difficult struggle for freedom. + +

+Designer: Tom Dalgliesh and Jerry Taylor. + +

+Copyright © 2002-2019 Columbia Games and Jerry Taylor. diff --git a/cards/card_1.jpg b/cards/card_1.jpg new file mode 100644 index 0000000..b832375 Binary files /dev/null and b/cards/card_1.jpg differ diff --git a/cards/card_1.png b/cards/card_1.png new file mode 100644 index 0000000..6dc15f9 Binary files /dev/null and b/cards/card_1.png differ diff --git a/cards/card_2.jpg b/cards/card_2.jpg new file mode 100644 index 0000000..7875059 Binary files /dev/null and b/cards/card_2.jpg differ diff --git a/cards/card_2.png b/cards/card_2.png new file mode 100644 index 0000000..dd48d31 Binary files /dev/null and b/cards/card_2.png differ diff --git a/cards/card_3.jpg b/cards/card_3.jpg new file mode 100644 index 0000000..517843d Binary files /dev/null and b/cards/card_3.jpg differ diff --git a/cards/card_3.png b/cards/card_3.png new file mode 100644 index 0000000..f57b45a Binary files /dev/null and b/cards/card_3.png differ diff --git a/cards/card_back.svg b/cards/card_back.svg new file mode 100644 index 0000000..ac35b71 --- /dev/null +++ b/cards/card_back.svg @@ -0,0 +1,90 @@ + + + + +Created by potrace 1.15, written by Peter Selinger 2001-2017 + + + + + + + + + + + + + + + + + + + + + + diff --git a/cards/card_herald.jpg b/cards/card_herald.jpg new file mode 100644 index 0000000..24d9105 Binary files /dev/null and b/cards/card_herald.jpg differ diff --git a/cards/card_herald.png b/cards/card_herald.png new file mode 100644 index 0000000..6f9723c Binary files /dev/null and b/cards/card_herald.png differ diff --git a/cards/card_pillage.jpg b/cards/card_pillage.jpg new file mode 100644 index 0000000..7f0c2c7 Binary files /dev/null and b/cards/card_pillage.jpg differ diff --git a/cards/card_pillage.png b/cards/card_pillage.png new file mode 100644 index 0000000..ada8d92 Binary files /dev/null and b/cards/card_pillage.png differ diff --git a/cards/card_sea_move.jpg b/cards/card_sea_move.jpg new file mode 100644 index 0000000..d618d97 Binary files /dev/null and b/cards/card_sea_move.jpg differ diff --git a/cards/card_sea_move.png b/cards/card_sea_move.png new file mode 100644 index 0000000..9547697 Binary files /dev/null and b/cards/card_sea_move.png differ diff --git a/cards/card_truce.jpg b/cards/card_truce.jpg new file mode 100644 index 0000000..b1f062a Binary files /dev/null and b/cards/card_truce.jpg differ diff --git a/cards/card_truce.png b/cards/card_truce.png new file mode 100644 index 0000000..b411bf1 Binary files /dev/null and b/cards/card_truce.png differ diff --git a/cards/card_victuals.jpg b/cards/card_victuals.jpg new file mode 100644 index 0000000..1114a8d Binary files /dev/null and b/cards/card_victuals.jpg differ diff --git a/cards/card_victuals.png b/cards/card_victuals.png new file mode 100644 index 0000000..3ac24e8 Binary files /dev/null and b/cards/card_victuals.png differ diff --git a/cover.1x.jpg b/cover.1x.jpg new file mode 100644 index 0000000..595cbf1 Binary files /dev/null and b/cover.1x.jpg differ diff --git a/cover.2x.jpg b/cover.2x.jpg new file mode 100644 index 0000000..9f0cbf4 Binary files /dev/null and b/cover.2x.jpg differ diff --git a/cover.jpg b/cover.jpg new file mode 100644 index 0000000..db2d852 Binary files /dev/null and b/cover.jpg differ diff --git a/data.js b/data.js new file mode 100644 index 0000000..8a710a6 --- /dev/null +++ b/data.js @@ -0,0 +1,314 @@ +"use strict"; + +const CARDS = { + 1: { + name: "Herald", + event: "herald", + image: "card_herald", + text: "Name an enemy noble (not Moray). Roll a die to convert him to your side at current strength.\n1-4 Success\n5-6 Failure\nIf a battle results, resolve it now with the defecting noble as the attacker." + }, + 2: { + name: "Pillage", + event: "pillage", + image: "card_pillage", + text: "Pillage one enemy group adjacent to a friendly group. The enemy blocks take two (2) hits (applied as per combat losses).\nPillaged step(s) may be added to friendly blocks in the pillaging group." + }, + 3: { + name: "Sea Move", + event: "sea_move", + image: "card_sea_move", + text: "Move one (1) or two (2) blocks from one coastal area to one other friendly (not vacant) coastal area (including England).\nThe Norse cannot use this card." + }, + 4: { + name: "Truce", + event: "truce", + image: "card_truce", + text: "Opponent can move, but not attack. Scots cannot enter England." + }, + 5: { + name: "Victuals", + event: "victuals", + image: "card_victuals", + text: "Distribute three (3) steps among friendly blocks in one group." + }, + 6: { name: "a 3", moves: 3, image: "card_3" }, + 7: { name: "a 3", moves: 3, image: "card_3" }, + 8: { name: "a 3", moves: 3, image: "card_3" }, + 9: { name: "a 2", moves: 2, image: "card_2" }, + 10: { name: "a 2", moves: 2, image: "card_2" }, + 11: { name: "a 2", moves: 2, image: "card_2" }, + 12: { name: "a 2", moves: 2, image: "card_2" }, + 13: { name: "a 2", moves: 2, image: "card_2" }, + 14: { name: "a 2", moves: 2, image: "card_2" }, + 15: { name: "a 2", moves: 2, image: "card_2" }, + 16: { name: "a 2", moves: 2, image: "card_2" }, + 17: { name: "a 2", moves: 2, image: "card_2" }, + 18: { name: "a 2", moves: 2, image: "card_2" }, + 19: { name: "a 1", moves: 1, image: "card_1" }, + 20: { name: "a 1", moves: 1, image: "card_1" }, + 21: { name: "a 1", moves: 1, image: "card_1" }, + 22: { name: "a 1", moves: 1, image: "card_1" }, + 23: { name: "a 1", moves: 1, image: "card_1" }, + 24: { name: "a 1", moves: 1, image: "card_1" }, + 25: { name: "a 1", moves: 1, image: "card_1" }, +}; + +let BLOCKS = {} + +let AREAS = { + "England": { x: 1360, y: 1750 }, + "Ross": { x: 583, y: 376 }, + "Garmoran": { x: 466, y: 573 }, + "Moray": { x: 644, y: 599 }, + "Strathspey": { x: 973, y: 436 }, + "Buchan": { x: 1218, y: 518 }, + "Lochaber": { x: 435, y: 766 }, + "Badenoch": { x: 834, y: 635 }, + "Mar": { x: 974, y: 709 }, + "Angus": { x: 1099, y: 820 }, + "Argyll": { x: 433, y: 1099 }, + "Atholl": { x: 714, y: 904 }, + "Lennox": { x: 626, y: 1244 }, + "Mentieth": { x: 748, y: 1067 }, + "Fife": { x: 966, y: 1089 }, + "Carrick": { x: 675, y: 1446 }, + "Lanark": { x: 830, y: 1375 }, + "Lothian": { x: 973, y: 1236 }, + "Selkirk": { x: 1015, y: 1379 }, + "Dunbar": { x: 1187, y: 1287 }, + "Galloway": { x: 685, y: 1667 }, + "Annan": { x: 946, y: 1566 }, + "Teviot": { x: 1151, y: 1424 }, + + "E. Bag": { x: 150, y: 1900 }, + "S. Bag": { x: 150, y: 50 }, +} + +let BORDERS = {}; + +(function () { + function border(A,B,T) { + if (A > B) + [A, B] = [B, A]; + let id = A + "/" + B; + AREAS[A].exits.push(B); + AREAS[B].exits.push(A); + BORDERS[id] = T; + } + + for (let a in AREAS) { + AREAS[a].cathedral = false; + AREAS[a].home = null; + AREAS[a].coastal = false; + AREAS[a].exits = []; + } + + AREAS["Strathspey"].cathedral = true; + AREAS["Lennox"].cathedral = true; + AREAS["Fife"].cathedral = true; + + AREAS["Ross"].home = "Ross"; + AREAS["Moray"].home = "Moray"; + AREAS["Buchan"].home = "Buchan"; + AREAS["Lochaber"].home = "Comyn"; + AREAS["Badenoch"].home = "Comyn"; + AREAS["Mar"].home = "Mar"; + AREAS["Angus"].home = "Angus"; + AREAS["Argyll"].home = "Argyll"; + AREAS["Atholl"].home = "Atholl"; + AREAS["Lennox"].home = "Lennox"; + AREAS["Mentieth"].home = "Mentieth"; + AREAS["Carrick"].home = "Bruce"; + AREAS["Lanark"].home = "Steward"; + AREAS["Dunbar"].home = "Dunbar"; + AREAS["Galloway"].home = "Galloway"; + AREAS["Annan"].home = "Bruce"; + + AREAS["England"].limit = 0; + AREAS["Ross"].limit = 1; + AREAS["Garmoran"].limit = 0; + AREAS["Moray"].limit = 2; + AREAS["Strathspey"].limit = 1; + AREAS["Buchan"].limit = 2; + AREAS["Lochaber"].limit = 1; + AREAS["Badenoch"].limit = 2; + AREAS["Mar"].limit = 1; + AREAS["Angus"].limit = 2; + AREAS["Argyll"].limit = 2; + AREAS["Atholl"].limit = 1; + AREAS["Lennox"].limit = 1; + AREAS["Mentieth"].limit = 3; + AREAS["Fife"].limit = 2; + AREAS["Carrick"].limit = 1; + AREAS["Lanark"].limit = 2; + AREAS["Lothian"].limit = 2; + AREAS["Selkirk"].limit = 0; + AREAS["Dunbar"].limit = 2; + AREAS["Galloway"].limit = 1; + AREAS["Annan"].limit = 2; + AREAS["Teviot"].limit = 1; + + function red(A,B) { border(A,B,"minor"); } + function black(A,B) { border(A,B,"major"); } + function northsea(A) { AREAS[A].coastal = true; } + function irishsea(A) { AREAS[A].coastal = true; } + + black("Buchan", "Angus") + black("Buchan", "Mar") + black("Carrick", "Annan") + black("Carrick", "Lanark") + black("England", "Annan") + black("England", "Dunbar") + black("Fife", "Angus") + black("Fife", "Mentieth") + black("Lanark", "Mentieth") + black("Lennox", "Carrick") + black("Lennox", "Lanark") + black("Lennox", "Mentieth") + black("Lothian", "Dunbar") + black("Lothian", "Lanark") + black("Lothian", "Mentieth") + black("Moray", "Lochaber") + black("Moray", "Strathspey") + black("Selkirk", "Teviot") + black("Strathspey", "Badenoch") + black("Strathspey", "Buchan") + black("Teviot", "Dunbar") + red("Angus", "Mar") + red("Argyll", "Lennox") + red("Atholl", "Angus") + red("Atholl", "Argyll") + red("Atholl", "Badenoch") + red("Atholl", "Fife") + red("Atholl", "Lennox") + red("Atholl", "Mar") + red("Atholl", "Mentieth") + red("Badenoch", "Lochaber") + red("Badenoch", "Mar") + red("Buchan", "Badenoch") + red("England", "Teviot") + red("Galloway", "Annan") + red("Lanark", "Annan") + red("Galloway", "Carrick") + red("Garmoran", "Lochaber") + red("Garmoran", "Moray") + red("Lochaber", "Argyll") + red("Lochaber", "Atholl") + red("Moray", "Badenoch") + red("Ross", "Garmoran") + red("Ross", "Moray") + red("Selkirk", "Annan") + red("Selkirk", "Dunbar") + red("Selkirk", "Lanark") + red("Selkirk", "Lothian") + red("Teviot", "Annan") + + northsea("England") + northsea("Ross") + northsea("Moray") + northsea("Strathspey") + northsea("Buchan") + northsea("Angus") + northsea("Mentieth") + northsea("Fife") + northsea("Lothian") + northsea("Dunbar") + + irishsea("England") + irishsea("Ross") + irishsea("Garmoran") + irishsea("Lochaber") + irishsea("Argyll") + irishsea("Lennox") + irishsea("Carrick") + irishsea("Galloway") + irishsea("Annan") + + function block(owner, type, name, move, combat, steps, mortal, image) { + let id = name; + if (type == 'nobles') + id = name + "/" + owner[0]; + let item = { + owner: owner, + type: type, + name: name, + move: move, + combat: combat, + steps: steps, + mortal: mortal, + image: image, + } + BLOCKS[id] = item; + } + + const A4 = "A4"; const A3 = "A3"; const A2 = "A2"; const A1 = "A1"; + const B4 = "B4"; const B3 = "B3"; const B2 = "B2"; const B1 = "B1"; + const C4 = "C4"; const C3 = "C3"; const C2 = "C2"; const C1 = "C1"; + + block("Scotland", "wallace", "Wallace", 3, A3, 4, true, 11); + block("Scotland", "king", "King", 3, A3, 4, true, 12); + block("Scotland", "infantry", "Douglas", 2, C3, 4, false, 13); + block("Scotland", "infantry", "Campbell", 2, C2, 4, false, 14); + block("Scotland", "infantry", "Graham", 2, C2, 4, false, 15); + block("Scotland", "infantry", "MacDonald", 2, C3, 3, false, 16); + block("Scotland", "infantry", "Lindsay", 2, C2, 3, false, 17); + + block("Scotland", "infantry", "Fraser", 2, C3, 3, false, 21); + block("Scotland", "infantry", "Barclay", 2, C2, 4, false, 22); + block("Scotland", "infantry", "Grant", 2, C2, 3, false, 23); + block("Scotland", "cavalry", "Keith", 3, B1, 3, false, 24); + block("Scotland", "archers", "Etterick", 3, B2, 2, false, 25); + block("Scotland", "norse", "Norse", 0, A2, 3, true, 26); + block("Scotland", "knights", "French Knights", 2, B3, 4, true, 27); + + block("Scotland", "nobles", "Comyn", 2, B2, 4, false, 31); + block("Scotland", "moray", "Moray", 2, B2, 3, true, 32); + block("Scotland", "nobles", "Angus", 2, B2, 3, false, 33); + block("Scotland", "nobles", "Argyll", 2, B2, 3, false, 34); + block("Scotland", "nobles", "Bruce", 2, B2, 4, false, 35); + block("Scotland", "nobles", "Mar", 2, B2, 3, false, 36); + block("Scotland", "nobles", "Lennox", 2, B2, 3, false, 37); + + block("Scotland", "nobles", "Buchan", 2, B2, 3, false, 41); + block("Scotland", "nobles", "Galloway", 2, B2, 3, false, 42); + block("Scotland", "nobles", "Ross", 2, B2, 3, false, 43); + block("Scotland", "nobles", "Atholl", 2, B2, 3, false, 44); + block("Scotland", "nobles", "Dunbar", 2, B2, 3, false, 45); + block("Scotland", "nobles", "Mentieth", 2, B2, 3, false, 46); + block("Scotland", "nobles", "Steward", 2, B2, 3, false, 47); + + block("England", "king", "Edward", 3, B4, 4, true, 61); + block("England", "archers", "Lancaster Archers", 2, B3, 3, false, 62); + block("England", "archers", "Wales Archers", 2, B3, 3, false, 63); + block("England", "knights", "Lancaster Knights", 2, B3, 4, false, 64); + block("England", "knights", "York Knights", 2, B3, 4, false, 65); + block("England", "knights", "Durham Knights", 2, B3, 3, false, 66); + block("England", "hobelars", "Hobelars", 3, A2, 3, true, 67); + + block("England", "infantry", "York Infantry", 2, C2, 4, false, 71); + block("England", "infantry", "Lancaster Infantry", 2, C2, 4, false, 72); + block("England", "infantry", "Northumber Infantry", 2, C2, 4, false, 73); + block("England", "infantry", "Durham Infantry", 2, C2, 3, false, 74); + block("England", "infantry", "Cumbria Infantry", 2, C2, 3, false, 75); + block("England", "infantry", "Westmor Infantry", 2, C2, 3, false, 82); + block("England", "infantry", "Wales Infantry", 2, C3, 3, false, 76); + block("England", "infantry", "Ulster Infantry", 2, C3, 3, false, 77); + + block("England", "nobles", "Comyn", 2, B2, 4, false, 81); + block("England", "nobles", "Angus", 2, B2, 3, false, 83); + block("England", "nobles", "Argyll", 2, B2, 3, false, 84); + block("England", "nobles", "Bruce", 2, B2, 4, false, 85); + block("England", "nobles", "Mar", 2, B2, 3, false, 86); + block("England", "nobles", "Lennox", 2, B2, 3, false, 87); + + block("England", "nobles", "Buchan", 2, B2, 3, false, 91); + block("England", "nobles", "Galloway", 2, B2, 3, false, 92); + block("England", "nobles", "Ross", 2, B2, 3, false, 93); + block("England", "nobles", "Atholl", 2, B2, 3, false, 94); + block("England", "nobles", "Dunbar", 2, B2, 3, false, 95); + block("England", "nobles", "Mentieth", 2, B2, 3, false, 96); + block("England", "nobles", "Steward", 2, B2, 3, false, 97); +})(); + +if (typeof module != 'undefined') + module.exports = { CARDS, BLOCKS, AREAS, BORDERS } diff --git a/info/blocks.html b/info/blocks.html new file mode 100644 index 0000000..531a535 --- /dev/null +++ b/info/blocks.html @@ -0,0 +1,181 @@ + +Hammer of the Scots - Blocks + + + +

+Hammer of the Scots - Blocks +

+ + + + +

Scottish

+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ +

English

+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ diff --git a/info/cards.html b/info/cards.html new file mode 100644 index 0000000..92d2e2f --- /dev/null +++ b/info/cards.html @@ -0,0 +1,42 @@ + +Hammer of the Scots - Cards + + +

Hammer of the Scots - Cards

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/info/notes.html b/info/notes.html new file mode 100644 index 0000000..8d7d9df --- /dev/null +++ b/info/notes.html @@ -0,0 +1,25 @@ + + +Hammer of the Scots - Notes + + + + + +
+ +

+Hammer of the Scots: Implementation Notes

+ +
+ +

+How do I designate the attacking main force? +

+The first block that moves into a contested area defines the main force. +Any other blocks who start from the same location and enter the battle across the same border also count as being part of the main force. +The attacking main force move is marked with an asterisk in the game log. + +


+ +

diff --git a/info/rules.html b/info/rules.html new file mode 100644 index 0000000..1cddc4b --- /dev/null +++ b/info/rules.html @@ -0,0 +1,18 @@ + +Hammer of the Scots - Rules + + + + + + + + + + + + + diff --git a/info/rules1.jpg b/info/rules1.jpg new file mode 100644 index 0000000..3a0a762 Binary files /dev/null and b/info/rules1.jpg differ diff --git a/info/rules2.jpg b/info/rules2.jpg new file mode 100644 index 0000000..2b7769e Binary files /dev/null and b/info/rules2.jpg differ diff --git a/info/rules3.jpg b/info/rules3.jpg new file mode 100644 index 0000000..029a0e1 Binary files /dev/null and b/info/rules3.jpg differ diff --git a/info/rules4.jpg b/info/rules4.jpg new file mode 100644 index 0000000..9de1b64 Binary files /dev/null and b/info/rules4.jpg differ diff --git a/info/rules5.jpg b/info/rules5.jpg new file mode 100644 index 0000000..0dd2717 Binary files /dev/null and b/info/rules5.jpg differ diff --git a/info/rules6.jpg b/info/rules6.jpg new file mode 100644 index 0000000..28b0453 Binary files /dev/null and b/info/rules6.jpg differ diff --git a/info/rules7.jpg b/info/rules7.jpg new file mode 100644 index 0000000..fe1b21c Binary files /dev/null and b/info/rules7.jpg differ diff --git a/info/rules8.jpg b/info/rules8.jpg new file mode 100644 index 0000000..59c66ff Binary files /dev/null and b/info/rules8.jpg differ diff --git a/map150.png b/map150.png new file mode 100644 index 0000000..4779910 Binary files /dev/null and b/map150.png differ diff --git a/map75.png b/map75.png new file mode 100644 index 0000000..359e5b7 Binary files /dev/null and b/map75.png differ diff --git a/new_blocks_england.png b/new_blocks_england.png new file mode 100644 index 0000000..44455d5 Binary files /dev/null and b/new_blocks_england.png differ diff --git a/new_blocks_scotland.png b/new_blocks_scotland.png new file mode 100644 index 0000000..492080c Binary files /dev/null and b/new_blocks_scotland.png differ diff --git a/old_blocks_england.png b/old_blocks_england.png new file mode 100644 index 0000000..1f13483 Binary files /dev/null and b/old_blocks_england.png differ diff --git a/old_blocks_scotland.png b/old_blocks_scotland.png new file mode 100644 index 0000000..da77448 Binary files /dev/null and b/old_blocks_scotland.png differ diff --git a/play.html b/play.html new file mode 100644 index 0000000..d397148 --- /dev/null +++ b/play.html @@ -0,0 +1,888 @@ + + + + + +HAMMER OF THE SCOTS + + + + + + + + + + + + + +
+ +
+
Chat
+
+
+
+ + + + + + + + + + + + + + + + + + + +
+
+ +
+
Name an enemy noble:
+
Angus
+
Argyll
+
Atholl
+
Bruce
+
Buchan
+
Comyn
+
Dunbar
+
Galloway
+
Lennox
+
Mar
+
Mentieth
+
Ross
+
Steward
+
+ +
+ +
+ + +
+
+
+ +
Connecting...
+ + + + + + + + + + + + + + + +
+ +
+ +
+
+
England ($USER)
+
+
+
+
+ +
+
+
Scotland ($USER)
+
+
+
+
+ +
+ +
+
+
+ +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ + +
+ +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ +
+ +
+ 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; +} diff --git a/thumbnail.jpg b/thumbnail.jpg new file mode 100644 index 0000000..3a6264b Binary files /dev/null and b/thumbnail.jpg differ diff --git a/turn_marker.jpg b/turn_marker.jpg new file mode 100644 index 0000000..860ca5f Binary files /dev/null and b/turn_marker.jpg differ diff --git a/ui.js b/ui.js new file mode 100644 index 0000000..5557a18 --- /dev/null +++ b/ui.js @@ -0,0 +1,740 @@ +"use strict"; + +const ENGLAND = "England"; +const SCOTLAND = "Scotland"; +const ENEMY = { Scotland: "England", England: "Scotland" } +const ENGLAND_BAG = "E. Bag"; +const SCOTLAND_BAG = "S. Bag"; + +const NOBLES = [ + "Angus", "Argyll", "Atholl", "Bruce", "Buchan", "Comyn", "Dunbar", + "Galloway", "Lennox", "Mar", "Mentieth", "Ross", "Steward" +]; + +let block_style = window.localStorage['hammer-of-the-scots/block-style'] || 'oldblocks'; +document.querySelector("body").classList.remove("oldblocks"); +document.querySelector("body").classList.remove("newblocks"); +document.querySelector("body").classList.add(block_style); + +function old_block_style() { + block_style = 'oldblocks'; + document.querySelector("body").classList.remove("oldblocks"); + document.querySelector("body").classList.remove("newblocks"); + document.querySelector("body").classList.add(block_style); + window.localStorage['hammer-of-the-scots/block-style'] = block_style; + update_map(); +} + +function new_block_style() { + block_style = 'newblocks'; + document.querySelector("body").classList.remove("oldblocks"); + document.querySelector("body").classList.remove("newblocks"); + document.querySelector("body").classList.add(block_style); + window.localStorage['hammer-of-the-scots/block-style'] = block_style; + update_map(); +} + +function toggle_blocks() { + document.getElementById("map").classList.toggle("hide_blocks"); +} + +let game = null; + +let ui = { + cards: {}, + areas: {}, + known: {}, + secret: { England: {}, Scotland: {} }, + battle_menu: {}, + battle_block: {}, + present: new Set(), +} + +function on_focus_area(evt) { + let where = evt.target.area; + document.getElementById("status").textContent = where; +} + +function on_blur_area(evt) { + document.getElementById("status").textContent = ""; +} + +function on_click_area(evt) { + let where = evt.target.area; + send_action('area', where); +} + +const STEP_TEXT = [ 0, "I", "II", "III", "IIII" ]; + +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 on_focus_secret_block(evt) { + let owner = evt.target.owner; + let text = (owner == ENGLAND) ? "English" : "Scottish"; + document.getElementById("status").textContent = text; +} + +function on_blur_secret_block(evt) { + document.getElementById("status").textContent = ""; +} + +function on_click_secret_block(evt) { +} + +function on_focus_map_block(evt) { + let b = evt.target.block; + let s = game.known[b][1]; + let text = block_name(b); + text += " " + BLOCKS[b].move + "-" + STEP_TEXT[s] + "-" + BLOCKS[b].combat; + if (BLOCKS[b].mortal) + text += ' \u271d'; + document.getElementById("status").textContent = text; +} + +function on_blur_map_block(evt) { + document.getElementById("status").textContent = ""; +} + +function on_click_map_block(evt) { + let b = evt.target.block; + send_action('block', b); +} + +function is_battle_reserve(who, list) { + for (let [b, s, m] of list) + if (who == b) + return true; + return false; +} + +function on_focus_battle_block(evt) { + let b = evt.target.block; + let msg = block_name(b); + if (is_battle_reserve(b, game.battle.ER)) + msg = "English Reserve"; + if (is_battle_reserve(b, game.battle.SR)) + msg = "Scottish Reserve"; + if (game.actions && game.actions.battle_fire && game.actions.battle_fire.includes(b)) + msg = "Fire with " + msg; + if (game.actions && game.actions.battle_hit && game.actions.battle_hit.includes(b)) + msg = "Take hit on " + msg; + document.getElementById("status").textContent = msg; +} + +function on_blur_battle_block(evt) { + document.getElementById("status").textContent = ""; +} + +function on_click_battle_block(evt) { + let b = evt.target.block; + send_action('block', b); +} + +function on_focus_battle_fire(evt) { + document.getElementById("status").textContent = + "Fire with " + block_name(evt.target.block); +} + +function on_focus_battle_retreat(evt) { + document.getElementById("status").textContent = + "Retreat with " + block_name(evt.target.block); +} + +function on_focus_battle_pass(evt) { + document.getElementById("status").textContent = + "Pass with " + block_name(evt.target.block); +} + +function on_focus_battle_hit(evt) { + document.getElementById("status").textContent = + "Take hit on " + block_name(evt.target.block); +} + +function on_blur_battle_button(evt) { + document.getElementById("status").textContent = ""; +} + +function on_click_battle_hit(evt) { send_action('battle_hit', evt.target.block); } +function on_click_battle_fire(evt) { send_action('battle_fire', evt.target.block); } +function on_click_battle_retreat(evt) { send_action('battle_retreat', evt.target.block); } +function on_click_battle_pass(evt) { send_action('battle_pass', evt.target.block); } + +function on_click_card(evt) { + let c = evt.target.id.split("+")[1] | 0; + send_action('play', c); +} + +function on_herald(noble) { + send_action('noble', noble); +} + +function on_button_undo(evt) { + send_action('undo'); +} + +function on_button_play_event(evt) { + send_action('play_event'); +} + +function on_button_end_move_phase(evt) { + send_action('end_move_phase'); +} + +function on_button_end_regroup(evt) { + send_action('end_regroup'); +} + +function on_button_end_retreat(evt) { + send_action('end_retreat'); +} + +function on_button_eliminate(evt) { + send_action('eliminate'); +} + +function on_button_disband(evt) { + send_action('disband'); +} + +function on_button_end_disbanding(evt) { + send_action('end_disbanding'); +} + +function on_button_end_builds(evt) { + send_action('end_builds'); +} + +function on_button_end_pillage(evt) { + send_action('end_pillage'); +} + +function on_button_pass(evt) { + send_action('pass'); +} + +function on_crown_bruce(evt) { + send_action('crown_bruce'); +} + +function on_crown_comyn(evt) { + send_action('crown_comyn'); +} + +function on_return_of_the_king(evt) { + send_action('return_of_the_king'); +} + +function build_battle_button(menu, b, c, click, enter, img_src) { + let img = new Image(); + img.draggable = false; + img.classList.add("action"); + img.classList.add(c); + img.setAttribute("src", img_src); + img.addEventListener("click", click); + img.addEventListener("mouseenter", enter); + img.addEventListener("mouseleave", on_blur_battle_button); + img.block = b; + menu.appendChild(img); +} + +function build_battle_block(b, block) { + let element = document.createElement("div"); + element.classList.add("block"); + element.classList.add("known"); + element.classList.add(BLOCKS[b].owner); + element.classList.add("block_" + block.image); + element.addEventListener("mouseenter", on_focus_battle_block); + element.addEventListener("mouseleave", on_blur_battle_block); + element.addEventListener("click", on_click_battle_block); + element.block = b; + ui.battle_block[b] = element; + + let menu_list = document.createElement("div"); + menu_list.classList.add("battle_menu_list"); + build_battle_button(menu_list, b, "hit", + on_click_battle_hit, on_focus_battle_hit, + "/images/cross-mark.svg"); + build_battle_button(menu_list, b, "fire", + on_click_battle_fire, on_focus_battle_fire, + "/images/pointy-sword.svg"); + build_battle_button(menu_list, b, "retreat", + on_click_battle_retreat, on_focus_battle_retreat, + "/images/flying-flag.svg"); + build_battle_button(menu_list, b, "pass", + on_click_battle_pass, on_focus_battle_pass, + "/images/sands-of-time.svg"); + + let menu = document.createElement("div"); + menu.classList.add("battle_menu"); + menu.appendChild(element); + menu.appendChild(menu_list); + ui.battle_menu[b] = menu; +} + +function build_known_block(b, block) { + let element = document.createElement("div"); + element.classList.add("block"); + element.classList.add("known"); + element.classList.add(BLOCKS[b].owner); + element.classList.add("block_" + block.image); + element.addEventListener("mouseenter", on_focus_map_block); + element.addEventListener("mouseleave", on_blur_map_block); + element.addEventListener("click", on_click_map_block); + element.block = b; + return element; +} + +function build_secret_block(b, block) { + let element = document.createElement("div"); + element.classList.add("block"); + element.classList.add("secret"); + element.classList.add(BLOCKS[b].owner); + element.addEventListener("mouseenter", on_focus_secret_block); + element.addEventListener("mouseleave", on_blur_secret_block); + element.addEventListener("click", on_click_secret_block); + element.owner = BLOCKS[b].owner; + return element; +} + +function build_map() { + let svgmap = document.getElementById("svgmap"); + + ui.blocks_element = document.getElementById("blocks"); + ui.offmap_element = document.getElementById("offmap"); + + for (let c = 1; c <= 25; ++c) { + ui.cards[c] = document.getElementById("card+"+c); + ui.cards[c].addEventListener("click", on_click_card); + } + + for (let name in AREAS) { + let area = AREAS[name]; + let element = svgmap.getElementById("area+"+name); + if (element) { + element.area = name; + element.addEventListener("mouseenter", on_focus_area); + element.addEventListener("mouseleave", on_blur_area); + element.addEventListener("click", on_click_area); + ui.areas[name] = element; + } + ui.secret.England[name] = []; + ui.secret.Scotland[name] = []; + } + ui.secret.England.offmap = []; + ui.secret.Scotland.offmap = []; + + for (let b in BLOCKS) { + let block = BLOCKS[b]; + build_battle_block(b, block); + ui.known[b] = build_known_block(b, block); + ui.secret[BLOCKS[b].owner].offmap.push(build_secret_block(b, block)); + } +} + +build_map(); + +function update_steps(b, steps, element) { + element.classList.remove("r1"); + element.classList.remove("r2"); + element.classList.remove("r3"); + element.classList.add("r"+(BLOCKS[b].steps - steps)); +} + +function layout_blocks(location, north, south) { + let wrap = 4; + let s = north.length; + let k = south.length; + let n = s + k; + let row, rows = []; + let i = 0; + + switch (location) { + case ENGLAND_BAG: + case SCOTLAND_BAG: + wrap = 28; + break; + case "Selkirk": + case "Lothian": + case "Dunbar": + case "Lanark": + case "Lennox": + case "Argyll": + case "Garmoran": + case "Mentieth": + wrap = 3; + break; + case "England": + wrap = 5; + } + + function new_line() { + rows.push(row = []); + i = 0; + } + + new_line(); + + while (north.length > 0) { + if (i == wrap) + new_line(); + row.push(north.shift()); + ++i; + } + + // Break early if north and south fit in exactly two rows + if (s > 0 && s <= wrap && k > 0 && k <= wrap) + new_line(); + + while (south.length > 0) { + if (i == wrap) + new_line(); + row.push(south.shift()); + ++i; + } + + for (let j = 0; j < rows.length; ++j) + for (i = 0; i < rows[j].length; ++i) + position_block(location, j, rows.length, i, rows[j].length, rows[j][i]); +} + +function position_block(location, row, n_rows, col, n_cols, element) { + let area = AREAS[location]; + let block_size = 60+6; + let padding = 4; + let offset = block_size + padding; + let row_size = (n_rows-1) * offset; + let col_size = (n_cols-1) * offset; + let x = area.x - block_size/2; + let y = area.y - block_size/2; + + let layout_major = 0.5; + let layout_minor = 0.5; + switch (location) { + case ENGLAND_BAG: + case SCOTLAND_BAG: + layout_major = 0; + layout_minor = 0; + break; + case ENGLAND: + layout_major = 1; + layout_minor = 1; + break; + case "Fife": + layout_major = 0.25; + layout_minor = 0.5; + break; + case "Mentieth": + layout_major = 0.5; + layout_minor = 0.25; + break; + } + + x -= col_size * layout_major; + y -= row_size * layout_minor; + + x += col * offset; + y += row * offset; + + element.style.left = (x|0)+"px"; + element.style.top = (y|0)+"px"; +} + +function show_block(element) { + if (element.parentElement != ui.blocks_element) + ui.blocks_element.appendChild(element); +} + +function hide_block(element) { + if (element.parentElement != ui.offmap_element) + ui.offmap_element.appendChild(element); +} + +function update_map(player) { + let overflow = { England: [], Scotland: [] }; + let layout = {}; + + document.getElementById("turn").setAttribute("class", "turn year_" + game.year); + + for (let area in AREAS) + layout[area] = { secret: [], known: [] }; + + // Move secret blocks to overflow queue if there are too many in a location + for (let area in AREAS) { + for (let color of [ENGLAND, SCOTLAND]) { + if (game.secret[color]) { + let max = game.secret[color][area] ? game.secret[color][area][0] : 0; + while (ui.secret[color][area].length > max) { + overflow[color].push(ui.secret[color][area].pop()); + } + } + } + } + + // Add secret blocks if there are too few in a location + for (let area in AREAS) { + for (let color of [ENGLAND, SCOTLAND]) { + if (game.secret[color]) { + let max = game.secret[color][area] ? game.secret[color][area][0] : 0; + while (ui.secret[color][area].length < max) { + if (overflow[color].length > 0) { + ui.secret[color][area].push(overflow[color].pop()); + } else { + let element = ui.secret[color].offmap.pop(); + show_block(element); + ui.secret[color][area].push(element); + } + } + } + } + } + + // Remove any blocks left in the overflow queue + for (let color of [ENGLAND, SCOTLAND]) { + while (overflow[color].length > 0) { + let element = overflow[color].pop(); + hide_block(element); + ui.secret[color].offmap.push(element); + } + } + + // Hide formerly known blocks + for (let b in BLOCKS) { + if (!(b in game.known)) { + hide_block(ui.known[b]); + } + } + + // Add secret blocks to layout + for (let area in AREAS) { + for (let color of [ENGLAND, SCOTLAND]) { + let i = 0, n = 0, m = 0; + if (game.secret[color] && game.secret[color][area]) { + n = game.secret[color][area][0]; + m = game.secret[color][area][1]; + } + for (let element of ui.secret[color][area]) { + if (i++ < n - m) + element.classList.remove("moved"); + else + element.classList.add("moved"); + layout[area].secret.push(element); + } + } + } + + // Add known blocks to layout + for (let b in game.known) { + let area = game.known[b][0]; + if (area) { + let steps = game.known[b][1]; + let moved = game.known[b][2]; + let element = ui.known[b]; + + layout[area].known.push(element); + + show_block(element); + update_steps(b, steps, element); + + if (moved) + element.classList.add("moved"); + else + element.classList.remove("moved"); + } + } + + // Layout blocks on map + for (let area in AREAS) { + if (player == ENGLAND) + layout_blocks(area, layout[area].secret, layout[area].known); + else + layout_blocks(area, layout[area].known, layout[area].secret); + } + + // Mark selections and highlights + + for (let where in AREAS) { + if (ui.areas[where]) { + ui.areas[where].classList.remove('highlight'); + ui.areas[where].classList.remove('where'); + } + } + if (game.actions && game.actions.area) + for (let where of game.actions.area) + ui.areas[where].classList.add('highlight'); + if (game.where) + ui.areas[game.where].classList.add('where'); + + for (let b in BLOCKS) { + ui.known[b].classList.remove('highlight'); + ui.known[b].classList.remove('selected'); + } + if (!game.battle) { + if (game.actions && game.actions.block) + for (let b of game.actions.block) + ui.known[b].classList.add('highlight'); + if (game.who) + ui.known[game.who].classList.add('selected'); + } +} + +function update_cards() { + let cards = game.hand; + for (let c = 1; c <= 25; ++c) { + ui.cards[c].classList.remove('enabled'); + if (cards && cards.includes(c)) + ui.cards[c].classList.add('show'); + else + ui.cards[c].classList.remove('show'); + } + + if (game.actions && game.actions.play) { + for (let c of game.actions.play) + ui.cards[c].classList.add('enabled'); + } + + if (!game.e_card) + document.querySelector("#england_card").className = "small_card card_back"; + else + document.querySelector("#england_card").className = "small_card " + CARDS[game.e_card].image; + if (!game.s_card) + document.querySelector("#scotland_card").className = "small_card card_back"; + else + document.querySelector("#scotland_card").className = "small_card " + CARDS[game.s_card].image; +} + +function update_battle(player) { + function fill_cell(name, list, reserve) { + let cell = window[name]; + + ui.present.clear(); + + for (let [block, steps, moved] of list) { + ui.present.add(block); + + if (block == game.who) + ui.battle_block[block].classList.add("selected"); + else + ui.battle_block[block].classList.remove("selected"); + + ui.battle_block[block].classList.remove("highlight"); + ui.battle_menu[block].classList.remove('hit'); + ui.battle_menu[block].classList.remove('fire'); + ui.battle_menu[block].classList.remove('retreat'); + ui.battle_menu[block].classList.remove('pass'); + + if (game.actions && game.actions.block && game.actions.block.includes(block)) + ui.battle_block[block].classList.add("highlight"); + if (game.actions && game.actions.battle_fire && game.actions.battle_fire.includes(block)) + ui.battle_menu[block].classList.add('fire'); + if (game.actions && game.actions.battle_retreat && game.actions.battle_retreat.includes(block)) + ui.battle_menu[block].classList.add('retreat'); + if (game.actions && game.actions.battle_pass && game.actions.battle_pass.includes(block)) + ui.battle_menu[block].classList.add('pass'); + if (game.actions && game.actions.battle_hit && game.actions.battle_hit.includes(block)) + ui.battle_menu[block].classList.add('hit'); + + update_steps(block, steps, ui.battle_block[block], false); + if (reserve) + ui.battle_block[block].classList.add("secret"); + else + ui.battle_block[block].classList.remove("secret"); + if (moved) + ui.battle_block[block].classList.add("moved"); + else + ui.battle_block[block].classList.remove("moved"); + if (reserve) + ui.battle_block[block].classList.remove("known"); + else + ui.battle_block[block].classList.add("known"); + } + + for (let b in BLOCKS) { + if (ui.present.has(b)) { + if (!cell.contains(ui.battle_menu[b])) + cell.appendChild(ui.battle_menu[b]); + } else { + if (cell.contains(ui.battle_menu[b])) + cell.removeChild(ui.battle_menu[b]); + } + } + } + + if (player == ENGLAND) { + fill_cell("FR", game.battle.ER, true); + fill_cell("FA", game.battle.EA, false); + fill_cell("FB", game.battle.EB, false); + fill_cell("FC", game.battle.EC, false); + fill_cell("EA", game.battle.SA, false); + fill_cell("EB", game.battle.SB, false); + fill_cell("EC", game.battle.SC, false); + fill_cell("ER", game.battle.SR, true); + } else { + fill_cell("ER", game.battle.ER, true); + fill_cell("EA", game.battle.EA, false); + fill_cell("EB", game.battle.EB, false); + fill_cell("EC", game.battle.EC, false); + fill_cell("FA", game.battle.SA, false); + fill_cell("FB", game.battle.SB, false); + fill_cell("FC", game.battle.SC, false); + fill_cell("FR", game.battle.SR, true); + } +} + +function on_update(state, player) { + game = state; + + show_action_button("#undo_button", "undo"); + show_action_button("#pass_button", "pass"); + show_action_button("#play_event_button", "play_event"); + show_action_button("#end_move_phase_button", "end_move_phase"); + show_action_button("#end_regroup_button", "end_regroup"); + show_action_button("#end_retreat_button", "end_retreat"); + show_action_button("#eliminate_button", "eliminate"); + show_action_button("#disband_button", "disband"); + show_action_button("#end_disbanding_button", "end_disbanding"); + show_action_button("#end_builds_button", "end_builds"); + show_action_button("#end_pillage_button", "end_pillage"); + show_action_button("#crown_bruce_button", "crown_bruce"); + show_action_button("#crown_comyn_button", "crown_comyn"); + show_action_button("#return_of_the_king_button", "return_of_the_king"); + + document.getElementById("england_vp").textContent = game.e_vp; + document.getElementById("scotland_vp").textContent = game.s_vp; + + update_cards(); + update_map(player); + + if (game.actions && game.actions.noble) { + document.querySelector(".herald").classList.add("show"); + for (let noble of NOBLES) { + let element = document.getElementById("herald+" + noble); + if (game.actions.noble.includes(noble)) + element.classList.add("show"); + else + element.classList.remove("show"); + } + } else { + document.querySelector(".herald").classList.remove("show"); + } + + if (game.battle) { + document.querySelector(".battle_header").textContent = game.battle.title; + document.querySelector(".battle_message").textContent = game.battle.flash; + document.querySelector(".battle").classList.add("show"); + update_battle(player); + } else { + document.querySelector(".battle").classList.remove("show"); + } +} + +drag_element_with_mouse(".battle", ".battle_header"); +drag_element_with_mouse(".herald", ".herald_header"); +scroll_with_middle_mouse(".grid_center", 2); + +init_client([ "England", "Scotland" ]); -- cgit v1.2.3