summaryrefslogtreecommitdiff
path: root/rules.js
diff options
context:
space:
mode:
Diffstat (limited to 'rules.js')
-rw-r--r--rules.js1592
1 files changed, 1511 insertions, 81 deletions
diff --git a/rules.js b/rules.js
index 142e0c4..4cbce82 100644
--- a/rules.js
+++ b/rules.js
@@ -1,138 +1,1568 @@
-"use strict";
+"use strict"
-let game = null;
-let view = null;
+// unit state: location (8 bits), steps (2 bits), supplied, disrupted, moved
-exports.roles = [ "Axis", "Allied" ];
+const max = Math.max
+const min = Math.min
+const abs = Math.abs
-exports.scenarios = [
- "1940",
- "1941",
- "1941-42",
- "Crusader",
- "Battleaxe",
- "1942",
- "Gazala",
- "Pursuit to Alamein",
-];
+function debug_hexes3(n, list) {
+ console.log("--", n, "--")
+ 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, "--")
+ 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, "--")
+ for (let y = 0; y < hexh; ++y)
+ console.log("".padStart(y," ") + list.slice(y*hexw, (y+1)*hexw).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
+const DUMMY_SUPPLY_COUNT = 14
+
+const hexw = 25
+const hexh = 9
+
+const hexcount = hexw * hexh
+const sidecount = hexcount * 3
+const hexdeploy = hexw * hexh
+const hexnext = [ 1, hexw, hexw-1, -1, -hexw, -(hexw-1) ]
+
+const first_hex = 7
+const last_hex = 215
+
+const AXIS = 'Axis'
+const ALLIED = 'Allied'
+
+const CLEAR = 2
+const PASS = 1
+const ROUGH = 0
+
+const NO_ROAD = 0
+const TRAIL = 1
+const TRACK = 2
+const HIGHWAY = 4
+
+const SUPPLY_RANGE = [ 1, 2, 3, -1, 3 ]
+
+const EL_AGHEILA = 151
+const ALEXANDRIA = 74
+const BENGHAZI = 54
+const TOBRUK = 37
+const BARDIA = 40
+const FT_CAPUZZO = 64
+const MERSA_BREGA = 152
+const BARDIA_FT_CAPUZZO = 122
+
+const region_egypt = regions["Egypt"]
+const region_el_agheila = regions["El Agheila"]
+const region_tobruk = regions["Tobruk"]
+const region_egypt_and_libya = regions["Libya"].concat(regions["Egypt"])
+const region_libya_and_sidi_omar = regions["Libya"].concat(regions["Sidi Omar"])
+const region_libya_and_sidi_omar_and_sollum = regions["Libya"].concat(regions["Sidi Omar"]).concat(regions["Sollum"])
+const region_egypt_and_tobruk = regions["Egypt"].concat(regions["Tobruk"])
+const region_libya_except_tobruk = regions["Libya"].filter(r => r !== TOBRUK)
+
+const region_all = []
+for (let x = first_hex; x <= last_hex; ++x)
+ region_all.push(x)
+
+function calc_distance(a, b) {
+ let ax = a % hexw, ay = (a / hexw)|0, az = -ax - ay
+ let bx = b % hexw, by = (b / hexw)|0, bz = -bx - by
+ return max(abs(bx-ax), abs(by-ay), abs(bz-az))
+}
+
+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
+}
+
+const distance_to = {
+ [EL_AGHEILA]: calc_distance_map(EL_AGHEILA),
+ [ALEXANDRIA]: calc_distance_map(ALEXANDRIA),
+ [BENGHAZI]: calc_distance_map(BENGHAZI),
+ [TOBRUK]: calc_distance_map(TOBRUK),
+ [BARDIA]: calc_distance_map(BARDIA),
+}
+
+function draw_supply_card(pile) {
+ let x = random(pile[0] + pile[1])
+ if (x < pile[0]) {
+ pile[0] --
+ return 0
+ } else {
+ pile[1] --
+ return 1
+ }
+}
+
+function deal_axis_supply_cards(n) {
+ for (let i = 0; i < n; ++i)
+ game.axis_hand[draw_supply_card(game.draw_pile)]++
+}
+
+function deal_allied_supply_cards(n) {
+ for (let i = 0; i < n; ++i)
+ game.allied_hand[draw_supply_card(game.draw_pile)]++
+}
+
+function find_unit(name) {
+ for (let u = 0; u < units.length; ++u)
+ if (units[u].name === name)
+ return u
+ throw new Error("cannot find named block: " + name)
+}
+
+function find_units(list) {
+ return list.map(name => find_unit(name))
+}
+
+function unit_speed(u) {
+ return units[u].speed
+}
+
+function unit_hex(u) {
+ return game.units[u] >>> 5
+}
+
+function set_unit_hex(u, x) {
+ game.units[u] = (game.units[u] & 31) | (x << 5)
+}
+
+function unit_lost_steps(u) {
+ return game.units[u] & 3
+}
+
+function set_unit_lost_steps(u, n) {
+ game.units[u] = (game.units[u] & ~3) | n
+}
-exports.ready = function (scenario, options, players) {
- return players.length === 2;
+function is_unit_supplied(u) {
+ return (game.units[u] & 4) === 4
}
+function set_unit_supplied(u) {
+ game.units[u] |= 4
+}
+
+function clear_unit_supplied(u) {
+ game.units[u] &= ~4
+}
+
+function is_unit_disrupted(u) {
+ return (game.units[u] & 8) === 8
+}
+
+function set_unit_disrupted(u) {
+ game.units[u] |= 8
+}
+
+function clear_unit_disrupted(u) {
+ game.units[u] &= ~8
+}
+
+function is_unit_moved(u) {
+ return (game.units[u] & 16) === 16
+}
+
+function set_unit_moved(u) {
+ game.units[u] |= 16
+}
+
+function clear_unit_moved(u) {
+ game.units[u] &= ~16
+}
+
+function unit_steps(u) {
+ return units[u].steps - unit_lost_steps(u)
+}
+
+function set_unit_steps(u, n) {
+ set_unit_lost_steps(u, units[u].steps - n)
+}
+
+function is_friendly_unit(u) {
+ if (game.active === AXIS)
+ return is_axis_unit(u)
+ return is_allied_unit(u)
+}
+
+function is_enemy_unit(u) {
+ if (game.active === ALLIED)
+ return is_axis_unit(u)
+ return is_allied_unit(u)
+}
+
+function is_friendly_hex(x) {
+ if (game.active === AXIS)
+ return is_axis_hex(x)
+ return is_allied_hex(x)
+}
+
+function is_enemy_hex(x) {
+ if (game.active === ALLIED)
+ return is_axis_hex(x)
+ return is_allied_hex(x)
+}
+
+function is_allied_unit(u) {
+ return units[u].nationality === 'allied'
+}
+
+function is_axis_unit(u) {
+ return units[u].nationality !== 'allied'
+}
+
+function is_axis_hex(x) {
+ let has_axis = false, has_allied = false
+ for (let u = 0; u < units.length; ++u) {
+ if (unit_hex(u) === x) {
+ if (is_axis_unit(u))
+ has_axis = true
+ else
+ has_allied = true
+ }
+ }
+ return has_axis && !has_allied
+}
+
+function is_allied_hex(x) {
+ let has_axis = false, has_allied = false
+ for (let u = 0; u < units.length; ++u) {
+ if (unit_hex(u) === x) {
+ if (is_axis_unit(u))
+ has_axis = true
+ else
+ has_allied = true
+ }
+ }
+ return !has_axis && has_allied
+}
+
+function is_battle_hex(x) {
+ let has_axis = false, has_allied = false
+ for (let u = 0; u < units.length; ++u) {
+ if (unit_hex(u) === x) {
+ if (is_axis_unit(u))
+ has_axis = true
+ else
+ has_allied = true
+ }
+ }
+ return has_axis && has_allied
+}
+
+function is_empty_hex(x) {
+ let has_axis = false, has_allied = false
+ for (let u = 0; u < units.length; ++u) {
+ if (unit_hex(u) === x) {
+ if (is_axis_unit(u))
+ has_axis = true
+ else
+ has_allied = true
+ }
+ }
+ return !has_axis && !has_allied
+}
+
+function has_axis_unit(x) {
+ for (let u = 0; u < units.length; ++u)
+ if (unit_hex(u) === x)
+ if (is_axis_unit(u))
+ return true
+ return false
+}
+
+function has_allied_unit(x) {
+ for (let u = 0; u < units.length; ++u)
+ if (unit_hex(u) === x)
+ if (is_allied_unit(u))
+ return true
+ return false
+}
+
+function has_friendly_unit(x) {
+ if (game.active === AXIS)
+ return has_axis_unit(x)
+ return has_allied_unit(x)
+}
+
+function has_enemy_unit(x) {
+ if (game.active === ALLIED)
+ return has_axis_unit(x)
+ return has_allied_unit(x)
+}
+
+// === SUPPLY NETWORK ===
+
+function is_side_unit(side, u) {
+ if (side === AXIS)
+ return is_axis_unit(u)
+ return is_allied_unit(u)
+}
+
+function list_control_hexes(side) {
+ let control = new Array(hexcount).fill(0)
+ for (let u = 0; u < units.length; ++u) {
+ if (is_side_unit(side, u)) {
+ let x = unit_hex(u)
+ if (x > 0 && x < hexdeploy)
+ control[x] = 1
+ }
+ }
+ return control
+}
+
+function to_side(a, b, s) {
+ if (s < 3)
+ return a * 3 + s
+ return b * 3 + s - 3
+}
+
+function ind(d, msg, here, ...extra) {
+ console.log(new Array(d).fill("-").join("") + msg, here, "("+hex_name[here]+")", extra.join(" "))
+}
+
+var supply_visited, supply_friendly, supply_enemy, supply_src, supply_net, supply_line
+var trace_total
+var trace_highway
+var trace_chain
+
+
+
+function trace_supply_highway(here, d) {
+ trace_highway++
+ ind(d, "> highway", here)
+
+ // TODO: hoist to call sites to avoid function call overhead
+ if (supply_src[here]) {
+ ind(d, "! source highway", here)
+ return true
+ }
+
+ let has_supply = false
+
+ supply_visited[here] = true
+
+ for (let s = 0; s < 6; ++s) {
+ let next = here + hexnext[s]
+ let side = to_side(here, next, s)
+
+ if (side_limit[side] === 0)
+ continue
+ if (supply_visited[next])
+ continue
+ if (supply_enemy[next])
+ continue
+
+ let road = side_road[side]
+ if (road === HIGHWAY) {
+ if (supply_friendly[next]) {
+ ind(d, "? highway head", next)
+ if (trace_supply_chain(next, d+1, 0, 3)) {
+ ind(d, "< highway chain", here, next)
+ supply_line[side] = 1
+ has_supply = true
+ }
+ } else {
+ if (trace_supply_highway(next, d+1)) {
+ ind(d, "< highway", here, next)
+ supply_line[side] = 1
+ has_supply = true
+ }
+ }
+ }
+ }
+
+ supply_visited[here] = false
+
+ if (has_supply) {
+ supply_net[here] = 1
+ supply_src[here] = 1
+ }
+
+ return has_supply
+}
+
+function trace_supply_chain(here, d, n, range) {
+ ind(d, "> chain", here, n, range)
+ trace_chain++
+
+ if (supply_src[here]) {
+ ind(d, "! source chain", here)
+ return true
+ }
+
+ let has_supply = false
+
+ supply_visited[here] = true
+
+ for (let s = 0; s < 6; ++s) {
+ let next = here + hexnext[s]
+ let side = to_side(here, next, s)
+
+ if (side_limit[side] === 0)
+ continue
+ if (supply_visited[next])
+ continue
+ if (supply_enemy[next])
+ continue
+
+ let road = side_road[side]
+
+ if (road === HIGHWAY) {
+ ind(d, "? chain highway", next)
+ if (supply_friendly[next]) {
+ ind(d, "? chain highway head", next)
+ if (trace_supply_chain(next, d+1, 0, 3)) {
+ ind(d, "< highway chain", here, next)
+ supply_line[side] = 1
+ has_supply = true
+ }
+ } else {
+ if (trace_supply_highway(next, d+1)) {
+ ind(d, "< chain highway", here, next)
+ supply_line[side] = 1
+ has_supply = true
+ }
+ }
+ } else {
+ let next_range = min(range, SUPPLY_RANGE[road])
+ if (n + 1 <= next_range) {
+ if (supply_friendly[next]) {
+ ind(d, "? chain head", next)
+ if (trace_supply_chain(next, d+1, 0, 3)) {
+ ind(d, "< highway chain", here, next)
+ supply_line[side] = 1
+ has_supply = true
+ }
+ } else {
+ if (trace_supply_chain(next, d+1, n+1, next_range)) {
+ ind(d, "< chain trail", here, next_range)
+ supply_line[side] = 1
+ has_supply = true
+ }
+ }
+ }
+ }
+ }
+
+ supply_visited[here] = false
+
+ if (has_supply) {
+ supply_net[here] = 1
+ if (supply_friendly[here])
+ supply_src[here] = 1
+ }
+
+ return has_supply
+}
+
+function trace_supply_network(start) {
+ supply_visited = new Array(hexcount).fill(false)
+ supply_net = new Array(hexcount).fill(0)
+ supply_line = new Array(sidecount).fill(0)
+ supply_src = new Array(hexcount).fill(0)
+
+ supply_src[start] = 1
+ supply_net[start] = 1
+
+ console.log("=== SUPPLY NETWORK ===")
+ // debug_hexes("FH", supply_friendly)
+ // debug_hexes("EH", supply_enemy)
+ // debug_hexes("SS1", supply_src)
+
+ trace_total = 0
+ for (let x = first_hex; x <= last_hex; ++x) {
+ if (supply_friendly[x]) {
+ trace_highway = trace_chain = 0
+ ind(0, "START", x)
+ trace_supply_chain(x, 0, 0, 3)
+ console.log("END", trace_highway, trace_chain)
+ trace_total += trace_highway + trace_chain
+ }
+ }
+ console.log("VISITS", trace_total)
+
+ debug_hexes("SS", supply_src)
+ debug_hexes("SN", supply_net)
+}
+
+function update_axis_supply_network() {
+ supply_friendly = list_control_hexes(AXIS)
+ supply_enemy = list_control_hexes(ALLIED)
+ trace_supply_network(EL_AGHEILA)
+ game.axis_supply = supply_net
+ game.axis_supply_line = supply_line
+}
+
+function update_allied_supply_network() {
+ supply_friendly = list_control_hexes(ALLIED)
+ supply_enemy = list_control_hexes(AXIS)
+ trace_supply_network(ALEXANDRIA)
+ game.allied_supply = supply_net
+ game.allied_supply_line = supply_line
+}
+
+function update_supply_networks() {
+ update_axis_supply_network()
+ update_allied_supply_network()
+}
+
+// === TURN ===
+
+// Supply check
+// Turn option
+// Movement
+// Combat
+// Blitz Movement
+// Blitz Combat
+// Final supply check
+// Reveal supply cards -> next player
+
+function end_player_turn() {
+ // TODO: end when both pass
+ if (game.phasing === AXIS)
+ game.phasing = ALLIED
+ else
+ game.phasing = AXIS
+ game.active = game.phasing
+ goto_player_turn()
+}
+
+function goto_player_turn() {
+ game.rommel = 0
+ game.group_moves = []
+ game.regroup_moves = []
+ goto_supply_check()
+}
+
+function goto_supply_check() {
+ goto_turn_option()
+}
+
+function goto_turn_option() {
+ game.state = 'turn_option'
+}
+
+states.turn_option = {
+ inactive: "turn option",
+ prompt() {
+ view.prompt = "Select Turn Option"
+ gen_action('basic')
+ gen_action('offensive')
+ gen_action('assault')
+ gen_action('blitz')
+ gen_action('pass')
+ },
+ basic() {
+ push_undo()
+ game.turn_option = 'basic'
+ goto_move_phase()
+ },
+ offensive() {
+ push_undo()
+ game.turn_option = 'offensive'
+ goto_move_phase()
+ },
+ assault() {
+ push_undo()
+ game.turn_option = 'assault'
+ goto_move_phase()
+ },
+ blitz() {
+ push_undo()
+ game.turn_option = 'blitz'
+ goto_move_phase()
+ },
+ pass() {
+ push_undo()
+ game.turn_option = 'pass'
+ goto_move_phase()
+ },
+}
+
+function goto_move_phase() {
+ game.state = 'select_moves'
+}
+
+states.select_moves = {
+ inactive: "move phase",
+ prompt() {
+ view.prompt = `Make Moves (${game.turn_option})`
+ gen_action('group')
+ gen_action('regroup')
+ },
+ group() {
+ push_undo()
+ game.state = 'group_move_from'
+ },
+ regroup() {
+ push_undo()
+ game.state = 'regroup_move'
+ },
+}
+
+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)
+ if (has_friendly_unit(x))
+ gen_action_hex(x)
+ if (game.active === AXIS && !game.rommel)
+ gen_action('rommel')
+ },
+ rommel() {
+ push_undo()
+ game.rommel = 1
+ },
+ hex(x) {
+ push_undo()
+ game.group_moves.push(x)
+ game.state = 'group_move_who'
+ },
+}
+
+states.group_move_who = {
+ inactive: "group move (who)",
+ 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)
+ }
+ }
+ gen_action('end_move')
+ },
+ next() {
+ // TODO: end move
+ push_undo()
+ game.state = 'select_moves'
+ },
+ unit(u) {
+ push_undo()
+ game.selected = [ u ]
+ game.state = 'group_move_to'
+ game.move_used = 0
+ game.move_road = 4 // HIGHWAY
+ },
+}
+
+states.group_move_to = {
+ inactive: "group move (to)",
+ 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)
+ }
+
+ for (let x = first_hex; x <= last_hex; ++x)
+ if (path_from[x] > 0)
+ gen_action_hex(x)
+
+ view.path_from = path_from
+ },
+ hex(x) {
+ push_undo()
+ let u = game.selected[0]
+ set_unit_hex(u, x)
+ },
+ stop() {
+ push_undo()
+ let u = game.selected[0]
+ set_unit_moved(u)
+ game.selected = []
+ game.state = 'group_move_who'
+ }
+}
+
+var path_cost = new Array(hexcount)
+var path_from = new Array(hexcount)
+
+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]])
+}
+
+// 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)
+
+ let queue = []
+ pq_push(queue, start, start_used, start_road)
+ path_cost[start] = start_used
+
+ let n = 0
+ while (queue.length > 0) {
+ //console.log(queue)
+ let [ here, here_used, here_road ] = queue.shift()
+ ++n
+
+ // already seen this hex from a shorter path
+ if (path_cost[here] < here_used)
+ continue
+
+ for (let s = 0; s < 6; ++s) {
+ let next = here + hexnext[s]
+
+ // can't go off-map
+ if (next < first_hex || next > last_hex)
+ continue
+
+ let side = to_side(here, next, s)
+
+ // can't cross this hexside
+ if (side_limit[side] === 0)
+ continue
+
+ let next_road = min(here_road, side_road[side])
+ let road_cost = here_road - next_road
+ let next_used = here_used + road_cost + 1
+
+ // not enough movement allowance to reach
+ if (next_used > speed)
+ continue
+
+ // a shorter path has already been found
+ if (next_used >= path_cost[next])
+ continue
+
+ ind(2, "path", next, next_used, next_road)
+
+ path_from[next] = here
+ path_cost[next] = next_used
+
+ // must stop
+ if (has_enemy_unit(next))
+ continue
+
+ pq_push(queue, next, next_used, next_road)
+ }
+ }
+
+ console.log("UCS VISITED", n)
+}
+
+var search_n
+function search_path_move_dfs(who, start, start_used, start_road) {
+ path_cost.fill(100)
+ path_from.fill(0)
+
+ 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 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]
+
+ // can't go off-map
+ if (next < first_hex || next > last_hex)
+ continue
+
+ let side = to_side(here, next, s)
+
+ // can't cross this hexside
+ if (side_limit[side] === 0)
+ continue
+
+ let next_road = min(here_road, side_road[side])
+ let road_cost = here_road - next_road
+ let next_used = here_used + road_cost + 1
+
+ // not enough movement allowance to reach
+ if (next_used > speed)
+ continue
+
+ // a shorter path has already been found
+ if (next_used >= path_cost[next])
+ continue
+
+ ind(d, "path", next, next_used, speed)
+
+ path_from[next] = here
+ path_cost[next] = next_used
+
+ // must stop
+ if (has_enemy_unit(next))
+ continue
+
+ search_path_move_rec(speed, next, next_used, next_road, d+1)
+ }
+}
+
+// === DEPLOYMENT ===
+
+states.free_deployment = {
+ inactive: "free deployment",
+ prompt() {
+ let scenario = SCENARIOS[game.scenario]
+ let deploy = hexdeploy + scenario.start
+ let done = true
+
+ if (game.active === AXIS)
+ view.prompt = "Axis Free Deployment"
+ else
+ view.prompt = "Allied Free Deployment"
+
+ // TODO: first/last_axis_unit
+ for (let u = 0; u < units.length; ++u) {
+ if (is_friendly_unit(u)) {
+ if (unit_hex(u) === deploy) {
+ done = false
+ gen_action_unit(u)
+ }
+ }
+ }
+
+ if (game.selected.length > 0) {
+ let list
+ if (game.active === AXIS)
+ list = scenario.axis_deployment
+ else
+ list = scenario.allied_deployment
+ // TODO: scenario.deployment_limit
+ for (let i = 0; i < list.length; ++i)
+ if (!is_enemy_hex(list[i]))
+ gen_action_hex(list[i])
+ }
+
+ if (done)
+ gen_action_next()
+ },
+ unit(u) {
+ if (game.selected.includes(u))
+ remove_from_array(game.selected, u)
+ else
+ game.selected.push(u)
+ },
+ hex(x) {
+ push_undo()
+ for (let i = 0; i < game.selected.length; ++i) {
+ let u = game.selected[i]
+ set_unit_hex(u, x)
+ }
+ game.selected.length = 0
+ },
+ next() {
+ if (game.active === AXIS)
+ game.active = ALLIED
+ else
+ end_free_deployment()
+ }
+}
+
+function end_free_deployment() {
+ game.phasing = game.first_player_turn
+ game.active = game.phasing
+
+ let scenario = SCENARIOS[game.scenario]
+ deal_axis_supply_cards(scenario.axis_initial_supply)
+ deal_allied_supply_cards(scenario.allied_initial_supply)
+
+ // TODO: mulligan
+
+ // No buildup first month
+ // No initiative first month
+ goto_player_turn()
+}
+
+// === SETUP ===
+
+function find_axis_units(a) {
+ let list = []
+ for (let u = 0; u < units.length; ++u)
+ if (units[u].nationality !== 'allied' && units[u].appearance === a)
+ list.push(u)
+ return list
+}
+
+function find_allied_units(a) {
+ let list = []
+ for (let u = 0; u < units.length; ++u)
+ if (units[u].nationality === 'allied' && units[u].appearance === a)
+ list.push(u)
+ return list
+}
+
+function setup_reinforcements(m) {
+ for (let u = 0; u < units.length; ++u) {
+ if (units[u].appearance === m) {
+ if (m === 'M')
+ set_unit_hex(u, 4)
+ else
+ set_unit_hex(u, hexdeploy + (m > 10 ? m - 10 : m))
+ }
+ }
+}
+
+function setup_units(where, steps, list) {
+ if (where < 0)
+ where = hexdeploy - where
+ for (let u of list) {
+ if (typeof u === 'string')
+ u = find_unit(u)
+ set_unit_hex(u, where)
+ if (steps < 0)
+ set_unit_steps(u, units[u].steps + steps)
+ else if (steps > 0)
+ set_unit_steps(u, steps)
+ }
+}
+
+const SCENARIOS = {
+ 1940: {
+ year: 1940,
+ start: 1,
+ end: 6,
+ axis_deployment: region_all, // XXX region_libya_and_sidi_omar,
+ allied_deployment: region_all, // XXX region_egypt,
+ axis_initial_supply: 6,
+ allied_initial_supply: 3,
+ special: {
+ no_rommel_bonus: true,
+ only_one_die_for_buildup: true,
+ }
+ },
+ "1941": {
+ year: 1941,
+ start: 1,
+ end: 10,
+ axis_deployment: [],
+ allied_deployment: region_egypt_and_libya,
+ axis_initial_supply: 6,
+ allied_initial_supply: 6,
+ },
+ "Crusader": {
+ year: 1941,
+ start: 8,
+ end: 10,
+ axis_deployment: region_libya_and_sidi_omar_and_sollum,
+ allied_deployment: region_egypt_and_tobruk,
+ axis_initial_supply: 10,
+ allied_initial_supply: 12,
+ deployment_limit: {
+ [TOBRUK]: 5,
+ },
+ special: {
+ allies_first_turn: true,
+ tobruk_minefield: true,
+ }
+ },
+ "Battleaxe": {
+ year: 1941,
+ start: 4,
+ end: 10,
+ axis_deployment: region_libya_except_tobruk,
+ allied_deployment: region_egypt_and_tobruk,
+ axis_initial_supply: 4,
+ allied_initial_supply: 8,
+ deployment_limit: {
+ [TOBRUK]: 5,
+ },
+ },
+ "1942": {
+ year: 1942,
+ start: 11,
+ end: 20,
+ axis_deployment: [ EL_AGHEILA, MERSA_BREGA ],
+ allied_deployment: region_egypt_and_libya,
+ axis_initial_supply: 5,
+ allied_initial_supply: 5,
+ deployment_limit: {
+ [EL_AGHEILA]: 14,
+ [MERSA_BREGA]: 10,
+ },
+ },
+ "Gazala": {
+ year: 1942,
+ start: 14,
+ end: 15,
+ axis_deployment: regions["West Line"],
+ allied_deployment: regions["East Line"],
+ axis_initial_supply: 10,
+ allied_initial_supply: 12,
+ special: {
+ gazala_pre_build: true,
+ }
+ },
+ "Pursuit to Alamein": {
+ year: 1942,
+ start: 15,
+ end: 20,
+ axis_deployment: regions["Libya"],
+ allied_deployment: regions["Egypt"],
+ axis_initial_supply: 8,
+ allied_initial_supply: 8,
+ },
+ "1941-42": {
+ year: 1941,
+ start: 1,
+ end: 20,
+ axis_deployment: [],
+ allied_deployment: region_egypt_and_libya,
+ axis_initial_supply: 6,
+ allied_initial_supply: 6,
+ },
+}
+
+const SETUP = {
+ "1940" (DEPLOY) {
+ setup_units(DEPLOY, 0, [ "Pav", "Bre", "Tre", "Bol", "Sav", "Sab", "Fas", "Ita" ])
+ setup_units(-3, 0, [ "Ari", "Pis"])
+ setup_units(-5, 0, [ "Lit", "Cen"])
+
+ setup_units(DEPLOY, 0, [ "7/7", "7", "7/SG", "4IN/3m" ])
+ setup_units(-2, 0, [ "4IN/5", "4IN/11" ])
+ setup_units(-4, 0, [ "Matilda/A", "7/4", "4IN/7m", "/Tob" ])
+ },
+
+ "1941" (DEPLOY) {
+ setup_units(EL_AGHEILA, 0, find_axis_units('S'))
+ setup_reinforcements(3)
+ setup_reinforcements(5)
+ setup_reinforcements(7)
+
+ setup_units(DEPLOY, 0, find_allied_units('S'))
+ setup_units(TOBRUK, 0, find_allied_units('T'))
+ setup_reinforcements(2)
+ setup_reinforcements(4)
+ setup_reinforcements(6)
+ setup_reinforcements(8)
+ setup_reinforcements(10)
+ },
+
+ "Crusader" (DEPLOY) {
+ setup_units(DEPLOY, 0, find_axis_units('S'))
+ setup_units(DEPLOY, 0, find_axis_units(3))
+ setup_units(DEPLOY, 0, find_axis_units(5))
+ setup_units(DEPLOY, 0, find_axis_units(7))
+
+ setup_units(DEPLOY, 0, find_allied_units('S'))
+ setup_units(DEPLOY, 0, find_allied_units('T'))
+ setup_units(DEPLOY, 0, find_allied_units(2))
+ setup_units(DEPLOY, 0, find_allied_units(4))
+ setup_units(DEPLOY, 0, find_allied_units(6))
+ setup_units(DEPLOY, 0, find_allied_units(8))
+ setup_units(0, 0, [ "2/3", "2/SG", "9AU/20", "7AU/18" ])
+ setup_reinforcements(10)
+ },
+
+ "Battleaxe" (DEPLOY) {
+ setup_units(DEPLOY, 0, [ "21/5", "21/3", "15/33", "90/155", "15/115", "88mm/A" ])
+ setup_units(DEPLOY, -1, [ "21/104" ])
+ setup_units(DEPLOY, 0, [ "Ari", "Lit", "Pav", "Bre", "Tre", "Bol", "Ita" ])
+ setup_reinforcements(5)
+ setup_reinforcements(7)
+
+ setup_units(DEPLOY, 0, [ "4IN/3m", "9AU/20", "70/14+16", "70/23",
+ "7/22G", "7/SG", "7AU/18", "Matilda/A", "/Tob", "/Pol",
+ "7/7", "4IN/5", "4IN/7m", "4IN/11", "Matilda/B", "/1AT",
+ "7/4", "7" ])
+ setup_reinforcements(6)
+ setup_reinforcements(8)
+ setup_reinforcements(10)
+ },
+
+ "1942" (DEPLOY) {
+ setup_units(DEPLOY, 0, [
+ "21/3",
+ "15/33",
+ "90/580",
+ "90/sv288",
+ "90/346",
+ "88mm/A",
+ ])
+ setup_units(DEPLOY, 1, [
+ "21/5",
+ "15/8",
+ "15/115",
+ "90/155"
+ ])
+ setup_units(DEPLOY, 1, [
+ "90/361",
+ "90/200",
+ "88mm/B",
+ "50mm",
+ "/104"
+ ])
+ setup_units(DEPLOY, 0, [ "Lit" ])
+ setup_units(DEPLOY, -1, [ "Ari", "Tri", "Pav", "Bre", "Tre", "Bol", "Sab", "Ita" ])
+ setup_reinforcements(17)
+ setup_reinforcements(19)
+ setup_reinforcements('M')
+
+ setup_units(TOBRUK, 0, [
+ "1/22",
+ "/1AT",
+ "Matilda/B",
+ "1SA/2+5",
+ "70/14+16",
+ "1SA/1",
+ "1SA/3",
+ "2SA/4+6",
+ "70/23",
+ "/Tob",
+ ])
+ setup_units(ALEXANDRIA, 1, [
+ "7/4",
+ "/32AT",
+ "2NZ/4",
+ "2NZ/5",
+ "2NZ/6",
+ "7/SG",
+ "4IN/5",
+ "/Pol",
+ ])
+ setup_reinforcements(12)
+ setup_reinforcements(14)
+ setup_reinforcements(16)
+ setup_reinforcements(18)
+ setup_reinforcements(20)
+ },
+
+ "Gazala" (DEPLOY) {
+ setup_units(DEPLOY, -1, [
+ "21/5",
+ "15/8",
+ "15/115",
+ "90/155",
+ "88mm/B",
+ "50mm",
+ "/104",
+ ])
+ setup_units(DEPLOY, 0, [
+ "21/3",
+ "15/33",
+ "90/580",
+ "90/sv288",
+ "90/346",
+ "90/361",
+ "90/200",
+ "88mm/A",
+ ])
+ setup_units(DEPLOY, 0, [ "Lit" ])
+ setup_units(DEPLOY, -1, [
+ "Ari", "Tri", "Pav", "Bre", "Tre", "Bol", "Sab", "Ita"
+ ])
+
+ setup_units(DEPLOY, 0, [
+ "Grant",
+ "/1AT",
+ "1/22",
+ "Matilda/B",
+ "1SA",
+ "1/201G",
+ "4IN/7m",
+ "4IN/3m",
+ "1/SG",
+ "1SA/2+5",
+ "70/14+16",
+ "1SA/1",
+ "1SA/3",
+ "2SA/4+6",
+ "70/23",
+ "4IN/11",
+ "5IN/29",
+ "6#/A",
+ "2#",
+ "/Tob",
+ ])
+ setup_units(ALEXANDRIA, 1, [
+ "1/2",
+ "7/4",
+ "/32AT",
+ "2NZ/4",
+ "2NZ/5",
+ "2NZ/6",
+ "7/SG",
+ "7/22G",
+ "4IN/5",
+ "/Pol",
+ ])
+ setup_units(ALEXANDRIA, 0, [
+ "10IN/161m",
+ "8IN/18",
+ "B",
+ "FF/2",
+ "5IN/9+10",
+ "10IN/21+25",
+ ])
+ },
+
+ "Pursuit to Alamein" (DEPLOY) {
+ setup_units(DEPLOY, 0, [
+ "21/3",
+ "15/33",
+ ])
+ setup_units(DEPLOY, -1, [
+ "21/5",
+ "15/8",
+ "15/115",
+ "90/346",
+ "90/sv288",
+ "90/361",
+ "90/200",
+ "88mm/A",
+ "88mm/B",
+ "50mm",
+ ])
+ setup_units(DEPLOY, 3, [
+ "Ari",
+ "Tri",
+ "Pav",
+ "Bre",
+ "Tre",
+ ])
+ setup_units(DEPLOY, 2, [
+ "Lit",
+ "Bol",
+ ])
+ setup_units(DEPLOY, 1, [
+ "Sab",
+ "Ita",
+ ])
+ setup_reinforcements(17)
+ setup_reinforcements(19)
+ setup_reinforcements('M')
+
+ setup_units(DEPLOY, 0, [
+ "10IN/161m",
+ "10IN/21+25",
+ "70/14+16",
+ "8IN/18",
+ "B",
+ ])
+ setup_units(DEPLOY, -1, [
+ "1/22",
+ "7/4",
+ "2NZ/4",
+ "2NZ/5",
+ "2NZ/6",
+ "1SA/2+5",
+ "1SA/1",
+ "1SA/3",
+ "70/23",
+ "5IN/29",
+ "6#/A",
+ ])
+ setup_units(ALEXANDRIA, 1, [
+ "Grant",
+ "1/2",
+ "/1AT",
+ "1SA",
+ "7/SG",
+ "1/SG",
+ "FF/2",
+ "5IN/9+10",
+ "4IN/5",
+ "4IN/11",
+ "/Pol",
+ "2#",
+ ])
+ setup_reinforcements(16)
+ setup_reinforcements(18)
+ setup_reinforcements(20)
+ },
+
+ "1941-42" (DEPLOY) {
+ SETUP["1941"](-1)
+
+ setup_reinforcements(11)
+ setup_reinforcements(17)
+ setup_reinforcements(19)
+ setup_reinforcements('M')
+
+ setup_reinforcements(12)
+ setup_reinforcements(14)
+ setup_reinforcements(16)
+ setup_reinforcements(18)
+ setup_reinforcements(20)
+ },
+
+}
+
+function setup(name) {
+ let scenario = SCENARIOS[name]
+ game.month = scenario.start
+
+ SETUP[name](-scenario.start)
+
+ game.active = 'Axis'
+ game.state = 'free_deployment'
+ game.selected = []
+}
+
+// === PUBLIC FUNCTIONS ===
+
+exports.roles = [ "Axis", "Allied" ]
+
+exports.scenarios = Object.keys(SCENARIOS)
+
+exports.setup = function (seed, scenario, options) {
+ game = {
+ seed: seed,
+ log: [],
+ undo: [],
+
+ phasing: AXIS,
+ active: AXIS,
+ selected: null,
+
+ scenario: scenario,
+ month: 0,
+
+ draw_pile: [ DUMMY_SUPPLY_COUNT, REAL_SUPPLY_COUNT ],
+ axis_hand: [ 0, 0 ],
+ allied_hand: [ 0, 0 ],
+
+ units: new Array(units.length).fill(0),
+
+ axis_minefields: [],
+ allied_minefields: [],
+
+ first_player_turn: AXIS,
+ }
+
+ setup(scenario)
+
+ return game
+}
+
+exports.view = function(state, current) {
+ game = state
+
+ //update_supply_networks()
+
+ view = {
+ month: game.month,
+ units: game.units,
+ 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,
+ }
+
+ return common_view(current)
+}
+
+function gen_action_next() {
+ gen_action('next')
+}
+
+function gen_action_unit(u) {
+ gen_action('unit', u)
+}
+
+function gen_action_hex(x) {
+ gen_action('hex', x)
+}
+
+// === COMMON TEMPLATE ===
+
function random(n) {
- return ((game.seed = game.seed * 69621 % 0x7fffffff) / 0x7fffffff) * n | 0;
+ // 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
}
function shuffle(deck) {
for (let i = deck.length - 1; i > 0; --i) {
- let j = random(i + 1);
- let tmp = deck[j];
- deck[j] = deck[i];
- deck[i] = tmp;
+ let j = random(i + 1)
+ let tmp = deck[j]
+ deck[j] = deck[i]
+ deck[i] = tmp
}
}
function remove_from_array(array, item) {
- let i = array.indexOf(item);
+ let i = array.indexOf(item)
if (i >= 0)
- array.splice(i, 1);
+ array.splice(i, 1)
}
-function logbr() {
- if (game.log.length > 0 && game.log[game.log.length-1] !== "")
- game.log.push("");
+function deep_copy(original) {
+ if (Array.isArray(original)) {
+ let n = original.length
+ let copy = new Array(n)
+ for (let i = 0; i < n; ++i) {
+ let v = original[i]
+ if (typeof v === "object" && v !== null)
+ copy[i] = deep_copy(v)
+ else
+ copy[i] = v
+ }
+ return copy
+ } else {
+ let copy = {}
+ for (let i in original) {
+ let v = original[i]
+ if (typeof v === "object" && v !== null)
+ copy[i] = deep_copy(v)
+ else
+ copy[i] = v
+ }
+ return copy
+ }
}
-function log(msg) {
- game.log.push(msg);
+function push_undo() {
+ let copy = {}
+ for (let k in game) {
+ let v = game[k]
+ if (k === "undo") continue
+ else if (k === "log") v = v.length
+ else if (typeof v === "object" && v !== null) v = deep_copy(v)
+ copy[k] = v
+ }
+ game.undo.push(copy)
+}
+
+function pop_undo() {
+ let save_log = game.log
+ let save_undo = game.undo
+ game = save_undo.pop()
+ save_log.length = game.log
+ game.log = save_log
+ game.undo = save_undo
}
function clear_undo() {
- game.undo = [];
+ game.undo = []
}
-function push_undo() {
- game.undo.push(JSON.stringify(game, (k,v) => {
- if (k === 'undo') return 0;
- if (k === 'log') return v.length;
- return v;
- }));
+function logbr() {
+ if (game.log.length > 0 && game.log[game.log.length-1] !== "")
+ game.log.push("")
}
-function pop_undo() {
- let save_undo = game.undo;
- let save_log = game.log;
- game = JSON.parse(save_undo.pop());
- game.undo = save_undo;
- save_log.length = game.log;
- game.log = save_log;
+function log(msg) {
+ game.log.push(msg)
}
function gen_action(action, argument) {
if (argument !== undefined) {
if (!(action in view.actions)) {
- view.actions[action] = [ argument ];
+ view.actions[action] = [ argument ]
} else {
if (!view.actions[action].includes(argument))
- view.actions[action].push(argument);
+ view.actions[action].push(argument)
}
} else {
- view.actions[action] = 1;
+ view.actions[action] = 1
}
}
-let states = {};
+function goto_game_over(result, victory) {
+ game.state = 'game_over'
+ game.active = "None"
+ game.result = result
+ game.victory = victory
+ logbr()
+ log(game.victory)
+}
-exports.setup = function (seed, scenario, options) {
- game = {
- seed: seed,
- log: [],
- undo: [],
- };
- return game;
+states.game_over = {
+ get inactive() {
+ return game.victory
+ },
+ prompt() {
+ view.prompt = game.victory
+ }
+}
+
+exports.resign = function (state, current) {
+ game = state
+ if (game.state !== 'game_over') {
+ for (let opponent of exports.roles) {
+ if (opponent !== current) {
+ goto_game_over(opponent, current + " resigned.")
+ break
+ }
+ }
+ }
+ return game
}
exports.action = function (state, current, action, arg) {
- game = state;
- update_aliases();
- let S = states[game.state];
+ game = state
+ let S = states[game.state]
if (action in S) {
- S[action](arg, current);
+ S[action](arg, current)
} else {
if (action === 'undo' && game.undo && game.undo.length > 0)
- pop_undo();
+ pop_undo()
else
- throw new Error("Invalid action: " + action);
+ throw new Error("Invalid action: " + action)
}
- return game;
-}
-
-exports.resign = function (state, current) {
- // No resigning allowed.
- return state;
+ return game
}
-exports.view = function(state, current) {
- game = state;
- update_aliases();
-
- view = {
- log: game.log,
- active: game.active,
- prompt: null,
- };
-
+function common_view(current) {
+ view.log = game.log
if (current === 'Observer' || game.active !== current) {
- view.prompt = `Waiting for ${game.active} \u2014 ${game.state}...`;
+ let inactive = states[game.state].inactive || game.state
+ view.prompt = `Waiting for ${game.active} \u2014 ${inactive}...`
} else {
view.actions = {}
- states[game.state].prompt();
+ states[game.state].prompt()
if (game.undo && game.undo.length > 0)
- view.actions.undo = 1;
+ view.actions.undo = 1
else
- view.actions.undo = 0;
+ view.actions.undo = 0
}
-
- return view;
+ return view
}