summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--play.html9
-rw-r--r--play.js6
-rw-r--r--rules.js603
3 files changed, 465 insertions, 153 deletions
diff --git a/play.html b/play.html
index beffffe..f439c31 100644
--- a/play.html
+++ b/play.html
@@ -27,12 +27,17 @@ header.your_turn { background-color: orange; }
#log .h2 { background-color: gainsboro; padding-top:2px; padding-bottom:2px; text-align: center; }
#log .h3 { background-color: lavender; padding-top:2px; padding-bottom:2px; text-align: center; }
#log > .i { padding-left: 20px; }
+#log > .ii { padding-left: 32px; }
#log > div > .i { padding-left: 12px; }
.action {
cursor: pointer;
}
+#log {
+ font-variant-numeric: tabular-nums;
+}
+
/* CARDS */
.hand {
@@ -201,7 +206,7 @@ header.your_turn { background-color: orange; }
table { border-collapse: collapse; font-size: 12px; user-select: none; }
td.blank { background-color: transparent; border: none }
td,th { border: 1px solid #222; text-align: center; padding: 2px 4px; }
-td { min-width: 20px; }
+td { min-width: 16px; }
th { background-color: #222; color: oldlace; }
td { background-color: oldlace; }
table .required_target { background-color: #b8d9ca }
@@ -352,7 +357,7 @@ svg .hex.to {
}
svg .hex.tip {
- stroke: white;
+ stroke: yellow;
stroke-dasharray: 4 4;
}
diff --git a/play.js b/play.js
index 43ba4bc..f9f2c75 100644
--- a/play.js
+++ b/play.js
@@ -880,6 +880,7 @@ function on_update() {
action_button("end_rout", "End rout")
action_button("end_retreat", "End retreat")
action_button("end_combat", "End combat")
+ action_button("end_deployment", "End deployment")
action_button("end_buildup", "End buildup")
action_button("end_turn", "End turn")
@@ -928,6 +929,11 @@ function on_log_line(text, cn) {
function on_log(text) {
let p = document.createElement("div")
+ if (text.match(/^>>/)) {
+ text = text.substring(2)
+ p.className = "ii"
+ }
+
if (text.match(/^>/)) {
text = text.substring(1)
p.className = "i"
diff --git a/rules.js b/rules.js
index f8e1628..caee7f1 100644
--- a/rules.js
+++ b/rules.js
@@ -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)