Add a terminal defeat state when the player loses every town - #6
Open
Dbhardwaj99 wants to merge 2 commits into
Open
Add a terminal defeat state when the player loses every town#6Dbhardwaj99 wants to merge 2 commits into
Dbhardwaj99 wants to merge 2 commits into
Conversation
Enemy raids can capture the player's last town, but nothing noticed. `sanitizeSelection()` and `runEnemyTurn` both look for the "next" player town and quietly do nothing when there is none, leaving `activeTownID` pointing at an enemy town — `activeTown` then falls back to `towns[0]` and the player keeps building in a town they do not own, forever. Every faction change funnels through `sanitizeSelection()`, so the check goes there: no player towns means `.defeat`, which stops the clock and dismisses any open sheet (a day can roll over while the build menu is up). `tick()` stops advancing days once the phase leaves `.town`. DefeatView mirrors VictoryView. The test target now also compiles ViewModels so the phase transition can be covered. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Three acceptance criteria were unmet: - "Automatic ticks and manual Next cannot advance or mutate a terminal campaign" — tick() was guarded but advanceDayManually() was not, so the Next Day button still rolled days over on a finished campaign. - "Present a clear loss state with a New Campaign action" — DefeatView now takes an onNewCampaign closure, wired to ContentView.startGame. - "A deterministic test ... proves a later advanceDay attempt leaves state unchanged" — the test now snapshots GameState after defeat and asserts both advanceDayManually() and tick() leave it byte-identical. Victory behavior is untouched, as the ticket requires. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes QUE-5
The bug
Enemy realms capture player towns in
runEnemyTurn, but losing the last one was never a state the game recognised. Two places fix upactiveTownIDafter a capture —GameViewModel.sanitizeSelection()andGameRules.runEnemyTurn— and both look for the "next" player-controlled town and quietly do nothing when there is none.activeTownIDis then left pointing at a town the player no longer owns, andactiveTownfalls back tostate.towns[0]. The player carries on building, training and taking turns in someone else's town, indefinitely..victorywas the only terminal phase.The fix
The player can only lose towns through an enemy capture, and every capture reaches the view model via
advanceDay→sanitizeSelection(). That single funnel is where the check goes, so both the timer path (tick) and the manual path (advanceDayManually) are covered by one guard rather than one per caller.GamePhase.defeat, andDefeatViewmirroringVictoryView.concedeCampaign()stops the clock and dismisses any open sheet — a day can roll over while the build menu is open, which is a real scenario for defeat (it never was for victory, which only fires from an explicit attack tap).tick()stops advancing days once the phase leaves.town.Testing
losingTheLastTownEndsTheCampaigndrives a realGameRules.resolveAttackcapture of the player's only town and asserts the phase turns over. Verified it fails without the guard (Expectation failed: (viewModel.phase → .town) == .defeat) and passes with it.The test target previously synced only
Models+Tests, soGameViewModelwas out of scope; it now also syncsViewModels, which depends on nothing beyond Foundation, Observation andModels.Note on the base branch
Targets
codex/campaign-integrityas requested. That branch is currently at the same commit asmain, so this can be retargeted atmainwith no conflicts if that is preferred.🤖 Generated with Claude Code