"use strict" // TODO: select hexside for engaging // TODO: fortress supply // TODO: oasis supply // TODO: legal pass withdrawal moves (reduce supply net, withdraw from fortress attack) // TODO: raiders // TODO: MINEFIELDS // TODO: FORCED MARCHES // TODO: SUPPLY COMMITMENT // TODO: BUILDUP // TODO: setup scenario specials // TODO: when is "fired" status cleared? // 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) // RULES: reveal minefields moved through (but not stopped at)? const max = Math.max const min = Math.min const abs = Math.abs var states = {} var game = null var view = null var after_rout_table = {} const { all_hexes, hex_exists, hex_road, side_road, side_limit, hex_name, units, regions } = require("./data") 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("")) } const AXIS = 'Axis' const ALLIED = 'Allied' 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 class_name = [ "armor", "infantry", "anti-tank", "artillery" ] 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 ARMOR = 0 const INFANTRY = 1 const ANTITANK = 2 const ARTILLERY = 3 const TRAIL = 1 const TRACK = 2 const HIGHWAY = 4 const SUPPLY_RANGE = [ 1, 2, 3, -1, 3 ] const FIREPOWER_MATRIX = [ [ SF, DF, SF, TF ], [ SF, SF, SF, TF ], [ DF, SF, SF, TF ], [ SF, DF, DF, SF ], ] const EL_AGHEILA = 151 const ALEXANDRIA = 74 const BENGHAZI = 54 const TOBRUK = 37 const BARDIA = 40 const MERSA_BREGA = 152 const JALO_OASIS = 204 const JARABUB_OASIS = 187 const SIWA_OASIS = 213 const BARDIA_FT_CAPUZZO = 122 const SS_NONE = 0 const SS_EL_AGHEILA = 1 const SS_ALEXANDRIA = 2 const SS_BARDIA = 3 const SS_BENGHAZI = 4 const SS_TOBRUK = 5 const SS_OASIS = 6 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_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 } // === STATE CACHES === 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 var presence_invalid = true var presence_axis = new Array(hexcount).fill(0) var presence_allied = new Array(hexcount).fill(0) var supply_axis_invalid = true var supply_axis_network = new Array(hexcount).fill(0) var supply_axis_line = new Array(sidecount).fill(0) var supply_allied_invalid = true var supply_allied_network = new Array(hexcount).fill(0) var supply_allied_line = new Array(sidecount).fill(0) var first_friendly_unit, last_friendly_unit var first_enemy_unit, last_enemy_unit function update_aliases() { if (game.active === AXIS) { first_friendly_unit = first_axis_unit last_friendly_unit = last_axis_unit first_enemy_unit = first_allied_unit last_enemy_unit = last_allied_unit } else { first_friendly_unit = first_allied_unit last_friendly_unit = last_allied_unit first_enemy_unit = first_axis_unit last_enemy_unit = last_axis_unit } } function load_state(state) { if (game !== state) { game = state presence_invalid = true supply_axis_invalid = true supply_allied_invalid = true update_aliases() } } // === UNIT STATE === // location (8 bits), supply source (3 bits), steps lost (2 bits), disrupted (1 bit) function apply_select(u) { if (game.selected === u) game.selected = -1 else game.selected = u } function pop_selected() { let u = game.selected game.selected = -1 return u } 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) { presence_invalid = true supply_axis_invalid = true supply_allied_invalid = true game.units[u] |= UNIT_DISRUPTED_MASK } function clear_unit_disrupted(u) { presence_invalid = true supply_axis_invalid = true supply_allied_invalid = true 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) { presence_invalid = true supply_axis_invalid = true supply_allied_invalid = true 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) { return is_unit_elite(u) ? 2 : 1 } function unit_hp(u) { return unit_steps(u) * unit_hp_per_step(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 u >= first_allied_unit && u <= last_allied_unit } function is_axis_unit(u) { return u >= first_axis_unit && u <= last_axis_unit } // === MAP STATE === function update_presence() { console.log("UPDATE PRESENCE") presence_invalid = false presence_axis.fill(0) for (let u = first_axis_unit; u <= last_axis_unit; ++u) if (is_unit_disrupted(u)) presence_axis[unit_hex(u)] |= 1 else presence_axis[unit_hex(u)] |= 2 presence_allied.fill(0) for (let u = first_allied_unit; u <= last_allied_unit; ++u) if (is_unit_disrupted(u)) presence_allied[unit_hex(u)] |= 1 else presence_allied[unit_hex(u)] |= 2 } function has_axis_unit(x) { if (presence_invalid) update_presence() return presence_axis[x] !== 0 } function has_allied_unit(x) { if (presence_invalid) update_presence() return presence_allied[x] !== 0 } function has_undisrupted_axis_unit(x) { if (presence_invalid) update_presence() return (presence_axis[x] & 2) !== 0 } function has_disrupted_axis_unit(x) { if (presence_invalid) update_presence() return (presence_axis[x] & 1) !== 0 } function has_undisrupted_allied_unit(x) { if (presence_invalid) update_presence() return (presence_allied[x] & 2) !== 0 } function has_disrupted_allied_unit(x) { if (presence_invalid) update_presence() return (presence_allied[x] & 1) !== 0 } function has_unshielded_disrupted_axis_unit(x) { if (presence_invalid) update_presence() return presence_axis[x] === 1 } function has_unshielded_disrupted_allied_unit(x) { if (presence_invalid) update_presence() return presence_allied[x] === 1 } function is_axis_hex(x) { if (presence_invalid) update_presence() return (presence_axis[x] !== 0) && (presence_allied[x] === 0) } function is_allied_hex(x) { if (presence_invalid) update_presence() return (presence_axis[x] === 0) && (presence_allied[x] !== 0) } function is_battle_hex(x) { if (presence_invalid) update_presence() return (presence_axis[x] !== 0) && (presence_allied[x] !== 0) } function is_empty_hex(x) { if (presence_invalid) update_presence() return (presence_axis[x] === 0) && (presence_allied[x] === 0) } 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_disrupted_enemy_unit(x) { if (game.active === ALLIED) return has_disrupted_axis_unit(x) return has_disrupted_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 has_unshielded_disrupted_enemy_unit(x) { if (game.active === ALLIED) return has_unshielded_disrupted_axis_unit(x) return has_unshielded_disrupted_allied_unit(x) } function has_unshielded_disrupted_friendly_unit(x) { if (game.active === ALLIED) return has_unshielded_disrupted_allied_unit(x) return has_unshielded_disrupted_axis_unit(x) } function is_overrun_hex(x) { return has_undisrupted_friendly_unit(x) && has_unshielded_disrupted_enemy_unit(x) } function is_enemy_rout_hex(x) { return has_undisrupted_friendly_unit(x) && has_unshielded_disrupted_enemy_unit(x) } function is_friendly_rout_hex(x) { return has_undisrupted_enemy_unit(x) && has_unshielded_disrupted_friendly_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 (is_axis_player()) { 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 (is_axis_player()) 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 (is_axis_player()) { 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) } } }) } function capture_fortress(fortress, capacity, control_prop, captured_prop) { if (game[control_prop] !== game.active) { if (has_undisrupted_friendly_unit(fortress) && !has_enemy_unit(fortress)) { supply_axis_invalid = true supply_allied_invalid = true log(`Captured #${fortress}!`) game[control_prop] = game.active if (!game[captured_prop]) { game[captured_prop] = 1 if (is_axis_player()) { let award = capacity log(`Awarded ${award} supply cards.`) deal_axis_supply_cards(award) } else { let award = Math.floor(capacity / 2) log(`Awarded ${award} supply cards.`) deal_allied_supply_cards(award) } } } } } // === 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_axis_unit(fn) { for (let u = first_axis_unit; u <= last_axis_unit; ++u) fn(u) } function for_each_allied_unit(fn) { for (let u = first_axis_unit; u <= last_axis_unit; ++u) fn(u) } function for_each_friendly_unit(fn) { for (let u = first_friendly_unit; u <= last_friendly_unit; ++u) fn(u) } function for_each_enemy_unit(fn) { for (let u = first_enemy_unit; u <= last_enemy_unit; ++u) fn(u) } function for_each_friendly_unit_in_hex(x, fn) { for (let u = first_friendly_unit; u <= last_friendly_unit; ++u) if (unit_hex(u) === x) fn(u) } function for_each_undisrupted_friendly_unit_in_hex(x, fn) { for (let u = first_friendly_unit; u <= last_friendly_unit; ++u) if (!is_unit_disrupted(u) && unit_hex(u) === x) fn(u) } function for_each_undisrupted_and_unmoved_friendly_unit_in_hex(x, fn) { for (let u = first_friendly_unit; u <= last_friendly_unit; ++u) if (!is_unit_disrupted(u) && unit_hex(u) === x && !is_unit_moved(u)) fn(u) } function hex_or_adjacent_has_undisrupted_and_unmoved_friendly_unit(here) { for (let u = first_friendly_unit; u <= last_friendly_unit; ++u) if (!is_unit_disrupted(u) && !is_unit_moved(u) && is_hex_or_adjacent_to(unit_hex(u), here)) return true return false } function for_each_enemy_unit_in_hex(x, fn) { for (let u = first_enemy_unit; u <= last_enemy_unit; ++u) if (unit_hex(u) === x) fn(u) } function for_each_undisrupted_enemy_unit_in_hex(x, fn) { for (let u = first_enemy_unit; u <= last_enemy_unit; ++u) if (!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 has_friendly_units_in_battle() { let result = false for_each_undisrupted_friendly_unit_in_hex(game.battle, u => { if (!is_unit_retreating(u)) result = true }) return result } 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)) if (!is_unit_retreating(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)) if (!is_unit_retreating(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 => { if (!is_unit_retreating(u)) hp[unit_class(u)] += 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 ind(d, msg, here, ...extra) { console.log(new Array(d).fill("-").join("") + msg, here, "("+hex_name[here]+")", extra.join(" ")) } var supply_defender, supply_defender_sides, supply_friendly, supply_enemy, supply_net, supply_line var supply_visited = new Array(hexcount).fill(0) var supply_src = new Array(hexcount).fill(0) var trace_highway var trace_chain function is_supply_line_blocked(here, next, side) { // impassable hexside if (side_limit[side] === 0) return true // undisrupted (only) enemies may block supply lines if (supply_enemy[next] > 1) { if (supply_friendly[next] > 1) { // battle hex, can only trace through if defender if (!set_has(supply_defender, next)) return true } else { // enemy hex return true } } // cannot trace through enemy hexsides if (supply_friendly[here] && supply_enemy[here]) if (!set_has(supply_defender_sides, side)) return true return false } 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] if (supply_visited[next]) continue let side = to_side(here, next, s) if (is_supply_line_blocked(here, next, side)) continue let road = side_road[side] if (road === HIGHWAY) { if (supply_friendly[next] > 1) { 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 if (supply_enemy[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] if (supply_visited[next]) continue let side = to_side(here, next, s) if (is_supply_line_blocked(here, next, side)) continue let road = side_road[side] if (road === HIGHWAY) { ind(d, "? chain highway", next) if (supply_friendly[next] > 1) { 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] > 1) { 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 // undisrupted units can chain supply if (supply_friendly[here] > 1 && supply_enemy[here] <= 1) supply_src[here] = 1 } return has_supply } function trace_supply_network(start) { 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) var trace_total = 0 for (let x of all_hexes) { if (supply_friendly[x] > 0) { 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() { supply_axis_invalid = false if (presence_invalid) update_presence() supply_net = supply_axis_network supply_line = supply_axis_line supply_defender = game.axis_hexes supply_defender_sides = game.axis_sides supply_friendly = presence_axis supply_enemy = presence_allied trace_supply_network(EL_AGHEILA) } function update_allied_supply() { supply_allied_invalid = false if (presence_invalid) update_presence() supply_net = supply_allied_network supply_line = supply_allied_line supply_defender = game.allied_hexes supply_defender_sides = game.allied_sides supply_friendly = presence_allied supply_enemy = presence_axis trace_supply_network(ALEXANDRIA) } function update_supply() { if (supply_axis_invalid) update_axis_supply() if (supply_allied_invalid) update_allied_supply() } function axis_supply_line() { if (supply_axis_invalid) update_axis_supply() return supply_axis_line } function axis_supply_network() { if (supply_axis_invalid) update_axis_supply() return supply_axis_network } function allied_supply_line() { if (supply_allied_invalid) update_allied_supply() return supply_allied_line } function allied_supply_network() { if (supply_allied_invalid) update_allied_supply() return supply_allied_network } function unit_supply_line(who) { // TODO: fortress supply if (is_axis_unit(who)) return axis_supply_line() return allied_supply_line() } function unit_supply_distance(who) { // TODO: fortress supply if (is_axis_unit(who)) return distance_to[EL_AGHEILA] return distance_to[ALEXANDRIA] } function friendly_supply_base() { if (is_axis_player()) return EL_AGHEILA return ALEXANDRIA } function friendly_supply_network() { if (is_axis_player()) return axis_supply_network() return allied_supply_network() } // === 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) 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(" - ") + ".") } // normal move: may not leave battle hex. may engage any enemy. may move freely. // normal withdrawal: may not leave battle hex. may engage disrupted enemy. must follow supply lines. // retreat move: must leave battle hex via friendly side. may ignore disrupted enemy. may move freely. // retreat withdrawal: must leave battle hex via friendly side. may ignore disrupted enemy. must follow supply lines. // TODO: cache search results from previous invocation function search_move(start, speed) { // Normal moves. search_init() search_move_bfs(path_from[0], path_cost[0], start, 0, speed, false, null, null) search_move_bfs(path_from[1], path_cost[1], start, 1, speed + 1, false, null, null) search_move_bfs(path_from[2], path_cost[2], start, 2, speed + 2, false, null, null) search_move_bfs(path_from[4], path_cost[4], start, 4, speed + 4, false, null, null) } function search_move_retreat(start, speed) { search_move_bfs(path_from[0], path_cost[0], start, 0, speed, true, null, null) search_move_bfs(path_from[1], path_cost[1], start, 1, speed + 1, true, null, null) search_move_bfs(path_from[2], path_cost[2], start, 2, speed + 2, true, null, null) search_move_bfs(path_from[4], path_cost[4], start, 4, speed + 4, true, null, null) } function search_withdraw(who, bonus) { let sline = unit_supply_line(who) let sdist = unit_supply_distance(who) let speed = unit_speed(who) + bonus let start = unit_hex(who) search_move_bfs(path_from[0], path_cost[0], start, 0, speed, false, sline, sdist) search_move_bfs(path_from[1], path_cost[1], start, 1, speed + 1, false, sline, sdist) search_move_bfs(path_from[2], path_cost[2], start, 2, speed + 2, false, sline, sdist) search_move_bfs(path_from[4], path_cost[4], start, 4, speed + 4, false, sline, sdist) } function search_withdraw_retreat(who, bonus) { let sline = unit_supply_line(who) let sdist = unit_supply_distance(who) let speed = unit_speed(who) + bonus let start = unit_hex(who) search_move_bfs(path_from[0], path_cost[0], start, 0, speed, true, sline, sdist) search_move_bfs(path_from[1], path_cost[1], start, 1, speed + 1, true, sline, sdist) search_move_bfs(path_from[2], path_cost[2], start, 2, speed + 2, true, sline, sdist) search_move_bfs(path_from[4], path_cost[4], start, 4, speed + 4, true, sline, sdist) } function search_init() { } // Breadth First Search function search_move_bfs(from, cost, start, road, max_cost, retreat, sline, sdist) { let path_enemy, friendly_sides if (presence_invalid) update_presence() if (is_axis_player()) { path_enemy = presence_allied friendly_sides = game.axis_sides } else { path_enemy = presence_axis friendly_sides = game.allied_sides } from.fill(0) cost.fill(15) cost[start] = 0 if (hex_road[start] < road) return let queue = [ start << 4 ] 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 if (sline) { // 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 } let next_enemy = path_enemy[next] if (retreat) { // must cross friendly hex-side to disengage if (here === start && !set_has(friendly_sides, side)) continue // may only ignore unshielded disrupted units if (next_enemy & 2) // has undisrupted enemy continue } else { if (sline) { // may only engage unshielded disrupted units if (next_enemy & 2) // has undisrupted enemy continue } // check hexside limit when engaging enemy units if (next_enemy) if ((game.side_limit[side] | 0) >= max_side) continue } from[next] = here cost[next] = next_cost if (!retreat) { // must stop when engaging enemy units if (next_enemy) continue } // enough movement allowance to keep going if (next_cost < max_cost) queue.push(next << 4 | next_cost) } } } function can_move_to(to, speed) { if (path_cost[4][to] <= speed + 4) return true if (path_cost[2][to] <= speed + 2) return true if (path_cost[1][to] <= speed + 1) return true if (path_cost[0][to] <= speed) return true return false } function pick_path(to, speed) { let road = 4 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 max_speed_of_undisrupted_and_unmoved_friendly_unit_in_hex(from) { let max_speed = 0 for_each_undisrupted_and_unmoved_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) { // TODO: forced march let speed = max_speed_of_undisrupted_and_unmoved_friendly_unit_in_hex(from) if (speed > 0) { search_move(from, speed + rommel) for (let x of all_hexes) if (!path_valid[x]) if (can_move_to(x, speed + rommel)) path_valid[x] = 1 } } // === TURN === function set_active_player() { game.active = game.phasing update_aliases() } function set_passive_player() { if (game.phasing === AXIS) game.active = ALLIED else game.active = AXIS update_aliases() } function set_enemy_player() { if (is_active_player()) set_passive_player() else set_active_player() } function is_active_player() { return game.active === game.phasing } function is_passive_player() { return game.active !== game.phasing } function is_axis_player() { return game.active === AXIS } function is_allied_player() { return game.active === ALLIED } function end_player_turn() { set_clear(game.partial_retreats) // 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 // reset moved and fired flags set_clear(game.fired) set_clear(game.moved) goto_initial_supply_check() } function goto_turn_option() { set_active_player() game.state = 'turn_option' } 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() }, } // === INITIAL & FINAL SUPPLY CHECK === function goto_initial_supply_check() { let snet = friendly_supply_network() let ssrc = friendly_supply_base() // TODO: fortress supply // TODO: assign fortress supply 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_initial_supply_check_rout() } function goto_initial_supply_check_rout() { let n = 0, where = 0 for (let x of all_hexes) { if (is_friendly_rout_hex(x)) { where = x n++ } } if (n === 0) goto_turn_option() else if (n === 1) goto_rout(where, false, goto_initial_supply_check_rout) else game.state = 'initial_supply_check_routs' } states.initial_supply_check_routs = { prompt() { view.prompt = `Initial Supply Check: Rout!` for (let x of all_hexes) if (is_enemy_rout_hex(x)) gen_action_hex(x) }, hex(where) { goto_rout(where, true, goto_initial_supply_check_rout) } } function goto_final_supply_check() { set_active_player() capture_fortress(BARDIA, 2, "bardia", "bardia_captured") capture_fortress(BENGHAZI, 2, "benghazi", "benghazi_captured") capture_fortress(TOBRUK, 5, "tobruk", "tobruk_captured") let snet = friendly_supply_network() let ssrc = friendly_supply_base() // TODO: fortress supply // TODO: assign unused fortress supply for_each_friendly_unit(u => { let x = unit_hex(u) if (is_map_hex(x) && !snet[x] && !is_unit_disrupted(u) && !is_unit_supplied(u)) { log(`Disrupted at #${x}`) set_unit_disrupted(u) } }) goto_final_supply_check_rout() } function goto_final_supply_check_rout() { let n = 0, where = 0 for (let x of all_hexes) { if (is_friendly_rout_hex(x)) { where = x n++ } } if (n === 0) end_player_turn() else if (n === 1) goto_rout(where, false, goto_final_supply_check_rout) else game.state = 'final_supply_check_routs' } states.final_supply_check_routs = { prompt() { view.prompt = `Final Supply Check: Rout!` for (let x of all_hexes) if (is_friendly_rout_hex(x)) gen_action_hex(x) }, hex(where) { goto_rout(where, false, goto_final_supply_check_rout) } } states.final_supply_check_routs = { prompt() { view.prompt = `Final Supply Check: Rout!` for (let x of all_hexes) if (is_friendly_rout_hex(x)) gen_action_hex(x) }, hex(where) { goto_rout(where, false, goto_final_supply_check_rout) } } // ==== 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') if (game.turn_option !== 'pass') gen_action('regroup') // TODO: needs work... 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() goto_final_supply_check() } } 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() }, } 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 (hex_or_adjacent_has_undisrupted_and_unmoved_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 of all_hexes) 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() }, } function end_move_phase() { game.side_limit = {} game.from1 = game.from2 = game.to1 = game.to2 = 0 // TODO: forced marches goto_refuse_battle() } // === GROUP AND REGROUP MOVEMENT === function goto_move() { 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() game.state = 'move' } states.move = { inactive: "move", prompt() { let rommel1 = (game.rommel === 1) ? 1 : 0 let rommel2 = (game.rommel === 2) ? 1 : 0 if (game.selected < 0) { view.prompt = `Move: Select unit to move.` // Select Group Move 1 if (!game.to1 && game.from1) { if (!is_battle_hex(game.from1)) { for_each_undisrupted_and_unmoved_friendly_unit_in_hex(game.from1, u => { gen_action_unit(u) }) } } // Select Group Move 2 if (!game.to2 && game.from2) { if (!is_battle_hex(game.from2)) { for_each_undisrupted_and_unmoved_friendly_unit_in_hex(game.from2, u => { gen_action_unit(u) }) } } // Select Regroup Move 1 if (game.to1) { for_each_hex_and_adjacent_hex(game.from1, from => { let speed = max_speed_of_undisrupted_and_unmoved_friendly_unit_in_hex(from) if (speed > 0 && !has_enemy_unit(from)) { // TODO: withdraw pass move search_move(from, speed + rommel1) for_each_undisrupted_and_unmoved_friendly_unit_in_hex(from, u => { if (can_move_to(game.to1, unit_speed(u) + rommel1)) gen_action_unit(u) }) } }) } // Select Regroup Move 2 if (game.to1) { for_each_hex_and_adjacent_hex(game.from2, from => { let speed = max_speed_of_undisrupted_and_unmoved_friendly_unit_in_hex(from) if (speed > 0 && !has_enemy_unit(from)) { // TODO: withdraw pass move search_move(from, speed + rommel2) for_each_undisrupted_and_unmoved_friendly_unit_in_hex(from, u => { if (can_move_to(game.to2, unit_speed(u) + rommel2)) gen_action_unit(u) }) } }) } // Retreat if (can_select_retreat_hex()) gen_action('retreat') // Overrun let has_overrun_hex = false for (let x of all_hexes) { if (is_overrun_hex(x)) { has_overrun_hex = true break } } if (has_overrun_hex) gen_action('overrun') else gen_action('end_move') } else { view.prompt = `Move: Select hex to move to.` // Deselect gen_action_unit(game.selected) // Move if (game.turn_option === 'pass') search_withdraw(game.selected, (rommel1 | rommel2)) else search_move(unit_hex(game.selected), unit_speed(game.selected) + (rommel1 | rommel2)) gen_move() } }, unit(who) { apply_select(who) }, hex(to) { apply_move(to) }, retreat() { push_undo() log_br() game.state = 'retreat_from' }, overrun() { let n = 0 let where = 0 for (let x of all_hexes) { if (is_overrun_hex(x)) { n ++ where = x } } if (n === 1) { goto_overrun(where) } else { push_undo() game.state = 'overrun' } }, end_move() { clear_undo() log_br() end_move_phase() } } states.overrun = { prompt() { view.prompt = `Overrun!` for (let x of all_hexes) if (is_overrun_hex(x)) gen_action_hex(x) }, hex(where) { // return to move state afterwards game.state = 'move' goto_overrun(where) }, } function goto_overrun(where) { log_h3(`Overrun at #${where}`) goto_rout(where, true, null) } function gen_move() { let rommel1 = (game.rommel === 1) ? 1 : 0 let rommel2 = (game.rommel === 2) ? 1 : 0 let speed = unit_speed(game.selected) let from = unit_hex(game.selected) if (!game.to1 && game.from1 === from) { for (let to of all_hexes) if (to != from && can_move_to(to, speed + rommel1)) gen_action_hex(to) } if (!game.to2 && game.from2 === from) { for (let to of all_hexes) if (to != from && can_move_to(to, speed + rommel2)) gen_action_hex(to) } if (game.to1 && is_hex_or_adjacent_to(from, game.from1)) { if (can_move_to(game.to1, speed + rommel1)) gen_action_hex(game.to1) } if (game.to2 && is_hex_or_adjacent_to(from, game.from2)) { if (can_move_to(game.to2, speed + rommel2)) gen_action_hex(game.to2) } } function apply_move(to) { let rommel1 = (game.rommel === 1) ? 1 : 0 let rommel2 = (game.rommel === 2) ? 1 : 0 let who = pop_selected() let from = unit_hex(who) let speed = unit_speed(who) push_undo() search_move(from, speed + (rommel1 | rommel2)) if (!game.to1 && game.from1 === from) if (can_move_to(to, speed + rommel1)) return move_unit(who, to, speed + rommel1) if (!game.to2 && game.from2 === from) if (can_move_to(to, speed + rommel2)) return move_unit(who, to, speed + rommel2) if (game.to1 === to && is_hex_or_adjacent_to(from, game.from1)) if (can_move_to(to, speed + rommel1)) return move_unit(who, to, speed + rommel1) if (game.to2 === to && is_hex_or_adjacent_to(from, game.from2)) if (can_move_to(to, speed + rommel2)) return move_unit(who, to, speed + rommel2) } // to check usable alternate paths to enter destination hex function can_move_via(via, to, speed, road) { let cost = path_cost[road][via] let side = to_side_id(via, to) let max_side = side_limit[side] console.log("can_move_via", via, to, speed, road, "=", cost) // too far if (cost + 1 > speed + road) return false // can't cross this hexside if (max_side === 0) return false // must stay on road for current bonus if (side_road[side] < road) return false // must stop on enemies if (has_enemy_unit(via)) return false // may not exceed hexside limit if (has_enemy_unit(to)) if ((game.side_limit[side] | 0) >= max_side) return false return true } function move_unit(who, to, speed) { let from = unit_hex(who) let road = pick_path(to, speed) if (has_enemy_unit(to)) { game.engage_via = [] for_each_adjacent_hex(to, via => { if ( (hex_road[from] >= 4 && can_move_via(via, to, speed, 4)) || (hex_road[from] >= 2 && can_move_via(via, to, speed, 2)) || (hex_road[from] >= 1 && can_move_via(via, to, speed, 1)) || (hex_road[from] >= 0 && can_move_via(via, to, speed, 0)) ) game.engage_via.push(via) }) console.log("ENGAGE", to, "FROM", game.engage_via) if (game.engage_via.length === 1) { engage_via(who, game.engage_via[0], to) } else { game.engage_who = who game.engage_to = to game.state = 'engage' return } } else { log(`>from #${from} to #${to}`) set_unit_moved(who) set_unit_hex(who, to) } } states.engage = { prompt() { view.prompt = `Move: Select which hex side to cross.` view.selected = game.engage_who for (let x of game.engage_via) gen_action_hex(x) }, hex(via) { engage_via(game.engage_who, via, game.engage_to) delete game.engage_via delete game.engage_who delete game.engage_to game.state = 'move' } } function engage_via(who, via, to) { let from = unit_hex(who) set_unit_moved(who) set_unit_hex(who, to) let side = to_side_id(via, 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) } log(`>from #${from} via #${via} to #${to}`) } // === RETREAT === function is_valid_retreat_hex(from) { if (game.turn_option === 'pass') return can_all_retreat(from) else return can_any_retreat(from) } function can_any_retreat(from) { let result = false for_each_undisrupted_and_unmoved_friendly_unit_in_hex(from, u => { if (result === false) { if (can_unit_retreat(u)) result = true } }) return result } function can_all_retreat(from) { let result = true for_each_undisrupted_friendly_unit_in_hex(from, u => { if (result === true && !is_unit_moved(u) && !can_unit_retreat(u)) result = false }) return result } function can_unit_retreat(who) { let rommel1 = (game.rommel === 1) ? 1 : 0 let rommel2 = (game.rommel === 2) ? 1 : 0 let from = unit_hex(who) if (!game.to1 && game.from1 === from) return can_unit_retreat_group_move(who) if (!game.to2 && game.from2 === from) return can_unit_retreat_group_move(who) if (game.to1 && is_hex_or_adjacent_to(from, game.from1)) if (can_unit_retreat_regroup_move(who, game.to1, rommel1)) return true if (game.to2 && is_hex_or_adjacent_to(from, game.from2)) if (can_unit_retreat_regroup_move(who, game.to2, rommel2)) return true return false } function can_unit_retreat_group_move(who) { if (game.turn_option === 'pass') return can_unit_disengage_and_withdraw(who) else return can_unit_disengage_and_move(who) } function can_unit_retreat_regroup_move(who, to, rommel) { if (game.turn_option === 'pass') return can_unit_disengage_and_withdraw_to(who, to, rommel) else return can_unit_disengage_and_move_to(who, to, rommel) } function is_friendly_hexside(side) { if (is_axis_player()) return set_has(game.axis_sides, side) return set_has(game.allied_sides, side) } function can_unit_disengage_and_withdraw(who) { let sline = unit_supply_line(who) let sdist = unit_supply_distance(who) let from = unit_hex(who) let result = false for_each_adjacent_hex(from, to => { let side = to_side_id(from, to) if (is_friendly_hexside(side) && !has_enemy_unit(to)) if (sline[side] && sdist[to] <= sdist[from]) result = true }) return result } function can_unit_disengage_and_move(who) { let from = unit_hex(who) let result = false for_each_adjacent_hex(from, to => { let side = to_side_id(from, to) if (is_friendly_hexside(side) && !has_enemy_unit(to)) result = true }) return result } function can_unit_disengage_and_withdraw_to(who, to, rommel) { search_withdraw_retreat(who, rommel) return can_move_to(to, unit_speed(who) + rommel) } function can_unit_disengage_and_move_to(who, to, rommel) { search_move_retreat(unit_hex(who), unit_speed(who) + rommel) return can_move_to(to, unit_speed(who) + rommel) } function can_select_retreat_hex() { if (!game.to1 && game.from1) if (is_valid_retreat_hex(game.from1)) return true if (!game.to2 && game.from2) if (is_valid_retreat_hex(game.from2)) return true if (game.to1) { let result = false for_each_hex_and_adjacent_hex(game.from1, x => { if (result === false && is_valid_retreat_hex(x)) result = true }) if (result) return true } if (game.to2) { let result = false for_each_hex_and_adjacent_hex(game.from2, x => { if (result === false && is_valid_retreat_hex(x)) result = true }) if (result) return true } return false } states.retreat_from = { prompt() { view.prompt = `Retreat: Select hex to retreat from.` if (!game.to1 && game.from1) { if (is_valid_retreat_hex(game.from1)) gen_action_hex(game.from1) } if (!game.to2 && game.from2) { if (is_valid_retreat_hex(game.from2)) gen_action_hex(game.from2) } if (game.to1) { for_each_hex_and_adjacent_hex(game.from1, x => { if (is_valid_retreat_hex(x)) gen_action_hex(x) }) } if (game.to2) { for_each_hex_and_adjacent_hex(game.from2, x => { if (is_valid_retreat_hex(x)) gen_action_hex(x) }) } gen_action('end_move') }, hex(x) { push_undo() game.retreat = x game.state = 'retreat_who' game.retreat_units = [] }, end_move() { clear_undo() log_br() end_move_phase() } } states.retreat_who = { prompt() { view.prompt = `Retreat: Select units to retreat.` let full_retreat = true for_each_undisrupted_and_unmoved_friendly_unit_in_hex(game.retreat, u => { if (!set_has(game.retreat_units, u)) full_retreat = false if (can_unit_retreat(u)) gen_action_unit(u) }) if (full_retreat) { view.actions.retreat = 1 } else { gen_action('select_all') if (game.retreat_units.length > 0 && game.turn_option !== 'pass') view.actions.retreat = 1 else view.actions.retreat = 0 } view.selected = game.retreat_units }, unit(u) { set_toggle(game.retreat_units, u) }, select_all() { for_each_undisrupted_and_unmoved_friendly_unit_in_hex(game.retreat, u => { if (!set_has(game.retreat_units, u)) if (can_unit_retreat(u)) set_add(game.retreat_units, u) }) }, retreat() { clear_undo() let full_retreat = true for_each_undisrupted_friendly_unit_in_hex(game.retreat, u => { if (!set_has(game.retreat_units, u)) full_retreat = false }) if (full_retreat) { goto_pursuit_fire_during_retreat(game.retreat) } else { 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]}.` view.selected = game.retreat_units gen_action('probe') gen_action('pass') }, probe() { set_active_player() game.state = 'probe_fire' game.battle = game.retreat game.hits = [ 0, 0, 0, 0 ] }, pass() { end_probe() }, } function end_probe() { game.flash = "" game.battle = 0 game.hits = 0 set_active_player() 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_move() else goto_pursuit_fire_during_retreat(game.retreat) } function goto_retreat_move() { set_active_player() let done = true for (let u of game.retreat_units) { if (unit_hex(u) === game.retreat) { done = false break } } if (done) end_retreat() else game.state = 'retreat_move' } states.retreat_move = { prompt() { view.prompt = `Retreat!` if (game.selected < 0) { 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') } else { let rommel1 = (game.rommel === 1) ? 1 : 0 let rommel2 = (game.rommel === 2) ? 1 : 0 gen_action_unit(game.selected) if (game.turn_option === 'pass') search_withdraw_retreat(game.selected, (rommel1 | rommel2)) else search_move_retreat(unit_hex(game.selected), unit_speed(game.selected) + (rommel1 | rommel2)) gen_move() } }, unit(who) { apply_select(who) }, hex(to) { let who = pop_selected() push_undo() set_unit_hex(who, to) set_unit_moved(who) set_unit_disrupted(who) }, end_retreat() { end_retreat() } } function end_retreat() { clear_undo() if (!is_battle_hex(game.retreat)) release_hex_control(game.retreat) game.retreat_units = null // no shielding units remain if (is_friendly_rout_hex(game.retreat)) goto_rout(game.retreat, false, end_retreat_2) // rear-guard eliminated all enemy units else if (is_enemy_rout_hex(game.retreat)) goto_rout(game.retreat, true, end_retreat_2) else end_retreat_2() } function end_retreat_2() { if (can_select_retreat_hex()) game.state = 'retreat_from' else end_move_phase() } // === REFUSE BATTLE === function goto_refuse_battle() { clear_undo() if (game.active_battles.length > 0) { set_passive_player() game.state = 'refuse_battle' } else { goto_combat_phase() } } function can_all_refuse_battle(from) { let result = true for_each_undisrupted_friendly_unit_in_hex(from, u => { if (result === true && !can_unit_refuse_battle(u)) result = false }) return result } function can_unit_refuse_battle(who) { return can_unit_disengage_and_withdraw(who) } states.refuse_battle = { inactive: "refuse battle", prompt() { view.prompt = `You may Refuse Battle.` for (let x of game.active_battles) if (can_all_refuse_battle(x)) gen_action_hex(x) gen_action('next') }, hex(x) { push_undo() log_h3(`Refused battle at #${x}`) game.refuse = x set_delete(game.active_battles, x) goto_pursuit_fire_during_refuse_battle(x) }, next() { goto_combat_phase() } } function goto_refuse_battle_move() { set_passive_player() if (has_undisrupted_friendly_unit(game.refuse)) game.state = 'refuse_battle_move' else end_refuse_battle_move() } states.refuse_battle_move = { inactive: "refuse battle (withdraw group move)", prompt() { view.prompt = `Refuse Battle: Withdraw units.` if (game.selected < 0) { let done = true for_each_undisrupted_friendly_unit_in_hex(game.refuse, u => { gen_action_unit(u) done = false }) if (done) gen_action('end_retreat') } else { let speed = unit_speed(game.selected) gen_action_unit(game.selected) search_withdraw_retreat(game.selected, 0) for (let to of all_hexes) if (to != game.refuse && can_move_to(to, speed)) gen_action_hex(to) } }, unit(who) { apply_select(who) }, hex(to) { let who = pop_selected() push_undo() log(`>to #${to}`) set_unit_hex(who, to) set_unit_disrupted(who) }, end_retreat() { end_refuse_battle_move() } } function end_refuse_battle_move() { clear_undo() if (is_friendly_rout_hex(game.refuse)) goto_rout(game.refuse, false, end_refuse_battle_move_2) else end_refuse_battle_move_2() } function end_refuse_battle_move_2() { release_hex_control(game.refuse) game.refuse = 0 goto_refuse_battle() } // === ROUT === // rout attrition // pursuit fire // withdraw by group move // eliminated if cannot function goto_rout(from, enemy, after) { // remember state and callback so we can resume after routing if (after) { if (!after_rout_table[after.name]) after_rout_table[after.name] = after after = after.name } game.rout = { state: game.state, active: game.active, after: after, from: from, attrition: [], } if (enemy) set_enemy_player() game.state = 'rout_attrition' } states.rout_attrition = { prompt() { view.prompt = "Rout: All units lose one step of rout attrition." for_each_friendly_unit_in_hex(game.rout.from, u => { if (!set_has(game.rout.attrition, u)) gen_action_unit(u) }) }, unit(who) { reduce_unit(who) set_add(game.rout.attrition, who) let done = true for_each_friendly_unit_in_hex(game.rout.from, u => { if (!set_has(game.rout.attrition, u)) done = false }) if (done) { delete game.rout.attrition goto_rout_fire(game.rout.from) } }, } function goto_rout_move() { if (has_friendly_unit(game.rout.from)) game.state = 'rout_move' else end_rout() } states.rout_move = { prompt() { view.prompt = `Rout: Withdraw units.` if (game.selected < 0) { let done = true for_each_friendly_unit_in_hex(game.rout.from, u => { gen_action_unit(u) done = false }) if (done) gen_action('end_rout') } else { let speed = unit_speed(game.selected) let eliminate = true search_withdraw_retreat(game.selected, 0) for (let to of all_hexes) { if (to != game.rout.from && can_move_to(to, speed)) { gen_action_hex(to) eliminate = false } } if (eliminate) gen_action('eliminate') } }, unit(who) { apply_select(who) }, eliminate() { let who = pop_selected() push_undo() log(`>eliminated`) eliminate_unit(who) }, hex(to) { let who = pop_selected() push_undo() log(`>to #${to}`) set_unit_hex(who, to) set_unit_disrupted(who) }, end_rout() { end_rout() } } function end_rout() { clear_undo() game.state = game.rout.state release_hex_control(game.rout.from) set_delete(game.active_battles, game.rout.from) if (game.active !== game.rout.active) set_enemy_player() let after = game.rout.after delete game.rout if (after) after_rout_table[after]() } // ==== COMBAT PHASE === function is_mandatory_combat(fortress, control_prop) { return is_battle_hex(fortress) && (game[control_prop] !== game.phasing) } function goto_combat_phase() { clear_undo() set_active_player() if (game.turn_option === 'pass') { if (is_mandatory_combat(BARDIA, "bardia")) return goto_rout(BARDIA, false, goto_combat_phase) if (is_mandatory_combat(BENGHAZI, "benghazi")) return goto_rout(BENGHAZI, false, goto_combat_phase) if (is_mandatory_combat(TOBRUK, "tobruk")) return goto_rout(TOBRUK, false, goto_combat_phase) } else { if (is_mandatory_combat(BARDIA, "bardia")) set_add(game.active_battles, BARDIA) if (is_mandatory_combat(BENGHAZI, "benghazi")) set_add(game.active_battles, BENGHAZI) if (is_mandatory_combat(TOBRUK, "tobruk")) set_add(game.active_battles, TOBRUK) } 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() { if (game.turn_option === 'blitz') { log_h2(`Blitz Turn`) if (game.rommel) game.rommel = 3 set_clear(game.fired) game.turn_option = 'second blitz' goto_move_phase() } else { goto_final_supply_check() } } // === BATTLES === // Normal Battle: // passive fire // active hits // active fire // passive hits function is_unit_retreating(u) { if (game.retreat_units) return set_has(game.retreat_units, u) return false } function is_assault_battle() { return set_has(game.assault_battles, game.battle) } function is_fortress_defensive_fire() { if ((game.state === 'battle_fire' && is_passive_player()) || (game.state === 'probe_fire' && is_active_player())) { if (game.battle === BENGHAZI) return game.benghazi === game.active if (game.battle === TOBRUK) return game.tobruk === game.active if (game.battle === BARDIA) return game.tobruk === game.active } return false } function is_minefield_offensive_fire() { if ((game.state === 'battle_fire' && is_active_player()) || (game.state === 'probe_fire' && is_passive_player())) { if (set_has(game.revealed_minefields)) { // DD advantage is lost if the defender initiated combat if (is_axis_player()) return set_has(game.allied_hexes, game.battle) else return set_has(game.axis_hexes, game.battle) } } return false } function roll_battle_fire(who, tc) { let fc = unit_class(who) let cv = unit_cv(who) // Double dice during assault and non-armor defenders in fortress! if (is_assault_battle()) cv += cv else if (fc !== ARMOR && is_fortress_defensive_fire()) cv += cv let fp = FIREPOWER_MATRIX[fc][tc] let result = [] let total = 0 for (let i = 0; i < cv; ++i) { let roll = random(6) + 1 result.push(roll) if (roll >= fp) ++total } // Double defense in minefields! if (is_minefield_offensive_fire()) total = total / 2 game.flash = `${unit_name(who)} fired ${firepower_name[fp]} at ${class_name[tc]}: ` + result.join(", ") log(game.flash) return total } function goto_battle(x) { clear_undo() game.battle = x // goto defensive fire set_passive_player() game.state = 'battle_fire' game.hits = [ 0, 0, 0, 0 ] } function end_battle() { set_delete(game.active_battles, game.battle) set_delete(game.assault_battles, game.battle) game.flash = "" game.battle = 0 game.hits = 0 set_active_player() if (game.active_battles.length > 0) game.state = 'select_battle' else end_combat_phase() } function apply_battle_fire(tc) { let who = pop_selected() set_unit_fired(who) game.hits[tc] += roll_battle_fire(who, tc) let hp = count_hp_in_battle() // clamp to available hit points game.hits[tc] = min(game.hits[tc], hp[tc]) // end when no more units to fire or all targets destroyed let done = true if (game.hits[0] < hp[0] || game.hits[1] < hp[1] || game.hits[2] < hp[2] || game.hits[3] < hp[3]) { for_each_undisrupted_friendly_unit_in_hex(game.battle, u => { if (!is_unit_fired(u) && !is_unit_retreating(u)) done = false }) } if (done) { goto_hits() } } function goto_hits() { set_enemy_player() // round down half-hits from double defense game.hits[0] |= 0 game.hits[1] |= 0 game.hits[2] |= 0 game.hits[3] |= 0 if (game.hits[0] + game.hits[1] + game.hits[2] + game.hits[3] > 0) { if (game.state === 'battle_fire') game.state = 'battle_hits' else game.state = 'probe_hits' } else { if (game.state === 'battle_fire') end_battle_hits() else end_probe_hits() } } function gen_battle_fire() { let arty = false for_each_undisrupted_friendly_unit_in_hex(game.battle, u => { if (is_artillery_unit(u) && !is_unit_retreating(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) && !is_unit_retreating(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 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 => { if (!is_unit_retreating(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() return done } function apply_battle_hit(who) { game.hits[unit_class(who)] -= reduce_unit(who) } states.battle_fire = { prompt() { if (game.active === game.phasing) view.prompt = `Battle: Offensive Fire!` else view.prompt = `Battle: Defensive Fire!` if (game.selected < 0) gen_battle_fire() else gen_battle_target() }, unit(who) { apply_select(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 = `Battle: Apply hits from Defensive Fire.` else view.prompt = `Battle: Apply hits from Offensive Fire.` gen_battle_hits() }, unit(who) { push_undo() apply_battle_hit(who) }, next() { clear_undo() end_battle_hits() }, } function end_battle_hits() { if (is_friendly_rout_hex(game.battle)) { goto_rout(game.battle, false, end_battle) } else if (game.active === game.phasing) { // goto offensive fire game.state = 'battle_fire' game.hits = [ 0, 0, 0, 0 ] } else { end_battle() } } states.probe_fire = { prompt() { if (game.active !== game.phasing) view.prompt = `Probe: Offensive Fire!` else view.prompt = `Probe: Defensive Fire!` if (game.selected < 0) gen_battle_fire() else gen_battle_target() }, unit(who) { apply_select(who) }, armor() { apply_battle_fire(ARMOR) }, infantry() { apply_battle_fire(INFANTRY) }, antitank() { apply_battle_fire(ANTITANK) }, artillery() { apply_battle_fire(ARTILLERY) }, } states.probe_hits = { prompt() { if (game.active !== game.phasing) view.prompt = `Probe: Apply hits from Defensive Fire.` else view.prompt = `Probe: Apply hits from Offensive Fire.` gen_battle_hits() }, unit(who) { push_undo() apply_battle_hit(who) }, next() { clear_undo() end_probe_hits() }, } function end_probe_hits() { if (game.active !== game.phasing && has_friendly_units_in_battle()) { // goto offensive fire game.state = 'probe_fire' game.hits = [ 0, 0, 0, 0 ] } else { end_probe() } } // === 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_rout_fire(where) { clear_undo() set_enemy_player() game.hits = 0 game.pursuit = where if (can_rout_fire(true)) game.state = 'rout_fire' else goto_rout_hits() } function goto_pursuit_fire_during_retreat(where) { clear_undo() set_passive_player() game.hits = 0 game.pursuit = where if (can_pursuit_fire(true)) 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_rout_hits() { set_enemy_player() if (game.hits > 0) game.state = 'rout_hits' else end_rout_fire() } function goto_pursuit_hits() { set_enemy_player() if (game.hits > 0) game.state = 'pursuit_hits' else end_pursuit_fire() } function slowest_enemy_unit_speed(where) { let r = 4 for_each_enemy_unit_in_hex(where, u => { let s = unit_speed(u) if (s < r) r = s }) return r } 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_rout_fire(verbose) { let result = false let slowest = slowest_enemy_unit_speed(game.pursuit) if (verbose) log(`Slowest routing unit was ${speed_name[slowest]} ${game.active} at #${game.pursuit}.`) for_each_undisrupted_friendly_unit_in_hex(game.pursuit, u => { if (unit_speed(u) >= slowest && !is_unit_fired(u)) result = true }) return result } function can_pursuit_fire(verbose) { let result = false let slowest = slowest_undisrupted_enemy_unit_speed(game.pursuit) if (verbose) log(`Slowest retreating unit was ${speed_name[slowest]} ${game.active} at #${game.pursuit}.`) 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_imp(who, n, hp) { if (n === 2) { let a = random(6) + 1 let b = random(6) + 1 game.flash = `${unit_name(who)} fired ${a}, ${b}` log(game.flash) if (a >= 4) game.hits++ if (b >= 4) game.hits++ } if (n === 1) { let a = random(6) + 1 game.flash = `${unit_name(who)} fired ${a}` log(`>%${who} pursuit fired ${a}.`) if (a >= 4) game.hits++ } if (game.hits > hp) game.hits = hp return game.hits === hp } function roll_pursuit_fire(who, n) { return roll_pursuit_fire_imp(who, n, count_hp_in_pursuit()) } function roll_rout_fire(who, n) { return roll_pursuit_fire_imp(who, n, count_hp_in_rout()) } 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) }) // allow saving fire if there are shielded enemy units if (has_disrupted_enemy_unit(game.pursuit)) 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.rout_fire = { inactive: "rout fire (fire)", prompt() { view.prompt = `Pursuit Fire (Rout).` let slowest = slowest_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) }) }, unit(who) { let slowest = slowest_enemy_unit_speed(game.pursuit) set_unit_fired(who) let done = roll_rout_fire(who, (unit_speed(who) > slowest ? 2 : 1)) if (done || !can_rout_fire(false)) goto_rout_hits() }, } function gen_pursuit_hits(normal_steps, elite_steps, iterate) { let done = true iterate(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') } 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() gen_pursuit_hits(normal_steps, elite_steps, for_each_undisrupted_friendly_unit_in_hex) }, unit(who) { push_undo() game.hits -= reduce_unit(who) }, next() { clear_undo() end_pursuit_fire() }, } states.rout_hits = { inactive: "rout fire (hits)", prompt() { view.prompt = `Pursuit Fire (Rout): Apply ${game.hits} hits.` let normal_steps = count_normal_steps_in_rout() let elite_steps = count_elite_steps_in_rout() gen_pursuit_hits(normal_steps, elite_steps, for_each_friendly_unit_in_hex) }, unit(who) { push_undo() game.hits -= reduce_unit(who) }, next() { clear_undo() end_rout_fire() }, } function end_pursuit_fire() { game.flash = "" game.pursuit = 0 if (game.retreat) { goto_retreat_move() } else { goto_refuse_battle_move() } } function end_rout_fire() { game.flash = "" game.pursuit = 0 goto_rout_move() } // === BUILD-UP === function end_month() { delete game.bardia_captured delete game.benghazi_captured delete game.tobruk_captured } // === DEPLOYMENT === states.free_deployment = { inactive: "free deployment", prompt() { let scenario = SCENARIOS[game.scenario] let deploy = hexdeploy + scenario.start let axis = (game.active === AXIS) let done = true view.prompt = axis ? "Axis Free Deployment" : "Allied Free Deployment" if (game.selected < 0) { for_each_friendly_unit_in_hex(deploy, u => { gen_action_unit(u) done = false }) } else { for (let x of (axis ? scenario.axis_deployment : scenario.allied_deployment)) { if (!is_enemy_hex(x)) { let limit = scenario.deployment_limit[x] | 0 if (!limit || count_friendly_units_in_hex(x) < limit) gen_action_hex(x) } } gen_action_unit(game.selected) done = false } if (done) gen_action_next() }, unit(u) { apply_select(u) }, hex(to) { let who = pop_selected() push_undo() log(`Deployed at #${to}.`) set_unit_hex(who, to) }, next() { clear_undo() if (is_axis_player()) { set_enemy_player() log_h2("Allied Deployment") } else { end_free_deployment() } } } function end_free_deployment() { game.phasing = AXIS set_active_player() 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 = first_axis_unit; u <= last_axis_unit; ++u) if (units[u].appearance === a) list.push(u) return list } function find_allied_units(a) { let list = [] for (let u = first_allied_unit; u <= last_allied_unit; ++u) if (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, deployment_limit: {}, 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, deployment_limit: {}, }, "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, deployment_limit: {}, 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, deployment_limit: {}, }, "1941-42": { year: 1941, start: 1, end: 20, axis_deployment: [], allied_deployment: region_egypt_and_libya, axis_initial_supply: 6, allied_initial_supply: 6, deployment_limit: {}, }, } 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.phasing = AXIS set_active_player() game.state = 'free_deployment' game.selected = -1 } // === 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: -1, scenario: scenario, month: 0, draw_pile: [ 14, 28 ], // 14 dummy supply + 28 real supply axis_hand: [ 0, 0 ], allied_hand: [ 0, 0 ], units: new Array(units.length).fill(0), moved: [], fired: [], recover: [], axis_minefields: [], allied_minefields: [], revealed_minefields: [], // fortress control bardia: ALLIED, benghazi: ALLIED, tobruk: ALLIED, // 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, // retreat partial_retreats: [], // remember partial retreats to forbid initiating combat retreat: 0, retreat_units: null, // refuse battle refuse: 0, // rout rout: null, // combat active_battles: [], assault_battles: [], pursuit: 0, battle: 0, hits: null, flash: null, } setup(scenario) return game } exports.view = function(state, current) { load_state(state) 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, } 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) } exports.query = function (state, current, q) { if (q === 'supply') { load_state(state) return { axis_supply: axis_supply_network(), axis_supply_line: axis_supply_line(), allied_supply: allied_supply_network(), allied_supply_line: allied_supply_line(), } } return null } 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 let state = save_undo.pop() save_log.length = state.log state.log = save_log state.undo = save_undo load_state(state) } 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) { load_state(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) { load_state(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 }