mirror of
https://github.com/nolen777/eagle0.git
synced 2026-07-28 21:35:42 +00:00
Evaluate move follow-ups as leaf compounds
This commit is contained in:
@@ -11,6 +11,7 @@
|
||||
#include <iomanip>
|
||||
#include <iostream>
|
||||
#include <limits>
|
||||
#include <optional>
|
||||
#include <sstream>
|
||||
#include <stdexcept>
|
||||
|
||||
@@ -117,41 +118,14 @@ static auto CommandSorter(
|
||||
return false;
|
||||
}
|
||||
|
||||
static auto FollowUpCommandScoreBonus(const CommandType commandType) -> ScoreValue {
|
||||
static auto IsCompoundLeafFollowUpCommand(const CommandType commandType) -> bool {
|
||||
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;
|
||||
case net::eagle0::shardok::common::ARCHERY_COMMAND:
|
||||
case net::eagle0::shardok::common::CHARGE_COMMAND: return true;
|
||||
default: return false;
|
||||
}
|
||||
}
|
||||
|
||||
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,
|
||||
@@ -162,6 +136,100 @@ AICommandEvaluator::AICommandEvaluator(
|
||||
battalionTypeGetter_(std::move(battalionTypeGetter)),
|
||||
scoreMoveFollowUps_(scoreMoveFollowUps) {}
|
||||
|
||||
auto AICommandEvaluator::BestLeafMoveFollowUpScore(
|
||||
const PlayerId pid,
|
||||
const bool isDefender,
|
||||
const int maxRepeatCount,
|
||||
const ShardokEngine& movedEngine,
|
||||
const AIStrategy& attackerStrategy,
|
||||
const CoordsSet& allCastleCoords,
|
||||
const std::chrono::steady_clock::time_point deadline) const -> std::optional<ScoreValue> {
|
||||
if (std::chrono::steady_clock::now() > deadline) { return std::nullopt; }
|
||||
|
||||
const CommandListSPtr followUpCommands = movedEngine.GetAvailableCommandsForAIPlayer(pid);
|
||||
if (!followUpCommands || followUpCommands->empty()) { return std::nullopt; }
|
||||
|
||||
std::optional<ScoreValue> bestScore;
|
||||
auto scoreFollowUpOutcome = [&](const size_t followUpIndex,
|
||||
const std::shared_ptr<RandomGenerator>& randomGenerator)
|
||||
-> std::optional<ScoreValue> {
|
||||
if (std::chrono::steady_clock::now() > deadline) { return std::nullopt; }
|
||||
|
||||
auto followUpEngine = std::make_shared<ShardokEngine>(movedEngine, false);
|
||||
try {
|
||||
followUpEngine->PostCommand(pid, followUpIndex, randomGenerator);
|
||||
} catch (const std::exception& e) {
|
||||
std::ostringstream context;
|
||||
context << "AI leaf compound follow-up failed while posting command"
|
||||
<< " player=" << static_cast<int>(pid) << " follow_up_index=" << followUpIndex
|
||||
<< ' ' << DescribeCommandAt(followUpCommands, followUpIndex);
|
||||
throw WithExceptionContext(context.str(), e);
|
||||
}
|
||||
|
||||
try {
|
||||
return scorer_.GuessedStateScore(
|
||||
isDefender,
|
||||
followUpEngine->GetCurrentGameState(),
|
||||
attackerStrategy,
|
||||
allCastleCoords);
|
||||
} catch (const std::exception& e) {
|
||||
std::ostringstream context;
|
||||
context << "AI leaf compound follow-up failed while scoring state"
|
||||
<< " player=" << static_cast<int>(pid) << " follow_up_index=" << followUpIndex
|
||||
<< ' ' << DescribeCommandAt(followUpCommands, followUpIndex);
|
||||
throw WithExceptionContext(context.str(), e);
|
||||
}
|
||||
};
|
||||
|
||||
for (size_t followUpIndex = 0; followUpIndex < followUpCommands->size(); ++followUpIndex) {
|
||||
const CommandSPtr& followUpCommand = followUpCommands->at(followUpIndex);
|
||||
const CommandType followUpType = followUpCommand->GetCommandType();
|
||||
if (!IsCompoundLeafFollowUpCommand(followUpType)) { continue; }
|
||||
|
||||
std::optional<ScoreValue> followUpScore;
|
||||
if (IsDeterministic(followUpType)) {
|
||||
followUpScore = scoreFollowUpOutcome(followUpIndex, AverageGenerator());
|
||||
} else if (followUpCommand->HasOdds()) {
|
||||
const auto successChancePercentile = followUpCommand->GetOddsPercentile();
|
||||
const double successChance = static_cast<double>(successChancePercentile) / 100.0;
|
||||
const auto successScore = scoreFollowUpOutcome(
|
||||
followUpIndex,
|
||||
std::make_shared<SequenceRandomGenerator>(
|
||||
std::vector{SuccessSampleForOdds(successChance)}));
|
||||
const auto failureScore = scoreFollowUpOutcome(
|
||||
followUpIndex,
|
||||
std::make_shared<SequenceRandomGenerator>(
|
||||
std::vector{FailureSampleForOdds(successChance)}));
|
||||
if (successScore.has_value() && failureScore.has_value()) {
|
||||
followUpScore = std::lerp(*failureScore, *successScore, successChance);
|
||||
}
|
||||
} else {
|
||||
ScoreValue sum = 0.0;
|
||||
int completedSamples = 0;
|
||||
const int sampleCount = std::max(1, maxRepeatCount);
|
||||
for (int repeatIteration = 0; repeatIteration < sampleCount; ++repeatIteration) {
|
||||
auto sequence =
|
||||
std::vector{RandomnessSampleForRepeat(repeatIteration, sampleCount)};
|
||||
const auto sampleScore = scoreFollowUpOutcome(
|
||||
followUpIndex,
|
||||
std::make_shared<SequenceRandomGenerator>(std::move(sequence)));
|
||||
if (!sampleScore.has_value()) { continue; }
|
||||
sum += *sampleScore;
|
||||
completedSamples++;
|
||||
}
|
||||
if (completedSamples > 0) {
|
||||
followUpScore = sum / static_cast<ScoreValue>(completedSamples);
|
||||
}
|
||||
}
|
||||
|
||||
if (followUpScore.has_value() && (!bestScore.has_value() || *followUpScore > *bestScore)) {
|
||||
bestScore = followUpScore;
|
||||
}
|
||||
}
|
||||
|
||||
return bestScore;
|
||||
}
|
||||
|
||||
auto AICommandEvaluator::PerformLookahead(
|
||||
const PlayerId pid,
|
||||
const bool isDefender,
|
||||
@@ -294,10 +362,11 @@ auto AICommandEvaluator::EvaluateWithRandomness(
|
||||
}
|
||||
|
||||
const auto guessedCommands = guessedEngine.GetAvailableCommandsForAIPlayer(pid);
|
||||
const ScoreValue leafMoveFollowUpBonus =
|
||||
scoreMoveFollowUps_ && remainingLookahead <= 0
|
||||
? LeafMoveFollowUpScoreBonus(guessedEngine, pid, commandIndex)
|
||||
: 0.0;
|
||||
const bool shouldExpandLeafMoveFollowUps =
|
||||
scoreMoveFollowUps_ && remainingLookahead <= 0 &&
|
||||
commandIndex < guessedCommands->size() &&
|
||||
guessedCommands->at(commandIndex)->GetCommandType() ==
|
||||
net::eagle0::shardok::common::MOVE_COMMAND;
|
||||
auto innerEngine = std::make_shared<ShardokEngine>(guessedEngine, false);
|
||||
try {
|
||||
innerEngine->PostCommand(pid, commandIndex, randomGenerator);
|
||||
@@ -314,11 +383,23 @@ auto AICommandEvaluator::EvaluateWithRandomness(
|
||||
ScoreValue innerUtility;
|
||||
try {
|
||||
innerUtility = scorer_.GuessedStateScore(
|
||||
isDefender,
|
||||
innerEngine->GetCurrentGameState(),
|
||||
attackerStrategy,
|
||||
allCastleCoords) +
|
||||
leafMoveFollowUpBonus;
|
||||
isDefender,
|
||||
innerEngine->GetCurrentGameState(),
|
||||
attackerStrategy,
|
||||
allCastleCoords);
|
||||
if (shouldExpandLeafMoveFollowUps) {
|
||||
const auto followUpScore = BestLeafMoveFollowUpScore(
|
||||
pid,
|
||||
isDefender,
|
||||
maxRepeatCount,
|
||||
*innerEngine,
|
||||
attackerStrategy,
|
||||
allCastleCoords,
|
||||
deadline);
|
||||
if (followUpScore.has_value()) {
|
||||
innerUtility = std::max(innerUtility, *followUpScore);
|
||||
}
|
||||
}
|
||||
} catch (const std::exception& e) {
|
||||
std::ostringstream context;
|
||||
context << "AI EvaluateWithRandomness failed while scoring simulated state"
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
|
||||
#include <chrono>
|
||||
#include <future>
|
||||
#include <optional>
|
||||
|
||||
#include "src/main/cpp/net/eagle0/shardok/ai/AIStrategy.hpp"
|
||||
#include "src/main/cpp/net/eagle0/shardok/library/GameStateW.hpp"
|
||||
@@ -112,6 +113,15 @@ private:
|
||||
const AIStrategy& attackerStrategy,
|
||||
const CoordsSet& allCastleCoords,
|
||||
std::chrono::steady_clock::time_point deadline) const -> ImmediateAndLookaheadScore;
|
||||
|
||||
[[nodiscard]] auto BestLeafMoveFollowUpScore(
|
||||
PlayerId pid,
|
||||
bool isDefender,
|
||||
int maxRepeatCount,
|
||||
const ShardokEngine& movedEngine,
|
||||
const AIStrategy& attackerStrategy,
|
||||
const CoordsSet& allCastleCoords,
|
||||
std::chrono::steady_clock::time_point deadline) const -> std::optional<ScoreValue>;
|
||||
};
|
||||
|
||||
} // namespace shardok
|
||||
|
||||
@@ -53,6 +53,18 @@ private:
|
||||
ScoreValue score_;
|
||||
};
|
||||
|
||||
class AttackerActionPointSpentScorer : public AIScoreCalculator {
|
||||
public:
|
||||
[[nodiscard]] auto GuessedStateScore(
|
||||
bool /*isDefender*/,
|
||||
const GameStateW& state,
|
||||
const AIStrategy& /*aiStrategy*/,
|
||||
const CoordsSet& /*allCastleCoords*/) const -> ScoreValue override {
|
||||
const auto* attacker = state->units()->Get(0);
|
||||
return 10.0 - attacker->remaining_action_points();
|
||||
}
|
||||
};
|
||||
|
||||
class GatedCastleCoordsScorer : public AIScoreCalculator {
|
||||
public:
|
||||
explicit GatedCastleCoordsScorer(std::shared_future<void> gate) : gate_(std::move(gate)) {}
|
||||
@@ -214,7 +226,7 @@ TEST_F(AICommandEvaluatorTest, LookaheadFutureOwnsCastleCoords) {
|
||||
EXPECT_TRUE(evaluation.completed);
|
||||
}
|
||||
|
||||
TEST_F(AICommandEvaluatorTest, LeafMoveWithArcheryFollowUpReceivesTacticalBonus) {
|
||||
TEST_F(AICommandEvaluatorTest, LeafMoveWithArcheryFollowUpCanUseCompoundScore) {
|
||||
auto attacker = GetUnitT1();
|
||||
attacker.mutable_location() = Coords(2, 3);
|
||||
attacker.mutate_remaining_action_points(10);
|
||||
@@ -247,7 +259,7 @@ TEST_F(AICommandEvaluatorTest, LeafMoveWithArcheryFollowUpReceivesTacticalBonus)
|
||||
net::eagle0::shardok::common::MOVE_COMMAND,
|
||||
commandsWithoutFollowUps->at(moveIndex)->GetCommandType());
|
||||
|
||||
const ConstantScorer scorer(100.0);
|
||||
const AttackerActionPointSpentScorer scorer;
|
||||
const auto apdCache = std::make_shared<ActionPointDistancesCache>();
|
||||
const auto battalionTypeGetter = [this](BattalionTypeId typeId) {
|
||||
return settings->GetGetter().GetBattalionType(typeId);
|
||||
@@ -255,6 +267,14 @@ TEST_F(AICommandEvaluatorTest, LeafMoveWithArcheryFollowUpReceivesTacticalBonus)
|
||||
const AICommandEvaluator evaluator(scorer, apdCache, battalionTypeGetter);
|
||||
|
||||
const CoordsSet emptyCastleCoords(state->hex_map());
|
||||
ShardokEngine moveOnlyEngine(engine, false);
|
||||
moveOnlyEngine.PostCommand(0, static_cast<int64_t>(moveIndex));
|
||||
const ScoreValue moveOnlyScore = scorer.GuessedStateScore(
|
||||
/*isDefender=*/false,
|
||||
moveOnlyEngine.GetCurrentGameState(),
|
||||
FleeStrategy,
|
||||
emptyCastleCoords);
|
||||
|
||||
auto result = evaluator.EvaluateCommand(
|
||||
0,
|
||||
/*isDefender=*/false,
|
||||
@@ -262,14 +282,14 @@ TEST_F(AICommandEvaluatorTest, LeafMoveWithArcheryFollowUpReceivesTacticalBonus)
|
||||
/*maxRepeatCount=*/1,
|
||||
engine,
|
||||
FleeStrategy,
|
||||
/*currentUtility=*/100.0,
|
||||
/*currentUtility=*/0.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);
|
||||
EXPECT_GT(evaluation.score, moveOnlyScore);
|
||||
}
|
||||
|
||||
} // namespace shardok
|
||||
|
||||
Reference in New Issue
Block a user