"use strict" // unit state: location (8 bits), steps (2 bits), supplied, disrupted, moved const max = Math.max const min = Math.min const abs = Math.abs 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 } 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) { // 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 } } function remove_from_array(array, item) { let i = array.indexOf(item) if (i >= 0) array.splice(i, 1) } 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 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 = [] } function logbr() { if (game.log.length > 0 && game.log[game.log.length-1] !== "") game.log.push("") } function log(msg) { game.log.push(msg) } function gen_action(action, argument) { if (argument !== undefined) { if (!(action in view.actions)) { view.actions[action] = [ argument ] } else { if (!view.actions[action].includes(argument)) view.actions[action].push(argument) } } else { view.actions[action] = 1 } } function goto_game_over(result, victory) { game.state = 'game_over' game.active = "None" game.result = result game.victory = victory logbr() log(game.victory) } 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 let S = states[game.state] if (action in S) { S[action](arg, current) } else { if (action === 'undo' && game.undo && game.undo.length > 0) pop_undo() else throw new Error("Invalid action: " + action) } return game } function common_view(current) { view.log = game.log if (current === 'Observer' || game.active !== current) { let inactive = states[game.state].inactive || game.state view.prompt = `Waiting for ${game.active} \u2014 ${inactive}...` } else { view.actions = {} states[game.state].prompt() if (game.undo && game.undo.length > 0) view.actions.undo = 1 else view.actions.undo = 0 } return view }