mirror of
https://github.com/nolen777/eagle0.git
synced 2026-07-29 11:15:48 +00:00
Compare commits
29
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
31d5702f0b | ||
|
|
9612c2154c | ||
|
|
fe92f69ca9 | ||
|
|
7e7c48315e | ||
|
|
126e26f8c0 | ||
|
|
bf0260dfc9 | ||
|
|
278a041d05 | ||
|
|
98ccac67c9 | ||
|
|
04bb8edac1 | ||
|
|
1b1d290ead | ||
|
|
1848c46a0a | ||
|
|
5df1cb5412 | ||
|
|
a7f4ef2d57 | ||
|
|
7ee22fc988 | ||
|
|
e5fdfd25c8 | ||
|
|
12d74ae0f1 | ||
|
|
47b63e7ad3 | ||
|
|
e116c7a5dc | ||
|
|
a8005aa099 | ||
|
|
86a309330f | ||
|
|
db9f2052c6 | ||
|
|
63e79b8fae | ||
|
|
5c042dd683 | ||
|
|
f65833fdcb | ||
|
|
a58c13af71 | ||
|
|
8fe416dc0e | ||
|
|
c74e0506b6 | ||
|
|
9144d7d7f4 | ||
|
|
6aa6b07e61 |
@@ -19,9 +19,9 @@ common --worker_sandboxing
|
||||
common --local_test_jobs=64
|
||||
common --jobs=64
|
||||
|
||||
common --cxxopt="--std=c++20"
|
||||
common --cxxopt="--std=c++23"
|
||||
common --cxxopt="-Wno-deprecated-non-prototype"
|
||||
common --host_cxxopt="--std=c++20"
|
||||
common --host_cxxopt="--std=c++23"
|
||||
|
||||
common --javacopt="-Xlint:-options"
|
||||
|
||||
|
||||
@@ -4,26 +4,32 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
|
||||
|
||||
## Project Overview
|
||||
|
||||
Eagle0 is a multi-language gaming system combining strategic turn-based gameplay (Eagle) with tactical hex-based combat (Shardok). The system integrates LLM-based narrative generation and supports both human and AI players.
|
||||
Eagle0 is a multi-language gaming system combining strategic turn-based gameplay (Eagle) with tactical hex-based
|
||||
combat (Shardok). The system integrates LLM-based narrative generation and supports both human and AI players.
|
||||
|
||||
## Architecture
|
||||
|
||||
**Three-Tier Game System:**
|
||||
|
||||
- **Unity Client (C#)**: Real-time strategy game client with integrated tactical combat UI
|
||||
- **Eagle (Scala)**: Strategic layer managing turn-based gameplay, diplomacy, hero progression, and province control
|
||||
- **Shardok (C++)**: Tactical layer handling real-time hex-based combat simulation with performance-critical battle resolution
|
||||
- **Shardok (C++)**: Tactical layer handling real-time hex-based combat simulation with performance-critical battle
|
||||
resolution
|
||||
|
||||
**Communication Flow:**
|
||||
|
||||
```
|
||||
Unity Client ↔ Eagle (gRPC streaming) ↔ Shardok (internal gRPC)
|
||||
```
|
||||
|
||||
**Key Entry Points:**
|
||||
|
||||
- `/src/main/csharp/net/eagle0/clients/unity/eagle0/` - Unity C# game client
|
||||
- `/src/main/scala/net/eagle0/eagle/Main.scala` - Eagle strategic game server
|
||||
- `/src/main/cpp/net/eagle0/shardok/shardok_server_main.cpp` - Shardok tactical server
|
||||
|
||||
**Protocol Buffer Architecture:**
|
||||
|
||||
- Extensive use of protobuf for type-safe communication
|
||||
- Separate packages: `api/` (client-facing), `internal/` (server state), `views/` (client projections)
|
||||
- Event sourcing pattern with immutable action history
|
||||
@@ -31,6 +37,7 @@ Unity Client ↔ Eagle (gRPC streaming) ↔ Shardok (internal gRPC)
|
||||
## Essential Commands
|
||||
|
||||
### Building
|
||||
|
||||
```bash
|
||||
# Build Eagle server (Scala strategic layer)
|
||||
bazel build //src/main/scala/net/eagle0/eagle:eagle_server_deploy.jar
|
||||
@@ -49,6 +56,7 @@ bazel build //src/main/cpp/net/eagle0/shardok:shardok-server
|
||||
```
|
||||
|
||||
### Running Services
|
||||
|
||||
```bash
|
||||
# Eagle server (port 40032)
|
||||
bazel run //src/main/scala/net/eagle0/eagle:eagle_server -- --eagle-grpc-port 40032
|
||||
@@ -60,6 +68,7 @@ bazel run //src/main/cpp/net/eagle0/shardok:shardok-server --compilation_mode=op
|
||||
```
|
||||
|
||||
### Testing
|
||||
|
||||
```bash
|
||||
# Run all tests
|
||||
bazel test //src/test/... //src/main/go/...
|
||||
@@ -70,12 +79,14 @@ bazel test //src/test/cpp/... # C++ Shardok tests
|
||||
```
|
||||
|
||||
### Code Generation
|
||||
|
||||
```bash
|
||||
bazel run gazelle # Update Go build files
|
||||
./scripts/updateActionResultTypes.sh # Update protocol buffer mappings
|
||||
```
|
||||
|
||||
### Code Formatting
|
||||
|
||||
```bash
|
||||
# ALWAYS run clang-format after making any C++ or C# code changes
|
||||
clang-format -i <modified_files>
|
||||
@@ -88,13 +99,14 @@ find . -name "*.cs" | xargs clang-format -i
|
||||
```
|
||||
|
||||
### Static Analysis
|
||||
|
||||
```bash
|
||||
# Run clang-tidy static analysis on C++ files
|
||||
# Note: This may show some header include errors but will still analyze the main file
|
||||
bazel run @llvm_toolchain//:clang-tidy -- --checks='readability-*,bugprone-*,clang-analyzer-*' <file_path> -- -I/Users/dancrosby/CodingProjects/github/eagle0 -std=c++20
|
||||
bazel run @llvm_toolchain//:clang-tidy -- --checks='readability-*,bugprone-*,clang-analyzer-*' <file_path> -- -I/Users/dancrosby/CodingProjects/github/eagle0 -std=c++23
|
||||
|
||||
# Example for AI files:
|
||||
bazel run @llvm_toolchain//:clang-tidy -- --checks='readability-*,bugprone-*,clang-analyzer-*' /Users/dancrosby/CodingProjects/github/eagle0/src/main/cpp/net/eagle0/shardok/ai/AIUnitScoreCalculator.cpp -- -I/Users/dancrosby/CodingProjects/github/eagle0 -std=c++20
|
||||
bazel run @llvm_toolchain//:clang-tidy -- --checks='readability-*,bugprone-*,clang-analyzer-*' /Users/dancrosby/CodingProjects/github/eagle0/src/main/cpp/net/eagle0/shardok/ai/AIUnitScoreCalculator.cpp -- -I/Users/dancrosby/CodingProjects/github/eagle0 -std=c++23
|
||||
```
|
||||
|
||||
## AI Algorithm Selection
|
||||
@@ -102,13 +114,17 @@ bazel run @llvm_toolchain//:clang-tidy -- --checks='readability-*,bugprone-*,cla
|
||||
Eagle0 supports two AI algorithms for tactical combat decision-making:
|
||||
|
||||
### Iterative Deepening AI (Default)
|
||||
|
||||
The original minimax-based AI with sophisticated randomness handling:
|
||||
|
||||
- **Advantages**: Proven, sophisticated randomness evaluation, comprehensive lookahead
|
||||
- **Use cases**: Production builds, scenarios requiring precise evaluation
|
||||
- **Performance**: Single-threaded, thorough evaluation
|
||||
|
||||
### Monte Carlo Tree Search AI (MCTS)
|
||||
|
||||
Modern MCTS-based AI with multithreading support:
|
||||
|
||||
- **Advantages**: Multithreaded, better performance on modern CPUs, anytime algorithm
|
||||
- **Use cases**: Performance testing, scenarios requiring fast decisions
|
||||
- **Performance**: Multithreaded, adaptive depth based on time budget
|
||||
@@ -141,30 +157,36 @@ bazel test //src/test/cpp/net/eagle0/shardok/ai:ai_mcts_test # If available
|
||||
|
||||
Both implementations are compatible with all existing interfaces and produce the same `SearchResult` structure.
|
||||
|
||||
**Note**: Both implementations are documented in `src/main/cpp/net/eagle0/shardok/ai/AI_SCORING_SYSTEM.md`, including recommendations for improving MCTS randomness handling.
|
||||
**Note**: Both implementations are documented in `src/main/cpp/net/eagle0/shardok/ai/AI_SCORING_SYSTEM.md`, including
|
||||
recommendations for improving MCTS randomness handling.
|
||||
|
||||
The AI algorithm selection is made at runtime when creating ShardokAIClient instances, allowing different AI strategies to be used for different players or game situations within the same server process.
|
||||
The AI algorithm selection is made at runtime when creating ShardokAIClient instances, allowing different AI strategies
|
||||
to be used for different players or game situations within the same server process.
|
||||
|
||||
## Language-Specific Patterns
|
||||
|
||||
**Scala (Strategic Layer):**
|
||||
|
||||
- Use `EngineImpl.scala` for core game logic modifications
|
||||
- Follow event sourcing pattern - all changes through immutable actions
|
||||
- gRPC streaming for real-time client updates via `EagleServiceImpl.scala`
|
||||
- LLM integration in `/common/llm_integration/` for narrative generation
|
||||
|
||||
**C++ (Tactical Layer):**
|
||||
|
||||
- Performance-critical combat in `ShardokEngine.hpp/.cpp`
|
||||
- FlatBuffers for efficient serialization in `/flatbuffer/` directory
|
||||
- AI systems in `/ai/` subdirectory with pluggable strategy selectors
|
||||
- Extensive unit testing with Google Test framework
|
||||
|
||||
**Protocol Buffers:**
|
||||
|
||||
- Three-layer structure: `api/` (client), `internal/` (server), `views/` (projections)
|
||||
- Use `shardok_internal_interface.proto` for Eagle-Shardok communication
|
||||
- Maintain backward compatibility when modifying existing messages
|
||||
|
||||
**C# (Unity Client):**
|
||||
|
||||
- Located in `/src/main/csharp/net/eagle0/clients/unity/eagle0/`
|
||||
- Uses Unity 6 (6000.0.32f1) with comprehensive protobuf integration (100+ .proto files)
|
||||
- Key components: `EagleConnection.cs` (gRPC client), `EagleGameController.cs` (main game logic)
|
||||
@@ -173,6 +195,7 @@ The AI algorithm selection is made at runtime when creating ShardokAIClient inst
|
||||
- Seamless transition between strategic gameplay and hex-based tactical combat
|
||||
|
||||
**Go (Build Tools):**
|
||||
|
||||
- Build automation and code generation utilities
|
||||
- AWS S3 integration for deployment artifacts
|
||||
|
||||
@@ -214,10 +237,12 @@ done
|
||||
```
|
||||
|
||||
**Important notes:**
|
||||
|
||||
- Run tests multiple times (3-5) to account for performance variance
|
||||
- Focus on commands evaluated at each depth rather than total commands
|
||||
- Commands at different depths aren't directly comparable (depth 3 is more valuable than depth 2)
|
||||
- **Always test performance changes** - what seems like an optimization may sometimes have unexpected overhead or behavior changes.
|
||||
- **Always test performance changes** - what seems like an optimization may sometimes have unexpected overhead or
|
||||
behavior changes.
|
||||
|
||||
## Game Content
|
||||
|
||||
|
||||
+1
-1
@@ -1,2 +1,2 @@
|
||||
|
||||
UNITY_VERSION='6000.1.11f1'
|
||||
UNITY_VERSION='6000.2.7f2'
|
||||
@@ -30,7 +30,7 @@ auto rloc(const string& execPath) -> string {
|
||||
const std::unique_ptr<Runfiles> runfiles(Runfiles::Create(execPath, &error));
|
||||
|
||||
if (runfiles == nullptr) {
|
||||
printf("Error! %s\n", error.c_str());
|
||||
fprintf(stderr, "Error! %s\n", error.c_str());
|
||||
abort();
|
||||
// error handling
|
||||
}
|
||||
@@ -67,9 +67,9 @@ auto FilesystemUtils::MapFilesDirectory() -> string {
|
||||
|
||||
void FilesystemUtils::MakeDirectoryIfNecessary(const string& directoryPath) {
|
||||
if (fs::create_directories(directoryPath))
|
||||
printf("Directory %s created\n", directoryPath.c_str());
|
||||
fprintf(stderr, "Directory %s created\n", directoryPath.c_str());
|
||||
else
|
||||
printf("No new directory created for %s\n", directoryPath.c_str());
|
||||
fprintf(stderr, "No new directory created for %s\n", directoryPath.c_str());
|
||||
}
|
||||
|
||||
auto FilesystemUtils::SaveFilesDirectory() -> string {
|
||||
@@ -129,11 +129,11 @@ auto FilesystemUtils::AtomicallySaveToPath(const string& path, const byte_vector
|
||||
if (ostr.good()) {
|
||||
const int err = rename(tempPath.c_str(), path.c_str());
|
||||
if (err == -1) {
|
||||
printf("Failed to move file to %s! Errno %d\n", path.c_str(), errno);
|
||||
fprintf(stderr, "Failed to move file to %s! Errno %d\n", path.c_str(), errno);
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
printf("Failed writing to %s!\n", tempPath.c_str());
|
||||
fprintf(stderr, "Failed writing to %s!\n", tempPath.c_str());
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,139 @@
|
||||
# MCTS (Monte Carlo Tree Search) Framework
|
||||
|
||||
This directory contains a game-agnostic Monte Carlo Tree Search implementation that can be used with any turn-based game. The framework separates the MCTS algorithm from game-specific logic through abstract interfaces.
|
||||
|
||||
## Core Abstract Classes
|
||||
|
||||
### `MCTSAction` (abstract/MCTSAction.hpp)
|
||||
Abstract interface for representing game actions/moves.
|
||||
|
||||
**Key Methods:**
|
||||
- `getIndex()` - Returns the action's unique identifier
|
||||
- `getDescription()` - Human-readable description for debugging/logging
|
||||
- `clone()` - Creates a deep copy of the action
|
||||
- `equals()` - Compares actions for equality
|
||||
|
||||
### `MCTSGameState` (abstract/MCTSGameState.hpp)
|
||||
Abstract interface for representing game states.
|
||||
|
||||
**Key Methods:**
|
||||
- `hash()` - Returns a hash for transposition table lookups
|
||||
- `score(playerId)` - Evaluates the state's value for a given player
|
||||
- `currentPlayerId()` - Returns whose turn it is
|
||||
- `isTerminal()` - Checks if the game has ended
|
||||
- `getWinner()` - Returns the winning player (if terminal)
|
||||
- `clone()` - Creates a deep copy of the state
|
||||
- `equals()` - Compares states for equality
|
||||
|
||||
### `MCTSGameEngine` (abstract/MCTSGameEngine.hpp)
|
||||
Abstract interface for game rule enforcement and state transitions. Many methods have efficient default implementations.
|
||||
|
||||
**Must Override (Pure Virtual):**
|
||||
- `applyAction(state, action)` - Applies an action to create a new state
|
||||
- `getLegalActions(state)` - Returns all valid moves from a state
|
||||
- `isTerminal(state)` - Checks if a state is game-ending
|
||||
- `evaluateState(state, playerId)` - Scores a state for a player
|
||||
|
||||
**Optional Overrides (Have Default Implementations):**
|
||||
- `applyActionMutable(state, action)` - Apply action in-place for efficiency (default: calls applyAction)
|
||||
- `filterActions(actions, state)` - Applies heuristic filtering (default: no filtering)
|
||||
- `simulateRandomPlayout(state, playerId, maxDepth, policy)` - Runs simulation (default: efficient mutable implementation)
|
||||
- `getActionScore(state, action, playerId)` - Scores an action (default: apply and evaluate)
|
||||
- `shouldStopSearch(state, iterations, startTime)` - Early termination (default: no early stop)
|
||||
|
||||
**Performance Features:**
|
||||
- The default `simulateRandomPlayout` clones the state once and mutates it throughout simulation for efficiency
|
||||
- Games can override `applyActionMutable` to provide even more efficient in-place updates
|
||||
- Games can override `simulateRandomPlayout` for custom optimizations (e.g., using internal engine state)
|
||||
|
||||
## MCTS Algorithm Implementation
|
||||
|
||||
### `AbstractMCTSAI` (abstract/AbstractMCTSAI.hpp)
|
||||
The main MCTS algorithm implementation that works with any game implementing the abstract interfaces.
|
||||
|
||||
**Key Features:**
|
||||
- **Selection**: Uses UCB1 (Upper Confidence Bound) for node selection
|
||||
- **Expansion**: Adds new nodes to the search tree
|
||||
- **Simulation**: Runs random playouts to estimate node values
|
||||
- **Backpropagation**: Updates node statistics with simulation results
|
||||
- **Multithreading**: Supports parallel MCTS with configurable thread count
|
||||
- **Path Compression**: Optimizes move sequences for better performance
|
||||
|
||||
**Configuration Options:**
|
||||
- `explorationConstant` - UCB1 exploration parameter (default: √2)
|
||||
- `maxSimulationDepth` - Maximum depth for random playouts
|
||||
- `maxTreeDepth` - Maximum tree depth to prevent stack overflow
|
||||
- `useMultithreading` - Enable parallel search
|
||||
- `numThreads` - Number of worker threads
|
||||
- `simulationPolicy` - Strategy for action selection during simulation
|
||||
|
||||
### `MCTSNode` (abstract/MCTSNode.hpp)
|
||||
Represents nodes in the MCTS search tree.
|
||||
|
||||
**Core Data:**
|
||||
- `action` - The action that led to this node
|
||||
- `actionIndex` - Index in the original actions array
|
||||
- `gameState` - The game state at this node
|
||||
- `visitCount` - Number of times this node was visited
|
||||
- `totalReward` - Sum of simulation rewards
|
||||
- `averageReward` - Average reward (totalReward / visitCount)
|
||||
- `children` - Child nodes in the search tree
|
||||
- `parent` - Parent node reference
|
||||
|
||||
**Key Methods:**
|
||||
- `CanExpand()` - Checks if node has untried actions
|
||||
- `GetBestChild(explorationConstant)` - UCB1-based child selection
|
||||
- `GetBestFinalChild()` - Most-visited child (for final move selection)
|
||||
- `CalculateUCB1(explorationConstant)` - Computes UCB1 value
|
||||
|
||||
## Simulation Policies
|
||||
|
||||
The framework supports multiple strategies for action selection during random playouts:
|
||||
|
||||
- **RANDOM** - Uniform random selection
|
||||
- **FILTERED_RANDOM** - Random selection from filtered action set
|
||||
- **BEST_IMMEDIATE** - Always choose the highest-scoring immediate action
|
||||
- **WEIGHTED_BEST_IMMEDIATE** - Weighted random selection based on action scores
|
||||
|
||||
## Type Definitions
|
||||
|
||||
### `MCTSTypes` (abstract/MCTSTypes.hpp)
|
||||
- `MCTSPlayerId` - Player identifier type (int)
|
||||
- `MCTSSimulationPolicy` - Enumeration of simulation strategies
|
||||
- `MCTSConfig` - Configuration structure for MCTS parameters
|
||||
|
||||
## Usage Pattern
|
||||
|
||||
To use this framework with your game:
|
||||
|
||||
1. **Implement the abstract interfaces** for your game:
|
||||
```cpp
|
||||
class MyGameAction : public MCTSAction { /* ... */ };
|
||||
class MyGameState : public MCTSGameState { /* ... */ };
|
||||
class MyGameEngine : public MCTSGameEngine { /* ... */ };
|
||||
```
|
||||
|
||||
2. **Create and configure the AI**:
|
||||
```cpp
|
||||
MCTSConfig config;
|
||||
config.explorationConstant = 1.414;
|
||||
config.maxSimulationDepth = 100;
|
||||
AbstractMCTSAI ai(playerId, config);
|
||||
```
|
||||
|
||||
3. **Run the search**:
|
||||
```cpp
|
||||
auto actions = engine.getLegalActions(currentState);
|
||||
auto result = ai.Search(engine, currentState, actions, timeLimit);
|
||||
auto bestAction = actions[result.bestActionIndex];
|
||||
```
|
||||
|
||||
## Testing
|
||||
|
||||
The framework includes comprehensive tests using a Tic-Tac-Toe implementation:
|
||||
- `MockTicTacToe.hpp` - Example implementation of all abstract interfaces
|
||||
- `AbstractMCTSAI_test.cpp` - Unit tests for the core algorithm
|
||||
- `MCTSIntegration_test.cpp` - Integration tests with complete games
|
||||
- `MCTSNode_test.cpp` - Tests for the node data structure
|
||||
|
||||
This demonstrates how to implement the interfaces and validates that the MCTS algorithm works correctly with any turn-based game.
|
||||
@@ -0,0 +1,467 @@
|
||||
//
|
||||
// Abstract MCTS AI implementation
|
||||
//
|
||||
|
||||
#include "AbstractMCTSAI.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
#include <atomic>
|
||||
#include <future>
|
||||
#include <mutex>
|
||||
#include <random>
|
||||
#include <thread>
|
||||
|
||||
namespace shardok::mcts {
|
||||
|
||||
AbstractMCTSAI::AbstractMCTSAI(MCTSPlayerId playerId, MCTSConfig config)
|
||||
: playerId_(playerId),
|
||||
config_(config) {}
|
||||
|
||||
auto AbstractMCTSAI::Search(
|
||||
const MCTSGameEngine& engine,
|
||||
const MCTSGameState& initialState,
|
||||
std::chrono::milliseconds timeLimit) const -> SearchResult {
|
||||
const auto startTime = std::chrono::steady_clock::now();
|
||||
const auto deadline = startTime + timeLimit;
|
||||
|
||||
// Build MCTS tree
|
||||
const auto rootNode = BuildMCTSTree(engine, initialState, deadline);
|
||||
|
||||
SearchResult result;
|
||||
result.searchTime = std::chrono::duration_cast<std::chrono::milliseconds>(
|
||||
std::chrono::steady_clock::now() - startTime);
|
||||
|
||||
if (!rootNode || rootNode->children.empty()) {
|
||||
// Fallback to first action if no tree was built
|
||||
result.bestActionIndex = 0;
|
||||
result.bestScore = 0.0;
|
||||
return result;
|
||||
}
|
||||
|
||||
// Find best child
|
||||
if (const auto* bestChild = rootNode->GetBestFinalChild();
|
||||
bestChild && bestChild->action && bestChild->actionIndex != SIZE_MAX) {
|
||||
// Use actionIndex which is the index into the filtered actions from
|
||||
// engine.getLegalActions()
|
||||
result.bestActionIndex = bestChild->actionIndex;
|
||||
result.bestScore = bestChild->averageReward;
|
||||
result.searchDepth = bestChild->depth;
|
||||
result.nodesEvaluated = rootNode->visitCount;
|
||||
|
||||
// Check if we found a winning move
|
||||
if (bestChild->gameState && bestChild->gameState->isTerminal() &&
|
||||
bestChild->gameState->getWinner() == playerId_) {
|
||||
result.foundWinningMove = true;
|
||||
}
|
||||
|
||||
// Log results
|
||||
LogSearchResults(rootNode.get(), bestChild, result);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
auto AbstractMCTSAI::BuildMCTSTree(
|
||||
const MCTSGameEngine& engine,
|
||||
const MCTSGameState& initialState,
|
||||
const std::chrono::steady_clock::time_point deadline) const -> std::unique_ptr<MCTSNode> {
|
||||
// Create root node
|
||||
auto root = std::make_unique<MCTSNode>(initialState.clone(), playerId_, 0);
|
||||
|
||||
// Get legal actions from engine for the root state
|
||||
const auto rootActions = engine.getLegalActions(initialState);
|
||||
|
||||
// Initialize untried actions from the root actions
|
||||
root->untriedActionIndices.reserve(rootActions.size());
|
||||
for (size_t i = 0; i < rootActions.size(); ++i) { root->untriedActionIndices.push_back(i); }
|
||||
|
||||
std::atomic<int> iterations{0};
|
||||
constexpr int maxIterations = 100000;
|
||||
|
||||
if (config_.useMultithreading && config_.numThreads > 1) {
|
||||
// Multithreaded MCTS
|
||||
std::mutex treeMutex;
|
||||
std::vector<std::future<void>> futures;
|
||||
|
||||
futures.reserve(config_.numThreads);
|
||||
for (int threadId = 0; threadId < config_.numThreads; ++threadId) {
|
||||
futures.push_back(std::async(std::launch::async, [&] {
|
||||
while (std::chrono::steady_clock::now() < deadline &&
|
||||
iterations.load() < maxIterations) {
|
||||
// Selection and Expansion (with lock - tree modification must be serialized)
|
||||
MCTSNode* expanded;
|
||||
{
|
||||
std::lock_guard lock(treeMutex);
|
||||
auto* selected = MCTSSelection(root.get());
|
||||
if (!selected) break;
|
||||
|
||||
// Expansion modifies tree structure - must be inside lock
|
||||
expanded = MCTSExpansion(selected, engine);
|
||||
}
|
||||
|
||||
// Simulation can run in parallel (doesn't modify tree)
|
||||
|
||||
// Backpropagation (with lock - modifies node statistics)
|
||||
{
|
||||
const double reward =
|
||||
MCTSSimulation(engine, *expanded->gameState, playerId_);
|
||||
std::lock_guard lock(treeMutex);
|
||||
MCTSBackpropagation(expanded, reward);
|
||||
iterations.fetch_add(1);
|
||||
}
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
// Wait for all threads to complete
|
||||
for (auto& future : futures) { future.wait(); }
|
||||
} else {
|
||||
// Single-threaded MCTS
|
||||
while (std::chrono::steady_clock::now() < deadline && iterations < maxIterations) {
|
||||
// Selection
|
||||
auto* selected = MCTSSelection(root.get());
|
||||
if (!selected) break;
|
||||
|
||||
// Expansion
|
||||
auto* expanded = MCTSExpansion(selected, engine);
|
||||
|
||||
// Simulation
|
||||
const double reward = MCTSSimulation(engine, *expanded->gameState, playerId_);
|
||||
|
||||
// Backpropagation
|
||||
MCTSBackpropagation(expanded, reward);
|
||||
|
||||
++iterations;
|
||||
|
||||
// Early termination check
|
||||
if (engine.shouldStopSearch(
|
||||
*root->gameState,
|
||||
iterations,
|
||||
std::chrono::steady_clock::now())) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return root;
|
||||
}
|
||||
|
||||
auto AbstractMCTSAI::MCTSSelection(MCTSNode* root) const -> MCTSNode* {
|
||||
MCTSNode* current = root;
|
||||
|
||||
while (!current->isTerminal && current->depth < config_.maxTreeDepth) {
|
||||
if (current->CanExpand()) {
|
||||
return current; // Node has untried actions
|
||||
} else if (!current->children.empty()) {
|
||||
current = current->GetBestChild(config_.explorationConstant);
|
||||
if (!current) break;
|
||||
} else {
|
||||
break; // Leaf node
|
||||
}
|
||||
}
|
||||
|
||||
return current;
|
||||
}
|
||||
|
||||
auto AbstractMCTSAI::MCTSExpansion(MCTSNode* node, const MCTSGameEngine& engine) const
|
||||
-> MCTSNode* {
|
||||
if (node->untriedActionIndices.empty() || node->isTerminal) {
|
||||
return node; // Nothing to expand
|
||||
}
|
||||
|
||||
// Select a random untried action
|
||||
thread_local std::mt19937 gen(std::random_device{}());
|
||||
std::uniform_int_distribution<size_t> dis(0, node->untriedActionIndices.size() - 1);
|
||||
const size_t randomIndex = dis(gen);
|
||||
const size_t actionIndex = node->untriedActionIndices[randomIndex];
|
||||
|
||||
// Remove from untried list
|
||||
node->untriedActionIndices.erase(std::next(
|
||||
node->untriedActionIndices.begin(),
|
||||
static_cast<std::vector<size_t>::difference_type>(randomIndex)));
|
||||
|
||||
if (node->untriedActionIndices.empty()) { node->fullyExpanded = true; }
|
||||
|
||||
// Get legal actions from engine (uses cached engine for performance)
|
||||
const auto nodeActions = engine.getLegalActions(*node->gameState);
|
||||
|
||||
// Create new child node
|
||||
if (actionIndex >= nodeActions.size()) {
|
||||
return node; // Invalid action index
|
||||
}
|
||||
|
||||
const auto& action = nodeActions[actionIndex];
|
||||
auto newState = engine.applyAction(*node->gameState, *action);
|
||||
if (!newState) {
|
||||
return node; // Failed to apply action
|
||||
}
|
||||
|
||||
auto child = std::make_unique<MCTSNode>(
|
||||
action->clone(),
|
||||
std::move(newState),
|
||||
node->gameState->currentPlayerId(),
|
||||
node->depth + 1,
|
||||
actionIndex);
|
||||
|
||||
// Set up child's untried actions if not terminal
|
||||
if (!child->isTerminal) {
|
||||
const auto childActions = engine.getLegalActions(*child->gameState);
|
||||
child->untriedActionIndices.reserve(childActions.size());
|
||||
for (size_t i = 0; i < childActions.size(); ++i) {
|
||||
child->untriedActionIndices.push_back(i);
|
||||
}
|
||||
}
|
||||
|
||||
// Calculate immediate and lookahead scores
|
||||
child->immediateScore = engine.evaluateState(*child->gameState, playerId_);
|
||||
child->lookaheadScore = child->immediateScore;
|
||||
|
||||
// Set parent and add to children
|
||||
child->parent = node;
|
||||
node->children.push_back(std::move(child));
|
||||
|
||||
return node->children.back().get();
|
||||
}
|
||||
|
||||
auto AbstractMCTSAI::MCTSSimulation(
|
||||
const MCTSGameEngine& engine,
|
||||
const MCTSGameState& state,
|
||||
const MCTSPlayerId startingPlayer) const -> double {
|
||||
if (state.isTerminal()) { return state.score(startingPlayer); }
|
||||
|
||||
// Create a mutable copy for simulation
|
||||
auto currentState = state.clone();
|
||||
int depth = 0;
|
||||
|
||||
// Simulate until terminal or max depth
|
||||
while (!currentState->isTerminal() && depth < config_.maxSimulationDepth) {
|
||||
const auto actions = engine.getLegalActions(*currentState);
|
||||
if (actions.empty()) { break; }
|
||||
|
||||
// Select action based on simulation policy
|
||||
const size_t selectedIndex = SelectSimulationAction(engine, *currentState, actions);
|
||||
if (selectedIndex >= actions.size()) { break; }
|
||||
|
||||
// Apply action
|
||||
auto newState = engine.applyAction(*currentState, *actions[selectedIndex]);
|
||||
if (!newState) { break; }
|
||||
|
||||
currentState = std::move(newState);
|
||||
depth++;
|
||||
}
|
||||
|
||||
return currentState->score(startingPlayer);
|
||||
}
|
||||
|
||||
auto AbstractMCTSAI::MCTSBackpropagation(MCTSNode* node, const double reward) -> void {
|
||||
while (node) {
|
||||
node->visitCount++;
|
||||
node->totalReward += reward;
|
||||
node->averageReward = node->totalReward / node->visitCount;
|
||||
|
||||
// Update lookahead score as weighted average
|
||||
if (node->visitCount == 1) {
|
||||
node->lookaheadScore = reward;
|
||||
} else {
|
||||
const double alpha = 1.0 / node->visitCount;
|
||||
node->lookaheadScore = (1.0 - alpha) * node->lookaheadScore + alpha * reward;
|
||||
}
|
||||
|
||||
node = node->parent;
|
||||
}
|
||||
}
|
||||
|
||||
auto AbstractMCTSAI::SelectSimulationAction(
|
||||
const MCTSGameEngine& engine,
|
||||
const MCTSGameState& state,
|
||||
const std::vector<std::unique_ptr<MCTSAction>>& actions) const -> size_t {
|
||||
if (actions.empty()) { return 0; }
|
||||
|
||||
thread_local std::mt19937 gen(std::random_device{}());
|
||||
|
||||
switch (config_.simulationPolicy) {
|
||||
case MCTSSimulationPolicy::RANDOM: {
|
||||
std::uniform_int_distribution<size_t> dis(0, actions.size() - 1);
|
||||
return dis(gen);
|
||||
}
|
||||
|
||||
case MCTSSimulationPolicy::FILTERED_RANDOM: {
|
||||
if (const auto filteredIndices = engine.filterActions(actions, state);
|
||||
!filteredIndices.empty()) {
|
||||
std::uniform_int_distribution<size_t> dis(0, filteredIndices.size() - 1);
|
||||
return filteredIndices[dis(gen)];
|
||||
}
|
||||
// Fall back to random
|
||||
std::uniform_int_distribution<size_t> dis(0, actions.size() - 1);
|
||||
return dis(gen);
|
||||
}
|
||||
|
||||
case MCTSSimulationPolicy::BEST_IMMEDIATE: {
|
||||
double bestScore = -std::numeric_limits<double>::max();
|
||||
size_t bestIndex = 0;
|
||||
|
||||
for (size_t i = 0; i < actions.size(); ++i) {
|
||||
const double score =
|
||||
engine.getActionScore(state, *actions[i], state.currentPlayerId());
|
||||
if (score > bestScore) {
|
||||
bestScore = score;
|
||||
bestIndex = i;
|
||||
}
|
||||
}
|
||||
return bestIndex;
|
||||
}
|
||||
|
||||
case MCTSSimulationPolicy::WEIGHTED_BEST_IMMEDIATE: {
|
||||
// Score all actions and weight by ranking
|
||||
std::vector<std::pair<size_t, double>> scores;
|
||||
scores.reserve(actions.size());
|
||||
|
||||
for (size_t i = 0; i < actions.size(); ++i) {
|
||||
const double score =
|
||||
engine.getActionScore(state, *actions[i], state.currentPlayerId());
|
||||
scores.emplace_back(i, score);
|
||||
}
|
||||
|
||||
// Sort by score
|
||||
std::ranges::sort(scores, [](const auto& a, const auto& b) {
|
||||
return a.second > b.second;
|
||||
});
|
||||
|
||||
// Create weights based on ranking
|
||||
std::vector<double> weights;
|
||||
weights.reserve(scores.size());
|
||||
for (size_t i = 0; i < scores.size(); ++i) {
|
||||
weights.push_back(1.0 / (static_cast<double>(i) + 1.0));
|
||||
}
|
||||
|
||||
// Select based on weights
|
||||
std::discrete_distribution<> dis(weights.begin(), weights.end());
|
||||
return scores[dis(gen)].first;
|
||||
}
|
||||
}
|
||||
|
||||
// Default to random
|
||||
std::uniform_int_distribution<size_t> dis(0, actions.size() - 1);
|
||||
return dis(gen);
|
||||
}
|
||||
|
||||
auto AbstractMCTSAI::FindNodeAtDepthWithHash(
|
||||
const MCTSNode* root,
|
||||
const int maxDepth,
|
||||
const uint64_t targetHash) -> const MCTSNode* {
|
||||
if (!root || root->depth >= maxDepth || root->stateHash == targetHash) { return root; }
|
||||
|
||||
// Breadth-first search for matching hash at specific depth
|
||||
std::vector<const MCTSNode*> currentLevel = {root};
|
||||
for (int d = 0; d < maxDepth && !currentLevel.empty(); ++d) {
|
||||
std::vector<const MCTSNode*> nextLevel;
|
||||
|
||||
for (const auto* node : currentLevel) {
|
||||
for (const auto& child : node->children) {
|
||||
if (child->stateHash == targetHash && child->depth <= maxDepth) {
|
||||
return child.get();
|
||||
}
|
||||
if (child->depth < maxDepth) { nextLevel.push_back(child.get()); }
|
||||
}
|
||||
}
|
||||
|
||||
currentLevel = std::move(nextLevel);
|
||||
}
|
||||
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
auto AbstractMCTSAI::LogSearchResults(
|
||||
const MCTSNode* rootNode,
|
||||
const MCTSNode* bestChild,
|
||||
const SearchResult& result) -> void {
|
||||
// Log selected command
|
||||
const std::string selectedDesc =
|
||||
bestChild->action ? bestChild->action->getDescription() : "Unknown";
|
||||
printf("MCTS: Selected %s (visit:%d, reward:%.2f, lookahead:%.2f) from %zu options\n",
|
||||
selectedDesc.c_str(),
|
||||
bestChild->visitCount,
|
||||
bestChild->averageReward,
|
||||
bestChild->lookaheadScore,
|
||||
rootNode->children.size());
|
||||
|
||||
// Log top 3 actions by visits
|
||||
std::vector<const MCTSNode*> sortedChildren;
|
||||
sortedChildren.reserve(rootNode->children.size());
|
||||
for (const auto& child : rootNode->children) { sortedChildren.push_back(child.get()); }
|
||||
std::ranges::sort(sortedChildren, [](const auto* a, const auto* b) {
|
||||
return a->visitCount > b->visitCount;
|
||||
});
|
||||
|
||||
printf("MCTS: Top actions by visits:\n");
|
||||
for (size_t i = 0; i < std::min(static_cast<size_t>(3), sortedChildren.size()); ++i) {
|
||||
const auto* child = sortedChildren[i];
|
||||
printf(" [%zu] visits:%d immediate:%.2f backprop:%.2f",
|
||||
i,
|
||||
child->visitCount,
|
||||
child->immediateScore,
|
||||
child->averageReward);
|
||||
|
||||
// Show the action's own description
|
||||
if (child->action) { printf(" %s", child->action->getDescription().c_str()); }
|
||||
|
||||
// Show sequence preview
|
||||
if (!child->children.empty()) {
|
||||
const MCTSNode* bestNext = nullptr;
|
||||
int maxVisits = 0;
|
||||
for (const auto& grandchild : child->children) {
|
||||
if (grandchild->visitCount > maxVisits) {
|
||||
maxVisits = grandchild->visitCount;
|
||||
bestNext = grandchild.get();
|
||||
}
|
||||
}
|
||||
if (bestNext && bestNext->action) {
|
||||
printf(" -> %s", bestNext->action->getDescription().c_str());
|
||||
}
|
||||
}
|
||||
printf("\n");
|
||||
}
|
||||
|
||||
printf("MCTS: Tree stats - max depth:%d, total nodes:%d, root visits:%d\n",
|
||||
result.searchDepth,
|
||||
result.nodesEvaluated,
|
||||
rootNode->visitCount);
|
||||
|
||||
// Log best sequence from chosen action (following most-visited path)
|
||||
std::vector<const MCTSNode*> bestSequence;
|
||||
bestSequence.reserve(10);
|
||||
const MCTSNode* current = bestChild;
|
||||
double sequenceScore = bestChild->averageReward;
|
||||
|
||||
while (current && bestSequence.size() < 10) {
|
||||
bestSequence.push_back(current);
|
||||
if (current->children.empty()) break;
|
||||
|
||||
// Find most visited child (standard MCTS principal variation)
|
||||
const MCTSNode* bestChildNode = nullptr;
|
||||
int maxVisits = 0;
|
||||
for (const auto& child : current->children) {
|
||||
if (child->visitCount > maxVisits) {
|
||||
maxVisits = child->visitCount;
|
||||
bestChildNode = child.get();
|
||||
}
|
||||
}
|
||||
current = bestChildNode;
|
||||
if (current) { sequenceScore = current->averageReward; }
|
||||
}
|
||||
|
||||
if (!bestSequence.empty()) {
|
||||
printf("MCTS: Best sequence from chosen action (final: %.2f):\n", sequenceScore);
|
||||
for (size_t i = 0; i < bestSequence.size(); ++i) {
|
||||
const auto* node = bestSequence[i];
|
||||
printf(" %zu.", i + 1);
|
||||
if (node->action) { printf(" %s", node->action->getDescription().c_str()); }
|
||||
printf(" (visits:%d, immediate:%.2f, backprop:%.2f)\n",
|
||||
node->visitCount,
|
||||
node->immediateScore,
|
||||
node->averageReward);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace shardok::mcts
|
||||
@@ -0,0 +1,89 @@
|
||||
//
|
||||
// Abstract MCTS AI implementation - game agnostic
|
||||
//
|
||||
|
||||
#ifndef EAGLE0_ABSTRACT_MCTSAI_HPP
|
||||
#define EAGLE0_ABSTRACT_MCTSAI_HPP
|
||||
|
||||
#include <chrono>
|
||||
#include <memory>
|
||||
#include <vector>
|
||||
|
||||
#include "MCTSAction.hpp"
|
||||
#include "MCTSGameEngine.hpp"
|
||||
#include "MCTSGameState.hpp"
|
||||
#include "MCTSNode.hpp"
|
||||
#include "MCTSTypes.hpp"
|
||||
|
||||
namespace shardok {
|
||||
namespace mcts {
|
||||
|
||||
class AbstractMCTSAI {
|
||||
public:
|
||||
// Search result structure
|
||||
struct SearchResult {
|
||||
size_t bestActionIndex = 0;
|
||||
double bestScore = 0.0;
|
||||
int searchDepth = 0;
|
||||
int nodesEvaluated = 0;
|
||||
std::chrono::milliseconds searchTime{0};
|
||||
bool foundWinningMove = false;
|
||||
};
|
||||
|
||||
explicit AbstractMCTSAI(MCTSPlayerId playerId, MCTSConfig config = MCTSConfig{});
|
||||
|
||||
// Main search interface
|
||||
[[nodiscard]] auto Search(
|
||||
const MCTSGameEngine& engine,
|
||||
const MCTSGameState& initialState,
|
||||
std::chrono::milliseconds timeLimit) const -> SearchResult;
|
||||
|
||||
// Configuration
|
||||
[[nodiscard]] auto GetConfig() const -> const MCTSConfig& { return config_; }
|
||||
void SetConfig(const MCTSConfig& newConfig) { config_ = newConfig; }
|
||||
|
||||
[[nodiscard]] auto FindNodeAtDepthWithHash(
|
||||
const MCTSNode* root,
|
||||
int maxDepth,
|
||||
uint64_t targetHash) -> const MCTSNode*;
|
||||
|
||||
private:
|
||||
MCTSPlayerId playerId_;
|
||||
MCTSConfig config_;
|
||||
|
||||
// Core MCTS algorithm
|
||||
[[nodiscard]] auto BuildMCTSTree(
|
||||
const MCTSGameEngine& engine,
|
||||
const MCTSGameState& initialState,
|
||||
std::chrono::steady_clock::time_point deadline) const -> std::unique_ptr<MCTSNode>;
|
||||
|
||||
// MCTS phases
|
||||
[[nodiscard]] auto MCTSSelection(MCTSNode* root) const -> MCTSNode*;
|
||||
|
||||
[[nodiscard]] auto MCTSExpansion(MCTSNode* node, const MCTSGameEngine& engine) const
|
||||
-> MCTSNode*;
|
||||
|
||||
[[nodiscard]] auto MCTSSimulation(
|
||||
const MCTSGameEngine& engine,
|
||||
const MCTSGameState& state,
|
||||
MCTSPlayerId startingPlayer) const -> double;
|
||||
|
||||
static auto MCTSBackpropagation(MCTSNode* node, double reward) -> void;
|
||||
|
||||
// Helper functions
|
||||
[[nodiscard]] auto SelectSimulationAction(
|
||||
const MCTSGameEngine& engine,
|
||||
const MCTSGameState& state,
|
||||
const std::vector<std::unique_ptr<MCTSAction>>& actions) const -> size_t;
|
||||
|
||||
// Logging
|
||||
static auto LogSearchResults(
|
||||
const MCTSNode* rootNode,
|
||||
const MCTSNode* bestChild,
|
||||
const SearchResult& result) -> void;
|
||||
};
|
||||
|
||||
} // namespace mcts
|
||||
} // namespace shardok
|
||||
|
||||
#endif // EAGLE0_ABSTRACT_MCTSAI_HPP
|
||||
@@ -0,0 +1,93 @@
|
||||
load("//tools:copts.bzl", "COPTS")
|
||||
|
||||
cc_library(
|
||||
name = "mcts_types",
|
||||
hdrs = ["MCTSTypes.hpp"],
|
||||
copts = COPTS,
|
||||
visibility = [
|
||||
"//src/main/cpp/net/eagle0/shardok/ai/mcts:__subpackages__",
|
||||
"//src/test/cpp/net/eagle0/common/mcts:__subpackages__",
|
||||
"//src/test/cpp/net/eagle0/shardok/ai/mcts:__subpackages__",
|
||||
],
|
||||
)
|
||||
|
||||
cc_library(
|
||||
name = "mcts_action",
|
||||
hdrs = ["MCTSAction.hpp"],
|
||||
copts = COPTS,
|
||||
visibility = [
|
||||
"//src/main/cpp/net/eagle0/shardok/ai/mcts:__subpackages__",
|
||||
"//src/test/cpp/net/eagle0/common/mcts:__subpackages__",
|
||||
"//src/test/cpp/net/eagle0/shardok/ai/mcts:__subpackages__",
|
||||
],
|
||||
)
|
||||
|
||||
cc_library(
|
||||
name = "mcts_game_state",
|
||||
hdrs = ["MCTSGameState.hpp"],
|
||||
copts = COPTS,
|
||||
visibility = [
|
||||
"//src/main/cpp/net/eagle0/shardok/ai/mcts:__subpackages__",
|
||||
"//src/test/cpp/net/eagle0/common/mcts:__subpackages__",
|
||||
"//src/test/cpp/net/eagle0/shardok/ai/mcts:__subpackages__",
|
||||
],
|
||||
deps = [
|
||||
":mcts_types",
|
||||
],
|
||||
)
|
||||
|
||||
cc_library(
|
||||
name = "mcts_game_engine",
|
||||
srcs = ["MCTSGameEngine.cpp"],
|
||||
hdrs = ["MCTSGameEngine.hpp"],
|
||||
copts = COPTS,
|
||||
visibility = [
|
||||
"//src/main/cpp/net/eagle0/shardok/ai/mcts:__subpackages__",
|
||||
"//src/test/cpp/net/eagle0/common/mcts:__subpackages__",
|
||||
"//src/test/cpp/net/eagle0/shardok/ai/mcts:__subpackages__",
|
||||
],
|
||||
deps = [
|
||||
":mcts_action",
|
||||
":mcts_game_state",
|
||||
":mcts_types",
|
||||
],
|
||||
)
|
||||
|
||||
cc_library(
|
||||
name = "mcts_node",
|
||||
hdrs = ["MCTSNode.hpp"],
|
||||
copts = COPTS,
|
||||
visibility = [
|
||||
"//src/main/cpp/net/eagle0/shardok/ai:__pkg__",
|
||||
"//src/main/cpp/net/eagle0/shardok/ai/mcts:__subpackages__",
|
||||
"//src/test/cpp/net/eagle0/common/mcts:__subpackages__",
|
||||
"//src/test/cpp/net/eagle0/shardok/ai/mcts:__subpackages__",
|
||||
],
|
||||
deps = [
|
||||
":mcts_action",
|
||||
":mcts_game_state",
|
||||
":mcts_types",
|
||||
],
|
||||
)
|
||||
|
||||
cc_library(
|
||||
name = "abstract_mcts_ai",
|
||||
srcs = ["AbstractMCTSAI.cpp"],
|
||||
hdrs = ["AbstractMCTSAI.hpp"],
|
||||
copts = COPTS,
|
||||
visibility = [
|
||||
"//src/main/cpp/net/eagle0/shardok/ai/mcts:__subpackages__",
|
||||
"//src/test/cpp/net/eagle0/common/mcts:__subpackages__",
|
||||
"//src/test/cpp/net/eagle0/shardok/ai/mcts:__subpackages__",
|
||||
],
|
||||
deps = [
|
||||
":mcts_action",
|
||||
":mcts_game_engine",
|
||||
":mcts_game_state",
|
||||
":mcts_node",
|
||||
":mcts_types",
|
||||
],
|
||||
)
|
||||
|
||||
# Individual targets are exposed above - no need for a catch-all target
|
||||
# Each component should be imported explicitly by its consumers
|
||||
@@ -0,0 +1,35 @@
|
||||
//
|
||||
// Abstract action interface for MCTS
|
||||
//
|
||||
|
||||
#ifndef EAGLE0_MCTS_ACTION_HPP
|
||||
#define EAGLE0_MCTS_ACTION_HPP
|
||||
|
||||
#include <memory>
|
||||
#include <string>
|
||||
|
||||
namespace shardok {
|
||||
namespace mcts {
|
||||
|
||||
// Abstract interface for game actions
|
||||
class MCTSAction {
|
||||
public:
|
||||
virtual ~MCTSAction() = default;
|
||||
|
||||
// Get a unique index for this action (used for command indexing)
|
||||
[[nodiscard]] virtual size_t getIndex() const = 0;
|
||||
|
||||
// Get a human-readable description for debugging/logging
|
||||
[[nodiscard]] virtual std::string getDescription() const = 0;
|
||||
|
||||
// Create a deep copy of this action
|
||||
[[nodiscard]] virtual std::unique_ptr<MCTSAction> clone() const = 0;
|
||||
|
||||
// Check if two actions are equivalent
|
||||
[[nodiscard]] virtual bool equals(const MCTSAction& other) const = 0;
|
||||
};
|
||||
|
||||
} // namespace mcts
|
||||
} // namespace shardok
|
||||
|
||||
#endif // EAGLE0_MCTS_ACTION_HPP
|
||||
@@ -0,0 +1,114 @@
|
||||
//
|
||||
// Default implementations for MCTSGameEngine
|
||||
//
|
||||
|
||||
#include "MCTSGameEngine.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
#include <limits>
|
||||
#include <random>
|
||||
#include <vector>
|
||||
|
||||
namespace shardok {
|
||||
namespace mcts {
|
||||
|
||||
double MCTSGameEngine::simulateRandomPlayout(
|
||||
const MCTSGameState& state,
|
||||
MCTSPlayerId playerId,
|
||||
int maxDepth,
|
||||
MCTSSimulationPolicy policy) const {
|
||||
// Clone state once and mutate it throughout simulation for efficiency
|
||||
auto currentState = state.clone();
|
||||
int depth = 0;
|
||||
|
||||
// Use thread-local random generator for thread safety
|
||||
static thread_local std::mt19937 gen(std::random_device{}());
|
||||
|
||||
// Simulate until terminal or max depth
|
||||
while (!currentState->isTerminal() && depth < maxDepth) {
|
||||
auto actions = getLegalActions(*currentState);
|
||||
if (actions.empty()) { break; }
|
||||
|
||||
size_t selectedIndex = 0;
|
||||
|
||||
// Select action based on policy
|
||||
switch (policy) {
|
||||
case MCTSSimulationPolicy::RANDOM: {
|
||||
std::uniform_int_distribution<> dis(0, actions.size() - 1);
|
||||
selectedIndex = dis(gen);
|
||||
break;
|
||||
}
|
||||
|
||||
case MCTSSimulationPolicy::FILTERED_RANDOM: {
|
||||
auto filteredIndices = filterActions(actions, *currentState);
|
||||
if (!filteredIndices.empty()) {
|
||||
std::uniform_int_distribution<> dis(0, filteredIndices.size() - 1);
|
||||
selectedIndex = filteredIndices[dis(gen)];
|
||||
} else {
|
||||
// Fall back to random if no actions pass filter
|
||||
std::uniform_int_distribution<> dis(0, actions.size() - 1);
|
||||
selectedIndex = dis(gen);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case MCTSSimulationPolicy::BEST_IMMEDIATE: {
|
||||
double bestScore = -std::numeric_limits<double>::infinity();
|
||||
for (size_t i = 0; i < actions.size(); ++i) {
|
||||
double score = getActionScore(
|
||||
*currentState,
|
||||
*actions[i],
|
||||
currentState->currentPlayerId());
|
||||
if (score > bestScore) {
|
||||
bestScore = score;
|
||||
selectedIndex = i;
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case MCTSSimulationPolicy::WEIGHTED_BEST_IMMEDIATE: {
|
||||
// Score all actions and weight by ranking
|
||||
std::vector<std::pair<size_t, double>> scores;
|
||||
scores.reserve(actions.size());
|
||||
|
||||
for (size_t i = 0; i < actions.size(); ++i) {
|
||||
double score = getActionScore(
|
||||
*currentState,
|
||||
*actions[i],
|
||||
currentState->currentPlayerId());
|
||||
scores.emplace_back(i, score);
|
||||
}
|
||||
|
||||
// Sort by score (descending)
|
||||
std::sort(scores.begin(), scores.end(), [](const auto& a, const auto& b) {
|
||||
return a.second > b.second;
|
||||
});
|
||||
|
||||
// Create weights based on ranking (1/rank)
|
||||
std::vector<double> weights;
|
||||
weights.reserve(scores.size());
|
||||
for (size_t i = 0; i < scores.size(); ++i) { weights.push_back(1.0 / (i + 1.0)); }
|
||||
|
||||
// Select based on weights
|
||||
std::discrete_distribution<> dis(weights.begin(), weights.end());
|
||||
selectedIndex = scores[dis(gen)].first;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Apply selected action using mutable version for efficiency
|
||||
applyActionMutable(currentState, *actions[selectedIndex]);
|
||||
if (!currentState) {
|
||||
break; // Failed to apply action
|
||||
}
|
||||
|
||||
depth++;
|
||||
}
|
||||
|
||||
// Return evaluation from original player's perspective
|
||||
return evaluateState(*currentState, playerId);
|
||||
}
|
||||
|
||||
} // namespace mcts
|
||||
} // namespace shardok
|
||||
@@ -0,0 +1,104 @@
|
||||
//
|
||||
// Abstract game engine interface for MCTS
|
||||
//
|
||||
|
||||
#ifndef EAGLE0_MCTS_GAME_ENGINE_HPP
|
||||
#define EAGLE0_MCTS_GAME_ENGINE_HPP
|
||||
|
||||
#include <chrono>
|
||||
#include <memory>
|
||||
#include <vector>
|
||||
|
||||
#include "MCTSAction.hpp"
|
||||
#include "MCTSGameState.hpp"
|
||||
#include "MCTSTypes.hpp"
|
||||
|
||||
namespace shardok {
|
||||
namespace mcts {
|
||||
|
||||
// Abstract interface for game engines
|
||||
class MCTSGameEngine {
|
||||
public:
|
||||
virtual ~MCTSGameEngine() = default;
|
||||
|
||||
// Apply an action to a state and return the resulting state
|
||||
[[nodiscard]] virtual std::unique_ptr<MCTSGameState> applyAction(
|
||||
const MCTSGameState& state,
|
||||
const MCTSAction& action) const = 0;
|
||||
|
||||
// Apply an action to a mutable state in-place (for efficient simulation)
|
||||
// Default: clone, apply, and move the result back
|
||||
// Override this for better performance
|
||||
virtual void applyActionMutable(std::unique_ptr<MCTSGameState>& state, const MCTSAction& action)
|
||||
const {
|
||||
state = applyAction(*state, action);
|
||||
}
|
||||
|
||||
// Get all legal actions for the current state
|
||||
[[nodiscard]] virtual std::vector<std::unique_ptr<MCTSAction>> getLegalActions(
|
||||
const MCTSGameState& state) const = 0;
|
||||
|
||||
// Check if a state is terminal
|
||||
[[nodiscard]] virtual bool isTerminal(const MCTSGameState& state) const = 0;
|
||||
|
||||
// Evaluate a state from the perspective of a player
|
||||
[[nodiscard]] virtual double evaluateState(const MCTSGameState& state, MCTSPlayerId playerId)
|
||||
const = 0;
|
||||
|
||||
// Filter actions based on game-specific heuristics
|
||||
// Returns indices of actions to keep
|
||||
// Default: no filtering (return all indices)
|
||||
[[nodiscard]] virtual std::vector<size_t> filterActions(
|
||||
const std::vector<std::unique_ptr<MCTSAction>>& actions,
|
||||
const MCTSGameState& /*state*/) const {
|
||||
std::vector<size_t> indices;
|
||||
indices.reserve(actions.size());
|
||||
for (size_t i = 0; i < actions.size(); ++i) { indices.push_back(i); }
|
||||
return indices;
|
||||
}
|
||||
|
||||
// Simulate a random playout from the given state
|
||||
// Default implementation uses policy to select actions
|
||||
[[nodiscard]] virtual double simulateRandomPlayout(
|
||||
const MCTSGameState& state,
|
||||
MCTSPlayerId playerId,
|
||||
int maxDepth,
|
||||
MCTSSimulationPolicy policy) const;
|
||||
|
||||
// Get the immediate score of applying an action
|
||||
// Default: apply the action and evaluate the resulting state
|
||||
[[nodiscard]] virtual double getActionScore(
|
||||
const MCTSGameState& state,
|
||||
const MCTSAction& action,
|
||||
MCTSPlayerId playerId) const {
|
||||
auto newState = applyAction(state, action);
|
||||
if (!newState) { return 0.0; }
|
||||
return evaluateState(*newState, playerId);
|
||||
}
|
||||
|
||||
// Check if we should stop searching (e.g., time limit, found winning move)
|
||||
[[nodiscard]] virtual bool shouldStopSearch(
|
||||
const MCTSGameState& /*state*/,
|
||||
int /*iterations*/,
|
||||
std::chrono::steady_clock::time_point /*startTime*/) const {
|
||||
// Default: no early stopping
|
||||
return false;
|
||||
}
|
||||
|
||||
// Map a filtered action index back to the original unfiltered index
|
||||
// This is needed when getLegalActions() applies filtering - the returned actions
|
||||
// may be a subset of all available actions, and this maps back to the original index.
|
||||
// Default implementation: no filtering, so filtered index = original index
|
||||
[[nodiscard]] virtual size_t mapFilteredIndexToOriginal(
|
||||
size_t filteredIndex,
|
||||
const MCTSGameState& state) const {
|
||||
// Default: no filtering, index stays the same
|
||||
(void)state; // Suppress unused parameter warning
|
||||
return filteredIndex;
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace mcts
|
||||
} // namespace shardok
|
||||
|
||||
#endif // EAGLE0_MCTS_GAME_ENGINE_HPP
|
||||
@@ -0,0 +1,50 @@
|
||||
//
|
||||
// Abstract game state interface for MCTS
|
||||
//
|
||||
|
||||
#ifndef EAGLE0_MCTS_GAME_STATE_HPP
|
||||
#define EAGLE0_MCTS_GAME_STATE_HPP
|
||||
|
||||
#include <cstdint>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
|
||||
#include "MCTSTypes.hpp"
|
||||
|
||||
namespace shardok {
|
||||
namespace mcts {
|
||||
|
||||
// Abstract interface for game states
|
||||
class MCTSGameState {
|
||||
public:
|
||||
virtual ~MCTSGameState() = default;
|
||||
|
||||
// Compute hash for transposition table
|
||||
[[nodiscard]] virtual uint64_t hash() const = 0;
|
||||
|
||||
// Evaluate the state from the perspective of the given player
|
||||
[[nodiscard]] virtual double score(MCTSPlayerId playerId) const = 0;
|
||||
|
||||
// Get the player whose turn it is
|
||||
[[nodiscard]] virtual MCTSPlayerId currentPlayerId() const = 0;
|
||||
|
||||
// Check if the game has ended
|
||||
[[nodiscard]] virtual bool isTerminal() const = 0;
|
||||
|
||||
// Create a deep copy of the state
|
||||
[[nodiscard]] virtual std::unique_ptr<MCTSGameState> clone() const = 0;
|
||||
|
||||
// Check if two states are equivalent
|
||||
[[nodiscard]] virtual bool equals(const MCTSGameState& other) const = 0;
|
||||
|
||||
// Get winner if terminal, or -1 if not terminal or draw
|
||||
[[nodiscard]] virtual MCTSPlayerId getWinner() const = 0;
|
||||
|
||||
// Optional: Get a string representation for debugging
|
||||
[[nodiscard]] virtual std::string toString() const { return "MCTSGameState"; }
|
||||
};
|
||||
|
||||
} // namespace mcts
|
||||
} // namespace shardok
|
||||
|
||||
#endif // EAGLE0_MCTS_GAME_STATE_HPP
|
||||
+53
-80
@@ -1,96 +1,94 @@
|
||||
//
|
||||
// Internal MCTS Node structure for Shardok AI
|
||||
// This is an implementation detail and should not be used by external code
|
||||
// Abstract MCTS Node structure for game-agnostic implementation
|
||||
//
|
||||
|
||||
#ifndef EAGLE0_INTERNAL_MCTSNODE_HPP
|
||||
#define EAGLE0_INTERNAL_MCTSNODE_HPP
|
||||
#ifndef EAGLE0_ABSTRACT_MCTSNODE_HPP
|
||||
#define EAGLE0_ABSTRACT_MCTSNODE_HPP
|
||||
|
||||
#include <cmath>
|
||||
#include <cstdio>
|
||||
#include <limits>
|
||||
#include <memory>
|
||||
#include <vector>
|
||||
|
||||
#include "src/main/cpp/net/eagle0/shardok/library/GameStateW.hpp"
|
||||
#include "src/main/cpp/net/eagle0/shardok/library/ShardokCTypes.h"
|
||||
|
||||
// Suppress the protobuf deprecation warning temporarily
|
||||
#pragma GCC diagnostic push
|
||||
#pragma GCC diagnostic ignored "-Wdeprecated-redundant-constexpr-static-def"
|
||||
#include "src/main/protobuf/net/eagle0/shardok/common/command_type.pb.h"
|
||||
#pragma GCC diagnostic pop
|
||||
#include "MCTSAction.hpp"
|
||||
#include "MCTSGameState.hpp"
|
||||
#include "MCTSTypes.hpp"
|
||||
|
||||
namespace shardok {
|
||||
namespace internal {
|
||||
namespace mcts {
|
||||
|
||||
// Import CommandType for use within the internal namespace
|
||||
using CommandType = net::eagle0::shardok::common::CommandType;
|
||||
|
||||
// MCTS Node structure
|
||||
// Abstract MCTS Node structure
|
||||
struct MCTSNode {
|
||||
// Command information
|
||||
size_t commandIndex;
|
||||
CommandType commandType;
|
||||
int actorUnitId = -1; // Unit performing the command (-1 if not applicable)
|
||||
int targetRow = -1; // Target coordinate row (-1 if not applicable)
|
||||
int targetCol = -1; // Target coordinate column (-1 if not applicable)
|
||||
// Action information
|
||||
std::unique_ptr<MCTSAction> action; // The action that led to this node (null for root)
|
||||
size_t actionIndex = SIZE_MAX; // Index in the original actions array (SIZE_MAX for root)
|
||||
|
||||
// Score information
|
||||
double immediateScore;
|
||||
double lookaheadScore;
|
||||
double immediateScore = 0.0;
|
||||
double lookaheadScore = 0.0;
|
||||
|
||||
// Game state after this command
|
||||
GameStateW resultingGameState;
|
||||
// Game state after this action
|
||||
std::unique_ptr<MCTSGameState> gameState;
|
||||
|
||||
// MCTS statistics
|
||||
int visitCount = 0;
|
||||
double totalReward = 0.0;
|
||||
double averageReward = 0.0;
|
||||
double ucb1Value = 0.0;
|
||||
mutable double ucb1Value = 0.0;
|
||||
|
||||
// Tree structure
|
||||
std::vector<std::unique_ptr<MCTSNode>> children;
|
||||
std::vector<size_t> untriedCommands;
|
||||
std::vector<size_t> untriedActionIndices;
|
||||
bool fullyExpanded = false;
|
||||
MCTSNode* parent = nullptr;
|
||||
|
||||
// Game context
|
||||
PlayerId playerId;
|
||||
MCTSPlayerId playerId;
|
||||
int depth = 0;
|
||||
bool isDefender = false;
|
||||
bool isTerminal = false;
|
||||
|
||||
// Transposition detection
|
||||
uint64_t stateHash = 0;
|
||||
bool isRedundant = false; // True if this node represents a duplicate state
|
||||
|
||||
MCTSNode(
|
||||
const size_t cmdIndex,
|
||||
const CommandType cmdType,
|
||||
const PlayerId pid,
|
||||
const int d,
|
||||
const bool defender)
|
||||
: commandIndex(cmdIndex),
|
||||
commandType(cmdType),
|
||||
immediateScore(0.0),
|
||||
lookaheadScore(0.0),
|
||||
// Constructor for root node
|
||||
MCTSNode(std::unique_ptr<MCTSGameState> state, MCTSPlayerId pid, int d)
|
||||
: gameState(std::move(state)),
|
||||
playerId(pid),
|
||||
depth(d),
|
||||
isDefender(defender) {}
|
||||
depth(d) {
|
||||
if (gameState) {
|
||||
stateHash = gameState->hash();
|
||||
isTerminal = gameState->isTerminal();
|
||||
}
|
||||
}
|
||||
|
||||
// Constructor for child node
|
||||
MCTSNode(
|
||||
std::unique_ptr<MCTSAction> act,
|
||||
std::unique_ptr<MCTSGameState> state,
|
||||
MCTSPlayerId pid,
|
||||
int d,
|
||||
size_t actIdx = SIZE_MAX)
|
||||
: action(std::move(act)),
|
||||
actionIndex(actIdx),
|
||||
gameState(std::move(state)),
|
||||
playerId(pid),
|
||||
depth(d) {
|
||||
if (gameState) {
|
||||
stateHash = gameState->hash();
|
||||
isTerminal = gameState->isTerminal();
|
||||
}
|
||||
}
|
||||
|
||||
// Iterative destructor to avoid stack overflow with deep trees
|
||||
~MCTSNode() {
|
||||
// Use iterative approach to destroy children
|
||||
std::vector<std::unique_ptr<MCTSNode>> nodesToDestroy;
|
||||
nodesToDestroy.swap(children);
|
||||
|
||||
while (!nodesToDestroy.empty()) {
|
||||
// Take ownership of all children from the current batch
|
||||
std::vector<std::unique_ptr<MCTSNode>> currentBatch;
|
||||
currentBatch.swap(nodesToDestroy);
|
||||
|
||||
// Collect grandchildren for next iteration
|
||||
for (const auto& node : currentBatch) {
|
||||
if (node && !node->children.empty()) {
|
||||
for (auto& child : node->children) {
|
||||
@@ -99,12 +97,11 @@ struct MCTSNode {
|
||||
node->children.clear();
|
||||
}
|
||||
}
|
||||
// currentBatch goes out of scope here, destroying nodes with no children
|
||||
}
|
||||
}
|
||||
|
||||
// Calculate UCB1 value for this node
|
||||
void CalculateUCB1(const double explorationConstant) {
|
||||
void CalculateUCB1(const double explorationConstant) const {
|
||||
if (visitCount == 0) {
|
||||
ucb1Value = std::numeric_limits<double>::max();
|
||||
} else if (parent && parent->visitCount > 0) {
|
||||
@@ -116,7 +113,7 @@ struct MCTSNode {
|
||||
}
|
||||
|
||||
// Check if this node can be expanded
|
||||
[[nodiscard]] bool CanExpand() const { return !fullyExpanded && !untriedCommands.empty(); }
|
||||
[[nodiscard]] bool CanExpand() const { return !fullyExpanded && !untriedActionIndices.empty(); }
|
||||
|
||||
// Get best child based on UCB1
|
||||
[[nodiscard]] MCTSNode* GetBestChild(const double explorationConstant) const {
|
||||
@@ -125,9 +122,6 @@ struct MCTSNode {
|
||||
MCTSNode* bestChild = nullptr;
|
||||
double bestValue = -std::numeric_limits<double>::max();
|
||||
|
||||
static int selectionCallCount = 0;
|
||||
const bool shouldDebug = selectionCallCount < 5;
|
||||
|
||||
for (auto& child : children) {
|
||||
// Skip redundant nodes
|
||||
if (child->isRedundant) continue;
|
||||
@@ -138,44 +132,24 @@ struct MCTSNode {
|
||||
bestValue = child->ucb1Value;
|
||||
bestChild = child.get();
|
||||
}
|
||||
|
||||
if (shouldDebug && child->visitCount > 0) {
|
||||
printf("UCB1 Debug: cmd:%zu visits:%d reward:%.2f ucb1:%.2f%s\n",
|
||||
child->commandIndex,
|
||||
child->visitCount,
|
||||
child->averageReward,
|
||||
child->ucb1Value,
|
||||
child->isRedundant ? " [REDUNDANT]" : "");
|
||||
}
|
||||
}
|
||||
|
||||
if (shouldDebug) {
|
||||
if (bestChild) {
|
||||
printf("UCB1 Selected: cmd:%zu ucb1:%.2f\n",
|
||||
bestChild->commandIndex,
|
||||
bestChild->ucb1Value);
|
||||
} else {
|
||||
printf("UCB1 Selected: nullptr (all children redundant)\n");
|
||||
}
|
||||
selectionCallCount++;
|
||||
}
|
||||
return bestChild;
|
||||
}
|
||||
|
||||
// Get best child based on average reward (for final selection)
|
||||
// Get best child based on visit count (for final selection)
|
||||
[[nodiscard]] MCTSNode* GetBestFinalChild() const {
|
||||
if (children.empty()) return nullptr;
|
||||
|
||||
MCTSNode* bestChild = nullptr;
|
||||
double bestScore = -std::numeric_limits<double>::max();
|
||||
int bestVisits = 0;
|
||||
double bestScore = -std::numeric_limits<double>::max();
|
||||
|
||||
for (const auto& child : children) {
|
||||
// Skip redundant nodes
|
||||
if (child->isRedundant) continue;
|
||||
|
||||
// For final selection, prefer most-visited node (robust child selection)
|
||||
// Only consider nodes that have been visited
|
||||
// Prefer most-visited node (robust child selection)
|
||||
if (child->visitCount > bestVisits) {
|
||||
bestVisits = child->visitCount;
|
||||
bestScore = child->averageReward;
|
||||
@@ -187,10 +161,9 @@ struct MCTSNode {
|
||||
}
|
||||
}
|
||||
|
||||
// If no child was visited (shouldn't happen), fall back to lookahead score
|
||||
// If no child was visited, fall back to lookahead score
|
||||
if (!bestChild && !children.empty()) {
|
||||
for (const auto& child : children) {
|
||||
// Skip redundant nodes
|
||||
if (child->isRedundant) continue;
|
||||
|
||||
if (child->lookaheadScore > bestScore) {
|
||||
@@ -204,7 +177,7 @@ struct MCTSNode {
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace internal
|
||||
} // namespace mcts
|
||||
} // namespace shardok
|
||||
|
||||
#endif // EAGLE0_INTERNAL_MCTSNODE_HPP
|
||||
#endif // EAGLE0_ABSTRACT_MCTSNODE_HPP
|
||||
@@ -0,0 +1,35 @@
|
||||
//
|
||||
// Core types for abstract MCTS implementation
|
||||
//
|
||||
|
||||
#ifndef EAGLE0_MCTS_TYPES_HPP
|
||||
#define EAGLE0_MCTS_TYPES_HPP
|
||||
|
||||
namespace shardok {
|
||||
namespace mcts {
|
||||
|
||||
// Abstract player identifier type
|
||||
using MCTSPlayerId = int;
|
||||
|
||||
// Simulation policy for MCTS rollouts
|
||||
enum class MCTSSimulationPolicy {
|
||||
RANDOM, // Pure random selection
|
||||
FILTERED_RANDOM, // Random from filtered actions
|
||||
BEST_IMMEDIATE, // Choose best immediate score
|
||||
WEIGHTED_BEST_IMMEDIATE // Random weighted by score ranking
|
||||
};
|
||||
|
||||
// Configuration for MCTS algorithm
|
||||
struct MCTSConfig {
|
||||
double explorationConstant = 1.414; // UCB1 constant (sqrt(2) by default)
|
||||
int maxSimulationDepth = 1000; // Maximum depth for rollout
|
||||
int maxTreeDepth = 2000; // Maximum tree depth to prevent stack overflow
|
||||
bool useMultithreading = true; // Enable parallel MCTS
|
||||
int numThreads = 16; // Number of threads for parallel MCTS
|
||||
MCTSSimulationPolicy simulationPolicy = MCTSSimulationPolicy::BEST_IMMEDIATE;
|
||||
};
|
||||
|
||||
} // namespace mcts
|
||||
} // namespace shardok
|
||||
|
||||
#endif // EAGLE0_MCTS_TYPES_HPP
|
||||
@@ -9,7 +9,6 @@
|
||||
#include <ranges>
|
||||
#include <unordered_map>
|
||||
|
||||
#include "src/main/cpp/net/eagle0/shardok/library/settings/GameSettings.hpp"
|
||||
#include "src/main/cpp/net/eagle0/shardok/library/util/HexMapUtils.hpp"
|
||||
|
||||
namespace shardok {
|
||||
@@ -76,30 +75,28 @@ auto MinDistanceIncludingBraving(
|
||||
auto EffectiveDistance(
|
||||
const Unit* unit,
|
||||
const HexMap* map,
|
||||
const MapId& mapId,
|
||||
const APDCache& apdCache,
|
||||
const AttackLocations& attackLocations,
|
||||
const SettingsGetter& settings,
|
||||
const int braveWaterCost) -> DIST_T {
|
||||
const APDCache& apdCache,
|
||||
const BattalionTypeGetter& battalionTypeGetter,
|
||||
ActionPoints braveWaterCost) -> DIST_T {
|
||||
return EffectiveDistance(
|
||||
unit,
|
||||
map,
|
||||
mapId,
|
||||
apdCache,
|
||||
attackLocations.LocationsWithEnemyInRange(unit),
|
||||
settings,
|
||||
apdCache,
|
||||
battalionTypeGetter,
|
||||
braveWaterCost);
|
||||
}
|
||||
|
||||
auto EffectiveDistance(
|
||||
const Unit* unit,
|
||||
const HexMap* map,
|
||||
const MapId& mapId,
|
||||
const APDCache& apdCache,
|
||||
const CoordsSet& locations,
|
||||
const SettingsGetter& settings,
|
||||
const int braveWaterCost) -> DIST_T {
|
||||
const auto& battType = settings.GetBattalionType(unit->battalion().type());
|
||||
const APDCache& apdCache,
|
||||
const BattalionTypeGetter& battalionTypeGetter,
|
||||
ActionPoints braveWaterCost) -> DIST_T {
|
||||
const auto mapId = ActionPointDistancesCache::GetMapId(map);
|
||||
const auto& battType = battalionTypeGetter(unit->battalion().type());
|
||||
const auto* notBravingApd = apdCache->GetRaw(map, mapId, battType, false);
|
||||
const ActionPointDistances* bravingApd = nullptr;
|
||||
if (battType->allowsBraveWater) {
|
||||
@@ -132,12 +129,12 @@ auto GenerateTargetPriorities(
|
||||
const vector<const Unit*>& remainingUnits,
|
||||
const APDCache& apdCache,
|
||||
const ALCache& alCache,
|
||||
const MapId& mapId,
|
||||
const SettingsGetter& settings,
|
||||
const BattalionTypeGetter& battalionTypeGetter,
|
||||
ActionPoints braveWaterCost,
|
||||
const bool isLateGame) -> vector<TargetPriorityList> {
|
||||
auto cc = map->column_count();
|
||||
|
||||
const auto braveWaterCost = settings.Backing().brave_water_action_point_cost();
|
||||
const auto mapId = ActionPointDistancesCache::GetMapId(map);
|
||||
|
||||
vector<TargetPriorityList> allTargetsUnitsAndDistances{};
|
||||
allTargetsUnitsAndDistances.reserve(remainingUnits.size());
|
||||
@@ -161,7 +158,7 @@ auto GenerateTargetPriorities(
|
||||
vector<TargetAndDistance> targetsWithDistance;
|
||||
|
||||
// Get APDs directly from cache (now with built-in thread-local optimization)
|
||||
const auto& battType = settings.GetBattalionType(unit->battalion().type());
|
||||
const auto& battType = battalionTypeGetter(unit->battalion().type());
|
||||
const auto* notBravingApd = apdCache->GetRaw(map, mapId, battType, false);
|
||||
const ActionPointDistances* bravingApd = nullptr;
|
||||
if (battType->allowsBraveWater) {
|
||||
|
||||
@@ -5,12 +5,12 @@
|
||||
#ifndef EAGLE0_AIATTACKGROUPS_HPP
|
||||
#define EAGLE0_AIATTACKGROUPS_HPP
|
||||
|
||||
#include <functional>
|
||||
#include <vector>
|
||||
|
||||
#include "src/main/cpp/net/eagle0/shardok/ai/AIAttackLocations.hpp"
|
||||
#include "src/main/cpp/net/eagle0/shardok/library/action_point_distances/ActionPointDistancesCache.hpp"
|
||||
#include "src/main/cpp/net/eagle0/shardok/library/map/CoordsSet.hpp"
|
||||
#include "src/main/cpp/net/eagle0/shardok/library/settings/GameSettings.hpp"
|
||||
#include "src/main/flatbuffer/net/eagle0/shardok/storage/game_state.hpp"
|
||||
#include "src/main/flatbuffer/net/eagle0/shardok/storage/player_info.hpp"
|
||||
#include "src/main/flatbuffer/net/eagle0/shardok/storage/unit.hpp"
|
||||
@@ -22,6 +22,8 @@ using Unit = net::eagle0::shardok::storage::fb::Unit;
|
||||
using net::eagle0::shardok::storage::fb::PlayerInfo;
|
||||
using std::vector;
|
||||
|
||||
using BattalionTypeGetter = std::function<BattalionTypeSPtr(BattalionTypeId)>;
|
||||
|
||||
struct TargetAndAttackLocations {
|
||||
Coords target;
|
||||
CoordsSet attackLocations;
|
||||
@@ -41,20 +43,18 @@ struct TargetPriorityList {
|
||||
auto EffectiveDistance(
|
||||
const Unit* unit,
|
||||
const HexMap* map,
|
||||
const MapId& mapId,
|
||||
const APDCache& apdCache,
|
||||
const AttackLocations& attackLocations,
|
||||
const SettingsGetter& settings,
|
||||
int braveWaterCost) -> DIST_T;
|
||||
const APDCache& apdCache,
|
||||
const BattalionTypeGetter& battalionTypeGetter,
|
||||
ActionPoints braveWaterCost) -> DIST_T;
|
||||
|
||||
auto EffectiveDistance(
|
||||
const Unit* unit,
|
||||
const HexMap* map,
|
||||
const MapId& mapId,
|
||||
const APDCache& apdCache,
|
||||
const CoordsSet& locations,
|
||||
const SettingsGetter& settings,
|
||||
int braveWaterCost) -> DIST_T;
|
||||
const APDCache& apdCache,
|
||||
const BattalionTypeGetter& battalionTypeGetter,
|
||||
ActionPoints braveWaterCost) -> DIST_T;
|
||||
|
||||
auto EffectiveDistance(
|
||||
const Unit* unit,
|
||||
@@ -71,8 +71,8 @@ auto GenerateTargetPriorities(
|
||||
const vector<const Unit*>& remainingUnits,
|
||||
const APDCache& apdCache,
|
||||
const ALCache& alCache,
|
||||
const MapId& mapId,
|
||||
const SettingsGetter& settings,
|
||||
const BattalionTypeGetter& battalionTypeGetter,
|
||||
ActionPoints braveWaterCost,
|
||||
bool isLateGame = false) -> vector<TargetPriorityList>;
|
||||
|
||||
} // namespace shardok
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
|
||||
#include "AIAttackerStrategySelector.hpp"
|
||||
|
||||
#include "AIAttackGroups.hpp"
|
||||
#include "src/main/cpp/net/eagle0/shardok/ai/AIFleeDecisionCalculator.hpp"
|
||||
#include "src/main/cpp/net/eagle0/shardok/ai/AIScoreUtilities.hpp"
|
||||
#include "src/main/cpp/net/eagle0/shardok/library/util/HexMapUtils.hpp"
|
||||
@@ -20,9 +21,11 @@ auto AIAttackerStrategySelector::BestAttackerStrategy(
|
||||
const PlayerId attackerPid,
|
||||
const GameStateW& gameState,
|
||||
const CoordsSet& criticalTileCoords,
|
||||
int maxRounds,
|
||||
const APDCache& apdCache,
|
||||
const ALCache& alCache,
|
||||
const SettingsGetter& settings,
|
||||
const BattalionTypeGetter& battalionTypeGetter,
|
||||
ActionPoints braveWaterCost,
|
||||
const AIWaterCrossingCommandChooser& waterCrossingCommandChooser,
|
||||
const vector<CommandProto>& /*availableCommands*/) -> AIStrategy {
|
||||
uint32_t attackerUnitCount = 0;
|
||||
@@ -63,12 +66,14 @@ auto AIAttackerStrategySelector::BestAttackerStrategy(
|
||||
if (canFlee && AIFleeDecisionCalculator::ShouldConsiderFleeing(
|
||||
attackerPid,
|
||||
gameState,
|
||||
settings,
|
||||
maxRounds,
|
||||
FLEE_CONSIDERATION_THRESHOLD)) {
|
||||
chosenStrategy = FleeStrategy;
|
||||
} else if (const CoordsSet startCrossingLocations =
|
||||
waterCrossingCommandChooser
|
||||
.StartCrossingFrom(settings, gameState, criticalTileCoords);
|
||||
waterCrossingCommandChooser.StartCrossingFrom(
|
||||
battalionTypeGetter,
|
||||
gameState,
|
||||
criticalTileCoords);
|
||||
!startCrossingLocations.empty()) {
|
||||
chosenStrategy = CrossRiversStrategy(startCrossingLocations);
|
||||
} else if (attackerUnitCount < criticalTileCoords.size()) {
|
||||
@@ -83,8 +88,8 @@ auto AIAttackerStrategySelector::BestAttackerStrategy(
|
||||
attackerUnits,
|
||||
apdCache,
|
||||
alCache,
|
||||
ActionPointDistancesCache::GetMapId(gameState->hex_map()),
|
||||
settings));
|
||||
battalionTypeGetter,
|
||||
braveWaterCost));
|
||||
}
|
||||
// If any critical tile is occupied by the defender, attack the castles.
|
||||
// Otherwise, try to hold the castles.
|
||||
@@ -100,8 +105,8 @@ auto AIAttackerStrategySelector::BestAttackerStrategy(
|
||||
attackerUnits,
|
||||
apdCache,
|
||||
alCache,
|
||||
ActionPointDistancesCache::GetMapId(gameState->hex_map()),
|
||||
settings));
|
||||
battalionTypeGetter,
|
||||
braveWaterCost));
|
||||
} else {
|
||||
chosenStrategy = HoldCastlesStrategy;
|
||||
}
|
||||
|
||||
@@ -6,9 +6,11 @@
|
||||
#define EAGLE0_AIATTACKERSTRATEGYSELECTOR_HPP
|
||||
|
||||
#include "src/main/cpp/net/eagle0/shardok/ai/AIAttackLocations.hpp"
|
||||
#include "src/main/cpp/net/eagle0/shardok/ai/AICommonTypes.hpp"
|
||||
#include "src/main/cpp/net/eagle0/shardok/ai/AIStrategy.hpp"
|
||||
#include "src/main/cpp/net/eagle0/shardok/ai/AIWaterCrossingCommandChooser.hpp"
|
||||
#include "src/main/cpp/net/eagle0/shardok/library/GameStateW.hpp"
|
||||
#include "src/main/cpp/net/eagle0/shardok/library/action_point_distances/ActionPointDistancesCache.hpp"
|
||||
#include "src/main/cpp/net/eagle0/shardok/library/map/CoordsSet.hpp"
|
||||
|
||||
namespace shardok {
|
||||
@@ -19,9 +21,11 @@ public:
|
||||
PlayerId attackerPid,
|
||||
const GameStateW& gameState,
|
||||
const CoordsSet& criticalTileCoords,
|
||||
int maxRounds,
|
||||
const APDCache& apdCache,
|
||||
const ALCache& alCache,
|
||||
const SettingsGetter& settings,
|
||||
const BattalionTypeGetter& battalionTypeGetter,
|
||||
ActionPoints braveWaterCost,
|
||||
const AIWaterCrossingCommandChooser& waterCrossingCommandChooser,
|
||||
const vector<CommandProto>& availableCommands) -> AIStrategy;
|
||||
};
|
||||
|
||||
@@ -0,0 +1,560 @@
|
||||
//
|
||||
// Command evaluator for AI lookahead search.
|
||||
// Extracted from AIScoreCalculator to separate concerns.
|
||||
//
|
||||
|
||||
#include "AICommandEvaluator.hpp"
|
||||
|
||||
#include <chrono>
|
||||
#include <cmath>
|
||||
#include <future>
|
||||
#include <limits>
|
||||
|
||||
#include "AICommandFilter.hpp"
|
||||
#include "AIScoreCalculator.hpp"
|
||||
#include "TranspositionTable.hpp"
|
||||
#include "src/main/cpp/net/eagle0/common/SequenceRandomGenerator.hpp"
|
||||
#include "src/main/cpp/net/eagle0/shardok/library/ShardokEngine.hpp"
|
||||
#include "src/main/cpp/net/eagle0/shardok/library/util/HexCubeUtils.hpp"
|
||||
|
||||
namespace shardok {
|
||||
|
||||
// No need to forward declare internal functions - use the public interface instead
|
||||
|
||||
// Helper constants and static variables
|
||||
static const std::vector<double> _averageSequence = {0.5};
|
||||
static const auto _averageGenerator = std::make_shared<SequenceRandomGenerator>(_averageSequence);
|
||||
|
||||
#define MULTITHREAD true
|
||||
#define LOGGING_ 0
|
||||
|
||||
// Helper function to determine if a command type is deterministic
|
||||
static auto IsDeterministic(const CommandType type) -> bool {
|
||||
switch (type) {
|
||||
case net::eagle0::shardok::common::MOVE_COMMAND:
|
||||
case net::eagle0::shardok::common::CONTROL_COMMAND:
|
||||
case net::eagle0::shardok::common::METEOR_START_COMMAND:
|
||||
case net::eagle0::shardok::common::METEOR_TARGET_COMMAND:
|
||||
case net::eagle0::shardok::common::METEOR_CANCEL_COMMAND:
|
||||
case net::eagle0::shardok::common::END_TURN_COMMAND:
|
||||
case net::eagle0::shardok::common::PLACE_UNIT_COMMAND:
|
||||
case net::eagle0::shardok::common::PLACE_HIDDEN_UNIT_COMMAND:
|
||||
case net::eagle0::shardok::common::UNIT_STOP_COMMAND:
|
||||
case net::eagle0::shardok::common::UNIT_REST_COMMAND:
|
||||
case net::eagle0::shardok::common::FLEE_COMMAND:
|
||||
case net::eagle0::shardok::common::REINFORCE_COMMAND:
|
||||
case net::eagle0::shardok::common::RETREAT_COMMAND:
|
||||
case net::eagle0::shardok::common::END_PLAYER_SETUP_COMMAND:
|
||||
case net::eagle0::shardok::common::HIDE_COMMAND:
|
||||
case net::eagle0::shardok::common::FORTIFY_COMMAND:
|
||||
case net::eagle0::shardok::common::BECOME_OUTLAW_COMMAND:
|
||||
case net::eagle0::shardok::common::HOLY_WAVE_COMMAND:
|
||||
case net::eagle0::shardok::common::REPAIR_COMMAND: return true;
|
||||
default: return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Helper function to sort commands by score
|
||||
static auto CommandSorter(
|
||||
const AICommandEvaluator::IndexAndScore& l,
|
||||
const AICommandEvaluator::IndexAndScore& r) -> bool {
|
||||
if (l.lookaheadScore < r.lookaheadScore) return true;
|
||||
if (l.lookaheadScore > r.lookaheadScore) return false;
|
||||
|
||||
// At this point the scores are tied
|
||||
if (l.immediateScore < r.immediateScore) return true;
|
||||
if (l.immediateScore > r.immediateScore) return false;
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
AICommandEvaluator::AICommandEvaluator(
|
||||
const AIScoreCalculator& scorer,
|
||||
const APDCache& apdCache,
|
||||
BattalionTypeGetter battalionTypeGetter)
|
||||
: scorer_(scorer),
|
||||
apdCache_(apdCache),
|
||||
battalionTypeGetter_(std::move(battalionTypeGetter)) {} // Move the function object
|
||||
|
||||
auto AICommandEvaluator::PerformLookahead(
|
||||
const PlayerId pid,
|
||||
const bool isDefender,
|
||||
const int remainingLookahead,
|
||||
const int maxRepeatCount,
|
||||
const std::shared_ptr<ShardokEngine>& innerEngine,
|
||||
const ScoreValue currentUtility,
|
||||
const AIStrategy& attackerStrategy,
|
||||
const CoordsSet& allCastleCoords,
|
||||
std::chrono::steady_clock::time_point deadline) const -> std::future<ScoreValue> {
|
||||
// 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);
|
||||
return p.get_future();
|
||||
}
|
||||
const auto nextUtility = currentUtility;
|
||||
|
||||
// Check if we've reached the depth limit before making recursive calls
|
||||
if (remainingLookahead <= 0) {
|
||||
// Store the current utility in the transposition table and return it
|
||||
// Note: Store with depth 1 since depth 0 indicates an empty entry in the transposition
|
||||
// table
|
||||
g_transpositionTable.store(innerEngine->GetCurrentGameState(), 1, pid, nextUtility);
|
||||
|
||||
std::promise<ScoreValue> p;
|
||||
p.set_value(nextUtility);
|
||||
return p.get_future();
|
||||
}
|
||||
|
||||
if (const CommandListSPtr nextCommands = innerEngine->GetAvailableCommandsForAIPlayer(pid);
|
||||
nextCommands && !nextCommands->empty()) {
|
||||
// Get the future from FindBestCommand without calling .get()
|
||||
auto bestCommandFuture = FindBestCommand(
|
||||
pid,
|
||||
isDefender,
|
||||
remainingLookahead - 1,
|
||||
maxRepeatCount,
|
||||
*innerEngine,
|
||||
attackerStrategy,
|
||||
nextUtility,
|
||||
allCastleCoords,
|
||||
deadline);
|
||||
|
||||
// Return a future that chains the best command evaluation
|
||||
return std::async(
|
||||
std::launch::deferred,
|
||||
[bestCommandFuture = std::move(bestCommandFuture),
|
||||
innerEngine,
|
||||
pid,
|
||||
nextUtility,
|
||||
remainingLookahead]() mutable -> ScoreValue {
|
||||
const auto [index, type, lookaheadScore, immediateScore] =
|
||||
bestCommandFuture.get();
|
||||
|
||||
ScoreValue resultScore;
|
||||
if (auto& nextCommand =
|
||||
innerEngine->GetAvailableCommandsForAIPlayer(pid)->at(index);
|
||||
nextCommand->GetCommandType() !=
|
||||
net::eagle0::shardok::common::END_TURN_COMMAND) {
|
||||
resultScore = immediateScore;
|
||||
} else {
|
||||
resultScore = nextUtility;
|
||||
}
|
||||
|
||||
// Store in transposition table before returning
|
||||
g_transpositionTable.store(
|
||||
innerEngine->GetCurrentGameState(),
|
||||
remainingLookahead,
|
||||
pid,
|
||||
resultScore);
|
||||
|
||||
return resultScore;
|
||||
});
|
||||
}
|
||||
|
||||
// No commands available, store and return the current utility as a future
|
||||
g_transpositionTable
|
||||
.store(innerEngine->GetCurrentGameState(), remainingLookahead, pid, nextUtility);
|
||||
|
||||
std::promise<ScoreValue> p;
|
||||
p.set_value(nextUtility);
|
||||
return p.get_future();
|
||||
}
|
||||
|
||||
auto AICommandEvaluator::EvaluateWithRandomness(
|
||||
const PlayerId pid,
|
||||
const bool isDefender,
|
||||
const uint32_t commandIndex,
|
||||
const int remainingLookahead,
|
||||
const int maxRepeatCount,
|
||||
const std::shared_ptr<RandomGenerator>& randomGenerator,
|
||||
const ShardokEngine& guessedEngine,
|
||||
const AIStrategy& attackerStrategy,
|
||||
const CoordsSet& allCastleCoords,
|
||||
std::chrono::steady_clock::time_point deadline) const -> ImmediateAndLookaheadScore {
|
||||
ImmediateAndLookaheadScore returnValue{};
|
||||
|
||||
// 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
|
||||
returnValue.immediateScore = 0.0;
|
||||
returnValue.lookaheadScore = p.get_future();
|
||||
return returnValue;
|
||||
}
|
||||
|
||||
auto innerEngine = std::make_shared<ShardokEngine>(guessedEngine, false);
|
||||
innerEngine->PostCommand(pid, commandIndex, randomGenerator);
|
||||
|
||||
auto innerUtility = scorer_.GuessedStateScore(
|
||||
isDefender,
|
||||
innerEngine->GetCurrentGameState(),
|
||||
attackerStrategy,
|
||||
allCastleCoords);
|
||||
|
||||
returnValue.immediateScore = innerUtility;
|
||||
|
||||
if (remainingLookahead <= 0) {
|
||||
std::promise<ScoreValue> p;
|
||||
returnValue.lookaheadScore = p.get_future();
|
||||
p.set_value(innerUtility);
|
||||
} else {
|
||||
auto lookaheadLambda = [this,
|
||||
pid,
|
||||
isDefender,
|
||||
remainingLookahead,
|
||||
maxRepeatCount,
|
||||
innerEngine,
|
||||
attackerStrategy,
|
||||
innerUtility,
|
||||
&allCastleCoords,
|
||||
deadline]() -> ScoreValue {
|
||||
auto lookaheadFuture = PerformLookahead(
|
||||
pid,
|
||||
isDefender,
|
||||
remainingLookahead,
|
||||
maxRepeatCount,
|
||||
innerEngine,
|
||||
innerUtility,
|
||||
attackerStrategy,
|
||||
allCastleCoords,
|
||||
deadline);
|
||||
return lookaheadFuture.get();
|
||||
};
|
||||
|
||||
#if MULTITHREAD
|
||||
auto launchPolicy = remainingLookahead == 1 ? std::launch::async : std::launch::deferred;
|
||||
returnValue.lookaheadScore = std::async(launchPolicy, lookaheadLambda);
|
||||
#else
|
||||
std::promise<ScoreValue> p;
|
||||
returnValue.lookaheadScore = p.get_future();
|
||||
auto lambdaResult = lookaheadLambda();
|
||||
p.set_value(lambdaResult);
|
||||
#endif
|
||||
}
|
||||
|
||||
return returnValue;
|
||||
}
|
||||
|
||||
auto AICommandEvaluator::FindBestCommand(
|
||||
const PlayerId pid,
|
||||
const bool isDefender,
|
||||
const int remainingLookahead,
|
||||
const int maxRepeatCount,
|
||||
const ShardokEngine& guessedEngine,
|
||||
const AIStrategy& attackerStrategy,
|
||||
const ScoreValue currentUtility,
|
||||
const CoordsSet& allCastleCoords,
|
||||
std::chrono::steady_clock::time_point deadline) const -> std::future<IndexAndScore> {
|
||||
const CommandListSPtr guessedDescriptors = guessedEngine.GetAvailableCommandsForAIPlayer(pid);
|
||||
|
||||
// Filter out obviously bad commands to reduce search space
|
||||
const std::vector<size_t> filteredIndices = AICommandFilter::FilterCommands(
|
||||
guessedDescriptors,
|
||||
pid,
|
||||
isDefender,
|
||||
guessedEngine.GetCurrentGameState(),
|
||||
apdCache_,
|
||||
battalionTypeGetter_);
|
||||
|
||||
const auto& gameState = guessedEngine.GetCurrentGameState();
|
||||
// Calculate minimum hex distance to enemies for this player
|
||||
double minDistToEnemies = std::numeric_limits<double>::max();
|
||||
const auto* units = gameState->units();
|
||||
|
||||
for (size_t i = 0; i < units->size(); ++i) {
|
||||
if (const auto* playerUnit = units->Get(static_cast<unsigned int>(i));
|
||||
playerUnit->player_id() == pid) {
|
||||
const auto& playerCoords = playerUnit->location();
|
||||
|
||||
for (size_t j = 0; j < units->size(); ++j) {
|
||||
if (const auto* enemyUnit = units->Get(static_cast<unsigned int>(j));
|
||||
enemyUnit->player_id() != pid) {
|
||||
const auto& enemyCoords = enemyUnit->location();
|
||||
|
||||
// Proper hex distance calculation using cube coordinates
|
||||
const Cube playerCube = OffsetToCube(playerCoords);
|
||||
const Cube enemyCube = OffsetToCube(enemyCoords);
|
||||
const int hexDistance = CubeDistance(playerCube, enemyCube);
|
||||
|
||||
minDistToEnemies = std::min(minDistToEnemies, static_cast<double>(hexDistance));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (minDistToEnemies == std::numeric_limits<double>::max()) {
|
||||
minDistToEnemies = 0.0; // No enemies found
|
||||
}
|
||||
|
||||
#if LOGGING_
|
||||
// Log command count and distance metrics for performance analysis
|
||||
const auto allCommandCount = guessedDescriptors->size();
|
||||
const auto filteredCommandCount = filteredIndices.size();
|
||||
const int currentRound = gameState->current_round();
|
||||
|
||||
printf("AI_COMMAND_COUNT: Round %d, Player %d, Defender %d, MinDist %.1f, Commands %zu -> %zu "
|
||||
"(%.1f%% filtered)\n",
|
||||
currentRound,
|
||||
static_cast<int>(pid),
|
||||
isDefender ? 1 : 0,
|
||||
minDistToEnemies,
|
||||
allCommandCount,
|
||||
filteredCommandCount,
|
||||
100.0 * (allCommandCount - filteredCommandCount) / allCommandCount);
|
||||
#endif
|
||||
|
||||
const auto commandCount = filteredIndices.size();
|
||||
|
||||
// Structure to hold all command evaluation data
|
||||
struct CommandEvaluation {
|
||||
size_t index;
|
||||
CommandType type;
|
||||
ScoreValue immediateScore;
|
||||
std::vector<std::future<ScoreValue>> lookaheadFutures;
|
||||
};
|
||||
|
||||
std::vector<CommandEvaluation> commandEvaluations(commandCount);
|
||||
|
||||
for (uint32_t index = 0; index < commandCount; index++) {
|
||||
const auto originalIndex = filteredIndices[index];
|
||||
const auto& guessedDescriptor = guessedDescriptors->at(originalIndex);
|
||||
const auto guessedCommandType = guessedDescriptor->GetCommandType();
|
||||
|
||||
commandEvaluations[index].index = originalIndex;
|
||||
commandEvaluations[index].type = guessedCommandType;
|
||||
|
||||
if (guessedCommandType == net::eagle0::shardok::common::END_TURN_COMMAND) {
|
||||
std::promise<ScoreValue> p;
|
||||
commandEvaluations[index].lookaheadFutures.push_back(p.get_future());
|
||||
p.set_value(currentUtility);
|
||||
commandEvaluations[index].immediateScore = currentUtility;
|
||||
} else if (IsDeterministic(guessedCommandType)) {
|
||||
auto [immediateScore, lookaheadScore] = EvaluateWithRandomness(
|
||||
pid,
|
||||
isDefender,
|
||||
originalIndex,
|
||||
remainingLookahead,
|
||||
maxRepeatCount,
|
||||
_averageGenerator,
|
||||
guessedEngine,
|
||||
attackerStrategy,
|
||||
allCastleCoords,
|
||||
deadline);
|
||||
|
||||
commandEvaluations[index].immediateScore = immediateScore;
|
||||
commandEvaluations[index].lookaheadFutures.push_back(std::move(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(
|
||||
pid,
|
||||
isDefender,
|
||||
originalIndex,
|
||||
remainingLookahead,
|
||||
maxRepeatCount,
|
||||
std::make_shared<SequenceRandomGenerator>(
|
||||
std::vector{1.0 - successChance / 2.0}),
|
||||
guessedEngine,
|
||||
attackerStrategy,
|
||||
allCastleCoords,
|
||||
deadline);
|
||||
|
||||
// Failure attempt uses the average of (1 - successChance) and 0 as the roll
|
||||
auto [failureImmediateScore, failureLookaheadScore] = EvaluateWithRandomness(
|
||||
pid,
|
||||
isDefender,
|
||||
originalIndex,
|
||||
remainingLookahead,
|
||||
maxRepeatCount,
|
||||
std::make_shared<SequenceRandomGenerator>(
|
||||
std::vector{(1.0 - successChance) / 2.0}),
|
||||
guessedEngine,
|
||||
attackerStrategy,
|
||||
allCastleCoords,
|
||||
deadline);
|
||||
|
||||
commandEvaluations[index].immediateScore =
|
||||
std::lerp(failureImmediateScore, successImmediateScore, successChance);
|
||||
|
||||
auto successSF = successLookaheadScore.share();
|
||||
auto failureSF = failureLookaheadScore.share();
|
||||
commandEvaluations[index].lookaheadFutures.push_back(std::async(
|
||||
std::launch::deferred,
|
||||
[successSF, failureSF, successChance]() -> double {
|
||||
return std::lerp(failureSF.get(), successSF.get(), successChance);
|
||||
}));
|
||||
} else {
|
||||
ScoreValue sum = 0.0;
|
||||
for (int repeatIteration = 0; repeatIteration < maxRepeatCount; repeatIteration++) {
|
||||
// In each iteration, use a double from [0, 1] as the random roll
|
||||
auto sequence = std::vector{
|
||||
static_cast<double>(repeatIteration) /
|
||||
static_cast<double>(maxRepeatCount - 1)};
|
||||
auto [immediateScore, lookaheadScore] = EvaluateWithRandomness(
|
||||
pid,
|
||||
isDefender,
|
||||
originalIndex,
|
||||
remainingLookahead,
|
||||
maxRepeatCount,
|
||||
std::make_shared<SequenceRandomGenerator>(sequence),
|
||||
guessedEngine,
|
||||
attackerStrategy,
|
||||
allCastleCoords,
|
||||
deadline);
|
||||
|
||||
sum += immediateScore;
|
||||
commandEvaluations[index].lookaheadFutures.push_back(std::move(lookaheadScore));
|
||||
}
|
||||
commandEvaluations[index].immediateScore = sum / maxRepeatCount;
|
||||
}
|
||||
}
|
||||
|
||||
// Return a future that will wait for all evaluations and find the best one
|
||||
return std::async(
|
||||
std::launch::deferred,
|
||||
[evals = std::move(commandEvaluations)]() mutable -> IndexAndScore {
|
||||
std::vector<IndexAndScore> allResults;
|
||||
allResults.reserve(evals.size());
|
||||
|
||||
// Wait for all futures and compute final scores
|
||||
for (auto& eval : evals) {
|
||||
ScoreValue totalLookaheadScore = 0.0;
|
||||
for (auto& future : eval.lookaheadFutures) {
|
||||
totalLookaheadScore += future.get();
|
||||
}
|
||||
ScoreValue avgLookaheadScore =
|
||||
eval.lookaheadFutures.empty()
|
||||
? eval.immediateScore
|
||||
: totalLookaheadScore / eval.lookaheadFutures.size();
|
||||
|
||||
allResults.push_back(IndexAndScore{
|
||||
.index = eval.index,
|
||||
.type = eval.type,
|
||||
.lookaheadScore = avgLookaheadScore,
|
||||
.immediateScore = eval.immediateScore});
|
||||
}
|
||||
// Find the best command using the existing sorter
|
||||
auto bestIt = std::ranges::max_element(allResults, CommandSorter);
|
||||
return *bestIt;
|
||||
});
|
||||
}
|
||||
|
||||
auto AICommandEvaluator::EvaluateCommand(
|
||||
const PlayerId pid,
|
||||
const bool isDefender,
|
||||
const int remainingLookahead,
|
||||
const int maxRepeatCount,
|
||||
const ShardokEngine& guessedEngine,
|
||||
const AIStrategy& attackerStrategy,
|
||||
const ScoreValue currentUtility,
|
||||
const CoordsSet& allCastleCoords,
|
||||
const size_t commandIndex,
|
||||
std::chrono::steady_clock::time_point deadline) const -> std::future<ScoreValue> {
|
||||
const CommandListSPtr guessedDescriptors = guessedEngine.GetAvailableCommandsForAIPlayer(pid);
|
||||
|
||||
if (commandIndex >= guessedDescriptors->size()) {
|
||||
std::promise<ScoreValue> p;
|
||||
p.set_value(currentUtility);
|
||||
return p.get_future();
|
||||
}
|
||||
|
||||
const auto& guessedDescriptor = guessedDescriptors->at(commandIndex);
|
||||
|
||||
if (const auto guessedCommandType = guessedDescriptor->GetCommandType();
|
||||
guessedCommandType == net::eagle0::shardok::common::END_TURN_COMMAND) {
|
||||
std::promise<ScoreValue> p;
|
||||
p.set_value(currentUtility);
|
||||
return p.get_future();
|
||||
} else if (IsDeterministic(guessedCommandType)) {
|
||||
auto [immediateScore, lookaheadScore] = EvaluateWithRandomness(
|
||||
pid,
|
||||
isDefender,
|
||||
commandIndex,
|
||||
remainingLookahead,
|
||||
maxRepeatCount,
|
||||
_averageGenerator,
|
||||
guessedEngine,
|
||||
attackerStrategy,
|
||||
allCastleCoords,
|
||||
deadline);
|
||||
return std::move(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(
|
||||
pid,
|
||||
isDefender,
|
||||
commandIndex,
|
||||
remainingLookahead,
|
||||
maxRepeatCount,
|
||||
std::make_shared<SequenceRandomGenerator>(std::vector{1.0 - successChance / 2.0}),
|
||||
guessedEngine,
|
||||
attackerStrategy,
|
||||
allCastleCoords,
|
||||
deadline);
|
||||
|
||||
// Failure attempt
|
||||
auto [failureImmediateScore, failureLookaheadScore] = EvaluateWithRandomness(
|
||||
pid,
|
||||
isDefender,
|
||||
commandIndex,
|
||||
remainingLookahead,
|
||||
maxRepeatCount,
|
||||
std::make_shared<SequenceRandomGenerator>(std::vector{(1.0 - successChance) / 2.0}),
|
||||
guessedEngine,
|
||||
attackerStrategy,
|
||||
allCastleCoords,
|
||||
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);
|
||||
});
|
||||
} else {
|
||||
// For non-deterministic commands without odds, use multiple attempts
|
||||
std::vector<std::future<ScoreValue>> lookaheadFutures;
|
||||
lookaheadFutures.reserve(maxRepeatCount);
|
||||
|
||||
for (int repeatIteration = 0; repeatIteration < maxRepeatCount; repeatIteration++) {
|
||||
auto sequence = std::vector{
|
||||
static_cast<double>(repeatIteration) / static_cast<double>(maxRepeatCount - 1)};
|
||||
auto [immediateScore, lookaheadScore] = EvaluateWithRandomness(
|
||||
pid,
|
||||
isDefender,
|
||||
commandIndex,
|
||||
remainingLookahead,
|
||||
maxRepeatCount,
|
||||
std::make_shared<SequenceRandomGenerator>(sequence),
|
||||
guessedEngine,
|
||||
attackerStrategy,
|
||||
allCastleCoords,
|
||||
deadline);
|
||||
|
||||
lookaheadFutures.push_back(std::move(lookaheadScore));
|
||||
}
|
||||
|
||||
// Return a future that computes the average when needed
|
||||
return std::async(
|
||||
std::launch::deferred,
|
||||
[lookaheadFutures = std::move(lookaheadFutures),
|
||||
maxRepeatCount]() mutable -> double {
|
||||
ScoreValue total = 0.0;
|
||||
for (auto& future : lookaheadFutures) { total += future.get(); }
|
||||
return total / maxRepeatCount;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace shardok
|
||||
@@ -0,0 +1,112 @@
|
||||
//
|
||||
// Command evaluator for AI lookahead search.
|
||||
// Separated from AIScoreCalculator to isolate pure state scoring from lookahead logic.
|
||||
//
|
||||
|
||||
#ifndef EAGLE0_AICOMMANDEVALUATOR_HPP
|
||||
#define EAGLE0_AICOMMANDEVALUATOR_HPP
|
||||
|
||||
#include <chrono>
|
||||
#include <future>
|
||||
|
||||
#include "src/main/cpp/net/eagle0/shardok/ai/AIStrategy.hpp"
|
||||
#include "src/main/cpp/net/eagle0/shardok/library/GameStateW.hpp"
|
||||
#include "src/main/cpp/net/eagle0/shardok/library/ShardokCTypes.h"
|
||||
#include "src/main/cpp/net/eagle0/shardok/library/action_point_distances/ActionPointDistancesCache.hpp"
|
||||
#include "src/main/cpp/net/eagle0/shardok/library/map/CoordsSet.hpp"
|
||||
#include "src/main/protobuf/net/eagle0/shardok/api/command_descriptor.pb.h"
|
||||
#include "src/main/protobuf/net/eagle0/shardok/common/command_type.pb.h"
|
||||
|
||||
namespace shardok {
|
||||
|
||||
// Forward declarations
|
||||
class AIScoreCalculator;
|
||||
class ShardokEngine;
|
||||
|
||||
using ScoreValue = double;
|
||||
using CommandProto = net::eagle0::shardok::api::CommandDescriptor;
|
||||
using CommandType = net::eagle0::shardok::common::CommandType;
|
||||
using BattalionTypeGetter = std::function<BattalionTypeSPtr(BattalionTypeId)>;
|
||||
|
||||
/// Evaluates commands with lookahead using minimax-style search.
|
||||
/// Uses AIScoreCalculator for pure state evaluation, adds recursive lookahead logic.
|
||||
class AICommandEvaluator {
|
||||
public:
|
||||
/// Construct evaluator with a scorer for state evaluation and dependencies for command
|
||||
/// filtering
|
||||
AICommandEvaluator(
|
||||
const AIScoreCalculator& scorer,
|
||||
const APDCache& apdCache,
|
||||
BattalionTypeGetter battalionTypeGetter); // Pass by value
|
||||
|
||||
/// Evaluates the score for a particular command index with lookahead.
|
||||
[[nodiscard]] auto EvaluateCommand(
|
||||
PlayerId pid,
|
||||
bool isDefender,
|
||||
int remainingLookahead,
|
||||
int maxRepeatCount,
|
||||
const ShardokEngine& guessedEngine,
|
||||
const AIStrategy& attackerStrategy,
|
||||
ScoreValue currentUtility,
|
||||
const CoordsSet& allCastleCoords,
|
||||
size_t commandIndex,
|
||||
std::chrono::steady_clock::time_point deadline) const -> std::future<ScoreValue>;
|
||||
|
||||
/// Find the best command among all available commands at the given depth.
|
||||
struct IndexAndScore {
|
||||
size_t index;
|
||||
CommandType type;
|
||||
ScoreValue lookaheadScore;
|
||||
ScoreValue immediateScore;
|
||||
};
|
||||
|
||||
[[nodiscard]] auto FindBestCommand(
|
||||
PlayerId pid,
|
||||
bool isDefender,
|
||||
int remainingLookahead,
|
||||
int maxRepeatCount,
|
||||
const ShardokEngine& guessedEngine,
|
||||
const AIStrategy& attackerStrategy,
|
||||
ScoreValue currentUtility,
|
||||
const CoordsSet& allCastleCoords,
|
||||
std::chrono::steady_clock::time_point deadline) const -> std::future<IndexAndScore>;
|
||||
|
||||
private:
|
||||
const AIScoreCalculator& scorer_;
|
||||
const APDCache& apdCache_;
|
||||
BattalionTypeGetter battalionTypeGetter_; // Store by value
|
||||
|
||||
struct ImmediateAndLookaheadScore {
|
||||
ScoreValue immediateScore;
|
||||
std::future<ScoreValue> lookaheadScore;
|
||||
};
|
||||
|
||||
/// Recursive lookahead calculator
|
||||
[[nodiscard]] auto PerformLookahead(
|
||||
PlayerId pid,
|
||||
bool isDefender,
|
||||
int remainingLookahead,
|
||||
int maxRepeatCount,
|
||||
const std::shared_ptr<ShardokEngine>& innerEngine,
|
||||
ScoreValue currentUtility,
|
||||
const AIStrategy& attackerStrategy,
|
||||
const CoordsSet& allCastleCoords,
|
||||
std::chrono::steady_clock::time_point deadline) const -> std::future<ScoreValue>;
|
||||
|
||||
/// Evaluate single command execution with randomness handling
|
||||
[[nodiscard]] auto EvaluateWithRandomness(
|
||||
PlayerId pid,
|
||||
bool isDefender,
|
||||
uint32_t commandIndex,
|
||||
int remainingLookahead,
|
||||
int maxRepeatCount,
|
||||
const std::shared_ptr<class RandomGenerator>& randomGenerator,
|
||||
const ShardokEngine& guessedEngine,
|
||||
const AIStrategy& attackerStrategy,
|
||||
const CoordsSet& allCastleCoords,
|
||||
std::chrono::steady_clock::time_point deadline) const -> ImmediateAndLookaheadScore;
|
||||
};
|
||||
|
||||
} // namespace shardok
|
||||
|
||||
#endif // EAGLE0_AICOMMANDEVALUATOR_HPP
|
||||
@@ -36,8 +36,8 @@ std::vector<size_t> AICommandFilter::FilterCommands(
|
||||
PlayerId pid,
|
||||
bool isDefender,
|
||||
const GameStateW& gameState,
|
||||
const SettingsGetter& settings,
|
||||
const APDCache& apdCache) {
|
||||
const APDCache& apdCache,
|
||||
const BattalionTypeGetter& battalionTypeGetter) {
|
||||
std::vector<size_t> filteredIndices;
|
||||
filteredIndices.reserve(commands->size());
|
||||
|
||||
@@ -66,8 +66,8 @@ std::vector<size_t> AICommandFilter::FilterCommands(
|
||||
pid,
|
||||
isDefender,
|
||||
gameState,
|
||||
settings,
|
||||
apdCache,
|
||||
battalionTypeGetter,
|
||||
enemyLocations,
|
||||
castleLocations,
|
||||
minDistToEnemies)) {
|
||||
@@ -80,16 +80,22 @@ std::vector<size_t> AICommandFilter::FilterCommands(
|
||||
pid,
|
||||
isDefender,
|
||||
gameState,
|
||||
settings,
|
||||
apdCache,
|
||||
battalionTypeGetter,
|
||||
enemyLocations,
|
||||
minDistToEnemies)) {
|
||||
shouldFilter = true;
|
||||
}
|
||||
|
||||
// Check strategic blunders
|
||||
if (!shouldFilter &&
|
||||
IsStrategicBlunder(*cmd, pid, isDefender, gameState, settings, minDistToEnemies)) {
|
||||
if (!shouldFilter && IsStrategicBlunder(
|
||||
*cmd,
|
||||
pid,
|
||||
isDefender,
|
||||
gameState,
|
||||
apdCache,
|
||||
battalionTypeGetter,
|
||||
minDistToEnemies)) {
|
||||
shouldFilter = true;
|
||||
}
|
||||
|
||||
@@ -104,8 +110,8 @@ bool AICommandFilter::IsWastefulAction(
|
||||
PlayerId pid,
|
||||
bool isDefender,
|
||||
const GameStateW& gameState,
|
||||
const SettingsGetter& settings,
|
||||
const APDCache& apdCache,
|
||||
const BattalionTypeGetter& battalionTypeGetter,
|
||||
const CoordsSet& enemyLocations,
|
||||
const CoordsSet& castleLocations,
|
||||
double minDistToEnemies) {
|
||||
@@ -269,7 +275,7 @@ bool AICommandFilter::IsWastefulAction(
|
||||
}
|
||||
|
||||
// Get action point distances for this unit's battalion type
|
||||
const auto& battType = settings.GetBattalionType(actingUnit->battalion().type());
|
||||
const auto& battType = battalionTypeGetter(actingUnit->battalion().type());
|
||||
const auto* apd = apdCache->GetRaw(
|
||||
gameState->hex_map(),
|
||||
ActionPointDistancesCache::GetMapId(gameState->hex_map()),
|
||||
@@ -407,8 +413,8 @@ bool AICommandFilter::IsWastefulMovement(
|
||||
PlayerId pid,
|
||||
bool isDefender,
|
||||
const GameStateW& gameState,
|
||||
const SettingsGetter& settings,
|
||||
const APDCache& apdCache,
|
||||
const BattalionTypeGetter& battalionTypeGetter,
|
||||
const CoordsSet& enemyLocations,
|
||||
double minDistToEnemies) {
|
||||
if (cmd.GetCommandType() != CommandType::MOVE_COMMAND) { return false; }
|
||||
@@ -449,7 +455,7 @@ bool AICommandFilter::IsWastefulMovement(
|
||||
static_cast<int8_t>(targetCoords.column())};
|
||||
|
||||
// Get action point distances for this unit's battalion type
|
||||
const auto& battType = settings.GetBattalionType(actingUnit->battalion().type());
|
||||
const auto& battType = battalionTypeGetter(actingUnit->battalion().type());
|
||||
const auto* apd = apdCache->GetRaw(
|
||||
gameState->hex_map(),
|
||||
ActionPointDistancesCache::GetMapId(gameState->hex_map()),
|
||||
@@ -490,7 +496,8 @@ bool AICommandFilter::IsStrategicBlunder(
|
||||
PlayerId /*pid*/,
|
||||
bool /*isDefender*/,
|
||||
const GameStateW& /*gameState*/,
|
||||
const SettingsGetter& /*settings*/,
|
||||
const APDCache& /*apdCache*/,
|
||||
const BattalionTypeGetter& /*battalionTypeGetter*/,
|
||||
double /*minDistToEnemies*/) {
|
||||
// Simplified strategic blunder detection for now
|
||||
// TODO: Implement proper castle abandonment detection
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
#include <memory>
|
||||
#include <vector>
|
||||
|
||||
#include "src/main/cpp/net/eagle0/shardok/ai/AICommonTypes.hpp"
|
||||
#include "src/main/cpp/net/eagle0/shardok/library/ShardokEngine.hpp"
|
||||
#include "src/main/cpp/net/eagle0/shardok/library/action_point_distances/ActionPointDistancesCache.hpp"
|
||||
#include "src/main/cpp/net/eagle0/shardok/library/map/CoordsSet.hpp"
|
||||
@@ -32,8 +33,8 @@ public:
|
||||
* @param pid Player ID making the move
|
||||
* @param isDefender True if this player is the defender
|
||||
* @param gameState Current game state
|
||||
* @param settings Game settings for parameter lookup
|
||||
* @param apdCache Action point distance cache for distance calculations
|
||||
* @param battalionTypeLookup Function to look up battalion types by ID
|
||||
* @return Filtered list of commands worth evaluating
|
||||
*/
|
||||
static std::vector<size_t> FilterCommands(
|
||||
@@ -41,8 +42,8 @@ public:
|
||||
PlayerId pid,
|
||||
bool isDefender,
|
||||
const GameStateW& gameState,
|
||||
const SettingsGetter& settings,
|
||||
const APDCache& apdCache);
|
||||
const APDCache& apdCache,
|
||||
const BattalionTypeGetter& battalionTypeLookup);
|
||||
|
||||
private:
|
||||
// Helper to build enemy locations once for efficiency
|
||||
@@ -54,8 +55,8 @@ private:
|
||||
PlayerId pid,
|
||||
bool isDefender,
|
||||
const GameStateW& gameState,
|
||||
const SettingsGetter& settings,
|
||||
const APDCache& apdCache,
|
||||
const BattalionTypeGetter& battalionTypeLookup,
|
||||
const CoordsSet& enemyLocations,
|
||||
const CoordsSet& castleLocations,
|
||||
double minDistToEnemies);
|
||||
@@ -66,8 +67,8 @@ private:
|
||||
PlayerId pid,
|
||||
bool isDefender,
|
||||
const GameStateW& gameState,
|
||||
const SettingsGetter& settings,
|
||||
const APDCache& apdCache,
|
||||
const BattalionTypeGetter& battalionTypeLookup,
|
||||
const CoordsSet& enemyLocations,
|
||||
double minDistToEnemies);
|
||||
|
||||
@@ -77,7 +78,8 @@ private:
|
||||
PlayerId pid,
|
||||
bool isDefender,
|
||||
const GameStateW& gameState,
|
||||
const SettingsGetter& settings,
|
||||
const APDCache& apdCache,
|
||||
const BattalionTypeGetter& battalionTypeLookup,
|
||||
double minDistToEnemies);
|
||||
|
||||
// Helper functions for distance and position analysis
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
//
|
||||
// AICommonTypes.hpp
|
||||
// Common type definitions used across AI utility functions
|
||||
//
|
||||
|
||||
#ifndef EAGLE0_AICOMMONTYPES_HPP
|
||||
#define EAGLE0_AICOMMONTYPES_HPP
|
||||
|
||||
#include <functional>
|
||||
|
||||
#include "src/main/cpp/net/eagle0/shardok/library/BattalionType.hpp"
|
||||
#include "src/main/cpp/net/eagle0/shardok/library/ShardokCTypes.h"
|
||||
|
||||
namespace shardok {
|
||||
|
||||
// Function type for looking up battalion types by ID
|
||||
// Used across AI utilities to get battalion type information without
|
||||
// needing to pass the entire scorer object
|
||||
using BattalionTypeGetter = std::function<BattalionTypeSPtr(BattalionTypeId)>;
|
||||
|
||||
} // namespace shardok
|
||||
|
||||
#endif // EAGLE0_AICOMMONTYPES_HPP
|
||||
@@ -7,6 +7,8 @@
|
||||
#include <algorithm>
|
||||
#include <ranges>
|
||||
|
||||
#include "src/main/cpp/net/eagle0/shardok/ai/AIAttackGroups.hpp"
|
||||
#include "src/main/cpp/net/eagle0/shardok/ai/AIScoreCalculator.hpp"
|
||||
#include "src/main/cpp/net/eagle0/shardok/ai/AIScoreUtilities.hpp"
|
||||
#include "src/main/cpp/net/eagle0/shardok/ai/AIWaterCrossingCalculator.hpp"
|
||||
#include "src/main/cpp/net/eagle0/shardok/library/util/HexMapUtils.hpp"
|
||||
@@ -19,8 +21,9 @@ constexpr double MINIMUM_RATIO_FOR_DEFENDER_TO_HOLD = 0.60;
|
||||
auto AIDefenderStrategySelector::BestDefenderStrategy(
|
||||
const GameStateW& gameState,
|
||||
const CoordsSet& criticalTileCoords,
|
||||
int maxRounds,
|
||||
const APDCache& apdCache,
|
||||
const SettingsGetter& settings) -> AIStrategy {
|
||||
const BattalionTypeGetter& battalionTypeGetter) -> AIStrategy {
|
||||
uint32_t attackerNonUndeadUnitCount = 0;
|
||||
uint32_t attackerNonUndeadUnitNotRequiringWaterCrossingCount = 0;
|
||||
int attackerTroops = 0;
|
||||
@@ -36,7 +39,7 @@ auto AIDefenderStrategySelector::BestDefenderStrategy(
|
||||
player->player_id(),
|
||||
criticalTileCoords,
|
||||
apdCache,
|
||||
settings);
|
||||
battalionTypeGetter);
|
||||
attackerUnitIdsRequiringWaterCrossing.insert(
|
||||
attackerUnitIdsRequiringWaterCrossing.end(),
|
||||
unitIdsRequiringWaterCrossing.begin(),
|
||||
@@ -71,7 +74,7 @@ auto AIDefenderStrategySelector::BestDefenderStrategy(
|
||||
}
|
||||
}
|
||||
|
||||
const int roundsRemaining = 32 - gameState->current_round();
|
||||
const int roundsRemaining = maxRounds - gameState->current_round();
|
||||
AIStrategy chosenStrategy;
|
||||
|
||||
// Defender will flee if
|
||||
|
||||
@@ -6,19 +6,23 @@
|
||||
#define EAGLE0_AIDEFENDERSTRATEGYSELECTOR_HPP
|
||||
|
||||
#include "src/main/cpp/net/eagle0/shardok/ai/AIAttackLocations.hpp"
|
||||
#include "src/main/cpp/net/eagle0/shardok/ai/AICommonTypes.hpp"
|
||||
#include "src/main/cpp/net/eagle0/shardok/ai/AIStrategy.hpp"
|
||||
#include "src/main/cpp/net/eagle0/shardok/library/GameStateW.hpp"
|
||||
#include "src/main/cpp/net/eagle0/shardok/library/action_point_distances/ActionPointDistancesCache.hpp"
|
||||
#include "src/main/cpp/net/eagle0/shardok/library/map/CoordsSet.hpp"
|
||||
#include "src/main/flatbuffer/net/eagle0/shardok/storage/game_state.hpp"
|
||||
|
||||
namespace shardok {
|
||||
|
||||
class AIDefenderStrategySelector {
|
||||
public:
|
||||
static auto BestDefenderStrategy(
|
||||
const GameStateW& gameState,
|
||||
const CoordsSet& criticalTileCoords,
|
||||
int maxRounds,
|
||||
const APDCache& apdCache,
|
||||
const SettingsGetter& settings) -> AIStrategy;
|
||||
const BattalionTypeGetter& battalionTypeGetter) -> AIStrategy;
|
||||
};
|
||||
} // namespace shardok
|
||||
|
||||
|
||||
@@ -49,8 +49,8 @@ auto DefenderDistanceBuf(
|
||||
const vector<const Unit *> &attackerUnits,
|
||||
const APDCache &apdCache,
|
||||
const ALCache &alCache,
|
||||
const SettingsGetter &settings,
|
||||
const int braveWaterActionPointCost,
|
||||
const BattalionTypeGetter &battalionTypeGetter,
|
||||
ActionPoints braveWaterCost,
|
||||
const bool lateGame,
|
||||
const bool includeUndead) -> double {
|
||||
const auto &locationsToAttackMe = alCache->CachedLocations(defenderLocation, lateGame);
|
||||
@@ -73,14 +73,14 @@ auto DefenderDistanceBuf(
|
||||
notBravingDistances[typeInt] = apdCache->GetRaw(
|
||||
hexMap,
|
||||
mapId,
|
||||
settings.GetBattalionType(attacker->battalion().type()),
|
||||
battalionTypeGetter(attacker->battalion().type()),
|
||||
false);
|
||||
bravingDistances[typeInt] = apdCache->GetRaw(
|
||||
hexMap,
|
||||
mapId,
|
||||
settings.GetBattalionType(attacker->battalion().type()),
|
||||
battalionTypeGetter(attacker->battalion().type()),
|
||||
true,
|
||||
braveWaterActionPointCost);
|
||||
braveWaterCost);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -6,10 +6,10 @@
|
||||
#define EAGLE0_AIDISTANCEDEBUF_HPP
|
||||
|
||||
#include "AIAttackLocations.hpp"
|
||||
#include "src/main/cpp/net/eagle0/shardok/ai/AICommonTypes.hpp"
|
||||
#include "src/main/cpp/net/eagle0/shardok/library/action_point_distances/ActionPointDistances.hpp"
|
||||
#include "src/main/cpp/net/eagle0/shardok/library/action_point_distances/ActionPointDistancesCache.hpp"
|
||||
#include "src/main/cpp/net/eagle0/shardok/library/map/CoordsSet.hpp"
|
||||
#include "src/main/cpp/net/eagle0/shardok/library/settings/GameSettings.hpp"
|
||||
|
||||
namespace shardok {
|
||||
|
||||
@@ -23,8 +23,8 @@ auto DefenderDistanceBuf(
|
||||
const vector<const Unit *> &attackerUnits,
|
||||
const APDCache &apdCache,
|
||||
const ALCache &alCache,
|
||||
const SettingsGetter &settings,
|
||||
int braveWaterActionPointCost,
|
||||
const BattalionTypeGetter &battalionTypeGetter,
|
||||
ActionPoints braveWaterCost,
|
||||
bool lateGame,
|
||||
bool includeUndead) -> double;
|
||||
|
||||
|
||||
@@ -23,7 +23,7 @@ auto AIFleeDecisionCalculator::GetFleeCommandIndex(
|
||||
auto AIFleeDecisionCalculator::EstimateCombatSuccess(
|
||||
PlayerId attackerPlayerId,
|
||||
const GameStateW& gameState,
|
||||
const SettingsGetter& settings) -> double {
|
||||
int maxRounds) -> double {
|
||||
if (gameState->status() == nullptr ||
|
||||
gameState->status()->state() !=
|
||||
net::eagle0::shardok::storage::fb::GameStatus_::State_GAME_RUNNING) {
|
||||
@@ -68,7 +68,7 @@ auto AIFleeDecisionCalculator::EstimateCombatSuccess(
|
||||
}
|
||||
}
|
||||
|
||||
const int roundsRemaining = settings.Backing().max_rounds() - gameState->current_round();
|
||||
const int roundsRemaining = maxRounds - gameState->current_round();
|
||||
|
||||
// Special case: Attacker has no heroes - automatic loss
|
||||
if (attackerHeroes == 0) {
|
||||
@@ -133,18 +133,16 @@ auto AIFleeDecisionCalculator::EstimateCombatSuccess(
|
||||
|
||||
auto AIFleeDecisionCalculator::EvaluateFleeVsFight(
|
||||
PlayerId playerId,
|
||||
const SettingsGetter& settingsGetter,
|
||||
const GameStateW& guessedState,
|
||||
const vector<CommandProto>& availableCommands,
|
||||
const vector<CommandProto>::const_iterator& fleeCommand,
|
||||
int maxRounds,
|
||||
int minimumFleeOddsThreshold,
|
||||
int desperateFleeThreshold,
|
||||
bool enableDebugLogging) -> FleeDecision {
|
||||
// Get flee success odds
|
||||
const int fleeSuccessChance = fleeCommand->odds().success_chance();
|
||||
|
||||
// Get thresholds from settings
|
||||
const int minimumFleeOddsThreshold = settingsGetter.Backing().ai_minimum_flee_odds_threshold();
|
||||
const int desperateFleeThreshold = settingsGetter.Backing().ai_desperate_flee_threshold();
|
||||
|
||||
if (enableDebugLogging) {
|
||||
printf("AI FinalRound: Evaluating flee (odds=%d%%)...\n", fleeSuccessChance);
|
||||
}
|
||||
@@ -163,7 +161,7 @@ auto AIFleeDecisionCalculator::EvaluateFleeVsFight(
|
||||
}
|
||||
|
||||
// Low flee odds - evaluate if fighting might be better
|
||||
const double combatWinChance = EstimateCombatSuccess(playerId, guessedState, settingsGetter);
|
||||
const double combatWinChance = EstimateCombatSuccess(playerId, guessedState, maxRounds);
|
||||
|
||||
// If combat situation is hopeless, even bad flee odds are better than certain death
|
||||
if (combatWinChance <= 0.05 && fleeSuccessChance >= desperateFleeThreshold) {
|
||||
@@ -215,11 +213,11 @@ auto AIFleeDecisionCalculator::EvaluateFleeVsFight(
|
||||
auto AIFleeDecisionCalculator::ShouldConsiderFleeing(
|
||||
PlayerId attackerPlayerId,
|
||||
const GameStateW& guessedState,
|
||||
const SettingsGetter& settings,
|
||||
int maxRounds,
|
||||
double fleeConsiderationThreshold) -> bool {
|
||||
// Get combat success probability
|
||||
const double combatSuccessChance =
|
||||
EstimateCombatSuccess(attackerPlayerId, guessedState, settings);
|
||||
EstimateCombatSuccess(attackerPlayerId, guessedState, maxRounds);
|
||||
|
||||
// Consider fleeing if combat success chance is below threshold
|
||||
return combatSuccessChance < fleeConsiderationThreshold;
|
||||
|
||||
@@ -10,7 +10,6 @@
|
||||
#define AIFleeDecisionCalculator_hpp
|
||||
|
||||
#include "src/main/cpp/net/eagle0/shardok/library/ShardokEngine.hpp"
|
||||
#include "src/main/cpp/net/eagle0/shardok/library/settings/GameSettings.hpp"
|
||||
#include "src/main/protobuf/net/eagle0/shardok/api/command_descriptor.pb.h"
|
||||
|
||||
namespace shardok {
|
||||
@@ -35,24 +34,26 @@ public:
|
||||
// Evaluate whether to flee or fight in the final round
|
||||
[[nodiscard]] static auto EvaluateFleeVsFight(
|
||||
PlayerId playerId,
|
||||
const SettingsGetter& settings,
|
||||
const GameStateW& guessedState,
|
||||
const vector<CommandProto>& availableCommands,
|
||||
const vector<CommandProto>::const_iterator& fleeCommand,
|
||||
int maxRounds,
|
||||
int minimumFleeOddsThreshold,
|
||||
int desperateFleeThreshold,
|
||||
bool enableDebugLogging = false) -> FleeDecision;
|
||||
|
||||
// Estimate probability of combat success for the attacker
|
||||
[[nodiscard]] static auto EstimateCombatSuccess(
|
||||
PlayerId attackerPlayerId,
|
||||
const GameStateW& guessedState,
|
||||
const SettingsGetter& settings) -> double;
|
||||
int maxRounds) -> double;
|
||||
|
||||
// Determine if the attacker should consider fleeing based on combat odds
|
||||
// Returns true if fleeing should be considered as an option
|
||||
[[nodiscard]] static auto ShouldConsiderFleeing(
|
||||
PlayerId attackerPlayerId,
|
||||
const GameStateW& guessedState,
|
||||
const SettingsGetter& settings,
|
||||
int maxRounds,
|
||||
double fleeConsiderationThreshold = 0.5) -> bool;
|
||||
|
||||
private:
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -5,23 +5,17 @@
|
||||
#ifndef EAGLE0_AISCORECALCULATOR_HPP
|
||||
#define EAGLE0_AISCORECALCULATOR_HPP
|
||||
|
||||
#include <chrono>
|
||||
#include <future>
|
||||
|
||||
#include "src/main/cpp/net/eagle0/shardok/ai/AIAttackLocations.hpp"
|
||||
#include "src/main/cpp/net/eagle0/shardok/ai/AIStrategy.hpp"
|
||||
#include "src/main/cpp/net/eagle0/shardok/library/GameStateW.hpp"
|
||||
#include "src/main/cpp/net/eagle0/shardok/library/ShardokCTypes.h"
|
||||
#include "src/main/cpp/net/eagle0/shardok/library/ShardokEngine.hpp"
|
||||
#include "src/main/cpp/net/eagle0/shardok/library/action_point_distances/ActionPointDistancesCache.hpp"
|
||||
#include "src/main/cpp/net/eagle0/shardok/library/settings/GameSettings.hpp"
|
||||
#include "src/main/flatbuffer/net/eagle0/shardok/storage/game_state.hpp"
|
||||
#include "src/main/cpp/net/eagle0/shardok/library/map/CoordsSet.hpp"
|
||||
#include "src/main/protobuf/net/eagle0/shardok/api/command_descriptor.pb.h"
|
||||
#include "src/main/protobuf/net/eagle0/shardok/api/game_state_view.pb.h"
|
||||
|
||||
namespace shardok {
|
||||
|
||||
using net::eagle0::shardok::api::GameStateView;
|
||||
using GameState = fb::GameState;
|
||||
using shardok::PlayerId;
|
||||
using std::future;
|
||||
using std::vector;
|
||||
@@ -29,34 +23,34 @@ using std::vector;
|
||||
using ScoreValue = double;
|
||||
using CommandProto = net::eagle0::shardok::api::CommandDescriptor;
|
||||
|
||||
// Forward declarations
|
||||
class ShardokEngine;
|
||||
struct AIStrategy;
|
||||
|
||||
/// Abstract base class for AI scoring algorithms.
|
||||
/// Allows testing different scoring strategies by implementing different scorers.
|
||||
class AIScoreCalculator {
|
||||
public:
|
||||
// Evaluate the score of a guessed game state based on the current AI strategy. DOES NOT perform
|
||||
// or evaluate any commands.
|
||||
[[nodiscard]] static auto GuessedStateScore(
|
||||
virtual ~AIScoreCalculator() = default;
|
||||
|
||||
// Rule of five: explicitly default or delete copy/move operations
|
||||
AIScoreCalculator(const AIScoreCalculator &) = default;
|
||||
AIScoreCalculator &operator=(const AIScoreCalculator &) = default;
|
||||
AIScoreCalculator(AIScoreCalculator &&) = default;
|
||||
AIScoreCalculator &operator=(AIScoreCalculator &&) = default;
|
||||
|
||||
protected:
|
||||
AIScoreCalculator() = default;
|
||||
|
||||
public:
|
||||
/// Evaluate the score of a guessed game state based on the current AI strategy.
|
||||
/// DOES NOT perform lookahead - this is pure state evaluation.
|
||||
/// For lookahead search, use AICommandEvaluator which depends on this interface.
|
||||
[[nodiscard]] virtual auto GuessedStateScore(
|
||||
bool isDefender,
|
||||
const GameStateW &state,
|
||||
const AIStrategy &aiStrategy,
|
||||
const CoordsSet &allCastleCoords,
|
||||
const SettingsGetter &settingsGetter,
|
||||
const APDCache &apdCache,
|
||||
const ALCache &alCache) -> ScoreValue;
|
||||
|
||||
// Evaluates the score for a particular command index for the given player, using lookahead.
|
||||
[[nodiscard]] static auto CommandScore(
|
||||
PlayerId pid,
|
||||
bool isDefender,
|
||||
int remainingLookahead,
|
||||
int maxRepeatCount,
|
||||
const ShardokEngine &guessedEngine,
|
||||
const AIStrategy &attackerStrategy,
|
||||
ScoreValue currentUtility,
|
||||
const SettingsGetter &settingsGetter,
|
||||
const CoordsSet &allCastleCoords,
|
||||
const APDCache &apdCache,
|
||||
const ALCache &alCache,
|
||||
size_t commandIndex,
|
||||
std::chrono::steady_clock::time_point deadline) -> std::future<ScoreValue>;
|
||||
const CoordsSet &allCastleCoords) const -> ScoreValue = 0;
|
||||
};
|
||||
|
||||
} // namespace shardok
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
//
|
||||
|
||||
#include "src/main/cpp/net/eagle0/shardok/ai/AIStrategy.hpp"
|
||||
|
||||
namespace shardok {
|
||||
AIStrategy FleeStrategy = AIStrategy{AIStrategy::STRATEGY_FLEE};
|
||||
AIStrategy HoldCastlesStrategy = AIStrategy{AIStrategy::STRATEGY_HOLD_CASTLES};
|
||||
|
||||
@@ -335,7 +335,8 @@ auto UnitValue(
|
||||
const AttackLocations &locationsThisSideCanAttackFrom,
|
||||
const CoordsSet &locationsInDangerFromEnemy,
|
||||
const ActionPointDistances *distances,
|
||||
const SettingsGetter &settings) -> ScoreValue {
|
||||
int meteorRange,
|
||||
double meteorCastVigorCost) -> ScoreValue {
|
||||
const auto &location = unit->location();
|
||||
if (location.row() < 0) return 0; // unplaced unit
|
||||
|
||||
@@ -380,8 +381,8 @@ auto UnitValue(
|
||||
roundsRemaining,
|
||||
attackerUnits,
|
||||
defenderUnits,
|
||||
settings.Backing().meteor_range(),
|
||||
settings.Backing().meteor_cast_vigor_cost());
|
||||
meteorRange,
|
||||
meteorCastVigorCost);
|
||||
|
||||
// scouting values
|
||||
// attack range
|
||||
|
||||
@@ -46,7 +46,8 @@ auto UnitValue(
|
||||
const AttackLocations &locationsThisSideCanAttackFrom,
|
||||
const CoordsSet &locationsInDangerFromEnemy,
|
||||
const ActionPointDistances *distances,
|
||||
const SettingsGetter &settings) -> ScoreValue;
|
||||
int meteorRange,
|
||||
double meteorCastVigorCost) -> ScoreValue;
|
||||
|
||||
} // namespace shardok
|
||||
|
||||
|
||||
@@ -43,8 +43,8 @@ auto AttackerDebufForOnFireCriticalTile(
|
||||
const vector<const Unit*>& extinguishingUnits,
|
||||
const APDCache& apdCache,
|
||||
const ALCache& alCache,
|
||||
const SettingsGetter& settings,
|
||||
const int braveWaterActionPointCost,
|
||||
const BattalionTypeGetter& battalionTypeGetter,
|
||||
ActionPoints braveWaterCost,
|
||||
const bool lateGame) -> double {
|
||||
double minDebuf = 99999.9;
|
||||
|
||||
@@ -60,8 +60,8 @@ auto AttackerDebufForOnFireCriticalTile(
|
||||
extinguishingUnits,
|
||||
apdCache,
|
||||
alCache,
|
||||
settings,
|
||||
braveWaterActionPointCost,
|
||||
battalionTypeGetter,
|
||||
braveWaterCost,
|
||||
lateGame,
|
||||
/* includeUndead = */ false);
|
||||
if (newDebuf < minDebuf) minDebuf = newDebuf;
|
||||
@@ -77,8 +77,8 @@ auto AttackerDebufForUnoccupiedCriticalTile(
|
||||
const vector<const Unit*>& claimableUnits,
|
||||
const APDCache& apdCache,
|
||||
const ALCache& alCache,
|
||||
const SettingsGetter& settings,
|
||||
const int braveWaterActionPointCost,
|
||||
const BattalionTypeGetter& battalionTypeGetter,
|
||||
ActionPoints braveWaterCost,
|
||||
const bool lateGame) -> double {
|
||||
return UNHELD_VALUE * DefenderDistanceBuf(
|
||||
criticalTileLocation,
|
||||
@@ -87,8 +87,8 @@ auto AttackerDebufForUnoccupiedCriticalTile(
|
||||
claimableUnits,
|
||||
apdCache,
|
||||
alCache,
|
||||
settings,
|
||||
braveWaterActionPointCost,
|
||||
battalionTypeGetter,
|
||||
braveWaterCost,
|
||||
lateGame,
|
||||
/* includeUndead = */ false);
|
||||
}
|
||||
@@ -100,8 +100,8 @@ auto AttackerDebufForDefenderOccupiedCriticalTile(
|
||||
const vector<const Unit*>& attackerUnits,
|
||||
const APDCache& apdCache,
|
||||
const ALCache& alCache,
|
||||
const SettingsGetter& settings,
|
||||
const int braveWaterActionPointCost,
|
||||
const BattalionTypeGetter& battalionTypeGetter,
|
||||
ActionPoints braveWaterCost,
|
||||
const bool lateGame) {
|
||||
const double baseUnitValue =
|
||||
defenderUnit->battalion().size() +
|
||||
@@ -117,8 +117,8 @@ auto AttackerDebufForDefenderOccupiedCriticalTile(
|
||||
attackerUnits,
|
||||
apdCache,
|
||||
alCache,
|
||||
settings,
|
||||
braveWaterActionPointCost,
|
||||
battalionTypeGetter,
|
||||
braveWaterCost,
|
||||
lateGame,
|
||||
/* includeUndead = */ false);
|
||||
}
|
||||
@@ -126,10 +126,7 @@ auto AttackerDebufForDefenderOccupiedCriticalTile(
|
||||
auto DefenderHoldsCriticalTilesVictoryScore(
|
||||
const GameStateW& gameState,
|
||||
const CoordsSet& criticalTileLocations,
|
||||
const PlayerInfo* player,
|
||||
const APDCache& /*apdCache*/,
|
||||
const ALCache& /*alCache*/,
|
||||
const SettingsGetter& /*settings*/) -> ScoreValue {
|
||||
const PlayerInfo* player) -> ScoreValue {
|
||||
ScoreValue total = 0.0;
|
||||
|
||||
const auto rc = gameState->hex_map()->row_count();
|
||||
@@ -159,7 +156,8 @@ auto AttackerHoldsCriticalTilesVictoryScore(
|
||||
const PlayerInfo* player,
|
||||
const APDCache& apdCache,
|
||||
const ALCache& alCache,
|
||||
const SettingsGetter& settings) -> ScoreValue {
|
||||
const BattalionTypeGetter& battalionTypeGetter,
|
||||
ActionPoints braveWaterCost) -> ScoreValue {
|
||||
vector<const Unit*> playerUnits{};
|
||||
vector<const Unit*> claimablePlayerUnits{};
|
||||
for (const Unit* unit : *gameState->units()) {
|
||||
@@ -175,7 +173,6 @@ auto AttackerHoldsCriticalTilesVictoryScore(
|
||||
return criticalTileLocations.size() * MAX_DEFENDER_HELD_VALUE;
|
||||
}
|
||||
|
||||
const int braveWaterActionPointCost = settings.Backing().brave_water_action_point_cost();
|
||||
const MapId mapId = apdCache->GetMapId(gameState->hex_map());
|
||||
|
||||
ScoreValue total = 0.0;
|
||||
@@ -202,8 +199,8 @@ auto AttackerHoldsCriticalTilesVictoryScore(
|
||||
claimablePlayerUnits,
|
||||
apdCache,
|
||||
alCache,
|
||||
settings,
|
||||
braveWaterActionPointCost,
|
||||
battalionTypeGetter,
|
||||
braveWaterCost,
|
||||
IsLateGame(gameState));
|
||||
total += BADLY_HELD_VALUE;
|
||||
}
|
||||
@@ -215,8 +212,8 @@ auto AttackerHoldsCriticalTilesVictoryScore(
|
||||
playerUnits,
|
||||
apdCache,
|
||||
alCache,
|
||||
settings,
|
||||
braveWaterActionPointCost,
|
||||
battalionTypeGetter,
|
||||
braveWaterCost,
|
||||
IsLateGame(gameState));
|
||||
}
|
||||
} else if (terrain->modifier().fire().present()) {
|
||||
@@ -227,8 +224,8 @@ auto AttackerHoldsCriticalTilesVictoryScore(
|
||||
claimablePlayerUnits,
|
||||
apdCache,
|
||||
alCache,
|
||||
settings,
|
||||
braveWaterActionPointCost,
|
||||
battalionTypeGetter,
|
||||
braveWaterCost,
|
||||
IsLateGame(gameState));
|
||||
} else {
|
||||
total -= AttackerDebufForUnoccupiedCriticalTile(
|
||||
@@ -238,8 +235,8 @@ auto AttackerHoldsCriticalTilesVictoryScore(
|
||||
claimablePlayerUnits,
|
||||
apdCache,
|
||||
alCache,
|
||||
settings,
|
||||
braveWaterActionPointCost,
|
||||
battalionTypeGetter,
|
||||
braveWaterCost,
|
||||
IsLateGame(gameState));
|
||||
}
|
||||
}
|
||||
@@ -252,7 +249,8 @@ auto LastPlayerStandingVictoryScore(
|
||||
const PlayerInfo* player,
|
||||
const APDCache& apdCache,
|
||||
const ALCache& alCache,
|
||||
const SettingsGetter& settings) -> ScoreValue {
|
||||
const BattalionTypeGetter& battalionTypeGetter,
|
||||
ActionPoints braveWaterCost) -> ScoreValue {
|
||||
if (!std::ranges::contains(
|
||||
*player->victory_conditions(),
|
||||
net::eagle0::shardok::storage::fb::
|
||||
@@ -285,8 +283,8 @@ auto LastPlayerStandingVictoryScore(
|
||||
playerUnits,
|
||||
apdCache,
|
||||
alCache,
|
||||
settings,
|
||||
5,
|
||||
battalionTypeGetter,
|
||||
braveWaterCost,
|
||||
IsLateGame(gameState),
|
||||
/* includeUndead = */ true);
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
|
||||
#include "src/main/cpp/net/eagle0/shardok/ai/AIAttackGroups.hpp"
|
||||
#include "src/main/cpp/net/eagle0/shardok/ai/AIAttackLocations.hpp"
|
||||
#include "src/main/cpp/net/eagle0/shardok/ai/AICommonTypes.hpp"
|
||||
#include "src/main/cpp/net/eagle0/shardok/library/GameStateW.hpp"
|
||||
#include "src/main/cpp/net/eagle0/shardok/library/action_point_distances/ActionPointDistancesCache.hpp"
|
||||
#include "src/main/cpp/net/eagle0/shardok/library/map/CoordsSet.hpp"
|
||||
@@ -29,22 +30,21 @@ auto AttackerHoldsCriticalTilesVictoryScore(
|
||||
const PlayerInfo* player,
|
||||
const APDCache& apdCache,
|
||||
const ALCache& alCache,
|
||||
const SettingsGetter& settings) -> ScoreValue;
|
||||
const BattalionTypeGetter& battalionTypeGetter,
|
||||
ActionPoints braveWaterCost) -> ScoreValue;
|
||||
|
||||
auto DefenderHoldsCriticalTilesVictoryScore(
|
||||
const GameStateW& gameState,
|
||||
const CoordsSet& criticalTileLocations,
|
||||
const PlayerInfo* player,
|
||||
const APDCache& apdCache,
|
||||
const ALCache& alCache,
|
||||
const SettingsGetter& settings) -> ScoreValue;
|
||||
const PlayerInfo* player) -> ScoreValue;
|
||||
|
||||
auto LastPlayerStandingVictoryScore(
|
||||
const GameStateW& gameState,
|
||||
const PlayerInfo* player,
|
||||
const APDCache& apdCache,
|
||||
const ALCache& alCache,
|
||||
const SettingsGetter& settings) -> ScoreValue;
|
||||
const BattalionTypeGetter& battalionTypeGetter,
|
||||
ActionPoints braveWaterCost) -> ScoreValue;
|
||||
|
||||
} // namespace shardok
|
||||
|
||||
|
||||
@@ -15,7 +15,7 @@ auto UnitIdsRequiringWaterCrossing(
|
||||
const PlayerId pid,
|
||||
const CoordsSet &destinations,
|
||||
const APDCache &apdCache,
|
||||
const SettingsGetter &settings) -> vector<UnitId> {
|
||||
const BattalionTypeGetter &battalionTypeGetter) -> vector<UnitId> {
|
||||
// Put out all the fires, except on bridges
|
||||
fb::HexMapW mapCopy = fb::CopyHexMap(gameState->hex_map());
|
||||
for (uint32_t index = 0; index < mapCopy->terrain()->size(); index++) {
|
||||
@@ -36,7 +36,7 @@ auto UnitIdsRequiringWaterCrossing(
|
||||
for (const auto *unit : *gameState->units()) {
|
||||
if (unit->player_id() != pid) continue;
|
||||
|
||||
const auto &battType = settings.GetBattalionType(unit->battalion().type());
|
||||
const auto &battType = battalionTypeGetter(unit->battalion().type());
|
||||
|
||||
if (unit->status() == net::eagle0::shardok::storage::fb::UnitStatus_NORMAL_UNIT) {
|
||||
for (const Coords &destination : destinations) {
|
||||
@@ -76,8 +76,7 @@ auto UnitIdsRequiringWaterCrossing(
|
||||
auto UnitIdsToCreateWaterCrossing(
|
||||
const GameStateW &gameState,
|
||||
const PlayerId pid,
|
||||
const APDCache & /*apdCache*/,
|
||||
const SettingsGetter &settings) -> vector<UnitId> {
|
||||
const BattalionTypeGetter &battalionTypeGetter) -> vector<UnitId> {
|
||||
vector<UnitId> unitIds{};
|
||||
|
||||
for (const auto *unit : *gameState->units()) {
|
||||
@@ -88,7 +87,7 @@ auto UnitIdsToCreateWaterCrossing(
|
||||
if (!unit->has_attached_hero()) continue;
|
||||
|
||||
const auto profession = unit->attached_hero().profession_info().profession();
|
||||
const auto &battalionType = settings.GetBattalionType(unit->battalion().type());
|
||||
const auto &battalionType = battalionTypeGetter(unit->battalion().type());
|
||||
|
||||
if (profession == net::eagle0::shardok::storage::fb::Profession_ENGINEER ||
|
||||
(profession == net::eagle0::shardok::storage::fb::Profession_MAGE &&
|
||||
@@ -199,14 +198,14 @@ auto IntendedCrossingStarts(
|
||||
const GameStateW &gameState,
|
||||
const vector<UnitId> &unitIdsCreatingCrossing,
|
||||
const CoordsSet &tilesToStartCrossingFrom,
|
||||
const MapId &mapId,
|
||||
const APDCache &apdCache,
|
||||
const SettingsGetter &settings) -> CoordsSet {
|
||||
const BattalionTypeGetter &battalionTypeGetter) -> CoordsSet {
|
||||
CoordsSet intendedCrossingStarts(gameState->hex_map());
|
||||
const MapId mapId = apdCache->GetMapId(gameState->hex_map());
|
||||
for (const UnitId uid : unitIdsCreatingCrossing) {
|
||||
const Unit *unit = gameState->units()->Get(uid);
|
||||
const Coords &location = unit->location();
|
||||
const auto &battalionType = settings.GetBattalionType(unit->battalion().type());
|
||||
const auto &battalionType = battalionTypeGetter(unit->battalion().type());
|
||||
const auto *apd = apdCache->GetRaw(gameState->hex_map(), mapId, battalionType, false);
|
||||
|
||||
if (location.row() >= 0) {
|
||||
@@ -219,4 +218,111 @@ auto IntendedCrossingStarts(
|
||||
return intendedCrossingStarts;
|
||||
}
|
||||
|
||||
using Unit = net::eagle0::shardok::storage::fb::Unit;
|
||||
|
||||
constexpr double kNoRequiredCrossingScore = std::numeric_limits<double>::max();
|
||||
constexpr double kNoCrossingCreatorsScore = std::numeric_limits<double>::min();
|
||||
|
||||
auto WaterCrossingScore(
|
||||
const PlayerId playerId,
|
||||
const BattalionTypeGetter &battalionTypeGetter,
|
||||
const GameStateW &gameState,
|
||||
const CoordsSet &castleCoords,
|
||||
const CoordsSet &startCrossingFrom,
|
||||
const APDCache &apdCache) -> double {
|
||||
uint32_t castleClaimCount = 0;
|
||||
for (const auto *unit : *gameState->units()) {
|
||||
if (unit->player_id() != playerId) continue;
|
||||
const auto status = unit->status();
|
||||
if (status != net::eagle0::shardok::storage::fb::UnitStatus_NORMAL_UNIT &&
|
||||
status != net::eagle0::shardok::storage::fb::UnitStatus_RESERVE_UNIT)
|
||||
continue;
|
||||
if (!unit->has_attached_hero()) continue;
|
||||
|
||||
++castleClaimCount;
|
||||
}
|
||||
|
||||
CoordsSet destinations = castleCoords;
|
||||
if (castleClaimCount < castleCoords.size()) {
|
||||
destinations = CoordsSet(gameState->hex_map());
|
||||
for (const auto *enemyUnit : *gameState->units()) {
|
||||
if (enemyUnit->player_id() == playerId) continue;
|
||||
const auto status = enemyUnit->status();
|
||||
if (status != net::eagle0::shardok::storage::fb::UnitStatus_NORMAL_UNIT) continue;
|
||||
AssertValid(enemyUnit->location(), gameState->hex_map());
|
||||
destinations.Add(enemyUnit->location());
|
||||
}
|
||||
}
|
||||
|
||||
const auto unitIdsRequiringCrossing = UnitIdsRequiringWaterCrossing(
|
||||
gameState,
|
||||
playerId,
|
||||
castleCoords,
|
||||
apdCache,
|
||||
battalionTypeGetter);
|
||||
if (unitIdsRequiringCrossing.empty()) return kNoRequiredCrossingScore;
|
||||
|
||||
const auto unitIdsCreatingCrossing =
|
||||
UnitIdsToCreateWaterCrossing(gameState, playerId, battalionTypeGetter);
|
||||
if (unitIdsCreatingCrossing.empty()) return kNoCrossingCreatorsScore;
|
||||
|
||||
double totalScore = 0;
|
||||
|
||||
const auto mapId = ActionPointDistancesCache::GetMapId(gameState->hex_map());
|
||||
|
||||
// First put a big penalty on the distance for units that can create a crossing
|
||||
for (const UnitId uid : unitIdsCreatingCrossing) {
|
||||
const Unit *unit = gameState->units()->Get(uid);
|
||||
const auto &battalionType = battalionTypeGetter(unit->battalion().type());
|
||||
Coords location = unit->location();
|
||||
|
||||
int thisDistance;
|
||||
if (location.row() < 0) thisDistance = 1000;
|
||||
else {
|
||||
const auto *apd = apdCache->GetRaw(gameState->hex_map(), mapId, battalionType, false);
|
||||
|
||||
thisDistance = MinimumDistance(apd, location, startCrossingFrom);
|
||||
}
|
||||
|
||||
totalScore -= thisDistance * 100.0;
|
||||
}
|
||||
|
||||
// Now a smaller penalty for distance for units that need to cross, except if they block -- then
|
||||
// a large penalty
|
||||
for (const UnitId uid : unitIdsRequiringCrossing) {
|
||||
// If this unit ID can also create a crossing, we already handled it
|
||||
if (std::ranges::contains(unitIdsCreatingCrossing, uid)) continue;
|
||||
|
||||
const Unit *unit = gameState->units()->Get(uid);
|
||||
const auto &battalionType = battalionTypeGetter(unit->battalion().type());
|
||||
Coords location = unit->location();
|
||||
const auto *apd = apdCache->GetRaw(gameState->hex_map(), mapId, battalionType, false);
|
||||
|
||||
int thisDistance;
|
||||
if (location.row() < 0) thisDistance = 1000;
|
||||
else { thisDistance = MinimumDistance(apd, location, startCrossingFrom); }
|
||||
|
||||
bool targetBlocks = false;
|
||||
// If we're not capable of creating a crossing, don't get in the way of somebody that is.
|
||||
for (const UnitId crossingUid : unitIdsCreatingCrossing) {
|
||||
const auto *crossingCapableUnit = gameState->units()->Get(crossingUid);
|
||||
|
||||
// Don't check for units that aren't yet placed
|
||||
if (crossingCapableUnit->location().row() < 0) continue;
|
||||
AssertValid(crossingCapableUnit->location(), gameState->hex_map());
|
||||
|
||||
if (thisDistance <
|
||||
MinimumDistance(apd, crossingCapableUnit->location(), startCrossingFrom)) {
|
||||
targetBlocks = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (targetBlocks) continue;
|
||||
|
||||
totalScore -= thisDistance;
|
||||
}
|
||||
|
||||
return totalScore;
|
||||
}
|
||||
|
||||
} // namespace shardok
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
#ifndef EAGLE0_AIWATERCROSSINGCALCULATOR_HPP
|
||||
#define EAGLE0_AIWATERCROSSINGCALCULATOR_HPP
|
||||
|
||||
#include "src/main/cpp/net/eagle0/shardok/ai/AICommonTypes.hpp"
|
||||
#include "src/main/cpp/net/eagle0/shardok/library/GameStateW.hpp"
|
||||
#include "src/main/cpp/net/eagle0/shardok/library/action_point_distances/ActionPointDistancesCache.hpp"
|
||||
#include "src/main/cpp/net/eagle0/shardok/library/map/CoordsSet.hpp"
|
||||
@@ -34,14 +35,13 @@ auto UnitIdsRequiringWaterCrossing(
|
||||
PlayerId pid,
|
||||
const CoordsSet& destinations,
|
||||
const APDCache& apdCache,
|
||||
const SettingsGetter& settings) -> vector<UnitId>;
|
||||
const BattalionTypeGetter& battalionTypeGetter) -> vector<UnitId>;
|
||||
|
||||
// Units belonging to the player that are capable of creating water crossings
|
||||
auto UnitIdsToCreateWaterCrossing(
|
||||
const GameStateW& gameState,
|
||||
PlayerId pid,
|
||||
const APDCache& apdCache,
|
||||
const SettingsGetter& settings) -> vector<UnitId>;
|
||||
const BattalionTypeGetter& battalionTypeGetter) -> vector<UnitId>;
|
||||
|
||||
// Whether a unit of the given type can reach destination from origin, given the current state
|
||||
// of the map
|
||||
@@ -71,9 +71,17 @@ auto IntendedCrossingStarts(
|
||||
const GameStateW& gameState,
|
||||
const vector<UnitId>& unitIdsCreatingCrossing,
|
||||
const CoordsSet& tilesToStartCrossingFrom,
|
||||
const MapId& mapId,
|
||||
const APDCache& apdCache,
|
||||
const SettingsGetter& settings) -> CoordsSet;
|
||||
const BattalionTypeGetter& battalionTypeGetter) -> CoordsSet;
|
||||
|
||||
// Calculate score based on water crossing strategy
|
||||
auto WaterCrossingScore(
|
||||
PlayerId playerId,
|
||||
const BattalionTypeGetter& battalionTypeGetter,
|
||||
const GameStateW& gameState,
|
||||
const CoordsSet& castleCoords,
|
||||
const CoordsSet& startCrossingFrom,
|
||||
const APDCache& apdCache) -> double;
|
||||
|
||||
} // namespace shardok
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@ constexpr ScoreValue kNoRequiredCrossingScore = std::numeric_limits<ScoreValue>:
|
||||
constexpr ScoreValue kNoCrossingCreatorsScore = std::numeric_limits<ScoreValue>::min();
|
||||
|
||||
[[nodiscard]] auto AIWaterCrossingCommandChooser::WaterCrossingScore(
|
||||
const SettingsGetter &settingsGetter,
|
||||
const BattalionTypeGetter &battalionTypeGetter,
|
||||
const GameStateW &gameState,
|
||||
const CoordsSet &castleCoords,
|
||||
const CoordsSet &startCrossingFrom) const -> ScoreValue {
|
||||
@@ -51,15 +51,13 @@ constexpr ScoreValue kNoCrossingCreatorsScore = std::numeric_limits<ScoreValue>:
|
||||
playerId,
|
||||
castleCoords,
|
||||
apdCache,
|
||||
settingsGetter);
|
||||
battalionTypeGetter);
|
||||
if (unitIdsRequiringCrossing.empty()) return kNoRequiredCrossingScore;
|
||||
|
||||
const auto unitIdsCreatingCrossing =
|
||||
UnitIdsToCreateWaterCrossing(gameState, playerId, apdCache, settingsGetter);
|
||||
UnitIdsToCreateWaterCrossing(gameState, playerId, battalionTypeGetter);
|
||||
if (unitIdsCreatingCrossing.empty()) return kNoCrossingCreatorsScore;
|
||||
|
||||
fprintf(stderr, "%lu units require a water crossing\n", unitIdsRequiringCrossing.size());
|
||||
|
||||
ScoreValue totalScore = 0;
|
||||
|
||||
const auto mapId = ActionPointDistancesCache::GetMapId(gameState->hex_map());
|
||||
@@ -67,7 +65,7 @@ constexpr ScoreValue kNoCrossingCreatorsScore = std::numeric_limits<ScoreValue>:
|
||||
// First put a big penalty on the distance for units that can create a crossing
|
||||
for (const UnitId uid : unitIdsCreatingCrossing) {
|
||||
const Unit *unit = gameState->units()->Get(uid);
|
||||
const auto &battalionType = settingsGetter.GetBattalionType(unit->battalion().type());
|
||||
const auto &battalionType = battalionTypeGetter(unit->battalion().type());
|
||||
Coords location = unit->location();
|
||||
|
||||
int thisDistance;
|
||||
@@ -88,7 +86,7 @@ constexpr ScoreValue kNoCrossingCreatorsScore = std::numeric_limits<ScoreValue>:
|
||||
if (std::ranges::contains(unitIdsCreatingCrossing, uid)) continue;
|
||||
|
||||
const Unit *unit = gameState->units()->Get(uid);
|
||||
const auto &battalionType = settingsGetter.GetBattalionType(unit->battalion().type());
|
||||
const auto &battalionType = battalionTypeGetter(unit->battalion().type());
|
||||
Coords location = unit->location();
|
||||
const auto *apd = apdCache->GetRaw(gameState->hex_map(), mapId, battalionType, false);
|
||||
|
||||
@@ -120,7 +118,7 @@ constexpr ScoreValue kNoCrossingCreatorsScore = std::numeric_limits<ScoreValue>:
|
||||
}
|
||||
|
||||
auto AIWaterCrossingCommandChooser::StartCrossingFrom(
|
||||
const SettingsGetter &settingsGetter,
|
||||
const BattalionTypeGetter &battalionTypeGetter,
|
||||
const GameStateW &gameState,
|
||||
const CoordsSet &castleCoords) const -> CoordsSet {
|
||||
CoordsSet startCrossingFrom(gameState->hex_map());
|
||||
@@ -154,16 +152,16 @@ auto AIWaterCrossingCommandChooser::StartCrossingFrom(
|
||||
playerId,
|
||||
castleCoords,
|
||||
apdCache,
|
||||
settingsGetter);
|
||||
battalionTypeGetter);
|
||||
if (unitIdsRequiringCrossing.empty()) return startCrossingFrom;
|
||||
|
||||
const auto unitIdsCreatingCrossing =
|
||||
UnitIdsToCreateWaterCrossing(gameState, playerId, apdCache, settingsGetter);
|
||||
UnitIdsToCreateWaterCrossing(gameState, playerId, battalionTypeGetter);
|
||||
if (unitIdsCreatingCrossing.empty()) return startCrossingFrom;
|
||||
|
||||
for (const UnitId uid : unitIdsRequiringCrossing) {
|
||||
const Unit *unit = gameState->units()->Get(uid);
|
||||
const auto &battalionType = settingsGetter.GetBattalionType(unit->battalion().type());
|
||||
const auto &battalionType = battalionTypeGetter(unit->battalion().type());
|
||||
Coords origin = unit->location();
|
||||
|
||||
// FIXME: this is just grabbing the first starting position, ideally we'd try them all
|
||||
|
||||
@@ -6,11 +6,10 @@
|
||||
#define EAGLE0_AIWATERCROSSINGCOMMANDCHOOSER_HPP
|
||||
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
#include "src/main/cpp/net/eagle0/shardok/ai/AICommonTypes.hpp"
|
||||
#include "src/main/cpp/net/eagle0/shardok/library/GameStateW.hpp"
|
||||
#include "src/main/cpp/net/eagle0/shardok/library/action_point_distances/ActionPointDistancesCache.hpp"
|
||||
#include "src/main/cpp/net/eagle0/shardok/library/fb_helpers/FlatbufferWrapper.hpp"
|
||||
#include "src/main/cpp/net/eagle0/shardok/library/map/CoordsSet.hpp"
|
||||
#include "src/main/cpp/net/eagle0/shardok/library/settings/GameSettings.hpp"
|
||||
#include "src/main/flatbuffer/net/eagle0/shardok/storage/game_state.hpp"
|
||||
@@ -33,13 +32,13 @@ public:
|
||||
: playerId(pid),
|
||||
apdCache(std::move(apdCache)) {}
|
||||
|
||||
auto StartCrossingFrom(
|
||||
const SettingsGetter &settingsGetter,
|
||||
[[nodiscard]] auto StartCrossingFrom(
|
||||
const BattalionTypeGetter &battalionTypeGetter,
|
||||
const GameStateW &gameState,
|
||||
const CoordsSet &castleCoords) const -> CoordsSet;
|
||||
|
||||
[[nodiscard]] auto WaterCrossingScore(
|
||||
const SettingsGetter &settingsGetter,
|
||||
const BattalionTypeGetter &battalionTypeGetter,
|
||||
const GameStateW &gameState,
|
||||
const CoordsSet &castleCoords,
|
||||
const CoordsSet &startCrossingFrom) const -> ScoreValue;
|
||||
|
||||
@@ -1,5 +1,16 @@
|
||||
load("//tools:copts.bzl", "COPTS")
|
||||
|
||||
cc_library(
|
||||
name = "ai_common_types",
|
||||
hdrs = ["AICommonTypes.hpp"],
|
||||
copts = COPTS,
|
||||
visibility = ["//visibility:public"],
|
||||
deps = [
|
||||
"//src/main/cpp/net/eagle0/shardok/library:battalion_type",
|
||||
"//src/main/cpp/net/eagle0/shardok/library:shardok_c_types",
|
||||
],
|
||||
)
|
||||
|
||||
cc_library(
|
||||
name = "ai_attacker_strategy_selector",
|
||||
srcs = ["AIAttackerStrategySelector.cpp"],
|
||||
@@ -33,9 +44,9 @@ cc_library(
|
||||
],
|
||||
deps = [
|
||||
":ai_attack_locations",
|
||||
":ai_common_types",
|
||||
":ai_score_utilities",
|
||||
"//src/main/cpp/net/eagle0/shardok/library/action_point_distances:action_point_distances_cache",
|
||||
"//src/main/cpp/net/eagle0/shardok/library/settings:game_settings",
|
||||
"//src/main/cpp/net/eagle0/shardok/library/util:hex_map_utils",
|
||||
"//src/main/flatbuffer/net/eagle0/shardok/storage:hex_map_cc_fbs",
|
||||
"//src/main/flatbuffer/net/eagle0/shardok/storage:unit_cc_fbs",
|
||||
@@ -90,6 +101,7 @@ cc_library(
|
||||
],
|
||||
deps = [
|
||||
":ai_attack_locations",
|
||||
":ai_common_types",
|
||||
":ai_score_utilities",
|
||||
"//src/main/cpp/net/eagle0/shardok/library/action_point_distances",
|
||||
"//src/main/cpp/net/eagle0/shardok/library/action_point_distances:action_point_distances_cache",
|
||||
@@ -142,12 +154,35 @@ cc_library(
|
||||
":ai_score_utilities",
|
||||
":ai_unit_score_calculator",
|
||||
"//src/main/cpp/net/eagle0/shardok/library:engine",
|
||||
"//src/main/cpp/net/eagle0/shardok/library/settings:game_settings",
|
||||
"//src/main/cpp/net/eagle0/shardok/library/util:hex_map_utils",
|
||||
"//src/main/protobuf/net/eagle0/shardok/api:command_descriptor_cc_proto",
|
||||
],
|
||||
)
|
||||
|
||||
cc_library(
|
||||
name = "ai_command_evaluator",
|
||||
srcs = ["AICommandEvaluator.cpp"],
|
||||
hdrs = ["AICommandEvaluator.hpp"],
|
||||
copts = COPTS,
|
||||
visibility = [
|
||||
"//src/main/cpp/net/eagle0/shardok/ai_performance_runner:__pkg__",
|
||||
"//src/test/cpp/net/eagle0/shardok/ai:__pkg__",
|
||||
],
|
||||
deps = [
|
||||
":ai_command_filter",
|
||||
":ai_score_calculator_interface",
|
||||
":ai_strategy",
|
||||
":transposition_table",
|
||||
"//src/main/cpp/net/eagle0/common:sequence_random_generator",
|
||||
"//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/map:coords_set",
|
||||
"//src/main/cpp/net/eagle0/shardok/library/util:hex_cube_utils",
|
||||
"//src/main/protobuf/net/eagle0/shardok/api:command_descriptor_cc_proto",
|
||||
"//src/main/protobuf/net/eagle0/shardok/common:command_type_cc_proto",
|
||||
],
|
||||
)
|
||||
|
||||
cc_library(
|
||||
name = "ai_command_filter",
|
||||
srcs = ["AICommandFilter.cpp"],
|
||||
@@ -155,10 +190,12 @@ cc_library(
|
||||
copts = COPTS,
|
||||
visibility = [
|
||||
"//src/main/cpp/net/eagle0/shardok/ai/mcts:__pkg__",
|
||||
"//src/main/cpp/net/eagle0/shardok/ai/mcts/adapters:__pkg__",
|
||||
"//src/main/cpp/net/eagle0/shardok/ai_performance_runner:__pkg__",
|
||||
"//src/test/cpp/net/eagle0/shardok/ai:__pkg__",
|
||||
],
|
||||
deps = [
|
||||
":ai_common_types",
|
||||
"//src/main/cpp/net/eagle0/shardok/library:engine",
|
||||
"//src/main/cpp/net/eagle0/shardok/library/action_point_distances:action_point_distances_cache",
|
||||
"//src/main/cpp/net/eagle0/shardok/library/settings:game_settings",
|
||||
@@ -184,24 +221,47 @@ cc_library(
|
||||
)
|
||||
|
||||
cc_library(
|
||||
name = "ai_score_calculator",
|
||||
srcs = ["AIScoreCalculator.cpp"],
|
||||
name = "ai_score_calculator_interface",
|
||||
hdrs = ["AIScoreCalculator.hpp"],
|
||||
copts = COPTS,
|
||||
visibility = [
|
||||
"//src/main/cpp/net/eagle0/shardok/ai/mcts:__pkg__",
|
||||
"//src/main/cpp/net/eagle0/shardok/ai/mcts/adapters:__pkg__",
|
||||
"//src/main/cpp/net/eagle0/shardok/ai_performance_runner:__pkg__",
|
||||
"//src/test/cpp/net/eagle0/shardok/ai:__pkg__",
|
||||
],
|
||||
deps = [
|
||||
":ai_attacker_strategy_selector",
|
||||
":ai_command_filter",
|
||||
":ai_attack_locations",
|
||||
"//src/main/cpp/net/eagle0/shardok/library:game_state_w",
|
||||
"//src/main/cpp/net/eagle0/shardok/library:shardok_c_types",
|
||||
"//src/main/cpp/net/eagle0/shardok/library/action_point_distances:action_point_distances_cache",
|
||||
"//src/main/cpp/net/eagle0/shardok/library/map:coords_set",
|
||||
"//src/main/protobuf/net/eagle0/shardok/api:command_descriptor_cc_proto",
|
||||
],
|
||||
)
|
||||
|
||||
cc_library(
|
||||
name = "standard_ai_score_calculator",
|
||||
srcs = ["StandardAIScoreCalculator.cpp"],
|
||||
hdrs = ["StandardAIScoreCalculator.hpp"],
|
||||
copts = COPTS,
|
||||
visibility = [
|
||||
"//src/main/cpp/net/eagle0/shardok/ai/mcts:__pkg__",
|
||||
"//src/main/cpp/net/eagle0/shardok/ai/mcts/adapters:__pkg__",
|
||||
"//src/main/cpp/net/eagle0/shardok/ai_performance_runner:__pkg__",
|
||||
"//src/test/cpp/net/eagle0/shardok/ai:__pkg__",
|
||||
"//src/test/cpp/net/eagle0/shardok/ai/mcts:__pkg__",
|
||||
],
|
||||
deps = [
|
||||
":ai_score_calculator_interface",
|
||||
":ai_strategy",
|
||||
":ai_unit_score_calculator",
|
||||
":ai_victory_condition_score_calculator",
|
||||
":transposition_table",
|
||||
"//src/main/cpp/net/eagle0/common:sequence_random_generator",
|
||||
":ai_water_crossing_calculator",
|
||||
"//src/main/cpp/net/eagle0/shardok/library:engine",
|
||||
"//src/main/cpp/net/eagle0/shardok/library/view_filters:game_state_guesser",
|
||||
"//src/main/cpp/net/eagle0/shardok/library:game_state_w",
|
||||
"//src/main/cpp/net/eagle0/shardok/library/map:coords_set",
|
||||
"//src/main/cpp/net/eagle0/shardok/library/settings:game_settings",
|
||||
],
|
||||
)
|
||||
|
||||
@@ -212,11 +272,13 @@ cc_library(
|
||||
copts = COPTS,
|
||||
visibility = [
|
||||
"//src/main/cpp/net/eagle0/shardok/ai/mcts:__pkg__",
|
||||
"//src/main/cpp/net/eagle0/shardok/ai/mcts/adapters:__pkg__",
|
||||
"//src/main/cpp/net/eagle0/shardok/ai_performance_runner:__pkg__",
|
||||
"//src/test/cpp/net/eagle0/shardok/ai:__subpackages__",
|
||||
],
|
||||
deps = [
|
||||
":ai_attack_groups",
|
||||
"//src/main/cpp/net/eagle0/shardok/library/map:coords_set",
|
||||
],
|
||||
)
|
||||
|
||||
@@ -248,6 +310,7 @@ cc_library(
|
||||
deps = [
|
||||
":ai_attack_groups",
|
||||
":ai_attack_locations",
|
||||
":ai_common_types",
|
||||
":ai_distance_debuf",
|
||||
":ai_score_utilities",
|
||||
"//src/main/cpp/net/eagle0/shardok/library:game_state_w",
|
||||
@@ -267,7 +330,9 @@ cc_library(
|
||||
"//src/test/cpp/net/eagle0/shardok/ai:__subpackages__",
|
||||
],
|
||||
deps = [
|
||||
":ai_common_types",
|
||||
":ai_minimum_distance_and_target",
|
||||
":ai_score_calculator_interface",
|
||||
"//src/main/cpp/net/eagle0/shardok/library:game_state_w",
|
||||
"//src/main/cpp/net/eagle0/shardok/library/action_point_distances",
|
||||
"//src/main/cpp/net/eagle0/shardok/library/action_point_distances:action_point_distances_cache",
|
||||
@@ -324,8 +389,9 @@ cc_library(
|
||||
],
|
||||
deps = [
|
||||
":ai_attacker_strategy_selector",
|
||||
":ai_command_evaluator",
|
||||
":ai_defender_strategy_selector",
|
||||
":ai_score_calculator",
|
||||
":ai_score_calculator_interface",
|
||||
":ai_time_budget",
|
||||
":ai_water_crossing_command_chooser",
|
||||
"//src/main/cpp/net/eagle0/common:time_utils",
|
||||
@@ -357,13 +423,15 @@ cc_library(
|
||||
":ai_defender_strategy_selector",
|
||||
":ai_flee_decision_calculator",
|
||||
":ai_iterative_deepening", # Direct dependency for runtime selection
|
||||
":ai_score_calculator",
|
||||
":ai_score_calculator_interface",
|
||||
":ai_time_budget",
|
||||
":ai_water_crossing_command_chooser",
|
||||
":standard_ai_score_calculator",
|
||||
"//src/main/cpp/net/eagle0/common:time_utils",
|
||||
"//src/main/cpp/net/eagle0/shardok/ai/mcts:mcts_ai", # Direct dependency for runtime selection
|
||||
"//src/main/cpp/net/eagle0/shardok/ai/mcts:shardok_mcts_ai", # MCTS with abstraction layer
|
||||
"//src/main/cpp/net/eagle0/shardok/library/action_point_distances",
|
||||
"//src/main/cpp/net/eagle0/shardok/library/action_point_distances:action_point_distances_cache",
|
||||
"//src/main/cpp/net/eagle0/shardok/library/util:game_state_dumper",
|
||||
"@com_google_protobuf//:protobuf",
|
||||
],
|
||||
)
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
#include <utility>
|
||||
|
||||
#include "AIAttackerStrategySelector.hpp"
|
||||
#include "AICommandEvaluator.hpp"
|
||||
#include "AIScoreCalculator.hpp"
|
||||
#include "TranspositionTable.hpp"
|
||||
#include "src/main/cpp/net/eagle0/shardok/library/ShardokEngine.hpp"
|
||||
@@ -23,14 +24,16 @@ IterativeDeepeningAI::IterativeDeepeningAI(
|
||||
const bool isDefender,
|
||||
AIStrategy strategy,
|
||||
const CoordsSet& castleCoords,
|
||||
const AIScoreCalculator& scorer,
|
||||
const APDCache& apdCache,
|
||||
const ALCache& alCache)
|
||||
BattalionTypeGetter battalionTypeGetter)
|
||||
: playerId(playerId),
|
||||
isDefender(isDefender),
|
||||
strategy(std::move(strategy)),
|
||||
castleCoords(castleCoords),
|
||||
scorer(scorer),
|
||||
apdCache(apdCache),
|
||||
alCache(alCache) {}
|
||||
battalionTypeGetter(std::move(battalionTypeGetter)) {} // Move the function object
|
||||
|
||||
auto IterativeDeepeningAI::IterativeSearch(
|
||||
const GameSettingsSPtr& settings,
|
||||
@@ -67,14 +70,8 @@ auto IterativeDeepeningAI::IterativeSearch(
|
||||
const auto& settingsGetter = settings->GetGetter();
|
||||
const auto guessedEngine = ShardokEngine(settings, state);
|
||||
const auto maxRepeatCount = settingsGetter.Backing().ai_utility_repeat_count();
|
||||
const ScoreValue currentUtility = AIScoreCalculator::GuessedStateScore(
|
||||
isDefender,
|
||||
state,
|
||||
strategy,
|
||||
castleCoords,
|
||||
settingsGetter,
|
||||
apdCache,
|
||||
alCache);
|
||||
const ScoreValue currentUtility =
|
||||
scorer.GuessedStateScore(isDefender, state, strategy, castleCoords);
|
||||
|
||||
// Initialize data structures for tracking scores at each depth
|
||||
scoresByDepth.clear();
|
||||
@@ -111,7 +108,7 @@ auto IterativeDeepeningAI::IterativeSearch(
|
||||
|
||||
auto future = SearchCommandAtDepthWithEngine(
|
||||
guessedEngine,
|
||||
settingsGetter,
|
||||
scorer,
|
||||
maxRepeatCount,
|
||||
commands,
|
||||
cmdIndex,
|
||||
@@ -270,7 +267,7 @@ bool IterativeDeepeningAI::IsTimeExpired(const AITimeBudget& budget) {
|
||||
|
||||
auto IterativeDeepeningAI::SearchCommandAtDepthWithEngine(
|
||||
const ShardokEngine& guessedEngine,
|
||||
const GameSettings::Getter& settingsGetter,
|
||||
const AIScoreCalculator& scorer,
|
||||
const int maxRepeatCount,
|
||||
const std::vector<CommandProto>& commands,
|
||||
const size_t commandIndex,
|
||||
@@ -292,55 +289,47 @@ auto IterativeDeepeningAI::SearchCommandAtDepthWithEngine(
|
||||
return p.get_future();
|
||||
}
|
||||
|
||||
try {
|
||||
// Track concurrent evaluations and adjust time accounting
|
||||
AIEvaluationCounter counter;
|
||||
const auto startTime = std::chrono::steady_clock::now();
|
||||
// Track concurrent evaluations and adjust time accounting
|
||||
AIEvaluationCounter counter;
|
||||
const auto startTime = std::chrono::steady_clock::now();
|
||||
|
||||
// Calculate deadline from remaining time budget
|
||||
const auto deadline = startTime + timeBudget.remainingBudget;
|
||||
// Calculate deadline from remaining time budget
|
||||
const auto deadline = startTime + timeBudget.remainingBudget;
|
||||
|
||||
// Get the future from CommandScore - don't wait yet
|
||||
// Note: CommandScore expects remainingLookahead, not desiredDepth
|
||||
// desiredDepth 1 = evaluate immediate (remainingLookahead 0)
|
||||
// desiredDepth 2 = look 1 move ahead (remainingLookahead 1)
|
||||
// desiredDepth N = look N-1 moves ahead (remainingLookahead N-1)
|
||||
auto commandScoreFuture = AIScoreCalculator::CommandScore(
|
||||
playerId,
|
||||
isDefender,
|
||||
desiredDepth - 1, // Convert desiredDepth to remainingLookahead
|
||||
maxRepeatCount,
|
||||
guessedEngine,
|
||||
strategy,
|
||||
currentUtility,
|
||||
settingsGetter,
|
||||
castleCoords,
|
||||
apdCache,
|
||||
alCache,
|
||||
commandIndex,
|
||||
deadline);
|
||||
// Create command evaluator for lookahead search
|
||||
AICommandEvaluator evaluator(scorer, apdCache, battalionTypeGetter);
|
||||
|
||||
// Calculate time and adjust budget before waiting
|
||||
// This is needed because we need to update timeBudget synchronously
|
||||
const auto commandScore = commandScoreFuture.get();
|
||||
// Get the future from EvaluateCommand - don't wait yet
|
||||
// Note: EvaluateCommand expects remainingLookahead, not desiredDepth
|
||||
// desiredDepth 1 = evaluate immediate (remainingLookahead 0)
|
||||
// desiredDepth 2 = look 1 move ahead (remainingLookahead 1)
|
||||
// desiredDepth N = look N-1 moves ahead (remainingLookahead N-1)
|
||||
auto commandScoreFuture = evaluator.EvaluateCommand(
|
||||
playerId,
|
||||
isDefender,
|
||||
desiredDepth - 1, // Convert desiredDepth to remainingLookahead
|
||||
maxRepeatCount,
|
||||
guessedEngine,
|
||||
strategy,
|
||||
currentUtility,
|
||||
castleCoords,
|
||||
commandIndex,
|
||||
deadline);
|
||||
|
||||
const auto elapsed = std::chrono::steady_clock::now() - startTime;
|
||||
const int concurrentCount = AIEvaluationCounter::GetCurrentCount();
|
||||
const auto adjustedElapsed = elapsed / std::max(1, concurrentCount);
|
||||
const auto adjustedElapsedMs =
|
||||
std::chrono::duration_cast<std::chrono::milliseconds>(adjustedElapsed);
|
||||
// Calculate time and adjust budget before waiting
|
||||
// This is needed because we need to update timeBudget synchronously
|
||||
const auto commandScore = commandScoreFuture.get();
|
||||
|
||||
// Deduct adjusted time from remaining budget
|
||||
timeBudget.remainingBudget -= adjustedElapsedMs;
|
||||
const auto elapsed = std::chrono::steady_clock::now() - startTime;
|
||||
const int concurrentCount = AIEvaluationCounter::GetCurrentCount();
|
||||
const auto adjustedElapsed = elapsed / std::max(1, concurrentCount);
|
||||
const auto adjustedElapsedMs =
|
||||
std::chrono::duration_cast<std::chrono::milliseconds>(adjustedElapsed);
|
||||
|
||||
result.bestScore = commandScore;
|
||||
} catch (const std::exception& e) {
|
||||
// If evaluation fails, return a neutral score rather than crashing
|
||||
#if DEBUG_ITERATIVE_DEEPENING_TIMINGS
|
||||
printf("SearchCommandAtDepthWithEngine: evaluation failed with exception: %s\n", e.what());
|
||||
#endif
|
||||
result.bestScore = 0.0;
|
||||
}
|
||||
// Deduct adjusted time from remaining budget
|
||||
timeBudget.remainingBudget -= adjustedElapsedMs;
|
||||
|
||||
result.bestScore = commandScore;
|
||||
|
||||
std::promise<SearchResult> p;
|
||||
p.set_value(result);
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
#include <future>
|
||||
#include <vector>
|
||||
|
||||
#include "AIScoreCalculator.hpp"
|
||||
#include "AIStrategy.hpp"
|
||||
#include "AITimeBudget.hpp"
|
||||
#include "src/main/cpp/net/eagle0/shardok/ai/AIAttackLocations.hpp"
|
||||
@@ -23,6 +24,7 @@ namespace shardok {
|
||||
class ShardokEngine;
|
||||
using ScoreValue = double;
|
||||
using CommandProto = net::eagle0::shardok::api::CommandDescriptor;
|
||||
using BattalionTypeGetter = std::function<BattalionTypeSPtr(BattalionTypeId)>;
|
||||
|
||||
/// Reason why AI evaluation completed at the achieved depth.
|
||||
enum class EvaluationCompletionReason {
|
||||
@@ -61,8 +63,9 @@ public:
|
||||
bool isDefender,
|
||||
AIStrategy strategy,
|
||||
const CoordsSet& castleCoords,
|
||||
const AIScoreCalculator& scorer,
|
||||
const APDCache& apdCache,
|
||||
const ALCache& alCache);
|
||||
BattalionTypeGetter battalionTypeGetter); // Pass by value
|
||||
|
||||
[[nodiscard]] SearchResult IterativeSearch(
|
||||
const GameSettingsSPtr& settings,
|
||||
@@ -75,8 +78,9 @@ private:
|
||||
bool isDefender;
|
||||
AIStrategy strategy;
|
||||
CoordsSet castleCoords;
|
||||
const AIScoreCalculator& scorer;
|
||||
const APDCache& apdCache;
|
||||
const ALCache& alCache;
|
||||
BattalionTypeGetter battalionTypeGetter; // Store by value, not reference!
|
||||
|
||||
// Reusable vectors to reduce memory allocations
|
||||
mutable std::vector<std::vector<ScoreValue>> scoresByDepth;
|
||||
@@ -87,7 +91,7 @@ private:
|
||||
|
||||
[[nodiscard]] std::future<SearchResult> SearchCommandAtDepthWithEngine(
|
||||
const ShardokEngine& guessedEngine,
|
||||
const GameSettings::Getter& settingsGetter,
|
||||
const AIScoreCalculator& scorer,
|
||||
int maxRepeatCount,
|
||||
const std::vector<CommandProto>& commands,
|
||||
size_t commandIndex,
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -13,14 +13,14 @@
|
||||
#include <google/protobuf/util/message_differencer.h>
|
||||
|
||||
#include "AIAttackerStrategySelector.hpp"
|
||||
#include "AIConfig.hpp" // Must come before other AI includes
|
||||
#include "AIConfig.hpp"
|
||||
#include "AIDefenderStrategySelector.hpp"
|
||||
#include "AIFleeDecisionCalculator.hpp"
|
||||
#include "AIScoreUtilities.hpp"
|
||||
#include "AITimeBudget.hpp"
|
||||
#include "IterativeDeepeningAI.hpp"
|
||||
#include "mcts/MCTSAI.hpp"
|
||||
#include "src/main/cpp/net/eagle0/common/TimeUtils.hpp"
|
||||
#include "StandardAIScoreCalculator.hpp"
|
||||
#include "mcts/ShardokMCTSAI.hpp"
|
||||
#include "src/main/cpp/net/eagle0/shardok/library/util/HexMapUtils.hpp"
|
||||
#include "src/main/cpp/net/eagle0/shardok/library/view_filters/GameStateGuesser.hpp"
|
||||
#include "src/main/protobuf/net/eagle0/shardok/api/action_result_view.pb.h"
|
||||
@@ -108,20 +108,33 @@ auto ShardokAIClient::StandardChooseCommandIndex(
|
||||
CheckCommand(realAvailableCommands[i], guessedCommands[i]);
|
||||
}
|
||||
|
||||
// Extract values directly from settings for strategy selection
|
||||
const auto maxRounds = settingsGetter.Backing().max_rounds();
|
||||
const auto braveWaterCost = settingsGetter.Backing().brave_water_action_point_cost();
|
||||
const auto battalionTypeGetter = [&settingsGetter](BattalionTypeId typeId) {
|
||||
return settingsGetter.GetBattalionType(typeId);
|
||||
};
|
||||
|
||||
// Create scorer for actual scoring during search
|
||||
const auto scorer = MakeStandardAIScoreCalculator(settingsGetter, apdCache, alCache);
|
||||
|
||||
// Determine strategy once for consistent scoring throughout iterative deepening
|
||||
const auto castleCoords = AllCastleCoords(guessedState->hex_map());
|
||||
const AIStrategy strategy = isDefender ? AIDefenderStrategySelector::BestDefenderStrategy(
|
||||
guessedState,
|
||||
castleCoords,
|
||||
maxRounds,
|
||||
apdCache,
|
||||
settingsGetter)
|
||||
battalionTypeGetter)
|
||||
: AIAttackerStrategySelector::BestAttackerStrategy(
|
||||
playerId,
|
||||
guessedState,
|
||||
castleCoords,
|
||||
maxRounds,
|
||||
apdCache,
|
||||
alCache,
|
||||
settingsGetter,
|
||||
battalionTypeGetter,
|
||||
braveWaterCost,
|
||||
waterCrossingCommandChooser,
|
||||
realAvailableCommands);
|
||||
|
||||
@@ -129,12 +142,19 @@ auto ShardokAIClient::StandardChooseCommandIndex(
|
||||
IterativeDeepeningAI::SearchResult search_result;
|
||||
|
||||
if (aiAlgorithmType == AIAlgorithmType::MCTS) {
|
||||
// Using Monte Carlo Tree Search AI
|
||||
MCTSAI ai(playerId, isDefender, strategy, castleCoords, apdCache, alCache);
|
||||
search_result = ai.Search(settings, guessedState, realAvailableCommands, timeBudget);
|
||||
// Using Monte Carlo Tree Search AI (with abstraction layer)
|
||||
ShardokMCTSAI ai(playerId, isDefender, strategy, castleCoords, *scorer, apdCache, alCache);
|
||||
search_result = ai.Search(settings, guessedState, timeBudget);
|
||||
} else {
|
||||
// Using Iterative Deepening AI (default)
|
||||
IterativeDeepeningAI ai(playerId, isDefender, strategy, castleCoords, apdCache, alCache);
|
||||
IterativeDeepeningAI ai(
|
||||
playerId,
|
||||
isDefender,
|
||||
strategy,
|
||||
castleCoords,
|
||||
*scorer,
|
||||
apdCache,
|
||||
battalionTypeGetter);
|
||||
search_result =
|
||||
ai.IterativeSearch(settings, guessedState, realAvailableCommands, timeBudget);
|
||||
}
|
||||
@@ -153,9 +173,11 @@ auto ShardokAIClient::StandardChooseCommandIndex(
|
||||
result.commandCountEvaluated,
|
||||
result.availableCommandCount);
|
||||
}
|
||||
printf("ID AI: Search complete - achieved depth %d for best command %zu\n",
|
||||
const auto chosenCommandType = realAvailableCommands[result.chosenIndex].type();
|
||||
printf("ID AI: Search complete - achieved depth %d for best command %zu (%s)\n",
|
||||
result.depthAchieved,
|
||||
result.chosenIndex);
|
||||
result.chosenIndex,
|
||||
net::eagle0::shardok::common::CommandType_Name(chosenCommandType).c_str());
|
||||
|
||||
fflush(stdout);
|
||||
}
|
||||
@@ -201,13 +223,21 @@ auto ShardokAIClient::FinalRoundAttackerChooseCommandIndex(
|
||||
return LateRoundAttackerChooseCommandIndex(settings, guessedState, realAvailableCommands);
|
||||
}
|
||||
|
||||
// Extract values directly from settings for flee decision evaluation
|
||||
const auto settingsGetter = settings->GetGetter();
|
||||
const auto maxRounds = settingsGetter.Backing().max_rounds();
|
||||
const auto minimumFleeOddsThreshold = settingsGetter.Backing().ai_minimum_flee_odds_threshold();
|
||||
const auto desperateFleeThreshold = settingsGetter.Backing().ai_desperate_flee_threshold();
|
||||
|
||||
// Use the flee decision calculator
|
||||
const auto fleeDecision = AIFleeDecisionCalculator::EvaluateFleeVsFight(
|
||||
playerId,
|
||||
settings->GetGetter(),
|
||||
guessedState,
|
||||
realAvailableCommands,
|
||||
fleeCommand,
|
||||
maxRounds,
|
||||
minimumFleeOddsThreshold,
|
||||
desperateFleeThreshold,
|
||||
#ifdef DEBUG_FLEE_DECISIONS
|
||||
true // Enable debug logging
|
||||
#else
|
||||
|
||||
@@ -0,0 +1,826 @@
|
||||
//
|
||||
// Standard implementation of AIScoreCalculator
|
||||
//
|
||||
|
||||
#include "StandardAIScoreCalculator.hpp"
|
||||
|
||||
#include <atomic>
|
||||
#include <chrono>
|
||||
#include <future>
|
||||
#include <unordered_map>
|
||||
|
||||
#include "AIAttackGroups.hpp"
|
||||
#include "AIAttackLocations.hpp"
|
||||
#include "AIScoreCalculator.hpp"
|
||||
#include "AIScoreUtilities.hpp"
|
||||
#include "AIStrategy.hpp"
|
||||
#include "AIUnitScoreCalculator.hpp"
|
||||
#include "AIVictoryConditionScoreCalculator.hpp"
|
||||
#include "AIWaterCrossingCalculator.hpp"
|
||||
#include "src/main/cpp/net/eagle0/shardok/library/ShardokEngine.hpp"
|
||||
#include "src/main/cpp/net/eagle0/shardok/library/action_point_distances/ActionPointDistancesCache.hpp"
|
||||
#include "src/main/cpp/net/eagle0/shardok/library/settings/GameSettings.hpp"
|
||||
#include "src/main/cpp/net/eagle0/shardok/library/util/HexCubeUtils.hpp"
|
||||
#include "src/main/cpp/net/eagle0/shardok/library/util/HexMapUtils.hpp"
|
||||
#include "src/main/protobuf/net/eagle0/shardok/api/unit_view.pb.h"
|
||||
|
||||
namespace shardok {
|
||||
|
||||
using net::eagle0::shardok::storage::fb::BattalionTypeId;
|
||||
|
||||
// Forward declare the implementation class
|
||||
class StandardAIScoreCalculator;
|
||||
|
||||
// Anonymous namespace for helper functions that don't need access to scorer
|
||||
namespace {
|
||||
|
||||
#define LOGGING_ 0
|
||||
#define PERFORMANCE_LOGGING_ 0
|
||||
|
||||
// Performance logging for AttackerScoreForState
|
||||
struct AttackerScorePerformanceLogger {
|
||||
static constexpr int LOG_INTERVAL = 100000;
|
||||
|
||||
static std::atomic<int> callCount;
|
||||
static std::atomic<double> intervalTime;
|
||||
static std::atomic<double> totalTime;
|
||||
|
||||
static void LogCall(double duration) {
|
||||
callCount.fetch_add(1);
|
||||
intervalTime.fetch_add(duration);
|
||||
totalTime.fetch_add(duration);
|
||||
|
||||
if (callCount.load() % LOG_INTERVAL == 0) {
|
||||
double intervalAvg = intervalTime.load() / LOG_INTERVAL;
|
||||
double overallAvg = totalTime.load() / callCount.load();
|
||||
printf("AttackerScoreForState: %d calls, last %d avg: %.1f µs, overall avg: %.1f µs\n",
|
||||
callCount.load(),
|
||||
LOG_INTERVAL,
|
||||
intervalAvg * 1000000.0,
|
||||
overallAvg * 1000000.0);
|
||||
intervalTime.store(0.0); // Reset for next interval
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
std::atomic<int> AttackerScorePerformanceLogger::callCount{0};
|
||||
std::atomic<double> AttackerScorePerformanceLogger::intervalTime{0.0};
|
||||
std::atomic<double> AttackerScorePerformanceLogger::totalTime{0.0};
|
||||
|
||||
// RAII timer for automatic performance logging
|
||||
class AttackerScoreTimer {
|
||||
private:
|
||||
std::chrono::high_resolution_clock::time_point startTime;
|
||||
|
||||
public:
|
||||
AttackerScoreTimer() : startTime(std::chrono::high_resolution_clock::now()) {}
|
||||
|
||||
~AttackerScoreTimer() {
|
||||
auto endTime = std::chrono::high_resolution_clock::now();
|
||||
auto duration =
|
||||
std::chrono::duration_cast<std::chrono::duration<double>>(endTime - startTime);
|
||||
AttackerScorePerformanceLogger::LogCall(duration.count());
|
||||
}
|
||||
};
|
||||
|
||||
// Memoization cache for EffectiveDistance calls
|
||||
struct EffectiveDistanceCache {
|
||||
struct CacheKey {
|
||||
UnitId unitId;
|
||||
Coords target;
|
||||
bool operator==(const CacheKey &other) const {
|
||||
return unitId == other.unitId && target == other.target;
|
||||
}
|
||||
};
|
||||
|
||||
struct CacheKeyHash {
|
||||
size_t operator()(const CacheKey &key) const {
|
||||
return std::hash<UnitId>{}(key.unitId) ^ (std::hash<int>{}(key.target.row()) << 1) ^
|
||||
(std::hash<int>{}(key.target.column()) << 2);
|
||||
}
|
||||
};
|
||||
|
||||
mutable gtl::flat_hash_map<CacheKey, DIST_T, CacheKeyHash> cache;
|
||||
|
||||
DIST_T GetOrCompute(
|
||||
const Unit *unit,
|
||||
const Coords &target,
|
||||
const ActionPointDistances *notBravingApd,
|
||||
const ActionPointDistances *bravingApd,
|
||||
const HexMap *hexMap) const {
|
||||
CacheKey key{unit->unit_id(), target};
|
||||
auto it = cache.find(key);
|
||||
if (it != cache.end()) { return it->second; }
|
||||
|
||||
CoordsSet targetSet(hexMap);
|
||||
targetSet.Add(target);
|
||||
|
||||
DIST_T result = EffectiveDistance(unit, notBravingApd, bravingApd, targetSet);
|
||||
cache[key] = result;
|
||||
return result;
|
||||
}
|
||||
};
|
||||
|
||||
#define MULTITHREAD true
|
||||
|
||||
constexpr double UNITS_BASE_MULTIPLIER = 0.05;
|
||||
|
||||
constexpr double FLEE_UNIT_SCORE = -10000;
|
||||
constexpr double FLEE_CONTROLLING_UNIT_SCORE = -10000;
|
||||
constexpr double CAPTURED_UNIT_SCORE = -10000;
|
||||
constexpr double CAPTURED_VIP_SCORE = -25000;
|
||||
|
||||
constexpr double kMaxProximityBuf = 1.5;
|
||||
constexpr double kDistanceDebufRatio = 8.0;
|
||||
|
||||
using std::async;
|
||||
using std::future;
|
||||
|
||||
using flatbuffers::FlatBufferBuilder;
|
||||
using flatbuffers::Offset;
|
||||
|
||||
using net::eagle0::shardok::api::HeroView;
|
||||
using net::eagle0::shardok::api::UnitView;
|
||||
using GameState = fb::GameState;
|
||||
using Unit = fb::Unit;
|
||||
|
||||
static auto RecursiveAttackerMultiplierForTargetDistance(
|
||||
const Unit *attackingUnit,
|
||||
vector<TargetAndAttackLocations>::const_iterator &priorityListNext,
|
||||
const vector<TargetAndAttackLocations>::const_iterator &priorityListEnd,
|
||||
const vector<const Unit *> &occupants,
|
||||
const HexMap *map,
|
||||
const BattalionTypeSPtr &battType,
|
||||
const ActionPointDistances *notBravingApd,
|
||||
const ActionPointDistances *bravingApd,
|
||||
bool isLateGame) -> double;
|
||||
|
||||
static auto RecursiveAttackerMultiplierForTargetDistance(
|
||||
const Unit *attackingUnit,
|
||||
vector<TargetAndAttackLocations>::const_iterator &priorityListNext,
|
||||
const vector<TargetAndAttackLocations>::const_iterator &priorityListEnd,
|
||||
const vector<const Unit *> &occupants,
|
||||
const HexMap *map,
|
||||
const BattalionTypeSPtr &battType,
|
||||
const ActionPointDistances *notBravingApd,
|
||||
const ActionPointDistances *bravingApd,
|
||||
const bool isLateGame) -> double {
|
||||
if (priorityListNext == priorityListEnd) return 1.0;
|
||||
|
||||
const auto &[target, attackLocations] = *priorityListNext;
|
||||
const Coords &topPriorityTarget = target;
|
||||
|
||||
// If the target is unoccupied or is occupied by this player, give the maximum multiplier, but
|
||||
// also add the bonus for the next up in the priority list
|
||||
if (const Unit *occupant = occupants
|
||||
[topPriorityTarget.row() * map->column_count() + topPriorityTarget.column()];
|
||||
!occupant || occupant->player_id() == attackingUnit->player_id()) {
|
||||
return kMaxProximityBuf + RecursiveAttackerMultiplierForTargetDistance(
|
||||
attackingUnit,
|
||||
++priorityListNext,
|
||||
priorityListEnd,
|
||||
occupants,
|
||||
map,
|
||||
battType,
|
||||
notBravingApd,
|
||||
bravingApd,
|
||||
isLateGame);
|
||||
}
|
||||
|
||||
// Use optimized EffectiveDistance with pre-computed ActionPointDistances
|
||||
// attackLocations is already the CoordsSet of attack locations for this target
|
||||
const DIST_T distance =
|
||||
EffectiveDistance(attackingUnit, notBravingApd, bravingApd, attackLocations);
|
||||
|
||||
return kMaxProximityBuf / (1 + distance / kDistanceDebufRatio);
|
||||
}
|
||||
|
||||
// Overload that accepts pre-computed ActionPointDistances
|
||||
auto AttackerMultiplierForTargetDistance(
|
||||
const Unit *attackingUnit,
|
||||
const vector<TargetAndAttackLocations> &priorityList,
|
||||
const vector<const Unit *> &occupants,
|
||||
const HexMap *map,
|
||||
const BattalionTypeSPtr &battType,
|
||||
const ActionPointDistances *notBravingApd,
|
||||
const ActionPointDistances *bravingApd,
|
||||
const bool isLateGame) -> double {
|
||||
auto iter = begin(priorityList);
|
||||
return RecursiveAttackerMultiplierForTargetDistance(
|
||||
attackingUnit,
|
||||
iter,
|
||||
end(priorityList),
|
||||
occupants,
|
||||
map,
|
||||
battType,
|
||||
notBravingApd,
|
||||
bravingApd,
|
||||
isLateGame);
|
||||
}
|
||||
|
||||
auto FleeStrategyScoreForState(const GameStateW &gameState, const PlayerId playerId) -> ScoreValue {
|
||||
ScoreValue scoreValue = 0.0;
|
||||
|
||||
const auto *gameStatePtr = gameState.Get();
|
||||
const auto *units = gameStatePtr->units();
|
||||
|
||||
for (const auto *unit : *units) {
|
||||
if (unit->status() != net::eagle0::shardok::storage::fb::UnitStatus_NORMAL_UNIT) continue;
|
||||
|
||||
if (unit->player_id() == playerId &&
|
||||
unit->battalion().type() != net::eagle0::shardok::storage::fb::BattalionTypeId_UNDEAD) {
|
||||
scoreValue += FLEE_UNIT_SCORE;
|
||||
|
||||
if (unit->has_attached_hero() &&
|
||||
unit->attached_hero().control_info().controlled_unit_id() != -1) {
|
||||
scoreValue += FLEE_CONTROLLING_UNIT_SCORE;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return scoreValue;
|
||||
}
|
||||
|
||||
} // anonymous namespace
|
||||
|
||||
/// Standard implementation of AIScoreCalculator that uses the default scoring algorithm.
|
||||
/// Stores specific setting values needed for scoring rather than the entire SettingsGetter.
|
||||
class StandardAIScoreCalculator : public AIScoreCalculator {
|
||||
public:
|
||||
StandardAIScoreCalculator(
|
||||
int maxRounds,
|
||||
ActionPoints braveWaterCost,
|
||||
int meteorRange,
|
||||
double meteorCastVigorCost,
|
||||
int minimumFleeOddsThreshold,
|
||||
int desperateFleeThreshold,
|
||||
std::unordered_map<BattalionTypeId, BattalionTypeSPtr> battalionTypes,
|
||||
const APDCache &apdCache,
|
||||
const ALCache &alCache)
|
||||
: maxRounds_(maxRounds),
|
||||
braveWaterCost_(braveWaterCost),
|
||||
meteorRange_(meteorRange),
|
||||
meteorCastVigorCost_(meteorCastVigorCost),
|
||||
minimumFleeOddsThreshold_(minimumFleeOddsThreshold),
|
||||
desperateFleeThreshold_(desperateFleeThreshold),
|
||||
battalionTypes_(std::move(battalionTypes)),
|
||||
apdCache_(apdCache),
|
||||
alCache_(alCache) {}
|
||||
|
||||
[[nodiscard]] auto GuessedStateScore(
|
||||
bool isDefender,
|
||||
const GameStateW &state,
|
||||
const AIStrategy &aiStrategy,
|
||||
const CoordsSet &allCastleCoords) const -> ScoreValue override;
|
||||
|
||||
private:
|
||||
// Internal accessor methods
|
||||
[[nodiscard]] auto GetBattalionType(BattalionTypeId typeId) const -> BattalionTypeSPtr {
|
||||
auto it = battalionTypes_.find(typeId);
|
||||
if (it == battalionTypes_.end()) {
|
||||
throw ShardokInternalErrorException("Unknown battalion type ID");
|
||||
}
|
||||
return it->second;
|
||||
}
|
||||
[[nodiscard]] auto GetApdCache() const -> const APDCache & { return apdCache_; }
|
||||
[[nodiscard]] auto GetAlCache() const -> const ALCache & { return alCache_; }
|
||||
[[nodiscard]] auto GetBraveWaterCost() const -> ActionPoints { return braveWaterCost_; }
|
||||
[[nodiscard]] auto GetMaxRounds() const -> int { return maxRounds_; }
|
||||
[[nodiscard]] auto GetMeteorRange() const -> int { return meteorRange_; }
|
||||
[[nodiscard]] auto GetMeteorCastVigorCost() const -> double { return meteorCastVigorCost_; }
|
||||
[[nodiscard]] auto GetMinimumFleeOddsThreshold() const -> int {
|
||||
return minimumFleeOddsThreshold_;
|
||||
}
|
||||
[[nodiscard]] auto GetDesperateFleeThreshold() const -> int { return desperateFleeThreshold_; }
|
||||
|
||||
// Implementation methods (converted from internal namespace functions)
|
||||
[[nodiscard]] auto AttackerUnitsScore(
|
||||
const GameStateW &gameState,
|
||||
int roundsRemaining,
|
||||
bool attackerWantsCastles,
|
||||
bool defenderShouldScatter,
|
||||
const vector<TargetPriorityList> &attackerTargetPriorities,
|
||||
const MapId &mapId) const -> ScoreValue;
|
||||
|
||||
[[nodiscard]] auto DefenderScatterStrategyScoreForState(
|
||||
const GameStateW &gameState,
|
||||
int roundsRemaining) const -> ScoreValue;
|
||||
|
||||
[[nodiscard]] auto DefenderHoldCastlesStrategyScoreForState(
|
||||
const GameStateW &gameState,
|
||||
const CoordsSet &castleCoords,
|
||||
int roundsRemaining) const -> ScoreValue;
|
||||
|
||||
[[nodiscard]] auto DefenderScoreForState(
|
||||
const GameStateW &gameState,
|
||||
const AIStrategy &defenderStrategy,
|
||||
const CoordsSet &castleCoords,
|
||||
int roundsRemaining) const -> ScoreValue;
|
||||
|
||||
[[nodiscard]] auto AttackerScoreForState(
|
||||
const GameStateW &gameState,
|
||||
const AIStrategy &attackerStrategy,
|
||||
const CoordsSet &castleCoords,
|
||||
int roundsRemaining) const -> ScoreValue;
|
||||
|
||||
// Scalar settings extracted from SettingsGetter
|
||||
int maxRounds_;
|
||||
ActionPoints braveWaterCost_;
|
||||
int meteorRange_;
|
||||
double meteorCastVigorCost_;
|
||||
int minimumFleeOddsThreshold_;
|
||||
int desperateFleeThreshold_;
|
||||
|
||||
// Battalion type lookup map
|
||||
std::unordered_map<BattalionTypeId, BattalionTypeSPtr> battalionTypes_;
|
||||
|
||||
// Caches (stored as references)
|
||||
const APDCache &apdCache_;
|
||||
const ALCache &alCache_;
|
||||
};
|
||||
|
||||
// Implementation of StandardAIScoreCalculator methods
|
||||
|
||||
auto StandardAIScoreCalculator::AttackerUnitsScore(
|
||||
const GameStateW &gameState,
|
||||
int roundsRemaining,
|
||||
bool attackerWantsCastles,
|
||||
bool defenderShouldScatter,
|
||||
const vector<TargetPriorityList> &attackerTargetPriorities,
|
||||
const MapId &mapId) const -> ScoreValue {
|
||||
// Cache frequently accessed FlatBuffer fields to avoid repeated offset calculations
|
||||
const auto *gameStateRawPtr = gameState.Get();
|
||||
const auto *cachedUnits = gameStateRawPtr->units();
|
||||
const auto *cachedHexMap = gameStateRawPtr->hex_map();
|
||||
|
||||
const int16_t cachedRowCount = cachedHexMap->row_count();
|
||||
const int16_t cachedColumnCount = cachedHexMap->column_count();
|
||||
const int cachedCurrentRound = gameStateRawPtr->current_round();
|
||||
|
||||
bool isLateGame = cachedCurrentRound > 18; // Inline IsLateGame for efficiency
|
||||
|
||||
// APDCache now has built-in thread-local caching - no need for PreCachedAPDs
|
||||
ActionPoints braveWaterCost = GetBraveWaterCost();
|
||||
|
||||
// Memoization cache for EffectiveDistance calls
|
||||
EffectiveDistanceCache distanceCache;
|
||||
|
||||
std::vector<const Unit *> attackerUnits{};
|
||||
std::vector<const Unit *> defenderUnits{};
|
||||
// Pre-allocate vectors based on estimated unit ratios to avoid reallocations
|
||||
const size_t estimatedUnitCount = cachedUnits->size();
|
||||
attackerUnits.reserve(estimatedUnitCount - 1);
|
||||
defenderUnits.reserve(estimatedUnitCount - 1);
|
||||
|
||||
double attackerUnitsValue = 0;
|
||||
double defenderUnitsValue = 0;
|
||||
|
||||
auto occupants = Occupants(*cachedUnits, cachedRowCount, cachedColumnCount);
|
||||
|
||||
for (const Unit *unit : *cachedUnits) {
|
||||
const auto *pi = PlayerInfoForPid(gameState, unit->player_id());
|
||||
if (pi == nullptr) { continue; }
|
||||
|
||||
switch (unit->status()) {
|
||||
case net::eagle0::shardok::storage::fb::UnitStatus_NORMAL_UNIT: {
|
||||
if (pi->is_defender()) {
|
||||
defenderUnits.push_back(unit);
|
||||
} else {
|
||||
attackerUnits.push_back(unit);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case net::eagle0::shardok::storage::fb::UnitStatus_CAPTURED_UNIT: {
|
||||
double thisScore = unit->has_attached_hero() && unit->attached_hero().is_vip()
|
||||
? CAPTURED_VIP_SCORE
|
||||
: CAPTURED_UNIT_SCORE;
|
||||
if (pi->is_defender()) {
|
||||
defenderUnitsValue += thisScore;
|
||||
} else {
|
||||
attackerUnitsValue += thisScore;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case net::eagle0::shardok::storage::fb::UnitStatus_DESTROYED_SUMMONED_UNIT:
|
||||
case net::eagle0::shardok::storage::fb::UnitStatus_FLED_UNIT:
|
||||
case net::eagle0::shardok::storage::fb::UnitStatus_NEVER_ENTERED_UNIT:
|
||||
case net::eagle0::shardok::storage::fb::UnitStatus_OUTLAWED_UNIT:
|
||||
case net::eagle0::shardok::storage::fb::UnitStatus_RESERVE_UNIT:
|
||||
case net::eagle0::shardok::storage::fb::UnitStatus_RETREATED_UNIT:
|
||||
case net::eagle0::shardok::storage::fb::UnitStatus_RESERVED_SLOT: break;
|
||||
|
||||
case net::eagle0::shardok::storage::fb::UnitStatus_UNKNOWN_UNIT:
|
||||
throw ShardokInternalErrorException("Unknown unit status");
|
||||
}
|
||||
}
|
||||
|
||||
double defenderAdvantage = 1.0 + static_cast<double>(cachedCurrentRound) / 31.0;
|
||||
|
||||
// Can we cache this somehow, it won't usually change within your turn
|
||||
auto attackLocationsForAttacker = GetAlCache()->CachedLocations(defenderUnits, isLateGame);
|
||||
const auto &locationsCausingDanger = attackLocationsForAttacker.AllLocations();
|
||||
|
||||
// Process attacker units using cached ActionPointDistances
|
||||
for (const Unit *unit : attackerUnits) {
|
||||
const int battTypeId = unit->battalion().type();
|
||||
|
||||
const auto &priorityList = std::ranges::find_if(
|
||||
attackerTargetPriorities,
|
||||
[&unit](const TargetPriorityList &tpl) {
|
||||
return tpl.attackingUnitId == unit->unit_id();
|
||||
});
|
||||
|
||||
// If there are any tiles being targeted, give this unit a multiplier based on how close
|
||||
// they are to being able to attack it
|
||||
double distanceMultiplier =
|
||||
priorityList == end(attackerTargetPriorities)
|
||||
? 1.0
|
||||
: AttackerMultiplierForTargetDistance(
|
||||
unit,
|
||||
priorityList->priorityOrder,
|
||||
occupants,
|
||||
cachedHexMap,
|
||||
GetBattalionType(static_cast<BattalionTypeId>(battTypeId)),
|
||||
GetApdCache()->GetRaw(
|
||||
cachedHexMap,
|
||||
mapId,
|
||||
GetBattalionType(
|
||||
static_cast<BattalionTypeId>(battTypeId)),
|
||||
false),
|
||||
GetBattalionType(static_cast<BattalionTypeId>(battTypeId))
|
||||
->allowsBraveWater
|
||||
? GetApdCache()->GetRaw(
|
||||
cachedHexMap,
|
||||
mapId,
|
||||
GetBattalionType(static_cast<BattalionTypeId>(
|
||||
battTypeId)),
|
||||
true,
|
||||
braveWaterCost)
|
||||
: nullptr,
|
||||
isLateGame);
|
||||
|
||||
auto uv = UnitValue(
|
||||
unit,
|
||||
true,
|
||||
attackerUnits,
|
||||
attackerWantsCastles,
|
||||
/* includeCastleBonus=*/true,
|
||||
defenderUnits,
|
||||
cachedHexMap,
|
||||
roundsRemaining,
|
||||
attackLocationsForAttacker,
|
||||
locationsCausingDanger,
|
||||
GetApdCache()->GetRaw(
|
||||
cachedHexMap,
|
||||
mapId,
|
||||
GetBattalionType(static_cast<BattalionTypeId>(battTypeId)),
|
||||
false),
|
||||
GetMeteorRange(),
|
||||
GetMeteorCastVigorCost());
|
||||
|
||||
attackerUnitsValue += distanceMultiplier * uv;
|
||||
}
|
||||
|
||||
auto attackLocationsForDefender = GetAlCache()->CachedLocations(attackerUnits, isLateGame);
|
||||
const auto &locationsCausingDangerForAttacker = attackLocationsForDefender.AllLocations();
|
||||
|
||||
for (const Unit *unit : defenderUnits) {
|
||||
auto defenderUnitId = unit->unit_id();
|
||||
const int battTypeId = unit->battalion().type();
|
||||
|
||||
auto dv = UnitValue(
|
||||
unit,
|
||||
false,
|
||||
attackerUnits,
|
||||
attackerWantsCastles,
|
||||
/* includeCastleBonus=*/!defenderShouldScatter,
|
||||
defenderUnits,
|
||||
cachedHexMap,
|
||||
roundsRemaining,
|
||||
attackLocationsForDefender,
|
||||
locationsCausingDangerForAttacker,
|
||||
GetApdCache()->GetRaw(
|
||||
cachedHexMap,
|
||||
mapId,
|
||||
GetBattalionType(static_cast<BattalionTypeId>(battTypeId)),
|
||||
false),
|
||||
GetMeteorRange(),
|
||||
GetMeteorCastVigorCost());
|
||||
|
||||
double distanceMultiplier = 1.0;
|
||||
|
||||
// If the defender is trying to scatter, than we want to be as far away from the nearest
|
||||
// attacker as possible, AND as far away from the nearest friendly as possible
|
||||
if (unit->location().row() > -1 && defenderShouldScatter) {
|
||||
CoordsSet myLocationSet(cachedHexMap);
|
||||
myLocationSet.Add(unit->location());
|
||||
|
||||
DIST_T closestDistanceToEnemy = 999;
|
||||
for (const auto &attackerUnit : attackerUnits) {
|
||||
const int attackerBattTypeId = attackerUnit->battalion().type();
|
||||
const DIST_T thisDistance = distanceCache.GetOrCompute(
|
||||
attackerUnit,
|
||||
unit->location(),
|
||||
GetApdCache()->GetRaw(
|
||||
cachedHexMap,
|
||||
mapId,
|
||||
GetBattalionType(static_cast<BattalionTypeId>(attackerBattTypeId)),
|
||||
false),
|
||||
GetBattalionType(static_cast<BattalionTypeId>(attackerBattTypeId))
|
||||
->allowsBraveWater
|
||||
? GetApdCache()->GetRaw(
|
||||
cachedHexMap,
|
||||
mapId,
|
||||
GetBattalionType(
|
||||
static_cast<BattalionTypeId>(attackerBattTypeId)),
|
||||
true,
|
||||
braveWaterCost)
|
||||
: nullptr,
|
||||
cachedHexMap);
|
||||
if (thisDistance < closestDistanceToEnemy) {
|
||||
closestDistanceToEnemy = thisDistance;
|
||||
}
|
||||
}
|
||||
|
||||
// If the best we can do puts us very close to the enemy, and the unit is almost
|
||||
// destroyed, return a negative value; better to flee
|
||||
if (unit->can_flee() && closestDistanceToEnemy < 5 && unit->battalion().size() < 10) {
|
||||
distanceMultiplier = -1;
|
||||
} else {
|
||||
DIST_T closestDistanceToFriendly = 1;
|
||||
if (defenderUnits.size() > 1) {
|
||||
for (const auto &defenderUnit : defenderUnits) {
|
||||
if (defenderUnit->unit_id() != defenderUnitId) {
|
||||
const int defenderBattTypeId = defenderUnit->battalion().type();
|
||||
const DIST_T thisDistance = distanceCache.GetOrCompute(
|
||||
defenderUnit,
|
||||
unit->location(),
|
||||
GetApdCache()->GetRaw(
|
||||
cachedHexMap,
|
||||
mapId,
|
||||
GetBattalionType(static_cast<BattalionTypeId>(
|
||||
defenderBattTypeId)),
|
||||
false),
|
||||
GetBattalionType(
|
||||
static_cast<BattalionTypeId>(defenderBattTypeId))
|
||||
->allowsBraveWater
|
||||
? GetApdCache()->GetRaw(
|
||||
cachedHexMap,
|
||||
mapId,
|
||||
GetBattalionType(static_cast<BattalionTypeId>(
|
||||
defenderBattTypeId)),
|
||||
true,
|
||||
braveWaterCost)
|
||||
: nullptr,
|
||||
cachedHexMap);
|
||||
if (thisDistance < closestDistanceToEnemy) {
|
||||
closestDistanceToFriendly = thisDistance;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
distanceMultiplier =
|
||||
(closestDistanceToEnemy + closestDistanceToFriendly / 5.0) / 5.0;
|
||||
}
|
||||
}
|
||||
|
||||
defenderUnitsValue += distanceMultiplier * dv;
|
||||
}
|
||||
|
||||
defenderUnitsValue *= defenderAdvantage;
|
||||
|
||||
return attackerUnitsValue - defenderUnitsValue;
|
||||
}
|
||||
|
||||
auto StandardAIScoreCalculator::DefenderScatterStrategyScoreForState(
|
||||
const GameStateW &gameState,
|
||||
const int roundsRemaining) const -> ScoreValue {
|
||||
const auto *gameStatePtr = gameState.Get();
|
||||
const auto *status = gameStatePtr->status();
|
||||
|
||||
if (status->state() == net::eagle0::shardok::storage::fb::GameStatus_::State_VICTORY) {
|
||||
const auto *winningIds = status->winning_shardok_ids();
|
||||
const auto *playerInfos = gameStatePtr->player_infos();
|
||||
|
||||
for (const PlayerId winningPid : *winningIds) {
|
||||
if (winningPid < 0) continue;
|
||||
if (playerInfos->Get(winningPid)->is_defender()) return INT_MAX;
|
||||
return INT_MIN;
|
||||
}
|
||||
return INT_MAX;
|
||||
}
|
||||
if (status->state() == net::eagle0::shardok::storage::fb::GameStatus_::State_DRAW) { return 0; }
|
||||
|
||||
const auto *hexMap = gameStatePtr->hex_map();
|
||||
|
||||
const auto mapId = ActionPointDistancesCache::GetMapId(hexMap);
|
||||
|
||||
const auto unitsTotal = -AttackerUnitsScore(
|
||||
gameState,
|
||||
roundsRemaining,
|
||||
/* attackerWantsCastles=*/false,
|
||||
/* defenderShouldScatter=*/true,
|
||||
{},
|
||||
mapId);
|
||||
|
||||
return unitsTotal;
|
||||
}
|
||||
|
||||
auto StandardAIScoreCalculator::DefenderHoldCastlesStrategyScoreForState(
|
||||
const GameStateW &gameState,
|
||||
const CoordsSet &castleCoords,
|
||||
const int roundsRemaining) const -> ScoreValue {
|
||||
const auto unitsTotal = -AttackerUnitsScore(
|
||||
gameState,
|
||||
roundsRemaining,
|
||||
/* attackerWantsCastles=*/true,
|
||||
/*defenderShouldScatter=*/false,
|
||||
{},
|
||||
ActionPointDistancesCache::GetMapId(gameState->hex_map()));
|
||||
|
||||
// Check the victory condition types
|
||||
ScoreValue victoryConditionTotal = 0.0;
|
||||
|
||||
const PlayerInfo *defenderPi = nullptr;
|
||||
for (const PlayerInfo *pi : *gameState->player_infos()) {
|
||||
if (pi->is_defender()) defenderPi = pi;
|
||||
}
|
||||
|
||||
victoryConditionTotal +=
|
||||
DefenderHoldsCriticalTilesVictoryScore(gameState, castleCoords, defenderPi);
|
||||
|
||||
const double unitsMultiplier =
|
||||
static_cast<double>(roundsRemaining) / static_cast<double>(GetMaxRounds());
|
||||
|
||||
return UNITS_BASE_MULTIPLIER * unitsMultiplier * unitsTotal + victoryConditionTotal;
|
||||
}
|
||||
|
||||
auto StandardAIScoreCalculator::DefenderScoreForState(
|
||||
const GameStateW &gameState,
|
||||
const AIStrategy &defenderStrategy,
|
||||
const CoordsSet &castleCoords,
|
||||
const int roundsRemaining) const -> ScoreValue {
|
||||
if (gameState->status()->state() ==
|
||||
net::eagle0::shardok::storage::fb::GameStatus_::State_VICTORY) {
|
||||
for (const PlayerId winningPid : *gameState->status()->winning_shardok_ids()) {
|
||||
if (winningPid < 0) continue;
|
||||
if (defenderStrategy.strategyType == AIStrategy::STRATEGY_FLEE) return 0;
|
||||
if (gameState->player_infos()->Get(winningPid)->is_defender()) return INT_MAX;
|
||||
return INT_MIN;
|
||||
}
|
||||
return INT_MIN;
|
||||
}
|
||||
if (gameState->status()->state() ==
|
||||
net::eagle0::shardok::storage::fb::GameStatus_::State_DRAW) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
switch (defenderStrategy.strategyType) {
|
||||
case AIStrategy::STRATEGY_ATTACK_CASTLES:
|
||||
throw ShardokInternalErrorException("Defender cannot use AttackCastlesStrategy");
|
||||
case AIStrategy::STRATEGY_ATTACK_UNITS:
|
||||
throw ShardokInternalErrorException("Defender cannot use AttackUnitsStrategy");
|
||||
case AIStrategy::STRATEGY_CROSS_RIVERS:
|
||||
throw ShardokInternalErrorException("Defender cannot use CrossRiversStrategy");
|
||||
case AIStrategy::STRATEGY_HOLD_CASTLES:
|
||||
return DefenderHoldCastlesStrategyScoreForState(
|
||||
gameState,
|
||||
castleCoords,
|
||||
roundsRemaining);
|
||||
case AIStrategy::STRATEGY_SCATTER:
|
||||
return DefenderScatterStrategyScoreForState(gameState, roundsRemaining);
|
||||
case AIStrategy::STRATEGY_FLEE:
|
||||
for (const auto *pi : *gameState->player_infos()) {
|
||||
if (pi->is_defender()) {
|
||||
return FleeStrategyScoreForState(gameState, pi->player_id());
|
||||
}
|
||||
}
|
||||
throw ShardokInternalErrorException("Unable to find defender for FleeStrategy");
|
||||
}
|
||||
throw ShardokInternalErrorException("Escaped AIStrategy switch");
|
||||
}
|
||||
|
||||
auto StandardAIScoreCalculator::AttackerScoreForState(
|
||||
const GameStateW &gameState,
|
||||
const AIStrategy &attackerStrategy,
|
||||
const CoordsSet &castleCoords,
|
||||
const int roundsRemaining) const -> ScoreValue {
|
||||
#if PERFORMANCE_LOGGING_
|
||||
AttackerScoreTimer timer;
|
||||
#endif // # PERFORMANCE_LOGGING_
|
||||
if (gameState->status()->state() ==
|
||||
net::eagle0::shardok::storage::fb::GameStatus_::State_VICTORY) {
|
||||
for (const PlayerId winningPid : *gameState->status()->winning_shardok_ids()) {
|
||||
if (winningPid < 0) continue;
|
||||
if (attackerStrategy.strategyType == AIStrategy::STRATEGY_FLEE) return 0;
|
||||
if (gameState->player_infos()->Get(winningPid)->is_defender()) return INT_MIN;
|
||||
return INT_MAX;
|
||||
}
|
||||
return INT_MAX;
|
||||
}
|
||||
if (gameState->status()->state() ==
|
||||
net::eagle0::shardok::storage::fb::GameStatus_::State_DRAW) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
const auto mapId = ActionPointDistancesCache::GetMapId(gameState->hex_map());
|
||||
|
||||
const auto unitsTotal = AttackerUnitsScore(
|
||||
gameState,
|
||||
roundsRemaining,
|
||||
attackerStrategy.strategyType == AIStrategy::STRATEGY_HOLD_CASTLES,
|
||||
/* defenderShouldScatter=*/false,
|
||||
attackerStrategy.targetPriorities,
|
||||
mapId);
|
||||
|
||||
// Check the victory condition types
|
||||
ScoreValue victoryConditionTotal = 0.0;
|
||||
for (const PlayerInfo *pi : *gameState->player_infos()) {
|
||||
if (pi->is_defender()) continue;
|
||||
|
||||
switch (attackerStrategy.strategyType) {
|
||||
case AIStrategy::STRATEGY_CROSS_RIVERS:
|
||||
victoryConditionTotal += WaterCrossingScore(
|
||||
pi->player_id(),
|
||||
[this](BattalionTypeId typeId) { return GetBattalionType(typeId); },
|
||||
gameState,
|
||||
castleCoords,
|
||||
attackerStrategy.targetLocations,
|
||||
GetApdCache());
|
||||
break;
|
||||
|
||||
case AIStrategy::STRATEGY_ATTACK_CASTLES:
|
||||
case AIStrategy::STRATEGY_ATTACK_UNITS:
|
||||
// already factored into AttackerUnitsScore
|
||||
break;
|
||||
|
||||
case AIStrategy::STRATEGY_HOLD_CASTLES:
|
||||
victoryConditionTotal += AttackerHoldsCriticalTilesVictoryScore(
|
||||
gameState,
|
||||
castleCoords,
|
||||
pi,
|
||||
GetApdCache(),
|
||||
GetAlCache(),
|
||||
[this](BattalionTypeId typeId) { return GetBattalionType(typeId); },
|
||||
GetBraveWaterCost());
|
||||
break;
|
||||
|
||||
case AIStrategy::STRATEGY_SCATTER:
|
||||
throw ShardokInternalErrorException("Attacker cannot use ScatterStrategy");
|
||||
|
||||
case AIStrategy::STRATEGY_FLEE:
|
||||
return FleeStrategyScoreForState(gameState, pi->player_id());
|
||||
}
|
||||
}
|
||||
|
||||
const double unitsMultiplier =
|
||||
static_cast<double>(roundsRemaining) / static_cast<double>(GetMaxRounds());
|
||||
|
||||
const double finalScore =
|
||||
UNITS_BASE_MULTIPLIER * unitsMultiplier * unitsTotal + victoryConditionTotal;
|
||||
|
||||
return finalScore;
|
||||
}
|
||||
|
||||
auto StandardAIScoreCalculator::GuessedStateScore(
|
||||
const bool isDefender,
|
||||
const GameStateW &state,
|
||||
const AIStrategy &aiStrategy,
|
||||
const CoordsSet &allCastleCoords) const -> ScoreValue {
|
||||
const int roundsRemaining = GetMaxRounds() - state->current_round();
|
||||
|
||||
if (isDefender) {
|
||||
return DefenderScoreForState(state, aiStrategy, allCastleCoords, roundsRemaining);
|
||||
}
|
||||
return AttackerScoreForState(state, aiStrategy, allCastleCoords, roundsRemaining);
|
||||
}
|
||||
|
||||
// Factory function implementation
|
||||
auto MakeStandardAIScoreCalculator(
|
||||
const SettingsGetter &settingsGetter,
|
||||
const APDCache &apdCache,
|
||||
const ALCache &alCache) -> std::unique_ptr<AIScoreCalculator> {
|
||||
// Extract all battalion types
|
||||
std::unordered_map<BattalionTypeId, BattalionTypeSPtr> battalionTypes;
|
||||
for (int typeId = BattalionTypeId::BattalionTypeId_MIN;
|
||||
typeId <= BattalionTypeId::BattalionTypeId_MAX;
|
||||
typeId++) {
|
||||
auto battalionTypeId = static_cast<BattalionTypeId>(typeId);
|
||||
battalionTypes[battalionTypeId] = settingsGetter.GetBattalionType(battalionTypeId);
|
||||
}
|
||||
|
||||
return std::make_unique<StandardAIScoreCalculator>(
|
||||
settingsGetter.Backing().max_rounds(),
|
||||
settingsGetter.Backing().brave_water_action_point_cost(),
|
||||
settingsGetter.Backing().meteor_range(),
|
||||
settingsGetter.Backing().meteor_cast_vigor_cost(),
|
||||
settingsGetter.Backing().ai_minimum_flee_odds_threshold(),
|
||||
settingsGetter.Backing().ai_desperate_flee_threshold(),
|
||||
std::move(battalionTypes),
|
||||
apdCache,
|
||||
alCache);
|
||||
}
|
||||
|
||||
} // namespace shardok
|
||||
@@ -0,0 +1,31 @@
|
||||
//
|
||||
// Standard implementation of AIScoreCalculator
|
||||
//
|
||||
|
||||
#ifndef EAGLE0_STANDARDAISCORECALCULATOR_HPP
|
||||
#define EAGLE0_STANDARDAISCORECALCULATOR_HPP
|
||||
|
||||
#include <memory>
|
||||
|
||||
#include "src/main/cpp/net/eagle0/shardok/ai/AIAttackLocations.hpp"
|
||||
#include "src/main/cpp/net/eagle0/shardok/library/action_point_distances/ActionPointDistancesCache.hpp"
|
||||
#include "src/main/cpp/net/eagle0/shardok/library/settings/GameSettings.hpp"
|
||||
|
||||
namespace shardok {
|
||||
|
||||
// Forward declarations
|
||||
class AIScoreCalculator;
|
||||
|
||||
using APDCache = std::shared_ptr<ActionPointDistancesCache>;
|
||||
using ALCache = std::unique_ptr<AttackLocationsCache>;
|
||||
|
||||
/// Factory function to create a StandardAIScoreCalculator.
|
||||
/// Returns a unique_ptr to AIScoreCalculator to hide the implementation.
|
||||
[[nodiscard]] auto MakeStandardAIScoreCalculator(
|
||||
const SettingsGetter& settingsGetter,
|
||||
const APDCache& apdCache,
|
||||
const ALCache& alCache) -> std::unique_ptr<AIScoreCalculator>;
|
||||
|
||||
} // namespace shardok
|
||||
|
||||
#endif // EAGLE0_STANDARDAISCORECALCULATOR_HPP
|
||||
@@ -1,29 +1,25 @@
|
||||
load("//tools:copts.bzl", "COPTS")
|
||||
|
||||
cc_library(
|
||||
name = "mcts_ai",
|
||||
srcs = ["MCTSAI.cpp"],
|
||||
hdrs = ["MCTSAI.hpp"],
|
||||
name = "shardok_mcts_ai",
|
||||
srcs = ["ShardokMCTSAI.cpp"],
|
||||
hdrs = ["ShardokMCTSAI.hpp"],
|
||||
copts = COPTS,
|
||||
visibility = [
|
||||
"//src/main/cpp/net/eagle0/shardok/ai:__pkg__",
|
||||
"//src/main/cpp/net/eagle0/shardok/ai_performance_runner:__pkg__",
|
||||
"//src/test/cpp/net/eagle0/shardok/ai:__pkg__",
|
||||
"//src/test/cpp/net/eagle0/shardok/ai/mcts:__pkg__",
|
||||
],
|
||||
deps = [
|
||||
"//src/main/cpp/net/eagle0/common:random_generator",
|
||||
"//src/main/cpp/net/eagle0/common:sequence_random_generator",
|
||||
"//src/main/cpp/net/eagle0/shardok/ai:ai_command_filter",
|
||||
"//src/main/cpp/net/eagle0/common/mcts/abstract:abstract_mcts_ai",
|
||||
"//src/main/cpp/net/eagle0/shardok/ai:ai_iterative_deepening", # For SearchResult compatibility
|
||||
"//src/main/cpp/net/eagle0/shardok/ai:ai_score_calculator",
|
||||
"//src/main/cpp/net/eagle0/shardok/ai:ai_strategy",
|
||||
"//src/main/cpp/net/eagle0/shardok/ai:ai_time_budget",
|
||||
"//src/main/cpp/net/eagle0/shardok/ai/mcts/internal:mcts_node",
|
||||
"//src/main/cpp/net/eagle0/shardok/library:engine",
|
||||
"//src/main/cpp/net/eagle0/shardok/ai/mcts/adapters:shardok_mcts_factory",
|
||||
"//src/main/cpp/net/eagle0/shardok/library:game_state_w",
|
||||
"//src/main/cpp/net/eagle0/shardok/library:shardok_c_types",
|
||||
"//src/main/cpp/net/eagle0/shardok/library/action_point_distances:action_point_distances_cache",
|
||||
"//src/main/cpp/net/eagle0/shardok/library/settings:game_settings",
|
||||
"//src/main/protobuf/net/eagle0/shardok/api:command_descriptor_cc_proto",
|
||||
"//src/main/protobuf/net/eagle0/shardok/common:command_type_cc_proto",
|
||||
],
|
||||
)
|
||||
|
||||
@@ -1,869 +0,0 @@
|
||||
//
|
||||
// MCTS-based AI implementation for Shardok
|
||||
//
|
||||
|
||||
#include "MCTSAI.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <future>
|
||||
#include <limits>
|
||||
#include <mutex>
|
||||
#include <random>
|
||||
#include <thread>
|
||||
|
||||
#include "internal/MCTSNode.hpp"
|
||||
#include "src/main/cpp/net/eagle0/common/SequenceRandomGenerator.hpp"
|
||||
#include "src/main/cpp/net/eagle0/shardok/ai/AICommandFilter.hpp"
|
||||
#include "src/main/cpp/net/eagle0/shardok/ai/AIScoreCalculator.hpp"
|
||||
#include "src/main/cpp/net/eagle0/shardok/library/ShardokEngine.hpp"
|
||||
#include "src/main/cpp/net/eagle0/shardok/library/util/HexMapUtils.hpp"
|
||||
#include "src/main/protobuf/net/eagle0/shardok/common/command_type.pb.h"
|
||||
|
||||
namespace shardok {
|
||||
|
||||
// Type alias for internal MCTSNode
|
||||
using MCTSNode = internal::MCTSNode;
|
||||
|
||||
// Static helper for average random generator
|
||||
static const std::vector _averageSequence = {0.5};
|
||||
static const auto _averageGenerator = std::make_shared<SequenceRandomGenerator>(_averageSequence);
|
||||
|
||||
MCTSAI::MCTSAI(
|
||||
const PlayerId playerId,
|
||||
const bool isDefender,
|
||||
AIStrategy strategy,
|
||||
const CoordsSet& castleCoords,
|
||||
const APDCache& apdCache,
|
||||
const ALCache& alCache,
|
||||
MCTSConfig config)
|
||||
: playerId(playerId),
|
||||
isDefender(isDefender),
|
||||
strategy(std::move(strategy)),
|
||||
castleCoords(castleCoords),
|
||||
apdCache(apdCache),
|
||||
alCache(alCache),
|
||||
config(std::move(config)) {}
|
||||
|
||||
auto MCTSAI::Search(
|
||||
const GameSettingsSPtr& settings,
|
||||
const GameStateW& state,
|
||||
const std::vector<CommandProto>& commands,
|
||||
const AITimeBudget& budget) const -> SearchResult {
|
||||
const auto startTime = std::chrono::steady_clock::now();
|
||||
SearchResult result;
|
||||
|
||||
if (commands.empty()) {
|
||||
result.searchCompleted = true;
|
||||
result.completionReason = EvaluationCompletionReason::RAN_OUT_OF_COMMANDS;
|
||||
return result;
|
||||
}
|
||||
|
||||
if (commands.size() == 1) {
|
||||
result.searchCompleted = true;
|
||||
result.bestCommandIndex = 0;
|
||||
result.bestScore = 0;
|
||||
result.availableCommandCount = 1;
|
||||
result.depthAchieved = 1;
|
||||
result.commandCountEvaluated = 1;
|
||||
result.completionReason = EvaluationCompletionReason::RAN_OUT_OF_COMMANDS;
|
||||
return result;
|
||||
}
|
||||
|
||||
const auto& settingsGetter = settings->GetGetter();
|
||||
|
||||
// Compute critical tiles once to avoid 8.5% runtime overhead in ShardokEngine construction
|
||||
const auto criticalTiles = GetCriticalTileLocations(state->hex_map());
|
||||
const auto guessedEngine = ShardokEngine(settings, state, criticalTiles);
|
||||
const auto deadline = startTime + budget.remainingBudget;
|
||||
|
||||
// Build MCTS tree
|
||||
auto rootNode = BuildMCTSTree(guessedEngine, settingsGetter, criticalTiles, deadline);
|
||||
|
||||
if (rootNode) {
|
||||
// Get best command from tree
|
||||
const MCTSNode* bestChild = rootNode->GetBestFinalChild();
|
||||
|
||||
if (bestChild) {
|
||||
result.searchCompleted = true;
|
||||
result.bestCommandIndex = bestChild->commandIndex;
|
||||
result.bestScore = bestChild->lookaheadScore;
|
||||
result.completionReason = EvaluationCompletionReason::RAN_OUT_OF_COMMANDS;
|
||||
|
||||
// Calculate max depth reached in the tree
|
||||
std::function<int(const MCTSNode*)> getMaxDepth = [&](const MCTSNode* node) -> int {
|
||||
int maxChildDepth = node->depth;
|
||||
for (const auto& child : node->children) {
|
||||
maxChildDepth = std::max(maxChildDepth, getMaxDepth(child.get()));
|
||||
}
|
||||
return maxChildDepth;
|
||||
};
|
||||
result.depthAchieved = getMaxDepth(rootNode.get());
|
||||
|
||||
// Count total nodes visited
|
||||
std::function<size_t(const MCTSNode*)> countVisited =
|
||||
[&](const MCTSNode* node) -> size_t {
|
||||
size_t count = (node->visitCount > 0) ? 1 : 0;
|
||||
for (const auto& child : node->children) { count += countVisited(child.get()); }
|
||||
return count;
|
||||
};
|
||||
|
||||
result.commandCountEvaluated = countVisited(rootNode.get());
|
||||
result.availableCommandCount = commands.size();
|
||||
|
||||
// MCTS-specific logging
|
||||
printf("MCTS: Selected command %zu (visit:%d, reward:%.2f, lookahead:%.2f) from %zu "
|
||||
"options\n",
|
||||
bestChild->commandIndex,
|
||||
bestChild->visitCount,
|
||||
bestChild->averageReward,
|
||||
bestChild->lookaheadScore,
|
||||
rootNode->children.size());
|
||||
|
||||
// Log top 3 commands for debugging with their best sequences
|
||||
std::vector<MCTSNode*> sortedChildren;
|
||||
for (const auto& child : rootNode->children) { sortedChildren.push_back(child.get()); }
|
||||
std::ranges::sort(sortedChildren, [](const MCTSNode* a, const MCTSNode* b) {
|
||||
return a->visitCount > b->visitCount;
|
||||
});
|
||||
|
||||
printf("MCTS: Top commands by visits:\n");
|
||||
for (size_t i = 0; i < std::min(static_cast<size_t>(3), sortedChildren.size()); ++i) {
|
||||
auto* child = sortedChildren[i];
|
||||
printf(" [%zu] cmd:%zu visits:%d immediate:%.2f backprop:%.2f type:%s",
|
||||
i,
|
||||
child->commandIndex,
|
||||
child->visitCount,
|
||||
child->immediateScore,
|
||||
child->averageReward,
|
||||
CommandType_Name(child->commandType).c_str());
|
||||
|
||||
// Show unit and target info for commands that have them
|
||||
if (child->actorUnitId >= 0) { printf(" unit:%d", child->actorUnitId); }
|
||||
if (child->targetRow >= 0 && child->targetCol >= 0) {
|
||||
printf(" target:(%d,%d)", child->targetRow, child->targetCol);
|
||||
}
|
||||
|
||||
// Show sequence preview for this command
|
||||
if (!child->children.empty()) {
|
||||
// Find best child by visits
|
||||
MCTSNode* bestNext = nullptr;
|
||||
int maxVisits = 0;
|
||||
for (const auto& grandchild : child->children) {
|
||||
if (grandchild->visitCount > maxVisits) {
|
||||
maxVisits = grandchild->visitCount;
|
||||
bestNext = grandchild.get();
|
||||
}
|
||||
}
|
||||
if (bestNext) {
|
||||
printf(" -> %s", CommandType_Name(bestNext->commandType).c_str());
|
||||
}
|
||||
}
|
||||
printf("\n");
|
||||
}
|
||||
|
||||
printf("MCTS: Tree stats - max depth:%zu, total nodes:%zu, root visits:%d\n",
|
||||
result.depthAchieved,
|
||||
result.commandCountEvaluated,
|
||||
rootNode->visitCount);
|
||||
|
||||
// Log the best command sequence from the chosen command
|
||||
struct SequenceNode {
|
||||
size_t commandIndex;
|
||||
std::string commandType;
|
||||
int actorUnitId;
|
||||
int targetRow;
|
||||
int targetCol;
|
||||
double immediateScore;
|
||||
double backpropScore;
|
||||
};
|
||||
std::vector<SequenceNode> bestSequence;
|
||||
bestSequence.reserve(5);
|
||||
auto current = const_cast<MCTSNode*>(bestChild);
|
||||
double sequenceScore = bestChild->averageReward;
|
||||
|
||||
// Trace the best path from chosen command (most visited child at each level)
|
||||
while (current) {
|
||||
bestSequence.push_back(
|
||||
{current->commandIndex,
|
||||
CommandType_Name(current->commandType),
|
||||
current->actorUnitId,
|
||||
current->targetRow,
|
||||
current->targetCol,
|
||||
current->immediateScore,
|
||||
current->averageReward});
|
||||
|
||||
// If no children, we've reached the end of the sequence
|
||||
if (current->children.empty()) { break; }
|
||||
|
||||
// Find most visited child
|
||||
MCTSNode* bestChildNode = nullptr;
|
||||
int maxVisits = 0;
|
||||
for (const auto& child : current->children) {
|
||||
if (child->visitCount > maxVisits) {
|
||||
maxVisits = child->visitCount;
|
||||
bestChildNode = child.get();
|
||||
}
|
||||
}
|
||||
current = bestChildNode;
|
||||
if (current) {
|
||||
sequenceScore = current->averageReward; // Update to final score
|
||||
}
|
||||
}
|
||||
|
||||
if (!bestSequence.empty()) {
|
||||
printf("MCTS: Best sequence from chosen command (final: %.2f):\n", sequenceScore);
|
||||
for (size_t i = 0; i < bestSequence.size(); ++i) {
|
||||
const auto& [commandIndex, commandType, actorUnitId, targetRow, targetCol, immediateScore, backpropScore] =
|
||||
bestSequence[i];
|
||||
printf(" %zu. cmd:%zu %s", i + 1, commandIndex, commandType.c_str());
|
||||
|
||||
// Add unit and target info if present
|
||||
if (actorUnitId >= 0) { printf(" unit:%d", actorUnitId); }
|
||||
if (targetRow >= 0 && targetCol >= 0) {
|
||||
printf(" target:(%d,%d)", targetRow, targetCol);
|
||||
}
|
||||
|
||||
printf(" (immediate:%.2f, backprop:%.2f)\n", immediateScore, backpropScore);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const auto endTime = std::chrono::steady_clock::now();
|
||||
result.timeUsed = std::chrono::duration_cast<std::chrono::milliseconds>(endTime - startTime);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
auto MCTSAI::BuildMCTSTree(
|
||||
const ShardokEngine& engine,
|
||||
const SettingsGetter& settingsGetter,
|
||||
const CoordsSet& criticalTileCoords,
|
||||
const std::chrono::steady_clock::time_point deadline) const -> std::unique_ptr<MCTSNode> {
|
||||
// Clear transposition registry for this search
|
||||
if (config.enableTranspositionDetection) { stateRegistry.clear(); }
|
||||
|
||||
// Create root node
|
||||
auto root = std::make_unique<MCTSNode>(
|
||||
0,
|
||||
net::eagle0::shardok::common::END_TURN_COMMAND,
|
||||
playerId,
|
||||
0,
|
||||
isDefender);
|
||||
root->resultingGameState = engine.GetCurrentGameState();
|
||||
|
||||
// Register root node in transposition table if enabled
|
||||
if (config.enableTranspositionDetection) {
|
||||
root->stateHash = root->resultingGameState.ComputeFNV1aHash();
|
||||
stateRegistry[root->stateHash] = root.get();
|
||||
}
|
||||
|
||||
// Initialize root with available commands
|
||||
const CommandListSPtr rootCommands = engine.GetAvailableCommandsForAIPlayer(playerId);
|
||||
if (!rootCommands || rootCommands->empty()) { return root; }
|
||||
|
||||
// Filter commands for better performance
|
||||
const std::vector<size_t> filteredIndices = AICommandFilter::FilterCommands(
|
||||
rootCommands,
|
||||
playerId,
|
||||
isDefender,
|
||||
engine.GetCurrentGameState(),
|
||||
settingsGetter,
|
||||
apdCache);
|
||||
|
||||
root->untriedCommands = filteredIndices;
|
||||
root->fullyExpanded = filteredIndices.empty();
|
||||
|
||||
// Main MCTS loop
|
||||
int iterations = 0;
|
||||
|
||||
printf("MCTS: Starting search with %zu filtered commands (budget: %.0fms)\n",
|
||||
filteredIndices.size(),
|
||||
std::chrono::duration<double, std::milli>(deadline - std::chrono::steady_clock::now())
|
||||
.count());
|
||||
|
||||
if (config.useMultithreading) {
|
||||
// Parallel MCTS: Run iterations until time budget expires
|
||||
const int numThreads =
|
||||
std::min(config.numThreads, static_cast<int>(std::thread::hardware_concurrency()));
|
||||
std::vector<std::future<void>> futures;
|
||||
std::mutex treeMutex; // Protect tree updates
|
||||
std::atomic totalIterations{0}; // Track iterations across threads
|
||||
|
||||
for (int t = 0; t < numThreads; ++t) {
|
||||
futures.push_back(std::async(std::launch::async, [&, this] {
|
||||
while (std::chrono::steady_clock::now() < deadline) {
|
||||
++totalIterations;
|
||||
|
||||
// Selection and expansion need locking
|
||||
MCTSNode* selected;
|
||||
{
|
||||
std::lock_guard lock(treeMutex);
|
||||
selected = MCTSSelection(root.get());
|
||||
if (selected && !selected->isTerminal && selected->CanExpand()) {
|
||||
selected = MCTSExpansion(
|
||||
selected,
|
||||
engine,
|
||||
settingsGetter,
|
||||
criticalTileCoords);
|
||||
}
|
||||
}
|
||||
|
||||
// Skip simulation if selection failed (all children redundant)
|
||||
if (!selected) continue;
|
||||
|
||||
// Simulation can run in parallel from the selected node's state
|
||||
// Note: Creating engine from node state is correct for MCTS simulation
|
||||
|
||||
// Backpropagation needs locking
|
||||
{
|
||||
ShardokEngine nodeEngine(
|
||||
engine.GetGameSettings(),
|
||||
selected->resultingGameState,
|
||||
criticalTileCoords);
|
||||
const double reward =
|
||||
MCTSSimulation(nodeEngine, selected->playerId, settingsGetter);
|
||||
std::lock_guard lock(treeMutex);
|
||||
MCTSBackpropagation(selected, reward);
|
||||
}
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
// Wait for all threads to complete
|
||||
for (auto& future : futures) { future.get(); }
|
||||
iterations = totalIterations.load(); // Get total from all threads
|
||||
} else {
|
||||
// Sequential MCTS - run until time budget expires
|
||||
while (std::chrono::steady_clock::now() < deadline) {
|
||||
// Selection
|
||||
MCTSNode* selected = MCTSSelection(root.get());
|
||||
|
||||
// Skip if selection failed (all children redundant)
|
||||
if (!selected) continue;
|
||||
|
||||
// Expansion
|
||||
if (!selected->isTerminal && selected->CanExpand()) {
|
||||
selected = MCTSExpansion(selected, engine, settingsGetter, criticalTileCoords);
|
||||
}
|
||||
|
||||
// Simulation (from selected node's state)
|
||||
ShardokEngine nodeEngine(
|
||||
engine.GetGameSettings(),
|
||||
selected->resultingGameState,
|
||||
criticalTileCoords);
|
||||
double reward = MCTSSimulation(
|
||||
nodeEngine,
|
||||
selected->playerId,
|
||||
settingsGetter); // Use node's player, not original player
|
||||
|
||||
// Backpropagation
|
||||
MCTSBackpropagation(selected, reward);
|
||||
|
||||
iterations++;
|
||||
}
|
||||
}
|
||||
|
||||
printf("MCTS: Completed %d iterations, root has %zu children\n",
|
||||
iterations,
|
||||
root->children.size());
|
||||
|
||||
return root;
|
||||
}
|
||||
|
||||
auto MCTSAI::MCTSSelection(MCTSNode* root) const -> MCTSNode* {
|
||||
MCTSNode* current = root;
|
||||
|
||||
while (!current->isTerminal && !current->isRedundant) {
|
||||
if (current->CanExpand()) {
|
||||
return current; // Node has untried commands
|
||||
} else if (!current->children.empty()) {
|
||||
current = current->GetBestChild(config.explorationConstant);
|
||||
if (!current) break;
|
||||
} else {
|
||||
break; // Leaf node
|
||||
}
|
||||
}
|
||||
|
||||
return current;
|
||||
}
|
||||
|
||||
auto MCTSAI::MCTSExpansion(
|
||||
MCTSNode* node,
|
||||
const ShardokEngine& engine,
|
||||
const SettingsGetter& settingsGetter,
|
||||
const CoordsSet& criticalTileCoords) const -> MCTSNode* {
|
||||
static int expansionCallCount = 0;
|
||||
if (expansionCallCount < 3) {
|
||||
printf("MCTSExpansion called %d: node depth:%d untried:%zu\n",
|
||||
expansionCallCount++,
|
||||
node->depth,
|
||||
node->untriedCommands.size());
|
||||
}
|
||||
|
||||
if (node->untriedCommands.empty()) return node;
|
||||
|
||||
// Don't expand beyond maximum depth to prevent unbounded tree growth
|
||||
if (node->depth >= config.maxTreeDepth) {
|
||||
node->fullyExpanded = true;
|
||||
node->untriedCommands.clear();
|
||||
return node;
|
||||
}
|
||||
|
||||
// Pick a random untried command
|
||||
std::random_device rd;
|
||||
std::mt19937 gen(rd());
|
||||
std::uniform_int_distribution dis(0, static_cast<int>(node->untriedCommands.size() - 1));
|
||||
const size_t randomIndex = dis(gen);
|
||||
|
||||
const auto commandIndex = node->untriedCommands[randomIndex];
|
||||
node->untriedCommands.erase(node->untriedCommands.begin() + randomIndex);
|
||||
|
||||
// Create engine from the node's current state (not root state!)
|
||||
const auto nodeEngine = std::make_shared<ShardokEngine>(
|
||||
engine.GetGameSettings(),
|
||||
node->resultingGameState,
|
||||
criticalTileCoords);
|
||||
|
||||
// Get command descriptor from the node's state
|
||||
const CommandListSPtr commands = nodeEngine->GetAvailableCommandsForAIPlayer(node->playerId);
|
||||
if (!commands || commandIndex >= commands->size()) return node;
|
||||
|
||||
const auto& command = commands->at(commandIndex);
|
||||
const auto commandType = command->GetCommandType();
|
||||
const auto descriptor = command->GetCommandProto();
|
||||
|
||||
// Create child node
|
||||
auto child = std::make_unique<MCTSNode>(
|
||||
commandIndex,
|
||||
commandType,
|
||||
node->playerId,
|
||||
node->depth + 1,
|
||||
node->isDefender);
|
||||
child->parent = node;
|
||||
|
||||
// Extract actor unit ID if present
|
||||
if (descriptor.has_actor()) { child->actorUnitId = descriptor.actor().value(); }
|
||||
|
||||
// Extract target coordinates if present
|
||||
// Note: In protobuf3, target is always present but may have default values
|
||||
// We'll always capture the coordinates - commands without targets will have (-1,-1) by default
|
||||
const auto& target = descriptor.target();
|
||||
child->targetRow = target.row();
|
||||
child->targetCol = target.column();
|
||||
|
||||
// Declare variables that will be used later
|
||||
|
||||
// Handle randomness appropriately based on command type
|
||||
if (command->HasOdds()) {
|
||||
// For commands with odds, use average roll for expansion
|
||||
// For expansion, use average roll regardless of success chance
|
||||
const auto generator = std::make_shared<SequenceRandomGenerator>(std::vector{0.5});
|
||||
nodeEngine->PostCommand(node->playerId, commandIndex, generator);
|
||||
} else {
|
||||
// Use average generator for deterministic evaluation
|
||||
nodeEngine->PostCommand(node->playerId, commandIndex, _averageGenerator);
|
||||
}
|
||||
|
||||
child->resultingGameState = nodeEngine->GetCurrentGameState();
|
||||
|
||||
// Check whose turn it is after executing the command
|
||||
PlayerId currentPlayer = nodeEngine->GetCurrentPlayerId();
|
||||
bool isOurTurn = (currentPlayer == playerId);
|
||||
|
||||
// Update child's player ID to reflect whose turn it actually is
|
||||
child->playerId = currentPlayer;
|
||||
|
||||
// Calculate immediate score (always from our perspective)
|
||||
child->immediateScore = AIScoreCalculator::GuessedStateScore(
|
||||
isDefender, // Use our original role, not node's
|
||||
child->resultingGameState,
|
||||
strategy,
|
||||
castleCoords,
|
||||
settingsGetter,
|
||||
apdCache,
|
||||
alCache);
|
||||
|
||||
// Initially, lookahead score equals immediate score
|
||||
child->lookaheadScore = child->immediateScore;
|
||||
|
||||
// Debug: Log first few expansions to see what's happening
|
||||
static int expansionCount = 0;
|
||||
if (expansionCount < 5) {
|
||||
printf("MCTS Expansion %d: cmd:%zu type:%s immediate_score:%.2f\n",
|
||||
expansionCount++,
|
||||
commandIndex,
|
||||
CommandType_Name(commandType).c_str(),
|
||||
child->immediateScore);
|
||||
}
|
||||
|
||||
// Check if terminal
|
||||
child->isTerminal = IsTerminalForPlayer(child->resultingGameState, playerId, settingsGetter);
|
||||
|
||||
// Transposition detection
|
||||
if (config.enableTranspositionDetection) {
|
||||
child->stateHash = child->resultingGameState.ComputeFNV1aHash();
|
||||
|
||||
auto existingIt = stateRegistry.find(child->stateHash);
|
||||
if (existingIt != stateRegistry.end()) {
|
||||
MCTSNode* existingNode = existingIt->second;
|
||||
|
||||
// Apply tie-breaking rules to determine which node to keep
|
||||
bool shouldPruneChild = false;
|
||||
|
||||
if (child->depth > existingNode->depth) {
|
||||
// Rule 1: Prune deeper node (current child is deeper)
|
||||
shouldPruneChild = true;
|
||||
} else if (child->depth == existingNode->depth) {
|
||||
// Rule 2: At same depth, prune node with higher command index
|
||||
if (child->commandIndex > existingNode->commandIndex) {
|
||||
shouldPruneChild = true;
|
||||
} else {
|
||||
// Current child wins - mark existing node as redundant
|
||||
existingNode->isRedundant = true;
|
||||
stateRegistry[child->stateHash] = child.get(); // Update registry
|
||||
}
|
||||
} else {
|
||||
// Child is shallower - mark existing node as redundant
|
||||
existingNode->isRedundant = true;
|
||||
stateRegistry[child->stateHash] = child.get(); // Update registry
|
||||
}
|
||||
|
||||
if (shouldPruneChild) {
|
||||
child->isRedundant = true;
|
||||
// Don't expand redundant nodes
|
||||
}
|
||||
} else {
|
||||
// New state - register it
|
||||
stateRegistry[child->stateHash] = child.get();
|
||||
}
|
||||
}
|
||||
|
||||
// Get available commands for child - only if it's still our turn and not redundant
|
||||
if (!child->isTerminal && !child->isRedundant && isOurTurn) {
|
||||
const CommandListSPtr childCommands =
|
||||
nodeEngine->GetAvailableCommandsForAIPlayer(currentPlayer);
|
||||
if (childCommands) {
|
||||
const std::vector<size_t> childFiltered = AICommandFilter::FilterCommands(
|
||||
childCommands,
|
||||
currentPlayer,
|
||||
isDefender, // Use our original role
|
||||
child->resultingGameState,
|
||||
settingsGetter,
|
||||
apdCache);
|
||||
child->untriedCommands = childFiltered;
|
||||
child->fullyExpanded = childFiltered.empty();
|
||||
}
|
||||
} else if (!isOurTurn) {
|
||||
// Mark as terminal if it's not our turn - we can't expand opponent moves
|
||||
child->isTerminal = true;
|
||||
child->fullyExpanded = true;
|
||||
}
|
||||
|
||||
// Update parent's expansion status
|
||||
if (node->untriedCommands.empty()) { node->fullyExpanded = true; }
|
||||
|
||||
MCTSNode* childPtr = child.get();
|
||||
node->children.push_back(std::move(child));
|
||||
|
||||
return childPtr;
|
||||
}
|
||||
|
||||
auto MCTSAI::MCTSSimulation(
|
||||
const ShardokEngine& engineState,
|
||||
PlayerId startingPlayer,
|
||||
const SettingsGetter& settingsGetter) const -> double {
|
||||
// Create copy for simulation
|
||||
auto simEngine = std::make_shared<ShardokEngine>(engineState, false);
|
||||
|
||||
// Always evaluate from our AI's perspective (not the startingPlayer's perspective)
|
||||
const double initialScore = AIScoreCalculator::GuessedStateScore(
|
||||
isDefender,
|
||||
simEngine->GetCurrentGameState(),
|
||||
strategy,
|
||||
castleCoords,
|
||||
settingsGetter,
|
||||
apdCache,
|
||||
alCache);
|
||||
|
||||
// Debug: Note that we're always scoring from our AI's perspective regardless of startingPlayer
|
||||
(void)startingPlayer; // Acknowledge parameter to avoid warning
|
||||
|
||||
// Fast rollout with random/heuristic moves until terminal
|
||||
int simulationSteps = 0;
|
||||
for (int step = 0; step < config.maxSimulationDepth; ++step) {
|
||||
const GameStateW& currentState = simEngine->GetCurrentGameState();
|
||||
|
||||
// Get whose turn it is
|
||||
const PlayerId currentPlayer = simEngine->GetCurrentPlayerId();
|
||||
|
||||
// Check if terminal
|
||||
if (IsTerminalForPlayer(currentState, playerId, settingsGetter)) { break; }
|
||||
|
||||
// Continue as long as it's still our turn (don't stop after each individual command)
|
||||
// In this game, a player can move multiple units before turn switches
|
||||
if (currentPlayer != playerId) {
|
||||
// Turn switched to opponent - stop simulation immediately
|
||||
break;
|
||||
}
|
||||
|
||||
simulationSteps++;
|
||||
|
||||
// Get available commands for current player
|
||||
const CommandListSPtr commands = simEngine->GetAvailableCommandsForAIPlayer(currentPlayer);
|
||||
if (!commands || commands->empty()) break;
|
||||
|
||||
// Select command based on simulation policy
|
||||
const auto commandIndex = static_cast<int>(
|
||||
SelectSimulationCommand(commands, currentPlayer, simEngine, settingsGetter));
|
||||
|
||||
// Execute command
|
||||
simEngine->PostCommand(currentPlayer, commandIndex, _averageGenerator);
|
||||
|
||||
// Debug: Only log if turn changed unexpectedly
|
||||
const PlayerId newPlayer = simEngine->GetCurrentPlayerId();
|
||||
static int debugCount = 0;
|
||||
if (newPlayer != currentPlayer && debugCount < 5) {
|
||||
const auto commandType = commands->at(commandIndex)->GetCommandType();
|
||||
printf("MCTS Sim step %d: cmd_type:%s player_before:%d player_after:%d\n",
|
||||
simulationSteps,
|
||||
CommandType_Name(commandType).c_str(),
|
||||
currentPlayer,
|
||||
newPlayer);
|
||||
printf(" WARNING: Turn changed after command!\n");
|
||||
debugCount++;
|
||||
}
|
||||
}
|
||||
|
||||
// Evaluate final position
|
||||
const double finalScore = AIScoreCalculator::GuessedStateScore(
|
||||
isDefender,
|
||||
simEngine->GetCurrentGameState(),
|
||||
strategy,
|
||||
castleCoords,
|
||||
settingsGetter,
|
||||
apdCache,
|
||||
alCache);
|
||||
|
||||
// Debug: Log first few simulations to see depth and score change
|
||||
static int simCount = 0;
|
||||
if (simCount < 3) {
|
||||
printf("MCTS Simulation %d: steps:%d initial:%.2f final:%.2f delta:%.2f\n",
|
||||
simCount++,
|
||||
simulationSteps,
|
||||
initialScore,
|
||||
finalScore,
|
||||
finalScore - initialScore);
|
||||
}
|
||||
|
||||
return finalScore;
|
||||
}
|
||||
|
||||
auto MCTSAI::MCTSBackpropagation(MCTSNode* node, double reward) -> void {
|
||||
while (node) {
|
||||
node->visitCount++;
|
||||
node->totalReward += reward;
|
||||
node->averageReward = node->totalReward / node->visitCount;
|
||||
|
||||
// Update lookahead score as weighted average
|
||||
if (node->visitCount == 1) {
|
||||
node->lookaheadScore = reward;
|
||||
} else {
|
||||
node->lookaheadScore =
|
||||
(node->lookaheadScore * (node->visitCount - 1) + reward) / node->visitCount;
|
||||
}
|
||||
|
||||
node = node->parent;
|
||||
}
|
||||
}
|
||||
|
||||
auto MCTSAI::IsTerminalForPlayer(
|
||||
const GameStateW& gameState,
|
||||
PlayerId /*currentPlayer*/,
|
||||
const SettingsGetter& settingsGetter) -> bool {
|
||||
// Check if game is over
|
||||
if (gameState->status()->state() ==
|
||||
net::eagle0::shardok::storage::fb::GameStatus_::State_VICTORY ||
|
||||
gameState->status()->state() ==
|
||||
net::eagle0::shardok::storage::fb::GameStatus_::State_DRAW) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Check max rounds
|
||||
if (gameState->current_round() >= settingsGetter.Backing().max_rounds()) { return true; }
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
auto MCTSAI::SelectSimulationCommand(
|
||||
const CommandListSPtr& commands,
|
||||
PlayerId currentPlayer,
|
||||
const std::shared_ptr<ShardokEngine>& simEngine,
|
||||
const SettingsGetter& settingsGetter) const -> size_t {
|
||||
if (commands->size() == 1) {
|
||||
return 0; // Only one choice
|
||||
}
|
||||
|
||||
std::random_device rd;
|
||||
std::mt19937 gen(rd());
|
||||
|
||||
switch (config.simulationPolicy) {
|
||||
case MCTSSimulationPolicy::RANDOM: {
|
||||
// Pure random selection
|
||||
std::uniform_int_distribution<> dis(0, commands->size() - 1);
|
||||
return dis(gen);
|
||||
}
|
||||
|
||||
case MCTSSimulationPolicy::FILTERED_RANDOM: {
|
||||
// Filter commands first, then random selection
|
||||
const auto filteredIndices = AICommandFilter::FilterCommands(
|
||||
commands,
|
||||
currentPlayer,
|
||||
isDefender,
|
||||
simEngine->GetCurrentGameState(),
|
||||
settingsGetter,
|
||||
apdCache);
|
||||
|
||||
if (filteredIndices.empty()) {
|
||||
// Fallback to random if no commands pass filter
|
||||
std::uniform_int_distribution<> dis(0, commands->size() - 1);
|
||||
return dis(gen);
|
||||
}
|
||||
|
||||
std::uniform_int_distribution<> dis(0, filteredIndices.size() - 1);
|
||||
return filteredIndices[dis(gen)];
|
||||
}
|
||||
|
||||
case MCTSSimulationPolicy::BEST_IMMEDIATE: {
|
||||
// Evaluate all commands and pick the best
|
||||
double bestScore = -std::numeric_limits<double>::max();
|
||||
size_t bestIndex = 0;
|
||||
|
||||
for (size_t i = 0; i < commands->size(); ++i) {
|
||||
// Create a temporary engine to evaluate this command
|
||||
const auto testEngine = std::make_shared<ShardokEngine>(*simEngine, false);
|
||||
|
||||
// Get score BEFORE executing command (for potential player flip comparison)
|
||||
const double preScore = AIScoreCalculator::GuessedStateScore(
|
||||
isDefender,
|
||||
testEngine->GetCurrentGameState(),
|
||||
strategy,
|
||||
castleCoords,
|
||||
settingsGetter,
|
||||
apdCache,
|
||||
alCache);
|
||||
|
||||
const PlayerId playerBefore = testEngine->GetCurrentPlayerId();
|
||||
testEngine->PostCommand(currentPlayer, i, _averageGenerator);
|
||||
const PlayerId playerAfter = testEngine->GetCurrentPlayerId();
|
||||
|
||||
double score;
|
||||
if (playerAfter != playerBefore) {
|
||||
// Player flipped - use pre-execution score to avoid opponent turn effects
|
||||
score = preScore;
|
||||
} else {
|
||||
// Normal command - use post-execution score
|
||||
score = AIScoreCalculator::GuessedStateScore(
|
||||
isDefender,
|
||||
testEngine->GetCurrentGameState(),
|
||||
strategy,
|
||||
castleCoords,
|
||||
settingsGetter,
|
||||
apdCache,
|
||||
alCache);
|
||||
}
|
||||
|
||||
if (score > bestScore) {
|
||||
bestScore = score;
|
||||
bestIndex = i;
|
||||
}
|
||||
}
|
||||
return bestIndex;
|
||||
}
|
||||
|
||||
case MCTSSimulationPolicy::WEIGHTED_BEST_IMMEDIATE: {
|
||||
// Evaluate all commands and weight by ranking
|
||||
struct CommandScore {
|
||||
size_t index;
|
||||
double score;
|
||||
};
|
||||
|
||||
std::vector<CommandScore> commandScores;
|
||||
commandScores.reserve(commands->size());
|
||||
|
||||
for (size_t i = 0; i < commands->size(); ++i) {
|
||||
// Create a temporary engine to evaluate this command
|
||||
const auto testEngine = std::make_shared<ShardokEngine>(*simEngine, false);
|
||||
|
||||
// Get score BEFORE executing command (for potential player flip comparison)
|
||||
const double preScore = AIScoreCalculator::GuessedStateScore(
|
||||
isDefender,
|
||||
testEngine->GetCurrentGameState(),
|
||||
strategy,
|
||||
castleCoords,
|
||||
settingsGetter,
|
||||
apdCache,
|
||||
alCache);
|
||||
|
||||
const PlayerId playerBefore = testEngine->GetCurrentPlayerId();
|
||||
testEngine->PostCommand(currentPlayer, i, _averageGenerator);
|
||||
const PlayerId playerAfter = testEngine->GetCurrentPlayerId();
|
||||
|
||||
double score;
|
||||
if (playerAfter != playerBefore) {
|
||||
// Player flipped - use pre-execution score to avoid opponent turn effects
|
||||
score = preScore;
|
||||
} else {
|
||||
// Normal command - use post-execution score
|
||||
score = AIScoreCalculator::GuessedStateScore(
|
||||
isDefender,
|
||||
testEngine->GetCurrentGameState(),
|
||||
strategy,
|
||||
castleCoords,
|
||||
settingsGetter,
|
||||
apdCache,
|
||||
alCache);
|
||||
}
|
||||
|
||||
commandScores.push_back({i, score});
|
||||
}
|
||||
|
||||
// Sort by score (best first)
|
||||
std::sort(
|
||||
commandScores.begin(),
|
||||
commandScores.end(),
|
||||
[](const CommandScore& a, const CommandScore& b) { return a.score > b.score; });
|
||||
|
||||
// Assign weights: 1.0 for best, 0.5 for second, 0.33 for third, etc.
|
||||
std::vector<double> weights;
|
||||
weights.reserve(commandScores.size());
|
||||
double totalWeight = 0.0;
|
||||
|
||||
for (size_t i = 0; i < commandScores.size(); ++i) {
|
||||
double weight = 1.0 / (i + 1); // 1/1, 1/2, 1/3, ...
|
||||
weights.push_back(weight);
|
||||
totalWeight += weight;
|
||||
}
|
||||
|
||||
// Random selection based on weights
|
||||
std::uniform_real_distribution dis(0.0, totalWeight);
|
||||
const double target = dis(gen);
|
||||
double cumulative = 0.0;
|
||||
|
||||
for (size_t i = 0; i < weights.size(); ++i) {
|
||||
cumulative += weights[i];
|
||||
if (cumulative >= target) { return commandScores[i].index; }
|
||||
}
|
||||
|
||||
// Fallback (shouldn't happen)
|
||||
return commandScores[0].index;
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback to random (shouldn't reach here)
|
||||
std::uniform_int_distribution dis(0, static_cast<int>(commands->size() - 1));
|
||||
return dis(gen);
|
||||
}
|
||||
|
||||
} // namespace shardok
|
||||
@@ -1,128 +0,0 @@
|
||||
//
|
||||
// MCTS-based AI system for Shardok
|
||||
// Alternative to IterativeDeepeningAI using Monte Carlo Tree Search
|
||||
//
|
||||
|
||||
#ifndef EAGLE0_MCTSAI_HPP
|
||||
#define EAGLE0_MCTSAI_HPP
|
||||
|
||||
#include <chrono>
|
||||
#include <future>
|
||||
#include <memory>
|
||||
#include <unordered_map>
|
||||
#include <vector>
|
||||
|
||||
#include "src/main/cpp/net/eagle0/shardok/ai/AIStrategy.hpp"
|
||||
#include "src/main/cpp/net/eagle0/shardok/ai/AITimeBudget.hpp"
|
||||
#include "src/main/cpp/net/eagle0/shardok/ai/IterativeDeepeningAI.hpp" // For SearchResult compatibility
|
||||
#include "src/main/cpp/net/eagle0/shardok/library/GameStateW.hpp"
|
||||
#include "src/main/cpp/net/eagle0/shardok/library/ShardokCTypes.h"
|
||||
#include "src/main/cpp/net/eagle0/shardok/library/ShardokCommand.hpp"
|
||||
#include "src/main/cpp/net/eagle0/shardok/library/action_point_distances/ActionPointDistancesCache.hpp"
|
||||
#include "src/main/cpp/net/eagle0/shardok/library/settings/GameSettings.hpp"
|
||||
#include "src/main/protobuf/net/eagle0/shardok/api/command_descriptor.pb.h"
|
||||
|
||||
namespace shardok {
|
||||
|
||||
// Forward declarations
|
||||
class ShardokEngine;
|
||||
|
||||
// MCTSNode is defined in internal/MCTSNode.hpp
|
||||
namespace internal {
|
||||
struct MCTSNode;
|
||||
}
|
||||
|
||||
// Simulation policy for MCTS rollouts
|
||||
enum class MCTSSimulationPolicy {
|
||||
RANDOM, // Pure random selection
|
||||
FILTERED_RANDOM, // Random from filtered commands
|
||||
BEST_IMMEDIATE, // Choose best immediate score
|
||||
WEIGHTED_BEST_IMMEDIATE // Random weighted by score ranking
|
||||
};
|
||||
|
||||
// Configuration for MCTS algorithm
|
||||
struct MCTSConfig {
|
||||
double explorationConstant = 1.414; // UCB1 constant (sqrt(2) by default)
|
||||
int maxSimulationDepth = 1000; // Maximum depth for rollout
|
||||
int maxTreeDepth = 2000; // Maximum tree depth to prevent stack overflow
|
||||
bool useMultithreading = true; // Enable parallel MCTS
|
||||
int numThreads = 16; // Number of threads for parallel MCTS (when enabled)
|
||||
MCTSSimulationPolicy simulationPolicy = MCTSSimulationPolicy::BEST_IMMEDIATE;
|
||||
bool enableTranspositionDetection = true; // Enable pruning of duplicate states
|
||||
};
|
||||
|
||||
class MCTSAI {
|
||||
public:
|
||||
using CommandProto = net::eagle0::shardok::api::CommandDescriptor;
|
||||
using SearchResult = IterativeDeepeningAI::SearchResult;
|
||||
|
||||
MCTSAI(PlayerId playerId,
|
||||
bool isDefender,
|
||||
AIStrategy strategy,
|
||||
const CoordsSet& castleCoords,
|
||||
const APDCache& apdCache,
|
||||
const ALCache& alCache,
|
||||
MCTSConfig config = MCTSConfig{});
|
||||
|
||||
// Main search interface - compatible with IterativeDeepeningAI
|
||||
[[nodiscard]] auto Search(
|
||||
const GameSettingsSPtr& settings,
|
||||
const GameStateW& state,
|
||||
const std::vector<CommandProto>& commands,
|
||||
const AITimeBudget& budget) const -> SearchResult;
|
||||
|
||||
// Get/set configuration
|
||||
[[nodiscard]] auto GetConfig() const -> const MCTSConfig& { return config; }
|
||||
void SetConfig(const MCTSConfig& newConfig) { config = newConfig; }
|
||||
|
||||
private:
|
||||
PlayerId playerId;
|
||||
bool isDefender;
|
||||
AIStrategy strategy;
|
||||
const CoordsSet& castleCoords;
|
||||
const APDCache& apdCache;
|
||||
const ALCache& alCache;
|
||||
MCTSConfig config;
|
||||
|
||||
// Transposition detection infrastructure
|
||||
mutable std::unordered_map<uint64_t, internal::MCTSNode*>
|
||||
stateRegistry; // Hash -> first node mapping
|
||||
|
||||
// Internal MCTS tree building
|
||||
[[nodiscard]] auto BuildMCTSTree(
|
||||
const ShardokEngine& engine,
|
||||
const SettingsGetter& settingsGetter,
|
||||
const CoordsSet& criticalTileCoords,
|
||||
std::chrono::steady_clock::time_point deadline) const
|
||||
-> std::unique_ptr<internal::MCTSNode>;
|
||||
|
||||
// MCTS algorithm phases
|
||||
auto MCTSSelection(internal::MCTSNode* root) const -> internal::MCTSNode*;
|
||||
auto MCTSExpansion(
|
||||
internal::MCTSNode* node,
|
||||
const ShardokEngine& engine,
|
||||
const SettingsGetter& settingsGetter,
|
||||
const CoordsSet& criticalTileCoords) const -> internal::MCTSNode*;
|
||||
auto MCTSSimulation(
|
||||
const ShardokEngine& engineState,
|
||||
PlayerId currentPlayer,
|
||||
const SettingsGetter& settingsGetter) const -> double;
|
||||
static auto MCTSBackpropagation(internal::MCTSNode* node, double reward) -> void;
|
||||
|
||||
// Helper functions
|
||||
[[nodiscard]] static auto IsTerminalForPlayer(
|
||||
const GameStateW& gameState,
|
||||
PlayerId currentPlayer,
|
||||
const SettingsGetter& settingsGetter) -> bool;
|
||||
|
||||
// Simulation command selection based on policy
|
||||
[[nodiscard]] auto SelectSimulationCommand(
|
||||
const CommandListSPtr& commands,
|
||||
PlayerId currentPlayer,
|
||||
const std::shared_ptr<ShardokEngine>& simEngine,
|
||||
const SettingsGetter& settingsGetter) const -> size_t;
|
||||
};
|
||||
|
||||
} // namespace shardok
|
||||
|
||||
#endif // EAGLE0_MCTSAI_HPP
|
||||
@@ -0,0 +1,108 @@
|
||||
//
|
||||
// Shardok-specific MCTS AI implementation using abstract interfaces
|
||||
//
|
||||
|
||||
#include "ShardokMCTSAI.hpp"
|
||||
|
||||
#include "adapters/ShardokGameEngine.hpp"
|
||||
#include "adapters/ShardokGameState.hpp"
|
||||
#include "src/main/cpp/net/eagle0/shardok/ai/AICommandFilter.hpp"
|
||||
#include "src/main/cpp/net/eagle0/shardok/ai/AIScoreCalculator.hpp"
|
||||
#include "src/main/cpp/net/eagle0/shardok/library/ShardokEngine.hpp"
|
||||
#include "src/main/cpp/net/eagle0/shardok/library/util/HexMapUtils.hpp"
|
||||
|
||||
namespace shardok {
|
||||
|
||||
ShardokMCTSAI::ShardokMCTSAI(
|
||||
PlayerId playerId,
|
||||
bool isDefender,
|
||||
AIStrategy strategy,
|
||||
const CoordsSet& castleCoords,
|
||||
const AIScoreCalculator& scoreCalculator,
|
||||
const APDCache& apdCache,
|
||||
const ALCache& alCache,
|
||||
MCTSConfig config)
|
||||
: abstractAI_(std::make_unique<mcts::AbstractMCTSAI>(
|
||||
static_cast<mcts::MCTSPlayerId>(playerId),
|
||||
config)),
|
||||
playerId_(playerId),
|
||||
isDefender_(isDefender),
|
||||
strategy_(strategy),
|
||||
castleCoords_(castleCoords),
|
||||
scoreCalculator_(scoreCalculator),
|
||||
apdCache_(apdCache),
|
||||
alCache_(alCache) {}
|
||||
|
||||
auto ShardokMCTSAI::Search(
|
||||
const GameSettingsSPtr& settings,
|
||||
const GameStateW& state,
|
||||
const AITimeBudget& budget) const -> SearchResult {
|
||||
// Compute critical tiles once to avoid 8.5% runtime overhead in ShardokEngine construction
|
||||
const auto criticalTiles = GetCriticalTileLocations(state->hex_map());
|
||||
|
||||
// Create Shardok engine for simulation
|
||||
ShardokEngine engine(settings, state, criticalTiles, 0, false);
|
||||
|
||||
// Create game state adapter
|
||||
auto gameState = mcts::ShardokMCTSFactory::createGameState(
|
||||
state,
|
||||
&scoreCalculator_, // Pass the score calculator
|
||||
settings, // Pass shared_ptr directly
|
||||
isDefender_,
|
||||
strategy_,
|
||||
castleCoords_,
|
||||
apdCache_,
|
||||
alCache_,
|
||||
criticalTiles);
|
||||
|
||||
// Create game engine adapter (passing critical tiles to avoid recomputation)
|
||||
auto gameEngine = mcts::ShardokMCTSFactory::createGameEngine(
|
||||
engine,
|
||||
nullptr, // commandFilter - simplified
|
||||
&scoreCalculator_, // Pass the score calculator
|
||||
settings,
|
||||
apdCache_,
|
||||
alCache_,
|
||||
playerId_,
|
||||
isDefender_,
|
||||
strategy_,
|
||||
castleCoords_,
|
||||
criticalTiles);
|
||||
|
||||
// Perform abstract search
|
||||
const auto timeLimit = budget.remainingBudget;
|
||||
const auto abstractResult = abstractAI_->Search(*gameEngine, *gameState, timeLimit);
|
||||
|
||||
// Get unfiltered command count for consistent reporting with IterativeDeepeningAI
|
||||
// (MCTS uses filtered commands internally, but we report unfiltered count for metrics)
|
||||
const auto unfilteredCommands = engine.GetAvailableCommandsForAIPlayer(
|
||||
static_cast<PlayerId>(gameState->currentPlayerId()));
|
||||
const size_t unfilteredCount = unfilteredCommands ? unfilteredCommands->size() : 0;
|
||||
|
||||
// Convert result back to Shardok format
|
||||
SearchResult result;
|
||||
// Map filtered index back to original unfiltered index
|
||||
result.bestCommandIndex =
|
||||
gameEngine->mapFilteredIndexToOriginal(abstractResult.bestActionIndex, *gameState);
|
||||
result.bestScore = abstractResult.bestScore;
|
||||
result.depthAchieved = static_cast<size_t>(abstractResult.searchDepth);
|
||||
result.commandCountEvaluated = static_cast<size_t>(abstractResult.nodesEvaluated);
|
||||
result.timeUsed = abstractResult.searchTime;
|
||||
result.availableCommandCount = unfilteredCount;
|
||||
result.minimumDepthCompleted =
|
||||
(abstractResult.searchDepth >= static_cast<int>(budget.minDepthRequired));
|
||||
result.searchCompleted = true; // MCTS is anytime - always returns a valid result
|
||||
|
||||
// Determine completion reason based on what actually happened
|
||||
if (abstractResult.foundWinningMove || unfilteredCount == 0) {
|
||||
// Found a terminal winning state or no commands available
|
||||
result.completionReason = EvaluationCompletionReason::RAN_OUT_OF_COMMANDS;
|
||||
} else {
|
||||
// Normal case - time budget exhausted while exploring
|
||||
result.completionReason = EvaluationCompletionReason::RAN_OUT_OF_TIME;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
} // namespace shardok
|
||||
@@ -0,0 +1,74 @@
|
||||
//
|
||||
// Shardok-specific MCTS AI that wraps the abstract implementation
|
||||
//
|
||||
|
||||
#ifndef EAGLE0_SHARDOK_MCTSAI_HPP
|
||||
#define EAGLE0_SHARDOK_MCTSAI_HPP
|
||||
|
||||
#include <memory>
|
||||
#include <vector>
|
||||
|
||||
#include "adapters/ShardokMCTSFactory.hpp"
|
||||
#include "src/main/cpp/net/eagle0/common/mcts/abstract/AbstractMCTSAI.hpp"
|
||||
#include "src/main/cpp/net/eagle0/shardok/ai/AIStrategy.hpp"
|
||||
#include "src/main/cpp/net/eagle0/shardok/ai/AITimeBudget.hpp"
|
||||
#include "src/main/cpp/net/eagle0/shardok/ai/IterativeDeepeningAI.hpp" // For SearchResult compatibility
|
||||
#include "src/main/cpp/net/eagle0/shardok/library/GameStateW.hpp"
|
||||
#include "src/main/cpp/net/eagle0/shardok/library/ShardokCTypes.h"
|
||||
#include "src/main/cpp/net/eagle0/shardok/library/action_point_distances/ActionPointDistancesCache.hpp"
|
||||
#include "src/main/cpp/net/eagle0/shardok/library/settings/GameSettings.hpp"
|
||||
|
||||
#pragma clang diagnostic push
|
||||
#pragma clang diagnostic ignored "-Wdeprecated-redundant-constexpr-static-def"
|
||||
#include "src/main/protobuf/net/eagle0/shardok/api/command_descriptor.pb.h"
|
||||
#pragma clang diagnostic pop
|
||||
|
||||
namespace shardok {
|
||||
|
||||
// Forward declarations
|
||||
class ShardokEngine;
|
||||
class AICommandFilter;
|
||||
class AIScoreCalculator;
|
||||
|
||||
class ShardokMCTSAI {
|
||||
public:
|
||||
using CommandProto = net::eagle0::shardok::api::CommandDescriptor;
|
||||
using SearchResult = IterativeDeepeningAI::SearchResult;
|
||||
using MCTSConfig = mcts::MCTSConfig;
|
||||
|
||||
ShardokMCTSAI(
|
||||
PlayerId playerId,
|
||||
bool isDefender,
|
||||
AIStrategy strategy,
|
||||
const CoordsSet& castleCoords,
|
||||
const AIScoreCalculator& scoreCalculator,
|
||||
const APDCache& apdCache,
|
||||
const ALCache& alCache,
|
||||
MCTSConfig config = MCTSConfig{});
|
||||
|
||||
// Main search interface - compatible with IterativeDeepeningAI
|
||||
[[nodiscard]] auto Search(
|
||||
const GameSettingsSPtr& settings,
|
||||
const GameStateW& state,
|
||||
const AITimeBudget& budget) const -> SearchResult;
|
||||
|
||||
// Configuration
|
||||
[[nodiscard]] auto GetConfig() const -> const MCTSConfig& { return abstractAI_->GetConfig(); }
|
||||
void SetConfig(const MCTSConfig& newConfig) { abstractAI_->SetConfig(newConfig); }
|
||||
|
||||
private:
|
||||
std::unique_ptr<mcts::AbstractMCTSAI> abstractAI_;
|
||||
|
||||
// Shardok-specific context
|
||||
PlayerId playerId_;
|
||||
bool isDefender_;
|
||||
AIStrategy strategy_;
|
||||
const CoordsSet& castleCoords_;
|
||||
const AIScoreCalculator& scoreCalculator_;
|
||||
const APDCache& apdCache_;
|
||||
const ALCache& alCache_;
|
||||
};
|
||||
|
||||
} // namespace shardok
|
||||
|
||||
#endif // EAGLE0_SHARDOK_MCTSAI_HPP
|
||||
@@ -0,0 +1,81 @@
|
||||
load("//tools:copts.bzl", "COPTS")
|
||||
|
||||
cc_library(
|
||||
name = "shardok_action",
|
||||
srcs = ["ShardokAction.cpp"],
|
||||
hdrs = ["ShardokAction.hpp"],
|
||||
copts = COPTS,
|
||||
visibility = [
|
||||
"//src/main/cpp/net/eagle0/shardok/ai/mcts:__pkg__",
|
||||
"//src/test/cpp/net/eagle0/common/mcts:__subpackages__",
|
||||
],
|
||||
deps = [
|
||||
"//src/main/cpp/net/eagle0/common/mcts/abstract:mcts_action",
|
||||
"//src/main/protobuf/net/eagle0/shardok/api:command_descriptor_cc_proto",
|
||||
"//src/main/protobuf/net/eagle0/shardok/common:command_type_cc_proto",
|
||||
],
|
||||
)
|
||||
|
||||
cc_library(
|
||||
name = "shardok_game_state",
|
||||
srcs = ["ShardokGameState.cpp"],
|
||||
hdrs = ["ShardokGameState.hpp"],
|
||||
copts = COPTS,
|
||||
visibility = [
|
||||
"//src/main/cpp/net/eagle0/shardok/ai/mcts:__pkg__",
|
||||
"//src/test/cpp/net/eagle0/common/mcts:__subpackages__",
|
||||
],
|
||||
deps = [
|
||||
"//src/main/cpp/net/eagle0/common/mcts/abstract:mcts_game_state",
|
||||
"//src/main/cpp/net/eagle0/shardok/ai:ai_score_calculator_interface",
|
||||
"//src/main/cpp/net/eagle0/shardok/ai:ai_strategy",
|
||||
"//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/settings:game_settings",
|
||||
],
|
||||
)
|
||||
|
||||
cc_library(
|
||||
name = "shardok_game_engine",
|
||||
srcs = ["ShardokGameEngine.cpp"],
|
||||
hdrs = ["ShardokGameEngine.hpp"],
|
||||
copts = COPTS,
|
||||
visibility = [
|
||||
"//src/main/cpp/net/eagle0/shardok/ai/mcts:__pkg__",
|
||||
"//src/test/cpp/net/eagle0/common/mcts:__subpackages__",
|
||||
],
|
||||
deps = [
|
||||
":shardok_action",
|
||||
":shardok_game_state",
|
||||
"//src/main/cpp/net/eagle0/common:sequence_random_generator",
|
||||
"//src/main/cpp/net/eagle0/common/mcts/abstract:mcts_game_engine",
|
||||
"//src/main/cpp/net/eagle0/shardok/ai:ai_command_filter",
|
||||
"//src/main/cpp/net/eagle0/shardok/ai:ai_score_calculator_interface",
|
||||
"//src/main/cpp/net/eagle0/shardok/library:engine",
|
||||
"//src/main/cpp/net/eagle0/shardok/library:shardok_c_types",
|
||||
"//src/main/cpp/net/eagle0/shardok/library/settings:game_settings",
|
||||
],
|
||||
)
|
||||
|
||||
cc_library(
|
||||
name = "shardok_mcts_factory",
|
||||
srcs = ["ShardokMCTSFactory.cpp"],
|
||||
hdrs = ["ShardokMCTSFactory.hpp"],
|
||||
copts = COPTS,
|
||||
visibility = [
|
||||
"//src/main/cpp/net/eagle0/shardok/ai:__pkg__",
|
||||
"//src/main/cpp/net/eagle0/shardok/ai/mcts:__pkg__",
|
||||
"//src/test/cpp/net/eagle0/common/mcts:__subpackages__",
|
||||
],
|
||||
deps = [
|
||||
":shardok_action",
|
||||
":shardok_game_engine",
|
||||
":shardok_game_state",
|
||||
"//src/main/cpp/net/eagle0/shardok/ai:ai_command_filter",
|
||||
"//src/main/cpp/net/eagle0/shardok/ai:ai_score_calculator_interface",
|
||||
"//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/settings:game_settings",
|
||||
"//src/main/protobuf/net/eagle0/shardok/api:command_descriptor_cc_proto",
|
||||
],
|
||||
)
|
||||
@@ -0,0 +1,71 @@
|
||||
//
|
||||
// Shardok-specific action adapter implementation
|
||||
//
|
||||
|
||||
#include "ShardokAction.hpp"
|
||||
|
||||
#include <sstream>
|
||||
|
||||
#pragma clang diagnostic push
|
||||
#pragma clang diagnostic ignored "-Wdeprecated-redundant-constexpr-static-def"
|
||||
#include "src/main/protobuf/net/eagle0/shardok/common/command_type.pb.h"
|
||||
#pragma clang diagnostic pop
|
||||
|
||||
namespace shardok {
|
||||
namespace mcts {
|
||||
|
||||
ShardokAction::ShardokAction(const CommandProto& command, size_t index)
|
||||
: command_(command),
|
||||
commandIndex_(index) {}
|
||||
|
||||
ShardokAction::ShardokAction(CommandProto&& command, size_t index)
|
||||
: command_(std::move(command)),
|
||||
commandIndex_(index) {}
|
||||
|
||||
int ShardokAction::getType() const { return static_cast<int>(command_.type()); }
|
||||
|
||||
std::string ShardokAction::getDescription() const {
|
||||
std::stringstream ss;
|
||||
|
||||
// Show player
|
||||
ss << "P" << static_cast<int>(command_.player()) << " ";
|
||||
|
||||
ss << net::eagle0::shardok::common::CommandType_Name(command_.type());
|
||||
|
||||
if (command_.has_actor()) { ss << " Unit:" << command_.actor().value(); }
|
||||
|
||||
if (command_.has_target()) {
|
||||
const auto& coord = command_.target();
|
||||
ss << " @(" << coord.row() << "," << coord.column() << ")";
|
||||
}
|
||||
|
||||
return ss.str();
|
||||
}
|
||||
|
||||
int ShardokAction::getActorId() const {
|
||||
if (command_.has_actor()) { return command_.actor().value(); }
|
||||
return -1;
|
||||
}
|
||||
|
||||
std::pair<int, int> ShardokAction::getTarget() const {
|
||||
if (command_.has_target()) {
|
||||
const auto& coord = command_.target();
|
||||
return std::make_pair(coord.row(), coord.column());
|
||||
}
|
||||
return std::make_pair(-1, -1);
|
||||
}
|
||||
|
||||
std::unique_ptr<MCTSAction> ShardokAction::clone() const {
|
||||
return std::make_unique<ShardokAction>(command_, commandIndex_);
|
||||
}
|
||||
|
||||
bool ShardokAction::equals(const MCTSAction& other) const {
|
||||
const auto* shardokOther = dynamic_cast<const ShardokAction*>(&other);
|
||||
if (!shardokOther) { return false; }
|
||||
|
||||
return commandIndex_ == shardokOther->commandIndex_ &&
|
||||
command_.SerializeAsString() == shardokOther->command_.SerializeAsString();
|
||||
}
|
||||
|
||||
} // namespace mcts
|
||||
} // namespace shardok
|
||||
@@ -0,0 +1,50 @@
|
||||
//
|
||||
// Shardok-specific action adapter for MCTS
|
||||
//
|
||||
|
||||
#ifndef EAGLE0_SHARDOK_ACTION_HPP
|
||||
#define EAGLE0_SHARDOK_ACTION_HPP
|
||||
|
||||
#include <memory>
|
||||
#include <string>
|
||||
|
||||
#include "src/main/cpp/net/eagle0/common/mcts/abstract/MCTSAction.hpp"
|
||||
|
||||
#pragma clang diagnostic push
|
||||
#pragma clang diagnostic ignored "-Wdeprecated-redundant-constexpr-static-def"
|
||||
#include "src/main/protobuf/net/eagle0/shardok/api/command_descriptor.pb.h"
|
||||
#pragma clang diagnostic pop
|
||||
|
||||
namespace shardok {
|
||||
namespace mcts {
|
||||
|
||||
class ShardokAction : public MCTSAction {
|
||||
public:
|
||||
using CommandProto = net::eagle0::shardok::api::CommandDescriptor;
|
||||
|
||||
ShardokAction(const CommandProto& command, size_t index);
|
||||
ShardokAction(CommandProto&& command, size_t index);
|
||||
|
||||
// MCTSAction interface implementation
|
||||
[[nodiscard]] size_t getIndex() const override { return commandIndex_; }
|
||||
[[nodiscard]] std::string getDescription() const override;
|
||||
[[nodiscard]] std::unique_ptr<MCTSAction> clone() const override;
|
||||
[[nodiscard]] bool equals(const MCTSAction& other) const override;
|
||||
|
||||
// Shardok-specific methods (not part of abstract interface)
|
||||
[[nodiscard]] int getType() const;
|
||||
[[nodiscard]] int getActorId() const;
|
||||
[[nodiscard]] std::pair<int, int> getTarget() const;
|
||||
|
||||
// Shardok-specific accessor
|
||||
[[nodiscard]] const CommandProto& getCommand() const { return command_; }
|
||||
|
||||
private:
|
||||
CommandProto command_;
|
||||
size_t commandIndex_;
|
||||
};
|
||||
|
||||
} // namespace mcts
|
||||
} // namespace shardok
|
||||
|
||||
#endif // EAGLE0_SHARDOK_ACTION_HPP
|
||||
@@ -0,0 +1,244 @@
|
||||
//
|
||||
// Shardok-specific game engine adapter implementation
|
||||
//
|
||||
|
||||
#include "ShardokGameEngine.hpp"
|
||||
|
||||
#include "ShardokAction.hpp"
|
||||
#include "ShardokGameState.hpp"
|
||||
#include "src/main/cpp/net/eagle0/shardok/ai/AICommandFilter.hpp"
|
||||
#include "src/main/cpp/net/eagle0/shardok/ai/AIScoreCalculator.hpp"
|
||||
#include "src/main/cpp/net/eagle0/shardok/library/ShardokEngine.hpp"
|
||||
#include "src/main/cpp/net/eagle0/shardok/library/settings/GameSettings.hpp"
|
||||
|
||||
#pragma clang diagnostic push
|
||||
#pragma clang diagnostic ignored "-Wdeprecated-redundant-constexpr-static-def"
|
||||
#include "src/main/protobuf/net/eagle0/shardok/common/command_type.pb.h"
|
||||
#pragma clang diagnostic pop
|
||||
|
||||
namespace shardok::mcts {
|
||||
|
||||
// Static helper for average random generator
|
||||
// Note: Using nullptr for simplicity - could be improved with proper random generator
|
||||
|
||||
ShardokGameEngine::ShardokGameEngine(
|
||||
[[maybe_unused]] const ShardokEngine* engine,
|
||||
const AICommandFilter* commandFilter,
|
||||
const AIScoreCalculator* scoreCalculator,
|
||||
const GameSettingsSPtr& gameSettings,
|
||||
const APDCache* apdCache,
|
||||
const ALCache* alCache,
|
||||
PlayerId playerId,
|
||||
bool isDefender,
|
||||
const AIStrategy& strategy,
|
||||
const CoordsSet& castleCoords,
|
||||
const CoordsSet& criticalTileCoords)
|
||||
: commandFilter_(commandFilter),
|
||||
scoreCalculator_(scoreCalculator),
|
||||
gameSettings_(gameSettings),
|
||||
apdCache_(apdCache),
|
||||
alCache_(alCache),
|
||||
playerId_(playerId),
|
||||
isDefender_(isDefender),
|
||||
strategy_(strategy),
|
||||
castleCoords_(castleCoords),
|
||||
criticalTileCoords_(criticalTileCoords) {}
|
||||
|
||||
std::unique_ptr<MCTSGameState> ShardokGameEngine::applyAction(
|
||||
const MCTSGameState& state,
|
||||
const MCTSAction& action) const {
|
||||
const auto* shardokState = dynamic_cast<const ShardokGameState*>(&state);
|
||||
const auto* shardokAction = dynamic_cast<const ShardokAction*>(&action);
|
||||
|
||||
if (!shardokState || !shardokAction) { return nullptr; }
|
||||
|
||||
const auto currentPlayer = static_cast<PlayerId>(state.currentPlayerId());
|
||||
|
||||
// Use cached engine if available (avoids recomputing GetAvailableCommands for same state)
|
||||
std::shared_ptr<ShardokEngine> engine;
|
||||
if (auto cachedEngine = shardokState->getCachedEngine()) {
|
||||
// Clone the cached engine to preserve command cache
|
||||
engine = std::make_shared<ShardokEngine>(*cachedEngine);
|
||||
} else {
|
||||
// Create fresh engine and populate command cache
|
||||
engine = std::make_shared<ShardokEngine>(
|
||||
gameSettings_,
|
||||
shardokState->getShardokState(),
|
||||
criticalTileCoords_,
|
||||
0,
|
||||
false);
|
||||
// Populate command cache (result intentionally unused, just populating cache)
|
||||
[[maybe_unused]] const auto commands =
|
||||
engine->GetAvailableCommandsForAIPlayer(currentPlayer);
|
||||
// Cache the engine for future use with this state
|
||||
shardokState->setCachedEngine(engine);
|
||||
// Clone it for applying the action (don't mutate the cached engine)
|
||||
engine = std::make_shared<ShardokEngine>(*engine);
|
||||
}
|
||||
|
||||
engine->PostCommand(currentPlayer, shardokAction->getIndex(), nullptr);
|
||||
|
||||
// Create and return the new state (don't cache the mutated engine)
|
||||
return std::make_unique<ShardokGameState>(
|
||||
engine->GetCurrentGameState(),
|
||||
scoreCalculator_,
|
||||
gameSettings_.get(),
|
||||
isDefender_,
|
||||
strategy_,
|
||||
castleCoords_,
|
||||
*apdCache_,
|
||||
*alCache_,
|
||||
criticalTileCoords_);
|
||||
}
|
||||
|
||||
void ShardokGameEngine::applyActionMutable(
|
||||
std::unique_ptr<MCTSGameState>& state,
|
||||
const MCTSAction& action) const {
|
||||
auto* shardokState = dynamic_cast<ShardokGameState*>(state.get());
|
||||
const auto* shardokAction = dynamic_cast<const ShardokAction*>(&action);
|
||||
|
||||
if (!shardokState || !shardokAction) {
|
||||
// Fallback to default implementation
|
||||
state = applyAction(*state, action);
|
||||
return;
|
||||
}
|
||||
|
||||
const auto currentPlayer = static_cast<PlayerId>(state->currentPlayerId());
|
||||
|
||||
// Use cached engine if available
|
||||
std::shared_ptr<ShardokEngine> engine;
|
||||
if (auto cachedEngine = shardokState->getCachedEngine()) {
|
||||
engine = std::make_shared<ShardokEngine>(*cachedEngine);
|
||||
} else {
|
||||
engine = std::make_shared<ShardokEngine>(
|
||||
gameSettings_,
|
||||
shardokState->getShardokState(),
|
||||
criticalTileCoords_,
|
||||
0,
|
||||
false);
|
||||
// Populate command cache (result intentionally unused, just populating cache)
|
||||
[[maybe_unused]] const auto commands =
|
||||
engine->GetAvailableCommandsForAIPlayer(currentPlayer);
|
||||
shardokState->setCachedEngine(engine);
|
||||
engine = std::make_shared<ShardokEngine>(*engine);
|
||||
}
|
||||
|
||||
engine->PostCommand(currentPlayer, shardokAction->getIndex(), nullptr);
|
||||
shardokState->getMutableShardokState() = engine->GetCurrentGameState();
|
||||
// Clear the cached engine since the state has been mutated
|
||||
shardokState->setCachedEngine(nullptr);
|
||||
}
|
||||
|
||||
std::vector<std::unique_ptr<MCTSAction>> ShardokGameEngine::getLegalActions(
|
||||
const MCTSGameState& state) const {
|
||||
const auto* shardokState = dynamic_cast<const ShardokGameState*>(&state);
|
||||
if (!shardokState) { return {}; }
|
||||
|
||||
const auto currentPlayer = static_cast<PlayerId>(state.currentPlayerId());
|
||||
|
||||
// Stop simulation if it's not our player's turn (turn boundary)
|
||||
if (currentPlayer != playerId_) { return {}; }
|
||||
|
||||
// Use cached engine if available, otherwise create and cache it
|
||||
std::shared_ptr<ShardokEngine> engine;
|
||||
if (auto cachedEngine = shardokState->getCachedEngine()) {
|
||||
engine = cachedEngine;
|
||||
} else {
|
||||
engine = std::make_shared<ShardokEngine>(
|
||||
gameSettings_,
|
||||
shardokState->getShardokState(),
|
||||
criticalTileCoords_);
|
||||
shardokState->setCachedEngine(engine);
|
||||
}
|
||||
|
||||
const CommandListSPtr commands = engine->GetAvailableCommandsForAIPlayer(currentPlayer);
|
||||
|
||||
if (!commands || commands->empty()) { return {}; }
|
||||
|
||||
// Filter commands using AICommandFilter (matching original MCTSAI behavior)
|
||||
// Use gameSettings for battalion type lookups
|
||||
const std::vector<size_t> filteredIndices = AICommandFilter::FilterCommands(
|
||||
commands,
|
||||
currentPlayer,
|
||||
isDefender_,
|
||||
shardokState->getShardokState(),
|
||||
*apdCache_,
|
||||
[this](BattalionTypeId typeId) {
|
||||
return gameSettings_->GetGetter().GetBattalionType(typeId);
|
||||
});
|
||||
|
||||
// Convert only filtered commands to MCTSActions
|
||||
std::vector<std::unique_ptr<MCTSAction>> actions;
|
||||
actions.reserve(filteredIndices.size());
|
||||
|
||||
for (const size_t idx : filteredIndices) {
|
||||
if (idx < commands->size()) {
|
||||
const auto& cmd = commands->at(idx);
|
||||
actions.push_back(std::make_unique<ShardokAction>(cmd->GetCommandProto(), idx));
|
||||
}
|
||||
}
|
||||
|
||||
return actions;
|
||||
}
|
||||
|
||||
bool ShardokGameEngine::isTerminal(const MCTSGameState& state) const { return state.isTerminal(); }
|
||||
|
||||
double ShardokGameEngine::evaluateState(const MCTSGameState& state, MCTSPlayerId playerId) const {
|
||||
return state.score(playerId);
|
||||
}
|
||||
|
||||
std::vector<size_t> ShardokGameEngine::filterActions(
|
||||
const std::vector<std::unique_ptr<MCTSAction>>& actions,
|
||||
const MCTSGameState& state) const {
|
||||
const auto* shardokState = dynamic_cast<const ShardokGameState*>(&state);
|
||||
if (!shardokState || !commandFilter_) {
|
||||
// No filtering - return all indices
|
||||
std::vector<size_t> indices;
|
||||
indices.reserve(actions.size());
|
||||
for (size_t i = 0; i < actions.size(); ++i) { indices.push_back(i); }
|
||||
return indices;
|
||||
}
|
||||
|
||||
// For now, return all indices - proper filtering would need more work
|
||||
// to match the AICommandFilter::FilterCommands signature
|
||||
std::vector<size_t> indices;
|
||||
indices.reserve(actions.size());
|
||||
for (size_t i = 0; i < actions.size(); ++i) { indices.push_back(i); }
|
||||
return indices;
|
||||
}
|
||||
|
||||
double ShardokGameEngine::getActionScore(
|
||||
const MCTSGameState& state,
|
||||
const MCTSAction& action,
|
||||
MCTSPlayerId playerId) const {
|
||||
auto newState = applyAction(state, action);
|
||||
if (!newState) { return 0.0; }
|
||||
return newState->score(playerId);
|
||||
}
|
||||
|
||||
bool ShardokGameEngine::shouldStopSearch(
|
||||
const MCTSGameState& /*state*/,
|
||||
int /*iterations*/,
|
||||
std::chrono::steady_clock::time_point /*startTime*/) const {
|
||||
// Could add early termination logic here
|
||||
return false;
|
||||
}
|
||||
|
||||
size_t ShardokGameEngine::mapFilteredIndexToOriginal(
|
||||
size_t filteredIndex,
|
||||
const MCTSGameState& state) const {
|
||||
// Get the filtered actions (uses cached engine)
|
||||
auto actions = getLegalActions(state);
|
||||
|
||||
// Check bounds
|
||||
if (filteredIndex >= actions.size()) { return filteredIndex; }
|
||||
|
||||
// Extract the original index from the ShardokAction
|
||||
const auto* shardokAction = dynamic_cast<const ShardokAction*>(actions[filteredIndex].get());
|
||||
if (!shardokAction) { return filteredIndex; }
|
||||
|
||||
// ShardokAction stores the original unfiltered index
|
||||
return shardokAction->getIndex();
|
||||
}
|
||||
|
||||
} // namespace shardok::mcts
|
||||
@@ -0,0 +1,99 @@
|
||||
//
|
||||
// Shardok-specific game engine adapter for MCTS
|
||||
//
|
||||
|
||||
#ifndef EAGLE0_SHARDOK_GAME_ENGINE_HPP
|
||||
#define EAGLE0_SHARDOK_GAME_ENGINE_HPP
|
||||
|
||||
#include <functional>
|
||||
#include <memory>
|
||||
#include <vector>
|
||||
|
||||
#include "src/main/cpp/net/eagle0/common/mcts/abstract/MCTSGameEngine.hpp"
|
||||
#include "src/main/cpp/net/eagle0/shardok/ai/AIAttackLocations.hpp"
|
||||
#include "src/main/cpp/net/eagle0/shardok/ai/AIStrategy.hpp"
|
||||
#include "src/main/cpp/net/eagle0/shardok/library/ShardokCTypes.h"
|
||||
#include "src/main/cpp/net/eagle0/shardok/library/ShardokEngine.hpp"
|
||||
#include "src/main/cpp/net/eagle0/shardok/library/action_point_distances/ActionPointDistancesCache.hpp"
|
||||
#include "src/main/cpp/net/eagle0/shardok/library/map/CoordsSet.hpp"
|
||||
#include "src/main/cpp/net/eagle0/shardok/library/settings/GameSettings.hpp"
|
||||
|
||||
namespace shardok {
|
||||
|
||||
// Forward declarations
|
||||
class AICommandFilter;
|
||||
class AIScoreCalculator;
|
||||
class RandomGenerator;
|
||||
|
||||
// Use existing type definitions from the Shardok codebase
|
||||
// GameSettingsSPtr and SettingsGetter are defined in GameSettings.hpp
|
||||
|
||||
namespace mcts {
|
||||
|
||||
class ShardokGameEngine : public MCTSGameEngine {
|
||||
public:
|
||||
ShardokGameEngine(
|
||||
const ShardokEngine* engine,
|
||||
const AICommandFilter* commandFilter,
|
||||
const AIScoreCalculator* scoreCalculator,
|
||||
const GameSettingsSPtr& gameSettings,
|
||||
const APDCache* apdCache,
|
||||
const ALCache* alCache,
|
||||
PlayerId playerId,
|
||||
bool isDefender,
|
||||
const AIStrategy& strategy,
|
||||
const CoordsSet& castleCoords,
|
||||
const CoordsSet& criticalTileCoords);
|
||||
|
||||
// MCTSGameEngine interface implementation
|
||||
[[nodiscard]] std::unique_ptr<MCTSGameState> applyAction(
|
||||
const MCTSGameState& state,
|
||||
const MCTSAction& action) const override;
|
||||
|
||||
void applyActionMutable(std::unique_ptr<MCTSGameState>& state, const MCTSAction& action)
|
||||
const override;
|
||||
|
||||
[[nodiscard]] std::vector<std::unique_ptr<MCTSAction>> getLegalActions(
|
||||
const MCTSGameState& state) const override;
|
||||
|
||||
[[nodiscard]] bool isTerminal(const MCTSGameState& state) const override;
|
||||
|
||||
[[nodiscard]] double evaluateState(const MCTSGameState& state, MCTSPlayerId playerId)
|
||||
const override;
|
||||
|
||||
[[nodiscard]] std::vector<size_t> filterActions(
|
||||
const std::vector<std::unique_ptr<MCTSAction>>& actions,
|
||||
const MCTSGameState& state) const override;
|
||||
|
||||
[[nodiscard]] double getActionScore(
|
||||
const MCTSGameState& state,
|
||||
const MCTSAction& action,
|
||||
MCTSPlayerId playerId) const override;
|
||||
|
||||
[[nodiscard]] bool shouldStopSearch(
|
||||
const MCTSGameState& state,
|
||||
int iterations,
|
||||
std::chrono::steady_clock::time_point startTime) const override;
|
||||
|
||||
[[nodiscard]] size_t mapFilteredIndexToOriginal(
|
||||
size_t filteredIndex,
|
||||
const MCTSGameState& state) const override;
|
||||
|
||||
private:
|
||||
const AICommandFilter* commandFilter_;
|
||||
const AIScoreCalculator* scoreCalculator_;
|
||||
GameSettingsSPtr gameSettings_;
|
||||
const APDCache* apdCache_;
|
||||
const ALCache* alCache_;
|
||||
PlayerId playerId_;
|
||||
bool isDefender_;
|
||||
AIStrategy strategy_;
|
||||
const CoordsSet& castleCoords_;
|
||||
// Computed once to avoid 8.5% overhead per engine construction
|
||||
const CoordsSet& criticalTileCoords_;
|
||||
};
|
||||
|
||||
} // namespace mcts
|
||||
} // namespace shardok
|
||||
|
||||
#endif // EAGLE0_SHARDOK_GAME_ENGINE_HPP
|
||||
@@ -0,0 +1,101 @@
|
||||
//
|
||||
// Shardok-specific game state adapter implementation
|
||||
//
|
||||
|
||||
#include "ShardokGameState.hpp"
|
||||
|
||||
#include <sstream>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
|
||||
#include "src/main/cpp/net/eagle0/shardok/ai/AIScoreCalculator.hpp"
|
||||
#include "src/main/cpp/net/eagle0/shardok/library/settings/GameSettings.hpp"
|
||||
|
||||
namespace shardok::mcts {
|
||||
|
||||
ShardokGameState::ShardokGameState(
|
||||
GameStateW state,
|
||||
const AIScoreCalculator* calculator,
|
||||
const GameSettings* settings,
|
||||
const bool isDefender,
|
||||
AIStrategy strategy,
|
||||
const CoordsSet& castleCoords,
|
||||
const APDCache& apdCache,
|
||||
const ALCache& alCache,
|
||||
const CoordsSet& criticalTileCoords)
|
||||
: state_(std::move(state)),
|
||||
scoreCalculator_(calculator),
|
||||
settings_(settings),
|
||||
isDefender_(isDefender),
|
||||
strategy_(std::move(strategy)),
|
||||
castleCoords_(castleCoords),
|
||||
apdCache_(apdCache),
|
||||
alCache_(alCache),
|
||||
criticalTileCoords_(criticalTileCoords) {}
|
||||
|
||||
uint64_t ShardokGameState::hash() const {
|
||||
if (!hashCached_) {
|
||||
cachedHash_ = state_.ComputeFNV1aHash();
|
||||
hashCached_ = true;
|
||||
}
|
||||
return cachedHash_;
|
||||
}
|
||||
|
||||
double ShardokGameState::score(MCTSPlayerId /*playerId*/) const {
|
||||
return scoreCalculator_->GuessedStateScore(isDefender_, state_, strategy_, castleCoords_);
|
||||
}
|
||||
|
||||
MCTSPlayerId ShardokGameState::currentPlayerId() const { return state_->current_player(); }
|
||||
|
||||
bool ShardokGameState::isTerminal() const {
|
||||
// Check if game status indicates the game is over
|
||||
if (state_->status()) {
|
||||
const auto gameStatus = state_->status()->state();
|
||||
if (gameStatus == net::eagle0::shardok::storage::fb::GameStatus_::State_VICTORY ||
|
||||
gameStatus == net::eagle0::shardok::storage::fb::GameStatus_::State_DRAW) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// Check max rounds
|
||||
if (state_->current_round() >= settings_->GetGetter().Backing().max_rounds()) { return true; }
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
std::unique_ptr<MCTSGameState> ShardokGameState::clone() const {
|
||||
auto cloned = std::make_unique<ShardokGameState>(
|
||||
state_,
|
||||
scoreCalculator_,
|
||||
settings_,
|
||||
isDefender_,
|
||||
strategy_,
|
||||
castleCoords_,
|
||||
apdCache_,
|
||||
alCache_,
|
||||
criticalTileCoords_);
|
||||
// Don't copy the cached engine - each state needs its own
|
||||
return cloned;
|
||||
}
|
||||
|
||||
bool ShardokGameState::equals(const MCTSGameState& other) const {
|
||||
const auto* shardokOther = dynamic_cast<const ShardokGameState*>(&other);
|
||||
if (!shardokOther) { return false; }
|
||||
|
||||
return hash() == shardokOther->hash();
|
||||
}
|
||||
|
||||
MCTSPlayerId ShardokGameState::getWinner() const {
|
||||
// Note: FlatBuffer doesn't have a winner field
|
||||
// In practice, this would need to determine winner from victory conditions
|
||||
return -1; // No winner
|
||||
}
|
||||
|
||||
std::string ShardokGameState::toString() const {
|
||||
std::stringstream ss;
|
||||
ss << "ShardokGameState[Round:" << static_cast<int>(state_->current_round())
|
||||
<< " Player:" << currentPlayerId() << " Hash:" << hash() << "]";
|
||||
return ss.str();
|
||||
}
|
||||
|
||||
} // namespace shardok::mcts
|
||||
@@ -0,0 +1,77 @@
|
||||
//
|
||||
// Shardok-specific game state adapter for MCTS
|
||||
//
|
||||
|
||||
#ifndef EAGLE0_SHARDOK_GAME_STATE_HPP
|
||||
#define EAGLE0_SHARDOK_GAME_STATE_HPP
|
||||
|
||||
#include <memory>
|
||||
|
||||
#include "src/main/cpp/net/eagle0/common/mcts/abstract/MCTSGameState.hpp"
|
||||
#include "src/main/cpp/net/eagle0/shardok/ai/AIStrategy.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"
|
||||
#include "src/main/cpp/net/eagle0/shardok/library/map/CoordsSet.hpp"
|
||||
#include "src/main/cpp/net/eagle0/shardok/library/settings/GameSettings.hpp"
|
||||
|
||||
namespace shardok {
|
||||
|
||||
// Forward declarations
|
||||
class AIScoreCalculator;
|
||||
|
||||
namespace mcts {
|
||||
|
||||
class ShardokGameState : public MCTSGameState {
|
||||
public:
|
||||
ShardokGameState(
|
||||
GameStateW state,
|
||||
const AIScoreCalculator* calculator,
|
||||
const GameSettings* settings,
|
||||
bool isDefender,
|
||||
AIStrategy strategy,
|
||||
const CoordsSet& castleCoords,
|
||||
const APDCache& apdCache,
|
||||
const ALCache& alCache,
|
||||
const CoordsSet& criticalTileCoords);
|
||||
|
||||
// MCTSGameState interface implementation
|
||||
[[nodiscard]] uint64_t hash() const override;
|
||||
[[nodiscard]] double score(MCTSPlayerId playerId) const override;
|
||||
[[nodiscard]] MCTSPlayerId currentPlayerId() const override;
|
||||
[[nodiscard]] bool isTerminal() const override;
|
||||
[[nodiscard]] std::unique_ptr<MCTSGameState> clone() const override;
|
||||
[[nodiscard]] bool equals(const MCTSGameState& other) const override;
|
||||
[[nodiscard]] MCTSPlayerId getWinner() const override;
|
||||
[[nodiscard]] std::string toString() const override;
|
||||
|
||||
// Shardok-specific accessors
|
||||
[[nodiscard]] const GameStateW& getShardokState() const { return state_; }
|
||||
[[nodiscard]] GameStateW& getMutableShardokState() { return state_; }
|
||||
[[nodiscard]] bool isDefender() const { return isDefender_; }
|
||||
[[nodiscard]] const GameSettings* getSettings() const { return settings_; }
|
||||
[[nodiscard]] const CoordsSet& getCriticalTileCoords() const { return criticalTileCoords_; }
|
||||
|
||||
// Engine caching for performance (avoids recomputing available commands)
|
||||
void setCachedEngine(std::shared_ptr<ShardokEngine> engine) const { cachedEngine_ = engine; }
|
||||
[[nodiscard]] std::shared_ptr<ShardokEngine> getCachedEngine() const { return cachedEngine_; }
|
||||
|
||||
private:
|
||||
GameStateW state_;
|
||||
const AIScoreCalculator* scoreCalculator_;
|
||||
const GameSettings* settings_;
|
||||
bool isDefender_;
|
||||
AIStrategy strategy_;
|
||||
const CoordsSet& castleCoords_;
|
||||
const APDCache& apdCache_;
|
||||
const ALCache& alCache_;
|
||||
mutable uint64_t cachedHash_ = 0;
|
||||
mutable bool hashCached_ = false;
|
||||
const CoordsSet& criticalTileCoords_;
|
||||
mutable std::shared_ptr<ShardokEngine> cachedEngine_; // Engine with cached available commands
|
||||
};
|
||||
|
||||
} // namespace mcts
|
||||
} // namespace shardok
|
||||
|
||||
#endif // EAGLE0_SHARDOK_GAME_STATE_HPP
|
||||
@@ -0,0 +1,89 @@
|
||||
//
|
||||
// Factory implementation for creating Shardok-specific MCTS components
|
||||
//
|
||||
|
||||
#include "ShardokMCTSFactory.hpp"
|
||||
|
||||
#include "ShardokAction.hpp"
|
||||
#include "ShardokGameEngine.hpp"
|
||||
#include "ShardokGameState.hpp"
|
||||
#include "src/main/cpp/net/eagle0/shardok/library/ShardokEngine.hpp"
|
||||
#include "src/main/cpp/net/eagle0/shardok/library/settings/GameSettings.hpp"
|
||||
|
||||
namespace shardok::mcts {
|
||||
|
||||
std::unique_ptr<MCTSGameEngine> ShardokMCTSFactory::createGameEngine(
|
||||
const ShardokEngine& engine,
|
||||
const AICommandFilter* commandFilter,
|
||||
const AIScoreCalculator* scoreCalculator,
|
||||
const GameSettingsSPtr& gameSettings,
|
||||
const APDCache& apdCache,
|
||||
const ALCache& alCache,
|
||||
PlayerId playerId,
|
||||
bool isDefender,
|
||||
const AIStrategy& strategy,
|
||||
const CoordsSet& castleCoords,
|
||||
const CoordsSet& criticalTileCoords) {
|
||||
return std::make_unique<ShardokGameEngine>(
|
||||
&engine,
|
||||
commandFilter,
|
||||
scoreCalculator,
|
||||
gameSettings,
|
||||
&apdCache,
|
||||
&alCache,
|
||||
playerId,
|
||||
isDefender,
|
||||
strategy,
|
||||
castleCoords,
|
||||
criticalTileCoords);
|
||||
}
|
||||
|
||||
std::unique_ptr<MCTSGameState> ShardokMCTSFactory::createGameState(
|
||||
const GameStateW& state,
|
||||
const AIScoreCalculator* scoreCalculator,
|
||||
const GameSettingsSPtr& settings,
|
||||
bool isDefender,
|
||||
const AIStrategy& strategy,
|
||||
const CoordsSet& castleCoords,
|
||||
const APDCache& apdCache,
|
||||
const ALCache& alCache,
|
||||
const CoordsSet& criticalTileCoords) {
|
||||
return std::make_unique<ShardokGameState>(
|
||||
state,
|
||||
scoreCalculator,
|
||||
settings.get(), // Get raw pointer from shared_ptr
|
||||
isDefender,
|
||||
strategy,
|
||||
castleCoords,
|
||||
apdCache,
|
||||
alCache,
|
||||
criticalTileCoords);
|
||||
}
|
||||
|
||||
std::vector<std::unique_ptr<MCTSAction>> ShardokMCTSFactory::createActions(
|
||||
const std::vector<CommandProto>& commands) {
|
||||
std::vector<std::unique_ptr<MCTSAction>> actions;
|
||||
actions.reserve(commands.size());
|
||||
|
||||
for (size_t i = 0; i < commands.size(); ++i) {
|
||||
actions.push_back(std::make_unique<ShardokAction>(commands[i], i));
|
||||
}
|
||||
|
||||
return actions;
|
||||
}
|
||||
|
||||
std::vector<std::unique_ptr<MCTSAction>> ShardokMCTSFactory::createActionsFromCommandList(
|
||||
const CommandListSPtr& commands) {
|
||||
std::vector<std::unique_ptr<MCTSAction>> actions;
|
||||
if (!commands) { return actions; }
|
||||
|
||||
actions.reserve(commands->size());
|
||||
for (size_t i = 0; i < commands->size(); ++i) {
|
||||
const auto& cmd = (*commands)[i];
|
||||
actions.push_back(std::make_unique<ShardokAction>(cmd->GetCommandProto(), i));
|
||||
}
|
||||
|
||||
return actions;
|
||||
}
|
||||
|
||||
} // namespace shardok::mcts
|
||||
@@ -0,0 +1,87 @@
|
||||
//
|
||||
// Factory for creating Shardok-specific MCTS components
|
||||
//
|
||||
|
||||
#ifndef EAGLE0_SHARDOK_MCTS_FACTORY_HPP
|
||||
#define EAGLE0_SHARDOK_MCTS_FACTORY_HPP
|
||||
|
||||
#include <functional>
|
||||
#include <memory>
|
||||
#include <vector>
|
||||
|
||||
#include "src/main/cpp/net/eagle0/shardok/ai/AIAttackLocations.hpp"
|
||||
#include "src/main/cpp/net/eagle0/shardok/ai/AIStrategy.hpp"
|
||||
#include "src/main/cpp/net/eagle0/shardok/library/ShardokCTypes.h"
|
||||
#include "src/main/cpp/net/eagle0/shardok/library/ShardokCommand.hpp"
|
||||
#include "src/main/cpp/net/eagle0/shardok/library/action_point_distances/ActionPointDistancesCache.hpp"
|
||||
#include "src/main/cpp/net/eagle0/shardok/library/map/CoordsSet.hpp"
|
||||
#include "src/main/cpp/net/eagle0/shardok/library/settings/GameSettings.hpp"
|
||||
#include "src/main/protobuf/net/eagle0/shardok/api/command_descriptor.pb.h"
|
||||
|
||||
namespace shardok {
|
||||
|
||||
// Forward declarations
|
||||
class ShardokEngine;
|
||||
class AICommandFilter;
|
||||
class AIScoreCalculator;
|
||||
class GameStateW;
|
||||
class GameSettings;
|
||||
|
||||
// Use existing type definitions to avoid conflicts
|
||||
// These are already defined in the Shardok codebase:
|
||||
// - APDCache in ActionPointDistancesCache.hpp
|
||||
// - ALCache in AIAttackLocations.hpp
|
||||
// - CommandListSPtr in ShardokCommand.hpp
|
||||
// - SettingsGetter in GameSettings.hpp
|
||||
using CommandProto = net::eagle0::shardok::api::CommandDescriptor;
|
||||
|
||||
namespace mcts {
|
||||
|
||||
// Forward declarations
|
||||
class MCTSGameEngine;
|
||||
class MCTSGameState;
|
||||
class MCTSAction;
|
||||
|
||||
class ShardokMCTSFactory {
|
||||
public:
|
||||
using CommandProto = net::eagle0::shardok::api::CommandDescriptor;
|
||||
|
||||
// Create a Shardok game engine adapter
|
||||
[[nodiscard]] static std::unique_ptr<MCTSGameEngine> createGameEngine(
|
||||
const ShardokEngine& engine,
|
||||
const AICommandFilter* commandFilter,
|
||||
const AIScoreCalculator* scoreCalculator,
|
||||
const GameSettingsSPtr& gameSettings,
|
||||
const APDCache& apdCache,
|
||||
const ALCache& alCache,
|
||||
PlayerId playerId,
|
||||
bool isDefender,
|
||||
const AIStrategy& strategy,
|
||||
const CoordsSet& castleCoords,
|
||||
const CoordsSet& criticalTileCoords);
|
||||
|
||||
// Create a Shardok game state adapter
|
||||
[[nodiscard]] static std::unique_ptr<MCTSGameState> createGameState(
|
||||
const GameStateW& state,
|
||||
const AIScoreCalculator* scoreCalculator,
|
||||
const GameSettingsSPtr& settings,
|
||||
bool isDefender,
|
||||
const AIStrategy& strategy,
|
||||
const CoordsSet& castleCoords,
|
||||
const APDCache& apdCache,
|
||||
const ALCache& alCache,
|
||||
const CoordsSet& criticalTileCoords);
|
||||
|
||||
// Convert Shardok commands to MCTS actions
|
||||
[[nodiscard]] static std::vector<std::unique_ptr<MCTSAction>> createActions(
|
||||
const std::vector<CommandProto>& commands);
|
||||
|
||||
// Convert from command list to MCTS actions
|
||||
[[nodiscard]] static std::vector<std::unique_ptr<MCTSAction>> createActionsFromCommandList(
|
||||
const CommandListSPtr& commands);
|
||||
};
|
||||
|
||||
} // namespace mcts
|
||||
} // namespace shardok
|
||||
|
||||
#endif // EAGLE0_SHARDOK_MCTS_FACTORY_HPP
|
||||
@@ -1,16 +0,0 @@
|
||||
load("//tools:copts.bzl", "COPTS")
|
||||
|
||||
cc_library(
|
||||
name = "mcts_node",
|
||||
hdrs = ["MCTSNode.hpp"],
|
||||
copts = COPTS,
|
||||
visibility = [
|
||||
"//src/main/cpp/net/eagle0/shardok/ai/mcts:__pkg__",
|
||||
"//src/test/cpp/net/eagle0/shardok/ai/mcts:__pkg__",
|
||||
],
|
||||
deps = [
|
||||
"//src/main/cpp/net/eagle0/shardok/library:game_state_w",
|
||||
"//src/main/cpp/net/eagle0/shardok/library:shardok_c_types",
|
||||
"//src/main/protobuf/net/eagle0/shardok/common:command_type_cc_proto",
|
||||
],
|
||||
)
|
||||
@@ -0,0 +1,56 @@
|
||||
load("//tools:copts.bzl", "COPTS")
|
||||
|
||||
# StateRecorder library for recording game states during self-play
|
||||
cc_library(
|
||||
name = "state_recorder",
|
||||
srcs = ["StateRecorder.cpp"],
|
||||
hdrs = ["StateRecorder.hpp"],
|
||||
copts = COPTS,
|
||||
visibility = ["//visibility:public"],
|
||||
deps = [
|
||||
"//src/main/cpp/net/eagle0/shardok/library:shardok_c_types",
|
||||
"//src/main/protobuf/net/eagle0/shardok/api:game_state_view_cc_proto",
|
||||
"@com_google_protobuf//:protobuf",
|
||||
],
|
||||
)
|
||||
|
||||
# Self-play battle runner with state recording
|
||||
cc_library(
|
||||
name = "self_play_battle_runner",
|
||||
srcs = ["SelfPlayBattleRunner.cpp"],
|
||||
hdrs = ["SelfPlayBattleRunner.hpp"],
|
||||
copts = COPTS,
|
||||
visibility = ["//visibility:public"],
|
||||
deps = [
|
||||
":state_recorder",
|
||||
"//src/main/cpp/net/eagle0/shardok/ai:shardok_ai_client",
|
||||
"//src/main/cpp/net/eagle0/shardok/ai_battle_simulator:ai_battle_config",
|
||||
"//src/main/cpp/net/eagle0/shardok/ai_battle_simulator:ai_battle_simulator",
|
||||
"//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/settings:game_settings",
|
||||
"//src/main/protobuf/net/eagle0/shardok/common:command_type_cc_proto",
|
||||
],
|
||||
)
|
||||
|
||||
# Self-play generator binary for generating ML training data
|
||||
cc_binary(
|
||||
name = "self_play_generator",
|
||||
srcs = ["self_play_generator_main.cpp"],
|
||||
copts = COPTS,
|
||||
data = [
|
||||
"//src/main/resources/net/eagle0/shardok:battalion_types",
|
||||
"//src/main/resources/net/eagle0/shardok:settings",
|
||||
"//src/main/resources/net/eagle0/shardok/maps",
|
||||
],
|
||||
deps = [
|
||||
":self_play_battle_runner",
|
||||
":state_recorder",
|
||||
"//src/main/cpp/net/eagle0/common:filesystem_utils",
|
||||
"//src/main/cpp/net/eagle0/common:tsv_parser",
|
||||
"//src/main/cpp/net/eagle0/shardok/ai_battle_simulator:ai_battle_config",
|
||||
"//src/main/cpp/net/eagle0/shardok/ai_battle_simulator:ai_battle_simulator",
|
||||
"//src/main/cpp/net/eagle0/shardok/library/settings:game_settings",
|
||||
"//src/main/cpp/net/eagle0/shardok/util:battalion_type_registrar",
|
||||
],
|
||||
)
|
||||
@@ -0,0 +1,166 @@
|
||||
# Machine Learning Training Data Collection (Phase 1)
|
||||
|
||||
## Overview
|
||||
|
||||
This directory contains the initial implementation of Phase 1 from the ML Value Function plan: data collection infrastructure for training a neural network to predict win probability from game states.
|
||||
|
||||
## Components
|
||||
|
||||
### StateRecorder (`StateRecorder.hpp/cpp`)
|
||||
|
||||
Records game states during self-play for ML training. Key features:
|
||||
- Records `GameStateView` (player's partial information view)
|
||||
- Stores player ID and round number for each state
|
||||
- Labels all states with final game outcome (win/loss/draw)
|
||||
- Binary file format for efficient storage
|
||||
|
||||
**Status**: ✅ **Fully implemented and tested**
|
||||
|
||||
### Self-Play Generator (`self_play_generator_main.cpp`)
|
||||
|
||||
Command-line tool to generate training data by running AI vs AI games.
|
||||
|
||||
**Current Status**: ⚠️ **Partially implemented**
|
||||
|
||||
The tool currently:
|
||||
- ✅ Runs complete AI vs AI battles using the existing `AiBattleSimulator`
|
||||
- ✅ Collects game outcome statistics (attacker wins, defender wins, draws)
|
||||
- ✅ Supports custom battle configurations
|
||||
- ✅ Command-line interface with configurable options
|
||||
|
||||
What's missing:
|
||||
- ❌ Per-action state recording (requires refactoring `AiBattleSimulator`)
|
||||
- ❌ Writing training data files
|
||||
|
||||
### Next Steps for Full Phase 1 Implementation
|
||||
|
||||
To complete Phase 1 data collection, we need to:
|
||||
|
||||
1. **Refactor AiBattleSimulator** to support extensibility:
|
||||
```cpp
|
||||
// Add callback parameter to RunBattlePhase
|
||||
using PostCommandCallback = std::function<void(
|
||||
const ShardokEngine& engine,
|
||||
PlayerId currentPlayer,
|
||||
const CommandProto& command,
|
||||
bool isEndTurn)>;
|
||||
|
||||
BattleResult RunBattlePhase(
|
||||
ShardokEngine& engine,
|
||||
ShardokAIClient& attackerAI,
|
||||
ShardokAIClient& defenderAI,
|
||||
const PostCommandCallback& callback = nullptr);
|
||||
```
|
||||
|
||||
2. **Integrate StateRecorder with game loop**:
|
||||
```cpp
|
||||
StateRecorder recorder;
|
||||
|
||||
auto recordState = [&](const ShardokEngine& engine, PlayerId player,
|
||||
const CommandProto& cmd, bool isEndTurn) {
|
||||
if (!isEndTurn) {
|
||||
auto view = engine.GetGameStateView(player);
|
||||
recorder.RecordStateView(view, player);
|
||||
}
|
||||
};
|
||||
|
||||
simulator.RunBattle(recordState);
|
||||
recorder.SetGameOutcome(result.winner);
|
||||
recorder.WriteToFile("game_0001.bin");
|
||||
```
|
||||
|
||||
3. **Test full pipeline**: Generate 100 games and verify data files
|
||||
|
||||
## Usage
|
||||
|
||||
### Build
|
||||
|
||||
```bash
|
||||
bazel build //src/main/cpp/net/eagle0/shardok/ai/ml:self_play_generator
|
||||
```
|
||||
|
||||
### Run (Current Implementation)
|
||||
|
||||
```bash
|
||||
# Generate 100 games with default settings
|
||||
./bazel-bin/src/main/cpp/net/eagle0/shardok/ai/ml/self_play_generator --num_games=100
|
||||
|
||||
# Generate games with custom config
|
||||
./bazel-bin/src/main/cpp/net/eagle0/shardok/ai/ml/self_play_generator \
|
||||
--config=my_battle.json \
|
||||
--num_games=1000 \
|
||||
--output_dir=/data/training \
|
||||
--verbose
|
||||
```
|
||||
|
||||
### Expected Output (After Full Implementation)
|
||||
|
||||
```
|
||||
/data/training/
|
||||
├── game_000000.bin # Binary file with StateRecords
|
||||
├── game_000001.bin
|
||||
├── game_000002.bin
|
||||
...
|
||||
└── game_099999.bin
|
||||
```
|
||||
|
||||
Each `.bin` file contains:
|
||||
- Multiple `StateRecord` entries (one per non-END_TURN action)
|
||||
- Each record has: serialized GameStateView, player ID, round number, win/loss label
|
||||
|
||||
## File Format Specification
|
||||
|
||||
Binary format (little-endian):
|
||||
```
|
||||
[uint32_t num_records]
|
||||
For each record:
|
||||
[uint32_t view_size]
|
||||
[uint8_t[] serialized_game_state_view] # Protobuf bytes
|
||||
[int8_t current_player] # 0 or 1
|
||||
[int32_t round_number]
|
||||
[bool current_player_won] # Label
|
||||
```
|
||||
|
||||
## Integration Points
|
||||
|
||||
- **AiBattleSimulator**: Existing infrastructure for running complete games
|
||||
- **ShardokEngine**: Provides `GetGameStateView(PlayerId)` for player perspectives
|
||||
- **GameStateView**: Protobuf representing player's partial information
|
||||
- **StateRecorder**: Records and serializes training examples
|
||||
|
||||
## Testing
|
||||
|
||||
```bash
|
||||
# Build and run tests
|
||||
bazel build //src/main/cpp/net/eagle0/shardok/ai/ml:state_recorder
|
||||
bazel test //src/main/cpp/net/eagle0/shardok/ai/ml:... # TODO: Add unit tests
|
||||
|
||||
# Quick integration test
|
||||
./bazel-bin/src/main/cpp/net/eagle0/shardok/ai/ml/self_play_generator \
|
||||
--num_games=2 \
|
||||
--verbose
|
||||
```
|
||||
|
||||
## Design Decisions
|
||||
|
||||
1. **Record player VIEW, not full state**: Ensures model trains on partial information that matches inference conditions
|
||||
|
||||
2. **Record AFTER each action**: Captures resulting positions the AI wants to evaluate (except END_TURN which is a transition)
|
||||
|
||||
3. **Current player perspective**: Makes model generalizable to both attacker and defender
|
||||
|
||||
4. **Binary format**: Compact storage for large datasets (expected 50K+ games)
|
||||
|
||||
## Performance Considerations
|
||||
|
||||
Expected data volumes:
|
||||
- 50,000 games × ~50 states/game = 2.5M training examples
|
||||
- Each state ~10-50KB serialized
|
||||
- Total: ~50-250GB uncompressed
|
||||
- Recommendation: Use gzip compression for storage
|
||||
|
||||
## References
|
||||
|
||||
- Full ML plan: `../ML_VALUE_FUNCTION_PLAN.md`
|
||||
- AiBattleSimulator: `../../ai_battle_simulator/`
|
||||
- GameStateView proto: `src/main/protobuf/net/eagle0/shardok/api/game_state_view.proto`
|
||||
@@ -0,0 +1,180 @@
|
||||
//
|
||||
// Self-Play Battle Runner Implementation
|
||||
//
|
||||
|
||||
#include "SelfPlayBattleRunner.hpp"
|
||||
|
||||
#include <iostream>
|
||||
|
||||
#include "src/main/cpp/net/eagle0/shardok/ai_battle_simulator/AiBattleSimulator.hpp"
|
||||
|
||||
#pragma clang diagnostic push
|
||||
#pragma clang diagnostic ignored "-Wdeprecated-redundant-constexpr-static-def"
|
||||
#include "src/main/protobuf/net/eagle0/shardok/common/command_type.pb.h"
|
||||
#pragma clang diagnostic pop
|
||||
|
||||
namespace shardok {
|
||||
namespace ml {
|
||||
|
||||
namespace {
|
||||
constexpr PlayerId ATTACKER_ID = 0;
|
||||
constexpr PlayerId DEFENDER_ID = 1;
|
||||
} // namespace
|
||||
|
||||
SelfPlayBattleRunner::SelfPlayBattleRunner(
|
||||
const net::eagle0::shardok::ai_battle_simulator::BattleConfig& config,
|
||||
GameSettingsSPtr settings)
|
||||
: config_(config),
|
||||
gameSettings_(std::move(settings)) {}
|
||||
|
||||
SelfPlayResult SelfPlayBattleRunner::RunBattleWithRecording(
|
||||
StateRecorder& recorder,
|
||||
AIAlgorithmType aiAlgorithm,
|
||||
bool verbose) {
|
||||
// Silence unused parameter warning (CreateAIClient uses config, not the parameter)
|
||||
(void)aiAlgorithm;
|
||||
|
||||
// Use AiBattleSimulator as friend to access its internals
|
||||
ai_battle_simulator::AiBattleSimulator simulator(config_, gameSettings_);
|
||||
|
||||
if (verbose) { std::cout << "Creating initial game state...\n"; }
|
||||
|
||||
// Use simulator's private method to create initial state
|
||||
auto gameState = simulator.CreateInitialGameState();
|
||||
const auto* hexMap = gameState->hex_map();
|
||||
|
||||
if (verbose) { std::cout << "Creating AI clients...\n"; }
|
||||
|
||||
// Create AI clients with the specified algorithm
|
||||
auto attackerAI = simulator.CreateAIClient(ATTACKER_ID, hexMap);
|
||||
auto defenderAI = simulator.CreateAIClient(DEFENDER_ID, hexMap);
|
||||
|
||||
// Override the AI algorithm type if needed
|
||||
// (CreateAIClient uses config, but we want to override it)
|
||||
// For now, we'll work with what the config specifies
|
||||
|
||||
// Create the engine
|
||||
if (verbose) { std::cout << "Creating engine...\n"; }
|
||||
ShardokEngine engine(simulator.gameSettings_, gameState);
|
||||
|
||||
// Run setup phase (without recording - setup moves aren't interesting for training)
|
||||
if (verbose) { std::cout << "Running setup phase...\n"; }
|
||||
auto setupResult = simulator.RunSetupPhase(engine, *attackerAI, *defenderAI);
|
||||
|
||||
if (setupResult.endReason != ai_battle_simulator::BattleResult::EndReason::DRAW) {
|
||||
// Game ended during setup (shouldn't normally happen)
|
||||
SelfPlayResult result;
|
||||
result.winner = setupResult.winner;
|
||||
result.totalRounds = setupResult.totalRounds;
|
||||
result.totalCommands = setupResult.totalCommands;
|
||||
result.statesRecorded = 0;
|
||||
result.description = setupResult.description;
|
||||
return result;
|
||||
}
|
||||
|
||||
// Run battle phase WITH recording
|
||||
if (verbose) { std::cout << "Running battle phase with recording...\n"; }
|
||||
return RunBattlePhaseWithRecording(engine, *attackerAI, *defenderAI, recorder, verbose);
|
||||
}
|
||||
|
||||
SelfPlayResult SelfPlayBattleRunner::RunBattlePhaseWithRecording(
|
||||
ShardokEngine& engine,
|
||||
ShardokAIClient& attackerAI,
|
||||
ShardokAIClient& defenderAI,
|
||||
StateRecorder& recorder,
|
||||
bool verbose) {
|
||||
int totalCommands = 0;
|
||||
int statesRecorded = 0;
|
||||
int currentRound = 1;
|
||||
|
||||
// Main game loop with state recording
|
||||
while (!engine.GameIsOver() && currentRound <= config_.max_rounds()) {
|
||||
auto currentState = engine.GetCurrentGameState();
|
||||
PlayerId currentPlayer = currentState->current_player();
|
||||
|
||||
auto availableCommands = engine.GetAvailableCommandProtos(currentPlayer, false);
|
||||
if (availableCommands.empty()) {
|
||||
if (verbose) {
|
||||
std::cout << "No commands available for player " << static_cast<int>(currentPlayer)
|
||||
<< "\n";
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
// Choose which AI to use
|
||||
ShardokAIClient& activeAI = (currentPlayer == ATTACKER_ID) ? attackerAI : defenderAI;
|
||||
|
||||
// Get AI decision
|
||||
auto choiceResults = activeAI.ChooseCommandIndex(engine);
|
||||
const auto& chosenCommand = availableCommands[choiceResults.chosenIndex];
|
||||
|
||||
// Check if this is an END_TURN command
|
||||
const bool isEndTurn =
|
||||
chosenCommand.type() == net::eagle0::shardok::common::END_TURN_COMMAND;
|
||||
|
||||
// Apply command
|
||||
engine.PostCommand(currentPlayer, choiceResults.chosenIndex);
|
||||
totalCommands++;
|
||||
|
||||
// Record state AFTER the command (unless it was END_TURN)
|
||||
if (!isEndTurn) {
|
||||
auto newState = engine.GetCurrentGameState();
|
||||
PlayerId stillCurrentPlayer = newState->current_player();
|
||||
|
||||
// Get the player's view (partial information)
|
||||
auto playerView = engine.GetGameStateView(stillCurrentPlayer);
|
||||
|
||||
// Record it!
|
||||
recorder.RecordStateView(playerView, stillCurrentPlayer);
|
||||
statesRecorded++;
|
||||
|
||||
if (verbose && statesRecorded % 50 == 0) {
|
||||
std::cout << " Recorded " << statesRecorded << " states...\n";
|
||||
}
|
||||
}
|
||||
|
||||
// Check round progression
|
||||
auto newState = engine.GetCurrentGameState();
|
||||
if (newState->current_round() > currentRound) {
|
||||
if (verbose) {
|
||||
std::cout << "Round " << currentRound << " completed. States: " << statesRecorded
|
||||
<< "\n";
|
||||
}
|
||||
currentRound = newState->current_round();
|
||||
}
|
||||
}
|
||||
|
||||
// Determine winner
|
||||
auto finalState = engine.GetCurrentGameState();
|
||||
const auto* status = finalState->status();
|
||||
PlayerId winner = -1;
|
||||
|
||||
if (status->state() == net::eagle0::shardok::storage::fb::GameStatus_::State_VICTORY) {
|
||||
const auto* winningIds = status->winning_shardok_ids();
|
||||
if (winningIds && winningIds->size() > 0) { winner = winningIds->Get(0); }
|
||||
}
|
||||
|
||||
SelfPlayResult result;
|
||||
result.winner = winner;
|
||||
result.totalRounds = currentRound;
|
||||
result.totalCommands = totalCommands;
|
||||
result.statesRecorded = statesRecorded;
|
||||
|
||||
if (winner == ATTACKER_ID) {
|
||||
result.description = "Attacker won";
|
||||
} else if (winner == DEFENDER_ID) {
|
||||
result.description = "Defender won";
|
||||
} else {
|
||||
result.description = "Draw";
|
||||
}
|
||||
|
||||
if (verbose) {
|
||||
std::cout << "Battle complete: " << result.description
|
||||
<< " (States recorded: " << statesRecorded << ")\n";
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
} // namespace ml
|
||||
} // namespace shardok
|
||||
@@ -0,0 +1,70 @@
|
||||
//
|
||||
// Self-Play Battle Runner - Runs battles with state recording for ML training
|
||||
//
|
||||
|
||||
#ifndef EAGLE0_SELF_PLAY_BATTLE_RUNNER_HPP
|
||||
#define EAGLE0_SELF_PLAY_BATTLE_RUNNER_HPP
|
||||
|
||||
#include "StateRecorder.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/library/settings/GameSettings.hpp"
|
||||
|
||||
#pragma clang diagnostic push
|
||||
#pragma clang diagnostic ignored "-Wdeprecated-redundant-constexpr-static-def"
|
||||
#include "src/main/protobuf/net/eagle0/shardok/ai_battle_simulator/ai_battle_config.pb.h"
|
||||
#pragma clang diagnostic pop
|
||||
|
||||
namespace shardok {
|
||||
namespace ml {
|
||||
|
||||
struct SelfPlayResult {
|
||||
PlayerId winner;
|
||||
int totalRounds;
|
||||
int totalCommands;
|
||||
int statesRecorded;
|
||||
std::string description;
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief Runs a complete battle with state recording for ML training.
|
||||
*
|
||||
* This class duplicates the core game loop from AiBattleSimulator
|
||||
* but adds StateRecorder integration to capture game states.
|
||||
*/
|
||||
class SelfPlayBattleRunner {
|
||||
public:
|
||||
explicit SelfPlayBattleRunner(
|
||||
const net::eagle0::shardok::ai_battle_simulator::BattleConfig& config,
|
||||
GameSettingsSPtr settings);
|
||||
|
||||
/**
|
||||
* @brief Run a complete battle and record states for ML training.
|
||||
*
|
||||
* @param recorder The StateRecorder to write game states to
|
||||
* @param aiAlgorithm Which AI algorithm to use for both players
|
||||
* @param verbose Whether to print detailed progress
|
||||
* @return Battle result including winner and statistics
|
||||
*/
|
||||
SelfPlayResult
|
||||
RunBattleWithRecording(StateRecorder& recorder, AIAlgorithmType aiAlgorithm, bool verbose);
|
||||
|
||||
private:
|
||||
const net::eagle0::shardok::ai_battle_simulator::BattleConfig config_;
|
||||
GameSettingsSPtr gameSettings_;
|
||||
|
||||
/**
|
||||
* @brief Run the battle phase with state recording.
|
||||
*/
|
||||
SelfPlayResult RunBattlePhaseWithRecording(
|
||||
ShardokEngine& engine,
|
||||
ShardokAIClient& attackerAI,
|
||||
ShardokAIClient& defenderAI,
|
||||
StateRecorder& recorder,
|
||||
bool verbose);
|
||||
};
|
||||
|
||||
} // namespace ml
|
||||
} // namespace shardok
|
||||
|
||||
#endif // EAGLE0_SELF_PLAY_BATTLE_RUNNER_HPP
|
||||
@@ -0,0 +1,86 @@
|
||||
//
|
||||
// StateRecorder Implementation
|
||||
//
|
||||
|
||||
#include "StateRecorder.hpp"
|
||||
|
||||
#include <fstream>
|
||||
#include <iostream>
|
||||
#include <stdexcept>
|
||||
|
||||
namespace shardok {
|
||||
namespace ml {
|
||||
|
||||
void StateRecorder::RecordStateView(
|
||||
const net::eagle0::shardok::api::GameStateView& view,
|
||||
PlayerId currentPlayer) {
|
||||
// Serialize the protobuf to bytes
|
||||
std::vector<uint8_t> serialized(view.ByteSizeLong());
|
||||
if (!view.SerializeToArray(serialized.data(), static_cast<int>(serialized.size()))) {
|
||||
throw std::runtime_error("Failed to serialize GameStateView");
|
||||
}
|
||||
|
||||
// Extract round number from the view
|
||||
const int roundNumber = view.current_round();
|
||||
|
||||
// Create and store record
|
||||
states_.emplace_back(std::move(serialized), currentPlayer, roundNumber);
|
||||
}
|
||||
|
||||
void StateRecorder::SetGameOutcome(PlayerId winner) {
|
||||
winner_ = winner;
|
||||
|
||||
// Label all states based on outcome
|
||||
for (auto& state : states_) {
|
||||
if (winner == -1) {
|
||||
// Draw - nobody won
|
||||
state.currentPlayerWon = false;
|
||||
} else {
|
||||
// Label based on whether this state's player won
|
||||
state.currentPlayerWon = (state.currentPlayer == winner);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void StateRecorder::WriteToFile(const std::string& filename) const {
|
||||
if (winner_ == -1) { throw std::runtime_error("Cannot write to file: game outcome not set"); }
|
||||
|
||||
std::ofstream file(filename, std::ios::binary);
|
||||
if (!file.is_open()) {
|
||||
throw std::runtime_error("Failed to open file for writing: " + filename);
|
||||
}
|
||||
|
||||
// Write number of records
|
||||
const auto numRecords = static_cast<uint32_t>(states_.size());
|
||||
file.write(reinterpret_cast<const char*>(&numRecords), sizeof(numRecords));
|
||||
|
||||
// Write each record
|
||||
for (const auto& state : states_) {
|
||||
// Write size of serialized view
|
||||
const auto viewSize = static_cast<uint32_t>(state.serializedStateView.size());
|
||||
file.write(reinterpret_cast<const char*>(&viewSize), sizeof(viewSize));
|
||||
|
||||
// Write serialized view bytes
|
||||
file.write(
|
||||
reinterpret_cast<const char*>(state.serializedStateView.data()),
|
||||
static_cast<std::streamsize>(viewSize));
|
||||
|
||||
// Write current player (as int8_t to save space)
|
||||
const auto player = static_cast<int8_t>(state.currentPlayer);
|
||||
file.write(reinterpret_cast<const char*>(&player), sizeof(player));
|
||||
|
||||
// Write round number
|
||||
const auto round = static_cast<int32_t>(state.roundNumber);
|
||||
file.write(reinterpret_cast<const char*>(&round), sizeof(round));
|
||||
|
||||
// Write label (bool)
|
||||
file.write(reinterpret_cast<const char*>(&state.currentPlayerWon), sizeof(bool));
|
||||
}
|
||||
|
||||
file.close();
|
||||
|
||||
if (!file.good()) { throw std::runtime_error("Error writing to file: " + filename); }
|
||||
}
|
||||
|
||||
} // namespace ml
|
||||
} // namespace shardok
|
||||
@@ -0,0 +1,118 @@
|
||||
//
|
||||
// StateRecorder - Records game states during self-play for ML training
|
||||
//
|
||||
|
||||
#ifndef EAGLE0_STATE_RECORDER_HPP
|
||||
#define EAGLE0_STATE_RECORDER_HPP
|
||||
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "src/main/cpp/net/eagle0/shardok/library/ShardokCTypes.h"
|
||||
|
||||
#pragma clang diagnostic push
|
||||
#pragma clang diagnostic ignored "-Wdeprecated-redundant-constexpr-static-def"
|
||||
#include "src/main/protobuf/net/eagle0/shardok/api/game_state_view.pb.h"
|
||||
#pragma clang diagnostic pop
|
||||
|
||||
namespace shardok {
|
||||
namespace ml {
|
||||
|
||||
/**
|
||||
* @brief Records game state views during self-play games for ML training data collection.
|
||||
*
|
||||
* Usage:
|
||||
* StateRecorder recorder;
|
||||
* while (!game.IsTerminal()) {
|
||||
* AI makes move...
|
||||
* recorder.RecordStateView(engine.GetGameStateView(currentPlayer), currentPlayer);
|
||||
* }
|
||||
* recorder.SetGameOutcome(winner);
|
||||
* recorder.WriteToFile("game_0001.bin");
|
||||
*/
|
||||
class StateRecorder {
|
||||
public:
|
||||
/**
|
||||
* @brief A single recorded state with metadata
|
||||
*/
|
||||
struct StateRecord {
|
||||
// Serialized protobuf GameStateView (player's partial information)
|
||||
std::vector<uint8_t> serializedStateView;
|
||||
|
||||
// Which player's perspective this is
|
||||
PlayerId currentPlayer;
|
||||
|
||||
// Which round this state occurred in
|
||||
int roundNumber;
|
||||
|
||||
// Label: Did the current player win? (filled in after game ends)
|
||||
bool currentPlayerWon;
|
||||
|
||||
StateRecord(std::vector<uint8_t> stateView, PlayerId player, int round)
|
||||
: serializedStateView(std::move(stateView)),
|
||||
currentPlayer(player),
|
||||
roundNumber(round),
|
||||
currentPlayerWon(false) {}
|
||||
};
|
||||
|
||||
StateRecorder() = default;
|
||||
|
||||
/**
|
||||
* @brief Record a game state from the current player's perspective.
|
||||
*
|
||||
* This should be called AFTER each action is applied (except END_TURN).
|
||||
* Records the resulting position that the player reached.
|
||||
*
|
||||
* @param view The GameStateView from the current player's perspective
|
||||
* @param currentPlayer The player ID whose turn it is
|
||||
*/
|
||||
void RecordStateView(
|
||||
const net::eagle0::shardok::api::GameStateView& view,
|
||||
PlayerId currentPlayer);
|
||||
|
||||
/**
|
||||
* @brief Set the game outcome and label all recorded states.
|
||||
*
|
||||
* @param winner The player ID who won (-1 for draw)
|
||||
*/
|
||||
void SetGameOutcome(PlayerId winner);
|
||||
|
||||
/**
|
||||
* @brief Write all recorded states to a binary file.
|
||||
*
|
||||
* File format:
|
||||
* [uint32_t num_records]
|
||||
* For each record:
|
||||
* [uint32_t view_size]
|
||||
* [uint8_t[] serialized_view]
|
||||
* [int8_t current_player]
|
||||
* [int32_t round_number]
|
||||
* [bool current_player_won]
|
||||
*
|
||||
* @param filename Path to output file
|
||||
*/
|
||||
void WriteToFile(const std::string& filename) const;
|
||||
|
||||
/**
|
||||
* @brief Get the number of states recorded.
|
||||
*/
|
||||
[[nodiscard]] size_t GetRecordCount() const { return states_.size(); }
|
||||
|
||||
/**
|
||||
* @brief Clear all recorded states.
|
||||
*/
|
||||
void Clear() {
|
||||
states_.clear();
|
||||
winner_ = -1;
|
||||
}
|
||||
|
||||
private:
|
||||
std::vector<StateRecord> states_;
|
||||
PlayerId winner_ = -1; // -1 means not set yet
|
||||
};
|
||||
|
||||
} // namespace ml
|
||||
} // namespace shardok
|
||||
|
||||
#endif // EAGLE0_STATE_RECORDER_HPP
|
||||
@@ -0,0 +1,319 @@
|
||||
//
|
||||
// Self-Play Generator - Generate ML training data from AI vs AI games
|
||||
//
|
||||
|
||||
#include <filesystem>
|
||||
#include <iomanip>
|
||||
#include <iostream>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
|
||||
#include "SelfPlayBattleRunner.hpp"
|
||||
#include "StateRecorder.hpp"
|
||||
#include "src/main/cpp/net/eagle0/common/FilesystemUtils.hpp"
|
||||
#include "src/main/cpp/net/eagle0/common/TsvParser.hpp"
|
||||
#include "src/main/cpp/net/eagle0/shardok/ai/ShardokAIClient.hpp"
|
||||
#include "src/main/cpp/net/eagle0/shardok/ai_battle_simulator/AiBattleConfig.hpp"
|
||||
#include "src/main/cpp/net/eagle0/shardok/ai_battle_simulator/AiBattleSimulator.hpp"
|
||||
#include "src/main/cpp/net/eagle0/shardok/library/ShardokEngine.hpp"
|
||||
#include "src/main/cpp/net/eagle0/shardok/library/action_point_distances/FixedActionPointDistances.hpp"
|
||||
#include "src/main/cpp/net/eagle0/shardok/library/fb_helpers/GameStateHelpers.hpp"
|
||||
#include "src/main/cpp/net/eagle0/shardok/library/settings/GameSettings.hpp"
|
||||
#include "src/main/cpp/net/eagle0/shardok/util/BattalionTypeRegistrar.hpp"
|
||||
#include "src/main/cpp/net/eagle0/shardok/util/MapLoader.hpp"
|
||||
|
||||
#pragma clang diagnostic push
|
||||
#pragma clang diagnostic ignored "-Wdeprecated-redundant-constexpr-static-def"
|
||||
#include "src/main/protobuf/net/eagle0/shardok/common/command_type.pb.h"
|
||||
#include "src/main/protobuf/net/eagle0/shardok/common/player_info.pb.h"
|
||||
#pragma clang diagnostic pop
|
||||
|
||||
namespace {
|
||||
|
||||
using shardok::AIAlgorithmType;
|
||||
using shardok::GameSettingsSPtr;
|
||||
using shardok::GameStateW;
|
||||
using shardok::PlayerId;
|
||||
using shardok::ShardokAIClient;
|
||||
using shardok::ShardokEngine;
|
||||
using shardok::ai_battle_simulator::AiBattleConfigLoader;
|
||||
using shardok::ai_battle_simulator::AiBattleSimulator;
|
||||
using shardok::ai_battle_simulator::BattleResult;
|
||||
using shardok::ml::StateRecorder;
|
||||
|
||||
constexpr PlayerId ATTACKER_ID = 0;
|
||||
constexpr PlayerId DEFENDER_ID = 1;
|
||||
|
||||
struct GeneratorConfig {
|
||||
int numGames = 100;
|
||||
std::string outputDir = "/tmp/shardok_training_data";
|
||||
std::string configPath; // If empty, use default config
|
||||
AIAlgorithmType aiAlgorithm = AIAlgorithmType::ITERATIVE_DEEPENING;
|
||||
bool verbose = false;
|
||||
};
|
||||
|
||||
void PrintUsage(const char* programName) {
|
||||
std::cout << "Self-Play Generator - Generate ML Training Data\n"
|
||||
<< "\n"
|
||||
<< "Usage:\n"
|
||||
<< " " << programName << " [OPTIONS]\n"
|
||||
<< "\n"
|
||||
<< "Options:\n"
|
||||
<< " --num_games=N Number of games to generate (default: 100)\n"
|
||||
<< " --output_dir=PATH Output directory for data files (default: "
|
||||
"/tmp/shardok_training_data)\n"
|
||||
<< " --config=PATH Battle config JSON file (default: use built-in "
|
||||
"config)\n"
|
||||
<< " --ai=TYPE AI algorithm: iterative_deepening or mcts (default: "
|
||||
"iterative_deepening)\n"
|
||||
<< " --verbose Print detailed game information\n"
|
||||
<< " --help Show this help message\n"
|
||||
<< "\n"
|
||||
<< "Examples:\n"
|
||||
<< " # Generate 1000 games with default settings\n"
|
||||
<< " " << programName << " --num_games=1000\n"
|
||||
<< "\n"
|
||||
<< " # Generate games using MCTS AI\n"
|
||||
<< " " << programName << " --num_games=500 --ai=mcts\n"
|
||||
<< "\n"
|
||||
<< " # Use custom battle config\n"
|
||||
<< " " << programName << " --config=my_battle.json --num_games=100\n"
|
||||
<< "\n";
|
||||
}
|
||||
|
||||
GeneratorConfig ParseCommandLine(int argc, char* argv[]) {
|
||||
GeneratorConfig config;
|
||||
|
||||
for (int i = 1; i < argc; ++i) {
|
||||
std::string arg(argv[i]);
|
||||
|
||||
if (arg == "--help" || arg == "-h") {
|
||||
PrintUsage(argv[0]);
|
||||
std::exit(0);
|
||||
} else if (arg.starts_with("--num_games=")) {
|
||||
config.numGames = std::stoi(arg.substr(12));
|
||||
} else if (arg.starts_with("--output_dir=")) {
|
||||
config.outputDir = arg.substr(13);
|
||||
} else if (arg.starts_with("--config=")) {
|
||||
config.configPath = arg.substr(9);
|
||||
} else if (arg.starts_with("--ai=")) {
|
||||
std::string aiType = arg.substr(5);
|
||||
if (aiType == "mcts") {
|
||||
config.aiAlgorithm = AIAlgorithmType::MCTS;
|
||||
} else if (aiType == "iterative_deepening") {
|
||||
config.aiAlgorithm = AIAlgorithmType::ITERATIVE_DEEPENING;
|
||||
} else {
|
||||
std::cerr << "Error: Unknown AI type: " << aiType << "\n";
|
||||
std::exit(1);
|
||||
}
|
||||
} else if (arg == "--verbose") {
|
||||
config.verbose = true;
|
||||
} else {
|
||||
std::cerr << "Error: Unknown argument: " << arg << "\n";
|
||||
PrintUsage(argv[0]);
|
||||
std::exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
return config;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Run a complete self-play game with state recording.
|
||||
*/
|
||||
struct GameResult {
|
||||
PlayerId winner;
|
||||
int totalRounds;
|
||||
int totalCommands;
|
||||
int statesRecorded;
|
||||
};
|
||||
|
||||
GameResult RunCompleteGameWithRecording(
|
||||
const net::eagle0::shardok::ai_battle_simulator::BattleConfig& battleConfig,
|
||||
const GameSettingsSPtr& gameSettings,
|
||||
AIAlgorithmType aiAlgorithm,
|
||||
StateRecorder& recorder,
|
||||
bool verbose) {
|
||||
// Use SelfPlayBattleRunner to run the game with full state recording
|
||||
shardok::ml::SelfPlayBattleRunner runner(battleConfig, gameSettings);
|
||||
auto result = runner.RunBattleWithRecording(recorder, aiAlgorithm, verbose);
|
||||
|
||||
// Convert to GameResult
|
||||
GameResult gameResult;
|
||||
gameResult.winner = result.winner;
|
||||
gameResult.totalRounds = result.totalRounds;
|
||||
gameResult.totalCommands = result.totalCommands;
|
||||
gameResult.statesRecorded = result.statesRecorded;
|
||||
|
||||
return gameResult;
|
||||
}
|
||||
|
||||
void GenerateTrainingData(const GeneratorConfig& config) {
|
||||
std::cout << "Self-Play Training Data Generator\n";
|
||||
std::cout << "==================================\n";
|
||||
std::cout << "Output directory: " << config.outputDir << "\n";
|
||||
std::cout << "Number of games: " << config.numGames << "\n";
|
||||
std::cout << "AI algorithm: "
|
||||
<< (config.aiAlgorithm == AIAlgorithmType::MCTS ? "MCTS" : "Iterative Deepening")
|
||||
<< "\n\n";
|
||||
|
||||
// Create output directory
|
||||
std::filesystem::create_directories(config.outputDir);
|
||||
|
||||
// Load battle config
|
||||
// Note: We don't create a single config upfront because we want RANDOMIZED configs per game
|
||||
if (!config.configPath.empty()) {
|
||||
// If user provides a custom config, validate it exists
|
||||
auto testConfig = AiBattleConfigLoader::LoadFromJsonFile(config.configPath);
|
||||
if (!testConfig) {
|
||||
std::cerr << "Error: Failed to load config from " << config.configPath << "\n";
|
||||
std::exit(1);
|
||||
}
|
||||
std::cout << "Using custom config from: " << config.configPath << "\n";
|
||||
} else {
|
||||
std::cout << "Using randomized battle configurations for ML training\n";
|
||||
}
|
||||
|
||||
// Initialize game settings once (shared across all games)
|
||||
auto gameSettings = std::make_shared<shardok::GameSettings>();
|
||||
auto setter = gameSettings->GetSetter();
|
||||
shardok::BattalionTypeRegistrar::RegisterBattalionTypes(setter);
|
||||
|
||||
const std::string settingsPath =
|
||||
FilesystemUtils::StaticShardokFilesDirectory() + "settings.tsv";
|
||||
const std::string settingsTsv = std::string(byte_vector::FromPath(settingsPath));
|
||||
TsvParser parser;
|
||||
const auto valuesAndTypes = parser.ParseColumnEntryTsv(settingsTsv);
|
||||
setter.SetFromTypesAndValues(valuesAndTypes[1], valuesAndTypes[0]);
|
||||
|
||||
std::cout << "\nGenerating games...\n";
|
||||
|
||||
int attackerWins = 0;
|
||||
int defenderWins = 0;
|
||||
int draws = 0;
|
||||
size_t totalStatesRecorded = 0;
|
||||
|
||||
for (int gameNum = 0; gameNum < config.numGames; ++gameNum) {
|
||||
StateRecorder recorder;
|
||||
|
||||
if (config.verbose || gameNum % 10 == 0) {
|
||||
std::cout << "Game " << (gameNum + 1) << "/" << config.numGames << "...\n";
|
||||
}
|
||||
|
||||
try {
|
||||
// Create a new randomized config for each game (or load from file)
|
||||
std::unique_ptr<net::eagle0::shardok::ai_battle_simulator::BattleConfig> battleConfig;
|
||||
if (config.configPath.empty()) {
|
||||
battleConfig = AiBattleConfigLoader::CreateRandomizedBattleConfig();
|
||||
} else {
|
||||
battleConfig = AiBattleConfigLoader::LoadFromJsonFile(config.configPath);
|
||||
}
|
||||
|
||||
// Override AI algorithm if specified
|
||||
battleConfig->mutable_attacker()->set_ai_algorithm(
|
||||
config.aiAlgorithm == AIAlgorithmType::MCTS
|
||||
? net::eagle0::shardok::ai_battle_simulator::MCTS
|
||||
: net::eagle0::shardok::ai_battle_simulator::ITERATIVE_DEEPENING);
|
||||
battleConfig->mutable_defender()->set_ai_algorithm(
|
||||
config.aiAlgorithm == AIAlgorithmType::MCTS
|
||||
? net::eagle0::shardok::ai_battle_simulator::MCTS
|
||||
: net::eagle0::shardok::ai_battle_simulator::ITERATIVE_DEEPENING);
|
||||
|
||||
if (config.verbose) {
|
||||
std::cout << " Config: Month=" << battleConfig->month()
|
||||
<< ", Attacker=" << battleConfig->attacker().units_size() << " units"
|
||||
<< ", Defender=" << battleConfig->defender().units_size() << " units\n";
|
||||
}
|
||||
|
||||
auto result = RunCompleteGameWithRecording(
|
||||
*battleConfig,
|
||||
gameSettings,
|
||||
config.aiAlgorithm,
|
||||
recorder,
|
||||
config.verbose);
|
||||
|
||||
// Update statistics
|
||||
if (result.winner == ATTACKER_ID) {
|
||||
attackerWins++;
|
||||
} else if (result.winner == DEFENDER_ID) {
|
||||
defenderWins++;
|
||||
} else {
|
||||
draws++;
|
||||
}
|
||||
|
||||
totalStatesRecorded += result.statesRecorded;
|
||||
|
||||
if (config.verbose) {
|
||||
std::cout << " Winner: "
|
||||
<< (result.winner == ATTACKER_ID ? "Attacker"
|
||||
: result.winner == DEFENDER_ID ? "Defender"
|
||||
: "Draw")
|
||||
<< "\n";
|
||||
std::cout << " Rounds: " << result.totalRounds
|
||||
<< ", Commands: " << result.totalCommands
|
||||
<< ", States: " << result.statesRecorded << "\n";
|
||||
}
|
||||
|
||||
// Write data file (when we have states recorded)
|
||||
if (result.statesRecorded > 0) {
|
||||
recorder.SetGameOutcome(result.winner);
|
||||
|
||||
std::ostringstream filename;
|
||||
filename << config.outputDir << "/game_" << std::setw(6) << std::setfill('0')
|
||||
<< gameNum << ".bin";
|
||||
recorder.WriteToFile(filename.str());
|
||||
}
|
||||
|
||||
} catch (const std::exception& e) {
|
||||
std::cerr << "Error in game " << gameNum << ": " << e.what() << "\n";
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// Print summary
|
||||
std::cout << "\n";
|
||||
std::cout << "==================================\n";
|
||||
std::cout << "Training Data Generation Complete\n";
|
||||
std::cout << "==================================\n";
|
||||
std::cout << "Games generated: " << config.numGames << "\n";
|
||||
std::cout << "Total states recorded: " << totalStatesRecorded << "\n";
|
||||
if (config.numGames > 0) {
|
||||
std::cout << "Average states per game: "
|
||||
<< (totalStatesRecorded / static_cast<double>(config.numGames)) << "\n";
|
||||
}
|
||||
std::cout << "\n";
|
||||
std::cout << "Outcomes:\n";
|
||||
std::cout << " Attacker wins: " << attackerWins << " ("
|
||||
<< (100.0 * attackerWins / config.numGames) << "%)\n";
|
||||
std::cout << " Defender wins: " << defenderWins << " ("
|
||||
<< (100.0 * defenderWins / config.numGames) << "%)\n";
|
||||
std::cout << " Draws: " << draws << " (" << (100.0 * draws / config.numGames) << "%)\n";
|
||||
std::cout << "\n";
|
||||
|
||||
if (totalStatesRecorded == 0) {
|
||||
std::cout << "WARNING: No states were recorded. Check verbose output for errors.\n";
|
||||
} else {
|
||||
std::cout << "Data files written to: " << config.outputDir << "\n";
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
int main(int argc, char* argv[]) {
|
||||
// Set exec path for FilesystemUtils
|
||||
FilesystemUtils::SetExecPath(argv[0]);
|
||||
|
||||
// Set cache directory for ActionPointDistances
|
||||
shardok::FixedActionPointDistances::SetCacheDirectory(
|
||||
FilesystemUtils::CacheFilesDirectory() + "apdCache/");
|
||||
|
||||
try {
|
||||
auto config = ParseCommandLine(argc, argv);
|
||||
GenerateTrainingData(config);
|
||||
return 0;
|
||||
|
||||
} catch (const std::exception& e) {
|
||||
std::cerr << "Fatal error: " << e.what() << "\n";
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,195 @@
|
||||
//
|
||||
// AI Battle Simulator Configuration Loader Implementation
|
||||
//
|
||||
|
||||
#include "AiBattleConfig.hpp"
|
||||
|
||||
#include <google/protobuf/util/json_util.h>
|
||||
|
||||
#include <fstream>
|
||||
#include <iostream>
|
||||
#include <random>
|
||||
#include <sstream>
|
||||
|
||||
namespace shardok {
|
||||
namespace ai_battle_simulator {
|
||||
|
||||
using net::eagle0::shardok::ai_battle_simulator::AIAlgorithmType;
|
||||
using net::eagle0::shardok::ai_battle_simulator::PlayerConfig;
|
||||
using net::eagle0::shardok::ai_battle_simulator::UnitConfig;
|
||||
|
||||
std::unique_ptr<BattleConfigProto> AiBattleConfigLoader::LoadFromJsonFile(
|
||||
const std::string& filepath) {
|
||||
std::ifstream file(filepath);
|
||||
if (!file.is_open()) {
|
||||
std::cerr << "Failed to open config file: " << filepath << std::endl;
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
std::stringstream buffer;
|
||||
buffer << file.rdbuf();
|
||||
return LoadFromJsonString(buffer.str());
|
||||
}
|
||||
|
||||
std::unique_ptr<BattleConfigProto> AiBattleConfigLoader::LoadFromJsonString(
|
||||
const std::string& jsonString) {
|
||||
auto config = std::make_unique<BattleConfigProto>();
|
||||
|
||||
google::protobuf::util::JsonParseOptions options;
|
||||
options.ignore_unknown_fields = false;
|
||||
|
||||
const auto status =
|
||||
google::protobuf::util::JsonStringToMessage(jsonString, config.get(), options);
|
||||
|
||||
if (!status.ok()) {
|
||||
std::cerr << "Failed to parse JSON config: " << status.message() << std::endl;
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
return config;
|
||||
}
|
||||
|
||||
std::unique_ptr<BattleConfigProto> AiBattleConfigLoader::CreateDefaultPerfConfig(int month) {
|
||||
auto config = std::make_unique<BattleConfigProto>();
|
||||
|
||||
// Basic setup matching Unity's "Perf" configuration
|
||||
config->set_map_name("Alah");
|
||||
config->set_month(month);
|
||||
config->set_max_rounds(40);
|
||||
config->set_random_seed(0); // Use system random
|
||||
|
||||
// Attacker configuration - 6 Longbowmen units with professions 1-6
|
||||
PlayerConfig* attacker = config->mutable_attacker();
|
||||
attacker->set_ai_algorithm(AIAlgorithmType::ITERATIVE_DEEPENING);
|
||||
|
||||
for (int profession = 1; profession <= 6; ++profession) {
|
||||
UnitConfig* unit = attacker->add_units();
|
||||
unit->set_profession(profession);
|
||||
unit->set_battalion_type_id(4); // Longbowmen (battalion type 4)
|
||||
unit->set_starting_position_index(0); // Attackers use position 0
|
||||
unit->mutable_battalion()->set_size(1000); // Match client battalion size
|
||||
}
|
||||
|
||||
// Defender configuration - 6 Light Infantry units with NO_PROFESSION (profession 14)
|
||||
PlayerConfig* defender = config->mutable_defender();
|
||||
defender->set_ai_algorithm(AIAlgorithmType::ITERATIVE_DEEPENING);
|
||||
|
||||
for (int i = 0; i < 6; ++i) {
|
||||
UnitConfig* unit = defender->add_units();
|
||||
unit->set_profession(14); // NO_PROFESSION to match client battles
|
||||
unit->set_battalion_type_id(0); // Light Infantry (battalion type 0)
|
||||
unit->set_starting_position_index(-1); // Defenders use position -1
|
||||
unit->mutable_battalion()->set_size(1000); // Match client battalion size
|
||||
}
|
||||
|
||||
return config;
|
||||
}
|
||||
|
||||
std::unique_ptr<BattleConfigProto> AiBattleConfigLoader::CreateRandomizedBattleConfig() {
|
||||
// Thread-local random number generator for thread safety
|
||||
static thread_local std::random_device rd;
|
||||
static thread_local std::mt19937 gen(rd());
|
||||
|
||||
auto config = std::make_unique<BattleConfigProto>();
|
||||
|
||||
// Fixed map (Alah) for consistent training
|
||||
config->set_map_name("Alah");
|
||||
|
||||
// Randomize month (1-12)
|
||||
std::uniform_int_distribution<> monthDist(1, 12);
|
||||
config->set_month(monthDist(gen));
|
||||
|
||||
// Max rounds and random seed
|
||||
config->set_max_rounds(40);
|
||||
config->set_random_seed(0); // Use system random
|
||||
|
||||
// Randomize number of units per side (1-10)
|
||||
std::uniform_int_distribution<> unitCountDist(1, 10);
|
||||
const int attackerUnitCount = unitCountDist(gen);
|
||||
const int defenderUnitCount = unitCountDist(gen);
|
||||
|
||||
// Battalion types: 0-4 (excluding Undead which is type 5)
|
||||
// 0=Light Infantry, 1=Heavy Infantry, 2=Light Cavalry, 3=Heavy Cavalry, 4=Longbowmen
|
||||
std::uniform_int_distribution<> battalionTypeDist(0, 4);
|
||||
|
||||
// Troop counts: 500-2000
|
||||
std::uniform_int_distribution<> troopCountDist(500, 2000);
|
||||
|
||||
// Profession distribution: 70% hero (1-6), 30% no profession (14)
|
||||
std::uniform_int_distribution<> heroProfessionDist(1, 6);
|
||||
std::uniform_real_distribution<> hasHeroDist(0.0, 1.0);
|
||||
|
||||
// Attacker starting position: 80% all same, 20% varied
|
||||
const bool varyAttackerPositions = (hasHeroDist(gen) < 0.2);
|
||||
std::uniform_int_distribution<> positionDist(0, 5);
|
||||
const int commonAttackerPosition = positionDist(gen);
|
||||
|
||||
// Configure attacker
|
||||
PlayerConfig* attacker = config->mutable_attacker();
|
||||
attacker->set_ai_algorithm(AIAlgorithmType::ITERATIVE_DEEPENING);
|
||||
|
||||
for (int i = 0; i < attackerUnitCount; ++i) {
|
||||
UnitConfig* unit = attacker->add_units();
|
||||
|
||||
// Randomize profession (70% hero, 30% no profession)
|
||||
if (hasHeroDist(gen) < 0.7) {
|
||||
unit->set_profession(heroProfessionDist(gen));
|
||||
} else {
|
||||
unit->set_profession(14); // NO_PROFESSION
|
||||
}
|
||||
|
||||
unit->set_battalion_type_id(battalionTypeDist(gen));
|
||||
unit->mutable_battalion()->set_size(troopCountDist(gen));
|
||||
|
||||
// Starting position: usually all same, occasionally varied
|
||||
if (varyAttackerPositions) {
|
||||
unit->set_starting_position_index(positionDist(gen));
|
||||
} else {
|
||||
unit->set_starting_position_index(commonAttackerPosition);
|
||||
}
|
||||
}
|
||||
|
||||
// Configure defender
|
||||
PlayerConfig* defender = config->mutable_defender();
|
||||
defender->set_ai_algorithm(AIAlgorithmType::ITERATIVE_DEEPENING);
|
||||
|
||||
for (int i = 0; i < defenderUnitCount; ++i) {
|
||||
UnitConfig* unit = defender->add_units();
|
||||
|
||||
// Randomize profession (70% hero, 30% no profession)
|
||||
if (hasHeroDist(gen) < 0.7) {
|
||||
unit->set_profession(heroProfessionDist(gen));
|
||||
} else {
|
||||
unit->set_profession(14); // NO_PROFESSION
|
||||
}
|
||||
|
||||
unit->set_battalion_type_id(battalionTypeDist(gen));
|
||||
unit->mutable_battalion()->set_size(troopCountDist(gen));
|
||||
|
||||
// Defenders always use position -1
|
||||
unit->set_starting_position_index(-1);
|
||||
}
|
||||
|
||||
return config;
|
||||
}
|
||||
|
||||
std::string AiBattleConfigLoader::ToJsonString(const BattleConfigProto& config) {
|
||||
std::string jsonString;
|
||||
|
||||
google::protobuf::util::JsonPrintOptions options;
|
||||
options.add_whitespace = true;
|
||||
options.always_print_fields_with_no_presence = true;
|
||||
options.preserve_proto_field_names = true;
|
||||
|
||||
const auto status = google::protobuf::util::MessageToJsonString(config, &jsonString, options);
|
||||
|
||||
if (!status.ok()) {
|
||||
std::cerr << "Failed to convert config to JSON: " << status.message() << std::endl;
|
||||
return "";
|
||||
}
|
||||
|
||||
return jsonString;
|
||||
}
|
||||
|
||||
} // namespace ai_battle_simulator
|
||||
} // namespace shardok
|
||||
@@ -0,0 +1,57 @@
|
||||
//
|
||||
// AI Battle Simulator Configuration Loader
|
||||
//
|
||||
|
||||
#ifndef EAGLE0_AI_BATTLE_CONFIG_HPP
|
||||
#define EAGLE0_AI_BATTLE_CONFIG_HPP
|
||||
|
||||
#include <memory>
|
||||
#include <string>
|
||||
|
||||
#pragma clang diagnostic push
|
||||
#pragma clang diagnostic ignored "-Wdeprecated-redundant-constexpr-static-def"
|
||||
#include "src/main/protobuf/net/eagle0/shardok/ai_battle_simulator/ai_battle_config.pb.h"
|
||||
#pragma clang diagnostic pop
|
||||
|
||||
namespace shardok {
|
||||
namespace ai_battle_simulator {
|
||||
|
||||
using BattleConfigProto = net::eagle0::shardok::ai_battle_simulator::BattleConfig;
|
||||
|
||||
class AiBattleConfigLoader {
|
||||
public:
|
||||
// Load battle configuration from a JSON file
|
||||
// Returns nullptr if loading fails
|
||||
[[nodiscard]] static std::unique_ptr<BattleConfigProto> LoadFromJsonFile(
|
||||
const std::string& filepath);
|
||||
|
||||
// Load battle configuration from a JSON string
|
||||
// Returns nullptr if parsing fails
|
||||
[[nodiscard]] static std::unique_ptr<BattleConfigProto> LoadFromJsonString(
|
||||
const std::string& jsonString);
|
||||
|
||||
// Generate a default "Perf" configuration matching Unity's Custom Battle Perf setup
|
||||
// - 6 units per side (Heavy Infantry, professions 1-6)
|
||||
// - Map: Alah
|
||||
// - Month: configurable (default 4 = April)
|
||||
// - Both players use Iterative Deepening AI
|
||||
[[nodiscard]] static std::unique_ptr<BattleConfigProto> CreateDefaultPerfConfig(int month = 4);
|
||||
|
||||
// Generate a randomized battle configuration for ML training
|
||||
// - Map: Always "Alah" (for consistent training)
|
||||
// - Month: Randomized (1-12)
|
||||
// - Units per side: 1-10 (randomized independently)
|
||||
// - Battalion types: 0-4 (Light/Heavy Infantry, Light/Heavy Cavalry, Longbowmen)
|
||||
// - Troop counts: 500-2000 per unit
|
||||
// - Hero professions: Mix of hero professions (1-6) and NO_PROFESSION (14)
|
||||
// - Starting positions: Attackers usually same index, occasionally varied
|
||||
[[nodiscard]] static std::unique_ptr<BattleConfigProto> CreateRandomizedBattleConfig();
|
||||
|
||||
// Convert battle configuration to JSON string for saving/debugging
|
||||
[[nodiscard]] static std::string ToJsonString(const BattleConfigProto& config);
|
||||
};
|
||||
|
||||
} // namespace ai_battle_simulator
|
||||
} // namespace shardok
|
||||
|
||||
#endif // EAGLE0_AI_BATTLE_CONFIG_HPP
|
||||
@@ -0,0 +1,565 @@
|
||||
//
|
||||
// AI Battle Simulator Implementation
|
||||
//
|
||||
|
||||
#include "AiBattleSimulator.hpp"
|
||||
|
||||
#include <iostream>
|
||||
|
||||
#include "src/main/cpp/net/eagle0/common/FilesystemUtils.hpp"
|
||||
#include "src/main/cpp/net/eagle0/common/TsvParser.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/library/fb_helpers/GameStateHelpers.hpp"
|
||||
#include "src/main/cpp/net/eagle0/shardok/library/settings/GameSettings.hpp"
|
||||
#include "src/main/cpp/net/eagle0/shardok/util/BattalionTypeRegistrar.hpp"
|
||||
#include "src/main/cpp/net/eagle0/shardok/util/MapLoader.hpp"
|
||||
#include "src/main/flatbuffer/net/eagle0/shardok/storage/unit.hpp"
|
||||
|
||||
#pragma clang diagnostic push
|
||||
#pragma clang diagnostic ignored "-Wdeprecated-redundant-constexpr-static-def"
|
||||
#include "src/main/protobuf/net/eagle0/shardok/common/player_info.pb.h"
|
||||
#pragma clang diagnostic pop
|
||||
|
||||
namespace shardok {
|
||||
namespace ai_battle_simulator {
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr PlayerId ATTACKER_ID = 0;
|
||||
constexpr PlayerId DEFENDER_ID = 1;
|
||||
|
||||
// Default values for unit configuration - matching client-initiated battles
|
||||
constexpr double DEFAULT_BATTALION_ARMAMENT = 102.0;
|
||||
constexpr double DEFAULT_BATTALION_TRAINING = 102.0;
|
||||
constexpr double DEFAULT_BATTALION_MORALE = 76.0;
|
||||
|
||||
constexpr int DEFAULT_HERO_STRENGTH = 102;
|
||||
constexpr int DEFAULT_HERO_AGILITY = 102;
|
||||
constexpr int DEFAULT_HERO_WISDOM = 102;
|
||||
constexpr int DEFAULT_HERO_CHARISMA = 102;
|
||||
constexpr int DEFAULT_HERO_CONSTITUTION = 102;
|
||||
constexpr int DEFAULT_HERO_BRAVERY = 102;
|
||||
constexpr int DEFAULT_HERO_INTEGRITY = 0;
|
||||
constexpr int DEFAULT_HERO_AMBITION = 0;
|
||||
constexpr int DEFAULT_HERO_VIGOR = 102;
|
||||
|
||||
// Convert proto AI algorithm type to ShardokAI enum
|
||||
AIAlgorithmType ConvertAIAlgorithmType(
|
||||
net::eagle0::shardok::ai_battle_simulator::AIAlgorithmType protoType) {
|
||||
switch (protoType) {
|
||||
case net::eagle0::shardok::ai_battle_simulator::MCTS: return AIAlgorithmType::MCTS;
|
||||
case net::eagle0::shardok::ai_battle_simulator::ITERATIVE_DEEPENING:
|
||||
default: return AIAlgorithmType::ITERATIVE_DEEPENING;
|
||||
}
|
||||
}
|
||||
|
||||
// Get value with default if not set (proto3 uses 0 as default, we check for that)
|
||||
template<typename T>
|
||||
T GetOrDefault(T value, T defaultValue) {
|
||||
return (value == 0) ? defaultValue : value;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
AiBattleSimulator::AiBattleSimulator(const BattleConfigProto& config, GameSettingsSPtr gameSettings)
|
||||
: config_(config),
|
||||
gameSettings_(std::move(gameSettings)) {
|
||||
if (!gameSettings_) {
|
||||
// Initialize default game settings
|
||||
gameSettings_ = std::make_shared<GameSettings>();
|
||||
auto setter = gameSettings_->GetSetter();
|
||||
|
||||
// Load battalion types
|
||||
BattalionTypeRegistrar::RegisterBattalionTypes(setter);
|
||||
|
||||
// Load settings from file
|
||||
const std::string settingsPath =
|
||||
FilesystemUtils::StaticShardokFilesDirectory() + "settings.tsv";
|
||||
const std::string settingsTsv = std::string(byte_vector::FromPath(settingsPath));
|
||||
|
||||
TsvParser parser;
|
||||
const auto valuesAndTypes = parser.ParseColumnEntryTsv(settingsTsv);
|
||||
setter.SetFromTypesAndValues(valuesAndTypes[1], valuesAndTypes[0]);
|
||||
}
|
||||
}
|
||||
|
||||
BattleResult AiBattleSimulator::RunBattle() {
|
||||
std::cout << "Starting AI vs AI battle simulation...\n";
|
||||
std::cout << " Map: " << config_.map_name() << "\n";
|
||||
std::cout << " Month: " << config_.month() << "\n";
|
||||
std::cout << " Max rounds: " << config_.max_rounds() << "\n";
|
||||
std::cout << " Attacker AI: "
|
||||
<< net::eagle0::shardok::ai_battle_simulator::AIAlgorithmType_Name(
|
||||
config_.attacker().ai_algorithm())
|
||||
<< "\n";
|
||||
std::cout << " Defender AI: "
|
||||
<< net::eagle0::shardok::ai_battle_simulator::AIAlgorithmType_Name(
|
||||
config_.defender().ai_algorithm())
|
||||
<< "\n";
|
||||
|
||||
// Create initial game state
|
||||
auto gameState = CreateInitialGameState();
|
||||
|
||||
// Get hex map from game state
|
||||
const auto* hexMap = gameState->hex_map();
|
||||
|
||||
// Create AI clients for both players
|
||||
auto attackerAI = CreateAIClient(ATTACKER_ID, hexMap);
|
||||
auto defenderAI = CreateAIClient(DEFENDER_ID, hexMap);
|
||||
|
||||
// Create the engine once for the entire simulation
|
||||
ShardokEngine engine(gameSettings_, gameState);
|
||||
|
||||
// Run setup phase
|
||||
std::cout << "\nStarting setup phase...\n";
|
||||
auto setupResult = RunSetupPhase(engine, *attackerAI, *defenderAI);
|
||||
if (setupResult.endReason != BattleResult::EndReason::DRAW) {
|
||||
// Game ended during setup (shouldn't happen normally)
|
||||
return setupResult;
|
||||
}
|
||||
|
||||
// Run battle phase
|
||||
std::cout << "\nStarting battle phase...\n";
|
||||
return RunBattlePhase(engine, *attackerAI, *defenderAI);
|
||||
}
|
||||
|
||||
GameStateW AiBattleSimulator::CreateInitialGameState() const {
|
||||
// Load map
|
||||
auto hexMapProto = LoadMap(config_.map_name());
|
||||
|
||||
// Create player info protos
|
||||
std::vector<net::eagle0::shardok::common::PlayerInfo> playerInfoProtos;
|
||||
|
||||
// Attacker info
|
||||
net::eagle0::shardok::common::PlayerInfo attackerInfo;
|
||||
attackerInfo.set_player_id(ATTACKER_ID);
|
||||
attackerInfo.set_is_defender(false);
|
||||
attackerInfo.set_starting_food(1000);
|
||||
attackerInfo.add_victory_conditions(
|
||||
net::eagle0::shardok::common::VICTORY_CONDITION_LAST_PLAYER_STANDING);
|
||||
attackerInfo.add_victory_conditions(
|
||||
net::eagle0::shardok::common::VICTORY_CONDITION_HOLDS_CRITICAL_TILES);
|
||||
playerInfoProtos.push_back(attackerInfo);
|
||||
|
||||
// Defender info
|
||||
net::eagle0::shardok::common::PlayerInfo defenderInfo;
|
||||
defenderInfo.set_player_id(DEFENDER_ID);
|
||||
defenderInfo.set_is_defender(true);
|
||||
defenderInfo.set_starting_food(1000);
|
||||
defenderInfo.add_victory_conditions(
|
||||
net::eagle0::shardok::common::VICTORY_CONDITION_LAST_PLAYER_STANDING);
|
||||
defenderInfo.add_victory_conditions(
|
||||
net::eagle0::shardok::common::VICTORY_CONDITION_WIN_AFTER_MAX_ROUNDS);
|
||||
playerInfoProtos.push_back(defenderInfo);
|
||||
|
||||
// Create units from config
|
||||
std::vector<net::eagle0::shardok::storage::fb::Unit> units;
|
||||
|
||||
// Attacker units
|
||||
for (int i = 0; i < config_.attacker().units_size(); ++i) {
|
||||
const auto& unitConfig = config_.attacker().units(i);
|
||||
|
||||
net::eagle0::shardok::storage::fb::Unit unit{};
|
||||
unit.mutate_player_id(ATTACKER_ID);
|
||||
unit.mutate_unit_id(i);
|
||||
unit.mutate_eagle_player_id(ATTACKER_ID);
|
||||
unit.mutable_location() = net::eagle0::shardok::storage::fb::Coords(-1, -1);
|
||||
unit.mutate_status(net::eagle0::shardok::storage::fb::UnitStatus_NORMAL_UNIT);
|
||||
unit.mutate_remaining_action_points(12);
|
||||
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(unitConfig.starting_position_index());
|
||||
unit.mutate_has_moved_in_zoc(false);
|
||||
unit.mutate_volleys_remaining(0);
|
||||
unit.mutate_food_remaining(1000.0f);
|
||||
|
||||
// Battalion
|
||||
net::eagle0::shardok::storage::fb::Battalion battalion;
|
||||
const auto battalionTypeId =
|
||||
static_cast<net::eagle0::shardok::storage::fb::BattalionTypeId>(
|
||||
unitConfig.battalion_type_id());
|
||||
battalion.mutate_type(battalionTypeId);
|
||||
|
||||
// Get battalion type to determine default capacity
|
||||
const auto& battalionType = gameSettings_->GetGetter().GetBattalionType(battalionTypeId);
|
||||
const double defaultSize = battalionType->capacity;
|
||||
|
||||
battalion.mutate_size(GetOrDefault(unitConfig.battalion().size(), defaultSize));
|
||||
battalion.mutate_armament(
|
||||
GetOrDefault(unitConfig.battalion().armament(), DEFAULT_BATTALION_ARMAMENT));
|
||||
battalion.mutate_training(
|
||||
GetOrDefault(unitConfig.battalion().training(), DEFAULT_BATTALION_TRAINING));
|
||||
battalion.mutate_morale(
|
||||
GetOrDefault(unitConfig.battalion().morale(), DEFAULT_BATTALION_MORALE));
|
||||
unit.mutable_battalion() = battalion;
|
||||
|
||||
// Hero
|
||||
if (unitConfig.profession() > 0) {
|
||||
unit.mutate_has_attached_hero(true);
|
||||
|
||||
net::eagle0::shardok::storage::fb::Hero hero;
|
||||
hero.mutate_strength(GetOrDefault(unitConfig.hero().strength(), DEFAULT_HERO_STRENGTH));
|
||||
hero.mutate_strength_xp(0);
|
||||
hero.mutate_agility(GetOrDefault(unitConfig.hero().agility(), DEFAULT_HERO_AGILITY));
|
||||
hero.mutate_agility_xp(0);
|
||||
hero.mutate_wisdom(GetOrDefault(unitConfig.hero().wisdom(), DEFAULT_HERO_WISDOM));
|
||||
hero.mutate_wisdom_xp(0);
|
||||
hero.mutate_charisma(GetOrDefault(unitConfig.hero().charisma(), DEFAULT_HERO_CHARISMA));
|
||||
hero.mutate_charisma_xp(0);
|
||||
hero.mutate_constitution(
|
||||
GetOrDefault(unitConfig.hero().constitution(), DEFAULT_HERO_CONSTITUTION));
|
||||
hero.mutate_constitution_xp(0);
|
||||
const int vigor = GetOrDefault(unitConfig.hero().vigor(), DEFAULT_HERO_VIGOR);
|
||||
hero.mutate_vigor(vigor);
|
||||
hero.mutate_starting_vigor(vigor);
|
||||
hero.mutate_spent_vigor(0);
|
||||
hero.mutate_bravery(GetOrDefault(unitConfig.hero().bravery(), DEFAULT_HERO_BRAVERY));
|
||||
hero.mutate_integrity(
|
||||
GetOrDefault(unitConfig.hero().integrity(), DEFAULT_HERO_INTEGRITY));
|
||||
hero.mutate_ambition(GetOrDefault(unitConfig.hero().ambition(), DEFAULT_HERO_AMBITION));
|
||||
hero.mutate_eagle_hero_id(i + 1);
|
||||
hero.mutate_is_vip(false);
|
||||
|
||||
hero.mutable_profession_info().mutate_profession(
|
||||
static_cast<net::eagle0::shardok::storage::fb::Profession>(
|
||||
unitConfig.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;
|
||||
} else {
|
||||
unit.mutate_has_attached_hero(false);
|
||||
}
|
||||
|
||||
// Initialize opponent knowledge
|
||||
unit.mutable_opponent_knowledge()->Mutate(0, 0);
|
||||
unit.mutable_opponent_knowledge()->Mutate(1, 0);
|
||||
|
||||
units.push_back(unit);
|
||||
}
|
||||
|
||||
// Defender units
|
||||
int defenderUnitIdStart = config_.attacker().units_size();
|
||||
for (int i = 0; i < config_.defender().units_size(); ++i) {
|
||||
const auto& unitConfig = config_.defender().units(i);
|
||||
|
||||
net::eagle0::shardok::storage::fb::Unit unit{};
|
||||
unit.mutate_player_id(DEFENDER_ID);
|
||||
unit.mutate_unit_id(defenderUnitIdStart + i);
|
||||
unit.mutate_eagle_player_id(DEFENDER_ID);
|
||||
unit.mutable_location() = net::eagle0::shardok::storage::fb::Coords(-1, -1);
|
||||
unit.mutate_status(net::eagle0::shardok::storage::fb::UnitStatus_NORMAL_UNIT);
|
||||
unit.mutate_remaining_action_points(0); // Player 1 starts with 0 AP (not their turn yet)
|
||||
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(unitConfig.starting_position_index());
|
||||
unit.mutate_has_moved_in_zoc(false);
|
||||
unit.mutate_volleys_remaining(0);
|
||||
unit.mutate_food_remaining(1000.0f);
|
||||
|
||||
// Battalion
|
||||
net::eagle0::shardok::storage::fb::Battalion battalion;
|
||||
const auto battalionTypeId =
|
||||
static_cast<net::eagle0::shardok::storage::fb::BattalionTypeId>(
|
||||
unitConfig.battalion_type_id());
|
||||
battalion.mutate_type(battalionTypeId);
|
||||
|
||||
// Get battalion type to determine default capacity
|
||||
const auto& battalionType = gameSettings_->GetGetter().GetBattalionType(battalionTypeId);
|
||||
const double defaultSize = battalionType->capacity;
|
||||
|
||||
battalion.mutate_size(GetOrDefault(unitConfig.battalion().size(), defaultSize));
|
||||
battalion.mutate_armament(
|
||||
GetOrDefault(unitConfig.battalion().armament(), DEFAULT_BATTALION_ARMAMENT));
|
||||
battalion.mutate_training(
|
||||
GetOrDefault(unitConfig.battalion().training(), DEFAULT_BATTALION_TRAINING));
|
||||
battalion.mutate_morale(
|
||||
GetOrDefault(unitConfig.battalion().morale(), DEFAULT_BATTALION_MORALE));
|
||||
unit.mutable_battalion() = battalion;
|
||||
|
||||
// Hero
|
||||
if (unitConfig.profession() > 0) {
|
||||
unit.mutate_has_attached_hero(true);
|
||||
|
||||
net::eagle0::shardok::storage::fb::Hero hero;
|
||||
hero.mutate_strength(GetOrDefault(unitConfig.hero().strength(), DEFAULT_HERO_STRENGTH));
|
||||
hero.mutate_strength_xp(0);
|
||||
hero.mutate_agility(GetOrDefault(unitConfig.hero().agility(), DEFAULT_HERO_AGILITY));
|
||||
hero.mutate_agility_xp(0);
|
||||
hero.mutate_wisdom(GetOrDefault(unitConfig.hero().wisdom(), DEFAULT_HERO_WISDOM));
|
||||
hero.mutate_wisdom_xp(0);
|
||||
hero.mutate_charisma(GetOrDefault(unitConfig.hero().charisma(), DEFAULT_HERO_CHARISMA));
|
||||
hero.mutate_charisma_xp(0);
|
||||
hero.mutate_constitution(
|
||||
GetOrDefault(unitConfig.hero().constitution(), DEFAULT_HERO_CONSTITUTION));
|
||||
hero.mutate_constitution_xp(0);
|
||||
const int vigor = GetOrDefault(unitConfig.hero().vigor(), DEFAULT_HERO_VIGOR);
|
||||
hero.mutate_vigor(vigor);
|
||||
hero.mutate_starting_vigor(vigor);
|
||||
hero.mutate_spent_vigor(0);
|
||||
hero.mutate_bravery(GetOrDefault(unitConfig.hero().bravery(), DEFAULT_HERO_BRAVERY));
|
||||
hero.mutate_integrity(
|
||||
GetOrDefault(unitConfig.hero().integrity(), DEFAULT_HERO_INTEGRITY));
|
||||
hero.mutate_ambition(GetOrDefault(unitConfig.hero().ambition(), DEFAULT_HERO_AMBITION));
|
||||
hero.mutate_eagle_hero_id(defenderUnitIdStart + i + 1);
|
||||
hero.mutate_is_vip(false);
|
||||
|
||||
hero.mutable_profession_info().mutate_profession(
|
||||
static_cast<net::eagle0::shardok::storage::fb::Profession>(
|
||||
unitConfig.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;
|
||||
} else {
|
||||
unit.mutate_has_attached_hero(false);
|
||||
}
|
||||
|
||||
// Initialize opponent knowledge
|
||||
unit.mutable_opponent_knowledge()->Mutate(0, 0);
|
||||
unit.mutable_opponent_knowledge()->Mutate(1, 0);
|
||||
|
||||
units.push_back(unit);
|
||||
}
|
||||
|
||||
// Create game state
|
||||
const bool isWinter = (config_.month() >= 10 || config_.month() <= 2);
|
||||
return shardok::fb::SetupInitialGameState(
|
||||
"ai_battle_sim",
|
||||
hexMapProto,
|
||||
playerInfoProtos,
|
||||
units,
|
||||
config_.month(),
|
||||
isWinter,
|
||||
gameSettings_->GetGetter());
|
||||
}
|
||||
|
||||
std::unique_ptr<ShardokAIClient> AiBattleSimulator::CreateAIClient(
|
||||
PlayerId playerId,
|
||||
const HexMap* hexMap) const {
|
||||
const auto& playerConfig = (playerId == ATTACKER_ID) ? config_.attacker() : config_.defender();
|
||||
const bool isDefender = (playerId == DEFENDER_ID);
|
||||
|
||||
AIAlgorithmType algorithmType = ConvertAIAlgorithmType(playerConfig.ai_algorithm());
|
||||
|
||||
return std::make_unique<ShardokAIClient>(
|
||||
playerId,
|
||||
isDefender,
|
||||
hexMap,
|
||||
gameSettings_->GetGetter(),
|
||||
algorithmType);
|
||||
}
|
||||
|
||||
BattleResult AiBattleSimulator::RunSetupPhase(
|
||||
ShardokEngine& engine,
|
||||
ShardokAIClient& attackerAI,
|
||||
ShardokAIClient& defenderAI) {
|
||||
int commandsExecuted = 0;
|
||||
|
||||
while (engine.GetCurrentGameState()->status()->state() ==
|
||||
net::eagle0::shardok::storage::fb::GameStatus_::State_SET_UP) {
|
||||
auto currentState = engine.GetCurrentGameState();
|
||||
PlayerId currentPlayer = currentState->current_player();
|
||||
|
||||
auto availableCommands = engine.GetAvailableCommandProtos(currentPlayer, false);
|
||||
|
||||
if (availableCommands.empty()) {
|
||||
std::cout << "No commands available during setup for player " << (int)currentPlayer
|
||||
<< "\n";
|
||||
break;
|
||||
}
|
||||
|
||||
// Choose which AI to use
|
||||
ShardokAIClient& activeAI = (currentPlayer == ATTACKER_ID) ? attackerAI : defenderAI;
|
||||
|
||||
// Get AI decision
|
||||
auto choiceResults = activeAI.ChooseCommandIndex(engine);
|
||||
|
||||
// Apply command
|
||||
engine.PostCommand(currentPlayer, choiceResults.chosenIndex);
|
||||
commandsExecuted++;
|
||||
|
||||
// Check if game ended unexpectedly
|
||||
if (engine.GameIsOver()) {
|
||||
return CreateResultFromGameState(engine.GetCurrentGameState(), 0, commandsExecuted);
|
||||
}
|
||||
}
|
||||
|
||||
std::cout << "Setup phase complete. Commands executed: " << commandsExecuted << "\n";
|
||||
|
||||
// Return a "not finished" result
|
||||
BattleResult result;
|
||||
result.winner = -1;
|
||||
result.totalRounds = 0;
|
||||
result.totalCommands = commandsExecuted;
|
||||
result.endReason = BattleResult::EndReason::DRAW; // Temporary placeholder
|
||||
result.description = "Setup phase completed";
|
||||
return result;
|
||||
}
|
||||
|
||||
BattleResult AiBattleSimulator::RunBattlePhase(
|
||||
ShardokEngine& engine,
|
||||
ShardokAIClient& attackerAI,
|
||||
ShardokAIClient& defenderAI) {
|
||||
int totalCommands = 0;
|
||||
int currentRound = 1;
|
||||
|
||||
while (!engine.GameIsOver() && currentRound <= config_.max_rounds()) {
|
||||
auto currentState = engine.GetCurrentGameState();
|
||||
PlayerId currentPlayer = currentState->current_player();
|
||||
|
||||
auto availableCommands = engine.GetAvailableCommandProtos(currentPlayer, false);
|
||||
|
||||
if (availableCommands.empty()) {
|
||||
std::cout << "No commands available for player " << (int)currentPlayer << "\n";
|
||||
break;
|
||||
}
|
||||
|
||||
// Choose which AI to use
|
||||
ShardokAIClient& activeAI = (currentPlayer == ATTACKER_ID) ? attackerAI : defenderAI;
|
||||
|
||||
// Get AI decision
|
||||
auto choiceResults = activeAI.ChooseCommandIndex(engine);
|
||||
|
||||
// Apply command
|
||||
engine.PostCommand(currentPlayer, choiceResults.chosenIndex);
|
||||
totalCommands++;
|
||||
|
||||
// Check round progression
|
||||
auto newState = engine.GetCurrentGameState();
|
||||
if (newState->current_round() > currentRound) {
|
||||
std::cout << "Round " << currentRound
|
||||
<< " completed. Commands this round: " << totalCommands << "\n";
|
||||
currentRound = newState->current_round();
|
||||
}
|
||||
}
|
||||
|
||||
std::cout << "Battle phase complete. Total rounds: " << currentRound
|
||||
<< ", Total commands: " << totalCommands << "\n";
|
||||
|
||||
return CreateResultFromGameState(engine.GetCurrentGameState(), currentRound, totalCommands);
|
||||
}
|
||||
|
||||
bool AiBattleSimulator::IsGameOver(const GameStateW& state) const {
|
||||
const auto stateValue = state->status()->state();
|
||||
return stateValue == net::eagle0::shardok::storage::fb::GameStatus_::State_VICTORY ||
|
||||
stateValue == net::eagle0::shardok::storage::fb::GameStatus_::State_DRAW;
|
||||
}
|
||||
|
||||
BattleResult AiBattleSimulator::CreateResultFromGameState(
|
||||
const GameStateW& state,
|
||||
int totalRounds,
|
||||
int totalCommands) const {
|
||||
BattleResult result;
|
||||
result.totalRounds = totalRounds;
|
||||
result.totalCommands = totalCommands;
|
||||
|
||||
const auto* status = state->status();
|
||||
const auto stateValue = status->state();
|
||||
|
||||
// Determine winner and end reason
|
||||
if (stateValue == net::eagle0::shardok::storage::fb::GameStatus_::State_VICTORY) {
|
||||
// Extract winner from winning_shardok_ids
|
||||
const auto* winningIds = status->winning_shardok_ids();
|
||||
if (winningIds && winningIds->size() > 0) {
|
||||
result.winner = winningIds->Get(0);
|
||||
|
||||
// Determine how the game ended
|
||||
if (result.winner == ATTACKER_ID) {
|
||||
// Attacker won
|
||||
if (totalRounds >= config_.max_rounds()) {
|
||||
result.endReason = BattleResult::EndReason::CRITICAL_TILES_CONTROLLED;
|
||||
result.description = "Attacker won by controlling critical tiles";
|
||||
} else {
|
||||
result.endReason = BattleResult::EndReason::LAST_PLAYER_STANDING;
|
||||
result.description = "Attacker won by eliminating all defenders";
|
||||
}
|
||||
} else if (result.winner == DEFENDER_ID) {
|
||||
// Defender won
|
||||
if (totalRounds >= config_.max_rounds()) {
|
||||
result.endReason = BattleResult::EndReason::MAX_ROUNDS_REACHED;
|
||||
result.description = "Defender won by surviving maximum rounds";
|
||||
} else {
|
||||
result.endReason = BattleResult::EndReason::LAST_PLAYER_STANDING;
|
||||
result.description = "Defender won by eliminating all attackers";
|
||||
}
|
||||
} else {
|
||||
result.winner = -1;
|
||||
result.endReason = BattleResult::EndReason::DRAW;
|
||||
result.description = "Game ended with unknown winner";
|
||||
}
|
||||
} else {
|
||||
result.winner = -1;
|
||||
result.endReason = BattleResult::EndReason::DRAW;
|
||||
result.description = "Game ended with no winner";
|
||||
}
|
||||
} else if (stateValue == net::eagle0::shardok::storage::fb::GameStatus_::State_DRAW) {
|
||||
result.winner = -1;
|
||||
result.endReason = BattleResult::EndReason::DRAW;
|
||||
result.description = "Game ended in a draw";
|
||||
} else {
|
||||
// Game not over yet (shouldn't happen)
|
||||
result.winner = -1;
|
||||
result.endReason = BattleResult::EndReason::DRAW;
|
||||
result.description = "Game incomplete";
|
||||
}
|
||||
|
||||
// Collect surviving units (for winner, or all units if draw)
|
||||
const auto* units = state->units();
|
||||
if (units) {
|
||||
for (const auto* unit : *units) {
|
||||
if (!unit) { continue; }
|
||||
|
||||
// Only include units that are still alive
|
||||
// Based on GameStateFilter.cpp logic
|
||||
const bool isAlive =
|
||||
unit->status() == net::eagle0::shardok::storage::fb::UnitStatus_NORMAL_UNIT ||
|
||||
unit->status() == net::eagle0::shardok::storage::fb::UnitStatus_RESERVE_UNIT ||
|
||||
unit->status() ==
|
||||
net::eagle0::shardok::storage::fb::UnitStatus_NEVER_ENTERED_UNIT;
|
||||
|
||||
if (!isAlive) { continue; }
|
||||
|
||||
// For victories, only include winner's units; for draws, include all
|
||||
if (result.winner != -1 && unit->player_id() != result.winner) { continue; }
|
||||
|
||||
SurvivingUnit survivor;
|
||||
survivor.unitId = unit->unit_id();
|
||||
survivor.battalionSize = unit->battalion().size();
|
||||
survivor.profession = unit->has_attached_hero()
|
||||
? unit->attached_hero().profession_info().profession()
|
||||
: 0;
|
||||
|
||||
// Get battalion type name
|
||||
const auto& battalionType =
|
||||
gameSettings_->GetGetter().GetBattalionType(unit->battalion().type());
|
||||
survivor.battalionTypeName = battalionType->name;
|
||||
|
||||
result.survivingUnits.push_back(survivor);
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
} // namespace ai_battle_simulator
|
||||
} // namespace shardok
|
||||
@@ -0,0 +1,126 @@
|
||||
//
|
||||
// AI Battle Simulator - Runs AI vs AI battles to completion
|
||||
//
|
||||
|
||||
#ifndef EAGLE0_AI_BATTLE_SIMULATOR_HPP
|
||||
#define EAGLE0_AI_BATTLE_SIMULATOR_HPP
|
||||
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "src/main/cpp/net/eagle0/shardok/library/GameStateW.hpp"
|
||||
#include "src/main/cpp/net/eagle0/shardok/library/ShardokCTypes.h"
|
||||
#include "src/main/cpp/net/eagle0/shardok/library/settings/GameSettings.hpp"
|
||||
#include "src/main/flatbuffer/net/eagle0/shardok/storage/hex_map.hpp"
|
||||
|
||||
#pragma clang diagnostic push
|
||||
#pragma clang diagnostic ignored "-Wdeprecated-redundant-constexpr-static-def"
|
||||
#include "src/main/protobuf/net/eagle0/shardok/ai_battle_simulator/ai_battle_config.pb.h"
|
||||
#pragma clang diagnostic pop
|
||||
|
||||
namespace shardok {
|
||||
|
||||
// Forward declarations
|
||||
class ShardokAIClient;
|
||||
class ShardokEngine;
|
||||
|
||||
// Forward declaration for ML friend class
|
||||
namespace ml {
|
||||
class SelfPlayBattleRunner;
|
||||
}
|
||||
|
||||
// Type alias for hex map
|
||||
using HexMap = net::eagle0::shardok::storage::fb::HexMap;
|
||||
|
||||
namespace ai_battle_simulator {
|
||||
|
||||
using BattleConfigProto = net::eagle0::shardok::ai_battle_simulator::BattleConfig;
|
||||
|
||||
// Information about a surviving unit
|
||||
struct SurvivingUnit {
|
||||
int unitId;
|
||||
std::string battalionTypeName;
|
||||
double battalionSize;
|
||||
int profession; // 0 if no hero
|
||||
};
|
||||
|
||||
// Result of a complete battle simulation
|
||||
struct BattleResult {
|
||||
// Winner's player ID (-1 if draw)
|
||||
PlayerId winner;
|
||||
|
||||
// Total number of rounds played
|
||||
int totalRounds;
|
||||
|
||||
// Total number of commands executed
|
||||
int totalCommands;
|
||||
|
||||
// How the game ended
|
||||
enum class EndReason {
|
||||
LAST_PLAYER_STANDING, // One player eliminated all opponent units
|
||||
CRITICAL_TILES_CONTROLLED, // Attacker controlled critical tiles
|
||||
MAX_ROUNDS_REACHED, // Defender won by surviving max rounds
|
||||
DRAW // Game ended in a draw
|
||||
};
|
||||
EndReason endReason;
|
||||
|
||||
// Human-readable description of the result
|
||||
std::string description;
|
||||
|
||||
// Surviving units (for the winner, or all units if draw)
|
||||
std::vector<SurvivingUnit> survivingUnits;
|
||||
};
|
||||
|
||||
class AiBattleSimulator {
|
||||
public:
|
||||
/**
|
||||
* Initialize the simulator with a battle configuration.
|
||||
*
|
||||
* @param config The battle configuration
|
||||
* @param gameSettings The game settings to use (if nullptr, will initialize default)
|
||||
*/
|
||||
explicit AiBattleSimulator(
|
||||
const BattleConfigProto& config,
|
||||
GameSettingsSPtr gameSettings = nullptr);
|
||||
|
||||
/**
|
||||
* Run the battle from start to completion.
|
||||
* This includes:
|
||||
* 1. Setup phase (placing units)
|
||||
* 2. Battle phase (combat until victory/defeat/draw)
|
||||
*
|
||||
* @return BattleResult containing winner and statistics
|
||||
*/
|
||||
[[nodiscard]] BattleResult RunBattle();
|
||||
|
||||
// Allow SelfPlayBattleRunner to access internals for ML training data collection
|
||||
friend class ml::SelfPlayBattleRunner;
|
||||
|
||||
private:
|
||||
// Configuration
|
||||
const BattleConfigProto config_;
|
||||
GameSettingsSPtr gameSettings_;
|
||||
|
||||
// Game state builder and helpers
|
||||
[[nodiscard]] GameStateW CreateInitialGameState() const;
|
||||
[[nodiscard]] std::unique_ptr<ShardokAIClient> CreateAIClient(
|
||||
PlayerId playerId,
|
||||
const HexMap* hexMap) const;
|
||||
|
||||
// Game loop helpers
|
||||
[[nodiscard]] BattleResult
|
||||
RunSetupPhase(ShardokEngine& engine, ShardokAIClient& attackerAI, ShardokAIClient& defenderAI);
|
||||
|
||||
[[nodiscard]] BattleResult
|
||||
RunBattlePhase(ShardokEngine& engine, ShardokAIClient& attackerAI, ShardokAIClient& defenderAI);
|
||||
|
||||
[[nodiscard]] bool IsGameOver(const GameStateW& state) const;
|
||||
[[nodiscard]] BattleResult
|
||||
CreateResultFromGameState(const GameStateW& state, int totalRounds, int totalCommands) const;
|
||||
};
|
||||
|
||||
} // namespace ai_battle_simulator
|
||||
} // namespace shardok
|
||||
|
||||
#endif // EAGLE0_AI_BATTLE_SIMULATOR_HPP
|
||||
@@ -0,0 +1,53 @@
|
||||
load("//tools:copts.bzl", "COPTS")
|
||||
|
||||
cc_library(
|
||||
name = "ai_battle_config",
|
||||
srcs = ["AiBattleConfig.cpp"],
|
||||
hdrs = ["AiBattleConfig.hpp"],
|
||||
copts = COPTS,
|
||||
visibility = ["//visibility:public"],
|
||||
deps = [
|
||||
"//src/main/protobuf/net/eagle0/shardok/ai_battle_simulator:ai_battle_config_cc_proto",
|
||||
"@com_google_protobuf//:protobuf",
|
||||
],
|
||||
)
|
||||
|
||||
cc_library(
|
||||
name = "ai_battle_simulator",
|
||||
srcs = ["AiBattleSimulator.cpp"],
|
||||
hdrs = ["AiBattleSimulator.hpp"],
|
||||
copts = COPTS,
|
||||
visibility = ["//visibility:public"],
|
||||
deps = [
|
||||
":ai_battle_config",
|
||||
"//src/main/cpp/net/eagle0/common:filesystem_utils",
|
||||
"//src/main/cpp/net/eagle0/common:tsv_parser",
|
||||
"//src/main/cpp/net/eagle0/shardok/ai:shardok_ai_client",
|
||||
"//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/fb_helpers:game_state_helpers",
|
||||
"//src/main/cpp/net/eagle0/shardok/library/settings:game_settings",
|
||||
"//src/main/cpp/net/eagle0/shardok/util:battalion_type_registrar",
|
||||
"//src/main/cpp/net/eagle0/shardok/util:map_loader",
|
||||
"//src/main/flatbuffer/net/eagle0/shardok/storage:unit_cc_fbs",
|
||||
"//src/main/protobuf/net/eagle0/shardok/ai_battle_simulator:ai_battle_config_cc_proto",
|
||||
"//src/main/protobuf/net/eagle0/shardok/common:player_info_cc_proto",
|
||||
],
|
||||
)
|
||||
|
||||
cc_binary(
|
||||
name = "ai_battle_simulator_main",
|
||||
srcs = ["ai_battle_simulator_main.cpp"],
|
||||
copts = COPTS,
|
||||
data = [
|
||||
"//src/main/resources/net/eagle0/shardok:battalion_types",
|
||||
"//src/main/resources/net/eagle0/shardok:settings",
|
||||
"//src/main/resources/net/eagle0/shardok/maps",
|
||||
],
|
||||
deps = [
|
||||
":ai_battle_config",
|
||||
":ai_battle_simulator",
|
||||
"//src/main/cpp/net/eagle0/common:filesystem_utils",
|
||||
"//src/main/cpp/net/eagle0/shardok/library/action_point_distances:fixed_action_point_distances",
|
||||
],
|
||||
)
|
||||
@@ -0,0 +1,163 @@
|
||||
//
|
||||
// AI Battle Simulator - CLI Entry Point
|
||||
//
|
||||
|
||||
#include <filesystem>
|
||||
#include <iostream>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
|
||||
#include "AiBattleConfig.hpp"
|
||||
#include "AiBattleSimulator.hpp"
|
||||
#include "src/main/cpp/net/eagle0/common/FilesystemUtils.hpp"
|
||||
#include "src/main/cpp/net/eagle0/shardok/library/action_point_distances/FixedActionPointDistances.hpp"
|
||||
|
||||
using shardok::ai_battle_simulator::AiBattleConfigLoader;
|
||||
using shardok::ai_battle_simulator::AiBattleSimulator;
|
||||
using shardok::ai_battle_simulator::BattleResult;
|
||||
|
||||
namespace {
|
||||
|
||||
void PrintUsage(const char* programName) {
|
||||
std::cout << "AI Battle Simulator - AI vs AI Battle Testing Tool\n"
|
||||
<< "\n"
|
||||
<< "Usage:\n"
|
||||
<< " " << programName << " --config=<path> Run battle from JSON config file\n"
|
||||
<< " " << programName << " --generate-config Generate sample config to stdout\n"
|
||||
<< " " << programName
|
||||
<< " --generate-config --output=<path> Generate sample config to file\n"
|
||||
<< " " << programName << " --help Show this help message\n"
|
||||
<< "\n"
|
||||
<< "Examples:\n"
|
||||
<< " # Generate sample config\n"
|
||||
<< " " << programName << " --generate-config > my_battle.json\n"
|
||||
<< "\n"
|
||||
<< " # Run battle from config\n"
|
||||
<< " " << programName << " --config=my_battle.json\n"
|
||||
<< "\n";
|
||||
}
|
||||
|
||||
void GenerateConfigFile(const std::string& outputPath) {
|
||||
auto config = AiBattleConfigLoader::CreateDefaultPerfConfig();
|
||||
std::string jsonString = AiBattleConfigLoader::ToJsonString(*config);
|
||||
|
||||
if (outputPath.empty() || outputPath == "-") {
|
||||
// Output to stdout
|
||||
std::cout << jsonString << "\n";
|
||||
} else {
|
||||
// Output to file
|
||||
std::ofstream outFile(outputPath);
|
||||
if (!outFile.is_open()) {
|
||||
std::cerr << "Error: Failed to open output file: " << outputPath << "\n";
|
||||
std::exit(1);
|
||||
}
|
||||
outFile << jsonString << "\n";
|
||||
std::cout << "Sample config written to: " << outputPath << "\n";
|
||||
}
|
||||
}
|
||||
|
||||
void RunBattleFromConfig(const std::string& configPath) {
|
||||
// Load config
|
||||
std::cout << "Loading config from: " << configPath << "\n";
|
||||
auto config = AiBattleConfigLoader::LoadFromJsonFile(configPath);
|
||||
|
||||
if (!config) {
|
||||
std::cerr << "Error: Failed to load config file\n";
|
||||
std::exit(1);
|
||||
}
|
||||
|
||||
// Create simulator
|
||||
AiBattleSimulator simulator(*config);
|
||||
|
||||
// Run battle
|
||||
auto result = simulator.RunBattle();
|
||||
|
||||
// Print results
|
||||
std::cout << "\n";
|
||||
std::cout << "=================================\n";
|
||||
std::cout << "Battle Complete\n";
|
||||
std::cout << "=================================\n";
|
||||
std::cout << "Winner: ";
|
||||
if (result.winner == 0) {
|
||||
std::cout << "Attacker (Player 0)\n";
|
||||
} else if (result.winner == 1) {
|
||||
std::cout << "Defender (Player 1)\n";
|
||||
} else {
|
||||
std::cout << "Draw\n";
|
||||
}
|
||||
std::cout << "Result: " << result.description << "\n";
|
||||
std::cout << "Total Rounds: " << result.totalRounds << "\n";
|
||||
std::cout << "Total Commands: " << result.totalCommands << "\n";
|
||||
|
||||
// Print surviving units
|
||||
if (!result.survivingUnits.empty()) {
|
||||
std::cout << "\nSurviving Units (" << result.survivingUnits.size() << "):\n";
|
||||
for (const auto& unit : result.survivingUnits) {
|
||||
std::cout << " Unit " << unit.unitId << ": " << unit.battalionTypeName << " ("
|
||||
<< static_cast<int>(unit.battalionSize) << " troops)";
|
||||
if (unit.profession > 0) { std::cout << " - Profession " << unit.profession; }
|
||||
std::cout << "\n";
|
||||
}
|
||||
}
|
||||
|
||||
std::cout << "=================================\n";
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
int main(int argc, char* argv[]) {
|
||||
// Set exec path for FilesystemUtils
|
||||
FilesystemUtils::SetExecPath(argv[0]);
|
||||
|
||||
// Set cache directory for ActionPointDistances
|
||||
shardok::FixedActionPointDistances::SetCacheDirectory(
|
||||
FilesystemUtils::CacheFilesDirectory() + "apdCache/");
|
||||
|
||||
try {
|
||||
if (argc < 2) {
|
||||
PrintUsage(argv[0]);
|
||||
return 1;
|
||||
}
|
||||
|
||||
std::string configPath;
|
||||
std::string outputPath;
|
||||
bool generateConfig = false;
|
||||
|
||||
// Parse command line arguments
|
||||
for (int i = 1; i < argc; ++i) {
|
||||
std::string arg(argv[i]);
|
||||
|
||||
if (arg == "--help" || arg == "-h") {
|
||||
PrintUsage(argv[0]);
|
||||
return 0;
|
||||
} else if (arg == "--generate-config") {
|
||||
generateConfig = true;
|
||||
} else if (arg.starts_with("--config=")) {
|
||||
configPath = arg.substr(9);
|
||||
} else if (arg.starts_with("--output=")) {
|
||||
outputPath = arg.substr(9);
|
||||
} else {
|
||||
std::cerr << "Error: Unknown argument: " << arg << "\n";
|
||||
PrintUsage(argv[0]);
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
// Execute appropriate action
|
||||
if (generateConfig) {
|
||||
GenerateConfigFile(outputPath);
|
||||
} else if (!configPath.empty()) {
|
||||
RunBattleFromConfig(configPath);
|
||||
} else {
|
||||
std::cerr << "Error: Either --generate-config or --config must be specified\n";
|
||||
PrintUsage(argv[0]);
|
||||
return 1;
|
||||
}
|
||||
|
||||
} catch (const std::exception& e) {
|
||||
std::cerr << "Fatal error: " << e.what() << "\n";
|
||||
return 1;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
{
|
||||
"map_name": "Alah",
|
||||
"month": 4,
|
||||
"max_rounds": 40,
|
||||
"random_seed": 0,
|
||||
"attacker": {
|
||||
"ai_algorithm": "ITERATIVE_DEEPENING",
|
||||
"units": [
|
||||
{
|
||||
"profession": 1,
|
||||
"battalion_type_id": 4,
|
||||
"starting_position_index": 0
|
||||
},
|
||||
{
|
||||
"profession": 2,
|
||||
"battalion_type_id": 4,
|
||||
"starting_position_index": 0
|
||||
},
|
||||
{
|
||||
"profession": 3,
|
||||
"battalion_type_id": 4,
|
||||
"starting_position_index": 0
|
||||
},
|
||||
{
|
||||
"profession": 4,
|
||||
"battalion_type_id": 4,
|
||||
"starting_position_index": 0
|
||||
},
|
||||
{
|
||||
"profession": 5,
|
||||
"battalion_type_id": 4,
|
||||
"starting_position_index": 0
|
||||
},
|
||||
{
|
||||
"profession": 6,
|
||||
"battalion_type_id": 4,
|
||||
"starting_position_index": 0
|
||||
}
|
||||
]
|
||||
},
|
||||
"defender": {
|
||||
"ai_algorithm": "ITERATIVE_DEEPENING",
|
||||
"units": [
|
||||
{
|
||||
"profession": 1,
|
||||
"battalion_type_id": 4,
|
||||
"starting_position_index": -1
|
||||
},
|
||||
{
|
||||
"profession": 2,
|
||||
"battalion_type_id": 4,
|
||||
"starting_position_index": -1
|
||||
},
|
||||
{
|
||||
"profession": 3,
|
||||
"battalion_type_id": 4,
|
||||
"starting_position_index": -1
|
||||
},
|
||||
{
|
||||
"profession": 4,
|
||||
"battalion_type_id": 4,
|
||||
"starting_position_index": -1
|
||||
},
|
||||
{
|
||||
"profession": 5,
|
||||
"battalion_type_id": 4,
|
||||
"starting_position_index": -1
|
||||
},
|
||||
{
|
||||
"profession": 6,
|
||||
"battalion_type_id": 4,
|
||||
"starting_position_index": -1
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -19,7 +19,7 @@ cc_binary(
|
||||
"//src/main/cpp/net/eagle0/shardok/ai:ai_attacker_strategy_selector",
|
||||
"//src/main/cpp/net/eagle0/shardok/ai:ai_defender_strategy_selector",
|
||||
"//src/main/cpp/net/eagle0/shardok/ai:ai_iterative_deepening",
|
||||
"//src/main/cpp/net/eagle0/shardok/ai:ai_score_calculator",
|
||||
"//src/main/cpp/net/eagle0/shardok/ai:ai_score_calculator_interface",
|
||||
"//src/main/cpp/net/eagle0/shardok/ai:ai_time_budget",
|
||||
"//src/main/cpp/net/eagle0/shardok/ai:ai_water_crossing_command_chooser",
|
||||
"//src/main/cpp/net/eagle0/shardok/ai:shardok_ai_client",
|
||||
|
||||
@@ -2,7 +2,9 @@
|
||||
|
||||
## Overview
|
||||
|
||||
This document outlines the implementation plan for an automated AI performance testing tool for Shardok. The tool will replicate the manual performance testing currently done through the Unity client's "Custom Battle" interface, providing reproducible and automated performance measurements.
|
||||
This document outlines the implementation plan for an automated AI performance testing tool for Shardok. The tool will
|
||||
replicate the manual performance testing currently done through the Unity client's "Custom Battle" interface, providing
|
||||
reproducible and automated performance measurements.
|
||||
|
||||
## Goals
|
||||
|
||||
@@ -52,7 +54,7 @@ struct PerformanceTestResults {
|
||||
|
||||
The default configuration replicates the Unity client's "Perf" button:
|
||||
|
||||
- **Map**: "Alah"
|
||||
- **Map**: "Alah"
|
||||
- **AI Player**: 6 units with professions 1-6, all battalion type 4 (Heavy Infantry)
|
||||
- **Human Player**: 6 units (no specific configuration needed since AI will control)
|
||||
- **Defender Toggle**: Configurable (affects starting positions)
|
||||
@@ -60,12 +62,14 @@ The default configuration replicates the Unity client's "Perf" button:
|
||||
### 3. Key Components
|
||||
|
||||
#### AIPerformanceRunner.cpp
|
||||
|
||||
- Main entry point with command-line argument parsing
|
||||
- Test execution loop
|
||||
- Results formatting and output
|
||||
- Integration with ShardokEngine and IterativeDeepeningAI
|
||||
|
||||
#### PerformanceTestGameStateBuilder.cpp
|
||||
|
||||
- Game state creation utilities (migrated from test code)
|
||||
- Map loading helpers
|
||||
- Unit placement logic
|
||||
@@ -91,7 +95,7 @@ cc_binary(
|
||||
"//src/main/cpp/net/eagle0/shardok/ai:ai_iterative_deepening",
|
||||
"//src/main/cpp/net/eagle0/shardok/ai:ai_attacker_strategy_selector",
|
||||
"//src/main/cpp/net/eagle0/shardok/ai:ai_defender_strategy_selector",
|
||||
"//src/main/cpp/net/eagle0/shardok/ai:ai_score_calculator",
|
||||
"//src/main/cpp/net/eagle0/shardok/ai:ai_score_calculator_interface",
|
||||
"//src/main/cpp/net/eagle0/shardok/ai:ai_time_budget",
|
||||
"//src/main/cpp/net/eagle0/shardok/ai:ai_water_crossing_command_chooser",
|
||||
"//src/main/cpp/net/eagle0/shardok/library:engine",
|
||||
@@ -169,21 +173,25 @@ Overall Results:
|
||||
### 7. Implementation Phases
|
||||
|
||||
#### Phase 1: Basic Infrastructure
|
||||
|
||||
1. Create directory structure and BUILD.bazel
|
||||
2. Implement PerformanceTestGameStateBuilder with minimal game state creation
|
||||
3. Create basic AIPerformanceRunner that can load a map and create players
|
||||
|
||||
#### Phase 2: AI Integration
|
||||
|
||||
1. Integrate IterativeDeepeningAI
|
||||
2. Implement performance metric collection
|
||||
3. Add basic output formatting
|
||||
|
||||
#### Phase 3: Full Feature Set
|
||||
|
||||
1. Add command-line argument parsing
|
||||
2. Implement multiple test configurations (Perf, Rivers, Custom)
|
||||
3. Add detailed performance metrics and analysis
|
||||
|
||||
#### Phase 4: Polish and Documentation
|
||||
|
||||
1. Create comprehensive README.md
|
||||
2. Add error handling and validation
|
||||
3. Implement baseline comparison features
|
||||
|
||||
@@ -75,7 +75,7 @@ vector<shared_ptr<ShardokAIClient>> ShardokGameController::MakeAIClients(
|
||||
pi.is_defender(),
|
||||
e->GetCurrentGameState()->hex_map(),
|
||||
e->GetGameSettings()->GetGetter(),
|
||||
AIAlgorithmType::MCTS);
|
||||
AIAlgorithmType::ITERATIVE_DEEPENING);
|
||||
|
||||
aic.push_back(newClient);
|
||||
}
|
||||
|
||||
@@ -42,6 +42,8 @@ cc_library(
|
||||
copts = COPTS,
|
||||
visibility = [
|
||||
"//src/main/cpp/net/eagle0/shardok/ai:__pkg__",
|
||||
"//src/main/cpp/net/eagle0/shardok/ai_battle_simulator:__pkg__",
|
||||
"//src/main/cpp/net/eagle0/shardok/ai_performance_runner:__pkg__",
|
||||
"//src/test/cpp/net/eagle0/shardok/library/action_point_distances:__pkg__",
|
||||
],
|
||||
deps = [
|
||||
|
||||
+4
-1
@@ -13,7 +13,10 @@ static const int ASYNC_COUNT = []() {
|
||||
const int cores = static_cast<int>(std::thread::hardware_concurrency());
|
||||
// Use cores-2 to leave room for OS and other processes, minimum 4 threads
|
||||
const int threadCount = std::max(4, cores - 4);
|
||||
printf("ActionPointDistances using %d threads (detected %d cores)\n", threadCount, cores);
|
||||
fprintf(stderr,
|
||||
"ActionPointDistances using %d threads (detected %d cores)\n",
|
||||
threadCount,
|
||||
cores);
|
||||
return threadCount;
|
||||
}();
|
||||
|
||||
|
||||
@@ -23,6 +23,7 @@ cc_library(
|
||||
copts = COPTS,
|
||||
visibility = [
|
||||
"//src/main/cpp/net/eagle0/shardok/ai:__pkg__",
|
||||
"//src/main/cpp/net/eagle0/shardok/ai_battle_simulator:__pkg__",
|
||||
"//src/main/cpp/net/eagle0/shardok/ai_performance_runner:__pkg__",
|
||||
"//src/main/cpp/net/eagle0/shardok/library:__subpackages__",
|
||||
"//src/test/cpp/net/eagle0/shardok:__subpackages__",
|
||||
|
||||
@@ -144,3 +144,18 @@ cc_library(
|
||||
"//src/main/protobuf/net/eagle0/shardok/api:unit_view_cc_proto",
|
||||
],
|
||||
)
|
||||
|
||||
cc_library(
|
||||
name = "game_state_dumper",
|
||||
srcs = ["GameStateDumper.cpp"],
|
||||
hdrs = ["GameStateDumper.hpp"],
|
||||
copts = COPTS,
|
||||
visibility = [
|
||||
"//src/main/cpp/net/eagle0/shardok:__subpackages__",
|
||||
"//src/test/cpp/net/eagle0/shardok:__subpackages__",
|
||||
],
|
||||
deps = [
|
||||
"//src/main/cpp/net/eagle0/shardok/library:game_state_w",
|
||||
"//src/main/cpp/net/eagle0/shardok/library/fb_helpers:game_state_helpers",
|
||||
],
|
||||
)
|
||||
|
||||
@@ -0,0 +1,128 @@
|
||||
//
|
||||
// GameStateDumper.cpp
|
||||
// Utility for dumping game state to file for debugging
|
||||
//
|
||||
|
||||
#include "GameStateDumper.hpp"
|
||||
|
||||
#include <fstream>
|
||||
#include <iostream>
|
||||
|
||||
namespace shardok {
|
||||
|
||||
void DumpGameStateToFile(const GameStateW& gameState, const std::string& gameId) {
|
||||
const std::string filePath = "/tmp/shardok_setup_complete_" + gameId + ".txt";
|
||||
std::ofstream out(filePath);
|
||||
if (!out.is_open()) {
|
||||
std::cerr << "Failed to open " << filePath << " for writing\n";
|
||||
return;
|
||||
}
|
||||
|
||||
out << "==================== GAME STATE DUMP ====================\n\n";
|
||||
out << "Game ID: " << gameId << "\n\n";
|
||||
|
||||
// Game status
|
||||
out << "Current Player: " << (int)gameState->current_player() << "\n";
|
||||
out << "Current Round: " << (int)gameState->current_round() << "\n";
|
||||
out << "Game State: ";
|
||||
switch (gameState->status()->state()) {
|
||||
case net::eagle0::shardok::storage::fb::GameStatus_::State_SET_UP: out << "SET_UP\n"; break;
|
||||
case net::eagle0::shardok::storage::fb::GameStatus_::State_GAME_RUNNING:
|
||||
out << "GAME_RUNNING\n";
|
||||
break;
|
||||
case net::eagle0::shardok::storage::fb::GameStatus_::State_VICTORY:
|
||||
out << "VICTORY\n";
|
||||
break;
|
||||
case net::eagle0::shardok::storage::fb::GameStatus_::State_DRAW: out << "DRAW\n"; break;
|
||||
default: out << "UNKNOWN\n";
|
||||
}
|
||||
out << "\n";
|
||||
|
||||
// Units
|
||||
out << "==================== UNITS ====================\n\n";
|
||||
const auto* units = gameState->units();
|
||||
if (units) {
|
||||
for (const auto* unit : *units) {
|
||||
if (!unit) continue;
|
||||
|
||||
out << "Unit ID: " << unit->unit_id() << "\n";
|
||||
out << " Player ID: " << (int)unit->player_id() << "\n";
|
||||
out << " Location: (" << (int)unit->location().row() << ", "
|
||||
<< (int)unit->location().column() << ")\n";
|
||||
out << " Starting Position Index: " << (int)unit->starting_position_index() << "\n";
|
||||
out << " Status: ";
|
||||
switch (unit->status()) {
|
||||
case net::eagle0::shardok::storage::fb::UnitStatus_NORMAL_UNIT:
|
||||
out << "NORMAL_UNIT\n";
|
||||
break;
|
||||
case net::eagle0::shardok::storage::fb::UnitStatus_RESERVE_UNIT:
|
||||
out << "RESERVE_UNIT\n";
|
||||
break;
|
||||
case net::eagle0::shardok::storage::fb::UnitStatus_NEVER_ENTERED_UNIT:
|
||||
out << "NEVER_ENTERED_UNIT\n";
|
||||
break;
|
||||
case net::eagle0::shardok::storage::fb::UnitStatus_FLED_UNIT:
|
||||
out << "FLED_UNIT\n";
|
||||
break;
|
||||
case net::eagle0::shardok::storage::fb::UnitStatus_CAPTURED_UNIT:
|
||||
out << "CAPTURED_UNIT\n";
|
||||
break;
|
||||
case net::eagle0::shardok::storage::fb::UnitStatus_DESTROYED_SUMMONED_UNIT:
|
||||
out << "DESTROYED_SUMMONED_UNIT\n";
|
||||
break;
|
||||
case net::eagle0::shardok::storage::fb::UnitStatus_RETREATED_UNIT:
|
||||
out << "RETREATED_UNIT\n";
|
||||
break;
|
||||
default: out << "OTHER (" << (int)unit->status() << ")\n";
|
||||
}
|
||||
out << " Remaining Action Points: " << (int)unit->remaining_action_points() << "\n";
|
||||
out << " Can Flee: " << (unit->can_flee() ? "true" : "false") << "\n";
|
||||
out << " Fortified: " << (unit->fortified() ? "true" : "false") << "\n";
|
||||
out << " Hidden: " << (unit->hidden() ? "true" : "false") << "\n";
|
||||
|
||||
// Battalion info
|
||||
const auto& battalion = unit->battalion();
|
||||
out << " Battalion Type: " << (int)battalion.type() << "\n";
|
||||
out << " Battalion Size: " << battalion.size() << "\n";
|
||||
out << " Battalion Armament: " << battalion.armament() << "\n";
|
||||
out << " Battalion Training: " << battalion.training() << "\n";
|
||||
out << " Battalion Morale: " << battalion.morale() << "\n";
|
||||
|
||||
// Hero info
|
||||
if (unit->has_attached_hero()) {
|
||||
const auto& hero = unit->attached_hero();
|
||||
out << " Has Hero: true\n";
|
||||
out << " Profession: " << (int)hero.profession_info().profession() << "\n";
|
||||
out << " Strength: " << (int)hero.strength() << "\n";
|
||||
out << " Agility: " << (int)hero.agility() << "\n";
|
||||
out << " Wisdom: " << (int)hero.wisdom() << "\n";
|
||||
out << " Charisma: " << (int)hero.charisma() << "\n";
|
||||
out << " Constitution: " << (int)hero.constitution() << "\n";
|
||||
out << " Bravery: " << (int)hero.bravery() << "\n";
|
||||
out << " Integrity: " << (int)hero.integrity() << "\n";
|
||||
out << " Ambition: " << (int)hero.ambition() << "\n";
|
||||
out << " Vigor: " << (int)hero.vigor() << "/" << (int)hero.starting_vigor()
|
||||
<< "\n";
|
||||
} else {
|
||||
out << " Has Hero: false\n";
|
||||
}
|
||||
|
||||
// Opponent knowledge
|
||||
out << " Opponent Knowledge:\n";
|
||||
const auto* opponentKnowledge = unit->opponent_knowledge();
|
||||
if (opponentKnowledge && opponentKnowledge->size() >= 2) {
|
||||
out << " Player 0: " << (int)opponentKnowledge->Get(0) << "\n";
|
||||
out << " Player 1: " << (int)opponentKnowledge->Get(1) << "\n";
|
||||
}
|
||||
|
||||
out << "\n";
|
||||
}
|
||||
}
|
||||
|
||||
out << "==================== END DUMP ====================\n";
|
||||
out.close();
|
||||
|
||||
std::cout << "Game state after setup dumped to " << filePath << "\n";
|
||||
}
|
||||
|
||||
} // namespace shardok
|
||||
@@ -0,0 +1,21 @@
|
||||
//
|
||||
// GameStateDumper.hpp
|
||||
// Utility for dumping game state to file for debugging
|
||||
//
|
||||
|
||||
#ifndef GAME_STATE_DUMPER_HPP
|
||||
#define GAME_STATE_DUMPER_HPP
|
||||
|
||||
#include <string>
|
||||
|
||||
#include "src/main/cpp/net/eagle0/shardok/library/fb_helpers/GameStateHelpers.hpp"
|
||||
|
||||
namespace shardok {
|
||||
|
||||
// Dump comprehensive game state to file for debugging
|
||||
// File will be created at /tmp/shardok_setup_complete_{gameId}.txt
|
||||
void DumpGameStateToFile(const GameStateW& gameState, const std::string& gameId);
|
||||
|
||||
} // namespace shardok
|
||||
|
||||
#endif // GAME_STATE_DUMPER_HPP
|
||||
@@ -28,7 +28,9 @@ using UnitViewProto = net::eagle0::shardok::api::UnitView;
|
||||
using Coords = net::eagle0::shardok::storage::fb::Coords;
|
||||
using Unit = net::eagle0::shardok::storage::fb::Unit;
|
||||
|
||||
constexpr int8_t kGuessedStat = 25;
|
||||
constexpr int8_t kGuessedHeroStat = 50;
|
||||
constexpr int8_t kGuessedBattalionStat = 0;
|
||||
constexpr int8_t kGuessedMorale = 50;
|
||||
|
||||
auto GuessedUnit(
|
||||
const SettingsGetter &settings,
|
||||
@@ -409,15 +411,15 @@ auto GuessedHero(const HeroViewProto &hv) -> net::eagle0::shardok::storage::fb::
|
||||
if (hv.has_stats()) {
|
||||
const auto &stats = hv.stats();
|
||||
hero.mutate_strength(stats.strength());
|
||||
hero.mutate_strength_xp(kGuessedStat);
|
||||
hero.mutate_strength_xp(kGuessedHeroStat);
|
||||
hero.mutate_agility(stats.agility());
|
||||
hero.mutate_agility_xp(kGuessedStat);
|
||||
hero.mutate_agility_xp(kGuessedHeroStat);
|
||||
hero.mutate_constitution(stats.constitution());
|
||||
hero.mutate_constitution_xp(kGuessedStat);
|
||||
hero.mutate_constitution_xp(kGuessedHeroStat);
|
||||
hero.mutate_charisma(stats.charisma());
|
||||
hero.mutate_charisma_xp(kGuessedStat);
|
||||
hero.mutate_charisma_xp(kGuessedHeroStat);
|
||||
hero.mutate_wisdom(stats.wisdom());
|
||||
hero.mutate_wisdom_xp(kGuessedStat);
|
||||
hero.mutate_wisdom_xp(kGuessedHeroStat);
|
||||
|
||||
hero.mutate_integrity(stats.integrity());
|
||||
hero.mutate_ambition(stats.ambition());
|
||||
@@ -426,27 +428,27 @@ auto GuessedHero(const HeroViewProto &hv) -> net::eagle0::shardok::storage::fb::
|
||||
|
||||
hero.mutate_vigor(stats.vigor());
|
||||
hero.mutate_starting_vigor(stats.starting_vigor());
|
||||
hero.mutate_spent_vigor(kGuessedStat);
|
||||
hero.mutate_spent_vigor(kGuessedHeroStat);
|
||||
} else {
|
||||
// Don't believe it's destroyed
|
||||
hero.mutate_strength(kGuessedStat);
|
||||
hero.mutate_strength_xp(kGuessedStat);
|
||||
hero.mutate_agility(kGuessedStat);
|
||||
hero.mutate_agility_xp(kGuessedStat);
|
||||
hero.mutate_constitution(kGuessedStat);
|
||||
hero.mutate_charisma(kGuessedStat);
|
||||
hero.mutate_charisma_xp(kGuessedStat);
|
||||
hero.mutate_wisdom(kGuessedStat);
|
||||
hero.mutate_wisdom_xp(kGuessedStat);
|
||||
hero.mutate_strength(kGuessedHeroStat);
|
||||
hero.mutate_strength_xp(kGuessedHeroStat);
|
||||
hero.mutate_agility(kGuessedHeroStat);
|
||||
hero.mutate_agility_xp(kGuessedHeroStat);
|
||||
hero.mutate_constitution(kGuessedHeroStat);
|
||||
hero.mutate_charisma(kGuessedHeroStat);
|
||||
hero.mutate_charisma_xp(kGuessedHeroStat);
|
||||
hero.mutate_wisdom(kGuessedHeroStat);
|
||||
hero.mutate_wisdom_xp(kGuessedHeroStat);
|
||||
|
||||
hero.mutate_integrity(kGuessedStat);
|
||||
hero.mutate_ambition(kGuessedStat);
|
||||
hero.mutate_gregariousness(kGuessedStat);
|
||||
hero.mutate_bravery(kGuessedStat);
|
||||
hero.mutate_integrity(kGuessedHeroStat);
|
||||
hero.mutate_ambition(kGuessedHeroStat);
|
||||
hero.mutate_gregariousness(kGuessedHeroStat);
|
||||
hero.mutate_bravery(kGuessedHeroStat);
|
||||
|
||||
hero.mutate_vigor(kGuessedStat);
|
||||
hero.mutate_starting_vigor(kGuessedStat);
|
||||
hero.mutate_spent_vigor(kGuessedStat);
|
||||
hero.mutate_vigor(kGuessedHeroStat);
|
||||
hero.mutate_starting_vigor(kGuessedHeroStat);
|
||||
hero.mutate_spent_vigor(kGuessedHeroStat);
|
||||
}
|
||||
|
||||
if (hv.has_profession_info()) {
|
||||
@@ -487,8 +489,8 @@ auto GuessedBattalion(
|
||||
// 2. Use average stats for this player if available
|
||||
// 3. Use kGuessedStat as default
|
||||
|
||||
int8_t armament = kGuessedStat;
|
||||
int8_t training = kGuessedStat;
|
||||
int8_t armament = kGuessedBattalionStat;
|
||||
int8_t training = kGuessedBattalionStat;
|
||||
|
||||
if (bv.has_armament()) {
|
||||
armament = bv.armament().value();
|
||||
@@ -505,11 +507,11 @@ auto GuessedBattalion(
|
||||
const net::eagle0::shardok::storage::fb::Battalion batt(
|
||||
static_cast<net::eagle0::shardok::storage::fb::BattalionTypeId>(bv.type()),
|
||||
bv.size(),
|
||||
bv.has_morale() ? bv.morale().value() : kGuessedStat,
|
||||
bv.has_morale() ? bv.morale().value() : kGuessedMorale,
|
||||
training,
|
||||
armament,
|
||||
-1,
|
||||
kGuessedStat);
|
||||
kGuessedMorale);
|
||||
|
||||
return batt;
|
||||
}
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
class GameStatePersister;
|
||||
|
||||
using net::eagle0::shardok::common::GameStatus;
|
||||
using net::eagle0::shardok::common::PlayerInfo;
|
||||
using PlayerInfoProto = net::eagle0::shardok::common::PlayerInfo;
|
||||
using std::string;
|
||||
using std::vector;
|
||||
using VictoryConditionProto = net::eagle0::shardok::common::VictoryCondition;
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -84,9 +84,11 @@ namespace eagle {
|
||||
|
||||
public Canvas shardokCanvas;
|
||||
|
||||
private KeyValuePair<ProvinceId, OneProvinceAvailableCommands> NextActiveProvinceKeyPair =>
|
||||
Model.AvailableCommandsByProvince.First(
|
||||
kvp => kvp.Key == Model.SuggestedProvinceId);
|
||||
private KeyValuePair<ProvinceId, OneProvinceAvailableCommands>? NextActiveProvinceKeyPair =>
|
||||
Model == null || Model.AvailableCommandsByProvince.Count == 0
|
||||
? null
|
||||
: Model.AvailableCommandsByProvince.First(
|
||||
kvp => kvp.Key == Model.SuggestedProvinceId);
|
||||
|
||||
void Start() {
|
||||
gameIdButton.GetComponentInChildren<Text>().text = "";
|
||||
@@ -272,7 +274,7 @@ namespace eagle {
|
||||
}
|
||||
|
||||
public void SelectNextActiveProvince() {
|
||||
mapController.SelectedProvinceId = NextActiveProvinceKeyPair.Key;
|
||||
mapController.SelectedProvinceId = NextActiveProvinceKeyPair?.Key;
|
||||
}
|
||||
|
||||
public void SelectButtonIndex(int buttonIndex) {
|
||||
@@ -561,7 +563,7 @@ namespace eagle {
|
||||
|
||||
default:
|
||||
// most command types will be here
|
||||
var newlySelectedProvinceId = NextActiveProvinceKeyPair.Key;
|
||||
var newlySelectedProvinceId = NextActiveProvinceKeyPair?.Key;
|
||||
mapController.SelectedProvinceId = newlySelectedProvinceId;
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -3,16 +3,16 @@
|
||||
"com.cysharp.yetanotherhttphandler": "https://github.com/Cysharp/YetAnotherHttpHandler.git?path=src/YetAnotherHttpHandler#1.5.3",
|
||||
"com.unity.2d.sprite": "1.0.0",
|
||||
"com.unity.2d.tilemap": "1.0.0",
|
||||
"com.unity.ai.navigation": "2.0.8",
|
||||
"com.unity.ai.navigation": "2.0.9",
|
||||
"com.unity.analytics": "3.8.1",
|
||||
"com.unity.collab-proxy": "2.8.2",
|
||||
"com.unity.collab-proxy": "2.9.3",
|
||||
"com.unity.ext.nunit": "2.0.5",
|
||||
"com.unity.ide.rider": "3.0.36",
|
||||
"com.unity.ide.rider": "3.0.38",
|
||||
"com.unity.ide.visualstudio": "2.0.23",
|
||||
"com.unity.multiplayer.center": "1.0.0",
|
||||
"com.unity.nuget.newtonsoft-json": "3.2.1",
|
||||
"com.unity.test-framework": "1.5.1",
|
||||
"com.unity.timeline": "1.8.7",
|
||||
"com.unity.test-framework": "1.6.0",
|
||||
"com.unity.timeline": "1.8.9",
|
||||
"com.unity.ugui": "2.0.0",
|
||||
"com.unity.modules.accessibility": "1.0.0",
|
||||
"com.unity.modules.ai": "1.0.0",
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
}
|
||||
},
|
||||
"com.unity.ai.navigation": {
|
||||
"version": "2.0.8",
|
||||
"version": "2.0.9",
|
||||
"depth": 0,
|
||||
"source": "registry",
|
||||
"dependencies": {
|
||||
@@ -42,7 +42,7 @@
|
||||
"url": "https://packages.unity.com"
|
||||
},
|
||||
"com.unity.collab-proxy": {
|
||||
"version": "2.8.2",
|
||||
"version": "2.9.3",
|
||||
"depth": 0,
|
||||
"source": "registry",
|
||||
"dependencies": {},
|
||||
@@ -55,7 +55,7 @@
|
||||
"dependencies": {}
|
||||
},
|
||||
"com.unity.ide.rider": {
|
||||
"version": "3.0.36",
|
||||
"version": "3.0.38",
|
||||
"depth": 0,
|
||||
"source": "registry",
|
||||
"dependencies": {
|
||||
@@ -88,7 +88,7 @@
|
||||
"url": "https://packages.unity.com"
|
||||
},
|
||||
"com.unity.services.analytics": {
|
||||
"version": "6.0.2",
|
||||
"version": "6.1.0",
|
||||
"depth": 1,
|
||||
"source": "registry",
|
||||
"dependencies": {
|
||||
@@ -110,7 +110,7 @@
|
||||
"url": "https://packages.unity.com"
|
||||
},
|
||||
"com.unity.test-framework": {
|
||||
"version": "1.5.1",
|
||||
"version": "1.6.0",
|
||||
"depth": 0,
|
||||
"source": "builtin",
|
||||
"dependencies": {
|
||||
@@ -120,7 +120,7 @@
|
||||
}
|
||||
},
|
||||
"com.unity.timeline": {
|
||||
"version": "1.8.7",
|
||||
"version": "1.8.9",
|
||||
"depth": 0,
|
||||
"source": "registry",
|
||||
"dependencies": {
|
||||
@@ -288,7 +288,8 @@
|
||||
"com.unity.modules.ui": "1.0.0",
|
||||
"com.unity.modules.imgui": "1.0.0",
|
||||
"com.unity.modules.jsonserialize": "1.0.0",
|
||||
"com.unity.modules.hierarchycore": "1.0.0"
|
||||
"com.unity.modules.hierarchycore": "1.0.0",
|
||||
"com.unity.modules.physics": "1.0.0"
|
||||
}
|
||||
},
|
||||
"com.unity.modules.umbra": {
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
m_EditorVersion: 6000.1.11f1
|
||||
m_EditorVersionWithRevision: 6000.1.11f1 (9b156bbbd4df)
|
||||
m_EditorVersion: 6000.2.7f2
|
||||
m_EditorVersionWithRevision: 6000.2.7f2 (2b518236b676)
|
||||
|
||||
@@ -26,6 +26,9 @@ EditorUserSettings:
|
||||
RecentlyUsedSceneGuid-6:
|
||||
value: 51070104510d505f5c565e2314750e44134e4c7a2a2a71357c714d62b0e2643c
|
||||
flags: 0
|
||||
RecentlyUsedSceneGuid-7:
|
||||
value: 015207020753505e590c597411770a4215164d282a7e7633757f496be4b0316c
|
||||
flags: 0
|
||||
RecentlyUsedScenePath-0:
|
||||
value: 224247031146467c0c0309321c22465e0319113e35
|
||||
flags: 0
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
load("@io_bazel_rules_go//proto:def.bzl", "go_proto_library")
|
||||
load("@rules_cc//cc:defs.bzl", "cc_proto_library")
|
||||
load("@rules_proto//proto:defs.bzl", "proto_library")
|
||||
|
||||
cc_proto_library(
|
||||
name = "ai_battle_config_cc_proto",
|
||||
visibility = ["//visibility:public"],
|
||||
deps = [":ai_battle_config_proto"],
|
||||
)
|
||||
|
||||
proto_library(
|
||||
name = "ai_battle_config_proto",
|
||||
srcs = ["ai_battle_config.proto"],
|
||||
visibility = ["//visibility:public"],
|
||||
)
|
||||
|
||||
go_proto_library(
|
||||
name = "ai_battle_config_go_proto",
|
||||
importpath = "github.com/nolen777/eagle0/src/main/protobuf/net/eagle0/shardok/ai_battle_simulator",
|
||||
proto = ":ai_battle_config_proto",
|
||||
visibility = ["//visibility:public"],
|
||||
)
|
||||
@@ -0,0 +1,92 @@
|
||||
syntax = "proto3";
|
||||
|
||||
package net.eagle0.shardok.ai_battle_simulator;
|
||||
|
||||
// Battalion configuration
|
||||
message BattalionConfig {
|
||||
// Battalion size (number of troops, default: 1000)
|
||||
double size = 1;
|
||||
|
||||
// Armament percentage (default: 100.0)
|
||||
double armament = 2;
|
||||
|
||||
// Training percentage (default: 100.0)
|
||||
double training = 3;
|
||||
|
||||
// Morale (default: 50.0)
|
||||
double morale = 4;
|
||||
}
|
||||
|
||||
// Hero configuration
|
||||
message HeroConfig {
|
||||
// Core attributes (default: 50 each)
|
||||
int32 strength = 1;
|
||||
int32 agility = 2;
|
||||
int32 wisdom = 3;
|
||||
int32 charisma = 4;
|
||||
int32 constitution = 5;
|
||||
|
||||
// Personality traits (default: 50 each)
|
||||
int32 bravery = 6;
|
||||
int32 integrity = 7;
|
||||
int32 ambition = 8;
|
||||
|
||||
// Vigor (default: 50)
|
||||
int32 vigor = 9;
|
||||
}
|
||||
|
||||
// Configuration for a single unit in the battle
|
||||
message UnitConfig {
|
||||
// Profession ID (1-6 in standard setup, 0 = no hero)
|
||||
int32 profession = 1;
|
||||
|
||||
// Battalion type ID (e.g., Heavy Infantry)
|
||||
int32 battalion_type_id = 2;
|
||||
|
||||
// Starting position index on the map
|
||||
int32 starting_position_index = 3;
|
||||
|
||||
// Battalion stats (optional, uses defaults if not specified)
|
||||
BattalionConfig battalion = 4;
|
||||
|
||||
// Hero stats (optional, uses defaults if not specified)
|
||||
// Only used if profession > 0
|
||||
HeroConfig hero = 5;
|
||||
}
|
||||
|
||||
// AI algorithm types
|
||||
enum AIAlgorithmType {
|
||||
AI_ALGORITHM_TYPE_UNSPECIFIED = 0;
|
||||
ITERATIVE_DEEPENING = 1;
|
||||
MCTS = 2;
|
||||
}
|
||||
|
||||
// Configuration for one player (attacker or defender)
|
||||
message PlayerConfig {
|
||||
// AI algorithm to use for this player
|
||||
AIAlgorithmType ai_algorithm = 1;
|
||||
|
||||
// Units for this player
|
||||
repeated UnitConfig units = 2;
|
||||
}
|
||||
|
||||
// Complete battle configuration
|
||||
message BattleConfig {
|
||||
// Map name (e.g., "Alah")
|
||||
string map_name = 1;
|
||||
|
||||
// Month (0-11)
|
||||
int32 month = 2;
|
||||
|
||||
// Attacker configuration
|
||||
PlayerConfig attacker = 3;
|
||||
|
||||
// Defender configuration
|
||||
PlayerConfig defender = 4;
|
||||
|
||||
// Maximum number of rounds before draw (default: 40)
|
||||
int32 max_rounds = 5;
|
||||
|
||||
// Random seed for reproducibility (0 = use system random)
|
||||
int32 random_seed = 6;
|
||||
}
|
||||
+4
-4
@@ -85,15 +85,15 @@ object AvailableHandleCapturedHeroCommandFactory extends AvailableCommandsFactor
|
||||
.provinces(provinceId)
|
||||
.capturedHeroes
|
||||
.partition(_.recruitmentAttempted) match {
|
||||
case (Vector(), Vector()) => None
|
||||
case (alreadyRecruited, notRecruited) if alreadyRecruited.isEmpty && notRecruited.isEmpty => None
|
||||
// If the top hero in either set doesn't have a message, return nothing, otherwise return just the one option
|
||||
case (Vector(), notRecruited) =>
|
||||
case (alreadyRecruited, notRecruitedNonEmpty) if alreadyRecruited.isEmpty =>
|
||||
firstOption(
|
||||
gameState = gameState,
|
||||
capturedHeroes = notRecruited.toVector,
|
||||
capturedHeroes = notRecruitedNonEmpty.toVector,
|
||||
provinceId = provinceId
|
||||
)
|
||||
case (alreadyRecruitedNonEmpty, _) =>
|
||||
case (alreadyRecruitedNonEmpty, _) =>
|
||||
firstOption(
|
||||
gameState = gameState,
|
||||
capturedHeroes = alreadyRecruitedNonEmpty.toVector,
|
||||
|
||||
@@ -151,7 +151,7 @@ object MarchCommand {
|
||||
actingFactionId = Some(actingFactionId),
|
||||
provinceId = Some(originProvinceId),
|
||||
provinceIdActed = Some(originProvinceId),
|
||||
changedHeroes = heroChanges.toVector,
|
||||
changedHeroes = heroChanges,
|
||||
changedProvinces = Vector(originProvinceChange, destinationProvinceChange)
|
||||
)
|
||||
}
|
||||
|
||||
@@ -50,6 +50,7 @@ object ReconCommand {
|
||||
actionResultType = ReconStartedResultType,
|
||||
actingFactionId = Some(factionId),
|
||||
provinceId = Some(actingProvinceId),
|
||||
provinceIdActed = Some(actingProvinceId),
|
||||
changedProvinces = Vector(
|
||||
ChangedProvinceC(
|
||||
provinceId = actingProvinceId,
|
||||
|
||||
@@ -58,7 +58,7 @@ object BattalionNameFilter {
|
||||
battalionIdsInProvincesWithMyMovingArmies ++
|
||||
battalionIdsInBattles
|
||||
).distinct
|
||||
.map(gs.battalions)
|
||||
.map(x => gs.battalions.getOrElse(x, gs.destroyedBattalions(x)))
|
||||
.map(b => b.id -> b.name)
|
||||
.toMap
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user