From b4b2b86d439e6bac48ab119b23d683c33988772f Mon Sep 17 00:00:00 2001
From: Tor Andersson <tor@ccxvii.net>
Date: Tue, 28 May 2024 13:14:04 +0200
Subject: offensive option

---
 play.js  |  16 +++--
 rules.js | 209 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++-------
 2 files changed, 199 insertions(+), 26 deletions(-)

diff --git a/play.js b/play.js
index be86670..04a75c8 100644
--- a/play.js
+++ b/play.js
@@ -147,9 +147,14 @@ function has_eased_victory(power) {
 }
 
 function count_total_objectives(pow) {
-	if (has_eased_victory(pow))
-		return objective1[pow].length
-	return objective1[pow].length + objective2[pow].length
+	let n = objective1[pow].length
+	if (!has_eased_victory(pow))
+		n += objective2[pow].length
+	if (pow === P_PRUSSIA && !view.oo)
+		n = 0
+	if (pow === P_AUSTRIA && view.oo)
+		n -= 4
+	return n
 }
 
 /* CARD TEXTS */
@@ -890,11 +895,12 @@ function on_update() {
 		ui.turns[i].classList.toggle("hide", (typeof view.fate === "object") || (i + 1 < view.fate))
 
 	for (let pow = 0; pow < 7; ++pow) {
-		// let banner = `${power_name[pow]} \u2013 ${view.pt[pow]}/${max_power_troops[pow]} troops`
 		let banner = `${power_name[pow]} \u2014 ${view.pt[pow]} troops`
 		let m_obj = count_total_objectives(pow)
 		if (m_obj > 0) {
 			let n_obj = count_captured_objectives(pow)
+			if (pow === P_AUSTRIA && view.oo)
+				m_obj += "*"
 			banner += ` \u2014 ${n_obj} of ${m_obj} objectives`
 		}
 
@@ -920,6 +926,8 @@ function on_update() {
 	}
 
 	ui.clock_of_fate.replaceChildren()
+	if (view.oo > 0)
+		ui.clock_of_fate.appendChild(ui.tc[view.oo])
 	ui.clock_of_fate.appendChild(ui.fate[0])
 	if (typeof view.fate === "object")
 		for (let c of view.fate)
diff --git a/rules.js b/rules.js
index ea98774..669135a 100644
--- a/rules.js
+++ b/rules.js
@@ -152,6 +152,7 @@ const all_power_depots = [
 	find_city_list([ "Koblenz", "Gemünden" ]),
 ]
 
+const COORDINATE_LINE_5 = 935
 const MUNSTER_Y = data.cities.y[find_city("Munster")]
 const HALLE = find_city("Halle")
 const KUSTRIN = find_city("Küstrin")
@@ -317,6 +318,17 @@ function is_reserve(c) {
 	return to_suit(c) === RESERVE
 }
 
+function is_south_of_line_5(s) {
+	return data.cities.y[s] > COORDINATE_LINE_5
+}
+
+function is_west_of(here, there) {
+	let dx = data.cities.x[there] - data.cities.x[here]
+	let dy = data.cities.y[there] - data.cities.y[here]
+	// more west than north/south
+	return dx < 0 && Math.abs(dx) >= Math.abs(dy)
+}
+
 function format_cards(list) {
 	return list.map(c => is_reserve(c) ? suit_name[RESERVE] : to_value(c) + suit_name[to_suit(c)]).join(", ")
 }
@@ -404,7 +416,11 @@ make_protect(P_HANOVER, data.country.Hanover)
 make_protect(P_AUSTRIA, data.country.Austria)
 
 function is_conquest_space(pow, s) {
-	// TODO: prussian offensive option
+	if (pow === P_PRUSSIA) {
+		if (is_offensive_option() || game.turn < 3)
+			return set_has(primary_objective[pow], s)
+		return false
+	}
 	if (has_eased_victory(pow))
 		return set_has(primary_objective[pow], s)
 	return set_has(full_objective[pow], s)
@@ -452,9 +468,14 @@ function remove_secondary_objectives(power) {
 		set_delete(game.conquest, s)
 }
 
+function remove_offensive_option_objectives() {
+	for (let s of primary_objective[P_PRUSSIA])
+		set_delete(game.conquest, s)
+}
+
 /* STATE */
 
-function turn_power_draw() {
+function tc_per_turn() {
 	let n = 0
 
 	if (game.scenario === 1 && game.power === P_PRUSSIA) {
@@ -508,6 +529,15 @@ function turn_power_draw() {
 	return n
 }
 
+function is_offensive_option() {
+	return !!game.oo
+}
+
+function is_offensive_option_failed() {
+	// if Austria has picked up the card AND subsidy reduction event has triggered
+	return game.oo < 0 && (set_has(game.fate, FC_POEMS) || set_has(game.fate, FC_LORD_BUTE))
+}
+
 function has_power_dropped_out(pow) {
 	if (game.scenario === 1)
 		return pow !== P_PRUSSIA && pow !== P_HANOVER && pow !== P_FRANCE
@@ -843,6 +873,9 @@ function goto_action_stage() {
 function end_action_stage() {
 	clear_undo()
 
+	if (check_offensive_option_victory())
+		return
+
 	if (++game.step === 7)
 		goto_clock_of_fate()
 	else
@@ -858,7 +891,31 @@ function has_conquered_all_of(list) {
 	return true
 }
 
+function has_conquered_one_of(list) {
+	for (let s of list)
+		if (set_has(game.conquest, s))
+			return true
+	return false
+}
+
+function check_offensive_option_victory() {
+	if (game.power === P_PRUSSIA && !is_offensive_option_failed()) {
+		if (has_conquered_all_of(primary_objective[P_PRUSSIA])) {
+			goto_game_over(R_FREDERICK, "Prussia won with offensive option.")
+			return true
+		}
+	}
+	return false
+}
+
 function check_power_victory(victory, city_list, power) {
+	if (power === P_AUSTRIA && is_offensive_option()) {
+		let n = count_captured_objectives(power)
+		if (n >= city_list.length - 4 && has_conquered_one_of(data.country.Saxony))
+			set_add(victory, power)
+		return
+	}
+		
 	if (has_conquered_all_of(city_list[power]))
 		set_add(victory, power)
 }
@@ -920,6 +977,14 @@ function has_eased_victory(power) {
 	return false
 }
 
+function count_captured_objectives(pow) {
+	let n = 0
+	for (let s of full_objective[pow])
+		if (set_has(view.conquest, s))
+			++n
+	return n
+}
+
 function check_victory_4() {
 	// Prussian victory
 	if (has_russia_dropped_out() && has_sweden_dropped_out() && has_france_dropped_out()) {
@@ -969,6 +1034,8 @@ function next_tactics_deck() {
 	if (game.draw)
 		for (let c of game.draw)
 			held[to_deck(c)]++
+	if (game.oo > 0)
+		held[to_deck(game.oo)]++
 
 	// find next unused deck
 	for (let i = 1; i < 5; ++i) {
@@ -1020,13 +1087,14 @@ function draw_tc(n) {
 		--n
 	}
 
+	if (k > 0)
+		log("Drew " + k + " TC.")
+
 	return draw
 }
 
 function goto_tactical_cards() {
-	let n = turn_power_draw()
-
-	game.draw = draw_tc(turn_power_draw())
+	game.draw = draw_tc(tc_per_turn())
 
 	if (should_power_discard_tc() && game.draw.length > 0)
 		game.state = "tactical_cards_discard"
@@ -1087,6 +1155,11 @@ function end_tactical_cards() {
 		set_add(game.hand[game.power], c)
 	delete game.draw
 
+	if (game.scenario >= 3 && game.turn === 3 && game.power === P_PRUSSIA) {
+		goto_declare_offensive_option()
+		return
+	}
+
 	// MARIA: supply is before movement
 
 	goto_movement()
@@ -1765,6 +1838,11 @@ function can_re_enter_supply_train(s) {
 function goto_recruit() {
 	game.count = 0
 
+	if (!can_recruit_anything_in_theory()) {
+		end_recruit()
+		return
+	}
+
 	game.recruit = {
 		pool: [],
 		used: [],
@@ -1773,12 +1851,6 @@ function goto_recruit() {
 		troops: 0,
 	}
 
-	// TODO: reveal too much if we skip recruitment phase?
-	if (!can_recruit_anything()) {
-		end_recruit()
-		return
-	}
-
 	// if all depots have enemy pieces, choose ONE city in given sector and COST is 8
 	if (has_available_depot())
 		game.state = "recruit"
@@ -1818,6 +1890,11 @@ function is_attack_position(s) {
 	return false
 }
 
+function can_recruit_anything_in_theory() {
+	let unused_everywhere = max_power_troops(game.power) - count_used_troops()
+	return unused_everywhere > 0 || count_eliminated_trains() > 0
+}
+
 function can_recruit_anything() {
 	let unused_everywhere = max_power_troops(game.power) - count_used_troops()
 	let elim_trains = count_eliminated_trains()
@@ -2289,7 +2366,7 @@ function play_card(c, sign) {
 
 function play_reserve(v, sign) {
 	if (fate_card_zero()) {
-		log(">" + POWER_NAME[game.power] + " 0R to " + (sign * game.count))
+		log(">" + POWER_NAME[game.power] + " 0 C" + C + " to " + (sign * game.count))
 		clear_fate_effect()
 		return
 	}
@@ -2299,9 +2376,9 @@ function play_reserve(v, sign) {
 	else
 		game.count += v
 	if (bonus > 0)
-		log(">" + POWER_NAME[game.power] + " " + (v-bonus) + "R +" + bonus + " to " + (sign * game.count))
+		log(">" + POWER_NAME[game.power] + " " + (v-bonus) + " C" + c + " + " + bonus + " to " + (sign * game.count))
 	else
-		log(">" + POWER_NAME[game.power] + " " + (v) + "R to " + (sign * game.count))
+		log(">" + POWER_NAME[game.power] + " " + (v) + " C" + c + " to " + (sign * game.count))
 	if (bonus > 0)
 		clear_fate_effect()
 }
@@ -2420,6 +2497,13 @@ function set_active_winner() {
 		set_active_defender()
 }
 
+function set_active_loser() {
+	if (game.count > 0)
+		set_active_defender()
+	else
+		set_active_attacker()
+}
+
 function remove_stack_from_combat(s) {
 	for (let i = game.combat.length - 2; i >= 0; i -= 2)
 		if (game.combat[i] === s || game.combat[i + 1] === s)
@@ -2431,6 +2515,8 @@ function goto_retreat() {
 	let hits = lost
 
 	let loser = get_loser()
+	let loser_power = get_stack_power(loser)
+	let winner_power = get_stack_power(get_winner())
 
 	// no more fighting for the loser
 	remove_stack_from_combat(loser)
@@ -2452,7 +2538,14 @@ function goto_retreat() {
 		}
 	}
 
-	log("P" + get_supreme_commander(loser) + " lost " + (lost-hits) + " troops.")
+	log(POWER_NAME[loser_power] + " lost " + (lost-hits) + " troops.")
+
+	// OO and Prussia loses vs Austria with at least -3
+	if (game.oo > 0 && loser_power === P_PRUSSIA && winner_power === P_AUSTRIA && lost >= 3) {
+		set_active_to_power(P_AUSTRIA)
+		game.state = "pick_up_oo_card_after_retreat"
+		return
+	}
 
 	resume_retreat()
 }
@@ -2837,6 +2930,10 @@ states.supply_eliminate = {
 	},
 	piece(x) {
 		let s = game.pos[x]
+
+		if (game.oo > 0 && game.power === P_PRUSSIA && is_south_of_line_5(s))
+			game.pick_up_oo = 1
+
 		for (let p of all_power_generals[game.power])
 			if (game.pos[p] === s)
 				eliminate_general(p)
@@ -2881,6 +2978,12 @@ states.supply_done = {
 function end_supply() {
 	delete game.supply
 
+	if (game.pick_up_oo) {
+		set_active_to_power(P_AUSTRIA)
+		game.state = "pick_up_oo_card_after_supply"
+		return
+	}
+
 	end_action_stage()
 }
 
@@ -3544,13 +3647,6 @@ states.laudon_done = {
 	},
 }
 
-function is_west_of(here, there) {
-	let dx = data.cities.x[there] - data.cities.x[here]
-	let dy = data.cities.y[there] - data.cities.y[here]
-	// more west than north/south
-	return dx < 0 && Math.abs(dx) >= Math.abs(dy)
-}
-
 states.prussia_may_move_hildburghausen_2_cities_westwards = {
 	inactive: "move Hildburghausen two cities westwards",
 	prompt() {
@@ -3692,6 +3788,71 @@ states.move_to_any_empty_adjacent_city = {
 	},
 }
 
+/* OFFENSIVE OPTION */
+
+function goto_declare_offensive_option() {
+	set_active_to_power(P_PRUSSIA)
+	game.state = "declare_offensive_option"
+}
+
+states.declare_offensive_option = {
+	inactive: "declare offensive option",
+	prompt() {
+		prompt("You may use the Offensive Option by setting aside a TC.")
+		for (let c of game.hand[game.power])
+			gen_action_card(c)
+		view.actions.pass = 1
+	},
+	card(c) {
+		push_undo()
+		log_br()
+		log("Declared Offensive Option.")
+		log("Set aside C" + c + " for Austria.")
+		game.oo = c
+		goto_movement()
+	},
+	pass() {
+		push_undo()
+		remove_offensive_option_objectives()
+		goto_movement()
+	},
+}
+
+states.pick_up_oo_card_after_retreat = {
+	inactive: "pick up set-aside TC",
+	prompt() {
+		prompt("Pick up the set-aside TC.")
+		gen_action_card(game.oo)
+	},
+	card(_) {
+		log_br()
+		log("Austria picked up set-aside C" + game.oo + ".")
+		set_add(game.hand[P_AUSTRIA], game.oo)
+		game.oo = -1
+
+		// control back to loser for retreat
+		set_active_loser()
+		resume_retreat()
+	},
+}
+
+states.pick_up_oo_card_after_supply = {
+	inactive: "pick up set-aside TC",
+	prompt() {
+		prompt("Pick up the set-aside TC.")
+		gen_action_card(game.oo)
+	},
+	card(_) {
+		log_br()
+		log("Austria picked up set-aside C" + game.oo + ".")
+		set_add(game.hand[P_AUSTRIA], game.oo)
+		game.oo = -1
+
+		delete game.pick_up_oo
+		end_action_stage()
+	},
+}
+
 /* SETUP */
 
 const POWER_FROM_SETUP_STEP_4 = [
@@ -3836,6 +3997,8 @@ function make_tactics_deck(n) {
 
 function make_tactics_discard(n) {
 	return make_tactics_deck(n).filter(c => {
+		if (c === game.oo)
+			return false
 		if (game.draw && set_has(game.draw, c))
 			return false
 		for (let pow of all_powers)
@@ -3860,6 +4023,7 @@ exports.setup = function (seed, scenario, options) {
 		step: 0,
 		clock: null,
 		fate: [],
+		oo: 0, // offensive option
 		vg: 0, // last victorious general for fate effect selection
 		fx: 0, // current card of fate effect
 		deck: null,
@@ -4081,6 +4245,7 @@ exports.view = function (state, player) {
 		conquest: game.conquest,
 		troops: mask_troops(player),
 		hand: mask_hand(player),
+		oo: game.oo,
 		pt: total_troops_list(),
 
 		power: game.power,
-- 
cgit v1.2.3