"use strict" // TODO: fortress supply // TODO: oasis supply // TODO: raiders // TODO: minefields // TODO: gazala scenario // TODO: legal pass withdrawal moves (reduce supply net, withdraw from fortress attack) // TOOD: reveal/hide blocks (hexes) // TODO: group move from queue holding box to base // TODO: unit class/speed in battle flash // TODO: redeployment // TODO: return for refit // TODO: replacements // TODO: malta units // RULES: may units redeploying out of battles cross enemy controlled hexsides? // RULES: when is "fired" status cleared? // RULES: disrupted units routed again in second enemy turn, will they still recover? // assume yes, easy to change (remove from game.recover set if routed) // RULES: reveal minefields moved through (but not stopped at)? // TODO: 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, units, regions } = require("./data") function debug_hexes3(n, list) { console.log("--", n, "--") list = list.map((x,i) => hex_exists[i] ? x : "") for (let y = 0; y < hexh; ++y) console.log("".padStart(y*2," ") + list.slice(y*hexw, (y+1)*hexw).map(x=>String(x).padStart(3, ' ')).join(" ")) } function debug_hexes2(n, list) { console.log("--", n, "--") list = list.map((x,i) => hex_exists[i] ? x : "") for (let y = 0; y < hexh; ++y) console.log("".padStart(y*2," ") + list.slice(y*hexw, (y+1)*hexw).map(x=>String(x).padStart(3, ' ')).join(" ")) } function debug_hexes(n, list) { console.log("--", n, "--") list = list.map((x,i) => hex_exists[i] ? x : "") for (let y = 0; y < hexh; ++y) console.log("".padStart(y," ") + list.slice(y*hexw, (y+1)*hexw).map(x=>String(x).padStart(2, ' ')).join("")) } const AXIS = 'Axis' const ALLIED = 'Allied' const 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 firepower_name = [ "0", "1", "2", "3", "TF", "DF", "SF" ] const speed_name = [ "zero", "leg", "motorized", "mechanized", "recon" ] const SF = 6 const DF = 5 const TF = 4 const ARMOR = 0 const INFANTRY = 1 const ANTITANK = 2 const ARTILLERY = 3 const TRAIL = 1 const TRACK = 2 const HIGHWAY = 4 const SUPPLY_RANGE = [ 1, 2, 3, -1, 3 ] const FIREPOWER_MATRIX = [ [ SF, DF, SF, TF ], [ SF, SF, SF, TF ], [ DF, SF, SF, TF ], [ SF, DF, DF, SF ], ] // 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_EL_AGHEILA = 1 const SS_ALEXANDRIA = 2 const SS_BARDIA = 3 const SS_BENGHAZI = 4 const SS_TOBRUK = 5 const SS_OASIS = 6 const hex_from_supply_source = [ 0, EL_AGHEILA, ALEXANDRIA, BARDIA, BENGHAZI, TOBRUK ] function supply_source_from_hex(hex) { switch (hex) { case 0: return 0 case EL_AGHEILA: return 1 case ALEXANDRIA: return 2 case BARDIA: return 3 case BENGHAZI: return 4 case TOBRUK: return 5 } } const region_egypt = regions["Egypt"] const region_egypt_and_libya = regions["Libya"].concat(regions["Egypt"]) const region_libya_and_sidi_omar = regions["Libya"].concat(regions["Sidi Omar"]) const region_libya_and_sidi_omar_and_sollum = regions["Libya"].concat(regions["Sidi Omar"]).concat(regions["Sollum"]) const region_egypt_and_tobruk = regions["Egypt"].concat(regions["Tobruk"]) const region_libya_except_tobruk = regions["Libya"].filter(r => r !== TOBRUK) function calc_distance(a, b) { let ax = a % hexw, ay = (a / hexw)|0, az = -ax - ay let bx = b % hexw, by = (b / hexw)|0, bz = -bx - by return max(abs(bx-ax), abs(by-ay), abs(bz-az)) } function calc_distance_map(supply) { let map = new Array(hexcount) for (let x = 0; x < hexcount; ++x) map[x] = calc_distance(supply, x) return map } const distance_to = { [EL_AGHEILA]: calc_distance_map(EL_AGHEILA), [ALEXANDRIA]: calc_distance_map(ALEXANDRIA), [BENGHAZI]: calc_distance_map(BENGHAZI), [TOBRUK]: calc_distance_map(TOBRUK), [BARDIA]: calc_distance_map(BARDIA), } function to_side(a, b, s) { if (s < 3) return a * 3 + s return b * 3 + s - 3 } function to_side_id(a, b) { if (a > b) { let c = b b = a a = c } if (a + hexnext[0] === b) return a * 3 + 0 else if (a + hexnext[1] === b) return a * 3 + 1 else if (a + hexnext[2] === b) return a * 3 + 2 throw new Error("not a hexside " + a + " to " + b); } function is_map_hex(x) { return x >= first_hex && x <= last_hex && hex_exists[x] === 1 } function is_hex_or_adjacent_to(x, where) { if (x === where) return true for (let s = 0; s < 6; ++s) { let y = where + hexnext[s] if (is_map_hex(y) && x === y) return true } return false } // === STATE CACHES === const first_axis_unit = 0 const first_allied_unit = units.findIndex(item => item.nationality === 'allied') const last_axis_unit = first_allied_unit - 1 const last_allied_unit = units.length - 1 var presence_invalid = true var presence_axis = new Array(hexcount).fill(0) var presence_allied = new Array(hexcount).fill(0) var supply_axis_invalid = true var supply_axis_network = new Array(hexcount).fill(0) var supply_axis_line = new Array(sidecount).fill(0) var supply_allied_invalid = true var supply_allied_network = new Array(hexcount).fill(0) var supply_allied_line = new Array(sidecount).fill(0) var first_friendly_unit, last_friendly_unit var first_enemy_unit, last_enemy_unit function 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 load_state(state) { if (game !== state) { game = state presence_invalid = true supply_axis_invalid = true supply_allied_invalid = true update_aliases() } } // === UNIT STATE === // location (8 bits), supply source (3 bits), steps lost (2 bits), disrupted (1 bit) function apply_select(u) { if (game.selected === u) game.selected = -1 else game.selected = u } function pop_selected() { let u = game.selected game.selected = -1 return u } const UNIT_DISRUPTED_SHIFT = 0 const UNIT_DISRUPTED_MASK = 1 << UNIT_DISRUPTED_SHIFT const UNIT_STEPS_SHIFT = 1 const UNIT_STEPS_MASK = 3 << UNIT_STEPS_SHIFT const UNIT_SUPPLY_SHIFT = 3 const UNIT_SUPPLY_MASK = 7 << UNIT_SUPPLY_SHIFT const UNIT_HEX_SHIFT = 6 const UNIT_HEX_MASK = 255 << UNIT_HEX_SHIFT function is_unit_disrupted(u) { return (game.units[u] & UNIT_DISRUPTED_MASK) === UNIT_DISRUPTED_MASK } function set_unit_disrupted(u) { presence_invalid = true supply_axis_invalid = true supply_allied_invalid = true game.units[u] |= UNIT_DISRUPTED_MASK } function clear_unit_disrupted(u) { presence_invalid = true supply_axis_invalid = true supply_allied_invalid = true game.units[u] &= ~UNIT_DISRUPTED_MASK } function unit_hex(u) { return (game.units[u] & UNIT_HEX_MASK) >> UNIT_HEX_SHIFT } function set_unit_hex(u, x) { presence_invalid = true supply_axis_invalid = true supply_allied_invalid = true game.units[u] = (game.units[u] & ~UNIT_HEX_MASK) | (x << UNIT_HEX_SHIFT) } function is_unit_supplied(u) { return ((game.units[u] & UNIT_SUPPLY_MASK) >> UNIT_SUPPLY_SHIFT) !== 0 } function is_unit_unsupplied(u) { return ((game.units[u] & UNIT_SUPPLY_MASK) >> UNIT_SUPPLY_SHIFT) === 0 } function unit_supply(u) { let src = (game.units[u] & UNIT_SUPPLY_MASK) >> UNIT_SUPPLY_SHIFT return hex_from_supply_source[src] } function set_unit_supply(u, hex) { let src = supply_source_from_hex(hex) game.units[u] = (game.units[u] & ~UNIT_SUPPLY_MASK) | (src << UNIT_SUPPLY_SHIFT) } function unit_lost_steps(u) { return (game.units[u] & UNIT_STEPS_MASK) >> UNIT_STEPS_SHIFT } function set_unit_lost_steps(u, n) { game.units[u] = (game.units[u] & ~UNIT_STEPS_MASK) | (n << UNIT_STEPS_SHIFT) } function unit_steps(u) { return units[u].steps - unit_lost_steps(u) } function set_unit_steps(u, n) { set_unit_lost_steps(u, units[u].steps - n) } function is_unit_moved(u) { return set_has(game.moved, u) } function set_unit_moved(u) { set_add(game.moved, u) } function is_unit_fired(u) { return set_has(game.fired, u) } function set_unit_fired(u) { set_add(game.fired, u) } function eliminate_unit(u) { set_unit_hex(u, 0) set_unit_lost_steps(u, 0) } function reduce_unit(u) { let s = unit_steps(u) let hp = unit_hp_per_step(u) if (s === 1) eliminate_unit(u) else set_unit_steps(u, s - 1) return hp } // === UNIT DATA === function find_unit(name) { for (let u = 0; u < units.length; ++u) if (units[u].name === name) return u throw new Error("cannot find named block: " + name) } function unit_name(u) { return units[u].name } function unit_speed(u) { return units[u].speed } function unit_class(u) { return units[u].class } function is_artillery_unit(u) { return units[u].class === ARTILLERY } function is_armor_unit(u) { return units[u].class === ARMOR } function is_infantry_unit(u) { return units[u].class === INFANTRY } function is_antitank_unit(u) { return units[u].class === ANTITANK } function is_unit_elite(u) { return units[u].elite } function unit_cv(u) { if (is_unit_elite(u)) return unit_steps(u) * 2 return unit_steps(u) } function unit_hp_per_step(u) { return is_unit_elite(u) ? 2 : 1 } function unit_hp(u) { return unit_steps(u) * unit_hp_per_step(u) } function is_friendly_hex(x) { if (game.active === AXIS) return is_axis_hex(x) return is_allied_hex(x) } function is_enemy_hex(x) { if (game.active === ALLIED) return is_axis_hex(x) return is_allied_hex(x) } function is_allied_unit(u) { return u >= first_allied_unit && u <= last_allied_unit } function is_axis_unit(u) { return u >= first_axis_unit && u <= last_axis_unit } // === MAP STATE === function friendly_base() { if (is_axis_player()) return EL_AGHEILA return ALEXANDRIA } function friendly_queue() { if (is_axis_player()) return AXIS_QUEUE return ALLIED_QUEUE } function friendly_refit() { if (is_axis_player()) return AXIS_REFIT return ALLIED_REFIT } function update_presence() { presence_invalid = false presence_axis.fill(0) for (let u = first_axis_unit; u <= last_axis_unit; ++u) if (is_unit_disrupted(u)) presence_axis[unit_hex(u)] |= 1 else presence_axis[unit_hex(u)] |= 2 presence_allied.fill(0) for (let u = first_allied_unit; u <= last_allied_unit; ++u) if (is_unit_disrupted(u)) presence_allied[unit_hex(u)] |= 1 else presence_allied[unit_hex(u)] |= 2 } function has_friendly_unit_in_month(month) { for (let u = first_friendly_unit; u <= last_friendly_unit; ++u) if (unit_hex(u) === hexdeploy + month) return true return false } function for_each_friendly_unit_in_month(month, fn) { for (let u = first_friendly_unit; u <= last_friendly_unit; ++u) if (unit_hex(u) === hexdeploy + month) fn(u) } function has_axis_unit(x) { if (presence_invalid) update_presence() return presence_axis[x] !== 0 } function has_allied_unit(x) { if (presence_invalid) update_presence() return presence_allied[x] !== 0 } function has_undisrupted_axis_unit(x) { if (presence_invalid) update_presence() return (presence_axis[x] & 2) !== 0 } function has_disrupted_axis_unit(x) { if (presence_invalid) update_presence() return (presence_axis[x] & 1) !== 0 } function has_undisrupted_allied_unit(x) { if (presence_invalid) update_presence() return (presence_allied[x] & 2) !== 0 } function has_disrupted_allied_unit(x) { if (presence_invalid) update_presence() return (presence_allied[x] & 1) !== 0 } function has_unshielded_disrupted_axis_unit(x) { if (presence_invalid) update_presence() return presence_axis[x] === 1 } function has_unshielded_disrupted_allied_unit(x) { if (presence_invalid) update_presence() return presence_allied[x] === 1 } function is_axis_hex(x) { if (presence_invalid) update_presence() return (presence_axis[x] !== 0) && (presence_allied[x] === 0) } function is_allied_hex(x) { if (presence_invalid) update_presence() return (presence_axis[x] === 0) && (presence_allied[x] !== 0) } function is_battle_hex(x) { if (presence_invalid) update_presence() return (presence_axis[x] !== 0) && (presence_allied[x] !== 0) } function is_empty_hex(x) { if (presence_invalid) update_presence() return (presence_axis[x] === 0) && (presence_allied[x] === 0) } function has_friendly_unit(x) { if (game.active === AXIS) return has_axis_unit(x) return has_allied_unit(x) } function has_undisrupted_friendly_unit(x) { if (game.active === AXIS) return has_undisrupted_axis_unit(x) return has_undisrupted_allied_unit(x) } function has_enemy_unit(x) { if (game.active === ALLIED) return has_axis_unit(x) return has_allied_unit(x) } function has_disrupted_enemy_unit(x) { if (game.active === ALLIED) return has_disrupted_axis_unit(x) return has_disrupted_allied_unit(x) } function has_undisrupted_enemy_unit(x) { if (game.active === ALLIED) return has_undisrupted_axis_unit(x) return has_undisrupted_allied_unit(x) } function has_unshielded_disrupted_enemy_unit(x) { if (game.active === ALLIED) return has_unshielded_disrupted_axis_unit(x) return has_unshielded_disrupted_allied_unit(x) } function has_unshielded_disrupted_friendly_unit(x) { if (game.active === ALLIED) return has_unshielded_disrupted_allied_unit(x) return has_unshielded_disrupted_axis_unit(x) } function is_overrun_hex(x) { return has_undisrupted_friendly_unit(x) && has_unshielded_disrupted_enemy_unit(x) } function is_enemy_rout_hex(x) { return has_undisrupted_friendly_unit(x) && has_unshielded_disrupted_enemy_unit(x) } function is_friendly_rout_hex(x) { return has_undisrupted_enemy_unit(x) && has_unshielded_disrupted_friendly_unit(x) } function is_new_battle_hex(a) { if (is_battle_hex(a)) return !set_has(game.axis_hexes, a) && !set_has(game.allied_hexes, a) return false } function claim_hexside_control(side) { if (is_axis_player()) { set_add(game.axis_sides, side) set_delete(game.allied_sides, side) } else { set_add(game.allied_sides, side) set_delete(game.axis_sides, side) } } function release_hex_control(a) { // no longer a battle hex: release hexsides if possible set_delete(game.axis_hexes, a) set_delete(game.allied_hexes, a) for_each_adjacent_hex(a, b => { if (!is_battle_hex(b)) { let side = to_side_id(a, b) set_delete(game.axis_sides, side) set_delete(game.allied_sides, side) } }) } function claim_hex_control_for_defender(a) { // a new battle hex: claim hex and hexsides for defender if (is_axis_player()) set_add(game.allied_hexes, a) else set_add(game.axis_hexes, a) for_each_adjacent_hex(a, b => { let side = to_side_id(a, b) if (side_limit[side] > 0) { if (is_axis_player()) { if (!set_has(game.axis_sides, side)) set_add(game.allied_sides, side) } else { if (!set_has(game.allied_sides, side)) set_add(game.axis_sides, side) } } }) } function capture_fortress(fortress, capacity) { if (!is_fortress_friendly_controlled(fortress)) { if (has_undisrupted_friendly_unit(fortress) && !has_enemy_unit(fortress)) { supply_axis_invalid = true supply_allied_invalid = true 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 === const FORTRESS_BIT = { [BARDIA]: 1, [BENGHAZI]: 2, [TOBRUK]: 4, } function is_fortress_axis_controlled(fortress) { return (game.fortress & FORTRESS_BIT[fortress]) === 0 } function set_fortress_axis_controlled(fortress) { game.fortress &= ~FORTRESS_BIT[fortress] } function set_fortress_allied_controlled(fortress) { game.fortress |= FORTRESS_BIT[fortress] } function set_fortress_captured(fortress) { let bit = FORTRESS_BIT[fortress] << 3 if (game.fortress & bit) return false game.fortress |= bit return true } function clear_fortresses_captured() { game.fortress &= 7 } function is_fortress_friendly_controlled(fortress) { if (is_axis_player()) return is_fortress_axis_controlled(fortress) return !is_fortress_axis_controlled(fortress) } function set_fortress_friendly_controlled(fortress) { if (is_axis_player()) set_fortress_axis_controlled(fortress) else set_fortress_allied_controlled(fortress) return set_fortress_captured(fortress) } function is_fortress_besieged(fortress) { let result = false let besieged = is_fortress_axis_controlled() ? 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_axis_unit(fn) { for (let u = first_axis_unit; u <= last_axis_unit; ++u) fn(u) } function for_each_axis_unit_on_map(fn) { for (let u = first_axis_unit; u <= last_axis_unit; ++u) if (is_map_hex(unit_hex(u))) fn(u) } function for_each_allied_unit(fn) { for (let u = first_allied_unit; u <= last_allied_unit; ++u) fn(u) } function for_each_allied_unit_on_map(fn) { for (let u = first_allied_unit; u <= last_allied_unit; ++u) if (is_map_hex(unit_hex(u))) fn(u) } function for_each_friendly_unit(fn) { for (let u = first_friendly_unit; u <= last_friendly_unit; ++u) fn(u) } function for_each_friendly_unit_on_map(fn) { for (let u = first_friendly_unit; u <= last_friendly_unit; ++u) if (is_map_hex(unit_hex(u))) fn(u) } function for_each_enemy_unit(fn) { for (let u = first_enemy_unit; u <= last_enemy_unit; ++u) fn(u) } function for_each_enemy_unit_on_map(fn) { for (let u = first_enemy_unit; u <= last_enemy_unit; ++u) if (is_map_hex(unit_hex(u))) fn(u) } function for_each_friendly_unit_in_hex(x, fn) { for (let u = first_friendly_unit; u <= last_friendly_unit; ++u) if (unit_hex(u) === x) fn(u) } function for_each_undisrupted_friendly_unit_in_hex(x, fn) { for (let u = first_friendly_unit; u <= last_friendly_unit; ++u) if (!is_unit_disrupted(u) && unit_hex(u) === x) fn(u) } function for_each_undisrupted_and_unmoved_friendly_unit_in_hex(x, fn) { for (let u = first_friendly_unit; u <= last_friendly_unit; ++u) if (!is_unit_disrupted(u) && unit_hex(u) === x && !is_unit_moved(u)) fn(u) } function hex_or_adjacent_has_undisrupted_and_unmoved_friendly_unit(here) { for (let u = first_friendly_unit; u <= last_friendly_unit; ++u) if (!is_unit_disrupted(u) && !is_unit_moved(u) && is_hex_or_adjacent_to(unit_hex(u), here)) return true return false } function for_each_enemy_unit_in_hex(x, fn) { for (let u = first_enemy_unit; u <= last_enemy_unit; ++u) if (unit_hex(u) === x) fn(u) } function for_each_undisrupted_enemy_unit_in_hex(x, fn) { for (let u = first_enemy_unit; u <= last_enemy_unit; ++u) if (!is_unit_disrupted(u) && unit_hex(u) === x) fn(u) } function count_battle_hexes() { let n = 0 for (let x of all_hexes) if (is_battle_hex(x)) ++n return n } function has_friendly_units_in_battle() { let result = false for_each_undisrupted_friendly_unit_in_hex(game.battle, u => { if (!is_unit_retreating(u)) result = true }) return result } function count_normal_steps_in_battle() { let steps = [ 0, 0, 0, 0 ] for_each_undisrupted_enemy_unit_in_hex(game.battle, u => { if (!is_unit_elite(u)) if (!is_unit_retreating(u)) steps[unit_class(u)] += unit_steps(u) }) return steps } function count_elite_steps_in_battle() { let steps = [ 0, 0, 0, 0 ] for_each_undisrupted_enemy_unit_in_hex(game.battle, u => { if (is_unit_elite(u)) if (!is_unit_retreating(u)) steps[unit_class(u)] += unit_steps(u) }) return steps } function count_hp_in_battle() { let hp = [ 0, 0, 0, 0 ] for_each_undisrupted_enemy_unit_in_hex(game.battle, u => { if (!is_unit_retreating(u)) hp[unit_class(u)] += unit_hp(u) }) return hp } function count_normal_steps_in_pursuit() { let steps = 0 for_each_undisrupted_enemy_unit_in_hex(game.pursuit, u => { if (!is_unit_elite(u)) steps += unit_steps(u) }) return steps } function count_elite_steps_in_pursuit() { let steps = 0 for_each_undisrupted_enemy_unit_in_hex(game.pursuit, u => { if (is_unit_elite(u)) steps += unit_steps(u) }) return steps } function count_hp_in_pursuit() { let hp = 0 for_each_undisrupted_enemy_unit_in_hex(game.pursuit, u => { hp += unit_hp(u) }) return hp } function count_normal_steps_in_rout() { let steps = 0 for_each_enemy_unit_in_hex(game.pursuit, u => { if (!is_unit_elite(u)) steps += unit_steps(u) }) return steps } function count_elite_steps_in_rout() { let steps = 0 for_each_enemy_unit_in_hex(game.pursuit, u => { if (is_unit_elite(u)) steps += unit_steps(u) }) return steps } function count_hp_in_rout() { let hp = 0 for_each_enemy_unit_in_hex(game.pursuit, u => { hp += unit_hp(u) }) return hp } // === SUPPLY CARDS === function draw_supply_card(pile) { let x = random(pile[0] + pile[1]) if (x < pile[0]) { pile[0] -- return 0 } else { pile[1] -- return 1 } } function deal_axis_supply_cards(n) { 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 var supply_visited = new Array(hexcount).fill(0) var supply_src = new Array(hexcount).fill(0) var trace_highway var trace_chain function is_supply_line_blocked(here, next, side) { // impassable hexside if (side_limit[side] === 0) return true // undisrupted (only) enemies may block supply lines if (supply_enemy[next] > 1) { if (supply_friendly[next] > 1) { // battle hex, can only trace through if defender if (!set_has(supply_defender, next)) return true } else { // enemy hex return true } } // cannot trace through enemy hexsides if (supply_friendly[here] && supply_enemy[here]) if (!set_has(supply_defender_sides, side)) return true return false } function trace_supply_highway(here, d) { trace_highway++ ind(d, "> highway", here) // TODO: hoist to call sites to avoid function call overhead if (supply_src[here]) { ind(d, "! source highway", here) return true } let has_supply = false supply_visited[here] = 1 for (let s = 0; s < 6; ++s) { let next = here + hexnext[s] if (supply_visited[next]) continue let side = to_side(here, next, s) if (is_supply_line_blocked(here, next, side)) continue let road = side_road[side] if (road === HIGHWAY) { if (supply_friendly[next] > 1) { ind(d, "? highway head", next) if (trace_supply_chain(next, d+1, 0, 3)) { ind(d, "< highway chain", here, next) supply_line[side] = 1 has_supply = true } } else { if (trace_supply_highway(next, d+1)) { ind(d, "< highway", here, next) supply_line[side] = 1 has_supply = true } } } } supply_visited[here] = 0 if (has_supply) { supply_net[here] = 1 if (supply_enemy[here] <= 1) supply_src[here] = 1 } return has_supply } function trace_supply_chain(here, d, n, range) { ind(d, "> chain", here, n, range) trace_chain++ if (supply_src[here]) { ind(d, "! source chain", here) return true } let has_supply = false supply_visited[here] = 1 for (let s = 0; s < 6; ++s) { let next = here + hexnext[s] if (supply_visited[next]) continue let side = to_side(here, next, s) if (is_supply_line_blocked(here, next, side)) continue let road = side_road[side] if (road === HIGHWAY) { ind(d, "? chain highway", next) if (supply_friendly[next] > 1) { ind(d, "? chain highway head", next) if (trace_supply_chain(next, d+1, 0, 3)) { ind(d, "< highway chain", here, next) supply_line[side] = 1 has_supply = true } } else { if (trace_supply_highway(next, d+1)) { ind(d, "< chain highway", here, next) supply_line[side] = 1 has_supply = true } } } else { let next_range = min(range, SUPPLY_RANGE[road]) if (n + 1 <= next_range) { if (supply_friendly[next] > 1) { ind(d, "? chain head", next) if (trace_supply_chain(next, d+1, 0, 3)) { ind(d, "< highway chain", here, next) supply_line[side] = 1 has_supply = true } } else { if (trace_supply_chain(next, d+1, n+1, next_range)) { ind(d, "< chain trail", here, next_range) supply_line[side] = 1 has_supply = true } } } } } supply_visited[here] = 0 if (has_supply) { supply_net[here] = 1 // undisrupted units can chain supply if (supply_friendly[here] > 1 && supply_enemy[here] <= 1) supply_src[here] = 1 } return has_supply } function trace_supply_network(start) { supply_visited.fill(0) supply_src.fill(0) supply_net.fill(0) supply_line.fill(0) supply_src[start] = 1 supply_net[start] = 1 console.log("=== SUPPLY NETWORK ===") // debug_hexes("FH", supply_friendly) // debug_hexes("EH", supply_enemy) // debug_hexes("SS1", supply_src) var trace_total = 0 for (let x of all_hexes) { if (supply_friendly[x] > 0) { trace_highway = trace_chain = 0 ind(0, "START", x) trace_supply_chain(x, 0, 0, 3) console.log("END", trace_highway, trace_chain) trace_total += trace_highway + trace_chain } } console.log("VISITS", trace_total) debug_hexes("SS", supply_src) debug_hexes("SN", supply_net) } function update_axis_supply() { supply_axis_invalid = false if (presence_invalid) update_presence() supply_net = supply_axis_network supply_line = supply_axis_line supply_defender = game.axis_hexes supply_defender_sides = game.axis_sides supply_friendly = presence_axis supply_enemy = presence_allied trace_supply_network(EL_AGHEILA) } function update_allied_supply() { supply_allied_invalid = false if (presence_invalid) update_presence() supply_net = supply_allied_network supply_line = supply_allied_line supply_defender = game.allied_hexes supply_defender_sides = game.allied_sides supply_friendly = presence_allied supply_enemy = presence_axis trace_supply_network(ALEXANDRIA) } function update_supply() { if (supply_axis_invalid) update_axis_supply() if (supply_allied_invalid) update_allied_supply() } function axis_supply_line() { if (supply_axis_invalid) update_axis_supply() return supply_axis_line } function axis_supply_network() { if (supply_axis_invalid) update_axis_supply() return supply_axis_network } function allied_supply_line() { if (supply_allied_invalid) update_allied_supply() return supply_allied_line } function allied_supply_network() { if (supply_allied_invalid) update_allied_supply() return supply_allied_network } function unit_supply_line(who) { // TODO: fortress supply if (is_axis_unit(who)) return axis_supply_line() return allied_supply_line() } function unit_supply_distance(who) { // TODO: fortress supply if (is_axis_unit(who)) return distance_to[EL_AGHEILA] return distance_to[ALEXANDRIA] } function friendly_supply_base() { if (is_axis_player()) return EL_AGHEILA return ALEXANDRIA } function friendly_supply_network() { if (is_axis_player()) return axis_supply_network() return allied_supply_network() } // === PATHING === const path_from = [ new Array(hexcount), new Array(hexcount), new Array(hexcount), null, new Array(hexcount) ] const path_cost = [ new Array(hexcount), new Array(hexcount), new Array(hexcount), null, new Array(hexcount) ] const path_valid = new Array(hexcount) function print_path(who, from, to, road) { let p = [ hex_name[to] ] while (to && to !== from) { to = path_from[road][to] p.unshift(hex_name[to]) } log(">" + p.join(" - ") + ".") } // normal move: may not leave battle hex. may engage any enemy. may move freely. // normal withdrawal: may not leave battle hex. may engage disrupted enemy. must follow supply lines. // retreat move: must leave battle hex via friendly side. may ignore disrupted enemy. may move freely. // retreat withdrawal: must leave battle hex via friendly side. may ignore disrupted enemy. must follow supply lines. // TODO: cache search results from previous invocation function search_move(start, speed) { // Normal moves. search_init() search_move_bfs(path_from[0], path_cost[0], start, 0, speed, false, null, null) search_move_bfs(path_from[1], path_cost[1], start, 1, speed + 1, false, null, null) search_move_bfs(path_from[2], path_cost[2], start, 2, speed + 2, false, null, null) search_move_bfs(path_from[4], path_cost[4], start, 4, speed + 4, false, null, null) } function search_move_retreat(start, speed) { search_move_bfs(path_from[0], path_cost[0], start, 0, speed, true, null, null) search_move_bfs(path_from[1], path_cost[1], start, 1, speed + 1, true, null, null) search_move_bfs(path_from[2], path_cost[2], start, 2, speed + 2, true, null, null) search_move_bfs(path_from[4], path_cost[4], start, 4, speed + 4, true, null, null) } function search_withdraw(who, bonus) { let sline = unit_supply_line(who) let sdist = unit_supply_distance(who) let speed = unit_speed(who) + bonus let start = unit_hex(who) search_move_bfs(path_from[0], path_cost[0], start, 0, speed, false, sline, sdist) search_move_bfs(path_from[1], path_cost[1], start, 1, speed + 1, false, sline, sdist) search_move_bfs(path_from[2], path_cost[2], start, 2, speed + 2, false, sline, sdist) search_move_bfs(path_from[4], path_cost[4], start, 4, speed + 4, false, sline, sdist) } function search_withdraw_retreat(who, bonus) { let sline = unit_supply_line(who) let sdist = unit_supply_distance(who) let speed = unit_speed(who) + bonus let start = unit_hex(who) search_move_bfs(path_from[0], path_cost[0], start, 0, speed, true, sline, sdist) search_move_bfs(path_from[1], path_cost[1], start, 1, speed + 1, true, sline, sdist) search_move_bfs(path_from[2], path_cost[2], start, 2, speed + 2, true, sline, sdist) search_move_bfs(path_from[4], path_cost[4], start, 4, speed + 4, true, sline, sdist) } function search_init() { } // Breadth First Search function search_move_bfs(from, cost, start, road, max_cost, retreat, sline, sdist) { let path_enemy, friendly_sides if (presence_invalid) update_presence() if (is_axis_player()) { path_enemy = presence_allied friendly_sides = game.axis_sides } else { path_enemy = presence_axis friendly_sides = game.allied_sides } from.fill(0) cost.fill(15) cost[start] = 0 if (hex_road[start] < road) return let queue = [ start << 4 ] while (queue.length > 0) { let item = queue.shift() let here = item >> 4 let here_cost = item & 15 let next_cost = here_cost + 1 for (let s = 0; s < 6; ++s) { let next = here + hexnext[s] // can't go off-map if (next < first_hex || next > last_hex || !hex_exists[next]) continue // already seen if (cost[next] < 15) continue let side = to_side(here, next, s) let max_side = side_limit[side] // can't cross this hexside if (max_side === 0) continue // must stay on road for current bonus if (side_road[side] < road) continue if (sline) { // must follow supply line if (sline[side] === 0) continue // may not increase distance to supply source (except Bardia/Ft. Capuzzo) if (sdist[next] > sdist[here] && side !== BARDIA_FT_CAPUZZO) continue } let next_enemy = path_enemy[next] if (retreat) { // must cross friendly hex-side to disengage if (here === start && !set_has(friendly_sides, side)) continue // may only ignore unshielded disrupted units if (next_enemy & 2) // has undisrupted enemy continue } else { if (sline) { // may only engage unshielded disrupted units if (next_enemy & 2) // has undisrupted enemy continue } // check hexside limit when engaging enemy units if (next_enemy) if ((game.side_limit[side] | 0) >= max_side) continue } from[next] = here cost[next] = next_cost if (!retreat) { // must stop when engaging enemy units if (next_enemy) continue } // enough movement allowance to keep going if (next_cost < max_cost) queue.push(next << 4 | next_cost) } } } function can_move_to(to, speed) { if (path_cost[4][to] <= speed + 4) return true if (path_cost[2][to] <= speed + 2) return true if (path_cost[1][to] <= speed + 1) return true if (path_cost[0][to] <= speed) return true return false } function max_speed_of_undisrupted_and_unmoved_friendly_unit_in_hex(from) { let max_speed = 0 for_each_undisrupted_and_unmoved_friendly_unit_in_hex(from, u => { let s = unit_speed(u) if (s > max_speed) max_speed = s }) return max_speed } function find_valid_regroup_destinations(from, rommel) { let speed = max_speed_of_undisrupted_and_unmoved_friendly_unit_in_hex(from) if (speed > 0) { search_move(from, speed + 1 + rommel) for (let x of all_hexes) if (!path_valid[x]) if (can_move_to(x, speed + 1 + rommel)) path_valid[x] = 1 } } // === SUPPLY COMMITMENT & TURN OPTION === function goto_turn_option() { game.state = 'turn_option' } function player_hand() { return is_axis_player() ? game.axis_hand : game.allied_hand } states.turn_option = { inactive: "turn option", prompt() { view.prompt = `Turn Option \u2014 committed ${game.commit[0]} real and ${game.commit[1]} dummy supply.` let hand = player_hand() if (game.commit[0] + game.commit[1] < 3) { if (hand[REAL] > 0) gen_action('real_card') if (hand[DUMMY] > 0) gen_action('dummy_card') } if (game.commit[0] >= 1) view.actions.basic = 1 else view.actions.basic = 0 if (game.commit[0] >= 2) view.actions.offensive = view.actions.assault = 1 else view.actions.offensive = view.actions.assault = 0 if (game.commit[0] >= 3) view.actions.blitz = 1 else view.actions.blitz = 0 if (game.commit[0] === 0) view.actions.pass = 1 else view.actions.pass = 0 }, basic() { push_undo() apply_turn_option('basic') }, offensive() { push_undo() apply_turn_option('offensive') }, assault() { push_undo() apply_turn_option('assault') }, blitz() { push_undo() apply_turn_option('blitz') }, pass() { push_undo() apply_turn_option('pass') }, real_card() { push_undo() let hand = player_hand() hand[REAL]-- game.commit[0]++ }, dummy_card() { push_undo() let hand = player_hand() hand[DUMMY]-- game.commit[1]++ }, } function apply_turn_option(option) { push_undo() game.turn_option = option let n = game.commit[0] + game.commit[1] if (n === 0) log(`Played zero supply cards.`) else if (n === 1) log(`Played one supply card.`) else if (n === 2) log(`Played two supply cards.`) else if (n === 3) log(`Played three supply cards.`) log_br() if (game.turn_option === 'pass') game.passed++ else game.passed = 0 goto_move_phase() } // === PLAYER TURN === function goto_player_turn() { set_active_player() log_h2(game.phasing) // paranoid resetting of state game.side_limit = {} game.rommel = 0 game.from1 = game.from2 = 0 game.to1 = game.to2 = 0 // reset moved and fired flags game.forced = [] set_clear(game.fired) set_clear(game.moved) game.commit = [ 0, 0 ] goto_initial_supply_check() } function end_player_turn() { clear_undo() // Forget partial retreats set_clear(game.partial_retreats) // Reveal supply cards log_br() log(`Supply Cards Revealed:\n${game.commit[0]} real and ${game.commit[1]} dummy.`) log_br() game.commit = [ 0, 0 ] if (check_sudden_death_victory()) return if (game.passed === 2) return end_month() if (game.phasing === AXIS) game.phasing = ALLIED else game.phasing = AXIS goto_player_turn() } // === INITIAL & FINAL SUPPLY CHECK === function goto_initial_supply_check() { let snet = friendly_supply_network() let ssrc = friendly_supply_base() // TODO: fortress supply // TODO: assign fortress supply for_each_friendly_unit_on_map(u => { let x = unit_hex(u) if (snet[x]) { set_unit_supply(u, ssrc) if (is_unit_disrupted(u) && set_has(game.recover, u) && !is_battle_hex(x)) { log(`Recovered at #${x}`) set_delete(game.recover, u) clear_unit_disrupted(u) } } else { set_unit_supply(u, 0) } }) 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) } } function goto_final_supply_check() { set_active_player() capture_fortress(BARDIA, 2) capture_fortress(BENGHAZI, 2) capture_fortress(TOBRUK, 5) let snet = friendly_supply_network() let ssrc = friendly_supply_base() // TODO: fortress supply // TODO: assign unused fortress supply for_each_friendly_unit_on_map(u => { let x = unit_hex(u) if (!snet[x] && !is_unit_disrupted(u) && !is_unit_supplied(u)) { log(`Disrupted at #${x}`) set_unit_disrupted(u) } }) goto_final_supply_check_rout() } function goto_final_supply_check_rout() { let n = 0, where = 0 for (let x of all_hexes) { if (is_friendly_rout_hex(x)) { where = x n++ } } if (n === 0) end_player_turn() else if (n === 1) goto_rout(where, false, goto_final_supply_check_rout) else game.state = 'final_supply_check_rout' } states.final_supply_check_rout = { prompt() { view.prompt = `Final Supply Check: Rout!` for (let x of all_hexes) if (is_friendly_rout_hex(x)) gen_action_hex(x) }, hex(where) { goto_rout(where, false, goto_final_supply_check_rout) } } // ==== MOVEMENT PHASE === function goto_move_phase() { game.state = 'select_moves' if (game.phasing === AXIS) { // Automatically select Rommel Move for 1-move turn options if (game.turn_option !== 'offensive' && game.turn_option !== 'blitz' && game.scenario !== "1940") game.rommel = 1 } } states.select_moves = { inactive: "move phase", prompt() { if (game.turn_option === 'offensive') { if (game.from1) view.prompt = `Designate second offensive move.` else view.prompt = `Designate first offensive move.` } else { view.prompt = `Designate ${game.turn_option} move.` } gen_action('group') if (game.turn_option !== 'pass') gen_action('regroup') // TODO: needs work... if (game.turn_option === 'pass') gen_action('end_turn') }, group() { push_undo() game.state = 'group_move_from' }, regroup() { push_undo() game.state = 'regroup_move_command_point' }, end_turn() { clear_undo() goto_final_supply_check() } } function gen_rommel_move() { if (game.phasing === AXIS && game.scenario !== "1940") view.actions.rommel = game.rommel ? 0 : 1 } states.group_move_from = { inactive: "group move (from)", prompt() { view.prompt = `Group Move: Select hex to move from.` gen_rommel_move() for (let x = first_hex; x <= last_hex; ++x) { if (x === game.from1 && !game.to1) continue if (has_undisrupted_friendly_unit(x)) gen_action_hex(x) } }, rommel() { push_undo() if (game.from1 === 0) game.rommel = 1 else game.rommel = 2 }, hex(x) { push_undo() if (game.from1 === 0) game.from1 = x else game.from2 = x if (game.turn_option === 'offensive' && !game.from2) game.state = 'select_moves' else goto_move() }, } states.regroup_move_command_point = { inactive: "regroup move (command point)", prompt() { view.prompt = `Regroup Move: Designate the command point hex.` gen_rommel_move() for (let x = first_hex; x <= last_hex; ++x) { if (!is_enemy_hex(x)) { if (hex_or_adjacent_has_undisrupted_and_unmoved_friendly_unit(x)) gen_action_hex(x) } } }, rommel() { push_undo() if (game.from1 === 0) game.rommel = 1 else game.rommel = 2 }, hex(x) { push_undo() if (game.from1 === 0) game.from1 = x else game.from2 = x game.state = 'regroup_move_destination' }, } states.regroup_move_destination = { inactive: "regroup move (destination)", prompt() { view.prompt = `Regroup Move: Select destination hex.` gen_rommel_move() let cp, rommel = false if (game.from2 === 0) cp = game.from1, rommel = (game.rommel === 1 ? 1 : 0) else cp = game.from2, rommel = (game.rommel === 2 ? 1 : 0) path_valid.fill(0) for_each_hex_and_adjacent_hex(cp, x => { find_valid_regroup_destinations(x, rommel) }) for (let x of all_hexes) if (path_valid[x]) gen_action_hex(x) }, rommel() { push_undo() if (game.from2 === 0) game.rommel = 1 else game.rommel = 2 }, hex(x) { push_undo() if (game.from2 === 0) game.to1 = x else game.to2 = x if (game.turn_option === 'offensive' && !game.from2) game.state = 'select_moves' else goto_move() }, } function end_movement() { game.from1 = game.from2 = game.to1 = game.to2 = 0 goto_forced_marches() } // === GROUP AND REGROUP MOVEMENT === function goto_move() { if (game.rommel === 1) { if (game.from1 && game.to1) log(`Rommel Regroup move\nfrom #${game.from1}\nto #${game.to1}.`) else if (game.from1) log(`Rommel Group move\nfrom #${game.from1}.`) } else { if (game.from1 && game.to1) log(`Regroup move\nfrom #${game.from1}\nto #${game.to1}.`) else if (game.from1) log(`Group move\nfrom #${game.from1}.`) } if (game.rommel === 2) { if (game.from2 && game.to2) log(`Rommel Regroup move\nfrom #${game.from2}\nto #${game.to2}.`) else if (game.from2) log(`Rommel Group move\nfrom #${game.from2}.`) } else { if (game.from2 && game.to2) log(`Regroup move\nfrom #${game.from2}\nto #${game.to2}.`) else if (game.from2) log(`Group move\nfrom #${game.from2}.`) } log_br() game.state = 'move' } states.move = { inactive: "move", prompt() { let rommel1 = (game.rommel === 1) ? 1 : 0 let rommel2 = (game.rommel === 2) ? 1 : 0 if (game.selected < 0) { view.prompt = `Move: Select unit to move.` // Select Group Move 1 if (!game.to1 && game.from1) { if (!is_battle_hex(game.from1)) { for_each_undisrupted_and_unmoved_friendly_unit_in_hex(game.from1, u => { gen_action_unit(u) }) } } // Select Group Move 2 if (!game.to2 && game.from2) { if (!is_battle_hex(game.from2)) { for_each_undisrupted_and_unmoved_friendly_unit_in_hex(game.from2, u => { gen_action_unit(u) }) } } // Select Regroup Move 1 if (game.to1) { for_each_hex_and_adjacent_hex(game.from1, from => { let speed = max_speed_of_undisrupted_and_unmoved_friendly_unit_in_hex(from) if (speed > 0 && !has_enemy_unit(from)) { // TODO: withdraw pass move search_move(from, speed + 1 + rommel1) for_each_undisrupted_and_unmoved_friendly_unit_in_hex(from, u => { if (can_move_to(game.to1, unit_speed(u) + 1 + rommel1)) gen_action_unit(u) }) } }) } // Select Regroup Move 2 if (game.to1) { for_each_hex_and_adjacent_hex(game.from2, from => { let speed = max_speed_of_undisrupted_and_unmoved_friendly_unit_in_hex(from) if (speed > 0 && !has_enemy_unit(from)) { // TODO: withdraw pass move search_move(from, speed + 1 + rommel2) for_each_undisrupted_and_unmoved_friendly_unit_in_hex(from, u => { if (can_move_to(game.to2, unit_speed(u) + 1 + rommel2)) gen_action_unit(u) }) } }) } // Retreat if (can_select_retreat_hex()) gen_action('retreat') // Overrun let has_overrun_hex = false for (let x of all_hexes) { if (is_overrun_hex(x)) { has_overrun_hex = true break } } if (has_overrun_hex) gen_action('overrun') else gen_action('end_move') } else { view.prompt = `Move: Select hex to move to.` // Deselect gen_action_unit(game.selected) // Move if (game.turn_option === 'pass') search_withdraw(game.selected, 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() { clear_undo() log_br() end_movement() } } states.overrun = { prompt() { view.prompt = `Overrun!` for (let x of all_hexes) if (is_overrun_hex(x)) gen_action_hex(x) }, hex(where) { // return to move state afterwards game.state = 'move' goto_overrun(where) }, } function goto_overrun(where) { log_h3(`Overrun at #${where}`) goto_rout(where, true, null) } function gen_move() { let rommel1 = (game.rommel === 1) ? 1 : 0 let rommel2 = (game.rommel === 2) ? 1 : 0 let speed = unit_speed(game.selected) let from = unit_hex(game.selected) if (!game.to1 && game.from1 === from) { for (let to of all_hexes) { if (to != from) { if (can_move_to(to, speed + rommel1)) gen_action_hex(to) else if (can_move_to(to, speed + 1 + rommel1)) gen_action_forced_march(to) } } } if (!game.to2 && game.from2 === from) { for (let to of all_hexes) { if (to != from) { if (can_move_to(to, speed + rommel2)) gen_action_hex(to) else if (can_move_to(to, speed + 1 + rommel2)) gen_action_forced_march(to) } } } if (game.to1 && is_hex_or_adjacent_to(from, game.from1)) { if (can_move_to(game.to1, speed + rommel1)) gen_action_hex(game.to1) else if (can_move_to(game.to1, speed + 1 + rommel1)) gen_action_forced_march(game.to1) } if (game.to2 && is_hex_or_adjacent_to(from, game.from2)) { if (can_move_to(game.to2, speed + rommel2)) gen_action_hex(game.to2) else if (can_move_to(game.to2, speed + 1 + rommel2)) gen_action_forced_march(game.to2) } } function apply_move(to) { let rommel1 = (game.rommel === 1) ? 1 : 0 let rommel2 = (game.rommel === 2) ? 1 : 0 let who = pop_selected() let from = unit_hex(who) let speed = unit_speed(who) push_undo() search_move(from, speed + 1 + (rommel1 | rommel2)) if (!game.to1 && game.from1 === from) if (can_move_to(to, speed + 1 + rommel1)) return move_unit(who, to, speed + 1 + rommel1, 1) if (!game.to2 && game.from2 === from) if (can_move_to(to, speed + 1 + rommel2)) return move_unit(who, to, speed + 1 + rommel2, 2) if (game.to1 === to && is_hex_or_adjacent_to(from, game.from1)) if (can_move_to(to, speed + 1 + rommel1)) return move_unit(who, to, speed + 1 + rommel1, 1) if (game.to2 === to && is_hex_or_adjacent_to(from, game.from2)) if (can_move_to(to, speed + 1 + rommel2)) return move_unit(who, to, speed + 1 + rommel2, 2) throw Error("bad move") } // to check usable alternate paths to enter destination hex function can_move_via(via, to, speed, road) { let cost = path_cost[road][via] let side = to_side_id(via, to) let max_side = side_limit[side] // too far if (cost + 1 > speed + road) return 0 // can't cross this hexside if (max_side === 0) return 0 // must stay on road for current bonus if (side_road[side] < road) return 0 // must stop on enemies if (has_enemy_unit(via)) return 0 // may not exceed hexside limit if (has_enemy_unit(to)) if ((game.side_limit[side] | 0) >= max_side) return 0 if (cost + 1 === speed + road) return 2 return 1 } function is_forced_march_move(from, to, speed) { let result = true if (hex_road[from] >= 0 && path_cost[0][to] < speed + 0) result = false if (hex_road[from] >= 1 && path_cost[1][to] < speed + 1) result = false if (hex_road[from] >= 2 && path_cost[2][to] < speed + 2) result = false if (hex_road[from] >= 4 && path_cost[4][to] < speed + 4) result = false return result } function move_via(who, to, speed, move) { let from = unit_hex(who) game.hexside = { who: who, to: to, via: [], forced: [], move: move } for_each_adjacent_hex(to, via => { let forced = false let unforced = false function check_road(bonus) { if (hex_road[from] >= bonus) { let k = can_move_via(via, to, speed, bonus) if (k === 2) forced = true else if (k === 1) unforced = true } } check_road(4) check_road(2) check_road(1) check_road(0) if (unforced) { game.hexside.via.push(via) game.hexside.forced.push(0) } else if (forced) { game.hexside.via.push(via) game.hexside.forced.push(1) } }) return game.hexside.via.length === 1 } function move_unit(who, to, speed, move) { let from = unit_hex(who) if (is_forced_march_move(from, to, speed)) { if (move_via(who, to, speed, move)) { forced_march_via(who, game.hexside.via[0], to, move) delete game.hexside } else { game.state = 'forced_march_via' } } else if (has_enemy_unit(to)) { if (move_via(who, to, speed, move)) { if (game.hexside.forced[0]) forced_march_via(who, game.hexside.via[0], to, move) else engage_via(who, game.hexside.via[0], to) delete game.hexside } else { game.state = 'engage_via' } } else { log(`>from #${from} to #${to}`) set_unit_moved(who) set_unit_hex(who, to) } } states.forced_march_via = { prompt() { view.prompt = `Move: Select which path to take.` view.selected = game.hexside.who for (let x of game.hexside.via) gen_action_hex(x) }, hex(via) { forced_march_via(game.hexside.who, via, game.hexside.to, game.hexside.move) delete game.hexside game.state = 'move' } } states.engage_via = { prompt() { view.prompt = `Move: Select which hex side to cross.` view.selected = game.hexside.who for (let i = 0; i < game.hexside.via.length; ++i) if (game.hexside.forced[i]) gen_action_forced_march(game.hexside.via[i]) else gen_action_hex(game.hexside.via[i]) }, forced_march(via) { forced_march_via(game.hexside.who, via, game.hexside.to, game.hexside.move) delete game.hexside game.state = 'move' }, hex(via) { engage_via(game.hexside.who, via, game.hexside.to) delete game.hexside game.state = 'move' } } function forced_march_via(who, via, to, move) { let from = unit_hex(who) set_unit_moved(who) set_unit_hex(who, via) // remember where we should advance to / return to if ((move === 1 && game.to1) || (move === 2 && game.to2)) game.forced.push([who, to, from]) else game.forced.push([who, to]) // attempted force marches affect hexside limits if (has_enemy_unit(to)) { let side = to_side_id(via, to) if (game.side_limit[side]) game.side_limit[side] = 2 else game.side_limit[side] = 1 } log(`>forced march from #${from} via #${via} to #${to}`) } function engage_via(who, via, to) { let from = unit_hex(who) set_unit_moved(who) set_unit_hex(who, to) let side = to_side_id(via, to) if (game.side_limit[side]) game.side_limit[side] = 2 else game.side_limit[side] = 1 claim_hexside_control(side) if (is_new_battle_hex(to)) { claim_hex_control_for_defender(to) set_add(game.active_battles, to) } if (from !== via) log(`>from #${from} via #${via} to #${to}`) else log(`>from #${from} to #${to}`) } // === FORCED MARCHES === function goto_forced_marches() { if (game.forced.length > 0) game.state = 'forced_marches' else end_forced_marches() } states.forced_marches = { prompt() { view.prompt = `Forced Marches!` for (let [who, to] of game.forced) gen_action_unit(who) }, unit(who) { 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 ${roll} success.`) if (has_enemy_unit(to)) { engage_via(who, via, to, false) } else { set_unit_hex(who, to) log(`>from #${via} to #${to}`) } } else { log(`Forced March roll ${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) { let result = true for_each_undisrupted_friendly_unit_in_hex(from, u => { if (result === true && !is_unit_moved(u) && !can_unit_retreat(u)) result = false }) return result } function can_unit_retreat(who) { let rommel1 = (game.rommel === 1) ? 1 : 0 let rommel2 = (game.rommel === 2) ? 1 : 0 let from = unit_hex(who) if (!game.to1 && game.from1 === from) return can_unit_retreat_group_move(who) if (!game.to2 && game.from2 === from) return can_unit_retreat_group_move(who) if (game.to1 && is_hex_or_adjacent_to(from, game.from1)) if (can_unit_retreat_regroup_move(who, game.to1, rommel1)) return true if (game.to2 && is_hex_or_adjacent_to(from, game.from2)) if (can_unit_retreat_regroup_move(who, game.to2, rommel2)) return true return false } function can_unit_retreat_group_move(who) { if (game.turn_option === 'pass') return can_unit_disengage_and_withdraw(who) else return can_unit_disengage_and_move(who) } function can_unit_retreat_regroup_move(who, to, rommel) { if (game.turn_option === 'pass') return can_unit_disengage_and_withdraw_to(who, to, 1 + rommel) else return can_unit_disengage_and_move_to(who, to, 1 + rommel) } function is_friendly_hexside(side) { if (is_axis_player()) return set_has(game.axis_sides, side) return set_has(game.allied_sides, side) } function can_unit_disengage_and_withdraw(who) { let sline = unit_supply_line(who) let sdist = unit_supply_distance(who) let from = unit_hex(who) let result = false for_each_adjacent_hex(from, to => { let side = to_side_id(from, to) if (is_friendly_hexside(side) && !has_enemy_unit(to)) if (sline[side] && sdist[to] <= sdist[from]) result = true }) return result } function can_unit_disengage_and_move(who) { let from = unit_hex(who) let result = false for_each_adjacent_hex(from, to => { let side = to_side_id(from, to) if (is_friendly_hexside(side) && !has_enemy_unit(to)) result = true }) return result } function can_unit_disengage_and_withdraw_to(who, to, extra) { search_withdraw_retreat(who, extra) return can_move_to(to, unit_speed(who) + extra) } function can_unit_disengage_and_move_to(who, to, extra) { search_move_retreat(unit_hex(who), unit_speed(who) + extra) return can_move_to(to, unit_speed(who) + extra) } function can_select_retreat_hex() { if (!game.to1 && game.from1) if (is_valid_retreat_hex(game.from1)) return true if (!game.to2 && game.from2) if (is_valid_retreat_hex(game.from2)) return true if (game.to1) { let result = false for_each_hex_and_adjacent_hex(game.from1, x => { if (result === false && is_valid_retreat_hex(x)) result = true }) if (result) return true } if (game.to2) { let result = false for_each_hex_and_adjacent_hex(game.from2, x => { if (result === false && is_valid_retreat_hex(x)) result = true }) if (result) return true } return false } states.retreat_from = { prompt() { view.prompt = `Retreat: Select hex to retreat from.` if (!game.to1 && game.from1) { if (is_valid_retreat_hex(game.from1)) gen_action_hex(game.from1) } if (!game.to2 && game.from2) { if (is_valid_retreat_hex(game.from2)) gen_action_hex(game.from2) } if (game.to1) { for_each_hex_and_adjacent_hex(game.from1, x => { if (is_valid_retreat_hex(x)) gen_action_hex(x) }) } if (game.to2) { for_each_hex_and_adjacent_hex(game.from2, x => { if (is_valid_retreat_hex(x)) gen_action_hex(x) }) } gen_action('end_move') }, hex(x) { push_undo() game.retreat = x game.state = 'retreat_who' game.retreat_units = [] }, end_move() { clear_undo() log_br() end_movement() } } states.retreat_who = { prompt() { view.prompt = `Retreat: Select units to retreat.` let full_retreat = true for_each_undisrupted_and_unmoved_friendly_unit_in_hex(game.retreat, u => { if (!set_has(game.retreat_units, u)) full_retreat = false if (can_unit_retreat(u)) gen_action_unit(u) }) if (full_retreat) { view.actions.retreat = 1 } else { gen_action('select_all') if (game.retreat_units.length > 0 && game.turn_option !== 'pass') view.actions.retreat = 1 else view.actions.retreat = 0 } view.selected = game.retreat_units }, unit(u) { set_toggle(game.retreat_units, u) }, select_all() { for_each_undisrupted_and_unmoved_friendly_unit_in_hex(game.retreat, u => { if (!set_has(game.retreat_units, u)) if (can_unit_retreat(u)) set_add(game.retreat_units, u) }) }, retreat() { clear_undo() let full_retreat = true for_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) game.retreat_units = null // no shielding units remain if (is_friendly_rout_hex(game.retreat)) goto_rout(game.retreat, false, end_retreat_2) // rear-guard eliminated all enemy units else if (is_enemy_rout_hex(game.retreat)) goto_rout(game.retreat, true, end_retreat_2) else end_retreat_2() } function end_retreat_2() { if (can_select_retreat_hex()) game.state = 'retreat_from' else end_movement() } // === REFUSE BATTLE === function can_select_refuse_battle_hex() { for (let x of game.active_battles) if (can_all_refuse_battle(x)) return true return false } function goto_refuse_battle() { set_passive_player() if (can_select_refuse_battle_hex()) { game.state = 'refuse_battle' } else { goto_combat_phase() } } function can_all_refuse_battle(from) { let result = true for_each_undisrupted_friendly_unit_in_hex(from, u => { if (result === true && !can_unit_refuse_battle(u)) result = false }) return result } function can_unit_refuse_battle(who) { return can_unit_disengage_and_withdraw(who) } states.refuse_battle = { inactive: "refuse battle", prompt() { view.prompt = `You may Refuse Battle.` for (let x of game.active_battles) if (can_all_refuse_battle(x)) gen_action_hex(x) gen_action('next') }, hex(x) { push_undo() log_h3(`Refused battle at #${x}`) game.refuse = x set_delete(game.active_battles, x) goto_pursuit_fire_during_refuse_battle(x) }, next() { goto_combat_phase() } } function goto_refuse_battle_move() { set_passive_player() if (has_undisrupted_friendly_unit(game.refuse)) game.state = 'refuse_battle_move' else end_refuse_battle_move() } states.refuse_battle_move = { inactive: "refuse battle (withdraw group move)", prompt() { view.prompt = `Refuse Battle: Withdraw units.` if (game.selected < 0) { let done = true for_each_undisrupted_friendly_unit_in_hex(game.refuse, u => { gen_action_unit(u) done = false }) if (done) gen_action('end_retreat') } else { let speed = unit_speed(game.selected) gen_action_unit(game.selected) search_withdraw_retreat(game.selected, 0) for (let to of all_hexes) if (to != game.refuse && can_move_to(to, speed)) gen_action_hex(to) } }, unit(who) { apply_select(who) }, hex(to) { let who = pop_selected() push_undo() log(`>to #${to}`) set_unit_hex(who, to) set_unit_disrupted(who) }, end_retreat() { end_refuse_battle_move() } } function end_refuse_battle_move() { if (is_friendly_rout_hex(game.refuse)) goto_rout(game.refuse, false, end_refuse_battle_move_2) else end_refuse_battle_move_2() } function end_refuse_battle_move_2() { release_hex_control(game.refuse) game.refuse = 0 goto_refuse_battle() } // === ROUT === // rout attrition // pursuit fire // withdraw by group move // eliminated if cannot function goto_rout(from, enemy, after) { // remember state and callback so we can resume after routing if (after) { if (!after_rout_table[after.name]) after_rout_table[after.name] = after after = after.name } game.rout = { state: game.state, active: game.active, after: after, from: from, attrition: [], } if (enemy) set_enemy_player() game.state = 'rout_attrition' } states.rout_attrition = { prompt() { view.prompt = "Rout: All units lose one step of rout attrition." for_each_friendly_unit_in_hex(game.rout.from, u => { if (!set_has(game.rout.attrition, u)) gen_action_unit(u) }) }, unit(who) { reduce_unit(who) set_add(game.rout.attrition, who) let done = true for_each_friendly_unit_in_hex(game.rout.from, u => { if (!set_has(game.rout.attrition, u)) done = false }) if (done) { delete game.rout.attrition goto_rout_fire(game.rout.from) } }, } function goto_rout_move() { if (has_friendly_unit(game.rout.from)) game.state = 'rout_move' else end_rout() } states.rout_move = { prompt() { view.prompt = `Rout: Withdraw units.` if (game.selected < 0) { let done = true for_each_friendly_unit_in_hex(game.rout.from, u => { gen_action_unit(u) done = false }) if (done) gen_action('end_rout') } else { let speed = unit_speed(game.selected) let eliminate = true search_withdraw_retreat(game.selected, 0) for (let to of all_hexes) { if (to != game.rout.from && can_move_to(to, speed)) { gen_action_hex(to) eliminate = false } } if (eliminate) gen_action('eliminate') } }, unit(who) { apply_select(who) }, eliminate() { let who = pop_selected() push_undo() log(`>eliminated`) eliminate_unit(who) }, hex(to) { let who = pop_selected() push_undo() log(`>to #${to}`) set_unit_hex(who, to) set_unit_disrupted(who) }, end_rout() { end_rout() } } function end_rout() { game.state = game.rout.state release_hex_control(game.rout.from) set_delete(game.active_battles, game.rout.from) if (game.active !== game.rout.active) set_enemy_player() let after = game.rout.after delete game.rout if (after) after_rout_table[after]() } // ==== COMBAT PHASE === function is_mandatory_combat(fortress) { if (is_battle_hex(fortress)) { if (game.phasing === AXIS) return is_fortress_allied_controlled() else return is_fortress_axis_controlled() } return false } function goto_combat_phase() { set_active_player() if (game.turn_option === 'pass') { if (is_mandatory_combat(BARDIA)) return goto_rout(BARDIA, false, goto_combat_phase) if (is_mandatory_combat(BENGHAZI)) return goto_rout(BENGHAZI, false, goto_combat_phase) if (is_mandatory_combat(TOBRUK)) return goto_rout(TOBRUK, false, goto_combat_phase) } else { if (is_mandatory_combat(BARDIA)) set_add(game.active_battles, BARDIA) if (is_mandatory_combat(BENGHAZI)) set_add(game.active_battles, BENGHAZI) if (is_mandatory_combat(TOBRUK)) set_add(game.active_battles, TOBRUK) } let n = count_battle_hexes() if (n > 0) { if (n > game.active_battles.length) return game.state = 'select_active_battles' if (game.turn_option === 'assault') return game.state = 'select_assault_battles' return goto_select_battle() } end_combat_phase() } states.select_active_battles = { inactive: "combat phase (select active battles)", prompt() { view.prompt = `Select active battles.` view.active_battles = game.active_battles for (let x of all_hexes) if (!set_has(game.active_battles, x) && is_battle_hex(x)) gen_action_hex(x) gen_action('next') }, hex(x) { push_undo() set_add(game.active_battles, x) }, next() { push_undo() if (game.active_battles.length > 0) { if (game.turn_option === 'assault') game.state = 'select_assault_battles' else game.state = 'select_battle' } else { end_combat_phase() } } } states.select_assault_battles = { inactive: "combat phase (select assault battles)", prompt() { view.prompt = `Select assault battles.` view.active_battles = game.active_battles view.assault_battles = game.assault_battles for (let x of game.active_battles) if (!set_has(game.assault_battles, x)) gen_action_hex(x) gen_action_next() }, hex(x) { push_undo() set_add(game.assault_battles, x) }, next() { push_undo() game.state = 'select_battle' } } function goto_select_battle() { if (game.active_battles.length > 0) { if (game.active_battles.length > 1) { game.state = 'select_battle' } else { goto_battle(game.active_battles[0]) } } else { end_combat_phase() } } states.select_battle = { inactive: "combat phase (select next battle)", prompt() { view.prompt = `Select next battle to resolve.` view.active_battles = game.active_battles view.assault_battles = game.assault_battles for (let x of game.active_battles) gen_action_hex(x) }, hex(x) { goto_battle(x) }, } function end_combat_phase() { if (game.turn_option === 'blitz') { goto_blitz_turn() } else { goto_final_supply_check() } } function goto_blitz_turn() { log_h2(`Blitz Turn`) if (game.rommel) game.rommel = 3 set_clear(game.fired) game.turn_option = 'second blitz' goto_move_phase() } // === BATTLES === // Normal Battle: // passive fire // active hits // active fire // passive hits function is_unit_retreating(u) { if (game.retreat_units) return set_has(game.retreat_units, u) return false } function is_assault_battle() { return set_has(game.assault_battles, game.battle) } function is_fortress_defensive_fire() { if ((game.state === 'battle_fire' && is_passive_player()) || (game.state === 'probe_fire' && is_active_player())) { if (game.battle === BENGHAZI) return game.benghazi === game.active if (game.battle === TOBRUK) return game.tobruk === game.active if (game.battle === BARDIA) return game.tobruk === game.active } return false } function is_minefield_offensive_fire() { if ((game.state === 'battle_fire' && is_active_player()) || (game.state === 'probe_fire' && is_passive_player())) { if (set_has(game.minefields)) { // DD advantage is lost if the defender initiated combat if (is_axis_player()) return set_has(game.allied_hexes, game.battle) else return set_has(game.axis_hexes, game.battle) } } return false } function roll_battle_fire(who, tc) { let fc = unit_class(who) let cv = unit_cv(who) // Double dice during assault and non-armor defenders in fortress! if (is_assault_battle()) cv += cv else if (fc !== ARMOR && is_fortress_defensive_fire()) cv += cv let fp = FIREPOWER_MATRIX[fc][tc] let result = [] let total = 0 for (let i = 0; i < cv; ++i) { let roll = roll_die() result.push(roll) if (roll >= fp) ++total } // Double defense in minefields! if (is_minefield_offensive_fire()) total = total / 2 game.flash = `${unit_name(who)} fired ${firepower_name[fp]} ${result.join(", ")} at ${class_name[tc]}` log(game.flash) return total } function goto_battle(x) { game.battle = x if (is_assault_battle()) log_h3(`Assault in #${x}`) else log_h3(`Battle in #${x}`) // goto defensive fire set_passive_player() game.state = 'battle_fire' game.hits = [ 0, 0, 0, 0 ] } function end_battle() { 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 // TODO: if (game.hits[0] + game.hits[1] + game.hits[2] + game.hits[3] > 0) { if (true) { if (game.state === 'battle_fire') game.state = 'battle_hits' else game.state = 'probe_hits' } else { if (game.state === 'battle_fire') end_battle_hits() else end_probe_hits() } } function gen_battle_fire() { let arty = false for_each_undisrupted_friendly_unit_in_hex(game.battle, u => { if (is_artillery_unit(u) && !is_unit_retreating(u)) { if (!is_unit_fired(u)) { gen_action_unit(u) arty = true } } }) if (!arty) { for_each_undisrupted_friendly_unit_in_hex(game.battle, u => { if (!is_unit_fired(u) && !is_unit_retreating(u)) gen_action_unit(u) }) } } function gen_battle_target() { let hp = count_hp_in_battle() for (let i = 0; i < 4; ++i) hp[i] -= game.hits[i] let who = game.selected let fc = unit_class(who) gen_action_unit(who) // deselect // armor must target armor if possible if (fc === ARMOR && hp[ARMOR] > 0) { gen_action('armor') return } // infantry must target infantry if possible if (fc === INFANTRY && hp[INFANTRY] > 0) { gen_action('infantry') return } if (hp[ARMOR] > 0) gen_action('armor') if (hp[INFANTRY] > 0) gen_action('infantry') if (hp[ANTITANK] > 0) gen_action('antitank') // only artillery may target artillery if other units are alive if (hp[ARTILLERY] > 0) { if (fc === ARTILLERY || (hp[ARTILLERY] <= 0 && hp[INFANTRY] <= 0 && hp[ANTITANK] <= 0)) gen_action('artillery') } } function gen_battle_hits() { let normal_steps = count_normal_steps_in_battle() let elite_steps = count_elite_steps_in_battle() let done = true for_each_undisrupted_friendly_unit_in_hex(game.battle, u => { if (!is_unit_retreating(u)) { let c = unit_class(u) if (is_unit_elite(u)) { if (game.hits[c] >= 2) { gen_action_unit(u) done = false } } else { if (game.hits[c] >= 1) { // If mixed elite and non-elite: must assign ALL damage. if (elite_steps[c] > 0 && normal_steps[c] === 1 && (game.hits[c] & 1) === 0) { // Eliminating the last non-elite must not leave an odd // number of hits remaining. } else { gen_action_unit(u) done = false } } } } }) if (done) gen_action_next() return done } function apply_battle_hit(who) { game.hits[unit_class(who)] -= reduce_unit(who) } states.battle_fire = { prompt() { if (game.active === game.phasing) view.prompt = `Battle: Offensive Fire!` else view.prompt = `Battle: Defensive Fire!` if (game.selected < 0) gen_battle_fire() else gen_battle_target() }, unit(who) { apply_select(who) }, armor() { apply_battle_fire(ARMOR) }, infantry() { apply_battle_fire(INFANTRY) }, antitank() { apply_battle_fire(ANTITANK) }, artillery() { apply_battle_fire(ARTILLERY) }, } states.battle_hits = { prompt() { if (game.active === game.phasing) view.prompt = `Battle: Apply hits from Defensive Fire.` else view.prompt = `Battle: Apply hits from Offensive Fire.` gen_battle_hits() }, unit(who) { push_undo() apply_battle_hit(who) }, next() { clear_undo() end_battle_hits() }, } function end_battle_hits() { if (is_friendly_rout_hex(game.battle)) { goto_rout(game.battle, false, end_battle) } else if (game.active === game.phasing) { // goto offensive fire game.state = 'battle_fire' game.hits = [ 0, 0, 0, 0 ] } else { end_battle() } } states.probe_fire = { prompt() { if (game.active !== game.phasing) view.prompt = `Probe: Offensive Fire!` else view.prompt = `Probe: Defensive Fire!` if (game.selected < 0) gen_battle_fire() else gen_battle_target() }, unit(who) { apply_select(who) }, armor() { apply_battle_fire(ARMOR) }, infantry() { apply_battle_fire(INFANTRY) }, antitank() { apply_battle_fire(ANTITANK) }, artillery() { apply_battle_fire(ARTILLERY) }, } states.probe_hits = { prompt() { if (game.active !== game.phasing) view.prompt = `Probe: Apply hits from Defensive Fire.` else view.prompt = `Probe: Apply hits from Offensive Fire.` gen_battle_hits() }, unit(who) { push_undo() apply_battle_hit(who) }, next() { clear_undo() end_probe_hits() }, } function end_probe_hits() { if (game.active !== game.phasing && has_friendly_units_in_battle()) { // goto offensive fire game.state = 'probe_fire' game.hits = [ 0, 0, 0, 0 ] } else { end_probe() } } // === PURSUIT FIRE === // Refuse battle // active pursuit fire // passive apply hits // passive moves // Retreat // passive pursuit fire // active apply hits // active moves // Rout // non-routing pursuit fire // routing apply hits // routing moves function goto_rout_fire(where) { set_enemy_player() game.hits = 0 game.pursuit = where if (can_rout_fire(true)) game.state = 'rout_fire' else goto_rout_hits() } function goto_pursuit_fire_during_retreat(where) { set_passive_player() game.hits = 0 game.pursuit = where if (can_pursuit_fire(true)) game.state = 'pursuit_fire' else goto_pursuit_hits() } function goto_pursuit_fire_during_refuse_battle(where) { set_active_player() game.hits = 0 game.pursuit = where if (can_pursuit_fire(true)) game.state = 'pursuit_fire' else end_pursuit_fire() } function goto_rout_hits() { set_enemy_player() if (game.hits > 0) game.state = 'rout_hits' else end_rout_fire() } function goto_pursuit_hits() { set_enemy_player() // TODO: if (game.hits > 0) if (true) game.state = 'pursuit_hits' else end_pursuit_fire() } function slowest_enemy_unit_speed(where) { let r = 4 for_each_enemy_unit_in_hex(where, u => { let s = unit_speed(u) if (s < r) r = s }) return r } function slowest_undisrupted_enemy_unit_speed(where) { let r = 4 for_each_undisrupted_enemy_unit_in_hex(where, u => { let s = unit_speed(u) if (s < r) r = s }) return r } function can_rout_fire(verbose) { let result = false let slowest = slowest_enemy_unit_speed(game.pursuit) if (verbose) log(`Slowest was ${speed_name[slowest]} unit.`) for_each_undisrupted_friendly_unit_in_hex(game.pursuit, u => { if (unit_speed(u) >= slowest && !is_unit_fired(u)) result = true }) return result } function can_pursuit_fire(verbose) { let result = false let slowest = slowest_undisrupted_enemy_unit_speed(game.pursuit) if (verbose) log(`Slowest was ${speed_name[slowest]} unit.`) for_each_undisrupted_friendly_unit_in_hex(game.pursuit, u => { if (unit_speed(u) >= slowest && !is_unit_fired(u)) result = true }) return result } function roll_pursuit_fire_imp(who, n, hp) { if (n === 2) { let a = roll_die() let b = roll_die() game.flash = `${unit_name(who)} fired ${a}, ${b}` log(game.flash) if (a >= 4) game.hits++ if (b >= 4) game.hits++ } if (n === 1) { let a = roll_die() game.flash = `${unit_name(who)} fired ${a}` log(`>%${who} pursuit fired ${a}.`) if (a >= 4) game.hits++ } if (game.hits > hp) game.hits = hp return game.hits === hp } function roll_pursuit_fire(who, n) { return roll_pursuit_fire_imp(who, n, count_hp_in_pursuit()) } function roll_rout_fire(who, n) { return roll_pursuit_fire_imp(who, n, count_hp_in_rout()) } states.pursuit_fire = { inactive: "pursuit fire (fire)", prompt() { view.prompt = `Pursuit Fire.` let slowest = slowest_undisrupted_enemy_unit_speed(game.pursuit) for_each_undisrupted_friendly_unit_in_hex(game.pursuit, u => { if (unit_speed(u) >= slowest && !is_unit_fired(u)) gen_action_unit(u) }) // allow saving fire if there are shielded enemy units if (has_disrupted_enemy_unit(game.pursuit)) gen_action('next') }, unit(who) { let slowest = slowest_undisrupted_enemy_unit_speed(game.pursuit) set_unit_fired(who) let done = roll_pursuit_fire(who, (unit_speed(who) > slowest ? 2 : 1)) if (done || !can_pursuit_fire(false)) goto_pursuit_hits() }, next() { goto_pursuit_hits() } } states.rout_fire = { inactive: "rout fire (fire)", prompt() { view.prompt = `Pursuit Fire (Rout).` let slowest = slowest_enemy_unit_speed(game.pursuit) for_each_undisrupted_friendly_unit_in_hex(game.pursuit, u => { if (unit_speed(u) >= slowest && !is_unit_fired(u)) gen_action_unit(u) }) }, unit(who) { let slowest = slowest_enemy_unit_speed(game.pursuit) set_unit_fired(who) let done = roll_rout_fire(who, (unit_speed(who) > slowest ? 2 : 1)) if (done || !can_rout_fire(false)) goto_rout_hits() }, } function gen_pursuit_hits(normal_steps, elite_steps, iterate) { let done = true iterate(game.pursuit, u => { if (is_unit_elite(u)) { if (game.hits >= 2) { gen_action_unit(u) done = false } } else { if (game.hits >= 1) { // If mixed elite and non-elite: must assign ALL damage. if (elite_steps > 0 && normal_steps === 1 && (game.hits & 1) === 0) { // Eliminating the last non-elite must not leave an odd // number of hits remaining. } else { gen_action_unit(u) done = false } } } }) if (done) gen_action('next') } states.pursuit_hits = { inactive: "pursuit fire (hits)", prompt() { view.prompt = `Pursuit Fire: Apply ${game.hits} hits.` let normal_steps = count_normal_steps_in_pursuit() let elite_steps = count_elite_steps_in_pursuit() gen_pursuit_hits(normal_steps, elite_steps, for_each_undisrupted_friendly_unit_in_hex) }, unit(who) { push_undo() game.hits -= reduce_unit(who) }, next() { clear_undo() end_pursuit_fire() }, } states.rout_hits = { inactive: "rout fire (hits)", prompt() { view.prompt = `Pursuit Fire (Rout): Apply ${game.hits} hits.` let normal_steps = count_normal_steps_in_rout() let elite_steps = count_elite_steps_in_rout() gen_pursuit_hits(normal_steps, elite_steps, for_each_friendly_unit_in_hex) }, unit(who) { push_undo() game.hits -= reduce_unit(who) }, next() { clear_undo() end_rout_fire() }, } function end_pursuit_fire() { game.flash = "" game.pursuit = 0 if (game.retreat) { goto_retreat_move() } else { goto_refuse_battle_move() } } function end_rout_fire() { game.flash = "" game.pursuit = 0 goto_rout_move() } // === BUILDUP === function end_month() { // Forget captured fortresses (for bonus cards) clear_fortresses_captured() if (game.month === current_scenario().end) return end_game() goto_buildup() } function goto_buildup() { ++game.month log_h1(`Month ${game.month}`) game.phasing = AXIS set_active_player() goto_buildup_discard() } function goto_buildup_discard() { 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() end_buildup_discard() }, } function end_buildup_discard() { if (is_axis_player()) { set_enemy_player() goto_buildup_discard() } else { goto_buildup_supply_check() } } function init_buildup() { // TODO: fortress supply // TODO: assign fortress supply game.buildup = { // redeployment network axis_network: axis_supply_network().slice(), allied_network: allied_supply_network().slice(), // extra cards purchased axis_cards: 0, allied_cards: 0, // remaining port capacity for sea redeployment bardia: 2, benghazi: 2, tobruk: 5, } } function goto_buildup_supply_check() { init_buildup() for_each_axis_unit_on_map(u => { let x = unit_hex(u) if (supply_axis_network[x]) set_unit_supply(u, EL_AGHEILA) else set_unit_supply(u, 0) }) for_each_allied_unit_on_map(u => { let x = unit_hex(u) if (supply_allied_network[x]) set_unit_supply(u, ALEXANDRIA) else set_unit_supply(u, 0) }) log_br() resume_buildup_supply_check() } function resume_buildup_supply_check() { game.state = 'buildup_supply_check' let done = true for_each_friendly_unit_on_map(u => { if (is_unit_unsupplied(u)) done = false }) if (done) { if (is_axis_player()) { log_br() set_enemy_player() resume_buildup_supply_check() } else { goto_buildup_point_determination() } } } states.buildup_supply_check = { 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_supply_check() }, } function goto_buildup_point_determination() { let axis, allied log_br() if (game.scenario === "1940") { axis = roll_die() allied = roll_die() log(`Axis rolled ${axis}.`) log(`Allied rolled ${axis}.`) } 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 ${axis_a} + ${axis_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 goto_buildup_reinforcements() { log_h2(game.active + " Buildup") game.state = 'buildup_reinforcements' } 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`) } 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 } states.spending_bps = { prompt() { view.prompt = `Buildup: Spend buildup points (${available_bps()} remain).` gen_action('end_buildup') }, end_buildup() { clear_undo() let n = available_bps() if (n > 20) { log(`Lost ${n - 20} unspent BPs.`) pay_bps(n - 20) } end_buildup_spending() } } function end_buildup_spending() { if (is_active_player()) { set_enemy_player() goto_buildup_reinforcements() } else { delete game.buildup goto_buildup_resupply() } } function goto_buildup_resupply() { log_h2("Resupply") log(`Shuffled supply cards.`) shuffle_cards() // Per-scenario allotment let axis_resupply = (game.month <= 10 || game.malta) ? 2 : 3 let allied_resupply = 3 // Extra cards purchased during buildup axis_resupply += game.buildup.axis_cards allied_resupply += game.buildup.allied_cards // Bonus from captured fortresses axis_resupply += game.axis_award allied_resupply += game.allied_award game.axis_award = 0 game.allied_award = 0 deal_axis_supply_cards(axis_resupply) deal_allied_supply_cards(allied_resupply) goto_player_initiative() } // === INITIATIVE === function goto_player_initiative() { game.phasing = AXIS set_passive_player() game.state = 'allied_player_initiative' } states.allied_player_initiative = { prompt() { view.prompt = "Initiative: You may challenge for the initiative." let hand = player_hand() if (hand[REAL] > 0) gen_action('real_card') if (hand[DUMMY] > 0) gen_action('dummy_card') gen_action_next() }, real_card() { log(`Allied challenged for the initiative.`) player_hand()[0]-- game.phasing = ALLIED set_passive_player() game.state = 'axis_player_initiative' }, dummy_card() { player_hand()[1]-- log(`Allied challenged for the initiative.`) set_active_player() game.state = 'axis_player_initiative' }, next() { goto_player_turn() } } states.axis_player_initiative = { prompt() { view.prompt = "Initiative: You may defend your initiative." let hand = player_hand() if (hand[REAL] > 0) gen_action('real_card') gen_action_next() }, real_card() { player_hand()[0]-- log("Axis defends the initiative.") if (game.phasing === ALLIED) log("Allied card was real.") else log("Allied card was a dummy.") game.phasing = AXIS goto_player_turn() }, next() { if (game.phasing === ALLIED) log("Allied siezed the initiative.") else log("Allied card was a dummy.") goto_player_turn() } } // === VICTORY CHECK === const EXIT_EAST_EDGE = [ 99, 123, 148 ] //, 172, 197 ] const EXIT_EAST = 172 function check_sudden_death_victory() { // Supplied units that move beyond the map "edge" exit the map. // Count the easternmost row of hexes and half-hexes. // In the original map this would be the half-hexes and the virtual hexes beyond the edge. for (let x of EXIT_EAST_EDGE) { for_each_axis_unit(u => { if (unit_hex(u) === x && is_unit_supplied(u)) { log(`Exited the east map edge.`) set_unit_hex(u, EXIT_EAST) } }) } let axis_exited = 0 for_each_axis_unit(u => { if (unit_hex(u) === EXIT_EAST) axis_exited++ }) if (is_axis_hex(ALEXANDRIA) || axis_exited >= 3) { log_br() log("Axis captured Alexandria!") return goto_game_over(AXIS, "Axis Strategic Victory!") } if (is_allied_hex(EL_AGHEILA)) { log_br() log("Allied captured El Agheila!") return goto_game_over(ALLIED, "Allied Strategic Victory!") } return false } function end_game() { let axis = 0 for_each_axis_unit_on_map(u => { axis += is_german_unit(u) ? 1.5 : 1.0 }) let allied = 0 for_each_allied_unit_on_map(u => { allied += 1.0 }) if (axis >= allied * 2) return goto_game_over(AXIS, "Axis Decisive Victory!") if (allied >= axis * 2) return goto_game_over(AXIS, "Allied Decisive Victory!") if (!is_fortress_besieged(TOBRUK)) { if (is_fortress_axis_controlled(TOBRUK)) return goto_game_over(AXIS, "Axis Positional Victory!") else return goto_game_over(ALLIED, "Allied Positional Victory!") } if (axis > allied) return goto_game_over(AXIS, "Axis Attrition Victory!") if (allied > axis) return goto_game_over(ALLIED, "Allied Attrition Victory!") return goto_game_over("Draw", "No Victory!") } // === DEPLOYMENT === function goto_free_deployment() { game.state = 'free_deployment' if (!has_friendly_unit_in_month(current_scenario().start)) end_free_deployment() } states.free_deployment = { inactive: "free deployment", prompt() { let scenario = current_scenario() let deploy = hexdeploy + scenario.start let axis = (game.active === AXIS) let done = true view.prompt = axis ? "Axis Free Deployment" : "Allied Free Deployment" if (game.selected < 0) { for_each_friendly_unit_in_hex(deploy, u => { gen_action_unit(u) done = false }) } else { for (let x of (axis ? scenario.axis_deployment : scenario.allied_deployment)) { if (!is_enemy_hex(x)) { let limit = scenario.deployment_limit[x] | 0 if (!limit || count_friendly_units_in_hex(x) < limit) gen_action_hex(x) } } gen_action_unit(game.selected) done = false } if (done) gen_action_next() }, unit(u) { apply_select(u) }, hex(to) { let who = pop_selected() push_undo() log(`Deployed at #${to}.`) set_unit_hex(who, to) }, next() { clear_undo() end_free_deployment() } } function end_free_deployment() { set_enemy_player() if (has_friendly_unit_in_month(current_scenario().start)) { log_h2("Allied Deployment") } else { goto_initial_supply_cards() } } function goto_initial_supply_cards() { game.phasing = AXIS set_active_player() log_br() let scenario = current_scenario() deal_axis_supply_cards(scenario.axis_initial_supply) deal_allied_supply_cards(scenario.allied_initial_supply) game.state = 'initial_supply_cards' } states.initial_supply_cards = { prompt() { view.prompt = `Setup: You may discard your entire hand and redraw a new one.` gen_action('discard') gen_action('keep') }, discard() { 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() { if (is_axis_player()) set_enemy_player() else begin_game() }, } function begin_game() { log_h1(`Month ${game.month}`) if (game.scenario === "Crusader") { game.phasing = ALLIED set_add(game.minefields, TOBRUK) } if (game.scenario === "Gazala") { // PreGame Buildup game.phasing = ALLIED init_buildup() game.axis_bps = 30 game.allied_bps = 30 set_active_player() goto_buildup_reinforcements() return } // No buildup first month // No initiative first month // XXX goto_player_turn() goto_buildup() } // === SETUP === function find_axis_units(a) { let list = [] for (let u = first_axis_unit; u <= last_axis_unit; ++u) if (units[u].appearance === a) list.push(u) return list } function find_allied_units(a) { let list = [] for (let u = first_allied_unit; u <= last_allied_unit; ++u) if (units[u].appearance === a) list.push(u) return list } function setup_reinforcements(m) { for (let u = 0; u < units.length; ++u) { if (units[u].appearance === m) { if (m === 'M') set_unit_hex(u, MALTA) else set_unit_hex(u, hexdeploy + m) } } } function setup_units(where, steps, list) { if (where < 0) where = hexdeploy - where for (let u of list) { if (typeof u === 'string') u = find_unit(u) set_unit_hex(u, where) if (steps < 0) set_unit_steps(u, units[u].steps + steps) else if (steps > 0) set_unit_steps(u, steps) } } function current_scenario() { return SCENARIOS[game.scenario] } const SCENARIOS = { "1940": { year: 1940, start: 1, end: 6, axis_deployment: region_libya_and_sidi_omar, allied_deployment: region_egypt, axis_initial_supply: 6, allied_initial_supply: 3, deployment_limit: {}, }, "1941": { year: 1941, start: 1, end: 10, axis_deployment: [], allied_deployment: region_egypt_and_libya, axis_initial_supply: 6, allied_initial_supply: 6, deployment_limit: {}, }, "Crusader": { year: 1941, start: 8, end: 10, axis_deployment: region_libya_and_sidi_omar_and_sollum, allied_deployment: region_egypt_and_tobruk, axis_initial_supply: 10, allied_initial_supply: 12, deployment_limit: { [TOBRUK]: 5, }, }, "Battleaxe": { year: 1941, start: 4, end: 10, axis_deployment: region_libya_except_tobruk, allied_deployment: region_egypt_and_tobruk, axis_initial_supply: 4, allied_initial_supply: 8, deployment_limit: { [TOBRUK]: 5, }, }, "1942": { year: 1942, start: 11, end: 20, axis_deployment: [ EL_AGHEILA, MERSA_BREGA ], allied_deployment: region_egypt_and_libya, axis_initial_supply: 5, allied_initial_supply: 5, deployment_limit: { [EL_AGHEILA]: 14, [MERSA_BREGA]: 10, }, }, "Gazala": { year: 1942, start: 14, end: 15, axis_deployment: regions["West Line"], allied_deployment: regions["East Line"], axis_initial_supply: 10, allied_initial_supply: 12, deployment_limit: {}, special: { gazala_pre_build: true, } }, "Pursuit to Alamein": { year: 1942, start: 15, end: 20, axis_deployment: regions["Libya"], allied_deployment: regions["Egypt"], axis_initial_supply: 8, allied_initial_supply: 8, deployment_limit: {}, }, "1941-42": { year: 1941, start: 1, end: 20, axis_deployment: [], allied_deployment: region_egypt_and_libya, axis_initial_supply: 6, allied_initial_supply: 6, deployment_limit: {}, }, } const SETUP = { "1940" (DEPLOY) { setup_units(DEPLOY, 0, [ "Pav", "Bre", "Tre", "Bol", "Sav", "Sab", "Fas", "Ita" ]) setup_units(-3, 0, [ "Ari", "Pis"]) setup_units(-5, 0, [ "Lit", "Cen"]) setup_units(DEPLOY, 0, [ "7/7", "7", "7/SG", "4IN/3m" ]) setup_units(-2, 0, [ "4IN/5", "4IN/11" ]) setup_units(-4, 0, [ "Matilda/A", "7/4", "4IN/7m", "/Tob" ]) }, "1941" (DEPLOY) { setup_units(EL_AGHEILA, 0, find_axis_units('S')) setup_reinforcements(3) setup_reinforcements(5) setup_reinforcements(7) setup_units(DEPLOY, 0, find_allied_units('S')) setup_units(TOBRUK, 0, find_allied_units('T')) setup_reinforcements(2) setup_reinforcements(4) setup_reinforcements(6) setup_reinforcements(8) setup_reinforcements(10) }, "Crusader" (DEPLOY) { setup_units(DEPLOY, 0, find_axis_units('S')) setup_units(DEPLOY, 0, find_axis_units(3)) setup_units(DEPLOY, 0, find_axis_units(5)) setup_units(DEPLOY, 0, find_axis_units(7)) setup_units(DEPLOY, 0, find_allied_units('S')) setup_units(DEPLOY, 0, find_allied_units('T')) setup_units(DEPLOY, 0, find_allied_units(2)) setup_units(DEPLOY, 0, find_allied_units(4)) setup_units(DEPLOY, 0, find_allied_units(6)) setup_units(DEPLOY, 0, find_allied_units(8)) setup_units(0, 0, [ "2/3", "2/SG", "9AU/20", "7AU/18" ]) setup_reinforcements(10) }, "Battleaxe" (DEPLOY) { setup_units(DEPLOY, 0, [ "21/5", "21/3", "15/33", "90/155", "15/115", "88mm/A" ]) setup_units(DEPLOY, -1, [ "21/104" ]) setup_units(DEPLOY, 0, [ "Ari", "Lit", "Pav", "Bre", "Tre", "Bol", "Ita" ]) setup_reinforcements(5) setup_reinforcements(7) setup_units(DEPLOY, 0, [ "4IN/3m", "9AU/20", "70/14+16", "70/23", "7/22G", "7/SG", "7AU/18", "Matilda/A", "/Tob", "/Pol", "7/7", "4IN/5", "4IN/7m", "4IN/11", "Matilda/B", "/1AT", "7/4", "7" ]) setup_reinforcements(6) setup_reinforcements(8) setup_reinforcements(10) }, "1942" (DEPLOY) { setup_units(DEPLOY, 0, [ "21/3", "15/33", "90/580", "90/sv288", "90/346", "88mm/A", ]) setup_units(DEPLOY, 1, [ "21/5", "15/8", "15/115", "90/155" ]) setup_units(DEPLOY, 1, [ "90/361", "90/200", "88mm/B", "50mm", "/104" ]) setup_units(DEPLOY, 0, [ "Lit" ]) setup_units(DEPLOY, -1, [ "Ari", "Tri", "Pav", "Bre", "Tre", "Bol", "Sab", "Ita" ]) setup_reinforcements(17) setup_reinforcements(19) setup_reinforcements('M') setup_units(TOBRUK, 0, [ "1/22", "/1AT", "Matilda/B", "1SA/2+5", "70/14+16", "1SA/1", "1SA/3", "2SA/4+6", "70/23", "/Tob", ]) setup_units(ALEXANDRIA, 1, [ "7/4", "/32AT", "2NZ/4", "2NZ/5", "2NZ/6", "7/SG", "4IN/5", "/Pol", ]) setup_reinforcements(12) setup_reinforcements(14) setup_reinforcements(16) setup_reinforcements(18) setup_reinforcements(20) }, "Gazala" (DEPLOY) { setup_units(DEPLOY, -1, [ "21/5", "15/8", "15/115", "90/155", "88mm/B", "50mm", "/104", ]) setup_units(DEPLOY, 0, [ "21/3", "15/33", "90/580", "90/sv288", "90/346", "90/361", "90/200", "88mm/A", ]) setup_units(DEPLOY, 0, [ "Lit" ]) setup_units(DEPLOY, -1, [ "Ari", "Tri", "Pav", "Bre", "Tre", "Bol", "Sab", "Ita" ]) setup_units(DEPLOY, 0, [ "Grant", "/1AT", "1/22", "Matilda/B", "1SA", "1/201G", "4IN/7m", "4IN/3m", "1/SG", "1SA/2+5", "70/14+16", "1SA/1", "1SA/3", "2SA/4+6", "70/23", "4IN/11", "5IN/29", "6#/A", "2#", "/Tob", ]) setup_units(ALEXANDRIA, 1, [ "1/2", "7/4", "/32AT", "2NZ/4", "2NZ/5", "2NZ/6", "7/SG", "7/22G", "4IN/5", "/Pol", ]) setup_units(ALEXANDRIA, 0, [ "10IN/161m", "8IN/18", "/B", "FF/2", "5IN/9+10", "10IN/21+25", ]) }, "Pursuit to Alamein" (DEPLOY) { setup_units(DEPLOY, 0, [ "21/3", "15/33", ]) setup_units(DEPLOY, -1, [ "21/5", "15/8", "15/115", "90/346", "90/sv288", "90/361", "90/200", "88mm/A", "88mm/B", "50mm", ]) setup_units(DEPLOY, 3, [ "Ari", "Tri", "Pav", "Bre", "Tre", ]) setup_units(DEPLOY, 2, [ "Lit", "Bol", ]) setup_units(DEPLOY, 1, [ "Sab", "Ita", ]) setup_reinforcements(17) setup_reinforcements(19) setup_reinforcements('M') setup_units(DEPLOY, 0, [ "10IN/161m", "10IN/21+25", "70/14+16", "8IN/18", "/B", ]) setup_units(DEPLOY, -1, [ "1/22", "7/4", "2NZ/4", "2NZ/5", "2NZ/6", "1SA/2+5", "1SA/1", "1SA/3", "70/23", "5IN/29", "6#/A", ]) setup_units(ALEXANDRIA, 1, [ "Grant", "1/2", "/1AT", "1SA", "7/SG", "1/SG", "FF/2", "5IN/9+10", "4IN/5", "4IN/11", "/Pol", "2#", ]) setup_reinforcements(16) setup_reinforcements(18) setup_reinforcements(20) }, "1941-42" (DEPLOY) { SETUP["1941"](-1) setup_reinforcements(11) setup_reinforcements(17) setup_reinforcements(19) setup_reinforcements('M') setup_reinforcements(12) setup_reinforcements(14) setup_reinforcements(16) setup_reinforcements(18) setup_reinforcements(20) }, } function setup_fortress(scenario, fortress) { if (scenario.allied_deployment.includes(fortress)) 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(units.length).fill(0), moved: [], fired: [], recover: [], axis_minefields: [], allied_minefields: [], minefields: [], // fortress control fortress: 7, axis_award: 0, allied_award: 0, // battle hexes (defender) axis_hexes: [], allied_hexes: [], // hexside control (for battle hexes) axis_sides: [], allied_sides: [], // current turn option and selected moves commit: [0, 0], turn_option: null, passed: 0, side_limit: {}, forced: null, rommel: 0, from1: 0, to1: 0, from2: 0, to2: 0, // retreat partial_retreats: [], // remember partial retreats to forbid initiating combat retreat: 0, retreat_units: null, // refuse battle refuse: 0, // rout rout: null, // combat active_battles: [], assault_battles: [], pursuit: 0, battle: 0, hits: null, flash: null, }) setup(scenario) return game } exports.view = function(state, current) { load_state(state) view = { month: game.month, units: game.units, 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, selected: game.selected, } if (current === AXIS) view.cards = game.axis_hand if (current === ALLIED) view.cards = game.allied_hand if (game.from1) view.from1 = game.from1 if (game.from2) view.from2 = game.from2 if (game.to1) view.to1 = game.to1 if (game.to2) view.to2 = game.to2 if (game.pursuit) view.pursuit = game.pursuit if (game.battle) view.battle = game.battle if (game.battle || game.pursuit) { view.hits = game.hits view.flash = game.flash } return common_view(current) } exports.query = function (state, current, q) { if (q === 'supply') { load_state(state) return { axis_supply: axis_supply_network(), axis_supply_line: axis_supply_line(), allied_supply: allied_supply_network(), allied_supply_line: allied_supply_line(), } } return null } function gen_action_next() { gen_action('next') } function gen_action_unit(u) { gen_action('unit', u) } function gen_action_hex(x) { gen_action('hex', x) } 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 }