mirror of
https://github.com/nolen777/eagle0.git
synced 2026-07-28 21:35:42 +00:00
Score AI move follow-up opportunities
This commit is contained in:
@@ -117,6 +117,41 @@ static auto CommandSorter(
|
||||
return false;
|
||||
}
|
||||
|
||||
static auto FollowUpCommandScoreBonus(const CommandType commandType) -> ScoreValue {
|
||||
switch (commandType) {
|
||||
case net::eagle0::shardok::common::CHARGE_COMMAND: return 30.0;
|
||||
case net::eagle0::shardok::common::ARCHERY_COMMAND: return 20.0;
|
||||
case net::eagle0::shardok::common::METEOR_START_COMMAND: return 35.0;
|
||||
case net::eagle0::shardok::common::LIGHTNING_BOLT_COMMAND: return 25.0;
|
||||
case net::eagle0::shardok::common::REDUCE_COMMAND: return 25.0;
|
||||
case net::eagle0::shardok::common::FEAR_COMMAND: return 15.0;
|
||||
case net::eagle0::shardok::common::START_FIRE_COMMAND: return 15.0;
|
||||
case net::eagle0::shardok::common::REPAIR_COMMAND: return 10.0;
|
||||
case net::eagle0::shardok::common::REINFORCE_COMMAND: return 10.0;
|
||||
case net::eagle0::shardok::common::SCOUT_COMMAND: return 5.0;
|
||||
default: return 0.0;
|
||||
}
|
||||
}
|
||||
|
||||
static auto LeafMoveFollowUpScoreBonus(
|
||||
const ShardokEngine& guessedEngine,
|
||||
const PlayerId pid,
|
||||
const size_t commandIndex) -> ScoreValue {
|
||||
const CommandListSPtr commandsWithFollowUps =
|
||||
guessedEngine.GetAvailableCommandsForAIPlayer(pid, true);
|
||||
if (!commandsWithFollowUps || commandIndex >= commandsWithFollowUps->size()) { return 0.0; }
|
||||
|
||||
const CommandSPtr& command = commandsWithFollowUps->at(commandIndex);
|
||||
if (command->GetCommandType() != net::eagle0::shardok::common::MOVE_COMMAND) { return 0.0; }
|
||||
|
||||
ScoreValue bonus = 0.0;
|
||||
const auto proto = command->GetCommandProto();
|
||||
for (const int followUpType : proto.follow_up_command_types()) {
|
||||
bonus += FollowUpCommandScoreBonus(static_cast<CommandType>(followUpType));
|
||||
}
|
||||
return bonus;
|
||||
}
|
||||
|
||||
AICommandEvaluator::AICommandEvaluator(
|
||||
const AIScoreCalculator& scorer,
|
||||
const APDCache& apdCache,
|
||||
@@ -257,6 +292,9 @@ auto AICommandEvaluator::EvaluateWithRandomness(
|
||||
}
|
||||
|
||||
const auto guessedCommands = guessedEngine.GetAvailableCommandsForAIPlayer(pid);
|
||||
const ScoreValue leafMoveFollowUpBonus =
|
||||
remainingLookahead <= 0 ? LeafMoveFollowUpScoreBonus(guessedEngine, pid, commandIndex)
|
||||
: 0.0;
|
||||
auto innerEngine = std::make_shared<ShardokEngine>(guessedEngine, false);
|
||||
try {
|
||||
innerEngine->PostCommand(pid, commandIndex, randomGenerator);
|
||||
@@ -273,10 +311,11 @@ auto AICommandEvaluator::EvaluateWithRandomness(
|
||||
ScoreValue innerUtility;
|
||||
try {
|
||||
innerUtility = scorer_.GuessedStateScore(
|
||||
isDefender,
|
||||
innerEngine->GetCurrentGameState(),
|
||||
attackerStrategy,
|
||||
allCastleCoords);
|
||||
isDefender,
|
||||
innerEngine->GetCurrentGameState(),
|
||||
attackerStrategy,
|
||||
allCastleCoords) +
|
||||
leafMoveFollowUpBonus;
|
||||
} catch (const std::exception& e) {
|
||||
std::ostringstream context;
|
||||
context << "AI EvaluateWithRandomness failed while scoring simulated state"
|
||||
|
||||
@@ -112,6 +112,7 @@ void ShardokEngine::ApplyAndAddActionResults(const std::vector<ActionResultProto
|
||||
|
||||
void ShardokEngine::ApplyAndAddActionResult(const ActionResultProto &result) {
|
||||
gameState = ApplyResult(std::move(gameState), result, settingsGetter);
|
||||
ClearCachedAvailableCommands();
|
||||
|
||||
if (trackHistory) {
|
||||
actionHistory.emplace_back();
|
||||
@@ -146,7 +147,13 @@ ShardokEngine::ShardokEngine(const ShardokEngine &toCopy, const bool trackHistor
|
||||
criticalTileCoords(toCopy.criticalTileCoords),
|
||||
tutorialController_(toCopy.tutorialController_),
|
||||
pendingReinforcementPlacement_(toCopy.pendingReinforcementPlacement_),
|
||||
cachedAvailableCommands(toCopy.cachedAvailableCommands) {}
|
||||
cachedAvailableCommandsWithoutFollowUps(toCopy.cachedAvailableCommandsWithoutFollowUps),
|
||||
cachedAvailableCommandsWithFollowUps(toCopy.cachedAvailableCommandsWithFollowUps) {}
|
||||
|
||||
void ShardokEngine::ClearCachedAvailableCommands() const {
|
||||
cachedAvailableCommandsWithoutFollowUps = nullptr;
|
||||
cachedAvailableCommandsWithFollowUps = nullptr;
|
||||
}
|
||||
|
||||
auto ShardokEngine::GetGameStateAtStartOfAction(const ActionId startingActionId) const
|
||||
-> GameStateW {
|
||||
@@ -316,13 +323,13 @@ auto ShardokEngine::PostWhilePlayerHasOnlyOneOption(
|
||||
net::eagle0::shardok::storage::fb::GameStatus_::State_GAME_RUNNING &&
|
||||
playerId == GetCurrentPlayerId() && playerId != UNCONTROLLED_PLAYER_ID) {
|
||||
// if there's only one command available, go ahead and take it.
|
||||
cachedAvailableCommands = availableCommandsFactory->GetAvailableCommands(
|
||||
cachedAvailableCommandsWithFollowUps = availableCommandsFactory->GetAvailableCommands(
|
||||
gameState,
|
||||
playerId,
|
||||
/* includeFollowUps=*/true);
|
||||
if (cachedAvailableCommands->size() == 1) {
|
||||
const auto loneCommand = cachedAvailableCommands->front();
|
||||
cachedAvailableCommands = nullptr;
|
||||
if (cachedAvailableCommandsWithFollowUps->size() == 1) {
|
||||
const auto loneCommand = cachedAvailableCommandsWithFollowUps->front();
|
||||
ClearCachedAvailableCommands();
|
||||
PostActionUnchecked(loneCommand, randomGenerator, std::nullopt);
|
||||
commandCount++;
|
||||
} else {
|
||||
@@ -471,7 +478,7 @@ void ShardokEngine::PostPlacementCommands(
|
||||
}
|
||||
}
|
||||
|
||||
cachedAvailableCommands = nullptr;
|
||||
ClearCachedAvailableCommands();
|
||||
|
||||
// now execute
|
||||
for (const auto &pi : placementInfos) {
|
||||
@@ -533,7 +540,7 @@ void ShardokEngine::PostFinishedPlacementCommand(
|
||||
if (it == placementCommands->end()) {
|
||||
throw ShardokClientErrorException("No finish placement command found");
|
||||
}
|
||||
cachedAvailableCommands = nullptr;
|
||||
ClearCachedAvailableCommands();
|
||||
|
||||
PostActionUnchecked(*it, randomGenerator, std::nullopt);
|
||||
}
|
||||
@@ -550,21 +557,22 @@ void ShardokEngine::PostCommand(
|
||||
}
|
||||
|
||||
// Make sure this command is available & legal
|
||||
if (!cachedAvailableCommands) {
|
||||
cachedAvailableCommands = availableCommandsFactory->GetAvailableCommands(
|
||||
if (!cachedAvailableCommandsWithFollowUps) {
|
||||
cachedAvailableCommandsWithFollowUps = availableCommandsFactory->GetAvailableCommands(
|
||||
gameState,
|
||||
player,
|
||||
/* includeFollowUps=*/true);
|
||||
}
|
||||
|
||||
if (commandIndex >= static_cast<int64_t>(cachedAvailableCommands->size()) || commandIndex < 0) {
|
||||
if (commandIndex >= static_cast<int64_t>(cachedAvailableCommandsWithFollowUps->size()) ||
|
||||
commandIndex < 0) {
|
||||
throw ShardokClientErrorException(
|
||||
string("Illegal command index ") + std::to_string(commandIndex) +
|
||||
string(", max is ") + std::to_string(cachedAvailableCommands->size()));
|
||||
string(", max is ") + std::to_string(cachedAvailableCommandsWithFollowUps->size()));
|
||||
}
|
||||
|
||||
const CommandSPtr command = (*cachedAvailableCommands)[commandIndex];
|
||||
cachedAvailableCommands = nullptr;
|
||||
const CommandSPtr command = (*cachedAvailableCommandsWithFollowUps)[commandIndex];
|
||||
ClearCachedAvailableCommands();
|
||||
|
||||
PostActionUnchecked(command, randomGenerator, roll);
|
||||
}
|
||||
@@ -665,6 +673,12 @@ auto ShardokEngine::GetPreviewCommands(const PlayerId playerId) const -> std::ve
|
||||
|
||||
auto ShardokEngine::GetAvailableCommandsForAIPlayer(const PlayerId playerId) const
|
||||
-> CommandListSPtr {
|
||||
return GetAvailableCommandsForAIPlayer(playerId, false);
|
||||
}
|
||||
|
||||
auto ShardokEngine::GetAvailableCommandsForAIPlayer(
|
||||
const PlayerId playerId,
|
||||
const bool includeFollowUps) const -> CommandListSPtr {
|
||||
if (GameIsOver(GetGameStatus())) {
|
||||
// The game is over
|
||||
return {};
|
||||
@@ -672,8 +686,12 @@ auto ShardokEngine::GetAvailableCommandsForAIPlayer(const PlayerId playerId) con
|
||||
|
||||
if (playerId != GetCurrentPlayerId()) return {};
|
||||
|
||||
CommandListSPtr &cachedAvailableCommands = includeFollowUps
|
||||
? cachedAvailableCommandsWithFollowUps
|
||||
: cachedAvailableCommandsWithoutFollowUps;
|
||||
|
||||
if (!cachedAvailableCommands) {
|
||||
cachedAvailableCommands = UncachedGetAvailableCommands(playerId, false);
|
||||
cachedAvailableCommands = UncachedGetAvailableCommands(playerId, includeFollowUps);
|
||||
}
|
||||
|
||||
return cachedAvailableCommands;
|
||||
@@ -688,11 +706,8 @@ auto ShardokEngine::GetAvailableCommandProtos(const PlayerId playerId, const boo
|
||||
|
||||
if (playerId != GetCurrentPlayerId()) return {};
|
||||
|
||||
if (!cachedAvailableCommands) {
|
||||
cachedAvailableCommands = UncachedGetAvailableCommands(playerId, includeFollowUps);
|
||||
}
|
||||
|
||||
return CommandProtosFromActionList(*cachedAvailableCommands);
|
||||
return CommandProtosFromActionList(
|
||||
*GetAvailableCommandsForAIPlayer(playerId, includeFollowUps));
|
||||
}
|
||||
|
||||
auto ShardokEngine::UncachedGetAvailableCommands(
|
||||
|
||||
@@ -53,7 +53,10 @@ private:
|
||||
TutorialBattleController tutorialController_;
|
||||
std::optional<ReinforcementPlacementInfo> pendingReinforcementPlacement_;
|
||||
|
||||
mutable CommandListSPtr cachedAvailableCommands{};
|
||||
mutable CommandListSPtr cachedAvailableCommandsWithoutFollowUps{};
|
||||
mutable CommandListSPtr cachedAvailableCommandsWithFollowUps{};
|
||||
|
||||
void ClearCachedAvailableCommands() const;
|
||||
|
||||
void ApplyAndAddActionResult(const ActionResult &result);
|
||||
|
||||
@@ -185,6 +188,8 @@ public:
|
||||
[[nodiscard]] auto GetPreviewCommands(PlayerId playerId) const -> std::vector<CommandProto>;
|
||||
|
||||
[[nodiscard]] auto GetAvailableCommandsForAIPlayer(PlayerId playerId) const -> CommandListSPtr;
|
||||
[[nodiscard]] auto GetAvailableCommandsForAIPlayer(PlayerId playerId, bool includeFollowUps)
|
||||
const -> CommandListSPtr;
|
||||
[[nodiscard]] auto GetAvailableCommandProtos(PlayerId playerId, bool includeFollowUps) const
|
||||
-> std::vector<CommandProto>;
|
||||
|
||||
|
||||
@@ -37,6 +37,22 @@ public:
|
||||
}
|
||||
};
|
||||
|
||||
class ConstantScorer : public AIScoreCalculator {
|
||||
public:
|
||||
explicit ConstantScorer(const ScoreValue score) : score_(score) {}
|
||||
|
||||
[[nodiscard]] auto GuessedStateScore(
|
||||
bool /*isDefender*/,
|
||||
const GameStateW& /*state*/,
|
||||
const AIStrategy& /*aiStrategy*/,
|
||||
const CoordsSet& /*allCastleCoords*/) const -> ScoreValue override {
|
||||
return score_;
|
||||
}
|
||||
|
||||
private:
|
||||
ScoreValue score_;
|
||||
};
|
||||
|
||||
class GatedCastleCoordsScorer : public AIScoreCalculator {
|
||||
public:
|
||||
explicit GatedCastleCoordsScorer(std::shared_future<void> gate) : gate_(std::move(gate)) {}
|
||||
@@ -84,6 +100,7 @@ protected:
|
||||
setter.SetInt(kSettingsKeys.fleeSuccessPerAdjacentEnemy, 0);
|
||||
setter.SetInt(kSettingsKeys.fleeSuccessPerAdjacentFriendly, 0);
|
||||
setter.SetInt(kSettingsKeys.fleeSuccessPerAdjacentImpassableTile, 0);
|
||||
setter.SetInt(kSettingsKeys.archeryActionPointCost, 5);
|
||||
|
||||
settings = GetGameSettings();
|
||||
}
|
||||
@@ -197,4 +214,62 @@ TEST_F(AICommandEvaluatorTest, LookaheadFutureOwnsCastleCoords) {
|
||||
EXPECT_TRUE(evaluation.completed);
|
||||
}
|
||||
|
||||
TEST_F(AICommandEvaluatorTest, LeafMoveWithArcheryFollowUpReceivesTacticalBonus) {
|
||||
auto attacker = GetUnitT1();
|
||||
attacker.mutable_location() = Coords(2, 3);
|
||||
attacker.mutate_remaining_action_points(10);
|
||||
|
||||
auto defender = GetUnitT2();
|
||||
defender.mutable_location() = Coords(2, 0);
|
||||
|
||||
std::vector attackers{attacker};
|
||||
std::vector defenders{defender};
|
||||
const auto enginePtr = GameWithUnits(BASIC_MAP, attackers, defenders);
|
||||
const ShardokEngine& engine = *enginePtr;
|
||||
const GameStateW& state = engine.GetCurrentGameState();
|
||||
|
||||
const auto commandsWithFollowUps = engine.GetAvailableCommandsForAIPlayer(0, true);
|
||||
const auto moveWithArcheryFollowUp =
|
||||
std::ranges::find_if(*commandsWithFollowUps, [](const CommandSPtr& command) {
|
||||
const auto proto = command->GetCommandProto();
|
||||
return proto.type() == net::eagle0::shardok::common::MOVE_COMMAND &&
|
||||
std::ranges::contains(
|
||||
proto.follow_up_command_types(),
|
||||
net::eagle0::shardok::common::ARCHERY_COMMAND);
|
||||
});
|
||||
ASSERT_NE(moveWithArcheryFollowUp, commandsWithFollowUps->end());
|
||||
const size_t moveIndex = static_cast<size_t>(
|
||||
std::distance(commandsWithFollowUps->begin(), moveWithArcheryFollowUp));
|
||||
|
||||
const auto commandsWithoutFollowUps = engine.GetAvailableCommandsForAIPlayer(0);
|
||||
ASSERT_LT(moveIndex, commandsWithoutFollowUps->size());
|
||||
ASSERT_EQ(
|
||||
net::eagle0::shardok::common::MOVE_COMMAND,
|
||||
commandsWithoutFollowUps->at(moveIndex)->GetCommandType());
|
||||
|
||||
const ConstantScorer scorer(100.0);
|
||||
const auto apdCache = std::make_shared<ActionPointDistancesCache>();
|
||||
const auto battalionTypeGetter = [this](BattalionTypeId typeId) {
|
||||
return settings->GetGetter().GetBattalionType(typeId);
|
||||
};
|
||||
const AICommandEvaluator evaluator(scorer, apdCache, battalionTypeGetter);
|
||||
|
||||
const CoordsSet emptyCastleCoords(state->hex_map());
|
||||
auto result = evaluator.EvaluateCommand(
|
||||
0,
|
||||
/*isDefender=*/false,
|
||||
/*remainingLookahead=*/0,
|
||||
/*maxRepeatCount=*/1,
|
||||
engine,
|
||||
FleeStrategy,
|
||||
/*currentUtility=*/100.0,
|
||||
emptyCastleCoords,
|
||||
moveIndex,
|
||||
std::chrono::steady_clock::now() + std::chrono::seconds(5));
|
||||
|
||||
const auto evaluation = result.get();
|
||||
EXPECT_TRUE(evaluation.completed);
|
||||
EXPECT_GT(evaluation.score, 100.0);
|
||||
}
|
||||
|
||||
} // namespace shardok
|
||||
|
||||
@@ -757,6 +757,30 @@ TEST_F(AvailableCommandsTests,
|
||||
EXPECT_TRUE(HasMoveWithFollowUpAction(commands, ARCHERY_COMMAND));
|
||||
}
|
||||
|
||||
TEST_F(AvailableCommandsTests, GetAvailableCommands_cachesFollowUpAndNoFollowUpListsSeparately) {
|
||||
vector<Unit> attackerUnits;
|
||||
attackerUnits.push_back(GetUnitT1());
|
||||
attackerUnits.back().mutable_location() = Coords(2, 3);
|
||||
|
||||
vector<Unit> defenderUnits;
|
||||
defenderUnits.push_back(GetUnitT2());
|
||||
defenderUnits.back().mutable_location() = Coords(2, 0);
|
||||
|
||||
const auto game = GameWithUnits(BASIC_MAP, attackerUnits, defenderUnits);
|
||||
|
||||
const auto commandsWithoutFollowUps = game->GetAvailableCommandsForAIPlayer(0);
|
||||
ASSERT_TRUE(std::ranges::any_of(*commandsWithoutFollowUps, [](const CommandSPtr &command) {
|
||||
return command->GetCommandType() == MOVE_COMMAND;
|
||||
}));
|
||||
|
||||
const vector<CommandProto> commandsWithFollowUps = game->GetAvailableCommandProtos(0, true);
|
||||
EXPECT_TRUE(HasMoveWithFollowUpAction(commandsWithFollowUps, ARCHERY_COMMAND));
|
||||
|
||||
const vector<CommandProto> commandsWithoutFollowUpsAgain =
|
||||
game->GetAvailableCommandProtos(0, false);
|
||||
EXPECT_FALSE(HasMoveWithFollowUpAction(commandsWithoutFollowUpsAgain, ARCHERY_COMMAND));
|
||||
}
|
||||
|
||||
TEST_F(AvailableCommandsTests,
|
||||
GetAvailableCommands_commandToMoveWithMeteorCostRemaining_includesStartMeteorAndLightning) {
|
||||
vector<Unit> attackerUnits;
|
||||
|
||||
@@ -24,6 +24,12 @@ constexpr int MONTH = 4;
|
||||
|
||||
auto MakePlayerInfo(int playerId, bool isDefender, int food) -> PlayerInfoProto;
|
||||
|
||||
auto CommandDescriptorsEqualIgnoringFollowUps(CommandProto lhs, CommandProto rhs) -> bool {
|
||||
lhs.clear_follow_up_command_types();
|
||||
rhs.clear_follow_up_command_types();
|
||||
return google::protobuf::util::MessageDifferencer::Equals(lhs, rhs);
|
||||
}
|
||||
|
||||
PlayerInfoProto ATTACKER_INFO = MakePlayerInfo(0, false, 1000);
|
||||
PlayerInfoProto DEFENDER_INFO = MakePlayerInfo(1, true, 1000);
|
||||
std::vector PLAYER_INFOS{ATTACKER_INFO, DEFENDER_INFO};
|
||||
@@ -387,13 +393,12 @@ auto postWithResults(
|
||||
const PlayerId playerId,
|
||||
const CommandProto &command) -> std::vector<ActionResultView> {
|
||||
std::vector<CommandProto> commands = game->GetAvailableCommandProtos(playerId, false);
|
||||
const auto index =
|
||||
std::ranges::find_if(
|
||||
commands,
|
||||
[&](const CommandProto &c) {
|
||||
return google::protobuf::util::MessageDifferencer::Equals(c, command);
|
||||
}) -
|
||||
commands.begin();
|
||||
const auto index = std::ranges::find_if(
|
||||
commands,
|
||||
[&](const CommandProto &c) {
|
||||
return CommandDescriptorsEqualIgnoringFollowUps(c, command);
|
||||
}) -
|
||||
commands.begin();
|
||||
|
||||
EXPECT_GE(index, 0);
|
||||
EXPECT_LT(static_cast<size_t>(index), commands.size());
|
||||
@@ -407,13 +412,12 @@ auto postWithResults(
|
||||
const CommandProto &command,
|
||||
std::shared_ptr<RandomGenerator> generator) -> std::vector<ActionResultView> {
|
||||
std::vector<CommandProto> commands = game->GetAvailableCommandProtos(playerId, false);
|
||||
const auto index =
|
||||
std::ranges::find_if(
|
||||
commands,
|
||||
[&](const CommandProto &c) {
|
||||
return google::protobuf::util::MessageDifferencer::Equals(c, command);
|
||||
}) -
|
||||
commands.begin();
|
||||
const auto index = std::ranges::find_if(
|
||||
commands,
|
||||
[&](const CommandProto &c) {
|
||||
return CommandDescriptorsEqualIgnoringFollowUps(c, command);
|
||||
}) -
|
||||
commands.begin();
|
||||
|
||||
EXPECT_GE(index, 0);
|
||||
EXPECT_LT(static_cast<size_t>(index), commands.size());
|
||||
|
||||
Reference in New Issue
Block a user