Compare commits

...
Author SHA1 Message Date
admin 1094976824 Enable adaptive MINIMAX/AVERAGING backpropagation for MCTS
This commit enables the previously commented-out adaptive MCTS strategy that
switches between MINIMAX and AVERAGING backpropagation based on proximity to
enemy units.

Key changes:
- When close to enemy (isCloseToEnemy=true):
  - Use maxPlayerFlips=1 (adversarial search through opponent's first response)
  - Use MINIMAX backpropagation (correctly models opponent choosing best response)
  - Set maxSimulationFlips=2 (simulate to opponent's second action for fair comparison)

- When far from enemy (isCloseToEnemy=false):
  - Use maxPlayerFlips=0 (single-player search through current player's turn)
  - Use AVERAGING backpropagation (naturally penalizes longer paths)
  - Set maxSimulationFlips=1 (simulate to opponent's first action for fair comparison)

The maxSimulationFlips values follow the pattern: maxSimulationFlips = maxPlayerFlips + 1
This ensures all leaf nodes are evaluated at a consistent game phase for fair comparison.

Depends on: PR #4526 (maxSimulationFlips feature)
2025-11-08 13:42:55 -08:00
adminandClaude 383f770a2d Add separate expansion and simulation horizons for MCTS
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>
2025-11-08 13:37:56 -08:00
4 changed files with 46 additions and 19 deletions
@@ -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,20 +135,31 @@ auto ShardokAIClient::StandardChooseCommandIndex(
// - MINIMAX correctly models opponent choosing best response
// - Explores through one opponent turn for tactical accuracy
auto adjustedMCTSConfig = mctsConfig;
// adjustedMCTSConfig.maxPlayerFlips = 0;
// if (timeBudget.isCloseToEnemy) {
// adjustedMCTSConfig.maxPlayerFlips = 1;
// adjustedMCTSConfig.backpropagationPolicy = mcts::MCTSBackpropagationPolicy::MINIMAX;
// if constexpr (kPerformanceLogging) {
// printf("MCTS Config: Close to enemy - using maxPlayerFlips=1, MINIMAX backprop\n");
// }
// } else {
// adjustedMCTSConfig.maxPlayerFlips = 0;
// adjustedMCTSConfig.backpropagationPolicy = mcts::MCTSBackpropagationPolicy::AVERAGING;
// if constexpr (kPerformanceLogging) {
// printf("MCTS Config: Far from enemy - using maxPlayerFlips=0, AVERAGING backprop\n");
// }
// }
// Enable adaptive MCTS strategy based on tactical situation
if (timeBudget.isCloseToEnemy) {
// Close to enemy: use adversarial search with MINIMAX backpropagation
adjustedMCTSConfig.maxPlayerFlips = 1;
adjustedMCTSConfig.backpropagationPolicy = mcts::MCTSBackpropagationPolicy::MINIMAX;
// For fair evaluation: simulate to next player flip (opponent's second action)
// This ensures leaves at playerFlips=0 and playerFlips=1 are compared fairly
adjustedMCTSConfig.maxSimulationFlips = 2;
if constexpr (kPerformanceLogging) {
printf("MCTS Config: Close to enemy - maxPlayerFlips=1, maxSimulationFlips=2, MINIMAX "
"backprop\n");
}
} else {
// Far from enemy: use single-player search with AVERAGING backpropagation
adjustedMCTSConfig.maxPlayerFlips = 0;
adjustedMCTSConfig.backpropagationPolicy = mcts::MCTSBackpropagationPolicy::AVERAGING;
// For fair evaluation: simulate to first player flip (opponent's first action)
// This ensures leaves in middle of our turn are compared at consistent phase
adjustedMCTSConfig.maxSimulationFlips = 1;
if constexpr (kPerformanceLogging) {
printf("MCTS Config: Far from enemy - maxPlayerFlips=0, maxSimulationFlips=1, "
"AVERAGING backprop\n");
}
}
assert(commandCount == realAvailableCommands->size());
// Verify that the AI's guessed state produces the same available commands as reality
@@ -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