mirror of
https://github.com/nolen777/eagle0.git
synced 2026-07-28 20:55:44 +00:00
Expose retreating army state in server views (#8686)
* Expose retreating armies in progress panels * Keep retreating army PR server-side
This commit is contained in:
@@ -25,6 +25,32 @@
|
||||
#include "src/main/protobuf/net/eagle0/shardok/api/game_state_view.pb.h"
|
||||
|
||||
namespace shardok {
|
||||
|
||||
auto PlayerBattleProgressFrom(const net::eagle0::shardok::storage::fb::GameState &gameState)
|
||||
-> std::vector<PlayerBattleProgress> {
|
||||
std::vector<PlayerBattleProgress> progress;
|
||||
progress.reserve(gameState.player_infos()->size());
|
||||
for (const auto *player : *gameState.player_infos()) {
|
||||
int32_t troopCount = 0;
|
||||
bool isRetreating = false;
|
||||
for (const auto *unit : *gameState.units()) {
|
||||
if (unit->player_id() != player->player_id()) continue;
|
||||
switch (unit->status()) {
|
||||
case net::eagle0::shardok::storage::fb::UnitStatus_NORMAL_UNIT:
|
||||
case net::eagle0::shardok::storage::fb::UnitStatus_RESERVE_UNIT:
|
||||
case net::eagle0::shardok::storage::fb::UnitStatus_NEVER_ENTERED_UNIT:
|
||||
troopCount += unit->battalion().size();
|
||||
break;
|
||||
case net::eagle0::shardok::storage::fb::UnitStatus_FLED_UNIT:
|
||||
isRetreating = true;
|
||||
break;
|
||||
default: break;
|
||||
}
|
||||
}
|
||||
progress.push_back(PlayerBattleProgress{player->player_id(), troopCount, isRetreating});
|
||||
}
|
||||
return progress;
|
||||
}
|
||||
using net::eagle0::common::ScopedShardokLatencyTrace;
|
||||
|
||||
using net::eagle0::shardok::common::GameStatus;
|
||||
@@ -588,22 +614,7 @@ auto ShardokGameController::GetUpdates(const int64_t startingActionId) -> AllUpd
|
||||
// Extract battle progress data from the FlatBuffer game state
|
||||
const auto *gs = engine->GetCurrentGameState().Get();
|
||||
updates.currentRound = gs->current_round();
|
||||
updates.playerTroopCounts.reserve(gs->player_infos()->size());
|
||||
for (const auto *player : *gs->player_infos()) {
|
||||
int32_t troopCount = 0;
|
||||
for (const auto *unit : *gs->units()) {
|
||||
if (unit->player_id() != player->player_id()) continue;
|
||||
switch (unit->status()) {
|
||||
case net::eagle0::shardok::storage::fb::UnitStatus_NORMAL_UNIT:
|
||||
case net::eagle0::shardok::storage::fb::UnitStatus_RESERVE_UNIT:
|
||||
case net::eagle0::shardok::storage::fb::UnitStatus_NEVER_ENTERED_UNIT:
|
||||
troopCount += unit->battalion().size();
|
||||
break;
|
||||
default: break;
|
||||
}
|
||||
}
|
||||
updates.playerTroopCounts.emplace_back(player->player_id(), troopCount);
|
||||
}
|
||||
updates.playerBattleProgress = PlayerBattleProgressFrom(*gs);
|
||||
}
|
||||
|
||||
if (recommendationEngine) {
|
||||
@@ -724,7 +735,7 @@ auto ShardokGameController::WaitForUpdatesAndPush(
|
||||
updates.newUnfilteredCount,
|
||||
updates.currentGameState,
|
||||
updates.currentRound,
|
||||
updates.playerTroopCounts);
|
||||
updates.playerBattleProgress);
|
||||
}
|
||||
|
||||
// Tutorial battle reset: defender lost, signal reset instead of game over
|
||||
|
||||
@@ -59,6 +59,16 @@ struct OnePlayerUpdates {
|
||||
availableCommands(acs) {}
|
||||
};
|
||||
|
||||
struct PlayerBattleProgress {
|
||||
int32_t playerId;
|
||||
int32_t troopCount;
|
||||
bool isRetreating;
|
||||
};
|
||||
|
||||
[[nodiscard]] auto PlayerBattleProgressFrom(
|
||||
const net::eagle0::shardok::storage::fb::GameState& gameState)
|
||||
-> std::vector<PlayerBattleProgress>;
|
||||
|
||||
/// Result of WaitForUpdatesAndPush indicating how the wait ended
|
||||
enum class WaitResult {
|
||||
GAME_OVER, // Game ended normally
|
||||
@@ -78,7 +88,7 @@ public:
|
||||
int32_t newUnfilteredCount,
|
||||
const byte_vector& currentGameState,
|
||||
int32_t currentRound,
|
||||
const std::vector<std::pair<int32_t, int32_t>>& playerTroopCounts) = 0;
|
||||
const std::vector<PlayerBattleProgress>& playerBattleProgress) = 0;
|
||||
|
||||
/// Called when the game ends
|
||||
virtual void OnGameOver(const GameOverInfo& info) = 0;
|
||||
@@ -189,7 +199,7 @@ public:
|
||||
int32_t newUnfilteredCount;
|
||||
byte_vector currentGameState;
|
||||
int32_t currentRound = 0;
|
||||
std::vector<std::pair<int32_t, int32_t>> playerTroopCounts; // (playerId, troopCount)
|
||||
std::vector<PlayerBattleProgress> playerBattleProgress;
|
||||
};
|
||||
auto GetUpdates(int64_t startingActionId) -> AllUpdates;
|
||||
auto GetCurrentGameStateBytes() -> byte_vector;
|
||||
|
||||
@@ -420,10 +420,11 @@ void EagleInterfaceImpl::PopulateGameStatusResponse(
|
||||
string(controller->GetCurrentGameStateBytes());
|
||||
|
||||
response->mutable_game_update_response()->set_current_round(results.currentRound);
|
||||
for (const auto &[playerId, troopCount] : results.playerTroopCounts) {
|
||||
for (const auto &[playerId, troopCount, isRetreating] : results.playerBattleProgress) {
|
||||
auto *ptc = response->mutable_game_update_response()->add_player_troop_counts();
|
||||
ptc->set_player_id(playerId);
|
||||
ptc->set_troop_count(troopCount);
|
||||
ptc->set_is_retreating(isRetreating);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -539,7 +540,7 @@ auto EagleInterfaceImpl::SubscribeToGame(
|
||||
int32_t newUnfilteredCount,
|
||||
const byte_vector ¤tGameState,
|
||||
int32_t currentRound,
|
||||
const vector<std::pair<int32_t, int32_t>> &playerTroopCounts) override {
|
||||
const vector<PlayerBattleProgress> &playerBattleProgress) override {
|
||||
if (!active_) return;
|
||||
|
||||
GameStatusResponse response;
|
||||
@@ -568,10 +569,11 @@ auto EagleInterfaceImpl::SubscribeToGame(
|
||||
std::string(currentGameState.begin(), currentGameState.end());
|
||||
|
||||
response.mutable_game_update_response()->set_current_round(currentRound);
|
||||
for (const auto &[playerId, troopCount] : playerTroopCounts) {
|
||||
for (const auto &[playerId, troopCount, isRetreating] : playerBattleProgress) {
|
||||
auto *ptc = response.mutable_game_update_response()->add_player_troop_counts();
|
||||
ptc->set_player_id(playerId);
|
||||
ptc->set_troop_count(troopCount);
|
||||
ptc->set_is_retreating(isRetreating);
|
||||
}
|
||||
|
||||
ScopedShardokLatencyTrace trace(
|
||||
|
||||
@@ -80,6 +80,7 @@ message OnePlayerUpdateResponse {
|
||||
message PlayerTroopCount {
|
||||
int32 player_id = 1;
|
||||
int32 troop_count = 2;
|
||||
bool is_retreating = 3;
|
||||
}
|
||||
|
||||
message GameUpdateResponse {
|
||||
|
||||
@@ -20,6 +20,7 @@ message Army {
|
||||
int32 faction_id = 1;
|
||||
repeated .net.eagle0.eagle.common.CombatUnit units = 2;
|
||||
.google.protobuf.Int32Value flee_province_id = 3;
|
||||
bool is_retreating = 4;
|
||||
}
|
||||
|
||||
message MovingArmy {
|
||||
|
||||
@@ -29,4 +29,5 @@ message IncomingArmyView {
|
||||
BattalionView battalion = 2;
|
||||
}
|
||||
repeated UnitDetails unit_details = 9;
|
||||
bool is_retreating = 10;
|
||||
}
|
||||
|
||||
@@ -34,4 +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
|
||||
}
|
||||
|
||||
+1
-1
@@ -69,7 +69,7 @@ object EndDefenseDecisionPhaseAction {
|
||||
// Swap destination/origin, clear flee province, set arrival to next round
|
||||
MovingArmy(
|
||||
id = army.id,
|
||||
army = army.army.copy(fleeProvinceId = None),
|
||||
army = army.army.copy(fleeProvinceId = None, isRetreating = true),
|
||||
arrivalRound = gameState.currentRoundId + 1,
|
||||
destinationProvinceId = army.originProvinceId, // going back home
|
||||
originProvinceId = province.id, // coming from paying province
|
||||
|
||||
+1
-1
@@ -82,7 +82,7 @@ case class PerformForcedTurnBackAction(gameState: GameState) extends ProtolessSe
|
||||
provinceId = fleePid,
|
||||
newIncomingArmies = Vector(
|
||||
MovingArmy(
|
||||
army = ia.army.copy(fleeProvinceId = None),
|
||||
army = ia.army.copy(fleeProvinceId = None, isRetreating = true),
|
||||
arrivalRound = gameState.currentRoundId + 1,
|
||||
destinationProvinceId = fleePid,
|
||||
originProvinceId = p.id,
|
||||
|
||||
+1
-1
@@ -910,7 +910,7 @@ case class ResolveBattleAction(
|
||||
newIncomingArmies = Vector(
|
||||
MovingArmy(
|
||||
id = 0, // ID will be generated by ActionResultApplierImpl
|
||||
army = fledArmy.copy(fleeProvinceId = None),
|
||||
army = fledArmy.copy(fleeProvinceId = None, isRetreating = true),
|
||||
arrivalRound = arrivalRound,
|
||||
destinationProvinceId = destinationProvince,
|
||||
originProvinceId = battleProvince,
|
||||
|
||||
+2
-1
@@ -40,7 +40,8 @@ case class WithdrawnArmiesReturnHomeAction(
|
||||
destinationProvinceId = fleeProvinceId,
|
||||
originProvinceId = originPid,
|
||||
army = ma.army.copy(
|
||||
fleeProvinceId = None
|
||||
fleeProvinceId = None,
|
||||
isRetreating = true
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
@@ -400,6 +400,7 @@ object ProvinceViewFilter {
|
||||
arrivalRoundId = ma.arrivalRound,
|
||||
food = ma.supplies.food,
|
||||
gold = ma.supplies.gold,
|
||||
isRetreating = ma.army.isRetreating,
|
||||
unitDetails =
|
||||
if showDetails then
|
||||
ma.army.units.map { cu =>
|
||||
|
||||
@@ -24,14 +24,16 @@ object ArmyConverter {
|
||||
ArmyProto(
|
||||
factionId = army.factionId,
|
||||
units = army.units.map(CombatUnitConverter.toProto),
|
||||
fleeProvinceId = army.fleeProvinceId
|
||||
fleeProvinceId = army.fleeProvinceId,
|
||||
isRetreating = army.isRetreating
|
||||
)
|
||||
|
||||
def fromProto(armyProto: ArmyProto): Army =
|
||||
Army(
|
||||
factionId = armyProto.factionId,
|
||||
units = armyProto.units.map(CombatUnitConverter.fromProto).toVector,
|
||||
fleeProvinceId = armyProto.fleeProvinceId
|
||||
fleeProvinceId = armyProto.fleeProvinceId,
|
||||
isRetreating = armyProto.isRetreating
|
||||
)
|
||||
|
||||
def toProto(movingArmy: MovingArmy): MovingArmyProto =
|
||||
|
||||
+3
-1
@@ -20,7 +20,9 @@ object ShardokBattleViewConverter {
|
||||
attackerTroopCount = view.attackerTroopCount,
|
||||
winningFactionId = view.winningFactionId,
|
||||
canFight = view.canFight,
|
||||
canObserve = view.canObserve
|
||||
canObserve = view.canObserve,
|
||||
defenderRetreating = view.defenderRetreating,
|
||||
attackerRetreating = view.attackerRetreating
|
||||
)
|
||||
|
||||
private def toProtoPlayerInfo(info: ShardokBattlePlayerInfo): ShardokBattlePlayerInfoProto =
|
||||
|
||||
+4
-2
@@ -15,7 +15,8 @@ object IncomingArmyViewConverter {
|
||||
arrivalRoundId = view.arrivalRoundId,
|
||||
food = view.food,
|
||||
gold = view.gold,
|
||||
unitDetails = view.unitDetails.map(unitDetailsToProto)
|
||||
unitDetails = view.unitDetails.map(unitDetailsToProto),
|
||||
isRetreating = view.isRetreating
|
||||
)
|
||||
|
||||
def fromProto(proto: IncomingArmyViewProto): IncomingArmyView =
|
||||
@@ -28,7 +29,8 @@ object IncomingArmyViewConverter {
|
||||
arrivalRoundId = proto.arrivalRoundId,
|
||||
food = proto.food,
|
||||
gold = proto.gold,
|
||||
unitDetails = proto.unitDetails.map(unitDetailsFromProto).toVector
|
||||
unitDetails = proto.unitDetails.map(unitDetailsFromProto).toVector,
|
||||
isRetreating = proto.isRetreating
|
||||
)
|
||||
|
||||
private def unitDetailsToProto(
|
||||
|
||||
@@ -5,7 +5,8 @@ import net.eagle0.eagle.{FactionId, MovingArmyId, ProvinceId, RoundId}
|
||||
case class Army(
|
||||
factionId: FactionId,
|
||||
units: Vector[CombatUnit],
|
||||
fleeProvinceId: Option[ProvinceId] = None
|
||||
fleeProvinceId: Option[ProvinceId] = None,
|
||||
isRetreating: Boolean = false
|
||||
)
|
||||
|
||||
case class MovingArmy(
|
||||
|
||||
@@ -22,5 +22,7 @@ case class ShardokBattleView(
|
||||
attackerTroopCount: Option[Int],
|
||||
winningFactionId: Option[FactionId],
|
||||
canFight: Boolean,
|
||||
canObserve: Boolean
|
||||
canObserve: Boolean,
|
||||
defenderRetreating: Boolean = false,
|
||||
attackerRetreating: Boolean = false
|
||||
)
|
||||
|
||||
@@ -17,5 +17,6 @@ case class IncomingArmyView(
|
||||
arrivalRoundId: RoundId,
|
||||
food: Int = 0,
|
||||
gold: Int = 0,
|
||||
unitDetails: Vector[UnitDetails] = Vector.empty
|
||||
unitDetails: Vector[UnitDetails] = Vector.empty,
|
||||
isRetreating: Boolean = false
|
||||
)
|
||||
|
||||
@@ -2,7 +2,12 @@ package net.eagle0.eagle.service.controller
|
||||
|
||||
import net.eagle0.eagle.FactionId
|
||||
|
||||
case class BattleProgressSnapshot(defenderTroops: Int, attackerTroops: Int) {
|
||||
case class BattleProgressSnapshot(
|
||||
defenderTroops: Int,
|
||||
attackerTroops: Int,
|
||||
defenderRetreating: Boolean = false,
|
||||
attackerRetreating: Boolean = false
|
||||
) {
|
||||
def defenderRatio: Float = {
|
||||
val total = defenderTroops + attackerTroops
|
||||
if total == 0 then 0.5f else defenderTroops.toFloat / total
|
||||
|
||||
@@ -505,20 +505,29 @@ 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 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 snapshot = BattleProgressSnapshot(defenderTroops, attackerTroops)
|
||||
val snapshot = BattleProgressSnapshot(
|
||||
defenderTroops,
|
||||
attackerTroops,
|
||||
defenderRetreating,
|
||||
attackerRetreating
|
||||
)
|
||||
|
||||
val tracker = current.getOrElse(
|
||||
shardokGameId,
|
||||
@@ -570,6 +579,8 @@ object GameController {
|
||||
defenderRatio = snapshot.defenderRatio,
|
||||
defenderTroopCount = Option.when(isAllied)(snapshot.defenderTroops),
|
||||
attackerTroopCount = Option.when(isAllied)(snapshot.attackerTroops),
|
||||
defenderRetreating = snapshot.defenderRetreating,
|
||||
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)
|
||||
|
||||
@@ -7,6 +7,7 @@ cc_test(
|
||||
copts = TEST_COPTS,
|
||||
linkstatic = True,
|
||||
deps = [
|
||||
"//src/main/cpp/net/eagle0/shardok/controller",
|
||||
"//src/main/cpp/net/eagle0/shardok/library:engine",
|
||||
"//src/test/cpp/net/eagle0/shardok/library:game_start_test_data",
|
||||
"//src/test/cpp/net/eagle0/shardok/library:hex_map_test_data",
|
||||
|
||||
@@ -1,36 +1,13 @@
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include "src/main/cpp/net/eagle0/shardok/controller/ShardokGameController.hpp"
|
||||
#include "src/main/cpp/net/eagle0/shardok/library/ShardokEngine.hpp"
|
||||
#include "src/test/cpp/net/eagle0/shardok/library/GameStart_test_data.hpp"
|
||||
#include "src/test/cpp/net/eagle0/shardok/library/HexMap_test_data.hpp"
|
||||
#include "src/test/cpp/net/eagle0/shardok/library/ShardokEngineBasedTestData.hpp"
|
||||
|
||||
using net::eagle0::shardok::storage::fb::UnitStatus_NEVER_ENTERED_UNIT;
|
||||
using net::eagle0::shardok::storage::fb::UnitStatus_NORMAL_UNIT;
|
||||
using net::eagle0::shardok::storage::fb::UnitStatus_RESERVE_UNIT;
|
||||
|
||||
// Helper: extract troop counts from a game state using the same logic as
|
||||
// ShardokGameController::GetUpdates().
|
||||
static auto ExtractTroopCounts(const shardok::GameStateW &gsW)
|
||||
-> std::vector<std::pair<int32_t, int32_t>> {
|
||||
std::vector<std::pair<int32_t, int32_t>> counts;
|
||||
const auto *gs = gsW.Get();
|
||||
|
||||
for (const auto *player : *gs->player_infos()) {
|
||||
int32_t troopCount = 0;
|
||||
for (const auto *unit : *gs->units()) {
|
||||
if (unit->player_id() != player->player_id()) continue;
|
||||
switch (unit->status()) {
|
||||
case UnitStatus_NORMAL_UNIT:
|
||||
case UnitStatus_RESERVE_UNIT:
|
||||
case UnitStatus_NEVER_ENTERED_UNIT: troopCount += unit->battalion().size(); break;
|
||||
default: break;
|
||||
}
|
||||
}
|
||||
counts.emplace_back(player->player_id(), troopCount);
|
||||
}
|
||||
return counts;
|
||||
}
|
||||
using net::eagle0::shardok::storage::fb::UnitStatus_FLED_UNIT;
|
||||
using net::eagle0::shardok::storage::fb::UnitStatus_RETREATED_UNIT;
|
||||
|
||||
TEST(BattleProgressFieldsTest, InitialState_HasCorrectRoundAndTroopCounts) {
|
||||
// GameWithUnits creates: player 0 = attacker, player 1 = defender
|
||||
@@ -50,17 +27,18 @@ TEST(BattleProgressFieldsTest, InitialState_HasCorrectRoundAndTroopCounts) {
|
||||
// GameWithUnitTs sets current_round to 1
|
||||
EXPECT_EQ(gs->current_round(), 1);
|
||||
|
||||
auto troopCounts = ExtractTroopCounts(gsW);
|
||||
auto battleProgress = shardok::PlayerBattleProgressFrom(*gs);
|
||||
|
||||
// Should have entries for both players
|
||||
ASSERT_EQ(troopCounts.size(), 2);
|
||||
ASSERT_EQ(battleProgress.size(), 2);
|
||||
|
||||
// Find attacker (player 0) and defender (player 1) counts
|
||||
int32_t attackerTroops = -1;
|
||||
int32_t defenderTroops = -1;
|
||||
for (const auto &[pid, count] : troopCounts) {
|
||||
for (const auto &[pid, count, isRetreating] : battleProgress) {
|
||||
if (pid == 0) attackerTroops = count;
|
||||
if (pid == 1) defenderTroops = count;
|
||||
EXPECT_FALSE(isRetreating);
|
||||
}
|
||||
|
||||
// 2 attacker units x 500 = 1000
|
||||
@@ -79,19 +57,40 @@ TEST(BattleProgressFieldsTest, ReservedSlotUnitsNotCounted) {
|
||||
// GameWithUnitsAndReservedSlots adds RESERVED_SLOT units (player_id = -1)
|
||||
auto engine = GameWithUnitsAndReservedSlots(BASIC_MAP, attackers, defenders, 5);
|
||||
|
||||
auto troopCounts = ExtractTroopCounts(engine->GetCurrentGameState());
|
||||
auto battleProgress = shardok::PlayerBattleProgressFrom(*engine->GetCurrentGameState().Get());
|
||||
|
||||
// Only the two real players should appear (RESERVED_SLOT units have player_id -1
|
||||
// and aren't associated with any player_info entry)
|
||||
ASSERT_EQ(troopCounts.size(), 2);
|
||||
ASSERT_EQ(battleProgress.size(), 2);
|
||||
|
||||
int32_t attackerTroops = -1;
|
||||
int32_t defenderTroops = -1;
|
||||
for (const auto &[pid, count] : troopCounts) {
|
||||
for (const auto &[pid, count, isRetreating] : battleProgress) {
|
||||
if (pid == 0) attackerTroops = count;
|
||||
if (pid == 1) defenderTroops = count;
|
||||
EXPECT_FALSE(isRetreating);
|
||||
}
|
||||
|
||||
EXPECT_EQ(attackerTroops, 500);
|
||||
EXPECT_EQ(defenderTroops, 500);
|
||||
}
|
||||
|
||||
TEST(BattleProgressFieldsTest, FledUnitMarksPlayerAsRetreatingButRetreatedUnitDoesNot) {
|
||||
auto fleeingAttacker = AddGenericUnit(0, 0, Coords(3, 3));
|
||||
vector<Unit> attackers{fleeingAttacker};
|
||||
|
||||
auto retreatedDefender = AddGenericUnit(1, 0, Coords(2, 2));
|
||||
vector<Unit> defenders{retreatedDefender};
|
||||
|
||||
auto engine = GameWithUnits(BASIC_MAP, attackers, defenders);
|
||||
auto gameState = engine->GetCurrentGameState();
|
||||
const_cast<Unit *>(gameState->mutable_units()->GetMutableObject(0))
|
||||
->mutate_status(UnitStatus_FLED_UNIT);
|
||||
const_cast<Unit *>(gameState->mutable_units()->GetMutableObject(1))
|
||||
->mutate_status(UnitStatus_RETREATED_UNIT);
|
||||
const auto progress = shardok::PlayerBattleProgressFrom(*gameState.Get());
|
||||
|
||||
ASSERT_EQ(progress.size(), 2);
|
||||
EXPECT_TRUE(progress[0].isRetreating);
|
||||
EXPECT_FALSE(progress[1].isRetreating);
|
||||
}
|
||||
|
||||
+1
@@ -153,6 +153,7 @@ class EndDefenseDecisionPhaseActionTest extends AnyFlatSpec with Matchers {
|
||||
province7Changes.newIncomingArmies.head.originProvinceId shouldBe defendingProvinceId
|
||||
province7Changes.newIncomingArmies.head.destinationProvinceId shouldBe 7
|
||||
province7Changes.newIncomingArmies.head.arrivalRound shouldBe 20
|
||||
province7Changes.newIncomingArmies.head.army.isRetreating shouldBe true
|
||||
}
|
||||
|
||||
// Province 27 should have incoming army returning
|
||||
|
||||
+2
-1
@@ -147,7 +147,8 @@ class PerformForcedTurnBackActionTest extends AnyFlatSpec with Matchers with Bef
|
||||
army = Army(
|
||||
factionId = 54,
|
||||
fleeProvinceId = None,
|
||||
units = Vector(CombatUnit(factionId = 54, heroId = 5, battalionId = None))
|
||||
units = Vector(CombatUnit(factionId = 54, heroId = 5, battalionId = None)),
|
||||
isRetreating = true
|
||||
),
|
||||
arrivalRound = 28,
|
||||
originProvinceId = destinationPid,
|
||||
|
||||
+1
@@ -2008,5 +2008,6 @@ class ResolveBattleActionTest extends AnyFlatSpec with MockFactory with Matchers
|
||||
(1, Some(88))
|
||||
)
|
||||
incomingArmy.army.fleeProvinceId shouldBe None // Consumed by fleeing
|
||||
incomingArmy.army.isRetreating shouldBe true
|
||||
}
|
||||
}
|
||||
|
||||
+6
-3
@@ -87,7 +87,8 @@ class WithdrawnArmiesReturnHomeActionTest extends AnyFlatSpec with Matchers {
|
||||
army = Army(
|
||||
fleeProvinceId = None,
|
||||
factionId = withdrawingFactionId,
|
||||
units = Vector.empty
|
||||
units = Vector.empty,
|
||||
isRetreating = true
|
||||
),
|
||||
arrivalRound = currentRoundId + 1,
|
||||
destinationProvinceId = 5,
|
||||
@@ -108,7 +109,8 @@ class WithdrawnArmiesReturnHomeActionTest extends AnyFlatSpec with Matchers {
|
||||
army = Army(
|
||||
fleeProvinceId = None,
|
||||
factionId = withdrawingFactionId,
|
||||
units = Vector.empty
|
||||
units = Vector.empty,
|
||||
isRetreating = true
|
||||
),
|
||||
arrivalRound = currentRoundId + 1,
|
||||
destinationProvinceId = 6,
|
||||
@@ -180,7 +182,8 @@ class WithdrawnArmiesReturnHomeActionTest extends AnyFlatSpec with Matchers {
|
||||
army = Army(
|
||||
fleeProvinceId = None,
|
||||
factionId = withdrawingFactionId,
|
||||
units = Vector.empty
|
||||
units = Vector.empty,
|
||||
isRetreating = true
|
||||
),
|
||||
arrivalRound = currentRoundId + 1,
|
||||
destinationProvinceId = 5,
|
||||
|
||||
+7
-1
@@ -22,6 +22,7 @@ class IncomingArmyViewConverterTest extends AnyFlatSpec with Matchers {
|
||||
arrivalRoundId = 15: RoundId,
|
||||
food = 100,
|
||||
gold = 200,
|
||||
isRetreating = true,
|
||||
unitDetails = Vector(
|
||||
UnitDetails(
|
||||
heroId = 50: HeroId,
|
||||
@@ -54,6 +55,7 @@ class IncomingArmyViewConverterTest extends AnyFlatSpec with Matchers {
|
||||
food,
|
||||
gold,
|
||||
unitDetails,
|
||||
isRetreating,
|
||||
_ /* unknownFields */
|
||||
) =>
|
||||
factionId shouldBe 1
|
||||
@@ -65,6 +67,7 @@ class IncomingArmyViewConverterTest extends AnyFlatSpec with Matchers {
|
||||
food shouldBe 100
|
||||
gold shouldBe 200
|
||||
unitDetails.size shouldBe 2
|
||||
isRetreating shouldBe true
|
||||
|
||||
// Verify unit details conversion
|
||||
unitDetails.head match {
|
||||
@@ -85,6 +88,7 @@ class IncomingArmyViewConverterTest extends AnyFlatSpec with Matchers {
|
||||
arrivalRoundId = 25,
|
||||
food = 300,
|
||||
gold = 400,
|
||||
isRetreating = true,
|
||||
unitDetails = Seq(
|
||||
IncomingArmyViewProto.UnitDetails(
|
||||
heroId = 60,
|
||||
@@ -115,7 +119,8 @@ class IncomingArmyViewConverterTest extends AnyFlatSpec with Matchers {
|
||||
arrivalRoundId,
|
||||
food,
|
||||
gold,
|
||||
unitDetails
|
||||
unitDetails,
|
||||
isRetreating
|
||||
) =>
|
||||
factionId shouldBe (2: FactionId)
|
||||
heroCount shouldBe 5
|
||||
@@ -126,6 +131,7 @@ class IncomingArmyViewConverterTest extends AnyFlatSpec with Matchers {
|
||||
food shouldBe 300
|
||||
gold shouldBe 400
|
||||
unitDetails.size shouldBe 1
|
||||
isRetreating shouldBe true
|
||||
|
||||
// Verify unit details conversion
|
||||
unitDetails.head match {
|
||||
|
||||
@@ -122,6 +122,27 @@ class BattleProgressTest extends AnyFlatSpec with Matchers {
|
||||
tracker.latestSnapshot.attackerTroops shouldBe 500
|
||||
}
|
||||
|
||||
it should "aggregate retreating status by battle side" in {
|
||||
val update = makeBattleUpdate(
|
||||
currentRound = 1,
|
||||
playerTroopCounts = Seq(
|
||||
PlayerTroopCount(playerId = 0, troopCount = 500),
|
||||
PlayerTroopCount(playerId = 1, troopCount = 300),
|
||||
PlayerTroopCount(playerId = 2, troopCount = 200, isRetreating = true)
|
||||
),
|
||||
battle = threePlayerBattle
|
||||
)
|
||||
|
||||
val result = GameController.updatedBattleProgress(Map.empty, "test-battle-1", update)
|
||||
|
||||
result("test-battle-1").latestSnapshot shouldBe BattleProgressSnapshot(
|
||||
defenderTroops = 500,
|
||||
attackerTroops = 500,
|
||||
defenderRetreating = false,
|
||||
attackerRetreating = true
|
||||
)
|
||||
}
|
||||
|
||||
it should "default to zero when defender has no troop count entry" in {
|
||||
val update = makeBattleUpdate(
|
||||
currentRound = 1,
|
||||
|
||||
Reference in New Issue
Block a user