Compare commits

...
Author SHA1 Message Date
adminandClaude 47481c2de6 Fix occupants cache performance by sharing cache across copies
Changes:
- Changed occupantsCache_ from unique_ptr to shared_ptr in GameStateW
- Modified copy constructors to share cache instead of rebuilding
- Updated RebuildOccupantsCache() and InitializeCache() to use make_shared
- Removed unnecessary RebuildOccupantsCache() call in GameStateGuesser.cpp

This fixes the performance regression where cache copies were expensive
due to full cache rebuilds on every GameStateW copy operation.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-07-20 20:42:41 -07:00
admin 4810e8e9a5 replaced some more caches 2025-07-20 18:04:39 -07:00
admin d46423bea8 fix the crasher 2025-07-20 18:04:39 -07:00
admin 3a2f9c0c8c a couple more 2025-07-20 18:04:39 -07:00
admin c0accd9d6b more use of new occupants cache 2025-07-20 18:04:38 -07:00
admin c3c80e3c41 put the occupants cache in GameStateW 2025-07-20 18:04:38 -07:00
35 changed files with 536 additions and 176 deletions
+3 -1
View File
@@ -74,7 +74,7 @@ bazel run gazelle # Update Go build files
### Code Formatting
```bash
# ALWAYS run clang-format after making any C++ or C# code changes
# MANDATORY: ALWAYS run clang-format after making any C++ or C# code changes
clang-format -i <modified_files>
# Format all C++ files in a directory:
@@ -84,6 +84,8 @@ find . -name "*.cpp" -o -name "*.hpp" | xargs clang-format -i
find . -name "*.cs" | xargs clang-format -i
```
**IMPORTANT FOR CLAUDE CODE**: After making any changes to C++ (.cpp, .hpp) or C# (.cs) files, you MUST immediately run clang-format on those files using the Bash tool. This is not optional - it's a mandatory step that must be done every time you modify code files. Use the exact command: `clang-format -i <file_path>` for each modified file.
## Language-Specific Patterns
**Scala (Strategic Layer):**
@@ -5,6 +5,7 @@
#include "AIAttackerStrategySelector.hpp"
#include "src/main/cpp/net/eagle0/shardok/ai/AIScoreUtilities.hpp"
#include "src/main/cpp/net/eagle0/shardok/library/GameStateW.hpp"
#include "src/main/cpp/net/eagle0/shardok/library/util/HexMapUtils.hpp"
namespace shardok {
@@ -15,7 +16,7 @@ constexpr double MAXIMUM_RATIO_FOR_ATTACKER_TO_FLEE = 0.50;
auto AIAttackerStrategySelector::BestAttackerStrategy(
const PlayerId attackerPid,
const net::eagle0::shardok::storage::fb::GameState* gameState,
const GameStateW& gameState,
const CoordsSet& criticalTileCoords,
const APDCache& apdCache,
const ALCache& alCache,
@@ -69,10 +70,7 @@ auto AIAttackerStrategySelector::BestAttackerStrategy(
chosenStrategy = CrossRiversStrategy(startCrossingLocations);
} else if (attackerUnitCount < criticalTileCoords.size()) {
chosenStrategy = AttackUnitsStrategy(GenerateTargetPriorities(
Occupants(
*gameState->units(),
gameState->hex_map()->row_count(),
gameState->hex_map()->column_count()),
gameState.GetOccupantsVector(),
gameState->hex_map(),
defenderPositions,
attackerPid,
@@ -86,10 +84,7 @@ auto AIAttackerStrategySelector::BestAttackerStrategy(
// Otherwise, try to hold the castles.
else if (defenderOccupiedCriticalTileCount > 0) {
chosenStrategy = AttackCastlesStrategy(GenerateTargetPriorities(
Occupants(
*gameState->units(),
gameState->hex_map()->row_count(),
gameState->hex_map()->column_count()),
gameState.GetOccupantsVector(),
gameState->hex_map(),
criticalTileCoords,
attackerPid,
@@ -8,6 +8,7 @@
#include "src/main/cpp/net/eagle0/shardok/ai/AIAttackLocations.hpp"
#include "src/main/cpp/net/eagle0/shardok/ai/AIStrategy.hpp"
#include "src/main/cpp/net/eagle0/shardok/ai/AIWaterCrossingCommandChooser.hpp"
#include "src/main/cpp/net/eagle0/shardok/library/GameStateW.hpp"
#include "src/main/cpp/net/eagle0/shardok/library/map/CoordsSet.hpp"
#include "src/main/flatbuffer/net/eagle0/shardok/storage/game_state.hpp"
@@ -19,7 +20,7 @@ class AIAttackerStrategySelector {
public:
static auto BestAttackerStrategy(
PlayerId attackerPid,
const GameState* gameState,
const GameStateW& gameState,
const CoordsSet& criticalTileCoords,
const APDCache& apdCache,
const ALCache& alCache,
@@ -17,6 +17,7 @@
#include "src/main/cpp/net/eagle0/shardok/ai/AIUnitScoreCalculator.hpp"
#include "src/main/cpp/net/eagle0/shardok/ai/AIVictoryConditionScoreCalculator.hpp"
#include "src/main/cpp/net/eagle0/shardok/ai/AIWaterCrossingCommandChooser.hpp"
#include "src/main/cpp/net/eagle0/shardok/library/GameStateW.hpp"
#include "src/main/cpp/net/eagle0/shardok/library/ShardokEngine.hpp"
#include "src/main/cpp/net/eagle0/shardok/library/action_point_distances/ActionPointDistancesCache.hpp"
#include "src/main/cpp/net/eagle0/shardok/library/util/HexCubeUtils.hpp"
@@ -253,7 +254,7 @@ auto AttackerMultiplierForTargetDistance(
}
auto AttackerUnitsScore(
const GameState *gameState,
const GameStateW &gameState,
int roundsRemaining,
const SettingsGetter &settings,
bool attackerWantsCastles,
@@ -276,10 +277,7 @@ auto AttackerUnitsScore(
double attackerUnitsValue = 0;
double defenderUnitsValue = 0;
auto occupants = Occupants(
*gameState->units(),
gameState->hex_map()->row_count(),
gameState->hex_map()->column_count());
const auto &occupants = gameState.GetOccupantsVector();
for (const Unit *unit : *gameState->units()) {
const auto *pi = PlayerInfoForPid(gameState, unit->player_id());
@@ -370,11 +368,9 @@ auto AttackerUnitsScore(
auto uv = UnitValue(
unit,
true,
attackerUnits,
gameState,
attackerWantsCastles,
/* includeCastleBonus=*/true,
defenderUnits,
gameState->hex_map(),
roundsRemaining,
attackLocationsForAttacker,
locationsCausingDanger,
@@ -398,11 +394,9 @@ auto AttackerUnitsScore(
auto dv = UnitValue(
unit,
false,
attackerUnits,
gameState,
attackerWantsCastles,
/* includeCastleBonus=*/!defenderShouldScatter,
defenderUnits,
gameState->hex_map(),
roundsRemaining,
attackLocationsForDefender,
locationsCausingDangerForAttacker,
@@ -526,7 +520,7 @@ auto AIScoreCalculator::FleeStrategyScoreForState(
}
auto AIScoreCalculator::DefenderScatterStrategyScoreForState(
const GameState *gameState,
const GameStateW &gameState,
const int roundsRemaining,
const SettingsGetter &settings,
const ALCache &alCache,
@@ -562,7 +556,7 @@ auto AIScoreCalculator::DefenderScatterStrategyScoreForState(
}
auto AIScoreCalculator::DefenderHoldCastlesStrategyScoreForState(
const GameState *gameState,
const GameStateW &gameState,
const CoordsSet &castleCoords,
const int roundsRemaining,
const SettingsGetter &settings,
@@ -602,7 +596,7 @@ auto AIScoreCalculator::DefenderHoldCastlesStrategyScoreForState(
}
auto AIScoreCalculator::DefenderScoreForState(
const GameState *gameState,
const GameStateW &gameState,
const AIStrategy &defenderStrategy,
const CoordsSet &castleCoords,
const int roundsRemaining,
@@ -658,7 +652,7 @@ auto AIScoreCalculator::DefenderScoreForState(
}
auto AIScoreCalculator::AttackerScoreForState(
const GameState *gameState,
const GameStateW &gameState,
const AIStrategy &attackerStrategy,
const CoordsSet &castleCoords,
const int roundsRemaining,
@@ -741,7 +735,7 @@ auto AIScoreCalculator::AttackerScoreForState(
[[nodiscard]] auto AIScoreCalculator::GuessedStateScore(
const bool isDefender,
const GameState *state,
const GameStateW &state,
const AIStrategy &aiStrategy,
const CoordsSet &allCastleCoords,
const SettingsGetter &settingsGetter,
@@ -843,7 +837,7 @@ auto AIScoreCalculator::CalcOne(
auto innerUtility = GuessedStateScore(
isDefender,
innerEngine->GetCurrentGameState(),
innerEngine->GetCurrentGameStateW(),
attackerStrategy,
allCastleCoords,
settingsGetter,
@@ -9,6 +9,7 @@
#include "src/main/cpp/net/eagle0/shardok/ai/AIAttackLocations.hpp"
#include "src/main/cpp/net/eagle0/shardok/ai/AIStrategy.hpp"
#include "src/main/cpp/net/eagle0/shardok/library/GameStateW.hpp"
#include "src/main/cpp/net/eagle0/shardok/library/ShardokCTypes.h"
#include "src/main/cpp/net/eagle0/shardok/library/ShardokEngine.hpp"
#include "src/main/cpp/net/eagle0/shardok/library/action_point_distances/ActionPointDistancesCache.hpp"
@@ -39,14 +40,14 @@ public:
private:
[[nodiscard]] static auto DefenderScatterStrategyScoreForState(
const GameState *gameState,
const GameStateW &gameState,
int roundsRemaining,
const SettingsGetter &settings,
const ALCache &alCache,
const APDCache &apdCache) -> ScoreValue;
[[nodiscard]] static auto DefenderHoldCastlesStrategyScoreForState(
const GameState *gameState,
const GameStateW &gameState,
const CoordsSet &castleCoords,
int roundsRemaining,
const SettingsGetter &settings,
@@ -58,7 +59,7 @@ private:
PlayerId playerId) -> ScoreValue;
[[nodiscard]] static auto DefenderScoreForState(
const GameState *gameState,
const GameStateW &gameState,
const AIStrategy &defenderStrategy,
const CoordsSet &castleCoords,
int roundsRemaining,
@@ -67,7 +68,7 @@ private:
const APDCache &apdCache) -> ScoreValue;
[[nodiscard]] static auto AttackerScoreForState(
const GameState *gameState,
const GameStateW &gameState,
const AIStrategy &attackerStrategy,
const CoordsSet &castleCoords,
int roundsRemaining,
@@ -129,7 +130,7 @@ private:
public:
[[nodiscard]] static auto GuessedStateScore(
bool isDefender,
const GameState *state,
const GameStateW &state,
const AIStrategy &aiStrategy,
const CoordsSet &allCastleCoords,
const SettingsGetter &settingsGetter,
@@ -7,6 +7,8 @@
#include <algorithm>
#include "AIAttackLocations.hpp"
#include "AIScoreUtilities.hpp"
#include "src/main/cpp/net/eagle0/shardok/library/GameStateW.hpp"
#include "src/main/cpp/net/eagle0/shardok/library/util/HexMapUtils.hpp"
namespace shardok {
@@ -131,8 +133,8 @@ auto lightningValue(const Unit *unit) -> double {
auto meteorDropRawValue(
const Coords &targetCoords,
const HexMap *map,
const vector<const Unit *> &enemyOccupants,
const vector<const Unit *> &friendlyOccupants) -> double {
const vector<const Unit *> &occupants,
const Unit *castingUnit) -> double {
double rawMeteorDropValue = 0.0;
if (targetCoords.row() >= 0) {
const double castleMultiplier = GetTerrain(map, targetCoords)->modifier().castle().present()
@@ -140,13 +142,16 @@ auto meteorDropRawValue(
: 1.0;
const int index = targetCoords.row() * map->column_count() + targetCoords.column();
if (const Unit *directOccupant = enemyOccupants[index]) {
rawMeteorDropValue += directOccupant->battalion().size() * kMeteorDirectTargetingEnemy *
castleMultiplier;
}
if (const Unit *directFriendly = friendlyOccupants[index]) {
rawMeteorDropValue += directFriendly->battalion().size() *
kMeteorDirectTargetingFriendly * castleMultiplier;
if (const Unit *directOccupant = occupants[index]) {
if (directOccupant->player_id() == castingUnit->player_id()) {
// Friendly unit
rawMeteorDropValue += directOccupant->battalion().size() *
kMeteorDirectTargetingFriendly * castleMultiplier;
} else {
// Enemy unit
rawMeteorDropValue += directOccupant->battalion().size() *
kMeteorDirectTargetingEnemy * castleMultiplier;
}
}
for (const auto &splashCoords : HexMapUtils::GetAdjacentCoords(map, targetCoords)) {
@@ -157,14 +162,16 @@ auto meteorDropRawValue(
const auto splashIndex =
splashCoords.row() * map->column_count() + splashCoords.column();
if (const Unit *splashOccupant = enemyOccupants[splashIndex]) {
rawMeteorDropValue += splashOccupant->battalion().size() *
kMeteorSplashTargetingEnemy * splashCastleMultiplier;
}
if (const Unit *splashFriendly = friendlyOccupants[splashIndex]) {
rawMeteorDropValue += splashFriendly->battalion().size() *
kMeteorSplashTargetingFriendly * splashCastleMultiplier;
if (const Unit *splashOccupant = occupants[splashIndex]) {
if (splashOccupant->player_id() == castingUnit->player_id()) {
// Friendly unit
rawMeteorDropValue += splashOccupant->battalion().size() *
kMeteorSplashTargetingFriendly * splashCastleMultiplier;
} else {
// Enemy unit
rawMeteorDropValue += splashOccupant->battalion().size() *
kMeteorSplashTargetingEnemy * splashCastleMultiplier;
}
}
}
} else {
@@ -177,14 +184,12 @@ auto meteorDropRawValue(
auto bestTargetValue(
const Unit *unit,
const HexMap *map,
const vector<const Unit *> &enemyOccupants,
const vector<const Unit *> &friendlyOccupants,
const vector<const Unit *> &occupants,
const int meteorRange) -> double {
double maxValue = 0.0;
for (const Coords &target : TilesWithinDistance(map, unit->location(), meteorRange)) {
if (const double thisTargetValue =
meteorDropRawValue(target, map, enemyOccupants, friendlyOccupants);
if (const double thisTargetValue = meteorDropRawValue(target, map, occupants, unit);
thisTargetValue > maxValue) {
maxValue = thisTargetValue;
}
@@ -197,20 +202,15 @@ auto meteorValue(
const Unit *unit,
const int roundsRemaining,
const HexMap *map,
const vector<const Unit *> &enemyUnits,
const vector<const Unit *> &friendlyUnits,
const vector<const Unit *> &occupants,
const int meteorRange,
const double minVigorToCast) -> double {
const auto enemyOccupants = Occupants(enemyUnits, map->row_count(), map->column_count());
const auto friendlyOccupants = Occupants(friendlyUnits, map->row_count(), map->column_count());
switch (unit->attached_hero().profession_info().meteor_cast_state()) {
case net::eagle0::shardok::storage::fb::MultiroundMagicState_NONE:
case net::eagle0::shardok::storage::fb::MultiroundMagicState_START: {
if ((roundsRemaining > 8 ||
(unit->attached_hero().vigor() > 50 && roundsRemaining > 4))) {
const double maxValue =
bestTargetValue(unit, map, enemyOccupants, friendlyOccupants, meteorRange);
const double maxValue = bestTargetValue(unit, map, occupants, meteorRange);
double multiplier = kMeteorStartRoundMultiplier;
if (unit->attached_hero().profession_info().meteor_cast_state() ==
@@ -233,18 +233,18 @@ auto meteorValue(
return meteorDropRawValue(
unit->attached_hero().profession_info().cast_target(),
map,
enemyOccupants,
friendlyOccupants);
occupants,
unit);
} else {
return bestTargetValue(unit, map, enemyOccupants, friendlyOccupants, meteorRange);
return bestTargetValue(unit, map, occupants, meteorRange);
}
case net::eagle0::shardok::storage::fb::MultiroundMagicState_CAST:
return kMeteorCastRoundMultiplier *
meteorDropRawValue(
unit->attached_hero().profession_info().cast_target(),
map,
enemyOccupants,
friendlyOccupants);
occupants,
unit);
default: break;
}
@@ -252,6 +252,7 @@ auto meteorValue(
return 0.0;
}
// Original version that accepts unit vectors for backward compatibility
auto GetRangedAttackBonus(
const Unit *unit,
const bool isAttacker,
@@ -263,10 +264,35 @@ auto GetRangedAttackBonus(
const vector<const Unit *> &defenderUnits,
const int meteorRange,
const double minVigorToCast) -> double {
vector<double> rangedAttackValues{};
// Create occupants vector for meteor calculations
vector<const Unit *> allUnits;
allUnits.insert(allUnits.end(), attackerUnits.begin(), attackerUnits.end());
allUnits.insert(allUnits.end(), defenderUnits.begin(), defenderUnits.end());
const auto occupants = Occupants(allUnits, map->row_count(), map->column_count());
return GetRangedAttackBonus(
unit,
isAttacker,
map,
terrain,
attackLocations,
roundsRemaining,
occupants,
meteorRange,
minVigorToCast);
}
const auto &enemyUnits = isAttacker ? defenderUnits : attackerUnits;
const auto &friendlyUnits = isAttacker ? attackerUnits : defenderUnits;
// Optimized version that accepts occupants vector directly
auto GetRangedAttackBonus(
const Unit *unit,
const bool isAttacker,
const HexMap *map,
const Terrain *terrain,
const AttackLocations &attackLocations,
const int roundsRemaining,
const vector<const Unit *> &occupants,
const int meteorRange,
const double minVigorToCast) -> double {
vector<double> rangedAttackValues{};
const auto &unitLocation = unit->location();
@@ -280,14 +306,8 @@ auto GetRangedAttackBonus(
net::eagle0::shardok::storage::fb::MultiroundMagicState_NONE) {
rangedAttackValues.push_back(lightningValue(unit));
} else {
const double mv = meteorValue(
unit,
roundsRemaining,
map,
enemyUnits,
friendlyUnits,
meteorRange,
minVigorToCast);
const double mv =
meteorValue(unit, roundsRemaining, map, occupants, meteorRange, minVigorToCast);
rangedAttackValues.push_back(mv);
}
}
@@ -325,16 +345,29 @@ auto GetRangedAttackBonus(
auto UnitValue(
const Unit *unit,
const bool isAttacker,
const vector<const Unit *> &attackerUnits,
const GameStateW &gameState,
const bool attackerWantsCastles,
const bool includeCastleBonus,
const vector<const Unit *> &defenderUnits,
const HexMap *map,
const int roundsRemaining,
const AttackLocations &locationsThisSideCanAttackFrom,
const CoordsSet &locationsInDangerFromEnemy,
const ActionPointDistances *distances,
const SettingsGetter &settings) -> ScoreValue {
const HexMap *map = gameState->hex_map();
// Get attacker and defender units from the game state
vector<const Unit *> attackerUnits{};
vector<const Unit *> defenderUnits{};
for (const Unit *u : *gameState->units()) {
if (u->status() != net::eagle0::shardok::storage::fb::UnitStatus_NORMAL_UNIT) continue;
const auto *pi = PlayerInfoForPid(gameState, u->player_id());
if (pi == nullptr) continue;
if (pi->is_defender()) {
defenderUnits.push_back(u);
} else {
attackerUnits.push_back(u);
}
}
const auto &location = unit->location();
if (location.row() < 0) return 0; // unplaced unit
@@ -376,8 +409,7 @@ auto UnitValue(
terrain,
locationsThisSideCanAttackFrom,
roundsRemaining,
attackerUnits,
defenderUnits,
gameState.GetOccupantsVector(),
settings.Backing().meteor_range(),
settings.Backing().meteor_cast_vigor_cost());
@@ -5,6 +5,7 @@
#ifndef EAGLE0_AIUNITSCORECALCULATOR_HPP
#define EAGLE0_AIUNITSCORECALCULATOR_HPP
#include "src/main/cpp/net/eagle0/shardok/library/GameStateW.hpp"
#include "src/main/cpp/net/eagle0/shardok/library/action_point_distances/ActionPointDistances.hpp"
#include "src/main/cpp/net/eagle0/shardok/library/map/CoordsSet.hpp"
#include "src/main/cpp/net/eagle0/shardok/library/settings/GameSettings.hpp"
@@ -34,14 +35,24 @@ auto GetRangedAttackBonus(
int meteorRange,
double minVigorToCast) -> double;
// Overload that accepts occupants vector for cached performance
auto GetRangedAttackBonus(
const Unit *unit,
bool isAttacker,
const HexMap *map,
const Terrain *terrain,
const AttackLocations &attackLocations,
int roundsRemaining,
const vector<const Unit *> &occupants,
int meteorRange,
double minVigorToCast) -> double;
auto UnitValue(
const Unit *unit,
bool isAttacker,
const vector<const Unit *> &attackerUnits,
const GameStateW &gameState,
bool attackerWantsCastles,
bool includeCastleBonus,
const vector<const Unit *> &defenderUnits,
const HexMap *map,
int roundsRemaining,
const AttackLocations &locationsThisSideCanAttackFrom,
const CoordsSet &locationsInDangerFromEnemy,
@@ -122,7 +122,7 @@ auto AttackerDebufForDefenderOccupiedCriticalTile(
}
auto DefenderHoldsCriticalTilesVictoryScore(
const net::eagle0::shardok::storage::fb::GameState* gameState,
const GameStateW& gameState,
const CoordsSet& criticalTileLocations,
const PlayerInfo* player,
const APDCache& apdCache,
@@ -130,9 +130,8 @@ auto DefenderHoldsCriticalTilesVictoryScore(
const SettingsGetter& settings) -> ScoreValue {
ScoreValue total = 0.0;
const auto rc = gameState->hex_map()->row_count();
const auto cc = gameState->hex_map()->column_count();
const auto occupants = Occupants(*gameState->units(), rc, cc);
const auto& occupants = gameState.GetOccupantsVector();
for (const Coords& criticalTileLocation : criticalTileLocations) {
const auto index = criticalTileLocation.row() * cc + criticalTileLocation.column();
@@ -152,7 +151,7 @@ auto DefenderHoldsCriticalTilesVictoryScore(
}
auto AttackerHoldsCriticalTilesVictoryScore(
const net::eagle0::shardok::storage::fb::GameState* gameState,
const GameStateW& gameState,
const CoordsSet& criticalTileLocations,
const PlayerInfo* player,
const APDCache& apdCache,
@@ -178,9 +177,8 @@ auto AttackerHoldsCriticalTilesVictoryScore(
ScoreValue total = 0.0;
const auto rc = gameState->hex_map()->row_count();
const auto cc = gameState->hex_map()->column_count();
const auto occupants = Occupants(*gameState->units(), rc, cc);
const auto& occupants = gameState.GetOccupantsVector();
for (const Coords& criticalTileLocation : criticalTileLocations) {
const auto index = criticalTileLocation.row() * cc + criticalTileLocation.column();
@@ -246,7 +244,7 @@ auto AttackerHoldsCriticalTilesVictoryScore(
}
auto LastPlayerStandingVictoryScore(
const GameState* gameState,
const GameStateW& gameState,
const PlayerInfo* player,
const APDCache& apdCache,
const ALCache& alCache,
@@ -9,6 +9,7 @@
#include "src/main/cpp/net/eagle0/shardok/ai/AIAttackGroups.hpp"
#include "src/main/cpp/net/eagle0/shardok/ai/AIAttackLocations.hpp"
#include "src/main/cpp/net/eagle0/shardok/library/GameStateW.hpp"
#include "src/main/cpp/net/eagle0/shardok/library/action_point_distances/ActionPointDistancesCache.hpp"
#include "src/main/cpp/net/eagle0/shardok/library/map/CoordsSet.hpp"
#include "src/main/cpp/net/eagle0/shardok/library/settings/GameSettings.hpp"
@@ -23,7 +24,7 @@ using std::vector;
using ScoreValue = double;
auto AttackerHoldsCriticalTilesVictoryScore(
const net::eagle0::shardok::storage::fb::GameState* gameState,
const GameStateW& gameState,
const CoordsSet& criticalTileLocations,
const PlayerInfo* player,
const APDCache& apdCache,
@@ -31,7 +32,7 @@ auto AttackerHoldsCriticalTilesVictoryScore(
const SettingsGetter& settings) -> ScoreValue;
auto DefenderHoldsCriticalTilesVictoryScore(
const net::eagle0::shardok::storage::fb::GameState* gameState,
const GameStateW& gameState,
const CoordsSet& criticalTileLocations,
const PlayerInfo* player,
const APDCache& apdCache,
@@ -39,7 +40,7 @@ auto DefenderHoldsCriticalTilesVictoryScore(
const SettingsGetter& settings) -> ScoreValue;
auto LastPlayerStandingVictoryScore(
const GameState* gameState,
const GameStateW& gameState,
const PlayerInfo* player,
const APDCache& apdCache,
const ALCache& alCache,
@@ -14,6 +14,7 @@ cc_library(
":ai_score_utilities",
":ai_strategy",
":ai_water_crossing_command_chooser",
"//src/main/cpp/net/eagle0/shardok/library:game_state_w",
"//src/main/cpp/net/eagle0/shardok/library/map:coords_set",
"//src/main/cpp/net/eagle0/shardok/library/util:hex_map_utils",
"//src/main/flatbuffer/net/eagle0/shardok/storage:game_state_cc_fbs",
@@ -162,6 +163,7 @@ cc_library(
":ai_victory_condition_score_calculator",
"//src/main/cpp/net/eagle0/common:sequence_random_generator",
"//src/main/cpp/net/eagle0/shardok/library:engine",
"//src/main/cpp/net/eagle0/shardok/library:game_state_w",
"//src/main/cpp/net/eagle0/shardok/library/view_filters:game_state_guesser",
],
)
@@ -191,6 +193,8 @@ cc_library(
],
deps = [
":ai_attack_locations",
":ai_score_utilities",
"//src/main/cpp/net/eagle0/shardok/library:game_state_w",
"//src/main/cpp/net/eagle0/shardok/library/action_point_distances",
"//src/main/cpp/net/eagle0/shardok/library/util:hex_map_utils",
],
@@ -210,6 +214,7 @@ cc_library(
":ai_attack_locations",
":ai_distance_debuf",
":ai_score_utilities",
"//src/main/cpp/net/eagle0/shardok/library:game_state_w",
"//src/main/cpp/net/eagle0/shardok/library/action_point_distances",
"//src/main/cpp/net/eagle0/shardok/library/action_point_distances:action_point_distances_cache",
"//src/main/flatbuffer/net/eagle0/shardok/storage:game_state_cc_fbs",
@@ -2,12 +2,16 @@ load("//tools:copts.bzl", "COPTS")
cc_library(
name = "game_state_w",
srcs = ["GameStateW.cpp"],
hdrs = ["GameStateW.hpp"],
copts = COPTS,
visibility = ["//visibility:public"],
deps = [
":shardok_c_types",
"//src/main/cpp/net/eagle0/common:container_utils",
"//src/main/cpp/net/eagle0/shardok/library/fb_helpers:flatbuffer_wrapper",
"//src/main/flatbuffer/net/eagle0/shardok/storage:game_state_cc_fbs",
"//src/main/flatbuffer/net/eagle0/shardok/storage:unit_cc_fbs",
],
)
@@ -0,0 +1,160 @@
//
// Created by Dan Crosby on 2025-01-17.
//
#include "GameStateW.hpp"
#include <stdexcept>
#include <utility>
#include "src/main/cpp/net/eagle0/common/ContainerUtils.hpp"
namespace shardok {
/**
* @class OccupantsCache
* @brief Cache for efficient unit location lookups.
*
* This cache maintains a vector indexed by map position (row * columnCount + column)
* containing pointers to units at each position. This converts O(n) unit lookups
* into O(1) operations.
*/
class OccupantsCache {
public:
OccupantsCache(const GameStateW& gameState) { Regenerate(gameState); }
void Regenerate(const GameStateW& gameState) {
const auto* state = gameState.Get();
if (!state || !state->hex_map() || !state->units()) {
occupants_.clear();
rowCount_ = 0;
columnCount_ = 0;
return;
}
rowCount_ = state->hex_map()->row_count();
columnCount_ = state->hex_map()->column_count();
const size_t mapSize = rowCount_ * columnCount_;
// Clear and resize the vector
occupants_.clear();
occupants_.resize(mapSize, nullptr);
// Populate the cache
for (const auto* unit : *state->units()) {
if (unit &&
unit->status() == net::eagle0::shardok::storage::fb::UnitStatus_NORMAL_UNIT) {
const auto& location = unit->location();
if (location.row() >= 0 && location.row() < rowCount_ && location.column() >= 0 &&
location.column() < columnCount_) {
const size_t index = location.row() * columnCount_ + location.column();
occupants_[index] = unit;
}
}
}
}
[[nodiscard]] auto GetOccupant(const net::eagle0::shardok::storage::fb::Coords& coords) const
-> const GameStateW::Unit* {
if (coords.row() < 0 || coords.row() >= rowCount_ || coords.column() < 0 ||
coords.column() >= columnCount_) {
return nullptr;
}
const size_t index = coords.row() * columnCount_ + coords.column();
return occupants_[index];
}
[[nodiscard]] auto GetOccupantsVector() const -> const std::vector<const GameStateW::Unit*>& {
return occupants_;
}
private:
std::vector<const GameStateW::Unit*> occupants_;
int16_t rowCount_ = 0;
int16_t columnCount_ = 0;
};
// GameStateW implementation
GameStateW::GameStateW() : BaseType() { InitializeCache(); }
GameStateW::GameStateW(const GameStateW& other)
: BaseType(other),
occupantsCache_(other.occupantsCache_) {
// Share the cache - no need to rebuild!
}
GameStateW::GameStateW(GameStateW&& other) noexcept
: BaseType(std::move(other)),
occupantsCache_(std::move(other.occupantsCache_)) {}
GameStateW& GameStateW::operator=(const GameStateW& other) {
if (this != &other) {
BaseType::operator=(other);
occupantsCache_ = other.occupantsCache_; // Share the cache
}
return *this;
}
GameStateW& GameStateW::operator=(GameStateW&& other) noexcept {
if (this != &other) {
BaseType::operator=(std::move(other));
occupantsCache_ = std::move(other.occupantsCache_);
}
return *this;
}
GameStateW::GameStateW(const BaseType& base) : BaseType(base) { InitializeCache(); }
GameStateW::GameStateW(BaseType&& base) : BaseType(std::move(base)) { InitializeCache(); }
GameStateW::GameStateW(flatbuffers::FlatBufferBuilder& fbb) : BaseType(fbb) { InitializeCache(); }
GameStateW::~GameStateW() = default;
auto GameStateW::GetOccupant(const net::eagle0::shardok::storage::fb::Coords& coords) const
-> const Unit* {
if (!occupantsCache_) {
throw std::runtime_error(
"GameStateW::GetOccupant() called with uninitialized occupants cache. "
"This indicates a bug where the cache was invalidated without being rebuilt. "
"Call RebuildOccupantsCache() or InitializeCache() after any mutations.");
}
return occupantsCache_->GetOccupant(coords);
}
auto GameStateW::GetOccupantsVector() const -> const std::vector<const Unit*>& {
if (!occupantsCache_) {
throw std::runtime_error(
"GameStateW::GetOccupantsVector() called with uninitialized occupants cache. "
"This indicates a bug where the cache was invalidated without being rebuilt. "
"Call RebuildOccupantsCache() or InitializeCache() after any mutations.");
}
return occupantsCache_->GetOccupantsVector();
}
auto GameStateW::GetKnownEnemyOccupant(
PlayerId playerId,
const std::vector<PlayerId>& allyPids,
const net::eagle0::shardok::storage::fb::Coords& coords) const -> const Unit* {
const auto* occupant = GetOccupant(coords);
if (occupant) {
if (!occupant->hidden() && occupant->player_id() != playerId &&
!common::Contains(allyPids, occupant->player_id())) {
return occupant;
}
}
return nullptr;
}
void GameStateW::RebuildOccupantsCache() {
occupantsCache_.reset();
occupantsCache_ = std::make_shared<OccupantsCache>(*this);
}
void GameStateW::InvalidateOccupantsCache() { occupantsCache_.reset(); }
void GameStateW::InitializeCache() { occupantsCache_ = std::make_shared<OccupantsCache>(*this); }
} // namespace shardok
@@ -5,11 +5,19 @@
#ifndef EAGLE0_GAMESTATEW_HPP
#define EAGLE0_GAMESTATEW_HPP
#include <memory>
#include <vector>
#include "src/main/cpp/net/eagle0/shardok/library/ShardokCTypes.h"
#include "src/main/cpp/net/eagle0/shardok/library/fb_helpers/FlatbufferWrapper.hpp"
#include "src/main/flatbuffer/net/eagle0/shardok/storage/game_state.hpp"
#include "src/main/flatbuffer/net/eagle0/shardok/storage/unit.hpp"
namespace shardok {
// Forward declaration for the cache
class OccupantsCache;
/**
* @class GameStateW
* @brief A wrapper class for the FlatBuffer-generated GameState type.
@@ -22,38 +30,116 @@ namespace shardok {
* This class is part of the shardok namespace and is designed to simplify
* interactions with the GameState FlatBuffer type while maintaining the
* flexibility and functionality of the Wrapper base class.
*
* The class includes an occupants cache for efficient unit location lookups,
* which significantly improves performance for operations that frequently
* query which units occupy specific map coordinates.
*/
class GameStateW : public Wrapper<net::eagle0::shardok::storage::fb::GameState> {
public:
using BaseType = Wrapper<net::eagle0::shardok::storage::fb::GameState>;
using Unit = net::eagle0::shardok::storage::fb::Unit;
// Inherit all constructors from Wrapper
using BaseType::BaseType;
// Default constructor
GameStateW() : BaseType() {}
GameStateW();
// Copy constructor
GameStateW(const GameStateW& other) : BaseType(other) {}
GameStateW(const GameStateW& other);
// Move constructor
GameStateW(GameStateW&& other) noexcept : BaseType(std::move(other)) {}
GameStateW(GameStateW&& other) noexcept;
// Copy assignment
GameStateW& operator=(const GameStateW& other) {
BaseType::operator=(other);
return *this;
}
GameStateW& operator=(const GameStateW& other);
// Move assignment
GameStateW& operator=(GameStateW&& other) noexcept {
BaseType::operator=(std::move(other));
return *this;
}
GameStateW& operator=(GameStateW&& other) noexcept;
// Constructor from base type
GameStateW(const BaseType& base) : BaseType(base) {}
GameStateW(BaseType&& base) : BaseType(std::move(base)) {}
GameStateW(const BaseType& base);
GameStateW(BaseType&& base);
// Constructor from FlatBufferBuilder
explicit GameStateW(flatbuffers::FlatBufferBuilder& fbb);
// Destructor
~GameStateW();
/**
* @brief Get the unit occupying the specified coordinates.
* @param coords The coordinates to check.
* @return Pointer to the unit at the coordinates, or nullptr if none.
*
* This is an O(1) operation using the internal occupants cache.
*/
[[nodiscard]] auto GetOccupant(const net::eagle0::shardok::storage::fb::Coords& coords) const
-> const Unit*;
/**
* @brief Get all occupants indexed by their position.
* @return Vector where index = row * columnCount + column contains unit pointer or nullptr.
*
* This provides direct access to the occupants cache for bulk operations.
*/
[[nodiscard]] auto GetOccupantsVector() const -> const std::vector<const Unit*>&;
/**
* @brief Get the known enemy unit occupying the specified coordinates.
* @param playerId The player ID to check enemies for.
* @param allyPids Vector of allied player IDs.
* @param coords The coordinates to check.
* @return Pointer to the enemy unit at the coordinates, or nullptr if none.
*
* This is an O(1) operation using the internal occupants cache.
*/
[[nodiscard]] auto GetKnownEnemyOccupant(
PlayerId playerId,
const std::vector<PlayerId>& allyPids,
const net::eagle0::shardok::storage::fb::Coords& coords) const -> const Unit*;
/**
* @brief Rebuild the occupants cache.
*
* This method invalidates the existing cache and immediately rebuilds it to reflect
* the current state of units. Call this method after any mutations to the game state
* that might affect unit positions.
*
* @warning This method is NOT thread-safe. It should only be called:
* - After mutations when the GameStateW instance is owned by a single thread
* - When transitioning ownership between threads (with proper synchronization)
*
* Concurrent calls to RebuildOccupantsCache() or calls concurrent with GetOccupant()
* will result in undefined behavior due to race conditions.
*/
void RebuildOccupantsCache();
/**
* @brief Initialize the occupants cache.
* This rebuilds the cache from the current game state.
*
* @warning This method is NOT thread-safe. It should only be called:
* - During GameStateW construction (automatically handled)
* - After mutations when the GameStateW instance is owned by a single thread
* - When transitioning ownership between threads (with proper synchronization)
*
* Concurrent calls to InitializeCache() or calls concurrent with GetOccupant()
* will result in undefined behavior due to race conditions.
*/
void InitializeCache();
/**
* @brief Force regeneration of the occupants cache.
* @deprecated Use RebuildOccupantsCache() instead. This method is kept for
* backward compatibility but should not be used in new code.
*/
void InvalidateOccupantsCache();
private:
// The occupants cache implementation - shared across copies for efficiency
std::shared_ptr<OccupantsCache> occupantsCache_;
};
} // namespace shardok
@@ -42,6 +42,10 @@ using TileModifierProto = net::eagle0::shardok::common::TileModifier;
return gameState.Get();
}
[[nodiscard]] auto ShardokEngine::GetCurrentGameStateW() const -> const GameStateW & {
return gameState;
}
[[nodiscard]] auto ShardokEngine::GetCurrentGameStateBytes() const -> byte_vector {
return gameState.ToByteVector();
}
@@ -253,7 +257,7 @@ auto ShardokEngine::PostWhilePlayerHasOnlyOneOption(
}
auto updateAction = UpdateGameStatusAction(
GetCurrentGameState(),
GetCurrentGameStateW(),
criticalTileCoords,
settingsGetter,
false /* atEndOfRound */);
@@ -267,7 +271,7 @@ void ShardokEngine::HandlePlayerTurnEnd(const std::shared_ptr<RandomGenerator> &
if (static_cast<uint32_t>(GetCurrentGameState()->current_player()) >=
GetCurrentGameState()->player_infos()->size()) {
ApplyAndAddActionResults(UpdateGameStatusAction(
GetCurrentGameState(),
GetCurrentGameStateW(),
criticalTileCoords,
settingsGetter,
true /* atEndOfRound */)
@@ -291,7 +295,7 @@ void ShardokEngine::HandlePlayerTurnEnd(const std::shared_ptr<RandomGenerator> &
} else {
const auto updateAction = UpdateGameStatusAction(
GetCurrentGameState(),
GetCurrentGameStateW(),
criticalTileCoords,
settingsGetter,
false /* atEndOfRound */);
@@ -356,7 +360,7 @@ void ShardokEngine::PostPlacementCommands(
}
const auto updateAction = UpdateGameStatusAction(
GetCurrentGameState(),
GetCurrentGameStateW(),
criticalTileCoords,
settingsGetter,
false /* atEndOfRound */);
@@ -438,7 +442,7 @@ void ShardokEngine::PostActionUnchecked(
}
const auto updateAction = UpdateGameStatusAction(
GetCurrentGameState(),
GetCurrentGameStateW(),
criticalTileCoords,
settingsGetter,
false /* atEndOfRound */);
@@ -112,8 +112,8 @@ public:
[[nodiscard]] auto GetGameStateAtStartOfAction(ActionId startingActionId) const -> GameStateW;
[[nodiscard]] auto GetCurrentGameState() const
-> net::eagle0::shardok::storage::fb::GameState const *;
[[nodiscard]] auto GetCurrentGameState() const -> fb::GameState const *;
[[nodiscard]] auto GetCurrentGameStateW() const -> const GameStateW &;
[[nodiscard]] auto GetCurrentGameStateBytes() const -> byte_vector;
@@ -28,7 +28,7 @@ auto PlayerSetupCommandFactory::AddAvailablePlaceAndHideUnitCommandsForOneUnit(
CoordsSet unusedStartingPositions(gameState->hex_map());
for (const Coords *possiblePosition : *thisUnitStartingPositions) {
if (!Occupant(gameState->units(), *possiblePosition)) {
if (!gameState.GetOccupant(*possiblePosition)) {
unusedStartingPositions.Add(*possiblePosition);
}
}
@@ -37,7 +37,7 @@ auto PlayerSetupCommandFactory::AddAvailablePlaceAndHideUnitCommandsForOneUnit(
CoordsSet unusedHidingPositions(gameState->hex_map());
for (const Coords &possibleHidingPosition : GetAllCoords(gameState->hex_map())) {
if (!Occupant(gameState->units(), possibleHidingPosition)) {
if (!gameState.GetOccupant(possibleHidingPosition)) {
const Terrain *terrain = GetTerrain(gameState->hex_map(), possibleHidingPosition);
if (AllowsHiding(terrain)) { unusedHidingPositions.Add(possibleHidingPosition); }
}
@@ -392,6 +392,9 @@ void MutatingApplyResult(
mutatingGameState->mutable_possible_chargee_ids()->Mutate(i, -1);
}
// Rebuild the occupants cache since units may have moved or been destroyed
mutatingGameState.RebuildOccupantsCache();
return;
}
@@ -71,14 +71,15 @@ auto ToVector(const Units &units) -> vector<const Unit *> {
auto FallIntoWaterAction::AdjacentsWithTerrain(
const Coords &location,
const GameState *gameState,
const GameStateW &gameState,
const PlayerId playerId) -> vector<AdjacentWithTerrain> {
const CoordsSet adjacentCoords = HexMapUtils::GetAdjacentCoords(gameState->hex_map(), location);
vector<AdjacentWithTerrain> vec{};
for (const Coords &adjCoords : adjacentCoords) {
const auto *possibleOccupant = Occupant(gameState->units(), adjCoords);
// Note: This function takes GameState*, not GameStateW, so cannot use cache
const auto *possibleOccupant = gameState.GetOccupant(adjCoords);
if (possibleOccupant &&
(possibleOccupant->player_id() == playerId || !possibleOccupant->hidden())) {
continue;
@@ -150,8 +151,7 @@ auto FallIntoWaterAction::InternalExecute(
Terrain bestTerrain{};
for (const auto &adjWithTerrain : adjacentCoordsAndTerrain) {
const auto *possibleOccupant =
Occupant(currentState->units(), adjWithTerrain.adjacentCoords);
const auto *possibleOccupant = currentState.GetOccupant(adjWithTerrain.adjacentCoords);
if (possibleOccupant && (possibleOccupant->player_id() == fallerAfter.player_id() ||
!possibleOccupant->hidden())) {
continue;
@@ -172,7 +172,7 @@ auto FallIntoWaterAction::InternalExecute(
}
}
if (found && !Occupant(currentState->units(), bestCoords)) {
if (found && !currentState.GetOccupant(bestCoords)) {
PercentileRollOdds odds = EscapeChance(
baseEscapeOdds,
bestTerrain,
@@ -35,7 +35,7 @@ private:
static auto AdjacentsWithTerrain(
const Coords& location,
const GameState* gameState,
const GameStateW& gameState,
PlayerId playerId) -> vector<AdjacentWithTerrain>;
[[nodiscard]] auto InternalExecute(
@@ -183,7 +183,7 @@ auto MeteorCastAction::PerformOneActorCast(
results.push_back(mainResult);
const Coords target = actorBefore->attached_hero().profession_info().cast_target();
const Unit *possibleOccupant = Occupant(runningGameState->units(), target);
const Unit *possibleOccupant = runningGameState.GetOccupant(target);
const Terrain *targetTerrain = GetTerrain(startingGameState->hex_map(), target);
if (possibleOccupant) {
// Direct damage action
@@ -248,7 +248,7 @@ auto MeteorCastAction::PerformOneActorCast(
for (const Coords &splashCoords : adjacentCoords) {
const auto &splashTerrain = GetTerrain(runningGameState->hex_map(), splashCoords);
const Unit *splashOccupant = Occupant(runningGameState->units(), splashCoords);
const Unit *splashOccupant = runningGameState.GetOccupant(splashCoords);
if (splashOccupant) {
MeteorUnitDamageAction splashUnitDamageAction(
settings,
@@ -301,7 +301,7 @@ auto MeteorCastAction::PerformOneActorCast(
// Check for fallen heroes
for (const Coords &coords : destroyedBridgeOrIceTiles) {
const auto *maybeOccupant = Occupant(runningGameState->units(), coords);
const auto *maybeOccupant = runningGameState.GetOccupant(coords);
if (maybeOccupant) {
const BattalionTypeSPtr &battalionType =
settings.GetBattalionType(maybeOccupant->battalion().type());
@@ -229,7 +229,7 @@ auto UpdateGameStatusAction::InternalExecute(
// Check for attacker occupying castles & towns
bool foundUncontrolledCriticalTile = false;
for (const auto& criticalTile : criticalTileLocations) {
const auto* possibleOccupant = Occupant(gameState->units(), criticalTile);
const auto* possibleOccupant = gameState.GetOccupant(criticalTile);
if (!possibleOccupant) {
foundUncontrolledCriticalTile = true;
@@ -23,7 +23,7 @@ private:
const std::shared_ptr<RandomGenerator>& generator) const
-> vector<ActionResult> override;
const GameState* gameState;
const GameStateW& gameState;
const CoordsSet criticalTileLocations;
const SettingsGetter settingsGetter;
const bool atEndOfRound;
@@ -34,7 +34,7 @@ private:
public:
UpdateGameStatusAction(
const GameState* gameState,
const GameStateW& gameState,
const CoordsSet& criticalTileLocations,
const SettingsGetter& settingsGetter,
const bool atEndOfRound)
@@ -39,7 +39,7 @@ auto UpdateOpponentKnowledgeAction::InternalExecute(
for (const Coords &adjCoords :
HexMapUtils::GetAdjacentCoords(currentState->hex_map(), unit->location())) {
const auto &occupantOptional = Occupant(currentState->units(), adjCoords);
const auto &occupantOptional = currentState.GetOccupant(adjCoords);
if (occupantOptional && occupantOptional->player_id() != unitPid) {
MutatingBumpOpponentKnowledge(
&unitAfter,
@@ -44,6 +44,8 @@ auto ReduceCommandFactory::AddAvailableReduceCommands(
const auto *const occupant = Occupant(units, reduceCoords);
if (occupant && occupant->player_id() == playerId) continue;
// This function takes a Units*, not a GameStateW, so we can't use the cache method
// yet
const auto *const enemyOccupant =
KnownEnemyOccupant(playerId, units, allyPids, reduceCoords);
@@ -40,7 +40,7 @@ auto HideCommand::InternalExecute(
hideResult.mutable_player()->set_value(GetPlayerId());
hideResult.mutable_actor()->set_value(actorId);
const auto *occupant = Occupant(currentState->units(), target);
const auto *occupant = currentState.GetOccupant(target);
if (occupant) {
auto occupantAfter = *occupant;
occupantAfter.mutate_hidden(false);
@@ -56,7 +56,7 @@ auto HideCommand::InternalExecute(
vector<Unit> adjacentEnemyRangers{};
for (const auto &adjCoords :
HexMapUtils::GetAdjacentCoords(currentState->hex_map(), target)) {
const auto *oneOverOccupant = Occupant(currentState->units(), adjCoords);
const auto *oneOverOccupant = currentState.GetOccupant(adjCoords);
if (!oneOverOccupant) continue;
PlayerId occupantPid = oneOverOccupant->player_id();
if (occupantPid == GetPlayerId()) continue;
@@ -230,7 +230,7 @@ auto HolyWaveCommand::InternalExecute(
auto allyPids = AlliedPids(runningState, GetPlayerId());
for (const auto &coords :
HexMapUtils::GetAdjacentCoords(runningState->hex_map(), actorBefore->location())) {
const auto *occupant = Occupant(runningState->units(), coords);
const auto *occupant = runningState.GetOccupant(coords);
if (occupant) {
if (occupant->battalion().type() ==
net::eagle0::shardok::storage::fb::BattalionTypeId_UNDEAD) {
@@ -50,7 +50,7 @@ auto RaiseDeadCommand::InternalExecute(
auto &mutableActingHero = actorAfter.mutable_attached_hero();
const auto *occupant = Occupant(currentState->units(), target);
const auto *occupant = currentState.GetOccupant(target);
if (!occupant && PercentileRollSucceeds(odds, roll)) {
result.set_type(ActionType::RAISED_UNDEAD);
@@ -63,7 +63,7 @@ auto ReduceCommand::InternalExecute(
damageGiven);
*result.add_changed_tile_modifiers() = MakeTmc(target, modifierAfter);
const Unit* targetOccupant = Occupant(currentState->units(), target);
const Unit* targetOccupant = currentState.GetOccupant(target);
if (targetOccupant) {
CombatDamage unitDamageGiven =
CombatDamage::Builder()
@@ -60,7 +60,7 @@ auto ScoutCommand::InternalExecute(
AddChangedUnit(result, actorAfter);
const auto* directUnit = Occupant(currentState->units(), target);
const auto* directUnit = currentState.GetOccupant(target);
if (directUnit) {
auto possibleTarget =
@@ -70,7 +70,7 @@ auto ScoutCommand::InternalExecute(
for (const auto& adjacentCoords :
HexMapUtils::GetAdjacentCoords(currentState->hex_map(), target)) {
const auto* adjacentUnit = Occupant(currentState->units(), adjacentCoords);
const auto* adjacentUnit = currentState.GetOccupant(adjacentCoords);
auto adjacentTarget =
InternalScoutOneTile(actor, adjacentUnit, scoutAdjacentKnowledgeIncrement);
if (adjacentTarget.has_value()) { AddChangedUnit(result, adjacentTarget.value()); }
@@ -298,6 +298,7 @@ auto GameStateGuesser::GuessedState(
}
}
// Cache is already initialized from construction, no need to rebuild
return gsw;
}
@@ -215,6 +215,7 @@ TEST_F(PlayerSetupCommandFactoryTest, unplacedDefendingRanger_cannotHideOnOccupi
// Put an enemy at 2, 2
attacker1->mutable_location() = Coords(2, 2);
attacker1->mutate_status(net::eagle0::shardok::storage::fb::UnitStatus_NORMAL_UNIT);
gameState.RebuildOccupantsCache();
CommandList commands{};
factory.AddAvailablePlayerSetupCommands(commands, 1, gameState);
@@ -66,7 +66,7 @@ TEST_F(UpdateGameStatusActionTests, onePlayerLeft_addsGameOverResult) {
GetGameSettingsGetter(),
false);
vector<ActionResult> results = action.Execute(GameStateW(), nullptr);
vector<ActionResult> results = action.Execute(gameState, nullptr);
EXPECT_GE(results.size(), 1);
EXPECT_EQ(net::eagle0::shardok::common::GAME_OVER, results[results.size() - 1].type());
EXPECT_EQ(
@@ -111,7 +111,7 @@ TEST_F(UpdateGameStatusActionTests, twoPlayersLeft_noGameOverResult) {
GetGameSettingsGetter(),
false);
vector<ActionResult> results = action.Execute(GameStateW(), nullptr);
vector<ActionResult> results = action.Execute(gameState, nullptr);
EXPECT_EQ(results.size(), 0);
}
@@ -146,16 +146,15 @@ TEST_F(UpdateGameStatusActionTests, onePlayerAllUnitsHidden_gameOverResult) {
auto gameState = Wrapper<GameState>(fbb);
gameState->mutable_units()->GetMutableObject(1)->mutate_hidden(true);
gameState->mutable_units()->GetMutableObject(1)->mutate_hidden(true);
GameStateW gameStateW(gameState);
auto action = UpdateGameStatusAction(
gameState,
gameStateW,
criticalTileLocations,
GetGameSettingsGetter(),
false);
vector<ActionResult> results = action.Execute(GameStateW(), nullptr);
vector<ActionResult> results = action.Execute(gameStateW, nullptr);
EXPECT_GE(results.size(), 1);
EXPECT_EQ(net::eagle0::shardok::common::GAME_OVER, results[results.size() - 1].type());
EXPECT_EQ(
@@ -198,7 +197,7 @@ TEST_F(UpdateGameStatusActionTests, remainingPlayerDoesNotHaveLastPlayerStanding
GetGameSettingsGetter(),
false);
vector<ActionResult> results = action.Execute(GameStateW(), nullptr);
vector<ActionResult> results = action.Execute(gameState, nullptr);
EXPECT_EQ(0, results.size());
}
@@ -230,21 +229,32 @@ TEST_F(UpdateGameStatusActionTests, attackerHoldsAllCastles_gameOverResult) {
statusBuilder.add_state(net::eagle0::shardok::storage::fb::GameStatus_::State_GAME_RUNNING);
auto statusOffset = statusBuilder.Finish();
// Create a minimal hex map for the cache
auto hexMapBuilder = net::eagle0::shardok::storage::fb::HexMapBuilder(fbb);
hexMapBuilder.add_row_count(10);
hexMapBuilder.add_column_count(10);
auto hexMapOffset = hexMapBuilder.Finish();
net::eagle0::shardok::storage::fb::GameStateBuilder gsBuilder(fbb);
gsBuilder.add_player_infos(playersOffset);
gsBuilder.add_units(unitsOffset);
gsBuilder.add_status(statusOffset);
gsBuilder.add_hex_map(hexMapOffset);
fbb.Finish(gsBuilder.Finish());
auto gameState = Wrapper<GameState>(fbb);
GameStateW gameStateW(gameState);
criticalTileLocations.Add(Coords(0, 4));
auto action =
UpdateGameStatusAction(gameState, criticalTileLocations, GetGameSettingsGetter(), true);
auto action = UpdateGameStatusAction(
gameStateW,
criticalTileLocations,
GetGameSettingsGetter(),
true);
vector<ActionResult> results = action.Execute(GameStateW(), nullptr);
vector<ActionResult> results = action.Execute(gameStateW, nullptr);
EXPECT_GE(results.size(), 1);
EXPECT_EQ(net::eagle0::shardok::common::GAME_OVER, results[results.size() - 1].type());
EXPECT_EQ(
@@ -287,18 +297,29 @@ TEST_F(UpdateGameStatusActionTests, attackerHoldsAllCastles_hiddenDefenderFlees)
gsBuilder.add_units(unitsOffset);
gsBuilder.add_status(statusOffset);
// Create a minimal hex map for the cache
auto hexMapBuilder = net::eagle0::shardok::storage::fb::HexMapBuilder(fbb);
hexMapBuilder.add_row_count(10);
hexMapBuilder.add_column_count(10);
auto hexMapOffset = hexMapBuilder.Finish();
gsBuilder.add_hex_map(hexMapOffset);
fbb.Finish(gsBuilder.Finish());
auto gameState = Wrapper<GameState>(fbb);
gameState->mutable_units()->GetMutableObject(2)->mutate_hidden(true);
gameState->mutable_units()->GetMutableObject(2)->mutate_can_flee(true);
GameStateW gameStateW(gameState);
criticalTileLocations.Add(Coords(0, 4));
auto action =
UpdateGameStatusAction(gameState, criticalTileLocations, GetGameSettingsGetter(), true);
auto action = UpdateGameStatusAction(
gameStateW,
criticalTileLocations,
GetGameSettingsGetter(),
true);
vector<ActionResult> results = action.Execute(GameStateW(), nullptr);
vector<ActionResult> results = action.Execute(gameStateW, nullptr);
EXPECT_GE(results.size(), 1);
auto finalResult = results[0];
@@ -351,13 +372,16 @@ TEST_F(UpdateGameStatusActionTests,
auto gameState = Wrapper<GameState>(fbb);
gameState->mutable_units()->GetMutableObject(2)->mutate_hidden(true);
gameState->mutable_units()->GetMutableObject(2)->mutate_can_flee(false);
GameStateW gameStateW(gameState);
criticalTileLocations.Add(Coords(0, 4));
auto action = UpdateGameStatusAction(
gameStateW,
criticalTileLocations,
GetGameSettingsGetter(),
true);
auto action =
UpdateGameStatusAction(gameState, criticalTileLocations, GetGameSettingsGetter(), true);
vector<ActionResult> results = action.Execute(GameStateW(), nullptr);
vector<ActionResult> results = action.Execute(gameStateW, nullptr);
EXPECT_GE(results.size(), 1);
auto finalResult = results[0];
@@ -405,15 +429,17 @@ TEST_F(UpdateGameStatusActionTests, attackerHoldsAllCastles_hiddenDefenderTooTir
auto gameState = Wrapper<GameState>(fbb);
gameState->mutable_units()->GetMutableObject(2)->mutate_hidden(true);
gameState->mutable_units()->GetMutableObject(2)->mutate_can_flee(true);
gameState->mutable_units()->GetMutableObject(2)->mutable_attached_hero().mutate_vigor(8);
GameStateW gameStateW(gameState);
criticalTileLocations.Add(Coords(0, 4));
auto action = UpdateGameStatusAction(
gameStateW,
criticalTileLocations,
GetGameSettingsGetter(),
true);
auto action =
UpdateGameStatusAction(gameState, criticalTileLocations, GetGameSettingsGetter(), true);
vector<ActionResult> results = action.Execute(GameStateW(), nullptr);
vector<ActionResult> results = action.Execute(gameStateW, nullptr);
EXPECT_GE(results.size(), 1);
auto finalResult = results[0];
@@ -461,13 +487,16 @@ TEST_F(UpdateGameStatusActionTests, attackerHoldsAllCastles_notHiddenDefender_do
auto gameState = Wrapper<GameState>(fbb);
gameState->mutable_units()->GetMutableObject(2)->mutate_hidden(false);
gameState->mutable_units()->GetMutableObject(2)->mutate_can_flee(true);
GameStateW gameStateW(gameState);
criticalTileLocations.Add(Coords(0, 4));
auto action = UpdateGameStatusAction(
gameStateW,
criticalTileLocations,
GetGameSettingsGetter(),
true);
auto action =
UpdateGameStatusAction(gameState, criticalTileLocations, GetGameSettingsGetter(), true);
vector<ActionResult> results = action.Execute(GameStateW(), nullptr);
vector<ActionResult> results = action.Execute(gameStateW, nullptr);
EXPECT_GE(results.size(), 1);
auto finalResult = results[0];
@@ -520,7 +549,7 @@ TEST_F(UpdateGameStatusActionTests, attackerHoldsAllCastles_notEndOfRound_noGame
GetGameSettingsGetter(),
false);
vector<ActionResult> results = action.Execute(GameStateW(), nullptr);
vector<ActionResult> results = action.Execute(gameState, nullptr);
EXPECT_EQ(0, results.size());
}
@@ -564,10 +593,14 @@ TEST_F(UpdateGameStatusActionTests,
criticalTileLocations.Add(Coords(0, 4));
auto action =
UpdateGameStatusAction(gameState, criticalTileLocations, GetGameSettingsGetter(), true);
GameStateW gameStateW(gameState);
auto action = UpdateGameStatusAction(
gameStateW,
criticalTileLocations,
GetGameSettingsGetter(),
true);
vector<ActionResult> results = action.Execute(GameStateW(), nullptr);
vector<ActionResult> results = action.Execute(gameStateW, nullptr);
EXPECT_EQ(0, results.size());
}
@@ -619,10 +652,14 @@ TEST_F(UpdateGameStatusActionTests, allianceVictory_alliesHaveConditionSet_retur
criticalTileLocations.Add(Coords(0, 4));
auto action =
UpdateGameStatusAction(gameState, criticalTileLocations, GetGameSettingsGetter(), true);
GameStateW gameStateW(gameState);
auto action = UpdateGameStatusAction(
gameStateW,
criticalTileLocations,
GetGameSettingsGetter(),
true);
vector<ActionResult> results = action.Execute(GameStateW(), nullptr);
vector<ActionResult> results = action.Execute(gameStateW, nullptr);
EXPECT_GE(results.size(), 1);
EXPECT_EQ(net::eagle0::shardok::common::GAME_OVER, results[results.size() - 1].type());
EXPECT_EQ(
@@ -678,10 +715,14 @@ TEST_F(UpdateGameStatusActionTests, allianceVictory_notAllAlliesHaveConditionSet
criticalTileLocations.Add(Coords(0, 4));
auto action =
UpdateGameStatusAction(gameState, criticalTileLocations, GetGameSettingsGetter(), true);
GameStateW gameStateW(gameState);
auto action = UpdateGameStatusAction(
gameStateW,
criticalTileLocations,
GetGameSettingsGetter(),
true);
vector<ActionResult> results = action.Execute(GameStateW(), nullptr);
vector<ActionResult> results = action.Execute(gameStateW, nullptr);
EXPECT_EQ(0, results.size());
}
@@ -733,9 +774,13 @@ TEST_F(UpdateGameStatusActionTests, allianceVictory_notAllAllied_returnsNoAction
criticalTileLocations.Add(Coords(0, 4));
auto action =
UpdateGameStatusAction(gameState, criticalTileLocations, GetGameSettingsGetter(), true);
GameStateW gameStateW(gameState);
auto action = UpdateGameStatusAction(
gameStateW,
criticalTileLocations,
GetGameSettingsGetter(),
true);
vector<ActionResult> results = action.Execute(GameStateW(), nullptr);
vector<ActionResult> results = action.Execute(gameStateW, nullptr);
EXPECT_EQ(0, results.size());
}
@@ -98,6 +98,7 @@ TEST_F(UpdateOpponentKnowledgeActionTests, attackerGainsOneOnDefender) {
TEST_F(UpdateOpponentKnowledgeActionTests, defenderGainsMoreIfAdjacent) {
defender->mutable_location() = adjacentPosition;
gameState.RebuildOccupantsCache();
const auto action = UpdateOpponentKnowledgeAction(GetGameSettingsGetter());
@@ -113,6 +114,7 @@ TEST_F(UpdateOpponentKnowledgeActionTests, defenderGainsMoreIfAdjacent) {
TEST_F(UpdateOpponentKnowledgeActionTests, attackerGainsMoreIfAdjacent) {
defender->mutable_location() = adjacentPosition;
gameState.RebuildOccupantsCache();
const auto action = UpdateOpponentKnowledgeAction(GetGameSettingsGetter());
@@ -45,6 +45,9 @@ class HideCommandTests : public ::testing::Test {
enemy->mutable_attached_hero().mutable_profession_info().mutate_profession(
net::eagle0::shardok::storage::fb::Profession_RANGER);
enemy->mutate_hidden(true);
// Rebuild cache after mutations
gameState.RebuildOccupantsCache();
}
public:
@@ -104,6 +107,7 @@ TEST_F(HideCommandTests, Execute_setsHidden) {
TEST_F(HideCommandTests, ExecuteIntoOccupant_fails) {
enemy->mutable_location() = target;
gameState.RebuildOccupantsCache();
const ActionResult hideResult =
HideCommand(cost, rangerPid, ranger->unit_id(), target, agiXp, minimumKnowledge)
@@ -114,6 +118,7 @@ TEST_F(HideCommandTests, ExecuteIntoOccupant_fails) {
TEST_F(HideCommandTests, ExecuteIntoOccupant_doesNotMove) {
enemy->mutable_location() = target;
gameState.RebuildOccupantsCache();
ActionResult hideResult =
HideCommand(cost, rangerPid, ranger->unit_id(), target, agiXp, minimumKnowledge)
@@ -125,6 +130,7 @@ TEST_F(HideCommandTests, ExecuteIntoOccupant_doesNotMove) {
TEST_F(HideCommandTests, ExecuteIntoOccupant_doesNotHide) {
enemy->mutable_location() = target;
gameState.RebuildOccupantsCache();
ActionResult hideResult =
HideCommand(cost, rangerPid, ranger->unit_id(), target, agiXp, minimumKnowledge)
@@ -136,6 +142,7 @@ TEST_F(HideCommandTests, ExecuteIntoOccupant_doesNotHide) {
TEST_F(HideCommandTests, ExecuteIntoOccupant_revealsOccupant) {
enemy->mutable_location() = target;
gameState.RebuildOccupantsCache();
ActionResult hideResult =
HideCommand(cost, rangerPid, ranger->unit_id(), target, agiXp, minimumKnowledge)
@@ -147,6 +154,7 @@ TEST_F(HideCommandTests, ExecuteIntoOccupant_revealsOccupant) {
TEST_F(HideCommandTests, ExecuteNextToRanger_fails) {
enemy->mutable_location() = Coords(2, 4);
gameState.RebuildOccupantsCache();
const ActionResult hideResult =
HideCommand(cost, rangerPid, ranger->unit_id(), target, agiXp, minimumKnowledge)
@@ -157,6 +165,7 @@ TEST_F(HideCommandTests, ExecuteNextToRanger_fails) {
TEST_F(HideCommandTests, ExecuteNextToRanger_doesNotHide) {
enemy->mutable_location() = Coords(2, 4);
gameState.RebuildOccupantsCache();
ActionResult hideResult =
HideCommand(cost, rangerPid, ranger->unit_id(), target, agiXp, minimumKnowledge)
@@ -168,6 +177,7 @@ TEST_F(HideCommandTests, ExecuteNextToRanger_doesNotHide) {
TEST_F(HideCommandTests, ExecuteNextToRanger_stillMoves) {
enemy->mutable_location() = Coords(2, 4);
gameState.RebuildOccupantsCache();
ActionResult hideResult =
HideCommand(cost, rangerPid, ranger->unit_id(), target, agiXp, minimumKnowledge)
@@ -179,6 +189,7 @@ TEST_F(HideCommandTests, ExecuteNextToRanger_stillMoves) {
TEST_F(HideCommandTests, ExecuteNextToRanger_revealsEnemy) {
enemy->mutable_location() = Coords(2, 4);
gameState.RebuildOccupantsCache();
ActionResult hideResult =
HideCommand(cost, rangerPid, ranger->unit_id(), target, agiXp, minimumKnowledge)
@@ -189,6 +189,7 @@ TEST_F(RaiseDeadCommandTest, successRollOnHiddenDefender_fails) {
other->mutable_location() = target;
other->mutate_hidden(true);
gameState.RebuildOccupantsCache();
const auto results = RaiseDeadCommand(
cost,