"use strict" // TODO: MINEFIELDS // TODO: create minefields // TODO: tear down 2 minefields to create new minefield // TODO: track minefields to be revealed when moved through // TODO: reveal minefields at end of movement // TODO: legal pass withdrawal moves (reduce supply net, withdraw from fortress attack) // TODO: fortress combat in pass turns (must withdraw) // TODO: log summaries (deploy, rebuild, move, etc) // TODO: put initial deployment stack somewhere more accessible (spread out along the top?) // UI: pause after all fires (in case 0 hits the dialog disappears fast) // TODO: black hit outline in battles ("steploss/bad" action) and skip "apply 0 hits" step // TODO: undo push/clear // 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 month_names_1940 = [ "", "September 1940", "October 1940", "November 1940", "December 1940", "January 1940", "February 1940", ] const month_names = [ "", "April 1941", "May 1941", "June 1941", "July 1941", "August 1941", "September 1941", "October 1941", "November 1941", "December 1941", "January 1942", "February 1942", "March 1942", "April 1942", "May 1942", "June 1942", "July 1942", "August 1942", "September 1942", "October 1942", "November 1942", ] function current_month_name() { if (game.scenario === "1940") return month_names_1940[game.month] else return month_names[game.month] } 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 MF_AXIS = 0 const MF_ALLIED = 1 const MF_VISIBLE = 2 const MF_REVEAL = 3 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_except_tobruk = regions["Libya"].concat(regions["Sidi Omar"]).concat(regions["Sollum"]).filter(r => r !== TOBRUK) 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) { invalidate_caches() game.units[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) { let x = unit_hex(u) if (x >= first_hex && x <= last_hex) { if (is_unit_disrupted(u)) presence_axis[x] |= 1 else presence_axis[x] |= 2 } } presence_allied.fill(0) for (let u = first_allied_unit; u <= last_allied_unit; ++u) { let x = unit_hex(u) if (x >= first_hex && x <= last_hex) { if (is_unit_disrupted(u)) presence_allied[x] |= 1 else presence_allied[x] |= 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_raw_hex(x) { for (let u = first_friendly_unit; u <= last_friendly_unit; ++u) if (unit_hex(u) === x) return true return false } function has_friendly_unit_in_month(month) { return has_friendly_unit_in_raw_hex(hexdeploy + month) } 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_port_besieged(port) { if (port === EL_AGHEILA || port === ALEXANDRIA) return false return is_fortress_besieged(port) } 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 count_hex_or_adjacent_has_undisrupted_and_unmoved_friendly_unit(here) { let n = 0 for_each_hex_and_adjacent_hex(here, x => { for (let u = first_friendly_unit; u <= last_friendly_unit; ++u) { if (!is_unit_disrupted(u) && !is_unit_moved(u) && unit_hex(u) === x) { n++ return } } }) return n } 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 = 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 } } // 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 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 return has_supply } function trace_supply_network(start, ss) { supply_visited.fill(0) supply_net.fill(0) supply_line.fill(0) supply_src = start 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_net.fill(0) supply_line.fill(0) supply_src = fortress 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 = base 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 === 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, enemy_sides 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 enemy_sides = game.allied_sides } else { path_enemy = presence_axis friendly_network = game.buildup.allied_network enemy_network = game.buildup.axis_network enemy_sides = game.axis_sides } 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 // if disengaging from battle, must not cross enemy hexside if (here === start && path_enemy[here] && set_has(enemy_sides, side)) 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 move_road(to, speed) { if (path_cost[4][to] <= speed + 4) return 4 if (path_cost[2][to] <= speed + 2) return 2 if (path_cost[1][to] <= speed + 1) return 1 return 0 } 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 } } // === MINEFIELDS === function visit_hex(x) { let mf_enemy = (game.active === AXIS) ? MF_AXIS : MF_ALLIED if (set_has(game.minefields[mf_enemy], x)) set_add(game.minefields[MF_REVEAL], x) console.log("VISIT", x, hex_name[x]) } function visit_path(from, to, speed) { let road = move_road(to, speed) while (to && to !== from) { visit_hex(to) to = path_from[road][to] } } function reveal_visited_minefields() { for (let x of game.minefields[MF_REVEAL]) { log(`Minefield at #${x}.`) set_delete(game.minefields[MF_AXIS], x) set_delete(game.minefields[MF_ALLIED], x) set_add(game.minefields[MF_VISIBLE], x) } set_clear(game.minefields[MF_REVEAL]) } // === 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 log_br() 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 if (game.commit[0] + game.commit[1] > 0) { 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) { game.passed = 0 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() 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_and_undisrupted_units() { let result = [] for_each_friendly_unit_on_map(u => { if (!is_unit_disrupted(u) && is_unit_unsupplied(u)) result.push(u) }) return result } 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) } } // remember enemy units that can recover on their next turn 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() set_clear(game.fired) log_br() capture_fortress(BARDIA, 2) capture_fortress(BENGHAZI, 2) capture_fortress(TOBRUK, 5) // Raiders! for (let [u, side] of game.raiders) { let x = unit_hex(u) if (is_map_hex(x) && is_enemy_hexside(side)) { log(`Disrupted raider at #${x}.`) set_unit_disrupted(u) } } game.raiders.length = 0 // Unsupplied and in danger of disruption! game.disrupt = all_friendly_unsupplied_and_undisrupted_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) } // Assign leftover fortress and oasis supply 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_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() { set_clear(game.fired) 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() reveal_visited_minefields() 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 of all_hexes) { if (x === game.from1 && !game.to1) continue if (has_undisrupted_friendly_unit(x)) gen_action_hex(x) } if (game.turn_option !== 'pass') if (has_friendly_unit_in_raw_hex(friendly_queue())) gen_action_hex(friendly_queue()) }, 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 (x === friendly_queue()) { for_each_friendly_unit_in_hex(friendly_queue(), u => { set_unit_hex(u, friendly_base()) set_unit_moved(u) }) } 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 of all_hexes) { if (!is_enemy_hex(x)) { let n = count_hex_or_adjacent_has_undisrupted_and_unmoved_friendly_unit(x) // TODO: allow one-hex regroup moves? (failed forced march abuse) if (n >= 2) 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, move) delete game.hexside } else { game.state = 'engage_via' } } else { log(`>from #${from} to #${to}`) visit_path(from, to, speed) 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) { let rommel1 = (game.rommel === 1) ? 1 : 0 let rommel2 = (game.rommel === 2) ? 1 : 0 let who = game.hexside.who let from = unit_hex(who) let speed = unit_speed[who] search_move(from, speed + 1 + (rommel1 | rommel2)) 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) { let rommel1 = (game.rommel === 1) ? 1 : 0 let rommel2 = (game.rommel === 2) ? 1 : 0 let who = game.hexside.who let from = unit_hex(who) let speed = unit_speed[who] search_move(from, speed + 1 + (rommel1 | rommel2)) forced_march_via(game.hexside.who, via, game.hexside.to, game.hexside.move) delete game.hexside game.state = 'move' }, hex(via) { let rommel1 = (game.rommel === 1) ? 1 : 0 let rommel2 = (game.rommel === 2) ? 1 : 0 let who = game.hexside.who let from = unit_hex(who) let speed = unit_speed[who] search_move(from, speed + (rommel1 | rommel2)) engage_via(game.hexside.who, via, game.hexside.to, game.hexside.move) delete game.hexside game.state = 'move' } } function forced_march_via(who, via, to, move) { let speed = unit_speed[who] + (game.rommel === move ? 1 : 0) let from = unit_hex(who) set_unit_moved(who) set_unit_hex(who, via) visit_path(from, via, speed) // 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, move) { let speed = unit_speed[who] + (game.rommel === move ? 1 : 0) let from = unit_hex(who) set_unit_moved(who) set_unit_hex(who, to) visit_path(from, via, speed) visit_hex(to) if (from !== via) log(`>from #${from} via #${via} to #${to}`) else log(`>from #${from} to #${to}`) engage_via_hexside(who, via, to) } function engage_via_hexside(who, via, to) { let side = to_side_id(via, to) if (game.side_limit[side]) game.side_limit[side] = 2 else game.side_limit[side] = 1 if (is_unit_supplied(who)) { claim_hexside_control(side) } else { // new battle or enemy hexside (is_friendly_hexside is false before claiming control) if (!is_friendly_hexside(side)) game.raiders.push([who, side]) } if (is_new_battle_hex(to)) { claim_hex_control_for_defender(to) set_add(game.active_battles, 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.`) visit_hex(to) if (has_enemy_unit(to)) { engage_via_hexside(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 is_enemy_hexside(side) { if (is_allied_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('pass') }, 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) }, pass() { 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 can_rout_from_hex(from) { let n = 0 for_each_adjacent_hex(from, to => { let side = to_side_id(from, to) if (side_limit[side] > 0 && !is_enemy_hexside(side)) ++n }) return n > 0 } 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() log_h3(`Rout at #${from}`) // RULES: Will be disrupted again, so won't be able to recover. for_each_friendly_unit_in_hex(from, u => { set_delete(game.recover, u) }) if (can_rout_from_hex(from)) game.state = 'rout_attrition' else game.state = 'rout_elimination' } states.rout_elimination = { prompt() { view.prompt = "Rout: Eliminate all units that can not disengage." for_each_friendly_unit_in_hex(game.rout.from, u => { gen_action_unit(u) }) }, unit(who) { log(`Eliminated at #${game.rout.from}.`) eliminate_unit(who) if (!has_friendly_unit(game.rout.from)) end_rout() }, } 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(fortress) else return is_fortress_axis_controlled(fortress) } return false } function goto_combat_phase() { set_active_player() reveal_visited_minefields() 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.selected_hexes = 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 goto_select_battle() } else { end_combat_phase() } } } states.select_assault_battles = { inactive: "combat phase (select assault battles)", prompt() { view.prompt = `Select assault battles.` view.selected_hexes = 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() goto_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 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 at #${x}`) else log_h3(`Battle at #${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[ARMOR] <= 0 && hp[INFANTRY] <= 0 && hp[ANTITANK] <= 0)) gen_action('artillery') } } function apply_auto_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] // armor must target armor if possible if (fc === ARMOR && hp[ARMOR] > 0) return apply_battle_fire(ARMOR) // infantry must target infantry if possible if (fc === INFANTRY && hp[INFANTRY] > 0) return apply_battle_fire(INFANTRY) // only one class remains if (hp[ARMOR] > 0 && hp[INFANTRY] === 0 && hp[ANTITANK] === 0 && hp[ARTILLERY] === 0) return apply_battle_fire(ARMOR) if (hp[ARMOR] === 0 && hp[INFANTRY] > 0 && hp[ANTITANK] === 0 && hp[ARTILLERY] === 0) return apply_battle_fire(INFANTRY) if (hp[ARMOR] === 0 && hp[INFANTRY] === 0 && hp[ANTITANK] > 0 && hp[ARTILLERY] === 0) return apply_battle_fire(ANTITANK) if (hp[ARMOR] === 0 && hp[INFANTRY] === 0 && hp[ANTITANK] === 0 && hp[ARTILLERY] > 0) return apply_battle_fire(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_hit(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_hit(u) done = false } } } } }) if (done) gen_action('end_hits') return done } function apply_battle_hit(who) { game.flash = "" 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) if (game.selected >= 0) apply_auto_target(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_hit(who) { push_undo() apply_battle_hit(who) }, end_hits() { 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 && has_friendly_units_in_battle()) { // 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_hit(who) { push_undo() apply_battle_hit(who) }, end_hits() { 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[0] + 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 } }) } gen_action('end_fire') }, 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) }, 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 } }) } gen_action('end_fire') }, unit(who) { let slowest = slowest_enemy_unit_speed(game.pursuit) roll_rout_fire(who, (unit_speed[who] > slowest ? 2 : 1)) set_unit_fired(who) }, 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_hit(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_hit(u) done = false } } } }) if (done) gen_action('end_hits') } 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_hit(who) { push_undo() game.hits -= reduce_unit(who) }, end_hits() { 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_hit(who) { push_undo() game.hits -= reduce_unit(who) }, end_hits() { 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(current_month_name()) 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) refitted++ }) for_each_friendly_unit_in_month(game.month, u => { set_unit_hex(u, 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) 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.minefields[MF_AXIS]) if (network[x]) ++n } else { let network = game.buildup.allied_network for (let x of game.minefields[MF_ALLIED]) 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_port_friendly(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_port_friendly(from) && is_port_friendly(to)) { let b_from = is_port_besieged(from) let b_to = is_port_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) }) if (game.month >= 11 && has_friendly_unit_in_raw_hex(MALTA)) gen_action_hex(MALTA) } 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) { if (to === MALTA) { push_undo() log(`Malta Group arrived.`) log(`Axis Resupply dropped to 2.`) let base = friendly_base() if (is_battle_hex(base)) base = friendly_queue() for_each_friendly_unit_in_hex(MALTA, u => { set_unit_hex(u, base) }) game.malta = 1 return } 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 game.selected = -1 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 { 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) delete game.buildup 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, 148, 197 ] const EXIT_EAST = 47 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' game.selected = [] if (!has_friendly_unit_in_month(current_scenario().start)) end_free_deployment() } function is_valid_deployment_hex(base, x, n) { // 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) + n <= 5 if (x === JALO_OASIS || x === JARABUB_OASIS || x === SIWA_OASIS) return count_friendly_units_in_hex(x) + n <= 1 return false } states.free_deployment = { inactive: "free deployment", prompt() { let scenario = current_scenario() let deploy = hexdeploy + scenario.start let axis = (game.active === AXIS) view.prompt = `Setup: ${game.active} Deployment.` // view.prompt = `Setup: Deploy units in a supplied location in the setup area.` let done = true for_each_friendly_unit_in_hex(deploy, u => { gen_action_unit(u) done = false }) if (done) gen_action_next() if (game.selected.length > 0) { 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) + game.selected.length <= limit) { if (is_valid_deployment_hex(base, x, game.selected.length)) gen_action_hex(x) } } } console.log("DEPLOYMENT SUPPLY VISITS", trace_total) } }, unit(u) { set_toggle(game.selected, u) }, hex(to) { let list = game.selected game.selected = [] push_undo() log(`Deployed ${list.length} at #${to}.`) for (let who of list) { set_unit_hex(who, to) } }, next() { clear_undo() end_free_deployment() } } function end_free_deployment() { set_enemy_player() if (has_friendly_unit_in_month(current_scenario().start)) { game.selected = [] log_h2("Allied Deployment") } else { game.selected = -1 goto_initial_supply_cards() } } function goto_initial_supply_cards() { game.phasing = AXIS set_active_player() set_clear(game.fired) 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(current_month_name()) 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) set_unit_supply(u, SS_BASE) } } } 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": { 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": { 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": { start: 8, end: 10, axis_deployment: sort_deployment_for_axis(region_libya_and_sidi_omar_and_sollum_except_tobruk), allied_deployment: sort_deployment_for_allied(region_egypt_and_tobruk), axis_initial_supply: 10, allied_initial_supply: 12, }, "Battleaxe": { 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": { 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": { 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": { 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": { 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/allied/visible/to-be-revealed minefields: [[],[],[],[]], // revealed to both players // 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) let scenario = current_scenario() view = { month: game.month, start: scenario.start, end: scenario.end, 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, } let player_minefields = null if (current === AXIS) { view.cards = game.axis_hand if (game.minefields[MF_AXIS].length > 0) player_minefields = game.minefields[MF_AXIS] } else if (current === ALLIED) { view.cards = game.allied_hand if (game.minefields[MF_ALLIED].length > 0) player_minefields = game.minefields[MF_ALLIED] } if (game.minefields[MF_VISIBLE].length > 0) { if (player_minefields) view.minefields = game.minefields[MF_VISIBLE].concat(player_minefields) else view.minefields = game.minefields[MF_VISIBLE] } else { if (player_minefields) view.minefields = player_minefields } 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 if (game.fired.length > 0) view.fired = game.fired } 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_unit_hit(u) { gen_action('unit_hit', 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 }