mirror of
https://github.com/nolen777/eagle0.git
synced 2026-07-29 02:15:43 +00:00
Add separate expansion and simulation horizons for MCTS (#4526)
Implements Option C from design discussion: separate tree expansion limits from leaf evaluation limits to ensure fair score comparisons. With games having sequential same-player actions, fixed tree depth creates unfair comparisons: - "MOVE away, MOVE back" (2 actions, still my turn) → evaluated mid-turn - "END_TURN" (1 action, now opponent's turn) → evaluated after turn Not comparable - different game phases! **Two independent limits:** 1. maxPlayerFlips (tree expansion): Controls how far to build tree 2. maxSimulationFlips (leaf evaluation): Controls evaluation horizon **For Shardok (maxPlayerFlips=0, maxSimulationFlips=1):** - Build tree through all my action sequences (playerFlips=0) - When hitting a leaf: simulate until playerFlips > maxSimulationFlips - Result: All leaves evaluated "after opponent responds" 1. Added maxSimulationFlips to MCTSConfig (default 0, backward compatible) 2. Updated MCTSSimulation to use maxSimulationFlips for horizon: - Early return check: startingPlayerFlips > maxSimulationFlips - Loop condition: playerFlips <= maxSimulationFlips - Allows one action AT the horizon before stopping 3. Configured Shardok to use maxSimulationFlips=1 for fair evaluation 4. Updated TicTacToe tests with appropriate simulation horizon values ✅ TicTacToe MCTS integration tests pass ✅ Abstract MCTS AI tests pass ✅ Shardok MCTS basic tests pass (now prefers ARCHERY over END_TURN) ⏳ AI integration test has timeout (expected - deeper simulation) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -301,14 +301,22 @@ auto AbstractMCTSAI::MCTSSimulation(
|
||||
const int startingPlayerFlips) const -> double {
|
||||
if (state.isTerminal()) { return state.score(startingPlayer); }
|
||||
|
||||
// If we've already exceeded the simulation horizon, don't simulate - just return immediate
|
||||
// score This ensures fair comparison: all leaves are evaluated at the same game phase Example:
|
||||
// maxSimulationFlips=1 means simulate THROUGH opponent's first response (i.e., allow one action
|
||||
// at playerFlips=1, then stop)
|
||||
if (startingPlayerFlips > config_.maxSimulationFlips) { return state.score(startingPlayer); }
|
||||
|
||||
// Create a mutable copy for simulation
|
||||
auto currentState = state.clone();
|
||||
int depth = 0;
|
||||
int playerFlips = startingPlayerFlips; // Start from the expanded node's flip count
|
||||
MCTSPlayerId previousPlayer = currentState->currentPlayerId();
|
||||
|
||||
// Simulate until terminal or max depth
|
||||
while (!currentState->isTerminal() && depth < config_.maxSimulationDepth) {
|
||||
// Simulate until we exceed the horizon, hit terminal state, or max depth
|
||||
// Note: We allow one action AT maxSimulationFlips before stopping
|
||||
while (!currentState->isTerminal() && depth < config_.maxSimulationDepth &&
|
||||
playerFlips <= config_.maxSimulationFlips) {
|
||||
// Track player changes
|
||||
const MCTSPlayerId currentPlayer = currentState->currentPlayerId();
|
||||
if (currentPlayer != previousPlayer) {
|
||||
@@ -321,7 +329,7 @@ auto AbstractMCTSAI::MCTSSimulation(
|
||||
*currentState,
|
||||
playerId_,
|
||||
playerFlips,
|
||||
config_.maxPlayerFlips);
|
||||
config_.maxSimulationFlips);
|
||||
if (actions.empty()) { break; }
|
||||
|
||||
// Determine if current player is maximizing or minimizing
|
||||
|
||||
@@ -44,8 +44,13 @@ struct MCTSConfig {
|
||||
int numThreads = 16; // Number of threads for parallel MCTS
|
||||
MCTSSimulationPolicy simulationPolicy = MCTSSimulationPolicy::BEST_IMMEDIATE;
|
||||
MCTSBackpropagationPolicy backpropagationPolicy = MCTSBackpropagationPolicy::AVERAGING;
|
||||
int maxPlayerFlips = 0; // Maximum number of player changes to explore (0 = stop at first
|
||||
// flip, 1 = explore through opponent's response, etc.)
|
||||
int maxPlayerFlips = 0; // Maximum number of player changes for tree expansion
|
||||
// (0 = expand through current player's turn only,
|
||||
// 1 = expand through opponent's first response, etc.)
|
||||
int maxSimulationFlips = 0; // Maximum player flips for leaf evaluation
|
||||
// When evaluating a leaf at playerFlips < maxSimulationFlips,
|
||||
// simulate forward to this phase for fair comparison
|
||||
// (default 0 = evaluate leaves as-is, backward compatible)
|
||||
std::string debugDumpPath = ""; // If non-empty, dump MCTS tree to this file path
|
||||
};
|
||||
|
||||
|
||||
@@ -135,6 +135,14 @@ auto ShardokAIClient::StandardChooseCommandIndex(
|
||||
// - MINIMAX correctly models opponent choosing best response
|
||||
// - Explores through one opponent turn for tactical accuracy
|
||||
auto adjustedMCTSConfig = mctsConfig;
|
||||
|
||||
// For fair evaluation: simulate leaves to opponent's turn start (maxSimulationFlips=1)
|
||||
// This ensures all leaves are scored at the same game phase:
|
||||
// - Leaves at playerFlips=0 (still my turn): simulate through END_TURN to playerFlips=1
|
||||
// - Leaves at playerFlips=1 (opponent's turn): evaluate immediately
|
||||
// Result: consistent comparison of "what happens after I end my turn"
|
||||
adjustedMCTSConfig.maxSimulationFlips = 1;
|
||||
|
||||
// adjustedMCTSConfig.maxPlayerFlips = 0;
|
||||
// if (timeBudget.isCloseToEnemy) {
|
||||
// adjustedMCTSConfig.maxPlayerFlips = 1;
|
||||
|
||||
@@ -287,6 +287,7 @@ TEST_F(MCTSIntegrationTest, AdaptiveExplorationExploitation) {
|
||||
TEST_F(MCTSIntegrationTest, EarlyTerminationOnWin_MaxPlayerFlips0) {
|
||||
// Explicitly set maxPlayerFlips=0 for single-player MCTS
|
||||
config_.maxPlayerFlips = 0;
|
||||
config_.maxSimulationFlips = 1; // Simulate to opponent's turn for fair evaluation
|
||||
|
||||
// If MCTS finds a guaranteed win, it should recognize it quickly
|
||||
// X X . <- Position 2 wins
|
||||
@@ -313,6 +314,7 @@ TEST_F(MCTSIntegrationTest, EarlyTerminationOnWin_MaxPlayerFlips0) {
|
||||
TEST_F(MCTSIntegrationTest, EarlyTerminationOnWin_MaxPlayerFlips1) {
|
||||
// Explicitly set maxPlayerFlips=1 for two-player adversarial MCTS
|
||||
config_.maxPlayerFlips = 1;
|
||||
config_.maxSimulationFlips = 2; // Simulate to next player flip for deeper evaluation
|
||||
|
||||
// If MCTS finds a guaranteed win, it should recognize it quickly
|
||||
// X X . <- Position 2 wins
|
||||
@@ -342,6 +344,7 @@ TEST_F(MCTSIntegrationTest, EarlyTerminationOnWin_MaxPlayerFlips8) {
|
||||
// Explicitly set maxPlayerFlips=8 for full game tree exploration
|
||||
// (TicTacToe has max 9 moves = 8 player flips)
|
||||
config_.maxPlayerFlips = 8;
|
||||
config_.maxSimulationFlips = 9; // Simulate to game completion
|
||||
|
||||
// If MCTS finds a guaranteed win, it should recognize it quickly
|
||||
// X X . <- Position 2 wins
|
||||
|
||||
Reference in New Issue
Block a user