Repository navigation
Add random opponent modeling and comparison test - #2
Conversation
- Refactor fitness() to call fitness_with_opponent_modeling() with configurable opponent behavior - When random_opponents=true, generate random moves for all opponents instead of using the default "continue straight" fallback - Add head-to-head comparison test that pits random opponent modeling against default modeling Initial results show default modeling slightly outperforms random (9 vs 7 wins), likely due to added noise in fitness evaluation and weak scoring function that only rewards survival/death without intermediate signals.
Opponent modeling: - Add OpponentModel enum with Default, Random, and Smart strategies - Smart opponents avoid walls/collisions, seek food when low health, and prefer center positions for more options - Default fitness() now uses Smart opponent modeling Improved scoring (score_game): - Add health bonus (0-50 points scaled) - Add length advantage over opponents (+/- 10 per length diff) - Add center proximity bonus (penalize being far from center) - Increase win bonus to 500 points Test results show Smart modeling beats Default 11-0 in head-to-head matchups, confirming the improvement.
Results with full game length: - Smart: 13 wins (best) - Random: 5 wins - Default: 0 wins Smart opponent modeling clearly outperforms both Random and Default when games run to conclusion.
Implementation: - Negamax struct with configurable search depth - Alpha-beta pruning for efficiency - Uses deterministic smart opponent modeling - Reuses score_game() for position evaluation Test results (depth 8, 30 games each): - Negamax vs Smart RHEA: 100% draws - Negamax vs Default RHEA: 25 RHEA wins, 5 draws RHEA currently outperforms Negamax, likely due to: 1. Negamax's conservative play under "smart opponent" assumption 2. RHEA's simpler model being more robust in practice 3. Evaluation function may not differentiate positions well enough The Negamax implementation is correct but may need: - Better evaluation heuristics - Move ordering for better pruning - Iterative deepening for time management
The previous implementation was not doing true adversarial search - it just predicted opponent moves and maximized. Now it properly: 1. For each player move, considers ALL opponent responses 2. Assumes opponent picks the move worst for us (minimax) 3. Picks the move with best worst-case outcome This is max-min over joint action space, giving branching factor 4x4=16 per depth level. Reduced depth from 8 to 4 to compensate. Results: Negamax now wins 18/60 games vs RHEA (previously 0/60). - vs Smart RHEA: 8-22 - vs Default RHEA: 10-20 RHEA still wins overall due to deeper lookahead (5 vs 4) and the computational cost of proper minimax.
- Added voronoi board control scoring to Negamax evaluate function - Fixed subtraction overflow in calculate_voronoi_scores when snake is on bottom row and direction is Down - Voronoi heuristic with weight of 3x for control difference - Results: Voronoi actually hurt Negamax performance vs Smart RHEA
Found two bugs that broke voronoi scoring after turn 0:
1. In advance_turn(), occupied was computed with inverted logic:
- Before: new_occupied = !0; new_occupied ^= snake.body_board;
(XOR from all-1s, meaning bit=0 = snake present)
- After: new_occupied = 0; new_occupied |= snake.body_board;
(OR from 0, meaning bit=1 = snake present)
This inconsistency with Game::create() meant voronoi worked on
turn 0 but was broken after advance_turn was called.
2. Added boundary checks BEFORE computing next_pos in voronoi BFS
to prevent subtraction overflow when snake is on bottom row.
Results: Negamax with voronoi now wins 100% vs Smart RHEA (was 0%)
The voronoi heuristic correctly guides territorial control strategy.
Pull Request Review: Add Random Opponent Modeling and Comparison TestSummaryThis PR introduces opponent modeling strategies to the RHEA implementation, adds a Negamax search algorithm, and includes comprehensive testing infrastructure. The changes are substantial and well-structured overall. Critical Issues🔴 CRITICAL BUG: Occupied Bitboard Logic Error (bitboard.rs:527-533)Location: The fix changes from XOR to OR operations for computing occupied spaces: // Before (WRONG - was inverting/XORing incorrectly)
let mut new_occupied: u128 = !0;
new_occupied = new_occupied ^ snake.head_board ^ snake.body_board;
// After (CORRECT)
let mut new_occupied: u128 = 0;
new_occupied = new_occupied | snake.head_board | snake.body_board;Impact: This is an excellent critical bug fix. The previous XOR logic was fundamentally broken - it would incorrectly mark spaces as occupied/unoccupied. The new OR logic correctly sets bits where snake bodies exist. Recommendation: ✅ This fix is correct and essential. Consider adding a unit test specifically for this logic to prevent regression. 🟡 Boundary Check Improvement (bitboard.rs:597-609)Location: The voronoi calculation now checks boundaries before computing // Check boundary conditions BEFORE computing next_pos to avoid overflow
let width = self.width as u128;
let row = pos / width;
let col = pos % width;
if (*dir == Direction::Left && col == 0) ||
(*dir == Direction::Right && col == width - 1) ||
(*dir == Direction::Down && row == 0) ||
(*dir == Direction::Up && row == width - 1) {
continue;
}Impact: Good defensive programming. Prevents edge case bugs in voronoi calculations. Recommendation: ✅ Approved. This is a solid improvement. Code Quality Issues🟡 Smart Move Implementation (rhea.rs:259-338)Concerns:
🟢 Score Function Improvements (rhea.rs:362-397)Positive Changes:
Concern: The scoring weights seem arbitrary. Consider:
Recommendation: Document scoring rationale or run parameter tuning tests. 🟡 Negamax Implementation (rhea.rs:426-717)Architecture:
Concerns:
Testing Issues🔴 Test Determinism (rhea.rs:785-809)Location: Test uses let mut rng = SmallRng::from_entropy();
let positions: [(Vec<u128>, Vec<u128>); 4] = [...];
let (p1_pos, p2_pos) = positions[rng.gen_range(0..positions.len())].clone();Problem: Non-deterministic tests can:
Recommendation: Use seeded RNG for reproducible tests: let mut rng = SmallRng::seed_from_u64(42); // Or parameterize seed🟡 Test CoverageMissing Tests:
Recommendation: Add focused unit tests before relying on integration tournament tests. Performance Considerations🟡 Algorithm Selection (rhea.rs:714-719)The PR adds algorithm selection but doesn't integrate it into Questions:
Recommendation: Add benchmarks comparing:
🟡 Memory AllocationsSeveral hot paths allocate vectors: let valid_moves: Vec<Direction> = directions.iter()... // Allocates in tight loopRecommendation: For performance-critical paths, consider:
Security Concerns✅ No Security Issues Detected
Best Practices🟢 Good Practices:
🟡 Could Improve:
Recommendations SummaryMust Fix Before Merge:
Should Fix:
Nice to Have:
Overall AssessmentVerdict: 🟢 Approve with Minor Changes This is a solid PR that:
The main concerns are:
The code quality is generally good, and the architectural decisions are sound. With the suggested fixes, this will be a strong addition to the codebase. Estimated effort to address issues: ~2-3 hours for critical fixes, 1-2 days for all recommendations. |
|
@claude fix these items from the review: Calculate center position dynamically instead of hardcoding 60 |
|
Claude finished @nosnaws's task in 3m 54s —— View job Fixed Review Items ✅All requested items from the review have been implemented:
Changes Made1. Made Tests DeterministicUpdated
Impact: Tests are now reproducible and won't have flaky failures due to random variance. 2. Documented and Fixed Magic Number 60Replaced all hardcoded
Impact: Code now works correctly for any board size, not just 11x11. Center calculation is self-documenting. 3. Added Comprehensive Unit TestsAdded 7 new unit tests for smart move logic (src/rhea.rs:1115-1222):
Impact: Better test coverage for the smart opponent modeling logic, making it easier to catch regressions. CommitPushed to branch
| |
…d unit tests - Make tests deterministic by using seeded RNG instead of from_entropy() - Updated simulate_head_to_head and simulate_algorithm_matchup to accept seed parameter - Each game uses deterministic seed based on game index for reproducibility - Replace hardcoded magic number 60 with dynamic center calculation - Changed from 'let center = 60u128' to 'let center = (width * width) / 2' - Added explanatory comments for center calculation - Fixed in get_smart_move_deterministic(), score_game(), and Negamax::evaluate() - Add comprehensive unit tests for smart move logic - test_smart_move_avoids_walls: Verifies wall avoidance - test_smart_move_avoids_neck: Verifies neck collision avoidance - test_smart_move_seeks_food: Verifies food-seeking behavior - test_smart_move_avoids_body_collision: Verifies body collision avoidance - test_smart_move_no_valid_moves: Tests edge case handling - test_smart_move_deterministic: Validates deterministic behavior - test_center_calculation_is_correct: Validates formula correctness Co-Authored-By: Alec Swanson <nosnaws@users.noreply.github.com>
Pull Request Review: Add Random Opponent Modeling and Comparison TestOverviewThis PR adds sophisticated opponent modeling strategies and a comprehensive testing framework for comparing their effectiveness. The changes are substantial (+940 lines) and introduce both new algorithms (Negamax with alpha-beta pruning) and extensive testing capabilities. Code Quality & Best PracticesStrengths ✅
Areas for Improvement 🔧1. Critical Bug in
|
|
@claude add an issues for these items
Issue: Creating new RNG instances in hot paths // Line 220 in rhea.rs
Line 297-299: let tail = *snake.body.last().unwrap(); |
|
Claude finished @nosnaws's task in 37s —— View job Creating GitHub Issues
|

configurable opponent behavior
instead of using the default "continue straight" fallback
against default modeling
Initial results show default modeling slightly outperforms random (9 vs 7
wins), likely due to added noise in fitness evaluation and weak scoring
function that only rewards survival/death without intermediate signals.