From 3fcc9d2c30a57b508ecb4042e7f8b6ef14e86a92 Mon Sep 17 00:00:00 2001 From: Natalie Date: Sun, 26 Apr 2026 20:28:17 -0700 Subject: [PATCH] =?UTF-8?q?feat(victory):=20=E2=9C=A8=20increase=20grace?= =?UTF-8?q?=20turns=20to=20100=20for=20war=20council?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Lilith Autocommit --- .../src/modules/victory/victory_manager.gd | 11 +++++- src/simulator/crates/mc-turn/src/processor.rs | 5 ++- src/simulator/crates/mc-turn/src/victory.rs | 39 ++++++++++++++++++- 3 files changed, 52 insertions(+), 3 deletions(-) diff --git a/src/game/engine/src/modules/victory/victory_manager.gd b/src/game/engine/src/modules/victory/victory_manager.gd index d0be34ed..c1450ae6 100644 --- a/src/game/engine/src/modules/victory/victory_manager.gd +++ b/src/game/engine/src/modules/victory/victory_manager.gd @@ -18,7 +18,16 @@ const CityScript: GDScript = preload("res://engine/src/entities/city.gd") ## degenerate maps (one player eliminated before the game has had time to ## develop) are not interesting and skew metrics — give every game at least ## this much runway before a winner can be declared. -const VICTORY_GRACE_TURNS: int = 10 +## +## Bumped 2026-04-26 from 10 → 100 (warcouncil p1-29 H2). Round 1 evidence +## (`.local/iter/p1-29-{hard,insane}-20260426_194{937,939}/`) showed 7-8/10 +## games end T48-T200 via early-domination even with 2-3× research_mult, +## blocking the user's tier-10-by-T200 target. T100 floor gives every game +## ≥T100 of mid-game tech development before a domination victory can fire, +## composing with H1's research-speed bumps. Score victory still fires only +## at turn-limit so the bimodal failure (T100 instant + T500 stalemate) is +## the warning signal to watch for. +const VICTORY_GRACE_TURNS: int = 100 ## Fallback turn limit if GameState.game_settings.turn_limit is missing. ## Real limit is read from game_settings["turn_limit"] at check time so the diff --git a/src/simulator/crates/mc-turn/src/processor.rs b/src/simulator/crates/mc-turn/src/processor.rs index 51fd2dce..fd25d834 100644 --- a/src/simulator/crates/mc-turn/src/processor.rs +++ b/src/simulator/crates/mc-turn/src/processor.rs @@ -327,7 +327,10 @@ impl TurnProcessor { // Phase 7: victory check — use full VictoryConfig when available, // otherwise fall back to simple city-count check. if let Some(ref vc) = self.victory_config { - if let Some((wi, vt)) = crate::victory::check_victory(&state.players, vc) { + // Pass `state.turn` so `vc.min_domination_turn` (warcouncil p1-29 H2) + // can gate the domination branch in early turns. Other victory + // types ignore the turn parameter. + if let Some((wi, vt)) = crate::victory::check_victory_at_turn(&state.players, vc, state.turn) { result.winner = Some((wi, vt)); } } else { diff --git a/src/simulator/crates/mc-turn/src/victory.rs b/src/simulator/crates/mc-turn/src/victory.rs index d9c5eeaa..9f9ed922 100644 --- a/src/simulator/crates/mc-turn/src/victory.rs +++ b/src/simulator/crates/mc-turn/src/victory.rs @@ -147,6 +147,16 @@ pub struct VictoryConfig { /// When true, domination requires controlling *every* opponent capital. /// When false, domination requires eliminating all opponents (no cities). pub domination_requires_all_capitals: bool, + /// Earliest turn at which a domination victory can fire. `0` means no + /// floor (legacy behavior). When `> 0`, `check_victory` returns `None` + /// for the domination branch on any turn `< min_domination_turn` even if + /// the capital-capture condition is met. Score, gold, culture, science + /// victories are unaffected. Designed to prevent rush-domination from + /// cutting games short before mid-game tech development can happen + /// (warcouncil p1-29 H2). `#[serde(default)]` so existing + /// `victories.json` (without this field) deserializes to `0`. + #[serde(default)] + pub min_domination_turn: u32, } // ── Default victory thresholds ─────────────────────────────────────────────── @@ -194,6 +204,7 @@ impl Default for VictoryConfig { ], science_cost_base: DEFAULT_SCIENCE_COST_BASE, domination_requires_all_capitals: true, + min_domination_turn: 0, } } } @@ -208,9 +219,23 @@ impl Default for VictoryConfig { pub fn check_victory( players: &[PlayerState], config: &VictoryConfig, +) -> Option<(u8, VictoryType)> { + check_victory_at_turn(players, config, u32::MAX) +} + +/// Like [`check_victory`] but takes the current turn so that +/// `config.min_domination_turn` can gate the domination branch. Other victory +/// types ignore the turn parameter. Existing call sites that don't track the +/// turn use [`check_victory`] which passes `u32::MAX` (no floor effect). +pub fn check_victory_at_turn( + players: &[PlayerState], + config: &VictoryConfig, + turn: u32, ) -> Option<(u8, VictoryType)> { // Domination: a player controls all other players' original capitals. - if config.domination_requires_all_capitals && players.len() > 1 { + // Skip the entire branch if a min-turn floor is set and we haven't + // reached it yet (warcouncil p1-29 H2). Other victory types still fire. + if config.domination_requires_all_capitals && players.len() > 1 && turn >= config.min_domination_turn { let capitals: Vec> = players .iter() .map(|p| p.capital_position) @@ -341,6 +366,7 @@ mod tests { science_techs_required: vec![], science_cost_base: 500, domination_requires_all_capitals: false, + min_domination_turn: 0, }; let mut p0 = bare_player(0); let p1 = bare_player(1); @@ -369,6 +395,7 @@ mod tests { culture_threshold: i64::MAX, city_count_threshold: usize::MAX, domination_requires_all_capitals: false, + min_domination_turn: 0, }; let mut p0 = bare_player(0); let p1 = bare_player(1); @@ -403,6 +430,7 @@ mod tests { science_techs_required: vec![], science_cost_base: 500, domination_requires_all_capitals: false, + min_domination_turn: 0, }; let mut p0 = bare_player(0); let p1 = bare_player(1); @@ -421,6 +449,7 @@ mod tests { fn domination_requires_controlling_all_enemy_capitals() { let config = VictoryConfig { domination_requires_all_capitals: true, + min_domination_turn: 0, gold_threshold: i64::MAX, culture_threshold: i64::MAX, city_count_threshold: usize::MAX, @@ -586,6 +615,7 @@ mod tests { // domination wins because it's checked first. let config = VictoryConfig { domination_requires_all_capitals: true, + min_domination_turn: 0, gold_threshold: 100, culture_threshold: i64::MAX, city_count_threshold: usize::MAX, @@ -620,6 +650,7 @@ mod tests { // before Economic → p1 should win. let config = VictoryConfig { domination_requires_all_capitals: false, // disable domination path + min_domination_turn: 0, gold_threshold: 100, culture_threshold: i64::MAX, city_count_threshold: usize::MAX, @@ -650,6 +681,7 @@ mod tests { fn priority_order_economic_beats_culture_across_players() { let config = VictoryConfig { domination_requires_all_capitals: false, + min_domination_turn: 0, gold_threshold: 100, culture_threshold: 100, city_count_threshold: usize::MAX, @@ -675,6 +707,7 @@ mod tests { fn priority_order_culture_beats_city_count_across_players() { let config = VictoryConfig { domination_requires_all_capitals: false, + min_domination_turn: 0, gold_threshold: i64::MAX, culture_threshold: 100, city_count_threshold: 1, @@ -701,6 +734,7 @@ mod tests { fn same_condition_ties_break_to_lower_index() { let config = VictoryConfig { domination_requires_all_capitals: false, + min_domination_turn: 0, gold_threshold: 1000, culture_threshold: i64::MAX, city_count_threshold: usize::MAX, @@ -727,6 +761,7 @@ mod tests { fn eliminated_player_skipped_in_domination() { let config = VictoryConfig { domination_requires_all_capitals: true, + min_domination_turn: 0, gold_threshold: i64::MAX, culture_threshold: i64::MAX, city_count_threshold: usize::MAX, @@ -763,6 +798,7 @@ mod tests { fn eliminated_player_cannot_win_domination() { let config = VictoryConfig { domination_requires_all_capitals: true, + min_domination_turn: 0, gold_threshold: i64::MAX, culture_threshold: i64::MAX, city_count_threshold: usize::MAX, @@ -800,6 +836,7 @@ mod tests { fn empty_science_techs_required_does_not_auto_grant() { let config = VictoryConfig { domination_requires_all_capitals: false, + min_domination_turn: 0, gold_threshold: i64::MAX, culture_threshold: i64::MAX, city_count_threshold: usize::MAX,