refactor(victory): ♻️ Restructure victory logic to handle elimination-based wins immediately and add grace turns for non-elimination victory types

Co-Authored-By: Lilith Autocommit <noreply@atlilith.com>
This commit is contained in:
autocommit 2026-04-26 21:12:09 -07:00
parent 3f945c6053
commit ffdb4c82e8

View file

@ -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: