mirror of
https://github.com/nolen777/eagle0.git
synced 2026-07-29 00:15:42 +00:00
Compare commits
3
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
31d5702f0b | ||
|
|
9612c2154c | ||
|
|
fe92f69ca9 |
File diff suppressed because it is too large
Load Diff
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -8,6 +8,7 @@
|
||||
|
||||
#include <fstream>
|
||||
#include <iostream>
|
||||
#include <random>
|
||||
#include <sstream>
|
||||
|
||||
namespace shardok {
|
||||
@@ -84,6 +85,94 @@ std::unique_ptr<BattleConfigProto> AiBattleConfigLoader::CreateDefaultPerfConfig
|
||||
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;
|
||||
|
||||
|
||||
@@ -37,6 +37,16 @@ public:
|
||||
// - 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);
|
||||
};
|
||||
|
||||
@@ -25,6 +25,11 @@ namespace shardok {
|
||||
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;
|
||||
|
||||
@@ -89,6 +94,9 @@ public:
|
||||
*/
|
||||
[[nodiscard]] BattleResult RunBattle();
|
||||
|
||||
// Allow SelfPlayBattleRunner to access internals for ML training data collection
|
||||
friend class ml::SelfPlayBattleRunner;
|
||||
|
||||
private:
|
||||
// Configuration
|
||||
const BattleConfigProto config_;
|
||||
|
||||
Reference in New Issue
Block a user