From caf3d137dcc1f428af3497b0a7070de45111a8bb Mon Sep 17 00:00:00 2001 From: luccabb Date: Mon, 19 Jan 2026 20:24:54 -0800 Subject: [PATCH] [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 --- moonfish/engines/alpha_beta.py | 72 ++++++++++++++++++++++++++++------ 1 file changed, 61 insertions(+), 11 deletions(-) diff --git a/moonfish/engines/alpha_beta.py b/moonfish/engines/alpha_beta.py index c9b0df1..8926b53 100644 --- a/moonfish/engines/alpha_beta.py +++ b/moonfish/engines/alpha_beta.py @@ -300,22 +300,72 @@ def negamax( moves.remove(tt_move) moves.insert(0, tt_move) - for move in moves: + in_check = board.is_check() + + for move_index, move in enumerate(moves): is_capture = board.is_capture(move) + gives_check = board.gives_check(move) + is_promotion = move.promotion is not None # make the move board.push(move) - board_score = -self.negamax( - board=board, - depth=depth - 1, - null_move=null_move, - cache=cache, - alpha=-beta, - beta=-alpha, - ply=ply + 1, - killers=killers, - )[0] + # Late Move Reductions (LMR): + # Reduce search depth for late quiet moves that are unlikely to be good + # Conditions: sufficient depth, late move, quiet (no capture/check/promotion) + reduction = 0 + if ( + depth >= 3 + and move_index >= 3 + and not is_capture + and not gives_check + and not is_promotion + and not in_check + ): + # Simple reduction: reduce by 1 ply + reduction = 1 + + # Principal Variation Search (PVS): + # For the first move, search with full window + # For subsequent moves, search with zero window first + if move_index == 0: + # First move: full window search + board_score = -self.negamax( + board=board, + depth=depth - 1, + null_move=null_move, + cache=cache, + alpha=-beta, + beta=-alpha, + ply=ply + 1, + killers=killers, + )[0] + else: + # Later moves: zero window search (with LMR reduction if applicable) + board_score = -self.negamax( + board=board, + depth=depth - 1 - reduction, + null_move=null_move, + cache=cache, + alpha=-alpha - 1, # Zero window + beta=-alpha, + ply=ply + 1, + killers=killers, + )[0] + + # If zero window search found a promising move, re-search with full window + if board_score > alpha and (board_score < beta or reduction > 0): + board_score = -self.negamax( + board=board, + depth=depth - 1, # Full depth (no reduction) + null_move=null_move, + cache=cache, + alpha=-beta, + beta=-alpha, + ply=ply + 1, + killers=killers, + )[0] + if board_score > self.config.checkmate_threshold: board_score -= 1 if board_score < -self.config.checkmate_threshold: