mirror of
https://github.com/nolen777/eagle0.git
synced 2026-07-28 21:55:42 +00:00
Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d12c81bc3b |
@@ -342,6 +342,21 @@ auto ShardokGameController::GetUpdates(const int64_t startingActionId) -> AllUpd
|
||||
updates.filteredResults.emplace_back(fid, actionResultViews, acs);
|
||||
}
|
||||
|
||||
// Per-watcher filtered views. A watcher is a non-participant Eagle
|
||||
// faction allied to one or more participants — they get a view that
|
||||
// treats their allied participants' units as fully visible (hero
|
||||
// stats, hidden-from-opponents actions like meteor casts, etc.).
|
||||
// Participants whose allies list named this watcher are passed in as
|
||||
// alliedPids so UnitFilter / ActionResultFilter reveal those units to
|
||||
// the watcher.
|
||||
for (const auto &[watcherFid, allyPids] : watcherAllies) {
|
||||
// Watchers can't post commands, so availableCommands is null.
|
||||
updates.filteredResults.emplace_back(
|
||||
watcherFid,
|
||||
engine->FilterNewResultsForWatcher(-1, allyPids, startingActionId),
|
||||
nullptr);
|
||||
}
|
||||
|
||||
updates.filteredResults.emplace_back(
|
||||
-1,
|
||||
engine->FilterNewResults(-1, startingActionId),
|
||||
|
||||
@@ -120,6 +120,11 @@ private:
|
||||
vector<shared_ptr<ShardokAIClient>> aiClients;
|
||||
std::thread aiThread;
|
||||
|
||||
// Each entry maps an Eagle faction id of a non-participant watcher to the
|
||||
// Shardok pids of participants whose allies list named this watcher. Used
|
||||
// when generating per-watcher filtered views in GetUpdates.
|
||||
const vector<std::pair<int32_t, vector<PlayerId>>> watcherAllies;
|
||||
|
||||
static auto MakeLogFilePath() -> string {
|
||||
const time_t timer = time(nullptr);
|
||||
char buf[255];
|
||||
@@ -139,14 +144,16 @@ public:
|
||||
ShardokGameController(
|
||||
unique_ptr<ShardokEngine> e,
|
||||
string mapName,
|
||||
string serializedRequest = "")
|
||||
string serializedRequest = "",
|
||||
vector<std::pair<int32_t, vector<PlayerId>>> watcherAllies = {})
|
||||
: serializedRequest(std::move(serializedRequest)),
|
||||
engine(std::move(e)),
|
||||
cachedGameId(engine->GetGameId()),
|
||||
mapName(std::move(mapName)),
|
||||
logFilePath(MakeLogFilePath()),
|
||||
aiClients(MakeAIClients(engine)),
|
||||
aiThread(&ShardokGameController::DoAIThread, this) {
|
||||
aiThread(&ShardokGameController::DoAIThread, this),
|
||||
watcherAllies(std::move(watcherAllies)) {
|
||||
std::unique_lock lk(masterLock);
|
||||
aiCondition.notify_one();
|
||||
}
|
||||
|
||||
@@ -217,6 +217,58 @@ auto ShardokEngine::GetGameStateView(const PlayerId askingPlayer) const
|
||||
return filteredHistory;
|
||||
}
|
||||
|
||||
[[nodiscard]] auto ShardokEngine::FilterNewResultsForWatcher(
|
||||
const PlayerId askingPlayer,
|
||||
const vector<PlayerId> &alliedPids,
|
||||
const int64_t previousActionCount) const
|
||||
-> vector<net::eagle0::shardok::api::ActionResultView> {
|
||||
if (previousActionCount < startingHistoryCount) {
|
||||
throw ShardokInternalErrorException("Unable to fetch history before startingHistoryCount");
|
||||
}
|
||||
|
||||
vector<ShardokActionWithResultingState> unfilteredHistory = GetGameHistory(previousActionCount);
|
||||
|
||||
vector<net::eagle0::shardok::api::ActionResultView> filteredHistory{};
|
||||
|
||||
GameStateW startingState = (previousActionCount == 0)
|
||||
? GameStateW{}
|
||||
: GetGameStateAtStartOfAction(previousActionCount);
|
||||
GameStateView startingView = (previousActionCount == 0) ? GameStateView{}
|
||||
: GameStateFilteredForPlayerWithAllies(
|
||||
settingsGetter,
|
||||
startingState,
|
||||
askingPlayer,
|
||||
alliedPids);
|
||||
|
||||
GameStateW &previousState = startingState;
|
||||
GameState *previousStatePtr = nullptr;
|
||||
GameStateView &previousView = startingView;
|
||||
|
||||
for (const ShardokActionWithResultingState &awrs : unfilteredHistory) {
|
||||
GameStateView viewAfter = GameStateFilteredForPlayerWithAllies(
|
||||
settingsGetter,
|
||||
GameStateW::FromByteString(awrs.state_after_fb()),
|
||||
askingPlayer,
|
||||
alliedPids);
|
||||
|
||||
if (auto filteredResult = ActionResultFilteredForPlayerWithAllies(
|
||||
previousStatePtr,
|
||||
previousView,
|
||||
viewAfter,
|
||||
settingsGetter,
|
||||
awrs.action_result(),
|
||||
askingPlayer,
|
||||
alliedPids);
|
||||
filteredResult.has_value()) {
|
||||
filteredHistory.push_back(*filteredResult);
|
||||
}
|
||||
previousState = GameStateW::FromByteString(awrs.state_after_fb());
|
||||
previousStatePtr = previousState.Get();
|
||||
previousView = viewAfter;
|
||||
}
|
||||
return filteredHistory;
|
||||
}
|
||||
|
||||
auto ShardokEngine::GetFilteredGameHistory(const PlayerId askingPlayer) const
|
||||
-> vector<net::eagle0::shardok::api::ActionResultView> {
|
||||
return FilterNewResults(askingPlayer, 0);
|
||||
|
||||
@@ -152,6 +152,16 @@ public:
|
||||
[[nodiscard]] auto FilterNewResults(PlayerId askingPlayer, int64_t previousActionCount) const
|
||||
-> vector<net::eagle0::shardok::api::ActionResultView>;
|
||||
|
||||
// Variant for non-participant watchers: askingPlayer is a sentinel (not in
|
||||
// player_infos) and alliedPids names the Shardok pids whose units the
|
||||
// watcher should see as "self/ally" — fully visible, including hero stats
|
||||
// and hidden actions.
|
||||
[[nodiscard]] auto FilterNewResultsForWatcher(
|
||||
PlayerId askingPlayer,
|
||||
const vector<PlayerId> &alliedPids,
|
||||
int64_t previousActionCount) const
|
||||
-> vector<net::eagle0::shardok::api::ActionResultView>;
|
||||
|
||||
[[nodiscard]] auto GetFilteredGameHistory(PlayerId askingPlayer) const
|
||||
-> vector<net::eagle0::shardok::api::ActionResultView>;
|
||||
|
||||
|
||||
@@ -4,6 +4,9 @@
|
||||
|
||||
#include "ActionResultFilter.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
#include <ranges>
|
||||
|
||||
#include "src/main/cpp/net/eagle0/shardok/library/unit/Unit.hpp"
|
||||
#include "src/main/cpp/net/eagle0/shardok/library/view_filters/GameStateFilter.hpp"
|
||||
#include "src/main/cpp/net/eagle0/shardok/library/view_filters/GameStateViewDiffer.hpp"
|
||||
@@ -60,6 +63,24 @@ auto TargetIsHiddenFromOpponents(const ActionResultType type) -> bool {
|
||||
const SettingsGetter &settings,
|
||||
const ActionResult &result,
|
||||
const PlayerId askingPlayer) -> std::optional<ActionResultView> {
|
||||
return ActionResultFilteredForPlayerWithAllies(
|
||||
beforeState,
|
||||
beforeView,
|
||||
afterView,
|
||||
settings,
|
||||
result,
|
||||
askingPlayer,
|
||||
std::vector<PlayerId>{});
|
||||
}
|
||||
|
||||
[[nodiscard]] auto ActionResultFilteredForPlayerWithAllies(
|
||||
const GameState *beforeState,
|
||||
const net::eagle0::shardok::api::GameStateView &beforeView,
|
||||
const net::eagle0::shardok::api::GameStateView &afterView,
|
||||
const SettingsGetter &settings,
|
||||
const ActionResult &result,
|
||||
const PlayerId askingPlayer,
|
||||
const std::vector<PlayerId> &alliedPids) -> std::optional<ActionResultView> {
|
||||
double opponentKnowledgeOfActor = 0.0;
|
||||
bool hiddenActor = false;
|
||||
bool wasHidden = false;
|
||||
@@ -76,13 +97,16 @@ auto TargetIsHiddenFromOpponents(const ActionResultType type) -> bool {
|
||||
beforeState->units()->size() > static_cast<unsigned int>(actorId) &&
|
||||
beforeState->units()->Get(result.actor().value())->hidden();
|
||||
}
|
||||
const PlayerId actorPlayer = result.has_player() ? result.player().value() : -1;
|
||||
const bool actorIsSelfOrAlly =
|
||||
askingPlayer == actorPlayer || std::ranges::contains(alliedPids, actorPlayer);
|
||||
if (IsHiddenFromOpponents(
|
||||
settings.Backing().minimum_knowledge_for_profession(),
|
||||
result.type(),
|
||||
opponentKnowledgeOfActor,
|
||||
hiddenActor,
|
||||
wasHidden) &&
|
||||
askingPlayer != result.player().value()) {
|
||||
!actorIsSelfOrAlly) {
|
||||
return std::optional<ActionResultView>();
|
||||
}
|
||||
|
||||
@@ -107,8 +131,7 @@ auto TargetIsHiddenFromOpponents(const ActionResultType type) -> bool {
|
||||
}
|
||||
|
||||
if (result.has_target_coords() &&
|
||||
((result.has_player() && result.player().value() == askingPlayer) ||
|
||||
!TargetIsHiddenFromOpponents(result.type()))) {
|
||||
(actorIsSelfOrAlly || !TargetIsHiddenFromOpponents(result.type()))) {
|
||||
*resultView.mutable_target_coords() = result.target_coords();
|
||||
} else {
|
||||
resultView.clear_target_coords();
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
#define EAGLE0_ACTIONRESULTFILTER_HPP
|
||||
|
||||
#include <optional>
|
||||
#include <vector>
|
||||
|
||||
#include "src/main/cpp/net/eagle0/shardok/library/ShardokCTypes.h"
|
||||
#include "src/main/cpp/net/eagle0/shardok/library/settings/GameSettings.hpp"
|
||||
@@ -25,6 +26,19 @@ using GameState = net::eagle0::shardok::storage::fb::GameState;
|
||||
const net::eagle0::shardok::storage::ActionResult &result,
|
||||
PlayerId askingPlayer) -> std::optional<net::eagle0::shardok::api::ActionResultView>;
|
||||
|
||||
// Variant that takes the asker's allied Shardok pids explicitly. Allied actor
|
||||
// actions (like hidden meteor casts) are revealed to the asker as if the
|
||||
// asker were the actor's owner.
|
||||
[[nodiscard]] auto ActionResultFilteredForPlayerWithAllies(
|
||||
const GameState *beforeState,
|
||||
const net::eagle0::shardok::api::GameStateView &beforeView,
|
||||
const net::eagle0::shardok::api::GameStateView &afterView,
|
||||
const SettingsGetter &settings,
|
||||
const net::eagle0::shardok::storage::ActionResult &result,
|
||||
PlayerId askingPlayer,
|
||||
const std::vector<PlayerId> &alliedPids)
|
||||
-> std::optional<net::eagle0::shardok::api::ActionResultView>;
|
||||
|
||||
} // namespace shardok
|
||||
|
||||
#endif // EAGLE0_ACTIONRESULTFILTER_HPP
|
||||
|
||||
@@ -19,6 +19,18 @@ auto GameStateFilteredForPlayer(
|
||||
const SettingsGetter& settings,
|
||||
const GameState* gameState,
|
||||
const PlayerId askingPlayer) -> GameStateView {
|
||||
return GameStateFilteredForPlayerWithAllies(
|
||||
settings,
|
||||
gameState,
|
||||
askingPlayer,
|
||||
AlliedPids(gameState, askingPlayer));
|
||||
}
|
||||
|
||||
auto GameStateFilteredForPlayerWithAllies(
|
||||
const SettingsGetter& settings,
|
||||
const GameState* gameState,
|
||||
const PlayerId askingPlayer,
|
||||
const std::vector<PlayerId>& alliedPids) -> GameStateView {
|
||||
GameStateView gsv{};
|
||||
|
||||
gsv.set_asking_player(askingPlayer);
|
||||
@@ -30,8 +42,6 @@ auto GameStateFilteredForPlayer(
|
||||
*gsv.add_player_infos() = fb::ToPlayerInfoProto(info);
|
||||
}
|
||||
|
||||
const auto& alliedPids = AlliedPids(gameState, askingPlayer);
|
||||
|
||||
for (const auto& unit : *gameState->units()) {
|
||||
switch (unit->status()) {
|
||||
case net::eagle0::shardok::storage::fb::UnitStatus_RESERVE_UNIT:
|
||||
|
||||
@@ -5,6 +5,8 @@
|
||||
#ifndef EAGLE0_GAMESTATEFILTER_HPP
|
||||
#define EAGLE0_GAMESTATEFILTER_HPP
|
||||
|
||||
#include <vector>
|
||||
|
||||
#include "src/main/cpp/net/eagle0/shardok/library/ShardokCTypes.h"
|
||||
#include "src/main/cpp/net/eagle0/shardok/library/settings/GameSettings.hpp"
|
||||
#include "src/main/flatbuffer/net/eagle0/shardok/storage/game_state.hpp"
|
||||
@@ -19,6 +21,16 @@ using GameState = net::eagle0::shardok::storage::fb::GameState;
|
||||
const GameState* gameState,
|
||||
PlayerId askingPlayer) -> net::eagle0::shardok::api::GameStateView;
|
||||
|
||||
// Variant that takes the asker's allied Shardok pids explicitly. Used for
|
||||
// non-participant watchers who have no entry in the GameState's player_infos
|
||||
// but should still see units owned by their participating allies as if those
|
||||
// units were their own.
|
||||
[[nodiscard]] auto GameStateFilteredForPlayerWithAllies(
|
||||
const SettingsGetter& settings,
|
||||
const GameState* gameState,
|
||||
PlayerId askingPlayer,
|
||||
const std::vector<PlayerId>& alliedPids) -> net::eagle0::shardok::api::GameStateView;
|
||||
|
||||
} // namespace shardok
|
||||
|
||||
#endif // EAGLE0_GAMESTATEFILTER_HPP
|
||||
|
||||
@@ -196,6 +196,23 @@ void EagleInterfaceImpl::StartGame(const NewGameRequest &request) {
|
||||
string unitsString;
|
||||
for (const auto &hid : heroIds) { unitsString += std::to_string(hid) + " "; }
|
||||
|
||||
// Build the watcher → allied-Shardok-pids mapping. A non-participant
|
||||
// faction "watches" via any participant that lists it as an ally.
|
||||
std::vector<std::pair<int32_t, std::vector<PlayerId>>> watcherAllies;
|
||||
watcherAllies.reserve(request.watcher_eagle_faction_ids_size());
|
||||
for (const int32_t watcherFid : request.watcher_eagle_faction_ids()) {
|
||||
std::vector<PlayerId> allyPids;
|
||||
for (int i = 0; i < request.user_infos_size(); i++) {
|
||||
for (const auto &ally : request.user_infos(i).allies()) {
|
||||
if (ally.eagle_faction_id() == watcherFid) {
|
||||
allyPids.push_back(static_cast<PlayerId>(i));
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!allyPids.empty()) { watcherAllies.emplace_back(watcherFid, std::move(allyPids)); }
|
||||
}
|
||||
|
||||
printf("Starting game %s with units %s\n", gameId.c_str(), unitsString.c_str());
|
||||
gamesManager->UnlockedCreateSpecifiedGame(
|
||||
gameId,
|
||||
@@ -204,7 +221,8 @@ void EagleInterfaceImpl::StartGame(const NewGameRequest &request) {
|
||||
customMapBytes,
|
||||
month,
|
||||
request.tutorial_battle_config(),
|
||||
request.SerializeAsString());
|
||||
request.SerializeAsString(),
|
||||
watcherAllies);
|
||||
}
|
||||
|
||||
auto EagleInterfaceImpl::PostCommand(
|
||||
|
||||
@@ -32,7 +32,9 @@ auto SetUpController(
|
||||
const vector<Unit> &units,
|
||||
const vector<PlayerInfoProto> &playerSetupInfos,
|
||||
const TutorialBattleConfigProto &tutorialConfig,
|
||||
const string &serializedRequest) -> shared_ptr<ShardokGameController>;
|
||||
const string &serializedRequest,
|
||||
const std::vector<std::pair<int32_t, std::vector<PlayerId>>> &watcherAllies = {})
|
||||
-> shared_ptr<ShardokGameController>;
|
||||
|
||||
ShardokGamesManager::ShardokGamesManager(const std::vector<std::string> &extraSettings) {
|
||||
gameSettings = SettingsLoader::LoadSettings();
|
||||
@@ -85,7 +87,8 @@ auto ShardokGamesManager::UnlockedCreateSpecifiedGame(
|
||||
const string &customMapBytes,
|
||||
const int month,
|
||||
const TutorialBattleConfigProto &tutorialConfig,
|
||||
const string &serializedRequest) -> GameId {
|
||||
const string &serializedRequest,
|
||||
const std::vector<std::pair<int32_t, std::vector<PlayerId>>> &watcherAllies) -> GameId {
|
||||
// Check for existing game with this ID and early-exit
|
||||
std::shared_lock<std::shared_mutex> lk(runningControllersLock);
|
||||
const auto existingGame = runningControllers.find(gameId);
|
||||
@@ -120,7 +123,8 @@ auto ShardokGamesManager::UnlockedCreateSpecifiedGame(
|
||||
units,
|
||||
playerInfos,
|
||||
tutorialConfig,
|
||||
serializedRequest);
|
||||
serializedRequest,
|
||||
watcherAllies);
|
||||
|
||||
return LockedCreateGame(controller, playerSetupInfos, serializedRequest);
|
||||
}
|
||||
@@ -147,7 +151,9 @@ auto SetUpController(
|
||||
const vector<Unit> &units,
|
||||
const vector<PlayerInfoProto> &playerInfos,
|
||||
const TutorialBattleConfigProto &tutorialConfig,
|
||||
const string &serializedRequest) -> shared_ptr<ShardokGameController> {
|
||||
const string &serializedRequest,
|
||||
const std::vector<std::pair<int32_t, std::vector<PlayerId>>> &watcherAllies)
|
||||
-> shared_ptr<ShardokGameController> {
|
||||
auto unplacedUnits = std::vector<Unit>();
|
||||
|
||||
UnitId nextUnitId = 0;
|
||||
@@ -220,7 +226,11 @@ auto SetUpController(
|
||||
|
||||
engine->SetTutorialBattleConfig(tutorialConfig);
|
||||
|
||||
return std::make_shared<ShardokGameController>(std::move(engine), mapName, serializedRequest);
|
||||
return std::make_shared<ShardokGameController>(
|
||||
std::move(engine),
|
||||
mapName,
|
||||
serializedRequest,
|
||||
watcherAllies);
|
||||
}
|
||||
|
||||
} // namespace shardok
|
||||
|
||||
@@ -110,7 +110,9 @@ public:
|
||||
const string &customMapBytes,
|
||||
int month,
|
||||
const TutorialBattleConfigProto &tutorialConfig,
|
||||
const string &serializedRequest) -> GameId;
|
||||
const string &serializedRequest,
|
||||
const std::vector<std::pair<int32_t, std::vector<PlayerId>>> &watcherAllies = {})
|
||||
-> GameId;
|
||||
|
||||
auto UnlockedCreateFromGameStateBytes(
|
||||
const byte_vector &gameStateBytes,
|
||||
|
||||
@@ -31,4 +31,10 @@ message NewGameRequest {
|
||||
|
||||
// Configuration for scripted tutorial battles
|
||||
TutorialBattleConfig tutorial_battle_config = 7;
|
||||
|
||||
// Eagle faction IDs of non-participants who should receive an ally-aware
|
||||
// filtered view of this battle (e.g., factions allied to a participant
|
||||
// but not themselves fighting). Each entry derives its allied participant
|
||||
// set from existing PlayerSetupInfo.allies entries that name this faction.
|
||||
repeated int32 watcher_eagle_faction_ids = 8;
|
||||
}
|
||||
|
||||
@@ -264,6 +264,16 @@ class ShardokInterfaceGrpcClient(
|
||||
s"${battle.players.map(sp => sp.getArmyGroup.armies.flatMap(_.getArmy.units).map(_.heroId).sorted.mkString(" ")).mkString(", ")}"
|
||||
)
|
||||
|
||||
val participantFids: Set[FactionId] = battle.players.map(_.eagleFid).toSet
|
||||
val watcherFids: Vector[FactionId] = battle.players.flatMap { sp =>
|
||||
eagleGameState
|
||||
.factions(sp.eagleFid)
|
||||
.factionRelationships
|
||||
.filter(_.relationshipLevel != HOSTILE)
|
||||
.map(_.targetFactionId)
|
||||
}.toVector.distinct
|
||||
.filterNot(participantFids.contains)
|
||||
|
||||
val newGameRequest = NewGameRequest(
|
||||
hexMapName = battle.hexMapName,
|
||||
gameId = battle.shardokGameId,
|
||||
@@ -276,7 +286,8 @@ class ShardokInterfaceGrpcClient(
|
||||
)
|
||||
},
|
||||
eagleGameId = eagleGameState.gameId,
|
||||
tutorialBattleConfig = tutorialBattleConfig
|
||||
tutorialBattleConfig = tutorialBattleConfig,
|
||||
watcherEagleFactionIds = watcherFids
|
||||
)
|
||||
|
||||
resolveNewGameRequest(battle, newGameRequest)
|
||||
|
||||
@@ -15,3 +15,18 @@ cc_test(
|
||||
"@googletest//:gtest_main",
|
||||
],
|
||||
)
|
||||
|
||||
cc_test(
|
||||
name = "watcher_filter_test",
|
||||
size = "small",
|
||||
srcs = ["WatcherFilter_test.cpp"],
|
||||
copts = TEST_COPTS,
|
||||
deps = [
|
||||
"//src/main/cpp/net/eagle0/shardok/library:engine",
|
||||
"//src/main/cpp/net/eagle0/shardok/library/view_filters:game_state_filter",
|
||||
"//src/test/cpp/net/eagle0/shardok/library:hex_map_test_data",
|
||||
"//src/test/cpp/net/eagle0/shardok/library:shardok_engine_based_test_data",
|
||||
"//src/test/cpp/net/eagle0/shardok/library:unit_tests_data",
|
||||
"@googletest//:gtest_main",
|
||||
],
|
||||
)
|
||||
|
||||
@@ -0,0 +1,108 @@
|
||||
//
|
||||
// Tests for the watcher path of GameStateFilter / ActionResultFilter, used
|
||||
// when a non-participant Eagle faction watches an ally's battle. Hero stats
|
||||
// for allied units must be visible to the watcher even though the watcher
|
||||
// has no opponent_knowledge entry.
|
||||
//
|
||||
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include "src/main/cpp/net/eagle0/shardok/library/ShardokEngine.hpp"
|
||||
#include "src/main/cpp/net/eagle0/shardok/library/settings/GameSettings.hpp"
|
||||
#include "src/main/cpp/net/eagle0/shardok/library/view_filters/GameStateFilter.hpp"
|
||||
#include "src/test/cpp/net/eagle0/shardok/library/HexMap_test_data.hpp"
|
||||
#include "src/test/cpp/net/eagle0/shardok/library/ShardokEngineBasedTestData.hpp"
|
||||
#include "src/test/cpp/net/eagle0/shardok/library/Unit_tests_data.hpp"
|
||||
|
||||
namespace shardok {
|
||||
|
||||
class WatcherFilterTest : public ::testing::Test {
|
||||
void SetUp() override {
|
||||
InitializeGameSettings();
|
||||
// The test settings default knowledge thresholds to 0; set them so the
|
||||
// observer-vs-watcher distinction is observable.
|
||||
GetGameSettingsSetter().SetInt(kSettingsKeys.minimumKnowledgeForHeroStats, 75);
|
||||
GetGameSettingsSetter().SetInt(kSettingsKeys.minimumKnowledgeForBattalionStats, 50);
|
||||
}
|
||||
};
|
||||
|
||||
TEST_F(WatcherFilterTest, AlliedParticipantHeroStatsVisibleToWatcher) {
|
||||
vector<Unit> attackerUnits;
|
||||
attackerUnits.push_back(GetUnitT1());
|
||||
attackerUnits.back().mutable_location() = Coords(4, 0);
|
||||
|
||||
vector<Unit> defenderUnits;
|
||||
defenderUnits.push_back(GetUnitT2());
|
||||
defenderUnits.back().mutable_location() = Coords(2, 0);
|
||||
|
||||
const auto game = GameWithUnits(BASIC_MAP, attackerUnits, defenderUnits);
|
||||
const auto* gs = game->GetCurrentGameState().Get();
|
||||
const auto& settings = game->GetGameSettings()->GetGetter();
|
||||
|
||||
// Watcher allied to player 0 (the attacker).
|
||||
const auto watcherView = GameStateFilteredForPlayerWithAllies(
|
||||
settings,
|
||||
gs,
|
||||
/*askingPlayer=*/-1,
|
||||
/*alliedPids=*/std::vector<PlayerId>{0});
|
||||
|
||||
// Generic observer (no alliedPids) — used for comparison.
|
||||
const auto observerView = GameStateFilteredForPlayer(settings, gs, -1);
|
||||
|
||||
// Find player 0's unit in the watcher view and confirm hero stats present.
|
||||
bool foundAllyWithHeroStats = false;
|
||||
for (const auto& uv : watcherView.units()) {
|
||||
if (uv.player_id() == 0 && uv.has_attached_hero() && uv.attached_hero().has_stats()) {
|
||||
foundAllyWithHeroStats = true;
|
||||
// Ally battalion stats also revealed.
|
||||
EXPECT_TRUE(uv.battalion().has_armament());
|
||||
EXPECT_TRUE(uv.battalion().has_morale());
|
||||
}
|
||||
}
|
||||
EXPECT_TRUE(foundAllyWithHeroStats)
|
||||
<< "Watcher allied to player 0 should see hero stats on player 0's unit";
|
||||
|
||||
// Sanity check: legacy observer view (no alliedPids) does NOT reveal
|
||||
// hero stats for the same unit, since opponent_knowledge defaults to 0.
|
||||
bool observerSawAllyHeroStats = false;
|
||||
for (const auto& uv : observerView.units()) {
|
||||
if (uv.player_id() == 0 && uv.has_attached_hero() && uv.attached_hero().has_stats()) {
|
||||
observerSawAllyHeroStats = true;
|
||||
}
|
||||
}
|
||||
EXPECT_FALSE(observerSawAllyHeroStats)
|
||||
<< "Generic observer should NOT see hero stats with default knowledge";
|
||||
}
|
||||
|
||||
TEST_F(WatcherFilterTest, NonAlliedEnemyUnitGatedByOpponentKnowledge) {
|
||||
vector<Unit> attackerUnits;
|
||||
attackerUnits.push_back(GetUnitT1());
|
||||
attackerUnits.back().mutable_location() = Coords(4, 0);
|
||||
|
||||
vector<Unit> defenderUnits;
|
||||
defenderUnits.push_back(GetUnitT2());
|
||||
defenderUnits.back().mutable_location() = Coords(2, 0);
|
||||
|
||||
const auto game = GameWithUnits(BASIC_MAP, attackerUnits, defenderUnits);
|
||||
const auto* gs = game->GetCurrentGameState().Get();
|
||||
const auto& settings = game->GetGameSettings()->GetGetter();
|
||||
|
||||
// Watcher allied only to player 0; player 1 is non-allied, default
|
||||
// opponent_knowledge of 0, so no stats should leak through.
|
||||
const auto watcherView = GameStateFilteredForPlayerWithAllies(
|
||||
settings,
|
||||
gs,
|
||||
/*askingPlayer=*/-1,
|
||||
/*alliedPids=*/std::vector<PlayerId>{0});
|
||||
|
||||
bool foundEnemyHeroStats = false;
|
||||
for (const auto& uv : watcherView.units()) {
|
||||
if (uv.player_id() == 1 && uv.has_attached_hero() && uv.attached_hero().has_stats()) {
|
||||
foundEnemyHeroStats = true;
|
||||
}
|
||||
}
|
||||
EXPECT_FALSE(foundEnemyHeroStats)
|
||||
<< "Watcher should not see non-allied enemy hero stats without sufficient knowledge";
|
||||
}
|
||||
|
||||
} // namespace shardok
|
||||
Reference in New Issue
Block a user