# Allied Victory: Battle Resolution Flow How allied victories work end-to-end, from Shardok tactical resolution through Eagle strategic processing to player-facing aftermath decisions. ## 1. Battle Setup (Eagle -> Shardok) When Eagle sends a battle to Shardok via `RequestBattlesAction`, each player gets specific victory conditions: | Player | Victory Conditions | |-------------|-----------------------------------------------------------------------| | Attacker(s) | `HoldsCriticalTiles`, `LastPlayerStanding`, `LastAllianceStanding` | | Defender | `LastPlayerStanding`, `WinAfterMaxRounds` | This is set in `RequestBattlesAction.shardokPlayers`. Alliance information is sent separately in `PlayerSetupInfo.allies`. Eagle reads each player's `factionRelationships` and includes all non-HOSTILE factions as allies (`ShardokInterfaceGrpcClient.scala:75-80`). ## 2. Victory Condition Evaluation (Shardok) Victory conditions are checked in `UpdateGameStatusAction.cpp` in priority order: ### Check 1: LastPlayerStanding (any time) If exactly **one** player has surviving units and has `LastPlayerStanding`: - That player is the sole winner. - This applies to solo victories only (1 survivor). ### Check 2: LastAllianceStanding (any time) If **multiple** players survive, check if they form a winning alliance: - ALL survivors must have `LastAllianceStanding` condition - ALL survivors must be **mutually** allied (every pair checks both directions) - If yes: all survivors are winners This triggers for AssaultProvince battles when allied attackers eliminate all defenders. Since the defender does **not** have `LastAllianceStanding`, the check correctly requires that only the allied attackers remain alive. ### Check 3: WinAfterMaxRounds (end-of-round only) If the round counter exceeds `max_rounds`: - Find the player with `WinAfterMaxRounds` (always the defender) - That player wins, even if they have no surviving units - Eagle validates exactly 1 player has this condition (`internalRequire` in `RequestBattlesAction`) ### Check 4: HoldsCriticalTiles (end-of-round only) **Single-player castle control**: If one player with `HoldsCriticalTiles` occupies ALL critical tiles with hero-bearing units, that player wins. Additionally, any other player who: - Has `HoldsCriticalTiles` - Is **mutually** allied with the castle holder is also added to `winning_shardok_ids` as a co-winner. **Allied castle control**: If multiple players collectively occupy all critical tiles with hero-bearing units, AND: - All occupants have `HoldsCriticalTiles` - All occupants are mutually allied Then all occupants are winners together. ## 3. EndGameCondition Assignment Per Player (Shardok -> Eagle) Shardok assigns each player a `Victory` or `Loss`. All winners receive `Victory`; there is no `AllyVictory` distinction. The `EndGameCondition` proto reserves fields 2 (`ally_victory`) and 3 (`draw`) for backwards compatibility but Eagle rejects both. `GameOverResponsePopulator` validates that any player allied to a winner is also in `winning_shardok_ids`. If an ally is missing from the winners list, it throws an internal error. **Known issue**: This validation is too strict for `LastPlayerStanding`. When one allied attacker is the sole survivor, their dead co-attacker is allied to the winner but legitimately not in `winning_shardok_ids` (they're dead). The throw should be changed to `Loss` for dead allies, or the check should only apply when the ally has surviving units. | Condition | Criterion | |-----------|-----------| | `Victory(type)` | Player is in `winning_shardok_ids` | | `Loss(type)` | Player is not in `winning_shardok_ids` | The `type` is the VictoryCondition that caused the game to end (e.g., `HoldsCriticalTiles`). ### When do multiple players get Victory? In standard AssaultProvince battles: 1. **Allied attackers jointly hold all castles** -> both get `Victory(HoldsCriticalTiles)`. 2. **One attacker holds all castles alone, ally has `HoldsCriticalTiles` and is mutually allied** -> both get `Victory(HoldsCriticalTiles)`. The ally is added to `winning_shardok_ids` by `UpdateGameStatusAction`. 3. **Allied attackers eliminate all defenders** -> both get `Victory(LastAllianceStanding)`. This triggers because both have `LastAllianceStanding` and are mutually allied. 4. **One attacker holds all castles, co-attacker is NOT allied** -> only the castle holder gets `Victory(HoldsCriticalTiles)`; the non-allied co-attacker gets `Loss`. ## 4. Eagle Processes Battle Results `ResolveBattleAction.scala` receives `BattleResolution` containing each player's `EndGameCondition` and resolved units. ### Winner/Loser Partition Players are split by `isVictory` (line 383-386): - `winningResolvedPlayers`: Victory - `losingResolvedPlayers`: Loss ### The `unitReturned` Function (line 765-788) Determines which units go home vs stay at the battle province: | Unit Status | Returned? | Notes | |---------------|-----------|-------| | `Fled` | Always | Sent to their army's flee province | | `NeverEntered`| Conditional | Only if: attacker AND has flee province AND did NOT win (`!isVictory`) | | `Captured` | Never | | | `Normal` | Never | | | `Retreated` | Never | | | `Outlawed` | Never | Becomes unaffiliated hero | Key point: `NeverEntered` units from **winning** factions stay at the battle province rather than being sent home. This is important for bounced co-attackers who are allied with the actual winner. ### Withdrawn/Fled Unit Routing (line 690-706) For units that ARE returned, `armyFromResolvedArmy` builds a returning army with `fleeProvinceId = None`. If the army has no `fleeProvinceId`, it becomes "shattered" (units become outlaws in the battle province). If the original army had a `fleeProvinceId` set, returned units are sent there as incoming armies. ### Battle Resolution Branching (line 397-552) Three branches based on who won: #### Branch A: Defender Won Triggered when: `winningShardokPlayers.exists(_.isDefender)` - Executes `ProvinceHeldAction` - All non-defender, non-returned, non-outlawed units are **captured** - Province stays with the defending faction #### Branch B: Single Attacker Won Triggered when: exactly 1 attacking winner - Executes `ProvinceConqueredAction` immediately - Winning attacker's non-fled units occupy the province - Losing defenders' non-fled units are captured - Province transfers to the attacker #### Branch C: Multiple Allied Attackers Won Triggered when: 2+ attacking winners - Executes `MultiVictorBattleSetupAction` - Creates `PendingConquestInfo` with aftermath claimants - Province enters the **Battle Aftermath** phase ## 5. Battle Aftermath (Multi-Victor Only) When multiple allied attackers win, the province enters an aftermath decision phase where players decide who keeps it. ### Claimant Setup Each attacking winner becomes an `AftermathClaimant` with: - `units`: their non-fled, non-outlawed units still at the battle province - `armySize`: total troop count of those units - `broughtGold` / `broughtFood`: supplies their armies carried Claimants are **sorted by army size descending** (largest army decides first). ### Decision Phase Claimants are presented with a choice **one at a time**, in order: **If no one has chosen "Keep" yet:** - **Keep Province**: claim the province for your faction - **Withdraw To**: pick an adjacent province to march your army to, optionally carrying up to the gold/food you brought **If someone already chose "Keep":** - **Withdraw To** is the only option (can't have two keepers) **Last undecided claimant:** - If no one has chosen "Keep" yet, the last claimant **automatically keeps** (no command shown, resolved by `AutoResolveBattleAftermathAction`) - This guarantees someone always claims the province ### Finalization Once all claimants have decided (`FinalizeAftermathAction`): 1. The keeper's units run through `ProvinceConqueredAction` -- province transfers to their faction 2. Withdrawing claimants' units become incoming armies to their chosen adjacent province, arriving next round, carrying the specified supplies 3. `PendingConquestInfo` is cleared from the province ## 6. Summary: Who Gets What ### Two allied attackers jointly hold all castles - Shardok: Both get `Victory(HoldsCriticalTiles)` (both in `winning_shardok_ids`) - Eagle: Both are attacking winners -> MultiVictorBattleSetupAction - Players: Largest army picks first: Keep or Withdraw. Last player auto-keeps if no one chose Keep. ### One attacker holds all castles, allied attacker doesn't - Shardok: Castle-holder gets `Victory(HoldsCriticalTiles)`. Mutually-allied co-attacker with `HoldsCriticalTiles` is added to `winning_shardok_ids` -> also gets `Victory(HoldsCriticalTiles)`. - Eagle: Both are attacking winners -> MultiVictorBattleSetupAction -> Battle Aftermath. - Players: Largest army picks first: Keep or Withdraw. ### Allied attackers eliminate all defenders - Shardok: Both attackers have `LastAllianceStanding` and are mutually allied -> both get `Victory(LastAllianceStanding)`. - Eagle: Both are attacking winners -> MultiVictorBattleSetupAction -> Battle Aftermath. - Players: Largest army picks first: Keep or Withdraw. ### Non-allied co-attackers: one holds all castles - Shardok: Castle-holder gets `Victory(HoldsCriticalTiles)`, non-allied co-attacker gets `Loss(HoldsCriticalTiles)`. - Eagle: Single attacker won -> ProvinceConqueredAction. Non-allied co-attacker's units are captured along with the defender's. ### One allied attacker is sole survivor (LastPlayerStanding) - Shardok: Sole survivor gets `Victory(LastPlayerStanding)`. Dead ally should get `Loss`. - **BUG**: `GameOverResponsePopulator` currently throws because dead ally is allied to winner but not in `winning_shardok_ids`. Needs fix — see known issue in Section 3. - Eagle (once fixed): Single attacker won -> ProvinceConqueredAction. Dead ally's units were already destroyed in battle. ### Single attacker wins (any condition, no co-attackers) - No alliance considerations. ProvinceConqueredAction immediately. ### Defender wins (WinAfterMaxRounds or LastPlayerStanding) - ProvinceHeldAction. All non-returned attacker units are captured. ## 7. Edge Cases and Known Issues ### BUG: Dead ally causes crash via LastPlayerStanding When one allied attacker is the sole survivor (both the co-attacker and defender are dead), `LastPlayerStanding` fires and only the survivor is in `winning_shardok_ids`. The `GameOverResponsePopulator` then sees the dead co-attacker is allied to the winner but not in `winning_shardok_ids`, and throws `ShardokInternalErrorException`. Fix: dead allies should receive `Loss`, not trigger a validation error. ### Non-allied co-attackers eliminate all defenders Two non-allied attackers who both survive after eliminating the defender cannot win via `LastAllianceStanding` (they aren't mutually allied). The game continues until max rounds, at which point the defender wins via `WinAfterMaxRounds`. They must capture all critical tiles. ### Non-allied co-attackers: one holds all castles Only the castle holder wins. The non-allied co-attacker gets `Loss` and their units are captured along with the defender's. The mutual-alliance check in `UpdateGameStatusAction` prevents non-allied players from being added to `winning_shardok_ids`. ## Key File References | Component | File | |-----------|------| | Victory condition checking | `src/main/cpp/net/eagle0/shardok/library/actions/UpdateGameStatusAction.cpp` | | Per-player EndGameCondition | `src/main/cpp/net/eagle0/shardok/server/GameOverResponsePopulator.cpp` | | Battle setup (victory conditions) | `src/main/scala/.../library/actions/impl/action/RequestBattlesAction.scala` | | Alliance communication | `src/main/scala/.../shardok_interface/ShardokInterfaceGrpcClient.scala:75-80` | | Battle result processing | `src/main/scala/.../library/actions/impl/action/ResolveBattleAction.scala` | | Unit return logic | `ResolveBattleAction.scala:765-788` (`unitReturned`) | | Multi-victor setup | `src/main/scala/.../library/actions/impl/action/MultiVictorBattleSetupAction.scala` | | Aftermath availability | `src/main/scala/.../library/actions/availability/AvailableBattleAftermathDecisionCommandFactory.scala` | | Aftermath command | `src/main/scala/.../library/actions/impl/command/BattleAftermathDecisionCommand.scala` | | Auto-resolve last claimant | `src/main/scala/.../library/actions/impl/action/AutoResolveBattleAftermathAction.scala` | | Finalize aftermath | `src/main/scala/.../library/actions/impl/action/FinalizeAftermathAction.scala` | | EndGameCondition enum | `src/main/scala/.../model/state/shardok_battle/EndGameCondition.scala` |