diff options
-rw-r--r-- | play.js | 7 | ||||
-rw-r--r-- | rules.js | 350 |
2 files changed, 320 insertions, 37 deletions
@@ -764,7 +764,7 @@ function target_button(action) { function on_update() { if (!ui.loaded) { - document.getElementById("prompt").textContent = "Waiting for map..." + document.getElementById("prompt").textContent = "ERROR" return setTimeout(on_update, 500) } @@ -803,6 +803,11 @@ function on_update() { action_button("retreat", "Retreat") action_button("probe", "Probe") + action_button("replacement", "Replacement") + action_button("refit", "Refit") + action_button("minefield", "Minefield") + action_button("extra_supply_card", "Card") + action_button("group", "Group") action_button("regroup", "Regroup") @@ -1,32 +1,29 @@ "use strict" // TOOD: reveal/hide blocks (hexes) + // TODO: fortress supply // TODO: oasis supply -// TODO: raiders -// TODO: minefields + +// TODO: RAIDERS +// TODO: MINEFIELDS + // TODO: group move from queue holding box to base +// TODO: 1942 malta group (reinforce, reduce supply card draw) // TODO: legal pass withdrawal moves (reduce supply net, withdraw from fortress attack) -// TODO: redeployment -// TODO: return for refit -// TODO: replacements -// TODO: malta units +// TODO: log summaries (deploy, rebuild, move, etc) -// TODO: black hit outline in battles ("steploss/bad" action) and skip "apply 0 hits" step +// RULES: for sea redeployment, can bases be "besieged"? (yes) +// RULES: may units redeploying out of battle hex leave disrupted units behind to be routed? (no) +// RULES: may units redeploying out of battle hex cross enemy controlled hexsides? (yes / doesn't matter) +// RULES: if disrupted units are routed again during their "full enemy turn", can they still recover? // RULES: may oasis supplied units refuse battle or withdraw to base? - -// RULES: may units redeploying out of battles cross enemy controlled hexsides? - -// RULES: return to refit land path -- how/why check? - // RULES: when is "fired" status cleared? +// RULES: are minefields moved through (but not stopped at) revealed? -// RULES: disrupted units routed again in second enemy turn, will they still recover? -// assume yes, easy to change (remove from game.recover set if routed) - -// RULES: reveal minefields moved through (but not stopped at)? +// TODO: black hit outline in battles ("steploss/bad" action) and skip "apply 0 hits" step // ERRATA: forbid single-group regroup moves or convert to group moves after the fact, // to prevent forced march abuse. @@ -427,6 +424,11 @@ function reduce_unit(u) { return hp } +function replace_unit(u) { + let lost = unit_lost_steps(u) + set_unit_lost_steps(u, lost - 1) +} + // === UNIT DATA === function find_unit(name) { @@ -1307,7 +1309,10 @@ function friendly_supply_network() { // === PATHING === +// NOTE: we don't actually need path_from but we can use it to show paths taken in client +// NOTE: we may need the path to reveal minefields moved through 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) @@ -1328,19 +1333,17 @@ function print_path(who, from, to, road) { // TODO: cache search results from previous invocation function search_move(start, speed) { - // Normal moves. - search_init() - search_move_bfs(path_from[0], path_cost[0], start, 0, speed, false, null, null) - search_move_bfs(path_from[1], path_cost[1], start, 1, speed + 1, false, null, null) - search_move_bfs(path_from[2], path_cost[2], start, 2, speed + 2, false, null, null) - search_move_bfs(path_from[4], path_cost[4], start, 4, speed + 4, false, null, null) + 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_move_bfs(path_from[0], path_cost[0], start, 0, speed, true, null, null) - search_move_bfs(path_from[1], path_cost[1], start, 1, speed + 1, true, null, null) - search_move_bfs(path_from[2], path_cost[2], start, 2, speed + 2, true, null, null) - search_move_bfs(path_from[4], path_cost[4], start, 4, speed + 4, true, null, null) + 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(who, bonus) { @@ -1349,10 +1352,10 @@ function search_withdraw(who, bonus) { let speed = unit_speed[who] + bonus let start = unit_hex(who) - search_move_bfs(path_from[0], path_cost[0], start, 0, speed, false, sline, sdist) - search_move_bfs(path_from[1], path_cost[1], start, 1, speed + 1, false, sline, sdist) - search_move_bfs(path_from[2], path_cost[2], start, 2, speed + 2, false, sline, sdist) - search_move_bfs(path_from[4], path_cost[4], start, 4, speed + 4, false, sline, sdist) + 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_retreat(who, bonus) { @@ -1361,17 +1364,21 @@ function search_withdraw_retreat(who, bonus) { let speed = unit_speed[who] + bonus let start = unit_hex(who) - search_move_bfs(path_from[0], path_cost[0], start, 0, speed, true, sline, sdist) - search_move_bfs(path_from[1], path_cost[1], start, 1, speed + 1, true, sline, sdist) - search_move_bfs(path_from[2], path_cost[2], start, 2, speed + 2, true, sline, sdist) - search_move_bfs(path_from[4], path_cost[4], start, 4, speed + 4, true, sline, sdist) + search_path_bfs(path_from[0], path_cost[0], start, 0, speed, true, sline, sdist) + search_path_bfs(path_from[1], path_cost[1], start, 1, speed + 1, true, sline, sdist) + search_path_bfs(path_from[2], path_cost[2], start, 2, speed + 2, true, sline, sdist) + search_path_bfs(path_from[4], path_cost[4], start, 4, speed + 4, true, sline, sdist) } -function search_init() { +function search_redeploy(start) { + search_path_redeploy_bfs(path_cost[0], start, 0, 100) + search_path_redeploy_bfs(path_cost[1], start, 1, 100) + search_path_redeploy_bfs(path_cost[2], start, 2, 100) + search_path_redeploy_bfs(path_cost[4], start, 4, 100) } // Breadth First Search -function search_move_bfs(from, cost, start, road, max_cost, retreat, sline, sdist) { +function search_path_bfs(from, cost, start, road, max_cost, retreat, sline, sdist) { let path_enemy, friendly_sides if (presence_invalid) @@ -1467,6 +1474,75 @@ function search_move_bfs(from, cost, start, road, max_cost, retreat, sline, sdis } } +function search_path_redeploy_bfs(cost, start, road) { + let path_enemy, friendly_network, enemy_network + + 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 + } else { + path_enemy = presence_axis + friendly_network = game.buildup.allied_network + enemy_network = game.buildup.axis_network + } + + 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 + + // 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 + + 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 @@ -4007,6 +4083,8 @@ function apply_reinforcements() { log(`>${early} early`) } +// === BUILDUP - SPENDING BPS === + function goto_buildup_spending() { game.state = 'spending_bps' } @@ -4025,11 +4103,204 @@ function pay_bps(n) { game.allied_bps -= n } +function count_secret_minefields() { + let n = 0 + if (is_axis_player()) { + let network = game.buildup.axis_network + for (let x of game.axis_minefields) + if (network[x]) + ++n + } else { + let network = game.buildup.allied_network + for (let x of game.allied_minefields) + if (network[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 + // TODO: can leave disrupted units behind to be routed? + for_each_undisrupted_friendly_unit_in_hex(from, u => { + n++ + }) + return n > 1 + } + return true +} + +function is_controlled_port(where) { + if (where === BARDIA || where === BENGHAZI || where === TOBRUK) + return is_fortress_friendly_controlled(where) + console.log("not a porT", 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_controlled_port(from) && is_controlled_port(to)) { + let b_from = is_fortress_besieged(from) + let b_to = is_fortress_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) { + if (unit_lost_steps(who) > 0 && bps <= replacement_cost(who)) + view.actions.replacement = 1 + else + view.actions.replacement = 0 + } + + // Quick deselect + gen_action_hex(from) + + if (can_redeploy_from(from)) { + search_redeploy(from) + + // Return for Refit + if (from !== base) { + if (can_move_to(base, 63)) + 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 = { prompt() { view.prompt = `Buildup: Spend buildup points (${available_bps()} remain).` + if (game.selected < 0) { + let bps = available_bps() + if (count_secret_minefields() >= 2 || bps >= 15) + gen_action('minefield') + if (bps >= 10) + gen_action('extra_supply_card') + for_each_friendly_unit_on_map(u => { + if (!is_unit_disrupted(u)) + gen_action_unit(u) + }) + } else { + gen_action_unit(game.selected) + gen_spending_bps() + } gen_action('end_buildup') }, + unit(who) { + if (game.selected < 0) + push_undo() + apply_select(who) + }, + extra_supply_card() { + push_undo() + log(`Purchased extra supply card.`) + pay_bps(10) + }, + minefield() { + push_undo() + game.state = 'minefield' + }, + replacement() { + log(`Replaced unit.`) + replace_unit(game.selected) + pay_bps(replacement_cost(game.selected)) + }, + refit() { + log(`Returned for Refit.`) + set_unit_hex(pop_selected(), friendly_refit()) + }, + hex(to) { + let who = game.selected + let from = unit_hex(who) + if (to === from) { + game.selected = -1 + } else if (to === friendly_refit()) { + log(`Returned for Refit.`) + set_unit_hex(pop_selected(), friendly_refit()) + } else { + search_redeploy(from) + if (can_move_to(to, 2)) { + log(`Redeployed to #${to}.`) + pay_bps(1) + } else { + log(`Sea Redeployed to #${to}.`) + 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) + } + }, end_buildup() { clear_undo() @@ -4053,6 +4324,13 @@ function end_buildup_spending() { } } +states.minefield = { + prompt() { + view.prompt = `Buildup: Build a minefield. (TODO!)` + // Tear down two existing, or pay 15 + }, +} + function goto_buildup_resupply() { log_h2("Resupply") log(`Shuffled supply cards.`) |