"use strict" // TODO: RAIDERS // TODO: MINEFIELDS // TODO: legal pass withdrawal moves (reduce supply net, withdraw from fortress attack) // TODO: fortress combat in pass turns (must withdraw) // TODO: group move from queue holding box to base // TODO: 1942 malta group (reinforce, reduce supply card draw) // TODO: log summaries (deploy, rebuild, move, etc) // TODO: put initial deployment stack somewhere more accessible (spread out along the top?) // TODO: multi-select deployment // RULES: may units redeploying out of battle hex cross enemy controlled hexsides? (yes / doesn't matter) // RULES: can fortress supplied units be part of supply lines for non-fortress supplied units for withdrawals to base? // RULES: can non-fortress supplied units be part of supply lines for fortress supplied units for withdrawals? // RULES: for sea redeployment, can bases be "besieged"? (yes) // RULES: may units redeploying out of battle hex leave disrupted units behind to be routed? (no) // RULES: may units returning for refit enter enemy supply network? (no) // RULES: if disrupted units are routed again during their "full enemy turn", can they still recover? // RULES: may oasis supplied units refuse battle or withdraw to base? (yes) // RULES: when is "fired" status cleared? // RULES: are minefields moved through (but not stopped at) revealed? // TODO: black hit outline in battles ("steploss/bad" action) and skip "apply 0 hits" step // ERRATA: forbid single-group regroup moves or convert to group moves after the fact, // to prevent forced march abuse. 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, regions, unit_name, unit_appearance, unit_elite, unit_class, unit_speed, unit_start_steps, } = 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 REAL = 0 const DUMMY = 1 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 class_name_cap = [ "Armor", "Infantry", "Anti-tank", "Artillery" ] const firepower_name = [ "0", "1", "2", "3", "TF", "DF", "SF" ] const speed_name = [ "zero", "leg", "motorized", "mechanized", "recon" ] const speed_name_cap = [ "Zero", "Leg", "Motorized", "Mechanized", "Recon" ] const die_face_hit = [ 0, '\u2776', '\u2777', '\u2778', '\u2779', '\u277A', '\u277B' ] const die_face_miss = [ 0, '\u2460', '\u2461', '\u2462', '\u2463', '\u2464', '\u2465' ] 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 ], ] // Off board return to refit holding const AXIS_REFIT = 102 const ALLIED_REFIT = 48 // Off board reinforcements to engaged base! const AXIS_QUEUE = 127 const ALLIED_QUEUE = 49 // Off board optional 1942 Malta reinforcements const MALTA = 4 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_BASE = 1 const SS_BARDIA = 2 const SS_BENGHAZI = 3 const SS_TOBRUK = 4 const SS_OASIS = 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) { let y = where + hexnext[s] if (is_map_hex(y) && x === y) return true } return false } // === STATE CACHES === const first_axis_unit = 0 const last_axis_unit = 33 const first_allied_unit = 34 const last_allied_unit = 93 const unit_count = 94 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 supply_bardia_invalid = true var supply_bardia_network = new Array(hexcount).fill(0) var supply_bardia_line = new Array(sidecount).fill(0) var supply_benghazi_invalid = true var supply_benghazi_network = new Array(hexcount).fill(0) var supply_benghazi_line = new Array(sidecount).fill(0) var supply_tobruk_invalid = true var supply_tobruk_network = new Array(hexcount).fill(0) var supply_tobruk_line = new Array(sidecount).fill(0) var supply_temp_network = new Array(hexcount).fill(0) var supply_temp_line = new Array(sidecount).fill(0) var first_friendly_unit, last_friendly_unit var first_enemy_unit, last_enemy_unit 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 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 invalidate_caches() { presence_invalid = true supply_axis_invalid = true supply_allied_invalid = true supply_tobruk_invalid = true supply_bardia_invalid = true supply_benghazi_invalid = true } function load_state(state) { if (game !== state) { game = state invalidate_caches() 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) { invalidate_caches() game.units[u] |= UNIT_DISRUPTED_MASK } function clear_unit_disrupted(u) { invalidate_caches() 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) { invalidate_caches() 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) { return (game.units[u] & UNIT_SUPPLY_MASK) >> UNIT_SUPPLY_SHIFT } function set_unit_supply(u, src) { 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 unit_start_steps[u] - unit_lost_steps(u) } function set_unit_steps(u, n) { set_unit_lost_steps(u, unit_start_steps[u] - 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) hide_unit(u) } 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 } function replace_unit(u) { let lost = unit_lost_steps(u) set_unit_lost_steps(u, lost - 1) } function reveal_units_in_hex(x) { for (let u = 0; u < unit_count; ++u) if (unit_hex(u) === x) set_add(game.revealed, u) } function hide_units_in_hex(x) { for (let u = 0; u < unit_count; ++u) if (unit_hex(u) === x) set_delete(game.revealed, u) } function hide_unit(u) { set_delete(game.revealed, u) } // === UNIT DATA === function find_unit(name) { for (let u = 0; u < unit_count; ++u) if (unit_name[u] === name) return u throw new Error("cannot find named block: " + name + unit_name) } 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 } function is_german_unit(u) { return (u >= 14 && u <= 33) } function is_elite_unit(u) { return unit_elite[u] } function is_artillery_unit(u) { return unit_class[u] === ARTILLERY } function unit_cv(u) { if (is_elite_unit(u)) return unit_steps(u) * 2 return unit_steps(u) } function unit_hp_per_step(u) { return is_elite_unit(u) ? 2 : 1 } function unit_hp(u) { return unit_steps(u) * unit_hp_per_step(u) } // === MAP STATE === function friendly_base() { if (is_axis_player()) return EL_AGHEILA return ALEXANDRIA } function friendly_queue() { if (is_axis_player()) return AXIS_QUEUE return ALLIED_QUEUE } function friendly_refit() { if (is_axis_player()) return AXIS_REFIT return ALLIED_REFIT } function 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 count_friendly_units_in_hex(x) { let n = 0 for (let u = first_friendly_unit; u <= last_friendly_unit; ++u) if (unit_hex(u) === x) n++ return n } function has_friendly_unit_in_month(month) { for (let u = first_friendly_unit; u <= last_friendly_unit; ++u) if (unit_hex(u) === hexdeploy + month) return true return false } function for_each_friendly_unit_in_month(month, fn) { for (let u = first_friendly_unit; u <= last_friendly_unit; ++u) if (unit_hex(u) === hexdeploy + month) fn(u) } 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 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 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_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) { if (!is_fortress_friendly_controlled(fortress)) { if (has_undisrupted_friendly_unit(fortress) && !has_enemy_unit(fortress)) { invalidate_caches() log(`Captured #${fortress}!`) let fresh = set_fortress_friendly_controlled(fortress) if (fresh) { if (is_axis_player()) { let award = capacity log(`Awarded ${award} supply cards.`) game.axis_award += award } else { let award = Math.floor(capacity / 2) log(`Awarded ${award} supply cards.`) game.allied_award += award } } } } } // === FORTRESSES === function fortress_bit(fortress) { if (fortress === BARDIA) return 1 if (fortress === BENGHAZI) return 2 if (fortress === TOBRUK) return 4 return 0 } function fortress_src(fortress) { if (fortress === BARDIA) return SS_BARDIA if (fortress === BENGHAZI) return SS_BENGHAZI if (fortress === TOBRUK) return SS_TOBRUK return SS_OASIS } function is_fortress_allied_controlled(fortress) { return (game.fortress & fortress_bit(fortress)) === 1 } function is_fortress_axis_controlled(fortress) { return (game.fortress & fortress_bit(fortress)) === 0 } function set_fortress_axis_controlled(fortress) { game.fortress &= ~fortress_bit(fortress) } function set_fortress_allied_controlled(fortress) { game.fortress |= fortress_bit(fortress) } function set_fortress_captured(fortress) { let bit = fortress_bit(fortress) << 3 if (game.fortress & bit) return false game.fortress |= bit return true } function clear_fortresses_captured() { game.fortress &= 7 } function is_fortress_friendly_controlled(fortress) { if (is_axis_player()) return is_fortress_axis_controlled(fortress) return !is_fortress_axis_controlled(fortress) } function set_fortress_friendly_controlled(fortress) { if (is_axis_player()) set_fortress_axis_controlled(fortress) else set_fortress_allied_controlled(fortress) return set_fortress_captured(fortress) } function is_fortress_besieged(fortress) { let result = false let besieged = is_fortress_axis_controlled(fortress) ? has_allied_unit : has_axis_unit for_each_adjacent_hex(fortress, x => { if (besieged(x)) result = true }) return result } // === 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_unit_on_map(fn) { for (let u = 0; u < unit_count; ++u) if (is_map_hex(unit_hex(u))) fn(u) } function for_each_axis_unit(fn) { for (let u = first_axis_unit; u <= last_axis_unit; ++u) fn(u) } function for_each_axis_unit_on_map(fn) { for (let u = first_axis_unit; u <= last_axis_unit; ++u) if (is_map_hex(unit_hex(u))) fn(u) } function for_each_allied_unit(fn) { for (let u = first_allied_unit; u <= last_allied_unit; ++u) fn(u) } function for_each_allied_unit_on_map(fn) { for (let u = first_allied_unit; u <= last_allied_unit; ++u) if (is_map_hex(unit_hex(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_friendly_unit_on_map(fn) { for (let u = first_friendly_unit; u <= last_friendly_unit; ++u) if (is_map_hex(unit_hex(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_enemy_unit_on_map(fn) { for (let u = first_enemy_unit; u <= last_enemy_unit; ++u) if (is_map_hex(unit_hex(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_and_reveal_battle_hexes() { let n = 0 for (let x of all_hexes) { if (is_battle_hex(x)) { reveal_units_in_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_elite_unit(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_elite_unit(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_elite_unit(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_elite_unit(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_elite_unit(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_elite_unit(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) { let cur = game.axis_hand[REAL] + game.axis_hand[DUMMY] if (cur + n > 16) n = 16 - cur log(`Axis drew ${n} cards.`) for (let i = 0; i < n; ++i) game.axis_hand[draw_supply_card(game.draw_pile)]++ } function deal_allied_supply_cards(n) { let cur = game.allied_hand[REAL] + game.allied_hand[DUMMY] if (cur + n > 16) n = 16 - cur log(`Allied drew ${n} cards.`) for (let i = 0; i < n; ++i) game.allied_hand[draw_supply_card(game.draw_pile)]++ } function shuffle_cards() { let real = game.axis_hand[REAL] + game.allied_hand[REAL] let dummy = game.axis_hand[DUMMY] + game.axis_hand[DUMMY] game.draw_pile[0] = 28 - real game.draw_pile[1] = 14 - dummy } // === 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, supply_fortress_enemy var supply_visited = new Array(hexcount).fill(0) var supply_src = new Array(hexcount).fill(0) var trace_total 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 } } // enemy controlled fortresses block supply lines if (next === BARDIA || next === BENGHAZI || next === TOBRUK) if (supply_fortress_enemy(next)) 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_total++ 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 (next < first_hex || next > last_hex || !hex_exists[next]) continue 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) { trace_total++ ind(d, "> chain", here, n, range) 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 (next < first_hex || next > last_hex || !hex_exists[next]) continue 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, ss) { supply_visited.fill(0) supply_src.fill(0) supply_net.fill(0) supply_line.fill(0) supply_src[start] = 1 supply_net[start] = 1 trace_total = 0 for (let x of all_hexes) if (supply_friendly[x] > 0) trace_supply_chain(x, 0, 0, 3) console.log("SUPPLY VISITS", trace_total) } function trace_fortress_network(fortress, ss) { supply_visited.fill(0) supply_src.fill(0) supply_net.fill(0) supply_line.fill(0) supply_src[fortress] = 1 supply_net[fortress] = 1 trace_total = 0 for (let u = 0; u < unit_count; ++u) { let x = unit_hex(u) if (is_map_hex(x) && unit_supply(u) === ss) { if (!supply_visited[x]) trace_supply_chain(x, 0, 0, 3) } } console.log("FORTRESS SUPPLY VISITS", trace_total) } function init_trace_supply(net, line, friendly) { if (presence_invalid) update_presence() supply_net = net supply_line = line if (friendly === AXIS) { supply_fortress_enemy = is_fortress_allied_controlled supply_defender = game.axis_hexes supply_defender_sides = game.axis_sides supply_friendly = presence_axis supply_enemy = presence_allied } else { supply_fortress_enemy = is_fortress_axis_controlled supply_defender = game.allied_hexes supply_defender_sides = game.allied_sides supply_friendly = presence_allied supply_enemy = presence_axis } } // For repeated supplied hex checks during deployment and fortress assignment function init_trace_supply_to_base_or_fortress() { init_trace_supply(supply_temp_network, supply_temp_line, game.active) supply_net.fill(0) } function can_trace_supply_to_base_or_fortress(base, from) { supply_visited.fill(0) supply_src.fill(0) supply_src[base] = 1 return trace_supply_chain(from, 0, 0, 3) } function update_axis_supply() { supply_axis_invalid = false init_trace_supply(supply_axis_network, supply_axis_line, AXIS) trace_supply_network(EL_AGHEILA, 0) } function update_allied_supply() { supply_allied_invalid = false init_trace_supply(supply_allied_network, supply_allied_line, ALLIED) trace_supply_network(ALEXANDRIA, 0) } function update_bardia_supply() { supply_bardia_invalid = false init_trace_supply(supply_bardia_network, supply_bardia_line, is_fortress_axis_controlled(BARDIA) ? AXIS : ALLIED) trace_fortress_network(BARDIA, SS_BARDIA) } function update_benghazi_supply() { supply_benghazi_invalid = false init_trace_supply(supply_benghazi_network, supply_benghazi_line, is_fortress_axis_controlled(BENGHAZI) ? AXIS : ALLIED) trace_fortress_network(BENGHAZI, SS_BENGHAZI) } function update_tobruk_supply() { supply_tobruk_invalid = false init_trace_supply(supply_tobruk_network, supply_tobruk_line, is_fortress_axis_controlled(TOBRUK) ? AXIS : ALLIED) trace_fortress_network(TOBRUK, SS_TOBRUK) } 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 bardia_supply_line() { console.log("X") if (supply_bardia_invalid) update_bardia_supply() debug_hexes("bardia-line", supply_bardia_line) return supply_bardia_line } function bardia_supply_network() { if (supply_bardia_invalid) update_bardia_supply() return supply_bardia_network } function benghazi_supply_line() { if (supply_benghazi_invalid) update_benghazi_supply() return supply_benghazi_line } function benghazi_supply_network() { if (supply_benghazi_invalid) update_benghazi_supply() return supply_benghazi_network } function tobruk_supply_line() { if (supply_tobruk_invalid) update_tobruk_supply() return supply_tobruk_line } function tobruk_supply_network() { if (supply_tobruk_invalid) update_tobruk_supply() return supply_tobruk_network } function unit_supply_line(who) { // TODO: allow oasis supplied units to trace to base? switch (unit_supply(who)) { case SS_BARDIA: return bardia_supply_line() case SS_BENGHAZI: return benghazi_supply_line() case SS_TOBRUK: return tobruk_supply_line() } if (is_axis_unit(who)) return axis_supply_line() return allied_supply_line() } function unit_supply_distance(who) { // TODO: allow oasis supplied units to trace to base? switch (unit_supply(who)) { case SS_BARDIA: return distance_to[BARDIA] case SS_BENGHAZI: return distance_to[BENGHAZI] case SS_TOBRUK: return distance_to[TOBRUK] } 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 === // NOTE: we don't actually need path_from but we can use it to show paths taken in client // NOTE: we may need the path to reveal minefields moved through 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) { search_path_bfs(path_from[0], path_cost[0], start, 0, speed, false, null, null) search_path_bfs(path_from[1], path_cost[1], start, 1, speed + 1, false, null, null) search_path_bfs(path_from[2], path_cost[2], start, 2, speed + 2, false, null, null) search_path_bfs(path_from[4], path_cost[4], start, 4, speed + 4, false, null, null) } function search_move_retreat(start, speed) { search_path_bfs(path_from[0], path_cost[0], start, 0, speed, true, null, null) search_path_bfs(path_from[1], path_cost[1], start, 1, speed + 1, true, null, null) search_path_bfs(path_from[2], path_cost[2], start, 2, speed + 2, true, null, null) search_path_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_path_bfs(path_from[0], path_cost[0], start, 0, speed, false, sline, sdist) search_path_bfs(path_from[1], path_cost[1], start, 1, speed + 1, false, sline, sdist) search_path_bfs(path_from[2], path_cost[2], start, 2, speed + 2, false, sline, sdist) search_path_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_path_bfs(path_from[0], path_cost[0], start, 0, speed, true, sline, sdist) search_path_bfs(path_from[1], path_cost[1], start, 1, speed + 1, true, sline, sdist) search_path_bfs(path_from[2], path_cost[2], start, 2, speed + 2, true, sline, sdist) search_path_bfs(path_from[4], path_cost[4], start, 4, speed + 4, true, sline, sdist) } function search_redeploy(start) { search_path_redeploy_bfs(path_cost[0], start, 0) search_path_redeploy_bfs(path_cost[1], start, 1) search_path_redeploy_bfs(path_cost[2], start, 2) search_path_redeploy_bfs(path_cost[4], start, 4) } // Breadth First Search function search_path_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 search_path_redeploy_bfs(cost, start, road) { let path_enemy, friendly_network, enemy_network if (presence_invalid) update_presence() if (is_axis_player()) { path_enemy = presence_allied friendly_network = game.buildup.axis_network enemy_network = game.buildup.allied_network } else { path_enemy = presence_axis friendly_network = game.buildup.allied_network enemy_network = game.buildup.axis_network } cost.fill(63) cost[start] = 0 if (hex_road[start] < road) return let queue = [ start << 6 ] while (queue.length > 0) { let item = queue.shift() let here = item >> 6 let here_cost = item & 63 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] < 63) 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 // must stay within supply network, and not enter enemy network if (!friendly_network[next] || enemy_network[next]) continue // may not move into or through battle hexes let next_enemy = path_enemy[next] if (next_enemy) continue cost[next] = next_cost // don't care about distance (need to find home base for refit) if (next_cost < 63) queue.push(next << 6 | 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 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) { let speed = max_speed_of_undisrupted_and_unmoved_friendly_unit_in_hex(from) if (speed > 0) { search_move(from, speed + 1 + rommel) for (let x of all_hexes) if (!path_valid[x]) if (can_move_to(x, speed + 1 + rommel)) path_valid[x] = 1 } } // === SUPPLY COMMITMENT & TURN OPTION === function goto_turn_option() { game.state = 'turn_option' } function player_hand() { return is_axis_player() ? game.axis_hand : game.allied_hand } states.turn_option = { inactive: "turn option", prompt() { view.prompt = `Turn Option \u2014 committed ${game.commit[0]} real and ${game.commit[1]} dummy supply.` let hand = player_hand() if (game.commit[0] + game.commit[1] < 3) { if (hand[REAL] > 0) gen_action('real_card') if (hand[DUMMY] > 0) gen_action('dummy_card') } if (game.commit[0] >= 1) view.actions.basic = 1 else view.actions.basic = 0 if (game.commit[0] >= 2) view.actions.offensive = view.actions.assault = 1 else view.actions.offensive = view.actions.assault = 0 if (game.commit[0] >= 3) view.actions.blitz = 1 else view.actions.blitz = 0 if (game.commit[0] === 0) view.actions.pass = 1 else view.actions.pass = 0 }, basic() { push_undo() apply_turn_option('basic') }, offensive() { push_undo() apply_turn_option('offensive') }, assault() { push_undo() apply_turn_option('assault') }, blitz() { push_undo() apply_turn_option('blitz') }, pass() { push_undo() apply_turn_option('pass') }, real_card() { push_undo() let hand = player_hand() hand[REAL]-- game.commit[0]++ }, dummy_card() { push_undo() let hand = player_hand() hand[DUMMY]-- game.commit[1]++ }, } function apply_turn_option(option) { push_undo() game.turn_option = option let n = game.commit[0] + game.commit[1] if (n === 0) log(`Played zero supply cards.`) else if (n === 1) log(`Played one supply card.`) else if (n === 2) log(`Played two supply cards.`) else if (n === 3) log(`Played three supply cards.`) log_br() if (game.turn_option === 'pass') game.passed++ else game.passed = 0 goto_move_phase() } // === PLAYER TURN === function goto_player_turn() { set_active_player() 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 game.forced = [] set_clear(game.fired) set_clear(game.moved) game.commit = [ 0, 0 ] goto_initial_supply_check() } function end_player_turn() { clear_undo() // Forget partial retreats set_clear(game.partial_retreats) // Reveal supply cards log_br() log(`Supply Cards Revealed:\n${game.commit[0]} real and ${game.commit[1]} dummy.`) log_br() game.commit = [ 0, 0 ] if (check_sudden_death_victory()) return if (game.passed === 2) return end_month() if (game.phasing === AXIS) game.phasing = ALLIED else game.phasing = AXIS goto_player_turn() } // === FORTRESS SUPPLY === function union_fortress_network(result, fort) { for (let i = first_hex; i <= last_hex; ++i) result[i] |= fort[i] } function union_fortress_line(result, fort) { for (let i = 0; i < sidecount; ++i) result[i] |= fort[i] } function union_axis_network() { let net = axis_supply_network().slice() if (net[BARDIA] === 0 && is_fortress_axis_controlled(BARDIA)) union_fortress_network(net, bardia_supply_network()) if (net[BENGHAZI] === 0 && is_fortress_axis_controlled(BENGHAZI)) union_fortress_network(net, benghazi_supply_network()) if (net[TOBRUK] === 0 && is_fortress_axis_controlled(TOBRUK)) union_fortress_network(net, tobruk_supply_network()) return net } function union_allied_network() { let net = allied_supply_network().slice() if (net[BARDIA] === 0 && is_fortress_allied_controlled(BARDIA)) union_fortress_network(net, bardia_supply_network()) if (net[BENGHAZI] === 0 && is_fortress_allied_controlled(BENGHAZI)) union_fortress_network(net, benghazi_supply_network()) if (net[TOBRUK] === 0 && is_fortress_allied_controlled(TOBRUK)) union_fortress_network(net, tobruk_supply_network()) return net } function union_axis_line() { let net = axis_supply_network() let line = axis_supply_line().slice() if (net[BARDIA] === 0 && is_fortress_axis_controlled(BARDIA)) union_fortress_line(line, bardia_supply_line()) if (net[BENGHAZI] === 0 && is_fortress_axis_controlled(BENGHAZI)) union_fortress_line(line, benghazi_supply_line()) if (net[TOBRUK] === 0 && is_fortress_axis_controlled(TOBRUK)) union_fortress_line(line, tobruk_supply_line()) return line } function union_allied_line() { let net = allied_supply_network() let line = allied_supply_line().slice() console.log("Xa") if (net[BARDIA] === 0 && is_fortress_allied_controlled(BARDIA)) union_fortress_line(line, bardia_supply_line()) if (net[BENGHAZI] === 0 && is_fortress_allied_controlled(BENGHAZI)) union_fortress_line(line, benghazi_supply_line()) if (net[TOBRUK] === 0 && is_fortress_allied_controlled(TOBRUK)) union_fortress_line(line, tobruk_supply_line()) return line } const FORTRESS_HEX_LIST = [ BARDIA, BENGHAZI, TOBRUK ] const FORTRESS_SRC_LIST = [ SS_BARDIA, SS_BENGHAZI, SS_TOBRUK ] function all_friendly_unsupplied_units() { let unsupplied = [] for_each_friendly_unit_on_map(u => { if (is_unit_unsupplied(u)) unsupplied.push(u) }) return unsupplied } function resume_fortress_supply() { while (game.assign < 3) { if (assign_fortress_supply()) return game.assign++ } end_fortress_supply() } function end_fortress_supply() { game.assign = 0 if (game.state === 'initial_fortress_supply') game.state = 'initial_oasis_supply' if (game.state === 'final_fortress_supply') game.state = 'final_oasis_supply' if (game.state === 'buildup_fortress_supply') game.state = 'buildup_oasis_supply' resume_oasis_supply() } function list_fortress_supply_candidates(fortress) { let dist = distance_to[fortress] let list = [] init_trace_supply_to_base_or_fortress() for_each_friendly_unit_on_map(u => { if (is_unit_unsupplied(u)) if (can_trace_supply_to_base_or_fortress(fortress, unit_hex(u))) list.push(u) }) list.sort((a,b) => dist[unit_hex(a)] - dist[unit_hex(b)]) return list } function auto_assign_fortress_supply(list, fortress, ss, ix) { let total = 0 let dist = distance_to[fortress] while (list.length > 0 && game.capacity[ix] > 0) { let d0 = dist[unit_hex(list[0])] let n = 0 for (let u of list) if (dist[unit_hex(u)] === d0) ++n if (n <= game.capacity[ix]) { for (let u of list) if (dist[unit_hex(u)] === d0) set_unit_supply(u, ss) game.capacity[ix] -= n total += n list = list.slice(n) } else { return true } } return false } function assign_fortress_supply() { let ix = game.assign let ss = FORTRESS_SRC_LIST[ix] let fortress = FORTRESS_HEX_LIST[ix] let base_net = friendly_supply_network() // Isolated friendly fortress with capacity! if (!base_net[fortress] && is_fortress_friendly_controlled(fortress) && game.capacity[ix] > 0) { let dist = distance_to[fortress] let list = list_fortress_supply_candidates(fortress) if (auto_assign_fortress_supply(list, fortress, ss, ix)) { return true } } return false } const xxx_fortress_supply = { prompt() { let ix = game.assign let ss = FORTRESS_SRC_LIST[ix] let fortress = FORTRESS_HEX_LIST[ix] view.prompt = `Supply Check: Assign fortress supply to ${hex_name[fortress]} (${game.capacity[ix]} capacity left).` if (game.capacity[ix] > 0) { let list = list_fortress_supply_candidates(fortress) if (list.length > 0) { let dist = distance_to[fortress] let d0 = dist[unit_hex(list[0])] for (let u of list) if (dist[unit_hex(u)] === d0) gen_action_unit(u) } } gen_action_next() }, unit(who) { let ix = game.assign let ss = FORTRESS_SRC_LIST[ix] push_undo() game.capacity[ix]-- set_unit_supply(who, ss) }, next() { push_undo() game.assign++ resume_fortress_supply() }, } states.initial_fortress_supply = xxx_fortress_supply states.final_fortress_supply = xxx_fortress_supply states.buildup_fortress_supply = xxx_fortress_supply // === OASIS SUPPLY === const OASIS_HEX_LIST = [ JALO_OASIS, JARABUB_OASIS, SIWA_OASIS ] function resume_oasis_supply() { while (game.assign < 3) { if (assign_oasis_supply()) return game.assign++ } end_oasis_supply() } function end_oasis_supply() { if (game.state === 'initial_oasis_supply') goto_initial_supply_check_recover() if (game.state === 'final_oasis_supply') goto_final_supply_check_disrupt() if (game.state === 'buildup_oasis_supply') end_buildup_supply_check() } function assign_oasis_supply() { let ix = game.assign if (game.oasis[ix] > 0) { let oasis = OASIS_HEX_LIST[ix] let enemy_battle = is_axis_player() ? game.allied_hexes : game.axis_hexes if (has_friendly_unit(oasis)) { if (!set_has(enemy_battle, oasis)) { let n = count_friendly_units_in_hex(oasis) if (n > 1) return true if (n === 1) { for_each_friendly_unit_in_hex(oasis, u => { game.oasis[ix] = 0 set_unit_supply(u, SS_OASIS) }) } } } } return false } const xxx_oasis_supply = { prompt() { let ix = game.assign let oasis = OASIS_HEX_LIST[ix] view.prompt = `Supply Check: Assign oasis supply to ${hex_name[oasis]}.` for_each_friendly_unit_in_hex(oasis, u => { gen_action_unit(u) }) }, unit(who) { let ix = game.assign push_undo() game.oasis[ix] = 0 set_unit_supply(who, SS_OASIS) game.assign++ resume_oasis_supply() }, } states.initial_oasis_supply = xxx_oasis_supply states.final_oasis_supply = xxx_oasis_supply states.buildup_oasis_supply = xxx_oasis_supply // === INITIAL SUPPLY CHECK === function goto_initial_supply_check() { let base_net = friendly_supply_network() for_each_friendly_unit_on_map(u => { if (base_net[unit_hex(u)]) set_unit_supply(u, SS_BASE) else set_unit_supply(u, SS_NONE) }) if (is_axis_player()) game.capacity = [ 1, 1, 2 ] else game.capacity = [ 2, 2, 5 ] game.oasis = [ 1, 1, 1 ] game.state = 'initial_fortress_supply' game.assign = 0 resume_fortress_supply() } function goto_initial_supply_check_recover() { for (let u of game.recover) { if (is_unit_disrupted(u) && is_unit_supplied(u) && !is_battle_hex(unit_hex(u))) { log(`Recovered at #${unit_hex(u)}`) clear_unit_disrupted(u) } } set_clear(game.recover) for_each_enemy_unit_on_map(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_rout' } states.initial_supply_check_rout = { 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) } } // === FINAL SUPPLY CHECK === function goto_final_supply_check() { set_active_player() log_br() capture_fortress(BARDIA, 2) capture_fortress(BENGHAZI, 2) capture_fortress(TOBRUK, 5) game.disrupt = all_friendly_unsupplied_units() // Now in supply! let base_net = friendly_supply_network() for (let u of game.disrupt) { if (base_net[unit_hex(u)]) set_unit_supply(u, SS_BASE) } game.state = 'final_fortress_supply' game.assign = 0 resume_fortress_supply() } function goto_final_supply_check_disrupt() { for (let u of game.disrupt) { if (!is_unit_disrupted(u) && !is_unit_supplied(u)) { log(`Disrupted at #${unit_hex(u)}`) set_unit_disrupted(u) } } delete game.disrupt 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_rout' } states.final_supply_check_rout = { 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_movement() { game.from1 = game.from2 = game.to1 = game.to2 = 0 goto_forced_marches() } // === GROUP AND REGROUP MOVEMENT === function goto_move() { if (game.rommel === 1) { if (game.from1 && game.to1) log(`Rommel Regroup move\nfrom #${game.from1}\nto #${game.to1}.`) else if (game.from1) log(`Rommel Group move\nfrom #${game.from1}.`) } else { if (game.from1 && game.to1) log(`Regroup move\nfrom #${game.from1}\nto #${game.to1}.`) else if (game.from1) log(`Group move\nfrom #${game.from1}.`) } if (game.rommel === 2) { if (game.from2 && game.to2) log(`Rommel Regroup move\nfrom #${game.from2}\nto #${game.to2}.`) else if (game.from2) log(`Rommel Group move\nfrom #${game.from2}.`) } else { if (game.from2 && game.to2) log(`Regroup move\nfrom #${game.from2}\nto #${game.to2}.`) else if (game.from2) log(`Group move\nfrom #${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 + 1 + rommel1) for_each_undisrupted_and_unmoved_friendly_unit_in_hex(from, u => { if (can_move_to(game.to1, unit_speed[u] + 1 + 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 + 1 + rommel2) for_each_undisrupted_and_unmoved_friendly_unit_in_hex(from, u => { if (can_move_to(game.to2, unit_speed[u] + 1 + 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_enemy_rout_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, 1 + (rommel1 | rommel2)) else search_move(unit_hex(game.selected), unit_speed[game.selected] + 1 + (rommel1 | rommel2)) gen_move() } }, unit(who) { apply_select(who) }, forced_march(to) { this.hex(to) }, 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() { push_undo() // XXX clear_undo() log_br() end_movement() } } states.overrun = { prompt() { view.prompt = `Overrun!` for (let x of all_hexes) if (is_enemy_rout_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) { if (can_move_to(to, speed + rommel1)) gen_action_hex(to) else if (can_move_to(to, speed + 1 + rommel1)) gen_action_forced_march(to) } } } if (!game.to2 && game.from2 === from) { for (let to of all_hexes) { if (to != from) { if (can_move_to(to, speed + rommel2)) gen_action_hex(to) else if (can_move_to(to, speed + 1 + rommel2)) gen_action_forced_march(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) else if (can_move_to(game.to1, speed + 1 + rommel1)) gen_action_forced_march(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) else if (can_move_to(game.to2, speed + 1 + rommel2)) gen_action_forced_march(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 + 1 + (rommel1 | rommel2)) if (!game.to1 && game.from1 === from) if (can_move_to(to, speed + 1 + rommel1)) return move_unit(who, to, speed + 1 + rommel1, 1) if (!game.to2 && game.from2 === from) if (can_move_to(to, speed + 1 + rommel2)) return move_unit(who, to, speed + 1 + rommel2, 2) if (game.to1 === to && is_hex_or_adjacent_to(from, game.from1)) if (can_move_to(to, speed + 1 + rommel1)) return move_unit(who, to, speed + 1 + rommel1, 1) if (game.to2 === to && is_hex_or_adjacent_to(from, game.from2)) if (can_move_to(to, speed + 1 + rommel2)) return move_unit(who, to, speed + 1 + rommel2, 2) throw Error("bad move") } // 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] // too far if (cost + 1 > speed + road) return 0 // can't cross this hexside if (max_side === 0) return 0 // must stay on road for current bonus if (side_road[side] < road) return 0 // must stop on enemies if (has_enemy_unit(via)) return 0 // may not exceed hexside limit if (has_enemy_unit(to)) if ((game.side_limit[side] | 0) >= max_side) return 0 if (cost + 1 === speed + road) return 2 return 1 } function is_forced_march_move(from, to, speed) { let result = true if (hex_road[from] >= 0 && path_cost[0][to] < speed + 0) result = false if (hex_road[from] >= 1 && path_cost[1][to] < speed + 1) result = false if (hex_road[from] >= 2 && path_cost[2][to] < speed + 2) result = false if (hex_road[from] >= 4 && path_cost[4][to] < speed + 4) result = false return result } function move_via(who, to, speed, move) { let from = unit_hex(who) game.hexside = { who: who, to: to, via: [], forced: [], move: move } for_each_adjacent_hex(to, via => { let forced = false let unforced = false function check_road(bonus) { if (hex_road[from] >= bonus) { let k = can_move_via(via, to, speed, bonus) if (k === 2) forced = true else if (k === 1) unforced = true } } check_road(4) check_road(2) check_road(1) check_road(0) if (unforced) { game.hexside.via.push(via) game.hexside.forced.push(0) } else if (forced) { game.hexside.via.push(via) game.hexside.forced.push(1) } }) return game.hexside.via.length === 1 } function move_unit(who, to, speed, move) { let from = unit_hex(who) if (is_forced_march_move(from, to, speed)) { if (move_via(who, to, speed, move)) { forced_march_via(who, game.hexside.via[0], to, move) delete game.hexside } else { game.state = 'forced_march_via' } } else if (has_enemy_unit(to)) { if (move_via(who, to, speed, move)) { if (game.hexside.forced[0]) forced_march_via(who, game.hexside.via[0], to, move) else engage_via(who, game.hexside.via[0], to) delete game.hexside } else { game.state = 'engage_via' } } else { log(`>from #${from} to #${to}`) set_unit_moved(who) set_unit_hex(who, to) } } states.forced_march_via = { prompt() { view.prompt = `Move: Select which path to take.` view.selected = game.hexside.who for (let x of game.hexside.via) gen_action_hex(x) }, hex(via) { forced_march_via(game.hexside.who, via, game.hexside.to, game.hexside.move) delete game.hexside game.state = 'move' } } states.engage_via = { prompt() { view.prompt = `Move: Select which hex side to cross.` view.selected = game.hexside.who for (let i = 0; i < game.hexside.via.length; ++i) if (game.hexside.forced[i]) gen_action_forced_march(game.hexside.via[i]) else gen_action_hex(game.hexside.via[i]) }, forced_march(via) { forced_march_via(game.hexside.who, via, game.hexside.to, game.hexside.move) delete game.hexside game.state = 'move' }, hex(via) { engage_via(game.hexside.who, via, game.hexside.to) delete game.hexside game.state = 'move' } } function forced_march_via(who, via, to, move) { let from = unit_hex(who) set_unit_moved(who) set_unit_hex(who, via) // remember where we should advance to / return to if ((move === 1 && game.to1) || (move === 2 && game.to2)) game.forced.push([who, to, from]) else game.forced.push([who, to]) // attempted force marches affect hexside limits if (has_enemy_unit(to)) { let side = to_side_id(via, to) if (game.side_limit[side]) game.side_limit[side] = 2 else game.side_limit[side] = 1 } log(`>forced march from #${from} via #${via} to #${to}`) } 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) } if (from !== via) log(`>from #${from} via #${via} to #${to}`) else log(`>from #${from} to #${to}`) } // === FORCED MARCHES === function goto_forced_marches() { if (game.forced.length > 0) game.state = 'forced_marches' else end_forced_marches() } states.forced_marches = { prompt() { view.prompt = `Forced Marches!` for (let [who, to] of game.forced) gen_action_unit(who) }, unit(who) { push_undo() // XXX let via = unit_hex(who) let ix = game.forced.findIndex(item => who === item[0]) let to = game.forced[ix][1] let from = game.forced[ix][2] || via let roll = roll_die() if (roll >= 4) { log(`Forced March roll ${die_face_hit[roll]} success.`) if (has_enemy_unit(to)) { engage_via(who, via, to, false) } else { set_unit_hex(who, to) log(`>from #${via} to #${to}`) } } else { log(`Forced March roll ${die_face_miss[roll]} failed!`) if (from !== via) { log(`>returned to #${from}`) set_unit_hex(who, from) } if (is_unit_disrupted(who)) reduce_unit(who) // was a retreating unit else set_unit_disrupted(who) } game.forced.splice(ix, 1) goto_forced_marches() } } function end_forced_marches() { game.side_limit = {} game.forced = null goto_forced_marches_rout() } function goto_forced_marches_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_refuse_battle() else if (n === 1) goto_rout(where, false, goto_forced_marches_rout) else game.state = 'forced_marches_rout' } states.forced_marches_rout = { prompt() { view.prompt = `Forced Marches: Rout!` for (let x of all_hexes) if (is_friendly_hexside(x)) gen_action_hex(x) }, hex(where) { goto_rout(where, false, goto_forced_marches_rout) } } // === 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) { if (!has_undisrupted_friendly_unit(from)) return false 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, 1 + rommel) else return can_unit_disengage_and_move_to(who, to, 1 + 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, extra) { search_withdraw_retreat(who, extra) return can_move_to(to, unit_speed[who] + extra) } function can_unit_disengage_and_move_to(who, to, extra) { search_move_retreat(unit_hex(who), unit_speed[who] + extra) return can_move_to(to, unit_speed[who] + extra) } 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_movement() } } 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 (let u of game.retreat_units) hide_unit(u) 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, 1 + (rommel1 | rommel2)) else search_move_retreat(unit_hex(game.selected), unit_speed[game.selected] + 1 + (rommel1 | rommel2)) gen_move() } }, unit(who) { apply_select(who) }, forced_march(to) { this.hex(to) }, hex(to) { let who = game.selected apply_move(to) set_unit_disrupted(who) }, end_retreat() { end_retreat() } } function end_retreat() { if (!is_battle_hex(game.retreat)) { release_hex_control(game.retreat) hide_units_in_hex(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_movement() } // === REFUSE BATTLE === function can_select_refuse_battle_hex() { for (let x of game.active_battles) if (can_all_refuse_battle(x)) return true return false } function goto_refuse_battle() { set_passive_player() if (can_select_refuse_battle_hex()) { 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() { 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() { 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) { if (is_battle_hex(fortress)) { if (game.phasing === AXIS) return is_fortress_allied_controlled() else return is_fortress_axis_controlled() } return false } function goto_combat_phase() { set_active_player() if (game.turn_option === 'pass') { if (is_mandatory_combat(BARDIA)) return goto_rout(BARDIA, false, goto_combat_phase) if (is_mandatory_combat(BENGHAZI)) return goto_rout(BENGHAZI, false, goto_combat_phase) if (is_mandatory_combat(TOBRUK)) return goto_rout(TOBRUK, false, goto_combat_phase) } else { if (is_mandatory_combat(BARDIA)) set_add(game.active_battles, BARDIA) if (is_mandatory_combat(BENGHAZI)) set_add(game.active_battles, BENGHAZI) if (is_mandatory_combat(TOBRUK)) set_add(game.active_battles, TOBRUK) } let n = count_and_reveal_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') { goto_blitz_turn() } else { goto_final_supply_check() } } function goto_blitz_turn() { log_h2(`Blitz Turn`) if (game.rommel) game.rommel = 3 set_clear(game.fired) game.turn_option = 'second blitz' goto_move_phase() } // === 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.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 if (is_assault_battle()) cv *= 2 // Double dice for non-armor defenders in fortress! if (fc !== ARMOR && is_fortress_defensive_fire()) cv *= 2 let fp = FIREPOWER_MATRIX[fc][tc] let result = [] let total = 0 for (let i = 0; i < cv; ++i) { let roll = roll_die() if (roll >= fp) { result.push(die_face_hit[roll]) ++total } else { result.push(die_face_miss[roll]) } } // Double defense in minefields! if (fc !== ARTILLERY && is_minefield_offensive_fire()) total = total / 2 game.flash = `${class_name_cap[fc]} ${firepower_name[fp]} ${result.join("")} at ${class_name[tc]}` log(game.flash) return total } function goto_battle(x) { game.battle = x if (is_assault_battle()) log_h3(`Assault in #${x}`) else log_h3(`Battle in #${x}`) // goto defensive fire set_passive_player() game.state = 'battle_fire' game.hits = [ 0, 0, 0, 0 ] } function end_battle() { if (!is_battle_hex(game.battle)) { release_hex_control(game.battle) hide_units_in_hex(game.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 // XXX if (true) { 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_elite_unit(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: ${format_allocate_hits()} from Defensive Fire.` else view.prompt = `Battle: ${format_allocate_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: ${format_allocate_hits()} from Defensive Fire.` else view.prompt = `Probe: ${format_allocate_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 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 goto_rout_fire(where) { set_enemy_player() game.hits = 0 game.pursuit = where let slowest = slowest_enemy_unit_speed(game.pursuit) log(`Slowest was ${speed_name[slowest]} unit.`) game.state = 'rout_fire' } function goto_pursuit_fire_during_retreat(where) { set_passive_player() game.hits = 0 game.pursuit = where let slowest = slowest_undisrupted_enemy_unit_speed(game.pursuit) log(`Slowest was ${speed_name[slowest]} unit.`) game.state = 'pursuit_fire' } function goto_pursuit_fire_during_refuse_battle(where) { set_active_player() game.hits = 0 game.pursuit = where let slowest = slowest_undisrupted_enemy_unit_speed(game.pursuit) log(`Slowest was ${speed_name[slowest]} unit.`) game.state = 'pursuit_fire' } function format_allocate_hits() { let hits = 0 if (typeof game.hits === 'number') hits = game.hits else hits = game.hits[1] + game.hits[2] + game.hits[3] if (hits === 0) return `Allocate zero hits` else if (hits === 1) return `Allocate 1 hit` else return `Allocate ${hits} hits` } function goto_rout_hits() { set_enemy_player() game.flash = "" if (game.hits > 0) game.state = 'rout_hits' else end_rout_fire() } function goto_pursuit_hits() { set_enemy_player() game.flash = "" if (game.hits > 0) game.state = 'pursuit_hits' else end_pursuit_fire() } function roll_pursuit_fire_imp(who, n, hp) { let speed = unit_speed[who] if (n === 2) { let a = roll_die() let b = roll_die() if (a >= 4) { game.hits++ a = die_face_hit[a] } else { a = die_face_miss[a] } if (b >= 4) { game.hits++ b = die_face_hit[b] } else { b = die_face_miss[b] } game.flash = `${speed_name_cap[speed]} fired ${a}${b}` } if (n === 1) { let a = roll_die() game.flash = `${speed_name_cap[speed]} fired ${a}` if (a >= 4) { game.hits++ a = die_face_hit[a] } else { a = die_face_miss[a] } } log(game.flash) 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 done = true if (game.hits < count_hp_in_pursuit()) { 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) done = false } }) } if (done) gen_action('end_fire') else gen_action('withhold') }, unit(who) { let slowest = slowest_undisrupted_enemy_unit_speed(game.pursuit) roll_pursuit_fire(who, (unit_speed[who] > slowest ? 2 : 1)) set_unit_fired(who) }, withhold() { goto_pursuit_hits() }, end_fire() { goto_pursuit_hits() }, } states.rout_fire = { inactive: "rout fire (fire)", prompt() { view.prompt = `Pursuit Fire (Rout).` let done = true if (game.hits < count_hp_in_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) done = false } }) } if (done) gen_action('end_fire') else gen_action('withhold') }, unit(who) { let slowest = slowest_enemy_unit_speed(game.pursuit) roll_rout_fire(who, (unit_speed[who] > slowest ? 2 : 1)) set_unit_fired(who) }, withhold() { goto_rout_hits() }, end_fire() { goto_rout_hits() }, } function gen_pursuit_hits(normal_steps, elite_steps, iterate) { let done = true iterate(game.pursuit, u => { if (is_elite_unit(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: " + format_allocate_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: " + format_allocate_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() } // === BUILDUP - SUPPLY CHECK === function end_month() { // Forget captured fortresses (for bonus cards) clear_fortresses_captured() if (game.month === current_scenario().end) return end_game() goto_buildup() } function goto_buildup() { ++game.month log_h1(`Month ${game.month}`) game.phasing = AXIS set_active_player() goto_buildup_discard() } function goto_buildup_discard() { log_br() game.state = 'buildup_discard' let hand = player_hand() if (hand[REAL] + hand[DUMMY] === 0) end_buildup_discard() } states.buildup_discard = { prompt() { view.prompt = "Buildup: Discard any unwanted dummy cards." let hand = player_hand() if (hand[DUMMY] > 0) gen_action('dummy_card') gen_action_next() }, dummy_card() { push_undo() log(game.active + " discarded dummy supply.") let hand = player_hand() hand[DUMMY]-- }, next() { clear_undo() goto_buildup_supply_check() }, } function goto_buildup_supply_check() { let base_net = friendly_supply_network() for_each_friendly_unit_on_map(u => { if (base_net[unit_hex(u)]) set_unit_supply(u, SS_BASE) else set_unit_supply(u, SS_NONE) }) if (is_axis_player()) game.capacity = [ 1, 1, 2 ] else game.capacity = [ 2, 2, 5 ] game.oasis = [ 1, 1, 1 ] game.state = 'buildup_fortress_supply' game.assign = 0 resume_fortress_supply() } function end_buildup_supply_check() { if (is_axis_player()) { set_enemy_player() goto_buildup_discard() } else { set_enemy_player() goto_buildup_supply_check_recover() } } function goto_buildup_supply_check_recover() { for_each_friendly_unit_on_map(u => { if (is_unit_supplied(u) && is_unit_disrupted(u) && !is_battle_hex(unit_hex(u))) { log(`Recovered at #${unit_hex(u)}.`) clear_unit_disrupted(u) } }) resume_buildup_eliminate_unsupplied() } function resume_buildup_eliminate_unsupplied() { game.state = 'buildup_eliminate_unsupplied' let done = true for_each_friendly_unit_on_map(u => { if (is_unit_unsupplied(u)) done = false }) if (done) { if (is_axis_player()) { set_enemy_player() goto_buildup_supply_check_recover() } else { goto_buildup_point_determination() } } } states.buildup_eliminate_unsupplied = { prompt() { view.prompt = `Buildup: Eliminate unsupplied units.` for_each_friendly_unit_on_map(u => { if (is_unit_unsupplied(u)) gen_action_unit(u) }) }, unit(u) { log(`Eliminated at #${unit_hex(u)}`) eliminate_unit(u) resume_buildup_eliminate_unsupplied() }, } // === BUILDUP - POINT DETERMINATION === function init_buildup() { game.buildup = { // redeployment network axis_network: union_axis_network(), axis_line: union_axis_line(), allied_network: union_allied_network(), allied_line: union_allied_line(), // extra cards purchased axis_cards: 0, allied_cards: 0, // remaining port capacity for sea redeployment bardia: 2, benghazi: 2, tobruk: 5, } } function goto_buildup_point_determination() { let axis, allied // take union of supply networks? init_buildup() log_br() if (game.scenario === "1940") { axis = roll_die() allied = roll_die() log(`Axis rolled ${axis}.`) log(`Allied rolled ${allied}.`) } else { let axis_a = roll_die() let axis_b = roll_die() let allied_a = roll_die() let allied_b = roll_die() axis = axis_a + axis_b allied = allied_a + allied_b log(`Axis rolled ${axis_a} + ${axis_b}.`) log(`Allied rolled ${allied_a} + ${allied_b}.`) } log(`Receive ${axis + allied} BPs.`) game.axis_bps += axis + allied game.allied_bps += axis + allied if (allied <= axis) game.phasing = ALLIED else game.phasing = AXIS set_active_player() goto_buildup_reinforcements() } function have_scheduled_reinforcements() { let refit = friendly_refit() for (let u = first_friendly_unit; u <= last_friendly_unit; ++u) { let x = unit_hex(u) if (x === refit || x === hexdeploy + game.month || x === hexdeploy + game.month + 1) return true } } function goto_buildup_reinforcements() { log_h2(game.active + " Buildup") if (have_scheduled_reinforcements()) game.state = 'buildup_reinforcements' else goto_buildup_spending() } states.buildup_reinforcements = { prompt() { view.prompt = `Buildup: Bring on reinforcements.` gen_action_hex(friendly_base()) }, hex(base) { apply_reinforcements() goto_buildup_spending() }, } function apply_reinforcements() { let base = friendly_base() let refitted = 0 let scheduled = 0 let early = 0 if (is_battle_hex(base)) base = friendly_queue() for_each_friendly_unit_in_hex(friendly_refit(), u => { set_unit_hex(u, base) set_unit_supply(u, SS_BASE) refitted++ }) for_each_friendly_unit_in_month(game.month, u => { set_unit_hex(u, base) set_unit_supply(u, SS_BASE) scheduled++ }) if (game.month < current_scenario().end) { for_each_friendly_unit_in_month(game.month + 1, u => { if (roll_die() <= 2) { set_unit_hex(u, base) set_unit_supply(u, SS_BASE) early++ } }) } log(`Reinforcements at #${base}:`) if (refitted > 0) log(`>${refitted} refitted`) log(`>${scheduled} on schedule`) log(`>${early} early`) } // === BUILDUP - SPENDING BPS === function goto_buildup_spending() { game.state = 'spending_bps' } function available_bps() { if (is_axis_player()) return game.axis_bps else return game.allied_bps } function pay_bps(n) { if (is_axis_player()) game.axis_bps -= n else game.allied_bps -= n } function count_secret_minefields() { let n = 0 if (is_axis_player()) { let network = game.buildup.axis_network for (let x of game.axis_minefields) if (network[x]) ++n } else { let network = game.buildup.allied_network for (let x of game.allied_minefields) if (network[x]) ++n } return n } function replacement_cost(who) { let cost = (unit_class[who] === INFANTRY) ? (unit_speed[who] > 1) ? 2 : 1 : 3 if (is_elite_unit(who)) return 2 * cost return cost } function can_redeploy_from(from) { if (is_battle_hex(from)) { let n = 0 // TODO: can leave disrupted units behind to be routed? for_each_undisrupted_friendly_unit_in_hex(from, u => { n++ }) return n > 1 } return true } function is_controlled_port(where) { if (where === BARDIA || where === BENGHAZI || where === TOBRUK) return is_fortress_friendly_controlled(where) return true } function sea_redeploy_cost(from, to) { if ((from === BARDIA || to === BARDIA) && game.buildup.bardia === 0) return 0 if ((from === BENGHAZI || to === BENGHAZI) && game.buildup.benghazi === 0) return 0 if ((from === TOBRUK || to === TOBRUK) && game.buildup.tobruk === 0) return 0 if (is_controlled_port(from) && is_controlled_port(to)) { let b_from = is_fortress_besieged(from) let b_to = is_fortress_besieged(to) if (b_from && b_to) return 0 if (b_from || b_to) { if (is_axis_player()) return 0 return 4 } return 1 } return 0 } function gen_sea_redeployment(from, to) { let cost = sea_redeploy_cost(from, to) if (cost && cost <= available_bps()) gen_action_hex(to) } function gen_spending_bps() { let who = game.selected let bps = available_bps() let base = friendly_base() let from = unit_hex(who) // Receive replacement in base if (from === base) { if (unit_lost_steps(who) > 0 && bps <= replacement_cost(who)) view.actions.replacement = 1 else view.actions.replacement = 0 } // Quick deselect gen_action_hex(from) if (can_redeploy_from(from)) { search_redeploy(from) // Return for Refit if (from !== base) { if (can_move_to(base, 63)) gen_action_hex(friendly_refit()) } // Redeployment if (bps > 0) { for (let x of all_hexes) if (x !== from && can_move_to(x, 2)) gen_action_hex(x) } // Sea Redeployment if (from === base) { gen_sea_redeployment(base, BARDIA) gen_sea_redeployment(base, BENGHAZI) gen_sea_redeployment(base, TOBRUK) } if (from === BARDIA) { gen_sea_redeployment(BARDIA, BENGHAZI) gen_sea_redeployment(BARDIA, TOBRUK) gen_sea_redeployment(BARDIA, base) } if (from === BENGHAZI) { gen_sea_redeployment(BENGHAZI, BARDIA) gen_sea_redeployment(BENGHAZI, TOBRUK) gen_sea_redeployment(BENGHAZI, base) } if (from === TOBRUK) { gen_sea_redeployment(TOBRUK, BARDIA) gen_sea_redeployment(TOBRUK, BENGHAZI) gen_sea_redeployment(TOBRUK, base) } } } states.spending_bps = { prompt() { view.prompt = `Buildup: Spend buildup points (${available_bps()} remain).` if (game.selected < 0) { let bps = available_bps() if (count_secret_minefields() >= 2 || bps >= 15) gen_action('minefield') if (bps >= 10) gen_action('extra_supply_card') for_each_friendly_unit_on_map(u => { if (!is_unit_disrupted(u)) gen_action_unit(u) }) } else { gen_action_unit(game.selected) gen_spending_bps() } gen_action('end_buildup') }, unit(who) { if (game.selected < 0) { push_undo() game.selected = who game.changed = 0 } else { if (!game.changed) pop_undo() else game.selected = -1 } }, extra_supply_card() { push_undo() log(`Purchased extra supply card.`) pay_bps(10) }, minefield() { push_undo() game.state = 'minefield' }, replacement() { game.changed = 1 log(`Replaced unit.`) replace_unit(game.selected) pay_bps(replacement_cost(game.selected)) }, refit() { game.changed = 1 log(`Returned for Refit.`) hide_unit(game.selected) set_unit_hex(pop_selected(), friendly_refit()) }, hex(to) { game.changed = 1 let who = game.selected let from = unit_hex(who) if (to === from) { game.selected = -1 } else if (to === friendly_refit()) { log(`Returned for Refit.`) hide_unit(game.selected) set_unit_hex(pop_selected(), friendly_refit()) } else { search_redeploy(from) if (can_move_to(to, 2)) { log(`Redeployed to #${to}.`) pay_bps(1) } else { log(`Sea Redeployed to #${to}.`) if (is_fortress_besieged(from) || is_fortress_besieged(to)) pay_bps(4) else pay_bps(1) if (from === BARDIA || to === BARDIA) game.buildup.bardia-- if (from === BENGHAZI || to === BENGHAZI) game.buildup.benghazi-- if (from === TOBRUK || to === TOBRUK) game.buildup.tobruk-- } set_unit_hex(who, to) hide_unit(who) } }, end_buildup() { delete game.changed clear_undo() let n = available_bps() if (n > 20) { log(`Lost ${n - 20} unspent BPs.`) pay_bps(n - 20) } end_buildup_spending() } } function end_buildup_spending() { if (is_active_player()) { set_enemy_player() goto_buildup_reinforcements() } else { delete game.buildup goto_buildup_resupply() } } states.minefield = { prompt() { view.prompt = `Buildup: Build a minefield. (TODO!)` // Tear down two existing, or pay 15 }, } function goto_buildup_resupply() { log_h2("Resupply") log(`Shuffled supply cards.`) shuffle_cards() // Per-scenario allotment let axis_resupply = (game.month <= 10 || game.malta) ? 2 : 3 let allied_resupply = 3 // Extra cards purchased during buildup axis_resupply += game.buildup.axis_cards allied_resupply += game.buildup.allied_cards // Bonus from captured fortresses axis_resupply += game.axis_award allied_resupply += game.allied_award game.axis_award = 0 game.allied_award = 0 deal_axis_supply_cards(axis_resupply) deal_allied_supply_cards(allied_resupply) goto_player_initiative() } // === INITIATIVE === function goto_player_initiative() { game.phasing = AXIS set_passive_player() game.state = 'allied_player_initiative' } states.allied_player_initiative = { prompt() { view.prompt = "Initiative: You may challenge for the initiative." let hand = player_hand() if (hand[REAL] > 0) gen_action('real_card') if (hand[DUMMY] > 0) gen_action('dummy_card') gen_action_next() }, real_card() { log(`Allied challenged for the initiative.`) player_hand()[0]-- game.phasing = ALLIED set_passive_player() game.state = 'axis_player_initiative' }, dummy_card() { player_hand()[1]-- log(`Allied challenged for the initiative.`) set_active_player() game.state = 'axis_player_initiative' }, next() { goto_player_turn() } } states.axis_player_initiative = { prompt() { view.prompt = "Initiative: You may defend your initiative." let hand = player_hand() if (hand[REAL] > 0) gen_action('real_card') gen_action_next() }, real_card() { player_hand()[0]-- log("Axis defends the initiative.") if (game.phasing === ALLIED) log("Allied card was real.") else log("Allied card was a dummy.") game.phasing = AXIS goto_player_turn() }, next() { if (game.phasing === ALLIED) log("Allied siezed the initiative.") else log("Allied card was a dummy.") goto_player_turn() } } // === VICTORY CHECK === const EXIT_EAST_EDGE = [ 99, 123, 148 ] //, 172, 197 ] const EXIT_EAST = 172 function check_sudden_death_victory() { // Supplied units that move beyond the map "edge" exit the map. // Count the easternmost row of hexes and half-hexes. // In the original map this would be the half-hexes and the virtual hexes beyond the edge. for (let x of EXIT_EAST_EDGE) { for_each_axis_unit(u => { if (unit_hex(u) === x && is_unit_supplied(u)) { log(`Exited the east map edge.`) set_unit_hex(u, EXIT_EAST) } }) } let axis_exited = 0 for_each_axis_unit(u => { if (unit_hex(u) === EXIT_EAST) axis_exited++ }) if (is_axis_hex(ALEXANDRIA) || axis_exited >= 3) { log_br() log("Axis captured Alexandria!") return goto_game_over(AXIS, "Axis Strategic Victory!") } if (is_allied_hex(EL_AGHEILA)) { log_br() log("Allied captured El Agheila!") return goto_game_over(ALLIED, "Allied Strategic Victory!") } return false } function end_game() { let axis = 0 for_each_axis_unit_on_map(u => { axis += is_german_unit(u) ? 1.5 : 1.0 }) let allied = 0 for_each_allied_unit_on_map(u => { allied += 1.0 }) if (axis >= allied * 2) return goto_game_over(AXIS, "Axis Decisive Victory!") if (allied >= axis * 2) return goto_game_over(AXIS, "Allied Decisive Victory!") if (!is_fortress_besieged(TOBRUK)) { if (is_fortress_axis_controlled(TOBRUK)) return goto_game_over(AXIS, "Axis Positional Victory!") else return goto_game_over(ALLIED, "Allied Positional Victory!") } if (axis > allied) return goto_game_over(AXIS, "Axis Attrition Victory!") if (allied > axis) return goto_game_over(ALLIED, "Allied Attrition Victory!") return goto_game_over("Draw", "No Victory!") } // === DEPLOYMENT === function goto_free_deployment() { game.state = 'free_deployment' if (!has_friendly_unit_in_month(current_scenario().start)) end_free_deployment() } function is_valid_deployment_hex(base, x) { // we've already seen this hex during a previous supplied hex check this go if (supply_temp_network[x] > 0) return true if (can_trace_supply_to_base_or_fortress(base, x)) return true if (x === TOBRUK) return count_friendly_units_in_hex(x) < 5 if (x === JALO_OASIS || x === JARABUB_OASIS || x === SIWA_OASIS) return count_friendly_units_in_hex(x) < 1 return false } states.free_deployment = { inactive: "free deployment", prompt() { let scenario = current_scenario() let deploy = hexdeploy + scenario.start let axis = (game.active === AXIS) let done = true view.prompt = `Setup: ${game.active} Deployment.` view.prompt = `Setup: Deploy units in a supplied location in the setup area.` if (game.selected < 0) { for_each_friendly_unit_in_hex(deploy, u => { gen_action_unit(u) done = false }) } else { trace_total = 0 let base = friendly_base() init_trace_supply_to_base_or_fortress() for (let x of (axis ? scenario.axis_deployment : scenario.allied_deployment)) { if (!is_enemy_hex(x)) { let limit = 0 if (scenario.deployment_limit) limit = scenario.deployment_limit[x] | 0 if (!limit || count_friendly_units_in_hex(x) < limit) { if (is_valid_deployment_hex(base, x)) gen_action_hex(x) } } } gen_action_unit(game.selected) done = false console.log("DEPLOYMENT SUPPLY VISITS", trace_total) } 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) set_unit_supply(who, SS_BASE) }, next() { clear_undo() end_free_deployment() } } function end_free_deployment() { set_enemy_player() if (has_friendly_unit_in_month(current_scenario().start)) { log_h2("Allied Deployment") } else { goto_initial_supply_cards() } } function goto_initial_supply_cards() { game.phasing = AXIS set_active_player() log_br() let scenario = current_scenario() deal_axis_supply_cards(scenario.axis_initial_supply) deal_allied_supply_cards(scenario.allied_initial_supply) game.state = 'initial_supply_cards' } states.initial_supply_cards = { prompt() { view.prompt = `Setup: You may discard your entire hand and redraw a new one.` gen_action('discard') gen_action('keep') }, discard() { push_undo() // XXX if (is_axis_player()) { log(`Axis discarded their hand.`) game.axis_hand[REAL] = 0 game.axis_hand[DUMMY] = 0 deal_axis_supply_cards(current_scenario().axis_initial_supply) set_enemy_player() } else { log(`Allied discarded their hand.`) game.allied_hand[REAL] = 0 game.allied_hand[DUMMY] = 0 deal_allied_supply_cards(current_scenario().allied_initial_supply) begin_game() } }, keep() { push_undo() // XXX if (is_axis_player()) set_enemy_player() else begin_game() }, } function begin_game() { log_h1(`Month ${game.month}`) if (game.scenario === "Crusader") { game.phasing = ALLIED set_add(game.minefields, TOBRUK) } if (game.scenario === "Gazala") { // PreGame Buildup game.phasing = ALLIED init_buildup() game.axis_bps = 30 game.allied_bps = 30 set_active_player() goto_buildup_reinforcements() return } // 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 (unit_appearance[u] === 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 (unit_appearance[u] === a) list.push(u) return list } function setup_reinforcements(m) { for (let u = 0; u < unit_count; ++u) { if (unit_appearance[u] === m) { if (m === "M") set_unit_hex(u, MALTA) else set_unit_hex(u, hexdeploy + 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, unit_start_steps[u] + steps) else if (steps > 0) set_unit_steps(u, steps) set_unit_supply(u, SS_BASE) } } function current_scenario() { return SCENARIOS[game.scenario] } function bring_to_front(list, hex) { if (list.includes(hex)) { remove_from_array(list, hex) list.unshift(hex) } } function sort_deployment_for_axis(list) { list = list.slice() list.reverse() bring_to_front(list, BARDIA) bring_to_front(list, TOBRUK) bring_to_front(list, 88) bring_to_front(list, 58) // Mechili bring_to_front(list, 130) // Haraga bring_to_front(list, 113) // Ft Maddalena return list } function sort_deployment_for_allied(list) { list = list.slice() list.reverse() return list } const SCENARIOS = { "1940": { year: 1940, start: 1, end: 6, axis_deployment: sort_deployment_for_axis(region_libya_and_sidi_omar), allied_deployment: sort_deployment_for_allied(region_egypt), axis_initial_supply: 6, allied_initial_supply: 3, }, "1941": { year: 1941, start: 1, end: 10, axis_deployment: [], allied_deployment: sort_deployment_for_allied(region_egypt_and_libya), axis_initial_supply: 6, allied_initial_supply: 6, }, "Crusader": { year: 1941, start: 8, end: 10, axis_deployment: sort_deployment_for_axis(region_libya_and_sidi_omar_and_sollum), allied_deployment: sort_deployment_for_allied(region_egypt_and_tobruk), axis_initial_supply: 10, allied_initial_supply: 12, }, "Battleaxe": { year: 1941, start: 4, end: 10, axis_deployment: sort_deployment_for_axis(region_libya_except_tobruk), allied_deployment: sort_deployment_for_allied(region_egypt_and_tobruk), axis_initial_supply: 4, allied_initial_supply: 8, }, "1942": { year: 1942, start: 11, end: 20, axis_deployment: [ EL_AGHEILA, MERSA_BREGA ], allied_deployment: sort_deployment_for_allied(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: sort_deployment_for_axis(regions["West Line"]), allied_deployment: sort_deployment_for_allied(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: sort_deployment_for_axis(regions["Libya"]), allied_deployment: sort_deployment_for_allied(regions["Egypt"]), axis_initial_supply: 8, allied_initial_supply: 8, }, "1941-42": { year: 1941, start: 1, end: 20, axis_deployment: [], allied_deployment: sort_deployment_for_allied(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)) set_fortress_allied_controlled(fortress) if (scenario.axis_deployment.includes(fortress)) set_fortress_axis_controlled(fortress) } function setup(scenario_name) { let scenario = SCENARIOS[scenario_name] game.month = scenario.start log_h1(scenario_name) SETUP[scenario_name](-scenario.start) setup_fortress(scenario, BARDIA) setup_fortress(scenario, BENGHAZI) setup_fortress(scenario, TOBRUK) log_h2("Axis Deployment") game.phasing = AXIS set_active_player() goto_free_deployment() } // === PUBLIC FUNCTIONS === exports.roles = [ "Axis", "Allied" ] exports.scenarios = Object.keys(SCENARIOS) exports.setup = function (seed, scenario, options) { load_state({ seed: seed, log: [], undo: [], state: null, phasing: AXIS, active: AXIS, selected: -1, scenario: scenario, month: 0, draw_pile: [ 28, 14 ], // 28 real supply + 14 dummy supply axis_hand: [ 0, 0 ], allied_hand: [ 0, 0 ], axis_bps: 0, allied_bps: 0, units: new Array(unit_count).fill(0), revealed: [], moved: [], fired: [], raiders: [], recover: [], axis_minefields: [], allied_minefields: [], minefields: [], // fortress control fortress: 7, axis_award: 0, allied_award: 0, assign: 0, // battle hexes (defender) axis_hexes: [], allied_hexes: [], // hexside control (for battle hexes) axis_sides: [], allied_sides: [], // current turn option and selected moves commit: [0, 0], turn_option: null, passed: 0, side_limit: {}, forced: null, 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, revealed: game.revealed, moved: game.moved, fortress: game.fortress, axis_hand: game.axis_hand[REAL] + game.axis_hand[DUMMY], allied_hand: game.allied_hand[REAL] + game.allied_hand[DUMMY], commit: game.commit[0] + game.commit[1], axis_hexes: game.axis_hexes, allied_hexes: game.allied_hexes, axis_sides: game.axis_sides, allied_sides: game.allied_sides, } if (current === AXIS) view.cards = game.axis_hand if (current === ALLIED) view.cards = game.allied_hand if (current === game.active) view.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) if (game.buildup) { return { axis_supply: game.buildup.axis_network, axis_supply_line: game.buildup.axis_line, allied_supply: game.buildup.allied_network, allied_supply_line: game.buildup.allied_line, } } else { return { axis_supply: union_axis_network(), axis_supply_line: union_axis_line(), allied_supply: union_allied_network(), allied_supply_line: union_allied_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) } function gen_action_forced_march(x) { gen_action('forced_march', 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 roll_die() { return random(6) + 1 } 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) return true } 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 (game.state === 'game_over') { view.prompt = game.victory } else 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 }