summaryrefslogtreecommitdiff
path: root/rules.js
diff options
context:
space:
mode:
authorTor Andersson <tor@ccxvii.net>2022-07-04 16:41:26 +0200
committerTor Andersson <tor@ccxvii.net>2022-11-17 13:11:25 +0100
commit689061a44d01813f2cd9faf0299321d0a1d6498a (patch)
treecb6de7cf3ffd3c1b0c02f2f8b4154ae2240cb094 /rules.js
parente8a5f5410a0e876d889a2a8137c34bb925f65408 (diff)
downloadrommel-in-the-desert-689061a44d01813f2cd9faf0299321d0a1d6498a.tar.gz
Moves.
Diffstat (limited to 'rules.js')
-rw-r--r--rules.js964
1 files changed, 787 insertions, 177 deletions
diff --git a/rules.js b/rules.js
index 4cbce82..bc6c618 100644
--- a/rules.js
+++ b/rules.js
@@ -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