Skip to content

Commit 3dd9239

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 f205ab8 commit 3dd9239

File tree

1 file changed

+61
-11
lines changed

1 file changed

+61
-11
lines changed

moonfish/engines/alpha_beta.py

Lines changed: 61 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -301,22 +301,72 @@ def negamax(
301301
moves.remove(tt_move)
302302
moves.insert(0, tt_move)
303303

304-
for move in moves:
304+
in_check = board.is_check()
305+
306+
for move_index, move in enumerate(moves):
305307
is_capture = board.is_capture(move)
308+
gives_check = board.gives_check(move)
309+
is_promotion = move.promotion is not None
306310

307311
# make the move
308312
board.push(move)
309313

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

0 commit comments

Comments
 (0)