diff options
Diffstat (limited to 'rules.js')
-rw-r--r-- | rules.js | 603 |
1 files changed, 452 insertions, 151 deletions
@@ -1,11 +1,15 @@ "use strict" -// TODO: withdrawal pass regroup moves +// TODO: remember withdrawal supply lines for regroup pass moves +// TODO: withdrawal pass regroup moves where 2+ moves combine to reduce supply network // TODO: clean up withdraw calculations and search_withdraw (pass supply source directly, not sample unit) // TODO: log summaries (deploy, rebuild, move, etc) // TODO: put initial deployment stack somewhere more accessible (spread out along the top?) +// UI: skip clicking destination if only one regroup move selected? + +// UI: flash message cleanup during battles // UI: separate colors for secret and visible minefields // UI: basic turn - skip move - direct to combat @@ -372,6 +376,10 @@ 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 @@ -571,6 +579,13 @@ function count_friendly_units_in_hex(x) { 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) @@ -947,13 +962,13 @@ function for_each_friendly_unit_in_hex(x, fn) { function for_each_undisrupted_friendly_unit_in_hex(x, fn) { for (let u = first_friendly_unit; u <= last_friendly_unit; ++u) - if (!is_unit_disrupted(u) && unit_hex(u) === x) + if (is_unit_undisrupted(u) && unit_hex(u) === x) fn(u) } function for_each_undisrupted_and_unmoved_friendly_unit_in_hex(x, fn) { for (let u = first_friendly_unit; u <= last_friendly_unit; ++u) - if (!is_unit_disrupted(u) && unit_hex(u) === x && !is_unit_moved(u)) + if (is_unit_undisrupted(u) && unit_hex(u) === x && !is_unit_moved(u)) fn(u) } @@ -961,7 +976,7 @@ function count_hex_or_adjacent_has_undisrupted_and_unmoved_friendly_unit(here) { let n = 0 for_each_hex_and_adjacent_hex(here, x => { for (let u = first_friendly_unit; u <= last_friendly_unit; ++u) { - if (!is_unit_disrupted(u) && !is_unit_moved(u) && unit_hex(u) === x) { + if (is_unit_undisrupted(u) && !is_unit_moved(u) && unit_hex(u) === x) { n++ return } @@ -978,7 +993,7 @@ function for_each_enemy_unit_in_hex(x, fn) { function for_each_undisrupted_enemy_unit_in_hex(x, fn) { for (let u = first_enemy_unit; u <= last_enemy_unit; ++u) - if (!is_unit_disrupted(u) && unit_hex(u) === x) + if (is_unit_undisrupted(u) && unit_hex(u) === x) fn(u) } @@ -1445,10 +1460,8 @@ function allied_supply_network() { } function bardia_supply_line() { - console.log("X") if (supply_bardia_invalid) update_bardia_supply() - debug_hexes("bardia-line", supply_bardia_line) return supply_bardia_line } @@ -1546,7 +1559,7 @@ function query_friendly_supply_network(src, x, y) { 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] = 1 + if (y) presence_axis[y] = 2 trace_supply_network(src) if (x) presence_axis[x] = save_x if (y) presence_axis[y] = save_y @@ -1555,7 +1568,7 @@ function query_friendly_supply_network(src, x, y) { 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] = 1 + if (y) presence_allied[y] = 2 trace_supply_network(src) if (x) presence_allied[x] = save_x if (y) presence_allied[y] = save_y @@ -1600,6 +1613,7 @@ function search_move_retreat(start, speed) { } function search_withdraw(who, bonus) { + // TODO: pass remembered supply line let sline = unit_supply_line(who) let sdist = unit_supply_distance(who) let speed = unit_speed[who] + bonus @@ -1824,6 +1838,19 @@ function move_road(to, speed) { 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 => { @@ -1859,9 +1886,16 @@ function is_enemy_hexside(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 can_unit_withdraw(who) { let result = false - if (is_unit_supplied(who)) { + if (unit_has_supply_line(who)) { let sline = unit_supply_line(who) let sdist = unit_supply_distance(who) let from = unit_hex(who) @@ -1877,7 +1911,7 @@ function can_unit_withdraw(who) { function can_unit_disengage_and_withdraw(who) { let result = false - if (is_unit_supplied(who)) { + if (unit_has_supply_line(who)) { let sline = unit_supply_line(who) let sdist = unit_supply_distance(who) let from = unit_hex(who) @@ -1903,7 +1937,7 @@ function can_unit_disengage_and_move(who) { } function can_unit_disengage_and_withdraw_to(who, to, extra) { - if (is_unit_supplied(who)) { + if (unit_has_supply_line(who)) { search_withdraw_retreat(who, extra) return can_move_to(to, unit_speed[who] + extra) } @@ -1917,6 +1951,17 @@ function can_unit_disengage_and_move_to(who, to, 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 @@ -1924,7 +1969,9 @@ function can_all_units_disengage_and_withdraw(from) { return result } -function can_all_units_withdraw(from) { +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)) @@ -1940,38 +1987,148 @@ function is_network_reduced(reference, candidate) { return false } -function is_valid_withdrawal_from(x) { +function is_valid_withdrawal_group_move_from(x) { if (is_battle_hex(x)) { // can retreat, will always reduce supply network - return can_all_units_disengage_and_withdraw(x); + return can_all_undisrupted_units_disengage_and_withdraw(x) } else { // non-retreat withdrawal, check if network is reduced after we leave this hex - if (can_all_units_withdraw(x)) { - let who = fastest_undisrupted_friendly_unit(x) - if (is_unit_supplied(who)) { - let ref_net = unit_supply_network(who) - let new_net = query_friendly_supply_network(unit_supply_source(who), x, 0) - if (is_network_reduced(ref_net, new_net)) - return true - } + if (can_all_undisrupted_units_withdraw(x)) { + // All units in hex have the same supply source + let who = any_friendly_undisrupted_unit_in_hex(x) + let net = unit_supply_network(who) + let new_net = query_friendly_supply_network(unit_supply_source(who), x, 0) + if (is_network_reduced(net, new_net)) + return true } } return false } -function is_valid_withdrawal_group_move(who, from, to) { - // TODO: pass actual source instead of sample unit +function is_valid_withdrawal_group_move_to(src, net, from, to) { if (is_battle_hex(from)) { return true } else { - let ref_net = unit_supply_network(who) - let new_net = query_friendly_supply_network(unit_supply_source(who), from, to) - if (is_network_reduced(ref_net, new_net)) + let new_net = query_friendly_supply_network(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 who = any_friendly_undisrupted_unit_in_hex(x) + let net = unit_supply_network(who) + let new_net = query_friendly_supply_network(unit_supply_source(who), 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 list_valid_withdrawal_regroup_command_points() { + let always = [] + let maybe = [] + for (let x of all_hexes) { + let status = is_valid_withdrawal_regroup_move_from(x) + if (status === 2) + always.push(x) + else if (status === 1) + maybe.push(x) + } + console.log("WITHDRAW REGROUP CANDIDATES", always, maybe) + // TODO: list valid permutations of 'maybe' hexes + return { always, maybe, to: null, evacuate: null } +} + +function gen_withdrawal_regroup_command_point() { + var m, n + for (let here of all_hexes) { + 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(game.withdraw.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) + gen_action_hex(here) + } + } +} + +function list_valid_withdrawal_regroup_destinations() { + let rommel1 = (game.rommel === 1) ? 1 : 0 + + // TODO: list hexes that can be reached by ALL units of one valid permutation of maybe-hexes + // remember + + // 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.withdraw.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) // TODO: remembered network + 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) + } + } + } + } + }) + + game.withdraw.to = result +} + +function gen_withdrawal_regroup_destination() { + // XXX + list_valid_withdrawal_regroup_destinations() + // XXX + for (let x of game.withdraw.to) + gen_action_hex(x) +} + // === MINEFIELDS === function visit_hex(x) { @@ -2074,8 +2231,6 @@ states.turn_option = { } function apply_turn_option(option) { - push_undo() - game.turn_option = option log_br() @@ -2095,6 +2250,7 @@ function apply_turn_option(option) { game.passed++ else game.passed = 0 + goto_move_phase() } @@ -2130,7 +2286,8 @@ function end_player_turn() { // Reveal supply cards if (game.commit[0] + game.commit[1] > 0) { log_br() - log(`Supply Cards Revealed:\n${game.commit[0]} real and ${game.commit[1]} dummy.`) + log(`Supply Cards Revealed`) + log(`>${game.commit[0]} real and ${game.commit[1]} dummy.`) log_br() } @@ -2216,7 +2373,7 @@ const FORTRESS_SRC_LIST = [ SS_BARDIA, SS_BENGHAZI, SS_TOBRUK ] function all_friendly_unsupplied_and_undisrupted_units() { let result = [] for_each_friendly_unit_on_map(u => { - if (!is_unit_disrupted(u) && is_unit_unsupplied(u)) + if (is_unit_undisrupted(u) && is_unit_unsupplied(u)) result.push(u) }) return result @@ -2265,9 +2422,12 @@ function auto_assign_fortress_supply(list, fortress, ss, ix) { if (dist[unit_hex(u)] === d0) ++n if (n <= game.capacity[ix]) { - for (let u of list) - if (dist[unit_hex(u)] === d0) + for (let u of list) { + if (dist[unit_hex(u)] === d0) { + log(`Assigned #${fortress} supply.`) set_unit_supply(u, ss) + } + } game.capacity[ix] -= n total += n list = list.slice(n) @@ -2319,6 +2479,7 @@ const xxx_fortress_supply = { let ss = FORTRESS_SRC_LIST[ix] push_undo() game.capacity[ix]-- + log(`Assigned #${fortress} supply.`) set_unit_supply(who, ss) }, next() { @@ -2367,6 +2528,7 @@ function assign_oasis_supply() { if (n === 1) { for_each_friendly_unit_in_hex(oasis, u => { game.oasis[ix] = 0 + log(`Assigned #${fortress} supply.`) set_unit_supply(u, SS_OASIS) }) } @@ -2389,6 +2551,7 @@ const xxx_oasis_supply = { let ix = game.assign push_undo() game.oasis[ix] = 0 + log(`Assigned #${fortress} supply.`) set_unit_supply(who, SS_OASIS) game.assign++ resume_oasis_supply() @@ -2423,8 +2586,8 @@ function goto_initial_supply_check() { function goto_initial_supply_check_recover() { for (let u of game.recover) { - if (is_unit_disrupted(u) && is_unit_supplied(u) && !is_battle_hex(unit_hex(u))) { - log(`Recovered at #${unit_hex(u)}`) + if (is_unit_supplied(u) && is_unit_disrupted(u) && !is_battle_hex(unit_hex(u))) { + log(`Recovered at #${unit_hex(u)}.`) clear_unit_disrupted(u) } } @@ -2513,7 +2676,7 @@ function goto_final_supply_check_disrupt() { set_unit_disrupted(u) } } - delete game.disrupt + game.disrupt = null goto_final_supply_check_rout() } @@ -2547,9 +2710,53 @@ states.final_supply_check_rout = { // ==== MOVEMENT PHASE === +function init_move_summary() { + game.summary = {} +} + +function push_move_summary(from, to, via, forced) { + let mm = (from) | (to << 8) | (via << 16) | (forced << 24) + game.summary[mm] = (game.summary[mm]|0) + 1 +} + +function flush_move_summary() { + 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).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 goto_move_phase() { set_clear(game.fired) game.state = 'select_moves' + init_move_summary() if (game.phasing === AXIS) { // Automatically select Rommel Move for 1-move turn options if (game.turn_option !== 'offensive' && game.turn_option !== 'blitz' && game.scenario !== "1940") @@ -2580,9 +2787,13 @@ states.select_moves = { regroup() { push_undo() game.state = 'regroup_move_command_point' + if (game.turn_option === 'pass') + game.withdraw = list_valid_withdrawal_regroup_command_points() }, end_turn() { clear_undo() + flush_move_summary() + game.summary = null reveal_visited_minefields() goto_final_supply_check() }, @@ -2627,7 +2838,7 @@ states.group_move_from = { } if (!mandatory) { for (let x of all_hexes) - if (has_undisrupted_friendly_unit(x) && is_valid_withdrawal_from(x)) + if (has_undisrupted_friendly_unit(x) && is_valid_withdrawal_group_move_from(x)) gen_action_hex(x) } } @@ -2648,6 +2859,7 @@ states.group_move_from = { 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) }) @@ -2660,12 +2872,21 @@ states.group_move_from = { if (game.turn_option === 'pass') { // Precalculate valid withdrawal move destinations here - let rommel1 = (game.rommel === 1) ? 1 : 0 - let who = fastest_undisrupted_friendly_unit(game.from1) - console.log("CALC WITHDRAWAL DESTINATIONS") - search_withdraw(who, 1 + rommel1) - game.withdraw = list_valid_withdrawal_group_moves(who, game.from1, unit_speed[who] + 1 + rommel1) - console.log("DONE") + 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 = 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") + } } }, } @@ -2685,8 +2906,7 @@ states.regroup_move_command_point = { } } } else { - // TODO: Withdrawal regroup moves - view.prompt = "TODO" + gen_withdrawal_regroup_command_point() } }, rommel() { @@ -2703,6 +2923,8 @@ states.regroup_move_command_point = { else game.from2 = x game.state = 'regroup_move_destination' + if (game.turn_option === 'pass') + list_valid_withdrawal_regroup_destinations() }, } @@ -2717,13 +2939,17 @@ states.regroup_move_destination = { else cp = game.from2, rommel = (game.rommel === 2 ? 1 : 0) - path_valid.fill(0) - for_each_hex_and_adjacent_hex(cp, x => { - find_valid_regroup_destinations(x, rommel) - }) - for (let x of all_hexes) - if (path_valid[x]) - gen_action_hex(x) + 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() + } }, rommel() { push_undo() @@ -2752,6 +2978,24 @@ function end_movement() { // === 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(who, 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 goto_move() { if (game.rommel === 1) { if (game.from1 && game.to1) @@ -2780,6 +3024,21 @@ function goto_move() { game.state = 'move' } +function can_end_move() { + if (game.turn_option !== 'pass') + return true + + if (game.to1) { + // TODO: regroup move: did we reduce the supply network? + for (let x of game.withdraw.always) + if (!has_friendly_unit(x)) + return true + } else { + if (!has_friendly_unit(game.from1)) + return true + } +} + states.move = { inactive: "move", prompt() { @@ -2811,14 +3070,15 @@ states.move = { // Select Regroup Move 1 if (game.to1) { for_each_hex_and_adjacent_hex(game.from1, from => { - let speed = max_speed_of_undisrupted_and_unmoved_friendly_unit_in_hex(from) - if (speed > 0 && !has_enemy_unit(from)) { - // TODO: withdraw pass move - search_move(from, speed + 1 + rommel1) - for_each_undisrupted_and_unmoved_friendly_unit_in_hex(from, u => { - if (can_move_to(game.to1, unit_speed[u] + 1 + rommel1)) - gen_action_unit(u) - }) + if (!has_enemy_unit(from)) { + 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) + }) + } } }) } @@ -2826,14 +3086,15 @@ states.move = { // Select Regroup Move 2 if (game.to1) { for_each_hex_and_adjacent_hex(game.from2, from => { - let speed = max_speed_of_undisrupted_and_unmoved_friendly_unit_in_hex(from) - if (speed > 0 && !has_enemy_unit(from)) { - // TODO: withdraw pass move - search_move(from, speed + 1 + rommel2) - for_each_undisrupted_and_unmoved_friendly_unit_in_hex(from, u => { - if (can_move_to(game.to2, unit_speed[u] + 1 + rommel2)) - gen_action_unit(u) - }) + if (!has_enemy_unit(from)) { + 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) + }) + } } }) } @@ -2851,18 +3112,14 @@ states.move = { } } - if (has_overrun_hex) + if (has_overrun_hex) { gen_action('overrun') - else { - if (game.turn_option !== 'pass') { + } else { + if (can_end_move()) gen_action('end_move') - } else { - if (!game.to1 && !has_friendly_unit(game.from1)) - gen_action('end_move') - } } } else { - view.prompt = `Move: Select hex to move to.` + view.prompt = `Move: Select destination hex.` // Deselect gen_action_unit(game.selected) @@ -2884,7 +3141,7 @@ states.move = { this.hex(to) }, hex(to) { - apply_move(to) + apply_move(to, false) }, retreat() { push_undo() @@ -2894,11 +3151,13 @@ states.move = { let n = 0 let where = 0 for (let x of all_hexes) { - if (is_overrun_hex(x)) { + if (is_enemy_rout_hex(x)) { n ++ where = x } } + flush_move_summary() + init_move_summary() if (n === 1) { goto_overrun(where) } else { @@ -2907,11 +3166,10 @@ states.move = { } }, end_move() { - push_undo() // XXX clear_undo() - log_br() + flush_move_summary() if (game.turn_option === 'pass') - delete game.withdraw + game.withdraw = null end_movement() } } @@ -2931,7 +3189,6 @@ states.overrun = { } function goto_overrun(where) { - log_h3(`Overrun at #${where}`) goto_rout(where, true, null) } @@ -2979,21 +3236,13 @@ function gen_move() { } } -function list_valid_withdrawal_group_moves(who, from, speed) { - let result = [] - for (let to of all_hexes) - if (to != from) - if (can_move_to(to, speed) && is_valid_withdrawal_group_move(who, game.from1, to)) - result.push(to) - return result -} - function gen_withdraw() { let rommel1 = (game.rommel === 1) ? 1 : 0 let speed = unit_speed[game.selected] let from = unit_hex(game.selected) - if (!game.to1 && game.from1 === from) { + // 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)) @@ -3004,7 +3253,8 @@ function gen_withdraw() { } } - if (game.to1 && is_hex_or_adjacent_to(from, game.from1)) { + // 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)) @@ -3012,7 +3262,7 @@ function gen_withdraw() { } } -function apply_move(to) { +function apply_move(to, is_retreat) { let rommel1 = (game.rommel === 1) ? 1 : 0 let rommel2 = (game.rommel === 2) ? 1 : 0 let who = pop_selected() @@ -3021,7 +3271,7 @@ function apply_move(to) { push_undo() - search_move(from, speed + 1 + (rommel1 | rommel2)) + search_current_move(who, is_retreat) if (!game.to1 && game.from1 === from) if (can_move_to(to, speed + 1 + rommel1)) @@ -3129,7 +3379,7 @@ function move_unit(who, to, speed, move) { if (is_forced_march_move(from, to, speed)) { if (move_via(who, to, speed, move)) { forced_march_via(who, game.hexside.via[0], to, move) - delete game.hexside + game.hexside = null } else { game.state = 'forced_march_via' } @@ -3141,14 +3391,14 @@ function move_unit(who, to, speed, move) { forced_march_via(who, game.hexside.via[0], to, move) else engage_via(who, game.hexside.via[0], to, move) - delete game.hexside + game.hexside = null } else { game.state = 'engage_via' } } else { - log(`>from #${from} to #${to}`) + push_move_summary(from, to, 0, 0) visit_path(from, to, speed) set_unit_moved(who) set_unit_hex(who, to) @@ -3172,7 +3422,7 @@ states.forced_march_via = { search_move(from, speed + 1 + (rommel1 | rommel2)) forced_march_via(game.hexside.who, via, game.hexside.to, game.hexside.move) - delete game.hexside + game.hexside = null game.state = 'move' } } @@ -3196,7 +3446,7 @@ states.engage_via = { search_move(from, speed + 1 + (rommel1 | rommel2)) forced_march_via(game.hexside.who, via, game.hexside.to, game.hexside.move) - delete game.hexside + game.hexside = null game.state = 'move' }, hex(via) { @@ -3208,7 +3458,7 @@ states.engage_via = { search_move(from, speed + (rommel1 | rommel2)) engage_via(game.hexside.who, via, game.hexside.to, game.hexside.move) - delete game.hexside + game.hexside = null game.state = 'move' } } @@ -3236,7 +3486,7 @@ function forced_march_via(who, via, to, move) { game.side_limit[side] = 1 } - log(`>forced march from #${from} via #${via} to #${to}`) + push_move_summary(from, to, via, 1) } function engage_via(who, via, to, move) { @@ -3250,9 +3500,9 @@ function engage_via(who, via, to, move) { visit_hex(to) if (from !== via) - log(`>from #${from} via #${via} to #${to}`) + push_move_summary(from, to, via, 0) else - log(`>from #${from} to #${to}`) + push_move_summary(from, to, 0, 0) engage_via_hexside(who, via, to) } @@ -3281,10 +3531,21 @@ function engage_via_hexside(who, via, to) { // === FORCED MARCHES === function goto_forced_marches() { - if (game.forced.length > 0) + if (game.forced.length > 0) { + log(`Forced Marches`) game.state = 'forced_marches' - else + } 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 = { @@ -3294,27 +3555,22 @@ states.forced_marches = { gen_action_unit(who) }, unit(who) { - push_undo() // XXX let via = unit_hex(who) let ix = game.forced.findIndex(item => who === item[0]) let to = game.forced[ix][1] let from = game.forced[ix][2] || via let roll = roll_die() if (roll >= 4) { - log(`Forced March roll ${die_face_hit[roll]} success.`) + log(`>${die_face_hit[roll]} to #${to}`) visit_hex(to) if (has_enemy_unit(to)) { engage_via_hexside(who, via, to, false) } else { set_unit_hex(who, to) - log(`>from #${via} to #${to}`) } } else { - log(`Forced March roll ${die_face_miss[roll]} failed!`) - if (from !== via) { - log(`>returned to #${from}`) - set_unit_hex(who, from) - } + log(`>${die_face_miss[roll]} disrupted at #${to}`) + set_unit_hex(who, from) if (is_unit_disrupted(who)) reduce_unit(who) // was a retreating unit else @@ -3322,7 +3578,7 @@ states.forced_marches = { } game.forced.splice(ix, 1) - goto_forced_marches() + resume_forced_marches() } } @@ -3363,10 +3619,13 @@ states.forced_marches_rout = { // === RETREAT === function is_valid_retreat_hex(from) { - if (game.turn_option === 'pass') - return can_all_retreat(from) - else - return can_any_retreat(from) + 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) { @@ -3491,7 +3750,8 @@ states.retreat_from = { }) } - gen_action('end_move') + if (can_end_move()) + gen_action('end_move') }, hex(x) { push_undo() @@ -3654,7 +3914,7 @@ states.retreat_move = { }, hex(to) { let who = game.selected - apply_move(to) + apply_move(to, true) set_unit_disrupted(who) }, end_retreat() { @@ -3682,9 +3942,8 @@ function end_retreat() { } function end_retreat_2() { - if (can_select_retreat_hex()) { + if (can_select_retreat_hex()) game.state = 'retreat_from' - } else end_movement() } @@ -3693,7 +3952,7 @@ function end_retreat_2() { function can_select_refuse_battle_hex() { for (let x of game.active_battles) - if (can_all_units_disengage_and_withdraw(x)) + if (can_all_undisrupted_units_disengage_and_withdraw(x)) return true return false } @@ -3712,7 +3971,7 @@ states.refuse_battle = { prompt() { view.prompt = `You may Refuse Battle.` for (let x of game.active_battles) - if (can_all_units_disengage_and_withdraw(x)) + if (can_all_undisrupted_units_disengage_and_withdraw(x)) gen_action_hex(x) gen_action('pass') }, @@ -3730,10 +3989,12 @@ states.refuse_battle = { function goto_refuse_battle_move() { set_passive_player() - if (has_undisrupted_friendly_unit(game.refuse)) + if (has_undisrupted_friendly_unit(game.refuse)) { game.state = 'refuse_battle_move' - else + log(`Withdrew`) + } else { end_refuse_battle_move() + } } states.refuse_battle_move = { @@ -3793,6 +4054,8 @@ function end_refuse_battle_move_2() { // eliminated if cannot function goto_rout(from, enemy, after) { + clear_undo() + // remember state and callback so we can resume after routing if (after) { @@ -3857,17 +4120,20 @@ states.rout_attrition = { done = false }) if (done) { - delete game.rout.attrition + game.rout.attrition = null goto_rout_fire(game.rout.from) } }, } function goto_rout_move() { - if (has_friendly_unit(game.rout.from)) + if (has_friendly_unit(game.rout.from)) { + // TODO: auto-eliminate if no withdraw path available game.state = 'rout_move' - else + log(`Withdrew`) + } else { end_rout() + } } states.rout_move = { @@ -3891,6 +4157,7 @@ states.rout_move = { eliminate = false } } + // TODO: should already have eliminated? if (eliminate) gen_action('eliminate') } @@ -3917,13 +4184,14 @@ states.rout_move = { } function end_rout() { + log_br() game.state = game.rout.state release_hex_control(game.rout.from) set_delete(game.active_battles, game.rout.from) if (game.active !== game.rout.active) set_enemy_player() let after = game.rout.after - delete game.rout + game.rout = null if (after) after_rout_table[after]() } @@ -4147,6 +4415,8 @@ function goto_battle(x) { log_h3(`Battle at #${x}`) // goto defensive fire + log_br() + log(`Defensive Fire`) set_passive_player() game.state = 'battle_fire' game.hits = [ 0, 0, 0, 0 ] @@ -4208,13 +4478,14 @@ function goto_hits() { game.hits[2] |= 0 game.hits[3] |= 0 - // XXX if (true) { if (game.hits[0] + game.hits[1] + game.hits[2] + game.hits[3] > 0) { + game.flash = format_allocate_hits() if (game.state === 'battle_fire') game.state = 'battle_hits' else game.state = 'probe_hits' } else { + game.flash = "No hits" if (game.state === 'battle_fire') end_battle_hits() else @@ -4405,6 +4676,8 @@ function end_battle_hits() { goto_rout(game.battle, false, end_battle) } else if (game.active === game.phasing && has_friendly_units_in_battle()) { // goto offensive fire + log_br() + log(`Offensive Fire`) game.state = 'battle_fire' game.hits = [ 0, 0, 0, 0 ] } else { @@ -4518,6 +4791,19 @@ function slowest_undisrupted_friendly_unit_speed(where) { return r } +function slowest_undisrupted_friendly_unit(where) { + let who = -1 + let r = 4 + 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 @@ -4901,6 +5187,9 @@ function init_buildup() { bardia: 2, benghazi: 2, tobruk: 5, + + // for undo tracking + changed: 0 } } @@ -5159,7 +5448,7 @@ states.spending_bps = { if (bps >= 10) gen_action('extra_supply_card') for_each_friendly_unit_on_map(u => { - if (!is_unit_disrupted(u)) + if (is_unit_undisrupted(u)) gen_action_unit(u) }) if (game.month >= 11 && has_friendly_unit_in_raw_hex(MALTA)) @@ -5174,9 +5463,9 @@ states.spending_bps = { if (game.selected < 0) { push_undo() game.selected = who - game.changed = 0 + game.buildup.changed = 0 } else { - if (!game.changed) + if (!game.buildup.changed) pop_undo() else game.selected = -1 @@ -5192,13 +5481,13 @@ states.spending_bps = { game.state = 'minefield' }, replacement() { - game.changed = 1 + game.buildup.changed = 1 log(`Replaced unit.`) replace_unit(game.selected) pay_bps(replacement_cost(game.selected)) }, refit() { - game.changed = 1 + game.buildup.changed = 1 log(`Returned for Refit.`) hide_unit(game.selected) set_unit_hex(pop_selected(), friendly_refit()) @@ -5218,7 +5507,7 @@ states.spending_bps = { return } - game.changed = 1 + game.buildup.changed = 1 let who = game.selected let from = unit_hex(who) if (to === from) { @@ -5250,7 +5539,6 @@ states.spending_bps = { } }, end_buildup() { - delete game.changed game.selected = -1 clear_undo() @@ -5379,7 +5667,7 @@ function goto_buildup_resupply() { deal_axis_supply_cards(axis_resupply) deal_allied_supply_cards(allied_resupply) - delete game.buildup + game.buildup = null goto_player_initiative() } @@ -5554,7 +5842,7 @@ states.free_deployment = { done = false }) if (done) - gen_action_next() + gen_action('end_deployment') if (game.selected.length > 0) { trace_total = 0 @@ -5589,7 +5877,7 @@ states.free_deployment = { set_unit_hex(who, to) } }, - next() { + end_deployment() { clear_undo() end_free_deployment() } @@ -5628,23 +5916,23 @@ states.initial_supply_cards = { gen_action('keep') }, discard() { - push_undo() // XXX if (is_axis_player()) { - log(`Axis discarded their hand.`) + let n = current_scenario().axis_initial_supply + log(`Axis discarded ${n} cards.`) game.axis_hand[REAL] = 0 game.axis_hand[DUMMY] = 0 - deal_axis_supply_cards(current_scenario().axis_initial_supply) + deal_axis_supply_cards(n) set_enemy_player() } else { - log(`Allied discarded their hand.`) + let n = current_scenario().allied_initial_supply + log(`Allied discarded ${n} cards.`) game.allied_hand[REAL] = 0 game.allied_hand[DUMMY] = 0 - deal_allied_supply_cards(current_scenario().allied_initial_supply) + deal_allied_supply_cards(n) begin_game() } }, keep() { - push_undo() // XXX if (is_axis_player()) set_enemy_player() else @@ -6170,6 +6458,10 @@ exports.setup = function (seed, scenario, options) { 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: [], @@ -6208,6 +6500,15 @@ exports.setup = function (seed, scenario, options) { battle: 0, hits: null, flash: null, + + // misc states + disrupt: null, + withdraw: null, + hexside: null, + buildup: null, + + // logging + summary: null, }) setup(scenario) @@ -6448,7 +6749,7 @@ function pop_undo() { } function clear_undo() { - // game.undo = [] + game.undo = [] } function log_br() { @@ -6525,7 +6826,7 @@ exports.resign = function (state, current) { exports.action = function (state, current, action, arg) { load_state(state) - // Object.seal(game) // XXX: don't allow adding properties + Object.seal(game) // XXX: don't allow adding properties let S = states[game.state] if (S && action in S) { S[action](arg, current) |