summaryrefslogtreecommitdiff
path: root/rules.js
diff options
context:
space:
mode:
Diffstat (limited to 'rules.js')
-rw-r--r--rules.js307
1 files changed, 215 insertions, 92 deletions
diff --git a/rules.js b/rules.js
index dbc4562..e50ac44 100644
--- a/rules.js
+++ b/rules.js
@@ -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 {