# 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` |
