From 6a75c9e0faae42b52fd8cffdfa7755bcf4d1271a Mon Sep 17 00:00:00 2001 From: Tor Andersson Date: Wed, 6 Dec 2023 15:08:46 +0100 Subject: Rewrite supply tracing code. Two passes. First a quick breadth first forward scan to find hexes that can be reached from the supply source at all. Second an exhaustive recursive search from each unit to find the actual used supply lines and hexes. This uses the results from the first search to quickly prune impossible paths. This also fixes a bug where some supply chains from highways via mixed highway, track, and trail to another bit of highway were missed. --- rules.js | 403 +++++++++++++++++++++++++++++++++++++++++---------------------- 1 file changed, 266 insertions(+), 137 deletions(-) diff --git a/rules.js b/rules.js index fcb889a..308bd53 100644 --- a/rules.js +++ b/rules.js @@ -11,10 +11,15 @@ const max = Math.max const min = Math.min const abs = Math.abs -const TIMEOUT = 500 +const TIMEOUT = 250 var timeout = 0 +function check_timeout() { + if (Date.now() > timeout) + throw new Error("timeout (too many withdrawal options)") +} + var states = {} var game = null var view = null @@ -120,6 +125,8 @@ const INFANTRY = 1 const ANTITANK = 2 const ARTILLERY = 3 +const TRAIL = 1 +const TRACK = 2 const HIGHWAY = 4 const SUPPLY_RANGE = [ 1, 2, 3, 0, 3 ] @@ -1133,151 +1140,273 @@ function shuffle_cards() { // === SUPPLY NETWORK === -var supply_defender, supply_defender_sides, supply_friendly, supply_enemy, supply_net, supply_line +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_src = new Array(hexcount).fill(0) +// 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 - // undisrupted (only) enemies may block supply lines - if (supply_enemy[next] > 1) { - if (supply_friendly[next] > 1) { - // into battle hex - // can only trace into if defender - if (!set_has(supply_defender, next)) - return true - // can only trace into through friendly sides - if (!set_has(supply_defender_sides, side)) - return true - } else { - // into enemy hex + // 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 - // can only cannot trace out through friendly sides - if (supply_friendly[here] > 0 && supply_enemy[here] > 0) { - if (set_has(game.axis_hexes, here) || set_has(game.allied_hexes, here)) - if (!set_has(supply_defender_sides, side)) - return true + 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 } -function check_timeout() { - if (Date.now() > timeout) - throw new Error("timeout (supply lines are too complicated)") -} +const TRACE_HIGHWAY = 0 +const TRACE_TRACK_1 = 1 +const TRACE_TRACK_2 = 2 +const TRACE_TRAIL_1 = 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) +{ + supply_net[start] = 1 -function trace_supply_highway(here, v) { - if (supply_src[here]) - return true + supply_visited.fill(8) - check_timeout() + let queue = [ (start << 3) | TRACE_HIGHWAY ] - let has_supply = false + while (queue.length > 0) { + let item = queue.shift() + let here = item >> 3 + let here_move = item & 7 - let save_v = supply_visited[here] - supply_visited[here] = v + for (let s = 0; s < 6; ++s) { + let next = here + hexnext[s] - 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 - 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_v = supply_visited[next] - if (next_v === v || (next_v > 0 && supply_friendly[next] > 1)) - continue + let next_move = forward_supply_next_move_type(here_move, side_road[side]) + if (next_move === TRACE_ABORT) + continue - let side = to_side(here, next, s) - if (is_supply_line_blocked(here, next, side)) - continue + if (side_road[side] === HIGHWAY) + supply_line[side] = 1 + else + supply_line[side] = 1 - let road = side_road[side] - if (road === HIGHWAY) { - if (supply_friendly[next] > 1) { - if (supply_net[next] || trace_supply_chain(next, 0, 3, v+1)) { - supply_line[side] = 1 - has_supply = true - } - } else { - if (trace_supply_highway(next, v)) { - supply_line[side] = 1 - has_supply = true - } - } + // already seen + 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 (side_road[side] === HIGHWAY) + supply_net[next] = 1 + else + supply_net[next] = 1 + + if (next_move !== TRACE_STOP) + queue.push((next << 3) | next_move) } } +} - supply_visited[here] = save_v +// PASS 2: Filter net to only include supply lines used by units. +// This is a recursive search to find _all_ possible paths. - if (has_supply) - supply_net[here] = 1 +const BACK_HEAD = 0 +const BACK_STOP = 7 +const BACK_ABORT = 8 - return has_supply -} +const BACK_HIGHWAY_1 = 1 +const BACK_HIGHWAY_2 = 2 +const BACK_HIGHWAY_3 = 3 -function trace_supply_chain(here, n, range, v) { - if (supply_src[here]) - return true +const BACK_TRACK_1 = 4 +const BACK_TRACK_2 = 5 +const BACK_TRACK_3 = BACK_STOP - check_timeout() +const BACK_TRAIL_1 = 6 +const BACK_TRAIL_2 = BACK_STOP +const BACK_TRAIL_3 = BACK_ABORT - let has_supply = false +// 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 +} - let save_v = supply_visited[here] - supply_visited[here] = v +function trace_unit_supply_line(path, here, here_move, ssrc) { + supply_visited[here] = 1 + let supplied = false for (let s = 0; s < 6; ++s) { let next = here + hexnext[s] - if (next < first_hex || next > last_hex || !hex_exists[next]) + + // can't go off-map + if (next < first_hex || next > last_hex) + continue + + // already seen + if (supply_visited[next]) continue - let next_v = supply_visited[next] - if (next_v === v || (next_v > 0 && supply_friendly[next] > 1)) + // 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 (is_supply_line_blocked(here, next, side)) + if (!supply_line[side]) continue - let road = side_road[side] - if (road === HIGHWAY) { - if (trace_supply_highway(next, v)) { - supply_line[side] = 1 - has_supply = true - } - } + let next_move = backward_supply_next_move_type(here_move, side_road[side]) + if (next_move === TRACE_ABORT) + continue - let next_range = min(range, SUPPLY_RANGE[road]) - if (n + 1 <= next_range) { - if (supply_friendly[next] > 1) { - if (supply_net[next] || trace_supply_chain(next, 0, 3, v+1)) { - supply_line[side] = 1 - has_supply = true - } - } else { - if (trace_supply_chain(next, n + 1, next_range, v)) { - supply_line[side] = 1 - has_supply = true - } - } + // reached supply source, or reached supplied highway (that we searched before) + /* if (next === ssrc) -- fallback without short cut in case the optimization below is wrong... */ + if (next === ssrc || (hex_road[next] === HIGHWAY && supply_net[next] === 2)) { + for (let x of path) + supply_line[x] = 2 + supply_line[side] = 2 + supply_net[next] = 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 + + path.push(side) + if (trace_unit_supply_line(path, next, next_move, ssrc)) + supplied = true + path.pop() } + supply_visited[here] = 0 - supply_visited[here] = save_v + if (supplied) + supply_net[here] = 2 - if (has_supply) - supply_net[here] = 1 + return supplied +} - return has_supply +// 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 all_hexes_sorted_by_distance_to_base(x) { @@ -1292,47 +1421,55 @@ const all_hexes_for_axis_supply = all_hexes_sorted_by_distance_to_base(EL_AGHEI const all_hexes_for_allied_supply = all_hexes_sorted_by_distance_to_base(ALEXANDRIA) function trace_supply_network(start) { + check_timeout() + let search_order = (start === EL_AGHEILA ? all_hexes_for_axis_supply : all_hexes_for_allied_supply) - supply_visited.fill(0) - supply_src.fill(0) supply_net.fill(0) supply_line.fill(0) - supply_src[start] = 1 - supply_net[start] = 1 + // Pass 1: Find hexes that are in supply range. - for (let x of search_order) { - if (supply_friendly[x] > 0 && x !== start) { - if (supply_friendly[x] > 1 && supply_net[x]) - continue - trace_supply_chain(x, 0, 3, 1) - } - } + trace_supply_network_1(start) + + // Pass 2: Filter hexes that have actual supply lines from units. + supply_visited.fill(0) + let path = [] + for (let x of search_order) + if (supply_friendly[x] > 0 && x !== start) + trace_unit_supply_line(path, x, BACK_HEAD, start) + + trace_supply_network_3() + supply_net[start] = 1 } function trace_fortress_network(fortress, ss) { - supply_visited.fill(0) - supply_src.fill(0) + check_timeout() + supply_net.fill(0) supply_line.fill(0) if (supply_enemy[fortress] > 1 && supply_friendly[fortress] <= 1) return - supply_src[fortress] = 1 - supply_net[fortress] = 1 + // 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_visited.fill(0) + let path = [] 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) { - if (supply_friendly[x] > 1 && supply_net[x]) - continue - trace_supply_chain(x, 0, 3, 1) + //if (supply_friendly[x] > 1 && supply_net[x]) continue + trace_unit_supply_line(path, x, BACK_HEAD, fortress) } } } + + trace_supply_network_3() + supply_net[fortress] = 1 } function init_trace_supply(net, line, friendly) { @@ -1341,31 +1478,28 @@ function init_trace_supply(net, line, friendly) { supply_net = net supply_line = line if (friendly === AXIS) { - supply_defender = game.axis_hexes - supply_defender_sides = game.axis_sides + supply_friendly_hexes = game.axis_hexes + supply_friendly_sides = game.axis_sides supply_friendly = presence_axis supply_enemy = presence_allied } else { - supply_defender = game.allied_hexes - supply_defender_sides = game.allied_sides + supply_friendly_hexes = game.allied_hexes + supply_friendly_sides = game.allied_sides supply_friendly = presence_allied supply_enemy = presence_axis } } -// For repeated supplied hex checks during deployment and fortress assignment -function init_trace_supply_to_base_or_fortress() { +// 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(base, from) { - if (from === base) - return true - supply_visited.fill(0) - supply_src.fill(0) - supply_src[base] = 1 - return trace_supply_chain(from, 0, 3, 1, 1) +function can_trace_supply_to_base_or_fortress(from) { + return supply_net[from] > 0 } function update_axis_supply() { @@ -2662,10 +2796,10 @@ function end_fortress_supply() { function list_fortress_supply_candidates(fortress) { let dist = distance_to[fortress] let list = [] - init_trace_supply_to_base_or_fortress() + 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(fortress, unit_hex(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)]) @@ -6690,11 +6824,8 @@ function goto_free_deployment() { game.summary = {} } -function is_valid_deployment_hex(base, x, n) { - // we've already seen this hex during a previous supplied hex check this go - if (supply_temp_network[x] > 0) - return true - if (can_trace_supply_to_base_or_fortress(base, x)) +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 @@ -6722,8 +6853,7 @@ states.free_deployment = { gen_action('end_deployment') if (game.selected.length > 0) { - let base = friendly_base() - init_trace_supply_to_base_or_fortress() + trace_supply_to_base_or_fortress(friendly_base()) for (let x of (axis ? scenario.axis_deployment : scenario.allied_deployment)) { if (!is_enemy_hex(x)) { @@ -6731,7 +6861,7 @@ states.free_deployment = { if (scenario.deployment_limit) limit = scenario.deployment_limit[x] | 0 if (!limit || count_friendly_units_in_hex(x) + game.selected.length <= limit) { - if (is_valid_deployment_hex(base, x, game.selected.length)) + if (is_valid_deployment_hex(x, game.selected.length)) gen_action_hex(x) } } @@ -7578,7 +7708,6 @@ exports.resign = function (state, current) { exports.action = function (state, current, action, arg) { timeout = Date.now() + TIMEOUT // don't think too long! load_state(state) - // Object.seal(game) // XXX: don't allow adding properties let S = states[game.state] if (S && action in S) { S[action](arg, current) -- cgit v1.2.3