diff options
Diffstat (limited to 'rules.js')
-rw-r--r-- | rules.js | 863 |
1 files changed, 483 insertions, 380 deletions
@@ -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 +} |