Skip to content

[6/6] Add futility pruning#38

Open
luccabb wants to merge 1 commit intofeature/lmr-pvsfrom
feature/futility-pruning
Open

[6/6] Add futility pruning#38
luccabb wants to merge 1 commit intofeature/lmr-pvsfrom
feature/futility-pruning

Conversation

@luccabb
Copy link
Owner

@luccabb luccabb commented Jan 20, 2026

Summary

  • Skip quiet moves at low depths when static evaluation is far below alpha
  • Margin-based pruning: depth 1 = 100cp, depth 2 = 200cp

Details

Futility Pruning

If the static evaluation plus a safety margin is still below alpha, quiet moves
are unlikely to improve the position enough to beat alpha. We skip them.

Implementation:

if depth <= 2 and not in_check:
    static_eval = self.eval_board(board)
    futility_margin = 100 * depth  # 100cp at depth 1, 200cp at depth 2
    can_futility_prune = static_eval + futility_margin < alpha

# In move loop:
if can_futility_prune and move_index > 0 and is_quiet_move:
    continue  # Skip this move

Safety Conditions

Pruning is conservative to avoid missing important moves:

  • Only at low depths (1-2 ply)
  • Never when in check
  • Never prune captures, checks, or promotions
  • Never prune the first move
  • Margin allows for reasonable positional improvement

Why it Works

At low depths near leaf nodes:

  • Quiet moves have limited potential to swing evaluation
  • If we're already losing by > margin, a quiet move won't save us
  • Captures/checks are still searched (tactical opportunities)

Test plan

  • All 64 unit tests pass
  • Verified pruning only happens for quiet moves at low depth

🤖 Generated with Claude Code

@luccabb luccabb force-pushed the feature/futility-pruning branch from 9f14f89 to 5019fdf Compare January 21, 2026 06:42
@luccabb luccabb force-pushed the feature/futility-pruning branch from 5019fdf to 369da0e Compare January 21, 2026 06:44
@luccabb luccabb changed the title [8/9] Add futility pruning [6/7] Add futility pruning Jan 21, 2026
@luccabb luccabb force-pushed the feature/futility-pruning branch from 369da0e to fe21981 Compare January 21, 2026 07:33
@luccabb luccabb changed the title [6/7] Add futility pruning [6/6] Add futility pruning Jan 21, 2026
@github-actions
Copy link

🔬 Stockfish Benchmark Results

vs Stockfish Skill Level 3

Metric Wins Losses Draws Total Win %
Overall 0 100 0 100 0%
As White 0 50 0 50 0%
As Black 0 50 0 50 0%

vs Stockfish Skill Level 4

Metric Wins Losses Draws Total Win %
Overall 1 99 0 100 1.0%
As White 0 50 0 50 0%
As Black 1 49 0 50 2.0%

vs Stockfish Skill Level 5

Metric Wins Losses Draws Total Win %
Overall 0 100 0 100 0%
As White 0 50 0 50 0%
As Black 0 50 0 50 0%
Configuration
  • 5 chunks × 20 rounds × 3 skill levels = 300 total games
  • Each opening played with colors reversed (-repeat) for fairness
  • Moonfish: 60s per move
  • Stockfish: 60+5 time control

Implements futility pruning to skip quiet moves that can't improve alpha:

**Futility Pruning:**
- At low depths (1-2), compute static evaluation
- If eval + margin < alpha, quiet moves can't help
- Skip quiet moves (no capture, check, or promotion)
- Never prune the first move (might be the only good one)

**Margin Calculation:**
- Depth 1: 100 centipawns margin
- Depth 2: 200 centipawns margin
- Larger margin at deeper depths allows for more potential improvement

**Conditions for pruning:**
- Depth <= 2
- Not in check (check positions are critical)
- Static eval + margin < alpha
- Move is quiet (not capture/check/promotion)
- Not the first move in the list

This is a forward pruning technique that can miss some moves, but
the marginsare conservative enough to rarely affect results while
significantly reducing nodes searched.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
@luccabb luccabb force-pushed the feature/futility-pruning branch from 65d1f14 to ef70357 Compare February 16, 2026 06:34
@luccabb
Copy link
Owner Author

luccabb commented Feb 16, 2026

/run-nps-benchmark

@github-actions
Copy link

⚡ NPS Benchmark Results

Metric Value
Depth 5
Positions 48
Total nodes 6462485
Total time 819.07s
Nodes/second 7889

Node count is the primary signal — it's deterministic and catches search behavior changes. If the node count changes, the PR changed search behavior. NPS is informational only (CI runner performance varies).

Per-position breakdown
Position  1/48: nodes=21252      time=2.75s  nps=7724
Position  2/48: nodes=265680     time=39.08s  nps=6799
Position  3/48: nodes=14666      time=1.16s  nps=12605
Position  4/48: nodes=384303     time=48.37s  nps=7945
Position  5/48: nodes=68572      time=9.78s  nps=7009
Position  6/48: nodes=137992     time=18.97s  nps=7274
Position  7/48: nodes=176141     time=24.54s  nps=7178
Position  8/48: nodes=100297     time=12.82s  nps=7824
Position  9/48: nodes=179833     time=23.02s  nps=7811
Position 10/48: nodes=93999      time=12.79s  nps=7352
Position 11/48: nodes=86459      time=10.80s  nps=8009
Position 12/48: nodes=318364     time=43.67s  nps=7290
Position 13/48: nodes=221369     time=30.23s  nps=7322
Position 14/48: nodes=43382      time=6.17s  nps=7036
Position 15/48: nodes=80374      time=9.60s  nps=8374
Position 16/48: nodes=22219      time=2.74s  nps=8106
Position 17/48: nodes=3802       time=0.30s  nps=12697
Position 18/48: nodes=2911       time=0.22s  nps=13119
Position 19/48: nodes=5644       time=0.49s  nps=11552
Position 20/48: nodes=30857      time=2.52s  nps=12227
Position 21/48: nodes=12801      time=1.20s  nps=10635
Position 22/48: nodes=636        time=0.05s  nps=13413
Position 23/48: nodes=1115       time=0.08s  nps=14605
Position 24/48: nodes=10577      time=0.84s  nps=12552
Position 25/48: nodes=6579       time=0.54s  nps=12178
Position 26/48: nodes=12469      time=1.03s  nps=12156
Position 27/48: nodes=25801      time=2.64s  nps=9772
Position 28/48: nodes=115697     time=12.68s  nps=9127
Position 29/48: nodes=28258      time=2.81s  nps=10057
Position 30/48: nodes=2774       time=0.22s  nps=12538
Position 31/48: nodes=202496     time=22.76s  nps=8898
Position 32/48: nodes=436165     time=56.73s  nps=7688
Position 33/48: nodes=150218     time=22.77s  nps=6598
Position 34/48: nodes=123790     time=15.23s  nps=8127
Position 35/48: nodes=195976     time=22.34s  nps=8773
Position 36/48: nodes=1742889    time=206.18s  nps=8453
Position 37/48: nodes=790867     time=114.52s  nps=6905
Position 38/48: nodes=1562       time=0.10s  nps=16115
Position 39/48: nodes=3156       time=0.23s  nps=13652
Position 40/48: nodes=3435       time=0.12s  nps=28423
Position 41/48: nodes=14228      time=1.18s  nps=12025
Position 42/48: nodes=13842      time=1.28s  nps=10823
Position 43/48: nodes=2321       time=0.16s  nps=14151
Position 44/48: nodes=25708      time=2.34s  nps=10972
Position 45/48: nodes=14544      time=1.58s  nps=9211
Position 46/48: nodes=266465     time=29.47s  nps=9040
Position 47/48: nodes=0          time=0.00s  nps=0  (terminal)
Position 48/48: nodes=0          time=0.00s  nps=0  (terminal)

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.

1 participant