diff options
author | Tor Andersson <tor@ccxvii.net> | 2022-10-02 17:33:01 +0200 |
---|---|---|
committer | Tor Andersson <tor@ccxvii.net> | 2022-11-30 13:26:51 +0100 |
commit | 11016ba45398d98a02e063de65e1e1fa9d73a58c (patch) | |
tree | 284ff3715ac5346067f504d9297b964f4c56b688 | |
parent | 04bd6f688a047ccaa804c4e32f64405873b2f49b (diff) | |
download | crusader-rex-11016ba45398d98a02e063de65e1e1fa9d73a58c.tar.gz |
Optimize game state representation using numbers instead of string ids.
-rw-r--r-- | data.js | 837 | ||||
-rw-r--r-- | play.css | 16 | ||||
-rw-r--r-- | play.js | 902 | ||||
-rw-r--r-- | rules.js | 863 |
4 files changed, 1436 insertions, 1182 deletions
@@ -1,4 +1,4 @@ -"use strict"; +"use strict" const CARDS = { 1: { name: "Assassins", event: "assassins", image: "card_assassins" }, @@ -28,418 +28,479 @@ const CARDS = { 25: { name: "a 1", moves: 1, image: "card_1" }, 26: { name: "a 1", moves: 1, image: "card_1" }, 27: { name: "a 1", moves: 1, image: "card_1" }, -}; - -const BLOCKS = {}; -const ROADS = {}; - -const SHIELDS = { - Antioch: [ "Bohemond", "Templars", "Turcopoles" ], - Latakia: [ "Bohemond" ], - "Sa\xf4ne": [ "Josselin" ], - Margat: [ "Hospitallers" ], - Krak: [ "Hospitallers" ], - Tartus: [ "Templars" ], - Tripoli: [ "Bohemond", "Raymond" ], - Beirut: [ "Turcopoles", "King Guy" ], - Sidon: [ "Reynald (Sidon)" ], - Beaufort: [ "Reynald (Sidon)" ], - Tyre: [ "Conrad", "King Guy" ], - Acre: [ "Turcopoles", "Hospitallers", "King Guy" ], - Tiberias: [ "Turcopoles", "Raymond" ], - Baisan: [ "Hospitallers" ], - Caesarea: [ "Walter" ], - Nablus: [ "Balian" ], - Amman: [ "Templars" ], - Jaffa: [ "King Guy" ], - Jerusalem: [ "King Guy", "Hospitallers", "Templars" ], - Ascalon: [ "Balian", "King Guy" ], - Hebron: [ "King Guy" ], - Gaza: [ "Templars" ], - Kerak: [ "Reynald (Kerak)" ], - Egypt: [ "Saladin", "Qara-Qush", "Yuzpah" ], - Aleppo: [ "Saladin", "Sanjar", "Zangi" ], - Ashtera: [ "Yazkuj" ], - Artah: [ "Sulaiman" ], - Damascus: [ "Saladin", "Keukburi", "Al Mashtub" ], - Homs: [ "Tuman", "Shirkuh" ], - Zerdana: [ "Jurdik" ], - Baalbek: [ "Bahram" ], - Hama: [ "Taqi al Din" ], - Banyas: [ "Qaimaz" ] } -// From edit.html output -const TOWNS = { - "Acre":{"x":452,"y":1566}, - "Ajlun":{"x":987,"y":1542}, - "Albara":{"x":810,"y":388}, - "Aleppo":{"x":1051,"y":108}, - "Amman":{"x":1088,"y":1838}, - "Anjar":{"x":753,"y":1129}, - "Antioch":{"x":471,"y":189}, - "Artah":{"x":865,"y":149}, - "Ascalon":{"x":365,"y":2077}, - "Ashtera":{"x":1038,"y":1419}, - "Baalbek":{"x":842,"y":1008}, - "Baisan":{"x":707,"y":1685}, - "Banyas":{"x":764,"y":1362}, - "Beaufort":{"x":605,"y":1354}, - "Beersheba":{"x":444,"y":2283}, - "Beirut":{"x":527,"y":1137}, - "Botron":{"x":540,"y":995}, - "Caesarea":{"x":402,"y":1754}, - "Damascus":{"x":1059,"y":1185}, - "Damiya":{"x":847,"y":1808}, - "Dimona":{"x":630,"y":2294}, - "Egypt":{"x":202,"y":2318}, - "Gaza":{"x":300,"y":2185}, - "Hama":{"x":1035,"y":477}, - "Harim":{"x":700,"y":120}, - "Hebron":{"x":680,"y":2115}, - "Homs":{"x":1053,"y":683}, - "Jaffa":{"x":399,"y":1923}, - "Jericho":{"x":836,"y":1931}, - "Jerusalem":{"x":680,"y":1980}, - "Kassab":{"x":426,"y":339}, - "Kerak":{"x":1008,"y":2076}, - "Krak":{"x":774,"y":726}, - "Lachish":{"x":495,"y":2148}, - "Lacum":{"x":919,"y":885}, - "Latakia":{"x":401,"y":445}, - "Legio":{"x":587,"y":1658}, - "Margat":{"x":540,"y":567}, - "Masyaf":{"x":756,"y":608}, - "Monterrand":{"x":920,"y":603}, - "Nablus":{"x":643,"y":1787}, - "Qaddas":{"x":1145,"y":916}, - "Ramallah":{"x":514,"y":1950}, - "Sa\xf4ne":{"x":650,"y":430}, - "Shughur":{"x":655,"y":300}, - "Sidon":{"x":493,"y":1276}, - "St. Simeon":{"x":364,"y":211}, - "Tartus":{"x":605,"y":718}, - "Tiberias":{"x":699,"y":1560}, - "Tripoli":{"x":621,"y":882}, - "Tyre":{"x":465,"y":1397}, - "Zerdana":{"x":1021,"y":300}, - "Zoar":{"x":955,"y":2278}, - "Germania":{"x":140,"y":272}, - "France":{"x":140,"y":573}, - "England":{"x":140,"y":873}, - "Sea":{"x":320,"y":900}, - "FP":{"x":15,"y":946}, - "SP":{"x":1275-15,"y":946}, - "Dead":{"x":50,"y":80}, -}; - -const PORTS = []; - -(function () { +const BLOCKS = [] +const block_index = {} + +const TOWNS = [] +const town_index = {} + +const ROADS = {} +const PORTS = [] +const SHIELDS = [] + +function init_towns() { + + // From edit.html output + const TOWNS_XY = { + "Acre":{"x":452,"y":1566}, + "Ajlun":{"x":987,"y":1542}, + "Albara":{"x":810,"y":388}, + "Aleppo":{"x":1051,"y":108}, + "Amman":{"x":1088,"y":1838}, + "Anjar":{"x":753,"y":1129}, + "Antioch":{"x":471,"y":189}, + "Artah":{"x":865,"y":149}, + "Ascalon":{"x":365,"y":2077}, + "Ashtera":{"x":1038,"y":1419}, + "Baalbek":{"x":842,"y":1008}, + "Baisan":{"x":707,"y":1685}, + "Banyas":{"x":764,"y":1362}, + "Beaufort":{"x":605,"y":1354}, + "Beersheba":{"x":444,"y":2283}, + "Beirut":{"x":527,"y":1137}, + "Botron":{"x":540,"y":995}, + "Caesarea":{"x":402,"y":1754}, + "Damascus":{"x":1059,"y":1185}, + "Damiya":{"x":847,"y":1808}, + "Dimona":{"x":630,"y":2294}, + "Egypt":{"x":202,"y":2318}, + "Gaza":{"x":300,"y":2185}, + "Hama":{"x":1035,"y":477}, + "Harim":{"x":700,"y":120}, + "Hebron":{"x":680,"y":2115}, + "Homs":{"x":1053,"y":683}, + "Jaffa":{"x":399,"y":1923}, + "Jericho":{"x":836,"y":1931}, + "Jerusalem":{"x":680,"y":1980}, + "Kassab":{"x":426,"y":339}, + "Kerak":{"x":1008,"y":2076}, + "Krak":{"x":774,"y":726}, + "Lachish":{"x":495,"y":2148}, + "Lacum":{"x":919,"y":885}, + "Latakia":{"x":401,"y":445}, + "Legio":{"x":587,"y":1658}, + "Margat":{"x":540,"y":567}, + "Masyaf":{"x":756,"y":608}, + "Monterrand":{"x":920,"y":603}, + "Nablus":{"x":643,"y":1787}, + "Qaddas":{"x":1145,"y":916}, + "Ramallah":{"x":514,"y":1950}, + "Sa\xf4ne":{"x":650,"y":430}, + "Shughur":{"x":655,"y":300}, + "Sidon":{"x":493,"y":1276}, + "St. Simeon":{"x":364,"y":211}, + "Tartus":{"x":605,"y":718}, + "Tiberias":{"x":699,"y":1560}, + "Tripoli":{"x":621,"y":882}, + "Tyre":{"x":465,"y":1397}, + "Zerdana":{"x":1021,"y":300}, + "Zoar":{"x":955,"y":2278}, + "Germania":{"x":140,"y":272}, + "France":{"x":140,"y":573}, + "England":{"x":140,"y":873}, + "Sea":{"x":320,"y":900}, + "FP":{"x":15,"y":946}, + "SP":{"x":1275-15,"y":946}, + "Dead":{"x":50,"y":80}, + "Nowhere":{"x":50,"y":80}, + } + + function town(axis, major_align, minor_align, wrap, region, name, rating, type) { + let i = town_index[name] = TOWNS.length + TOWNS.push({ + name, + region, + type, + rating, + port: (type === 'port' || type === 'fortified-port'), + fortified_port: (type === 'fortified-port'), + exits: [], + layout: { + x: TOWNS_XY[name].x, + y: TOWNS_XY[name].y, + axis, + major: 1 - major_align, + minor: 1 - minor_align, + wrap + } + }) + if (type === 'port' || type === 'fortified-port') + PORTS.push(i) + } + + town('Y', 1.0, 1.0, 50, "Pool", "Nowhere", 0, "pool") + town('Y', 1.0, 1.0, 3, "Pool", "Dead", 0, "pool") + town('Y', 0.5, 1.0, 50, "Pool", "FP", 0, "pool") + town('Y', 0.5, 1.0, 50, "Pool", "SP", 0, "pool") + town('Y', 1.0, 1.0, 1, "Pool", "Sea", 0, "pool") + + town('Y', 1.0, 0.5, 3, "Staging", "England", 3, "staging") + town('Y', 1.0, 0.5, 3, "Staging", "France", 3, "staging") + town('Y', 1.0, 0.5, 3, "Staging", "Germania", 3, "staging") + + town('X', 1.0, 0.5, 3, "Syria", "Aleppo", 3, "town") + town('Y', 0.5, 0.5, 3, "Syria", "Artah", 1, "town") + town('X', 1.0, 0.5, 3, "Syria", "Zerdana", 1, "town") + town('X', 1.0, 0.5, 3, "Syria", "Hama", 1, "town") + town('X', 0.9, 0.5, 3, "Syria", "Homs", 2, "town") + town('X', 0.3, 0.5, 3, "Syria", "Lacum", 0, "town") + town('X', 0.3, 0.5, 3, "Syria", "Qaddas", 0, "town") + town('X', 0.5, 1.0, 3, "Syria", "Baalbek", 1, "town") + town('X', 0.5, 1.0, 3, "Syria", "Anjar", 0, "town") + town('X', 0.5, 0.5, 4, "Syria", "Damascus", 4, "town") + town('X', 1.0, 0.5, 3, "Syria", "Banyas", 1, "town") + town('X', 1.0, 0.5, 3, "Syria", "Ashtera", 1, "town") + town('X', 1.0, 0.5, 3, "Syria", "Ajlun", 0, "town") + + town('X', 0.0, 0.5, 3, "Antioch", "St. Simeon", 0, "port") + town('Y', 0.5, 0.5, 3, "Antioch", "Antioch", 3, "town") + town('Y', 0.5, 0.5, 3, "Antioch", "Harim", 0, "town") + town('X', 0.5, 0.5, 3, "Antioch", "Kassab", 0, "town") + town('X', 0.5, 0.5, 3, "Antioch", "Shughur", 0, "town") + town('X', 0.0, 0.5, 3, "Antioch", "Latakia", 1, "port") + town('X', 0.5, 0.5, 3, "Antioch", "Sa\xf4ne", 1, "town") + town('Y', 0.5, 0.5, 3, "Antioch", "Albara", 0, "town") + town('X', 0.0, 0.5, 3, "Antioch", "Margat", 1, "port") + + town('X', 0.5, 0.5, 1, "Masyaf", "Masyaf", 1, "town") + + town('Y', 0.5, 0.5, 3, "Tripoli", "Monterrand", 0, "town") + town('X', 0.0, 0.5, 3, "Tripoli", "Tartus", 1, "port") + town('X', 1.0, 0.5, 3, "Tripoli", "Krak", 1, "town") + town('X', 0.0, 0.5, 3, "Tripoli", "Tripoli", 2, "fortified-port") + town('X', 0.0, 0.5, 3, "Tripoli", "Botron", 0, "town") + + town('X', 0.0, 0.5, 3, "Jerusalem", "Beirut", 2, "port") + town('X', 0.0, 0.5, 3, "Jerusalem", "Sidon", 1, "port") + town('X', 0.0, 0.5, 3, "Jerusalem", "Tyre", 2, "fortified-port") + town('Y', 0.5, 0.5, 3, "Jerusalem", "Beaufort", 1, "town") + town('X', 0.0, 0.5, 3, "Jerusalem", "Acre", 3, "port") + town('X', 1.0, 0.5, 3, "Jerusalem", "Tiberias", 2, "town") + town('Y', 1.0, 0.5, 3, "Jerusalem", "Legio", 0, "town") + town('X', 1.0, 0.5, 3, "Jerusalem", "Baisan", 1, "town") + town('X', 0.0, 0.5, 3, "Jerusalem", "Caesarea", 1, "port") + town('X', 0.5, 0.5, 3, "Jerusalem", "Nablus", 1, "town") + town('X', 0.5, 0.5, 3, "Jerusalem", "Damiya", 0, "town") + town('X', 0.5, 0.5, 3, "Jerusalem", "Amman", 1, "town") + town('X', 0.0, 0.5, 3, "Jerusalem", "Jaffa", 1, "port") + town('Y', 0.5, 0.5, 3, "Jerusalem", "Ramallah", 0, "town") + town('X', 0.5, 0.4, 3, "Jerusalem", "Jerusalem", 3, "town") + town('Y', 0.5, 0.5, 3, "Jerusalem", "Jericho", 0, "town") + town('X', 0.1, 0.5, 6, "Jerusalem", "Ascalon", 2, "port") + town('Y', 0.5, 0.5, 3, "Jerusalem", "Lachish", 0, "town") + town('X', 0.5, 1.0, 3, "Jerusalem", "Hebron", 1, "town") + town('X', 1.0, 0.5, 3, "Jerusalem", "Kerak", 1, "town") + town('X', 0.5, 0.5, 6, "Jerusalem", "Gaza", 1, "town") + town('Y', 0.5, 0.5, 3, "Jerusalem", "Beersheba", 0, "town") + town('X', 0.5, 0.5, 3, "Jerusalem", "Dimona", 0, "town") + town('X', 1.0, 0.5, 3, "Jerusalem", "Zoar", 0, "town") + + town('X', 0.5, 0.5, 4, "Egypt", "Egypt", 4, "port") +} + +function init_roads() { + function road(a,b,type) { + a = town_index[a] + b = town_index[b] + let id = (a < b) ? a * 100 + b : b * 100 + a + ROADS[id] = type + TOWNS[a].exits.push(b) + TOWNS[b].exits.push(a) + } + + function offmap(a,b,type) { + a = town_index[a] + b = town_index[b] + let id = (a < b) ? a * 100 + b : b * 100 + a + ROADS[id] = type + } + + function iron_bridge(A,B) { road(A,B,"iron-bridge"); } + function major(A,B) { road(A,B,"major"); } + function minor(A,B) { road(A,B,"minor"); } + + iron_bridge("Antioch", "Harim") + major("Harim", "Artah") + major("Artah", "Aleppo") + major("Aleppo", "Zerdana") + major("Zerdana", "Hama") + major("Hama", "Albara") + major("Hama", "Monterrand") + major("Hama", "Homs") + major("Albara", "Shughur") + major("Shughur", "Harim") + major("Monterrand", "Krak") + major("Krak", "Homs") + major("Krak", "Tripoli") + major("Tripoli", "Tartus") + major("Tripoli", "Botron") + major("Tartus", "Margat") + major("Margat", "Latakia") + major("Botron", "Beirut") + major("Beirut", "Sidon") + major("Sidon", "Tyre") + major("Tyre", "Beaufort") + major("Beaufort", "Banyas") + major("Banyas", "Damascus") + major("Damascus", "Qaddas") + major("Qaddas", "Homs") + major("Homs", "Lacum") + major("Lacum", "Baalbek") + major("Baalbek", "Anjar") + major("Anjar", "Beaufort") + major("Damascus", "Ashtera") + major("Ashtera", "Ajlun") + major("Ajlun", "Amman") + major("Amman", "Kerak") + major("Kerak", "Zoar") + major("Zoar", "Hebron") + major("Hebron", "Jerusalem") + major("Jerusalem", "Ramallah") + major("Ramallah", "Jaffa") + major("Jaffa", "Ascalon") + major("Ascalon", "Gaza") + major("Gaza", "Egypt") + major("Ajlun", "Tiberias") + major("Tiberias", "Acre") + major("Acre", "Legio") + major("Legio", "Baisan") + major("Baisan", "Tiberias") + major("Baisan", "Nablus") + major("Nablus", "Legio") + major("Nablus", "Jerusalem") + major("Acre", "Caesarea") + major("Caesarea", "Jaffa") + + minor("St. Simeon", "Antioch") + minor("Antioch", "Kassab") + minor("Kassab", "Latakia") + minor("Latakia", "Sa\xf4ne") + minor("Sa\xf4ne", "Shughur") + minor("Sa\xf4ne", "Albara") + minor("Albara", "Zerdana") + minor("Zerdana", "Artah") + + minor("Monterrand", "Homs") + + minor("Tartus", "Krak") + minor("Krak", "Lacum") + minor("Lacum", "Qaddas") + minor("Tripoli", "Baalbek") + minor("Beirut", "Anjar") + minor("Anjar", "Damascus") + minor("Sidon", "Beaufort") + minor("Tiberias", "Banyas") + minor("Banyas", "Ashtera") + minor("Tyre", "Acre") + minor("Caesarea", "Nablus") + minor("Nablus", "Damiya") + minor("Damiya", "Baisan") + minor("Damiya", "Amman") + minor("Amman", "Jericho") + minor("Jericho", "Damiya") + minor("Jericho", "Kerak") + minor("Jericho", "Jerusalem") + + minor("Ramallah", "Ascalon") + minor("Ascalon", "Lachish") + minor("Lachish", "Gaza") + minor("Gaza", "Beersheba") + minor("Beersheba", "Egypt") + minor("Beersheba", "Dimona") + minor("Dimona", "Zoar") + minor("Dimona", "Hebron") + minor("Hebron", "Lachish") + + offmap("Germania", "St. Simeon", 'minor') + offmap("Germania", "Aleppo", 'major') + offmap("Germania", "Antioch", 'major') + + for (let town of TOWNS) + town.exits.sort((a,b)=>a-b) + +} + +function init_blocks() { let nomads = { Arabs: 1, Turks: 1, Kurds: 1 } function army(rc, owner, name, home, move, steps, combat, order, plural) { - let id = name; + let id = name if (order === 'Military Orders' || order === 'Pilgrims' || order === 'Turcopoles') - id = home + " " + name; + id = home + " " + name if (order === 'Nomads') - id += " " + nomads[name]++; + id += " " + nomads[name]++ if (name === 'Reynald' || name === 'Raymond') - id += " (" + home + ")"; - if (id in BLOCKS) - throw Error("Name clash: " + id + " order:"+order + " " + JSON.stringify(nomads)); - BLOCKS[id] = { + id += " (" + home + ")" + if (id in block_index) + throw Error("Name clash: " + id + " order:"+order + " " + JSON.stringify(nomads)) + + let home_idx = town_index[home] | 0 + if (home === "Normandy") home_idx = town_index["England"] + if (home === "Aquitaine") home_idx = town_index["England"] + if (home === "Bourgogne") home_idx = town_index["France"] + if (home === "Flanders") home_idx = town_index["France"] + + block_index[id] = BLOCKS.length + BLOCKS.push({ + id: id, owner: owner, name: name, plural: plural, type: order.toLowerCase().replace(/ /g, "_"), - home: home, + home: home_idx, + home_name: home, move: move, steps: steps, - combat: combat, + initiative: combat[0], + fire_power: combat[1] | 0, image: rc, - } + }) } function frank(rc, name, home, move, steps, combat, order, plural) { - army(rc, "Franks", name, home, move, steps, combat, order, plural); + army(rc, "Franks", name, home, move, steps, combat, order, plural) } + function saracen(rc, name, home, move, steps, combat, order, plural) { - army(rc, "Saracens", name, home, move, steps, combat, order, plural); + army(rc, "Saracens", name, home, move, steps, combat, order, plural) } + frank(11, "Richard", "England", 3, 4, "B4", "Crusaders", 0) + frank(12, "Philippe", "France", 2, 4, "B3", "Crusaders", 0) + frank(13, "Barbarossa", "Germania", 2, 4, "B3", "Crusaders", 0) + frank(14, "Templars", "Jerusalem", 3, 3, "B3", "Military Orders", 1) + frank(15, "Templars", "Antioch", 3, 3, "B3", "Military Orders", 1) + frank(16, "Templars", "Gaza", 3, 3, "B3", "Military Orders", 1) + frank(17, "Templars", "Tartus", 3, 2, "B3", "Military Orders", 1) + + frank(21, "Robert", "Normandy", 2, 3, "B3", "Crusaders", 0) + frank(22, "Hugues", "Bourgogne", 2, 4, "B2", "Crusaders", 0) + frank(23, "Frederik", "Germania", 2, 3, "B2", "Crusaders", 0) + frank(24, "Hospitallers", "Jerusalem", 3, 4, "B3", "Military Orders", 1) + frank(25, "Hospitallers", "Acre", 3, 3, "B3", "Military Orders", 1) + frank(26, "Hospitallers", "Krak", 3, 2, "B3", "Military Orders", 1) + frank(27, "Reynald", "Sidon", 2, 3, "B2", "Outremers", 0) + + frank(31, "Crossbows", "Aquitaine", 2, 3, "A2", "Crusaders", 1) + frank(32, "Fileps", "Flanders", 2, 3, "B3", "Crusaders", 0) + frank(33, "Leopold", "Germania", 2, 3, "B3", "Crusaders", 0) + frank(34, "Conrad", "Tyre", 2, 4, "B3", "Outremers", 0) + frank(35, "Balian", "Nablus", 2, 3, "B2", "Outremers", 0) + frank(36, "Walter", "Caesarea", 2, 3, "B2", "Outremers", 0) + frank(37, "Raymond", "Tiberias", 2, 3, "B2", "Outremers", 0) + + frank(41, "Turcopoles", "Antioch", 3, 3, "A2", "Turcopoles", 1) + frank(42, "Pilgrims", "Genoa", 2, 4, "C2", "Pilgrims", 1) + frank(43, "Pilgrims", "Sicily", 2, 3, "C2", "Pilgrims", 1) + frank(44, "King Guy", "Jerusalem", 2, 4, "B2", "Outremers", 0) + frank(45, "Reynald", "Kerak", 3, 2, "B3", "Outremers", 0) + frank(46, "Bohemond", "Antioch", 2, 4, "B2", "Outremers", 0) + frank(47, "Raymond", "Tripoli", 2, 4, "B2", "Outremers", 0) + + frank(51, "Turcopoles", "Beirut", 3, 3, "A2", "Turcopoles", 1) + frank(52, "Pilgrims", "Brittany", 2, 4, "C2", "Pilgrims", 1) + frank(53, "Josselin", "Sa\xf4ne", 2, 3, "B2", "Outremers", 0) + + saracen(55, "Qara-Qush", "Egypt", 3, 3, "B3", "Emirs", 0) + saracen(56, "Zangi", "Aleppo", 3, 3, "B2", "Emirs", 0) + saracen(57, "Sanjar", "Aleppo", 3, 3, "B2", "Emirs", 0) + + saracen(61, "Yazkuj", "Ashtera", 3, 2, "B2", "Emirs", 0) + saracen(62, "Sulaiman", "Artah", 3, 2, "B2", "Emirs", 0) + saracen(63, "Keukburi", "Damascus", 3, 3, "B3", "Emirs", 0) + saracen(64, "Shirkuh", "Homs", 3, 3, "B2", "Emirs", 0) + saracen(65, "Jurdik", "Zerdana", 3, 3, "B2", "Emirs", 0) + saracen(66, "Bahram", "Baalbek", 3, 3, "B2", "Emirs", 0) + saracen(67, "Tuman", "Homs", 3, 3, "B3", "Emirs", 0) + + saracen(71, "Taqi al Din", "Hama", 3, 4, "A2", "Emirs", 0) + saracen(72, "Al Mashtub", "Damascus", 3, 4, "B3", "Emirs", 0) + saracen(73, "Al Adil", "Egypt", 3, 4, "A2", "Emirs", 0) + saracen(74, "Saladin", "Damascus", 3, 4, "A3", "Emirs", 0) + saracen(75, "Al Aziz", "Egypt", 3, 3, "B2", "Emirs", 0) + saracen(76, "Al Afdal", "Damascus", 3, 3, "B3", "Emirs", 0) + saracen(77, "Al Zahir", "Aleppo", 3, 3, "A2", "Emirs", 0) + + saracen(81, "Yuzpah", "Egypt", 3, 4, "B2", "Emirs", 0) + saracen(82, "Qaimaz", "Banyas", 3, 3, "B2", "Emirs", 0) + + saracen(83, "Kurds", "Damascus", 3, 4, "C1", "Nomads", 1) + saracen(84, "Kurds", "Damascus", 3, 4, "C1", "Nomads", 1) + saracen(85, "Kurds", "Damascus", 3, 3, "C2", "Nomads", 1) + saracen(86, "Kurds", "Damascus", 3, 3, "C2", "Nomads", 1) + + saracen(91, "Turks", "Aleppo", 3, 3, "A2", "Nomads", 1) + saracen(92, "Turks", "Aleppo", 3, 3, "A2", "Nomads", 1) + saracen(93, "Turks", "Aleppo", 3, 4, "A1", "Nomads", 1) + saracen(94, "Turks", "Aleppo", 3, 4, "A1", "Nomads", 1) + + saracen(95, "Arabs", "Egypt", 3, 3, "B2", "Nomads", 1) + saracen(96, "Arabs", "Egypt", 3, 3, "B2", "Nomads", 1) + saracen(97, "Arabs", "Egypt", 3, 4, "B1", "Nomads", 1) + saracen(87, "Arabs", "Egypt", 3, 4, "B1", "Nomads", 1) + + // The assassins are not a real unit + army(54, "Assassins", "Assassins", "Masyaf", 0, 3, "A3", "Assassins", 1) +} - frank(11, "Richard", "England", 3, 4, "B4", "Crusaders", 0); - frank(12, "Philippe", "France", 2, 4, "B3", "Crusaders", 0); - frank(13, "Barbarossa", "Germania", 2, 4, "B3", "Crusaders", 0); - frank(14, "Templars", "Jerusalem", 3, 3, "B3", "Military Orders", 1); - frank(15, "Templars", "Antioch", 3, 3, "B3", "Military Orders", 1); - frank(16, "Templars", "Gaza", 3, 3, "B3", "Military Orders", 1); - frank(17, "Templars", "Tartus", 3, 2, "B3", "Military Orders", 1); - - frank(21, "Robert", "Normandy", 2, 3, "B3", "Crusaders", 0); - frank(22, "Hugues", "Bourgogne", 2, 4, "B2", "Crusaders", 0); - frank(23, "Frederik", "Germania", 2, 3, "B2", "Crusaders", 0); - frank(24, "Hospitallers", "Jerusalem", 3, 4, "B3", "Military Orders", 1); - frank(25, "Hospitallers", "Acre", 3, 3, "B3", "Military Orders", 1); - frank(26, "Hospitallers", "Krak", 3, 2, "B3", "Military Orders", 1); - frank(27, "Reynald", "Sidon", 2, 3, "B2", "Outremers", 0); - - frank(31, "Crossbows", "Aquitaine", 2, 3, "A2", "Crusaders", 1); - frank(32, "Fileps", "Flanders", 2, 3, "B3", "Crusaders", 0); - frank(33, "Leopold", "Germania", 2, 3, "B3", "Crusaders", 0); - frank(34, "Conrad", "Tyre", 2, 4, "B3", "Outremers", 0); - frank(35, "Balian", "Nablus", 2, 3, "B2", "Outremers", 0); - frank(36, "Walter", "Caesarea", 2, 3, "B2", "Outremers", 0); - frank(37, "Raymond", "Tiberias", 2, 3, "B2", "Outremers", 0); - - frank(41, "Turcopoles", "Antioch", 3, 3, "A2", "Turcopoles", 1); - frank(42, "Pilgrims", "Genoa", 2, 4, "C2", "Pilgrims", 1); - frank(43, "Pilgrims", "Sicily", 2, 3, "C2", "Pilgrims", 1); - frank(44, "King Guy", "Jerusalem", 2, 4, "B2", "Outremers", 0); - frank(45, "Reynald", "Kerak", 3, 2, "B3", "Outremers", 0); - frank(46, "Bohemond", "Antioch", 2, 4, "B2", "Outremers", 0); - frank(47, "Raymond", "Tripoli", 2, 4, "B2", "Outremers", 0); - - frank(51, "Turcopoles", "Beirut", 3, 3, "A2", "Turcopoles", 1); - frank(52, "Pilgrims", "Brittany", 2, 4, "C2", "Pilgrims", 1); - frank(53, "Josselin", "Sa\xf4ne", 2, 3, "B2", "Outremers", 0); - - army(54, "Assassins", "Assassins", "Masyaf", 0, 3, "A3", "Assassins", 1); - - saracen(55, "Qara-Qush", "Egypt", 3, 3, "B3", "Emirs", 0); - saracen(56, "Zangi", "Aleppo", 3, 3, "B2", "Emirs", 0); - saracen(57, "Sanjar", "Aleppo", 3, 3, "B2", "Emirs", 0); - - saracen(61, "Yazkuj", "Ashtera", 3, 2, "B2", "Emirs", 0); - saracen(62, "Sulaiman", "Artah", 3, 2, "B2", "Emirs", 0); - saracen(63, "Keukburi", "Damascus", 3, 3, "B3", "Emirs", 0); - saracen(64, "Shirkuh", "Homs", 3, 3, "B2", "Emirs", 0); - saracen(65, "Jurdik", "Zerdana", 3, 3, "B2", "Emirs", 0); - saracen(66, "Bahram", "Baalbek", 3, 3, "B2", "Emirs", 0); - saracen(67, "Tuman", "Homs", 3, 3, "B3", "Emirs", 0); - - saracen(71, "Taqi al Din", "Hama", 3, 4, "A2", "Emirs", 0); - saracen(72, "Al Mashtub", "Damascus", 3, 4, "B3", "Emirs", 0); - saracen(73, "Al Adil", "Egypt", 3, 4, "A2", "Emirs", 0); - saracen(74, "Saladin", "Damascus", 3, 4, "A3", "Emirs", 0); - saracen(75, "Al Aziz", "Egypt", 3, 3, "B2", "Emirs", 0); - saracen(76, "Al Afdal", "Damascus", 3, 3, "B3", "Emirs", 0); - saracen(77, "Al Zahir", "Aleppo", 3, 3, "A2", "Emirs", 0); - - saracen(81, "Yuzpah", "Egypt", 3, 4, "B2", "Emirs", 0); - saracen(82, "Qaimaz", "Banyas", 3, 3, "B2", "Emirs", 0); - - saracen(83, "Kurds", "Damascus", 3, 4, "C1", "Nomads", 1); - saracen(84, "Kurds", "Damascus", 3, 4, "C1", "Nomads", 1); - saracen(85, "Kurds", "Damascus", 3, 3, "C2", "Nomads", 1); - saracen(86, "Kurds", "Damascus", 3, 3, "C2", "Nomads", 1); - - saracen(91, "Turks", "Aleppo", 3, 3, "A2", "Nomads", 1); - saracen(92, "Turks", "Aleppo", 3, 3, "A2", "Nomads", 1); - saracen(93, "Turks", "Aleppo", 3, 4, "A1", "Nomads", 1); - saracen(94, "Turks", "Aleppo", 3, 4, "A1", "Nomads", 1); - - saracen(95, "Arabs", "Egypt", 3, 3, "B2", "Nomads", 1); - saracen(96, "Arabs", "Egypt", 3, 3, "B2", "Nomads", 1); - saracen(97, "Arabs", "Egypt", 3, 4, "B1", "Nomads", 1); - saracen(87, "Arabs", "Egypt", 3, 4, "B1", "Nomads", 1); - - function town(axis, major_align, minor_align, wrap, region, name, rating, type) { - TOWNS[name].region = region; - TOWNS[name].rating = rating; - if (type === 'port' || type === 'fortified-port') - TOWNS[name].port = true; - if (type === 'fortified-port') - TOWNS[name].fortified_port = true; - if (TOWNS[name].port) - PORTS.push(name); - TOWNS[name].exits = []; - TOWNS[name].layout_axis = axis; - TOWNS[name].layout_major = 1 - major_align; - TOWNS[name].layout_minor = 1 - minor_align; - TOWNS[name].wrap = wrap; - } - - town('Y', 0.5, 1.0, 50, "Pool", "FP", 0, "pool"); - town('Y', 0.5, 1.0, 50, "Pool", "SP", 0, "pool"); - town('Y', 1.0, 1.0, 3, "Pool", "Dead", 0, "pool"); - town('Y', 1.0, 1.0, 1, "Pool", "Sea", 0, "pool"); - - town('Y', 1.0, 0.5, 3, "Staging", "England", 3, "staging"); - town('Y', 1.0, 0.5, 3, "Staging", "France", 3, "staging"); - town('Y', 1.0, 0.5, 3, "Staging", "Germania", 3, "staging"); - - - town('X', 1.0, 0.5, 3, "Syria", "Aleppo", 3, "town"); - town('Y', 0.5, 0.5, 3, "Syria", "Artah", 1, "town"); - town('X', 1.0, 0.5, 3, "Syria", "Zerdana", 1, "town"); - town('X', 1.0, 0.5, 3, "Syria", "Hama", 1, "town"); - town('X', 0.9, 0.5, 3, "Syria", "Homs", 2, "town"); - town('X', 0.3, 0.5, 3, "Syria", "Lacum", 0, "town"); - town('X', 0.3, 0.5, 3, "Syria", "Qaddas", 0, "town"); - town('X', 0.5, 1.0, 3, "Syria", "Baalbek", 1, "town"); - town('X', 0.5, 1.0, 3, "Syria", "Anjar", 0, "town"); - town('X', 0.5, 0.5, 4, "Syria", "Damascus", 4, "town"); - town('X', 1.0, 0.5, 3, "Syria", "Banyas", 1, "town"); - town('X', 1.0, 0.5, 3, "Syria", "Ashtera", 1, "town"); - town('X', 1.0, 0.5, 3, "Syria", "Ajlun", 0, "town"); - - town('X', 0.0, 0.5, 3, "Antioch", "St. Simeon", 0, "port"); - town('Y', 0.5, 0.5, 3, "Antioch", "Antioch", 3, "town"); - town('Y', 0.5, 0.5, 3, "Antioch", "Harim", 0, "town"); - town('X', 0.5, 0.5, 3, "Antioch", "Kassab", 0, "town"); - town('X', 0.5, 0.5, 3, "Antioch", "Shughur", 0, "town"); - town('X', 0.0, 0.5, 3, "Antioch", "Latakia", 1, "port"); - town('X', 0.5, 0.5, 3, "Antioch", "Sa\xf4ne", 1, "town"); - town('Y', 0.5, 0.5, 3, "Antioch", "Albara", 0, "town"); - town('X', 0.0, 0.5, 3, "Antioch", "Margat", 1, "port"); - - town('X', 0.5, 0.5, 1, "Masyaf", "Masyaf", 1, "town"); - - town('Y', 0.5, 0.5, 3, "Tripoli", "Monterrand", 0, "town"); - town('X', 0.0, 0.5, 3, "Tripoli", "Tartus", 1, "port"); - town('X', 1.0, 0.5, 3, "Tripoli", "Krak", 1, "town"); - town('X', 0.0, 0.5, 3, "Tripoli", "Tripoli", 2, "fortified-port"); - town('X', 0.0, 0.5, 3, "Tripoli", "Botron", 0, "town"); - - town('X', 0.0, 0.5, 3, "Jerusalem", "Beirut", 2, "port"); - town('X', 0.0, 0.5, 3, "Jerusalem", "Sidon", 1, "port"); - town('X', 0.0, 0.5, 3, "Jerusalem", "Tyre", 2, "fortified-port"); - town('Y', 0.5, 0.5, 3, "Jerusalem", "Beaufort", 1, "town"); - town('X', 0.0, 0.5, 3, "Jerusalem", "Acre", 3, "port"); - town('X', 1.0, 0.5, 3, "Jerusalem", "Tiberias", 2, "town"); - town('Y', 1.0, 0.5, 3, "Jerusalem", "Legio", 0, "town"); - town('X', 1.0, 0.5, 3, "Jerusalem", "Baisan", 1, "town"); - town('X', 0.0, 0.5, 3, "Jerusalem", "Caesarea", 1, "port"); - town('X', 0.5, 0.5, 3, "Jerusalem", "Nablus", 1, "town"); - town('X', 0.5, 0.5, 3, "Jerusalem", "Damiya", 0, "town"); - town('X', 0.5, 0.5, 3, "Jerusalem", "Amman", 1, "town"); - town('X', 0.0, 0.5, 3, "Jerusalem", "Jaffa", 1, "port"); - town('Y', 0.5, 0.5, 3, "Jerusalem", "Ramallah", 0, "town"); - town('X', 0.5, 0.4, 3, "Jerusalem", "Jerusalem", 3, "town"); - town('Y', 0.5, 0.5, 3, "Jerusalem", "Jericho", 0, "town"); - town('X', 0.1, 0.5, 6, "Jerusalem", "Ascalon", 2, "port"); - town('Y', 0.5, 0.5, 3, "Jerusalem", "Lachish", 0, "town"); - town('X', 0.5, 1.0, 3, "Jerusalem", "Hebron", 1, "town"); - town('X', 1.0, 0.5, 3, "Jerusalem", "Kerak", 1, "town"); - town('X', 0.5, 0.5, 6, "Jerusalem", "Gaza", 1, "town"); - town('Y', 0.5, 0.5, 3, "Jerusalem", "Beersheba", 0, "town"); - town('X', 0.5, 0.5, 3, "Jerusalem", "Dimona", 0, "town"); - town('X', 1.0, 0.5, 3, "Jerusalem", "Zoar", 0, "town"); - - town('X', 0.5, 0.5, 4, "Egypt", "Egypt", 4, "port"); - - function road(A,B,type) { - let id = (A < B) ? (A + "/" + B) : (B + "/" + A); - ROADS[id] = type; - TOWNS[A].exits.push(B); - TOWNS[B].exits.push(A); +function init_shields() { + for (let i = 0; i < TOWNS.length; ++i) + SHIELDS[i] = [] + function shield(town, block_names) { + town = town_index[town] + for (let name of block_names) { + if (name in block_index) { + SHIELDS[town].push(block_index[name]) + } else { + for (let b = 0; b < BLOCKS.length; ++b) + if (BLOCKS[b].name === name) + SHIELDS[town].push(b) + } + } + SHIELDS[town].sort((a,b)=>a-b) } - function iron_bridge(A,B) { road(A,B,"iron-bridge"); } - function major(A,B) { road(A,B,"major"); } - function minor(A,B) { road(A,B,"minor"); } + shield("Antioch", [ "Bohemond", "Templars", "Turcopoles" ]) + shield("Latakia", [ "Bohemond" ]) + shield("Sa\xf4ne", [ "Josselin" ]) + shield("Margat", [ "Hospitallers" ]) + shield("Krak", [ "Hospitallers" ]) + shield("Tartus", [ "Templars" ]) + shield("Tripoli", [ "Bohemond", "Raymond" ]) + shield("Beirut", [ "Turcopoles", "King Guy" ]) + shield("Sidon", [ "Reynald (Sidon)" ]) + shield("Beaufort", [ "Reynald (Sidon)" ]) + shield("Tyre", [ "Conrad", "King Guy" ]) + shield("Acre", [ "Turcopoles", "Hospitallers", "King Guy" ]) + shield("Tiberias", [ "Turcopoles", "Raymond" ]) + shield("Baisan", [ "Hospitallers" ]) + shield("Caesarea", [ "Walter" ]) + shield("Nablus", [ "Balian" ]) + shield("Amman", [ "Templars" ]) + shield("Jaffa", [ "King Guy" ]) + shield("Jerusalem", [ "King Guy", "Hospitallers", "Templars" ]) + shield("Ascalon", [ "Balian", "King Guy" ]) + shield("Hebron", [ "King Guy" ]) + shield("Gaza", [ "Templars" ]) + shield("Kerak", [ "Reynald (Kerak)" ]) + shield("Egypt", [ "Saladin", "Al Adil", "Al Aziz", "Al Afdal", "Al Zahir", "Qara-Qush", "Yuzpah" ]) + shield("Aleppo", [ "Saladin", "Al Adil", "Al Aziz", "Al Afdal", "Al Zahir", "Sanjar", "Zangi" ]) + shield("Ashtera", [ "Yazkuj" ]) + shield("Artah", [ "Sulaiman" ]) + shield("Damascus", [ "Saladin", "Al Adil", "Al Aziz", "Al Afdal", "Al Zahir", "Keukburi", "Al Mashtub" ]) + shield("Homs", [ "Tuman", "Shirkuh" ]) + shield("Zerdana", [ "Jurdik" ]) + shield("Baalbek", [ "Bahram" ]) + shield("Hama", [ "Taqi al Din" ]) + shield("Banyas", [ "Qaimaz" ]) +} - iron_bridge("Antioch", "Harim"); - major("Harim", "Artah"); - major("Artah", "Aleppo"); - major("Aleppo", "Zerdana"); - major("Zerdana", "Hama"); - major("Hama", "Albara"); - major("Hama", "Monterrand"); - major("Hama", "Homs"); - major("Albara", "Shughur"); - major("Shughur", "Harim"); - major("Monterrand", "Krak"); - major("Krak", "Homs"); - major("Krak", "Tripoli"); - major("Tripoli", "Tartus"); - major("Tripoli", "Botron"); - major("Tartus", "Margat"); - major("Margat", "Latakia"); - major("Botron", "Beirut"); - major("Beirut", "Sidon"); - major("Sidon", "Tyre"); - major("Tyre", "Beaufort"); - major("Beaufort", "Banyas"); - major("Banyas", "Damascus"); - major("Damascus", "Qaddas"); - major("Qaddas", "Homs"); - major("Homs", "Lacum"); - major("Lacum", "Baalbek"); - major("Baalbek", "Anjar"); - major("Anjar", "Beaufort"); - major("Damascus", "Ashtera"); - major("Ashtera", "Ajlun"); - major("Ajlun", "Amman"); - major("Amman", "Kerak"); - major("Kerak", "Zoar"); - major("Zoar", "Hebron"); - major("Hebron", "Jerusalem"); - major("Jerusalem", "Ramallah"); - major("Ramallah", "Jaffa"); - major("Jaffa", "Ascalon"); - major("Ascalon", "Gaza"); - major("Gaza", "Egypt"); - major("Ajlun", "Tiberias"); - major("Tiberias", "Acre"); - major("Acre", "Legio"); - major("Legio", "Baisan"); - major("Baisan", "Tiberias"); - major("Baisan", "Nablus"); - major("Nablus", "Legio"); - major("Nablus", "Jerusalem"); - major("Acre", "Caesarea"); - major("Caesarea", "Jaffa"); - - minor("St. Simeon", "Antioch"); - minor("Antioch", "Kassab"); - minor("Kassab", "Latakia"); - minor("Latakia", "Sa\xf4ne"); - minor("Sa\xf4ne", "Shughur"); - minor("Sa\xf4ne", "Albara"); - minor("Albara", "Zerdana"); - minor("Zerdana", "Artah"); - - minor("Monterrand", "Homs"); - - minor("Tartus", "Krak"); - minor("Krak", "Lacum"); - minor("Lacum", "Qaddas"); - minor("Tripoli", "Baalbek"); - minor("Beirut", "Anjar"); - minor("Anjar", "Damascus"); - minor("Sidon", "Beaufort"); - minor("Tiberias", "Banyas"); - minor("Banyas", "Ashtera"); - minor("Tyre", "Acre"); - minor("Caesarea", "Nablus"); - minor("Nablus", "Damiya"); - minor("Damiya", "Baisan"); - minor("Damiya", "Amman"); - minor("Amman", "Jericho"); - minor("Jericho", "Damiya"); - minor("Jericho", "Kerak"); - minor("Jericho", "Jerusalem"); - - minor("Ramallah", "Ascalon"); - minor("Ascalon", "Lachish"); - minor("Lachish", "Gaza"); - minor("Gaza", "Beersheba"); - minor("Beersheba", "Egypt"); - minor("Beersheba", "Dimona"); - minor("Dimona", "Zoar"); - minor("Dimona", "Hebron"); - minor("Hebron", "Lachish"); - - // off-map roads - ROADS["Germania/St. Simeon"] = 'minor'; - ROADS["Aleppo/Germania"] = 'major'; - ROADS["Antioch/Germania"] = 'major'; - - // TODO: seats and alternate seats -})(); +init_towns() +init_roads() +init_blocks() +init_shields() if (typeof module !== 'undefined') - module.exports = { CARDS, BLOCKS, TOWNS, PORTS, ROADS, SHIELDS } + module.exports = { CARDS, BLOCKS, TOWNS, PORTS, ROADS, SHIELDS, block_index, town_index } @@ -9,12 +9,14 @@ body.Saracens header.your_turn { background-color: lightgreen; } .role_vp { float: right; } #log { background-color: whitesmoke; } -#log div { padding-left: 20px; text-indent: -12px; } -#log .st { background-color: #246; color: white; font-weight: bold; } +#log div { padding-left: 24px; text-indent: -12px; } +#log div.i { padding-left: 36px; text-indent: -12px; } +#log .h1 { background-color: #246; color: white; font-weight: bold; } #log .F { background-color: khaki; } #log .S { background-color: darkseagreen; } -#log .bs { background-color: lightgray; } -#log .br { font-style: italic; text-decoration: underline; } +#log .h3 { background-color: lightgray; } +#log .h4 { font-style: italic; text-decoration: underline; } +#log .tip:hover { text-decoration: underline; cursor: pointer; } #map #timeline { position: absolute; @@ -142,6 +144,12 @@ body.Saracens header.your_turn { background-color: lightgreen; } opacity: 0.6; z-index: 9; } +.town.tip { + opacity: 1; + border-color: yellow; + border-style: dashed; + z-index: 9; +} .town.muster { opacity: 0.6; border-color: brown; @@ -1,219 +1,302 @@ -"use strict"; +"use strict" + +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 +} + +const FRANKS = "Franks" +const SARACENS = "Saracens" -const FRANKS = "Franks"; -const SARACENS = "Saracens"; -const ASSASSINS = "Assassins"; const ENEMY = { Saracens: "Franks", Franks: "Saracens" } -const DEAD = "Dead"; -const F_POOL = "FP"; -const S_POOL = "SP"; -const ENGLAND = "England"; -const FRANCE = "France"; -const GERMANIA = "Germania"; + +const NOWHERE = 0 +const DEAD = 1 +const F_POOL = 2 +const S_POOL = 3 +const SEA = 4 + +const ENGLAND = 5 +const FRANCE = 6 +const GERMANIA = 7 + +const SHIELD_NAMES = {} +SHIELD_NAMES[town_index["Antioch"]] = "Bohemond, Templars, Turcopoles" +SHIELD_NAMES[town_index["Latakia"]] = "Bohemond" +SHIELD_NAMES[town_index["Sa\xf4ne"]] = "Josselin" +SHIELD_NAMES[town_index["Margat"]] = "Hospitallers" +SHIELD_NAMES[town_index["Krak"]] = "Hospitallers" +SHIELD_NAMES[town_index["Tartus"]] = "Templars" +SHIELD_NAMES[town_index["Tripoli"]] = "Bohemond, Raymond" +SHIELD_NAMES[town_index["Beirut"]] = "Turcopoles, King Guy" +SHIELD_NAMES[town_index["Sidon"]] = "Reynald (Sidon)" +SHIELD_NAMES[town_index["Beaufort"]] = "Reynald (Sidon)" +SHIELD_NAMES[town_index["Tyre"]] = "Conrad, King Guy" +SHIELD_NAMES[town_index["Acre"]] = "Turcopoles, Hospitallers, King Guy" +SHIELD_NAMES[town_index["Tiberias"]] = "Turcopoles, Raymond" +SHIELD_NAMES[town_index["Baisan"]] = "Hospitallers" +SHIELD_NAMES[town_index["Caesarea"]] = "Walter" +SHIELD_NAMES[town_index["Nablus"]] = "Balian" +SHIELD_NAMES[town_index["Amman"]] = "Templars" +SHIELD_NAMES[town_index["Jaffa"]] = "King Guy" +SHIELD_NAMES[town_index["Jerusalem"]] = "King Guy, Hospitallers, Templars" +SHIELD_NAMES[town_index["Ascalon"]] = "Balian, King Guy" +SHIELD_NAMES[town_index["Hebron"]] = "King Guy" +SHIELD_NAMES[town_index["Gaza"]] = "Templars" +SHIELD_NAMES[town_index["Kerak"]] = "Reynald (Kerak)" +SHIELD_NAMES[town_index["Egypt"]] = "Saladin, Qara-Qush, Yuzpah" +SHIELD_NAMES[town_index["Aleppo"]] = "Saladin, Sanjar, Zangi" +SHIELD_NAMES[town_index["Ashtera"]] = "Yazkuj" +SHIELD_NAMES[town_index["Artah"]] = "Sulaiman" +SHIELD_NAMES[town_index["Damascus"]] = "Saladin, Keukburi, Al Mashtub" +SHIELD_NAMES[town_index["Homs"]] = "Tuman, Shirkuh" +SHIELD_NAMES[town_index["Zerdana"]] = "Jurdik" +SHIELD_NAMES[town_index["Baalbek"]] = "Bahram" +SHIELD_NAMES[town_index["Hama"]] = "Taqi al Din" +SHIELD_NAMES[town_index["Banyas"]] = "Qaimaz" const KINGDOM = { "Syria": "Syria", "Jerusalem": "Kingdom of Jerusalem", "Antioch": "Principality of Antioch", "Tripoli": "County of Tripoli", -}; +} const VICTORY_TOWNS = [ - "Aleppo", "Damascus", "Egypt", - "Antioch", "Tripoli", "Acre", "Jerusalem" -]; + town_index["Aleppo"], + town_index["Damascus"], + town_index["Egypt"], + town_index["Antioch"], + town_index["Tripoli"], + town_index["Acre"], + town_index["Jerusalem"] +] -let label_layout = window.localStorage['crusader-rex/label-layout'] || 'spread'; +let label_layout = window.localStorage['crusader-rex/label-layout'] || 'spread' function set_spread_layout() { - label_layout = 'spread'; - window.localStorage['crusader-rex/label-layout'] = label_layout; - update_map(); + label_layout = 'spread' + window.localStorage['crusader-rex/label-layout'] = label_layout + update_map() } function set_stack_layout() { - label_layout = 'stack'; - window.localStorage['crusader-rex/label-layout'] = label_layout; - update_map(); + label_layout = 'stack' + window.localStorage['crusader-rex/label-layout'] = label_layout + update_map() } function toggle_blocks() { - document.getElementById("map").classList.toggle("hide_blocks"); + document.getElementById("map").classList.toggle("hide_blocks") } let ui = { - cards: {}, - card_backs: {}, - towns: {}, - blocks: {}, - battle_menu: {}, - battle_block: {}, + cards: [], + card_backs: [], + towns: [], + blocks: [], + battle_menu: [], + battle_block: [], present: new Set(), } +function on_focus_space_tip(x) { + ui.towns[x].classList.add("tip") +} + +function on_blur_space_tip(x) { + ui.towns[x].classList.remove("tip") +} + +function on_click_space_tip(x) { + ui.towns[x].scrollIntoView({ block:"center", inline:"center", behavior:"smooth" }) +} + +function sub_space_name(match, p1, offset, string) { + let x = p1 | 0 + let n = TOWNS[x].name + return `<span class="tip" onmouseenter="on_focus_space_tip(${x})" onmouseleave="on_blur_space_tip(${x})" onclick="on_click_space_tip(${x})">${n}</span>` +} + function on_log(text) { - let p = document.createElement("div"); - text = text.replace(/&/g, "&"); - text = text.replace(/</g, "<"); - text = text.replace(/>/g, ">"); + let p = document.createElement("div") + + if (text.match(/^>/)) { + text = text.substring(1) + p.className = "i" + } + + text = text.replace(/&/g, "&") + text = text.replace(/</g, "<") + text = text.replace(/>/g, ">") - text = text.replace(/\u2192 /g, "\u2192\xa0"); + text = text.replace(/\u2192 /g, "\u2192\xa0") - text = text.replace(/^([A-Z]):/, '<span class="$1"> $1 </span>'); + text = text.replace(/^([A-Z]):/, '<span class="$1"> $1 </span>') - if (text.match(/^~ .* ~$/)) - p.className = 'br', text = text.substring(2, text.length-2); - else if (text.match(/^Start Franks/)) - p.className = 'F'; - else if (text.match(/^Start Saracens/)) - p.className = 'S'; - else if (text.match(/^Start /)) - p.className = 'st', text = text.replace(/\.$/, ""); - else if (text.match(/^(Battle in)/)) - p.className = 'bs'; + text = text.replace(/#(\d+)/g, sub_space_name) - if (text.match(/^Start /)) - text = text.substring(6); + if (text.match(/^\.h1 /)) + p.className = 'h1', text = text.substring(4) + if (text.match(/^\.h2 F/)) + p.className = 'h2 F', text = text.substring(4) + if (text.match(/^\.h2 S/)) + p.className = 'h2 S', text = text.substring(4) + if (text.match(/^\.h3 /)) + p.className = 'h3', text = text.substring(4) + if (text.match(/^\.h4 /)) + p.className = 'h4', text = text.substring(4) - p.innerHTML = text; - return p; + p.innerHTML = text + return p } function on_focus_town(evt) { - let where = evt.target.town; - let text = where; - if (where in SHIELDS) - text += " \u2014 " + SHIELDS[where].join(", "); - let kingdom = KINGDOM[TOWNS[where].region]; + let where = evt.target.town + let text = TOWNS[where].name + if (where in SHIELD_NAMES) + text += " \u2014 " + SHIELD_NAMES[where] + let kingdom = KINGDOM[TOWNS[where].region] if (kingdom) - text += " \u2014 " + kingdom; + text += " \u2014 " + kingdom if (VICTORY_TOWNS.includes(where)) - text += " \u2014 1 VP"; - document.getElementById("status").textContent = text; + text += " \u2014 1 VP" + document.getElementById("status").textContent = text } function on_blur_town(evt) { - document.getElementById("status").textContent = ""; + document.getElementById("status").textContent = "" } function on_click_town(evt) { - let where = evt.target.town; - send_action('town', where); + let where = evt.target.town + send_action('town', where) } -const STEP_TEXT = [ 0, "I", "II", "III", "IIII" ]; -const HEIR_TEXT = [ 0, '\u00b9', '\u00b2', '\u00b3', '\u2074', '\u2075' ]; +const STEP_TEXT = [ 0, "I", "II", "III", "IIII" ] +const HEIR_TEXT = [ 0, '\u00b9', '\u00b2', '\u00b3', '\u2074', '\u2075' ] -function block_name(who) { return who; } +function block_name(who) { return BLOCKS[who].name; } function block_home(who) { return BLOCKS[who].home; } function block_owner(who) { return BLOCKS[who].owner; } function on_focus_map_block(evt) { - let info = BLOCKS[evt.target.block]; - let where = view.location[evt.target.block]; - if ((info.owner === player || info.owner === ASSASSINS) && where !== S_POOL && where !== F_POOL) { - let text = info.name + " "; + let info = BLOCKS[evt.target.block] + let where = view.location[evt.target.block] + if ((info.owner === player || info.owner === "Assassins") && where !== S_POOL && where !== F_POOL) { + let text = info.name + " " if (info.move) - text += info.move + "-"; - text += STEP_TEXT[info.steps] + "-" + info.combat; - document.getElementById("status").textContent = text; + text += info.move + "-" + text += STEP_TEXT[info.steps] + "-" + info.initiative + info.fire_power + document.getElementById("status").textContent = text } else { - document.getElementById("status").textContent = info.owner; + document.getElementById("status").textContent = info.owner } } function on_blur_map_block(evt) { - document.getElementById("status").textContent = ""; + document.getElementById("status").textContent = "" } function on_click_map_block(evt) { - let b = evt.target.block; + let b = evt.target.block if (!view.battle) - send_action('block', b); + send_action('block', b) } function on_focus_battle_block(evt) { - let b = evt.target.block; - let msg; + let b = evt.target.block + let msg if (!evt.target.classList.contains("known")) { if (block_owner(b) === FRANKS) - msg = "Franks"; + msg = "Franks" else if (block_owner(b) === SARACENS) - msg = "Saracens"; + msg = "Saracens" } else { - msg = block_name(b); + msg = block_name(b) } if (view.actions && view.actions.fire && view.actions.fire.includes(b)) - msg = "Fire with " + msg; + msg = "Fire with " + msg else if (view.actions && view.actions.storm && view.actions.storm.includes(b)) - msg = "Storm with " + msg; + msg = "Storm with " + msg else if (view.actions && view.actions.sally && view.actions.sally.includes(b)) - msg = "Sally with " + msg; + msg = "Sally with " + msg else if (view.actions && view.actions.withdraw && view.actions.withdraw.includes(b)) - msg = "Withdraw with " + msg; + msg = "Withdraw with " + msg else if (view.actions && view.actions.hit && view.actions.hit.includes(b)) - msg = "Take hit on " + msg; + msg = "Take hit on " + msg - document.getElementById("status").textContent = msg; + document.getElementById("status").textContent = msg } function on_blur_battle_block(evt) { - document.getElementById("status").textContent = ""; + document.getElementById("status").textContent = "" } function on_click_battle_block(evt) { - let b = evt.target.block; - send_action('block', b); + let b = evt.target.block + send_action('block', b) } function on_focus_fire(evt) { document.getElementById("status").textContent = - "Fire with " + block_name(evt.target.block); + "Fire with " + block_name(evt.target.block) } function on_focus_retreat(evt) { if (view.battle.storming.includes(evt.target.block)) document.getElementById("status").textContent = - "Withdraw with " + block_name(evt.target.block); + "Withdraw with " + block_name(evt.target.block) else document.getElementById("status").textContent = - "Retreat with " + block_name(evt.target.block); + "Retreat with " + block_name(evt.target.block) } function on_focus_harry(evt) { document.getElementById("status").textContent = - "Harry with " + block_name(evt.target.block); + "Harry with " + block_name(evt.target.block) } function on_focus_charge(evt) { document.getElementById("status").textContent = - "Charge with " + block_name(evt.target.block); + "Charge with " + block_name(evt.target.block) } function on_focus_withdraw(evt) { document.getElementById("status").textContent = - "Withdraw with " + block_name(evt.target.block); + "Withdraw with " + block_name(evt.target.block) } function on_focus_storm(evt) { document.getElementById("status").textContent = - "Storm with " + block_name(evt.target.block); + "Storm with " + block_name(evt.target.block) } function on_focus_sally(evt) { document.getElementById("status").textContent = - "Sally with " + block_name(evt.target.block); + "Sally with " + block_name(evt.target.block) } function on_focus_hit(evt) { document.getElementById("status").textContent = - "Take hit on " + block_name(evt.target.block); + "Take hit on " + block_name(evt.target.block) } function on_blur_battle_button(evt) { - document.getElementById("status").textContent = ""; + document.getElementById("status").textContent = "" } function on_click_hit(evt) { send_action('hit', evt.target.block); } @@ -226,573 +309,572 @@ function on_click_storm(evt) { send_action('storm', evt.target.block); } function on_click_sally(evt) { send_action('sally', evt.target.block); } function on_click_card(evt) { - let c = evt.target.id.split("+")[1] | 0; - send_action('play', c); + let c = evt.target.id.split("+")[1] | 0 + send_action('play', c) } function build_battle_button(menu, b, c, click, enter, img_src) { - let img = new Image(); - img.draggable = false; - img.classList.add("action"); - img.classList.add(c); - img.setAttribute("src", img_src); - img.addEventListener("click", click); - img.addEventListener("mouseenter", enter); - img.addEventListener("mouseleave", on_blur_battle_button); - img.block = b; - menu.appendChild(img); + let img = new Image() + img.draggable = false + img.classList.add("action") + img.classList.add(c) + img.setAttribute("src", img_src) + img.addEventListener("click", click) + img.addEventListener("mouseenter", enter) + img.addEventListener("mouseleave", on_blur_battle_button) + img.block = b + menu.appendChild(img) } function battle_block_class_name(block) { - return `block block_${block.image} ${block.owner}`; + return `block block_${block.image} ${block.owner}` } function build_battle_block(b, block) { - let element = document.createElement("div"); - element.className = battle_block_class_name(block); - element.addEventListener("mouseenter", on_focus_battle_block); - element.addEventListener("mouseleave", on_blur_battle_block); - element.addEventListener("click", on_click_battle_block); - element.block = b; - ui.battle_block[b] = element; + let element = document.createElement("div") + element.className = battle_block_class_name(block) + element.addEventListener("mouseenter", on_focus_battle_block) + element.addEventListener("mouseleave", on_blur_battle_block) + element.addEventListener("click", on_click_battle_block) + element.block = b + ui.battle_block[b] = element - let menu_list = document.createElement("div"); - menu_list.className = "battle_menu_list"; + let menu_list = document.createElement("div") + menu_list.className = "battle_menu_list" build_battle_button(menu_list, b, "hit", on_click_hit, on_focus_hit, - "/images/cross-mark.svg"); + "/images/cross-mark.svg") build_battle_button(menu_list, b, "charge", on_click_charge, on_focus_charge, - "/images/mounted-knight.svg"); + "/images/mounted-knight.svg") build_battle_button(menu_list, b, "fire", on_click_fire, on_focus_fire, - "/images/pointy-sword.svg"); + "/images/pointy-sword.svg") build_battle_button(menu_list, b, "harry", on_click_harry, on_focus_harry, - "/images/arrow-flights.svg"); + "/images/arrow-flights.svg") build_battle_button(menu_list, b, "retreat", on_click_retreat, on_focus_retreat, - "/images/flying-flag.svg"); + "/images/flying-flag.svg") build_battle_button(menu_list, b, "withdraw", on_click_withdraw, on_focus_withdraw, - "/images/stone-tower.svg"); + "/images/stone-tower.svg") build_battle_button(menu_list, b, "storm", on_click_storm, on_focus_storm, - "/images/siege-tower.svg"); + "/images/siege-tower.svg") build_battle_button(menu_list, b, "sally", on_click_sally, on_focus_sally, - "/images/doorway.svg"); + "/images/doorway.svg") - let menu = document.createElement("div"); - menu.className = "battle_menu"; - menu.appendChild(element); - menu.appendChild(menu_list); - menu.block = b; - ui.battle_menu[b] = menu; + let menu = document.createElement("div") + menu.className = "battle_menu" + menu.appendChild(element) + menu.appendChild(menu_list) + menu.block = b + ui.battle_menu[b] = menu } function build_map_block(b, block) { - let element = document.createElement("div"); - element.classList.add("block"); - element.classList.add("known"); - element.classList.add(BLOCKS[b].owner); - element.classList.add("block_" + block.image); - element.addEventListener("mouseenter", on_focus_map_block); - element.addEventListener("mouseleave", on_blur_map_block); - element.addEventListener("click", on_click_map_block); - element.block = b; - return element; + let element = document.createElement("div") + element.classList.add("block") + element.classList.add("known") + element.classList.add(BLOCKS[b].owner) + element.classList.add("block_" + block.image) + element.addEventListener("mouseenter", on_focus_map_block) + element.addEventListener("mouseleave", on_blur_map_block) + element.addEventListener("click", on_click_map_block) + element.block = b + return element } function build_town(t, town) { - let element = document.createElement("div"); - element.town = t; - element.classList.add("town"); - element.addEventListener("mouseenter", on_focus_town); - element.addEventListener("mouseleave", on_blur_town); - element.addEventListener("click", on_click_town); - ui.towns_element.appendChild(element); - return element; + let element = document.createElement("div") + element.town = t + element.classList.add("town") + element.addEventListener("mouseenter", on_focus_town) + element.addEventListener("mouseleave", on_blur_town) + element.addEventListener("click", on_click_town) + ui.towns_element.appendChild(element) + return element } function build_map() { - let element; + let element - ui.blocks_element = document.getElementById("blocks"); - ui.offmap_element = document.getElementById("offmap"); - ui.towns_element = document.getElementById("towns"); + ui.blocks_element = document.getElementById("blocks") + ui.offmap_element = document.getElementById("offmap") + ui.towns_element = document.getElementById("towns") for (let c = 1; c <= 27; ++c) { - ui.cards[c] = document.getElementById("card+"+c); - ui.cards[c].addEventListener("click", on_click_card); + ui.cards[c] = document.getElementById("card+"+c) + ui.cards[c].addEventListener("click", on_click_card) } for (let c = 1; c <= 6; ++c) - ui.card_backs[c] = document.getElementById("back+"+c); - - for (let name in TOWNS) { - let town = TOWNS[name]; - if (name === F_POOL || name === S_POOL || name === DEAD) - continue; - if (name === "Sea") { - element = document.getElementById("svgmap").getElementById("sea"); - element.town = "Sea"; - element.addEventListener("mouseenter", on_focus_town); - element.addEventListener("mouseleave", on_blur_town); - element.addEventListener("click", on_click_town); - ui.towns[name] = element; + ui.card_backs[c] = document.getElementById("back+"+c) + + for (let t = SEA; t < TOWNS.length; ++t) { + let town = TOWNS[t] + let name = town.name + if (t === SEA) { + element = document.getElementById("svgmap").getElementById("sea") + element.town = SEA + element.addEventListener("mouseenter", on_focus_town) + element.addEventListener("mouseleave", on_blur_town) + element.addEventListener("click", on_click_town) + ui.towns[t] = element } else { - element = ui.towns[name] = build_town(name, town); - let xo = Math.round(element.offsetWidth/2); - let yo = Math.round(element.offsetHeight/2); - element.style.left = (town.x - xo) + "px"; - element.style.top = (town.y - yo) + "px"; + element = ui.towns[t] = build_town(t, town) + let xo = Math.round(element.offsetWidth/2) + let yo = Math.round(element.offsetHeight/2) + element.style.left = (town.layout.x - xo) + "px" + element.style.top = (town.layout.y - yo) + "px" } } - for (let b in BLOCKS) { - let block = BLOCKS[b]; - ui.blocks[b] = build_map_block(b, block); - build_battle_block(b, block); + for (let b = 0; b < BLOCKS.length; ++b) { + let block = BLOCKS[b] + ui.blocks[b] = build_map_block(b, block) + build_battle_block(b, block) } } function update_steps(b, steps, element) { - element.classList.remove("r0"); - element.classList.remove("r1"); - element.classList.remove("r2"); - element.classList.remove("r3"); - element.classList.add("r"+(BLOCKS[b].steps - steps)); + element.classList.remove("r0") + element.classList.remove("r1") + element.classList.remove("r2") + element.classList.remove("r3") + element.classList.add("r"+(BLOCKS[b].steps - steps)) } function layout_blocks(location, secret, known) { if (label_layout === 'stack') - document.getElementById("map").classList.add("stack_layout"); + document.getElementById("map").classList.add("stack_layout") else - document.getElementById("map").classList.remove("stack_layout"); + document.getElementById("map").classList.remove("stack_layout") if (label_layout === 'spread' || (location === S_POOL || location === F_POOL || location === DEAD || location === ENGLAND || location === FRANCE || location === GERMANIA)) - layout_blocks_spread(location, secret, known); + layout_blocks_spread(location, secret, known) else - layout_blocks_stacked(location, secret, known); + layout_blocks_stacked(location, secret, known) } function layout_blocks_spread(town, north, south) { - let wrap = TOWNS[town].wrap; - let rows = []; + let wrap = TOWNS[town].layout.wrap + let rows = [] if ((north.length > wrap || south.length > wrap) || (north.length + south.length <= 3)) { - north = north.concat(south); - south = []; + north = north.concat(south) + south = [] } function wrap_row(input) { while (input.length > wrap) { - rows.push(input.slice(0, wrap)); - input = input.slice(wrap); + rows.push(input.slice(0, wrap)) + input = input.slice(wrap) } if (input.length > 0) - rows.push(input); + rows.push(input) } - wrap_row(north); - wrap_row(south); + wrap_row(north) + wrap_row(south) - if (TOWNS[town].layout_minor > 0.5) - rows.reverse(); + if (TOWNS[town].layout.minor > 0.5) + rows.reverse() for (let r = 0; r < rows.length; ++r) { - let cols = rows[r]; + let cols = rows[r] for (let c = 0; c < cols.length; ++c) - position_block(town, r, rows.length, c, cols.length, cols[c]); + position_block(town, r, rows.length, c, cols.length, cols[c]) } } function position_block(town, row, n_rows, col, n_cols, element) { - let space = TOWNS[town]; - let block_size = 60+6; - let padding = 4; + let space = TOWNS[town] + let block_size = 60+6 + let padding = 4 if (town === ENGLAND || town === FRANCE || town === GERMANIA) - padding = 21; - let offset = block_size + padding; - let row_size = (n_rows-1) * offset; - let col_size = (n_cols-1) * offset; - let x = space.x; - let y = space.y; - - if (space.layout_axis === 'X') { - x -= col_size * space.layout_major; - y -= row_size * space.layout_minor; - x += col * offset; - y += row * offset; + padding = 21 + let offset = block_size + padding + let row_size = (n_rows-1) * offset + let col_size = (n_cols-1) * offset + let x = space.layout.x + let y = space.layout.y + + if (space.layout.axis === 'X') { + x -= col_size * space.layout.major + y -= row_size * space.layout.minor + x += col * offset + y += row * offset } else { - y -= col_size * space.layout_major; - x -= row_size * space.layout_minor; - y += col * offset; - x += row * offset; + y -= col_size * space.layout.major + x -= row_size * space.layout.minor + y += col * offset + x += row * offset } - element.style.left = ((x - block_size/2)|0)+"px"; - element.style.top = ((y - block_size/2)|0)+"px"; + element.style.left = ((x - block_size/2)|0)+"px" + element.style.top = ((y - block_size/2)|0)+"px" } function layout_blocks_stacked(location, secret, known) { - let s = secret.length; - let k = known.length; - let both = secret.length > 0 && known.length > 0; - let i = 0; + let s = secret.length + let k = known.length + let both = secret.length > 0 && known.length > 0 + let i = 0 while (secret.length > 0) - position_block_stacked(location, i++, (s-1)/2, both ? 1 : 0, secret.shift()); - i = 0; + position_block_stacked(location, i++, (s-1)/2, both ? 1 : 0, secret.shift()) + i = 0 while (known.length > 0) - position_block_stacked(location, i++, (k-1)/2, 0, known.shift()); + position_block_stacked(location, i++, (k-1)/2, 0, known.shift()) } function position_block_stacked(location, i, c, k, element) { - let space = TOWNS[location]; - let block_size = 60+6; - let x = space.x + (i - c) * 16 + k * 12; - let y = space.y + (i - c) * 16 - k * 12; - element.style.left = ((x - block_size/2)|0)+"px"; - element.style.top = ((y - block_size/2)|0)+"px"; + let space = TOWNS[location] + let block_size = 60+6 + let x = space.x + (i - c) * 16 + k * 12 + let y = space.y + (i - c) * 16 - k * 12 + element.style.left = ((x - block_size/2)|0)+"px" + element.style.top = ((y - block_size/2)|0)+"px" } function show_block(element) { if (element.parentElement !== ui.blocks_element) - ui.blocks_element.appendChild(element); + ui.blocks_element.appendChild(element) } function hide_block(element) { if (element.parentElement !== ui.offmap_element) - ui.offmap_element.appendChild(element); + ui.offmap_element.appendChild(element) } function is_known_block(info, who, town) { if (view.game_over && player === 'Observer') - return true; + return true if (town === DEAD) - return true; + return true if ((town === S_POOL || town === F_POOL) && who !== view.who) - return false; - if (info.owner === player || info.owner === ASSASSINS || who === view.assassinate) - return true; - return false; + return false + if (info.owner === player || info.owner === "Assassins" || who === view.assassinate) + return true + return false } function update_map() { - let layout = {}; + let layout = {} - document.getElementById("frank_vp").textContent = view.f_vp + " VP"; - document.getElementById("saracen_vp").textContent = view.s_vp + " VP"; - document.getElementById("timeline").className = "year_" + view.year; + document.getElementById("frank_vp").textContent = view.f_vp + " VP" + document.getElementById("saracen_vp").textContent = view.s_vp + " VP" + document.getElementById("timeline").className = "year_" + view.year if (view.turn < 1) document.getElementById("turn_info").textContent = - "Year " + view.year; + "Year " + view.year else if (view.turn < 6) document.getElementById("turn_info").textContent = - "Turn " + view.turn + " of Year " + view.year; + "Turn " + view.turn + " of Year " + view.year else document.getElementById("turn_info").textContent = - "Winter Turn of Year " + view.year; + "Winter Turn of Year " + view.year - for (let town in TOWNS) - layout[town] = { north: [], south: [] }; + for (let t = 0; t < TOWNS.length; ++t) + layout[t] = { north: [], south: [] } - for (let b in view.location) { - let info = BLOCKS[b]; - let element = ui.blocks[b]; - let town = view.location[b]; - let moved = view.moved[b] ? " moved" : ""; + for (let b = 0; b < BLOCKS.length; ++b) { + let info = BLOCKS[b] + let element = ui.blocks[b] + let town = view.location[b] + let moved = set_has(view.moved, b) ? " moved" : "" if (town === DEAD) { - moved = " moved"; + moved = " moved" } - if (town === null) { - town = DEAD; - moved = " removed"; + if (town === NOWHERE) { + town = DEAD + moved = " removed" } if (is_known_block(info, b, town)) { - let image = " block_" + info.image; - let steps = " r" + (info.steps - view.steps[b]); - let known = " known"; - element.classList = info.owner + known + " block" + image + steps + moved; + let image = " block_" + info.image + let steps = " r" + (info.steps - view.steps[b]) + let known = " known" + element.classList = info.owner + known + " block" + image + steps + moved } else { - let besieging = ""; + let besieging = "" if (view.sieges[town] === info.owner) { if (view.winter_campaign === town) - besieging = " winter_campaign"; + besieging = " winter_campaign" else - besieging = " besieging"; + besieging = " besieging" } - let jihad = ""; + let jihad = "" if (view.jihad === town && info.owner === view.p1) - jihad = " jihad"; - element.classList = info.owner + " block" + moved + besieging + jihad; + jihad = " jihad" + element.classList = info.owner + " block" + moved + besieging + jihad } if (town !== DEAD) { if (info.owner === FRANKS) - layout[town].north.push(element); + layout[town].north.push(element) else - layout[town].south.push(element); + layout[town].south.push(element) } - show_block(element); + show_block(element) } - for (let b in view.location) { - let info = BLOCKS[b]; - let element = ui.blocks[b]; - let town = view.location[b]; + for (let b = 0; b < BLOCKS.length; ++b) { + let info = BLOCKS[b] + let element = ui.blocks[b] + let town = view.location[b] if (town === DEAD) { if (info.owner === FRANKS) - layout[F_POOL].north.unshift(element); + layout[F_POOL].north.unshift(element) else - layout[S_POOL].south.unshift(element); + layout[S_POOL].south.unshift(element) } } - for (let b in view.location) { - let info = BLOCKS[b]; - let element = ui.blocks[b]; - let town = view.location[b]; - if (town === null) { + for (let b = 0; b < BLOCKS.length; ++b) { + let info = BLOCKS[b] + let element = ui.blocks[b] + let town = view.location[b] + if (town === NOWHERE) { if (info.owner === FRANKS) - layout[F_POOL].north.unshift(element); + layout[F_POOL].north.unshift(element) else - layout[S_POOL].south.unshift(element); + layout[S_POOL].south.unshift(element) } } - for (let town in TOWNS) - layout_blocks(town, layout[town].north, layout[town].south); + for (let t = 0; t < TOWNS.length; ++t) + layout_blocks(t, layout[t].north, layout[t].south) - for (let where in TOWNS) { - if (ui.towns[where]) { - ui.towns[where].classList.remove('highlight'); - ui.towns[where].classList.remove('muster'); + for (let t = SEA; t < TOWNS.length; ++t) { + if (ui.towns[t]) { + ui.towns[t].classList.remove('highlight') + ui.towns[t].classList.remove('muster') } } if (view.actions && view.actions.town) - for (let where of view.actions.town) - ui.towns[where].classList.add('highlight'); + for (let t of view.actions.town) + ui.towns[t].classList.add('highlight') if (view.muster) - ui.towns[view.muster].classList.add('muster'); + ui.towns[view.muster].classList.add('muster') if (!view.battle) { if (view.actions && view.actions.block) for (let b of view.actions.block) - ui.blocks[b].classList.add('highlight'); + ui.blocks[b].classList.add('highlight') } - if (view.who && !view.battle) - ui.blocks[view.who].classList.add('selected'); + if (view.who >= 0 && !view.battle) + ui.blocks[view.who].classList.add('selected') for (let b of view.castle) - ui.blocks[b].classList.add('castle'); + ui.blocks[b].classList.add('castle') } function update_card_display(element, card, prior_card) { if (!card && !prior_card) { - element.className = "show card card_back"; + element.className = "show card card_back" } else if (prior_card) { - element.className = "show card prior " + CARDS[prior_card].image; + element.className = "show card prior " + CARDS[prior_card].image } else { - element.className = "show card " + CARDS[card].image; + element.className = "show card " + CARDS[card].image } } function update_cards() { - update_card_display(document.getElementById("frank_card"), view.f_card, view.prior_f_card); - update_card_display(document.getElementById("saracen_card"), view.s_card, view.prior_s_card); + update_card_display(document.getElementById("frank_card"), view.f_card, view.prior_f_card) + update_card_display(document.getElementById("saracen_card"), view.s_card, view.prior_s_card) for (let c = 1; c <= 27; ++c) { - let element = ui.cards[c]; + let element = ui.cards[c] if (view.hand.includes(c)) { - element.classList.add("show"); + element.classList.add("show") if (view.actions && view.actions.play) { if (view.actions.play.includes(c)) { - element.classList.add("enabled"); - element.classList.remove("disabled"); + element.classList.add("enabled") + element.classList.remove("disabled") } else { - element.classList.remove("enabled"); - element.classList.add("disabled"); + element.classList.remove("enabled") + element.classList.add("disabled") } } else { - element.classList.remove("enabled"); - element.classList.remove("disabled"); + element.classList.remove("enabled") + element.classList.remove("disabled") } } else { - element.classList.remove("show"); + element.classList.remove("show") } } - let n = view.hand.length; + let n = view.hand.length for (let c = 1; c <= 6; ++c) if (c <= n && player === 'Observer') - ui.card_backs[c].classList.add("show"); + ui.card_backs[c].classList.add("show") else - ui.card_backs[c].classList.remove("show"); + ui.card_backs[c].classList.remove("show") } function compare_blocks(a, b) { - let aa = BLOCKS[a].combat; - let bb = BLOCKS[b].combat; + let aa = BLOCKS[a].initiative + BLOCKS[a].fire_power + let bb = BLOCKS[b].initiative + BLOCKS[b].fire_power if (aa === bb) - return (a < b) ? -1 : (a > b) ? 1 : 0; - return (aa < bb) ? -1 : (aa > bb) ? 1 : 0; + return (a < b) ? -1 : (a > b) ? 1 : 0 + return (aa < bb) ? -1 : (aa > bb) ? 1 : 0 } function insert_battle_block(root, node, block) { for (let i = 0; i < root.children.length; ++i) { - let prev = root.children[i]; + let prev = root.children[i] if (compare_blocks(prev.block, block) > 0) { - root.insertBefore(node, prev); - return; + root.insertBefore(node, prev) + return } } - root.appendChild(node); + root.appendChild(node) } function update_battle() { function fill_cell(name, list, show) { - let cell = document.getElementById(name); + let cell = document.getElementById(name) - ui.present.clear(); + ui.present.clear() for (let block of list) { - ui.present.add(block); + ui.present.add(block) if (!cell.contains(ui.battle_menu[block])) - insert_battle_block(cell, ui.battle_menu[block], block); + insert_battle_block(cell, ui.battle_menu[block], block) - ui.battle_menu[block].className = "battle_menu"; + ui.battle_menu[block].className = "battle_menu" if (view.actions && view.actions.fire && view.actions.fire.includes(block)) - ui.battle_menu[block].classList.add('fire'); + ui.battle_menu[block].classList.add('fire') if (view.actions && view.actions.retreat && view.actions.retreat.includes(block)) - ui.battle_menu[block].classList.add('retreat'); + ui.battle_menu[block].classList.add('retreat') if (view.actions && view.actions.harry && view.actions.harry.includes(block)) - ui.battle_menu[block].classList.add('harry'); + ui.battle_menu[block].classList.add('harry') if (view.actions && view.actions.charge && view.actions.charge.includes(block)) - ui.battle_menu[block].classList.add('charge'); + ui.battle_menu[block].classList.add('charge') if (view.actions && view.actions.withdraw && view.actions.withdraw.includes(block)) - ui.battle_menu[block].classList.add('withdraw'); + ui.battle_menu[block].classList.add('withdraw') if (view.actions && view.actions.storm && view.actions.storm.includes(block)) - ui.battle_menu[block].classList.add('storm'); + ui.battle_menu[block].classList.add('storm') if (view.actions && view.actions.sally && view.actions.sally.includes(block)) - ui.battle_menu[block].classList.add('sally'); + ui.battle_menu[block].classList.add('sally') if (view.actions && view.actions.charge && view.actions.charge.includes(block)) - ui.battle_menu[block].classList.add('charge'); + ui.battle_menu[block].classList.add('charge') if (view.actions && view.actions.treachery && view.actions.treachery.includes(block)) - ui.battle_menu[block].classList.add('treachery'); + ui.battle_menu[block].classList.add('treachery') if (view.actions && view.actions.hit && view.actions.hit.includes(block)) - ui.battle_menu[block].classList.add('hit'); + ui.battle_menu[block].classList.add('hit') - let class_name = battle_block_class_name(BLOCKS[block]); + let class_name = battle_block_class_name(BLOCKS[block]) if (view.actions && view.actions.block && view.actions.block.includes(block)) - class_name += " highlight"; - if (view.moved[block]) - class_name += " moved"; + class_name += " highlight" + if (set_has(view.moved, block)) + class_name += " moved" if (block === view.who) - class_name += " selected"; + class_name += " selected" if (block === view.battle.halfhit) - class_name += " halfhit"; + class_name += " halfhit" if (view.jihad === view.battle.town && block_owner(block) === view.p1) - class_name += " jihad"; + class_name += " jihad" if (view.battle.sallying.includes(block)) - show = true; + show = true if (view.battle.storming.includes(block)) - show = true; + show = true if (show || block_owner(block) === player) { - class_name += " known"; - ui.battle_block[block].className = class_name; - update_steps(block, view.steps[block], ui.battle_block[block], false); + class_name += " known" + ui.battle_block[block].className = class_name + update_steps(block, view.steps[block], ui.battle_block[block], false) } else { - ui.battle_block[block].className = class_name; + ui.battle_block[block].className = class_name } } - for (let b in BLOCKS) { + for (let b = 0; b < BLOCKS.length; ++b) { if (!ui.present.has(b)) { if (cell.contains(ui.battle_menu[b])) - cell.removeChild(ui.battle_menu[b]); + cell.removeChild(ui.battle_menu[b]) } } } if (player === FRANKS) { - fill_cell("ER", view.battle.SR, false); - fill_cell("EC", view.battle.SC, view.battle.show_castle); - fill_cell("EF", view.battle.SF, view.battle.show_field); - fill_cell("FF", view.battle.FF, view.battle.show_field); - fill_cell("FC", view.battle.FC, view.battle.show_castle); - fill_cell("FR", view.battle.FR, false); - document.getElementById("FC").className = "c" + view.battle.FCS; - document.getElementById("EC").className = "c" + view.battle.SCS; + fill_cell("ER", view.battle.SR, false) + fill_cell("EC", view.battle.SC, view.battle.show_castle) + fill_cell("EF", view.battle.SF, view.battle.show_field) + fill_cell("FF", view.battle.FF, view.battle.show_field) + fill_cell("FC", view.battle.FC, view.battle.show_castle) + fill_cell("FR", view.battle.FR, false) + document.getElementById("FC").className = "c" + view.battle.FCS + document.getElementById("EC").className = "c" + view.battle.SCS } else { - fill_cell("ER", view.battle.FR, false); - fill_cell("EC", view.battle.FC, view.battle.show_castle); - fill_cell("EF", view.battle.FF, view.battle.show_field); - fill_cell("FF", view.battle.SF, view.battle.show_field); - fill_cell("FC", view.battle.SC, view.battle.show_castle); - fill_cell("FR", view.battle.SR, false); - document.getElementById("EC").className = "c" + view.battle.FCS; - document.getElementById("FC").className = "c" + view.battle.SCS; + fill_cell("ER", view.battle.FR, false) + fill_cell("EC", view.battle.FC, view.battle.show_castle) + fill_cell("EF", view.battle.FF, view.battle.show_field) + fill_cell("FF", view.battle.SF, view.battle.show_field) + fill_cell("FC", view.battle.SC, view.battle.show_castle) + fill_cell("FR", view.battle.SR, false) + document.getElementById("EC").className = "c" + view.battle.FCS + document.getElementById("FC").className = "c" + view.battle.SCS } } -let flash_timer = 0; +let flash_timer = 0 function start_flash() { - let element = document.getElementById("battle_message"); - let tick = true; + let element = document.getElementById("battle_message") + let tick = true if (flash_timer) - return; + return flash_timer = setInterval(function () { if (!view.flash_next) { - element.textContent = view.battle ? view.battle.flash : ""; - clearInterval(flash_timer); - flash_timer = 0; + element.textContent = view.battle ? view.battle.flash : "" + clearInterval(flash_timer) + flash_timer = 0 } else { - element.textContent = tick ? view.battle.flash : view.flash_next; - tick = !tick; + element.textContent = tick ? view.battle.flash : view.flash_next + tick = !tick } - }, 1000); + }, 1000) } function on_update() { - action_button("eliminate", "Eliminate"); - action_button("winter_campaign", "Winter campaign"); - action_button("sea_move", "Sea move"); - action_button("end_sea_move", "End sea move"); - action_button("group_move", "Group move"); - action_button("end_group_move", "End group move"); - action_button("muster", "Muster"); - action_button("end_muster", "End muster"); - action_button("end_retreat", "End retreat"); - action_button("end_regroup", "End regroup"); - action_button("end_move_phase", "End move phase"); - action_button("pass", "Pass"); - action_button("next", "Next"); - action_button("undo", "Undo"); - - document.getElementById("frank_vp").textContent = view.f_vp; - document.getElementById("saracen_vp").textContent = view.s_vp; - - update_cards(); - update_map(); + action_button("eliminate", "Eliminate") + action_button("winter_campaign", "Winter campaign") + action_button("sea_move", "Sea move") + action_button("end_sea_move", "End sea move") + action_button("group_move", "Group move") + action_button("end_group_move", "End group move") + action_button("muster", "Muster") + action_button("end_muster", "End muster") + action_button("end_retreat", "End retreat") + action_button("end_regroup", "End regroup") + action_button("end_move_phase", "End move phase") + action_button("pass", "Pass") + action_button("next", "Next") + action_button("undo", "Undo") + + document.getElementById("frank_vp").textContent = view.f_vp + document.getElementById("saracen_vp").textContent = view.s_vp + + update_cards() + update_map() if (view.battle) { - document.getElementById("battle_header").textContent = view.battle.title; - document.getElementById("battle_message").textContent = view.battle.flash; + document.getElementById("battle_header").textContent = view.battle.title + document.getElementById("battle_message").textContent = view.battle.flash if (view.flash_next) - start_flash(); - document.getElementById("battle").classList.add("show"); - update_battle(); + start_flash() + document.getElementById("battle").classList.add("show") + update_battle() } else { - document.getElementById("battle").classList.remove("show"); + document.getElementById("battle").classList.remove("show") } } -build_map(); +build_map() -drag_element_with_mouse("#battle", "#battle_header"); -scroll_with_middle_mouse("main", 3); +drag_element_with_mouse("#battle", "#battle_header") +scroll_with_middle_mouse("main", 3) @@ -16,38 +16,54 @@ exports.roles = [ "Saracens", ] -const { CARDS, BLOCKS, TOWNS, PORTS, ROADS, SHIELDS } = require('./data') +const { CARDS, BLOCKS, TOWNS, PORTS, ROADS, SHIELDS, block_index, town_index } = require('./data') const FRANKS = "Franks" const SARACENS = "Saracens" -const ASSASSINS = "Assassins" const OBSERVER = "Observer" const BOTH = "Both" -const ELIMINATED = null -const DEAD = "Dead" -const F_POOL = "FP" -const S_POOL = "SP" -const SEA = "Sea" -const ENGLAND = "England" -const FRANCE = "France" -const GERMANIA = "Germania" -const TYRE = "Tyre" -const TRIPOLI = "Tripoli" -const ALEPPO = "Aleppo" -const ANTIOCH = "Antioch" -const ST_SIMEON = "St. Simeon" -const DAMASCUS = "Damascus" -const MASYAF = "Masyaf" -const SALADIN = "Saladin" + +const NOWHERE = 0 +const DEAD = 1 +const F_POOL = 2 +const S_POOL = 3 +const SEA = 4 + +const ENGLAND = 5 +const FRANCE = 6 +const GERMANIA = 7 + +const first_town = 5 // TODO: exclude staging areas? include sea? +const last_town = TOWNS.length - 1 +const last_block = BLOCKS.length - 2 // assassins are not a real block + +const ALEPPO = town_index["Aleppo"] +const ANTIOCH = town_index["Antioch"] +const DAMASCUS = town_index["Damascus"] +const MASYAF = town_index["Masyaf"] +const ST_SIMEON = town_index["St. Simeon"] +const TRIPOLI = town_index["Tripoli"] +const TYRE = town_index["Tyre"] + +const NOBODY = -1 +const ASSASSINS = block_index["Assassins"] +const RICHARD = block_index["Richard"] +const ROBERT = block_index["Robert"] +const CROSSBOWS = block_index["Crossbows"] +const SALADIN = block_index["Saladin"] +const AL_ADIL = block_index["Al Adil"] +const AL_AZIZ = block_index["Al Aziz"] +const AL_AFDAL = block_index["Al Afdal"] +const AL_ZAHIR = block_index["Al Zahir"] + +const ENGLISH_CRUSADERS = [ RICHARD, ROBERT, CROSSBOWS ] +const GERMAN_CRUSADERS = [ "Barbarossa", "Frederik", "Leopold" ].map(name => block_index[name]) +const FRENCH_CRUSADERS = [ "Philippe", "Hugues", "Fileps" ].map(name => block_index[name]) +const SALADIN_FAMILY = [ SALADIN, AL_ADIL, AL_AZIZ, AL_AFDAL, AL_ZAHIR ] const INTRIGUE = 3 const WINTER_CAMPAIGN = 6 -const ENGLISH_CRUSADERS = [ "Richard", "Robert", "Crossbows" ] -const GERMAN_CRUSADERS = [ "Barbarossa", "Frederik", "Leopold" ] -const FRENCH_CRUSADERS = [ "Philippe", "Hugues", "Fileps" ] -const SALADIN_FAMILY = [ "Saladin", "Al Adil", "Al Aziz", "Al Afdal", "Al Zahir" ] - const GERMAN_ROADS = [ ST_SIMEON, ANTIOCH, ALEPPO ] const KINGDOMS = { @@ -59,8 +75,13 @@ const KINGDOMS = { } const VICTORY_TOWNS = [ - "Aleppo", "Damascus", "Egypt", - "Antioch", "Tripoli", "Acre", "Jerusalem" + town_index["Aleppo"], + town_index["Damascus"], + town_index["Egypt"], + town_index["Antioch"], + town_index["Tripoli"], + town_index["Acre"], + town_index["Jerusalem"] ] // serif cirled numbers @@ -72,16 +93,6 @@ const ATTACK_MARK = "*" const RESERVE_MARK_1 = "\u2020" const RESERVE_MARK_2 = "\u2021" -// Only used by UI layer for layout. remove from game logic. -delete TOWNS[DEAD] -delete TOWNS[F_POOL] -delete TOWNS[S_POOL] -delete TOWNS[SEA] - -// Quick lists for fast iteration -const BLOCKLIST = Object.keys(BLOCKS) -const TOWNLIST = Object.keys(TOWNS) - let states = {} let game = null @@ -96,6 +107,10 @@ function log(s) { game.log.push(s) } +function logi(s) { + game.log.push(">" + s) +} + function active_adjective() { return (game.active === FRANKS ? "Frank" : "Saracen") } @@ -113,7 +128,7 @@ function log_move_start(from) { function log_move_continue(to, mark) { if (mark) - game.move_buf.push(to + mark) + game.move_buf.push("#" + to + mark) else game.move_buf.push(to) } @@ -125,28 +140,42 @@ function log_move_end() { } function print_summary(text, skip_if_empty = false) { + if (skip_if_empty && game.summary.length === 0) { + delete game.summary + return + } + + let lines = game.summary.map(function (move) { + let s = "" + for (let i = 0; i < move.length; ++i) { + let x = move[i] + if (i > 0) + s += " \u2192 " + if (typeof x === 'number') + s += "#" + x + else + s += x + } + return s + }).sort() + delete game.summary + + log(text) + + let last = lines[0] let n = 0 - function print_move(last) { - return "\n" + n + " " + last.join(" \u2192 ") - } - if (!skip_if_empty || game.summary.length > 0) { - game.summary.sort() - let last = game.summary[0] - for (let entry of game.summary) { - if (entry.toString() !== last.toString()) { - text += print_move(last) - n = 0 - } - ++n - last = entry + for (let entry of lines) { + if (entry !== last) { + logi(n + " " + last) + n = 0 } - if (n > 0) - text += print_move(last) - else - text += "\nnothing." - log(text) + ++n + last = entry } - delete game.summary + if (n > 0) + logi(n + " " + last) + else + logi("nothing.") } function enemy(p) { @@ -169,56 +198,6 @@ function remove_from_array(array, item) { array.splice(i, 1) } -function deep_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] = deep_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] = deep_copy(v) - else - copy[i] = v - } - return copy - } -} - -function push_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 = deep_copy(v) - copy[k] = v - } - game.undo.push(copy) -} - -function pop_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 clear_undo() { - game.undo = [] -} - function gen_action_undo(view) { if (!view.actions) view.actions = {} @@ -239,7 +218,7 @@ function gen_action(view, action, argument) { view.actions[action].push(argument) } } else { - view.actions[action] = true + view.actions[action] = 1 } } @@ -266,28 +245,30 @@ function deal_cards(deck, n) { function select_random_block(where) { let list = [] - for (let b of BLOCKLIST) + for (let b = 0; b <= last_block; ++b) if (game.location[b] === where) list.push(b) if (list.length === 0) - return null + return NOBODY return list[random(list.length)] } function select_random_enemy_block(where) { let list = [] - for (let b of BLOCKLIST) + for (let b = 0; b <= last_block; ++b) if (game.location[b] === where && block_owner(b) === enemy(game.active)) list.push(b) if (list.length === 0) - return null + return NOBODY return list[random(list.length)] } +function town_name(where) { + return TOWNS[where].name +} + function block_name(who) { - if (BLOCKS[who].type === 'nomads') - return BLOCKS[who].name - return who + return BLOCKS[who].name } function block_type(who) { @@ -295,52 +276,23 @@ function block_type(who) { } function block_home(who) { - let home = BLOCKS[who].home - if (home === "Normandy") return "England" - if (home === "Aquitaine") return "England" - if (home === "Bourgogne") return "France" - if (home === "Flanders") return "France" - return home + return BLOCKS[who].home } function list_seats(who) { - switch (block_type(who)) { - case 'nomads': + if (block_type(who) === 'nomads') return [ block_home(who) ] - case 'turcopoles': - who = "Turcopoles" - break - case 'military_orders': - who = BLOCKS[who].name - break - } - if (is_saladin_family(who)) - who = SALADIN - if (who === "Raymond (Tiberias)" || who === "Raymond (Tripoli)") - who = "Raymond" let list = [] - for (let town in SHIELDS) - if (SHIELDS[town].includes(who)) + for (let town = first_town; town <= last_town; ++town) + if (set_has(SHIELDS[town], who)) list.push(town) return list } function is_home_seat(where, who) { - if (is_saladin_family(who)) - who = SALADIN - switch (block_type(who)) { - case 'nomads': + if (block_type(who) === 'nomads') return where === block_home(who) - case 'turcopoles': - who = "Turcopoles" - break - case 'military_orders': - who = BLOCKS[who].name - break - } - if (who === "Raymond (Tiberias)" || who === "Raymond (Tripoli)") - who = "Raymond" - if (SHIELDS[where] && SHIELDS[where].includes(who)) + if (set_has(SHIELDS[where], who)) return true return false } @@ -356,11 +308,11 @@ function block_owner(who) { } function block_initiative(who) { - return BLOCKS[who].combat[0] + return BLOCKS[who].initiative } function block_fire_power(who) { - return BLOCKS[who].combat[1] | 0 + return BLOCKS[who].fire_power } function block_move(who) { @@ -372,11 +324,11 @@ function block_max_steps(who) { } function is_saladin_family(who) { - return who === "Saladin" || who === "Al Adil" || who === "Al Aziz" || who === "Al Afdal" || who === "Al Zahir" + return who === SALADIN || who === AL_ADIL || who === AL_AZIZ || who === AL_AFDAL || who === AL_ZAHIR } function is_english_crusader(who) { - return (who === "Richard" || who === "Robert" || who === "Crossbows") + return who === RICHARD || who === ROBERT || who === CROSSBOWS } function are_crusaders_not_in_pool(crusaders) { @@ -398,7 +350,7 @@ function is_block_on_land(who) { } function road_id(a, b) { - return (a < b) ? a + "/" + b : b + "/" + a + return (a < b) ? a * 100 + b : b * 100 + a } function road_was_last_used_by_enemy(from, to) { @@ -423,7 +375,7 @@ function reset_road_limits() { function count_player(p, where) { let count = 0 - for (let b of BLOCKLIST) + for (let b = 0; b <= last_block; ++b) if (game.location[b] === where && block_owner(b) === p) ++count return count @@ -432,7 +384,7 @@ function count_player(p, where) { function count_friendly(where) { let p = game.active let count = 0 - for (let b of BLOCKLIST) + for (let b = 0; b <= last_block; ++b) if (game.location[b] === where && block_owner(b) === p) ++count return count @@ -441,7 +393,7 @@ function count_friendly(where) { function count_enemy(where) { let p = enemy(game.active) let count = 0 - for (let b of BLOCKLIST) + for (let b = 0; b <= last_block; ++b) if (game.location[b] === where && block_owner(b) === p) ++count return count @@ -450,7 +402,7 @@ function count_enemy(where) { function count_friendly_in_field(where) { let p = game.active let count = 0 - for (let b of BLOCKLIST) + for (let b = 0; b <= last_block; ++b) if (game.location[b] === where && block_owner(b) === p) if (!is_block_in_castle(b)) ++count @@ -460,7 +412,7 @@ function count_friendly_in_field(where) { function count_enemy_in_field(where) { let p = enemy(game.active) let count = 0 - for (let b of BLOCKLIST) + for (let b = 0; b <= last_block; ++b) if (game.location[b] === where && block_owner(b) === p) if (!is_block_in_castle(b)) ++count @@ -470,7 +422,7 @@ function count_enemy_in_field(where) { function count_friendly_in_field_excluding_reserves(where) { let p = game.active let count = 0 - for (let b of BLOCKLIST) + for (let b = 0; b <= last_block; ++b) if (game.location[b] === where && block_owner(b) === p) if (!is_block_in_castle(b) && !is_reserve(b)) ++count @@ -480,7 +432,7 @@ function count_friendly_in_field_excluding_reserves(where) { function count_enemy_in_field_excluding_reserves(where) { let p = enemy(game.active) let count = 0 - for (let b of BLOCKLIST) + for (let b = 0; b <= last_block; ++b) if (game.location[b] === where && block_owner(b) === p) if (!is_block_in_castle(b) && !is_reserve(b)) ++count @@ -489,24 +441,24 @@ function count_enemy_in_field_excluding_reserves(where) { function count_blocks_in_castle(where) { let n = 0 - for (let b of BLOCKLIST) - if (game.location[b] === where && game.castle.includes(b)) + for (let b = 0; b <= last_block; ++b) + if (game.location[b] === where && set_has(game.castle, b)) ++n return n } function count_enemy_in_field_and_reserve(where) { let n = 0 - for (let b of BLOCKLIST) + for (let b = 0; b <= last_block; ++b) if (block_owner(b) !== game.active) - if (game.location[b] === where && !game.castle.includes(b)) + if (game.location[b] === where && !set_has(game.castle, b)) ++n return n } function count_reserves(where) { let n = 0 - for (let b of BLOCKLIST) + for (let b = 0; b <= last_block; ++b) if (block_owner(b) === game.active) if (game.location[b] === where && is_reserve(b)) ++n @@ -573,7 +525,7 @@ function is_enemy_battle_field() { } function is_reserve(who) { - return game.reserves1.includes(who) || game.reserves2.includes(who) + return set_has(game.reserves1, who) || set_has(game.reserves2, who) } function is_field_attacker(who) { @@ -599,7 +551,7 @@ function is_block_in_field(who) { } function is_siege_attacker(who) { - return game.storming.includes(who) + return set_has(game.storming, who) } function is_siege_defender(who) { @@ -607,7 +559,7 @@ function is_siege_defender(who) { } function is_siege_combatant(who) { - return game.storming.includes(who) || is_block_in_castle_in(who, game.where) + return set_has(game.storming, who) || is_block_in_castle_in(who, game.where) } function castle_limit(where) { @@ -631,15 +583,15 @@ function is_under_siege(where) { } function is_block_in_castle(b) { - return game.castle.includes(b) + return set_has(game.castle, b) } function is_block_in_castle_in(b, town) { - return game.location[b] === town && game.castle.includes(b) + return game.location[b] === town && set_has(game.castle, b) } function besieged_player(where) { - for (let b of BLOCKLIST) + for (let b = 0; b <= last_block; ++b) if (is_block_in_castle_in(b, where)) return block_owner(b) return null @@ -661,13 +613,13 @@ function can_activate(who) { return block_owner(who) === game.active && is_block_on_map(who) && !is_block_in_castle(who) && - !game.moved[who] + !set_has(game.moved, who) } function can_activate_for_sea_move(who) { return block_owner(who) === game.active && is_block_on_map(who) && - !game.moved[who] + !set_has(game.moved, who) } function count_pinning(where) { @@ -676,7 +628,7 @@ function count_pinning(where) { function count_pinned(where) { let count = 0 - for (let b of BLOCKLIST) + for (let b = 0; b <= last_block; ++b) if (game.location[b] === where && block_owner(b) === game.active) if (!is_reserve(b)) ++count @@ -916,7 +868,7 @@ function can_block_muster_with_3_moves(n0, muster) { if (can_block_use_road_to_muster(n1, n2)) { if (n2 === muster) return true - if (TOWNS[n2].exits.includes(muster)) + if (set_has(TOWNS[n2].exits, muster)) if (can_block_use_road_to_muster(n2, muster)) return true } @@ -933,7 +885,7 @@ function can_block_muster_with_2_moves(n0, muster, avoid) { if (can_block_use_road_to_muster(n0, n1)) { if (n1 === muster) return true - if (TOWNS[n1].exits.includes(muster)) + if (set_has(TOWNS[n1].exits, muster)) if (can_block_use_road_to_muster(n1, muster)) return true } @@ -942,7 +894,7 @@ function can_block_muster_with_2_moves(n0, muster, avoid) { } function can_block_muster_with_1_move(n0, muster) { - if (TOWNS[n0].exits.includes(muster)) + if (set_has(TOWNS[n0].exits, muster)) return can_block_use_road_to_muster(n0, muster) return false } @@ -957,20 +909,20 @@ function can_block_muster(who, muster) { if (block_move(who) === 3) return can_block_muster_with_3_moves(from, muster) else - return can_block_muster_with_2_moves(from, muster, null) + return can_block_muster_with_2_moves(from, muster, NOWHERE) } return false } function can_muster_to(muster) { - for (let b of BLOCKLIST) + for (let b = 0; b <= last_block; ++b) if (can_block_muster(b, muster)) return true return false } function can_muster_anywhere() { - for (let where of TOWNLIST) + for (let where = first_town; where <= last_town; ++where) if (is_friendly_field(where)) if (can_muster_to(where)) return true @@ -979,21 +931,21 @@ function can_muster_anywhere() { function lift_siege(where) { if (is_under_siege(where) && !is_contested_town(where)) { - log("Siege lifted in " + where + ".") - for (let b of BLOCKLIST) + log("Siege lifted at #" + where + ".") + for (let b = 0; b <= last_block; ++b) if (is_block_in_castle_in(b, where)) - remove_from_array(game.castle, b) + set_delete(game.castle, b) } } function lift_all_sieges() { - for (let t of TOWNLIST) - lift_siege(t) + for (let town = first_town; town <= last_town; ++town) + lift_siege(town) } function reset_blocks() { - for (let b of BLOCKLIST) { - game.location[b] = ELIMINATED + for (let b = 0; b <= last_block; ++b) { + game.location[b] = NOWHERE game.steps[b] = block_max_steps(b) } } @@ -1006,19 +958,19 @@ function deploy(who, where) { function disband(who) { game.summary.push([game.location[who]]) if (is_saladin_family(who) || block_type(who) === 'crusaders' || block_type(who) === 'military_orders') - game.location[who] = ELIMINATED // permanently eliminated + game.location[who] = NOWHERE // permanently eliminated else game.location[who] = DEAD // into to the pool next year game.steps[who] = block_max_steps(who) } function eliminate_block(who) { - remove_from_array(game.castle, who) - if (game.sallying) remove_from_array(game.sallying, who) - if (game.storming) remove_from_array(game.storming, who) + set_delete(game.castle, who) + if (game.sallying) set_delete(game.sallying, who) + if (game.storming) set_delete(game.storming, who) log(block_name(who) + " was eliminated.") if (is_saladin_family(who) || block_type(who) === 'crusaders' || block_type(who) === 'military_orders') - game.location[who] = ELIMINATED // permanently eliminated + game.location[who] = NOWHERE // permanently eliminated else game.location[who] = DEAD // into to the pool next year game.steps[who] = block_max_steps(who) @@ -1036,9 +988,9 @@ function reduce_block(who) { function is_valid_frank_deployment() { let errors = [] - for (let town of TOWNLIST) + for (let town = first_town; town <= last_town; ++town) if (!is_within_castle_limit(town)) - errors.push(town) + errors.push(TOWNS[town].name) return errors } @@ -1055,7 +1007,7 @@ states.frank_deployment = { let errors = is_valid_frank_deployment() if (errors.length === 0) gen_action(view, 'next') - for (let b of BLOCKLIST) { + for (let b = 0; b <= last_block; ++b) { if (block_owner(b) === game.active && is_block_on_land(b)) if (list_seats(b).length > 1) gen_action(view, 'block', b) @@ -1081,7 +1033,7 @@ states.frank_deployment_to = { prompt: function (view, current) { if (is_inactive_player(current)) return view.prompt = "Deployment: Waiting for " + game.active + "." - view.prompt = "Deployment: Move " + game.who + " to " + join(list_seats(game.who), "or") + "." + view.prompt = "Deployment: Move " + game.who + " to " + join(list_seats(game.who).map(town_name), "or") + "." gen_action_undo(view) gen_action(view, 'block', game.who) let from = game.location[game.who] @@ -1091,7 +1043,7 @@ states.frank_deployment_to = { }, town: function (where) { game.location[game.who] = where - game.who = null + game.who = NOBODY game.state = 'frank_deployment' }, block: pop_undo, @@ -1101,7 +1053,7 @@ states.frank_deployment_to = { function goto_saracen_deployment() { for (let i = 0; i < 4; ++i) { let nomad = select_random_block(S_POOL) - log(BLOCKS[nomad].name + " arrived in " + block_home(nomad) + ".") + log(BLOCKS[nomad].name + " arrived in #" + block_home(nomad) + ".") deploy(nomad, block_home(nomad)) } game.active = SARACENS @@ -1127,11 +1079,11 @@ states.saracen_deployment = { let saladin = game.location[SALADIN] game.location[SALADIN] = game.location[who] game.location[who] = saladin - game.who = null + game.who = NOBODY }, next: function () { clear_undo() - game.who = null + game.who = NOBODY start_year() }, undo: pop_undo @@ -1180,7 +1132,7 @@ function check_sudden_death() { function start_year() { log("") - log("Start Year " + game.year) + log(".h1 Year " + game.year) game.turn = 1 @@ -1195,7 +1147,7 @@ function start_year() { function start_game_turn() { log("") - log("Start Turn " + game.turn + " of Year " + game.year) + log(".h1 Turn " + game.turn + " of Year " + game.year) game.guide = null game.jihad = null @@ -1204,9 +1156,9 @@ function start_game_turn() { reset_road_limits() game.last_used = {} game.attacker = {} - game.reserves1 = [] - game.reserves2 = [] - game.moved = {} + set_clear(game.reserves1) + set_clear(game.reserves2) + set_clear(game.moved) goto_card_phase() } @@ -1350,7 +1302,7 @@ function reveal_cards() { function start_player_turn() { log("") - log("Start " + game.active) + log(".h2 " + game.active) reset_road_limits() game.main_road = {} let card = CARDS[game.active === FRANKS ? game.f_card : game.s_card] @@ -1393,7 +1345,7 @@ states.assassins = { if (is_inactive_player(current)) return view.prompt = "Assassins: Waiting for " + game.active + "." view.prompt = "Assassins: Choose one enemy block." - for (let b of BLOCKLIST) { + for (let b = 0; b <= last_block; ++b) { if (is_block_on_land(b) && block_owner(b) === enemy(game.active)) gen_action(view, 'block', b) } @@ -1412,7 +1364,7 @@ states.assassins_show_1 = { view.who = ASSASSINS if (is_inactive_player(current)) return view.prompt = "Assassins: Waiting for " + game.active + "." - view.prompt = "Assassins: The assassins target " + block_name(game.who) + " in " + game.where + "." + view.prompt = "Assassins: The assassins target " + block_name(game.who) + " in " + town_name(game.where) + "." gen_action(view, 'next') gen_action(view, 'block', game.who) gen_action(view, 'block', ASSASSINS) @@ -1432,7 +1384,7 @@ states.assassins_show_2 = { view.who = ASSASSINS if (is_inactive_player(current)) return view.prompt = "Assassins: Waiting for " + game.active + "." - view.prompt = "Assassins: The assassins hit " + block_name(game.who) + " in " + game.where + "." + view.prompt = "Assassins: The assassins hit " + block_name(game.who) + " in " + town_name(game.where) + "." gen_action(view, 'next') gen_action(view, 'block', ASSASSINS) gen_action(view, 'town', MASYAF) @@ -1445,8 +1397,8 @@ states.assassins_show_2 = { function assassins_next_2() { lift_siege(game.where) game.location[ASSASSINS] = MASYAF - game.who = null - game.where = null + game.who = NOBODY + game.where = NOWHERE end_player_turn() } @@ -1463,7 +1415,7 @@ function assassinate(who, where) { } } hits = Math.min(hits, game.steps[who]) - log("Assassins hit " + block_name(who) + " in " + where + ": " + rolls.join("") + ".") + log("Assassins hit " + block_name(who) + " at #" + where + ": " + rolls.join("") + ".") for (let i = 0; i < hits; ++i) reduce_block(who) } @@ -1482,7 +1434,7 @@ function goto_jihad() { function goto_select_jihad() { game.jihad_list = [] - for (let where of TOWNLIST) + for (let where = first_town; where <= last_town; ++where) if (is_contested_field(where) || besieging_player(where) === game.active) game.jihad_list.push(where) if (game.jihad_list.length === 0) { @@ -1517,7 +1469,7 @@ states.select_jihad = { function goto_manna() { game.state = 'manna' game.moves = 3 - game.moved = {} + set_clear(game.moved) game.summary = [] } @@ -1529,8 +1481,8 @@ states.manna = { gen_action_undo(view) gen_action(view, 'next') if (game.moves > 0) { - for (let b of BLOCKLIST) { - if (is_block_on_land(b) && block_owner(b) === game.active && !game.moved[b]) + for (let b = 0; b <= last_block; ++b) { + if (is_block_on_land(b) && block_owner(b) === game.active && !set_has(game.moved, b)) if (game.steps[b] < block_max_steps(b)) gen_action(view, 'block', b) } @@ -1541,12 +1493,12 @@ states.manna = { game.summary.push([game.location[who]]) ++game.steps[who] --game.moves - game.moved[who] = 1 + set_add(game.moved, who) }, next: function () { clear_undo() print_summary(game.active + " used Manna:") - game.moved = {} + set_clear(game.moved) end_player_turn() }, undo: pop_undo @@ -1558,11 +1510,11 @@ function queue_attack(who, round) { if (round === 1) return ATTACK_MARK if (round === 2) { - game.reserves1.push(who) + set_add(game.reserves1, who) return RESERVE_MARK_1 } if (round === 3) { - game.reserves2.push(who) + set_add(game.reserves2, who) return RESERVE_MARK_2 } } @@ -1619,8 +1571,8 @@ function end_move_phase() { } clear_undo() - game.who = null - game.where = null + game.who = NOBODY + game.where = NOWHERE game.moves = 0 // declined to use winter campaign @@ -1669,7 +1621,7 @@ states.move_phase = { gen_action_undo(view) gen_action(view, 'end_move_phase') if (game.moves > 0) { - for (let b of BLOCKLIST) { + for (let b = 0; b <= last_block; ++b) { if (can_block_land_move(b)) gen_action(view, 'block', b) if (can_block_sea_move(b)) @@ -1717,7 +1669,7 @@ states.move_phase_event = { view.prompt = group_move_name(0) + "Choose a block to group move." gen_action_undo(view) gen_action(view, 'end_move_phase') - for (let b of BLOCKLIST) + for (let b = 0; b <= last_block; ++b) if (can_block_land_move(b)) gen_action(view, 'block', b) }, @@ -1726,7 +1678,7 @@ states.move_phase_event = { game.where = game.location[who] game.who = who game.distance = 0 - game.last_from = null + game.last_from = NOWHERE game.state = 'group_move_to' }, end_move_phase: end_move_phase, @@ -1777,7 +1729,7 @@ states.move_phase_to = { log_move_start(from) let mark = move_block(game.who, from, to) if (mark) - log_move_continue(to + mark) + log_move_continue(to, mark) else log_move_continue(to) lift_siege(from) @@ -1801,7 +1753,7 @@ function group_move_name() { } function can_group_move_more() { - for (let b of BLOCKLIST) + for (let b = 0; b <= last_block; ++b) if (game.location[b] === game.where) if (can_block_land_move(b)) return true @@ -1812,13 +1764,13 @@ states.group_move_who = { prompt: function (view, current) { if (is_inactive_player(current)) return view.prompt = group_move_name(1) + "Waiting for " + game.active + "." - view.prompt = group_move_name(0) + "Move blocks from " + game.where + "." + view.prompt = group_move_name(0) + "Move blocks from " + town_name(game.where) + "." gen_action_undo(view) if (game.active === game.guide || game.active === game.jihad) gen_action(view, 'end_move_phase') else gen_action(view, 'end_group_move') - for (let b of BLOCKLIST) + for (let b = 0; b <= last_block; ++b) if (game.location[b] === game.where) if (can_block_land_move(b)) gen_action(view, 'block', b) @@ -1827,7 +1779,7 @@ states.group_move_who = { push_undo() game.who = who game.distance = 0 - game.last_from = null + game.last_from = NOWHERE game.state = 'group_move_to' }, end_move_phase: function () { @@ -1868,7 +1820,7 @@ states.group_move_to = { log_move_start(from) let mark = move_block(game.who, from, to) if (mark) - log_move_continue(to + mark) + log_move_continue(to, mark) else log_move_continue(to) lift_siege(from) @@ -1882,9 +1834,9 @@ states.group_move_to = { function end_move() { if (game.distance > 0) - game.moved[game.who] = 1 + set_add(game.moved, game.who) log_move_end() - game.who = null + game.who = NOBODY game.distance = 0 if (can_group_move_more()) game.state = 'group_move_who' @@ -1893,7 +1845,7 @@ function end_move() { } function end_group_move() { - print_summary(game.active + " activated " + game.where + ":") + print_summary(game.active + " activated #" + game.where + ":") game.state = 'move_phase' } @@ -1914,14 +1866,14 @@ states.german_move_to = { --game.moves let from = GERMANIA game.location[game.who] = to - game.moved[game.who] = 1 + set_add(game.moved, game.who) game.distance = 0 let mark = move_block(game.who, from, to) if (mark) - log(game.active + " moved:\n Germania \u2192 " + to + mark + ".") + log(game.active + " moved:\n Germania \u2192 #" + to + mark + ".") else - log(game.active + " moved:\n Germania \u2192 " + to + ".") - game.who = null + log(game.active + " moved:\n Germania \u2192 #" + to + ".") + game.who = NOBODY game.state = 'move_phase' }, block: pop_undo, @@ -1954,29 +1906,32 @@ states.sea_move_to = { let from = game.where game.location[game.who] = to - game.moved[game.who] = 1 + set_add(game.moved, game.who) lift_siege(from) - remove_from_array(game.castle, game.who) + set_delete(game.castle, game.who) if (besieged_player(to) === game.active && is_more_room_in_castle(to)) { // Move into besieged fortified port - game.castle.push(game.who) - log(game.active + " sea moved:\n" + from + " \u2192 " + to + " castle.") + set_add(game.castle, game.who) + log(game.active + " sea moved:") + logi("#" + from + " \u2192 #" + to + " castle.") } else if (!is_friendly_port(to)) { // English Crusaders attack! game.attacker[to] = FRANKS game.main_road[to] = "England" - log(game.active + " sea moved:\n" + from + " \u2192 " + to + ATTACK_MARK + ".") + log(game.active + " sea moved:") + logi("#" + from + " \u2192 #" + to + ATTACK_MARK + ".") } else { // Normal move. - log(game.active + " sea moved:\n" + from + " \u2192 " + to + ".") + log(game.active + " sea moved:") + logi("#" + from + " \u2192 #" + to + ".") } - game.who = null + game.who = NOBODY game.state = 'move_phase' }, block: pop_undo, @@ -1991,7 +1946,7 @@ states.muster = { return view.prompt = "Move Phase: Waiting for " + game.active + "." view.prompt = "Muster: Choose a friendly muster town." gen_action_undo(view) - for (let where of TOWNLIST) { + for (let where = first_town; where <= last_town; ++where) { // cannot start or reinforce battles in winter if (is_winter()) { if (is_friendly_town(where)) @@ -2017,11 +1972,11 @@ states.muster_who = { prompt: function (view, current) { if (is_inactive_player(current)) return view.prompt = "Move Phase: Waiting for " + game.active + "." - view.prompt = "Muster: Move blocks to " + game.where + "." + view.prompt = "Muster: Move blocks to " + town_name(game.where) + "." view.muster = game.where gen_action_undo(view) gen_action(view, 'end_muster') - for (let b of BLOCKLIST) + for (let b = 0; b <= last_block; ++b) if (can_block_muster(b, game.where)) gen_action(view, 'block', b) }, @@ -2031,8 +1986,8 @@ states.muster_who = { game.state = 'muster_move_1' }, end_muster: function () { - print_summary(game.active + " mustered to " + game.where + ":") - game.where = null + print_summary(game.active + " mustered to #" + game.where + ":") + game.where = NOWHERE game.state = 'move_phase' }, undo: pop_undo, @@ -2042,7 +1997,7 @@ states.muster_move_1 = { prompt: function (view, current) { if (is_inactive_player(current)) return view.prompt = "Move Phase: Waiting for " + game.active + "." - view.prompt = "Muster: Move " + block_name(game.who) + " to " + game.where + "." + view.prompt = "Muster: Move " + block_name(game.who) + " to " + town_name(game.where) + "." view.muster = game.where gen_action_undo(view) gen_action(view, 'block', game.who) @@ -2084,7 +2039,7 @@ states.muster_move_2 = { prompt: function (view, current) { if (is_inactive_player(current)) return view.prompt = "Move Phase: Waiting for " + game.active + "." - view.prompt = "Muster: Move " + block_name(game.who) + " to " + game.where + "." + view.prompt = "Muster: Move " + block_name(game.who) + " to " + town_name(game.where) + "." view.muster = game.where gen_action_undo(view) let from = game.location[game.who] @@ -2122,7 +2077,7 @@ states.muster_move_3 = { prompt: function (view, current) { if (is_inactive_player(current)) return view.prompt = "Move Phase: Waiting for " + game.active + "." - view.prompt = "Muster: Move " + block_name(game.who) + " to " + game.where + "." + view.prompt = "Muster: Move " + block_name(game.who) + " to " + town_name(game.where) + "." view.muster = game.where gen_action_undo(view) let from = game.location[game.who] @@ -2145,8 +2100,8 @@ states.muster_move_3 = { function end_muster_move() { log_move_end() - game.moved[game.who] = 1 - game.who = null + set_add(game.moved, game.who) + game.who = NOBODY game.state = 'muster_who' } @@ -2158,12 +2113,12 @@ states.winter_campaign = { return view.prompt = "Move Phase: Waiting for " + game.active + "." view.prompt = "Winter Campaign: Select a siege to maintain over the winter." gen_action_undo(view) - for (let town of TOWNLIST) + for (let town = first_town; town <= last_town; ++town) if (is_friendly_field(town) && is_under_siege(town)) gen_action(view, 'town', town) }, town: function (where) { - log(game.active + " winter campaigned in " + where + ".") + log(game.active + " winter campaigned at #" + where + ".") game.winter_campaign = where game.state = 'move_phase' }, @@ -2174,14 +2129,14 @@ states.winter_campaign = { function goto_combat_phase() { if (is_winter()) { - game.moved = {} + set_clear(game.moved) return end_game_turn() } game.combat_list = [] - for (let where of TOWNLIST) + for (let where = first_town; where <= last_town; ++where) if (is_contested_town(where)) - game.combat_list.push(where) + set_add(game.combat_list, where) resume_combat_phase() } @@ -2206,7 +2161,7 @@ states.combat_phase = { gen_action(view, 'town', where) }, town: function (where) { - remove_from_array(game.combat_list, where) + set_delete(game.combat_list, where) game.where = where start_combat() }, @@ -2215,9 +2170,9 @@ states.combat_phase = { function start_combat() { game.flash = "" log("") - log("Battle in " + game.where) + log(".h3 Battle at #" + game.where) game.combat_round = 0 - game.halfhit = null + game.halfhit = NOBODY game.storming = [] game.sallying = [] @@ -2226,7 +2181,7 @@ function start_combat() { if (is_castle_town(game.where)) { if (!is_under_siege(game.where)) { - log("~ Combat Deployment ~") + log(".h4 Combat Deployment") game.castle_owner = enemy(game.attacker[game.where]) game.active = game.castle_owner game.state = 'combat_deployment' @@ -2257,7 +2212,7 @@ function end_combat() { delete game.sallying delete game.show_castle delete game.show_field - game.where = null + game.where = NOWHERE game.flash = "" game.combat_round = 0 @@ -2276,9 +2231,9 @@ states.combat_deployment = { let n = count_blocks_in_castle(game.where) let have_options = false if (n < max) { - for (let b of BLOCKLIST) { + for (let b = 0; b <= last_block; ++b) { if (block_owner(b) === game.active && !is_reserve(b)) { - if (game.location[b] === game.where && !game.castle.includes(b)) { + if (game.location[b] === game.where && !set_has(game.castle, b)) { gen_action(view, 'withdraw', b) gen_action(view, 'block', b) have_options = true @@ -2293,11 +2248,11 @@ states.combat_deployment = { }, withdraw: function (who) { push_undo() - game.castle.push(who) + set_add(game.castle, who) }, block: function (who) { push_undo() - game.castle.push(who) + set_add(game.castle, who) }, next: function () { clear_undo() @@ -2319,7 +2274,7 @@ states.combat_deployment = { function print_retreat_summary() { if (game.summary && game.summary.length > 0) - print_summary("Retreated from " + game.where + ":") + print_summary("Retreated from #" + game.where + ":") } function goto_regroup() { @@ -2339,7 +2294,7 @@ states.regroup = { view.prompt = "Regroup: Choose a block to move." gen_action_undo(view) gen_action(view, 'end_regroup') - for (let b of BLOCKLIST) { + for (let b = 0; b <= last_block; ++b) { if (game.location[b] === game.where) { if (can_block_regroup(b)) gen_action(view, 'block', b) @@ -2377,13 +2332,13 @@ states.regroup_to = { }, town: function (to) { // We can regroup while reserves are still on the way... - remove_from_array(game.reserves1, game.who) - remove_from_array(game.reserves2, game.who) + set_delete(game.reserves1, game.who) + set_delete(game.reserves2, game.who) let from = game.where game.summary.push([from, to]) move_block(game.who, game.where, to) - game.who = null + game.who = NOBODY game.state = 'regroup' }, block: pop_undo, @@ -2407,14 +2362,14 @@ function next_combat_round() { function bring_on_reserves(reserves) { let f = 0 let s = 0 - for (let b of BLOCKLIST) { + for (let b = 0; b <= last_block; ++b) { if (game.location[b] === game.where) { - if (reserves.includes(b)) { + if (set_has(reserves, b)) { if (block_owner(b) === FRANKS) ++f else ++s - remove_from_array(reserves, b) + set_delete(reserves, b) } } } @@ -2425,18 +2380,18 @@ function bring_on_reserves(reserves) { } function clear_reserves(where) { - for (let b of BLOCKLIST) { + for (let b = 0; b <= last_block; ++b) { if (game.location[b] === where) { - remove_from_array(game.reserves1, b) - remove_from_array(game.reserves2, b) + set_delete(game.reserves1, b) + set_delete(game.reserves2, b) } } } function reset_moved_for_combat() { - for (let b in game.moved) game.moved[b] = 0 - for (let b of game.reserves1) game.moved[b] = 1 - for (let b of game.reserves2) game.moved[b] = 1 + set_clear(game.moved) + for (let b of game.reserves1) set_add(game.moved, b) + for (let b of game.reserves2) set_add(game.moved, b) } function goto_combat_round(new_combat_round) { @@ -2455,7 +2410,7 @@ function goto_combat_round(new_combat_round) { } } - log("~ Combat Round " + game.combat_round + " ~") + log(".h4 Combat Round " + game.combat_round) if (game.combat_round === 2) bring_on_reserves(game.reserves1) @@ -2470,7 +2425,7 @@ function goto_combat_round(new_combat_round) { log("Relief forces arrived!") if (game.storming.length > 0) { log("Storming canceled by arriving relief force.") - game.halfhit = null + game.halfhit = NOBODY game.storming.length = 0 } let old_attacker = game.attacker[game.where] @@ -2510,9 +2465,9 @@ states.declare_storm = { view.prompt = "Siege Declaration: Declare which blocks should storm the castle." let have_options = false if (game.storming.length < castle_limit(game.where)) { - for (let b of BLOCKLIST) { + for (let b = 0; b <= last_block; ++b) { if (block_owner(b) === game.active && !is_reserve(b)) { - if (game.location[b] === game.where && !game.storming.includes(b)) { + if (game.location[b] === game.where && !set_has(game.storming, b)) { gen_action(view, 'storm', b) gen_action(view, 'block', b) have_options = true @@ -2545,7 +2500,7 @@ states.declare_storm = { function storm_with_block(who) { push_undo() - game.storming.push(who) + set_add(game.storming, who) if (game.storming.length > 1) game.flash = game.active + " stormed with " + game.storming.length + " blocks." else @@ -2568,9 +2523,9 @@ states.declare_sally = { return view.prompt = "Siege Declaration: Waiting for " + game.active + " to declare sally." view.prompt = "Siege Declaration: Declare which blocks should sally onto the field." let have_options = false - for (let b of BLOCKLIST) { + for (let b = 0; b <= last_block; ++b) { if (block_owner(b) === game.active && !is_reserve(b) && is_block_in_castle(b)) { - if (game.location[b] === game.where && !game.sallying.includes(b)) { + if (game.location[b] === game.where && !set_has(game.sallying, b)) { gen_action(view, 'sally', b) gen_action(view, 'block', b) have_options = true @@ -2608,8 +2563,8 @@ states.declare_sally = { function sally_with_block(who) { push_undo() - remove_from_array(game.castle, who) - game.sallying.push(who) + set_delete(game.castle, who) + set_add(game.sallying, who) if (game.sallying.length > 1) game.flash = game.active + " sallied with " + game.sallying.length + " blocks." else @@ -2624,17 +2579,17 @@ function goto_retreat_after_combat() { // withdraw all sallying blocks to castle. for (let b of game.sallying) - game.castle.push(b) + set_add(game.castle, b) game.sallying.length = 0 // TODO: 6.2 - In Sieges, the attacker /may/ retreat or stay on siege. // withdraw all storming blocks to the field. - game.halfhit = null + game.halfhit = NOBODY game.storming.length = 0 if (is_contested_field(game.where)) { - log("~ Retreat ~") + log(".h4 Retreat") game.active = game.attacker[game.where] game.state = 'retreat' game.summary = [] @@ -2652,7 +2607,7 @@ states.retreat = { view.prompt = "Retreat: Choose a block to move." gen_action_undo(view) let can_retreat = false - for (let b of BLOCKLIST) { + for (let b = 0; b <= last_block; ++b) { if (game.location[b] === game.where && !is_block_in_castle(b) && can_block_retreat(b)) { gen_action(view, 'block', b) can_retreat = true @@ -2663,7 +2618,7 @@ states.retreat = { }, end_retreat: function () { clear_undo() - for (let b of BLOCKLIST) + for (let b = 0; b <= last_block; ++b) if (game.location[b] === game.where && !is_block_in_castle(b) && block_owner(b) === game.active) eliminate_block(b) print_summary(game.active + " retreated:") @@ -2699,12 +2654,12 @@ states.retreat_to = { let from = game.where game.summary.push([from, to]) move_block(game.who, game.where, to) - game.who = null + game.who = NOBODY game.state = 'retreat' }, eliminate: function () { eliminate_block(game.who) - game.who = null + game.who = NOBODY game.state = 'retreat' }, block: pop_undo, @@ -2714,13 +2669,13 @@ states.retreat_to = { // SIEGE ATTRITION function goto_siege_attrition() { - log("~ Siege Attrition ~") + log(".h4 Siege Attrition") game.active = besieged_player(game.where) game.state = 'siege_attrition' game.attrition_list = [] - for (let b of BLOCKLIST) + for (let b = 0; b <= last_block; ++b) if (is_block_in_castle_in(b, game.where)) - game.attrition_list.push(b) + set_add(game.attrition_list, b) } function resume_siege_attrition() { @@ -2728,7 +2683,7 @@ function resume_siege_attrition() { delete game.attrition_list if (!is_under_siege(game.where)) { game.active = enemy(game.active) - log(game.where + " fell to siege attrition.") + log("#" + game.where + " fell to siege attrition.") goto_regroup() } else { log("Siege continued.") @@ -2741,7 +2696,7 @@ states.siege_attrition = { prompt: function (view, current) { if (is_inactive_player(current)) return view.prompt = "Siege Attrition: Waiting for " + game.active + "." - view.prompt = "Siege Attrition: Roll for siege attrition in " + game.where + "." + view.prompt = "Siege Attrition: Roll for siege attrition in " + town_name(game.where) + "." for (let b of game.attrition_list) gen_action(view, 'block', b) }, @@ -2754,7 +2709,7 @@ states.siege_attrition = { } else { log("Attrition roll " + DIE_MISS[die] + ".") } - remove_from_array(game.attrition_list, who) + set_delete(game.attrition_list, who) resume_siege_attrition() } } @@ -2763,8 +2718,8 @@ states.siege_attrition = { function filter_battle_blocks(ci, is_candidate) { let output = null - for (let b of BLOCKLIST) { - if (is_candidate(b) && !game.moved[b]) { + for (let b = 0; b <= last_block; ++b) { + if (is_candidate(b) && !set_has(game.moved, b)) { if (block_initiative(b) === ci) { if (!output) output = [] @@ -2870,7 +2825,7 @@ states.field_battle = { for (let b of game.battle_list) { gen_action(view, 'block', b) // take default action gen_action(view, 'fire', b) - if (game.sallying.includes(b)) { + if (set_has(game.sallying, b)) { // Only sallying forces may withdraw into the castle gen_action(view, 'withdraw', b) } else { @@ -2919,7 +2874,7 @@ function resume_siege_battle() { if (is_enemy_town(game.where)) { game.active = enemy(game.active) - game.halfhit = null + game.halfhit = NOBODY log("Siege battle won by " + game.active + ".") return goto_regroup() } @@ -2935,7 +2890,7 @@ function resume_siege_battle() { } if (game.storming.length === 0) { - game.halfhit = null + game.halfhit = NOBODY log("Storming repulsed.") return next_combat_round() } @@ -2953,7 +2908,7 @@ states.siege_battle = { for (let b of game.battle_list) { gen_action(view, 'block', b) // take default action gen_action(view, 'fire', b) - if (game.storming.includes(b)) + if (set_has(game.storming, b)) gen_action(view, 'retreat', b) } }, @@ -2975,11 +2930,11 @@ function goto_field_battle_hits() { function list_field_victims() { let max = 0 - for (let b of BLOCKLIST) + for (let b = 0; b <= last_block; ++b) if (block_owner(b) === game.active && is_field_combatant(b) && game.steps[b] > max) max = game.steps[b] let list = [] - for (let b of BLOCKLIST) + for (let b = 0; b <= last_block; ++b) if (block_owner(b) === game.active && is_field_combatant(b) && game.steps[b] === max) list.push(b) return list @@ -3035,14 +2990,14 @@ function goto_siege_battle_hits() { } function list_siege_victims() { - if (game.halfhit && block_owner(game.halfhit) === game.active) + if (game.halfhit !== NOBODY && block_owner(game.halfhit) === game.active) return [ game.halfhit ] let max = 0 - for (let b of BLOCKLIST) + for (let b = 0; b <= last_block; ++b) if (block_owner(b) === game.active && is_siege_combatant(b) && game.steps[b] > max) max = game.steps[b] let list = [] - for (let b of BLOCKLIST) + for (let b = 0; b <= last_block; ++b) if (block_owner(b) === game.active && is_siege_combatant(b) && game.steps[b] === max) list.push(b) return list @@ -3069,7 +3024,7 @@ function apply_siege_battle_hit_to(who, flash) { msg += "hit." log(game.active[0] + ": " + msg) reduce_block(who) - game.halfhit = null + game.halfhit = NOBODY } else { if (is_block_in_castle(who)) { msg += "half-hit." @@ -3107,7 +3062,7 @@ function roll_attack(active, b, verb, is_charge) { let fire = block_fire_power(b, game.where) + is_charge let rolls = [] let steps = game.steps[b] - let name = block_name(b) + " " + BLOCKS[b].combat + let name = block_name(b) + " " + BLOCKS[b].initiative + BLOCKS[b].fire_power let self = 0 for (let i = 0; i < steps; ++i) { let die = roll_d6() @@ -3146,7 +3101,7 @@ function roll_attack(active, b, verb, is_charge) { } function field_fire_with_block(b) { - game.moved[b] = 1 + set_add(game.moved, b) roll_attack(game.active, b, "fired", 0) if (game.hits > 0) { goto_field_battle_hits() @@ -3156,7 +3111,7 @@ function field_fire_with_block(b) { } function siege_fire_with_block(b) { - game.moved[b] = 1 + set_add(game.moved, b) roll_attack(game.active, b, "fired", 0) if (game.hits > 0) { goto_siege_battle_hits() @@ -3166,7 +3121,7 @@ function siege_fire_with_block(b) { } function charge_with_block(b) { - game.moved[b] = 1 + set_add(game.moved, b) roll_attack(game.active, b, "charged", 1) if (game.hits > 0) { goto_field_battle_hits() @@ -3176,19 +3131,19 @@ function charge_with_block(b) { } function field_withdraw_with_block(b) { - game.flash = b + " withdrew." + game.flash = block_name(b) + " withdrew." log(game.active[0] + ": " + game.flash) - game.moved[b] = 1 - remove_from_array(game.sallying, b) - game.castle.push(b) + set_add(game.moved, b) + set_delete(game.sallying, b) + set_add(game.castle, b) resume_field_battle() } function siege_withdraw_with_block(b) { - game.flash = b + " withdrew." + game.flash = block_name(b) + " withdrew." log(game.active[0] + ": " + game.flash) - game.moved[b] = 1 - remove_from_array(game.storming, b) + set_add(game.moved, b) + set_delete(game.storming, b) resume_siege_battle() } @@ -3217,7 +3172,7 @@ states.harry = { game.summary.push([game.active, to]) game.location[game.who] = to move_block(game.who, game.where, to) - game.who = null + game.who = NOBODY resume_field_battle() }, } @@ -3244,15 +3199,15 @@ states.retreat_in_battle = { game.summary.push([game.active, to]) game.location[game.who] = to move_block(game.who, game.where, to) - game.who = null + game.who = NOBODY resume_field_battle() }, block: function () { - game.who = null + game.who = NOBODY resume_field_battle() }, undo: function () { - game.who = null + game.who = NOBODY resume_field_battle() } } @@ -3274,11 +3229,11 @@ function start_draw_phase() { game.state = 'draw_phase' if (game.active === FRANKS) { game.who = select_random_block(F_POOL) - if (!game.who) + if (game.who === NOBODY) end_draw_phase() } else { game.who = select_random_block(S_POOL) - if (!game.who) + if (game.who === NOBODY) end_draw_phase() } } @@ -3296,7 +3251,7 @@ states.draw_phase = { break case 'pilgrims': view.prompt = "Draw Phase: Place " + block_name(game.who) + " in a friendly port." - for (let town of TOWNLIST) { + for (let town = first_town; town <= last_town; ++town) { if (is_friendly_port(town) || can_enter_besieged_port(town)) { gen_action(view, 'town', town) can_place = true @@ -3307,9 +3262,9 @@ states.draw_phase = { case 'outremers': case 'emirs': case 'nomads': - view.prompt = "Draw Phase: Place " + BLOCKS[game.who].name + " at full strength in " - + list_seats(game.who).join(", ") + " or at strength 1 in any friendly town." - for (let town of TOWNLIST) { + view.prompt = "Draw Phase: Place " + block_name(game.who) + " at full strength in " + + list_seats(game.who).map(town_name).join(", ") + " or at strength 1 in any friendly town." + for (let town = first_town; town <= last_town; ++town) { if (town === ENGLAND || town === FRANCE || town === GERMANIA) continue // FAQ claims besieger controls town for draw purposes @@ -3326,7 +3281,7 @@ states.draw_phase = { town: function (where) { let type = block_type(game.who) - log(game.active + " placed drawn block in " + where + ".") + log(game.active + " deployed at #" + where + ".") game.location[game.who] = where if (type === 'turcopoles' || type === 'outremers' || type === 'emirs' || type === 'nomads') { @@ -3337,10 +3292,10 @@ states.draw_phase = { } else { game.steps[game.who] = block_max_steps(game.who) if (can_enter_besieged_port(where)) - game.castle.push(game.who) + set_add(game.castle, game.who) } - game.who = null + game.who = NOBODY end_draw_phase() }, next: function () { @@ -3372,7 +3327,7 @@ function end_game_turn() { function goto_winter_1() { log("") - log("Start Winter of " + game.year) + log(".h1 Winter of " + game.year) log("") if (game.winter_campaign) goto_winter_siege_attrition() @@ -3381,15 +3336,15 @@ function goto_winter_1() { } function goto_winter_siege_attrition() { - log(game.active + " winter campaigned in " + game.winter_campaign + ".") + log(game.active + " winter campaigned at #" + game.winter_campaign + ".") game.where = game.winter_campaign game.active = besieged_player(game.where) game.state = 'winter_siege_attrition' game.attrition_list = [] - for (let b of BLOCKLIST) + for (let b = 0; b <= last_block; ++b) if (is_block_in_castle_in(b, game.where)) - game.attrition_list.push(b) + set_add(game.attrition_list, b) } function resume_winter_siege_attrition() { @@ -3397,7 +3352,7 @@ function resume_winter_siege_attrition() { delete game.attrition_list if (!is_under_siege(game.where)) { game.active = enemy(game.active) - log(game.where + " fell to siege attrition.") + log("#" + game.where + " fell to siege attrition.") goto_regroup() } else { log("Siege continued.") @@ -3410,7 +3365,7 @@ states.winter_siege_attrition = { prompt: function (view, current) { if (is_inactive_player(current)) return view.prompt = "Winter Siege Attrition: Waiting for " + game.active + "." - view.prompt = "Winter Siege Attrition: Roll for siege attrition in " + game.where + "." + view.prompt = "Winter Siege Attrition: Roll for siege attrition in " + town_name(game.where) + "." for (let b of game.attrition_list) gen_action(view, 'block', b) }, @@ -3423,13 +3378,13 @@ states.winter_siege_attrition = { } else { log("Attrition roll " + DIE_MISS[die] + ".") } - remove_from_array(game.attrition_list, who) + set_delete(game.attrition_list, who) resume_winter_siege_attrition() } } function goto_winter_2() { - game.where = null + game.where = NOWHERE eliminate_besieging_blocks(FRANKS) eliminate_besieging_blocks(SARACENS) lift_all_sieges() @@ -3442,7 +3397,7 @@ function goto_winter_2() { function eliminate_besieging_blocks(owner) { game.summary = [] - for (let b of BLOCKLIST) { + for (let b = 0; b <= last_block; ++b) { if (block_owner(b) === owner) { let where = game.location[b] if (where === game.winter_campaign) @@ -3459,7 +3414,7 @@ function eliminate_besieging_blocks(owner) { } function need_winter_supply_check() { - for (let town of TOWNLIST) { + for (let town = first_town; town <= last_town; ++town) { if (town === game.winter_campaign) continue if (is_friendly_town(town) && !is_within_castle_limit(town)) @@ -3491,7 +3446,7 @@ states.winter_supply = { return view.prompt = "Winter Supply: Waiting for " + game.active + "." gen_action_undo(view) let okay_to_end = true - for (let b of BLOCKLIST) { + for (let b = 0; b <= last_block; ++b) { if (block_owner(b) === game.active) { if (is_block_on_land(b)) { let where = game.location[b] @@ -3535,7 +3490,7 @@ states.winter_supply = { function goto_winter_replacements() { game.rp = {} - for (let town of TOWNLIST) + for (let town = first_town; town <= last_town; ++town) if (is_under_siege(town)) game.rp[town] = 0 else @@ -3559,7 +3514,7 @@ states.winter_replacements = { view.prompt = "Winter Replacements: Distribute replacement points." gen_action_undo(view) let okay_to_end = true - for (let b of BLOCKLIST) { + for (let b = 0; b <= last_block; ++b) { if (block_owner(b) === game.active && is_block_on_land(b)) { let where = game.location[b] let cost = replacement_cost(where) @@ -3616,7 +3571,7 @@ function goto_year_end() { game.result = SARACENS } else { game.victory = "The game ended in a draw." - game.result = null + game.result = "None" } log("") log(game.victory) @@ -3624,7 +3579,7 @@ function goto_year_end() { } // Return eliminated blocks to pool. - for (let b of BLOCKLIST) + for (let b = 0; b <= last_block; ++b) if (game.location[b] === DEAD) game.location[b] = block_pool(b) @@ -3646,7 +3601,7 @@ function setup_game() { reset_blocks() game.year = 1187 game.turn = 0 - for (let b of BLOCKLIST) { + for (let b = 0; b <= last_block; ++b) { if (block_owner(b) === FRANKS) { switch (block_type(b)) { case 'pilgrims': @@ -3666,10 +3621,8 @@ function setup_game() { if (block_type(b) === 'nomads') deploy(b, block_pool(b)) } - if (block_owner(b) === ASSASSINS) { - deploy(b, block_home(b)) - } } + deploy(ASSASSINS, MASYAF) goto_frank_deployment() } @@ -3692,9 +3645,9 @@ function make_battle_view() { } if (is_under_siege(game.where) && !is_contested_battle_field(game.where)) - battle.title = enemy(game.castle_owner) + " besiege " + game.where + battle.title = enemy(game.castle_owner) + " besiege " + town_name(game.where) else - battle.title = game.attacker[game.where] + " attack " + game.where + battle.title = game.attacker[game.where] + " attack " + town_name(game.where) if (game.combat_round === 0) battle.title += " \u2014 Combat Deployment" else @@ -3703,17 +3656,17 @@ function make_battle_view() { battle.title += " \u2014 Jihad!" function fill_cell(cell, owner, fn) { - for (let b of BLOCKLIST) + for (let b = 0; b <= last_block; ++b) if (game.location[b] === game.where & block_owner(b) === owner && fn(b)) cell.push(b) } fill_cell(battle.FR, FRANKS, b => is_reserve(b)) fill_cell(battle.FC, FRANKS, b => is_block_in_castle(b)) - fill_cell(battle.FF, FRANKS, b => is_block_in_field(b) && !game.storming.includes(b)) - fill_cell(battle.FF, SARACENS, b => is_block_in_field(b) && game.storming.includes(b)) - fill_cell(battle.SF, FRANKS, b => is_block_in_field(b) && game.storming.includes(b)) - fill_cell(battle.SF, SARACENS, b => is_block_in_field(b) && !game.storming.includes(b)) + fill_cell(battle.FF, FRANKS, b => is_block_in_field(b) && !set_has(game.storming, b)) + fill_cell(battle.FF, SARACENS, b => is_block_in_field(b) && set_has(game.storming, b)) + fill_cell(battle.SF, FRANKS, b => is_block_in_field(b) && set_has(game.storming, b)) + fill_cell(battle.SF, SARACENS, b => is_block_in_field(b) && !set_has(game.storming, b)) fill_cell(battle.SC, SARACENS, b => is_block_in_castle(b)) fill_cell(battle.SR, SARACENS, b => is_reserve(b)) @@ -3726,31 +3679,39 @@ exports.setup = function (seed, scenario, options) { log: [], undo: [], + active: null, + moves: 0, + who: NOBODY, + where: NOWHERE, + + show_cards: false, s_hand: [], f_hand: [], s_card: 0, f_card: 0, + + location: [], + steps: [], + attacker: {}, road_limit: {}, last_used: {}, - location: {}, + castle: [], main_road: {}, - moved: {}, - moves: 0, - prompt: null, + moved: [], reserves1: [], reserves2: [], - show_cards: false, - steps: {}, - who: null, - where: null, } // Old RNG for ancient replays if (options.rng) game.rng = options.rng + log(".h1 Crusader Rex") + log("") + + if (options && options.iron_bridge) { game.iron_bridge = 1 log("Iron Bridge:\nThe road between Antioch and Harim has a move limit of 3.") @@ -3786,9 +3747,9 @@ exports.resign = function (state, current) { function make_siege_view() { let list = {} - for (let t of TOWNLIST) - if (is_under_siege(t)) - list[t] = besieging_player(t) + for (let town = first_town; town <= last_town; ++town) + if (is_under_siege(town)) + list[town] = besieging_player(town) return list } @@ -3815,7 +3776,7 @@ exports.view = function(state, current) { f_card: (game.show_cards || current === FRANKS) ? game.f_card : 0, s_card: (game.show_cards || current === SARACENS) ? game.s_card : 0, hand: (current === FRANKS) ? game.f_hand : (current === SARACENS) ? game.s_hand : observer_hand(), - who: (game.active === current) ? game.who : null, + who: (game.active === current) ? game.who : NOBODY, location: game.location, castle: game.castle, steps: game.steps, @@ -3823,7 +3784,6 @@ exports.view = function(state, current) { sieges: make_siege_view(), battle: null, prompt: null, - actions: null, } if (game.jihad && game.jihad !== game.p1) @@ -3838,3 +3798,146 @@ exports.view = function(state, current) { return view } + + +// === COMMON LIBRARY === + +// remove item at index (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 + return array +} + +// insert item at index (faster than splice) +function array_insert(array, index, item) { + for (let i = array.length; i > index; --i) + array[i] = array[i - 1] + array[index] = item + return 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 set + } + 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 + return array_remove(set, m) + } + return set +} + +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 + return array_remove(set, m) + } + return array_insert(set, a, item) +} + +// 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 + } +} + +function clear_undo() { + if (game.undo.length > 0) + game.undo = [] +} + +function push_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() { + 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 +} |