summaryrefslogtreecommitdiff
path: root/rules.js
diff options
context:
space:
mode:
authorTor Andersson <tor@ccxvii.net>2022-07-28 15:02:48 +0200
committerTor Andersson <tor@ccxvii.net>2022-11-17 13:11:26 +0100
commit1a29b9c975564261bfe4c715223f535bb71d8f09 (patch)
treef717b1c0cd2615e4c28e64eec45580387b12f2e6 /rules.js
parent9fb38f18590120409dea918132ccf437958de734 (diff)
downloadrommel-in-the-desert-1a29b9c975564261bfe4c715223f535bb71d8f09.tar.gz
Buildup turn structure. Resupply. Initiative challenge.
Diffstat (limited to 'rules.js')
-rw-r--r--rules.js461
1 files changed, 390 insertions, 71 deletions
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)