Label strategically retreating armies in battle progress (#8707)

* Label strategically retreating battle armies

* Show strategic retreats in initial battle progress
This commit is contained in:
2026-07-20 21:49:25 -07:00
committed by GitHub
parent e6bac582b1
commit c7c7167b6a
6 changed files with 90 additions and 13 deletions
@@ -34,6 +34,6 @@ message ShardokBattleView {
.google.protobuf.Int32Value winning_faction_id = 10; // set when battle ended and result is revealed
bool can_fight = 11; // true only for this viewer's highest fightable battle
bool can_observe = 12; // true if this viewer can currently observe this battle
bool defender_retreating = 13; // true once any defending unit has fled
bool attacker_retreating = 14; // true once any attacking unit has fled
bool defender_retreating = 13; // true for an army already retreating or once a defending unit has fled
bool attacker_retreating = 14; // true for an army already retreating or once an attacking unit has fled
}
@@ -30,6 +30,8 @@ object BattleFilter {
val total = defTroops + atkTroops
val ratio = if total == 0 then 0.5f else defTroops.toFloat / total
val showCounts = fid.exists(f => isParticipantOrAlly(f, battle, factions))
val defenderRetreating = sideHasRetreatingArmy(battle, isDefender = true)
val attackerRetreating = sideHasRetreatingArmy(battle, isDefender = false)
val shardokPlayer =
battle.players.find(sp => fid.contains(sp.eagleFid))
@@ -46,7 +48,9 @@ object BattleFilter {
attackerTroopCount = Option.when(showCounts)(atkTroops),
winningFactionId = None,
canFight = false,
canObserve = false
canObserve = false,
defenderRetreating = defenderRetreating,
attackerRetreating = attackerRetreating
)
}.getOrElse {
ShardokBattleView(
@@ -63,11 +67,22 @@ object BattleFilter {
attackerTroopCount = Option.when(showCounts)(atkTroops),
winningFactionId = None,
canFight = false,
canObserve = false
canObserve = false,
defenderRetreating = defenderRetreating,
attackerRetreating = attackerRetreating
)
}
}
private def sideHasRetreatingArmy(
battle: ShardokBattle,
isDefender: Boolean
): Boolean =
battle.players.exists { player =>
player.isDefender == isDefender &&
player.armyGroup.exists(_.armies.exists(_.army.isRetreating))
}
private def initialTroopCounts(
battle: ShardokBattle,
battalions: Map[BattalionId, BattalionT]
@@ -571,22 +571,28 @@ object GameController {
val currentRound = battleUpdate.currentRound
// Determine which player IDs are defenders from the battle's player list
val defenderPlayerIds = battleUpdate.battle.players.zipWithIndex
val defenderPlayerIds = battleUpdate.battle.players.zipWithIndex
.filter(_._1.isDefender)
.map(_._2)
.toSet
val defenderTroops = battleUpdate.playerTroopCounts
val retreatingArmyPlayerIds = battleUpdate.battle.players.zipWithIndex.collect {
case (player, playerId) if player.armyGroup.exists(_.armies.exists(_.army.exists(_.isRetreating))) =>
playerId
}.toSet
val defenderTroops = battleUpdate.playerTroopCounts
.filter(ptc => defenderPlayerIds.contains(ptc.playerId))
.map(_.troopCount)
.sum
val attackerTroops = battleUpdate.playerTroopCounts
val attackerTroops = battleUpdate.playerTroopCounts
.filterNot(ptc => defenderPlayerIds.contains(ptc.playerId))
.map(_.troopCount)
.sum
val defenderRetreating = battleUpdate.playerTroopCounts
.exists(ptc => defenderPlayerIds.contains(ptc.playerId) && ptc.isRetreating)
val attackerRetreating = battleUpdate.playerTroopCounts
.exists(ptc => !defenderPlayerIds.contains(ptc.playerId) && ptc.isRetreating)
val defenderRetreating = battleUpdate.playerTroopCounts
.exists(ptc => defenderPlayerIds.contains(ptc.playerId) && ptc.isRetreating) ||
retreatingArmyPlayerIds.exists(defenderPlayerIds.contains)
val attackerRetreating = battleUpdate.playerTroopCounts
.exists(ptc => !defenderPlayerIds.contains(ptc.playerId) && ptc.isRetreating) ||
retreatingArmyPlayerIds.exists(playerId => !defenderPlayerIds.contains(playerId))
val snapshot = BattleProgressSnapshot(
defenderTroops,
@@ -645,8 +651,8 @@ object GameController {
defenderRatio = snapshot.defenderRatio,
defenderTroopCount = Option.when(isAllied)(snapshot.defenderTroops),
attackerTroopCount = Option.when(isAllied)(snapshot.attackerTroops),
defenderRetreating = snapshot.defenderRetreating,
attackerRetreating = snapshot.attackerRetreating,
defenderRetreating = view.defenderRetreating || snapshot.defenderRetreating,
attackerRetreating = view.attackerRetreating || snapshot.attackerRetreating,
winningFactionId =
// Only reveal winner when the gated round has caught up to the end round
tracker.endedAtRound.filter(_ <= gatedRound).flatMap(_ => tracker.winningFactionId)
@@ -211,6 +211,29 @@ class BattleFilterTest extends AnyFlatSpec with Matchers {
view.defenderRatio shouldBe (50f / 150f +- 0.001f)
}
it `should` "mark an attacker that was already retreating in the initial battle view" in {
val originalBattle = battleWithBattalions
val retreatingAttacker = originalBattle
.players(1)
.copy(
armyGroup = originalBattle.players(1).armyGroup.map { group =>
group.copy(
armies = group.armies.map { movingArmy =>
movingArmy.copy(army = movingArmy.army.copy(isRetreating = true))
}
)
}
)
val battleWithRetreating = originalBattle.copy(
players = originalBattle.players.updated(1, retreatingAttacker)
)
val view = BattleFilter.filterBattle(battleWithRetreating, factions, Some(1), testBattalions)
view.attackerRetreating shouldBe true
view.defenderRetreating shouldBe false
}
it `should` "show correct ratio but no troop counts for non-participants" in {
val view = BattleFilter.filterBattle(battleWithBattalions, factionsWithThird, Some(3), testBattalions)
@@ -61,6 +61,7 @@ scala_test(
name = "battle_progress_test",
srcs = ["BattleProgressTest.scala"],
deps = [
"//src/main/protobuf/net/eagle0/eagle/internal:army_scala_proto",
"//src/main/protobuf/net/eagle0/eagle/internal:shardok_battle_scala_proto",
"//src/main/scala/net/eagle0/eagle:eagle_pkg",
"//src/main/scala/net/eagle0/eagle/service/controller:battle_progress_tracker",
@@ -1,6 +1,7 @@
package net.eagle0.eagle.service.controller
import net.eagle0.common.shardok_internal_interface.PlayerTroopCount
import net.eagle0.eagle.internal.army.{Army, HostileArmyGroup, MovingArmy}
import net.eagle0.eagle.internal.shardok_battle.{ShardokBattle, ShardokPlayer}
import net.eagle0.eagle.shardok_interface.BattleUpdate
import org.scalatest.flatspec.AnyFlatSpec
@@ -143,6 +144,37 @@ class BattleProgressTest extends AnyFlatSpec with Matchers {
)
}
it should "mark an attacker whose strategic army was already retreating" in {
val retreatingAttacker = attacker.copy(
armyGroup = Some(
HostileArmyGroup(
armies = Seq(
MovingArmy(
army = Some(Army(isRetreating = true))
)
)
)
)
)
val update = makeBattleUpdate(
currentRound = 1,
playerTroopCounts = Seq(
PlayerTroopCount(playerId = 0, troopCount = 500),
PlayerTroopCount(playerId = 1, troopCount = 300)
),
battle = twoPlayerBattle.copy(players = Seq(defender, retreatingAttacker))
)
val result = GameController.updatedBattleProgress(Map.empty, "test-battle-1", update)
result("test-battle-1").latestSnapshot shouldBe BattleProgressSnapshot(
defenderTroops = 500,
attackerTroops = 300,
defenderRetreating = false,
attackerRetreating = true
)
}
it should "default to zero when defender has no troop count entry" in {
val update = makeBattleUpdate(
currentRound = 1,