Skip to content

Commit aa2d0a7

Browse files
luccabbclaude
andcommitted
[7/9] Add Late Move Reductions (LMR) and Principal Variation Search (PVS)
Implements two key search optimizations: **Late Move Reductions (LMR):** - Reduce search depth for late quiet moves (move_index >= 3) - Only apply when: depth >= 3, not in check, move is quiet - Quiet moves = no capture, no check, no promotion - Simple reduction of 1 ply (more aggressive formulas tested but hurt accuracy) - Re-search at full depth if reduced search finds promising score **Principal Variation Search (PVS):** - First move: search with full alpha-beta window - Later moves: search with zero window (alpha, alpha+1) - If zero window search beats alpha, re-search with full window - Saves time when first move is best (which is often true with good ordering) Both techniques work together: - PVS assumes first move is best (good with TT/killer/MVV-LVA ordering) - LMR reduces work on moves unlikely to be best - Combined, they significantly reduce nodes searched Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent 6e50edb commit aa2d0a7

1 file changed

Lines changed: 61 additions & 11 deletions

File tree

moonfish/engines/alpha_beta.py

Lines changed: 61 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -308,22 +308,72 @@ def negamax(
308308
moves.remove(tt_move)
309309
moves.insert(0, tt_move)
310310

311-
for move in moves:
311+
in_check = board.is_check()
312+
313+
for move_index, move in enumerate(moves):
312314
is_capture = board.is_capture(move)
315+
gives_check = board.gives_check(move)
316+
is_promotion = move.promotion is not None
313317

314318
# make the move
315319
board.push(move)
316320

317-
board_score = -self.negamax(
318-
board,
319-
depth - 1,
320-
null_move,
321-
cache,
322-
-beta,
323-
-alpha,
324-
ply + 1,
325-
killers,
326-
)[0]
321+
# Late Move Reductions (LMR):
322+
# Reduce search depth for late quiet moves that are unlikely to be good
323+
# Conditions: sufficient depth, late move, quiet (no capture/check/promotion)
324+
reduction = 0
325+
if (
326+
depth >= 3
327+
and move_index >= 3
328+
and not is_capture
329+
and not gives_check
330+
and not is_promotion
331+
and not in_check
332+
):
333+
# Simple reduction: reduce by 1 ply
334+
reduction = 1
335+
336+
# Principal Variation Search (PVS):
337+
# For the first move, search with full window
338+
# For subsequent moves, search with zero window first
339+
if move_index == 0:
340+
# First move: full window search
341+
board_score = -self.negamax(
342+
board,
343+
depth - 1,
344+
null_move,
345+
cache,
346+
-beta,
347+
-alpha,
348+
ply + 1,
349+
killers,
350+
)[0]
351+
else:
352+
# Later moves: zero window search (with LMR reduction if applicable)
353+
board_score = -self.negamax(
354+
board,
355+
depth - 1 - reduction,
356+
null_move,
357+
cache,
358+
-alpha - 1, # Zero window
359+
-alpha,
360+
ply + 1,
361+
killers,
362+
)[0]
363+
364+
# If zero window search found a promising move, re-search with full window
365+
if board_score > alpha and (board_score < beta or reduction > 0):
366+
board_score = -self.negamax(
367+
board,
368+
depth - 1, # Full depth (no reduction)
369+
null_move,
370+
cache,
371+
-beta,
372+
-alpha,
373+
ply + 1,
374+
killers,
375+
)[0]
376+
327377
if board_score > self.config.checkmate_threshold:
328378
board_score -= 1
329379
if board_score < -self.config.checkmate_threshold:

0 commit comments

Comments
 (0)