From 7bcc2f0c4d46ad1a8e841fa7bc6ce318594784e3 Mon Sep 17 00:00:00 2001 From: luccabb Date: Tue, 20 Jan 2026 23:17:59 -0800 Subject: [PATCH 1/3] Improve quiescence search Enhances quiescence search with better draw detection and check handling: - Add draw detection: fifty-move rule, insufficient material, repetition - Proper check handling: when in check, search all evasions instead of only tactical moves (position is unstable, can't use stand-pat) - Add is_tactical_move() helper for cleaner move filtering - Update organize_moves_quiescence() to use tactical move detection Co-Authored-By: Claude Opus 4.5 --- moonfish/engines/alpha_beta.py | 70 ++++++++++++++++++++++++---------- moonfish/move_ordering.py | 31 ++++++++++++--- 2 files changed, 75 insertions(+), 26 deletions(-) diff --git a/moonfish/engines/alpha_beta.py b/moonfish/engines/alpha_beta.py index 6d25360..bf272fc 100644 --- a/moonfish/engines/alpha_beta.py +++ b/moonfish/engines/alpha_beta.py @@ -91,39 +91,69 @@ def quiescence_search( Returns: - best_score: returns best move's score. """ - if board.is_stalemate(): - return 0 + in_check = board.is_check() if board.is_checkmate(): return -self.config.checkmate_score - stand_pat = self.eval_board(board) + if board.is_stalemate(): + return 0 - # recursion base case - if depth == 0: - return stand_pat + # Draw detection: fifty-move rule, insufficient material + # Note: Repetition is checked after making moves, not here + if board.is_fifty_moves() or board.is_insufficient_material(): + return 0 - # beta-cutoff - if stand_pat >= beta: - return beta + stand_pat = self.eval_board(board) + + # When in check, we can't use stand-pat for pruning (position is unstable) + # We must search all evasions. However, still respect depth limit. + if in_check: + # In check: search all evasions, but don't use stand-pat for cutoffs + if depth <= 0: + # At depth limit while in check: return evaluation + # (not ideal but prevents infinite recursion) + return stand_pat + + best_score = float("-inf") + moves = list(board.legal_moves) # All evasions + else: + # Not in check: normal quiescence behavior + # recursion base case + if depth <= 0: + return stand_pat + + # beta-cutoff: position is already good enough + if stand_pat >= beta: + return beta - # alpha update - alpha = max(alpha, stand_pat) + # Use stand-pat as baseline (we can always choose not to capture) + best_score = stand_pat + alpha = max(alpha, stand_pat) - # get moves for quiescence search - moves = organize_moves_quiescence(board) + # Only tactical moves when not in check + moves = organize_moves_quiescence(board) for move in moves: # make move and get score board.push(move) - score = -self.quiescence_search( - board=board, - depth=depth - 1, - alpha=-beta, - beta=-alpha, - ) + + # Check if this move leads to a repetition (draw) + if board.is_repetition(2): + score: float = 0 # Draw score + else: + score = -self.quiescence_search( + board=board, + depth=depth - 1, + alpha=-beta, + beta=-alpha, + ) + board.pop() + if score > best_score: + best_score = score + # beta-cutoff if score >= beta: return beta @@ -131,7 +161,7 @@ def quiescence_search( # alpha-update alpha = max(alpha, score) - return alpha + return best_score def negamax( self, diff --git a/moonfish/move_ordering.py b/moonfish/move_ordering.py index f210e9c..630ec81 100644 --- a/moonfish/move_ordering.py +++ b/moonfish/move_ordering.py @@ -32,26 +32,45 @@ def organize_moves(board: Board): return captures + non_captures +def is_tactical_move(board: Board, move: Move) -> bool: + """ + Check if a move is tactical (should be searched in quiescence). + + Tactical moves are: + - Captures (change material) + - Promotions (significant material gain) + - Moves that give check (forcing) + """ + return ( + board.is_capture(move) or move.promotion is not None or board.gives_check(move) + ) + + def organize_moves_quiescence(board: Board): """ This function receives a board and it returns a list of all the possible moves for the current player, sorted by importance. + Only returns tactical moves: captures, promotions, and checks. + Arguments: - board: chess board state Returns: - - moves: list of all the possible moves for the current player sorted based on importance. + - moves: list of tactical moves sorted by importance (MVV-LVA). """ phase = get_phase(board) - # filter only important moves for quiescence search - captures = filter( - lambda move: board.is_zeroing(move) or board.gives_check(move), + + # Filter only tactical moves for quiescence search + # (captures, promotions, checks - NOT quiet pawn pushes) + tactical_moves = filter( + lambda move: is_tactical_move(board, move), board.legal_moves, ) - # sort moves by importance + + # Sort moves by importance using MVV-LVA moves = sorted( - captures, + tactical_moves, key=lambda move: mvv_lva(board, move, phase), reverse=(board.turn == BLACK), ) From dda1ee2186e53784557e0ded51a0aae6fdf49f5c Mon Sep 17 00:00:00 2001 From: Lucca Bertoncini Date: Mon, 2 Feb 2026 12:56:00 -0800 Subject: [PATCH 2/3] fix typo --- moonfish/engines/alpha_beta.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/moonfish/engines/alpha_beta.py b/moonfish/engines/alpha_beta.py index bf272fc..f421acf 100644 --- a/moonfish/engines/alpha_beta.py +++ b/moonfish/engines/alpha_beta.py @@ -140,7 +140,7 @@ def quiescence_search( # Check if this move leads to a repetition (draw) if board.is_repetition(2): - score: float = 0 # Draw score + score = 0 # Draw score else: score = -self.quiescence_search( board=board, From f2f2799df423f052cfb54cdb2fdf70c1f17f882a Mon Sep 17 00:00:00 2001 From: Lucca Bertoncini Date: Wed, 4 Feb 2026 11:26:34 -0800 Subject: [PATCH 3/3] Fix mypy type error in quiescence search Use float literal 0.0 instead of int 0 for draw score to match the return type of quiescence_search. --- moonfish/engines/alpha_beta.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/moonfish/engines/alpha_beta.py b/moonfish/engines/alpha_beta.py index f421acf..5244aed 100644 --- a/moonfish/engines/alpha_beta.py +++ b/moonfish/engines/alpha_beta.py @@ -140,7 +140,7 @@ def quiescence_search( # Check if this move leads to a repetition (draw) if board.is_repetition(2): - score = 0 # Draw score + score = 0.0 # Draw score else: score = -self.quiescence_search( board=board,