diff options
Diffstat (limited to 'rules.js')
-rw-r--r-- | rules.js | 964 |
1 files changed, 787 insertions, 177 deletions
@@ -6,30 +6,33 @@ const max = Math.max const min = Math.min const abs = Math.abs +var states = {} +var game = null +var view = null + +let { hex_exists, hex_road, side_road, side_limit, hex_name, units, regions } = require("./data") + function debug_hexes3(n, list) { console.log("--", n, "--") + list = list.map((x,i) => hex_exists[i] ? x : "") for (let y = 0; y < hexh; ++y) console.log("".padStart(y*2," ") + list.slice(y*hexw, (y+1)*hexw).map(x=>String(x).padStart(3, ' ')).join(" ")) } function debug_hexes2(n, list) { console.log("--", n, "--") + list = list.map((x,i) => hex_exists[i] ? x : "") for (let y = 0; y < hexh; ++y) console.log("".padStart(y*2," ") + list.slice(y*hexw, (y+1)*hexw).map(x=>String(x).padStart(3, ' ')).join(" ")) } function debug_hexes(n, list) { console.log("--", n, "--") + list = list.map((x,i) => hex_exists[i] ? x : "") for (let y = 0; y < hexh; ++y) - console.log("".padStart(y," ") + list.slice(y*hexw, (y+1)*hexw).join("")) + console.log("".padStart(y," ") + list.slice(y*hexw, (y+1)*hexw).map(x=>String(x).padStart(2, ' ')).join("")) } -var states = {} -var game = null -var view = null - -let { hex_road, side_road, side_limit, hex_name, units, regions } = require("./data") - // Card deck has 42 cards, of which 28 are supply cards, and 14 are dummy cards. // Represent draw pile and hands as [ dummy_supply_count, real_supply_count ] const REAL_SUPPLY_COUNT = 28 @@ -38,13 +41,14 @@ const DUMMY_SUPPLY_COUNT = 14 const hexw = 25 const hexh = 9 -const hexcount = hexw * hexh -const sidecount = hexcount * 3 +const first_hex = 7 +const last_hex = 215 + const hexdeploy = hexw * hexh const hexnext = [ 1, hexw, hexw-1, -1, -hexw, -(hexw-1) ] -const first_hex = 7 -const last_hex = 215 +const hexcount = last_hex + 1 +const sidecount = hexcount * 3 const AXIS = 'Axis' const ALLIED = 'Allied' @@ -92,7 +96,6 @@ function calc_distance_map(supply) { let map = new Array(hexcount) for (let x = 0; x < hexcount; ++x) map[x] = calc_distance(supply, x) - debug_hexes2(hex_name[supply], map) return map } @@ -132,10 +135,26 @@ function find_unit(name) { throw new Error("cannot find named block: " + name) } +function is_map_hex(x) { + return next >= first_hex && next <= last_hex && hex_exists[next] === 1 +} + +function is_hex_or_adjacent_to(x, where) { + if (x === where) return true + for (let s = 0; s < 6; ++s) + if (x === where + hexnext[s]) + return true + return false +} + function find_units(list) { return list.map(name => find_unit(name)) } +function unit_name(u) { + return units[u].name +} + function unit_speed(u) { return units[u].speed } @@ -233,6 +252,8 @@ function is_axis_unit(u) { } function is_axis_hex(x) { + if (!hex_exists[x]) + return false let has_axis = false, has_allied = false for (let u = 0; u < units.length; ++u) { if (unit_hex(u) === x) { @@ -246,6 +267,8 @@ function is_axis_hex(x) { } function is_allied_hex(x) { + if (!hex_exists[x]) + return false let has_axis = false, has_allied = false for (let u = 0; u < units.length; ++u) { if (unit_hex(u) === x) { @@ -259,6 +282,8 @@ function is_allied_hex(x) { } function is_battle_hex(x) { + if (!hex_exists[x]) + return false let has_axis = false, has_allied = false for (let u = 0; u < units.length; ++u) { if (unit_hex(u) === x) { @@ -272,6 +297,8 @@ function is_battle_hex(x) { } function is_empty_hex(x) { + if (!hex_exists[x]) + return false let has_axis = false, has_allied = false for (let u = 0; u < units.length; ++u) { if (unit_hex(u) === x) { @@ -285,6 +312,8 @@ function is_empty_hex(x) { } function has_axis_unit(x) { + if (!hex_exists[x]) + return false for (let u = 0; u < units.length; ++u) if (unit_hex(u) === x) if (is_axis_unit(u)) @@ -293,6 +322,8 @@ function has_axis_unit(x) { } function has_allied_unit(x) { + if (!hex_exists[x]) + return false for (let u = 0; u < units.length; ++u) if (unit_hex(u) === x) if (is_allied_unit(u)) @@ -312,6 +343,70 @@ function has_enemy_unit(x) { return has_allied_unit(x) } +function claim_hexside_control(side) { + if (game.active === AXIS) { + set_add(game.axis_sides, side) + set_delete(game.allied_sides, side) + } else { + set_add(game.allied_sides, side) + set_delete(game.axis_sides, side) + } +} + +function release_hex_control(a) { + // no longer a battle hex: release hexsides if possible + set_delete(game.axis_hexes, a) + set_delete(game.allied_hexes, a) + for (let s = 0; s < 6; ++s) { + let b = a + hexnext[s] + if (b >= first_hex && b <= last_hex && hex_exists[b]) { + if (!is_battle_hex(b)) { + let side = to_side_id(a, b) + set_delete(game.axis_sides, side) + set_delete(game.allied_sides, side) + } + } + } +} + +function is_new_battle_hex(a) { + if (is_battle_hex(a)) + return !set_has(game.axis_hexes) && !set_has(game.allied_hexes) + return false +} + +function claim_hex_control_for_defender(a) { + // a new battle hex: claim hex and hexsides for defender + + if (game.active === AXIS) + set_add(game.allied_hexes, a) + else + set_add(game.axis_hexes, a) + + for (let s = 0; s < 6; ++s) { + let b = a + hexnext[s] + if (b >= first_hex && b <= last_hex && hex_exists[b]) { + let side = to_side_id(a, b) + if (side_limit[side] > 0) { + if (game.active === AXIS) { + if (!set_has(game.axis_sides, side)) + set_add(game.allied_sides, side) + } else { + if (!set_has(game.allied_sides, side)) + set_add(game.axis_sides, side) + } + } + } + } +} + +function claim_stuff() { + for (let x = first_hex; x <= last_hex; ++x) + if (hex_exists[x]) + if (is_new_battle_hex(x)) + claim_hex_control_for_defender(x) +} + // === SUPPLY NETWORK === function is_side_unit(side, u) { @@ -338,6 +433,21 @@ function to_side(a, b, s) { return b * 3 + s - 3 } +function to_side_id(a, b) { + if (a > b) { + let c = b + b = a + a = c + } + if (a + hexnext[0] === b) + return a * 3 + 0 + else if (a + hexnext[1] === b) + return a * 3 + 1 + else if (a + hexnext[2] === b) + return a * 3 + 2 + throw new Error("not a hexside " + a + " to " + b); +} + function ind(d, msg, here, ...extra) { console.log(new Array(d).fill("-").join("") + msg, here, "("+hex_name[here]+")", extra.join(" ")) } @@ -347,8 +457,6 @@ var trace_total var trace_highway var trace_chain - - function trace_supply_highway(here, d) { trace_highway++ ind(d, "> highway", here) @@ -528,6 +636,219 @@ function update_supply_networks() { update_allied_supply_network() } +function clear_supply_networks() { + game.axis_supply = null + game.axis_supply_line = null + game.allied_supply = null + game.allied_supply_line = null +} + +// === MOVEMENT === + +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) +const path_enemy = new Array(hexcount) + +function search_move(start, start_cost, start_road) { + // recon=4, forced march=+1, rommel bonus=+1 + let limit = 6 + + path_enemy.fill(0) + for (let u = 0; u < units.length; ++u) { + if (is_enemy_unit(u)) { + let x = unit_hex(u) + if (x >= first_hex && x <= last_hex) + path_enemy[x] = 1 + } + } + + search_move_bfs(path_from[0], path_cost[0], start, start_cost, 0, limit) + if (start_road >= 1) + search_move_bfs(path_from[1], path_cost[1], start, start_cost, 1, limit + 1) + if (start_road >= 2) + search_move_bfs(path_from[2], path_cost[2], start, start_cost, 2, limit + 2) + if (start_road >= 4) + search_move_bfs(path_from[4], path_cost[4], start, start_cost, 4, limit + 4) + + let grid = new Array(hexcount).fill('-') + for (let x = first_hex; x <= last_hex; ++x) { + for (let speed = 4; speed >= 1; --speed) { + if (path_cost[0][x] <= speed) + grid[x] = speed + if (path_cost[1][x] <= speed + 1) + grid[x] = speed + if (path_cost[2][x] <= speed + 2) + grid[x] = speed + if (path_cost[4][x] <= speed + 4) + grid[x] = speed + } + } + grid[start] = '@' + + debug_hexes2("reach", path_cost[0]) +} + +// Breadth First Search +function search_move_bfs(from, cost, start, start_cost, road, max_cost) { + let queue = [ start << 4 | start_cost ] + + from.fill(0) + cost.fill(15) + cost[start] = start_cost + + while (queue.length > 0) { + let item = queue.shift() + let here = item >> 4 + let here_cost = item & 15 + let next_cost = here_cost + 1 + + for (let s = 0; s < 6; ++s) { + let next = here + hexnext[s] + + // can't go off-map + if (next < first_hex || next > last_hex || !hex_exists[next]) + continue + + // already seen + if (cost[next] < 15) + continue + + let side = to_side(here, next, s) + let max_side = side_limit[side] + + // can't cross this hexside + if (max_side === 0) + continue + + // must stay on road for current bonus + if (side_road[side] < road) + continue + + // check hexside limit + if (path_enemy[next] && (game.side_limit[side] | 0) >= max_side) + continue + + from[next] = here + cost[next] = next_cost + + // must stop + if (path_enemy[next]) + continue + + // enough movement allowance to keep going + if (next_cost < max_cost) + queue.push(next << 4 | next_cost) + } + } +} + +function can_move_to(to, road, speed) { + // TODO: engagement & hexside limit + if (road >= 4 && path_cost[4][to] <= speed + 4) + return true + if (road >= 2 && path_cost[2][to] <= speed + 2) + return true + if (road >= 1 && path_cost[1][to] <= speed + 1) + return true + if (path_cost[0][to] <= speed) + return true + return false +} + +function can_move_from(from, road, speed) { + // TODO: engagement & hexside limit + return can_move_to(from, road, speed) +} + +function can_force_march_to(to, road, speed) { + if (road >= 4 && path_cost[4][to] <= speed + 5) + return true + if (road >= 2 && path_cost[2][to] <= speed + 3) + return true + if (road >= 1 && path_cost[1][to] <= speed + 2) + return true + if (path_cost[0][to] <= speed + 1) + return true + return false +} + +function pick_path(to, road, speed) { + let next_cost = 15, next_road = 0 + if (path_cost[0][to] <= speed) { + next_cost = path_cost[0][to] + next_road = 0 + } + if (road >= 1 && path_cost[1][to] <= speed + 1) { + if (path_cost[1][to] <= next_cost) { + next_cost = path_cost[1][to] + next_road = 1 + } + } + if (road >= 2 && path_cost[2][to] <= speed + 2) { + if (path_cost[2][to] <= next_cost) { + next_cost = path_cost[2][to] + next_road = 2 + } + } + if (road >= 4 && path_cost[4][to] <= speed + 4) { + if (path_cost[4][to] <= next_cost) { + next_cost = path_cost[4][to] + next_road = 4 + } + } + return next_road +} + +function pay_movement_cost(to, this_road, speed) { +} + +function neighbor_has_friendly_unit(here) { + for (let s = 0; s < 6; ++s) { + let next = here + hexnext[s] + if (next >= first_hex && next <= last_hex) + if (has_friendly_unit(next)) + return true + } + return false +} + +function for_each_hex_and_adjacent_hex(here, fn) { + fn(here) + for (let s = 0; s < 6; ++s) { + let next = here + hexnext[s] + if (next >= first_hex && next <= last_hex && hex_exists[next]) + fn(next) + } +} + +function for_each_friendly_unit_in_hex(x, fn) { + for (let u = 0; u < units.length; ++u) + if (is_friendly_unit(u) && unit_hex(u) === x) + fn(u) +} + +function max_speed_of_friendly_unit_in_hex(from) { + let max = 0 + for_each_friendly_unit_in_hex(from, u => { + let s = unit_speed(u) + if (s > max) + max = s + }) + return max +} + +function find_valid_regroup_destinations(from, rommel) { + let speed = max_speed_of_friendly_unit_in_hex(from) + if (speed > 0) { + search_move(from, 0, 4) + for (let x = first_hex; x <= last_hex; ++x) + if (!path_valid[x]) + if (can_move_to(x, 4, speed + rommel)) + path_valid[x] = 1 + } +} + // === TURN === // Supply check @@ -551,8 +872,8 @@ function end_player_turn() { function goto_player_turn() { game.rommel = 0 - game.group_moves = [] - game.regroup_moves = [] + game.from1 = game.from2 = 0 + game.to1 = game.to2 = 0 goto_supply_check() } @@ -603,12 +924,24 @@ states.turn_option = { function goto_move_phase() { game.state = 'select_moves' + if (game.active === AXIS) { + // Automatically select Rommel Move for 1-move turn options + if (game.turn_option !== 'offensive' && game.turn_option !== 'blitz') + game.rommel = 1 + } } states.select_moves = { inactive: "move phase", prompt() { - view.prompt = `Make Moves (${game.turn_option})` + if (game.turn_option === 'offensive') { + if (game.from1) + view.prompt = `Designate second offensive move.` + else + view.prompt = `Designate first offensive move.` + } else { + view.prompt = `Designate ${game.turn_option} move.` + } gen_action('group') gen_action('regroup') }, @@ -618,232 +951,418 @@ states.select_moves = { }, regroup() { push_undo() - game.state = 'regroup_move' + game.state = 'regroup_move_command_point' }, } +function gen_rommel_move() { + if (game.active === AXIS) + view.actions.rommel = game.rommel ? 0 : 1 +} + states.group_move_from = { inactive: "group move (from)", prompt() { view.prompt = `Group Move: Select hex to move from.` - for (let x = first_hex; x <= last_hex; ++x) + gen_rommel_move() + for (let x = first_hex; x <= last_hex; ++x) { + if (x === game.from1 && !game.to1) + continue if (has_friendly_unit(x)) gen_action_hex(x) - if (game.active === AXIS && !game.rommel) - gen_action('rommel') + } }, rommel() { push_undo() - game.rommel = 1 + if (game.from1 === 0) + game.rommel = 1 + else + game.rommel = 2 }, hex(x) { push_undo() - game.group_moves.push(x) - game.state = 'group_move_who' + if (game.from1 === 0) + game.from1 = x + else + game.from2 = x + if (game.turn_option === 'offensive' && !game.from2) + game.state = 'select_moves' + else + goto_move_who() }, } -states.group_move_who = { - inactive: "group move (who)", +states.regroup_move_command_point = { + inactive: "regroup move (command point)", prompt() { - view.prompt = `Group Move: Select unit to move.` - for (let i = 0; i < game.group_moves.length; ++i) { - let from = game.group_moves[i] - for (let u = 0; u < units.length; ++u) { - if (unit_hex(u) === from && !is_unit_moved(u)) - gen_action_unit(u) + view.prompt = `Regroup Move: Designate the command point hex.` + gen_rommel_move() + for (let x = first_hex; x <= last_hex; ++x) { + if (!is_enemy_hex(x)) { + if (has_friendly_unit(x) || neighbor_has_friendly_unit(x)) + gen_action_hex(x) } } - gen_action('end_move') }, - next() { - // TODO: end move + rommel() { push_undo() - game.state = 'select_moves' + if (game.from1 === 0) + game.rommel = 1 + else + game.rommel = 2 }, - unit(u) { + hex(x) { push_undo() - game.selected = [ u ] - game.state = 'group_move_to' - game.move_used = 0 - game.move_road = 4 // HIGHWAY + if (game.from1 === 0) + game.from1 = x + else + game.from2 = x + game.state = 'regroup_move_destination' }, } -states.group_move_to = { - inactive: "group move (to)", +states.regroup_move_destination = { + inactive: "regroup move (destination)", prompt() { - view.prompt = `Group Move: Select where to move.` - let u = game.selected[0] - let from = unit_hex(u) - if (game.move_used > 0) - gen_action('stop') - - var t0, t1 - t0 = Date.now() - //for (let i = 0; i < 100000; ++i) - search_path_move_dfs(u, from, game.move_used, game.move_road) - t1 = Date.now() - console.log("DFS", (t1 - t0) / 1000) - - var a = path_from.toString() - - t0 = Date.now() - //for (let i = 0; i < 100000; ++i) - search_path_move_ucs(u, from, game.move_used, game.move_road) - t1 = Date.now() - console.log("DFS", (t1 - t0) / 1000) - - var b = path_from.toString() - if (a !== b) { - console.log(a) - console.log(b) - } - + view.prompt = `Regroup Move: Select destination hex.` + gen_rommel_move() + let cp, rommel = false + if (game.from2 === 0) + cp = game.from1, rommel = (game.rommel === 1 ? 1 : 0) + 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 = first_hex; x <= last_hex; ++x) - if (path_from[x] > 0) + if (path_valid[x]) gen_action_hex(x) - - view.path_from = path_from + }, + rommel() { + push_undo() + if (game.from2 === 0) + game.rommel = 1 + else + game.rommel = 2 }, hex(x) { push_undo() - let u = game.selected[0] - set_unit_hex(u, x) + if (game.from2 === 0) + game.to1 = x + else + game.to2 = x + if (game.turn_option === 'offensive' && !game.from2) + game.state = 'select_moves' + else + goto_move_who() }, - stop() { +} + +function goto_move_who() { + if (game.rommel === 1) { + if (game.from1 && game.to1) + log(`Regroup move from ${game.from1} to ${game.to1} (Rommel).`) + else if (game.from1) + log(`Group move from ${game.from1} (Rommel).`) + } else { + if (game.from1 && game.to1) + log(`Regroup move from ${game.from1} to ${game.to1}.`) + else if (game.from1) + log(`Group move from ${game.from1}.`) + } + if (game.rommel === 2) { + if (game.from2 && game.to2) + log(`Regroup move from ${game.from2} to ${game.to2} (Rommel).`) + else if (game.from2) + log(`Group move from ${game.from2} (Rommel).`) + } else { + if (game.from2 && game.to2) + log(`Regroup move from ${game.from2} to ${game.to2}.`) + else if (game.from2) + log(`Group move from ${game.from2}.`) + } + game.state = 'move_who' +} + +function gen_group_move_who(from) { + for_each_friendly_unit_in_hex(from, u => { + if (!is_unit_moved(u)) + gen_action_unit(u) + }) +} + +function gen_regroup_move_who(command_point, destination, rommel) { + search_move(destination, 0, 4) + for_each_hex_and_adjacent_hex(command_point, x => { + if (x !== destination) { + for_each_friendly_unit_in_hex(x, u => { + if (!is_unit_moved(u) && can_move_from(x, 4, unit_speed(u) + rommel)) + gen_action_unit(u) + }) + } + }) +} + +states.move_who = { + inactive: "move (who)", + prompt() { + view.prompt = `Move: Select unit to move.` + if (game.from1) { + if (game.to1) + gen_regroup_move_who(game.from1, game.to1, game.rommel === 1 ? 1 : 0) + else + gen_group_move_who(game.from1) + } + if (game.from2) { + if (game.to2) + gen_regroup_move_who(game.from2, game.to2, game.rommel === 2 ? 1 : 0) + else + gen_group_move_who(game.from2) + } + // TODO: retreat + gen_action('retreat') + gen_action('end_move') + }, + unit(who) { push_undo() - let u = game.selected[0] - set_unit_moved(u) - game.selected = [] - game.state = 'group_move_who' + game.selected = [ who ] + game.state = 'move_to' + game.move_used = 0 + game.move_road = 4 + }, + end_move() { + clear_supply_networks() } } -var path_cost = new Array(hexcount) -var path_from = new Array(hexcount) +function rommel_group_move_bonus(from) { + if (game.rommel === 1 && from === game.from1 && !game.to1) + return 1 + if (game.rommel === 2 && from === game.from2 && !game.to2) + return 1 + return 0 +} -function pq_push(queue, hex, used, road) { - for (let i = 0, n = queue.length; i < n; ++i) - if (queue[i][1] > used) - return queue.splice(i, 0, [hex, used, road, hex_name[hex]]) - queue.push([hex, used, road, hex_name[hex]]) +function print_path(who, from, to, road) { + let p = [ hex_name[to] ] + while (to && to !== from) { + to = path_from[road][to] + p.unshift(hex_name[to]) + } + log(unit_name(who) + " moved " + p.join(", ") + ".") } -// Uniform Cost Search -function search_path_move_ucs(who, start, start_used, start_road) { - let speed = unit_speed(who) + HIGHWAY - path_cost.fill(100) - path_from.fill(0) +function print_pathX(who, start, to, road) { + let from = path_from[road][to] + log(`M ${hex_name[from]} to ${hex_name[to]}`) + while (from && from !== start) { + to = from + from = path_from[road][from] + log(`M ${hex_name[from]} to ${hex_name[to]}`) + } +} - let queue = [] - pq_push(queue, start, start_used, start_road) - path_cost[start] = start_used +function apply_move(move, who, from, to) { + let speed = unit_speed(who) + (move === game.rommel ? 1 : 0) + let road = pick_path(to, game.move_road, speed) - let n = 0 - while (queue.length > 0) { - //console.log(queue) - let [ here, here_used, here_road ] = queue.shift() - ++n + print_path(who, unit_hex(who), to, road) - // already seen this hex from a shorter path - if (path_cost[here] < here_used) - continue + game.move_road = road + game.move_used = path_cost[road][to] - for (let s = 0; s < 6; ++s) { - let next = here + hexnext[s] + set_unit_moved(who) + set_unit_hex(who, to) - // can't go off-map - if (next < first_hex || next > last_hex) - continue + pay_movement_cost(to, game.move_road, speed) - let side = to_side(here, next, s) + if (is_battle_hex(to)) { + let side = to_side_id(to, path_from[road][to]) - // can't cross this hexside - if (side_limit[side] === 0) - continue + log(`cross ${side} ${hex_name[to]}/${hex_name[path_from[road][to]]}`) - let next_road = min(here_road, side_road[side]) - let road_cost = here_road - next_road - let next_used = here_used + road_cost + 1 + if (game.side_limit[side]) + game.side_limit[side] = 2 + else + game.side_limit[side] = 1 - // not enough movement allowance to reach - if (next_used > speed) - continue + claim_hexside_control(side) + if (is_new_battle_hex(to)) { + claim_hex_control_for_defender(to) + game.battles.push(to) + } + return true + } - // a shorter path has already been found - if (next_used >= path_cost[next]) - continue + if (game.move_used === speed + game.move_road) + return true - ind(2, "path", next, next_used, next_road) + return false +} - path_from[next] = here - path_cost[next] = next_used +function unit_speed_1(who) { + return unit_speed(who) + (game.rommel === 1 ? 1 : 0) +} - // must stop - if (has_enemy_unit(next)) - continue +function unit_speed_2(who) { + return unit_speed(who) + (game.rommel === 2 ? 1 : 0) +} - pq_push(queue, next, next_used, next_road) - } - } +function can_move_regroup_1(who, from, to) { + if (to === game.to1 && is_hex_or_adjacent_to(from, game.from1)) + if (can_move_to(game.to1, 4, unit_speed_1(who))) + return true + return false +} - console.log("UCS VISITED", n) +function can_move_regroup_2(who, from, to) { + if (to === game.to2 && is_hex_or_adjacent_to(from, game.from2)) + if (can_move_to(game.to2, 4, unit_speed_2(who))) + return true + return false } -var search_n -function search_path_move_dfs(who, start, start_used, start_road) { - path_cost.fill(100) - path_from.fill(0) +function can_move_group_1(who, from, to) { + if (from === game.from1 && !game.to1) + if (can_move_to(to, game.move_road, unit_speed_1(who))) + return true + return false +} - ind(0, "?path", start, start_used, unit_speed(who)) - search_n = 1 - path_cost[start] = 0 - path_from[start] = 0 - search_path_move_rec(unit_speed(who) + HIGHWAY, start, start_used, start_road, 2) - console.log("DFS VISITED", search_n) +function can_move_group_2(who, from, to) { + if (from === game.from2 && !game.to2) + if (can_move_to(to, game.move_road, unit_speed_2(who))) + return true + return false } -function search_path_move_rec(speed, here, here_used, here_road, d) { - ++search_n - for (let s = 0; s < 6; ++s) { - let next = here + hexnext[s] +states.move_to = { + inactive: "move (to)", + prompt() { + view.prompt = `Move: Select where to move.` + let who = game.selected[0] + let from = unit_hex(who) - // can't go off-map - if (next < first_hex || next > last_hex) - continue + search_move(from, 0, 4) - let side = to_side(here, next, s) + if (from === game.from1 && !game.to1) + for (let to = first_hex; to <= last_hex; ++to) + if (to != from && hex_exists[to] && can_move_group_1(who, from, to)) + gen_action_hex(to) - // can't cross this hexside - if (side_limit[side] === 0) - continue + if (from === game.from2 && !game.to2) + for (let to = first_hex; to <= last_hex; ++to) + if (to != from && hex_exists[to] && can_move_group_2(who, from, to)) + gen_action_hex(to) - let next_road = min(here_road, side_road[side]) - let road_cost = here_road - next_road - let next_used = here_used + road_cost + 1 + if (can_move_regroup_1(who, from, game.to1)) + gen_action_hex(game.to1) - // not enough movement allowance to reach - if (next_used > speed) - continue + if (can_move_regroup_2(who, from, game.to2)) + gen_action_hex(game.to2) + }, + hex(to) { + push_undo() + let who = game.selected[0] + let from = unit_hex(who) - // a shorter path has already been found - if (next_used >= path_cost[next]) - continue + search_move(from, 0, 4) - ind(d, "path", next, next_used, speed) + if (can_move_group_1(who, from, to)) { + log(`group moved ${who} to ${to}`) + game.move_from = from + if (apply_move(1, who, from, to)) + stop_move(who) + else + game.state = 'group_move_to' + return + } - path_from[next] = here - path_cost[next] = next_used + if (can_move_group_2(who, from, to)) { + log(`group moved ${who} to ${to}`) + game.move_from = from + if (apply_move(2, who, from, to)) + stop_move(who) + else + game.state = 'group_move_to' + return + } - // must stop - if (has_enemy_unit(next)) - continue + if (can_move_regroup_1(who, from, to)) { + log(`regrouped ${who} to ${to}`) + apply_move(1, who, from, to) + stop_move(who) + game.state = 'move_who' + return + } - search_path_move_rec(speed, next, next_used, next_road, d+1) + if (can_move_regroup_2(who, from, to)) { + log(`regrouped ${who} to ${to}`) + apply_move(2, who, from, to) + stop_move(who) + game.state = 'move_who' + return + } + }, +} + +states.group_move_to = { + inactive: "group move (to)", + prompt() { + view.prompt = `Group Move: Select where to move.` + let who = game.selected[0] + let from = unit_hex(who) + if (game.move_used > 0) + gen_action('stop') + + search_move(from, game.move_used, game.move_road) + + if (game.move_from === game.from1 && !game.to1) + for (let to = first_hex; to <= last_hex; ++to) + if (to != from && hex_exists[to] && can_move_group_1(who, game.move_from, to)) + gen_action_hex(to) + + if (game.move_from === game.from2 && !game.to2) + for (let to = first_hex; to <= last_hex; ++to) + if (to != from && hex_exists[to] && can_move_group_2(who, game.move_from, to)) + gen_action_hex(to) + }, + hex(to) { + let who = game.selected[0] + let from = unit_hex(who) + + search_move(from, game.move_used, game.move_road) + + if (can_move_group_1(who, game.move_from, to)) { + log(`group continued ${who} to ${to}`) + if (apply_move(1, who, from, to)) + stop_move(who) + return + } + + if (can_move_group_2(who, game.move_from, to)) { + log(`group continued ${who} to ${to}`) + if (apply_move(2, who, from, to)) + stop_move(who) + return + } + }, + stop() { + let who = game.selected[0] + stop_move(who) } } +function stop_move(who) { + set_unit_moved(who) + game.move_from = 0 + game.move_road = 4 + game.move_used = 0 + game.selected = [] + game.state = 'move_who' +} + // === DEPLOYMENT === states.free_deployment = { @@ -898,6 +1417,7 @@ states.free_deployment = { game.selected.length = 0 }, next() { + clear_undo() if (game.active === AXIS) game.active = ALLIED else @@ -1358,12 +1878,14 @@ exports.setup = function (seed, scenario, options) { log: [], undo: [], + state: null, phasing: AXIS, active: AXIS, selected: null, scenario: scenario, month: 0, + first_player_turn: AXIS, draw_pile: [ DUMMY_SUPPLY_COUNT, REAL_SUPPLY_COUNT ], axis_hand: [ 0, 0 ], @@ -1374,7 +1896,34 @@ exports.setup = function (seed, scenario, options) { axis_minefields: [], allied_minefields: [], - first_player_turn: AXIS, + // supply networks + axis_supply: null, + axis_supply_line: null, + allied_supply: null, + allied_supply_line: null, + + // battle hexes (defender) + axis_hexes: [], + allied_hexes: [], + + // hexside control (for battle hexes) + axis_sides: [], + allied_sides: [], + + // current turn option and moves + turn_option: null, + side_limit: {}, + battles: [], + rommel: 0, + from1: 0, + to1: 0, + from2: 0, + to2: 0, + + // current group move state + move_from: 0, + move_used: 0, + move_road: 4, } setup(scenario) @@ -1385,18 +1934,28 @@ exports.setup = function (seed, scenario, options) { exports.view = function(state, current) { game = state - //update_supply_networks() + // update_supply_networks() view = { month: game.month, units: game.units, + axis_hexes: game.axis_hexes, + allied_hexes: game.allied_hexes, + axis_sides: game.axis_sides, + allied_sides: game.allied_sides, selected: game.selected, - axis_supply: game.axis_supply, - axis_supply_line: game.axis_supply_line, - allied_supply: game.allied_supply, - allied_supply_line: game.allied_supply_line, + // axis_supply: game.axis_supply, + // axis_supply_line: game.axis_supply_line, + // allied_supply: game.allied_supply, + // allied_supply_line: game.allied_supply_line, } + if (game.rommel) view.rommel = game.rommel + if (game.from1) view.from1 = game.from1 + if (game.from2) view.from2 = game.from2 + if (game.to1) view.to1 = game.to1 + if (game.to2) view.to2 = game.to2 + return common_view(current) } @@ -1416,7 +1975,7 @@ function gen_action_hex(x) { function random(n) { // https://www.ams.org/journals/mcom/1999-68-225/S0025-5718-99-00996-5/S0025-5718-99-00996-5.pdf - return (game.seed = game.seed * 185852 % 34359738337) % n + return (game.seed = game.seed * 200105 % 34359738337) % n } function shuffle(deck) { @@ -1428,6 +1987,53 @@ function shuffle(deck) { } } +// Sorted array treated as Set (for JSON) +function set_index(set, item) { + let a = 0 + let b = set.length - 1 + while (a <= b) { + let m = (a + b) >> 1 + let x = set[m] + if (item < x) + b = m - 1 + else if (item > x) + a = m + 1 + else + return m + } + return -1 +} + +function set_has(set, item) { + return set_index(set, item) >= 0 +} + +function set_add(set, item) { + let a = 0 + let b = set.length - 1 + while (a <= b) { + let m = (a + b) >> 1 + let x = set[m] + if (item < x) + b = m - 1 + else if (item > x) + a = m + 1 + else + return + } + set.splice(a, 0, item) +} + +function set_delete(set, item) { + let i = set_index(set, item) + if (i >= 0) + set.splice(i, 1) +} + +function set_clear(set) { + set.length = 0 +} + function remove_from_array(array, item) { let i = array.indexOf(item) if (i >= 0) @@ -1539,8 +2145,9 @@ exports.resign = function (state, current) { exports.action = function (state, current, action, arg) { game = state + // Object.seal(game) // XXX: don't allow adding properties let S = states[game.state] - if (action in S) { + if (S && action in S) { S[action](arg, current) } else { if (action === 'undo' && game.undo && game.undo.length > 0) @@ -1558,7 +2165,10 @@ function common_view(current) { view.prompt = `Waiting for ${game.active} \u2014 ${inactive}...` } else { view.actions = {} - states[game.state].prompt() + if (states[game.state]) + states[game.state].prompt() + else + view.prompt = "Unknown state: " + game.state if (game.undo && game.undo.length > 0) view.actions.undo = 1 else |