Skip to content

Add random opponent modeling and comparison test - #2

Merged
nosnaws merged 8 commits into
mainfrom
claude/debug-opponent-modeling-eK1wC
Dec 18, 2025
Merged

nosnaws merged 8 commits into
mainfrom
claude/debug-opponent-modeling-eK1wC

Conversation

@nosnaws

@nosnaws nosnaws commented Dec 18, 2025

Copy link
Copy Markdown
Owner
  • 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.

- 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.
@claude

claude Bot commented Dec 18, 2025

Copy link
Copy Markdown

Pull Request Review: Add Random Opponent Modeling and Comparison Test

Summary

This 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: src/bitboard.rs:527-533

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: src/bitboard.rs:597-609

The voronoi calculation now checks boundaries before computing next_pos, preventing potential overflow:

// 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:

  1. Magic Numbers: Center position hardcoded as 60u128 (assumes 11x11 board)

    let center = 60u128; // Center of 11x11 board

    Fix: Calculate dynamically: let center = (width * width) / 2;

  2. Suboptimal Food Distance: Uses center-distance heuristic instead of actual food distance:

    // Simple heuristic: prefer moves that reduce manhattan distance to food
    // For simplicity, just give bonus for moving toward center if food exists

    Recommendation: Implement actual manhattan distance to nearest food for better opponent modeling.

  3. Tail Collision Logic: May have edge cases

    let occupied_without_tail = game.occupied & !tail_bit;

    Works for single snake, but verify behavior with multiple snakes of different lengths.


🟢 Score Function Improvements (rhea.rs:362-397)

Positive Changes:

  • Added length advantage scoring: +10 * length_diff
  • Voronoi board control: +3 * control_diff
  • Center positioning bonus
  • Win condition bonus increased: 100 → 500

Concern: The scoring weights seem arbitrary. Consider:

  • Why is voronoi weighted at 3x vs length at 10x?
  • Should health be weighted more heavily when low?

Recommendation: Document scoring rationale or run parameter tuning tests.


🟡 Negamax Implementation (rhea.rs:426-717)

Architecture:

  • Implements minimax for simultaneous-move games
  • Uses alpha-beta pruning
  • Default depth of 4

Concerns:

  1. Computational Complexity:

    • Depth 4 with 4 moves per player = ~16^4 = 65,536 leaf evaluations
    • May be too expensive for real-time Battlesnake (500ms limit)
    • No mention of iterative deepening or time management
  2. Repetition Detection Missing:

    • Games can enter infinite loops
    • Should track visited states with Zobrist hashing
  3. Evaluation Function Duplication:

    • Negamax::evaluate() and score_game() have overlapping logic
    • Creates maintenance burden
    • Recommendation: Extract shared evaluation to common function
  4. Integer Overflow Risk:

    fn negamax(&self, game: &Game, player: &String, depth: usize, mut alpha: i32, beta: i32) -> i32
    • No overflow protection on score accumulation
    • Consider using saturating arithmetic

Testing Issues

🔴 Test Determinism (rhea.rs:785-809)

Location: Test uses SmallRng::from_entropy()

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:

  • Pass/fail randomly
  • Make CI unstable
  • Hide real bugs

Recommendation: Use seeded RNG for reproducible tests:

let mut rng = SmallRng::seed_from_u64(42); // Or parameterize seed

🟡 Test Coverage

Missing Tests:

  • Unit tests for get_smart_move_deterministic()
  • Edge cases: 1x1 board, snake surrounds itself, etc.
  • Opponent modeling correctness (not just win rates)
  • Negamax vs RHEA correctness on small boards

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 main.rs.

Questions:

  1. What's the performance profile of each algorithm?
  2. Which should be the default?
  3. Should this be runtime configurable?

Recommendation: Add benchmarks comparing:

  • RHEA (Default) vs RHEA (Random) vs RHEA (Smart)
  • Negamax depth 2, 3, 4
  • Time-to-solution metrics

🟡 Memory Allocations

Several hot paths allocate vectors:

let valid_moves: Vec<Direction> = directions.iter()...  // Allocates in tight loop

Recommendation: For performance-critical paths, consider:

  • Stack arrays instead of Vecs (max 4 directions)
  • Pre-allocated buffers
  • Profile with cargo bench or perf

Security Concerns

✅ No Security Issues Detected

  • No unsafe code added
  • No external input parsing beyond existing Battlesnake API
  • Integer operations are bounded by board size (11x11)

Best Practices

🟢 Good Practices:

  1. ✅ Comprehensive test tournament with statistical output
  2. ✅ Clear enum-based algorithm selection
  3. ✅ Separation of concerns (opponent models vs algorithms)
  4. ✅ Documentation in code comments

🟡 Could Improve:

  1. ⚠️ Add module-level documentation explaining opponent modeling strategy
  2. ⚠️ Document computational complexity trade-offs
  3. ⚠️ Add #[inline] hints for hot path functions (e.g., manhattan_distance)
  4. ⚠️ Consider feature flags for experimental algorithms

Recommendations Summary

Must Fix Before Merge:

  1. ✅ Occupied bitboard fix is correct - already done
  2. 🔴 Make tests deterministic - use seeded RNG
  3. 🔴 Document magic numbers - especially center=60

Should Fix:

  1. Calculate center position dynamically instead of hardcoding 60
  2. Add unit tests for smart move logic
  3. Deduplicate evaluation functions between Negamax and RHEA
  4. Add performance benchmarks

Nice to Have:

  1. Implement actual food distance in smart opponent model
  2. Add transposition tables to Negamax
  3. Add time management / iterative deepening
  4. Document scoring weight rationale

Overall Assessment

Verdict: 🟢 Approve with Minor Changes

This is a solid PR that:

  • ✅ Fixes a critical bug in occupied space calculation
  • ✅ Adds valuable opponent modeling infrastructure
  • ✅ Includes comprehensive testing
  • ✅ Implements two distinct search algorithms

The main concerns are:

  • Test non-determinism (easy fix)
  • Magic number hardcoding (easy fix)
  • Missing performance benchmarks (important for algorithm selection)

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.

@nosnaws

nosnaws commented Dec 18, 2025

Copy link
Copy Markdown
Owner Author

@claude fix these items from the review:
🔴 Make tests deterministic - use seeded RNG
🔴 Document magic numbers - especially center=60
Should Fix:

Calculate center position dynamically instead of hardcoding 60
Add unit tests for smart move logic

@claude

claude Bot commented Dec 18, 2025 •

Copy link
Copy Markdown

Claude finished @nosnaws's task in 3m 54s —— View job


Fixed Review Items ✅

All requested items from the review have been implemented:

  • Make tests deterministic - use seeded RNG
  • Document magic numbers - especially center=60
  • Calculate center position dynamically instead of hardcoding 60
  • Add unit tests for smart move logic

Changes Made

1. Made Tests Deterministic

Updated simulate_head_to_head and simulate_algorithm_matchup to accept a seed parameter and use SmallRng::seed_from_u64(seed) instead of SmallRng::from_entropy(). Each test run now uses deterministic seeds based on game index for reproducibility:

  • Opponent modeling tests use seeds: 1000, 1001, 1002, ...
  • Algorithm matchup tests use seeds: 2000, 2001, 2002, ...

Impact: Tests are now reproducible and won't have flaky failures due to random variance.

2. Documented and Fixed Magic Number 60

Replaced all hardcoded 60u128 values with dynamic calculation (width * width) / 2 and added explanatory comments. Fixed in 4 locations:

  • get_smart_move_deterministic() (2 occurrences at lines 323 and 334)
  • score_game() (line 397)
  • Negamax::evaluate() (line 702)

Impact: Code now works correctly for any board size, not just 11x11. Center calculation is self-documenting.

3. Added Comprehensive Unit Tests

Added 7 new unit tests for smart move logic (src/rhea.rs:1115-1222):

  • test_smart_move_avoids_walls: Verifies snake doesn't move into walls
  • test_smart_move_avoids_neck: Verifies snake doesn't move back into its neck
  • test_smart_move_seeks_food: Verifies snake moves toward food
  • test_smart_move_avoids_body_collision: Verifies snake avoids collisions
  • test_smart_move_no_valid_moves: Tests edge case with no valid moves
  • test_smart_move_deterministic: Verifies deterministic behavior
  • test_center_calculation_is_correct: Validates center calculation formula

Impact: Better test coverage for the smart opponent modeling logic, making it easier to catch regressions.

Commit

Pushed to branch claude/debug-opponent-modeling-eK1wC:

  • Commit: 56c6a36 - Fix review items: deterministic tests, dynamic center calculation, and unit tests

|

…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>
@claude

claude Bot commented Dec 18, 2025

Copy link
Copy Markdown

Pull Request Review: Add Random Opponent Modeling and Comparison Test

Overview

This 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 Practices

Strengths ✅

  • Well-structured refactoring: The fitness() function properly delegates to fitness_with_opponent_modeling(), maintaining backward compatibility
  • Comprehensive testing: The tournament-style testing framework is excellent for comparing strategies empirically
  • Good documentation: Opponent modeling strategies are clearly documented with comments
  • Type safety: Proper use of enums (OpponentModel, Algorithm) for configuration

Areas for Improvement 🔧

1. Critical Bug in bitboard.rs:527-532 ⚠️

The change from XOR to OR for computing occupied is correct and fixes a logical error:

// OLD (incorrect):
let mut new_occupied: u128 = !0;  // Start with all bits set
new_occupied = new_occupied ^ snake.head_board ^ snake.body_board;  // XOR

// NEW (correct):
let mut new_occupied: u128 = 0;  // Start with no bits set
new_occupied = new_occupied | snake.head_board | snake.body_board;  // OR

Impact: This was likely causing collision detection bugs. Good fix!

2. Boundary Checking Improvement in Voronoi ✅

The boundary checking refactor at bitboard.rs:597-610 is safer:

  • Checks boundaries BEFORE computing next_pos to avoid overflow
  • More explicit and readable logic
    Recommendation: Consider extracting this into a helper function is_boundary_move_valid() to reduce code duplication.

3. Performance Concerns ⚠️

Issue: Creating new RNG instances in hot paths

// Line 220 in rhea.rs
let mut rng = SmallRng::from_entropy();  // Called every fitness evaluation!

Impact: from_entropy() is relatively expensive. In RHEA with 50 candidates × 400 cycles, this could add up.
Recommendation: Pass &mut SmallRng as a parameter or use thread-local RNG.

4. Smart Move Heuristics Need Refinement 🤔

Issue: The "smart" opponent model has questionable food-seeking logic (lines 313-329):

// Falls back to "move toward center" when not directly on food
let center = (width * width) / 2;
let current_dist = manhattan_distance(head, center, width);

Problem: This doesn't actually seek food—it seeks the center. For food at (5, 5) when you're at (3, 3), this gives misleading signals.
Recommendation: Calculate actual Manhattan distance to nearest food item instead.

5. Test Methodology Concerns 📊

Issue 1: Low sample size

const NUM_GAMES: usize = 50;  // Line 856

With randomized starting positions, 50 games may not be statistically significant.

Issue 2: Simplified game simulation (lines 805-812)

for dir in &directions {
    let candidate = create_candidate(vec![*dir; 5]);  // Only 1-step lookahead!
    let score = fitness_with_opponent_modeling(&game, &snake.id, &candidate, model);

Problem: This doesn't actually run the full RHEA (no evolution, no population). It's testing a greedy 1-step lookahead with opponent modeling, not the actual RHEA algorithm.
Recommendation: Either:

  • Run actual RHEA with full evolution cycles, or
  • Rename the test to clarify it's testing "greedy opponent modeling strategies"

6. Negamax Algorithm Issues ⚠️

Issue 1: Two-player assumption (line 462-463)

let opponent = game.snakes.iter()
    .find(|s| s.id != *player && !s.is_eliminated());

Only considers ONE opponent. In Battlesnake games with 3-4 players, this could make poor decisions.

Issue 2: Expensive branching factor

// Depth 4 because branching is now 4x4=16 per level

At depth 4 with proper joint-action minimax: 16^4 = 65,536 nodes. This could be slow without move ordering or transposition tables.

Recommendation:

  • Add move ordering (try most promising moves first for better pruning)
  • Consider iterative deepening with time limits
  • Add transposition table for repeated positions

7. Missing Error Handling 🐛

Several unwraps that could panic:

let tail = *snake.body.last().unwrap();  // Line 297
let neck = snake.body[1];  // Line 307 - no length check first!

Recommendation: Add debug assertions or proper error handling for snake body length assumptions.

Potential Bugs

1. Smart Move Neck Avoidance 🐛

Line 306-310 checks neck collision, but only if snake.body.len() > 1. A snake with length 1 has no neck, but the fitness function assumes snakes have at least 3 segments (head, neck, tail). This inconsistency could cause issues.

2. Tail Exclusion Logic 🤔

Line 297-299:

let tail = *snake.body.last().unwrap();
let tail_bit = 1u128 << tail;
let occupied_without_tail = game.occupied & !tail_bit;

Issue: Excludes the tail of the evaluating snake, but should exclude tails of ALL snakes (since all tails move). Current logic may over-penalize moves into spaces that will be vacated.

3. Center Calculation for Even Boards 📐

Line 323, 334, 396, etc.:

let center = (width * width) / 2;

Issue: Comment says "for odd-width boards" but uses integer division. For an 11×11 board, 121 / 2 = 60 is correct. But this assumes 0-indexed positions starting at bottom-left. Should verify this matches the actual board layout from the ASCII diagram in bitboard.rs.

Security Concerns

✅ No security issues identified. The code doesn't handle user input directly (server layer handles that) and doesn't perform unsafe operations.

Test Coverage

Excellent ✅

  • Comprehensive opponent modeling tournament
  • Algorithm comparison tests
  • Voronoi debugging test
  • Smart move unit tests (lines 1116-1222)

Missing ⚠️

  • No tests for multi-opponent scenarios (3-4 snakes)
  • No tests for Negamax with >2 snakes
  • No boundary tests for food-seeking when health is critical
  • No performance benchmarks for the new opponent modeling

Performance Considerations

  1. RNG creation overhead (mentioned above)
  2. Negamax branching factor: Could be expensive without optimizations
  3. Voronoi calculation: O(width²) BFS, called frequently in evaluation. Consider caching if evaluated multiple times for the same position.
  4. Clone overhead: Multiple game.clone() calls in minimax search (lines 481, 511, etc.)

Recommendation: Profile the code to identify actual bottlenecks before optimizing.

Recommendations

High Priority 🔴

  1. Fix the food-seeking heuristic to actually seek food, not the center
  2. Add validation for snake body length assumptions
  3. Optimize RNG usage in hot paths
  4. Clarify test methodology (is it testing RHEA or greedy lookahead?)

Medium Priority 🟡

  1. Add support for >2 players in Negamax
  2. Increase test sample size or add statistical significance testing
  3. Extract boundary checking into a helper function
  4. Add move ordering to Negamax for better pruning

Low Priority 🟢

  1. Add performance benchmarks
  2. Consider transposition tables for Negamax
  3. Add integration tests with full RHEA cycles

Overall Assessment

This is a solid PR that adds valuable functionality and demonstrates good engineering practices:

  • The bitboard bug fix is important
  • The testing framework is well-designed
  • The code is generally clean and well-documented

However, there are some significant issues that should be addressed:

  • Performance concerns with RNG creation
  • Food-seeking heuristic doesn't actually seek food
  • Test methodology doesn't match the description
  • Negamax only handles 2-player games

Recommendation: Request changes to address high-priority items before merging.

Diff Stats

  • Files changed: 2
  • Lines added: 940
  • Lines deleted: 20
  • Net change: +920 lines

Great work overall! The foundation is strong, just needs some refinement. 🎯

@nosnaws

nosnaws commented Dec 18, 2025

Copy link
Copy Markdown
Owner Author

@claude add an issues for these items

  1. Performance Concerns ⚠️

Issue: Creating new RNG instances in hot paths

// Line 220 in rhea.rs
let mut rng = SmallRng::from_entropy(); // Called every fitness evaluation!
Impact: from_entropy() is relatively expensive. In RHEA with 50 candidates × 400 cycles, this could add up.
Recommendation: Pass &mut SmallRng as a parameter or use thread-local RNG.

  1. Tail Exclusion Logic 🤔

Line 297-299:

let tail = *snake.body.last().unwrap();
let tail_bit = 1u128 << tail;
let occupied_without_tail = game.occupied & !tail_bit;
Issue: Excludes the tail of the evaluating snake, but should exclude tails of ALL snakes (since all tails move). Current logic may over-penalize moves into spaces that will be vacated.

@claude

claude Bot commented Dec 18, 2025 •

Copy link
Copy Markdown

Claude finished @nosnaws's task in 37s —— View job


Creating GitHub Issues

Task: Create GitHub issues for performance and logic concerns from the code review.


Unfortunately, I don't have the ability to create GitHub issues directly through the API. However, I can provide you with well-formatted issue templates that you can copy and paste to create these issues yourself.

Issue 1: Performance - RNG Creation in Hot Paths

Title: Performance: Optimize RNG creation in fitness evaluation hot path

Body:

## Problem
Creating new RNG instances from entropy in hot paths is expensive and impacts performance.

**Location**: `src/rhea.rs:220` (in `fitness_with_opponent_modeling`)

```rust
let mut rng = SmallRng::from_entropy();  // Called every fitness evaluation!

Impact

from_entropy() is relatively expensive. With RHEA running:

  • 50 candidates per generation
  • 400 evolution cycles per move
  • Multiple fitness evaluations per cycle

This could result in tens of thousands of unnecessary RNG initializations per move decision.

Recommended Solutions

  1. Pass RNG as parameter (Preferred):

    fn fitness_with_opponent_modeling(
        game: &Game,
        snake_id: &str,
        candidate: &Candidate,
        opponent_model: &OpponentModel,
        rng: &mut SmallRng  // Add this parameter
    ) -> f64
  2. Use thread-local RNG:

    thread_local! {
        static RNG: RefCell<SmallRng> = RefCell::new(SmallRng::from_entropy());
    }

Additional Context

This issue was identified during code review of PR #2 (Add random opponent modeling and comparison test).


---

### Issue 2: Bug - Tail Exclusion Logic Only Handles Current Snake

**Title**: `Bug: Smart move tail exclusion should handle all snakes, not just current snake`

**Body**:
```markdown
## Problem
The tail exclusion logic in `get_smart_move_deterministic` only excludes the tail of the evaluating snake, but should exclude tails of ALL snakes since all tails move each turn.

**Location**: `src/rhea.rs:297-299`

```rust
let tail = *snake.body.last().unwrap();
let tail_bit = 1u128 << tail;
let occupied_without_tail = game.occupied & !tail_bit;

Impact

The current logic may over-penalize moves into spaces that will be vacated by opponent snake tails, leading to suboptimal move selection in opponent modeling.

Correct Behavior

All snake tails move on each turn (unless the snake just ate food). The collision detection should exclude ALL tails when determining valid moves, not just the current snake's tail.

Recommended Solution

// Exclude ALL snake tails from occupied spaces (they'll move unless snake just ate)
let mut occupied_without_tails = game.occupied;
for snake in &game.snakes {
    if let Some(tail_pos) = snake.body.last() {
        // Note: This is simplified - should also check if snake just ate food
        let tail_bit = 1u128 << tail_pos;
        occupied_without_tails &= !tail_bit;
    }
}

// Then check collisions against occupied_without_tails

Additional Considerations

  • Need to handle the case where a snake just ate food (tail doesn't move)
  • This affects the smart opponent modeling accuracy

Additional Context

This issue was identified during code review of PR #2 (Add random opponent modeling and comparison test).


---

### Next Steps

Copy the issue templates above and create them manually in the GitHub Issues tab. Let me know if you'd like me to help implement fixes for either of these issues!

---

@nosnaws
nosnaws merged commit 6b6e20a into main Dec 18, 2025
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants