From 186fe39912a37280f2103dac69571eb5cb9a815e Mon Sep 17 00:00:00 2001
From: Tor Andersson <tor@ccxvii.net>
Date: Tue, 26 Jul 2022 16:45:07 +0200
Subject: capture fortresses

---
 rules.js | 160 ++++++++++++++++++++++++++++++++++++++++++++++++++++++---------
 1 file changed, 138 insertions(+), 22 deletions(-)

diff --git a/rules.js b/rules.js
index 7cd0ac4..b6f3ee1 100644
--- a/rules.js
+++ b/rules.js
@@ -1,8 +1,25 @@
 "use strict"
 
+// TODO: fortress battles mandatory combat!
+// TODO: claim fortress control
+
+// TODO: FORCED MARCHES
+// TODO: BLITZ TURN
+// TODO: FINAL SUPPLY CHECK
+// TODO: SUPPLY CARDS (playing and revealing and choosing turn option)
+// TODO: BUILDUP
+
 // TODO: first_friendly_unit / for_each_friendly_unit
 
-// TODO: be smart about caching update_supply_networks calls
+// TODO: when is "fired" status cleared?
+
+// TODO: cache_valid, cache_axis, cache_allied (presence and disruption per hex)
+//   instead of iterating units in is_axis_hex, etc.
+//   invalidate when loading state
+//   invalidate when disrupting, recovering, eliminating or moving units
+
+// TODO: cache supply lines
+//   same as presence cache
 
 // RULES: disrupted units routed again in second enemy turn, will they still recover?
 // assume yes, easy to change (remove from game.recover set if routed)
@@ -573,7 +590,7 @@ function is_new_battle_hex(a) {
 }
 
 function claim_hexside_control(side) {
-	if (game.active === AXIS) {
+	if (is_axis_player()) {
 		set_add(game.axis_sides, side)
 		set_delete(game.allied_sides, side)
 	} else {
@@ -598,7 +615,7 @@ function release_hex_control(a) {
 function claim_hex_control_for_defender(a) {
 	// a new battle hex: claim hex and hexsides for defender
 
-	if (game.active === AXIS)
+	if (is_axis_player())
 		set_add(game.allied_hexes, a)
 	else
 		set_add(game.axis_hexes, a)
@@ -606,7 +623,7 @@ function claim_hex_control_for_defender(a) {
 	for_each_adjacent_hex(a, b => {
 		let side = to_side_id(a, b)
 		if (side_limit[side] > 0) {
-			if (game.active === AXIS) {
+			if (is_axis_player()) {
 				if (!set_has(game.axis_sides, side))
 					set_add(game.allied_sides, side)
 			} else {
@@ -617,6 +634,27 @@ function claim_hex_control_for_defender(a) {
 	})
 }
 
+function capture_fortress(fortress, capacity, control_prop, captured_prop) {
+	if (game[control_prop] !== game.active) {
+		if (has_undisrupted_friendly_unit(fortress) && !has_enemy_unit(fortress)) {
+			log(`Captured #${fortress}!`)
+			game[control_prop] = game.active
+			if (!game[captured_prop]) {
+				game[captured_prop] = 1
+				if (is_axis_player()) {
+					let award = capacity
+					log(`Awarded ${award} supply cards.`)
+					deal_axis_supply_cards(award)
+				} else {
+					let award = Math.floor(capacity / 2)
+					log(`Awarded ${award} supply cards.`)
+					deal_allied_supply_cards(award)
+				}
+			}
+		}
+	}
+}
+
 // === ITERATORS ===
 
 function for_each_adjacent_hex(here, fn) {
@@ -1124,8 +1162,8 @@ function search_move_retreat(start, speed) {
 
 function search_withdraw(start, speed) {
 	update_supply_networks()
-	let sline = game.active === AXIS ? game.axis_supply_line : game.allied_supply_line
-	let sdist = game.active === AXIS ? distance_to[EL_AGHEILA] : distance_to[ALEXANDRIA]
+	let sline = is_axis_player() ? game.axis_supply_line : game.allied_supply_line
+	let sdist = is_axis_player() ? distance_to[EL_AGHEILA] : distance_to[ALEXANDRIA]
 
 	search_init()
 	search_move_bfs(path_from[0], path_cost[0], start, 0, speed, false, sline, sdist)
@@ -1136,8 +1174,8 @@ function search_withdraw(start, speed) {
 
 function search_withdraw_retreat(start, speed) {
 	update_supply_networks()
-	let sline = game.active === AXIS ? game.axis_supply_line : game.allied_supply_line
-	let sdist = game.active === AXIS ? distance_to[EL_AGHEILA] : distance_to[ALEXANDRIA]
+	let sline = is_axis_player() ? game.axis_supply_line : game.allied_supply_line
+	let sdist = is_axis_player() ? distance_to[EL_AGHEILA] : distance_to[ALEXANDRIA]
 
 	search_init()
 	search_move_bfs(path_from[0], path_cost[0], start, 0, speed, true, sline, sdist)
@@ -1162,7 +1200,7 @@ function search_init() {
 
 // Breadth First Search
 function search_move_bfs(from, cost, start, road, max_cost, retreat, sline, sdist) {
-	let friendly_sides = (game.active === AXIS) ? game.axis_sides : game.allied_sides
+	let friendly_sides = (is_axis_player()) ? game.axis_sides : game.allied_sides
 
 	from.fill(0)
 	cost.fill(15)
@@ -1325,6 +1363,22 @@ function set_active_player() {
 	game.active = game.phasing
 }
 
+function is_active_player() {
+	return game.active === game.phasing
+}
+
+function is_passive_player() {
+	return game.active !== game.phasing
+}
+
+function is_axis_player() {
+	return game.active === AXIS
+}
+
+function is_allied_player() {
+	return game.active === ALLIED
+}
+
 function set_passive_player() {
 	if (game.phasing === AXIS)
 		game.active = ALLIED
@@ -1372,6 +1426,9 @@ function goto_initial_supply_check() {
 	let snet = game.phasing === AXIS ? game.axis_supply : game.allied_supply
 	let ssrc = game.phasing === AXIS ? EL_AGHEILA : ALEXANDRIA
 
+	// TODO: fortress supply
+	// TODO: assign fortress supply
+
 	for_each_friendly_unit(u => {
 		let x = unit_hex(u)
 		if (snet[x]) {
@@ -1390,9 +1447,40 @@ function goto_initial_supply_check() {
 			set_add(game.recover, u)
 	})
 
+	// TODO: check for enemy routs
+
 	goto_turn_option()
 }
 
+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")
+
+	update_supply_networks()
+	let snet = game.phasing === AXIS ? game.axis_supply : game.allied_supply
+	let ssrc = game.phasing === AXIS ? EL_AGHEILA : ALEXANDRIA
+
+	// TODO: fortress supply
+	// TODO: assign unused fortress supply
+
+	for_each_friendly_unit(u => {
+		let x = unit_hex(u)
+		if (!snet[x]) {
+			if (!is_unit_disrupted(u) && !is_unit_supplied(u)) {
+				log(`Disrupted at #${x}`)
+				set_unit_disrupted(u)
+			}
+		}
+	})
+
+	// TODO: rout friendly units
+
+	end_player_turn()
+}
+
 function goto_turn_option() {
 	game.state = 'turn_option'
 }
@@ -1472,7 +1560,7 @@ states.select_moves = {
 	},
 	end_turn() {
 		clear_undo()
-		end_player_turn()
+		goto_final_supply_check()
 	}
 }
 
@@ -1951,8 +2039,8 @@ states.retreat_from = {
 		view.prompt = `Retreat: Select hex to retreat from.`
 
 		update_supply_networks()
-		let sline = game.active === AXIS ? game.axis_supply_line : game.allied_supply_line
-		let sdist = game.active === AXIS ? distance_to[EL_AGHEILA] : distance_to[ALEXANDRIA]
+		let sline = is_axis_player() ? game.axis_supply_line : game.allied_supply_line
+		let sdist = is_axis_player() ? distance_to[EL_AGHEILA] : distance_to[ALEXANDRIA]
 
 		if (!game.to1 && game.from1) {
 			if (can_retreat_with_group_move(game.from1, sline, sdist))
@@ -2173,8 +2261,8 @@ states.refuse_battle = {
 		view.prompt = `You may Refuse Battle.`
 
 		update_supply_networks()
-		let sline = game.active === AXIS ? game.axis_supply_line : game.allied_supply_line
-		let sdist = game.active === AXIS ? distance_to[EL_AGHEILA] : distance_to[ALEXANDRIA]
+		let sline = is_axis_player() ? game.axis_supply_line : game.allied_supply_line
+		let sdist = is_axis_player() ? distance_to[EL_AGHEILA] : distance_to[ALEXANDRIA]
 
 		for (let x of game.active_battles)
 			if (can_disengage_and_withdraw(x, sline, sdist))
@@ -2459,9 +2547,7 @@ states.select_battle = {
 
 function end_combat_phase() {
 	// TODO: blitz
-	// TODO: final supply check
-	// TODO: supply cards revealed
-	end_player_turn()
+	goto_final_supply_check()
 }
 
 // === BATTLES ===
@@ -2478,9 +2564,32 @@ function is_unit_retreating(u) {
 	return false
 }
 
+function is_assault_battle() {
+	return set_has(game.assault_battles, game.battle)
+}
+
+function is_fortress_defender() {
+	if ((game.state === 'battle_fire' && is_passive_player()) || (game.state === 'probe_fire' && is_active_player())) {
+		if (game.battle === BENGHAZI)
+			return game.benghazi === game.active
+		if (game.battle === TOBRUK)
+			return game.tobruk === game.active
+		if (game.battle === BARDIA)
+			return game.tobruk === game.active
+	}
+	return false
+}
+
 function roll_battle_fire(who, tc) {
 	let fc = unit_class(who)
 	let cv = unit_cv(who)
+
+	// Double dice during assault!
+	if (is_assault_battle())
+		cv += cv
+	else if (fc !== ARMOR && is_fortress_defender())
+		cv += cv
+
 	console.log("FIRE", unit_name(who), cv)
 	let fp = FIREPOWER_MATRIX[fc][tc]
 	let result = []
@@ -3010,6 +3119,14 @@ function end_rout_fire() {
 	goto_rout_move()
 }
 
+// === BUILD-UP ===
+
+function end_month() {
+	delete game.bardia_captured
+	delete game.benghazi_captured
+	delete game.tobruk_captured
+}
+
 // === DEPLOYMENT ===
 
 states.free_deployment = {
@@ -3053,8 +3170,8 @@ states.free_deployment = {
 	},
 	next() {
 		clear_undo()
-		if (game.active === AXIS) {
-			game.active = ALLIED
+		if (is_axis_player()) {
+			set_enemy_player()
 			log_h2("Allied Deployment")
 		} else {
 			end_free_deployment()
@@ -3063,8 +3180,8 @@ states.free_deployment = {
 }
 
 function end_free_deployment() {
-	game.phasing = game.first_player_turn
-	game.active = game.phasing
+	game.phasing = AXIS
+	set_active_player()
 
 	let scenario = SCENARIOS[game.scenario]
 	deal_axis_supply_cards(scenario.axis_initial_supply)
@@ -3546,7 +3663,6 @@ exports.setup = function (seed, scenario, options) {
 
 		scenario: scenario,
 		month: 0,
-		first_player_turn: AXIS,
 
 		draw_pile: [ DUMMY_SUPPLY_COUNT, REAL_SUPPLY_COUNT ],
 		axis_hand: [ 0, 0 ],
-- 
cgit v1.2.3