From ffdb4c82e8c07616a785e384c788534776a997ed Mon Sep 17 00:00:00 2001 From: autocommit Date: Sun, 26 Apr 2026 21:12:09 -0700 Subject: [PATCH] =?UTF-8?q?refactor(victory):=20=E2=99=BB=EF=B8=8F=20Restr?= =?UTF-8?q?ucture=20victory=20logic=20to=20handle=20elimination-based=20wi?= =?UTF-8?q?ns=20immediately=20and=20add=20grace=20turns=20for=20non-elimin?= =?UTF-8?q?ation=20victory=20types?= 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 | 50 +++++++++++++++---- 1 file changed, 40 insertions(+), 10 deletions(-) diff --git a/src/game/engine/src/modules/victory/victory_manager.gd b/src/game/engine/src/modules/victory/victory_manager.gd index c1450ae6..4ed9ad92 100644 --- a/src/game/engine/src/modules/victory/victory_manager.gd +++ b/src/game/engine/src/modules/victory/victory_manager.gd @@ -50,14 +50,30 @@ func _resolved_max_turns() -> int: func check_all(_game_map: RefCounted) -> void: if _game_over: return - if GameState.turn_number < VICTORY_GRACE_TURNS: + + # Elimination ALWAYS fires regardless of grace — once a player is the only + # one alive, the game is structurally over (eliminated players can't + # recover). Forcing surviving players to keep playing past elimination + # until the grace turn just stalls games (warcouncil p1-29 H2 v1 bug: + # T42 elimination + 100-turn grace → games stalled in_progress until + # safety_timeout). Elimination victories are inherently a sign that + # rush-domination already won; no game-development purpose served by + # delay. + var elim_winner: int = _check_elimination_winner() + if elim_winner >= 0: + _game_over = true + EventBus.victory_achieved.emit(elim_winner, "domination") return - var winner_index: int = _check_domination() - if winner_index >= 0: - _game_over = true - EventBus.victory_achieved.emit(winner_index, "domination") - return + # Capture-all-capitals victory IS gated by grace turns. Slows + # rush-domination so games reach mid-game tech development before a + # captor can secure all capitals. + if GameState.turn_number >= VICTORY_GRACE_TURNS: + var winner_index: int = _check_capture_winner() + if winner_index >= 0: + _game_over = true + EventBus.victory_achieved.emit(winner_index, "domination") + return # Score fallback: at max turns, award the highest-scoring player. if GameState.turn_number >= _resolved_max_turns(): @@ -68,9 +84,10 @@ func check_all(_game_map: RefCounted) -> void: return -## Domination: a player owns every opponent's original capital, OR is the -## last player with at least one city (elimination fallback). -func _check_domination() -> int: +## Capture-all-capitals: a player owns every opponent's original capital. +## Gated by VICTORY_GRACE_TURNS — early-game capture wins before mid-game +## tech development are blocked. +func _check_capture_winner() -> int: var capital_owner_by_player: Dictionary = {} # player_index → current city.owner for player: Variant in GameState.players: if not player is PlayerScript: @@ -94,8 +111,12 @@ func _check_domination() -> int: break if owns_all_other_capitals and candidate.cities.size() > 0: return idx + return -1 - # Elimination fallback. + +## Elimination: only one player has cities or a living founder. Always +## eligible regardless of grace — see check_all() docstring. +func _check_elimination_winner() -> int: var alive_players: Array[int] = [] for player: Variant in GameState.players: if not player is PlayerScript: @@ -111,6 +132,15 @@ func _check_domination() -> int: return -1 +## Legacy combined check kept for any external callers that referenced the +## previous API. Delegates to the split functions; respects no grace. +func _check_domination() -> int: + var capture_winner: int = _check_capture_winner() + if capture_winner >= 0: + return capture_winner + return _check_elimination_winner() + + func _has_living_founder(player: RefCounted) -> bool: for unit: Variant in player.units: if unit is UnitScript and unit.is_alive() and unit.can_found_city: