diff options
-rw-r--r-- | play.html | 40 | ||||
-rw-r--r-- | play.js | 17 | ||||
-rw-r--r-- | rules.js | 307 |
3 files changed, 250 insertions, 114 deletions
@@ -290,6 +290,10 @@ svg .side.allied_supply.axis_supply { stroke: yellow; } +svg .fortress.axis { + fill: #264; +} + svg .hex.action { stroke: white; stroke-width: 2; @@ -394,12 +398,14 @@ svg .hex.tip { box-shadow: 0 0 0 2px yellow; } -.unit.disrupted { - border-color: crimson; +.unit.moved { + //border-color: black; + //background-color: silver; + filter: grayscale(50%) } -.unit.moved { - border-color: black; +.unit.disrupted { + border-color: crimson; } .unit.unsupplied { @@ -580,12 +586,7 @@ svg .hex.tip { <div class="debug menu_item" onclick="send_restart('1941-42')">⚠ Restart 1941-42</div> </div> </div> - <div class="menu"> - <div class="menu_title"><img src="/images/wooden-sign.svg"></div> - <div class="menu_popup"> - <div class="menu_item" onclick="send_query('supply')">Supply lines</div> - </div> - </div> + <div class="icon_button" onclick="toggle_supply()"><img src="/images/oil-drum.svg"></div> <div class="icon_button" onclick="toggle_units()"><img src="/images/earth-africa-europe.svg"></div> <div class="icon_button" onclick="toggle_zoom()"><img src="/images/magnifying-glass.svg"></div> <div class="icon_button" onclick="toggle_log()"><img src="/images/scroll-quill.svg"></div> @@ -736,12 +737,11 @@ svg .hex.tip { <path d="m 2405,501 10,0 -5,-10 z" /> </g> -<path id="forts" d="M1095.63 181.17l-8.974-15.543 8.974-15.544h17.95l8.973 15.544-8.973 15.543zM169.443 294.884l-8.974-15.543 8.974-15.544h17.95l8.973 15.544-8.973 15.543zm1288.455-72.1l-8.975-15.543 8.975-15.543h17.95l8.972 15.543-8.972 15.543z" - fill="#642" - stroke="#cbae07" - stroke-width="3" - stroke-linejoin="round" - /> +<g id="fortresses" fill="#642" stroke="#cbae07" stroke-width="3" stroke-linejoin="round"> +<path class="fortress" id="fortress_benghazi" d="M170 295l-8.974-15.543 8.974-15.544h17.95l8.973 15.544-8.973 15.543z" /> +<path class="fortress" id="fortress_tobruk" d="M1095 181l-8.974-15.543 8.974-15.544h17.95l8.973 15.544-8.973 15.543z" /> +<path class="fortress" id="fortress_bardia" d="M1458 223l-8.975-15.543 8.975-15.543h17.95l8.972 15.543-8.972 15.543z" /> +</g> <path id="towns" d="M1606.5 422.4c0 4-6 4-6 0s6-4 6 0m80.2 47.6c0 4-6 4-6 0s6-4 6 0m816.6-178.8a3.7 3.7 0 01-3.7 3.7c-5 0-5-7.5 0-7.4a3.7 3.7 0 013.7 3.7M777.4 93c0 4-6 4-6 0s6-4 6 0M1800 585.3c0 4.5-6.8 4.5-6.8 0s6.8-4.5 6.8 0M303.2 250.6c0 4-6 4-6 0s6-4 6 0M813.6 286c0 4-6 4-6 0s6-4 6 0m7-131c0 4-6 4-6 0s6-4 6 0m1227.6 194.2c0 4-6 4-6 0s6-4 6 0M317.8 724.8a4.2 4.2 0 01-8.4 0c0-5.5 8.4-5.5 8.4 0M577 512.3c0 4-6 4-6 0 .1-3.9 5.9-3.9 6 0m-79.1 58.4a3 3 0 01-3 3c-4 0-4-6 0-6a3 3 0 013 3M420.8 279c0 4-6 4-6 0s6-4 6 0m-146 134.6a3 3 0 01-3 3c-4 0-4-6 0-6a3 3 0 013 3M1694 360a4.2 4.2 0 11-8.3 0c0-5.6 8.3-5.6 8.3 0m-257.8-85.6a4.2 4.2 0 01-8.3 0c0-5.5 8.3-5.5 8.3 0m-4.9 102.4a4.2 4.2 0 11-8.3 0c0-5.5 8.2-5.5 8.3 0M145.9 395.7a4.2 4.2 0 11-8.3 0c0-5.5 8.2-5.5 8.3 0M302 530a4.2 4.2 0 11-8.3 0c0-5.5 8.3-5.5 8.3 0m82.3 108.4a4.2 4.2 0 11-8.3 0c0-5.5 8.2-5.5 8.3 0m2031-336.2a4.2 4.2 0 11-8.4 0c0-5.5 8.3-5.5 8.3 0m-2259 380a4.2 4.2 0 11-8.3 0c0-5.6 8.3-5.6 8.3 0m110.4-409.5a4.2 4.2 0 01-8.3 0c0-5.4 8.2-5.4 8.3 0m143.8 137.5a4.2 4.2 0 11-8.4 0c.1-5.4 8.3-5.4 8.4 0M120 809.1c0 4-6 4-6 0s6-4 6 0m2159.4-524.6a3.7 3.7 0 01-3.7 3.7 3.7 3.7 0 01-3.7-3.7c0-5 7.4-5 7.4 0m-160.3 11c0 5-7.3 5-7.4 0 0-4.8 7.4-4.8 7.4 0m-333.3 184.9c0 4-6 4-6 0s6-4 6 0m-544.4-214.7a3.7 3.7 0 01-7.3 0c0-4.9 7.3-4.9 7.3 0m-25-40.4c-.1 3.8-5.8 3.8-6 0 0-4 6-4 6 0m88.4-15.4a3.7 3.7 0 11-7.4 0c.1-4.8 7.3-4.8 7.4 0m711.9 48a6.3 6.3 0 01-12.5 0 6.3 6.3 0 1112.5 0m-266.5 1.7a6.2 6.2 0 11-6.2-6.3 6.3 6.3 0 016.2 6.2m-246.7 23.6a6.3 6.3 0 11-12.5 0 6.3 6.3 0 0112.5 0m108.7 12.5a4.2 4.2 0 11-8.3 0 4.2 4.2 0 018.3 0M1919.8 401c0 4.5-6.7 4.5-6.7 0s6.7-4.5 6.7 0m-530 0c-.1 3.3-5 3.3-5.2 0 0-3.5 5.2-3.5 5.2 0M826.5 117.3a3.7 3.7 0 11-7.5 0c0-5 7.4-5 7.5 0M378 807.6c0 3.5-5.2 3.5-5.2 0s5.2-3.4 5.2 0M1323.7 379a4.2 4.2 0 11-8.3 0c0-5.5 8.2-5.5 8.3 0m111.6 130c0 4-6 4-6 0s6-4 6 0m-1219.8 2.9c0 4-6 4-6 0s6-4 6 0M350 171a6.3 6.3 0 11-12.5 0 6.3 6.3 0 0112.5 0m-70.3 15.4a4.2 4.2 0 11-8.4 0c.1-5.4 8.3-5.4 8.4 0M589.3 71.7A6.3 6.3 0 01583 78a6.3 6.3 0 116.3-6.3M495 165.5a4.2 4.2 0 11-8.4 0c.1-5.5 8.3-5.5 8.4 0m121-40.1a4.2 4.2 0 11-8.3 0c.1-5.5 8.2-5.5 8.3 0m46.2-36a4.2 4.2 0 11-8.4 0c0-5.6 8.4-5.6 8.4 0m20 176.2a4.2 4.2 0 11-8.3 0c0-5.5 8.2-5.5 8.3 0m77-198.1a6.3 6.3 0 11-12.5 0 6.3 6.3 0 0112.5 0m148.3 131a6.3 6.3 0 11-12.5 0 6.3 6.3 0 0112.5 0m42.8 87a4.2 4.2 0 01-8.4 0c.1-5.4 8.3-5.4 8.4 0m61.5-72.4a4.2 4.2 0 01-8.4 0c.1-5.5 8.3-5.5 8.4 0m-1.8 70.6a4.2 4.2 0 11-8.3 0c0-5.5 8.2-5.5 8.3 0m-20.4 84.8a4.2 4.2 0 11-8.3 0c0-5.5 8.2-5.5 8.3 0m148.4-107.8a4.2 4.2 0 01-8.4 0c.1-5.5 8.3-5.5 8.4 0m48 112.9a4.2 4.2 0 01-8.3 0c0-5.6 8.4-5.6 8.4 0m-451 50a4.2 4.2 0 01-4.2 4.1 4.2 4.2 0 114.1-4.1m350.6-25.5c0 3.5-5.2 3.5-5.2 0 0-3.4 5.2-3.4 5.2 0M437.2 116.5a4.2 4.2 0 11-8.4 0c.2-5.4 8.3-5.4 8.4 0M227.9 633a6.3 6.3 0 11-12.5 0 6.3 6.3 0 0112.5 0m124.2-50.5c0 4-6 4-6 0s6-4 6 0" fill="#642" @@ -824,10 +824,10 @@ svg .hex.tip { <text x="180" y="70" font-size="13">Copyright © 2022 Columbia Games Inc.</text> </g> -<g font-family="Source Sans" font-weight="bold" xfill="#fde4c4" fill="#fed"> -<text text-anchor="middle" font-size="19" x="178" y="285">2</text> -<text text-anchor="middle" font-size="19" x="1104" y="172">5</text> -<text text-anchor="middle" font-size="19" x="1466" y="213">2</text> +<g font-family="Source Sans" font-weight="bold" fill="#fed"> +<text text-anchor="middle" font-size="19" x="179" y="285">2</text> +<text text-anchor="middle" font-size="19" x="1104" y="171">5</text> +<text text-anchor="middle" font-size="19" x="1467" y="213">2</text> </g> <g font-family="Source Serif" font-weight="bold" font-style="italic"> @@ -45,7 +45,6 @@ let ui = { allied_supply: document.getElementById("allied_supply"), turn_info: document.getElementById("turn_info"), hand: document.getElementById("hand"), - battle: document.getElementById("battle"), battle_hits: [ document.getElementById("hits_armor"), @@ -303,6 +302,13 @@ function toggle_units() { let showing_supply = false +function toggle_supply() { + if (!showing_supply) + send_query('supply') + else + hide_supply() +} + function show_supply(reply) { showing_supply = true view.axis_supply = reply.axis_supply @@ -452,6 +458,10 @@ function build_hexes() { } document.getElementById("mapsvg").getElementById("grid").setAttribute("d", path.join(" ")) + + ui.benghazi = document.getElementById("mapsvg").getElementById("fortress_benghazi") + ui.bardia = document.getElementById("mapsvg").getElementById("fortress_bardia") + ui.tobruk = document.getElementById("mapsvg").getElementById("fortress_tobruk") } function build_units() { @@ -503,6 +513,10 @@ for (let i = 0; i < stack.length; ++i) stack[i] = [] function update_map() { + ui.bardia.classList.toggle("axis", (view.fortress & 1) === 0) + ui.benghazi.classList.toggle("axis", (view.fortress & 2) === 0) + ui.tobruk.classList.toggle("axis", (view.fortress & 4) === 0) + for (let i = 0; i < stack.length; ++i) stack[i].length = 0 for (let u = 0; u < units.length; ++u) { @@ -518,7 +532,6 @@ function update_map() { } } - for (let hex = 0; hex < stack.length; ++hex) { let start_x = ui.hex_x[hex] let start_y = ui.hex_y[hex] @@ -8,6 +8,8 @@ // TODO: BUILDUP // TODO: MINEFIELDS +// TOOD: reveal/hide blocks (hexes) + // TODO: setup scenario specials // TODO: when is "fired" status cleared? @@ -673,17 +675,16 @@ function claim_hex_control_for_defender(a) { }) } -function capture_fortress(fortress, capacity, control_prop, captured_prop) { - if (game[control_prop] !== game.active) { +function capture_fortress(fortress, capacity) { + if (!is_fortress_friendly_controlled(fortress)) { if (has_undisrupted_friendly_unit(fortress) && !has_enemy_unit(fortress)) { supply_axis_invalid = true supply_allied_invalid = true log(`Captured #${fortress}!`) - game[control_prop] = game.active - if (!game[captured_prop]) { - game[captured_prop] = 1 + let fresh = set_fortress_friendly_controlled(fortress) + if (fresh) { if (is_axis_player()) { let award = capacity log(`Awarded ${award} supply cards.`) @@ -698,6 +699,62 @@ function capture_fortress(fortress, capacity, control_prop, captured_prop) { } } +// === FORTRESSES === + +const FORTRESS_BIT = { + [BARDIA]: 1, + [BENGHAZI]: 2, + [TOBRUK]: 4, +} + +function is_fortress_axis_controlled(fortress) { + return (game.fortress & FORTRESS_BIT[fortress]) === 0 +} + +function set_fortress_axis_controlled(fortress) { + game.fortress &= ~FORTRESS_BIT[fortress] +} + +function set_fortress_allied_controlled(fortress) { + game.fortress |= FORTRESS_BIT[fortress] +} + +function set_fortress_captured(fortress) { + let bit = FORTRESS_BIT[fortress] << 3 + if (game.fortress & bit) + return false + game.fortress |= bit + return true +} + +function clear_fortresses_captured() { + game.fortress &= 7 +} + +function is_fortress_friendly_controlled(fortress) { + if (is_axis_player()) + return is_fortress_axis_controlled(fortress) + return !is_fortress_axis_controlled(fortress) +} + +function set_fortress_friendly_controlled(fortress) { + if (is_axis_player()) + set_fortress_axis_controlled(fortress) + else + set_fortress_allied_controlled(fortress) + return set_fortress_captured(fortress) +} + +function is_fortress_besieged(fortress) { + let result = false + let besieged = is_fortress_axis_controlled() ? has_allied_unit : has_axis_unit + for_each_adjacent_hex(fortress, x => { + if (besieged(x)) + result = true + }) + return result +} + // === ITERATORS === function for_each_adjacent_hex(here, fn) { @@ -1371,15 +1428,17 @@ function find_valid_regroup_destinations(from, rommel) { } } -// === SUPPLY COMMITMENT === +// === SUPPLY COMMITMENT & TURN OPTION === -function goto_supply_commitment() { - game.state = 'supply_commitment' +function goto_turn_option() { + game.state = 'turn_option' } -states.supply_commitment = { +states.turn_option = { + inactive: "turn option", prompt() { - view.prompt = `Supply Commitment: ${game.commit[0]} real and ${game.commit[1]} dummy.` + view.prompt = `Select Turn Option: ${game.commit[0]} real and ${game.commit[1]} dummy supply.` + let hand = is_axis_player() ? game.axis_hand : game.allied_hand if (game.commit[0] + game.commit[1] < 3) { if (hand[0] > 0) @@ -1387,47 +1446,7 @@ states.supply_commitment = { if (hand[1] > 0) gen_action('dummy_card') } - gen_action_next() - }, - real_card() { - push_undo() - let hand = is_axis_player() ? game.axis_hand : game.allied_hand - hand[0]-- - game.commit[0]++ - }, - dummy_card() { - push_undo() - let hand = is_axis_player() ? game.axis_hand : game.allied_hand - hand[1]-- - game.commit[1]++ - }, - next() { - push_undo() - goto_turn_option() - }, -} - -function goto_turn_option() { - set_active_player() - let n = game.commit[0] + game.commit[1] - if (n === 0) - log(`Played zero supply cards.`) - else if (n === 1) - log(`Played one supply card.`) - else if (n === 2) - log(`Played two supply cards.`) - else if (n === 3) - log(`Played three supply cards.`) - log_br() - - game.state = 'turn_option' -} - -states.turn_option = { - inactive: "turn option", - prompt() { - view.prompt = "Select Turn Option" if (game.commit[0] >= 1) view.actions.basic = 1 else @@ -1447,36 +1466,61 @@ states.turn_option = { }, basic() { push_undo() - game.turn_option = 'basic' - game.passed = 0 - goto_move_phase() + apply_turn_option('basic') }, offensive() { push_undo() - game.turn_option = 'offensive' - game.passed = 0 - goto_move_phase() + apply_turn_option('offensive') }, assault() { push_undo() - game.turn_option = 'assault' - game.passed = 0 - goto_move_phase() + apply_turn_option('assault') }, blitz() { push_undo() - game.turn_option = 'blitz' - game.passed = 0 - goto_move_phase() + apply_turn_option('blitz') }, pass() { push_undo() - game.turn_option = 'pass' - game.passed ++ - goto_move_phase() + apply_turn_option('pass') + }, + real_card() { + push_undo() + let hand = is_axis_player() ? game.axis_hand : game.allied_hand + hand[0]-- + game.commit[0]++ + }, + dummy_card() { + push_undo() + let hand = is_axis_player() ? game.axis_hand : game.allied_hand + hand[1]-- + game.commit[1]++ }, } +function apply_turn_option(option) { + push_undo() + + game.turn_option = option + + let n = game.commit[0] + game.commit[1] + if (n === 0) + log(`Played zero supply cards.`) + else if (n === 1) + log(`Played one supply card.`) + else if (n === 2) + log(`Played two supply cards.`) + else if (n === 3) + log(`Played three supply cards.`) + log_br() + + if (game.turn_option === 'pass') + game.passed++ + else + game.passed = 0 + goto_move_phase() +} + // === PLAYER TURN === function goto_player_turn() { @@ -1507,8 +1551,13 @@ function end_player_turn() { // Reveal supply cards log_br() log(`Supply Cards Revealed:\n${game.commit[0]} real and ${game.commit[1]} dummy.`) + log_br() + game.commit = [ 0, 0 ] + if (check_sudden_death_victory()) + return + if (game.passed === 2) return end_month() @@ -1559,7 +1608,7 @@ function goto_initial_supply_check_rout() { } } if (n === 0) - goto_supply_commitment() + goto_turn_option() else if (n === 1) goto_rout(where, false, goto_initial_supply_check_rout) else @@ -1581,9 +1630,9 @@ states.initial_supply_check_rout = { function goto_final_supply_check() { set_active_player() - capture_fortress(BARDIA, 2, "bardia", "bardia_captured") - capture_fortress(BENGHAZI, 2, "benghazi", "benghazi_captured") - capture_fortress(TOBRUK, 5, "tobruk", "tobruk_captured") + capture_fortress(BARDIA, 2) + capture_fortress(BENGHAZI, 2) + capture_fortress(TOBRUK, 5) let snet = friendly_supply_network() let ssrc = friendly_supply_base() @@ -2874,26 +2923,32 @@ function end_rout() { // ==== COMBAT PHASE === -function is_mandatory_combat(fortress, control_prop) { - return is_battle_hex(fortress) && (game[control_prop] !== game.phasing) +function is_mandatory_combat(fortress) { + if (is_battle_hex(fortress)) { + if (game.phasing === AXIS) + return is_fortress_allied_controlled() + else + return is_fortress_axis_controlled() + } + return false } function goto_combat_phase() { set_active_player() if (game.turn_option === 'pass') { - if (is_mandatory_combat(BARDIA, "bardia")) + if (is_mandatory_combat(BARDIA)) return goto_rout(BARDIA, false, goto_combat_phase) - if (is_mandatory_combat(BENGHAZI, "benghazi")) + if (is_mandatory_combat(BENGHAZI)) return goto_rout(BENGHAZI, false, goto_combat_phase) - if (is_mandatory_combat(TOBRUK, "tobruk")) + if (is_mandatory_combat(TOBRUK)) return goto_rout(TOBRUK, false, goto_combat_phase) } else { - if (is_mandatory_combat(BARDIA, "bardia")) + if (is_mandatory_combat(BARDIA)) set_add(game.active_battles, BARDIA) - if (is_mandatory_combat(BENGHAZI, "benghazi")) + if (is_mandatory_combat(BENGHAZI)) set_add(game.active_battles, BENGHAZI) - if (is_mandatory_combat(TOBRUK, "tobruk")) + if (is_mandatory_combat(TOBRUK)) set_add(game.active_battles, TOBRUK) } @@ -3615,11 +3670,78 @@ function end_rout_fire() { // === BUILD-UP === function end_month() { - delete game.bardia_captured - delete game.benghazi_captured - delete game.tobruk_captured - // TODO: check end game and victory - throw new Error("end month not done yet") + // Forget captured fortresses (for bonus cards) + clear_fortresses_captured() + + if (game.month === SCENARIOS[game.scenario].end) + return end_game() + + goto_buildup() +} + +// === VICTORY CHECK === + +const EXIT_EAST_EDGE = [ 99, 123, 148 ] +const EXIT_EAST_STASH = 49 + +function check_sudden_death_victory() { + // Supplied units that move beyond the map "edge" exit the map. + // Count the easternmost row of hexes and half-hexes. + // In the original map this would be the half-hexes and the virtual hexes beyond the edge. + for (let x of EXIT_EAST_EDGE) { + for_each_axis_unit(u => { + if (unit_hex(u) === x && is_unit_supplied(u)) { + log(`Exited the east map edge.`) + set_unit_hex(u, EXIT_EAST_STASH) + } + }) + } + + let axis_exited = 0 + for_each_axis_unit(u => { + if (unit_hex(u) === EXIT_EAST_STASH) + axis_exited++ + }) + + if (is_axis_hex(ALEXANDRIA) || axis_exited >= 3) + return goto_game_over(ALLIED, "Allied Strategic Victory!") + if (is_allied_hex(EL_AGHEILA)) + return goto_game_over(AXIS, "Axis Strategic Victory!") + + return false +} + +function end_game() { + let axis = 0 + for_each_axis_unit(u => { + if (is_map_hex(unit_hex(u))) + axis += is_german_unit(u) ? 1.5 : 1.0 + }) + + let allied = 0 + for_each_allied_unit(u => { + if (is_map_hex(unit_hex(u))) + allied += 1.0 + }) + + if (axis >= allied * 2) + return goto_game_over(AXIS, "Axis Decisive Victory!") + if (allied >= axis * 2) + return goto_game_over(AXIS, "Allied Decisive Victory!") + + if (!is_fortress_besieged(TOBRUK)) { + if (is_fortress_axis_controlled(TOBRUK)) + return goto_game_over(AXIS, "Axis Positional Victory!") + else + return goto_game_over(ALLIED, "Allied Positional Victory!") + } + + if (axis > allied) + return goto_game_over(AXIS, "Axis Attrition Victory!") + if (allied > axis) + return goto_game_over(ALLIED, "Allied Attrition Victory!") + + return goto_game_over("Draw", "No Victory!") } // === DEPLOYMENT === @@ -4159,10 +4281,9 @@ const SETUP = { function setup_fortress(scenario, fortress) { if (scenario.allied_deployment.includes(fortress)) - return ALLIED + set_fortress_allied_controlled(fortress) if (scenario.axis_deployment.includes(fortress)) - return AXIS - throw new Error("invalid setup") + set_fortress_axis_controlled(fortress) } function setup(name) { @@ -4173,9 +4294,9 @@ function setup(name) { SETUP[name](-scenario.start) - game.bardia = setup_fortress(scenario, BARDIA) - game.benghazi = setup_fortress(scenario, BENGHAZI) - game.tobruk = setup_fortress(scenario, TOBRUK) + setup_fortress(scenario, BARDIA) + setup_fortress(scenario, BENGHAZI) + setup_fortress(scenario, TOBRUK) log_h2("Axis Deployment") game.phasing = AXIS @@ -4218,9 +4339,7 @@ exports.setup = function (seed, scenario, options) { revealed_minefields: [], // fortress control - bardia: ALLIED, - benghazi: ALLIED, - tobruk: ALLIED, + fortress: 7, // battle hexes (defender) axis_hexes: [], @@ -4274,6 +4393,7 @@ exports.view = function(state, current) { month: game.month, units: game.units, moved: game.moved, + fortress: game.fortress, axis_hand: game.axis_hand[0] + game.axis_hand[1], allied_hand: game.allied_hand[0] + game.allied_hand[1], commit: game.commit[0] + game.commit[1], @@ -4507,6 +4627,7 @@ function goto_game_over(result, victory) { game.victory = victory log_br() log(game.victory) + return true } states.game_over = { @@ -4548,7 +4669,9 @@ exports.action = function (state, current, action, arg) { function common_view(current) { view.log = game.log - if (current === 'Observer' || game.active !== current) { + if (game.state === 'game_over') { + view.prompt = game.victory + } else if (current === 'Observer' || game.active !== current) { let inactive = states[game.state].inactive || game.state view.prompt = `Waiting for ${game.active} \u2014 ${inactive}...` } else { |