"use strict" // TODO: retreat withdraw co-exist with disrupted enemy - no hexside limits or control // TODO: force withdrawal of mandatory combat during pass turn (only show end turn if not possible) // TODO: rout during probe combat? // TODO: allow one-hex regroup moves? (failed forced march abuse) // TODO: rewrite regroup to group if only one hex moved (failed forced march abuse) const max = Math.max const min = Math.min const abs = Math.abs const TIMEOUT = 250 var timeout = 0 function check_timeout() { if (Date.now() > timeout) throw "TIMEOUT" } var states = {} var game = null var view = null const { all_hexes, hex_exists, hex_road, side_road, side_limit, hex_name, regions, unit_name, unit_appearance, unit_elite, unit_class, unit_speed, unit_max_steps, } = require("./data.js") var after_rout_table = { end_battle: end_battle, end_refuse_battle_move_2: end_refuse_battle_move_2, end_retreat_2: end_retreat_2, goto_combat_phase: goto_combat_phase, goto_final_supply_check_rout: goto_final_supply_check_rout, goto_forced_marches_rout: goto_forced_marches_rout, goto_initial_supply_check_rout: goto_initial_supply_check_rout, } 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_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("")) } // === CRC32 CHECKSUM === const CRC32C_TABLE = new Int32Array([ 0x00000000, 0xf26b8303, 0xe13b70f7, 0x1350f3f4, 0xc79a971f, 0x35f1141c, 0x26a1e7e8, 0xd4ca64eb, 0x8ad958cf, 0x78b2dbcc, 0x6be22838, 0x9989ab3b, 0x4d43cfd0, 0xbf284cd3, 0xac78bf27, 0x5e133c24, 0x105ec76f, 0xe235446c, 0xf165b798, 0x030e349b, 0xd7c45070, 0x25afd373, 0x36ff2087, 0xc494a384, 0x9a879fa0, 0x68ec1ca3, 0x7bbcef57, 0x89d76c54, 0x5d1d08bf, 0xaf768bbc, 0xbc267848, 0x4e4dfb4b, 0x20bd8ede, 0xd2d60ddd, 0xc186fe29, 0x33ed7d2a, 0xe72719c1, 0x154c9ac2, 0x061c6936, 0xf477ea35, 0xaa64d611, 0x580f5512, 0x4b5fa6e6, 0xb93425e5, 0x6dfe410e, 0x9f95c20d, 0x8cc531f9, 0x7eaeb2fa, 0x30e349b1, 0xc288cab2, 0xd1d83946, 0x23b3ba45, 0xf779deae, 0x05125dad, 0x1642ae59, 0xe4292d5a, 0xba3a117e, 0x4851927d, 0x5b016189, 0xa96ae28a, 0x7da08661, 0x8fcb0562, 0x9c9bf696, 0x6ef07595, 0x417b1dbc, 0xb3109ebf, 0xa0406d4b, 0x522bee48, 0x86e18aa3, 0x748a09a0, 0x67dafa54, 0x95b17957, 0xcba24573, 0x39c9c670, 0x2a993584, 0xd8f2b687, 0x0c38d26c, 0xfe53516f, 0xed03a29b, 0x1f682198, 0x5125dad3, 0xa34e59d0, 0xb01eaa24, 0x42752927, 0x96bf4dcc, 0x64d4cecf, 0x77843d3b, 0x85efbe38, 0xdbfc821c, 0x2997011f, 0x3ac7f2eb, 0xc8ac71e8, 0x1c661503, 0xee0d9600, 0xfd5d65f4, 0x0f36e6f7, 0x61c69362, 0x93ad1061, 0x80fde395, 0x72966096, 0xa65c047d, 0x5437877e, 0x4767748a, 0xb50cf789, 0xeb1fcbad, 0x197448ae, 0x0a24bb5a, 0xf84f3859, 0x2c855cb2, 0xdeeedfb1, 0xcdbe2c45, 0x3fd5af46, 0x7198540d, 0x83f3d70e, 0x90a324fa, 0x62c8a7f9, 0xb602c312, 0x44694011, 0x5739b3e5, 0xa55230e6, 0xfb410cc2, 0x092a8fc1, 0x1a7a7c35, 0xe811ff36, 0x3cdb9bdd, 0xceb018de, 0xdde0eb2a, 0x2f8b6829, 0x82f63b78, 0x709db87b, 0x63cd4b8f, 0x91a6c88c, 0x456cac67, 0xb7072f64, 0xa457dc90, 0x563c5f93, 0x082f63b7, 0xfa44e0b4, 0xe9141340, 0x1b7f9043, 0xcfb5f4a8, 0x3dde77ab, 0x2e8e845f, 0xdce5075c, 0x92a8fc17, 0x60c37f14, 0x73938ce0, 0x81f80fe3, 0x55326b08, 0xa759e80b, 0xb4091bff, 0x466298fc, 0x1871a4d8, 0xea1a27db, 0xf94ad42f, 0x0b21572c, 0xdfeb33c7, 0x2d80b0c4, 0x3ed04330, 0xccbbc033, 0xa24bb5a6, 0x502036a5, 0x4370c551, 0xb11b4652, 0x65d122b9, 0x97baa1ba, 0x84ea524e, 0x7681d14d, 0x2892ed69, 0xdaf96e6a, 0xc9a99d9e, 0x3bc21e9d, 0xef087a76, 0x1d63f975, 0x0e330a81, 0xfc588982, 0xb21572c9, 0x407ef1ca, 0x532e023e, 0xa145813d, 0x758fe5d6, 0x87e466d5, 0x94b49521, 0x66df1622, 0x38cc2a06, 0xcaa7a905, 0xd9f75af1, 0x2b9cd9f2, 0xff56bd19, 0x0d3d3e1a, 0x1e6dcdee, 0xec064eed, 0xc38d26c4, 0x31e6a5c7, 0x22b65633, 0xd0ddd530, 0x0417b1db, 0xf67c32d8, 0xe52cc12c, 0x1747422f, 0x49547e0b, 0xbb3ffd08, 0xa86f0efc, 0x5a048dff, 0x8ecee914, 0x7ca56a17, 0x6ff599e3, 0x9d9e1ae0, 0xd3d3e1ab, 0x21b862a8, 0x32e8915c, 0xc083125f, 0x144976b4, 0xe622f5b7, 0xf5720643, 0x07198540, 0x590ab964, 0xab613a67, 0xb831c993, 0x4a5a4a90, 0x9e902e7b, 0x6cfbad78, 0x7fab5e8c, 0x8dc0dd8f, 0xe330a81a, 0x115b2b19, 0x020bd8ed, 0xf0605bee, 0x24aa3f05, 0xd6c1bc06, 0xc5914ff2, 0x37faccf1, 0x69e9f0d5, 0x9b8273d6, 0x88d28022, 0x7ab90321, 0xae7367ca, 0x5c18e4c9, 0x4f48173d, 0xbd23943e, 0xf36e6f75, 0x0105ec76, 0x12551f82, 0xe03e9c81, 0x34f4f86a, 0xc69f7b69, 0xd5cf889d, 0x27a40b9e, 0x79b737ba, 0x8bdcb4b9, 0x988c474d, 0x6ae7c44e, 0xbe2da0a5, 0x4c4623a6, 0x5f16d052, 0xad7d5351 ]) // fast with unlikely collisions function supply_memo_idx_crc32c(data, tail1, tail2) { var x = 0 for (var i = 0, n = data.length; i < n; ++i) x = CRC32C_TABLE[(x ^ data[i]) & 0xff] ^ (x >>> 8) x = CRC32C_TABLE[(x ^ tail1) & 0xff] ^ (x >>> 8) x = CRC32C_TABLE[(x ^ tail2) & 0xff] ^ (x >>> 8) return x } // slow but guaranteed no collisions function supply_memo_idx_slow(data, tail1, tail2) { var x = 0n for (var i = 0, n = data.length; i < n; ++i) x = (x << 8n) | BigInt(data[i]) x = (x << 8n) | BigInt(tail1) x = (x << 8n) | BigInt(tail2) return x } // === CONSTS === const AXIS = 'Axis' const ALLIED = 'Allied' const REAL = 0 const DUMMY = 1 const hexw = 25 const hexh = 9 const first_hex = 7 const last_hex = 215 const hexdeploy = hexw * hexh const hexnext = [ 1, hexw, hexw-1, -1, -hexw, -(hexw-1) ] const hexcount = last_hex + 1 const sidecount = hexcount * 3 const class_name = [ "armor", "infantry", "anti-tank", "artillery" ] const class_name_cap = [ "Armor", "Infantry", "Anti-tank", "Artillery" ] const firepower_name = [ "0", "1", "2", "3", "TF", "DF", "SF" ] const speed_name = [ "zero", "leg", "motorized", "mechanized", "recon" ] const speed_name_cap = [ "Zero", "Leg", "Motorized", "Mechanized", "Recon" ] const die_face_hit = [ 0, '\u2776', '\u2777', '\u2778', '\u2779', '\u277A', '\u277B' ] const die_face_miss = [ 0, '\u2460', '\u2461', '\u2462', '\u2463', '\u2464', '\u2465' ] const month_names_1940 = [ "", "September 1940", "October 1940", "November 1940", "December 1940", "January 1940", "February 1940", ] const month_names = [ "", "April 1941", "May 1941", "June 1941", "July 1941", "August 1941", "September 1941", "October 1941", "November 1941", "December 1941", "January 1942", "February 1942", "March 1942", "April 1942", "May 1942", "June 1942", "July 1942", "August 1942", "September 1942", "October 1942", "November 1942", ] function current_month_name() { if (game.scenario === "1940") return month_names_1940[game.month] else return month_names[game.month] } const SF = 6 const DF = 5 const TF = 4 const ARMOR = 0 const INFANTRY = 1 const ANTITANK = 2 const ARTILLERY = 3 const TRAIL = 1 const TRACK = 2 const HIGHWAY = 4 const FIREPOWER_MATRIX = [ [ SF, DF, SF, TF ], [ SF, SF, DF, 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 // Free deployment holding box const DEPLOY = 1 const ELIMINATED = 2 // 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 SIDI_OMAR = 89 const SOLLUM = 65 const BARDIA_FT_CAPUZZO = 122 const SS_NONE = 0 const SS_BASE = 1 const SS_BARDIA = 2 const SS_BENGHAZI = 3 const SS_TOBRUK = 4 const SS_OASIS = 5 const MF_AXIS = 0 const MF_ALLIED = 1 const MF_VISIBLE = 2 const MF_REVEAL = 3 const EGYPT = regions.Egypt const LIBYA = regions.Libya const LIBYA_NO_TOBRUK = LIBYA.filter(r => r !== TOBRUK) function calc_distance(a, b) { let ax = a % hexw, ay = (a / hexw)|0, az = -ax - ay let bx = b % hexw, by = (b / hexw)|0, bz = -bx - by return max(abs(bx-ax), abs(by-ay), abs(bz-az)) } function calc_distance_map(supply) { let map = new Array(hexcount) for (let x = 0; x < hexcount; ++x) map[x] = calc_distance(supply, x) return map } const distance_to = { [EL_AGHEILA]: calc_distance_map(EL_AGHEILA), [ALEXANDRIA]: calc_distance_map(ALEXANDRIA), [BENGHAZI]: calc_distance_map(BENGHAZI), [TOBRUK]: calc_distance_map(TOBRUK), [BARDIA]: calc_distance_map(BARDIA), } function to_side(a, b, s) { if (s < 3) return a * 3 + s return b * 3 + s - 3 } function to_side_id(a, b) { if (a > b) { let c = b b = a a = c } if (a + hexnext[0] === b) return a * 3 + 0 else if (a + hexnext[1] === b) return a * 3 + 1 else if (a + hexnext[2] === b) return a * 3 + 2 throw new Error("not a hexside " + a + " to " + b) } function is_map_hex(x) { return x >= first_hex && x <= last_hex && hex_exists[x] === 1 } function is_hex_or_adjacent_to(x, where) { if (x === where) return true for (let s = 0; s < 6; ++s) { let y = where + hexnext[s] if (is_map_hex(y) && x === y) return true } return false } // === STATE CACHES === const first_axis_unit = 0 const last_axis_unit = 33 const first_allied_unit = 34 const last_allied_unit = 93 const unit_count = 94 var presence_invalid = true var presence_axis = new Array(hexcount).fill(0) var presence_allied = new Array(hexcount).fill(0) var supply_axis_invalid = true var supply_axis_network = new Array(hexcount).fill(0) var supply_axis_line = new Array(sidecount).fill(0) var supply_allied_invalid = true var supply_allied_network = new Array(hexcount).fill(0) var supply_allied_line = new Array(sidecount).fill(0) var supply_bardia_invalid = true var supply_bardia_network = new Array(hexcount).fill(0) var supply_bardia_line = new Array(sidecount).fill(0) var supply_benghazi_invalid = true var supply_benghazi_network = new Array(hexcount).fill(0) var supply_benghazi_line = new Array(sidecount).fill(0) var supply_tobruk_invalid = true var supply_tobruk_network = new Array(hexcount).fill(0) var supply_tobruk_line = new Array(sidecount).fill(0) var supply_temp_network = new Array(hexcount).fill(0) var supply_temp_line = new Array(sidecount).fill(0) var first_friendly_unit, last_friendly_unit var first_enemy_unit, last_enemy_unit function set_active_player() { clear_undo() if (game.active !== game.phasing) { game.active = game.phasing update_aliases() } } function set_passive_player() { clear_undo() let nonphasing = (game.phasing === AXIS ? ALLIED : AXIS) if (game.active !== nonphasing) { game.active = nonphasing update_aliases() } } function set_enemy_player() { if (is_active_player()) set_passive_player() else set_active_player() } function is_active_player() { return game.active === game.phasing } function is_passive_player() { return game.active !== game.phasing } function is_axis_player() { return game.active === AXIS } function is_allied_player() { return game.active === ALLIED } function update_aliases() { if (game.active === AXIS) { first_friendly_unit = first_axis_unit last_friendly_unit = last_axis_unit first_enemy_unit = first_allied_unit last_enemy_unit = last_allied_unit } else { first_friendly_unit = first_allied_unit last_friendly_unit = last_allied_unit first_enemy_unit = first_axis_unit last_enemy_unit = last_axis_unit } } function invalidate_caches() { presence_invalid = true supply_axis_invalid = true supply_allied_invalid = true supply_tobruk_invalid = true supply_bardia_invalid = true supply_benghazi_invalid = true } function load_state(state) { if (game !== state) { game = state invalidate_caches() update_aliases() } } // === UNIT STATE === // location (8 bits), supply source (3 bits), steps lost (2 bits), disrupted (1 bit) function apply_select(u) { if (game.selected === u) game.selected = -1 else game.selected = u } function pop_selected() { let u = game.selected game.selected = -1 return u } const UNIT_DISRUPTED_SHIFT = 0 const UNIT_DISRUPTED_MASK = 1 << UNIT_DISRUPTED_SHIFT const UNIT_STEPS_SHIFT = 1 const UNIT_STEPS_MASK = 3 << UNIT_STEPS_SHIFT const UNIT_SUPPLY_SHIFT = 3 const UNIT_SUPPLY_MASK = 7 << UNIT_SUPPLY_SHIFT const UNIT_HEX_SHIFT = 6 const UNIT_HEX_MASK = 255 << UNIT_HEX_SHIFT function is_unit_disrupted(u) { return (game.units[u] & UNIT_DISRUPTED_MASK) === UNIT_DISRUPTED_MASK } function is_unit_undisrupted(u) { return (game.units[u] & UNIT_DISRUPTED_MASK) !== UNIT_DISRUPTED_MASK } function set_unit_disrupted(u) { invalidate_caches() game.units[u] |= UNIT_DISRUPTED_MASK } function clear_unit_disrupted(u) { invalidate_caches() game.units[u] &= ~UNIT_DISRUPTED_MASK } function unit_hex(u) { return (game.units[u] & UNIT_HEX_MASK) >> UNIT_HEX_SHIFT } function set_unit_hex(u, x) { invalidate_caches() game.units[u] = (game.units[u] & ~UNIT_HEX_MASK) | (x << UNIT_HEX_SHIFT) } function is_unit_supplied(u) { return ((game.units[u] & UNIT_SUPPLY_MASK) >> UNIT_SUPPLY_SHIFT) !== 0 } function is_unit_unsupplied(u) { return ((game.units[u] & UNIT_SUPPLY_MASK) >> UNIT_SUPPLY_SHIFT) === 0 } function unit_supply(u) { return (game.units[u] & UNIT_SUPPLY_MASK) >> UNIT_SUPPLY_SHIFT } function set_unit_supply(u, src) { game.units[u] = (game.units[u] & ~UNIT_SUPPLY_MASK) | (src << UNIT_SUPPLY_SHIFT) } function unit_lost_steps(u) { return (game.units[u] & UNIT_STEPS_MASK) >> UNIT_STEPS_SHIFT } function set_unit_lost_steps(u, n) { game.units[u] = (game.units[u] & ~UNIT_STEPS_MASK) | (n << UNIT_STEPS_SHIFT) } function unit_steps(u) { return unit_max_steps[u] - unit_lost_steps(u) } function set_unit_steps(u, n) { set_unit_lost_steps(u, unit_max_steps[u] - n) } function is_unit_moved(u) { return set_has(game.moved, u) } function set_unit_moved(u) { set_add(game.moved, u) } function is_unit_fired(u) { return set_has(game.fired, u) } function set_unit_fired(u) { set_add(game.fired, u) } function eliminate_unit(u) { invalidate_caches() game.units[u] = 0 set_unit_hex(u, ELIMINATED) hide_unit(u) } function is_unit_eliminated(u) { return unit_hex(u) === ELIMINATED } function reduce_unit(u) { let s = unit_steps(u) let hp = unit_hp_per_step(u) if (s === 1) eliminate_unit(u) else set_unit_steps(u, s - 1) return hp } function replace_unit(u) { let lost = unit_lost_steps(u) set_unit_lost_steps(u, lost - 1) } function reveal_units_in_hex(x) { for (let u = 0; u < unit_count; ++u) if (unit_hex(u) === x) set_add(game.revealed, u) } function hide_friendly_units_in_hex(x) { for (let u = first_friendly_unit; u <= last_friendly_unit; ++u) if (unit_hex(u) === x) set_delete(game.revealed, u) } function hide_units_in_hex(x) { for (let u = 0; u < unit_count; ++u) if (unit_hex(u) === x) set_delete(game.revealed, u) } function hide_unit(u) { set_delete(game.revealed, u) } // === UNIT DATA === function find_unit(name) { for (let u = 0; u < unit_count; ++u) if (unit_name[u] === name) return u throw new Error("cannot find named block: " + name + unit_name) } function is_axis_unit(u) { return u >= first_axis_unit && u <= last_axis_unit } function is_german_unit(u) { return (u >= 14 && u <= 33) } function is_elite_unit(u) { return unit_elite[u] } function is_artillery_unit(u) { return unit_class[u] === ARTILLERY } function unit_cv(u) { if (is_elite_unit(u)) return unit_steps(u) * 2 return unit_steps(u) } function unit_hp_per_step(u) { return is_elite_unit(u) ? 2 : 1 } function unit_hp(u) { return unit_steps(u) * unit_hp_per_step(u) } // === MAP STATE === function friendly_base() { if (is_axis_player()) return EL_AGHEILA return ALEXANDRIA } function friendly_queue() { if (is_axis_player()) return AXIS_QUEUE return ALLIED_QUEUE } function friendly_refit() { if (is_axis_player()) return AXIS_REFIT return ALLIED_REFIT } function update_presence() { presence_invalid = false presence_axis.fill(0) for (let u = first_axis_unit; u <= last_axis_unit; ++u) { let x = unit_hex(u) if (x >= first_hex && x <= last_hex) { if (is_unit_disrupted(u)) presence_axis[x] |= 1 else presence_axis[x] |= 2 } } presence_allied.fill(0) for (let u = first_allied_unit; u <= last_allied_unit; ++u) { let x = unit_hex(u) if (x >= first_hex && x <= last_hex) { if (is_unit_disrupted(u)) presence_allied[x] |= 1 else presence_allied[x] |= 2 } } } function count_friendly_units_in_hex(x) { let n = 0 for (let u = first_friendly_unit; u <= last_friendly_unit; ++u) if (unit_hex(u) === x) n++ return n } function any_friendly_undisrupted_unit_in_hex(x) { for (let u = first_friendly_unit; u <= last_friendly_unit; ++u) if (is_unit_undisrupted(u) && unit_hex(u) === x) return u throw Error("ASSERT: hex must have friendly undisrupted unit") } function has_friendly_unit_in_raw_hex(x) { for (let u = first_friendly_unit; u <= last_friendly_unit; ++u) if (unit_hex(u) === x) return true return false } function 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) { // battle hex if both sides present and at least one is undisrupted // disrupted units from both sides can peacefully co-exist if (presence_invalid) update_presence() let a = presence_axis[x] let b = presence_allied[x] return (a > 1 && b > 0) || (b > 1 && a > 0) } function is_enemy_hex(x) { if (game.active === ALLIED) return is_axis_hex(x) return is_allied_hex(x) } function has_friendly_unit(x) { if (game.active === AXIS) return has_axis_unit(x) return has_allied_unit(x) } function has_undisrupted_friendly_unit(x) { if (game.active === AXIS) return has_undisrupted_axis_unit(x) return has_undisrupted_allied_unit(x) } function has_disrupted_friendly_unit(x) { if (game.active === AXIS) return has_disrupted_axis_unit(x) return has_disrupted_allied_unit(x) } function has_enemy_unit(x) { if (game.active === ALLIED) return has_axis_unit(x) return has_allied_unit(x) } function has_undisrupted_enemy_unit(x) { if (game.active === ALLIED) return has_undisrupted_axis_unit(x) return has_undisrupted_allied_unit(x) } function has_unshielded_disrupted_enemy_unit(x) { if (game.active === ALLIED) return has_unshielded_disrupted_axis_unit(x) return has_unshielded_disrupted_allied_unit(x) } function has_unshielded_disrupted_friendly_unit(x) { if (game.active === ALLIED) return has_unshielded_disrupted_allied_unit(x) return has_unshielded_disrupted_axis_unit(x) } function is_enemy_rout_hex(x) { return has_undisrupted_friendly_unit(x) && has_unshielded_disrupted_enemy_unit(x) } function is_friendly_rout_hex(x) { return has_undisrupted_enemy_unit(x) && has_unshielded_disrupted_friendly_unit(x) } function is_new_battle_hex(a) { if (is_battle_hex(a)) return !set_has(game.axis_hexes, a) && !set_has(game.allied_hexes, a) return false } function claim_hexside_control(side) { if (is_axis_player()) { set_add(game.axis_sides, side) set_delete(game.allied_sides, side) } else { set_add(game.allied_sides, side) set_delete(game.axis_sides, side) } } function release_hex_control(a) { // no longer a battle hex: release hexsides if possible set_delete(game.axis_hexes, a) set_delete(game.allied_hexes, a) for_each_adjacent_hex(a, b => { if (!is_battle_hex(b)) { let side = to_side_id(a, b) set_delete(game.axis_sides, side) set_delete(game.allied_sides, side) } }) hide_units_in_hex(a) } function claim_hex_control_for_defender(a) { // a new battle hex: claim hex and hexsides for defender if (is_axis_player()) set_add(game.allied_hexes, a) else set_add(game.axis_hexes, a) for_each_adjacent_hex(a, b => { let side = to_side_id(a, b) if (side_limit[side] > 0) { if (is_axis_player()) { if (!set_has(game.axis_sides, side)) set_add(game.allied_sides, side) } else { if (!set_has(game.allied_sides, side)) set_add(game.axis_sides, side) } } }) } function capture_fortress(fortress, capacity) { if (!is_fortress_friendly_controlled(fortress)) { if (has_undisrupted_friendly_unit(fortress) && !has_enemy_unit(fortress)) { invalidate_caches() let fresh = set_fortress_friendly_controlled(fortress) if (fresh) { log(`Captured #${fortress}!`) if (is_axis_player()) { let award = capacity log(`Awarded ${award} bonus supply.`) game.axis_award += award } else { let award = Math.floor(capacity / 2) log(`Awarded ${award} bonus supply.`) game.allied_award += award } } else { log(`Recaptured #${fortress}!`) if (is_allied_player()) { let award = capacity log(`Canceled ${award} bonus supply.`) game.axis_award -= award } else { let award = Math.floor(capacity / 2) log(`Canceled ${award} bonus supply.`) game.allied_award -= award } } } } } // === FORTRESSES === function fortress_bit(fortress) { if (fortress === BARDIA) return 1 if (fortress === BENGHAZI) return 2 if (fortress === TOBRUK) return 4 return 0 } function is_fortress_allied_controlled(fortress) { return (game.fortress & fortress_bit(fortress)) !== 0 } 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 toggle_fortress_captured(fortress) { let bit = fortress_bit(fortress) << 3 let old = game.fortress & bit game.fortress ^= bit return old === 0 } 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 toggle_fortress_captured(fortress) } function is_port_besieged(port) { if (port === EL_AGHEILA || port === ALEXANDRIA) return false return is_fortress_besieged(port) } function is_fortress_besieged(fortress) { let result = false let besieged = is_fortress_axis_controlled(fortress) ? has_allied_unit : has_axis_unit for_each_adjacent_hex(fortress, x => { if (besieged(x)) result = true }) return result } // === ITERATORS === function for_each_adjacent_hex(here, fn) { for (let s = 0; s < 6; ++s) { let next = here + hexnext[s] if (is_map_hex(next)) fn(next, to_side(here, next, s)) } } 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_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_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_undisrupted(u) && unit_hex(u) === x) fn(u) } function has_undisrupted_and_unmoved_friendly_unit(x) { if (has_undisrupted_friendly_unit(x)) { for (let u = first_friendly_unit; u <= last_friendly_unit; ++u) if (unit_hex(u) === x && !is_unit_moved(u)) return true } return false } 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_undisrupted(u) && unit_hex(u) === x && !is_unit_moved(u)) fn(u) } function count_hex_or_adjacent_that_has_any_unit_that_can_move(here) { let n = 0 for_each_hex_and_adjacent_hex(here, x => { if (has_undisrupted_and_unmoved_friendly_unit(x)) { if (!has_enemy_unit(x) || can_any_disengage(x)) { n++ return } } }) return n } function for_each_enemy_unit_in_hex(x, fn) { for (let u = first_enemy_unit; u <= last_enemy_unit; ++u) if (unit_hex(u) === x) fn(u) } function for_each_undisrupted_enemy_unit_in_hex(x, fn) { for (let u = first_enemy_unit; u <= last_enemy_unit; ++u) if (is_unit_undisrupted(u) && unit_hex(u) === x) fn(u) } function count_and_reveal_battle_hexes() { let n = 0 for (let x of all_hexes) { if (is_battle_hex(x)) { reveal_units_in_hex(x) ++n } } return n } function has_friendly_units_in_battle() { let result = false for_each_undisrupted_friendly_unit_in_hex(game.battle, u => { if (!is_unit_retreating(u)) result = true }) return result } function count_normal_steps_in_battle() { let steps = [ 0, 0, 0, 0 ] for_each_undisrupted_friendly_unit_in_hex(game.battle, u => { if (!is_elite_unit(u)) if (!is_unit_retreating(u)) steps[unit_class[u]] += unit_steps(u) }) return steps } function count_elite_steps_in_battle() { let steps = [ 0, 0, 0, 0 ] for_each_undisrupted_friendly_unit_in_hex(game.battle, u => { if (is_elite_unit(u)) if (!is_unit_retreating(u)) steps[unit_class[u]] += unit_steps(u) }) return steps } function count_hp_in_battle() { let hp = [ 0, 0, 0, 0 ] for_each_undisrupted_enemy_unit_in_hex(game.battle, u => { if (!is_unit_retreating(u)) hp[unit_class[u]] += unit_hp(u) }) return hp } function count_normal_steps_in_pursuit() { let steps = 0 for_each_undisrupted_enemy_unit_in_hex(game.pursuit, u => { if (!is_elite_unit(u)) steps += unit_steps(u) }) return steps } function count_elite_steps_in_pursuit() { let steps = 0 for_each_undisrupted_enemy_unit_in_hex(game.pursuit, u => { if (is_elite_unit(u)) steps += unit_steps(u) }) return steps } function count_hp_in_pursuit() { let hp = 0 for_each_undisrupted_enemy_unit_in_hex(game.pursuit, u => { hp += unit_hp(u) }) return hp } function count_normal_steps_in_rout() { let steps = 0 for_each_enemy_unit_in_hex(game.pursuit, u => { if (!is_elite_unit(u)) steps += unit_steps(u) }) return steps } function count_elite_steps_in_rout() { let steps = 0 for_each_enemy_unit_in_hex(game.pursuit, u => { if (is_elite_unit(u)) steps += unit_steps(u) }) return steps } function count_hp_in_rout() { let hp = 0 for_each_enemy_unit_in_hex(game.pursuit, u => { hp += unit_hp(u) }) return hp } // === SUPPLY CARDS === function draw_supply_card(pile) { clear_undo() 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 if (n > 0) { 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 if (n > 0) { log(`Allied drew ${n} cards.`) for (let i = 0; i < n; ++i) game.allied_hand[draw_supply_card(game.draw_pile)]++ } } function shuffle_cards() { clear_undo() 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 === var supply_friendly_hexes, supply_friendly_sides, supply_friendly, supply_enemy, supply_net, supply_line var supply_visited = new Array(hexcount).fill(0) var supply_memo = new Set() // PASS 1: Forward scan from source to compute potential supply net and valid hex side crossings. // Simple uniform cost search. // NOTE: trace direction is from supply to unit function is_supply_line_blocked(here, next, side) { // impassable hexside if (side_limit[side] === 0) return true // into enemy hex if (supply_enemy[next] > 1 && supply_friendly[next] === 0) return true // into battle hex if (is_battle_hex(next)) { // through friendly hex sides if (!set_has(supply_friendly_sides, side)) return true } // out of battle hex if (is_battle_hex(here)) { // only defender (own control) if (!set_has(supply_friendly_hexes, here)) return true // through friendly hex sides if (!set_has(supply_friendly_sides, side)) return true } return false } const TRACE_HIGHWAY = 0 const TRACE_TRACK_1 = 1 const TRACE_TRAIL_1 = 2 const TRACE_TRACK_2 = 3 const TRACE_STOP = 7 const TRACE_ABORT = 8 // from supply source outwards function forward_supply_next_move_type(here_move, road) { if (here_move === TRACE_HIGHWAY) { if (road === HIGHWAY) return TRACE_HIGHWAY if (road === TRACK) return TRACE_TRACK_1 if (road === TRAIL) return TRACE_TRAIL_1 return TRACE_STOP } else if (here_move === TRACE_TRACK_1) { if (road === HIGHWAY) return TRACE_TRACK_2 if (road === TRACK) return TRACE_TRACK_2 if (road === TRAIL) return TRACE_STOP return TRACE_ABORT } else if (here_move === TRACE_TRAIL_1) { if (road === HIGHWAY) return TRACE_STOP if (road === TRACK) return TRACE_STOP if (road === TRAIL) return TRACE_STOP return TRACE_ABORT } else if (here_move === TRACE_TRACK_2) { if (road === HIGHWAY) return TRACE_STOP if (road === TRACK) return TRACE_STOP if (road === TRAIL) return TRACE_ABORT return TRACE_ABORT } return TRACE_ABORT } function trace_supply_network_1(start) { let search_order = [] supply_net[start] = 1 supply_visited.fill(8) let queue = [ (start << 3) | TRACE_HIGHWAY ] while (queue.length > 0) { let item = queue.shift() let here = item >> 3 let here_move = item & 7 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 let side = to_side(here, next, s) if (is_supply_line_blocked(here, next, side)) continue let next_move = forward_supply_next_move_type(here_move, side_road[side]) if (next_move === TRACE_ABORT) continue supply_line[side] = 1 // already seen (at same or better) if (next_move >= supply_visited[next]) continue // new supply chain at undisrupted friendly unit if (supply_friendly[next] > 1) next_move = TRACE_HIGHWAY supply_visited[next] = next_move if (supply_friendly[next] > 0 && !supply_net[next]) search_order.push(next) supply_net[next] = 1 if (next_move !== TRACE_STOP) queue.push((next << 3) | next_move) } } return search_order } // PASS 2: Filter net to only include supply lines used by units. // This is a recursive search to find _all_ possible paths. const BACK_HEAD = 0 const BACK_STOP = 7 const BACK_ABORT = 8 const BACK_HIGHWAY_1 = 1 const BACK_HIGHWAY_2 = 2 const BACK_HIGHWAY_3 = 3 const BACK_TRACK_1 = 4 const BACK_TRACK_2 = 5 const BACK_TRACK_3 = BACK_STOP const BACK_TRAIL_1 = 6 const BACK_TRAIL_2 = BACK_STOP const BACK_TRAIL_3 = BACK_ABORT // from unit back to supply source // TODO - double check all! function backward_supply_next_move_type(here_move, road) { if (here_move === BACK_HEAD) { if (road === HIGHWAY) return BACK_HIGHWAY_1 if (road === TRACK) return BACK_TRACK_1 if (road === TRAIL) return BACK_TRAIL_1 return BACK_STOP } if (here_move === BACK_HIGHWAY_1) { if (road === HIGHWAY) return BACK_HIGHWAY_2 if (road === TRACK) return BACK_TRACK_2 if (road === TRAIL) return BACK_TRAIL_2 return BACK_ABORT } if (here_move === BACK_HIGHWAY_2) { if (road === HIGHWAY) return BACK_HIGHWAY_3 if (road === TRACK) return BACK_TRACK_3 if (road === TRAIL) return BACK_TRAIL_3 return BACK_ABORT } if (here_move === BACK_HIGHWAY_3) { if (road === HIGHWAY) return BACK_HIGHWAY_3 if (road === TRACK) return BACK_ABORT if (road === TRAIL) return BACK_ABORT return BACK_ABORT } if (here_move === BACK_TRACK_1) { if (road === HIGHWAY) return BACK_TRACK_2 if (road === TRACK) return BACK_TRACK_2 if (road === TRAIL) return BACK_TRAIL_2 return BACK_ABORT } if (here_move === BACK_TRACK_2) { if (road === HIGHWAY) return BACK_TRACK_3 if (road === TRACK) return BACK_TRACK_3 if (road === TRAIL) return BACK_TRAIL_3 return BACK_ABORT } if (here_move === BACK_TRAIL_1) { if (road === HIGHWAY) return BACK_TRAIL_2 if (road === TRACK) return BACK_TRAIL_2 if (road === TRAIL) return BACK_TRAIL_2 return BACK_ABORT } return BACK_ABORT } function path_includes(list, x) { for (var i = list.length - 1; i >= 0; --i) if (list[i] === x) return true return false } function trace_unit_supply_line(full_path, path, here, here_move, ssrc, from_dir) { // CORRECT (but slow): let memo_idx = supply_memo_idx_slow(full_path, here, here_move * 6 + from_dir) // CRC32-C we pray for no collisions! let memo_idx = supply_memo_idx_crc32c(full_path, here, here_move * 6 + from_dir) if (supply_memo.has(memo_idx)) { return true } path.push(here) if (supply_friendly[here] > 1) full_path.push(here) let supplied = false for (let s = 0; s < 6; ++s) { let next = here + hexnext[s] // can't go off-map if (next < first_hex || next > last_hex) continue // already seen if (path_includes(full_path, next)) // supply chain - heads (can't loop back) continue if (path_includes(path, next)) // supply chain - intermediate links (can't double back from _same_ head) continue // not part of supply network (from pass 1) if (!supply_net[next]) continue // supply line blocked (from pass 1) let side = to_side(here, next, s) if (!supply_line[side]) continue let next_move = backward_supply_next_move_type(here_move, side_road[side]) if (next_move === TRACE_ABORT) continue // reached supply source if (next === ssrc) { supply_net[next] = 2 supply_line[side] = 2 supplied = true continue } // new supply chain at undisrupted friendly unit if (supply_friendly[next] > 1) next_move = BACK_HEAD // on highway but cannot depart from it (traveled too far on non-highway already)! if (next_move === BACK_STOP && hex_road[next] === HIGHWAY) next_move = BACK_HIGHWAY_3 if (trace_unit_supply_line(full_path, supply_friendly[next] > 1 ? [] : path, next, next_move, ssrc, s)) { supply_line[side] = 2 supplied = true } } path.pop() if (supply_friendly[here] > 1) full_path.pop() if (supplied) { supply_net[here] = 2 supply_memo.add(memo_idx) } return supplied } // finalize state after pass 2 function trace_supply_network_3() { for (let i = 0; i < sidecount; ++i) { if (supply_line[i] === 2) supply_line[i] = 1 else supply_line[i] = 0 } for (let i = 0; i < hexcount; ++i) { if (supply_net[i] === 2) supply_net[i] = 1 else supply_net[i] = 0 } } function trace_supply_network(start) { check_timeout() supply_net.fill(0) supply_line.fill(0) // Pass 1: Find hexes that are in supply range. let search_order = trace_supply_network_1(start) // Pass 2: Filter hexes that have actual supply lines from units. supply_memo.clear() for (let x of search_order) { if (supply_friendly[x] > 0 && x !== start) { trace_unit_supply_line([], [], x, BACK_HEAD, start, 0) } } trace_supply_network_3() supply_net[start] = 1 } function trace_fortress_network(fortress, ss) { check_timeout() supply_net.fill(0) supply_line.fill(0) if (supply_enemy[fortress] > 1 && supply_friendly[fortress] <= 1) return // Pass 1: Find hexes that could hold units supplied by fortress. trace_supply_network_1(fortress) // Pass 2: Filter hexes that have actual supply lines from units. supply_memo.clear() for (let u = 0; u < unit_count; ++u) { if (unit_supply(u) === ss) { let x = unit_hex(u) if (is_map_hex(x) && x !== fortress) { trace_unit_supply_line([], [], x, BACK_HEAD, fortress, 0) } } } trace_supply_network_3() supply_net[fortress] = 1 } function init_trace_supply(net, line, friendly) { if (presence_invalid) update_presence() supply_net = net supply_line = line if (friendly === AXIS) { supply_friendly_hexes = game.axis_hexes supply_friendly_sides = game.axis_sides supply_friendly = presence_axis supply_enemy = presence_allied } else { supply_friendly_hexes = game.allied_hexes supply_friendly_sides = game.allied_sides supply_friendly = presence_allied supply_enemy = presence_axis } } // For supplied hex checks during deployment and fortress assignment function trace_supply_to_base_or_fortress(source) { init_trace_supply(supply_temp_network, supply_temp_line, game.active) supply_net.fill(0) supply_line.fill(0) trace_supply_network_1(source) } function can_trace_supply_to_base_or_fortress(from) { return supply_net[from] > 0 } function update_axis_supply() { supply_axis_invalid = false init_trace_supply(supply_axis_network, supply_axis_line, AXIS) trace_supply_network(EL_AGHEILA) } function update_allied_supply() { supply_allied_invalid = false init_trace_supply(supply_allied_network, supply_allied_line, ALLIED) trace_supply_network(ALEXANDRIA) } function update_bardia_supply() { supply_bardia_invalid = false init_trace_supply(supply_bardia_network, supply_bardia_line, is_fortress_axis_controlled(BARDIA) ? AXIS : ALLIED) trace_fortress_network(BARDIA, SS_BARDIA) } function update_benghazi_supply() { supply_benghazi_invalid = false init_trace_supply(supply_benghazi_network, supply_benghazi_line, is_fortress_axis_controlled(BENGHAZI) ? AXIS : ALLIED) trace_fortress_network(BENGHAZI, SS_BENGHAZI) } function update_tobruk_supply() { supply_tobruk_invalid = false init_trace_supply(supply_tobruk_network, supply_tobruk_line, is_fortress_axis_controlled(TOBRUK) ? AXIS : ALLIED) trace_fortress_network(TOBRUK, SS_TOBRUK) } function axis_supply_line() { if (supply_axis_invalid) update_axis_supply() return supply_axis_line } function axis_supply_network() { if (supply_axis_invalid) update_axis_supply() return supply_axis_network } function allied_supply_line() { if (supply_allied_invalid) update_allied_supply() return supply_allied_line } function allied_supply_network() { if (supply_allied_invalid) update_allied_supply() return supply_allied_network } function bardia_supply_line() { if (supply_bardia_invalid) update_bardia_supply() return supply_bardia_line } function bardia_supply_network() { if (supply_bardia_invalid) update_bardia_supply() return supply_bardia_network } function benghazi_supply_line() { if (supply_benghazi_invalid) update_benghazi_supply() return supply_benghazi_line } function benghazi_supply_network() { if (supply_benghazi_invalid) update_benghazi_supply() return supply_benghazi_network } function tobruk_supply_line() { if (supply_tobruk_invalid) update_tobruk_supply() return supply_tobruk_line } function tobruk_supply_network() { if (supply_tobruk_invalid) update_tobruk_supply() return supply_tobruk_network } function unit_supply_line(who) { // Use saved supply line during withdrawal regroup moves. if (is_active_player() && game.withdraw && game.withdraw.supply_line) { switch (unit_supply(who)) { case SS_BARDIA: return game.withdraw.bardia_line case SS_BENGHAZI: return game.withdraw.benghazi_line case SS_TOBRUK: return game.withdraw.tobruk_line } return game.withdraw.supply_line } switch (unit_supply(who)) { case SS_BARDIA: return bardia_supply_line() case SS_BENGHAZI: return benghazi_supply_line() case SS_TOBRUK: return tobruk_supply_line() } if (is_axis_unit(who)) return axis_supply_line() return allied_supply_line() } function unit_supply_network(who) { switch (unit_supply(who)) { case SS_BARDIA: return bardia_supply_network() case SS_BENGHAZI: return benghazi_supply_network() case SS_TOBRUK: return tobruk_supply_network() } if (is_axis_unit(who)) return axis_supply_network() return allied_supply_network() } function unit_supply_distance(who) { switch (unit_supply(who)) { case SS_BARDIA: return distance_to[BARDIA] case SS_BENGHAZI: return distance_to[BENGHAZI] case SS_TOBRUK: return distance_to[TOBRUK] } if (is_axis_unit(who)) return distance_to[EL_AGHEILA] return distance_to[ALEXANDRIA] } function unit_supply_source(who) { switch (unit_supply(who)) { case SS_BARDIA: return BARDIA case SS_BENGHAZI: return BENGHAZI case SS_TOBRUK: return TOBRUK } if (is_axis_unit(who)) return EL_AGHEILA return ALEXANDRIA } function hex_supply_source(x) { // TODO: overlapping fortress supply? let who = any_friendly_undisrupted_unit_in_hex(x) return unit_supply_source(who) } function hex_supply_network(x) { // TODO: overlapping fortress supply? let who = any_friendly_undisrupted_unit_in_hex(x) return unit_supply_network(who) } function src_supply_network(src) { if (src === EL_AGHEILA) return axis_supply_network() if (src === ALEXANDRIA) return allied_supply_network() if (src === BARDIA) return bardia_supply_network() if (src === BENGHAZI) return benghazi_supply_network() if (src === TOBRUK) return tobruk_supply_network() } function friendly_supply_network() { if (is_axis_player()) return axis_supply_network() return allied_supply_network() } function friendly_supply_line() { if (is_axis_player()) return axis_supply_line() return allied_supply_line() } function query_friendly_supply_network_from_to(src, x, y) { let save_x let save_y if (is_axis_player()) { init_trace_supply(supply_temp_network, supply_temp_line, AXIS) if (x) save_x = presence_axis[x] if (y) save_y = presence_axis[y] if (x) presence_axis[x] = 0 if (y) presence_axis[y] = 2 trace_supply_network(src) if (x) presence_axis[x] = save_x if (y) presence_axis[y] = save_y } else { init_trace_supply(supply_temp_network, supply_temp_line, ALLIED) if (x) save_x = presence_allied[x] if (y) save_y = presence_allied[y] if (x) presence_allied[x] = 0 if (y) presence_allied[y] = 2 trace_supply_network(src) if (x) presence_allied[x] = save_x if (y) presence_allied[y] = save_y } return supply_temp_network } function query_friendly_supply_network(src) { if (is_axis_player()) init_trace_supply(supply_temp_network, supply_temp_line, AXIS) else init_trace_supply(supply_temp_network, supply_temp_line, ALLIED) trace_supply_network(src) return supply_temp_network } // === PATHING === const own_seen = new Array(hexcount) const own_supply_line = new Array(sidecount) function search_own_supply_line(here, ssrc, sline, sdist) { own_seen.fill(0) own_supply_line.fill(0) search_own_supply_line_rec([], here, ssrc, sline, sdist) } function search_own_supply_line_rec(path, here, ssrc, sline, sdist) { own_seen[here] = 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) continue // already seen if (own_seen[next]) continue let side = to_side(here, next, s) // must follow supply line if (sline[side] === 0) continue // reached supply source if (next === ssrc) { for (let x of path) own_supply_line[x] = 1 own_supply_line[side] = 1 continue } path.push(side) search_own_supply_line_rec(path, next, ssrc, sline, sdist) path.pop() } own_seen[here] = 0 } 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) // normal move: may not leave battle hex. may engage any enemy. may move freely. // normal withdrawal: may not leave battle hex. may engage disrupted enemy. must follow supply lines. // retreat move: must leave battle hex via friendly side. may ignore disrupted enemy. may move freely. // retreat withdrawal: must leave battle hex via friendly side. may ignore disrupted enemy. must follow supply lines. // TODO: cache search results from previous invocation function search_move(start, speed) { search_path_bfs(path_from[0], path_cost[0], start, 0, speed, false, null, null) search_path_bfs(path_from[1], path_cost[1], start, 1, speed + 1, false, null, null) search_path_bfs(path_from[2], path_cost[2], start, 2, speed + 2, false, null, null) search_path_bfs(path_from[4], path_cost[4], start, 4, speed + 4, false, null, null) } function search_move_retreat(start, speed) { search_path_bfs(path_from[0], path_cost[0], start, 0, speed, true, null, null) search_path_bfs(path_from[1], path_cost[1], start, 1, speed + 1, true, null, null) search_path_bfs(path_from[2], path_cost[2], start, 2, speed + 2, true, null, null) search_path_bfs(path_from[4], path_cost[4], start, 4, speed + 4, true, null, null) } function search_withdraw_fast(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) // This is a fast variant that allows following any supply line. // Only used when iterating all withdrawal permutations to find // possible valid group/regroup moves. search_path_bfs(path_from[0], path_cost[0], start, 0, speed, false, sline, sdist) search_path_bfs(path_from[1], path_cost[1], start, 1, speed + 1, false, sline, sdist) search_path_bfs(path_from[2], path_cost[2], start, 2, speed + 2, false, sline, sdist) search_path_bfs(path_from[4], path_cost[4], start, 4, speed + 4, false, sline, sdist) } function search_withdraw(who, bonus) { let ssrc = unit_supply_source(who) let sline = unit_supply_line(who) let sdist = unit_supply_distance(who) let speed = unit_speed[who] + bonus let start = unit_hex(who) search_own_supply_line(start, ssrc, sline, sdist) search_path_bfs(path_from[0], path_cost[0], start, 0, speed, false, own_supply_line, sdist) search_path_bfs(path_from[1], path_cost[1], start, 1, speed + 1, false, own_supply_line, sdist) search_path_bfs(path_from[2], path_cost[2], start, 2, speed + 2, false, own_supply_line, sdist) search_path_bfs(path_from[4], path_cost[4], start, 4, speed + 4, false, own_supply_line, sdist) } function search_withdraw_retreat(who, bonus) { let ssrc = unit_supply_source(who) let sline = unit_supply_line(who) let sdist = unit_supply_distance(who) let speed = unit_speed[who] + bonus let start = unit_hex(who) search_own_supply_line(start, ssrc, sline, sdist) search_path_bfs(path_from[0], path_cost[0], start, 0, speed, true, own_supply_line, sdist) search_path_bfs(path_from[1], path_cost[1], start, 1, speed + 1, true, own_supply_line, sdist) search_path_bfs(path_from[2], path_cost[2], start, 2, speed + 2, true, own_supply_line, sdist) search_path_bfs(path_from[4], path_cost[4], start, 4, speed + 4, true, own_supply_line, sdist) } function search_redeploy(start) { search_path_redeploy_bfs(path_cost[0], start, 0) search_path_redeploy_bfs(path_cost[1], start, 1) search_path_redeploy_bfs(path_cost[2], start, 2) search_path_redeploy_bfs(path_cost[4], start, 4) } // Breadth First Search function search_path_bfs(from, cost, start, road, max_cost, retreat, sline, sdist) { let path_enemy, enemy_sides let exit1, exit2 if (presence_invalid) update_presence() if (is_axis_player()) { path_enemy = presence_allied enemy_sides = game.allied_sides exit1 = exit2 = 175 } else { path_enemy = presence_axis enemy_sides = game.axis_sides exit1 = 99 exit2 = 148 } 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 // can't enter own map edge if (next === exit1 || next === exit2) 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(enemy_sides, side)) continue // may only ignore unshielded disrupted units if (next_enemy & 2) // has undisrupted enemy continue } else { if (sline) { // may only engage unshielded disrupted units if (next_enemy & 2) // has undisrupted enemy continue } // check hexside limit when engaging enemy units if (next_enemy) if ((game.side_limit[side] | 0) >= max_side) continue } from[next] = here cost[next] = next_cost if (!retreat) { // must stop when engaging enemy units if (next_enemy) continue } // enough movement allowance to keep going if (next_cost < max_cost) queue.push(next << 4 | next_cost) } } } function search_path_redeploy_bfs(cost, start, road) { let path_enemy, friendly_network, enemy_network, enemy_sides let exit1, exit2 if (presence_invalid) update_presence() if (is_axis_player()) { path_enemy = presence_allied friendly_network = game.buildup.axis_network enemy_network = game.buildup.allied_network enemy_sides = game.allied_sides exit1 = exit2 = 175 } else { path_enemy = presence_axis friendly_network = game.buildup.allied_network enemy_network = game.buildup.axis_network enemy_sides = game.axis_sides exit1 = 99 exit2 = 148 } cost.fill(63) cost[start] = 0 if (hex_road[start] < road) return let queue = [ start << 6 ] while (queue.length > 0) { let item = queue.shift() let here = item >> 6 let here_cost = item & 63 let next_cost = here_cost + 1 for (let s = 0; s < 6; ++s) { let next = here + hexnext[s] // can't go off-map if (next < first_hex || next > last_hex || !hex_exists[next]) continue // can't enter own map edge if (next === exit1 || next === exit2) continue // already seen if (cost[next] < 63) continue let side = to_side(here, next, s) let max_side = side_limit[side] // can't cross this hexside if (max_side === 0) continue // must stay on road for current bonus if (side_road[side] < road) continue // must stay within supply network, and not enter enemy network if (!friendly_network[next] || enemy_network[next]) continue // may not move into or through battle hexes let next_enemy = path_enemy[next] if (next_enemy) continue // if disengaging from battle, must not cross enemy hexside if (here === start && path_enemy[here] && set_has(enemy_sides, side)) continue cost[next] = next_cost // don't care about distance (need to find home base for refit) if (next_cost < 63) queue.push(next << 6 | next_cost) } } } function can_move_to(to, speed) { if (path_cost[4][to] <= speed + 4) return true if (path_cost[2][to] <= speed + 2) return true if (path_cost[1][to] <= speed + 1) return true if (path_cost[0][to] <= speed) return true return false } function move_road(to, speed) { if (path_cost[4][to] <= speed + 4) return 4 if (path_cost[2][to] <= speed + 2) return 2 if (path_cost[1][to] <= speed + 1) return 1 return 0 } function fastest_undisrupted_and_unmoved_friendly_unit_in_hex(from) { let max_speed = 0 let who = -1 for_each_undisrupted_and_unmoved_friendly_unit_in_hex(from, u => { let s = unit_speed[u] if (s > max_speed) { who = u max_speed = s } }) return who } 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 } } // === WITHDRAWAL CHECKS === function is_friendly_hexside(side) { if (is_axis_player()) return set_has(game.axis_sides, side) return set_has(game.allied_sides, side) } function is_enemy_hexside(side) { if (is_allied_player()) return set_has(game.axis_sides, side) return set_has(game.allied_sides, side) } function unit_has_supply_line(who) { let snet = unit_supply_network(who) if (snet[unit_hex(who)]) return true return false } function is_move_closer(sdist, from, to, side) { return side === BARDIA_FT_CAPUZZO || sdist[to] <= sdist[from] } function can_unit_withdraw(who) { let result = false if (unit_has_supply_line(who)) { let ssrc = unit_supply_source(who) let sline = unit_supply_line(who) let sdist = unit_supply_distance(who) let from = unit_hex(who) search_own_supply_line(from, ssrc, sline, sdist) for_each_adjacent_hex(from, to => { let side = to_side_id(from, to) if (side_limit[side] > 0 && !has_undisrupted_enemy_unit(to)) if (own_supply_line[side] && is_move_closer(sdist, from, to, side)) result = true }) } return result } function can_unit_disengage_and_withdraw(who) { let result = false if (unit_has_supply_line(who)) { let ssrc = unit_supply_source(who) let sline = unit_supply_line(who) let sdist = unit_supply_distance(who) let from = unit_hex(who) search_own_supply_line(from, ssrc, sline, sdist) for_each_adjacent_hex(from, to => { let side = to_side_id(from, to) if (side_limit[side] > 0 && !is_enemy_hexside(side) && !has_undisrupted_enemy_unit(to)) if (own_supply_line[side] && is_move_closer(sdist, from, to, side)) 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 (side_limit[side] > 0 && !is_enemy_hexside(side) && !has_undisrupted_enemy_unit(to)) result = true }) return result } function can_unit_disengage_and_withdraw_to(who, to, extra) { if (unit_has_supply_line(who)) { search_withdraw_retreat(who, extra) return can_move_to(to, unit_speed[who] + extra) } return false } 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_all_units_disengage_and_withdraw(from) { let result = true for_each_friendly_unit_in_hex(from, u => { if (result === true && !can_unit_disengage_and_withdraw(u)) result = false }) return result } function can_all_undisrupted_units_disengage_and_withdraw(from) { if (!has_undisrupted_friendly_unit(from)) return false let result = true for_each_undisrupted_friendly_unit_in_hex(from, u => { if (result === true && !can_unit_disengage_and_withdraw(u)) result = false }) return result } function can_all_undisrupted_units_withdraw(from) { if (!has_undisrupted_friendly_unit(from)) return false let result = true for_each_undisrupted_friendly_unit_in_hex(from, u => { if (result === true && !can_unit_withdraw(u)) result = false }) return result } function is_network_reduced(reference, candidate) { for (let x = first_hex; x <= last_hex; ++x) if (reference[x] - candidate[x] === 1) return true return false } function is_valid_withdrawal_group_move_from(x) { if (is_battle_hex(x)) { // can retreat, will always reduce supply network return can_all_undisrupted_units_disengage_and_withdraw(x) } else { // non-retreat withdrawal, check if network is reduced after we leave this hex if (!has_disrupted_friendly_unit(x) && can_all_undisrupted_units_withdraw(x)) { // All units in hex have the same supply source let src = hex_supply_source(x) let net = hex_supply_network(x) let new_net = query_friendly_supply_network_from_to(src, x, 0) if (is_network_reduced(net, new_net)) return true } } return false } function is_valid_withdrawal_group_move_to(src, net, from, to) { if (is_battle_hex(from)) { return true } else { let new_net = query_friendly_supply_network_from_to(src, from, to) if (is_network_reduced(net, new_net)) return true } return false } function list_valid_withdrawal_group_moves_to(src, net, from, speed) { let result = [] for (let to of all_hexes) if (to != from) if (can_move_to(to, speed) && is_valid_withdrawal_group_move_to(src, net, game.from1, to)) result.push(to) return result } function is_valid_withdrawal_regroup_move_from(x) { // 0 = never, 1 = maybe, 2 = always if (is_battle_hex(x)) { // can retreat, will always reduce supply network if (can_all_undisrupted_units_disengage_and_withdraw(x)) return 2 } else { // non-retreat withdrawal, check if network is reduced after we leave this hex if (can_all_undisrupted_units_withdraw(x)) { let src = hex_supply_source(x) let net = hex_supply_network(x) let new_net = query_friendly_supply_network_from_to(src, x, 0) if (is_network_reduced(net, new_net)) return 2 // does not reduce network by itself, but maybe in cooperation with other hex withdrawals? return 1 } } return 0 } function is_valid_withdrawal_regroup_permutation(src) { let net = src_supply_network(src) let new_net = query_friendly_supply_network(src) return is_network_reduced(net, new_net) } function includes_mandatory_hex(from, mandatory) { if (mandatory.length === 0) return true for (let m of mandatory) if (is_hex_or_adjacent_to(from, m)) return true return false } function list_valid_withdrawal_regroup_command_points() { let mandatory = [] let always = [] let test = [] let maybe = null for (let fortress of FORTRESS_HEX_LIST) { if (is_mandatory_combat(fortress)) { set_add(mandatory, fortress) for_each_hex_and_adjacent_hex(fortress, x => { let status = is_valid_withdrawal_regroup_move_from(x) if (status === 2) set_add(always, x) else if (status === 1) set_add(test, x) }) } } if (mandatory.length === 0) { for (let x of all_hexes) { let status = is_valid_withdrawal_regroup_move_from(x) if (status === 2) set_add(always, x) else if (status === 1) set_add(test, x) } } // console.log("WITHDRAW REGROUP CANDIDATES", always, test) if (is_axis_player()) maybe = list_withdrawal_permutations(EL_AGHEILA, test) else maybe = list_withdrawal_permutations(ALEXANDRIA, test) // TODO: fortress supply let from = [] let m, n // Usable command points for (let here of all_hexes) { if (!includes_mandatory_hex(here, mandatory)) continue if (here in maybe) set_add(from, here) else if (!is_enemy_hex(here)) { m = n = 0 for_each_hex_and_adjacent_hex(here, x => { // Must include at least one valid withdrawal hex to evacuate fully if (set_has(always, x)) m++ // TODO: allow one-hex regroup moves? (failed forced march abuse) // Must include at least two hexes to qualify as a regroup move if (has_undisrupted_friendly_unit(x)) n++ }) if (m >= 1 && n >= 2) set_add(from, here) } } return { from, always, maybe, to: null, evacuate: null } } function count_bits(v) { let c = 0 for (; v; ++c) v &= v - 1 return c } function list_withdrawal_permutations(src, maybe) { let rommel1 = (game.rommel === 1) ? 1 : 0 let result = {} let hexes // impossible... if (maybe.length < 2) return {} // console.log("LIST WITHDRAWAL REGROUPS", maybe) // List all possible command points with more than one 'maybe' hex let cmd_maybe = {} let cmd_points = {} for (let here of all_hexes) { if (!is_enemy_hex(here)) { hexes = null for_each_hex_and_adjacent_hex(here, x => { if (set_has(maybe, x) && hex_supply_source(x) === src) { if (!hexes) hexes = [] set_add(hexes, x) } }) if (hexes && hexes.length > 1) { let key = hexes.join(",") cmd_maybe[key] = hexes if (!(key in cmd_points)) cmd_points[key] = [] cmd_points[key].push(here) } } } // For each unique set of "maybe" command points, // find all permutations of removing more than one "maybe" hex. if (presence_invalid) update_presence() let presence_friendly = is_axis_player() ? presence_axis : presence_allied for (let key in cmd_maybe) { hexes = cmd_maybe[key] let n = hexes.length let n2 = 1 << n path_valid.fill(1) for (let bits = 3; bits < n2; ++bits) { if (count_bits(bits) >= 2) { // Evacuate presence for testing for (let i = 0; i < n; ++i) if (bits & (1 << i)) presence_friendly[hexes[i]] &= 1 // clear 'undisrupted' // Find the hexes that all units in the evacuated hexes can reach for (let i = 0; i < n; ++i) { if (bits & (1 << i)) { let from = hexes[i] let who = slowest_undisrupted_friendly_unit(from) let speed = unit_speed[who] // NOTE: Inaccurate withdrawal search... // It may yield false positives that will result // in having to undo if you pick an option that // required withdrawal along supply lines not // one's own. This should be very unlikely. if (true) search_withdraw_fast(who, 1 + rommel1) else search_withdraw(who, 1 + rommel1) for (let to = 0; to < hexcount; ++to) if (!can_move_to(to, speed + 1 + rommel1)) path_valid[to] = 0 } } // Test supply net for all possible destinations for (let x of all_hexes) { if (path_valid[x]) { let save_x = presence_friendly[x] presence_friendly[x] = 2 if (is_valid_withdrawal_regroup_permutation(src)) { for (let here of cmd_points[key]) { if (!(here in result)) result[here] = [] set_add(result[here], x) } } presence_friendly[x] = save_x } } // Restore presence for (let i = 0; i < n; ++i) if (bits & (1 << i)) presence_friendly[hexes[i]] |= 2 // set 'undisrupted' } } } // console.log("MAYBE", result) return result } function list_valid_withdrawal_regroup_destinations() { let rommel1 = (game.rommel === 1) ? 1 : 0 // Find hexes that can be reached by ALL units of ONE always-hex // ... that also reduces the network (either by full retreat, or checking move) let result = [] for_each_hex_and_adjacent_hex(game.from1, from => { if (set_has(game.regroup.always, from)) { if (is_battle_hex(from)) { let who = slowest_undisrupted_friendly_unit(from) let speed = unit_speed[who] search_withdraw_retreat(who, 1 + rommel1) for (let to of all_hexes) { if (to != from && can_move_to(to, speed + 1 + rommel1)) { set_add(result, to) } } } else { let who = slowest_undisrupted_friendly_unit(from) let speed = unit_speed[who] let src = unit_supply_source(who) let net = unit_supply_network(who) search_withdraw(who, 1 + rommel1) for (let to of all_hexes) { if (to != from && can_move_to(to, speed + 1 + rommel1)) { if (is_valid_withdrawal_group_move_to(src, net, from, to)) set_add(result, to) } } } } }) // List hexes that can be reached by ALL units of one valid permutation of maybe-hexes. if (game.from1 in game.regroup.maybe) { for (let to of game.regroup.maybe[game.from1]) set_add(result, to) } game.withdraw.to = result } function gen_withdrawal_regroup_destination() { for (let x of game.withdraw.to) gen_action_hex(x) } // === MINEFIELDS === function visit_hex(x) { let mf_enemy = (game.active === AXIS) ? MF_ALLIED : MF_AXIS if (set_has(game.minefields[mf_enemy], x)) set_add(game.minefields[MF_REVEAL], x) } function visit_path(from, to, speed) { let road = move_road(to, speed) while (to && to !== from) { visit_hex(to) to = path_from[road][to] } } function reveal_visited_minefields() { for (let x of game.minefields[MF_REVEAL]) { log(`Minefield at #${x}.`) set_delete(game.minefields[MF_AXIS], x) set_delete(game.minefields[MF_ALLIED], x) set_add(game.minefields[MF_VISIBLE], x) } set_clear(game.minefields[MF_REVEAL]) } // === SUPPLY COMMITMENT & TURN OPTION === function goto_turn_option() { game.commit = [ 0, 0 ] game.state = 'turn_option' } function player_hand() { return is_axis_player() ? game.axis_hand : game.allied_hand } states.turn_option = { inactive: "turn option", prompt() { if (game.commit[0] === 0 && game.commit[1] === 0) view.prompt = `Turn Option: Committed zero supply.` else if (game.commit[1] === 0) view.prompt = `Turn Option: Committed ${game.commit[0]} real supply.` else if (game.commit[0] === 0) view.prompt = `Turn Option: Committed ${game.commit[1]} dummy supply.` else view.prompt = `Turn Option: 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) { game.turn_option = option log_br() let n = game.commit[0] + game.commit[1] if (n === 0) log(`Played zero supply cards.`) else if (n === 1) log(`Played one supply card.`) else if (n === 2) log(`Played two supply cards.`) else if (n === 3) log(`Played three supply cards.`) log_br() if (game.turn_option === 'pass') game.passed++ else game.passed = 0 goto_move_phase() } // === PLAYER TURN === function goto_player_turn() { set_active_player() log_h2(game.phasing) // paranoid resetting of state game.side_limit = {} game.rommel = 0 game.from1 = game.from2 = 0 game.to1 = game.to2 = 0 // reset moved and fired flags set_clear(game.fired) set_clear(game.moved) game.commit = null goto_initial_supply_check() } function end_player_turn() { // Forget partial retreats set_clear(game.partial_retreats) // Reveal supply cards if (game.commit[0] + game.commit[1] > 0) { log_br() if (game.commit[0] === 0 && game.commit[1] === 0) log(`Revealed zero supply.`) else if (game.commit[1] === 0) log(`Revealed ${game.commit[0]} real supply.`) else if (game.commit[0] === 0) log(`Revealed ${game.commit[1]} dummy supply.`) else log(`Revealed ${game.commit[0]} real and ${game.commit[1]} dummy supply.`) log_br() } game.commit = null if (check_sudden_death_victory()) return if (game.passed === 2) { game.passed = 0 return end_month() } if (game.phasing === AXIS) game.phasing = ALLIED else game.phasing = AXIS goto_player_turn() } // === FORTRESS SUPPLY === function union_fortress_network(result, fort) { for (let i = first_hex; i <= last_hex; ++i) result[i] |= fort[i] } function union_fortress_line(result, fort) { for (let i = 0; i < sidecount; ++i) result[i] |= fort[i] } function union_axis_network() { let net = axis_supply_network().slice() if (net[BARDIA] === 0 && is_fortress_axis_controlled(BARDIA)) union_fortress_network(net, bardia_supply_network()) if (net[BENGHAZI] === 0 && is_fortress_axis_controlled(BENGHAZI)) union_fortress_network(net, benghazi_supply_network()) if (net[TOBRUK] === 0 && is_fortress_axis_controlled(TOBRUK)) union_fortress_network(net, tobruk_supply_network()) return net } function union_allied_network() { let net = allied_supply_network().slice() if (net[BARDIA] === 0 && is_fortress_allied_controlled(BARDIA)) union_fortress_network(net, bardia_supply_network()) if (net[BENGHAZI] === 0 && is_fortress_allied_controlled(BENGHAZI)) union_fortress_network(net, benghazi_supply_network()) if (net[TOBRUK] === 0 && is_fortress_allied_controlled(TOBRUK)) union_fortress_network(net, tobruk_supply_network()) return net } function union_axis_line() { let net = axis_supply_network() let line = axis_supply_line().slice() if (net[BARDIA] === 0 && is_fortress_axis_controlled(BARDIA)) union_fortress_line(line, bardia_supply_line()) if (net[BENGHAZI] === 0 && is_fortress_axis_controlled(BENGHAZI)) union_fortress_line(line, benghazi_supply_line()) if (net[TOBRUK] === 0 && is_fortress_axis_controlled(TOBRUK)) union_fortress_line(line, tobruk_supply_line()) return line } function union_allied_line() { let net = allied_supply_network() let line = allied_supply_line().slice() if (net[BARDIA] === 0 && is_fortress_allied_controlled(BARDIA)) union_fortress_line(line, bardia_supply_line()) if (net[BENGHAZI] === 0 && is_fortress_allied_controlled(BENGHAZI)) union_fortress_line(line, benghazi_supply_line()) if (net[TOBRUK] === 0 && is_fortress_allied_controlled(TOBRUK)) union_fortress_line(line, tobruk_supply_line()) return line } const FORTRESS_HEX_LIST = [ BARDIA, BENGHAZI, TOBRUK ] const FORTRESS_SRC_LIST = [ SS_BARDIA, SS_BENGHAZI, SS_TOBRUK ] function all_friendly_unsupplied_units() { let result = [] for_each_friendly_unit_on_map(u => { if (is_unit_unsupplied(u)) result.push(u) }) return result } function goto_fortress_supply(state) { game.state = state game.assign = 0 game.summary = [ 0, 0, 0 ] resume_fortress_supply() } function resume_fortress_supply() { while (game.assign < 3) { if (assign_fortress_supply()) return game.assign++ } end_fortress_supply() } function end_fortress_supply() { for (let ix = 0; ix < 3; ++ix) if (game.summary[ix] > 0) log(`Assigned ${game.summary[ix]} #${FORTRESS_HEX_LIST[ix]} supply.`) game.summary = null game.assign = 0 if (game.state === 'initial_fortress_supply') game.state = 'initial_oasis_supply' else if (game.state === 'final_fortress_supply') game.state = 'final_oasis_supply' else if (game.state === 'buildup_fortress_supply') game.state = 'buildup_oasis_supply' else if (game.state === 'end_game_fortress_supply') game.state = 'end_game_oasis_supply' resume_oasis_supply() } function list_fortress_supply_candidates(fortress) { let dist = distance_to[fortress] let list = [] trace_supply_to_base_or_fortress(fortress) for_each_friendly_unit_on_map(u => { if (is_unit_unsupplied(u)) if (can_trace_supply_to_base_or_fortress(unit_hex(u))) list.push(u) }) list.sort((a,b) => dist[unit_hex(a)] - dist[unit_hex(b)]) return list } function auto_assign_fortress_supply(list, fortress, ss, ix) { let dist = distance_to[fortress] while (list.length > 0 && game.capacity[ix] > 0) { let d0 = dist[unit_hex(list[0])] let n = 0 for (let u of list) if (dist[unit_hex(u)] === d0) ++n if (n <= game.capacity[ix]) { game.summary[ix] += n for (let u of list) if (dist[unit_hex(u)] === d0) set_unit_supply(u, ss) game.capacity[ix] -= n list = list.slice(n) } else { return true } } return false } function assign_fortress_supply() { let ix = game.assign let ss = FORTRESS_SRC_LIST[ix] let fortress = FORTRESS_HEX_LIST[ix] let base_net = friendly_supply_network() // Isolated friendly fortress with capacity! if (!base_net[fortress] && is_fortress_friendly_controlled(fortress) && game.capacity[ix] > 0) { let list = list_fortress_supply_candidates(fortress) if (auto_assign_fortress_supply(list, fortress, ss, ix)) return true } return false } const xxx_fortress_supply = { inactive: "supply check", prompt() { let ix = game.assign let fortress = FORTRESS_HEX_LIST[ix] view.prompt = `Supply Check: Assign fortress supply to ${hex_name[fortress]} (${game.capacity[ix]} capacity left).` if (game.capacity[ix] > 0) { let list = list_fortress_supply_candidates(fortress) if (list.length > 0) { let dist = distance_to[fortress] let d0 = dist[unit_hex(list[0])] for (let u of list) if (dist[unit_hex(u)] === d0) gen_action_unit(u) } } gen_action('next') }, unit(who) { let ix = game.assign let ss = FORTRESS_SRC_LIST[ix] push_undo() game.capacity[ix]-- game.summary[ix] ++ set_unit_supply(who, ss) }, next() { push_undo() game.assign++ resume_fortress_supply() }, } states.initial_fortress_supply = xxx_fortress_supply states.final_fortress_supply = xxx_fortress_supply states.buildup_fortress_supply = xxx_fortress_supply states.end_game_fortress_supply = xxx_fortress_supply // === OASIS SUPPLY === const OASIS_HEX_LIST = [ JALO_OASIS, JARABUB_OASIS, SIWA_OASIS ] function resume_oasis_supply() { while (game.assign < 3) { if (assign_oasis_supply()) return game.assign++ } end_oasis_supply() } function end_oasis_supply() { if (game.state === 'initial_oasis_supply') goto_initial_supply_check_recover() else if (game.state === 'final_oasis_supply') goto_final_supply_check_disrupt() else if (game.state === 'buildup_oasis_supply') goto_buildup_supply_check_recover() else if (game.state === 'end_game_oasis_supply') goto_end_game_supply_check_recover() } function assign_oasis_supply() { let ix = game.assign if (game.oasis[ix] > 0) { let oasis = OASIS_HEX_LIST[ix] let enemy_battle = is_axis_player() ? game.allied_hexes : game.axis_hexes if (has_friendly_unit(oasis)) { if (!set_has(enemy_battle, oasis)) { let n = count_friendly_units_in_hex(oasis) if (n > 1) return true if (n === 1) { for_each_friendly_unit_in_hex(oasis, u => { game.oasis[ix] = 0 log(`Assigned #${oasis} supply.`) set_unit_supply(u, SS_OASIS) }) } } } } return false } const xxx_oasis_supply = { inactive: "supply check", prompt() { let ix = game.assign let oasis = OASIS_HEX_LIST[ix] view.prompt = `Supply Check: Assign oasis supply to ${hex_name[oasis]}.` for_each_friendly_unit_in_hex(oasis, u => { gen_action_unit(u) }) }, unit(who) { let ix = game.assign let oasis = OASIS_HEX_LIST[ix] push_undo() game.oasis[ix] = 0 log(`Assigned #${oasis} supply.`) set_unit_supply(who, SS_OASIS) game.assign++ resume_oasis_supply() }, } states.initial_oasis_supply = xxx_oasis_supply states.final_oasis_supply = xxx_oasis_supply states.buildup_oasis_supply = xxx_oasis_supply states.end_game_oasis_supply = xxx_oasis_supply // === INITIAL SUPPLY CHECK === function goto_initial_supply_check() { let base_net = friendly_supply_network() for_each_friendly_unit_on_map(u => { if (base_net[unit_hex(u)]) set_unit_supply(u, SS_BASE) else set_unit_supply(u, SS_NONE) }) if (is_axis_player()) game.capacity = [ 1, 1, 2 ] else game.capacity = [ 2, 2, 5 ] game.oasis = [ 1, 1, 1 ] goto_fortress_supply('initial_fortress_supply') } function goto_initial_supply_check_recover() { let summary = [] for (let u of game.recover) { if (is_unit_supplied(u) && is_unit_disrupted(u) && !is_battle_hex(unit_hex(u))) { set_add(summary, unit_hex(u)) clear_unit_disrupted(u) } } for (let x of summary) log(`Recovered at #${x}.`) // remember enemy units that can recover on their next turn set_clear(game.recover) for_each_enemy_unit_on_map(u => { if (is_unit_disrupted(u)) set_add(game.recover, u) }) goto_initial_supply_check_rout() } function goto_initial_supply_check_rout() { let rout = false for (let x of all_hexes) if (is_enemy_rout_hex(x)) rout = true if (rout) game.state = 'initial_supply_check_rout' else goto_turn_option() } states.initial_supply_check_rout = { inactive: "supply check", prompt() { view.prompt = `Initial Supply Check: Rout!` for (let x of all_hexes) if (is_enemy_rout_hex(x)) gen_action_hex(x) }, hex(where) { goto_rout(where, true, goto_initial_supply_check_rout) } } // === FINAL SUPPLY CHECK === function goto_final_supply_check() { set_active_player() set_clear(game.fired) log_br() capture_fortress(BARDIA, 2) capture_fortress(BENGHAZI, 2) capture_fortress(TOBRUK, 5) // Raiders! for (let [u, side] of game.raiders) { let x = unit_hex(u) if (is_map_hex(x) && is_enemy_hexside(side)) { log(`Disrupted raider at #${x}.`) set_unit_disrupted(u) } } game.raiders.length = 0 // Unsupplied and in danger of disruption! game.disrupt = all_friendly_unsupplied_units() // Now in supply! let base_net = friendly_supply_network() let summary = [] for (let u of game.disrupt) { if (base_net[unit_hex(u)]) { set_add(summary, unit_hex(u)) set_unit_supply(u, SS_BASE) } } for (let x of summary) log(`Restored supply at #${x}.`) // Assign leftover fortress and oasis supply goto_fortress_supply('final_fortress_supply') } function goto_final_supply_check_disrupt() { let summary = [] for (let u of game.disrupt) { if (is_unit_unsupplied(u) && is_unit_undisrupted(u)) { set_add(summary, unit_hex(u)) set_unit_disrupted(u) } } for (let x of summary) log(`Disrupted at #${x}.`) game.disrupt = null goto_final_supply_check_rout() } function goto_final_supply_check_rout() { let rout = false for (let x of all_hexes) if (is_friendly_rout_hex(x)) rout = true if (rout) game.state = 'final_supply_check_rout' else end_player_turn() } states.final_supply_check_rout = { inactive: "supply check", 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) } } function save_withdrawal_supply_lines() { game.withdraw = {} game.withdraw.supply_net = friendly_supply_network().slice() game.withdraw.supply_line = friendly_supply_line().slice() // Units assigned fortress supply MUST withdraw using fortress supply lines, // even if they are currently in the base supply network. if (is_fortress_friendly_controlled(BARDIA)) { game.withdraw.bardia_net = bardia_supply_network().slice() game.withdraw.bardia_line = bardia_supply_line().slice() } if (is_fortress_friendly_controlled(BENGHAZI)) { game.withdraw.benghazi_net = benghazi_supply_network().slice() game.withdraw.benghazi_line = benghazi_supply_line().slice() } if (is_fortress_friendly_controlled(TOBRUK)) { game.withdraw.tobruk_net = tobruk_supply_network().slice() game.withdraw.tobruk_line = tobruk_supply_line().slice() } } // === MOVEMENT PHASE === function push_move_summary(from, to, via, forced) { let mm = (from) | (to << 8) | (via << 16) | (forced << 24) if (game.summary === null) game.summary = {} game.summary[mm] = (game.summary[mm]|0) + 1 } function flush_move_summary() { if (game.summary === null) return log_br() if (!game.from1 && !game.from2) { log(`Passed.`) } else if (!game.from2 && !game.to1) { log(`Moved from #${game.from1}`) } else if (!game.from2 && game.to1) { log(`Moved to #${game.to1}`) } else { log(`Moved`) } let keys = Object.keys(game.summary).map(Number).sort((a,b)=>a-b) for (let mm of keys) { let n = game.summary[mm] let from = (mm) & 255 let to = (mm >>> 8 ) & 255 let via = (mm >>> 16) & 255 let forced = (mm >>> 24) & 1 if (!game.from2 && !game.to1 && from === game.from1) { log(`>${n} to #${to}`) } else if (!game.from2 && game.to1 && to === game.to1) { log(`>${n} from #${from}`) } else { log(`>${n} #${from} to #${to}`) } if (via) { if (forced) log(`>>via #${via} *`) else log(`>>via #${via}`) } } log_br() game.summary = null } function flush_withdraw_summary() { log("Withdrew\n") for (let key in game.summary) { let n = game.summary[key] let to = +key if (to === 0) log(`>${n} eliminated`) else log(`>${n} to #${to}`) } game.summary = null } function goto_move_phase() { set_clear(game.fired) game.used = 0 game.forced = [] game.state = 'select_moves' if (game.phasing === AXIS && game.scenario !== "1940" && game.rommel === 0) if (game.turn_option !== 'offensive' && game.turn_option !== 'blitz') game.rommel = 1 if (game.turn_option === 'pass') { try { save_withdrawal_supply_lines() game.group = list_valid_withdrawal_group_moves() game.regroup = list_valid_withdrawal_regroup_command_points() } catch (err) { if (err !== "TIMEOUT") throw err game.state = 'select_moves_timeout' } } else { game.group = list_valid_group_moves() game.regroup = list_valid_regroup_moves() } } function show_move_commands() { if (game.from1 && game.from2) view.selected_hexes = [ game.from1, game.from2 ] else if (game.from1) view.selected_hexes = game.from1 } function has_valid_group_move_left() { if (!game.group) return false if (game.group.length > 1) return true if (game.group.length > 0) { if (!game.from1) return true if (!game.to1) return true } return false } function has_valid_regroup_move_left() { if (game.regroup) { if (game.turn_option === 'pass') return game.regroup.from.length > 0 return game.regroup.length > 0 } return false } states.select_moves = { inactive: "movement", prompt() { show_move_commands() if (game.turn_option === 'offensive') { if (game.from1) view.prompt = `Movement: Designate second offensive move.` else view.prompt = `Movement: Designate first offensive move.` } else if (game.turn_option === 'blitz') { view.prompt = `Movement: Designate first blitz move.` } else { view.prompt = `Movement: Designate ${game.turn_option} move.` } if (game.state === 'select_moves_timeout') { if (game.group && !game.regroup) view.prompt = "Movement: Computation timed out. Regroup moves not available." else view.prompt = "Movement: Computation timed out. Some moves may not be available." } let can_group_move = has_valid_group_move_left() ? 1 : 0 let can_regroup_move = has_valid_regroup_move_left() ? 1 : 0 if (game.phasing === AXIS && game.scenario !== "1940" && game.rommel === 0) { if (game.turn_option === 'offensive' || game.turn_option === 'blitz') { view.actions.group = can_group_move view.actions.regroup = can_regroup_move } view.actions.group_rommel = can_group_move view.actions.regroup_rommel = can_regroup_move } else { view.actions.group = can_group_move view.actions.regroup = can_regroup_move } if (game.turn_option === 'pass') { if (has_mandatory_withdrawals()) view.actions.end_turn = 0 else view.actions.end_turn = 1 } else if (!can_group_move && !can_regroup_move) { gen_action('end_move') } }, group_rommel() { push_undo() game.rommel = (game.from1 === 0) ? 1 : 2 game.state = 'group_move_from' }, group() { push_undo() game.state = 'group_move_from' }, regroup_rommel() { push_undo() game.rommel = (game.from1 === 0) ? 1 : 2 game.state = 'regroup_move_command_point' }, regroup() { push_undo() game.state = 'regroup_move_command_point' }, end_move() { end_movement() }, end_turn() { game.summary = null game.group = null game.regroup = null game.withdraw = null goto_combat_phase() }, } states.select_moves_timeout = states.select_moves function list_valid_group_moves() { let result = [] for (let x of all_hexes) { if (has_undisrupted_and_unmoved_friendly_unit(x)) { if (!has_enemy_unit(x) || can_any_disengage(x)) set_add(result, x) } } if (has_friendly_unit_in_raw_hex(friendly_queue())) set_add(result, friendly_queue()) return result } function list_valid_withdrawal_group_moves() { let result = [] let mandatory = false if (is_mandatory_combat(BARDIA) && is_valid_withdrawal_group_move_from(BARDIA)) { set_add(result, BARDIA) mandatory = true } if (is_mandatory_combat(BENGHAZI) && is_valid_withdrawal_group_move_from(BENGHAZI)) { set_add(result, BENGHAZI) mandatory = true } if (is_mandatory_combat(TOBRUK) && is_valid_withdrawal_group_move_from(TOBRUK)) { set_add(result, TOBRUK) mandatory = true } try { if (!mandatory) { for (let x of all_hexes) { if (has_undisrupted_friendly_unit(x)) { if (is_valid_withdrawal_group_move_from(x)) set_add(result, x) } } } } catch (err) { if (err !== "TIMEOUT") throw err } return result } function list_valid_regroup_moves() { let result = [] for (let x of all_hexes) { if (!is_enemy_hex(x)) { let n = count_hex_or_adjacent_that_has_any_unit_that_can_move(x) // TODO: allow one-hex regroup moves? (failed forced march abuse) if (n >= 2) set_add(result, x) } } return result } states.group_move_from = { inactive: "movement", prompt() { show_move_commands() view.prompt = `Group Move: Select group to move.` for (let x of game.group) if (x !== game.from1 || game.to1) gen_action_hex(x) }, hex(x) { push_undo() if (game.from1 === 0) { game.from1 = x print_move_1() } else { game.from2 = x print_move_2() } if (x === friendly_queue()) { for_each_friendly_unit_in_hex(friendly_queue(), u => { push_move_summary(friendly_queue(), friendly_base(), 0, 0) set_unit_hex(u, friendly_base()) set_unit_moved(u) }) } if (game.turn_option === 'offensive' && !game.from2) game.state = 'select_moves' else goto_move() if (game.turn_option === 'pass') { // Precalculate valid withdrawal move destinations here if (!is_battle_hex(game.from1)) { let rommel1 = (game.rommel === 1) ? 1 : 0 // Note: All units in hex have the same supply source. let who = fastest_undisrupted_friendly_unit(game.from1) let src = unit_supply_source(who) let net = unit_supply_network(who) // console.log("CALC WITHDRAWAL DESTINATIONS") search_withdraw(who, 1 + rommel1) game.withdraw.to = list_valid_withdrawal_group_moves_to(src, net, game.from1, unit_speed[who] + 1 + rommel1) // console.log("DONE") } else { // console.log("CALC WITHDRAWAL SKIPPED: full retreat") } } }, } states.regroup_move_command_point = { inactive: "movement", prompt() { show_move_commands() view.prompt = `Regroup Move: Select the command point hex.` if (game.turn_option !== 'pass') { for (let x of game.regroup) gen_action_hex(x) } else { for (let x of game.regroup.from) gen_action_hex(x) } }, hex(x) { push_undo() if (game.from1 === 0) game.from1 = x else game.from2 = x game.state = 'regroup_move_destination' if (game.turn_option === 'pass') list_valid_withdrawal_regroup_destinations() }, } states.regroup_move_destination = { inactive: "movement", prompt() { show_move_commands() view.prompt = `Regroup Move: Select the destination hex.` let cp, rommel if (game.from2 === 0) cp = game.from1, rommel = (game.rommel === 1 ? 1 : 0) else cp = game.from2, rommel = (game.rommel === 2 ? 1 : 0) if (game.turn_option !== 'pass') { 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) } else { gen_withdrawal_regroup_destination() } }, hex(x) { push_undo() if (game.from2 === 0) { game.to1 = x print_move_1() } else { game.to2 = x print_move_2() } if (game.turn_option === 'offensive' && !game.from2) game.state = 'select_moves' else goto_move() }, } function end_movement() { // TODO: track whether a regroup move has affected more than one hex // scan summary to detect (only bother if forced march is involved) // TODO: convert single hex regroup move to group move for forced march attempts flush_move_summary() game.group = null game.regroup = null game.withdraw = null game.from1 = game.from2 = game.to1 = game.to2 = 0 goto_forced_marches() } // === GROUP AND REGROUP MOVEMENT === function search_current_move(who, is_retreat) { let rommel1 = (game.rommel === 1) ? 1 : 0 let rommel2 = (game.rommel === 2) ? 1 : 0 let from = unit_hex(who) let speed = unit_speed[who] if (game.turn_option !== 'pass') { if (is_retreat) search_move_retreat(from, speed + 1 + (rommel1 | rommel2)) else search_move(from, speed + 1 + (rommel1 | rommel2)) } else { if (is_retreat) search_withdraw_retreat(who, 1 + rommel1) else search_withdraw(who, 1 + rommel1) } } function print_move_1() { if (game.rommel === 1) { if (game.from1 && game.to1) log(`Regroup move with Rommel\nfrom #${game.from1}\nto #${game.to1}.`) else if (game.from1) log(`Group move with Rommel\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}.`) } } function print_move_2() { if (game.rommel === 2) { if (game.from2 && game.to2) log(`Regroup (R) move\nfrom #${game.from2}\nto #${game.to2}.`) else if (game.from2) log(`Group (R) 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}.`) } } function goto_move() { log_br() game.state = 'move' } function is_withdraw_network_reduced() { if (game.withdraw.supply_net) { if (is_network_reduced(game.withdraw.supply_net, friendly_supply_network())) return true if (game.withdraw.bardia_net && is_network_reduced(game.withdraw.bardia_net, bardia_supply_network())) return true if (game.withdraw.benghazi_net && is_network_reduced(game.withdraw.benghazi_net, benghazi_supply_network())) return true if (game.withdraw.tobruk_net && is_network_reduced(game.withdraw.tobruk_net, tobruk_supply_network())) return true } return false } function can_end_move() { if (game.turn_option !== 'pass') return true if (game.to1) { // must retreat from mandatory combat! for (let fortress of FORTRESS_HEX_LIST) { if (is_hex_or_adjacent_to(game.from1, fortress)) if (is_mandatory_combat(fortress) && set_has(game.regroup.always, fortress)) return !has_friendly_unit(fortress) } // quick check for (let x of game.regroup.always) if (!has_friendly_unit(x)) return true // full check return is_withdraw_network_reduced() } else { if (!has_friendly_unit(game.from1)) return true } } function gen_action_end_move() { if (game.turn_option === 'offensive') { if (game.used !== 3) gen_action('confirm_end_move') else gen_action('end_move') } else { if (game.used !== 1) gen_action('confirm_end_move') else gen_action('end_move') } } states.move = { inactive: "movement", prompt() { let rommel1 = (game.rommel === 1) ? 1 : 0 let rommel2 = (game.rommel === 2) ? 1 : 0 if (game.selected < 0) { let can_move = false // 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) can_move = true }) } } // 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) can_move = true }) } } // Select Regroup Move 1 if (game.to1) { for_each_hex_and_adjacent_hex(game.from1, from => { if (!has_enemy_unit(from) && from !== game.to1) { let fastest = fastest_undisrupted_and_unmoved_friendly_unit_in_hex(from) if (fastest >= 0) { search_current_move(fastest, false) 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) can_move = true } }) } } }) } // Select Regroup Move 2 if (game.to2) { for_each_hex_and_adjacent_hex(game.from2, from => { if (!has_enemy_unit(from) && from !== game.to2) { let fastest = fastest_undisrupted_and_unmoved_friendly_unit_in_hex(from) if (fastest >= 0) { search_current_move(fastest, false) 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) can_move = true } }) } } }) } // Overrun let has_overrun_hex = false for (let x of all_hexes) { if (is_enemy_rout_hex(x)) { has_overrun_hex = true break } } if (has_overrun_hex) { if (can_move) view.prompt = `Movement: Select unit to move, or hex to overrun.` else view.prompt = `Movement: Select hex to overrun.` gen_action('overrun') } else { if (can_select_retreat_hex()) { if (can_move) view.prompt = `Movement: Select unit to move, or retreat after all other movement.` else view.prompt = `Movement: You may retreat.` gen_action('retreat') if (can_end_move()) gen_action_end_move() } else { if (can_move) view.prompt = `Movement: Select unit to move.` else view.prompt = `Movement: Done.` if (can_end_move()) gen_action_end_move() } } } else { view.prompt = `Movement: Select destination.` // Deselect gen_action_unit(game.selected) // Move if (game.turn_option !== 'pass') { search_move(unit_hex(game.selected), unit_speed[game.selected] + 1 + (rommel1 | rommel2)) gen_move() } else { search_withdraw(game.selected, 1 + rommel1) gen_withdraw() } } }, unit(who) { apply_select(who) }, forced_march(to) { this.hex(to) }, hex(to) { apply_move(to, false) }, retreat() { push_undo() log_br() game.state = 'retreat_from' }, overrun() { push_undo() flush_move_summary() game.state = 'overrun' if (false) { let n = 0 let where = 0 for (let x of all_hexes) { if (is_enemy_rout_hex(x)) { n ++ where = x } } if (n === 1) return goto_overrun(where) } }, confirm_end_move() { this.end_move() }, end_move() { end_movement() } } states.overrun = { inactive: "movement", prompt() { view.prompt = `Movement: Select hex to overrun.` for (let x of all_hexes) if (is_enemy_rout_hex(x)) gen_action_hex(x) }, hex(where) { // return to move state afterwards game.state = 'move' goto_overrun(where) }, } function goto_overrun(where) { goto_rout(where, true, null) } function do_gen_move_to(_from, to, speed) { if (can_move_to(to, speed)) { gen_action_hex(to) } else if (can_move_to(to, speed + 1)) { if (!view.actions.hex || !set_has(view.actions.hex, to)) gen_action_forced_march(to) } } 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) // view.path = {} if (rommel1) { if (!game.to1 && game.from1 === from) for (let to of all_hexes) if (to != from) do_gen_move_to(from, to, speed + 1) if (game.to1 && is_hex_or_adjacent_to(from, game.from1)) do_gen_move_to(from, game.to1, speed + 1) } if (rommel2) { if (!game.to2 && game.from2 === from) for (let to of all_hexes) if (to != from) do_gen_move_to(from, to, speed + 1) if (game.to2 && is_hex_or_adjacent_to(from, game.from2)) do_gen_move_to(from, game.to2, speed + 1) } if (!rommel1) { if (!game.to1 && game.from1 === from) for (let to of all_hexes) if (to != from) do_gen_move_to(from, to, speed) if (game.to1 && is_hex_or_adjacent_to(from, game.from1)) do_gen_move_to(from, game.to1, speed) } if (!rommel2) { if (!game.to2 && game.from2 === from) for (let to of all_hexes) if (to != from) do_gen_move_to(from, to, speed) if (game.to2 && is_hex_or_adjacent_to(from, game.from2)) do_gen_move_to(from, game.to2, speed) } } function gen_withdraw() { let rommel1 = (game.rommel === 1) ? 1 : 0 let speed = unit_speed[game.selected] let from = unit_hex(game.selected) // view.path = {} // Group Move Withdraw if (!game.to1) { for (let to of all_hexes) { if (to != from) { if (can_move_to(to, speed + rommel1) && set_has(game.withdraw.to, to)) { gen_action_hex(to) } else if (can_move_to(to, speed + 1 + rommel1) && set_has(game.withdraw.to, to)) { gen_action_forced_march(to) } } } } // Regroup Move Withdraw if (game.to1) { 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) } } } function apply_move(to, is_retreat) { 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_current_move(who, is_retreat) if (rommel1) { if (!game.to1 && game.from1 === from) if (can_move_to(to, speed + 2)) return move_unit(who, to, speed + 2, 1) if (game.to1 === to && is_hex_or_adjacent_to(from, game.from1)) if (can_move_to(to, speed + 2)) return move_unit(who, to, speed + 2, 1) } if (rommel2) { if (!game.to2 && game.from2 === from) if (can_move_to(to, speed + 2)) return move_unit(who, to, speed + 2, 2) if (game.to2 === to && is_hex_or_adjacent_to(from, game.from2)) if (can_move_to(to, speed + 2)) return move_unit(who, to, speed + 2, 2) } if (!rommel1) { if (!game.to1 && game.from1 === from) if (can_move_to(to, speed + 1)) return move_unit(who, to, speed + 1, 1) if (game.to1 === to && is_hex_or_adjacent_to(from, game.from1)) if (can_move_to(to, speed + 1)) return move_unit(who, to, speed + 1, 1) } if (!rommel2) { if (!game.to2 && game.from2 === from) if (can_move_to(to, speed + 1)) return move_unit(who, to, speed + 1, 2) if (game.to2 === to && is_hex_or_adjacent_to(from, game.from2)) if (can_move_to(to, speed + 1)) return move_unit(who, to, speed + 1, 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, side) => { let forced = false let unforced = false if (game.turn_option === 'pass') if (!own_supply_line[side]) return 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 is_engagement_move(to) { if (game.retreat > 0) { // retreating units may co-exist with disrupted enemy units return has_undisrupted_enemy_unit(to) } return has_enemy_unit(to) } function move_unit(who, to, speed, move) { let from = unit_hex(who) game.used |= move if (is_forced_march_move(from, to, speed) || is_engagement_move(to)) { if (move_via(who, to, speed, move)) { if (game.hexside.forced[0]) forced_march_via(who, game.hexside.via[0], to, move) else engage_via(who, game.hexside.via[0], to, move) game.hexside = null } else { game.state = 'move_via' } } else { push_move_summary(from, to, 0, 0) visit_path(from, to, speed) set_unit_moved(who) set_unit_hex(who, to) } } function resume_move() { if (game.retreat > 0) game.state = 'retreat_move' else game.state = 'move' } states.move_via = { inactive: "movement", prompt() { view.prompt = `Movement: Select which path to take.` view.selected = game.hexside.who view.selected_hexes = game.hexside.to // view.path = {} let rommel = (game.hexside.move === game.rommel) ? 1 : 0 let who = game.hexside.who let from = unit_hex(who) let speed = unit_speed[who] search_move(from, speed + rommel) for (let i = 0; i < game.hexside.via.length; ++i) { let x = game.hexside.via[i] if (game.hexside.forced[i]) gen_action_forced_march(x) else gen_action_hex(x) } }, forced_march(via) { let rommel = (game.hexside.move === game.rommel) ? 1 : 0 let who = game.hexside.who let from = unit_hex(who) let speed = unit_speed[who] search_move(from, speed + 1 + rommel) forced_march_via(game.hexside.who, via, game.hexside.to, game.hexside.move) game.hexside = null resume_move() }, hex(via) { let rommel = (game.hexside.move === game.rommel) ? 1 : 0 let who = game.hexside.who let from = unit_hex(who) let speed = unit_speed[who] search_move(from, speed + rommel) engage_via(game.hexside.who, via, game.hexside.to, game.hexside.move) game.hexside = null resume_move() } } function forced_march_via(who, via, to, move) { let speed = unit_speed[who] + (game.rommel === move ? 1 : 0) let from = unit_hex(who) set_unit_moved(who) set_unit_hex(who, via) visit_path(from, via, speed) // remember where we should advance to / return to if ((move === 1 && game.to1) || (move === 2 && game.to2)) game.forced.push([who, to, from]) else game.forced.push([who, to]) // attempted force marches affect hexside limits if (has_enemy_unit(to)) { let side = to_side_id(via, to) if (game.side_limit[side]) game.side_limit[side] = 2 else game.side_limit[side] = 1 } push_move_summary(from, to, via, 1) } function engage_via(who, via, to, move) { let speed = unit_speed[who] + (game.rommel === move ? 1 : 0) let from = unit_hex(who) set_unit_moved(who) set_unit_hex(who, to) visit_path(from, via, speed) visit_hex(to) if (from !== via) push_move_summary(from, to, via, 0) else push_move_summary(from, to, 0, 0) engage_via_hexside(who, via, to) } function engage_via_hexside(who, via, to) { let side = to_side_id(via, to) if (game.side_limit[side]) game.side_limit[side] = 2 else game.side_limit[side] = 1 if (is_unit_supplied(who)) { claim_hexside_control(side) } else { // new battle or enemy hexside (is_friendly_hexside is false before claiming control) if (!is_friendly_hexside(side)) game.raiders.push([who, side]) } if (is_new_battle_hex(to)) { claim_hex_control_for_defender(to) set_add(game.new_battles, to) } } // === FORCED MARCHES === function goto_forced_marches() { if (game.forced.length > 0) { log(`Forced Marches`) game.state = 'forced_marches' } else { end_forced_marches() } } function resume_forced_marches() { if (game.forced.length > 0) { game.state = 'forced_marches' } else { log_br() end_forced_marches() } } states.forced_marches = { inactive: "forced marches", prompt() { view.prompt = `Forced Marches!` for (let [who] 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(`>${die_face_hit[roll]} to #${to}`) visit_hex(to) set_unit_hex(who, to) if (has_enemy_unit(to)) engage_via_hexside(who, via, to) } else { log(`>${die_face_miss[roll]} disrupted at #${from}`) set_unit_hex(who, from) if (is_unit_disrupted(who)) { reduce_unit(who) // was a retreating unit if (is_unit_eliminated(who)) log(`Eliminated at #${from}.`) } else { set_unit_disrupted(who) } } array_remove(game.forced, ix) resume_forced_marches() } } function end_forced_marches() { game.side_limit = {} game.forced = null goto_forced_marches_rout() } function goto_forced_marches_rout() { let rout = false for (let x of all_hexes) { if (is_friendly_rout_hex(x)) rout = true if (is_enemy_rout_hex(x)) rout = true } if (rout) game.state = 'forced_marches_rout' else goto_refuse_battle() } states.forced_marches_rout = { inactive: "forced marches", prompt() { view.prompt = `Forced Marches: Rout!` for (let x of all_hexes) { if (is_friendly_rout_hex(x)) gen_action_hex(x) if (is_enemy_rout_hex(x)) gen_action_hex(x) } }, hex(where) { goto_rout(where, is_enemy_rout_hex(where), goto_forced_marches_rout) } } // === RETREAT === function is_valid_retreat_hex(from) { if (is_battle_hex(from)) { if (game.turn_option === 'pass') return can_all_retreat(from) else return can_any_retreat(from) } return false } 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_any_disengage(from) { let result = false for_each_undisrupted_and_unmoved_friendly_unit_in_hex(from, u => { if (result === false && can_unit_disengage_and_move(u)) result = true }) return result } function can_any_retreat(from) { let result = false for_each_undisrupted_and_unmoved_friendly_unit_in_hex(from, u => { if (result === false && can_unit_retreat(u)) result = true }) return result } function can_all_retreat(from) { if (!has_undisrupted_friendly_unit(from)) return false let result = true for_each_undisrupted_friendly_unit_in_hex(from, u => { if (result === true && !is_unit_moved(u) && !can_unit_retreat(u)) result = false }) return result } function can_unit_retreat_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 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 && !has_undisrupted_enemy_unit(game.to1)) { let result = false for_each_hex_and_adjacent_hex(game.from1, x => { if (result === false && x !== game.to1 && is_valid_retreat_hex(x)) result = true }) if (result) return true } if (game.to2 && !has_undisrupted_enemy_unit(game.to2)) { let result = false for_each_hex_and_adjacent_hex(game.from2, x => { if (result === false && x !== game.to2 && is_valid_retreat_hex(x)) result = true }) if (result) return true } return false } states.retreat_from = { inactive: "retreat", 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) }) } if (can_end_move()) gen_action('end_move') }, hex(x) { push_undo() goto_retreat_who(x) }, end_move() { log_br() end_movement() } } function goto_retreat_who(from) { game.retreat = from if (game.turn_option === 'pass') { game.state = 'retreat_full' game.retreat_units = [] for_each_undisrupted_and_unmoved_friendly_unit_in_hex(game.retreat, u => { if (can_unit_retreat(u)) game.retreat_units.push(u) }) } else { game.state = 'retreat_who' game.retreat_units = [] } } states.retreat_who = { inactive: "retreat", 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) 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() { apply_retreat() }, } states.retreat_full = { inactive: "retreat", prompt() { view.prompt = `Retreat: All units must retreat.` view.selected = game.retreat_units gen_action('retreat') }, retreat() { apply_retreat() }, } function apply_retreat() { let full_retreat = true for (let u of game.retreat_units) hide_unit(u) for_each_undisrupted_friendly_unit_in_hex(game.retreat, u => { if (!set_has(game.retreat_units, u)) full_retreat = false }) if (full_retreat) { log_h4(`Full retreat from #${game.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 = { inactive: "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() { log_h3(`Probe Combat at #${game.retreat}`) 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) { log_h4(`Partial retreat from #${game.retreat}.`) goto_retreat_move() } else { log_h4(`Full retreat from #${game.retreat}`) 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 = { inactive: "retreat", prompt() { 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) { view.prompt = `Retreat: Done.` gen_action('end_retreat') } else { view.prompt = `Retreat: Select unit to withdraw.` } } else { view.prompt = `Retreat: Select destination.` 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, true) set_unit_disrupted(who) }, end_retreat() { end_retreat() } } function end_retreat() { if ((game.to1 || game.to2) && game.forced.length > 0) { // Don't release in case a forced march regroup teleports units back into battle. // Wait until goto_combat_phase to release these hexes. } else { if (!is_battle_hex(game.retreat)) release_hex_control(game.retreat) } game.retreat_units = null // mark shielding units as moved for_each_undisrupted_friendly_unit_in_hex(game.retreat, u => { set_unit_moved(u) }) // 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() { game.retreat = 0 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.new_battles) if (can_all_undisrupted_units_disengage_and_withdraw(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() } } states.refuse_battle = { inactive: "refuse battle", prompt() { view.selected_hexes = game.refuse for (let x of game.new_battles) if (can_all_undisrupted_units_disengage_and_withdraw(x)) gen_action_hex(x) if (game.refuse > 0) { view.prompt = `Refuse Battle: You may retreat from ${hex_name[game.refuse]}.` gen_action('retreat') gen_action('undo') } else { view.prompt = `Refuse Battle: Select an attacked hex to refuse battle.` gen_action('pass') } }, undo() { game.refuse = 0 }, hex(x) { if (x === game.refuse) game.refuse = 0 else game.refuse = x }, retreat() { log_h4(`Refused battle at #${game.refuse}`) set_delete(game.new_battles, game.refuse) goto_pursuit_fire_during_refuse_battle(game.refuse) }, pass() { goto_combat_phase() } } function goto_refuse_battle_move() { set_passive_player() if (has_undisrupted_friendly_unit(game.refuse)) { game.state = 'refuse_battle_move' game.summary = {} } else { end_refuse_battle_move() } } states.refuse_battle_move = { inactive: "refuse battle", prompt() { 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) { view.prompt = `Refuse Battle: Done.` gen_action('end_retreat') } else { view.prompt = `Refuse Battle: Select unit to withdraw.` } } else { view.prompt = `Refuse Battle: Select destination.` 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() game.summary[to] = (game.summary[to]|0) + 1 set_unit_hex(who, to) set_unit_disrupted(who) }, end_retreat() { flush_withdraw_summary() 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) after = after.name game.rout = { state: game.state, active: game.active, after: after, from: from, attrition: [], } if (enemy) set_enemy_player() log_h4(`Routed at #${from}`) // RULES: Will be disrupted again, so won't be able to recover. for_each_friendly_unit_in_hex(from, u => { set_delete(game.recover, u) }) if (can_all_units_disengage_and_withdraw(from)) { game.state = 'rout_attrition' } else { game.state = 'rout_elimination' game.summary = 0 } } states.rout_elimination = { inactive: "rout", prompt() { view.prompt = "Rout: Eliminate all units that can not disengage." for_each_friendly_unit_in_hex(game.rout.from, u => { gen_action_unit(u) }) }, unit(who) { game.summary++ eliminate_unit(who) if (!has_friendly_unit(game.rout.from)) { log(`Eliminated ${game.summary} at #${game.rout.from}.`) game.summary = null end_rout() } }, } states.rout_attrition = { inactive: "rout", 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) { let d = 0, e = 0 for (let u of game.rout.attrition) if (is_unit_eliminated(u)) ++e else ++d if (d > 0) log(`Reduced ${e} at #${game.rout.from}.`) if (e > 0) log(`Eliminated ${e} at #${game.rout.from}.`) game.rout.attrition = null if (has_friendly_unit(game.rout.from)) goto_rout_fire(game.rout.from) else end_rout() } }, } function goto_rout_move() { if (has_friendly_unit(game.rout.from)) { hide_friendly_units_in_hex(game.rout.from) // TODO: auto-eliminate if no withdraw path available game.state = 'rout_move' game.summary = {} } else { end_rout() } } states.rout_move = { inactive: "rout", prompt() { 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) { view.prompt = `Rout: Done.` gen_action('end_rout') } else { view.prompt = `Rout: Select unit to withdraw.` } } else { view.prompt = `Rout: Select destination.` 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 } } // TODO: should already have eliminated? if (eliminate) gen_action('eliminate') } }, unit(who) { apply_select(who) }, eliminate() { let who = pop_selected() push_undo() game.summary[0] = (game.summary[0]|0) + 1 eliminate_unit(who) }, hex(to) { let who = pop_selected() push_undo() game.summary[to] = (game.summary[to]|0) + 1 set_unit_hex(who, to) set_unit_disrupted(who) }, end_rout() { flush_withdraw_summary() end_rout() } } function end_rout() { log_br() game.state = game.rout.state release_hex_control(game.rout.from) set_delete(game.new_battles, game.rout.from) if (game.active !== game.rout.active) set_enemy_player() let after = game.rout.after game.rout = null if (after) after_rout_table[after]() } // ==== COMBAT PHASE === function has_mandatory_withdrawals() { return ( (is_mandatory_combat(BARDIA) && is_valid_withdrawal_group_move_from(BARDIA)) || (is_mandatory_combat(BENGHAZI) && is_valid_withdrawal_group_move_from(BENGHAZI)) || (is_mandatory_combat(TOBRUK) && is_valid_withdrawal_group_move_from(TOBRUK)) ) } function is_mandatory_combat(fortress) { if (is_battle_hex(fortress)) { if (game.phasing === AXIS) return is_fortress_allied_controlled(fortress) else return is_fortress_axis_controlled(fortress) } return false } function release_retreat_hex_control() { for (let x of all_hexes) if (!is_battle_hex(x)) if (set_has(game.axis_hexes, x) || set_has(game.allied_hexes, x)) release_hex_control(x) } function goto_combat_phase() { set_active_player() // Release hexes that were retreated from here release_retreat_hex_control() reveal_visited_minefields() if (game.turn_option === 'pass') { if (is_mandatory_combat(BARDIA)) return goto_rout(BARDIA, false, goto_combat_phase) if (is_mandatory_combat(BENGHAZI)) return goto_rout(BENGHAZI, false, goto_combat_phase) if (is_mandatory_combat(TOBRUK)) return goto_rout(TOBRUK, false, goto_combat_phase) return end_combat_phase() } // exception - not mandatory during blitz combat if (game.turn_option !== 'second blitz') { if (is_mandatory_combat(BARDIA)) set_add(game.new_battles, BARDIA) if (is_mandatory_combat(BENGHAZI)) set_add(game.new_battles, BENGHAZI) if (is_mandatory_combat(TOBRUK)) set_add(game.new_battles, TOBRUK) } for (let x of game.new_battles) set_add(game.active_battles, x) let n = count_and_reveal_battle_hexes() if (n > 0) { if (n > game.active_battles.length) return game.state = 'select_active_battles' if (game.turn_option === 'assault') return game.state = 'select_assault_battles' return goto_select_battle() } end_combat_phase() } states.select_active_battles = { inactive: "combat phase", prompt() { view.prompt = `Combat: Select active battles.` view.selected_hexes = game.active_battles for (let x of all_hexes) { if (!set_has(game.active_battles, x) && is_battle_hex(x)) gen_action_hex(x) if (set_has(game.active_battles, x) && !set_has(game.new_battles, x)) gen_action_hex(x) } if (game.active_battles.length === 0) gen_action('pass') else gen_action('next') if (game.active_battles.length > game.new_battles.length) gen_action('undo') }, undo() { game.active_battles = game.new_battles.slice() }, hex(x) { set_toggle(game.active_battles, x) }, pass() { end_combat_phase() }, next() { if (game.active_battles.length > 0) { if (game.turn_option === 'assault') game.state = 'select_assault_battles' else goto_select_battle() } else { end_combat_phase() } } } states.select_assault_battles = { inactive: "combat phase", prompt() { view.prompt = `Combat: Select assault battles.` view.selected_hexes = game.assault_battles for (let x of game.active_battles) gen_action_hex(x) if (game.assault_battles.length === 0) gen_action('pass') else { gen_action('undo') gen_action('next') } }, undo() { set_clear(game.assault_battles) }, hex(x) { set_toggle(game.assault_battles, x) }, pass() { goto_select_battle() }, next() { goto_select_battle() } } function goto_select_battle() { if (game.active_battles.length > 0) { if (game.active_battles.length > game.assault_battles.length) { log_br() log("Active battles") for (let x of game.active_battles) if (!set_has(game.assault_battles, x)) log(`>#${x}`) } if (game.assault_battles.length > 0) { log_br() log("Assault battles") for (let x of game.assault_battles) log(`>#${x}`) } // TODO: auto-select here? if (game.active_battles.length === 1) goto_battle(game.active_battles[0]) else game.state = 'select_battle' } else { end_combat_phase() } } states.select_battle = { inactive: "combat phase", prompt() { view.prompt = `Combat: Select next battle.` 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`) game.used = 0 if (game.rommel) game.rommel = 3 game.turn_option = 'second blitz' goto_move_phase() } // === BATTLES === // Normal Battle: // passive fire // active hits // active fire // passive hits function is_unit_retreating(u) { if (game.retreat_units) return set_has(game.retreat_units, u) return false } function is_assault_battle() { return set_has(game.assault_battles, game.battle) } function is_fortress_defensive_fire() { if ((game.state === 'battle_fire' && is_passive_player()) || (game.state === 'probe_fire' && is_active_player())) { if (game.battle === BARDIA) return is_fortress_friendly_controlled(BARDIA) if (game.battle === BENGHAZI) return is_fortress_friendly_controlled(BENGHAZI) if (game.battle === TOBRUK) return is_fortress_friendly_controlled(TOBRUK) } 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[MF_VISIBLE], game.battle)) { // 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) let n = 1 // Double dice during assault if (is_assault_battle()) n *= 2 // Double dice for non-armor defenders in fortress! if (fc !== ARMOR && is_fortress_defensive_fire()) n *= 2 let fp = FIREPOWER_MATRIX[fc][tc] let result = [] let total = 0 for (let k = 0; k < n; ++k) { if (k > 0) result.push(" ") for (let i = 0; i < cv; ++i) { let roll = roll_die() if (roll >= fp) { result.push(die_face_hit[roll]) ++total } else { result.push(die_face_miss[roll]) } } } // Double defense in minefields! if (fc !== ARTILLERY && is_minefield_offensive_fire()) total = total / 2 game.flash = `${class_name_cap[fc]} ${firepower_name[fp]} ${result.join("")} at ${class_name[tc]}.` log(game.flash) return total } function goto_battle(x) { game.battle = x if (is_assault_battle()) log_h3(`Assault at #${x}`) else log_h3(`Battle at #${x}`) // goto defensive fire log_h4(`Defensive Fire`) set_passive_player() game.state = 'battle_fire' game.hits = [ 0, 0, 0, 0 ] } function end_battle() { if (!is_battle_hex(game.battle)) release_hex_control(game.battle) set_delete(game.new_battles, game.battle) set_delete(game.active_battles, game.battle) set_delete(game.assault_battles, game.battle) game.flash = "" game.battle = 0 game.hits = 0 set_active_player() if (game.active_battles.length > 0) game.state = 'select_battle' else end_combat_phase() } function apply_battle_fire(tc) { let who = pop_selected() set_unit_fired(who) game.hits[tc] += roll_battle_fire(who, tc) let hp = count_hp_in_battle() // clamp to available hit points game.hits[tc] = min(game.hits[tc], hp[tc]) } 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 game.flash = "Inflicted " + format_hits() + "." log(game.flash) if (game.hits[0] + game.hits[1] + game.hits[2] + game.hits[3] > 0) { if (game.state === 'battle_fire') game.state = 'battle_hits' else game.state = 'probe_hits' } else { if (game.state === 'battle_fire') end_battle_hits() else end_probe_hits() } } function gen_battle_fire() { let arty = false let done = true let hp = count_hp_in_battle() 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_artillery_unit(u) && !is_unit_retreating(u)) { if (!is_unit_fired(u)) { gen_action_unit(u) arty = true done = false } } }) 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) done = false } }) } } if (done) gen_action('end_fire') } function gen_battle_target() { let hp = count_hp_in_battle() for (let i = 0; i < 4; ++i) hp[i] -= game.hits[i] let who = game.selected let fc = unit_class[who] gen_action_unit(who) // deselect // armor must target armor if possible if (fc === ARMOR && hp[ARMOR] > 0) { gen_action('armor') return } // infantry must target infantry if possible if (fc === INFANTRY && hp[INFANTRY] > 0) { gen_action('infantry') return } if (hp[ARMOR] > 0) gen_action('armor') if (hp[INFANTRY] > 0) gen_action('infantry') if (hp[ANTITANK] > 0) gen_action('antitank') // only artillery may target artillery if other units are alive if (hp[ARTILLERY] > 0) { if (fc === ARTILLERY || (hp[ARMOR] <= 0 && hp[INFANTRY] <= 0 && hp[ANTITANK] <= 0)) gen_action('artillery') } } function apply_auto_target(who) { let hp = count_hp_in_battle() for (let i = 0; i < 4; ++i) hp[i] -= game.hits[i] let fc = unit_class[who] // armor must target armor if possible if (fc === ARMOR && hp[ARMOR] > 0) return apply_battle_fire(ARMOR) // infantry must target infantry if possible if (fc === INFANTRY && hp[INFANTRY] > 0) return apply_battle_fire(INFANTRY) // only one class remains if (fc === ARTILLERY) { // Artillery may always target anything if (hp[ARMOR] > 0 && hp[INFANTRY] === 0 && hp[ANTITANK] === 0 && hp[ARTILLERY] === 0) return apply_battle_fire(ARMOR) if (hp[ARMOR] === 0 && hp[INFANTRY] > 0 && hp[ANTITANK] === 0 && hp[ARTILLERY] === 0) return apply_battle_fire(INFANTRY) if (hp[ARMOR] === 0 && hp[INFANTRY] === 0 && hp[ANTITANK] > 0 && hp[ARTILLERY] === 0) return apply_battle_fire(ANTITANK) if (hp[ARMOR] === 0 && hp[INFANTRY] === 0 && hp[ANTITANK] === 0 && hp[ARTILLERY] > 0) return apply_battle_fire(ARTILLERY) } else { // Non-artillery may only target artillery if no other targets remain if (hp[ARMOR] > 0 && hp[INFANTRY] === 0 && hp[ANTITANK] === 0) return apply_battle_fire(ARMOR) if (hp[ARMOR] === 0 && hp[INFANTRY] > 0 && hp[ANTITANK] === 0) return apply_battle_fire(INFANTRY) if (hp[ARMOR] === 0 && hp[INFANTRY] === 0 && hp[ANTITANK] > 0) return apply_battle_fire(ANTITANK) if (hp[ARMOR] === 0 && hp[INFANTRY] === 0 && hp[ANTITANK] === 0 && hp[ARTILLERY] > 0) return apply_battle_fire(ARTILLERY) } } function gen_battle_hits() { let normal_steps = count_normal_steps_in_battle() let elite_steps = count_elite_steps_in_battle() let done = true for_each_undisrupted_friendly_unit_in_hex(game.battle, u => { if (!is_unit_retreating(u)) { let c = unit_class[u] if (is_elite_unit(u)) { if (game.hits[c] >= 2) { gen_action_unit_hit(u) done = false } } else { if (game.hits[c] >= 1) { // If mixed elite and non-elite: must assign ALL damage. if (elite_steps[c] > 0 && normal_steps[c] === 1 && (game.hits[c] & 1) === 0) { // Eliminating the last non-elite must not leave an odd // number of hits remaining. } else { gen_action_unit_hit(u) done = false } } } } }) if (done) gen_action('end_hits') return done } function apply_battle_hit(who) { if (unit_steps(who) === 1) { log(`Eliminated ${class_name[unit_class[who]]}.`) game.flash = `Eliminated ${class_name[unit_class[who]]} \u2014 ` } else { game.flash = `Reduced ${class_name[unit_class[who]]} \u2014 ` } game.hits[unit_class[who]] -= reduce_unit(who) game.flash += format_hits() + " left." } states.battle_fire = { inactive: "battle fire", show_battle: true, prompt() { if (game.active === game.phasing) view.prompt = `Battle: Offensive Fire!` else view.prompt = `Battle: Defensive Fire!` if (game.selected < 0) gen_battle_fire() else gen_battle_target() }, unit(who) { apply_select(who) if (game.selected >= 0) apply_auto_target(who) }, armor() { apply_battle_fire(ARMOR) }, infantry() { apply_battle_fire(INFANTRY) }, antitank() { apply_battle_fire(ANTITANK) }, artillery() { apply_battle_fire(ARTILLERY) }, end_fire() { goto_hits() }, } states.battle_hits = { inactive: "battle hits", show_battle: true, prompt() { if (game.active === game.phasing) view.prompt = `Battle: Allocate ${format_hits()} from Defensive Fire.` else view.prompt = `Battle: Allocate ${format_hits()} from Offensive Fire.` gen_battle_hits() }, unit_hit(who) { push_undo() apply_battle_hit(who) }, end_hits() { end_battle_hits() }, } function end_battle_hits() { if (is_friendly_rout_hex(game.battle)) { goto_rout(game.battle, false, end_battle) } else if (game.active === game.phasing && has_friendly_units_in_battle()) { // goto offensive fire log_h4(`Offensive Fire`) game.state = 'battle_fire' game.hits = [ 0, 0, 0, 0 ] } else { end_battle() } } states.probe_fire = { inactive: "probe combat fire", show_battle: true, 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) if (game.selected >= 0) apply_auto_target(who) }, armor() { apply_battle_fire(ARMOR) }, infantry() { apply_battle_fire(INFANTRY) }, antitank() { apply_battle_fire(ANTITANK) }, artillery() { apply_battle_fire(ARTILLERY) }, end_fire() { goto_hits() }, } states.probe_hits = { inactive: "probe combat hits", show_battle: true, prompt() { if (game.active !== game.phasing) view.prompt = `Probe: Allocate ${format_hits()} from Defensive Fire.` else view.prompt = `Probe: Allocate ${format_hits()} from Offensive Fire.` gen_battle_hits() }, unit_hit(who) { push_undo() apply_battle_hit(who) }, end_hits() { end_probe_hits() }, } function end_probe_hits() { // TODO: rout during probe combat? if (game.active !== game.phasing && has_friendly_units_in_battle()) { // goto offensive fire game.state = 'probe_fire' game.hits = [ 0, 0, 0, 0 ] } else { end_probe() } } // === PURSUIT FIRE === // Refuse battle // active pursuit fire // passive apply hits // passive moves // Retreat // passive pursuit fire // active apply hits // active moves // Rout // non-routing pursuit fire // routing apply hits // routing moves function slowest_enemy_unit_speed(where) { let r = 5 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 = 5 for_each_undisrupted_enemy_unit_in_hex(where, u => { let s = unit_speed[u] if (s < r) r = s }) return r } function slowest_undisrupted_friendly_unit(where) { let who = -1 let r = 5 for_each_undisrupted_friendly_unit_in_hex(where, u => { let s = unit_speed[u] if (s < r) { who = u r = s } }) return who } function fastest_undisrupted_friendly_unit(where) { let who = -1 let r = 0 for_each_undisrupted_friendly_unit_in_hex(where, u => { let s = unit_speed[u] if (s > r) { who = u r = s } }) return who } function fastest_undisrupted_friendly_unit_speed(where) { let r = 0 for_each_undisrupted_friendly_unit_in_hex(where, u => { let s = unit_speed[u] if (s > r) r = s }) return r } function has_hidden_friendly_unit(x) { for (let u = first_friendly_unit; u <= last_friendly_unit; ++u) if (unit_hex(u) === x && !set_has(game.revealed, u)) return true return false } function goto_rout_fire(where) { set_enemy_player() game.hits = 0 game.pursuit = where game.flash = "" let slowest = slowest_enemy_unit_speed(game.pursuit) let fastest = fastest_undisrupted_friendly_unit_speed(game.pursuit) log(`Slowest was ${speed_name[slowest]} unit.`) if (slowest <= fastest || has_hidden_friendly_unit(game.pursuit)) { game.state = 'rout_fire' } else { // too fast for pursuit fire, and we already know all the units involved set_enemy_player() end_rout_fire() } } function goto_pursuit_fire_during_retreat(where) { set_passive_player() game.hits = 0 game.pursuit = where game.flash = "" let slowest = slowest_undisrupted_enemy_unit_speed(game.pursuit) let fastest = fastest_undisrupted_friendly_unit_speed(game.pursuit) log(`Slowest was ${speed_name[slowest]} unit.`) if (slowest <= fastest) { game.state = 'pursuit_fire' } else { // too fast for pursuit fire, and we already know all the units involved end_pursuit_fire() } } function goto_pursuit_fire_during_refuse_battle(where) { set_active_player() game.hits = 0 game.pursuit = where let slowest = slowest_undisrupted_enemy_unit_speed(game.pursuit) log(`Slowest was ${speed_name[slowest]} unit.`) game.state = 'pursuit_fire' } function format_hits() { if (typeof game.hits === 'number') { if (game.hits === 0) return "zero hits" if (game.hits === 1) return "1 hit" return game.hits + " hits" } let n = game.hits[0] + game.hits[1] + game.hits[2] + game.hits[3] if (n === 0) return `zero hits` let s = [] if (game.hits[ARMOR] > 0) s.push(game.hits[ARMOR] + " armor") if (game.hits[INFANTRY] > 0) s.push(game.hits[INFANTRY] + " infantry") if (game.hits[ANTITANK] > 0) s.push(game.hits[ANTITANK] + " anti-tank") if (game.hits[ARTILLERY] > 0) s.push(game.hits[ARTILLERY] + " artillery") if (n === 1) return s.join(", ") + " hit" return s.join(", ") + " hits" } function goto_rout_hits() { set_enemy_player() game.flash = "" if (game.hits > 0) game.state = 'rout_hits' else end_rout_fire() } function goto_pursuit_hits() { set_enemy_player() game.flash = "" if (game.hits > 0) game.state = 'pursuit_hits' else end_pursuit_fire() } function roll_pursuit_fire_imp(who, n, hp) { let speed = unit_speed[who] if (n === 2) { let astr, bstr let a = roll_die() let b = roll_die() if (a >= 4) { game.hits++ astr = die_face_hit[a] } else { astr = die_face_miss[a] } if (b >= 4) { game.hits++ bstr = die_face_hit[b] } else { bstr = die_face_miss[b] } game.flash = `${speed_name_cap[speed]} fired ${astr}${bstr}.` } if (n === 1) { let astr let a = roll_die() if (a >= 4) { game.hits++ astr = die_face_hit[a] } else { astr = die_face_miss[a] } game.flash = `${speed_name_cap[speed]} fired ${astr}.` } log(game.flash) if (game.hits > hp) game.hits = hp return game.hits === hp } function roll_pursuit_fire(who, n) { return roll_pursuit_fire_imp(who, n, count_hp_in_pursuit()) } function roll_rout_fire(who, n) { return roll_pursuit_fire_imp(who, n, count_hp_in_rout()) } states.pursuit_fire = { inactive: "pursuit fire", show_pursuit: true, prompt() { view.prompt = `Pursuit Fire: Fire or withhold.` if (game.hits < count_hp_in_pursuit()) { let slowest = slowest_undisrupted_enemy_unit_speed(game.pursuit) for_each_undisrupted_friendly_unit_in_hex(game.pursuit, u => { if (unit_speed[u] >= slowest && !is_unit_fired(u)) { gen_action_unit(u) } }) } gen_action('end_fire') }, unit(who) { let slowest = slowest_undisrupted_enemy_unit_speed(game.pursuit) roll_pursuit_fire(who, (unit_speed[who] > slowest ? 2 : 1)) set_unit_fired(who) }, end_fire() { goto_pursuit_hits() }, } states.rout_fire = { inactive: "rout fire", show_pursuit: true, prompt() { view.prompt = `Pursuit Fire: Fire or withhold.` if (game.hits < count_hp_in_rout()) { let slowest = slowest_enemy_unit_speed(game.pursuit) for_each_undisrupted_friendly_unit_in_hex(game.pursuit, u => { if (unit_speed[u] >= slowest && !is_unit_fired(u)) { gen_action_unit(u) } }) } gen_action('end_fire') }, unit(who) { let slowest = slowest_enemy_unit_speed(game.pursuit) roll_rout_fire(who, (unit_speed[who] > slowest ? 2 : 1)) set_unit_fired(who) }, end_fire() { goto_rout_hits() }, } function gen_pursuit_hits(normal_steps, elite_steps, iterate) { let done = true iterate(game.pursuit, u => { if (is_elite_unit(u)) { if (game.hits >= 2) { gen_action_unit_hit(u) done = false } } else { if (game.hits >= 1) { // If mixed elite and non-elite: must assign ALL damage. if (elite_steps > 0 && normal_steps === 1 && (game.hits & 1) === 0) { // Eliminating the last non-elite must not leave an odd // number of hits remaining. } else { gen_action_unit_hit(u) done = false } } } }) if (done) gen_action('end_hits') } states.pursuit_hits = { inactive: "pursuit hits", show_pursuit: true, prompt() { view.prompt = "Pursuit Fire: Allocate " + format_hits() + "." let normal_steps = count_normal_steps_in_pursuit() let elite_steps = count_elite_steps_in_pursuit() gen_pursuit_hits(normal_steps, elite_steps, for_each_undisrupted_friendly_unit_in_hex) }, unit_hit(who) { push_undo() // TODO: flash? let where = unit_hex(who) game.hits -= reduce_unit(who) if (is_unit_eliminated(who)) log(`Eliminated at #${where}.`) }, end_hits() { end_pursuit_fire() }, } states.rout_hits = { inactive: "rout hits", show_pursuit: true, prompt() { view.prompt = "Pursuit Fire: Allocate " + format_hits() + "." let normal_steps = count_normal_steps_in_rout() let elite_steps = count_elite_steps_in_rout() gen_pursuit_hits(normal_steps, elite_steps, for_each_friendly_unit_in_hex) }, unit_hit(who) { push_undo() // TODO: flash? let where = unit_hex(who) game.hits -= reduce_unit(who) if (is_unit_eliminated(who)) log(`Eliminated at #${where}.`) }, end_hits() { end_rout_fire() }, } function end_pursuit_fire() { game.flash = "" game.pursuit = 0 if (game.retreat) { goto_retreat_move() } else { goto_refuse_battle_move() } } function end_rout_fire() { game.flash = "" game.pursuit = 0 goto_rout_move() } // === BUILDUP - SUPPLY CHECK === function end_month() { set_clear(game.fired) set_clear(game.moved) // Forget captured fortresses (for bonus cards) clear_fortresses_captured() if (game.month === current_scenario().end) return goto_end_game() goto_buildup() } function goto_buildup() { ++game.month log_h1(current_month_name()) game.phasing = AXIS set_active_player() goto_buildup_supply_check() } function goto_buildup_supply_check() { let base_net = friendly_supply_network() for_each_friendly_unit_on_map(u => { if (base_net[unit_hex(u)]) set_unit_supply(u, SS_BASE) else set_unit_supply(u, SS_NONE) }) if (is_axis_player()) game.capacity = [ 1, 1, 2 ] else game.capacity = [ 2, 2, 5 ] game.oasis = [ 1, 1, 1 ] goto_fortress_supply('buildup_fortress_supply') } function goto_buildup_supply_check_recover() { let summary = [] for_each_friendly_unit_on_map(u => { if (is_unit_supplied(u) && is_unit_disrupted(u) && !is_battle_hex(unit_hex(u))) { set_add(summary, unit_hex(u)) clear_unit_disrupted(u) } }) for (let x of summary) log(`Recovered at #${x}.`) resume_buildup_eliminate_unsupplied() } function resume_buildup_eliminate_unsupplied() { game.state = 'buildup_eliminate_unsupplied' let done = true for_each_friendly_unit_on_map(u => { if (is_unit_unsupplied(u)) done = false }) if (done) { if (is_axis_player()) { set_enemy_player() goto_buildup_supply_check() } else { goto_buildup_point_determination() } } } states.buildup_eliminate_unsupplied = { inactive: "buildup", prompt() { view.prompt = `Buildup: Eliminate unsupplied units.` for_each_friendly_unit_on_map(u => { if (is_unit_unsupplied(u)) gen_action_unit(u) }) }, unit(u) { log(`Eliminated at #${unit_hex(u)}.`) eliminate_unit(u) resume_buildup_eliminate_unsupplied() }, } // === BUILDUP - POINT DETERMINATION === function is_pregame_buildup() { return game.scenario === "Gazala" && game.month === 14 } function goto_pregame_buildup() { game.phasing = ALLIED init_buildup() game.axis_bps = 30 game.allied_bps = 30 set_active_player() goto_buildup_discard() } function init_buildup() { game.buildup = { // redeployment network axis_network: union_axis_network(), axis_line: union_axis_line(), allied_network: union_allied_network(), allied_line: union_allied_line(), // extra cards purchased axis_cards: 0, allied_cards: 0, // remaining port capacity for sea redeployment bardia: is_fortress_axis_controlled(BARDIA) ? 1 : 2, benghazi: is_fortress_axis_controlled(BENGHAZI) ? 1 : 2, tobruk: is_fortress_axis_controlled(TOBRUK) ? 2 : 5, // for undo tracking changed: 0 } } function goto_buildup_point_determination() { let axis, allied // take union of supply networks? init_buildup() log_br() if (game.scenario === "1940") { axis = roll_die() allied = roll_die() log(`Axis rolled ${axis}.`) log(`Allied rolled ${allied}.`) } else { let axis_a = roll_die() let axis_b = roll_die() let allied_a = roll_die() let allied_b = roll_die() axis = axis_a + axis_b allied = allied_a + allied_b log(`Axis rolled ${axis_a} + ${axis_b}.`) log(`Allied rolled ${allied_a} + ${allied_b}.`) } log(`Received ${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_discard() } // === BUILDUP - DISCARD === function goto_buildup_discard() { log_h2(game.active + " Buildup") let hand = player_hand() if (hand[REAL] + hand[DUMMY] === 0) { goto_buildup_reinforcements() } else { game.state = 'buildup_discard' game.summary = 0 } } states.buildup_discard = { inactive: "buildup", 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() let hand = player_hand() hand[DUMMY]-- game.summary++ }, next() { log(`${game.active} discarded ${game.summary} dummy supply.`) goto_buildup_reinforcements() }, } // === BUILDUP - REINFORCEMENTS === function have_scheduled_reinforcements() { let refit = friendly_refit() for (let u = first_friendly_unit; u <= last_friendly_unit; ++u) { let x = unit_hex(u) if (x === refit || x === hexdeploy + game.month || x === hexdeploy + game.month + 1) return true } } function goto_buildup_reinforcements() { if (have_scheduled_reinforcements()) game.state = 'buildup_reinforcements' else goto_buildup_spending() } states.buildup_reinforcements = { inactive: "buildup", prompt() { view.prompt = `Buildup: Bring on reinforcements.` view.selected = [] for_each_friendly_unit_in_month(game.month, u => { set_add(view.selected, u) }) if (game.month < current_scenario().end) { for_each_friendly_unit_in_month(game.month+1, u => { set_add(view.selected, u) }) } gen_action_hex(friendly_base()) }, hex(_base) { apply_reinforcements() goto_buildup_spending() }, } function apply_reinforcements() { let base = friendly_base() let refitted = 0 let scheduled = 0 let early = 0 if (is_battle_hex(base)) base = friendly_queue() for_each_friendly_unit_in_hex(friendly_refit(), u => { set_unit_hex(u, base) refitted++ }) for_each_friendly_unit_in_month(game.month, u => { set_unit_hex(u, base) scheduled++ }) if (game.month < current_scenario().end) { for_each_friendly_unit_in_month(game.month + 1, u => { if (roll_die() <= 2) { set_unit_hex(u, base) early++ } }) } log(`Reinforcements at #${base}`) if (refitted > 0) log(`>${refitted} refitted`) log(`>${scheduled} on schedule`) log(`>${early} early`) } // === BUILDUP - SPENDING BPS === function goto_buildup_spending() { game.state = 'spending_bps' game.summary = { refit: 0, redeployed: {}, replaced_steps: 0, replaced_bps: 0, dismantled: 0, built: 0, cards: 0, } } function print_buildup_summary() { let keys = Object.keys(game.summary.redeployed).map(Number).sort((a,b)=>a-b) if (keys.length > 0) { log("Redeployed") for (let mm of keys) { let n = game.summary.redeployed[mm] let from = (mm) & 255 let to = (mm >>> 8 ) & 255 let sea = (mm >>> 16) & 255 if (sea) log(`>${n} #${from} to #${to} *`) else log(`>${n} #${from} to #${to}`) } } if (game.summary.refit > 0) log(`Returned ${game.summary.refit} for Refit.`) if (game.summary.replaced_steps > 0) log(`Replaced ${game.summary.replaced_steps} steps for ${game.summary.replaced_bps} BPs.`) if (game.summary.dismantled > 0) log(`Dismantled ${game.summary.dismantled} minefields.`) if (game.summary.built === 1) log(`Built 1 minefield.`) else if (game.summary.built > 1) log(`Built ${game.summary.built} minefields.`) let cards = is_axis_player() ? game.buildup.axis_cards : game.buildup.allied_cards if (cards === 1) log(`Purchased 1 extra card.`) else if (cards > 1) log(`Purchased ${cards} extra cards.`) game.summary = null } function push_buildup_summary(from, to, sea) { let mm = (from) | (to << 8) | (sea << 16) game.summary.redeployed[mm] = (game.summary.redeployed[mm]|0) + 1 } function available_bps() { if (is_axis_player()) return game.axis_bps else return game.allied_bps } function pay_bps(n) { if (is_axis_player()) game.axis_bps -= n else game.allied_bps -= n } function count_friendly_minefields() { let net = friendly_buildup_network() let n = 0 for (let x of friendly_minefields()) if (net[x] && !is_battle_hex(x)) ++n for (let x of game.minefields[MF_VISIBLE]) if (net[x] && !is_battle_hex(x)) ++n return n } function replacement_cost(who) { let cost = (unit_class[who] === INFANTRY) ? (unit_speed[who] > 1) ? 2 : 1 : 3 if (is_elite_unit(who)) return 2 * cost return cost } function can_redeploy_from(from) { if (is_battle_hex(from)) { let n = 0 for_each_undisrupted_friendly_unit_in_hex(from, _u => { n++ }) return n > 1 } return true } function is_port_friendly(where) { if (where === BARDIA || where === BENGHAZI || where === TOBRUK) return is_fortress_friendly_controlled(where) return true } function sea_redeploy_cost(from, to) { if ((from === BARDIA || to === BARDIA) && game.buildup.bardia === 0) return 0 if ((from === BENGHAZI || to === BENGHAZI) && game.buildup.benghazi === 0) return 0 if ((from === TOBRUK || to === TOBRUK) && game.buildup.tobruk === 0) return 0 if (is_port_friendly(from) && is_port_friendly(to)) { let b_from = is_port_besieged(from) let b_to = is_port_besieged(to) if (b_from && b_to) return 0 if (b_from || b_to) { if (is_axis_player()) return 0 return 4 } return 1 } return 0 } function gen_sea_redeployment(from, to) { let cost = sea_redeploy_cost(from, to) if (cost && cost <= available_bps()) gen_action_hex(to) } function gen_spending_bps() { let who = game.selected let bps = available_bps() let base = friendly_base() let from = unit_hex(who) // Receive replacement in base if (from === base && unit_lost_steps(who) > 0) { if (bps >= replacement_cost(who)) view.actions.replacement = 1 else view.actions.replacement = 0 } if (can_redeploy_from(from)) { search_redeploy(from) // Return for Refit if (from !== base) { if (path_cost[0][base] < 63) { view.actions.refit = 1 gen_action_hex(friendly_refit()) } } // Redeployment if (bps > 0) { for (let x of all_hexes) if (x !== from && can_move_to(x, 2)) gen_action_hex(x) } // Sea Redeployment if (from === base) { gen_sea_redeployment(base, BARDIA) gen_sea_redeployment(base, BENGHAZI) gen_sea_redeployment(base, TOBRUK) } if (from === BARDIA) { gen_sea_redeployment(BARDIA, BENGHAZI) gen_sea_redeployment(BARDIA, TOBRUK) gen_sea_redeployment(BARDIA, base) } if (from === BENGHAZI) { gen_sea_redeployment(BENGHAZI, BARDIA) gen_sea_redeployment(BENGHAZI, TOBRUK) gen_sea_redeployment(BENGHAZI, base) } if (from === TOBRUK) { gen_sea_redeployment(TOBRUK, BARDIA) gen_sea_redeployment(TOBRUK, BENGHAZI) gen_sea_redeployment(TOBRUK, base) } } } states.spending_bps = { inactive: "buildup", prompt() { view.prompt = `Buildup: Spend buildup points (${available_bps()} remain).` let bps = available_bps() for_each_friendly_unit_on_map(u => { if (is_unit_undisrupted(u)) gen_action_unit(u) }) if (game.selected < 0) { if (game.month >= 11 && has_friendly_unit_in_raw_hex(MALTA)) gen_action_hex(MALTA) if (count_friendly_minefields() >= 1 || bps >= 15) gen_action('minefield') if (bps >= 10) gen_action('extra_supply_card') } else { gen_spending_bps() } gen_action('end_buildup') }, unit(who) { if (game.selected < 0) { push_undo() game.selected = who game.buildup.changed = 0 } else { if (who === game.selected) { if (unit_hex(who) === friendly_base() && unit_lost_steps(who) > 0 && available_bps() >= replacement_cost(who)) return this.replacement() if (!game.buildup.changed) pop_undo() else game.selected = -1 } else { game.selected = -1 if (game.buildup.changed) push_undo() game.selected = who game.buildup.changed = 0 } } }, extra_supply_card() { push_undo() if (is_axis_player()) game.buildup.axis_cards++ else game.buildup.allied_cards++ pay_bps(10) }, minefield() { push_undo() game.state = 'minefield' }, replacement() { game.buildup.changed = 1 game.summary.replaced_steps ++ game.summary.replaced_bps += replacement_cost(game.selected) replace_unit(game.selected) pay_bps(replacement_cost(game.selected)) }, refit() { game.buildup.changed = 1 game.summary.refit ++ hide_unit(game.selected) set_unit_hex(pop_selected(), friendly_refit()) }, hex(to) { if (to === MALTA) { push_undo() log(`Malta Group arrived.`) log(`Axis Resupply dropped to 2.`) let base = friendly_base() if (is_battle_hex(base)) base = friendly_queue() for_each_friendly_unit_in_hex(MALTA, u => { set_unit_hex(u, base) }) game.malta = 1 return } game.buildup.changed = 1 let who = game.selected let from = unit_hex(who) if (to === from) { game.selected = -1 } else if (to === friendly_refit()) { game.summary.refit ++ hide_unit(game.selected) set_unit_hex(pop_selected(), friendly_refit()) } else { search_redeploy(from) if (can_move_to(to, 2)) { push_buildup_summary(from, to, 0) pay_bps(1) } else { push_buildup_summary(from, to, 1) if (is_fortress_besieged(from) || is_fortress_besieged(to)) pay_bps(4) else pay_bps(1) if (from === BARDIA || to === BARDIA) game.buildup.bardia-- if (from === BENGHAZI || to === BENGHAZI) game.buildup.benghazi-- if (from === TOBRUK || to === TOBRUK) game.buildup.tobruk-- } set_unit_hex(who, to) hide_unit(who) } }, end_buildup() { end_buildup_spending() } } function end_buildup_spending() { game.selected = -1 print_buildup_summary() let n = available_bps() if (n > 20) { log(`Lost ${n - 20} unspent BPs.`) pay_bps(n - 20) } log(`Saved ${available_bps()} BPs.`) if (is_active_player()) { set_enemy_player() goto_buildup_discard() } else { goto_buildup_resupply() } } function friendly_minefields() { return is_axis_player() ? game.minefields[MF_AXIS] : game.minefields[MF_ALLIED] } function friendly_buildup_network() { return is_axis_player() ? game.buildup.axis_network : game.buildup.allied_network } states.minefield = { inactive: "buildup", prompt() { view.prompt = `Buildup: Build a minefield.` let mfs = friendly_minefields() if (count_friendly_minefields() >= 1) gen_action('dismantle') if (available_bps() >= 15) { let net = friendly_buildup_network() for (let x of all_hexes) if (net[x] && !is_battle_hex(x) && !set_has(mfs, x)) gen_action_hex(x) } }, dismantle() { push_undo() if (count_friendly_minefields() >= 2) game.state = 'dismantle1' else game.state = 'dismantle0' }, hex(x) { let mfs = friendly_minefields() game.summary.built ++ pay_bps(15) set_add(mfs, x) game.state = 'spending_bps' } } states.dismantle0 = { inactive: "buildup", prompt() { view.prompt = `Buildup: Dismantle minefield.` let net = friendly_buildup_network() for (let x of friendly_minefields()) if (net[x] && !is_battle_hex(x)) gen_action_hex(x) for (let x of game.minefields[MF_VISIBLE]) if (net[x] && !is_battle_hex(x)) gen_action_hex(x) }, hex(x) { set_delete(friendly_minefields(), x) set_delete(game.minefields[MF_VISIBLE], x) game.state = 'spending_bps' }, } states.dismantle1 = { inactive: "buildup", prompt() { view.prompt = `Buildup: Dismantle first minefield to build a new one.` let net = friendly_buildup_network() for (let x of friendly_minefields()) if (net[x] && !is_battle_hex(x)) gen_action_hex(x) for (let x of game.minefields[MF_VISIBLE]) if (net[x] && !is_battle_hex(x)) gen_action_hex(x) }, hex(x) { set_delete(friendly_minefields(), x) set_delete(game.minefields[MF_VISIBLE], x) game.state = 'dismantle2' }, } states.dismantle2 = { inactive: "buildup", prompt() { view.prompt = `Buildup: Dismantle second minefield to build a new one.` let net = friendly_buildup_network() for (let x of friendly_minefields()) if (net[x] && !is_battle_hex(x)) gen_action_hex(x) for (let x of game.minefields[MF_VISIBLE]) if (net[x] && !is_battle_hex(x)) gen_action_hex(x) }, hex(x) { set_delete(friendly_minefields(), x) set_delete(game.minefields[MF_VISIBLE], x) game.state = 'dismantle3' }, } states.dismantle3 = { inactive: "buildup", prompt() { view.prompt = `Buildup: Build a new minefield at no cost.` let mfs = friendly_minefields() let net = friendly_buildup_network() for (let x of all_hexes) if (net[x] && !is_battle_hex(x) && !set_has(mfs, x) && !set_has(game.minefields[MF_VISIBLE], x)) gen_action_hex(x) }, hex(x) { let mfs = friendly_minefields() game.summary.dismantled += 2 game.summary.built ++ set_add(mfs, x) game.state = 'spending_bps' }, } function goto_buildup_resupply() { // Per-scenario allotment let axis_resupply = (game.month <= 10 || game.malta) ? 2 : 3 let allied_resupply = 3 if (is_pregame_buildup()) axis_resupply = allied_resupply = 0 log_h2("Resupply") log(`Shuffled supply cards.`) shuffle_cards() // 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) game.buildup = null goto_player_initiative() } // === INITIATIVE === function goto_player_initiative() { log_h2("Initiative") game.phasing = AXIS set_passive_player() game.state = 'allied_player_initiative' } states.allied_player_initiative = { inactive: "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('pass') }, real_card() { log(`Allied player 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 player challenged for the initiative.`) set_active_player() game.state = 'axis_player_initiative' }, pass() { log(`Allied player did not challenge for the initiative.`) goto_player_turn() } } states.axis_player_initiative = { inactive: "initiative", prompt() { view.prompt = "Initiative: You may defend your initiative." let hand = player_hand() if (hand[REAL] > 0) gen_action('real_card') gen_action('pass') }, 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() }, pass() { if (game.phasing === ALLIED) log("Allied siezed the initiative.") else log("Allied card was a dummy.") goto_player_turn() } } // === END GAME SUPPLY CHECK (AFTER LAST MONTH, BEFORE VICTORY CHECK) === function goto_end_game() { log_h1("End Game") game.phasing = AXIS set_active_player() goto_end_game_supply_check() } function goto_end_game_supply_check() { let base_net = friendly_supply_network() for_each_friendly_unit_on_map(u => { if (base_net[unit_hex(u)]) set_unit_supply(u, SS_BASE) else set_unit_supply(u, SS_NONE) }) if (is_axis_player()) game.capacity = [ 1, 1, 2 ] else game.capacity = [ 2, 2, 5 ] game.oasis = [ 1, 1, 1 ] goto_fortress_supply('end_game_fortress_supply') } function goto_end_game_supply_check_recover() { let summary = [] for_each_friendly_unit_on_map(u => { if (is_unit_supplied(u) && is_unit_disrupted(u) && !is_battle_hex(unit_hex(u))) { set_add(summary, unit_hex(u)) clear_unit_disrupted(u) } }) for (let x of summary) log(`Recovered at #${x}.`) resume_end_game_eliminate_unsupplied() } function resume_end_game_eliminate_unsupplied() { game.state = 'end_game_eliminate_unsupplied' let done = true for_each_friendly_unit_on_map(u => { if (is_unit_unsupplied(u)) done = false }) if (done) { if (is_axis_player()) { set_enemy_player() goto_end_game_supply_check() } else { end_game() } } } states.end_game_eliminate_unsupplied = { inactive: "buildup", prompt() { view.prompt = `End Game: 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_end_game_eliminate_unsupplied() }, } // === VICTORY CHECK === const EXIT_EAST_EDGE = [ 99, 148 ] const EXIT_WEST_EDGE = [ 175 ] const EXIT_EAST = 47 const EXIT_WEST = 53 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) } }) } for (let x of EXIT_WEST_EDGE) { for_each_allied_unit(u => { if (unit_hex(u) === x && is_unit_supplied(u)) { log(`Exited the west map edge.`) set_unit_hex(u, EXIT_WEST) } }) } let axis_exited = 0 for_each_axis_unit(u => { if (unit_hex(u) === EXIT_EAST) axis_exited++ }) let allied_exited = 0 for_each_allied_unit(u => { if (unit_hex(u) === EXIT_WEST) allied_exited++ }) if (is_axis_hex(ALEXANDRIA)) { log_br() log("Axis captured Alexandria!") return goto_game_over(AXIS, "Axis Strategic Victory!") } if (axis_exited >= 3) { log_br() log("Axis exited three supplied units!") 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!") } if (is_allied_hex(EL_AGHEILA) || allied_exited >= 3) { log_br() log("Allied exited three supplied units!") 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(ALLIED, "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() { log_h2(`${game.active} Deployment`) game.state = 'free_deployment' if (!has_friendly_unit_in_raw_hex(DEPLOY)) goto_initial_supply_cards() game.selected = [] game.summary = {} } function is_valid_deployment_hex(x, n) { if (can_trace_supply_to_base_or_fortress(x)) return true if (x === TOBRUK) return count_friendly_units_in_hex(x) + n <= 5 if (x === JALO_OASIS || x === JARABUB_OASIS || x === SIWA_OASIS) return count_friendly_units_in_hex(x) + n <= 1 return false } states.free_deployment = { inactive: "setup", prompt() { let scenario = current_scenario() let axis = (game.active === AXIS) view.prompt = `Setup: ${game.active} Deployment.` view.deploy = 1 // view.prompt = `Setup: Deploy units in a supplied location in the setup area.` let done = true for_each_friendly_unit_in_hex(DEPLOY, u => { gen_action_unit(u) done = false }) if (done) gen_action('end_deployment') if (game.selected.length > 0) { trace_supply_to_base_or_fortress(friendly_base()) for (let x of (axis ? scenario.axis_deployment : scenario.allied_deployment)) { if (!is_enemy_hex(x)) { let limit = 0 if (scenario.deployment_limit) limit = scenario.deployment_limit[x] | 0 if (!limit || count_friendly_units_in_hex(x) + game.selected.length <= limit) { if (is_valid_deployment_hex(x, game.selected.length)) gen_action_hex(x) } } } } }, unit(u) { set_toggle(game.selected, u) }, hex(to) { let list = game.selected game.selected = [] push_undo() game.summary[to] = (game.summary[to] | 0) + list.length for (let who of list) set_unit_hex(who, to) }, end_deployment() { log(`Deployed`) let keys = Object.keys(game.summary).map(Number).sort((a,b)=>a-b) for (let x of keys) log(`>${game.summary[x]} at #${x}`) game.summary = null goto_initial_supply_cards() } } function goto_initial_supply_cards() { game.state = 'initial_supply_cards' if (is_axis_player()) deal_axis_supply_cards(current_scenario().axis_initial_supply) else deal_allied_supply_cards(current_scenario().allied_initial_supply) } states.initial_supply_cards = { inactive: "setup", 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 all cards.`) game.axis_hand[REAL] = 0 game.axis_hand[DUMMY] = 0 deal_axis_supply_cards(current_scenario().axis_initial_supply) } else { log(`Allied discarded all cards.`) game.allied_hand[REAL] = 0 game.allied_hand[DUMMY] = 0 deal_allied_supply_cards(current_scenario().allied_initial_supply) } end_free_deployment() }, keep() { end_free_deployment() }, } function end_free_deployment() { set_enemy_player() if (has_friendly_unit_in_raw_hex(DEPLOY)) { goto_free_deployment() } else { game.selected = -1 game.summary = null begin_game() } } function begin_game() { log_h1(current_month_name()) if (game.scenario === "Crusader") { game.phasing = ALLIED } // No buildup first month // No initiative first month if (is_pregame_buildup()) goto_pregame_buildup() else goto_player_turn() } // === SETUP === function find_axis_units(a) { let list = [] for (let u = first_axis_unit; u <= last_axis_unit; ++u) if (unit_appearance[u] === a) list.push(u) return list } function find_allied_units(a) { let list = [] for (let u = first_allied_unit; u <= last_allied_unit; ++u) if (unit_appearance[u] === a) list.push(u) return list } function setup_reinforcements(m) { for (let u = 0; u < unit_count; ++u) { if (unit_appearance[u] === m) { if (m === "M") set_unit_hex(u, MALTA) else set_unit_hex(u, hexdeploy + m) set_unit_supply(u, SS_BASE) } } } function setup_units(where, steps, list) { if (where < 0) where = hexdeploy - where for (let u of list) { if (typeof u === 'string') u = find_unit(u) set_unit_hex(u, where) if (steps < 0) set_unit_steps(u, unit_max_steps[u] + steps) else if (steps > 0) set_unit_steps(u, steps) set_unit_supply(u, SS_BASE) } } function current_scenario() { return SCENARIOS[game.scenario] } function sort_deployment_for_axis(list) { list = list.slice() list.sort((a,b) => distance_to[EL_AGHEILA][b] - distance_to[EL_AGHEILA][a]) return list } function sort_deployment_for_allied(list) { list = list.slice() list.sort((a,b) => distance_to[ALEXANDRIA][b] - distance_to[ALEXANDRIA][a]) return list } const SCENARIOS = { "1940": { start: 1, end: 6, axis_deployment: sort_deployment_for_axis([...LIBYA, SIDI_OMAR]), allied_deployment: sort_deployment_for_allied(EGYPT), axis_initial_supply: 6, allied_initial_supply: 3, }, "1941": { start: 1, end: 10, axis_deployment: [], allied_deployment: sort_deployment_for_allied([...EGYPT, ...LIBYA]), axis_initial_supply: 6, allied_initial_supply: 6, }, "Crusader": { start: 8, end: 10, axis_deployment: sort_deployment_for_axis([...LIBYA_NO_TOBRUK, SIDI_OMAR, SOLLUM]), allied_deployment: sort_deployment_for_allied([...EGYPT, TOBRUK]), axis_initial_supply: 10, allied_initial_supply: 12, deployment_limit: { [TOBRUK]: 5 }, }, "Battleaxe": { start: 4, end: 10, axis_deployment: sort_deployment_for_axis(LIBYA_NO_TOBRUK), allied_deployment: sort_deployment_for_allied([...EGYPT, TOBRUK]), axis_initial_supply: 4, allied_initial_supply: 8, deployment_limit: { [TOBRUK]: 5 }, }, "1942": { start: 11, end: 20, axis_deployment: [ EL_AGHEILA, MERSA_BREGA ], allied_deployment: sort_deployment_for_allied([...EGYPT, ...LIBYA]), axis_initial_supply: 5, allied_initial_supply: 5, deployment_limit: { [EL_AGHEILA]: 14, [MERSA_BREGA]: 10, }, }, "Gazala": { start: 14, end: 15, axis_deployment: sort_deployment_for_axis(regions["West Line"]), allied_deployment: sort_deployment_for_allied(regions["East Line"]), axis_initial_supply: 10, allied_initial_supply: 12, }, "Pursuit to Alamein": { start: 15, end: 20, axis_deployment: sort_deployment_for_axis(LIBYA), allied_deployment: sort_deployment_for_allied(EGYPT), axis_initial_supply: 8, allied_initial_supply: 8, }, "1941-42": { start: 1, end: 20, axis_deployment: [], allied_deployment: sort_deployment_for_allied([...EGYPT, ...LIBYA]), axis_initial_supply: 6, allied_initial_supply: 6, }, } const SETUP = { "1940" () { 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" () { 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" () { 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) set_add(game.minefields[MF_VISIBLE], TOBRUK) }, "Battleaxe" () { 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" () { setup_units(DEPLOY, 0, [ "21/3", "15/33", "90/580", "90/sv288", "90/346", "88mm/A", ]) setup_units(DEPLOY, 2, [ "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(DEPLOY, 0, [ "1/2", "1SA", "7/22G", "1/201G", "4IN/7m", "4IN/3m", "1/SG", "4IN/11", "5IN/29", "2#", ]) 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" () { 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" () { 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" () { SETUP["1941"]() 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]() setup_fortress(scenario, BARDIA) setup_fortress(scenario, BENGHAZI) setup_fortress(scenario, TOBRUK) game.phasing = AXIS set_active_player() goto_free_deployment() } // === PUBLIC FUNCTIONS === exports.roles = [ "Axis", "Allied" ] exports.scenarios = [ "1940", "1941", "1942", "Battleaxe", "Crusader", "Gazala", "Pursuit to Alamein", "1941-42" ] exports.setup = function (seed, scenario, _options) { load_state({ seed: seed, log: [], undo: [], state: null, phasing: AXIS, active: AXIS, selected: -1, scenario: scenario, month: 0, draw_pile: [ 28, 14 ], // 28 real supply + 14 dummy supply axis_hand: [ 0, 0 ], allied_hand: [ 0, 0 ], axis_bps: 0, allied_bps: 0, units: new Array(unit_count).fill(0), revealed: [], moved: [], fired: [], raiders: [], recover: [], // axis/allied/visible/to-be-revealed minefields: [[],[],[],[]], // revealed to both players // fortress control fortress: 7, axis_award: 0, allied_award: 0, assign: 0, // fortress and oasis supply capacity capacity: [ 0, 0, 0 ], oasis: [ 0, 0, 0 ], // battle hexes (defender) axis_hexes: [], allied_hexes: [], // hexside control (for battle hexes) axis_sides: [], allied_sides: [], // current turn option and selected moves commit: null, turn_option: null, passed: 0, side_limit: {}, used: 0, 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 new_battles: [], active_battles: [], assault_battles: [], pursuit: 0, battle: 0, hits: null, flash: null, // misc states group: null, regroup: null, disrupt: null, withdraw: null, hexside: null, buildup: null, // logging summary: null, }) setup(scenario) return game } exports.view = function(state, current) { timeout = Date.now() + TIMEOUT // don't think too long! load_state(state) let scenario = current_scenario() view = { month: game.month, start: scenario.start, end: scenario.end, units: game.units, revealed: game.revealed, moved: game.moved, fortress: game.fortress, axis_hand: game.axis_hand[REAL] + game.axis_hand[DUMMY], allied_hand: game.allied_hand[REAL] + game.allied_hand[DUMMY], phasing: game.phasing, commit: game.commit ? (game.commit[0] + game.commit[1]) : -1, axis_hexes: game.axis_hexes, allied_hexes: game.allied_hexes, axis_sides: game.axis_sides, allied_sides: game.allied_sides, } let player_minefields = null if (current === AXIS) { view.cards = game.axis_hand if (game.minefields[MF_AXIS].length > 0) player_minefields = game.minefields[MF_AXIS] } else if (current === ALLIED) { view.cards = game.allied_hand if (game.minefields[MF_ALLIED].length > 0) player_minefields = game.minefields[MF_ALLIED] } if (game.minefields[MF_VISIBLE].length > 0) { if (player_minefields) view.minefields = game.minefields[MF_VISIBLE].concat(player_minefields) else view.minefields = game.minefields[MF_VISIBLE] } else { if (player_minefields) view.minefields = player_minefields } if (current === game.active) view.selected = game.selected if (states[game.state].show_battle) { view.battle = game.battle view.retreat = game.retreat_units } if (states[game.state].show_pursuit) view.pursuit = game.pursuit if (view.battle || view.pursuit) { view.hits = game.hits view.flash = game.flash if (game.fired.length > 0) view.fired = game.fired } return common_view(current) } exports.query = function (state, _player, q) { timeout = Date.now() + TIMEOUT // don't think too long! if (q === 'supply') { load_state(state) if (game.buildup) { return { axis_supply: game.buildup.axis_network, axis_supply_line: game.buildup.axis_line, allied_supply: game.buildup.allied_network, allied_supply_line: game.buildup.allied_line, } } else { return { axis_supply: union_axis_network(), axis_supply_line: union_axis_line(), allied_supply: union_allied_network(), allied_supply_line: union_allied_line(), } } } return null } function gen_action_unit(u) { gen_action('unit', u) } function gen_action_unit_hit(u) { gen_action('unit_hit', u) } function gen_action_hex(x) { gen_action('hex', x) } function gen_action_forced_march(x) { gen_action('forced_march', x) } // === COMMON TEMPLATE === function roll_die() { clear_undo() return random(6) + 1 } 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 log_h4(msg) { log_br() log(".h4 " + msg) } function gen_action(action, argument) { if (argument !== undefined) { if (!(action in view.actions)) { view.actions[action] = [ argument ] } else { set_add(view.actions[action], 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.action = function (state, current, action, arg) { timeout = Date.now() + TIMEOUT // don't think too long! load_state(state) 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 (view.actions.undo === undefined) { if (game.undo && game.undo.length > 0) view.actions.undo = 1 else view.actions.undo = 0 } } return view } // === COMMON LIBRARY === function random(range) { // An MLCG using integer arithmetic with doubles. // https://www.ams.org/journals/mcom/1999-68-225/S0025-5718-99-00996-5/S0025-5718-99-00996-5.pdf // m = 2**35 − 31 return (game.seed = game.seed * 200105 % 34359738337) % range } // remove item at index (faster than splice) function array_remove(array, index) { let n = array.length for (let i = index + 1; i < n; ++i) array[i - 1] = array[i] array.length = n - 1 } // insert item at index (faster than splice) function array_insert(array, index, item) { for (let i = array.length; i > index; --i) array[i] = array[i - 1] array[index] = item } function set_clear(set) { set.length = 0 } function set_has(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 true } return false } 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 } array_insert(set, a, item) } function set_delete(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 { array_remove(set, m) return } } } function set_toggle(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 { array_remove(set, m) return } } array_insert(set, a, item) } // Fast deep copy for objects without cycles function object_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] = object_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] = object_copy(v) else copy[i] = v } return copy } } function clear_undo() { if (game.undo.length > 0) game.undo = [] } 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 = object_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) }