mirror of
https://github.com/nolen777/eagle0.git
synced 2026-07-28 22:35:42 +00:00
Fix ID timeout score pollution
This commit is contained in:
@@ -91,15 +91,15 @@ auto AICommandEvaluator::PerformLookahead(
|
||||
const ScoreValue currentUtility,
|
||||
const AIStrategy& attackerStrategy,
|
||||
const CoordsSet& allCastleCoords,
|
||||
std::chrono::steady_clock::time_point deadline) const -> std::future<ScoreValue> {
|
||||
std::chrono::steady_clock::time_point deadline) const -> std::future<EvaluationResult> {
|
||||
// Check transposition table before expensive computation
|
||||
auto cachedScore =
|
||||
g_transpositionTable.probe(innerEngine->GetCurrentGameState(), remainingLookahead, pid);
|
||||
|
||||
if (cachedScore.has_value()) {
|
||||
// Return cached result immediately
|
||||
std::promise<ScoreValue> p;
|
||||
p.set_value(*cachedScore);
|
||||
std::promise<EvaluationResult> p;
|
||||
p.set_value(EvaluationResult{.score = *cachedScore, .completed = true});
|
||||
return p.get_future();
|
||||
}
|
||||
const auto nextUtility = currentUtility;
|
||||
@@ -111,8 +111,8 @@ auto AICommandEvaluator::PerformLookahead(
|
||||
// table
|
||||
g_transpositionTable.store(innerEngine->GetCurrentGameState(), 1, pid, nextUtility);
|
||||
|
||||
std::promise<ScoreValue> p;
|
||||
p.set_value(nextUtility);
|
||||
std::promise<EvaluationResult> p;
|
||||
p.set_value(EvaluationResult{.score = nextUtility, .completed = true});
|
||||
return p.get_future();
|
||||
}
|
||||
|
||||
@@ -137,16 +137,18 @@ auto AICommandEvaluator::PerformLookahead(
|
||||
innerEngine,
|
||||
pid,
|
||||
nextUtility,
|
||||
remainingLookahead]() mutable -> ScoreValue {
|
||||
const auto [index, type, lookaheadScore, immediateScore] =
|
||||
bestCommandFuture.get();
|
||||
remainingLookahead]() mutable -> EvaluationResult {
|
||||
const auto bestCommand = bestCommandFuture.get();
|
||||
if (!bestCommand.completed) {
|
||||
return EvaluationResult{.score = nextUtility, .completed = false};
|
||||
}
|
||||
|
||||
ScoreValue resultScore;
|
||||
if (auto& nextCommand =
|
||||
innerEngine->GetAvailableCommandsForAIPlayer(pid)->at(index);
|
||||
if (auto& nextCommand = innerEngine->GetAvailableCommandsForAIPlayer(pid)->at(
|
||||
bestCommand.index);
|
||||
nextCommand->GetCommandType() !=
|
||||
net::eagle0::shardok::common::END_TURN_COMMAND) {
|
||||
resultScore = immediateScore;
|
||||
resultScore = bestCommand.immediateScore;
|
||||
} else {
|
||||
resultScore = nextUtility;
|
||||
}
|
||||
@@ -158,7 +160,7 @@ auto AICommandEvaluator::PerformLookahead(
|
||||
pid,
|
||||
resultScore);
|
||||
|
||||
return resultScore;
|
||||
return EvaluationResult{.score = resultScore, .completed = true};
|
||||
});
|
||||
}
|
||||
|
||||
@@ -166,8 +168,8 @@ auto AICommandEvaluator::PerformLookahead(
|
||||
g_transpositionTable
|
||||
.store(innerEngine->GetCurrentGameState(), remainingLookahead, pid, nextUtility);
|
||||
|
||||
std::promise<ScoreValue> p;
|
||||
p.set_value(nextUtility);
|
||||
std::promise<EvaluationResult> p;
|
||||
p.set_value(EvaluationResult{.score = nextUtility, .completed = true});
|
||||
return p.get_future();
|
||||
}
|
||||
|
||||
@@ -186,10 +188,10 @@ auto AICommandEvaluator::EvaluateWithRandomness(
|
||||
|
||||
// Check if we've exceeded the deadline
|
||||
if (std::chrono::steady_clock::now() > deadline) {
|
||||
// Return with a default score and an empty future that resolves immediately
|
||||
std::promise<ScoreValue> p;
|
||||
p.set_value(0.0); // Default timeout score
|
||||
std::promise<EvaluationResult> p;
|
||||
p.set_value(EvaluationResult{.score = 0.0, .completed = false});
|
||||
returnValue.immediateScore = 0.0;
|
||||
returnValue.completed = false;
|
||||
returnValue.lookaheadScore = p.get_future();
|
||||
return returnValue;
|
||||
}
|
||||
@@ -204,11 +206,12 @@ auto AICommandEvaluator::EvaluateWithRandomness(
|
||||
allCastleCoords);
|
||||
|
||||
returnValue.immediateScore = innerUtility;
|
||||
returnValue.completed = true;
|
||||
|
||||
if (remainingLookahead <= 0) {
|
||||
std::promise<ScoreValue> p;
|
||||
std::promise<EvaluationResult> p;
|
||||
returnValue.lookaheadScore = p.get_future();
|
||||
p.set_value(innerUtility);
|
||||
p.set_value(EvaluationResult{.score = innerUtility, .completed = true});
|
||||
} else {
|
||||
auto lookaheadLambda = [this,
|
||||
pid,
|
||||
@@ -219,7 +222,7 @@ auto AICommandEvaluator::EvaluateWithRandomness(
|
||||
attackerStrategy,
|
||||
innerUtility,
|
||||
&allCastleCoords,
|
||||
deadline]() -> ScoreValue {
|
||||
deadline]() -> EvaluationResult {
|
||||
auto lookaheadFuture = PerformLookahead(
|
||||
pid,
|
||||
isDefender,
|
||||
@@ -237,7 +240,7 @@ auto AICommandEvaluator::EvaluateWithRandomness(
|
||||
auto launchPolicy = remainingLookahead == 1 ? std::launch::async : std::launch::deferred;
|
||||
returnValue.lookaheadScore = std::async(launchPolicy, lookaheadLambda);
|
||||
#else
|
||||
std::promise<ScoreValue> p;
|
||||
std::promise<EvaluationResult> p;
|
||||
returnValue.lookaheadScore = p.get_future();
|
||||
auto lambdaResult = lookaheadLambda();
|
||||
p.set_value(lambdaResult);
|
||||
@@ -322,7 +325,8 @@ auto AICommandEvaluator::FindBestCommand(
|
||||
size_t index;
|
||||
CommandType type;
|
||||
ScoreValue immediateScore;
|
||||
std::vector<std::future<ScoreValue>> lookaheadFutures;
|
||||
bool immediateCompleted = true;
|
||||
std::vector<std::future<EvaluationResult>> lookaheadFutures;
|
||||
};
|
||||
|
||||
std::vector<CommandEvaluation> commandEvaluations(commandCount);
|
||||
@@ -336,12 +340,12 @@ auto AICommandEvaluator::FindBestCommand(
|
||||
commandEvaluations[index].type = guessedCommandType;
|
||||
|
||||
if (guessedCommandType == net::eagle0::shardok::common::END_TURN_COMMAND) {
|
||||
std::promise<ScoreValue> p;
|
||||
std::promise<EvaluationResult> p;
|
||||
commandEvaluations[index].lookaheadFutures.push_back(p.get_future());
|
||||
p.set_value(currentUtility);
|
||||
p.set_value(EvaluationResult{.score = currentUtility, .completed = true});
|
||||
commandEvaluations[index].immediateScore = currentUtility;
|
||||
} else if (IsDeterministic(guessedCommandType)) {
|
||||
auto [immediateScore, lookaheadScore] = EvaluateWithRandomness(
|
||||
auto evaluation = EvaluateWithRandomness(
|
||||
pid,
|
||||
isDefender,
|
||||
originalIndex,
|
||||
@@ -353,14 +357,16 @@ auto AICommandEvaluator::FindBestCommand(
|
||||
allCastleCoords,
|
||||
deadline);
|
||||
|
||||
commandEvaluations[index].immediateScore = immediateScore;
|
||||
commandEvaluations[index].lookaheadFutures.push_back(std::move(lookaheadScore));
|
||||
commandEvaluations[index].immediateScore = evaluation.immediateScore;
|
||||
commandEvaluations[index].immediateCompleted = evaluation.completed;
|
||||
commandEvaluations[index].lookaheadFutures.push_back(
|
||||
std::move(evaluation.lookaheadScore));
|
||||
} else if (guessedDescriptor->HasOdds()) {
|
||||
const auto successChancePercentile = guessedDescriptor->GetOddsPercentile();
|
||||
const double successChance = static_cast<double>(successChancePercentile) / 100.0;
|
||||
|
||||
// Success attempt uses 1.0 - (successChance / 2) as the roll
|
||||
auto [successImmediateScore, successLookaheadScore] = EvaluateWithRandomness(
|
||||
auto successEvaluation = EvaluateWithRandomness(
|
||||
pid,
|
||||
isDefender,
|
||||
originalIndex,
|
||||
@@ -374,7 +380,7 @@ auto AICommandEvaluator::FindBestCommand(
|
||||
deadline);
|
||||
|
||||
// Failure attempt uses the average of (1 - successChance) and 0 as the roll
|
||||
auto [failureImmediateScore, failureLookaheadScore] = EvaluateWithRandomness(
|
||||
auto failureEvaluation = EvaluateWithRandomness(
|
||||
pid,
|
||||
isDefender,
|
||||
originalIndex,
|
||||
@@ -387,15 +393,26 @@ auto AICommandEvaluator::FindBestCommand(
|
||||
allCastleCoords,
|
||||
deadline);
|
||||
|
||||
commandEvaluations[index].immediateScore =
|
||||
std::lerp(failureImmediateScore, successImmediateScore, successChance);
|
||||
commandEvaluations[index].immediateCompleted =
|
||||
failureEvaluation.completed && successEvaluation.completed;
|
||||
commandEvaluations[index].immediateScore = std::lerp(
|
||||
failureEvaluation.immediateScore,
|
||||
successEvaluation.immediateScore,
|
||||
successChance);
|
||||
|
||||
auto successSF = successLookaheadScore.share();
|
||||
auto failureSF = failureLookaheadScore.share();
|
||||
auto successSF = successEvaluation.lookaheadScore.share();
|
||||
auto failureSF = failureEvaluation.lookaheadScore.share();
|
||||
commandEvaluations[index].lookaheadFutures.push_back(std::async(
|
||||
std::launch::deferred,
|
||||
[successSF, failureSF, successChance]() -> double {
|
||||
return std::lerp(failureSF.get(), successSF.get(), successChance);
|
||||
[successSF, failureSF, successChance]() -> EvaluationResult {
|
||||
const auto failure = failureSF.get();
|
||||
const auto success = successSF.get();
|
||||
if (!failure.completed || !success.completed) {
|
||||
return EvaluationResult{.score = 0.0, .completed = false};
|
||||
}
|
||||
return EvaluationResult{
|
||||
.score = std::lerp(failure.score, success.score, successChance),
|
||||
.completed = true};
|
||||
}));
|
||||
} else {
|
||||
ScoreValue sum = 0.0;
|
||||
@@ -404,7 +421,7 @@ auto AICommandEvaluator::FindBestCommand(
|
||||
// In each iteration, use a double from [0, 1] as the random roll
|
||||
auto sequence =
|
||||
std::vector{RandomnessSampleForRepeat(repeatIteration, sampleCount)};
|
||||
auto [immediateScore, lookaheadScore] = EvaluateWithRandomness(
|
||||
auto evaluation = EvaluateWithRandomness(
|
||||
pid,
|
||||
isDefender,
|
||||
originalIndex,
|
||||
@@ -416,8 +433,10 @@ auto AICommandEvaluator::FindBestCommand(
|
||||
allCastleCoords,
|
||||
deadline);
|
||||
|
||||
sum += immediateScore;
|
||||
commandEvaluations[index].lookaheadFutures.push_back(std::move(lookaheadScore));
|
||||
if (!evaluation.completed) { commandEvaluations[index].immediateCompleted = false; }
|
||||
sum += evaluation.immediateScore;
|
||||
commandEvaluations[index].lookaheadFutures.push_back(
|
||||
std::move(evaluation.lookaheadScore));
|
||||
}
|
||||
commandEvaluations[index].immediateScore = sum / sampleCount;
|
||||
}
|
||||
@@ -433,19 +452,33 @@ auto AICommandEvaluator::FindBestCommand(
|
||||
// Wait for all futures and compute final scores
|
||||
for (auto& eval : evals) {
|
||||
ScoreValue totalLookaheadScore = 0.0;
|
||||
bool completed = eval.immediateCompleted;
|
||||
for (auto& future : eval.lookaheadFutures) {
|
||||
totalLookaheadScore += future.get();
|
||||
const auto lookahead = future.get();
|
||||
if (!lookahead.completed) { completed = false; }
|
||||
totalLookaheadScore += lookahead.score;
|
||||
}
|
||||
ScoreValue avgLookaheadScore =
|
||||
eval.lookaheadFutures.empty()
|
||||
? eval.immediateScore
|
||||
: totalLookaheadScore / eval.lookaheadFutures.size();
|
||||
|
||||
if (!completed) { continue; }
|
||||
|
||||
allResults.push_back(IndexAndScore{
|
||||
.index = eval.index,
|
||||
.type = eval.type,
|
||||
.lookaheadScore = avgLookaheadScore,
|
||||
.immediateScore = eval.immediateScore});
|
||||
.immediateScore = eval.immediateScore,
|
||||
.completed = true});
|
||||
}
|
||||
if (allResults.empty()) {
|
||||
return IndexAndScore{
|
||||
.index = 0,
|
||||
.type = net::eagle0::shardok::common::UNKNOWN_COMMAND,
|
||||
.lookaheadScore = 0.0,
|
||||
.immediateScore = 0.0,
|
||||
.completed = false};
|
||||
}
|
||||
// Find the best command using the existing sorter
|
||||
auto bestIt = std::ranges::max_element(allResults, CommandSorter);
|
||||
@@ -463,12 +496,12 @@ auto AICommandEvaluator::EvaluateCommand(
|
||||
const ScoreValue currentUtility,
|
||||
const CoordsSet& allCastleCoords,
|
||||
const size_t commandIndex,
|
||||
std::chrono::steady_clock::time_point deadline) const -> std::future<ScoreValue> {
|
||||
std::chrono::steady_clock::time_point deadline) const -> std::future<EvaluationResult> {
|
||||
const CommandListSPtr guessedDescriptors = guessedEngine.GetAvailableCommandsForAIPlayer(pid);
|
||||
|
||||
if (commandIndex >= guessedDescriptors->size()) {
|
||||
std::promise<ScoreValue> p;
|
||||
p.set_value(currentUtility);
|
||||
std::promise<EvaluationResult> p;
|
||||
p.set_value(EvaluationResult{.score = currentUtility, .completed = true});
|
||||
return p.get_future();
|
||||
}
|
||||
|
||||
@@ -476,11 +509,11 @@ auto AICommandEvaluator::EvaluateCommand(
|
||||
|
||||
if (const auto guessedCommandType = guessedDescriptor->GetCommandType();
|
||||
guessedCommandType == net::eagle0::shardok::common::END_TURN_COMMAND) {
|
||||
std::promise<ScoreValue> p;
|
||||
p.set_value(currentUtility);
|
||||
std::promise<EvaluationResult> p;
|
||||
p.set_value(EvaluationResult{.score = currentUtility, .completed = true});
|
||||
return p.get_future();
|
||||
} else if (IsDeterministic(guessedCommandType)) {
|
||||
auto [immediateScore, lookaheadScore] = EvaluateWithRandomness(
|
||||
auto evaluation = EvaluateWithRandomness(
|
||||
pid,
|
||||
isDefender,
|
||||
commandIndex,
|
||||
@@ -491,13 +524,13 @@ auto AICommandEvaluator::EvaluateCommand(
|
||||
attackerStrategy,
|
||||
allCastleCoords,
|
||||
deadline);
|
||||
return std::move(lookaheadScore);
|
||||
return std::move(evaluation.lookaheadScore);
|
||||
} else if (guessedDescriptor->HasOdds()) {
|
||||
const auto successChancePercentile = guessedDescriptor->GetOddsPercentile();
|
||||
const double successChance = static_cast<double>(successChancePercentile) / 100.0;
|
||||
|
||||
// Success attempt
|
||||
auto [successImmediateScore, successLookaheadScore] = EvaluateWithRandomness(
|
||||
auto successEvaluation = EvaluateWithRandomness(
|
||||
pid,
|
||||
isDefender,
|
||||
commandIndex,
|
||||
@@ -510,7 +543,7 @@ auto AICommandEvaluator::EvaluateCommand(
|
||||
deadline);
|
||||
|
||||
// Failure attempt
|
||||
auto [failureImmediateScore, failureLookaheadScore] = EvaluateWithRandomness(
|
||||
auto failureEvaluation = EvaluateWithRandomness(
|
||||
pid,
|
||||
isDefender,
|
||||
commandIndex,
|
||||
@@ -523,20 +556,29 @@ auto AICommandEvaluator::EvaluateCommand(
|
||||
deadline);
|
||||
|
||||
// Return weighted average of success and failure
|
||||
auto successSF = successLookaheadScore.share();
|
||||
auto failureSF = failureLookaheadScore.share();
|
||||
return std::async(std::launch::deferred, [successSF, failureSF, successChance]() -> double {
|
||||
return std::lerp(failureSF.get(), successSF.get(), successChance);
|
||||
});
|
||||
auto successSF = successEvaluation.lookaheadScore.share();
|
||||
auto failureSF = failureEvaluation.lookaheadScore.share();
|
||||
return std::async(
|
||||
std::launch::deferred,
|
||||
[successSF, failureSF, successChance]() -> EvaluationResult {
|
||||
const auto failure = failureSF.get();
|
||||
const auto success = successSF.get();
|
||||
if (!failure.completed || !success.completed) {
|
||||
return EvaluationResult{.score = 0.0, .completed = false};
|
||||
}
|
||||
return EvaluationResult{
|
||||
.score = std::lerp(failure.score, success.score, successChance),
|
||||
.completed = true};
|
||||
});
|
||||
} else {
|
||||
// For non-deterministic commands without odds, use multiple attempts
|
||||
std::vector<std::future<ScoreValue>> lookaheadFutures;
|
||||
std::vector<std::future<EvaluationResult>> lookaheadFutures;
|
||||
const int sampleCount = std::max(1, maxRepeatCount);
|
||||
lookaheadFutures.reserve(sampleCount);
|
||||
|
||||
for (int repeatIteration = 0; repeatIteration < sampleCount; repeatIteration++) {
|
||||
auto sequence = std::vector{RandomnessSampleForRepeat(repeatIteration, sampleCount)};
|
||||
auto [immediateScore, lookaheadScore] = EvaluateWithRandomness(
|
||||
auto evaluation = EvaluateWithRandomness(
|
||||
pid,
|
||||
isDefender,
|
||||
commandIndex,
|
||||
@@ -548,16 +590,25 @@ auto AICommandEvaluator::EvaluateCommand(
|
||||
allCastleCoords,
|
||||
deadline);
|
||||
|
||||
lookaheadFutures.push_back(std::move(lookaheadScore));
|
||||
lookaheadFutures.push_back(std::move(evaluation.lookaheadScore));
|
||||
}
|
||||
|
||||
// Return a future that computes the average when needed
|
||||
return std::async(
|
||||
std::launch::deferred,
|
||||
[lookaheadFutures = std::move(lookaheadFutures), sampleCount]() mutable -> double {
|
||||
[lookaheadFutures = std::move(lookaheadFutures),
|
||||
sampleCount]() mutable -> EvaluationResult {
|
||||
ScoreValue total = 0.0;
|
||||
for (auto& future : lookaheadFutures) { total += future.get(); }
|
||||
return total / sampleCount;
|
||||
bool completed = true;
|
||||
for (auto& future : lookaheadFutures) {
|
||||
const auto result = future.get();
|
||||
if (!result.completed) { completed = false; }
|
||||
total += result.score;
|
||||
}
|
||||
if (!completed) { return EvaluationResult{.score = 0.0, .completed = false}; }
|
||||
return EvaluationResult{
|
||||
.score = total / static_cast<ScoreValue>(sampleCount),
|
||||
.completed = true};
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -38,6 +38,11 @@ public:
|
||||
BattalionTypeGetter battalionTypeGetter); // Pass by value
|
||||
|
||||
/// Evaluates the score for a particular command index with lookahead.
|
||||
struct EvaluationResult {
|
||||
ScoreValue score;
|
||||
bool completed;
|
||||
};
|
||||
|
||||
[[nodiscard]] auto EvaluateCommand(
|
||||
PlayerId pid,
|
||||
bool isDefender,
|
||||
@@ -48,7 +53,7 @@ public:
|
||||
ScoreValue currentUtility,
|
||||
const CoordsSet& allCastleCoords,
|
||||
size_t commandIndex,
|
||||
std::chrono::steady_clock::time_point deadline) const -> std::future<ScoreValue>;
|
||||
std::chrono::steady_clock::time_point deadline) const -> std::future<EvaluationResult>;
|
||||
|
||||
/// Find the best command among all available commands at the given depth.
|
||||
struct IndexAndScore {
|
||||
@@ -56,6 +61,7 @@ public:
|
||||
CommandType type;
|
||||
ScoreValue lookaheadScore;
|
||||
ScoreValue immediateScore;
|
||||
bool completed;
|
||||
};
|
||||
|
||||
[[nodiscard]] auto FindBestCommand(
|
||||
@@ -76,7 +82,8 @@ private:
|
||||
|
||||
struct ImmediateAndLookaheadScore {
|
||||
ScoreValue immediateScore;
|
||||
std::future<ScoreValue> lookaheadScore;
|
||||
bool completed;
|
||||
std::future<EvaluationResult> lookaheadScore;
|
||||
};
|
||||
|
||||
/// Recursive lookahead calculator
|
||||
@@ -89,7 +96,7 @@ private:
|
||||
ScoreValue currentUtility,
|
||||
const AIStrategy& attackerStrategy,
|
||||
const CoordsSet& allCastleCoords,
|
||||
std::chrono::steady_clock::time_point deadline) const -> std::future<ScoreValue>;
|
||||
std::chrono::steady_clock::time_point deadline) const -> std::future<EvaluationResult>;
|
||||
|
||||
/// Evaluate single command execution with randomness handling
|
||||
[[nodiscard]] auto EvaluateWithRandomness(
|
||||
|
||||
@@ -131,6 +131,10 @@ auto IterativeDeepeningAI::IterativeSearch(
|
||||
// Now wait for all futures and collect results
|
||||
for (auto& [cmdIndex, future] : futures) {
|
||||
auto cmdResult = future.get();
|
||||
if (!cmdResult.searchCompleted) {
|
||||
allEvaluated = false;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Ensure scoresByDepth[cmdIndex] has enough space
|
||||
if (scoresByDepth[cmdIndex].size() <= currentDepth) {
|
||||
@@ -252,7 +256,9 @@ auto IterativeDeepeningAI::IterativeSearch(
|
||||
completionReason = EvaluationCompletionReason::RAN_OUT_OF_COMMANDS;
|
||||
}
|
||||
|
||||
// Select best result from highest depth achieved for each command
|
||||
// Select best result from the highest completed depth achieved for each command. Commands that
|
||||
// timed out at a depth are not recorded for that depth, so partial deeper work can help without
|
||||
// letting timeout fallback scores compete.
|
||||
result = SelectBestResult(scoresByDepth, highestDepthCompleted);
|
||||
result.minimumDepthCompleted = result.depthAchieved >= timeBudget.minDepthRequired;
|
||||
result.searchCompleted = result.minimumDepthCompleted;
|
||||
@@ -343,7 +349,9 @@ auto IterativeDeepeningAI::SearchCommandAtDepthWithEngine(
|
||||
// Deduct adjusted time from remaining budget
|
||||
timeBudget.remainingBudget -= adjustedElapsedMs;
|
||||
|
||||
result.bestScore = commandScore;
|
||||
result.bestScore = commandScore.score;
|
||||
result.searchCompleted = commandScore.completed;
|
||||
result.minimumDepthCompleted = commandScore.completed;
|
||||
|
||||
std::promise<SearchResult> p;
|
||||
p.set_value(result);
|
||||
@@ -356,7 +364,7 @@ auto IterativeDeepeningAI::GetCommandsSortedByPreviousDepth(
|
||||
const std::vector<size_t>& highestDepthCompleted,
|
||||
const std::vector<size_t>& filteredIndices) -> std::vector<size_t> {
|
||||
if (currentDepth == 1) {
|
||||
// For depth 1, return filtered indices in natural order
|
||||
// For depth 1, preserve the upstream command-factory priority order.
|
||||
return filteredIndices;
|
||||
}
|
||||
|
||||
|
||||
@@ -111,4 +111,4 @@ private:
|
||||
|
||||
} // namespace shardok
|
||||
|
||||
#endif // EAGLE0_ITERATIVEDEEPENINGAI_HPP
|
||||
#endif // EAGLE0_ITERATIVEDEEPENINGAI_HPP
|
||||
|
||||
@@ -1143,32 +1143,36 @@ TEST_F(AIIntegrationTest, StandardScorer_ArcheryEvaluationBeatsEndTurnWithSingle
|
||||
const double currentScore = scorer->GuessedStateScore(false, gameState, strategy, castleCoords);
|
||||
AICommandEvaluator evaluator(*scorer, apdCache, battalionTypeGetter);
|
||||
const auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(10);
|
||||
const double archeryScore = evaluator
|
||||
.EvaluateCommand(
|
||||
0,
|
||||
false,
|
||||
0,
|
||||
0,
|
||||
engine,
|
||||
strategy,
|
||||
currentScore,
|
||||
castleCoords,
|
||||
std::distance(commands->begin(), archeryCommand),
|
||||
deadline)
|
||||
.get();
|
||||
const double endTurnScore = evaluator
|
||||
.EvaluateCommand(
|
||||
0,
|
||||
false,
|
||||
0,
|
||||
0,
|
||||
engine,
|
||||
strategy,
|
||||
currentScore,
|
||||
castleCoords,
|
||||
std::distance(commands->begin(), endTurnCommand),
|
||||
deadline)
|
||||
.get();
|
||||
const auto archeryResult = evaluator
|
||||
.EvaluateCommand(
|
||||
0,
|
||||
false,
|
||||
0,
|
||||
0,
|
||||
engine,
|
||||
strategy,
|
||||
currentScore,
|
||||
castleCoords,
|
||||
std::distance(commands->begin(), archeryCommand),
|
||||
deadline)
|
||||
.get();
|
||||
const auto endTurnResult = evaluator
|
||||
.EvaluateCommand(
|
||||
0,
|
||||
false,
|
||||
0,
|
||||
0,
|
||||
engine,
|
||||
strategy,
|
||||
currentScore,
|
||||
castleCoords,
|
||||
std::distance(commands->begin(), endTurnCommand),
|
||||
deadline)
|
||||
.get();
|
||||
ASSERT_TRUE(archeryResult.completed);
|
||||
ASSERT_TRUE(endTurnResult.completed);
|
||||
const double archeryScore = archeryResult.score;
|
||||
const double endTurnScore = endTurnResult.score;
|
||||
|
||||
EXPECT_TRUE(std::isfinite(archeryScore));
|
||||
EXPECT_GT(archeryScore, endTurnScore)
|
||||
|
||||
@@ -215,3 +215,30 @@ cc_test(
|
||||
"@googletest//:gtest_main",
|
||||
],
|
||||
)
|
||||
|
||||
cc_test(
|
||||
name = "real_battle_decision_regression_test",
|
||||
size = "large",
|
||||
srcs = ["RealBattleDecisionRegressionTest.cpp"],
|
||||
copts = TEST_COPTS,
|
||||
data = [
|
||||
"testdata/kings_loyalists_before_mage_fire.fb",
|
||||
"//src/main/resources/net/eagle0/shardok:battalion_types",
|
||||
"//src/main/resources/net/eagle0/shardok:settings",
|
||||
],
|
||||
linkstatic = True,
|
||||
tags = ["exclusive"],
|
||||
deps = [
|
||||
"//src/main/cpp/net/eagle0/common:filesystem_utils",
|
||||
"//src/main/cpp/net/eagle0/common:random_generator",
|
||||
"//src/main/cpp/net/eagle0/shardok/ai:shardok_ai_client",
|
||||
"//src/main/cpp/net/eagle0/shardok/ai:transposition_table",
|
||||
"//src/main/cpp/net/eagle0/shardok/ai_testing_common:ai_client_factory",
|
||||
"//src/main/cpp/net/eagle0/shardok/ai_testing_common:game_settings_factory",
|
||||
"//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/action_point_distances:action_point_distances_cache",
|
||||
"@googletest//:gtest",
|
||||
"@googletest//:gtest_main",
|
||||
],
|
||||
)
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <filesystem>
|
||||
|
||||
#include "src/main/cpp/net/eagle0/common/FilesystemUtils.hpp"
|
||||
#include "src/main/cpp/net/eagle0/common/RandomGenerator.hpp"
|
||||
#include "src/main/cpp/net/eagle0/shardok/ai/AIConfig.hpp"
|
||||
#include "src/main/cpp/net/eagle0/shardok/ai/ShardokAIClient.hpp"
|
||||
#include "src/main/cpp/net/eagle0/shardok/ai/TranspositionTable.hpp"
|
||||
#include "src/main/cpp/net/eagle0/shardok/ai_testing_common/AIClientFactory.hpp"
|
||||
#include "src/main/cpp/net/eagle0/shardok/ai_testing_common/GameSettingsFactory.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"
|
||||
|
||||
namespace shardok {
|
||||
|
||||
extern TranspositionTable g_transpositionTable;
|
||||
|
||||
namespace {
|
||||
|
||||
using net::eagle0::shardok::common::START_FIRE_COMMAND;
|
||||
|
||||
constexpr PlayerId kAttackerPlayerId = 0;
|
||||
|
||||
class RealBattleDecisionRegressionTest : public testing::Test {
|
||||
protected:
|
||||
void SetUp() override {
|
||||
auto path = std::filesystem::current_path();
|
||||
path.append(
|
||||
"bazel-bin/src/test/cpp/net/eagle0/shardok/ai/"
|
||||
"real_battle_decision_regression_test");
|
||||
FilesystemUtils::SetExecPath(path);
|
||||
|
||||
g_transpositionTable.clear();
|
||||
ActionPointDistancesCache::ClearThreadLocalCache();
|
||||
|
||||
settings = ai_testing_common::GameSettingsFactory::CreateDefault();
|
||||
auto setter = settings->GetSetter();
|
||||
setter.SetInt("maxRounds", 31);
|
||||
setter.SetInt("minLookaheadTurns", 1);
|
||||
setter.SetDouble("allAiBattleTimeBudgetMaximum", 0.2);
|
||||
setter.SetRandomGenerator(std::make_shared<StdLibraryGenerator>(1));
|
||||
}
|
||||
|
||||
GameSettingsSPtr settings;
|
||||
};
|
||||
|
||||
TEST_F(RealBattleDecisionRegressionTest, DoesNotChooseKingsLoyalistsMageDistantFire) {
|
||||
// Real battle 68f300179888acd7_52_2add2586 immediately before recorded action 70.
|
||||
// The live stream chose START_FIRE with this mage at (5, 0); the current deterministic
|
||||
// test budget reproduces the same bad class of choice at a nearby distant tile.
|
||||
const auto fixturePath =
|
||||
std::filesystem::current_path() /
|
||||
"src/test/cpp/net/eagle0/shardok/ai/testdata/kings_loyalists_before_mage_fire.fb";
|
||||
const auto state = GameStateW::LoadFrom(fixturePath.string());
|
||||
const ShardokEngine engine(settings, state);
|
||||
const auto commands = engine.GetAvailableCommandsForAIPlayer(kAttackerPlayerId);
|
||||
const auto ai = ai_testing_common::AIClientFactory::Create(
|
||||
kAttackerPlayerId,
|
||||
false,
|
||||
state->hex_map(),
|
||||
settings->GetGetter(),
|
||||
AIAlgorithmType::ITERATIVE_DEEPENING,
|
||||
ScoringCalculatorType::STANDARD,
|
||||
true);
|
||||
|
||||
const auto choice = ai->ChooseCommandIndex(engine);
|
||||
|
||||
ASSERT_LT(choice.chosenIndex, commands->size());
|
||||
const auto& chosenCommand = commands->at(choice.chosenIndex);
|
||||
EXPECT_FALSE(
|
||||
chosenCommand->GetCommandType() == START_FIRE_COMMAND &&
|
||||
chosenCommand->GetActorUnitId() == 3);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
} // namespace shardok
|
||||
BIN
Binary file not shown.
Reference in New Issue
Block a user