"use strict" // TODO: search_path with actual unit speed // TODO: partial moves during regroup (to allow deciding entry hex-side) // TODO: first_friendly_unit / for_each_friendly_unit // RULES: can refuse battle from hex with mixed undisrupted and disrupted units? // assume yes, followed by rout // RULES: disrupted units routed again in second enemy turn, will they still recover? // assume yes, easy to change (remove from game.recover set if routed) // unit state: location (8 bits), supply source (3 bits), steps (2 bits), disrupted (1 bit) // SEQUENCE // -------- // Supply check // Turn option // Movement // declare moves // normal moves // rout // retreats // declare full/partial retreats // probe combat // pursuit fire // withdraw // rout // pursuit fire // withdraw // forced marches // rout // pursuit fire // withdraw // refuse battle // pursuit fire // withdraw // rout // pursuit fire // withdraw // Combat // declare active // declare assault // resolve // defensive fire // rout // offensive fire // rout // Blitz Movement // Blitz Combat // Final supply check // rout // Reveal supply cards -> next player const max = Math.max const min = Math.min const abs = Math.abs var states = {} var game = null var view = null let { all_hexes, hex_exists, hex_road, side_road, side_limit, hex_name, units, regions } = require("./data") const first_axis_unit = 0 const first_allied_unit = units.findIndex(item => item.nationality === 'allied') const last_axis_unit = first_allied_unit - 1 const last_allied_unit = units.length - 1 function debug_hexes3(n, list) { console.log("--", n, "--") list = list.map((x,i) => hex_exists[i] ? x : "") for (let y = 0; y < hexh; ++y) console.log("".padStart(y*2," ") + list.slice(y*hexw, (y+1)*hexw).map(x=>String(x).padStart(3, ' ')).join(" ")) } function debug_hexes2(n, list) { console.log("--", n, "--") list = list.map((x,i) => hex_exists[i] ? x : "") for (let y = 0; y < hexh; ++y) console.log("".padStart(y*2," ") + list.slice(y*hexw, (y+1)*hexw).map(x=>String(x).padStart(3, ' ')).join(" ")) } function debug_hexes(n, list) { console.log("--", n, "--") list = list.map((x,i) => hex_exists[i] ? x : "") for (let y = 0; y < hexh; ++y) console.log("".padStart(y," ") + list.slice(y*hexw, (y+1)*hexw).map(x=>String(x).padStart(2, ' ')).join("")) } // Card deck has 42 cards, of which 28 are supply cards, and 14 are dummy cards. // Represent draw pile and hands as [ dummy_supply_count, real_supply_count ] const REAL_SUPPLY_COUNT = 28 const DUMMY_SUPPLY_COUNT = 14 const hexw = 25 const hexh = 9 const first_hex = 7 const last_hex = 215 const hexdeploy = hexw * hexh const hexnext = [ 1, hexw, hexw-1, -1, -hexw, -(hexw-1) ] const hexcount = last_hex + 1 const sidecount = hexcount * 3 const AXIS = 'Axis' const ALLIED = 'Allied' const firepower_name = [ "0", "1", "2", "3", "TF", "DF", "SF" ] const speed_name = [ "zero", "leg", "motorized", "mechanized", "recon" ] const SF = 6 const DF = 5 const TF = 4 const class_name = [ "armor", "infantry", "anti-tank", "artillery" ] const ARMOR = 0 const INFANTRY = 1 const ANTITANK = 2 const ARTILLERY = 3 const FIREPOWER_MATRIX = [ [ SF, DF, SF, TF ], [ SF, SF, SF, TF ], [ DF, SF, SF, TF ], [ SF, DF, DF, SF ], ] const CLEAR = 2 const PASS = 1 const ROUGH = 0 const NO_ROAD = 0 const TRAIL = 1 const TRACK = 2 const HIGHWAY = 4 const SUPPLY_RANGE = [ 1, 2, 3, -1, 3 ] const EL_AGHEILA = 151 const ALEXANDRIA = 74 const BENGHAZI = 54 const TOBRUK = 37 const BARDIA = 40 const FT_CAPUZZO = 64 const MERSA_BREGA = 152 const BARDIA_FT_CAPUZZO = 122 const hex_from_supply_source = [ 0, EL_AGHEILA, ALEXANDRIA, BARDIA, BENGHAZI, TOBRUK ] function supply_source_from_hex(hex) { switch (hex) { case 0: return 0 case EL_AGHEILA: return 1 case ALEXANDRIA: return 2 case BARDIA: return 3 case BENGHAZI: return 4 case TOBRUK: return 5 } } const region_egypt = regions["Egypt"] const region_el_agheila = regions["El Agheila"] const region_tobruk = regions["Tobruk"] const region_egypt_and_libya = regions["Libya"].concat(regions["Egypt"]) const region_libya_and_sidi_omar = regions["Libya"].concat(regions["Sidi Omar"]) const region_libya_and_sidi_omar_and_sollum = regions["Libya"].concat(regions["Sidi Omar"]).concat(regions["Sollum"]) const region_egypt_and_tobruk = regions["Egypt"].concat(regions["Tobruk"]) const region_libya_except_tobruk = regions["Libya"].filter(r => r !== TOBRUK) function calc_distance(a, b) { let ax = a % hexw, ay = (a / hexw)|0, az = -ax - ay let bx = b % hexw, by = (b / hexw)|0, bz = -bx - by return max(abs(bx-ax), abs(by-ay), abs(bz-az)) } function calc_distance_map(supply) { let map = new Array(hexcount) for (let x = 0; x < hexcount; ++x) map[x] = calc_distance(supply, x) return map } const distance_to = { [EL_AGHEILA]: calc_distance_map(EL_AGHEILA), [ALEXANDRIA]: calc_distance_map(ALEXANDRIA), [BENGHAZI]: calc_distance_map(BENGHAZI), [TOBRUK]: calc_distance_map(TOBRUK), [BARDIA]: calc_distance_map(BARDIA), } function to_side(a, b, s) { if (s < 3) return a * 3 + s return b * 3 + s - 3 } function to_side_id(a, b) { if (a > b) { let c = b b = a a = c } if (a + hexnext[0] === b) return a * 3 + 0 else if (a + hexnext[1] === b) return a * 3 + 1 else if (a + hexnext[2] === b) return a * 3 + 2 throw new Error("not a hexside " + a + " to " + b); } function is_map_hex(x) { return x >= first_hex && x <= last_hex && hex_exists[x] === 1 } function is_hex_or_adjacent_to(x, where) { if (x === where) return true for (let s = 0; s < 6; ++s) if (x === where + hexnext[s]) return true return false } // === UNIT STATE === const UNIT_DISRUPTED_SHIFT = 0 const UNIT_DISRUPTED_MASK = 1 << UNIT_DISRUPTED_SHIFT const UNIT_STEPS_SHIFT = 1 const UNIT_STEPS_MASK = 3 << UNIT_STEPS_SHIFT const UNIT_SUPPLY_SHIFT = 3 const UNIT_SUPPLY_MASK = 7 << UNIT_SUPPLY_SHIFT const UNIT_HEX_SHIFT = 6 const UNIT_HEX_MASK = 255 << UNIT_HEX_SHIFT function is_unit_disrupted(u) { return (game.units[u] & UNIT_DISRUPTED_MASK) === UNIT_DISRUPTED_MASK } function set_unit_disrupted(u) { game.units[u] |= UNIT_DISRUPTED_MASK } function clear_unit_disrupted(u) { game.units[u] &= ~UNIT_DISRUPTED_MASK } function unit_hex(u) { return (game.units[u] & UNIT_HEX_MASK) >> UNIT_HEX_SHIFT } function set_unit_hex(u, x) { game.units[u] = (game.units[u] & ~UNIT_HEX_MASK) | (x << UNIT_HEX_SHIFT) } function is_unit_supplied(u) { return ((game.units[u] & UNIT_SUPPLY_MASK) >> UNIT_SUPPLY_SHIFT) !== 0 } function is_unit_unsupplied(u) { return ((game.units[u] & UNIT_SUPPLY_MASK) >> UNIT_SUPPLY_SHIFT) === 0 } function unit_supply(u) { let src = (game.units[u] & UNIT_SUPPLY_MASK) >> UNIT_SUPPLY_SHIFT return hex_from_supply_source[src] } function set_unit_supply(u, hex) { let src = supply_source_from_hex(hex) game.units[u] = (game.units[u] & ~UNIT_SUPPLY_MASK) | (src << UNIT_SUPPLY_SHIFT) } function unit_lost_steps(u) { return (game.units[u] & UNIT_STEPS_MASK) >> UNIT_STEPS_SHIFT } function set_unit_lost_steps(u, n) { game.units[u] = (game.units[u] & ~UNIT_STEPS_MASK) | (n << UNIT_STEPS_SHIFT) } function unit_steps(u) { return units[u].steps - unit_lost_steps(u) } function set_unit_steps(u, n) { set_unit_lost_steps(u, units[u].steps - n) } function is_unit_moved(u) { return set_has(game.moved, u) } function set_unit_moved(u) { set_add(game.moved, u) } function is_unit_fired(u) { return set_has(game.fired, u) } function set_unit_fired(u) { set_add(game.fired, u) } function eliminate_unit(u) { set_unit_hex(u, 0) set_unit_lost_steps(u, 0) } function reduce_unit(u) { let s = unit_steps(u) let hp = unit_hp_per_step(u) if (s === 1) eliminate_unit(u) else set_unit_steps(u, s - 1) return hp } // === UNIT DATA === function find_unit(name) { for (let u = 0; u < units.length; ++u) if (units[u].name === name) return u throw new Error("cannot find named block: " + name) } function unit_name(u) { return units[u].name } function unit_speed(u) { return units[u].speed } function unit_class(u) { return units[u].class } function is_artillery_unit(u) { return units[u].class === ARTILLERY } function is_armor_unit(u) { return units[u].class === ARMOR } function is_infantry_unit(u) { return units[u].class === INFANTRY } function is_antitank_unit(u) { return units[u].class === ANTITANK } function is_unit_elite(u) { return units[u].elite } function unit_cv(u) { if (is_unit_elite(u)) return unit_steps(u) * 2 return unit_steps(u) } function unit_hp_per_step(u) { // TODO: double defense, minefields, etc return is_unit_elite(u) ? 2 : 1 } function unit_hp(u) { return unit_steps(u) * unit_hp_per_step(u) } function is_friendly_unit(u) { if (game.active === AXIS) return is_axis_unit(u) return is_allied_unit(u) } function is_enemy_unit(u) { if (game.active === ALLIED) return is_axis_unit(u) return is_allied_unit(u) } function is_friendly_hex(x) { if (game.active === AXIS) return is_axis_hex(x) return is_allied_hex(x) } function is_enemy_hex(x) { if (game.active === ALLIED) return is_axis_hex(x) return is_allied_hex(x) } function is_allied_unit(u) { return units[u].nationality === 'allied' } function is_axis_unit(u) { return units[u].nationality !== 'allied' } // === MAP STATE === function has_axis_unit(x) { for (let u = first_axis_unit; u <= last_axis_unit; ++u) if (unit_hex(u) === x) return true return false } function has_allied_unit(x) { for (let u = first_allied_unit; u <= last_allied_unit; ++u) if (unit_hex(u) === x) return true return false } function has_undisrupted_axis_unit(x) { for (let u = first_axis_unit; u <= last_axis_unit; ++u) if (!is_unit_disrupted(u) && unit_hex(u) === x) return true return false } function has_undisrupted_allied_unit(x) { for (let u = first_allied_unit; u <= last_allied_unit; ++u) if (!is_unit_disrupted(u) && unit_hex(u) === x) return true return false } function is_axis_hex(x) { let has_axis = has_axis_unit(x) let has_allied = has_allied_unit(x) return has_axis && !has_allied } function is_allied_hex(x) { let has_axis = has_axis_unit(x) let has_allied = has_allied_unit(x) return !has_axis && has_allied } function is_battle_hex(x) { let has_axis = has_axis_unit(x) let has_allied = has_allied_unit(x) return has_axis && has_allied } function is_empty_hex(x) { let has_axis = has_axis_unit(x) let has_allied = has_allied_unit(x) return !has_axis && !has_allied } function has_friendly_unit(x) { if (game.active === AXIS) return has_axis_unit(x) return has_allied_unit(x) } function has_undisrupted_friendly_unit(x) { if (game.active === AXIS) return has_undisrupted_axis_unit(x) return has_undisrupted_allied_unit(x) } function has_enemy_unit(x) { if (game.active === ALLIED) return has_axis_unit(x) return has_allied_unit(x) } function has_undisrupted_enemy_unit(x) { if (game.active === ALLIED) return has_undisrupted_axis_unit(x) return has_undisrupted_allied_unit(x) } function is_new_battle_hex(a) { if (is_battle_hex(a)) return !set_has(game.axis_hexes, a) && !set_has(game.allied_hexes, a) return false } function claim_hexside_control(side) { if (game.active === AXIS) { set_add(game.axis_sides, side) set_delete(game.allied_sides, side) } else { set_add(game.allied_sides, side) set_delete(game.axis_sides, side) } } function release_hex_control(a) { // no longer a battle hex: release hexsides if possible set_delete(game.axis_hexes, a) set_delete(game.allied_hexes, a) for_each_adjacent_hex(a, b => { if (!is_battle_hex(b)) { let side = to_side_id(a, b) set_delete(game.axis_sides, side) set_delete(game.allied_sides, side) } }) } function claim_hex_control_for_defender(a) { // a new battle hex: claim hex and hexsides for defender if (game.active === AXIS) set_add(game.allied_hexes, a) else set_add(game.axis_hexes, a) for_each_adjacent_hex(a, b => { let side = to_side_id(a, b) if (side_limit[side] > 0) { if (game.active === AXIS) { if (!set_has(game.axis_sides, side)) set_add(game.allied_sides, side) } else { if (!set_has(game.allied_sides, side)) set_add(game.axis_sides, side) } } }) } // === ITERATORS === function for_each_adjacent_hex(here, fn) { for (let s = 0; s < 6; ++s) { let next = here + hexnext[s] if (is_map_hex(next)) fn(next) } } function for_each_hex_and_adjacent_hex(here, fn) { fn(here) for (let s = 0; s < 6; ++s) { let next = here + hexnext[s] if (is_map_hex(next)) fn(next) } } function for_each_friendly_unit(fn) { // TODO: first/last_enemy_unit for (let u = 0; u < units.length; ++u) if (is_friendly_unit(u)) fn(u) } function for_each_enemy_unit(fn) { // TODO: first/last_enemy_unit for (let u = 0; u < units.length; ++u) if (is_enemy_unit(u)) fn(u) } function for_each_friendly_unit_in_hex(x, fn) { // TODO: first/last_enemy_unit for (let u = 0; u < units.length; ++u) if (is_friendly_unit(u) && unit_hex(u) === x) fn(u) } function for_each_undisrupted_friendly_unit_in_hex(x, fn) { // TODO: first/last_enemy_unit for (let u = 0; u < units.length; ++u) if (is_friendly_unit(u) && !is_unit_disrupted(u) && unit_hex(u) === x) fn(u) } function for_each_enemy_unit_in_hex(x, fn) { // TODO: first/last_enemy_unit for (let u = 0; u < units.length; ++u) if (is_enemy_unit(u) && unit_hex(u) === x) fn(u) } function for_each_undisrupted_enemy_unit_in_hex(x, fn) { // TODO: first/last_enemy_unit for (let u = 0; u < units.length; ++u) if (is_enemy_unit(u) && !is_unit_disrupted(u) && unit_hex(u) === x) fn(u) } function count_battle_hexes() { let n = 0 for (let x of all_hexes) if (is_battle_hex(x)) ++n return n } function count_normal_steps_in_battle() { let steps = [ 0, 0, 0, 0 ] for_each_undisrupted_enemy_unit_in_hex(game.battle, u => { if (!is_unit_elite(u)) steps[unit_class(u)] += unit_steps(u) }) return steps } function count_elite_steps_in_battle() { let steps = [ 0, 0, 0, 0 ] for_each_undisrupted_enemy_unit_in_hex(game.battle, u => { if (is_unit_elite(u)) steps[unit_class(u)] += unit_steps(u) }) return steps } function count_hp_in_battle() { let hp = [ 0, 0, 0, 0 ] for_each_undisrupted_enemy_unit_in_hex(game.battle, u => { hp[unit_class(u)] += unit_hp(u) }) return hp } function count_hp_in_battle_of_class(tc) { let hp = 0 for_each_undisrupted_enemy_unit_in_hex(game.battle, u => { if (unit_class(u) === tc) hp += unit_hp(u) }) return hp } function count_normal_steps_in_pursuit() { let steps = 0 for_each_undisrupted_enemy_unit_in_hex(game.pursuit, u => { if (!is_unit_elite(u)) steps += unit_steps(u) }) return steps } function count_elite_steps_in_pursuit() { let steps = 0 for_each_undisrupted_enemy_unit_in_hex(game.pursuit, u => { if (is_unit_elite(u)) steps += unit_steps(u) }) return steps } function count_hp_in_pursuit() { let hp = 0 for_each_undisrupted_enemy_unit_in_hex(game.pursuit, u => { hp += unit_hp(u) }) return hp } function count_normal_steps_in_rout() { let steps = 0 for_each_enemy_unit_in_hex(game.pursuit, u => { if (!is_unit_elite(u)) steps += unit_steps(u) }) return steps } function count_elite_steps_in_rout() { let steps = 0 for_each_enemy_unit_in_hex(game.pursuit, u => { if (is_unit_elite(u)) steps += unit_steps(u) }) return steps } function count_hp_in_rout() { let hp = 0 for_each_enemy_unit_in_hex(game.pursuit, u => { hp += unit_hp(u) }) return hp } // === SUPPLY CARDS === function draw_supply_card(pile) { let x = random(pile[0] + pile[1]) if (x < pile[0]) { pile[0] -- return 0 } else { pile[1] -- return 1 } } function deal_axis_supply_cards(n) { for (let i = 0; i < n; ++i) game.axis_hand[draw_supply_card(game.draw_pile)]++ } function deal_allied_supply_cards(n) { for (let i = 0; i < n; ++i) game.allied_hand[draw_supply_card(game.draw_pile)]++ } // === SUPPLY NETWORK === function is_side_unit(side, u) { if (side === AXIS) return is_axis_unit(u) return is_allied_unit(u) } function list_control_hexes(side) { let control = new Array(hexcount).fill(0) for (let u = 0; u < units.length; ++u) { if (is_side_unit(side, u)) { let x = unit_hex(u) if (x > 0 && x < hexdeploy) control[x] = 1 } } return control } function ind(d, msg, here, ...extra) { console.log(new Array(d).fill("-").join("") + msg, here, "("+hex_name[here]+")", extra.join(" ")) } var supply_visited, supply_friendly, supply_enemy, supply_src, supply_net, supply_line var trace_total var trace_highway var trace_chain function trace_supply_highway(here, d) { trace_highway++ ind(d, "> highway", here) // TODO: hoist to call sites to avoid function call overhead if (supply_src[here]) { ind(d, "! source highway", here) return true } let has_supply = false supply_visited[here] = 1 for (let s = 0; s < 6; ++s) { let next = here + hexnext[s] let side = to_side(here, next, s) if (side_limit[side] === 0) continue if (supply_visited[next]) continue if (supply_enemy[next]) continue let road = side_road[side] if (road === HIGHWAY) { if (supply_friendly[next]) { ind(d, "? highway head", next) if (trace_supply_chain(next, d+1, 0, 3)) { ind(d, "< highway chain", here, next) supply_line[side] = 1 has_supply = true } } else { if (trace_supply_highway(next, d+1)) { ind(d, "< highway", here, next) supply_line[side] = 1 has_supply = true } } } } supply_visited[here] = 0 if (has_supply) { supply_net[here] = 1 supply_src[here] = 1 } return has_supply } function trace_supply_chain(here, d, n, range) { ind(d, "> chain", here, n, range) trace_chain++ if (supply_src[here]) { ind(d, "! source chain", here) return true } let has_supply = false supply_visited[here] = 1 for (let s = 0; s < 6; ++s) { let next = here + hexnext[s] let side = to_side(here, next, s) if (side_limit[side] === 0) continue if (supply_visited[next]) continue if (supply_enemy[next]) continue let road = side_road[side] if (road === HIGHWAY) { ind(d, "? chain highway", next) if (supply_friendly[next]) { ind(d, "? chain highway head", next) if (trace_supply_chain(next, d+1, 0, 3)) { ind(d, "< highway chain", here, next) supply_line[side] = 1 has_supply = true } } else { if (trace_supply_highway(next, d+1)) { ind(d, "< chain highway", here, next) supply_line[side] = 1 has_supply = true } } } else { let next_range = min(range, SUPPLY_RANGE[road]) if (n + 1 <= next_range) { if (supply_friendly[next]) { ind(d, "? chain head", next) if (trace_supply_chain(next, d+1, 0, 3)) { ind(d, "< highway chain", here, next) supply_line[side] = 1 has_supply = true } } else { if (trace_supply_chain(next, d+1, n+1, next_range)) { ind(d, "< chain trail", here, next_range) supply_line[side] = 1 has_supply = true } } } } } supply_visited[here] = 0 if (has_supply) { supply_net[here] = 1 if (supply_friendly[here]) supply_src[here] = 1 } return has_supply } var supply_visited = new Array(hexcount) var supply_src = new Array(hexcount) function trace_supply_network(start) { supply_net = new Array(hexcount) supply_line = new Array(sidecount) supply_visited.fill(0) supply_src.fill(0) supply_net.fill(0) supply_line.fill(0) supply_src[start] = 1 supply_net[start] = 1 console.log("=== SUPPLY NETWORK ===") // debug_hexes("FH", supply_friendly) // debug_hexes("EH", supply_enemy) // debug_hexes("SS1", supply_src) trace_total = 0 for (let x = first_hex; x <= last_hex; ++x) { if (supply_friendly[x]) { trace_highway = trace_chain = 0 ind(0, "START", x) trace_supply_chain(x, 0, 0, 3) console.log("END", trace_highway, trace_chain) trace_total += trace_highway + trace_chain } } console.log("VISITS", trace_total) debug_hexes("SS", supply_src) debug_hexes("SN", supply_net) } function update_axis_supply_network() { supply_friendly = list_control_hexes(AXIS) supply_enemy = list_control_hexes(ALLIED) trace_supply_network(EL_AGHEILA) game.axis_supply = supply_net game.axis_supply_line = supply_line } function update_allied_supply_network() { supply_friendly = list_control_hexes(ALLIED) supply_enemy = list_control_hexes(AXIS) trace_supply_network(ALEXANDRIA) game.allied_supply = supply_net game.allied_supply_line = supply_line } function update_supply_networks() { update_axis_supply_network() update_allied_supply_network() } function clear_supply_networks() { game.axis_supply = null game.axis_supply_line = null game.allied_supply = null game.allied_supply_line = null } // === PATHING === const path_from = [ new Array(hexcount), new Array(hexcount), new Array(hexcount), null, new Array(hexcount) ] const path_cost = [ new Array(hexcount), new Array(hexcount), new Array(hexcount), null, new Array(hexcount) ] const path_valid = new Array(hexcount) const path_enemy = new Array(hexcount) function print_path(who, from, to, road) { let p = [ hex_name[to] ] while (to && to !== from) { to = path_from[road][to] p.unshift(hex_name[to]) } log(">" + p.join(" - ") + ".") } function search_move(start, start_cost, start_road) { // recon=4, forced march=+1, rommel bonus=+1 let limit = 6 path_enemy.fill(0) for (let u = 0; u < units.length; ++u) { if (is_enemy_unit(u)) { let x = unit_hex(u) if (x >= first_hex && x <= last_hex) path_enemy[x] = 1 } } search_move_bfs(path_from[0], path_cost[0], start, start_cost, 0, limit) if (start_road >= 1) search_move_bfs(path_from[1], path_cost[1], start, start_cost, 1, limit + 1) if (start_road >= 2) search_move_bfs(path_from[2], path_cost[2], start, start_cost, 2, limit + 2) if (start_road >= 4) search_move_bfs(path_from[4], path_cost[4], start, start_cost, 4, limit + 4) let grid = new Array(hexcount).fill('-') for (let x = first_hex; x <= last_hex; ++x) { for (let speed = 4; speed >= 1; --speed) { if (path_cost[0][x] <= speed) grid[x] = speed if (path_cost[1][x] <= speed + 1) grid[x] = speed if (path_cost[2][x] <= speed + 2) grid[x] = speed if (path_cost[4][x] <= speed + 4) grid[x] = speed } } grid[start] = '@' debug_hexes2("reach", path_cost[0]) } // Breadth First Search function search_move_bfs(from, cost, start, start_cost, road, max_cost) { let queue = [ start << 4 | start_cost ] from.fill(0) cost.fill(15) cost[start] = start_cost while (queue.length > 0) { let item = queue.shift() let here = item >> 4 let here_cost = item & 15 let next_cost = here_cost + 1 for (let s = 0; s < 6; ++s) { let next = here + hexnext[s] // can't go off-map if (next < first_hex || next > last_hex || !hex_exists[next]) continue // already seen if (cost[next] < 15) continue let side = to_side(here, next, s) let max_side = side_limit[side] // can't cross this hexside if (max_side === 0) continue // must stay on road for current bonus if (side_road[side] < road) continue // check hexside limit if (path_enemy[next] && (game.side_limit[side] | 0) >= max_side) continue from[next] = here cost[next] = next_cost // must stop if (path_enemy[next]) continue // enough movement allowance to keep going if (next_cost < max_cost) queue.push(next << 4 | next_cost) } } } function search_withdraw(start, is_pass_move) { // recon=4, forced march=+1, rommel bonus=+1 let limit = is_pass_move ? 6 : 4 path_enemy.fill(0) for (let u = 0; u < units.length; ++u) { if (is_enemy_unit(u)) { let x = unit_hex(u) if (x >= first_hex && x <= last_hex) path_enemy[x] = 1 } } let sline = game.active === AXIS ? game.axis_supply_line : game.allied_supply_line let sdist = game.active === AXIS ? distance_to[EL_AGHEILA] : distance_to[ALEXANDRIA] search_withdraw_bfs(path_from[0], path_cost[0], start, 0, limit, sline, sdist) search_withdraw_bfs(path_from[1], path_cost[1], start, 1, limit + 1, sline, sdist) search_withdraw_bfs(path_from[2], path_cost[2], start, 2, limit + 2, sline, sdist) search_withdraw_bfs(path_from[4], path_cost[4], start, 4, limit + 4, sline, sdist) let grid = new Array(hexcount).fill('-') for (let x = first_hex; x <= last_hex; ++x) { for (let speed = 4; speed >= 1; --speed) { if (path_cost[0][x] <= speed) grid[x] = speed if (path_cost[1][x] <= speed + 1) grid[x] = speed if (path_cost[2][x] <= speed + 2) grid[x] = speed if (path_cost[4][x] <= speed + 4) grid[x] = speed } } grid[start] = '@' debug_hexes2("reach", path_cost[0]) } // Breadth First Search function search_withdraw_bfs(from, cost, start, road, max_cost, sline, sdist) { let queue = [ start << 4 ] from.fill(0) cost.fill(15) cost[start] = 0 while (queue.length > 0) { let item = queue.shift() let here = item >> 4 let here_cost = item & 15 let next_cost = here_cost + 1 for (let s = 0; s < 6; ++s) { let next = here + hexnext[s] // can't go off-map if (next < first_hex || next > last_hex || !hex_exists[next]) continue // already seen if (cost[next] < 15) continue let side = to_side(here, next, s) let max_side = side_limit[side] // must follow supply line if (sline[side] === 0) continue // may not increase distance to supply source (except Bardia/Ft. Capuzzo) if (sdist[next] > sdist[here] && side !== BARDIA_FT_CAPUZZO) continue // must stay on road for current bonus if (side_road[side] < road) continue // cannot enter enemy hex if (path_enemy[next]) continue from[next] = here cost[next] = next_cost // enough movement allowance to keep going if (next_cost < max_cost) queue.push(next << 4 | next_cost) } } } // TODO: search_retreat where first hexside exit is restricted to friendly function can_move_to(to, road, speed) { // TODO: engagement & hexside limit if (road >= 4 && path_cost[4][to] <= speed + 4) return true if (road >= 2 && path_cost[2][to] <= speed + 2) return true if (road >= 1 && path_cost[1][to] <= speed + 1) return true if (path_cost[0][to] <= speed) return true return false } function can_move_from(from, road, speed) { // TODO: engagement & hexside limit return can_move_to(from, road, speed) } function can_force_march_to(to, road, speed) { if (road >= 4 && path_cost[4][to] <= speed + 5) return true if (road >= 2 && path_cost[2][to] <= speed + 3) return true if (road >= 1 && path_cost[1][to] <= speed + 2) return true if (path_cost[0][to] <= speed + 1) return true return false } function pick_path(to, road, speed) { let next_cost = 15, next_road = 0 if (path_cost[0][to] <= speed) { next_cost = path_cost[0][to] next_road = 0 } if (road >= 1 && path_cost[1][to] <= speed + 1) { if (path_cost[1][to] <= next_cost) { next_cost = path_cost[1][to] next_road = 1 } } if (road >= 2 && path_cost[2][to] <= speed + 2) { if (path_cost[2][to] <= next_cost) { next_cost = path_cost[2][to] next_road = 2 } } if (road >= 4 && path_cost[4][to] <= speed + 4) { if (path_cost[4][to] <= next_cost) { next_cost = path_cost[4][to] next_road = 4 } } return next_road } function adjacent_hex_has_undisrupted_friendly_unit(here) { for (let s = 0; s < 6; ++s) { let next = here + hexnext[s] if (is_map_hex(next)) if (has_undisrupted_friendly_unit(next)) return true } return false } function max_speed_of_undisrupted_friendly_unit_in_hex(from) { let max_speed = 0 for_each_undisrupted_friendly_unit_in_hex(from, u => { let s = unit_speed(u) if (s > max_speed) max_speed = s }) return max_speed } function find_valid_regroup_destinations(from, rommel) { let speed = max_speed_of_undisrupted_friendly_unit_in_hex(from) if (speed > 0) { search_move(from, 0, 4) for (let x = first_hex; x <= last_hex; ++x) if (!path_valid[x]) if (can_move_to(x, 4, speed + rommel)) path_valid[x] = 1 } } // === TURN === function set_active_player() { game.active = game.phasing } function set_passive_player() { if (game.phasing === AXIS) game.active = ALLIED else game.active = AXIS } function set_enemy_player() { if (game.active === AXIS) game.active = ALLIED else game.active = AXIS } function end_player_turn() { // TODO: end when both pass if (game.phasing === AXIS) game.phasing = ALLIED else game.phasing = AXIS set_active_player() goto_player_turn() } function goto_player_turn() { log_h2(game.phasing) // paranoid resetting of state game.side_limit = {} game.rommel = 0 game.from1 = game.from2 = 0 game.to1 = game.to2 = 0 goto_initial_supply_check() } function goto_initial_supply_check() { update_supply_networks() let snet = game.phasing === AXIS ? game.axis_supply : game.allied_supply let ssrc = game.phasing === AXIS ? EL_AGHEILA : ALEXANDRIA for_each_friendly_unit(u => { let x = unit_hex(u) if (snet[x]) { set_unit_supply(u, ssrc) if (is_unit_disrupted(u) && set_has(game.recover, u) && !is_battle_hex(x)) { log(`Recovered at #${x}`) set_delete(game.recover, u) clear_unit_disrupted(u) } } }) set_clear(game.recover) for_each_enemy_unit(u => { if (is_unit_disrupted(u)) set_add(game.recover, u) }) goto_turn_option() } function clear_all_unit_moved() { game.moved.length = 0 } function goto_turn_option() { game.state = 'turn_option' clear_all_unit_moved() } states.turn_option = { inactive: "turn option", prompt() { view.prompt = "Select Turn Option" gen_action('basic') gen_action('offensive') gen_action('assault') gen_action('blitz') gen_action('pass') }, basic() { push_undo() game.turn_option = 'basic' goto_move_phase() }, offensive() { push_undo() game.turn_option = 'offensive' goto_move_phase() }, assault() { push_undo() game.turn_option = 'assault' goto_move_phase() }, blitz() { push_undo() game.turn_option = 'blitz' goto_move_phase() }, pass() { push_undo() game.turn_option = 'pass' goto_move_phase() }, } // ==== MOVEMENT PHASE === function goto_move_phase() { game.state = 'select_moves' if (game.phasing === AXIS) { // Automatically select Rommel Move for 1-move turn options if (game.turn_option !== 'offensive' && game.turn_option !== 'blitz' && game.scenario !== "1940") game.rommel = 1 } } states.select_moves = { inactive: "move phase", prompt() { if (game.turn_option === 'offensive') { if (game.from1) view.prompt = `Designate second offensive move.` else view.prompt = `Designate first offensive move.` } else { view.prompt = `Designate ${game.turn_option} move.` } gen_action('group') gen_action('regroup') if (game.turn_option === 'pass') gen_action('end_turn') }, group() { push_undo() game.state = 'group_move_from' }, regroup() { push_undo() game.state = 'regroup_move_command_point' }, end_turn() { clear_undo() end_player_turn() } } function gen_rommel_move() { if (game.phasing === AXIS && game.scenario !== "1940") view.actions.rommel = game.rommel ? 0 : 1 } states.group_move_from = { inactive: "group move (from)", prompt() { view.prompt = `Group Move: Select hex to move from.` gen_rommel_move() for (let x = first_hex; x <= last_hex; ++x) { if (x === game.from1 && !game.to1) continue if (has_undisrupted_friendly_unit(x)) gen_action_hex(x) } }, rommel() { push_undo() if (game.from1 === 0) game.rommel = 1 else game.rommel = 2 }, hex(x) { push_undo() if (game.from1 === 0) game.from1 = x else game.from2 = x if (game.turn_option === 'offensive' && !game.from2) game.state = 'select_moves' else goto_move_who() }, } states.regroup_move_command_point = { inactive: "regroup move (command point)", prompt() { view.prompt = `Regroup Move: Designate the command point hex.` gen_rommel_move() for (let x = first_hex; x <= last_hex; ++x) { if (!is_enemy_hex(x)) { if (has_undisrupted_friendly_unit(x) || adjacent_hex_has_undisrupted_friendly_unit(x)) gen_action_hex(x) } } }, rommel() { push_undo() if (game.from1 === 0) game.rommel = 1 else game.rommel = 2 }, hex(x) { push_undo() if (game.from1 === 0) game.from1 = x else game.from2 = x game.state = 'regroup_move_destination' }, } states.regroup_move_destination = { inactive: "regroup move (destination)", prompt() { view.prompt = `Regroup Move: Select destination hex.` gen_rommel_move() let cp, rommel = false if (game.from2 === 0) cp = game.from1, rommel = (game.rommel === 1 ? 1 : 0) else cp = game.from2, rommel = (game.rommel === 2 ? 1 : 0) path_valid.fill(0) for_each_hex_and_adjacent_hex(cp, x => { find_valid_regroup_destinations(x, rommel) }) for (let x = first_hex; x <= last_hex; ++x) if (path_valid[x]) gen_action_hex(x) }, rommel() { push_undo() if (game.from2 === 0) game.rommel = 1 else game.rommel = 2 }, hex(x) { push_undo() if (game.from2 === 0) game.to1 = x else game.to2 = x if (game.turn_option === 'offensive' && !game.from2) game.state = 'select_moves' else goto_move_who() }, } function goto_move_who() { if (game.rommel === 1) { if (game.from1 && game.to1) log(`Regroup move from #${game.from1} to #${game.to1} (Rommel).`) else if (game.from1) log(`Group move from #${game.from1} (Rommel).`) } else { if (game.from1 && game.to1) log(`Regroup move\nfrom #${game.from1}\nto #${game.to1}.`) else if (game.from1) log(`Group move from #${game.from1}.`) } if (game.rommel === 2) { if (game.from2 && game.to2) log(`Regroup move from #${game.from2} to #${game.to2} (Rommel).`) else if (game.from2) log(`Group move from #${game.from2} (Rommel).`) } else { if (game.from2 && game.to2) log(`Regroup move from #${game.from2} to #${game.to2}.`) else if (game.from2) log(`Group move from #${game.from2}.`) } log_br() log("Moved:") game.state = 'move_who' } function gen_group_move_who(may_retreat, from) { if (may_retreat !== is_battle_hex(from)) return for_each_undisrupted_friendly_unit_in_hex(from, u => { if (!is_unit_moved(u)) gen_action_unit(u) }) } function gen_regroup_move_who(may_retreat, command_point, destination, rommel) { search_move(destination, 0, 4) for_each_hex_and_adjacent_hex(command_point, x => { if (x !== destination) { if (may_retreat !== is_battle_hex(x)) return for_each_undisrupted_friendly_unit_in_hex(x, u => { if (!is_unit_moved(u) && can_move_from(x, 4, unit_speed(u) + rommel)) gen_action_unit(u) }) } }) } states.move_who = { inactive: "move (who)", prompt() { view.prompt = `Move: Select unit to move.` if (game.from1) { if (game.to1) gen_regroup_move_who(false, game.from1, game.to1, game.rommel === 1 ? 1 : 0) else gen_group_move_who(false, game.from1) } if (game.from2) { if (game.to2) gen_regroup_move_who(false, game.from2, game.to2, game.rommel === 2 ? 1 : 0) else gen_group_move_who(false, game.from2) } // TODO: only retreat if possible gen_action('retreat') gen_action('end_move') }, unit(who) { push_undo() game.selected = [ who ] game.state = 'move_to' game.move_used = 0 game.move_road = 4 }, retreat() { push_undo() log_br() game.state = 'retreat_select_from' }, end_move() { clear_undo() log_br() end_move_phase() } } function end_move_phase() { game.side_limit = {} game.rommel = 0 game.from1 = game.from2 = game.to1 = game.to2 = 0 // TODO: forced marches goto_refuse_battle() } function apply_move(move, who, from, to) { let speed = unit_speed(who) + (move === game.rommel ? 1 : 0) let road = pick_path(to, game.move_road, speed) print_path(who, unit_hex(who), to, road) game.move_road = road game.move_used = path_cost[road][to] set_unit_moved(who) set_unit_hex(who, to) if (is_battle_hex(to)) { let side = to_side_id(to, path_from[road][to]) if (game.side_limit[side]) game.side_limit[side] = 2 else game.side_limit[side] = 1 claim_hexside_control(side) if (is_new_battle_hex(to)) { claim_hex_control_for_defender(to) set_add(game.active_battles, to) } return true } if (game.move_used === speed + game.move_road) return true return false } function unit_speed_1(who) { return unit_speed(who) + (game.rommel === 1 ? 1 : 0) } function unit_speed_2(who) { return unit_speed(who) + (game.rommel === 2 ? 1 : 0) } function can_move_regroup_1(who, from, to) { if (to === game.to1 && is_hex_or_adjacent_to(from, game.from1)) if (can_move_to(game.to1, 4, unit_speed_1(who))) return true return false } function can_move_regroup_2(who, from, to) { if (to === game.to2 && is_hex_or_adjacent_to(from, game.from2)) if (can_move_to(game.to2, 4, unit_speed_2(who))) return true return false } function can_move_group_1(who, from, to) { if (from === game.from1 && !game.to1) if (can_move_to(to, game.move_road, unit_speed_1(who))) return true return false } function can_move_group_2(who, from, to) { if (from === game.from2 && !game.to2) if (can_move_to(to, game.move_road, unit_speed_2(who))) return true return false } states.move_to = { inactive: "move (to)", prompt() { view.prompt = `Move: Select where to move.` let who = game.selected[0] let from = unit_hex(who) search_move(from, 0, 4) if (from === game.from1 && !game.to1) for (let to of all_hexes) if (to != from && can_move_group_1(who, from, to)) gen_action_hex(to) if (from === game.from2 && !game.to2) for (let to of all_hexes) if (to != from && can_move_group_2(who, from, to)) gen_action_hex(to) if (can_move_regroup_1(who, from, game.to1)) gen_action_hex(game.to1) if (can_move_regroup_2(who, from, game.to2)) gen_action_hex(game.to2) gen_action_unit(who) }, unit(who) { pop_undo() }, hex(to) { push_undo() let who = game.selected[0] let from = unit_hex(who) search_move(from, 0, 4) if (can_move_group_1(who, from, to)) { game.move_from = from if (apply_move(1, who, from, to)) stop_move(who) else game.state = 'group_move_to' return } if (can_move_group_2(who, from, to)) { game.move_from = from if (apply_move(2, who, from, to)) stop_move(who) else game.state = 'group_move_to' return } if (can_move_regroup_1(who, from, to)) { apply_move(1, who, from, to) stop_move(who) game.state = 'move_who' return } if (can_move_regroup_2(who, from, to)) { apply_move(2, who, from, to) stop_move(who) game.state = 'move_who' return } }, } states.group_move_to = { inactive: "group move (to)", prompt() { view.prompt = `Group Move: Select where to move.` let who = game.selected[0] let from = unit_hex(who) if (game.move_used > 0) gen_action('stop') search_move(from, game.move_used, game.move_road) if (game.move_from === game.from1 && !game.to1) for (let to of all_hexes) if (to != from && can_move_group_1(who, game.move_from, to)) gen_action_hex(to) if (game.move_from === game.from2 && !game.to2) for (let to of all_hexes) if (to != from && can_move_group_2(who, game.move_from, to)) gen_action_hex(to) gen_action_unit(who) }, unit(who) { this.stop() }, hex(to) { let who = game.selected[0] let from = unit_hex(who) search_move(from, game.move_used, game.move_road) if (can_move_group_1(who, game.move_from, to)) { log(`group continued ${who} to ${to}`) if (apply_move(1, who, from, to)) stop_move(who) return } if (can_move_group_2(who, game.move_from, to)) { log(`group continued ${who} to ${to}`) if (apply_move(2, who, from, to)) stop_move(who) return } }, stop() { let who = game.selected[0] stop_move(who) } } function stop_move(who) { set_unit_moved(who) game.move_from = 0 game.move_road = 4 game.move_used = 0 game.selected = [] game.state = 'move_who' } // === RETREAT === function can_select_retreat_hex() { let result = false if (game.from1) { if (game.to1) { for_each_hex_and_adjacent_hex(game.from1, x => { if (is_battle_hex(x) && !set_has(game.partial_retreats, x)) result = true }) } else { if (is_battle_hex(game.from1) && !set_has(game.partial_retreats, game.from1)) result = true } } if (game.from2) { if (game.to2) { for_each_hex_and_adjacent_hex(game.from2, x => { if (is_battle_hex(x) && !set_has(game.partial_retreats, x)) result = true }) } else { if (is_battle_hex(game.from2) && !set_has(game.partial_retreats, game.from2)) result = true } } return result } states.retreat_select_from = { prompt() { view.prompt = `Retreat: Select hex to retreat from.` if (game.from1) { if (game.to1) { for_each_hex_and_adjacent_hex(game.from1, x => { if (is_battle_hex(x) && !set_has(game.partial_retreats, x)) gen_action_hex(x) }) } else { if (is_battle_hex(game.from1) && !set_has(game.partial_retreats, game.from1)) gen_action_hex(game.from1) } } if (game.from2) { if (game.to2) { for_each_hex_and_adjacent_hex(game.from2, x => { if (is_battle_hex(x) && !set_has(game.partial_retreats, x)) gen_action_hex(x) }) } else { if (is_battle_hex(game.from2) && !set_has(game.partial_retreats, game.from2)) gen_action_hex(game.from2) } } }, hex(x) { push_undo() game.retreat = x game.state = 'retreat_select_who' } } states.retreat_select_who = { prompt() { view.prompt = `Retreat: Select unit to move.` let full_retreat = true for_each_undisrupted_friendly_unit_in_hex(game.retreat, u => { if (!set_has(game.selected, u)) full_retreat = false gen_action_unit(u) }) // No Partial Retreat allowed for Pass turn option. if (game.turn_option !== 'pass' && game.selected.length > 0) gen_action('partial_retreat') gen_action('full_retreat') }, unit(u) { console.log("set toggle", u) set_toggle(game.selected, u) }, full_retreat() { clear_undo() for_each_undisrupted_friendly_unit_in_hex(game.retreat, u => { set_add(game.selected, u) }) game.retreat_units = game.selected game.selected = [] goto_pursuit_fire_during_retreat(game.retreat) }, partial_retreat() { clear_undo() set_add(game.partial_retreats, game.retreat) set_passive_player() game.state = 'provoke_probe_combat' }, } states.provoke_probe_combat = { prompt() { view.prompt = `Retreat: You may provoke probe combat at ${hex_name[game.retreat]}.` gen_action('probe_combat') gen_action('pass') }, // TODO: probe_combat pass() { set_active_player() game.retreat_units = game.selected game.selected = [] let shielded = false for_each_undisrupted_friendly_unit_in_hex(game.retreat, u => { if (!set_has(game.retreat_units, u)) shielded = true }) if (shielded) goto_retreat_who() else goto_pursuit_fire_during_retreat(game.retreat) }, } function goto_retreat_who() { set_active_player() game.state = 'retreat_who' } states.retreat_who = { prompt() { view.prompt = `Retreat: Select unit to move.` let done = true for (let u of game.retreat_units) { if (unit_hex(u) === game.retreat) { gen_action_unit(u) done = false } } if (done) gen_action('end_retreat') }, unit(who) { push_undo() game.selected = [ who ] game.state = 'retreat_to' }, end_retreat() { clear_undo() if (can_select_retreat_hex()) game.state = 'retreat_select_from' else end_move_phase() } } states.retreat_to = { prompt() { view.prompt = `Retreat: Select destination.` let who = game.selected[0] gen_action_unit(who) if (game.turn_option === 'pass') { search_withdraw(game.retreat, true) } else { search_retreat(game.retreat) } if (from === game.from1 && !game.to1) for (let to of all_hexes) if (to != from && can_move_group_1(who, from, to)) gen_action_hex(to) if (from === game.from2 && !game.to2) for (let to of all_hexes) if (to != from && can_move_group_2(who, from, to)) gen_action_hex(to) if (can_move_regroup_1(who, from, game.to1)) gen_action_hex(game.to1) if (can_move_regroup_2(who, from, game.to2)) gen_action_hex(game.to2) }, unit(who) { pop_undo() }, hex(to) { push_undo() let who = game.selected[0] set_unit_hex(who, to) set_unit_disrupted(who) game.selected = [] game.state = 'retreat_who' }, } // === REFUSE BATTLE === function gen_withdraw_group_move(who, from) { for (let to of all_hexes) if (to != from && can_move_to(to, 4, unit_speed(who))) gen_action_hex(to) } function goto_refuse_battle() { clear_undo() if (game.active_battles.length > 0) { set_passive_player() game.state = 'refuse_battle' } else { goto_combat_phase() } } states.refuse_battle = { inactive: "refuse battle", prompt() { view.prompt = `You may Refuse Battle.` for (let x of game.active_battles) // TODO: check if there are possible retreat paths gen_action_hex(x) gen_action('next') }, hex(x) { push_undo() log_h3(`Refused battle at #${x}`) set_delete(game.active_battles, x) goto_pursuit_fire_during_refuse_battle(x) }, next() { goto_combat_phase() } } states.refuse_battle_who = { inactive: "refuse battle (withdraw group move: who)", prompt() { view.prompt = `Withdraw: Select unit to move.` let done = true for_each_undisrupted_friendly_unit_in_hex(game.from1, u => { gen_action_unit(u) done = false }) if (done) gen_action('end_retreat') }, unit(u) { push_undo() game.selected = [ u ] game.state = 'refuse_battle_to' }, end_retreat() { clear_undo() release_hex_control(game.from1) game.from1 = 0 goto_refuse_battle() } } states.refuse_battle_to = { inactive: "refuse battle (withdraw group move: to)", prompt() { view.prompt = `Withdraw: Select destination.` let who = game.selected[0] update_supply_networks() search_withdraw(game.from1, false) gen_withdraw_group_move(who, game.from1) gen_action_unit(who) }, unit(who) { pop_undo() }, hex(to) { let who = game.selected[0] log(`>to #${to}`) set_unit_hex(who, to) set_unit_disrupted(who) game.selected = [] game.state = 'refuse_battle_who' }, } // ==== COMBAT PHASE === function goto_combat_phase() { clear_undo() set_active_player() let n = count_battle_hexes() if (n > 0) { if (n > game.active_battles.length) return game.state = 'select_active_battles' if (game.turn_option === 'assault') return game.state = 'select_assault_battles' return goto_select_battle() } end_combat_phase() } states.select_active_battles = { inactive: "combat phase (select active battles)", prompt() { view.prompt = `Select active battles.` view.active_battles = game.active_battles for (let x of all_hexes) if (!set_has(game.active_battles, x) && is_battle_hex(x)) gen_action_hex(x) gen_action('next') }, hex(x) { push_undo() set_add(game.active_battles, x) }, next() { push_undo() if (game.active_battles.length > 0) { if (game.turn_option === 'assault') game.state = 'select_assault_battles' else game.state = 'select_battle' } else { end_combat_phase() } } } states.select_assault_battles = { inactive: "combat phase (select assault battles)", prompt() { view.prompt = `Select assault battles.` view.active_battles = game.active_battles view.assault_battles = game.assault_battles for (let x of game.active_battles) if (!set_has(game.assault_battles, x)) gen_action_hex(x) gen_action_next() }, hex(x) { push_undo() set_add(game.assault_battles, x) }, next() { push_undo() game.state = 'select_battle' } } function goto_select_battle() { if (game.active_battles.length > 0) { if (game.active_battles.length > 1) { game.state = 'select_battle' } else { goto_battle(game.active_battles[0]) } } else { end_combat_phase() } } states.select_battle = { inactive: "combat phase (select next battle)", prompt() { view.prompt = `Select next battle to resolve.` view.active_battles = game.active_battles view.assault_battles = game.assault_battles for (let x of game.active_battles) gen_action_hex(x) }, hex(x) { goto_battle(x) }, } function end_combat_phase() { // TODO: blitz // TODO: final supply check // TODO: supply cards revealed end_player_turn() } // === BATTLES === // Normal Battle: // passive fire // active hits // active fire // passive hits function roll_fire(who, fp, tc) { let roll = random(6) + 1 log(`${who} fired ${firepower_name[fp]} ${roll} at ${class_name[tc]}`) if (roll >= fp) return 1 return 0 } function goto_battle(x) { clear_undo() game.battle = x game.fired = [] // goto defensive fire set_passive_player() game.state = 'battle_fire' game.hits = [ 0, 0, 0, 0 ] } function apply_battle_fire(tc) { let firing = game.selected[0] game.selected.length = 0 let fp = FIREPOWER_MATRIX[unit_class(firing)][tc] let cv = unit_cv(firing) set_unit_fired(firing) for (let i = 0; i < cv; ++i) game.hits[tc] += roll_fire(firing, fp, tc) // clamp to available hit points game.hits[tc] = min(game.hits[tc], count_hp_in_battle_of_class(tc)) // end firing when all done let done = true for_each_undisrupted_friendly_unit_in_hex(game.battle, u => { if (!is_unit_fired(u)) done = false }) if (done) { set_enemy_player() game.state = 'battle_hits' } } function gen_battle_fire() { let arty = false for_each_undisrupted_friendly_unit_in_hex(game.battle, u => { if (is_artillery_unit(u)) { if (!is_unit_fired(u)) { gen_action_unit(u) arty = true } } }) if (!arty) { for_each_undisrupted_friendly_unit_in_hex(game.battle, u => { if (!is_unit_fired(u)) gen_action_unit(u) }) } } function gen_battle_target() { let hp = count_hp_in_battle() for (let i = 0; i < 4; ++i) hp[i] -= game.hits[i] let who = game.selected[0] let fc = unit_class(who) gen_action_unit(who) // deselect // armor must target armor if possible if (fc === ARMOR && hp[ARMOR] > 0) { gen_action('armor') return } // infantry must target infantry if possible if (fc === INFANTRY && hp[INFANTRY] > 0) { gen_action('infantry') return } if (hp[ARMOR] > 0) gen_action('armor') if (hp[INFANTRY] > 0) gen_action('infantry') if (hp[ANTITANK] > 0) gen_action('antitank') // only artillery may target artillery if other units are alive if (hp[ARTILLERY] > 0) { if (fc === ARTILLERY || (hp[ARTILLERY] <= 0 && hp[INFANTRY] <= 0 && hp[ANTITANK] <= 0)) gen_action('artillery') } } function gen_battle_hits() { let normal_steps = count_normal_steps_in_battle() let elite_steps = count_elite_steps_in_battle() let done = true for_each_undisrupted_friendly_unit_in_hex(game.battle, u => { let c = unit_class(u) if (is_unit_elite(u)) { if (game.hits[c] >= 2) { gen_action_unit(u) done = false } } else { if (game.hits[c] >= 1) { // If mixed elite and non-elite: must assign ALL damage. if (elite_steps[c] > 0 && normal_steps[c] === 1 && (game.hits[c] & 1) === 0) { // Eliminating the last non-elite must not leave an odd // number of hits remaining. } else { gen_action_unit(u) done = false } } } }) if (done) gen_action_next() } function apply_battle_hit(who) { game.hits[unit_class(who)] -= reduce_unit(who) } states.battle_fire = { prompt() { if (game.active === game.phasing) view.prompt = `Offensive Fire!` else view.prompt = `Defensive Fire!` if (game.selected.length > 0) { gen_battle_target() } else { gen_battle_fire() } }, unit(who) { if (game.selected.length > 0) game.selected.length = 0 else game.selected.push(who) }, armor() { apply_battle_fire(ARMOR) }, infantry() { apply_battle_fire(INFANTRY) }, antitank() { apply_battle_fire(ANTITANK) }, artillery() { apply_battle_fire(ARTILLERY) }, } states.battle_hits = { prompt() { if (game.active === game.phasing) view.prompt = `Apply hits from Defensive Fire.` else view.prompt = `Apply hits from Offensive Fire.` view.prompt = `Apply hits.` gen_battle_hits() }, unit(who) { push_undo() apply_battle_hit(who) }, next() { clear_undo() if (game.active === game.phasing) { // goto offensive fire game.state = 'battle_fire' game.hits = [ 0, 0, 0, 0 ] } else { end_battle() } }, } function end_battle() { clear_undo() set_active_player() set_delete(game.active_battles, game.battle) set_delete(game.assault_battles, game.battle) game.battle = 0 if (game.active_battles.length > 0) game.state = 'select_battle' else end_combat_phase() } function end_probe() { game.battle = 0 resume_retreat() } // === PURSUIT FIRE === // Refuse battle // active pursuit fire // passive apply hits // passive moves // Retreat // passive pursuit fire // active apply hits // active moves // Rout // non-routing pursuit fire // routing apply hits // routing moves function goto_pursuit_fire_during_retreat(where) { clear_undo() set_passive_player() game.hits = 0 game.pursuit = where if (can_pursuit_fire()) game.state = 'pursuit_fire' else goto_pursuit_hits() } function goto_pursuit_fire_during_refuse_battle(where) { clear_undo() set_active_player() game.hits = 0 game.pursuit = where if (can_pursuit_fire(true)) game.state = 'pursuit_fire' else goto_pursuit_hits() } function goto_pursuit_hits() { set_enemy_player() if (game.hits > 0) { let hp = count_hp_in_pursuit() if (game.hits > hp) game.hits = hp game.state = 'pursuit_hits' } else { end_pursuit_fire() } } function slowest_undisrupted_enemy_unit_speed(where) { let r = 4 for_each_undisrupted_enemy_unit_in_hex(where, u => { let s = unit_speed(u) if (s < r) r = s }) return r } function can_pursuit_fire(verbose) { let result = false let slowest = slowest_undisrupted_enemy_unit_speed(game.pursuit) if (verbose) log(`Slowest enemy was ${speed_name[slowest]}.`) for_each_undisrupted_friendly_unit_in_hex(game.pursuit, u => { if (unit_speed(u) >= slowest && !is_unit_fired(u)) result = true }) return result } function roll_pursuit_fire(who, n) { if (n === 2) { let a = random(6) + 1 let b = random(6) + 1 log(`>%${who} pursuit fired ${a}, ${b}.`) if (a >= 4) game.hits++ if (b >= 4) game.hits++ } if (n === 1) { let a = random(6) + 1 log(`>%${who} pursuit fired ${a}.`) if (a >= 4) game.hits++ } let hp = count_hp_in_pursuit() if (game.hits > hp) game.hits = hp return game.hits === hp } states.pursuit_fire = { inactive: "pursuit fire (fire)", prompt() { view.prompt = `Pursuit Fire.` let slowest = slowest_undisrupted_enemy_unit_speed(game.pursuit) for_each_undisrupted_friendly_unit_in_hex(game.pursuit, u => { if (unit_speed(u) >= slowest && !is_unit_fired(u)) gen_action_unit(u) }) // TODO: only save fire if there are shielded enemy units? gen_action('next') }, unit(who) { let slowest = slowest_undisrupted_enemy_unit_speed(game.pursuit) set_unit_fired(who) let done = roll_pursuit_fire(who, (unit_speed(who) > slowest ? 2 : 1)) if (done || !can_pursuit_fire(false)) goto_pursuit_hits() }, next() { goto_pursuit_hits() } } states.pursuit_hits = { inactive: "pursuit fire (hits)", prompt() { view.prompt = `Pursuit Fire: Apply ${game.hits} hits.` let normal_steps = count_normal_steps_in_pursuit() let elite_steps = count_elite_steps_in_pursuit() let done = true for_each_undisrupted_friendly_unit_in_hex(game.pursuit, u => { if (is_unit_elite(u)) { if (game.hits >= 2) { gen_action_unit(u) done = false } } else { if (game.hits >= 1) { // If mixed elite and non-elite: must assign ALL damage. if (elite_steps > 0 && normal_steps === 1 && (game.hits & 1) === 0) { // Eliminating the last non-elite must not leave an odd // number of hits remaining. } else { gen_action_unit(u) done = false } } } }) if (done) gen_action('next') }, unit(who) { push_undo() game.hits -= reduce_unit(who) }, next() { clear_undo() end_pursuit_fire() }, } function end_pursuit_fire() { game.from1 = game.pursuit game.pursuit = 0 if (game.retreat) { game.state = 'retreat_who' } else { game.state = 'refuse_battle_who' } } // === DEPLOYMENT === states.free_deployment = { inactive: "free deployment", prompt() { let scenario = SCENARIOS[game.scenario] let deploy = hexdeploy + scenario.start let done = true if (game.active === AXIS) view.prompt = "Axis Free Deployment" else view.prompt = "Allied Free Deployment" // TODO: first/last_axis_unit for (let u = 0; u < units.length; ++u) { if (is_friendly_unit(u)) { if (unit_hex(u) === deploy) { done = false gen_action_unit(u) } } } if (game.selected.length > 0) { let list if (game.active === AXIS) list = scenario.axis_deployment else list = scenario.allied_deployment // TODO: scenario.deployment_limit for (let i = 0; i < list.length; ++i) if (!is_enemy_hex(list[i])) gen_action_hex(list[i]) } if (done) gen_action_next() }, unit(u) { if (set_has(game.selected, u)) set_delete(game.selected, u) else set_add(game.selected, u) }, hex(x) { push_undo() log(`Deployed ${game.selected.length} at #${x}.`) for (let i = 0; i < game.selected.length; ++i) { let u = game.selected[i] set_unit_hex(u, x) } set_clear(game.selected) }, next() { clear_undo() if (game.active === AXIS) { game.active = ALLIED log_h2("Allied Deployment") } else { end_free_deployment() } } } function end_free_deployment() { game.phasing = game.first_player_turn game.active = game.phasing let scenario = SCENARIOS[game.scenario] deal_axis_supply_cards(scenario.axis_initial_supply) deal_allied_supply_cards(scenario.allied_initial_supply) // TODO: mulligan log_br() log_h1(`Month ${game.month}`) log_br() // No buildup first month // No initiative first month goto_player_turn() } // === SETUP === function find_axis_units(a) { let list = [] for (let u = 0; u < units.length; ++u) if (units[u].nationality !== 'allied' && units[u].appearance === a) list.push(u) return list } function find_allied_units(a) { let list = [] for (let u = 0; u < units.length; ++u) if (units[u].nationality === 'allied' && units[u].appearance === a) list.push(u) return list } function setup_reinforcements(m) { for (let u = 0; u < units.length; ++u) { if (units[u].appearance === m) { if (m === 'M') set_unit_hex(u, 4) else set_unit_hex(u, hexdeploy + (m > 10 ? m - 10 : m)) } } } function setup_units(where, steps, list) { if (where < 0) where = hexdeploy - where for (let u of list) { if (typeof u === 'string') u = find_unit(u) set_unit_hex(u, where) if (steps < 0) set_unit_steps(u, units[u].steps + steps) else if (steps > 0) set_unit_steps(u, steps) } } const SCENARIOS = { "1940": { year: 1940, start: 1, end: 6, axis_deployment: region_libya_and_sidi_omar, allied_deployment: region_egypt, axis_initial_supply: 6, allied_initial_supply: 3, special: { no_rommel_bonus: true, only_one_die_for_buildup: true, } }, "1941": { year: 1941, start: 1, end: 10, axis_deployment: [], allied_deployment: region_egypt_and_libya, axis_initial_supply: 6, allied_initial_supply: 6, }, "Crusader": { year: 1941, start: 8, end: 10, axis_deployment: region_libya_and_sidi_omar_and_sollum, allied_deployment: region_egypt_and_tobruk, axis_initial_supply: 10, allied_initial_supply: 12, deployment_limit: { [TOBRUK]: 5, }, special: { allies_first_turn: true, tobruk_minefield: true, } }, "Battleaxe": { year: 1941, start: 4, end: 10, axis_deployment: region_libya_except_tobruk, allied_deployment: region_egypt_and_tobruk, axis_initial_supply: 4, allied_initial_supply: 8, deployment_limit: { [TOBRUK]: 5, }, }, "1942": { year: 1942, start: 11, end: 20, axis_deployment: [ EL_AGHEILA, MERSA_BREGA ], allied_deployment: region_egypt_and_libya, axis_initial_supply: 5, allied_initial_supply: 5, deployment_limit: { [EL_AGHEILA]: 14, [MERSA_BREGA]: 10, }, }, "Gazala": { year: 1942, start: 14, end: 15, axis_deployment: regions["West Line"], allied_deployment: regions["East Line"], axis_initial_supply: 10, allied_initial_supply: 12, special: { gazala_pre_build: true, } }, "Pursuit to Alamein": { year: 1942, start: 15, end: 20, axis_deployment: regions["Libya"], allied_deployment: regions["Egypt"], axis_initial_supply: 8, allied_initial_supply: 8, }, "1941-42": { year: 1941, start: 1, end: 20, axis_deployment: [], allied_deployment: region_egypt_and_libya, axis_initial_supply: 6, allied_initial_supply: 6, }, } const SETUP = { "1940" (DEPLOY) { setup_units(DEPLOY, 0, [ "Pav", "Bre", "Tre", "Bol", "Sav", "Sab", "Fas", "Ita" ]) setup_units(-3, 0, [ "Ari", "Pis"]) setup_units(-5, 0, [ "Lit", "Cen"]) setup_units(DEPLOY, 0, [ "7/7", "7", "7/SG", "4IN/3m" ]) setup_units(-2, 0, [ "4IN/5", "4IN/11" ]) setup_units(-4, 0, [ "Matilda/A", "7/4", "4IN/7m", "/Tob" ]) }, "1941" (DEPLOY) { setup_units(EL_AGHEILA, 0, find_axis_units('S')) setup_reinforcements(3) setup_reinforcements(5) setup_reinforcements(7) setup_units(DEPLOY, 0, find_allied_units('S')) setup_units(TOBRUK, 0, find_allied_units('T')) setup_reinforcements(2) setup_reinforcements(4) setup_reinforcements(6) setup_reinforcements(8) setup_reinforcements(10) }, "Crusader" (DEPLOY) { setup_units(DEPLOY, 0, find_axis_units('S')) setup_units(DEPLOY, 0, find_axis_units(3)) setup_units(DEPLOY, 0, find_axis_units(5)) setup_units(DEPLOY, 0, find_axis_units(7)) setup_units(DEPLOY, 0, find_allied_units('S')) setup_units(DEPLOY, 0, find_allied_units('T')) setup_units(DEPLOY, 0, find_allied_units(2)) setup_units(DEPLOY, 0, find_allied_units(4)) setup_units(DEPLOY, 0, find_allied_units(6)) setup_units(DEPLOY, 0, find_allied_units(8)) setup_units(0, 0, [ "2/3", "2/SG", "9AU/20", "7AU/18" ]) setup_reinforcements(10) }, "Battleaxe" (DEPLOY) { setup_units(DEPLOY, 0, [ "21/5", "21/3", "15/33", "90/155", "15/115", "88mm/A" ]) setup_units(DEPLOY, -1, [ "21/104" ]) setup_units(DEPLOY, 0, [ "Ari", "Lit", "Pav", "Bre", "Tre", "Bol", "Ita" ]) setup_reinforcements(5) setup_reinforcements(7) setup_units(DEPLOY, 0, [ "4IN/3m", "9AU/20", "70/14+16", "70/23", "7/22G", "7/SG", "7AU/18", "Matilda/A", "/Tob", "/Pol", "7/7", "4IN/5", "4IN/7m", "4IN/11", "Matilda/B", "/1AT", "7/4", "7" ]) setup_reinforcements(6) setup_reinforcements(8) setup_reinforcements(10) }, "1942" (DEPLOY) { setup_units(DEPLOY, 0, [ "21/3", "15/33", "90/580", "90/sv288", "90/346", "88mm/A", ]) setup_units(DEPLOY, 1, [ "21/5", "15/8", "15/115", "90/155" ]) setup_units(DEPLOY, 1, [ "90/361", "90/200", "88mm/B", "50mm", "/104" ]) setup_units(DEPLOY, 0, [ "Lit" ]) setup_units(DEPLOY, -1, [ "Ari", "Tri", "Pav", "Bre", "Tre", "Bol", "Sab", "Ita" ]) setup_reinforcements(17) setup_reinforcements(19) setup_reinforcements('M') setup_units(TOBRUK, 0, [ "1/22", "/1AT", "Matilda/B", "1SA/2+5", "70/14+16", "1SA/1", "1SA/3", "2SA/4+6", "70/23", "/Tob", ]) setup_units(ALEXANDRIA, 1, [ "7/4", "/32AT", "2NZ/4", "2NZ/5", "2NZ/6", "7/SG", "4IN/5", "/Pol", ]) setup_reinforcements(12) setup_reinforcements(14) setup_reinforcements(16) setup_reinforcements(18) setup_reinforcements(20) }, "Gazala" (DEPLOY) { setup_units(DEPLOY, -1, [ "21/5", "15/8", "15/115", "90/155", "88mm/B", "50mm", "/104", ]) setup_units(DEPLOY, 0, [ "21/3", "15/33", "90/580", "90/sv288", "90/346", "90/361", "90/200", "88mm/A", ]) setup_units(DEPLOY, 0, [ "Lit" ]) setup_units(DEPLOY, -1, [ "Ari", "Tri", "Pav", "Bre", "Tre", "Bol", "Sab", "Ita" ]) setup_units(DEPLOY, 0, [ "Grant", "/1AT", "1/22", "Matilda/B", "1SA", "1/201G", "4IN/7m", "4IN/3m", "1/SG", "1SA/2+5", "70/14+16", "1SA/1", "1SA/3", "2SA/4+6", "70/23", "4IN/11", "5IN/29", "6#/A", "2#", "/Tob", ]) setup_units(ALEXANDRIA, 1, [ "1/2", "7/4", "/32AT", "2NZ/4", "2NZ/5", "2NZ/6", "7/SG", "7/22G", "4IN/5", "/Pol", ]) setup_units(ALEXANDRIA, 0, [ "10IN/161m", "8IN/18", "/B", "FF/2", "5IN/9+10", "10IN/21+25", ]) }, "Pursuit to Alamein" (DEPLOY) { setup_units(DEPLOY, 0, [ "21/3", "15/33", ]) setup_units(DEPLOY, -1, [ "21/5", "15/8", "15/115", "90/346", "90/sv288", "90/361", "90/200", "88mm/A", "88mm/B", "50mm", ]) setup_units(DEPLOY, 3, [ "Ari", "Tri", "Pav", "Bre", "Tre", ]) setup_units(DEPLOY, 2, [ "Lit", "Bol", ]) setup_units(DEPLOY, 1, [ "Sab", "Ita", ]) setup_reinforcements(17) setup_reinforcements(19) setup_reinforcements('M') setup_units(DEPLOY, 0, [ "10IN/161m", "10IN/21+25", "70/14+16", "8IN/18", "B", ]) setup_units(DEPLOY, -1, [ "1/22", "7/4", "2NZ/4", "2NZ/5", "2NZ/6", "1SA/2+5", "1SA/1", "1SA/3", "70/23", "5IN/29", "6#/A", ]) setup_units(ALEXANDRIA, 1, [ "Grant", "1/2", "/1AT", "1SA", "7/SG", "1/SG", "FF/2", "5IN/9+10", "4IN/5", "4IN/11", "/Pol", "2#", ]) setup_reinforcements(16) setup_reinforcements(18) setup_reinforcements(20) }, "1941-42" (DEPLOY) { SETUP["1941"](-1) setup_reinforcements(11) setup_reinforcements(17) setup_reinforcements(19) setup_reinforcements('M') setup_reinforcements(12) setup_reinforcements(14) setup_reinforcements(16) setup_reinforcements(18) setup_reinforcements(20) }, } function setup_fortress(scenario, fortress) { if (scenario.allied_deployment.includes(fortress)) return ALLIED if (scenario.axis_deployment.includes(fortress)) return AXIS throw new Error("invalid setup") } function setup(name) { let scenario = SCENARIOS[name] game.month = scenario.start log_h1(name) SETUP[name](-scenario.start) game.bardia = setup_fortress(scenario, BARDIA) game.benghazi = setup_fortress(scenario, BENGHAZI) game.tobruk = setup_fortress(scenario, TOBRUK) log_h2("Axis Deployment") game.active = 'Axis' game.state = 'free_deployment' game.selected = [] } // === PUBLIC FUNCTIONS === exports.roles = [ "Axis", "Allied" ] exports.scenarios = Object.keys(SCENARIOS) exports.setup = function (seed, scenario, options) { game = { seed: seed, log: [], undo: [], state: null, phasing: AXIS, active: AXIS, selected: null, scenario: scenario, month: 0, first_player_turn: AXIS, draw_pile: [ DUMMY_SUPPLY_COUNT, REAL_SUPPLY_COUNT ], axis_hand: [ 0, 0 ], allied_hand: [ 0, 0 ], units: new Array(units.length).fill(0), moved: [], fired: [], recover: [], axis_minefields: [], allied_minefields: [], // fortress control bardia: ALLIED, benghazi: ALLIED, tobruk: ALLIED, // supply networks axis_supply: null, axis_supply_line: null, allied_supply: null, allied_supply_line: null, // battle hexes (defender) axis_hexes: [], allied_hexes: [], // hexside control (for battle hexes) axis_sides: [], allied_sides: [], // current turn option and selected moves turn_option: null, side_limit: {}, rommel: 0, from1: 0, to1: 0, from2: 0, to2: 0, // current group move state move_from: 0, move_used: 0, move_road: 4, // retreat partial_retreats: [], // remember partial retreats to forbid initiating combat retreat: 0, retreat_units: null, // combat active_battles: [], assault_battles: [], pursuit: 0, battle: 0, hits: null, flash: null, } setup(scenario) return game } exports.view = function(state, current) { game = state // update_supply_networks() view = { month: game.month, units: game.units, moved: game.moved, axis_hexes: game.axis_hexes, allied_hexes: game.allied_hexes, axis_sides: game.axis_sides, allied_sides: game.allied_sides, selected: game.selected, // axis_supply: game.axis_supply, // axis_supply_line: game.axis_supply_line, // allied_supply: game.allied_supply, // allied_supply_line: game.allied_supply_line, } if (game.rommel) view.rommel = game.rommel if (game.from1) view.from1 = game.from1 if (game.from2) view.from2 = game.from2 if (game.to1) view.to1 = game.to1 if (game.to2) view.to2 = game.to2 if (game.pursuit) view.pursuit = game.pursuit if (game.battle) view.battle = game.battle if (game.battle || game.pursuit) { view.hits = game.hits view.flash = game.flash } return common_view(current) } function gen_action_next() { gen_action('next') } function gen_action_unit(u) { gen_action('unit', u) } function gen_action_hex(x) { gen_action('hex', x) } // === COMMON TEMPLATE === function random(range) { // https://www.ams.org/journals/mcom/1999-68-225/S0025-5718-99-00996-5/S0025-5718-99-00996-5.pdf return (game.seed = game.seed * 200105 % 34359738337) % range } function shuffle(deck) { for (let i = deck.length - 1; i > 0; --i) { let j = random(i + 1) let tmp = deck[j] deck[j] = deck[i] deck[i] = tmp } } // Sorted array treated as Set (for JSON) function set_index(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 m } return -1 } function set_has(set, item) { return set_index(set, item) >= 0 } 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.splice(a, 0, item) } function set_delete(set, item) { let i = set_index(set, item) if (i >= 0) set.splice(i, 1) } function set_clear(set) { set.length = 0 } function set_toggle(set, item) { if (set_has(set, item)) set_delete(set, item) else set_add(set, item) } function remove_from_array(array, item) { let i = array.indexOf(item) if (i >= 0) 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 log_br() { if (game.log.length > 0 && game.log[game.log.length-1] !== "") game.log.push("") } function log(msg) { game.log.push(msg) } function log_h1(msg) { log_br() log(".h1 " + msg) log_br() } function log_h2(msg) { log_br() log(".h2 " + msg) log_br() } function log_h3(msg) { log_br() log(".h3 " + msg) log_br() } function gen_action(action, argument) { if (argument !== undefined) { if (!(action in view.actions)) { view.actions[action] = [ argument ] } else { if (!view.actions[action].includes(argument)) view.actions[action].push(argument) } } else { view.actions[action] = 1 } } function goto_game_over(result, victory) { game.state = 'game_over' game.active = "None" game.result = result game.victory = victory log_br() log(game.victory) } states.game_over = { get inactive() { return game.victory }, prompt() { view.prompt = game.victory } } exports.resign = function (state, current) { game = state if (game.state !== 'game_over') { for (let opponent of exports.roles) { if (opponent !== current) { goto_game_over(opponent, current + " resigned.") break } } } return game } exports.action = function (state, current, action, arg) { game = state // Object.seal(game) // XXX: don't allow adding properties let S = states[game.state] if (S && action in S) { S[action](arg, current) } else { if (action === 'undo' && game.undo && game.undo.length > 0) pop_undo() else throw new Error("Invalid action: " + action) } return game } function common_view(current) { view.log = game.log if (current === 'Observer' || game.active !== current) { let inactive = states[game.state].inactive || game.state view.prompt = `Waiting for ${game.active} \u2014 ${inactive}...` } else { view.actions = {} if (states[game.state]) states[game.state].prompt() else view.prompt = "Unknown state: " + game.state if (game.undo && game.undo.length > 0) view.actions.undo = 1 else view.actions.undo = 0 } return view }