diff options
author | Tor Andersson <tor@ccxvii.net> | 2024-05-16 21:33:44 +0200 |
---|---|---|
committer | Tor Andersson <tor@ccxvii.net> | 2024-08-21 00:28:20 +0200 |
commit | 842b8915070cdb982a212a1be2a9805e095bf571 (patch) | |
tree | 8a357d25153cd1872ada90e8dfd5bef3e3931493 | |
parent | 8652add81fc0b12238754a28495d5e0e59babbaf (diff) | |
download | washingtons-war-842b8915070cdb982a212a1be2a9805e095bf571.tar.gz |
numeric ids
-rw-r--r-- | cards.js | 328 | ||||
-rw-r--r-- | data.js | 1155 | ||||
-rw-r--r-- | play.html | 1 | ||||
-rw-r--r-- | play.js | 131 | ||||
-rw-r--r-- | rules.js | 1248 |
5 files changed, 1669 insertions, 1194 deletions
diff --git a/cards.js b/cards.js deleted file mode 100644 index cdcbd3b..0000000 --- a/cards.js +++ /dev/null @@ -1,328 +0,0 @@ -const CARD_3_OPS = { title: "3 OPS", type: 'ops', count: 3 } -const CARD_2_OPS = { title: "2 OPS", type: 'ops', count: 2 } -const CARD_1_OPS = { title: "1 OPS", type: 'ops', count: 1 } - -const CARDS = { - 1:CARD_3_OPS, 2:CARD_3_OPS, 3:CARD_3_OPS, 4:CARD_3_OPS, 5:CARD_3_OPS, - 6:CARD_3_OPS, 7:CARD_3_OPS, 8:CARD_3_OPS, 9:CARD_3_OPS, 10:CARD_3_OPS, - 11:CARD_3_OPS, 12:CARD_3_OPS, 13:CARD_3_OPS, 14:CARD_3_OPS, 15:CARD_3_OPS, - 16:CARD_3_OPS, 17:CARD_3_OPS, 18:CARD_3_OPS, 19:CARD_3_OPS, 20:CARD_3_OPS, - 21:CARD_3_OPS, 22:CARD_3_OPS, - - 23:CARD_2_OPS, 24:CARD_2_OPS, 25:CARD_2_OPS, 26:CARD_2_OPS, 27:CARD_2_OPS, - 28:CARD_2_OPS, 29:CARD_2_OPS, 30:CARD_2_OPS, 31:CARD_2_OPS, 32:CARD_2_OPS, - 33:CARD_2_OPS, 34:CARD_2_OPS, 35:CARD_2_OPS, 36:CARD_2_OPS, 37:CARD_2_OPS, - 38:CARD_2_OPS, 39:CARD_2_OPS, 40:CARD_2_OPS, 41:CARD_2_OPS, 42:CARD_2_OPS, - 43:CARD_2_OPS, 44:CARD_2_OPS, - - 45:CARD_1_OPS, 46:CARD_1_OPS, 47:CARD_1_OPS, 48:CARD_1_OPS, 49:CARD_1_OPS, - 50:CARD_1_OPS, 51:CARD_1_OPS, 52:CARD_1_OPS, 53:CARD_1_OPS, 54:CARD_1_OPS, - 55:CARD_1_OPS, 56:CARD_1_OPS, 57:CARD_1_OPS, 58:CARD_1_OPS, 59:CARD_1_OPS, - 60:CARD_1_OPS, 61:CARD_1_OPS, 62:CARD_1_OPS, 63:CARD_1_OPS, 64:CARD_1_OPS, - 65:CARD_1_OPS, 66:CARD_1_OPS, - - 67: { - title: "Minor Campaign", - type: 'campaign', - count: 2 - }, - - 68: { - title: "Minor Campaign", - type: 'campaign', - count: 2 - }, - - 69: { - title: "Minor Campaign", - type: 'campaign', - count: 2 - }, - - 70: { - title: "Major Campaign", - type: 'campaign', - count: 3 - }, - - 71: { - title: "North's Government Falls - The War Ends in 1779", - type: 'mandatory-event', - event: 'the_war_ends', - year: 1779 - }, - - 72: { - title: "North's Government Falls - The War Ends in 1780", - type: 'mandatory-event', - event: 'the_war_ends', - year: 1780 - }, - - 73: { - title: "North's Government Falls - The War Ends in 1781", - type: 'mandatory-event', - event: 'the_war_ends', - year: 1781 - }, - - 74: { - title: "North's Government Falls - The War Ends in 1782", - type: 'mandatory-event', - event: 'the_war_ends', - year: 1782 - }, - - 75: { - title: "North's Government Falls - The War Ends in 1783", - type: 'mandatory-event', - event: 'the_war_ends', - year: 1783 - }, - - 76: { - title: "Henry Know Continental Artillery Commander", - type: 'american-battle', - }, - - 77: { - title: "Jane McCrea Indian Atrocity Sparks Outrage", - type: 'american-event', - event: 'place_american_pc', - count: 2, - once: true, - }, - - 78: { - title: "Iroquois Uprising!", - type: 'british-event', - event: 'remove_american_pc_from_non_port', - where: [ 'NH', 'NY', 'PA' ], - count: 2, - }, - - 79: { - title: "Joseph Brant Leads an Iroquois Raid", - type: 'british-event', - event: 'remove_american_pc_from_non_port', - where: [ 'NH', 'NY', 'PA' ], - count: 2, - }, - - 80: { - title: "Lt. Colonel Simcoe's Queen's Rangers", - type: 'british-event', - event: 'remove_american_pc_within_two_spaces_of_a_british_general', - count: 2, - }, - - 81: { - title: "D'Estaing Sails for the Caribbean", - type: 'british-event', - when: 'after_french_alliance', - event: 'remove_french_navy', - }, - - 82: { - title: "Banastre Tarleton's Waxhaws Massacre", - type: 'british-event-or-battle', - event: 'remove_american_cu', - }, - - 83: { - title: "Lord George Germaine Offers Royal Amnesty", - type: 'british-event', - event: 'remove_american_pc', - count: 2, - }, - - 84: { - title: "George Rogers Clark Leads a Western Offensive", - type: 'american-event', - event: 'remove_random_british_card', - }, - - 85: { - title: "Don Bernardo Galvez Captures Pensacola", - type: 'american-event', - when: 'european_war_in_effect', - event: 'remove_british_cu', - count: 2, - }, - - 86: { - title: "Baron von Steuben Trains the Continental Army", - type: 'american-event', - event: 'baron_von_steuben_trains_the_continental_army', - }, - - 87: { - title: "Lord North Offers a Royal Amnesty", - type: 'british-event', - event: 'remove_american_pc', - count: 4, - once: true, - }, - - 88: { - title: "The Swamp Fox, Francis Marion", - type: 'american-event', - event: 'remove_british_pc_from', - where: [ 'NC', 'SC', 'GA' ], - count: 2, - }, - - 89: { - title: "The Gamecock: Thomas Sumter", - type: 'american-event', - event: 'remove_british_pc_from', - where: [ 'NC', 'SC', 'GA' ], - count: 2, - }, - - 90: { - title: "Josiah Martin Rallies North Carolina Loyalists", - type: 'british-event', - event: 'remove_american_pc_from', - where: [ 'NC' ], - count: 2, - }, - - 91: { - title: 'Thomas Paine Publishes Pamphlets "Common Sense" and "The American Crisis"', - type: 'american-event', - event: 'place_american_pc_in', - where: [ 'NH', 'NY', 'MA', 'CT', 'RI', 'PA', 'NJ', 'MD', 'DE', 'VA', 'NC', 'SC', 'GA' ], - count: 3, - once: true, - }, - - 92: { - title: "Nathan Hale, American Martyr", - type: 'american-event', - event: 'place_american_pc', - count: 2, - once: true, - }, - - 93: { - title: "John Glover's Marblehead Regiment", - type: 'american-event', - event: 'john_glovers_marblehead_regiment', - }, - - 94: { - title: "Pennsylvania and New Jersey Line Mutinies", - type: 'british-event', - event: 'pennsylvania_and_new_jersey_line_mutinies', - }, - - 95: { - title: "William Pitt Urges Peace Talks", - type: 'british-event', - when: 'before_french_alliance', - event: 'remove_american_pc', - count: 2, - reshuffle: 'if_played', - }, - - 96: { - title: "Hortelez et Cie: Clandestine French and Spanish Aid", - type: 'american-event', - when: 'before_french_alliance', - event: 'advance_french_alliance', - reshuffle: 'if_discarded', - count: 2, - }, - - 97: { - title: "Admiral Suffren Wins a Naval Victory", - type: 'american-event', - when: 'after_french_alliance', - event: 'remove_random_british_card', - }, - - 98: { - title: '"Mad" Anthony Wayne', - type: 'american-battle', - }, - - 99: { - title: "Declaration of Independence", - type: 'mandatory-event', - event: 'declaration_of_independence', - once: true, - reshuffle: 'if_played', - tournament: true, - }, - - 100: { - title: "Benedict Arnold's Treason Undermines the Patriot Cause", - type: 'british-battle', - event: 'remove_benedict_arnold', - once: true, - }, - - 101: { - title: "Benjamin Franklin: Minister to France", - type: 'mandatory-event', - event: 'advance_french_alliance', - count: 4, - once: true, - tournament: true, - }, - - 102: { - title: "Admiral Rodney Captures St. Eustatius", - type: 'british-event', - event: 'remove_random_american_card', - when: 'european_war_in_effect', - once: true, - }, - - 103: { - title: "Thaddeus Kosciuszco Constructs Engineering Works", - type: 'american-battle', - }, - - 104: { - title: "Light Horse Harry Lee", - type: 'american-battle', - }, - - 105: { - title: "Morgan's Riflemen", - type: 'american-battle', - }, - - 106: { - title: "John Paul Jones' Shipping Raids", - type: 'american-event', - event: 'remove_random_british_card', - }, - - 107: { - title: "British Light Infantry", - type: 'british-battle', - }, - - 108: { - title: "Lord Sandwich Coastal Raids", - type: 'british-event', - event: 'lord_sandwich_coastal_raids', - }, - - 109: { - title: "Edward Bancroft, British Double Agent", - type: 'british-event', - event: 'remove_random_american_card', - }, - - 110: { - title: "Hessian Infantry Bayonet Charge", - type: 'british-battle', - }, -} - -if (typeof module == 'object') - module.exports = CARDS; @@ -1,360 +1,823 @@ "use strict" -let BOXES = {} -let BLOCKADE = {} -let COLONIES = {} -let SPACES = {} -let PATH_INDEX = {} -let PATH_NAME -let PATH_TYPE - -const GENERALS = { - Arnold: { owner: "American", strategy: 1, battle: 3, agility: 2, bonus: false }, - Gates: { owner: "American", strategy: 2, battle: 2, agility: 1, bonus: false }, - Greene: { owner: "American", strategy: 1, battle: 4, agility: 2, bonus: true }, - Lafayette: { owner: "American", strategy: 1, battle: 2, agility: 1, bonus: false }, - Lee: { owner: "American", strategy: 2, battle: 1, agility: 1, bonus: false }, - Lincoln: { owner: "American", strategy: 2, battle: 1, agility: 1, bonus: false }, - Washington: { owner: "American", strategy: 1, battle: 5, agility: 2, bonus: true }, - - Burgoyne: { owner: "British", strategy: 2, battle: 2, agility: 1, bonus: false }, - Carleton: { owner: "British", strategy: 3, battle: 3, agility: 2, bonus: false }, - Clinton: { owner: "British", strategy: 3, battle: 4, agility: 2, bonus: false }, - Cornwallis: { owner: "British", strategy: 2, battle: 4, agility: 2, bonus: false }, - Howe: { owner: "British", strategy: 3, battle: 6, agility: 3, bonus: false }, - - Rochambeau: { owner: "French", strategy: 2, battle: 4, agility: 2, bonus: false }, +var data + +;(function () { + +function array_insert(array, index, item) { + for (let i = array.length; i > index; --i) + array[i] = array[i - 1] + array[index] = item } -function getEdge(A, B) { - if (A > B) - return PATHS[B + "/" + A] - return PATHS[A + "/" + B] +function set_add(set, item) { + let a = 0 + let b = set.length - 1 + while (a <= b) { + let m = (a + b) >> 1 + let x = set[m] + if (item < x) + b = m - 1 + else if (item > x) + a = m + 1 + else + return + } + array_insert(set, a, item) } -;(function () { - let info = {} +function make_index_from_list(list) { + let map = {} + for (let i = 0; i < list.length; ++i) + map[list[i]] = i + return map +} - function box(category, A, x, y) { - x += 40 - y += 36 - BOXES[A] = { name: A, category: category, x: x, y: y } - } +const BOXES = {} - function space(colony, name, x, y, type) { - x += 40 - y += 36 - if (type == "winter-quarters") { - x += 1 - y += 1 - } - if (!(colony in COLONIES)) - COLONIES[colony] = [] - COLONIES[colony].push(name) - SPACES[name] = { name: name, colony: colony, type: type, port: false, x: x, y: y, exits: [] } - } +// === GENERALS === - function line(A, B, type) { - if (A > B) { - let C = B - B = A - A = C - } - let id = A + "/" + B - SPACES[A].exits.push(B) - SPACES[B].exits.push(A) - info[id] = type - if (!(A in PATH_INDEX)) - PATH_INDEX[A] = {} - if (!(B in PATH_INDEX)) - PATH_INDEX[B] = {} - PATH_INDEX[A][B] = -1 - PATH_INDEX[B][A] = -1 - } +const generals = [ + { name: "Arnold", owner: "American", strategy: 1, battle: 3, agility: 2, bonus: false }, + { name: "Gates", owner: "American", strategy: 2, battle: 2, agility: 1, bonus: false }, + { name: "Greene", owner: "American", strategy: 1, battle: 4, agility: 2, bonus: true }, + { name: "Lafayette", owner: "American", strategy: 1, battle: 2, agility: 1, bonus: false }, + { name: "Lee", owner: "American", strategy: 2, battle: 1, agility: 1, bonus: false }, + { name: "Lincoln", owner: "American", strategy: 2, battle: 1, agility: 1, bonus: false }, + { name: "Washington", owner: "American", strategy: 1, battle: 5, agility: 2, bonus: true }, - space("CA", "Fort Detroit", 55, 673, "regular-space") - space("CA", "Montreal", 1000, 159, "fortified-port") - space("CA", "Quebec", 1122, 57, "fortified-port") - space("CT", "Hartford", 1183, 749, "winter-quarters") - space("CT", "New Haven", 1088, 736, "regular-space") - space("DE", "Wilmington DE", 885, 1079, "winter-quarters") - space("GA", "Augusta", 140, 2060, "regular-space") - space("GA", "Savannah", 227, 2271, "regular-space") - space("GA", "St. Mary's", 180, 2394, "regular-space") - space("MA", "Barnstable", 1394, 705, "regular-space") - space("MA", "Boston", 1327, 592, "winter-quarters") - space("MA", "Falmouth", 1334, 410, "regular-space") - space("MA", "Lexington Concord", 1218, 628, "regular-space") - space("MA", "Springfield", 1095, 624, "winter-quarters") - space("MD", "Baltimore", 769, 1105, "winter-quarters") - space("MD", "Fort Cumberland", 446, 1098, "regular-space") - space("MD", "Frederick Town", 646, 1097, "regular-space") - space("NC", "Charlotte", 215, 1653, "regular-space") - space("NC", "Gilbert Town", 123, 1568, "regular-space") - space("NC", "Hillsboro", 484, 1582, "regular-space") - space("NC", "New Bern", 764, 1670, "regular-space") - space("NC", "Salem", 308, 1561, "regular-space") - space("NC", "Wake (Raleigh)", 606, 1671, "regular-space") - space("NC", "Wilmington NC", 676, 1816, "regular-space") - space("NH", "Brattleboro", 1102, 486, "regular-space") - space("NH", "Concord", 1214, 460, "regular-space") - space("NH", "Norwich", 1125, 342, "winter-quarters") - space("NJ", "Monmouth", 998, 1074, "regular-space") - space("NJ", "Morristown", 864, 831, "regular-space") - space("NJ", "New Brunswick", 961, 942, "regular-space") - space("NY", "Albany", 965, 626, "winter-quarters") - space("NY", "Fort Niagara", 461, 526, "regular-space") - space("NY", "Fort Stanwix", 877, 483, "winter-quarters") - space("NY", "Genesee", 668, 582, "regular-space") - space("NY", "Long Island", 1147, 868, "regular-space") - space("NY", "New York", 1024, 838, "winter-quarters") - space("NY", "Oswego", 768, 424, "regular-space") - space("NY", "Saratoga", 992, 496, "regular-space") - space("NY", "Ticonderoga", 993, 355, "winter-quarters") - space("NY", "Westchester", 955, 747, "regular-space") - space("PA", "Bassett Town", 147, 984, "regular-space") - space("PA", "Harrisburg", 555, 880, "regular-space") - space("PA", "Philadelphia", 846, 961, "fortified-port") - space("PA", "Pittsburgh", 439, 958, "winter-quarters") - space("PA", "Reading", 684, 900, "regular-space") - space("PA", "Wyoming Valley", 685, 747, "winter-quarters") - space("PA", "York", 589, 992, "regular-space") - space("RI", "Newport", 1289, 736, "winter-quarters") - space("SC", "Camden", 346, 1773, "regular-space") - space("SC", "Charleston", 444, 2050, "fortified-port") - space("SC", "Cheraw", 496, 1795, "regular-space") - space("SC", "Eutaw Springs", 404, 1905, "regular-space") - space("SC", "Fort Prince George", 74, 1773, "regular-space") - space("SC", "Georgetown", 574, 1915, "regular-space") - space("SC", "Ninety Six", 222, 1932, "regular-space") - space("VA", "Abingdon", 61, 1451, "regular-space") - space("VA", "Alexandria", 653, 1217, "winter-quarters") - space("VA", "Charlottesville", 434, 1257, "winter-quarters") - space("VA", "Fincastle", 349, 1402, "regular-space") - space("VA", "Fort Chiswell", 212, 1408, "regular-space") - space("VA", "Lynch's Ferry", 486, 1444, "regular-space") - space("VA", "Norfolk", 781, 1450, "winter-quarters") - space("VA", "Petersburg", 634, 1446, "regular-space") - space("VA", "Point Pleasant", 132, 1218, "regular-space") - space("VA", "Richmond", 608, 1323, "winter-quarters") - space("VA", "Yorktown", 767, 1317, "regular-space") - - box("SEA", "Sea1", 1277, 65, "blockade") - box("SEA", "Sea2", 1469, 514, "blockade") - box("SEA", "Sea3", 1213, 981, "blockade") - box("SEA", "Sea4", 1058, 1174, "blockade") - box("SEA", "Sea5", 947, 1395, "blockade") - box("SEA", "Sea6", 637, 2027, "blockade") - box("SEA", "Sea7", 359, 2358, "blockade") - - box("HOLDING", "Continental Congress Dispersed", 545, 350) - box("HOLDING", "Captured Generals", 1463, 80) - box("HOLDING", "British Leader Reinforcements", 1425, 1745) - box("HOLDING", "American Leader Reinforcements", 400, 310) - box("HOLDING", "French Reinforcements", 150, 340) - - box("ALLIANCE", "French Alliance Track 0", 898, 2240) - box("ALLIANCE", "French Alliance Track 1", 964, 2240) - box("ALLIANCE", "French Alliance Track 2", 1030, 2240) - box("ALLIANCE", "French Alliance Track 3", 1096, 2240) - box("ALLIANCE", "French Alliance Track 4", 1162, 2240) - box("ALLIANCE", "French Alliance Track 5", 1228, 2240) - box("ALLIANCE", "French Alliance Track 6", 1294, 2240) - box("ALLIANCE", "French Alliance Track 7", 1360, 2240) - box("ALLIANCE", "French Alliance Track 8", 1426, 2240) - box("ALLIANCE", "French Alliance Track 9", 1492, 2240) - - box("TURN", "Game Turn 1775", 898, 2370) - box("TURN", "Game Turn 1776", 964, 2370) - box("TURN", "Game Turn 1777", 1030, 2370) - box("TURN", "Game Turn 1778", 1096, 2370) - box("TURN", "Game Turn 1779", 1162, 2370) - box("TURN", "Game Turn 1780", 1228, 2370) - box("TURN", "Game Turn 1781", 1294, 2370) - box("TURN", "Game Turn 1782", 1360, 2370) - box("TURN", "Game Turn 1783", 1426, 2370) - - box("CONTROL", "CA", 1376, 967) - box("CONTROL", "NH", 1458, 1006) - box("CONTROL", "NY", 1375, 1081) - box("CONTROL", "MA", 1491, 1081) - box("CONTROL", "CT", 1455, 1156) - box("CONTROL", "RI", 1533, 1156) - box("CONTROL", "PA", 1299, 1194) - box("CONTROL", "NJ", 1376, 1189) - box("CONTROL", "MD", 1299, 1266) - box("CONTROL", "DE", 1376, 1266) - box("CONTROL", "VA", 1241, 1337) - box("CONTROL", "NC", 1260, 1407) - box("CONTROL", "SC", 1212, 1479) - box("CONTROL", "GA", 1164, 1550) - - function wilderness(A, B) { - line(A, B, "wilderness") - } - function path(A, B) { - line(A, B, "path") - } - function sea(space, zone) { - SPACES[space].port = true - BLOCKADE[space] = zone - } + { name: "Rochambeau", owner: "French", strategy: 2, battle: 4, agility: 2, bonus: false }, - wilderness("Quebec", "Falmouth") - wilderness("Fort Detroit", "Bassett Town") - wilderness("Bassett Town", "Point Pleasant") - - path("Quebec", "Montreal") - path("Montreal", "Ticonderoga") - path("Montreal", "Oswego") - path("Oswego", "Fort Niagara") - path("Oswego", "Fort Stanwix") - path("Fort Niagara", "Fort Detroit") - path("Fort Niagara", "Genesee") - path("Ticonderoga", "Norwich") - path("Ticonderoga", "Saratoga") - path("Norwich", "Brattleboro") - path("Norwich", "Concord") - path("Concord", "Falmouth") - path("Concord", "Brattleboro") - path("Saratoga", "Brattleboro") - path("Saratoga", "Albany") - path("Brattleboro", "Springfield") - path("Fort Stanwix", "Albany") - path("Albany", "Springfield") - path("Lexington Concord", "Springfield") - path("Lexington Concord", "Boston") - path("Falmouth", "Boston") - path("Barnstable", "Boston") - path("Newport", "Boston") - path("Newport", "Hartford") - path("Springfield", "Hartford") - path("New Haven", "Hartford") - path("New Haven", "New York") - path("New Haven", "Westchester") - path("New York", "Westchester") - path("New York", "Long Island") - path("Newport", "Lexington Concord") - path("Albany", "Westchester") - path("Fort Stanwix", "Genesee") - path("Genesee", "Wyoming Valley") - path("Fort Niagara", "Pittsburgh") - path("Morristown", "Westchester") - path("Morristown", "Wyoming Valley") - path("Morristown", "Reading") - path("Morristown", "New Brunswick") - path("Morristown", "New York") - path("New York", "New Brunswick") - path("Monmouth", "New Brunswick") - path("Philadelphia", "New Brunswick") - path("Monmouth", "Philadelphia") - path("Reading", "Philadelphia") - path("Wyoming Valley", "Reading") - path("Wilmington DE", "Philadelphia") - path("Wilmington DE", "Baltimore") - path("Bassett Town", "Pittsburgh") - path("Fort Cumberland", "Pittsburgh") - path("Harrisburg", "Pittsburgh") - path("Harrisburg", "York") - path("Reading", "York") - path("Harrisburg", "Reading") - path("Frederick Town", "York") - path("Frederick Town", "Baltimore") - path("Frederick Town", "Alexandria") - path("Frederick Town", "Charlottesville") - path("Frederick Town", "Fort Cumberland") - path("Richmond", "Alexandria") - path("Richmond", "Yorktown") - path("Richmond", "Petersburg") - path("Richmond", "Lynch's Ferry") - path("Richmond", "Charlottesville") - path("Richmond", "Norfolk") - path("Fincastle", "Charlottesville") - path("Fincastle", "Lynch's Ferry") - path("Fincastle", "Fort Chiswell") - path("Point Pleasant", "Fort Chiswell") - path("Abingdon", "Fort Chiswell") - path("Abingdon", "Gilbert Town") - path("Fort Prince George", "Gilbert Town") - path("Charlotte", "Gilbert Town") - path("Charlotte", "Salem") - path("Hillsboro", "Salem") - path("Cheraw", "Salem") - path("Lynch's Ferry", "Salem") - path("Charlotte", "Camden") - path("Abingdon", "Fort Prince George") - path("New Bern", "Norfolk") - path("Petersburg", "Norfolk") - path("Petersburg", "Wake (Raleigh)") - path("New Bern", "Wake (Raleigh)") - path("Hillsboro", "Wake (Raleigh)") - path("Petersburg", "Hillsboro") - path("Petersburg", "Lynch's Ferry") - path("Wilmington NC", "Wake (Raleigh)") - path("Wilmington NC", "Cheraw") - path("Wilmington NC", "Georgetown") - path("Cheraw", "Georgetown") - path("Cheraw", "Camden") - path("Ninety Six", "Fort Prince George") - path("Ninety Six", "Camden") - path("Ninety Six", "Augusta") - path("Eutaw Springs", "Camden") - path("Eutaw Springs", "Charleston") - path("Georgetown", "Charleston") - path("Savannah", "Charleston") - path("Alexandria", "Baltimore") - path("Savannah", "Augusta") - path("Savannah", "St. Mary's") - path("Charlottesville", "Lynch's Ferry") - - sea("Quebec", "Sea1") - sea("Montreal", "Sea1") - - sea("Falmouth", "Sea2") - sea("Boston", "Sea2") - sea("Barnstable", "Sea2") - sea("Newport", "Sea2") - - sea("New Haven", "Sea3") - sea("New York", "Sea3") - sea("Long Island", "Sea3") - - sea("Philadelphia", "Sea4") - sea("Wilmington DE", "Sea4") - - sea("Baltimore", "Sea5") - sea("Alexandria", "Sea5") - sea("Yorktown", "Sea5") - sea("Norfolk", "Sea5") - - sea("New Bern", "Sea6") - sea("Wilmington NC", "Sea6") - sea("Charleston", "Sea6") - - sea("Savannah", "Sea7") - sea("St. Mary's", "Sea7") - - PATH_NAME = Object.keys(info).sort() - PATH_TYPE = [] - for (let id of PATH_NAME) - PATH_TYPE.push(info[id]) - for (let a in PATH_INDEX) { - for (let b in PATH_INDEX[a]) { - let id = a < b ? a + "/" + b : b + "/" + a - PATH_INDEX[a][b] = PATH_NAME.indexOf(id) - } - } -})() + { name: "Burgoyne", owner: "British", strategy: 2, battle: 2, agility: 1, bonus: false }, + { name: "Carleton", owner: "British", strategy: 3, battle: 3, agility: 2, bonus: false }, + { name: "Clinton", owner: "British", strategy: 3, battle: 4, agility: 2, bonus: false }, + { name: "Cornwallis", owner: "British", strategy: 2, battle: 4, agility: 2, bonus: false }, + { name: "Howe", owner: "British", strategy: 3, battle: 6, agility: 3, bonus: false }, +] + +const general_index = make_index_from_list(generals.map(x=>x.name)) + +// === SPACES === + +const spaces = [] +const space_index = {} -if (typeof module == "object") { - module.exports = { - GENERALS: GENERALS, - BOXES: BOXES, - COLONIES: COLONIES, - SPACES: SPACES, - PATH_NAME: PATH_NAME, - PATH_TYPE: PATH_TYPE, - PATH_INDEX: PATH_INDEX, - BLOCKADE, +const colony_name = [ "Canada", "NH", "NY", "MA", "CT", "RI", "PA", "NJ", "MD", "DE", "VA", "NC", "SC", "GA" ] +const colony_spaces = colony_name.map(x => []) + +const Canada = 0 +const NH = 1 +const NY = 2 +const MA = 3 +const CT = 4 +const RI = 5 +const PA = 6 +const NJ = 7 +const MD = 8 +const DE = 9 +const VA = 10 +const NC = 11 +const SC = 12 +const GA = 13 + +function box(category, A, x, y) { + x += 40 + y += 36 + BOXES[A] = { name: A, category: category, x: x, y: y } +} + +function space(colony, name, x, y, type) { + x += 40 + y += 36 + if (type === "winter-quarters") { + x += 1 + y += 1 } + + let ix = spaces.length + space_index[name] = ix + spaces.push({ name: name, colony, type, port: 0, path: [], wilderness: [], adjacent: [], x, y }) + set_add(colony_spaces[colony], ix) +} + +function not_space(type, name, x, y) { + // box that's not a space + x += 40 + y += 36 + let ix = spaces.length + space_index[name] = ix + spaces.push({ name, type, x, y }) +} + +function line(A, B, type) { + A = space_index[A] + B = space_index[B] + set_add(spaces[A].adjacent, B) + set_add(spaces[B].adjacent, A) + set_add(spaces[A][type], B) + set_add(spaces[B][type], A) +} + +space(Canada, "Fort Detroit", 55, 673, "regular-space") +space(Canada, "Montreal", 1000, 159, "fortified-port") +space(Canada, "Quebec", 1122, 57, "fortified-port") +space(CT, "Hartford", 1183, 749, "winter-quarters") +space(CT, "New Haven", 1088, 736, "regular-space") +space(DE, "Wilmington DE", 885, 1079, "winter-quarters") +space(GA, "Augusta", 140, 2060, "regular-space") +space(GA, "Savannah", 227, 2271, "regular-space") +space(GA, "St. Mary's", 180, 2394, "regular-space") +space(MA, "Barnstable", 1394, 705, "regular-space") +space(MA, "Boston", 1327, 592, "winter-quarters") +space(MA, "Falmouth", 1334, 410, "regular-space") +space(MA, "Lexington Concord", 1218, 628, "regular-space") +space(MA, "Springfield", 1095, 624, "winter-quarters") +space(MD, "Baltimore", 769, 1105, "winter-quarters") +space(MD, "Fort Cumberland", 446, 1098, "regular-space") +space(MD, "Frederick Town", 646, 1097, "regular-space") +space(NC, "Charlotte", 215, 1653, "regular-space") +space(NC, "Gilbert Town", 123, 1568, "regular-space") +space(NC, "Hillsboro", 484, 1582, "regular-space") +space(NC, "New Bern", 764, 1670, "regular-space") +space(NC, "Salem", 308, 1561, "regular-space") +space(NC, "Wake (Raleigh)", 606, 1671, "regular-space") +space(NC, "Wilmington NC", 676, 1816, "regular-space") +space(NH, "Brattleboro", 1102, 486, "regular-space") +space(NH, "Concord", 1214, 460, "regular-space") +space(NH, "Norwich", 1125, 342, "winter-quarters") +space(NJ, "Monmouth", 998, 1074, "regular-space") +space(NJ, "Morristown", 864, 831, "regular-space") +space(NJ, "New Brunswick", 961, 942, "regular-space") +space(NY, "Albany", 965, 626, "winter-quarters") +space(NY, "Fort Niagara", 461, 526, "regular-space") +space(NY, "Fort Stanwix", 877, 483, "winter-quarters") +space(NY, "Genesee", 668, 582, "regular-space") +space(NY, "Long Island", 1147, 868, "regular-space") +space(NY, "New York", 1024, 838, "winter-quarters") +space(NY, "Oswego", 768, 424, "regular-space") +space(NY, "Saratoga", 992, 496, "regular-space") +space(NY, "Ticonderoga", 993, 355, "winter-quarters") +space(NY, "Westchester", 955, 747, "regular-space") +space(PA, "Bassett Town", 147, 984, "regular-space") +space(PA, "Harrisburg", 555, 880, "regular-space") +space(PA, "Philadelphia", 846, 961, "fortified-port") +space(PA, "Pittsburgh", 439, 958, "winter-quarters") +space(PA, "Reading", 684, 900, "regular-space") +space(PA, "Wyoming Valley", 685, 747, "winter-quarters") +space(PA, "York", 589, 992, "regular-space") +space(RI, "Newport", 1289, 736, "winter-quarters") +space(SC, "Camden", 346, 1773, "regular-space") +space(SC, "Charleston", 444, 2050, "fortified-port") +space(SC, "Cheraw", 496, 1795, "regular-space") +space(SC, "Eutaw Springs", 404, 1905, "regular-space") +space(SC, "Fort Prince George", 74, 1773, "regular-space") +space(SC, "Georgetown", 574, 1915, "regular-space") +space(SC, "Ninety Six", 222, 1932, "regular-space") +space(VA, "Abingdon", 61, 1451, "regular-space") +space(VA, "Alexandria", 653, 1217, "winter-quarters") +space(VA, "Charlottesville", 434, 1257, "winter-quarters") +space(VA, "Fincastle", 349, 1402, "regular-space") +space(VA, "Fort Chiswell", 212, 1408, "regular-space") +space(VA, "Lynch's Ferry", 486, 1444, "regular-space") +space(VA, "Norfolk", 781, 1450, "winter-quarters") +space(VA, "Petersburg", 634, 1446, "regular-space") +space(VA, "Point Pleasant", 132, 1218, "regular-space") +space(VA, "Richmond", 608, 1323, "winter-quarters") +space(VA, "Yorktown", 767, 1317, "regular-space") + +not_space("box", "Continental Congress Dispersed", 545, 350) +not_space("box", "Captured Generals", 1463, 80) +not_space("box", "British Leader Reinforcements", 1425, 1745) +not_space("box", "American Leader Reinforcements", 400, 310) +not_space("box", "French Reinforcements", 150, 340) + +box("SEA", "Sea1", 1277, 65, "blockade") +box("SEA", "Sea2", 1469, 514, "blockade") +box("SEA", "Sea3", 1213, 981, "blockade") +box("SEA", "Sea4", 1058, 1174, "blockade") +box("SEA", "Sea5", 947, 1395, "blockade") +box("SEA", "Sea6", 637, 2027, "blockade") +box("SEA", "Sea7", 359, 2358, "blockade") + +box("ALLIANCE", "French Alliance Track 0", 898, 2240) +box("ALLIANCE", "French Alliance Track 1", 964, 2240) +box("ALLIANCE", "French Alliance Track 2", 1030, 2240) +box("ALLIANCE", "French Alliance Track 3", 1096, 2240) +box("ALLIANCE", "French Alliance Track 4", 1162, 2240) +box("ALLIANCE", "French Alliance Track 5", 1228, 2240) +box("ALLIANCE", "French Alliance Track 6", 1294, 2240) +box("ALLIANCE", "French Alliance Track 7", 1360, 2240) +box("ALLIANCE", "French Alliance Track 8", 1426, 2240) +box("ALLIANCE", "French Alliance Track 9", 1492, 2240) + +box("TURN", "Game Turn 1775", 898, 2370) +box("TURN", "Game Turn 1776", 964, 2370) +box("TURN", "Game Turn 1777", 1030, 2370) +box("TURN", "Game Turn 1778", 1096, 2370) +box("TURN", "Game Turn 1779", 1162, 2370) +box("TURN", "Game Turn 1780", 1228, 2370) +box("TURN", "Game Turn 1781", 1294, 2370) +box("TURN", "Game Turn 1782", 1360, 2370) +box("TURN", "Game Turn 1783", 1426, 2370) + +box("CONTROL", "Canada", 1376, 967) +box("CONTROL", "NH", 1458, 1006) +box("CONTROL", "NY", 1375, 1081) +box("CONTROL", "MA", 1491, 1081) +box("CONTROL", "CT", 1455, 1156) +box("CONTROL", "RI", 1533, 1156) +box("CONTROL", "PA", 1299, 1194) +box("CONTROL", "NJ", 1376, 1189) +box("CONTROL", "MD", 1299, 1266) +box("CONTROL", "DE", 1376, 1266) +box("CONTROL", "VA", 1241, 1337) +box("CONTROL", "NC", 1260, 1407) +box("CONTROL", "SC", 1212, 1479) +box("CONTROL", "GA", 1164, 1550) + +function wilderness(A, B) { + line(A, B, "wilderness") +} + +function path(A, B) { + line(A, B, "path") +} + +function sea(space, zone) { + spaces[space_index[space]].port = zone +} + +wilderness("Quebec", "Falmouth") +wilderness("Fort Detroit", "Bassett Town") +wilderness("Bassett Town", "Point Pleasant") + +path("Quebec", "Montreal") +path("Montreal", "Ticonderoga") +path("Montreal", "Oswego") +path("Oswego", "Fort Niagara") +path("Oswego", "Fort Stanwix") +path("Fort Niagara", "Fort Detroit") +path("Fort Niagara", "Genesee") +path("Ticonderoga", "Norwich") +path("Ticonderoga", "Saratoga") +path("Norwich", "Brattleboro") +path("Norwich", "Concord") +path("Concord", "Falmouth") +path("Concord", "Brattleboro") +path("Saratoga", "Brattleboro") +path("Saratoga", "Albany") +path("Brattleboro", "Springfield") +path("Fort Stanwix", "Albany") +path("Albany", "Springfield") +path("Lexington Concord", "Springfield") +path("Lexington Concord", "Boston") +path("Falmouth", "Boston") +path("Barnstable", "Boston") +path("Newport", "Boston") +path("Newport", "Hartford") +path("Springfield", "Hartford") +path("New Haven", "Hartford") +path("New Haven", "New York") +path("New Haven", "Westchester") +path("New York", "Westchester") +path("New York", "Long Island") +path("Newport", "Lexington Concord") +path("Albany", "Westchester") +path("Fort Stanwix", "Genesee") +path("Genesee", "Wyoming Valley") +path("Fort Niagara", "Pittsburgh") +path("Morristown", "Westchester") +path("Morristown", "Wyoming Valley") +path("Morristown", "Reading") +path("Morristown", "New Brunswick") +path("Morristown", "New York") +path("New York", "New Brunswick") +path("Monmouth", "New Brunswick") +path("Philadelphia", "New Brunswick") +path("Monmouth", "Philadelphia") +path("Reading", "Philadelphia") +path("Wyoming Valley", "Reading") +path("Wilmington DE", "Philadelphia") +path("Wilmington DE", "Baltimore") +path("Bassett Town", "Pittsburgh") +path("Fort Cumberland", "Pittsburgh") +path("Harrisburg", "Pittsburgh") +path("Harrisburg", "York") +path("Reading", "York") +path("Harrisburg", "Reading") +path("Frederick Town", "York") +path("Frederick Town", "Baltimore") +path("Frederick Town", "Alexandria") +path("Frederick Town", "Charlottesville") +path("Frederick Town", "Fort Cumberland") +path("Richmond", "Alexandria") +path("Richmond", "Yorktown") +path("Richmond", "Petersburg") +path("Richmond", "Lynch's Ferry") +path("Richmond", "Charlottesville") +path("Richmond", "Norfolk") +path("Fincastle", "Charlottesville") +path("Fincastle", "Lynch's Ferry") +path("Fincastle", "Fort Chiswell") +path("Point Pleasant", "Fort Chiswell") +path("Abingdon", "Fort Chiswell") +path("Abingdon", "Gilbert Town") +path("Fort Prince George", "Gilbert Town") +path("Charlotte", "Gilbert Town") +path("Charlotte", "Salem") +path("Hillsboro", "Salem") +path("Cheraw", "Salem") +path("Lynch's Ferry", "Salem") +path("Charlotte", "Camden") +path("Abingdon", "Fort Prince George") +path("New Bern", "Norfolk") +path("Petersburg", "Norfolk") +path("Petersburg", "Wake (Raleigh)") +path("New Bern", "Wake (Raleigh)") +path("Hillsboro", "Wake (Raleigh)") +path("Petersburg", "Hillsboro") +path("Petersburg", "Lynch's Ferry") +path("Wilmington NC", "Wake (Raleigh)") +path("Wilmington NC", "Cheraw") +path("Wilmington NC", "Georgetown") +path("Cheraw", "Georgetown") +path("Cheraw", "Camden") +path("Ninety Six", "Fort Prince George") +path("Ninety Six", "Camden") +path("Ninety Six", "Augusta") +path("Eutaw Springs", "Camden") +path("Eutaw Springs", "Charleston") +path("Georgetown", "Charleston") +path("Savannah", "Charleston") +path("Alexandria", "Baltimore") +path("Savannah", "Augusta") +path("Savannah", "St. Mary's") +path("Charlottesville", "Lynch's Ferry") + +sea("Quebec", 1) +sea("Montreal", 1) + +sea("Falmouth", 2) +sea("Boston", 2) +sea("Barnstable", 2) +sea("Newport", 2) + +sea("New Haven", 3) +sea("New York", 3) +sea("Long Island", 3) + +sea("Philadelphia", 4) +sea("Wilmington DE", 4) + +sea("Baltimore", 5) +sea("Alexandria", 5) +sea("Yorktown", 5) +sea("Norfolk", 5) + +sea("New Bern", 6) +sea("Wilmington NC", 6) +sea("Charleston", 6) + +sea("Savannah", 7) +sea("St. Mary's", 7) + +// === CARDS === + +const CARD_3_OPS = { title: "3 OPS", type: "ops", count: 3 } +const CARD_2_OPS = { title: "2 OPS", type: "ops", count: 2 } +const CARD_1_OPS = { title: "1 OPS", type: "ops", count: 1 } + +const cards = [ + null, + + CARD_3_OPS, + CARD_3_OPS, + CARD_3_OPS, + CARD_3_OPS, + CARD_3_OPS, + CARD_3_OPS, + CARD_3_OPS, + CARD_3_OPS, + CARD_3_OPS, + CARD_3_OPS, + CARD_3_OPS, + CARD_3_OPS, + CARD_3_OPS, + CARD_3_OPS, + CARD_3_OPS, + CARD_3_OPS, + CARD_3_OPS, + CARD_3_OPS, + CARD_3_OPS, + CARD_3_OPS, + CARD_3_OPS, + CARD_3_OPS, + + CARD_2_OPS, + CARD_2_OPS, + CARD_2_OPS, + CARD_2_OPS, + CARD_2_OPS, + CARD_2_OPS, + CARD_2_OPS, + CARD_2_OPS, + CARD_2_OPS, + CARD_2_OPS, + CARD_2_OPS, + CARD_2_OPS, + CARD_2_OPS, + CARD_2_OPS, + CARD_2_OPS, + CARD_2_OPS, + CARD_2_OPS, + CARD_2_OPS, + CARD_2_OPS, + CARD_2_OPS, + CARD_2_OPS, + CARD_2_OPS, + + CARD_1_OPS, + CARD_1_OPS, + CARD_1_OPS, + CARD_1_OPS, + CARD_1_OPS, + CARD_1_OPS, + CARD_1_OPS, + CARD_1_OPS, + CARD_1_OPS, + CARD_1_OPS, + CARD_1_OPS, + CARD_1_OPS, + CARD_1_OPS, + CARD_1_OPS, + CARD_1_OPS, + CARD_1_OPS, + CARD_1_OPS, + CARD_1_OPS, + CARD_1_OPS, + CARD_1_OPS, + CARD_1_OPS, + CARD_1_OPS, + + /* 67 */ + { + title: "Minor Campaign", + type: "campaign", + count: 2, + }, + + /* 68 */ + { + title: "Minor Campaign", + type: "campaign", + count: 2, + }, + + /* 69 */ + { + title: "Minor Campaign", + type: "campaign", + count: 2, + }, + + /* 70 */ + { + title: "Major Campaign", + type: "campaign", + count: 3, + }, + + /* 71 */ + { + title: "North's Government Falls - The War Ends in 1779", + type: "mandatory-event", + event: "the_war_ends", + year: 1779, + }, + + /* 72 */ + { + title: "North's Government Falls - The War Ends in 1780", + type: "mandatory-event", + event: "the_war_ends", + year: 1780, + }, + + /* 73 */ + { + title: "North's Government Falls - The War Ends in 1781", + type: "mandatory-event", + event: "the_war_ends", + year: 1781, + }, + + /* 74 */ + { + title: "North's Government Falls - The War Ends in 1782", + type: "mandatory-event", + event: "the_war_ends", + year: 1782, + }, + + /* 75 */ + { + title: "North's Government Falls - The War Ends in 1783", + type: "mandatory-event", + event: "the_war_ends", + year: 1783, + }, + + /* 76 */ + { + title: "Henry Know Continental Artillery Commander", + type: "american-battle", + }, + + /* 77 */ + { + title: "Jane McCrea Indian Atrocity Sparks Outrage", + type: "american-event", + event: "place_american_pc", + count: 2, + once: true, + }, + + /* 78 */ + { + title: "Iroquois Uprising!", + type: "british-event", + event: "remove_american_pc_from_non_port", + where: [ NH, NY, PA ], + count: 2, + }, + + /* 79 */ + { + title: "Joseph Brant Leads an Iroquois Raid", + type: "british-event", + event: "remove_american_pc_from_non_port", + where: [ NH, NY, PA ], + count: 2, + }, + + /* 80 */ + { + title: "Lt. Colonel Simcoe's Queen's Rangers", + type: "british-event", + event: "remove_american_pc_within_two_spaces_of_a_british_general", + count: 2, + }, + + /* 81 */ + { + title: "D'Estaing Sails for the Caribbean", + type: "british-event", + when: "after_french_alliance", + event: "remove_french_navy", + }, + + /* 82 */ + { + title: "Banastre Tarleton's Waxhaws Massacre", + type: "british-event-or-battle", + event: "remove_american_cu", + }, + + /* 83 */ + { + title: "Lord George Germaine Offers Royal Amnesty", + type: "british-event", + event: "remove_american_pc", + count: 2, + }, + + /* 84 */ + { + title: "George Rogers Clark Leads a Western Offensive", + type: "american-event", + event: "remove_random_british_card", + }, + + /* 85 */ + { + title: "Don Bernardo Galvez Captures Pensacola", + type: "american-event", + when: "european_war_in_effect", + event: "remove_british_cu", + count: 2, + }, + + /* 86 */ + { + title: "Baron von Steuben Trains the Continental Army", + type: "american-event", + event: "baron_von_steuben_trains_the_continental_army", + }, + + /* 87 */ + { + title: "Lord North Offers a Royal Amnesty", + type: "british-event", + event: "remove_american_pc", + count: 4, + once: true, + }, + + /* 88 */ + { + title: "The Swamp Fox, Francis Marion", + type: "american-event", + event: "remove_british_pc_from", + where: [ NC, SC, GA ], + count: 2, + }, + + /* 89 */ + { + title: "The Gamecock: Thomas Sumter", + type: "american-event", + event: "remove_british_pc_from", + where: [ NC, SC, GA ], + count: 2, + }, + + /* 90 */ + { + title: "Josiah Martin Rallies North Carolina Loyalists", + type: "british-event", + event: "remove_american_pc_from", + where: [ NC ], + count: 2, + }, + + /* 91 */ + { + title: 'Thomas Paine Publishes Pamphlets "Common Sense" and "The American Crisis"', + type: "american-event", + event: "place_american_pc_in", + where: [ NH, NY, MA, CT, RI, PA, NJ, MD, DE, VA, NC, SC, GA ], + count: 3, + once: true, + }, + + /* 92 */ + { + title: "Nathan Hale, American Martyr", + type: "american-event", + event: "place_american_pc", + count: 2, + once: true, + }, + + /* 93 */ + { + title: "John Glover's Marblehead Regiment", + type: "american-event", + event: "john_glovers_marblehead_regiment", + }, + + /* 94 */ + { + title: "Pennsylvania and New Jersey Line Mutinies", + type: "british-event", + event: "pennsylvania_and_new_jersey_line_mutinies", + }, + + /* 95 */ + { + title: "William Pitt Urges Peace Talks", + type: "british-event", + when: "before_french_alliance", + event: "remove_american_pc", + count: 2, + reshuffle: "if_played", + }, + + /* 96 */ + { + title: "Hortelez et Cie: Clandestine French and Spanish Aid", + type: "american-event", + when: "before_french_alliance", + event: "advance_french_alliance", + reshuffle: "if_discarded", + count: 2, + }, + + /* 97 */ + { + title: "Admiral Suffren Wins a Naval Victory", + type: "american-event", + when: "after_french_alliance", + event: "remove_random_british_card", + }, + + /* 98 */ + { + title: '"Mad" Anthony Wayne', + type: "american-battle", + }, + + /* 99 */ + { + title: "Declaration of Independence", + type: "mandatory-event", + event: "declaration_of_independence", + once: true, + reshuffle: "if_played", + tournament: true, + }, + + /* 100 */ + { + title: "Benedict Arnold's Treason Undermines the Patriot Cause", + type: "british-battle", + event: "remove_benedict_arnold", + once: true, + }, + + /* 101 */ + { + title: "Benjamin Franklin: Minister to France", + type: "mandatory-event", + event: "advance_french_alliance", + count: 4, + once: true, + tournament: true, + }, + + /* 102 */ + { + title: "Admiral Rodney Captures St. Eustatius", + type: "british-event", + event: "remove_random_american_card", + when: "european_war_in_effect", + once: true, + }, + + /* 103 */ + { + title: "Thaddeus Kosciuszco Constructs Engineering Works", + type: "american-battle", + }, + + /* 104 */ + { + title: "Light Horse Harry Lee", + type: "american-battle", + }, + + /* 105 */ + { + title: "Morgan's Riflemen", + type: "american-battle", + }, + + /* 106 */ + { + title: "John Paul Jones' Shipping Raids", + type: "american-event", + event: "remove_random_british_card", + }, + + /* 107 */ + { + title: "British Light Infantry", + type: "british-battle", + }, + + /* 108 */ + { + title: "Lord Sandwich Coastal Raids", + type: "british-event", + event: "lord_sandwich_coastal_raids", + }, + + /* 109 */ + { + title: "Edward Bancroft, British Double Agent", + type: "british-event", + event: "remove_random_american_card", + }, + + /* 110 */ + { + title: "Hessian Infantry Bayonet Charge", + type: "british-battle", + }, +] + +data = { + BOXES, + cards, + general_index, + generals, + space_index, + spaces, + colony_name, + colony_spaces, } + +})() + +if (typeof module === "object") + module.exports = data @@ -11,7 +11,6 @@ <link rel="stylesheet" href="play.css"> <script defer src="/common/client.js"></script> <script defer src="data.js"></script> -<script defer src="cards.js"></script> <script defer src="play.js"></script> </script> </head> @@ -1,20 +1,32 @@ "use strict" -const THE_13_COLONIES = [ "NH", "NY", "MA", "CT", "RI", "PA", "NJ", "MD", "DE", "VA", "NC", "SC", "GA" ] -const ALL_COLONIES = THE_13_COLONIES.concat([ "CA" ]) -const AMERICAN_GENERALS = [ "Arnold", "Gates", "Greene", "Lafayette", "Lee", "Lincoln", "Washington" ] -const CONTINENTAL_CONGRESS_DISPERSED = "Continental Congress Dispersed" +const PC_NONE = 0 +const PC_BRITISH = 1 +const PC_AMERICAN = 2 + +const GENERALS = data.generals +const CARDS = data.cards +const SPACES = data.spaces +const COLONIES = data.colony_spaces +const BOXES = data.BOXES + +const CONTINENTAL_CONGRESS_DISPERSED = data.space_index["Continental Congress Dispersed"] +const BLOCKADE_ZONES = [ "Sea1", "Sea2", "Sea3", "Sea4", "Sea5", "Sea6", "Sea7" ] const BRITISH = "British" const AMERICAN = "American" -const BLOCKADE_ZONES = [ "Sea1", "Sea2", "Sea3", "Sea4", "Sea5", "Sea6", "Sea7" ] + +const space_count = 66 +const general_count = data.generals.length let ui = { cards: {}, - spaces: {}, - generals: {}, - control: {}, - blockade: {}, + spaces: [], + generals: [], + pc: [], cu: [], + control: [], + + blockade: {}, } function on_focus_card_tip(card_number) { @@ -43,6 +55,7 @@ function on_log(text) { text = text.replace(/#(\d+)/g, '<span class="tip" onmouseenter="on_focus_card_tip($1)" onmouseleave="on_blur_card_tip()">$&</span>') + // text = text.replace(/%(\d+)/g, sub_space_name) if (text.match(/^\.h1 /)) { p.className = "h1" @@ -70,12 +83,12 @@ function onHoverCard(X) { } function onFocusNode(evt) { - let space = SPACES[evt.target.id] + let space = SPACES[evt.target.my_id] document.getElementById("status").textContent = space.name } function onBlurNode(evt) { - let space = SPACES[evt.target.id] + let space = SPACES[evt.target.my_id] document.getElementById("status").textContent = "" } @@ -91,7 +104,7 @@ function build_marker(container, id, x, y, w, h, classList) { e.classList.add("marker") for (let c of classList) e.classList.add(c) - e.setAttribute("id", id) + e.my_id = id e.style.left = ((x - w / 2) | 0) + "px" e.style.top = ((y - h / 2) | 0) + "px" document.getElementById(container).appendChild(e) @@ -99,7 +112,11 @@ function build_marker(container, id, x, y, w, h, classList) { } function update_marker(e, space) { - let box = SPACES[space] || BOXES[space] + let box + if (typeof space === "number") + box = SPACES[space] + else + box = BOXES[space] e.style.left = ((box.x - e.foo.w / 2) | 0) + "px" e.style.top = ((box.y - e.foo.h / 2) | 0) + "px" } @@ -143,17 +160,17 @@ function build_map() { return e } - for (let name in SPACES) { - let space = SPACES[name] + for (let i = 0; i < SPACES.length; ++i) { + let space = SPACES[i] if (space.colony != null) { - let e = buildNode(space.type, space.x, space.y, SPACES[name].colony) - e.setAttribute("id", name) + let e = buildNode(space.type, space.x, space.y, space.colony) + e.my_id = i e.addEventListener("mouseenter", onFocusNode) e.addEventListener("mouseleave", onBlurNode) e.addEventListener("click", on_space) - ui.spaces[name] = e + ui.spaces[i] = e document.getElementById("spaces").appendChild(e) - build_marker("pc", name + "-pc", space.x, space.y, 67, 58.5, [ "pc" ]) + ui.pc[i] = build_marker("pc", name + "-pc", space.x, space.y, 67, 58.5, [ "pc" ]) } } @@ -182,8 +199,8 @@ function build_map() { ui.french_navy = build_marker( "markers", "FrenchNavy", - BOXES["French Reinforcements"].x - 130 / 2 - 10, - BOXES["French Reinforcements"].y - 32, + SPACES[data.space_index["French Reinforcements"]].x - 130 / 2 - 10, + SPACES[data.space_index["French Reinforcements"]].y - 32, 126 / 2, 252 / 2, [ "french-navy" ] @@ -192,15 +209,16 @@ function build_map() { ui.congress = build_marker( "markers", "Congress", - SPACES["Philadelphia"].x, - SPACES["Philadelphia"].y, + SPACES[data.space_index["Philadelphia"]].x, + SPACES[data.space_index["Philadelphia"]].y, 113 / 2, 113 / 2, [ "congress" ] ) - for (let c in COLONIES) { - ui.control[c] = build_marker("markers", "control_" + c, BOXES[c].x, BOXES[c].y, 38 + 8, 38 + 8, [ "control" ]) + for (let c = 0; c <= 13; ++c) { + let name = data.colony_name[c] + ui.control[c] = build_marker("markers", "control_" + name, BOXES[name].x, BOXES[name].y, 38 + 8, 38 + 8, [ "control" ]) } for (let c = 1; c <= 110; ++c) { @@ -208,9 +226,10 @@ function build_map() { ui.cards[c].addEventListener("click", on_card) } - for (let g in GENERALS) { + for (let g = 0; g < general_count; ++g) { let color = GENERALS[g].owner.toLowerCase() - ui.generals[g] = build_marker("generals", g, 0, 0, 126 / 2, 252 / 2, [ "general", color, g, "offmap" ]) + let name = GENERALS[g].name + ui.generals[g] = build_marker("generals", g, 0, 0, 126 / 2, 252 / 2, [ "general", color, name, "offmap" ]) ui.generals[g].addEventListener("click", on_general) } } @@ -237,40 +256,43 @@ function update_units() { else ui.french_alliance.classList.remove("european-war") - if (view.french_navy == "French Reinforcements") { - let x = BOXES["French Reinforcements"].x - 130 / 2 - 10 - let y = BOXES["French Reinforcements"].y - 32 + if (view.french_navy < 0) { + let x = SPACES[data.space_index["French Reinforcements"]].x - 130 / 2 - 10 + let y = SPACES[data.space_index["French Reinforcements"]].y - 32 let w = 126 / 2 let h = 252 / 2 ui.french_navy.style.left = ((x - w / 2) | 0) + "px" ui.french_navy.style.top = ((y - h / 2) | 0) + "px" } else { - update_marker(ui.french_navy, view.french_navy) + let s + if (view.french_navy > 1700) + s = "Game Turn "+ view.french_navy + else + s = "Sea "+ view.french_navy + update_marker(ui.french_navy, s) } - for (let space in SPACES) { + for (let space = 0; space < space_count; ++space) { let space_pc = view.pc[space] - let e = document.getElementById(space + "-pc") - if (e) { - if (space_pc === BRITISH) { - e.classList.remove("american") - e.classList.add("british") - } else if (space_pc === AMERICAN) { - e.classList.add("american") - e.classList.remove("british") - } else { - e.classList.remove("american") - e.classList.remove("british") - } + let e = ui.pc[space] + if (space_pc === PC_BRITISH) { + e.classList.remove("american") + e.classList.add("british") + } else if (space_pc === PC_AMERICAN) { + e.classList.add("american") + e.classList.remove("british") + } else { + e.classList.remove("american") + e.classList.remove("british") } } - for (let c in COLONIES) { + for (let c = 0; c <= 13; ++c) { let control = 0 for (let space of COLONIES[c]) { - if (view.pc[space] == BRITISH) + if (view.pc[space] == PC_BRITISH) --control - else if (view.pc[space] == AMERICAN) + else if (view.pc[space] == PC_AMERICAN) ++control } if (control < 0) @@ -284,8 +306,8 @@ function update_units() { let offset = {} for (let g in GENERALS) { let e = ui.generals[g] - let unit = view.generals[g] - let space = SPACES[unit.location] || BOXES[unit.location] + let loc = view.generals[g] + let space = SPACES[loc] || BOXES[loc] if (space) { let o = offset[space.name] | 0 update_marker_xy(e, space.x + o * generalX, space.y + o * generalY - 32) @@ -320,7 +342,7 @@ build_map() function player_info(player, nc, nq) { let info = "" if (player == AMERICAN) { - if (view.pennsylvania_and_new_jersey_line_mutinies || view.congress == CONTINENTAL_CONGRESS_DISPERSED) + if (view.pennsylvania_and_new_jersey_line_mutinies || view.congress === CONTINENTAL_CONGRESS_DISPERSED) info += "\u{1f6ab} " } if (nq > 0) @@ -390,10 +412,10 @@ function on_update() { ui.cards[c].classList.remove("show") } - for (let space in SPACES) + for (let space = 0; space < space_count; ++space) ui.spaces[space].classList.remove("enabled") - for (let general in GENERALS) + for (let general = 0; general < general_count; ++general) ui.generals[general].classList.remove("enabled") for (let zone of BLOCKADE_ZONES) @@ -404,6 +426,7 @@ function on_update() { if (player != view.active) return + if (view.actions) for (let action of Object.keys(view.actions)) { let args = view.actions[action] switch (action) { @@ -518,7 +541,7 @@ function get_action_from_arg(x) { function on_space(evt) { if (view.actions) { - let space = evt.target.id + let space = evt.target.my_id let action = get_action_from_arg(space) if (action) send_action(action, space) @@ -527,7 +550,7 @@ function on_space(evt) { function on_general(evt) { if (view.actions) { - let general = evt.target.id + let general = evt.target.my_id let action = get_action_from_arg(general) if (action) send_action(action, general) @@ -7,47 +7,77 @@ exports.scenarios = [ "Historical" ] exports.roles = [ "American", "British" ] -const CARDS = require("./cards") -const DATA = require("./data") -const SPACES = DATA.SPACES -const COLONIES = DATA.COLONIES -const GENERALS = DATA.GENERALS -const BLOCKADE = DATA.BLOCKADE -const PATH_INDEX = DATA.PATH_INDEX -const PATH_NAME = DATA.PATH_NAME -const PATH_TYPE = DATA.PATH_TYPE +const data = require("./data") + +const CARDS = data.cards +const SPACES = data.spaces +const COLONIES = data.colony_spaces +const GENERALS = data.generals + +const Canada = 0 +const NH = 1 +const NY = 2 +const MA = 3 +const CT = 4 +const RI = 5 +const PA = 6 +const NJ = 7 +const MD = 8 +const DE = 9 +const VA = 10 +const NC = 11 +const SC = 12 +const GA = 13 const BRITISH = "British" const AMERICAN = "American" const FRENCH = "French" -const BRITISH_GENERALS = [ "Burgoyne", "Carleton", "Clinton", "Cornwallis", "Howe" ] -const AMERICAN_GENERALS = [ "Arnold", "Gates", "Greene", "Lafayette", "Lee", "Lincoln", "Washington", "Rochambeau" ] -const WASHINGTON = "Washington" -const ROCHAMBEAU = "Rochambeau" -const ARNOLD = "Arnold" - -const CAPTURED_GENERALS = "Captured Generals" -const CONTINENTAL_CONGRESS_DISPERSED = "Continental Congress Dispersed" -const BRITISH_REINFORCEMENTS = "British Leader Reinforcements" -const AMERICAN_REINFORCEMENTS = "American Leader Reinforcements" -const FRENCH_REINFORCEMENTS = "French Reinforcements" -const TURN_TRACK = { - 1775: "Game Turn 1775", - 1776: "Game Turn 1776", - 1777: "Game Turn 1777", - 1778: "Game Turn 1778", - 1779: "Game Turn 1779", - 1780: "Game Turn 1780", - 1781: "Game Turn 1781", - 1782: "Game Turn 1782", - 1783: "Game Turn 1783", -} - -const FALMOUTH_QUEBEC = "Falmouth/Quebec" - -const THE_13_COLONIES = [ "NH", "NY", "MA", "CT", "RI", "PA", "NJ", "MD", "DE", "VA", "NC", "SC", "GA" ] -const SOUTH_OF_WINTER_ATTRITION_LINE = [ "NC", "SC", "GA" ] +const PC_NONE = 0 +const PC_BRITISH = 1 +const PC_AMERICAN = 2 + +const AMERICAN_GENERALS = [ 0, 1, 2, 3, 4, 5, 6, 7 ] +const BRITISH_GENERALS = [ 8, 9, 10, 11, 12 ] + +const NOBODY = -1 +const ARNOLD = data.general_index["Arnold"] +const BURGOYNE = data.general_index["Burgoyne"] +const CLINTON = data.general_index["Clinton"] +const CORNWALLIS = data.general_index["Cornwallis"] +const GATES = data.general_index["Gates"] +const GREENE = data.general_index["Greene"] +const LAFAYETTE = data.general_index["Lafayette"] +const LEE = data.general_index["Lee"] +const LINCOLN = data.general_index["Lincoln"] +const ROCHAMBEAU = data.general_index["Rochambeau"] +const WASHINGTON = data.general_index["Washington"] +const CARLETON = data.general_index["Carleton"] +const HOWE = data.general_index["Howe"] + +const NOWHERE = -1 +const BOSTON = data.space_index["Boston"] +const CHARLESTON = data.space_index["Charleston"] +const FORT_DETROIT = data.space_index["Fort Detroit"] +const GILBERT_TOWN = data.space_index["Gilbert Town"] +const LEXINGTON_CONCORD = data.space_index["Lexington Concord"] +const MONTREAL = data.space_index["Montreal"] +const NEWPORT = data.space_index["Newport"] +const NINETY_SIX = data.space_index["Ninety Six"] +const NORFOLK = data.space_index["Norfolk"] +const PHILADELPHIA = data.space_index["Philadelphia"] +const QUEBEC = data.space_index["Quebec"] +const WILMINGTON_NC = data.space_index["Wilmington NC"] + +const CAPTURED_GENERALS = data.space_index["Captured Generals"] +const CONTINENTAL_CONGRESS_DISPERSED = data.space_index["Continental Congress Dispersed"] +const BRITISH_REINFORCEMENTS = data.space_index["British Leader Reinforcements"] +const AMERICAN_REINFORCEMENTS = data.space_index["American Leader Reinforcements"] +const FRENCH_REINFORCEMENTS = data.space_index["French Reinforcements"] +const ELIMINATED = data.spaces.length + +const THE_13_COLONIES = [ 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13 ] +const SOUTH_OF_WINTER_ATTRITION_LINE = [ 11, 12, 13 ] const CAMPAIGN_CARDS = [ 67, 68, 69, 70 ] const DECLARATION_OF_INDEPENDENCE = 99 @@ -55,9 +85,10 @@ const BARON_VON_STEUBEN = 86 const WAR_ENDS_1779 = 71 const BENJAMIN_FRANKLIN = 101 -const ENEMY = { American: BRITISH, British: AMERICAN } +const space_count = 66 +const all_spaces = new Array(space_count).fill(0).map((_,i)=>i) -const default_options = {} +const ENEMY = { American: BRITISH, British: AMERICAN } let states = {} let events = {} @@ -65,107 +96,28 @@ let events = {} let game let view -function random(n) { - return (game.seed = (game.seed * 200105) % 34359738337) % n -} - -function logbr() { - if (game.log.length > 0 && game.log[game.log.length - 1] !== "") - game.log.push("") -} - -function log(s) { - game.log.push(s) -} - -function logp(s) { - game.log.push(game.active[0] + " " + s) -} - -function object_copy(original) { - if (Array.isArray(original)) { - let n = original.length - let copy = new Array(n) - for (let i = 0; i < n; ++i) { - let v = original[i] - if (typeof v === "object" && v !== null) - copy[i] = object_copy(v) - else - copy[i] = v - } - return copy - } else { - let copy = {} - for (let i in original) { - let v = original[i] - if (typeof v === "object" && v !== null) - copy[i] = object_copy(v) - else - copy[i] = v - } - return copy - } -} - -function clear_undo() { - if (game.undo) { - game.undo.length = 0 - } -} - -function push_undo() { - if (game.undo) { - let copy = {} - for (let k in game) { - let v = game[k] - if (k === "undo") - continue - else if (k === "log") - v = v.length - else if (typeof v === "object" && v !== null) - v = object_copy(v) - copy[k] = v - } - game.undo.push(copy) - } -} - -function pop_undo() { - if (game.undo) { - let save_log = game.log - let save_undo = game.undo - game = save_undo.pop() - save_log.length = game.log - game.log = save_log - game.undo = save_undo - } -} - -function remove_from_array(array, item) { - let i = array.indexOf(item) - if (i >= 0) - array.splice(i, 1) -} - function setup_game(seed) { game = { seed: seed, year: 1775, - congress: "Philadelphia", + congress: data.space_index["Philadelphia"], french_alliance: 0, french_alliance_triggered: false, european_war: false, - french_navy: FRENCH_REINFORCEMENTS, + french_navy: -1, regulars: true, war_ends: 0, played_british_reinforcements: 0, played_american_reinforcements: [], - pc: {}, - generals: {}, - moved: {}, + pc: new Array(space_count).fill(PC_NONE), + generals: new Array(general_count).fill(NOWHERE), + moved: [], cu: [], - control: {}, + + // TODO: compute on the fly + control: [], + deck: create_deck(), discard: [], @@ -182,22 +134,13 @@ function setup_game(seed) { actions: [], } - function spawn_unit(owner, location, pc, cu, name) { + function spawn_unit(owner, location, pc, cu, general=NOBODY) { if (pc) - game.pc[location] = owner - if (name) { - game.generals[name] = { - location: location, - } - } - if (cu > 0) { - game.cu.push({ - owner: owner, - location: location, - count: cu, - moved: 0, - }) - } + set_space_pc(location, pc) + if (general !== NOBODY) + set_general_location(general, location) + if (cu > 0) + spawn_cu(owner, location, count) } function british(place, pc, cu, ld) { @@ -210,31 +153,31 @@ function setup_game(seed) { spawn_unit(FRENCH, place, pc, cu, ld) } - british("Quebec", true, 2, "Carleton") - british("Montreal", true) - british("Fort Detroit", true, 1) - british("Boston", true, 5, "Howe") - british("Norfolk", true) - british("Gilbert Town", true) - british("Wilmington NC", true) - british("Ninety Six", true) + british(QUEBEC, PC_BRITISH, 2, CARLETON) + british(MONTREAL, PC_BRITISH) + british(FORT_DETROIT, PC_BRITISH, 1) + british(BOSTON, PC_BRITISH, 5, HOWE) + british(NORFOLK, PC_BRITISH) + british(GILBERT_TOWN, PC_BRITISH) + british(WILMINGTON_NC, PC_BRITISH) + british(NINETY_SIX, PC_BRITISH) - american("Lexington Concord", true, 5, "Washington") - american("Newport", false, 2, "Greene") - american("Charleston", true, 2) - american("Philadelphia", true) + american(LEXINGTON_CONCORD, PC_AMERICAN, 5, WASHINGTON) + american(NEWPORT, PC_NONE, 2, GREENE) + american(CHARLESTON, PC_AMERICAN, 2) + american(PHILADELPHIA, PC_AMERICAN) - british(BRITISH_REINFORCEMENTS, false, 0, "Burgoyne") - british(BRITISH_REINFORCEMENTS, false, 0, "Clinton") - british(BRITISH_REINFORCEMENTS, false, 0, "Cornwallis") + british(BRITISH_REINFORCEMENTS, PC_NONE, 0, BURGOYNE) + british(BRITISH_REINFORCEMENTS, PC_NONE, 0, CLINTON) + british(BRITISH_REINFORCEMENTS, PC_NONE, 0, CORNWALLIS) - american(AMERICAN_REINFORCEMENTS, false, 0, "Arnold") - american(AMERICAN_REINFORCEMENTS, false, 0, "Lincoln") - american(AMERICAN_REINFORCEMENTS, false, 0, "Gates") - american(AMERICAN_REINFORCEMENTS, false, 0, "Lee") - american(AMERICAN_REINFORCEMENTS, false, 0, "Lafayette") + american(AMERICAN_REINFORCEMENTS, PC_NONE, 0, ARNOLD) + american(AMERICAN_REINFORCEMENTS, PC_NONE, 0, LINCOLN) + american(AMERICAN_REINFORCEMENTS, PC_NONE, 0, GATES) + american(AMERICAN_REINFORCEMENTS, PC_NONE, 0, LEE) + american(AMERICAN_REINFORCEMENTS, PC_NONE, 0, LAFAYETTE) - french(FRENCH_REINFORCEMENTS, false, 5, "Rochambeau") + french(FRENCH_REINFORCEMENTS, PC_NONE, 5, ROCHAMBEAU) goto_committees_of_correspondence() } @@ -275,7 +218,7 @@ function deal_card() { function last_discard() { if (game.discard.length > 0) return game.discard[game.discard.length - 1] - return null + return 0 } function active_hand() { @@ -289,7 +232,7 @@ function play_card(c, reason) { log(game.active[0] + " played #" + c) if (CARDS[c].reshuffle === "if_played") game.reshuffle = true - remove_from_array(active_hand(), c) + array_remove_item(active_hand(), c) game.last_played = c if (!CARDS[c].once) game.discard.push(c) @@ -298,7 +241,7 @@ function play_card(c, reason) { } function discard_card_from_hand(hand, c) { - remove_from_array(hand, c) + array_remove_item(hand, c) game.discard.push(c) if (CARDS[c].reshuffle === "if_discarded") game.reshuffle = true @@ -345,7 +288,7 @@ function can_play_reinforcements() { if (game.played_british_reinforcements === 0) { let n = count_british_cu(BRITISH_REINFORCEMENTS) for (let g of BRITISH_GENERALS) - if (game.generals[g].location === BRITISH_REINFORCEMENTS) + if (is_general_at_location(g, BRITISH_REINFORCEMENTS)) ++n return n > 0 } @@ -356,14 +299,17 @@ function can_play_reinforcements() { return false } +function is_map_space(s) { + return s >= 0 && s <= 65 +} + function is_port(where) { return SPACES[where].port } function is_non_blockaded_port(where) { - if (SPACES[where].port && BLOCKADE[where] !== game.french_navy) - return true - return false + let port = SPACES[where].port + return (port > 0 && port !== game.french_navy) } function is_fortified_port(where) { @@ -376,7 +322,7 @@ function is_continental_congress_dispersed() { function is_winter_quarter_space(where) { let colony = SPACES[where].colony - if (colony === "GA" || colony === "SC" || colony === "NC") + if (set_has(SOUTH_OF_WINTER_ATTRITION_LINE, colony)) return true // south of winter attrition line let type = SPACES[where].type if (type === "winter-quarters" || type === "fortified-port") @@ -393,11 +339,11 @@ function allowed_to_place_american_pc() { } function is_british_militia(space) { - return game.control[SPACES[space].colony] === BRITISH + return game.control[SPACES[space].colony] === PC_BRITISH } function is_american_militia(space) { - return game.control[SPACES[space].colony] === AMERICAN + return game.control[SPACES[space].colony] === PC_AMERICAN } function is_american_winter_offensive() { @@ -408,16 +354,20 @@ function is_american_winter_offensive() { /* PC */ +function set_space_pc(space, pc) { + game.pc[space] = pc +} + function has_no_pc(space) { - return game.pc[space] !== BRITISH && game.pc[space] !== AMERICAN + return game.pc[space] === PC_NONE } function has_british_pc(space) { - return game.pc[space] === BRITISH + return game.pc[space] === PC_BRITISH } function has_american_pc(space) { - return game.pc[space] === AMERICAN + return game.pc[space] === PC_AMERICAN } function has_enemy_pc(space) { @@ -428,11 +378,11 @@ function has_enemy_pc(space) { } function is_adjacent_to_british_pc(a) { - for (let b of SPACES[a].exits) + for (let b of SPACES[a].adjacent) if (has_british_pc(b)) return true if (SPACES[a].port) { - for (let b in SPACES) { + for (let b of all_spaces) { if (SPACES[b].port) if (has_british_pc(b)) return true @@ -442,7 +392,7 @@ function is_adjacent_to_british_pc(a) { } function is_adjacent_to_american_pc(a) { - for (let b of SPACES[a].exits) + for (let b of SPACES[a].adjacent) if (has_american_pc(b)) return true return false @@ -451,13 +401,13 @@ function is_adjacent_to_american_pc(a) { function place_british_pc(space) { logp("placed PC in " + space) if (game.british_pc_space_list) - remove_from_array(game.british_pc_space_list, space) - game.pc[space] = BRITISH + array_remove_item(game.british_pc_space_list, space) + set_space_pc(space, PC_BRITISH) } function place_american_pc(space) { logp("placed PC in " + space) - game.pc[space] = AMERICAN + set_space_pc(space, PC_AMERICAN) } function remove_pc(space) { @@ -465,7 +415,7 @@ function remove_pc(space) { logp("removed PC in " + space) else logp("removed PC in " + space) - game.pc[space] = undefined + set_space_pc(space, PC_NONE) } function flip_pc(space) { @@ -473,24 +423,27 @@ function flip_pc(space) { logp("flipped PC in " + space) else logp("flipped PC in " + space) - game.pc[space] = ENEMY[game.pc[space]] + if (has_british_pc(space)) + set_space_pc(space, PC_AMERICAN) + else + set_space_pc(space, PC_BRITISH) } function update_colony_control() { - for (let c in COLONIES) { + for (let c = 0; c <= 13; ++c) { let control = 0 for (let space of COLONIES[c]) { - if (game.pc[space] === BRITISH) + if (has_british_pc(space)) --control - else if (game.pc[space] === AMERICAN) + else if (has_american_pc(space)) ++control } if (control < 0) - game.control[c] = BRITISH + game.control[c] = PC_BRITISH else if (control > 0) - game.control[c] = AMERICAN + game.control[c] = PC_AMERICAN else - game.control[c] = undefined + game.control[c] = PC_NONE } } @@ -505,6 +458,11 @@ function find_cu(owner, space) { return null } +function reset_moved_cu() { + for (let cu of game.cu) + cu.moved = 0 +} + function find_british_cu(space) { return find_cu(BRITISH, space) } @@ -605,7 +563,7 @@ function remove_cu(owner, where, count) { let cu = find_cu(owner, where) if (count >= cu.count) { let i = game.cu.indexOf(cu) - remove_from_array(game.cu, cu) + array_remove_item(game.cu, cu) } else { cu.count -= count } @@ -655,38 +613,54 @@ function move_british_cu(from, to, count) { /* GENERALS */ +function location_of_general(g) { + return game.generals[g] +} + +function set_general_location(g, s) { + game.generals[g] = s +} + +function is_general_at_location(g, s) { + return location_of_general(g) === s +} + function is_general_on_map(g) { - switch (game.generals[g].location) { - case null: /* killed */ - case CAPTURED_GENERALS: - case BRITISH_REINFORCEMENTS: - case AMERICAN_REINFORCEMENTS: - case FRENCH_REINFORCEMENTS: - return false - } - return true + return is_map_space(location_of_general(g)) +} + +function has_general_moved(g) { + return set_has(game.moved, g) +} + +function set_general_moved(g) { + set_add(game.moved, g) +} + +function reset_moved_generals() { + set_clear(game.moved) } function find_british_general(where) { for (let general of BRITISH_GENERALS) - if (game.generals[general].location === where) + if (is_general_at_location(general, where)) return general - return null + return NOBODY } function find_american_or_french_general(where) { for (let general of AMERICAN_GENERALS) - if (game.generals[general].location === where) + if (is_general_at_location(general, where)) return general - return null + return NOBODY } function has_british_general(where) { - return find_british_general(where) !== null + return find_british_general(where) !== NOBODY } function has_american_or_french_general(where) { - return find_american_or_french_general(where) !== null + return find_american_or_french_general(where) !== NOBODY } function has_enemy_general(where) { @@ -704,7 +678,7 @@ function count_friendly_generals(where) { list = AMERICAN_GENERALS let count = 0 for (let g of list) - if (location_of_general(g) === where) + if (is_general_at_location(g, where)) ++count return count } @@ -754,11 +728,11 @@ function can_activate_american_general(c) { } function move_general(who, where) { - game.generals[who].location = where + set_general_location(who, where) } function capture_washington() { - game.generals[WASHINGTON].location = null + set_general_location(WASHINGTON, ELIMINATED) if (!game.french_alliance_triggered) { game.french_alliance -= 3 @@ -792,9 +766,9 @@ function capture_enemy_general(where) { } function remove_benedict_arnold() { - if (game.generals[ARNOLD].location) { + if (!is_general_at_location(ARNOLD, ELIMINATED)) { log("Removed Arnold from the game!") - game.generals[ARNOLD].location = null + set_general_location(ARNOLD, ELIMINATED) } } @@ -830,10 +804,10 @@ function has_no_american_unit(where) { function place_british_reinforcements(who, count, where) { let already_there = find_british_general(where) - if (who && already_there) { + if (who !== NOBODY && already_there !== NOBODY) { move_general(already_there, BRITISH_REINFORCEMENTS) } - if (who) { + if (who !== NOBODY) { logp("reinforced " + where + " with " + who) move_general(who, where) } @@ -848,15 +822,16 @@ function place_british_reinforcements(who, count, where) { } function place_american_reinforcements(who, count, where) { +console.log("PLACE AM", who, count, where) let already_there = find_american_or_french_general(where) - if (who && already_there) { + if (who !== NOBODY && already_there !== NOBODY) { // Never replace Washington if (already_there === WASHINGTON) - who = null + who = NOBODY else move_general(already_there, AMERICAN_REINFORCEMENTS) } - if (who) { + if (who !== NOBODY) { logp("reinforced " + where + " with " + who) move_general(who, where) } @@ -868,14 +843,14 @@ function place_american_reinforcements(who, count, where) { function place_french_reinforcements(who, where) { let already_there = find_american_or_french_general(where) - if (who && already_there) { + if (who !== NOBODY && already_there !== NOBODY) { // Never replace Washington if (already_there === WASHINGTON) - who = null + who = NOBODY else move_general(already_there, AMERICAN_REINFORCEMENTS) } - if (who) { + if (who !== NOBODY) { logp("reinforced " + where + " with " + who) move_general(who, where) } @@ -884,10 +859,6 @@ function place_french_reinforcements(who, where) { move_cu(FRENCH, AMERICAN_REINFORCEMENTS, where, count_french_cu(AMERICAN_REINFORCEMENTS)) } -function location_of_general(g) { - return game.generals[g].location -} - function pickup_max_british_cu(where) { game.carry_british = count_unmoved_british_cu(where) if (game.carry_british > 5) @@ -1017,7 +988,7 @@ function gen_remove_british_pc_from(list_of_colonies) { } function gen_remove_american_pc() { - for (let space in SPACES) { + for (let space of all_spaces) { if (has_american_pc(space) && has_no_american_unit(space)) { gen_action("remove_pc", space) } @@ -1047,26 +1018,26 @@ function gen_remove_american_pc_from_non_port(list_of_colonies) { } function gen_remove_american_pc_within_two_spaces_of_a_british_general() { - let candidates = {} + let candidates = [] for (let g of BRITISH_GENERALS) { - let a = game.generals[g].location - if (a in SPACES) { - candidates[a] = true - for (let b of SPACES[a].exits) { - candidates[b] = true - for (let c of SPACES[b].exits) { - candidates[c] = true + let a = location_of_general(g) + if (is_map_space(a)) { + set_add(candidates, a) + for (let b of SPACES[a].adjacent) { + set_add(candidates, b) + for (let c of SPACES[b].adjacent) { + set_add(candidates, c) } } } } - for (let space in candidates) + for (let space of candidates) if (has_american_pc(space) && has_no_american_unit(space)) gen_action("remove_pc", space) } function gen_place_american_pc() { - for (let space in SPACES) { + for (let space of all_spaces) { if (has_no_pc(space) && has_no_british_playing_piece(space)) { gen_action("place_american_pc", space) } @@ -1095,7 +1066,7 @@ function goto_committees_of_correspondence() { states.committees_of_correspondence = { inactive: "Committees of Correspondence", - prompt: function (current) { + prompt(current) { view.prompt = "Committees of Correspondence: Place 1 PC marker in each of the 13 colonies. " + game.coc.length + " left." if (game.coc.length > 0) @@ -1103,13 +1074,13 @@ states.committees_of_correspondence = { else gen_pass() }, - place_american_pc: function (space) { + place_american_pc(space) { push_undo() let colony = SPACES[space].colony - remove_from_array(game.coc, colony) + array_remove_item(game.coc, colony) place_american_pc(space) }, - pass: function () { + pass() { clear_undo() goto_for_the_king() }, @@ -1128,19 +1099,19 @@ function goto_for_the_king() { states.for_the_king = { inactive: "For the King", - prompt: function (current) { + prompt(current) { view.prompt = "For the King: Place 3 PC markers. " + game.count + " left." if (game.count > 0) gen_british_pc_ops() else gen_pass() }, - place_british_pc: function (space) { + place_british_pc(space) { push_undo() place_british_pc(space) --game.count }, - pass: function () { + pass() { clear_undo() gen_british_pc_ops_end() goto_start_year() @@ -1152,7 +1123,7 @@ states.for_the_king = { function automatic_victory() { let n_american = 0 let n_british = 0 - for (let space in SPACES) { + for (let space of all_spaces) { n_american += count_french_cu(space) + count_american_cu(space) if (SPACES[space].colony !== "CA") n_british += count_british_cu(space) @@ -1183,10 +1154,10 @@ function goto_start_year() { // Prisoner exchange for (let g of BRITISH_GENERALS) - if (game.generals[g].location === CAPTURED_GENERALS) + if (is_general_at_location(g, CAPTURED_GENERALS)) move_general(g, BRITISH_REINFORCEMENTS) for (let g of AMERICAN_GENERALS) - if (game.generals[g].location === CAPTURED_GENERALS) + if (is_general_at_location(g, CAPTURED_GENERALS)) move_general(g, AMERICAN_REINFORCEMENTS) switch (game.year) { @@ -1246,7 +1217,7 @@ function goto_start_year() { } states.british_declare_first = { - prompt: function (current) { + prompt(current) { view.prompt = "Declare yourself as the first player by playing a campaign card?" gen_pass() for (let c of CAMPAIGN_CARDS) { @@ -1255,13 +1226,13 @@ states.british_declare_first = { } } }, - card_campaign: function (c) { + card_campaign(c) { delete game.congress_was_dispersed logp("went first by playing a campaign card") game.active = BRITISH goto_campaign(c) }, - pass: function () { + pass() { if (game.congress_was_dispersed) game.active = BRITISH else @@ -1272,16 +1243,16 @@ states.british_declare_first = { } states.choose_first_player = { - prompt: function (current) { + prompt(current) { view.prompt = "Choose who will play the first strategy card." gen_action("american_first") gen_action("british_first") }, - american_first: function (c) { + american_first(c) { logp("went first") goto_strategy_phase(AMERICAN) }, - british_first: function (c) { + british_first(c) { logp("went first") goto_strategy_phase(BRITISH) }, @@ -1302,42 +1273,42 @@ function goto_strategy_phase(new_active) { states.strategy_phase = { inactive: "strategy phase", - prompt: function (current) { + prompt(current) { view.prompt = "Play a strategy card." gen_strategy_plays(active_hand()) }, - card_campaign: function (c) { + card_campaign(c) { game.did_discard_event = false clear_queue() goto_campaign(c) }, - card_play_event: function (c) { + card_play_event(c) { push_undo() game.did_discard_event = false clear_queue() do_event(c) }, - card_discard_event: function (c) { + card_discard_event(c) { push_undo() game.did_discard_event = true clear_queue() discard_card(c, "PC action") game.state = "discard_event_pc_action" }, - card_ops_pc: function (c) { + card_ops_pc(c) { push_undo() game.did_discard_event = false clear_queue() play_card(c, "for PC") goto_ops_pc(CARDS[c].count) }, - card_ops_reinforcements: function (c) { + card_ops_reinforcements(c) { push_undo() game.did_discard_event = false clear_queue() goto_ops_reinforcements(c) }, - card_ops_queue: function (c) { + card_ops_queue(c) { game.did_discard_event = false play_card(c, "to queue") if (game.active === BRITISH) @@ -1346,12 +1317,12 @@ states.strategy_phase = { game.a_queue += CARDS[c].count end_strategy_card() }, - card_ops_general: function (c) { + card_ops_general(c) { push_undo() game.did_discard_event = false goto_ops_general(c) }, - exchange_for_discard: function (c) { + exchange_for_discard(c) { game.did_discard_event = false let d = game.discard.pop() discard_card(c) @@ -1380,7 +1351,7 @@ function end_strategy_card() { if (!game.french_alliance_triggered && game.french_alliance === 9) { log("The French signed an alliance with the Americans!") game.french_alliance_triggered = true - if (game.french_navy === FRENCH_REINFORCEMENTS) { + if (game.french_navy === -1) { game.save_active = game.active game.active = AMERICAN game.state = "place_french_navy_trigger" @@ -1388,9 +1359,8 @@ function end_strategy_card() { } } - game.moved = {} - for (let cu of game.cu) - cu.moved = 0 + reset_moved_generals() + reset_moved_cu() goto_strategy_phase(ENEMY[game.active]) @@ -1401,6 +1371,7 @@ function end_strategy_card() { if (hand.length === 0) return goto_winter_attrition_phase() } + } function clear_queue() { @@ -1455,7 +1426,7 @@ function gen_strategy_plays(hand) { /* DISCARD EVENT CARD FOR PC ACTION */ states.discard_event_pc_action = { - prompt: function (current) { + prompt(current) { view.prompt = "Place, flip, or remove PC marker." gen_pass() if (game.active === BRITISH) @@ -1463,29 +1434,29 @@ states.discard_event_pc_action = { else gen_american_discard_event_pc_action() }, - place_british_pc: function (space) { + place_british_pc(space) { place_british_pc(space) end_strategy_card() }, - place_american_pc: function (space) { + place_american_pc(space) { place_american_pc(space) end_strategy_card() }, - remove_pc: function (space) { + remove_pc(space) { remove_pc(space) end_strategy_card() }, - flip_pc: function (space) { + flip_pc(space) { flip_pc(space) end_strategy_card() }, - pass: function () { + pass() { end_strategy_card() }, } function gen_british_discard_event_pc_action() { - for (let space in SPACES) { + for (let space of all_spaces) { if (is_adjacent_to_british_pc(space)) { if (has_no_pc(space) && has_no_american_unit(space)) gen_action("place_british_pc", space) @@ -1498,7 +1469,7 @@ function gen_british_discard_event_pc_action() { } function gen_american_discard_event_pc_action() { - for (let space in SPACES) { + for (let space of all_spaces) { if (is_adjacent_to_american_pc(space)) { if (has_no_pc(space) && has_no_british_cu(space)) { if (allowed_to_place_american_pc()) @@ -1522,7 +1493,7 @@ function goto_ops_pc(count) { } states.ops_pc = { - prompt: function (current) { + prompt(current) { view.prompt = "Place or flip PC markers. " + game.count + " left." gen_pass() if (game.count > 0) { @@ -1532,22 +1503,22 @@ states.ops_pc = { gen_american_pc_ops() } }, - place_british_pc: function (space) { + place_british_pc(space) { push_undo() place_british_pc(space) --game.count }, - place_american_pc: function (space) { + place_american_pc(space) { push_undo() place_american_pc(space) --game.count }, - flip_pc: function (space) { + flip_pc(space) { push_undo() flip_pc(space) --game.count }, - pass: function () { + pass() { if (game.active === BRITISH) gen_british_pc_ops_end() end_strategy_card() @@ -1556,7 +1527,7 @@ states.ops_pc = { function gen_british_pc_ops_start() { game.british_pc_space_list = [] - for (let space in SPACES) { + for (let space of all_spaces) { if (has_no_pc(space) && has_no_american_unit(space)) { if (is_adjacent_to_british_pc(space)) game.british_pc_space_list.push(space) @@ -1567,7 +1538,7 @@ function gen_british_pc_ops_start() { function gen_british_pc_ops() { for (let space of game.british_pc_space_list) gen_action("place_british_pc", space) - for (let space in SPACES) { + for (let space of all_spaces) { if (has_british_army(space)) { if (has_no_pc(space)) gen_action("place_british_pc", space) @@ -1582,7 +1553,7 @@ function gen_british_pc_ops_end(space) { } function gen_american_pc_ops() { - for (let space in SPACES) { + for (let space of all_spaces) { if (has_no_pc(space) && has_no_british_cu(space)) { if (allowed_to_place_american_pc()) gen_action("place_american_pc", space) @@ -1609,86 +1580,85 @@ function goto_ops_reinforcements(c) { } states.ops_british_reinforcements_who = { - prompt: function (current) { + prompt(current) { view.prompt = "Reinforcements: choose an available general or pass to bring only CU." view.prompt += " Carrying " + game.count + " British CU." gen_pass() gen_british_reinforcements_who() }, - drop_british_cu: function () { + drop_british_cu() { --game.count }, - pickup_british_cu: function () { + pickup_british_cu() { ++game.count }, - select_general: function (g) { + select_general(g) { push_undo() game.state = "ops_british_reinforcements_where" game.who = g }, - pass: function () { + pass() { push_undo() game.state = "ops_british_reinforcements_where" - game.who = null + game.who = NOBODY }, } states.ops_british_reinforcements_where = { - prompt: function (current) { + prompt(current) { view.prompt = "Reinforcements: choose a port space." view.prompt += " Carrying " + game.count + " British CU." gen_british_reinforcements_where() }, - drop_british_cu: function () { + drop_british_cu() { --game.count }, - pickup_british_cu: function () { + pickup_british_cu() { ++game.count }, - place_reinforcements: function (space) { + place_reinforcements(space) { place_british_reinforcements(game.who, game.count, space) end_strategy_card() - game.who = null + game.who = NOBODY }, } states.ops_american_reinforcements_who = { - prompt: function (current) { + prompt(current) { view.prompt = "Reinforcements: choose an available general or pass to bring only CU." gen_pass() gen_american_reinforcements_who() }, - select_general: function (g) { + select_general(g) { push_undo() game.state = "ops_american_reinforcements_where" game.who = g }, - pass: function () { + pass() { push_undo() game.state = "ops_american_reinforcements_where" - game.who = null + game.who = NOBODY }, } states.ops_american_reinforcements_where = { - prompt: function (current) { + prompt(current) { view.prompt = "Reinforcements: choose a space." gen_american_reinforcements_where(game.who) }, - place_reinforcements: function (space) { + place_reinforcements(space) { if (game.who === ROCHAMBEAU) place_french_reinforcements(game.who, space) else place_american_reinforcements(game.who, game.count, space) end_strategy_card() - game.who = null + game.who = NOBODY }, } function gen_british_reinforcements_who() { for (let g of BRITISH_GENERALS) { - let general = game.generals[g] - if (general.location === BRITISH_REINFORCEMENTS) { + if (is_general_at_location(g, BRITISH_REINFORCEMENTS)) { gen_action("select_general", g) } } @@ -1699,7 +1669,7 @@ function gen_british_reinforcements_who() { } function gen_british_reinforcements_where() { - for (let space in SPACES) { + for (let space of all_spaces) { if (is_non_blockaded_port(space)) if (!has_american_or_french_cu(space) && !has_american_pc(space)) gen_action("place_reinforcements", space) @@ -1712,15 +1682,14 @@ function gen_british_reinforcements_where() { function gen_american_reinforcements_who() { for (let g of AMERICAN_GENERALS) { - let general = game.generals[g] - if (general.location === AMERICAN_REINFORCEMENTS) { + if (is_general_at_location(g, AMERICAN_REINFORCEMENTS)) { gen_action("select_general", g) } } } function gen_american_reinforcements_where(general) { - for (let space in SPACES) { + for (let space of all_spaces) { if (!has_british_cu(space) && !has_british_pc(space)) { if (general === ROCHAMBEAU) { if (SPACES[space].port) @@ -1747,7 +1716,7 @@ function goto_ops_general(c) { } states.ops_general_who = { - prompt: function (current) { + prompt(current) { if (game.campaign && game.landing_party) view.prompt = "Campaign: Activate a general or use a landing party. " + game.campaign + " left." else if (game.campaign) @@ -1759,21 +1728,21 @@ states.ops_general_who = { gen_activate_general() gen_pass() }, - place_british_pc: function (where) { + place_british_pc(where) { game.landing_party = 0 place_british_pc(where) end_strategy_card() }, - flip_pc: function (where) { + flip_pc(where) { game.landing_party = 0 flip_pc(where) end_strategy_card() }, - select_general: function (g) { + select_general(g) { push_undo() goto_ops_general_move(g, false) }, - pass: function () { + pass() { if (game.campaign > 0) game.campaign = 0 end_strategy_card() @@ -1781,7 +1750,7 @@ states.ops_general_who = { } function gen_landing_party() { - for (let space in SPACES) { + for (let space of all_spaces) { if (!is_fortified_port(space) && is_non_blockaded_port(space)) { if (has_american_pc(space) && has_no_american_unit(space)) gen_action("flip_pc", space) @@ -1800,13 +1769,13 @@ function gen_activate_general() { function gen_activate_british_general() { for (let g of BRITISH_GENERALS) - if (is_general_on_map(g) && GENERALS[g].strategy <= game.count && !game.moved[g]) + if (is_general_on_map(g) && GENERALS[g].strategy <= game.count && !has_general_moved(g)) gen_action("select_general", g) } function gen_activate_american_general() { for (let g of AMERICAN_GENERALS) - if (is_general_on_map(g) && GENERALS[g].strategy <= game.count && !game.moved[g]) + if (is_general_on_map(g) && GENERALS[g].strategy <= game.count && !has_general_moved(g)) gen_action("select_general", g) } @@ -1816,11 +1785,11 @@ function goto_remove_general(where) { } states.remove_general = { - prompt: function (current) { + prompt(current) { view.prompt = "Remove a general to the reinforcements box." gen_remove_general() }, - select_general: function (g) { + select_general(g) { if (game.active === BRITISH) move_general(g, BRITISH_REINFORCEMENTS) else @@ -1834,11 +1803,11 @@ function goto_remove_general_after_intercept() { } states.remove_general_after_intercept = { - prompt: function (current) { + prompt(current) { view.prompt = "Remove a general to the reinforcements box." gen_remove_general() }, - select_general: function (g) { + select_general(g) { if (game.active === BRITISH) move_general(g, BRITISH_REINFORCEMENTS) else @@ -1853,11 +1822,11 @@ function goto_remove_general_after_retreat(where) { } states.remove_general_after_retreat = { - prompt: function (current) { + prompt(current) { view.prompt = "Remove a general to the reinforcements box." gen_remove_general() }, - select_general: function (g) { + select_general(g) { if (game.active === BRITISH) move_general(g, BRITISH_REINFORCEMENTS) else @@ -1875,14 +1844,14 @@ function gen_remove_general() { function gen_remove_british_general() { for (let g of BRITISH_GENERALS) - if (location_of_general(g) === game.where) + if (is_general_at_location(g, game.where)) gen_action("select_general", g) } function gen_remove_american_general() { for (let g of AMERICAN_GENERALS) if (g !== WASHINGTON) - if (location_of_general(g) === game.where) + if (is_general_at_location(g, game.where)) gen_action("select_general", g) } @@ -1909,7 +1878,7 @@ function goto_ops_general_move(g, marblehead) { } states.ops_general_move = { - prompt: function (current) { + prompt(current) { view.prompt = "Move " + game.who + " with " if (game.carry_british > 0) { view.prompt += game.carry_british + " British CU." @@ -1940,45 +1909,45 @@ states.ops_general_move = { gen_move_general() }, - pickup_british_cu: function () { + pickup_british_cu() { ++game.carry_british }, - pickup_american_cu: function () { + pickup_american_cu() { ++game.carry_american }, - pickup_french_cu: function () { + pickup_french_cu() { ++game.carry_french }, - drop_british_cu: function () { + drop_british_cu() { push_undo() --game.carry_british - if (game.moved[game.who]) + if (has_general_moved(game.who)) mark_moved_cu(BRITISH, location_of_general(game.who), 1) }, - drop_american_cu: function () { + drop_american_cu() { push_undo() --game.carry_american - if (game.moved[game.who]) + if (has_general_moved(game.who)) mark_moved_cu(AMERICAN, location_of_general(game.who), 1) }, - drop_french_cu: function () { + drop_french_cu() { push_undo() --game.carry_french - if (game.moved[game.who]) + if (has_general_moved(game.who)) mark_moved_cu(FRENCH, location_of_general(game.who), 1) }, - move: function (to) { + move(to) { push_undo() - game.moved[game.who] = 1 + set_general_moved(game.who) let from = location_of_general(game.who) let cu = game.carry_british + game.carry_american + game.carry_french let intercept = false if (game.active === BRITISH) { - let is_sea_move = path_type(from, to) === undefined + let is_sea_move = path_type(from, to) === "sea" if (has_american_pc(to) && cu > 0 && !is_sea_move && !has_british_cu(to)) intercept = can_intercept_to(to) } @@ -2002,7 +1971,7 @@ states.ops_general_move = { else resume_moving(from, to) }, - pass: function () { + pass() { clear_undo() let where = location_of_general(game.who) end_move() @@ -2021,10 +1990,10 @@ function resume_moving(from, to) { } function can_intercept_to(to) { - for (let space of SPACES[to].exits) { + for (let space of SPACES[to].adjacent) { if (has_american_army(space)) { let g = find_american_or_french_general(space) - if (g && !game.moved[g]) + if (g && !has_general_moved(g)) return true } } @@ -2032,10 +2001,10 @@ function can_intercept_to(to) { } function gen_intercept() { - for (let space of SPACES[game.where].exits) { + for (let space of SPACES[game.where].adjacent) { if (has_american_army(space)) { let g = find_american_or_french_general(space) - if (g && !game.moved[g]) + if (g && !has_general_moved(g)) gen_action("select_general", g) } } @@ -2044,7 +2013,7 @@ function gen_intercept() { function goto_intercept(from, where) { clear_undo() game.save_who = game.who - game.who = null + game.who = NOBODY game.from = from game.where = where game.active = AMERICAN @@ -2052,13 +2021,13 @@ function goto_intercept(from, where) { } states.intercept = { - prompt: function (current) { + prompt(current) { view.prompt = "Intercept " + game.save_who + " in " + game.where + "?" gen_pass() gen_intercept() }, - select_general: function (g) { - game.moved[g] = 1 + select_general(g) { + set_general_moved(g) let die = roll_d6() if (die <= GENERALS[g].agility) { log(g + " intercepted (" + die + " <= " + GENERALS[g].agility + ")") @@ -2085,7 +2054,7 @@ states.intercept = { end_intercept() } }, - pass: function () { + pass() { end_intercept() }, } @@ -2101,12 +2070,12 @@ function end_intercept() { function end_move() { let where = location_of_general(game.who) - if (game.moved[game.who]) { + if (has_general_moved(game.who)) { mark_moved_cu(BRITISH, where, game.carry_british) mark_moved_cu(AMERICAN, where, game.carry_american) mark_moved_cu(FRENCH, where, game.carry_french) } - game.who = null + game.who = NOBODY delete game.mobility delete game.carry_british delete game.carry_american @@ -2114,11 +2083,11 @@ function end_move() { } function path_type(from, to) { - return PATH_TYPE[PATH_INDEX[from][to]] -} - -function path_name(from, to) { - return PATH_NAME[PATH_INDEX[from][to]] + if (set_has(SPACES[from].path, to)) + return "path" + if (set_has(SPACES[from].wilderness, to)) + return "wilderness" + return "sea" } function gen_carry_cu() { @@ -2143,22 +2112,24 @@ function gen_carry_cu() { function movement_cost(from, to) { switch (path_type(from, to)) { - case undefined: + case "sea": return 4 /* must be a sea connection if no direct path */ case "wilderness": return 3 - default: + case "path": return 1 + default: + throw "IMPOSSIBLE" } } function gen_move_general() { let from = location_of_general(game.who) let alone = game.carry_british + game.carry_american + game.carry_french === 0 - for (let to of SPACES[from].exits) { + for (let to of SPACES[from].adjacent) { let mp = 1 if (path_type(from, to) === "wilderness") { - if (path_name(from, to) === FALMOUTH_QUEBEC) + if ((from === QUEBEC && to === FALMOUTH) || (to === QUEBEC && from === FALMOUTH)) if (game.who !== ARNOLD) continue mp = 3 @@ -2184,7 +2155,7 @@ function gen_move_general() { } if (game.active === BRITISH && game.count === 4) { if (is_non_blockaded_port(from)) { - for (let to in SPACES) { + for (let to of all_spaces) { if (to !== from) { if (is_non_blockaded_port(to)) { if (!has_american_pc(to) && !has_american_or_french_cu(to)) { @@ -2218,7 +2189,7 @@ events.the_war_ends = function (c, card) { logp("played #" + c) log("The war will end in " + card.year) game.last_played = c - remove_from_array(active_hand(), c) + array_remove_item(active_hand(), c) if (game.war_ends) game.discard.push(WAR_ENDS_1779 + game.war_ends - 1779) game.war_ends = card.year @@ -2284,7 +2255,7 @@ events.advance_french_alliance = function (c, card) { events.remove_french_navy = function (c, card) { play_card(c) - game.french_navy = TURN_TRACK[game.year + 1] + game.french_navy = game.year + 1 end_strategy_card() } @@ -2296,19 +2267,19 @@ events.remove_british_pc_from = function (c, card) { } states.remove_british_pc_from = { - prompt: function (current) { + prompt(current) { view.prompt = "Remove British PC markers from " + game.where.join(", ") + ". " + game.count + " left." gen_pass() gen_remove_british_pc_from(game.where) }, - remove_pc: function (where) { + remove_pc(where) { remove_pc(where) if (--game.count === 0) { delete game.where end_strategy_card() } }, - pass: function () { + pass() { delete game.where end_strategy_card() }, @@ -2321,18 +2292,18 @@ events.remove_american_pc = function (c, card) { } states.remove_american_pc = { - prompt: function (current) { + prompt(current) { view.prompt = "Remove American PC markers. " + game.count + " left." gen_pass() gen_remove_american_pc() }, - remove_pc: function (where) { + remove_pc(where) { remove_pc(where) if (--game.count === 0) { end_strategy_card() } }, - pass: function () { + pass() { end_strategy_card() }, } @@ -2345,19 +2316,19 @@ events.remove_american_pc_from = function (c, card) { } states.remove_american_pc_from = { - prompt: function (current) { + prompt(current) { view.prompt = "Remove American PC markers from " + game.where.join(", ") + ". " + game.count + " left." gen_pass() gen_remove_american_pc_from(game.where) }, - remove_pc: function (where) { + remove_pc(where) { remove_pc(where) if (--game.count === 0) { delete game.where end_strategy_card() } }, - pass: function () { + pass() { delete game.where end_strategy_card() }, @@ -2371,20 +2342,20 @@ events.remove_american_pc_from_non_port = function (c, card) { } states.remove_american_pc_from_non_port = { - prompt: function (current) { + prompt(current) { view.prompt = "Remove American PC markers from non-Port space in " + game.where.join(", ") + ". " + game.count + " left." gen_pass() gen_remove_american_pc_from_non_port(game.where) }, - remove_pc: function (where) { + remove_pc(where) { remove_pc(where) if (--game.count === 0) { delete game.where end_strategy_card() } }, - pass: function () { + pass() { delete game.where end_strategy_card() }, @@ -2397,19 +2368,19 @@ events.remove_american_pc_within_two_spaces_of_a_british_general = function (c, } states.remove_american_pc_within_two_spaces_of_a_british_general = { - prompt: function (current) { + prompt(current) { view.prompt = "Remove American PC markers within two spaces of a British general. " + game.count + " left." gen_pass() gen_remove_american_pc_within_two_spaces_of_a_british_general() }, - remove_pc: function (where) { + remove_pc(where) { remove_pc(where) if (--game.count === 0) { delete game.where end_strategy_card() } }, - pass: function () { + pass() { delete game.where end_strategy_card() }, @@ -2422,18 +2393,18 @@ events.place_american_pc = function (c, card) { } states.place_american_pc = { - prompt: function (current) { + prompt(current) { view.prompt = "Place American PC markers. " + game.count + " left." gen_pass() gen_place_american_pc() }, - place_american_pc: function (where) { + place_american_pc(where) { place_american_pc(where) if (--game.count === 0) { end_strategy_card() } }, - pass: function () { + pass() { end_strategy_card() }, } @@ -2446,19 +2417,19 @@ events.place_american_pc_in = function (c, card) { } states.place_american_pc_in = { - prompt: function (current) { + prompt(current) { view.prompt = "Place American PC markers in " + game.where.join(", ") + ". " + game.count + " left." gen_pass() gen_place_american_pc_in(game.where) }, - place_american_pc: function (where) { + place_american_pc(where) { place_american_pc(where) if (--game.count === 0) { delete game.where end_strategy_card() } }, - pass: function () { + pass() { delete game.where end_strategy_card() }, @@ -2468,32 +2439,32 @@ events.lord_sandwich_coastal_raids = function (c, card) { play_card(c) game.state = "lord_sandwich_coastal_raids" game.count = 2 - game.where = null + game.where = NOWHERE } states.lord_sandwich_coastal_raids = { - prompt: function (current) { + prompt(current) { view.prompt = "Remove two or flip one American PC in a port space." gen_pass() gen_lord_sandwich_coastal_raids(game.where) }, - place_british_pc: function (where) { + place_british_pc(where) { place_british_pc(where) end_strategy_card() }, - remove_pc: function (where) { + remove_pc(where) { game.where = where remove_pc(where) if (--game.count === 0) end_strategy_card() }, - pass: function () { + pass() { end_strategy_card() }, } function gen_lord_sandwich_coastal_raids(first_removed) { - for (let space in SPACES) { + for (let space of all_spaces) { if (SPACES[space].port) if (has_american_pc(space) && has_no_american_unit(space)) gen_action("remove_pc", space) @@ -2508,23 +2479,23 @@ events.remove_american_cu = function (c, card) { } states.remove_american_cu = { - prompt: function (current) { + prompt(current) { view.prompt = "Remove one American CU from any space." gen_pass() gen_remove_american_cu() }, - remove_cu: function (where) { + remove_cu(where) { let cu = find_american_cu(where) || find_french_cu(where) remove_cu(cu.owner, where, 1) end_strategy_card() }, - pass: function () { + pass() { end_strategy_card() }, } function gen_remove_american_cu() { - for (let space in SPACES) { + for (let space of all_spaces) { if (has_american_or_french_cu(space)) gen_action("remove_cu", space) } @@ -2537,24 +2508,24 @@ events.remove_british_cu = function (c, card) { } states.remove_british_cu = { - prompt: function (current) { + prompt(current) { view.prompt = "Remove " + game.count + " British CU from any space." gen_pass() gen_remove_british_cu() }, - remove_cu: function (where) { + remove_cu(where) { let cu = find_british_cu(where) remove_cu(cu.owner, where, 1) if (--game.count === 0) end_strategy_card() }, - pass: function () { + pass() { end_strategy_card() }, } function gen_remove_british_cu() { - for (let space in SPACES) { + for (let space of all_spaces) { if (has_british_cu(space)) gen_action("remove_cu", space) } @@ -2565,29 +2536,29 @@ events.pennsylvania_and_new_jersey_line_mutinies = function (c, card) { game.pennsylvania_and_new_jersey_line_mutinies = true game.state = "pennsylvania_and_new_jersey_line_mutinies" game.count = 2 - game.where = null + game.where = NOWHERE } states.pennsylvania_and_new_jersey_line_mutinies = { - prompt: function (current) { + prompt(current) { view.prompt = "Remove two American CUs from the map, one each from two different spaces." gen_pass() gen_pennsylvania_and_new_jersey_line_mutinies(game.where) }, - remove_cu: function (where) { + remove_cu(where) { let cu = find_american_cu(where) || find_french_cu(where) remove_cu(cu.owner, where, 1) game.where = where if (--game.count === 0) end_strategy_card() }, - pass: function () { + pass() { end_strategy_card() }, } function gen_pennsylvania_and_new_jersey_line_mutinies(first_removed) { - for (let space in SPACES) { + for (let space of all_spaces) { if (has_american_or_french_cu(space)) if (space !== first_removed) gen_action("remove_cu", space) @@ -2601,11 +2572,11 @@ events.john_glovers_marblehead_regiment = function (c, card) { } states.john_glovers_marblehead_regiment_who = { - prompt: function (current) { + prompt(current) { view.prompt = "Activate an American general." gen_activate_general() }, - select_general: function (g) { + select_general(g) { goto_ops_general_move(g, true) }, } @@ -2619,20 +2590,20 @@ events.declaration_of_independence = function (c, card) { } states.declaration_of_independence = { - prompt: function (current) { + prompt(current) { view.prompt = "Declaration of Independence: Place 1 PC marker in each of the 13 colonies. " view.prompt += game.doi.length + " left." gen_pass() gen_place_american_pc_in(game.doi) }, - place_american_pc: function (space) { + place_american_pc(space) { let colony = SPACES[space].colony - remove_from_array(game.doi, colony) + array_remove_item(game.doi, colony) place_american_pc(space) if (game.doi.length === 0) end_declaration_of_independence() }, - pass: function () { + pass() { end_declaration_of_independence() }, } @@ -2656,18 +2627,18 @@ function goto_george_washington_captured() { } states.george_washington_captured = { - prompt: function (current) { + prompt(current) { view.prompt = "George Washington is captured! Remove American PC markers. " + game.count + " left." gen_pass() gen_remove_american_pc() }, - remove_pc: function (where) { + remove_pc(where) { remove_pc(where) if (--game.count === 0) { end_george_washington_captured() } }, - pass: function () { + pass() { end_george_washington_captured() }, } @@ -2697,7 +2668,7 @@ function can_retreat_before_battle(where) { return false // can't retreat if attempted (successful or not) interception! let g = find_american_or_french_general(where) - if (g && !game.moved[g]) + if (g && !has_general_moved(g)) return true return false } @@ -2723,12 +2694,12 @@ function goto_retreat_before_battle() { } states.retreat_before_battle = { - prompt: function (current) { + prompt(current) { view.prompt = "Attempt retreat before battle?" gen_pass() gen_defender_retreat() }, - move: function (to) { + move(to) { let agility = GENERALS[game.who].agility if (GENERALS[game.who].bonus) agility += 2 @@ -2743,7 +2714,7 @@ states.retreat_before_battle = { end_retreat_before_battle() } }, - pass: function () { + pass() { end_retreat_before_battle() }, } @@ -2759,11 +2730,11 @@ function goto_remove_general_after_retreat_before_battle(to) { } states.remove_general_after_retreat_before_battle = { - prompt: function (current) { + prompt(current) { view.prompt = "Remove a general to the reinforcements box." gen_remove_general() }, - select_general: function (g) { + select_general(g) { if (game.active === BRITISH) move_general(g, BRITISH_REINFORCEMENTS) else @@ -2808,7 +2779,7 @@ function can_attacker_retreat() { function gen_defender_retreat() { let from = game.where - for (let to of SPACES[from].exits) { + for (let to of SPACES[from].adjacent) { if (can_defender_retreat(to)) gen_action("move", to) } @@ -2823,7 +2794,7 @@ function gen_defender_retreat() { } } if (can_sea_retreat) { - for (let to in SPACES) { + for (let to of all_spaces) { if (to !== from && is_non_blockaded_port(to)) { if (!has_american_pc(to) && !has_american_or_french_cu(to)) { gen_action("move", to) @@ -2840,7 +2811,7 @@ function gen_attacker_retreat() { } function end_retreat_before_battle() { - game.who = null + game.who = NOBODY goto_play_attacker_battle_card() } @@ -2850,12 +2821,12 @@ function goto_play_attacker_battle_card() { } states.play_attacker_battle_card = { - prompt: function (current) { + prompt(current) { view.prompt = "Attack: Play or discard event for DRM." gen_pass() gen_battle_card() }, - card_battle_play: function (c) { + card_battle_play(c) { play_card(c, "for +2 DRM") if (game.active === BRITISH) { if (CARDS[c].event === "remove_benedict_arnold") @@ -2868,7 +2839,7 @@ states.play_attacker_battle_card = { } goto_play_defender_battle_card() }, - card_battle_discard: function (c) { + card_battle_discard(c) { discard_card(c, "for +1 DRM") if (game.active === BRITISH) { game.b_draw_after_battle = true @@ -2879,7 +2850,7 @@ states.play_attacker_battle_card = { } goto_play_defender_battle_card() }, - pass: function () { + pass() { goto_play_defender_battle_card() }, } @@ -2890,12 +2861,12 @@ function goto_play_defender_battle_card() { } states.play_defender_battle_card = { - prompt: function (current) { + prompt(current) { view.prompt = "Defend: Play or discard event for DRM." gen_pass() gen_battle_card() }, - card_battle_play: function (c) { + card_battle_play(c) { play_card(c, "for +2 DRM") if (game.active === BRITISH) { if (CARDS[c].event === "remove_benedict_arnold") @@ -2908,7 +2879,7 @@ states.play_defender_battle_card = { } resolve_battle() }, - card_battle_discard: function (c) { + card_battle_discard(c) { discard_card(c, "for +1 DRM") if (game.active === BRITISH) { game.b_draw_after_battle = true @@ -2919,7 +2890,7 @@ states.play_defender_battle_card = { } resolve_battle() }, - pass: function () { + pass() { resolve_battle() }, } @@ -3005,7 +2976,7 @@ function apply_british_combat_losses(max) { cu.count -= max return max } - remove_from_array(game.cu, cu) + array_remove_item(game.cu, cu) return cu.count } @@ -3016,7 +2987,7 @@ function apply_american_combat_losses(max) { cu.count -= max return max } - remove_from_array(game.cu, cu) + array_remove_item(game.cu, cu) return cu.count } return 0 @@ -3029,7 +3000,7 @@ function apply_french_combat_losses(max) { cu.count -= max return max } - remove_from_array(game.cu, cu) + array_remove_item(game.cu, cu) return cu.count } return 0 @@ -3176,11 +3147,11 @@ function resolve_battle() { function goto_retreat_after_battle(victor) { if (victor === BRITISH) { game.who = find_american_or_french_general(game.where) - if (game.who === null && count_american_and_french_cu(game.where) === 0) + if (game.who === NOBODY && count_american_and_french_cu(game.where) === 0) return end_battle() } else { game.who = find_british_general(game.where) - if (game.who === null && count_british_cu(game.where) === 0) + if (game.who === NOBODY && count_british_cu(game.where) === 0) return end_battle() } game.active = ENEMY[victor] @@ -3188,7 +3159,7 @@ function goto_retreat_after_battle(victor) { } states.retreat_after_battle = { - prompt: function (current) { + prompt(current) { view.prompt = "Retreat after battle." gen_action("surrender") if (game.active === game.attacker) @@ -3196,7 +3167,7 @@ states.retreat_after_battle = { else gen_defender_retreat() }, - move: function (to) { + move(to) { logp("retreated to " + to) if (game.active === BRITISH) retreat_british_army(game.where, to) @@ -3207,7 +3178,7 @@ states.retreat_after_battle = { else end_battle() }, - surrender: function () { + surrender() { // End battle here, so if Washington is captured we can handle the interrupt state. let active = game.active let where = game.where @@ -3244,8 +3215,8 @@ function end_battle() { delete game.attack_from delete game.british_losses delete game.attacker - game.where = null - game.who = null + game.where = NOWHERE + game.who = NOBODY end_strategy_card() } @@ -3270,12 +3241,12 @@ function goto_winter_attrition_phase() { logbr() log("Winter Attrition") - for (let space in SPACES) { + for (let space of all_spaces) { let wq = is_winter_quarter_space(space) let n_british = count_british_cu(space) let n_american = count_american_cu(space) let n_french = count_french_cu(space) - let has_washington = game.generals[WASHINGTON].location === space + let has_washington = is_general_at_location(WASHINGTON, space) if (n_british === 1 && !wq) apply_single_winter_attrition(BRITISH, space, has_british_general(space)) @@ -3323,7 +3294,7 @@ function goto_winter_attrition_phase() { } function goto_french_naval_phase() { - if (game.french_navy !== FRENCH_REINFORCEMENTS) { + if (game.french_navy !== -1) { game.active = AMERICAN game.state = "place_french_navy" } else { @@ -3342,11 +3313,11 @@ function gen_place_french_navy() { } states.place_french_navy_trigger = { - prompt: function (current) { + prompt(current) { view.prompt = "Place the French Navy in a blockade zone." gen_place_french_navy() }, - place_navy: function (zone) { + place_navy(zone) { logp("placed French Navy.") game.french_navy = zone game.active = game.save_active @@ -3356,11 +3327,11 @@ states.place_french_navy_trigger = { } states.place_french_navy = { - prompt: function (current) { + prompt(current) { view.prompt = "Place the French Navy in a blockade zone." gen_place_french_navy() }, - place_navy: function (zone) { + place_navy(zone) { logp("placed French Navy.") game.french_navy = zone goto_political_control_phase() @@ -3368,7 +3339,7 @@ states.place_french_navy = { } function place_pc_markers_segment() { - for (let space in SPACES) { + for (let space of all_spaces) { if (has_american_army(space)) { if (has_no_pc(space)) place_american_pc(space) @@ -3423,22 +3394,24 @@ function is_british_pc_path(space) { } function spread_american_path(seen, from) { - for (let to of SPACES[from].exits) { - if (to in seen) + // TODO: BFS + for (let to of SPACES[from].adjacent) { + if (set_has(seen, to)) continue if (is_american_pc_path(to)) { - seen[to] = 1 + set_add(seen, to) spread_american_path(seen, to) } } } function spread_british_path(seen, from) { - for (let to of SPACES[from].exits) { - if (to in seen) + // TODO: BFS + for (let to of SPACES[from].adjacent) { + if (set_has(seen, to)) continue if (is_british_pc_path(to)) { - seen[to] = 1 + set_add(seen, to) spread_british_path(seen, to) } } @@ -3446,35 +3419,35 @@ function spread_british_path(seen, from) { function remove_isolated_american_pc_segment() { log("Removed isolated American PC") - let seen = {} - for (let space in SPACES) { + let seen = [] + for (let space of all_spaces) { if (is_american_pc_root(space)) { - seen[space] = 1 + set_add(seen, space) spread_american_path(seen, space) } } - for (let space in SPACES) - if (has_american_pc(space) && !seen[space]) + for (let space of all_spaces) + if (has_american_pc(space) && !set_has(seen, space)) remove_pc(space) } function remove_isolated_british_pc_segment() { log("Removed isolated British PC") - let seen = {} - for (let space in SPACES) { + let seen = [] + for (let space of all_spaces) { if (is_british_pc_root(space)) { - seen[space] = 1 + set_add(seen, space) spread_british_path(seen, space) } } - for (let space in SPACES) - if (has_british_pc(space) && !seen[space]) + for (let space of all_spaces) + if (has_british_pc(space) && !set_has(seen, space)) remove_pc(space) } function gen_place_continental_congress() { let n = 0 - for (let space in SPACES) { + for (let space of all_spaces) { if (SPACES[space].colony !== "CA") { if (has_american_pc(space) && has_no_british_playing_piece(space)) { gen_action("place_continental_congress", space) @@ -3495,21 +3468,22 @@ function goto_political_control_phase() { } states.return_continental_congress = { - prompt: function () { + prompt() { view.prompt = "Return Continental Congress to a space in the 13 colonies." if (gen_place_continental_congress() === 0) gen_pass() }, - place_continental_congress: function (where) { + place_continental_congress(where) { game.congress = where goto_political_control_phase_2() }, - pass: function () { + pass() { goto_political_control_phase_2() }, } function goto_political_control_phase_2() { + // TODO: manually place and remove place_pc_markers_segment() remove_isolated_american_pc_segment() remove_isolated_british_pc_segment() @@ -3517,18 +3491,18 @@ function goto_political_control_phase_2() { } states.european_war = { - prompt: function () { + prompt() { view.prompt = "European War: Remove 2 British CU from any spaces. " + game.count + " left." gen_pass() gen_remove_british_cu() }, - remove_cu: function (where) { + remove_cu(where) { let cu = find_british_cu(where) remove_cu(BRITISH, where, 1) if (--game.count === 0) goto_end_phase() }, - pass: function () { + pass() { goto_end_phase() }, } @@ -3537,8 +3511,8 @@ function norths_government_falls() { update_colony_control() let n_american = 0 - for (let c in COLONIES) - if (game.control[c] === AMERICAN) + for (let c = 0; c <= 13; ++C) + if (game.control[c] === PC_AMERICAN) ++n_american if (n_american >= 7) @@ -3573,7 +3547,7 @@ function goto_end_phase() { } states.game_over = { - prompt: function () { + prompt() { view.prompt = game.victory }, } @@ -3659,3 +3633,347 @@ exports.view = function (state, current) { return view } + + +// === COMMON LIBRARY === + +function log(s) { + game.log.push(s) +} + +function logp(s) { + game.log.push(game.active + " " + s) +} + +function logbr(s) { + if (game.log.length > 0 && game.log[game.log.length - 1] !== "") + game.log.push("") +} + +function clear_undo() { + if (game.undo) { + game.undo.length = 0 + } +} + +function push_undo() { + if (game.undo) { + let copy = {} + for (let k in game) { + let v = game[k] + if (k === "undo") + continue + else if (k === "log") + v = v.length + else if (typeof v === "object" && v !== null) + v = object_copy(v) + copy[k] = v + } + game.undo.push(copy) + } +} + +function pop_undo() { + if (game.undo) { + let save_log = game.log + let save_undo = game.undo + game = save_undo.pop() + save_log.length = game.log + game.log = save_log + game.undo = save_undo + } +} + +function random(range) { + // An MLCG using integer arithmetic with doubles. + // https://www.ams.org/journals/mcom/1999-68-225/S0025-5718-99-00996-5/S0025-5718-99-00996-5.pdf + // m = 2**35 − 31 + return (game.seed = game.seed * 200105 % 34359738337) % range +} + +function random_bigint(range) { + // Largest MLCG that will fit its state in a double. + // Uses BigInt for arithmetic, so is an order of magnitude slower. + // https://www.ams.org/journals/mcom/1999-68-225/S0025-5718-99-00996-5/S0025-5718-99-00996-5.pdf + // m = 2**53 - 111 + return (game.seed = Number(BigInt(game.seed) * 5667072534355537n % 9007199254740881n)) % range +} + +function shuffle(list) { + // Fisher-Yates shuffle + for (let i = list.length - 1; i > 0; --i) { + let j = random(i + 1) + let tmp = list[j] + list[j] = list[i] + list[i] = tmp + } +} + +function shuffle_bigint(list) { + // Fisher-Yates shuffle + for (let i = list.length - 1; i > 0; --i) { + let j = random_bigint(i + 1) + let tmp = list[j] + list[j] = list[i] + list[i] = tmp + } +} + +// Fast deep copy for objects without cycles +function object_copy(original) { + if (Array.isArray(original)) { + let n = original.length + let copy = new Array(n) + for (let i = 0; i < n; ++i) { + let v = original[i] + if (typeof v === "object" && v !== null) + copy[i] = object_copy(v) + else + copy[i] = v + } + return copy + } else { + let copy = {} + for (let i in original) { + let v = original[i] + if (typeof v === "object" && v !== null) + copy[i] = object_copy(v) + else + copy[i] = v + } + return copy + } +} + +// Array remove and insert (faster than splice) + +function array_remove(array, index) { + let n = array.length + for (let i = index + 1; i < n; ++i) + array[i - 1] = array[i] + array.length = n - 1 +} + +function array_remove_item(array, item) { + let n = array.length + for (let i = 0; i < n; ++i) + if (array[i] === item) + return array_remove(array, i) +} + +function array_insert(array, index, item) { + for (let i = array.length; i > index; --i) + array[i] = array[i - 1] + array[index] = item +} + +function array_remove_pair(array, index) { + let n = array.length + for (let i = index + 2; i < n; ++i) + array[i - 2] = array[i] + array.length = n - 2 +} + +function array_insert_pair(array, index, key, value) { + for (let i = array.length; i > index; i -= 2) { + array[i] = array[i-2] + array[i+1] = array[i-1] + } + array[index] = key + array[index+1] = value +} + +// Set as plain sorted array + +function set_clear(set) { + set.length = 0 +} + +function set_has(set, item) { + let a = 0 + let b = set.length - 1 + while (a <= b) { + let m = (a + b) >> 1 + let x = set[m] + if (item < x) + b = m - 1 + else if (item > x) + a = m + 1 + else + return true + } + return false +} + +function set_add(set, item) { + let a = 0 + let b = set.length - 1 + while (a <= b) { + let m = (a + b) >> 1 + let x = set[m] + if (item < x) + b = m - 1 + else if (item > x) + a = m + 1 + else + return + } + array_insert(set, a, item) +} + +function set_delete(set, item) { + let a = 0 + let b = set.length - 1 + while (a <= b) { + let m = (a + b) >> 1 + let x = set[m] + if (item < x) + b = m - 1 + else if (item > x) + a = m + 1 + else { + array_remove(set, m) + return + } + } +} + +function set_toggle(set, item) { + let a = 0 + let b = set.length - 1 + while (a <= b) { + let m = (a + b) >> 1 + let x = set[m] + if (item < x) + b = m - 1 + else if (item > x) + a = m + 1 + else { + array_remove(set, m) + return + } + } + array_insert(set, a, item) +} + +// Map as plain sorted array of key/value pairs + +function map_clear(map) { + map.length = 0 +} + +function map_has(map, key) { + let a = 0 + let b = (map.length >> 1) - 1 + while (a <= b) { + let m = (a + b) >> 1 + let x = map[m<<1] + if (key < x) + b = m - 1 + else if (key > x) + a = m + 1 + else + return true + } + return false +} + +function map_get(map, key, missing) { + let a = 0 + let b = (map.length >> 1) - 1 + while (a <= b) { + let m = (a + b) >> 1 + let x = map[m<<1] + if (key < x) + b = m - 1 + else if (key > x) + a = m + 1 + else + return map[(m<<1)+1] + } + return missing +} + +function map_set(map, key, value) { + let a = 0 + let b = (map.length >> 1) - 1 + while (a <= b) { + let m = (a + b) >> 1 + let x = map[m<<1] + if (key < x) + b = m - 1 + else if (key > x) + a = m + 1 + else { + map[(m<<1)+1] = value + return + } + } + array_insert_pair(map, a<<1, key, value) +} + +function map_delete(map, key) { + let a = 0 + let b = (map.length >> 1) - 1 + while (a <= b) { + let m = (a + b) >> 1 + let x = map[m<<1] + if (key < x) + b = m - 1 + else if (key > x) + a = m + 1 + else { + array_remove_pair(map, m<<1) + return + } + } +} + +function object_diff(a, b) { + if (a === b) + return false + if (a !== null && b !== null && typeof a === "object" && typeof b === "object") { + if (Array.isArray(a)) { + if (!Array.isArray(b)) + return true + let a_length = a.length + if (b.length !== a_length) + return true + for (let i = 0; i < a_length; ++i) + if (object_diff(a[i], b[i])) + return true + return false + } + for (let key in a) + if (object_diff(a[key], b[key])) + return true + for (let key in b) + if (!(key in a)) + return true + return false + } + return true +} + +// same as Object.groupBy +function object_group_by(items, callback) { + let groups = {} + if (typeof callback === "function") { + for (let item of items) { + let key = callback(item) + if (key in groups) + groups[key].push(item) + else + groups[key] = [ item ] + } + } else { + for (let item of items) { + let key = item[callback] + if (key in groups) + groups[key].push(item) + else + groups[key] = [ item ] + } + } + return groups +} |