From 1a29b9c975564261bfe4c715223f535bb71d8f09 Mon Sep 17 00:00:00 2001 From: Tor Andersson Date: Thu, 28 Jul 2022 15:02:48 +0200 Subject: Buildup turn structure. Resupply. Initiative challenge. --- play.js | 8 +- rules.js | 461 +++++++++++++++++++++++++++++++++++++++++++++++++++++---------- 2 files changed, 394 insertions(+), 75 deletions(-) diff --git a/play.js b/play.js index ef1ed55..4cdbaf3 100644 --- a/play.js +++ b/play.js @@ -709,10 +709,10 @@ function on_update() { ui.allied_supply.textContent = view.allied_hand ui.turn_info.textContent = `Month: ${view.month}\nSupply Commitment: ${view.commit}` - if (view.actions && (view.actions.real_card || view.actions.dummy_card)) - ui.cards.forEach(elt => elt.classList.add("action")) - else - ui.cards.forEach(elt => elt.classList.remove("action")) + for (let i = 0; i < 28; ++i) + ui.cards[i].classList.toggle("action", !!(view.actions && view.actions.real_card)) + for (let i = 0; i < 14; ++i) + ui.cards[i+28].classList.toggle("action", !!(view.actions && view.actions.dummy_card)) action_button("discard", "Discard") action_button("keep", "Keep") diff --git a/rules.js b/rules.js index 87b55cf..1062f35 100644 --- a/rules.js +++ b/rules.js @@ -19,6 +19,11 @@ // RULES: reveal minefields moved through (but not stopped at)? +// TODO: exit hexes off-limits to allied (or unsupplied) units? + +// TODO: forbid single-group regroup moves or convert to group moves after the fact, +// to prevent forced march abuse. + const max = Math.max const min = Math.min const abs = Math.abs @@ -55,6 +60,9 @@ function debug_hexes(n, list) { const AXIS = 'Axis' const ALLIED = 'Allied' +const REAL = 0 +const DUMMY = 1 + const hexw = 25 const hexh = 9 const first_hex = 7 @@ -688,11 +696,11 @@ function capture_fortress(fortress, capacity) { if (is_axis_player()) { let award = capacity log(`Awarded ${award} supply cards.`) - deal_axis_supply_cards(award) + game.axis_award += award } else { let award = Math.floor(capacity / 2) log(`Awarded ${award} supply cards.`) - deal_allied_supply_cards(award) + game.allied_award += award } } } @@ -779,21 +787,45 @@ function for_each_axis_unit(fn) { fn(u) } -function for_each_allied_unit(fn) { +function for_each_axis_unit_on_map(fn) { for (let u = first_axis_unit; u <= last_axis_unit; ++u) + if (is_map_hex(unit_hex(u))) + fn(u) +} + +function for_each_allied_unit(fn) { + for (let u = first_allied_unit; u <= last_allied_unit; ++u) fn(u) } +function for_each_allied_unit_on_map(fn) { + for (let u = first_allied_unit; u <= last_allied_unit; ++u) + if (is_map_hex(unit_hex(u))) + fn(u) +} + function for_each_friendly_unit(fn) { for (let u = first_friendly_unit; u <= last_friendly_unit; ++u) fn(u) } +function for_each_friendly_unit_on_map(fn) { + for (let u = first_friendly_unit; u <= last_friendly_unit; ++u) + if (is_map_hex(unit_hex(u))) + fn(u) +} + function for_each_enemy_unit(fn) { for (let u = first_enemy_unit; u <= last_enemy_unit; ++u) fn(u) } +function for_each_enemy_unit_on_map(fn) { + for (let u = first_enemy_unit; u <= last_enemy_unit; ++u) + if (is_map_hex(unit_hex(u))) + fn(u) +} + function for_each_friendly_unit_in_hex(x, fn) { for (let u = first_friendly_unit; u <= last_friendly_unit; ++u) if (unit_hex(u) === x) @@ -931,8 +963,6 @@ function count_hp_in_rout() { // === SUPPLY CARDS === -// TODO: reshuffle! - function draw_supply_card(pile) { let x = random(pile[0] + pile[1]) if (x < pile[0]) { @@ -945,15 +975,30 @@ function draw_supply_card(pile) { } function deal_axis_supply_cards(n) { + let cur = game.axis_hand[REAL] + game.axis_hand[DUMMY] + if (cur + n > 16) + n = 16 - cur + log(`Axis drew ${n} cards.`) for (let i = 0; i < n; ++i) game.axis_hand[draw_supply_card(game.draw_pile)]++ } function deal_allied_supply_cards(n) { + let cur = game.allied_hand[REAL] + game.allied_hand[DUMMY] + if (cur + n > 16) + n = 16 - cur + log(`Allied drew ${n} cards.`) for (let i = 0; i < n; ++i) game.allied_hand[draw_supply_card(game.draw_pile)]++ } +function shuffle_cards() { + let real = game.axis_hand[REAL] + game.allied_hand[REAL] + let dummy = game.axis_hand[DUMMY] + game.axis_hand[DUMMY] + game.draw_pile[0] = 28 - real + game.draw_pile[1] = 14 - dummy +} + // === SUPPLY NETWORK === function ind(d, msg, here, ...extra) { @@ -1434,16 +1479,20 @@ function goto_turn_option() { game.state = 'turn_option' } +function player_hand() { + return is_axis_player() ? game.axis_hand : game.allied_hand +} + states.turn_option = { inactive: "turn option", prompt() { 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 + let hand = player_hand() if (game.commit[0] + game.commit[1] < 3) { - if (hand[0] > 0) + if (hand[REAL] > 0) gen_action('real_card') - if (hand[1] > 0) + if (hand[DUMMY] > 0) gen_action('dummy_card') } @@ -1486,14 +1535,14 @@ states.turn_option = { }, real_card() { push_undo() - let hand = is_axis_player() ? game.axis_hand : game.allied_hand - hand[0]-- + let hand = player_hand() + hand[REAL]-- game.commit[0]++ }, dummy_card() { push_undo() - let hand = is_axis_player() ? game.axis_hand : game.allied_hand - hand[1]-- + let hand = player_hand() + hand[DUMMY]-- game.commit[1]++ }, } @@ -1545,6 +1594,8 @@ function goto_player_turn() { } function end_player_turn() { + clear_undo() + // Forget partial retreats set_clear(game.partial_retreats) @@ -1578,7 +1629,7 @@ function goto_initial_supply_check() { // TODO: fortress supply // TODO: assign fortress supply - for_each_friendly_unit(u => { + for_each_friendly_unit_on_map(u => { let x = unit_hex(u) if (snet[x]) { set_unit_supply(u, ssrc) @@ -1593,7 +1644,7 @@ function goto_initial_supply_check() { }) set_clear(game.recover) - for_each_enemy_unit(u => { + for_each_enemy_unit_on_map(u => { if (is_unit_disrupted(u)) set_add(game.recover, u) }) @@ -1642,9 +1693,9 @@ function goto_final_supply_check() { // TODO: fortress supply // TODO: assign unused fortress supply - for_each_friendly_unit(u => { + for_each_friendly_unit_on_map(u => { let x = unit_hex(u) - if (is_map_hex(x) && !snet[x] && !is_unit_disrupted(u) && !is_unit_supplied(u)) { + if (!snet[x] && !is_unit_disrupted(u) && !is_unit_supplied(u)) { log(`Disrupted at #${x}`) set_unit_disrupted(u) } @@ -1838,25 +1889,25 @@ function end_movement() { function goto_move() { if (game.rommel === 1) { if (game.from1 && game.to1) - log(`Regroup move from #${game.from1} to #${game.to1} (Rommel).`) + log(`Rommel Regroup move\nfrom #${game.from1}\nto #${game.to1}.`) else if (game.from1) - log(`Group move from #${game.from1} (Rommel).`) + log(`Rommel Group move\nfrom #${game.from1}.`) } else { if (game.from1 && game.to1) log(`Regroup move\nfrom #${game.from1}\nto #${game.to1}.`) else if (game.from1) - log(`Group move from #${game.from1}.`) + log(`Group move\nfrom #${game.from1}.`) } if (game.rommel === 2) { if (game.from2 && game.to2) - log(`Regroup move from #${game.from2} to #${game.to2} (Rommel).`) + log(`Rommel Regroup move\nfrom #${game.from2}\nto #${game.to2}.`) else if (game.from2) - log(`Group move from #${game.from2} (Rommel).`) + log(`Rommel Group move\nfrom #${game.from2}.`) } else { if (game.from2 && game.to2) - log(`Regroup move from #${game.from2} to #${game.to2}.`) + log(`Regroup move\nfrom #${game.from2}\nto #${game.to2}.`) else if (game.from2) - log(`Group move from #${game.from2}.`) + log(`Group move\nfrom #${game.from2}.`) } log_br() game.state = 'move' @@ -2297,7 +2348,7 @@ states.forced_marches = { let ix = game.forced.findIndex(item => who === item[0]) let to = game.forced[ix][1] let from = game.forced[ix][2] || via - let roll = random(6) + 1 + let roll = roll_die() if (roll >= 4) { log(`Forced March roll ${roll} success.`) if (has_enemy_unit(to)) { @@ -3088,7 +3139,7 @@ function is_fortress_defensive_fire() { function is_minefield_offensive_fire() { if ((game.state === 'battle_fire' && is_active_player()) || (game.state === 'probe_fire' && is_passive_player())) { - if (set_has(game.revealed_minefields)) { + if (set_has(game.minefields)) { // DD advantage is lost if the defender initiated combat if (is_axis_player()) return set_has(game.allied_hexes, game.battle) @@ -3113,7 +3164,7 @@ function roll_battle_fire(who, tc) { let result = [] let total = 0 for (let i = 0; i < cv; ++i) { - let roll = random(6) + 1 + let roll = roll_die() result.push(roll) if (roll >= fp) ++total @@ -3519,8 +3570,8 @@ function can_pursuit_fire(verbose) { function roll_pursuit_fire_imp(who, n, hp) { if (n === 2) { - let a = random(6) + 1 - let b = random(6) + 1 + let a = roll_die() + let b = roll_die() game.flash = `${unit_name(who)} fired ${a}, ${b}` log(game.flash) if (a >= 4) @@ -3529,7 +3580,7 @@ function roll_pursuit_fire_imp(who, n, hp) { game.hits++ } if (n === 1) { - let a = random(6) + 1 + let a = roll_die() game.flash = `${unit_name(who)} fired ${a}` log(`>%${who} pursuit fired ${a}.`) if (a >= 4) @@ -3675,12 +3726,268 @@ function end_month() { // Forget captured fortresses (for bonus cards) clear_fortresses_captured() - if (game.month === SCENARIOS[game.scenario].end) + if (game.month === current_scenario().end) return end_game() goto_buildup() } +function goto_buildup() { + ++game.month + log_h1(`Month ${game.month}`) + + game.phasing = AXIS + set_active_player() + goto_buildup_discard() +} + +function goto_buildup_discard() { + game.state = 'buildup_discard' + let hand = player_hand() + if (hand[REAL] + hand[DUMMY] === 0) + end_buildup_discard() +} + +states.buildup_discard = { + prompt() { + view.prompt = "Buildup: Discard any unwanted dummy cards." + let hand = player_hand() + if (hand[DUMMY] > 0) + gen_action('dummy_card') + gen_action_next() + }, + dummy_card() { + push_undo() + log(game.active + " discarded dummy supply.") + let hand = player_hand() + hand[DUMMY]-- + }, + next() { + clear_undo() + end_buildup_discard() + }, +} + +function end_buildup_discard() { + if (is_axis_player()) { + set_enemy_player() + goto_buildup_discard() + } else { + goto_buildup_supply_check() + } +} + +function goto_buildup_supply_check() { + // TODO: fortress supply + // TODO: assign fortress supply + + // Remember supply networks throughout buildup. + game.buildup = { + axis_network: axis_supply_network().slice(), + allied_network: allied_supply_network().slice(), + axis_cards: 0, + allied_cards: 0, + } + + debug_hexes("axis supply", supply_axis_network) + debug_hexes("allied supply", supply_allied_network) + + for_each_axis_unit_on_map(u => { + let x = unit_hex(u) + if (supply_axis_network[x]) + set_unit_supply(u, EL_AGHEILA) + else + set_unit_supply(u, 0) + }) + + for_each_allied_unit_on_map(u => { + let x = unit_hex(u) + if (supply_allied_network[x]) + set_unit_supply(u, ALEXANDRIA) + else + set_unit_supply(u, 0) + }) + + log_br() + + resume_buildup_supply_check() +} + +function resume_buildup_supply_check() { + game.state = 'buildup_supply_check' + let done = true + for_each_friendly_unit_on_map(u => { + if (is_unit_unsupplied(u)) + done = false + }) + if (done) { + if (is_axis_player()) { + log_br() + set_enemy_player() + resume_buildup_supply_check() + } else { + goto_buildup_point_determination() + } + } +} + +states.buildup_supply_check = { + prompt() { + view.prompt = `Buildup: Eliminate unsupplied units.` + for_each_friendly_unit_on_map(u => { + if (is_unit_unsupplied(u)) + gen_action_unit(u) + }) + }, + unit(u) { + log(`>eliminated at #${unit_hex(u)}`) + eliminate_unit(u) + resume_buildup_supply_check() + }, +} + +function goto_buildup_point_determination() { + let axis, allied + + log_br() + if (game.scenario === "1940") { + axis = roll_die() + allied = roll_die() + log(`Axis rolled ${axis}.`) + log(`Allied rolled ${axis}.`) + } else { + let axis_a = roll_die() + let axis_b = roll_die() + let allied_a = roll_die() + let allied_b = roll_die() + axis = axis_a + axis_b + allied = allied_a + allied_b + log(`Axis rolled ${axis_a} + ${axis_b}.`) + log(`Allied rolled ${axis_a} + ${axis_b}.`) + } + + log(`Receive ${axis + allied} BPs.`) + game.axis_bps += axis + allied + game.allied_bps += axis + allied + + if (allied <= axis) + game.phasing = ALLIED + else + game.phasing = AXIS + + set_active_player() + goto_buildup_reinforcements() +} + +function goto_buildup_reinforcements() { + log_h2(game.active + " Buildup") + + goto_buildup_spending() +} + +function goto_buildup_spending() { + end_buildup_spending() +} + +function end_buildup_spending() { + if (is_active_player()) { + set_enemy_player() + goto_buildup_reinforcements() + } else { + goto_buildup_resupply() + } +} + +function goto_buildup_resupply() { + log_h2("Resupply") + log(`Shuffled supply cards.`) + + shuffle_cards() + + // Per-scenario allotment + let axis_resupply = (game.month > 10 ? 2 : 3) + let allied_resupply = 3 + + // Extra cards purchased during buildup + axis_resupply += game.buildup.axis_cards + allied_resupply += game.buildup.allied_cards + + // Bonus from captured fortresses + axis_resupply += game.axis_award + allied_resupply += game.allied_award + + game.axis_award = 0 + game.allied_award = 0 + + deal_axis_supply_cards(axis_resupply) + deal_allied_supply_cards(allied_resupply) + + goto_player_initiative() +} + +// === INITIATIVE === + +function goto_player_initiative() { + game.phasing = AXIS + set_passive_player() + game.state = 'allied_player_initiative' +} + +states.allied_player_initiative = { + prompt() { + view.prompt = "Initiative: You may challenge for the initiative." + let hand = player_hand() + if (hand[REAL] > 0) + gen_action('real_card') + if (hand[DUMMY] > 0) + gen_action('dummy_card') + gen_action_next() + }, + real_card() { + log(`Allied challenged for the initiative.`) + player_hand()[0]-- + game.phasing = ALLIED + set_passive_player() + game.state = 'axis_player_initiative' + }, + dummy_card() { + player_hand()[1]-- + log(`Allied challenged for the initiative.`) + set_active_player() + game.state = 'axis_player_initiative' + }, + next() { + goto_player_turn() + } +} + +states.axis_player_initiative = { + prompt() { + view.prompt = "Initiative: You may defend your initiative." + let hand = player_hand() + if (hand[REAL] > 0) + gen_action('real_card') + gen_action_next() + }, + real_card() { + player_hand()[0]-- + log("Axis defends the initiative.") + if (game.phasing === ALLIED) + log("Allied card was real.") + else + log("Allied card was a dummy.") + game.phasing = AXIS + goto_player_turn() + }, + next() { + if (game.phasing === ALLIED) + log("Allied siezed the initiative.") + else + log("Allied card was a dummy.") + goto_player_turn() + } +} + // === VICTORY CHECK === const EXIT_EAST_EDGE = [ 99, 123, 148 ] //, 172, 197 ] @@ -3705,25 +4012,29 @@ function check_sudden_death_victory() { axis_exited++ }) - if (is_axis_hex(ALEXANDRIA) || axis_exited >= 3) - return goto_game_over(ALLIED, "Allied Strategic Victory!") - if (is_allied_hex(EL_AGHEILA)) + if (is_axis_hex(ALEXANDRIA) || axis_exited >= 3) { + log_br() + log("Axis captured Alexandria!") return goto_game_over(AXIS, "Axis Strategic Victory!") + } + if (is_allied_hex(EL_AGHEILA)) { + log_br() + log("Allied captured El Agheila!") + return goto_game_over(ALLIED, "Allied 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 + for_each_axis_unit_on_map(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 + for_each_allied_unit_on_map(u => { + allied += 1.0 }) if (axis >= allied * 2) @@ -3750,14 +4061,14 @@ function end_game() { function goto_free_deployment() { game.state = 'free_deployment' - if (!has_friendly_unit_in_month(SCENARIOS[game.scenario].start)) + if (!has_friendly_unit_in_month(current_scenario().start)) end_free_deployment() } states.free_deployment = { inactive: "free deployment", prompt() { - let scenario = SCENARIOS[game.scenario] + let scenario = current_scenario() let deploy = hexdeploy + scenario.start let axis = (game.active === AXIS) let done = true @@ -3801,7 +4112,7 @@ states.free_deployment = { function end_free_deployment() { set_enemy_player() - if (has_friendly_unit_in_month(SCENARIOS[game.scenario].start)) { + if (has_friendly_unit_in_month(current_scenario().start)) { log_h2("Allied Deployment") } else { goto_initial_supply_cards() @@ -3812,7 +4123,9 @@ function goto_initial_supply_cards() { game.phasing = AXIS set_active_player() - let scenario = SCENARIOS[game.scenario] + log_br() + + let scenario = current_scenario() deal_axis_supply_cards(scenario.axis_initial_supply) deal_allied_supply_cards(scenario.allied_initial_supply) @@ -3826,20 +4139,17 @@ states.initial_supply_cards = { gen_action('keep') }, discard() { - let scenario = SCENARIOS[game.scenario] if (is_axis_player()) { - log_br() - log(`Axis player drew a new hand.`) - game.axis_hand[0] = 0 - game.axis_hand[1] = 0 - deal_axis_supply_cards(scenario.axis_initial_supply) + log(`Axis discarded their hand.`) + game.axis_hand[REAL] = 0 + game.axis_hand[DUMMY] = 0 + deal_axis_supply_cards(current_scenario().axis_initial_supply) set_enemy_player() } else { - log_br() - log(`Allied player drew a new hand.`) - game.allied_hand[0] = 0 - game.allied_hand[1] = 0 - deal_allied_supply_cards(scenario.allied_initial_supply) + log(`Allied discarded their hand.`) + game.allied_hand[REAL] = 0 + game.allied_hand[DUMMY] = 0 + deal_allied_supply_cards(current_scenario().allied_initial_supply) begin_game() } }, @@ -3852,12 +4162,16 @@ states.initial_supply_cards = { } function begin_game() { - log_br() log_h1(`Month ${game.month}`) - log_br() + + if (game.scenario === "Crusader") { + game.phasing = ALLIED + set_add(game.minefields, TOBRUK) + } // No buildup first month // No initiative first month + goto_player_turn() } @@ -3904,6 +4218,10 @@ function setup_units(where, steps, list) { } } +function current_scenario() { + return SCENARIOS[game.scenario] +} + const SCENARIOS = { "1940": { year: 1940, @@ -3914,10 +4232,6 @@ const SCENARIOS = { axis_initial_supply: 6, allied_initial_supply: 3, deployment_limit: {}, - special: { - no_rommel_bonus: true, - only_one_die_for_buildup: true, - } }, "1941": { year: 1941, @@ -3940,10 +4254,6 @@ const SCENARIOS = { deployment_limit: { [TOBRUK]: 5, }, - special: { - allies_first_turn: true, - tobruk_minefield: true, - } }, "Battleaxe": { year: 1941, @@ -4288,13 +4598,13 @@ function setup_fortress(scenario, fortress) { set_fortress_axis_controlled(fortress) } -function setup(name) { - let scenario = SCENARIOS[name] +function setup(scenario_name) { + let scenario = SCENARIOS[scenario_name] game.month = scenario.start - log_h1(name) + log_h1(scenario_name) - SETUP[name](-scenario.start) + SETUP[scenario_name](-scenario.start) setup_fortress(scenario, BARDIA) setup_fortress(scenario, BENGHAZI) @@ -4331,6 +4641,9 @@ exports.setup = function (seed, scenario, options) { axis_hand: [ 0, 0 ], allied_hand: [ 0, 0 ], + axis_bps: 0, + allied_bps: 0, + units: new Array(units.length).fill(0), moved: [], fired: [], @@ -4338,10 +4651,12 @@ exports.setup = function (seed, scenario, options) { axis_minefields: [], allied_minefields: [], - revealed_minefields: [], + minefields: [], // fortress control fortress: 7, + axis_award: 0, + allied_award: 0, // battle hexes (defender) axis_hexes: [], @@ -4396,8 +4711,8 @@ exports.view = function(state, current) { 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], + axis_hand: game.axis_hand[REAL] + game.axis_hand[DUMMY], + allied_hand: game.allied_hand[REAL] + game.allied_hand[DUMMY], commit: game.commit[0] + game.commit[1], axis_hexes: game.axis_hexes, allied_hexes: game.allied_hexes, @@ -4462,6 +4777,10 @@ function random(range) { return (game.seed = game.seed * 200105 % 34359738337) % range } +function roll_die() { + return random(6) + 1 +} + function shuffle(deck) { for (let i = deck.length - 1; i > 0; --i) { let j = random(i + 1) -- cgit v1.2.3