Cache player roles during AI scoring (#8793)

* Cache player roles during AI scoring

* Use dense player IDs for AI scoring
This commit is contained in:
2026-07-26 07:28:38 -07:00
committed by GitHub
parent 39a374fc42
commit a7990f65aa
2 changed files with 33 additions and 6 deletions
@@ -7,7 +7,6 @@
#include <array>
#include "AIScoreCalculatorSharedUtilities.hpp"
#include "src/main/cpp/net/eagle0/shardok/ai/AIScoreUtilities.hpp"
#include "src/main/cpp/net/eagle0/shardok/ai/AIStrategy.hpp"
#include "src/main/cpp/net/eagle0/shardok/ai/AIUnitScoreCalculator.hpp"
#include "src/main/cpp/net/eagle0/shardok/ai/AIWaterCrossingCalculator.hpp"
@@ -92,13 +91,24 @@ auto AbstractAIScoreCalculator::CalculateUnitsScoreComponents(
auto occupants = Occupants(*cachedUnits, cachedRowCount, cachedColumnCount);
// StartGame assigns real player IDs densely from zero, matching their player_infos indexes.
// Use that invariant instead of scanning PlayerInfo for every unit or initializing a separate
// lookup table for every evaluated state.
const auto *playerInfos = gameStateRawPtr->player_infos();
if (playerInfos == nullptr) { return UnitsScoreComponents{0.0, 0.0}; }
for (const Unit *unit : *cachedUnits) {
const auto *pi = PlayerInfoForPid(gameState, unit->player_id());
if (pi == nullptr) { continue; }
const PlayerId playerId = unit->player_id();
if (playerId < 0 || static_cast<flatbuffers::uoffset_t>(playerId) >= playerInfos->size()) {
continue;
}
const auto *playerInfo = playerInfos->Get(static_cast<flatbuffers::uoffset_t>(playerId));
if (playerInfo->player_id() != playerId) { continue; }
const bool isDefender = playerInfo->is_defender();
switch (unit->status()) {
case net::eagle0::shardok::storage::fb::UnitStatus_NORMAL_UNIT: {
if (pi->is_defender()) {
if (isDefender) {
defenderUnits.push_back(unit);
} else {
attackerUnits.push_back(unit);
@@ -109,7 +119,7 @@ auto AbstractAIScoreCalculator::CalculateUnitsScoreComponents(
double thisScore = unit->has_attached_hero() && unit->attached_hero().is_vip()
? CAPTURED_VIP_SCORE
: CAPTURED_UNIT_SCORE;
if (pi->is_defender()) {
if (isDefender) {
defenderUnitsValue += thisScore;
} else {
attackerUnitsValue += thisScore;
@@ -48,7 +48,7 @@ protected:
}
// Helper to create a running game state
auto CreateRunningGameState() -> GameStateW {
auto CreateRunningGameState(const std::vector<Unit>& additionalUnits = {}) -> GameStateW {
flatbuffers::FlatBufferBuilder fbb;
fbb.ForceDefaults(true);
@@ -64,6 +64,7 @@ protected:
AddGenericUnit(0, 0, Coords(0, 0)), // Attacker unit
AddGenericUnit(1, 1, Coords(5, 5)) // Defender unit
};
units.insert(units.end(), additionalUnits.begin(), additionalUnits.end());
const auto unitsOffset = fbb.CreateVectorOfSortedStructs(&units);
net::eagle0::shardok::storage::fb::GameStateBuilder gsb(fbb);
@@ -96,6 +97,22 @@ TEST_F(MCTSOptimizedAIScoreCalculatorTest, NonTerminalState_ScoreIsBounded) {
EXPECT_LE(score, 200.0) << "Score should be bounded (<= 200)";
}
TEST_F(MCTSOptimizedAIScoreCalculatorTest, UnitsWithoutPlayerInfoAreIgnored) {
auto reservedUnit = AddGenericUnit(-1, 2, Coords(0, 1));
reservedUnit.mutate_status(net::eagle0::shardok::storage::fb::UnitStatus_RESERVED_SLOT);
auto uncontrolledUnit = AddGenericUnit(UNCONTROLLED_PLAYER_ID, 3, Coords(0, 2));
uncontrolledUnit.mutate_status(
net::eagle0::shardok::storage::fb::UnitStatus_DESTROYED_SUMMONED_UNIT);
const auto baseState = CreateRunningGameState();
const auto stateWithSpecialPlayerIds = CreateRunningGameState({reservedUnit, uncontrolledUnit});
const CoordsSet castleCoords(BASIC_MAP_FB);
EXPECT_DOUBLE_EQ(
scorer->GuessedStateScore(false, baseState, strategy, castleCoords),
scorer->GuessedStateScore(false, stateWithSpecialPlayerIds, strategy, castleCoords));
}
// Test with stronger attacker
TEST_F(MCTSOptimizedAIScoreCalculatorTest, StrongerAttacker_PositiveScore) {
// Create state with multiple attacker units vs one defender