From e8a5f5410a0e876d889a2a8137c34bb925f65408 Mon Sep 17 00:00:00 2001 From: Tor Andersson Date: Fri, 8 Apr 2022 01:29:54 +0200 Subject: Assets. --- rules.js | 1592 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 1511 insertions(+), 81 deletions(-) (limited to 'rules.js') 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 } -- cgit v1.2.3