|
|
|
@@ -7,11 +7,21 @@
|
|
|
|
|
#include <filesystem>
|
|
|
|
|
|
|
|
|
|
#include "src/main/cpp/net/eagle0/common/FilesystemUtils.hpp"
|
|
|
|
|
#include "src/main/cpp/net/eagle0/shardok/ai/AICommandFilter.hpp"
|
|
|
|
|
#include "src/main/cpp/net/eagle0/shardok/ai/AITimeBudget.hpp"
|
|
|
|
|
#include "src/main/cpp/net/eagle0/shardok/ai/ShardokAIClient.hpp"
|
|
|
|
|
#include "src/main/cpp/net/eagle0/shardok/library/ShardokEngine.hpp"
|
|
|
|
|
#include "src/main/cpp/net/eagle0/shardok/util/MapLoader.hpp"
|
|
|
|
|
#include "src/test/cpp/net/eagle0/shardok/library/AIPerformanceTestHelpers.hpp"
|
|
|
|
|
#include "src/test/cpp/net/eagle0/shardok/library/GameSettings_test_utils.hpp"
|
|
|
|
|
#include "src/test/cpp/net/eagle0/shardok/library/ShardokEngineBasedTestData.hpp"
|
|
|
|
|
|
|
|
|
|
namespace shardok {
|
|
|
|
|
namespace mcts {
|
|
|
|
|
// Extern declaration for cache clearing function
|
|
|
|
|
extern void clearLegalActionsCache_ForTesting();
|
|
|
|
|
} // namespace mcts
|
|
|
|
|
} // namespace shardok
|
|
|
|
|
|
|
|
|
|
namespace shardok {
|
|
|
|
|
|
|
|
|
@@ -233,4 +243,554 @@ TEST_F(MCTSSetupPhaseTest, DefenderDoesNotEndSetupWithReserveUnitsMinimax) {
|
|
|
|
|
<< "Defender should NOT end setup when it has units still in reserve (MINIMAX)!";
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Test the RAISE_DEAD scenario where attacker chose RAISE_DEAD instead of moving forward
|
|
|
|
|
TEST_F(MCTSSetupPhaseTest, AttackerRaiseDeadScenario) {
|
|
|
|
|
// This test reproduces a scenario from actual gameplay where:
|
|
|
|
|
// - Attacker placed 6 units during setup
|
|
|
|
|
// - Moved 5 units toward the enemy
|
|
|
|
|
// - Surprisingly chose RAISE_DEAD with the 6th unit instead of moving it forward
|
|
|
|
|
//
|
|
|
|
|
// Unit positions right before RAISE_DEAD was chosen:
|
|
|
|
|
// - Unit 3 at (4, 5)
|
|
|
|
|
// - Unit 5 at (4, 4)
|
|
|
|
|
// - Unit 2 at (3, 3)
|
|
|
|
|
// - Unit 0 at (3, 4)
|
|
|
|
|
// - Unit 1 at (1, 1) - this is the unit that chose RAISE_DEAD at (0, 2)
|
|
|
|
|
// - Unit 4 at (3, 2)
|
|
|
|
|
|
|
|
|
|
auto gameState = CreatePerfTestGameState(settings, false); // false = attacker is P0
|
|
|
|
|
ShardokEngine engine(settings, gameState);
|
|
|
|
|
|
|
|
|
|
const PlayerId attackerPlayerId = 0;
|
|
|
|
|
const PlayerId defenderPlayerId = 1;
|
|
|
|
|
const auto* hexMap = engine.GetCurrentGameState()->hex_map();
|
|
|
|
|
|
|
|
|
|
// Create AI client for attacker WITHOUT tree dumping initially
|
|
|
|
|
// We'll enable it only for the turn where we want to analyze
|
|
|
|
|
ShardokAIClient attackerAI(
|
|
|
|
|
attackerPlayerId,
|
|
|
|
|
false,
|
|
|
|
|
hexMap,
|
|
|
|
|
settings->GetGetter(),
|
|
|
|
|
AIAlgorithmType::MCTS,
|
|
|
|
|
ScoringCalculatorType::MCTS_OPTIMIZED,
|
|
|
|
|
MakeMCTSConfig(0));
|
|
|
|
|
|
|
|
|
|
// Place attacker units at the positions they were at before RAISE_DEAD
|
|
|
|
|
// Unit placements based on extracted game data:
|
|
|
|
|
// Original placements: Unit2@(1,0), Unit1@(0,0), Unit3@(1,2), Unit5@(0,2), Unit4@(3,0),
|
|
|
|
|
// Unit0@(0,3) After moves: Unit3@(4,5), Unit5@(4,4), Unit2@(3,3), Unit0@(3,4), Unit1@(1,1),
|
|
|
|
|
// Unit4@(3,2)
|
|
|
|
|
|
|
|
|
|
// For simplicity, we'll just play through setup naturally and move units
|
|
|
|
|
// Place all 6 attacker units
|
|
|
|
|
for (int i = 0; i < 6; ++i) {
|
|
|
|
|
const auto choiceResults = attackerAI.ChooseCommandIndex(engine);
|
|
|
|
|
engine.PostCommand(attackerPlayerId, choiceResults.chosenIndex);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// End attacker setup
|
|
|
|
|
{
|
|
|
|
|
const auto commands = engine.GetAvailableCommandsForAIPlayer(attackerPlayerId);
|
|
|
|
|
for (size_t i = 0; i < commands->size(); ++i) {
|
|
|
|
|
if ((*commands)[i]->GetCommandType() ==
|
|
|
|
|
net::eagle0::shardok::common::CommandType::END_PLAYER_SETUP_COMMAND) {
|
|
|
|
|
engine.PostCommand(attackerPlayerId, i);
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Create defender AI
|
|
|
|
|
ShardokAIClient defenderAI(
|
|
|
|
|
defenderPlayerId,
|
|
|
|
|
true,
|
|
|
|
|
hexMap,
|
|
|
|
|
settings->GetGetter(),
|
|
|
|
|
AIAlgorithmType::MCTS,
|
|
|
|
|
ScoringCalculatorType::MCTS_OPTIMIZED,
|
|
|
|
|
MakeMCTSConfig(0));
|
|
|
|
|
|
|
|
|
|
// Place all 6 defender units
|
|
|
|
|
for (int i = 0; i < 6; ++i) {
|
|
|
|
|
const auto choiceResults = defenderAI.ChooseCommandIndex(engine);
|
|
|
|
|
engine.PostCommand(defenderPlayerId, choiceResults.chosenIndex);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// End defender setup
|
|
|
|
|
{
|
|
|
|
|
const auto commands = engine.GetAvailableCommandsForAIPlayer(defenderPlayerId);
|
|
|
|
|
for (size_t i = 0; i < commands->size(); ++i) {
|
|
|
|
|
if ((*commands)[i]->GetCommandType() ==
|
|
|
|
|
net::eagle0::shardok::common::CommandType::END_PLAYER_SETUP_COMMAND) {
|
|
|
|
|
engine.PostCommand(defenderPlayerId, i);
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Now we're in combat phase. Have attacker make several moves
|
|
|
|
|
// We'll let the AI play naturally for a few turns to see if it makes the RAISE_DEAD choice
|
|
|
|
|
printf("=== Combat Phase Started ===\n");
|
|
|
|
|
|
|
|
|
|
// Let attacker make 10 moves and see what happens
|
|
|
|
|
for (int turn = 0; turn < 10; ++turn) {
|
|
|
|
|
ASSERT_EQ(engine.GetCurrentGameState()->current_player(), attackerPlayerId);
|
|
|
|
|
|
|
|
|
|
const auto choiceResults = attackerAI.ChooseCommandIndex(engine);
|
|
|
|
|
const auto commands = engine.GetAvailableCommandsForAIPlayer(attackerPlayerId);
|
|
|
|
|
|
|
|
|
|
ASSERT_LT(choiceResults.chosenIndex, commands->size());
|
|
|
|
|
const auto& chosenCommand = (*commands)[choiceResults.chosenIndex];
|
|
|
|
|
|
|
|
|
|
printf("Turn %d: Attacker chose command type %d\n",
|
|
|
|
|
turn,
|
|
|
|
|
static_cast<int>(chosenCommand->GetCommandType()));
|
|
|
|
|
|
|
|
|
|
// Check if RAISE_DEAD was chosen
|
|
|
|
|
if (chosenCommand->GetCommandType() ==
|
|
|
|
|
net::eagle0::shardok::common::CommandType::RAISE_DEAD_COMMAND) {
|
|
|
|
|
printf("*** RAISE_DEAD chosen on turn %d ***\n", turn);
|
|
|
|
|
// This is interesting - log it but don't fail the test
|
|
|
|
|
// We want to see if this behavior reproduces
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
engine.PostCommand(attackerPlayerId, choiceResults.chosenIndex);
|
|
|
|
|
|
|
|
|
|
// If END_TURN was chosen, exit loop
|
|
|
|
|
if (chosenCommand->GetCommandType() ==
|
|
|
|
|
net::eagle0::shardok::common::CommandType::END_TURN_COMMAND) {
|
|
|
|
|
printf("Attacker ended turn after %d moves\n", turn + 1);
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Test passes if we got through without crashing
|
|
|
|
|
// The purpose is to observe if RAISE_DEAD behavior reproduces
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Focused test to investigate HOLY_WAVE decision with tree dumping
|
|
|
|
|
TEST_F(MCTSSetupPhaseTest, InvestigateHolyWaveBehavior) {
|
|
|
|
|
// This test specifically investigates why MCTS chose HOLY_WAVE instead of MOVE
|
|
|
|
|
// We'll enable tree dumping to analyze the decision-making process
|
|
|
|
|
|
|
|
|
|
auto gameState = CreatePerfTestGameState(settings, false); // false = attacker is P0
|
|
|
|
|
ShardokEngine engine(settings, gameState);
|
|
|
|
|
|
|
|
|
|
const PlayerId attackerPlayerId = 0;
|
|
|
|
|
const PlayerId defenderPlayerId = 1;
|
|
|
|
|
const auto* hexMap = engine.GetCurrentGameState()->hex_map();
|
|
|
|
|
|
|
|
|
|
// Create AI client WITHOUT tree dumping for setup phase
|
|
|
|
|
ShardokAIClient attackerAI(
|
|
|
|
|
attackerPlayerId,
|
|
|
|
|
false,
|
|
|
|
|
hexMap,
|
|
|
|
|
settings->GetGetter(),
|
|
|
|
|
AIAlgorithmType::MCTS,
|
|
|
|
|
ScoringCalculatorType::MCTS_OPTIMIZED,
|
|
|
|
|
MakeMCTSConfig(0));
|
|
|
|
|
|
|
|
|
|
ShardokAIClient defenderAI(
|
|
|
|
|
defenderPlayerId,
|
|
|
|
|
true,
|
|
|
|
|
hexMap,
|
|
|
|
|
settings->GetGetter(),
|
|
|
|
|
AIAlgorithmType::MCTS,
|
|
|
|
|
ScoringCalculatorType::MCTS_OPTIMIZED,
|
|
|
|
|
MakeMCTSConfig(0));
|
|
|
|
|
|
|
|
|
|
// Play through setup phase naturally
|
|
|
|
|
for (int i = 0; i < 6; ++i) {
|
|
|
|
|
const auto choiceResults = attackerAI.ChooseCommandIndex(engine);
|
|
|
|
|
engine.PostCommand(attackerPlayerId, choiceResults.chosenIndex);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// End attacker setup
|
|
|
|
|
{
|
|
|
|
|
const auto commands = engine.GetAvailableCommandsForAIPlayer(attackerPlayerId);
|
|
|
|
|
for (size_t i = 0; i < commands->size(); ++i) {
|
|
|
|
|
if ((*commands)[i]->GetCommandType() ==
|
|
|
|
|
net::eagle0::shardok::common::CommandType::END_PLAYER_SETUP_COMMAND) {
|
|
|
|
|
engine.PostCommand(attackerPlayerId, i);
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Place all 6 defender units
|
|
|
|
|
for (int i = 0; i < 6; ++i) {
|
|
|
|
|
const auto choiceResults = defenderAI.ChooseCommandIndex(engine);
|
|
|
|
|
engine.PostCommand(defenderPlayerId, choiceResults.chosenIndex);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// End defender setup
|
|
|
|
|
{
|
|
|
|
|
const auto commands = engine.GetAvailableCommandsForAIPlayer(defenderPlayerId);
|
|
|
|
|
for (size_t i = 0; i < commands->size(); ++i) {
|
|
|
|
|
if ((*commands)[i]->GetCommandType() ==
|
|
|
|
|
net::eagle0::shardok::common::CommandType::END_PLAYER_SETUP_COMMAND) {
|
|
|
|
|
engine.PostCommand(defenderPlayerId, i);
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
printf("=== Combat Phase - Investigating Decision-Making ===\n");
|
|
|
|
|
|
|
|
|
|
// Now create a NEW AI client with tree dumping enabled for combat phase
|
|
|
|
|
auto configWithDump = MakeMCTSConfig(0);
|
|
|
|
|
configWithDump.debugDumpPath = "/tmp/mcts_holy_wave_turn.txt";
|
|
|
|
|
|
|
|
|
|
ShardokAIClient attackerAIWithDump(
|
|
|
|
|
attackerPlayerId,
|
|
|
|
|
false,
|
|
|
|
|
hexMap,
|
|
|
|
|
settings->GetGetter(),
|
|
|
|
|
AIAlgorithmType::MCTS,
|
|
|
|
|
ScoringCalculatorType::MCTS_OPTIMIZED,
|
|
|
|
|
configWithDump);
|
|
|
|
|
|
|
|
|
|
// Let attacker make several moves, dumping tree for each
|
|
|
|
|
for (int turn = 0; turn < 10; ++turn) {
|
|
|
|
|
ASSERT_EQ(engine.GetCurrentGameState()->current_player(), attackerPlayerId);
|
|
|
|
|
|
|
|
|
|
// Update dump path for this turn
|
|
|
|
|
configWithDump.debugDumpPath = "/tmp/mcts_tree_turn_" + std::to_string(turn) + ".txt";
|
|
|
|
|
ShardokAIClient turnAI(
|
|
|
|
|
attackerPlayerId,
|
|
|
|
|
false,
|
|
|
|
|
hexMap,
|
|
|
|
|
settings->GetGetter(),
|
|
|
|
|
AIAlgorithmType::MCTS,
|
|
|
|
|
ScoringCalculatorType::MCTS_OPTIMIZED,
|
|
|
|
|
configWithDump);
|
|
|
|
|
|
|
|
|
|
const auto choiceResults = turnAI.ChooseCommandIndex(engine);
|
|
|
|
|
const auto commands = engine.GetAvailableCommandsForAIPlayer(attackerPlayerId);
|
|
|
|
|
|
|
|
|
|
ASSERT_LT(choiceResults.chosenIndex, commands->size());
|
|
|
|
|
const auto& chosenCommand = (*commands)[choiceResults.chosenIndex];
|
|
|
|
|
|
|
|
|
|
printf("Turn %d: Chose command type %d, tree dumped to /tmp/mcts_tree_turn_%d.txt\n",
|
|
|
|
|
turn,
|
|
|
|
|
static_cast<int>(chosenCommand->GetCommandType()),
|
|
|
|
|
turn);
|
|
|
|
|
|
|
|
|
|
// If HOLY_WAVE or RAISE_DEAD was chosen, note it specially
|
|
|
|
|
if (chosenCommand->GetCommandType() ==
|
|
|
|
|
net::eagle0::shardok::common::CommandType::HOLY_WAVE_COMMAND) {
|
|
|
|
|
printf("*** HOLY_WAVE chosen on turn %d - check /tmp/mcts_tree_turn_%d.txt ***\n",
|
|
|
|
|
turn,
|
|
|
|
|
turn);
|
|
|
|
|
}
|
|
|
|
|
if (chosenCommand->GetCommandType() ==
|
|
|
|
|
net::eagle0::shardok::common::CommandType::RAISE_DEAD_COMMAND) {
|
|
|
|
|
printf("*** RAISE_DEAD chosen on turn %d - check /tmp/mcts_tree_turn_%d.txt ***\n",
|
|
|
|
|
turn,
|
|
|
|
|
turn);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
engine.PostCommand(attackerPlayerId, choiceResults.chosenIndex);
|
|
|
|
|
|
|
|
|
|
// If END_TURN was chosen, exit loop
|
|
|
|
|
if (chosenCommand->GetCommandType() ==
|
|
|
|
|
net::eagle0::shardok::common::CommandType::END_TURN_COMMAND) {
|
|
|
|
|
printf("Attacker ended turn after %d moves\n", turn + 1);
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Test to reproduce the exact RAISE_DEAD scenario from actual gameplay
|
|
|
|
|
// This test creates the board state right before RAISE_DEAD was chosen
|
|
|
|
|
TEST_F(MCTSSetupPhaseTest, ExactRaiseDeadReproduction) {
|
|
|
|
|
// This test reproduces the exact scenario where P0 chose RAISE_DEAD
|
|
|
|
|
// instead of moving forward. Unit positions are from actual gameplay.
|
|
|
|
|
//
|
|
|
|
|
// Attacker (P0) positions before RAISE_DEAD:
|
|
|
|
|
// - Unit 0 at (3, 4)
|
|
|
|
|
// - Unit 1 at (1, 1) ← This unit chose RAISE_DEAD at (0, 2)
|
|
|
|
|
// - Unit 2 at (3, 3)
|
|
|
|
|
// - Unit 3 at (4, 5)
|
|
|
|
|
// - Unit 4 at (3, 2)
|
|
|
|
|
// - Unit 5 at (4, 4)
|
|
|
|
|
//
|
|
|
|
|
// We'll place defenders at castle positions and starting areas
|
|
|
|
|
|
|
|
|
|
// Clear static cache to prevent stale data from previous tests
|
|
|
|
|
mcts::clearLegalActionsCache_ForTesting();
|
|
|
|
|
|
|
|
|
|
// Configure RAISE_DEAD settings (essential - range must be > 0!)
|
|
|
|
|
GetGameSettingsSetter().SetInt(kSettingsKeys.raiseDeadRange, 2);
|
|
|
|
|
GetGameSettingsSetter().SetInt(kSettingsKeys.raiseDeadActionPointCost, 5);
|
|
|
|
|
GetGameSettingsSetter().SetInt(kSettingsKeys.raiseDeadBaseOdds, 100);
|
|
|
|
|
|
|
|
|
|
// Configure AI time budget for thorough search (3000ms total)
|
|
|
|
|
// With ~205 commands, 15ms per command gives ~3075ms budget
|
|
|
|
|
GetGameSettingsSetter().SetDouble("lookaheadTimeBudgetPerCommandSetupMs", 15.0);
|
|
|
|
|
GetGameSettingsSetter().SetDouble("lookaheadTimeBudgetPerCommandCloseMs", 15.0);
|
|
|
|
|
GetGameSettingsSetter().SetDouble("lookaheadTimeBudgetPerCommandFarMs", 15.0);
|
|
|
|
|
GetGameSettingsSetter().SetDouble("lookaheadTimeBudgetMaximumSeconds", 10.0);
|
|
|
|
|
|
|
|
|
|
// Create units manually using test helpers
|
|
|
|
|
vector<Unit> attackerUnits;
|
|
|
|
|
vector<Unit> defenderUnits;
|
|
|
|
|
|
|
|
|
|
// Helper lambda to create a unit at a specific position
|
|
|
|
|
auto createUnit = [this](PlayerId playerId,
|
|
|
|
|
UnitId unitId,
|
|
|
|
|
Coords position,
|
|
|
|
|
int profession = 1,
|
|
|
|
|
std::optional<net::eagle0::shardok::storage::fb::BattalionTypeId>
|
|
|
|
|
battalionTypeOverride = std::nullopt,
|
|
|
|
|
int actionPoints = 12) {
|
|
|
|
|
Unit unit{};
|
|
|
|
|
unit.mutate_player_id(playerId);
|
|
|
|
|
unit.mutate_unit_id(unitId);
|
|
|
|
|
unit.mutate_eagle_player_id(playerId);
|
|
|
|
|
unit.mutable_location() = position;
|
|
|
|
|
unit.mutate_status(net::eagle0::shardok::storage::fb::UnitStatus_NORMAL_UNIT);
|
|
|
|
|
unit.mutate_remaining_action_points(actionPoints);
|
|
|
|
|
unit.mutate_hidden(false);
|
|
|
|
|
unit.mutate_fortified(false);
|
|
|
|
|
unit.mutate_can_flee(true);
|
|
|
|
|
unit.mutate_can_start_fire(false);
|
|
|
|
|
unit.mutate_can_archery(false);
|
|
|
|
|
unit.mutate_stun_rounds_remaining(0);
|
|
|
|
|
unit.mutate_commanding_unit_id(-1);
|
|
|
|
|
unit.mutate_targeted_unit(-1);
|
|
|
|
|
unit.mutate_starting_position_index(playerId == 0 ? 0 : -1);
|
|
|
|
|
unit.mutate_has_moved_in_zoc(false);
|
|
|
|
|
unit.mutate_volleys_remaining(0);
|
|
|
|
|
unit.mutate_food_remaining(1000.0f);
|
|
|
|
|
|
|
|
|
|
// Battalion
|
|
|
|
|
auto battalionType = battalionTypeOverride.value_or(
|
|
|
|
|
playerId == 0 ? net::eagle0::shardok::storage::fb::BattalionTypeId_LONGBOWMEN
|
|
|
|
|
: net::eagle0::shardok::storage::fb::BattalionTypeId_LIGHT_INFANTRY);
|
|
|
|
|
const auto battalionTypeInfo = settings->GetGetter().GetBattalionType(battalionType);
|
|
|
|
|
net::eagle0::shardok::storage::fb::Battalion battalion;
|
|
|
|
|
battalion.mutate_type(battalionType);
|
|
|
|
|
battalion.mutate_size(static_cast<float>(battalionTypeInfo->capacity));
|
|
|
|
|
battalion.mutate_armament(100.0f);
|
|
|
|
|
battalion.mutate_training(100.0f);
|
|
|
|
|
battalion.mutate_morale(50.0f);
|
|
|
|
|
unit.mutable_battalion() = battalion;
|
|
|
|
|
|
|
|
|
|
// Hero
|
|
|
|
|
unit.mutate_has_attached_hero(true);
|
|
|
|
|
net::eagle0::shardok::storage::fb::Hero hero;
|
|
|
|
|
hero.mutate_strength(102);
|
|
|
|
|
hero.mutate_strength_xp(0);
|
|
|
|
|
hero.mutate_agility(102);
|
|
|
|
|
hero.mutate_agility_xp(0);
|
|
|
|
|
hero.mutate_wisdom(102);
|
|
|
|
|
hero.mutate_wisdom_xp(0);
|
|
|
|
|
hero.mutate_charisma(102);
|
|
|
|
|
hero.mutate_charisma_xp(0);
|
|
|
|
|
hero.mutate_constitution(102);
|
|
|
|
|
hero.mutate_constitution_xp(0);
|
|
|
|
|
hero.mutate_vigor(102);
|
|
|
|
|
hero.mutate_starting_vigor(102);
|
|
|
|
|
hero.mutate_spent_vigor(0);
|
|
|
|
|
hero.mutate_bravery(102);
|
|
|
|
|
hero.mutate_integrity(50);
|
|
|
|
|
hero.mutate_ambition(50);
|
|
|
|
|
hero.mutate_eagle_hero_id(unitId + 1);
|
|
|
|
|
hero.mutate_is_vip(false);
|
|
|
|
|
hero.mutable_profession_info().mutate_profession(
|
|
|
|
|
static_cast<net::eagle0::shardok::storage::fb::Profession>(profession));
|
|
|
|
|
hero.mutable_profession_info().mutate_meteor_cast_state(
|
|
|
|
|
net::eagle0::shardok::storage::fb::MultiroundMagicState_NONE);
|
|
|
|
|
hero.mutable_control_info().mutate_controlled_unit_id(-1);
|
|
|
|
|
hero.mutable_control_info().mutate_controlled_this_round(false);
|
|
|
|
|
unit.mutable_attached_hero() = hero;
|
|
|
|
|
|
|
|
|
|
unit.mutable_opponent_knowledge()->Mutate(0, 0);
|
|
|
|
|
unit.mutable_opponent_knowledge()->Mutate(1, 0);
|
|
|
|
|
|
|
|
|
|
return unit;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// Create attacker units - spread out to leave empty hexes around NECROMANCER
|
|
|
|
|
// Unit 0 has 0 action points to eliminate ALL movement options
|
|
|
|
|
attackerUnits.push_back(createUnit(0, 0, Coords(4, 5), 1, std::nullopt, 0)); // Unit 0 - no AP
|
|
|
|
|
// Unit 1 - NECROMANCER with no flee option
|
|
|
|
|
{
|
|
|
|
|
auto unit = createUnit(
|
|
|
|
|
0,
|
|
|
|
|
1,
|
|
|
|
|
Coords(1, 1), // NECROMANCER with space around it
|
|
|
|
|
net::eagle0::shardok::storage::fb::Profession_NECROMANCER,
|
|
|
|
|
net::eagle0::shardok::storage::fb::BattalionTypeId_LIGHT_INFANTRY);
|
|
|
|
|
unit.mutate_can_flee(false); // Disable FLEE to force other actions
|
|
|
|
|
attackerUnits.push_back(unit);
|
|
|
|
|
}
|
|
|
|
|
// Units 2-5: No action points, so only NECROMANCER can act
|
|
|
|
|
attackerUnits.push_back(createUnit(0, 2, Coords(5, 6), 3, std::nullopt, 0)); // Unit 2 - no AP
|
|
|
|
|
attackerUnits.push_back(createUnit(0, 3, Coords(6, 5), 4, std::nullopt, 0)); // Unit 3 - no AP
|
|
|
|
|
attackerUnits.push_back(createUnit(0, 4, Coords(6, 6), 5, std::nullopt, 0)); // Unit 4 - no AP
|
|
|
|
|
attackerUnits.push_back(createUnit(0, 5, Coords(5, 7), 6, std::nullopt, 0)); // Unit 5 - no AP
|
|
|
|
|
|
|
|
|
|
// Create defender units at castle and defensive positions
|
|
|
|
|
// (Approximating since we don't have exact positions from original game)
|
|
|
|
|
defenderUnits.push_back(createUnit(1, 6, Coords(10, 13), 0)); // Castle position
|
|
|
|
|
defenderUnits.push_back(createUnit(1, 7, Coords(11, 12), 0)); // Castle position
|
|
|
|
|
defenderUnits.push_back(createUnit(1, 8, Coords(10, 12), 0)); // Castle position
|
|
|
|
|
defenderUnits.push_back(createUnit(1, 9, Coords(11, 10), 0)); // Defensive line
|
|
|
|
|
defenderUnits.push_back(createUnit(1, 10, Coords(11, 13), 0)); // Defensive line
|
|
|
|
|
defenderUnits.push_back(createUnit(1, 11, Coords(11, 9), 0)); // Defensive line
|
|
|
|
|
|
|
|
|
|
// Create game with reserved slots using the new helper function
|
|
|
|
|
// This will add 10 RESERVED_SLOT units for RAISE_DEAD
|
|
|
|
|
auto hexMapProto = LoadMap("Alah");
|
|
|
|
|
auto engine = GameWithUnitsAndReservedSlots(hexMapProto, attackerUnits, defenderUnits, 10);
|
|
|
|
|
|
|
|
|
|
// Verify we're in combat phase
|
|
|
|
|
ASSERT_EQ(
|
|
|
|
|
engine->GetCurrentGameState()->status()->state(),
|
|
|
|
|
net::eagle0::shardok::storage::fb::GameStatus_::State_GAME_RUNNING);
|
|
|
|
|
|
|
|
|
|
const PlayerId attackerPlayerId = 0;
|
|
|
|
|
const auto* hexMap = engine->GetCurrentGameState()->hex_map();
|
|
|
|
|
|
|
|
|
|
// Create AI with tree dumping enabled
|
|
|
|
|
// maxPlayerFlips=1 allows exploring opponent's immediate responses
|
|
|
|
|
// (0 would stop after first action that ends turn)
|
|
|
|
|
auto config = MakeMCTSConfig(1);
|
|
|
|
|
config.debugDumpPath = "/tmp/mcts_raise_dead_reproduction.txt";
|
|
|
|
|
|
|
|
|
|
ShardokAIClient attackerAI(
|
|
|
|
|
attackerPlayerId,
|
|
|
|
|
false,
|
|
|
|
|
hexMap,
|
|
|
|
|
settings->GetGetter(),
|
|
|
|
|
AIAlgorithmType::MCTS,
|
|
|
|
|
ScoringCalculatorType::MCTS_OPTIMIZED,
|
|
|
|
|
config);
|
|
|
|
|
|
|
|
|
|
printf("=== RAISE_DEAD Reproduction Test ===\n");
|
|
|
|
|
printf("Unit positions:\n");
|
|
|
|
|
printf(" Unit 0 at (2, 2)\n");
|
|
|
|
|
printf(" Unit 1 at (1, 1) ← NECROMANCER hero for RAISE_DEAD (original position)\n");
|
|
|
|
|
printf(" Unit 2 at (3, 3)\n");
|
|
|
|
|
printf(" Unit 3 at (4, 5)\n");
|
|
|
|
|
printf(" Unit 4 at (3, 2)\n");
|
|
|
|
|
printf(" Unit 5 at (4, 4)\n");
|
|
|
|
|
|
|
|
|
|
// Verify Unit 1 configuration
|
|
|
|
|
const auto* unit1 = engine->GetCurrentGameState()->units()->Get(1);
|
|
|
|
|
printf("\nUnit 1 verification:\n");
|
|
|
|
|
printf(" Has hero: %s\n", unit1->has_attached_hero() ? "YES" : "NO");
|
|
|
|
|
if (unit1->has_attached_hero()) {
|
|
|
|
|
printf(" Hero profession: %d (NECROMANCER=%d)\n",
|
|
|
|
|
unit1->attached_hero().profession_info().profession(),
|
|
|
|
|
net::eagle0::shardok::storage::fb::Profession_NECROMANCER);
|
|
|
|
|
printf(" Controlled unit ID: %d\n",
|
|
|
|
|
unit1->attached_hero().control_info().controlled_unit_id());
|
|
|
|
|
}
|
|
|
|
|
printf(" Battalion type: %d (LIGHT_INFANTRY=%d)\n",
|
|
|
|
|
unit1->battalion().type(),
|
|
|
|
|
net::eagle0::shardok::storage::fb::BattalionTypeId_LIGHT_INFANTRY);
|
|
|
|
|
const auto battalionTypeInfo =
|
|
|
|
|
settings->GetGetter().GetBattalionType(unit1->battalion().type());
|
|
|
|
|
printf(" Allows casting: %s\n\n", battalionTypeInfo->allowsCasting ? "YES" : "NO");
|
|
|
|
|
|
|
|
|
|
// Check actual unit position and status in game state
|
|
|
|
|
printf("Unit 1 action points: %d\n", unit1->remaining_action_points());
|
|
|
|
|
printf("Unit 1 player ID: %d\n", unit1->player_id());
|
|
|
|
|
printf("Unit 1 status: %d\n\n", unit1->status());
|
|
|
|
|
|
|
|
|
|
// Check all units in game and their statuses
|
|
|
|
|
printf("Total units in game: %u\n", engine->GetCurrentGameState()->units()->size());
|
|
|
|
|
int reservedCount = 0;
|
|
|
|
|
for (unsigned int i = 0; i < engine->GetCurrentGameState()->units()->size(); ++i) {
|
|
|
|
|
const auto* u = engine->GetCurrentGameState()->units()->Get(i);
|
|
|
|
|
if (u->status() == net::eagle0::shardok::storage::fb::UnitStatus_RESERVED_SLOT) {
|
|
|
|
|
reservedCount++;
|
|
|
|
|
printf(" Unit %u: RESERVED_SLOT\n", i);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
printf("Reserved slots available: %d\n\n", reservedCount);
|
|
|
|
|
|
|
|
|
|
// Check for RAISE_DEAD commands before running AI
|
|
|
|
|
const auto preCheckCommands = engine->GetAvailableCommandsForAIPlayer(attackerPlayerId);
|
|
|
|
|
int raiseDeadCountBefore = 0;
|
|
|
|
|
printf("Checking %zu total commands for player %d\n",
|
|
|
|
|
preCheckCommands->size(),
|
|
|
|
|
attackerPlayerId);
|
|
|
|
|
for (size_t i = 0; i < preCheckCommands->size(); ++i) {
|
|
|
|
|
auto cmdType = (*preCheckCommands)[i]->GetCommandType();
|
|
|
|
|
if (cmdType == net::eagle0::shardok::common::CommandType::RAISE_DEAD_COMMAND) {
|
|
|
|
|
raiseDeadCountBefore++;
|
|
|
|
|
printf("Found RAISE_DEAD command #%d\n", raiseDeadCountBefore);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
printf("Total RAISE_DEAD commands available: %d\n\n", raiseDeadCountBefore);
|
|
|
|
|
|
|
|
|
|
// Test command filtering
|
|
|
|
|
printf("=== Testing Command Filter ===\n");
|
|
|
|
|
printf("State units before AI runs:\n");
|
|
|
|
|
for (unsigned int i = 0; i < engine->GetCurrentGameState()->units()->size() && i < 6; ++i) {
|
|
|
|
|
const auto* u = engine->GetCurrentGameState()->units()->Get(i);
|
|
|
|
|
printf(" Unit %u: AP=%d, pos=(%d,%d)\n",
|
|
|
|
|
i,
|
|
|
|
|
u->remaining_action_points(),
|
|
|
|
|
u->location().row(),
|
|
|
|
|
u->location().column());
|
|
|
|
|
}
|
|
|
|
|
auto testAPDCache = std::make_shared<ActionPointDistancesCache>();
|
|
|
|
|
const auto filteredIndices = AICommandFilter::FilterCommands(
|
|
|
|
|
preCheckCommands,
|
|
|
|
|
attackerPlayerId,
|
|
|
|
|
false, // isDefender
|
|
|
|
|
engine->GetCurrentGameState(),
|
|
|
|
|
testAPDCache,
|
|
|
|
|
[this](net::eagle0::shardok::storage::fb::BattalionTypeId typeId) {
|
|
|
|
|
return settings->GetGetter().GetBattalionType(typeId);
|
|
|
|
|
});
|
|
|
|
|
printf("After filtering: %zu commands (from %zu total)\n",
|
|
|
|
|
filteredIndices.size(),
|
|
|
|
|
preCheckCommands->size());
|
|
|
|
|
int raiseDeadAfterFilter = 0;
|
|
|
|
|
for (size_t idx : filteredIndices) {
|
|
|
|
|
if ((*preCheckCommands)[idx]->GetCommandType() ==
|
|
|
|
|
net::eagle0::shardok::common::CommandType::RAISE_DEAD_COMMAND) {
|
|
|
|
|
raiseDeadAfterFilter++;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
printf("RAISE_DEAD commands after filter: %d\n\n", raiseDeadAfterFilter);
|
|
|
|
|
|
|
|
|
|
// Run AI and see what it chooses
|
|
|
|
|
const auto choiceResults = attackerAI.ChooseCommandIndex(*engine);
|
|
|
|
|
const auto commands = engine->GetAvailableCommandsForAIPlayer(attackerPlayerId);
|
|
|
|
|
|
|
|
|
|
ASSERT_LT(choiceResults.chosenIndex, commands->size());
|
|
|
|
|
const auto& chosenCommand = (*commands)[choiceResults.chosenIndex];
|
|
|
|
|
|
|
|
|
|
printf("AI chose command type: %d\n", static_cast<int>(chosenCommand->GetCommandType()));
|
|
|
|
|
|
|
|
|
|
// Check if RAISE_DEAD was chosen
|
|
|
|
|
if (chosenCommand->GetCommandType() ==
|
|
|
|
|
net::eagle0::shardok::common::CommandType::RAISE_DEAD_COMMAND) {
|
|
|
|
|
printf("*** RAISE_DEAD REPRODUCED! Check /tmp/mcts_raise_dead_reproduction.txt ***\n");
|
|
|
|
|
printf("This confirms the behavior from original gameplay.\n");
|
|
|
|
|
} else {
|
|
|
|
|
printf("AI chose different command (type %d instead of RAISE_DEAD)\n",
|
|
|
|
|
static_cast<int>(chosenCommand->GetCommandType()));
|
|
|
|
|
printf("Check /tmp/mcts_raise_dead_reproduction.txt for decision tree\n");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Log available RAISE_DEAD commands
|
|
|
|
|
int raiseDeadCount = 0;
|
|
|
|
|
for (size_t i = 0; i < commands->size(); ++i) {
|
|
|
|
|
if ((*commands)[i]->GetCommandType() ==
|
|
|
|
|
net::eagle0::shardok::common::CommandType::RAISE_DEAD_COMMAND) {
|
|
|
|
|
raiseDeadCount++;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
printf("Available RAISE_DEAD commands: %d\n", raiseDeadCount);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
} // namespace shardok
|
|
|
|
|