Compare commits

...
Author SHA1 Message Date
adminandClaude Opus 4.6 3a780bf2cb Document allied victory battle resolution flow
End-to-end documentation of how allied victories work from Shardok
tactical resolution through Eagle strategic processing to aftermath
decisions. Covers victory condition evaluation, EndGameCondition
assignment, unit routing, and multi-victor aftermath phase. Flags
three potential issues for future consideration.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-08 08:40:53 -07:00
+267
View File
@@ -0,0 +1,267 @@
# 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` |
| Defender | `LastPlayerStanding`, `WinAfterMaxRounds` |
Attackers do **not** receive `LastAllianceStanding`. 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
**In practice, this never triggers for AssaultProvince battles** because attackers don't receive
`LastAllianceStanding`. It exists for other battle types.
### 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
- If no player has this condition: the Shardok proto produces a Draw, but Eagle rejects draws
(the `internalRequire` in `RequestBattlesAction` guarantees exactly 1 player has this condition)
### 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 alone.
**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)
After determining winners, `EagleInterfaceGrpcServer.cpp:381-409` assigns each player an
`EndGameCondition`:
| Condition | Criterion |
|-----------|-----------|
| `Victory(type)` | Player is in `winning_shardok_ids` |
| `AllyVictory(type)` | Player is NOT in winners, but has an ally who IS |
| `Loss(type)` | Neither of the above |
The `type` is the VictoryCondition that caused the game to end (e.g., `HoldsCriticalTiles`).
### When does AllyVictory actually occur?
Since attackers don't have `LastAllianceStanding`, the only scenarios producing `AllyVictory` in
standard AssaultProvince battles are:
1. **One attacker holds all castles alone** -> that attacker gets `Victory(HoldsCriticalTiles)`,
their allied co-attacker gets `AllyVictory(HoldsCriticalTiles)`.
2. **Defender wins via WinAfterMaxRounds** -> defender gets `Victory(WinAfterMaxRounds)`. If an
attacker is allied with the defender (unusual, see note below), they'd get
`AllyVictory(WinAfterMaxRounds)`.
When allied attackers **jointly** hold all castles, both get `Victory(HoldsCriticalTiles)` -- not
AllyVictory -- because both are in `winning_shardok_ids`.
## 4. Eagle Processes Battle Results
`ResolveBattleAction.scala` receives `BattleResolution` containing each player's
`EndGameCondition` and resolved units.
### Winner/Loser Partition
Players are split by `isWinOrAllyVictory` (line 383-386):
- `winningResolvedPlayers`: Victory OR AllyVictory
- `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 (`!isWinOrAllyVictory`) |
| `Captured` | Never | |
| `Normal` | Never | |
| `Retreated` | Never | |
| `Outlawed` | Never | Becomes unaffiliated hero |
Key point: `NeverEntered` units from **winning** factions (including AllyVictory) 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:
- **Victory players**: `fleeProvinceId = None` (no special routing)
- **AllyVictory players**: `fleeProvinceId = Some(destinationProvinceId)` (routed to their
original march destination)
If a returning army has `fleeProvinceId`, it's sent there as an incoming army. If not, the army
is "shattered" (units become outlaws in the battle province).
### 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)`, ally gets
`AllyVictory(HoldsCriticalTiles)`
- Eagle: Both are attacking winners -> MultiVictorBattleSetupAction
- Players: Same aftermath decision as above.
### Allied attackers eliminate all defenders (LastPlayerStanding impossible with 2 survivors)
- Shardok: LastAllianceStanding check fails (attackers don't have this condition).
Game continues until max rounds -> defender wins via `WinAfterMaxRounds` (even if dead).
- Eagle: Defender won -> ProvinceHeldAction. Province stays with defender.
- Players: The attack effectively fails. Attacker units are captured.
### Single attacker wins (any condition)
- No alliance considerations. ProvinceConqueredAction immediately.
### Defender wins (WinAfterMaxRounds or LastPlayerStanding)
- ProvinceHeldAction. All non-returned attacker units are captured.
## 7. Potential Issues
### Allied attackers can't win by elimination alone
Since attackers receive `LastPlayerStanding` but not `LastAllianceStanding`, two allied attackers
who destroy the entire defending army **cannot win**. The game continues until max rounds, at which
point the (dead) defender wins via `WinAfterMaxRounds`. The attackers must capture all critical
tiles to actually win.
This is probably intentional -- it forces allied attackers to achieve a concrete tactical objective
(castle control) rather than simply killing troops. But it means that on maps with castles, even
total elimination of the enemy doesn't count as victory for allied attackers.
### Defender-won branch doesn't filter allied winners
In `ResolveBattleAction`, the "Defender won" branch (line 403) computes `capturedUnits` as all
resolved players except the winning defender. If an attacker had `AllyVictory` (meaning they're
allied with the defender), their non-returned units would still be "captured". This is technically
a bug, but it cannot occur in practice because Eagle prevents attacking an allied faction's
province.
### AllyVictory fled-unit routing
When an AllyVictory player has fled units, `armyFromResolvedArmy` assigns
`fleeProvinceId = Some(destinationProvinceId)`. This routes the fled army to the original march
destination. For Victory players, `fleeProvinceId = None`, which causes the returning army to be
"shattered" (units become outlaws). This asymmetry between Victory and AllyVictory in fled-unit
routing is potentially surprising -- a solo victor's fled units are shattered, but an
ally-victor's fled units arrive safely at their destination.
## 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/EagleInterfaceGrpcServer.cpp:381-409` |
| 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` |