From 61fda66ba1bc11ad27654eff8db731bb79f544c9 Mon Sep 17 00:00:00 2001 From: luccabb Date: Mon, 19 Jan 2026 20:20:45 -0800 Subject: [PATCH 1/6] [5/9] Add MVV-LVA capture ordering and killer moves heuristic Improves move ordering for better pruning: **MVV-LVA (Most Valuable Victim - Least Valuable Attacker):** - Sort captures by MVV-LVA score in `organize_moves()` - Best captures (e.g., pawn takes queen) searched first - Increases beta cutoff rate for better pruning **Killer Moves Heuristic:** - Track quiet moves that cause beta cutoffs (2 per ply) - Add `killers` parameter to `organize_moves()` and `negamax()` - Killer moves searched after captures, before other quiet moves - Killers stored in a table indexed by ply depth **Move Ordering Priority:** 1. Captures (sorted by MVV-LVA) 2. Killer moves (quiet moves that caused cutoffs at this ply) 3. Other quiet moves (shuffled for variety) Co-Authored-By: Claude Opus 4.5 --- moonfish/engines/alpha_beta.py | 35 ++++++++++++++++++++++++++++++++-- moonfish/move_ordering.py | 28 +++++++++++++++++++++++---- 2 files changed, 57 insertions(+), 6 deletions(-) diff --git a/moonfish/engines/alpha_beta.py b/moonfish/engines/alpha_beta.py index 21046df..48498b4 100644 --- a/moonfish/engines/alpha_beta.py +++ b/moonfish/engines/alpha_beta.py @@ -190,6 +190,8 @@ def negamax( cache: DictProxy | CACHE_TYPE, alpha: float = NEG_INF, beta: float = INF, + ply: int = 0, + killers: list | None = None, ) -> tuple[float | int, Move | None]: """ This functions receives a board, depth and a player; and it returns @@ -283,6 +285,8 @@ def negamax( cache, -beta, -beta + 1, + ply + 1, + killers, )[0] board.pop() if board_score >= beta: @@ -292,9 +296,12 @@ def negamax( best_move = None best_score = NEG_INF - moves = organize_moves(board) + ply_killers = killers[ply] if killers and ply < len(killers) else None + moves = organize_moves(board, ply_killers) for move in moves: + is_capture = board.is_capture(move) + # make the move board.push(move) @@ -305,6 +312,8 @@ def negamax( cache, -beta, -alpha, + ply + 1, + killers, )[0] if board_score > self.config.checkmate_threshold: board_score -= 1 @@ -321,6 +330,18 @@ def negamax( # beta-cutoff: opponent won't allow this position if best_score >= beta: + # Update killer moves for quiet moves that cause beta cutoff + # Add to killers if not already there (keep 2 killers per ply) + if ( + killers + and not is_capture + and ply < len(killers) + and move not in killers[ply] + ): + killers[ply].insert(0, move) + if len(killers[ply]) > 2: + killers[ply].pop() + # LOWER_BOUND: true score is at least best_score cache[cache_key] = (best_score, best_move, Bound.LOWER_BOUND, depth) return best_score, best_move @@ -348,8 +369,18 @@ def search_move(self, board: Board) -> Move: # create shared cache cache: CACHE_TYPE = {} + # Killer moves table: 2 killers per ply + # Max ply is roughly target_depth + quiescence_depth + some buffer + max_ply = self.config.negamax_depth + self.config.quiescence_search_depth + 10 + killers: list = [[] for _ in range(max_ply)] + best_move = self.negamax( - board, self.config.negamax_depth, self.config.null_move, cache + board, + self.config.negamax_depth, + self.config.null_move, + cache, + ply=0, + killers=killers, )[1] assert best_move is not None, "Best move from root should not be None" return best_move diff --git a/moonfish/move_ordering.py b/moonfish/move_ordering.py index 630ec81..546a1d9 100644 --- a/moonfish/move_ordering.py +++ b/moonfish/move_ordering.py @@ -1,25 +1,28 @@ import random +from typing import List, Optional from chess import BLACK, Board, Move from moonfish.psqt import evaluate_capture, evaluate_piece, get_phase -def organize_moves(board: Board): +def organize_moves(board: Board, killers: Optional[List[Move]] = None): """ This function receives a board and it returns a list of all the possible moves for the current player, sorted by importance. - It sends capturing moves at the starting positions in - the array (to try to increase pruning and do so earlier). + + Order: captures (sorted by MVV-LVA) -> killer moves -> other quiet moves Arguments: - board: chess board state + - killers: optional list of killer moves for this ply Returns: - legal_moves: list of all the possible moves for the current player. """ non_captures = [] captures = [] + phase = get_phase(board) for move in board.legal_moves: if board.is_capture(move): @@ -27,8 +30,25 @@ def organize_moves(board: Board): else: non_captures.append(move) - random.shuffle(captures) + # Sort captures by MVV-LVA (best captures first) + captures.sort( + key=lambda move: mvv_lva(board, move, phase), reverse=(board.turn != BLACK) + ) + + # Shuffle non-captures for variety, then we'll extract killers random.shuffle(non_captures) + + # Extract killer moves from non-captures and put them first + if killers: + killer_moves = [] + remaining_quiet = [] + for move in non_captures: + if move in killers: + killer_moves.append(move) + else: + remaining_quiet.append(move) + non_captures = killer_moves + remaining_quiet + return captures + non_captures From 947d2a8b0a316829f4e28f696f4286c6050a09cc Mon Sep 17 00:00:00 2001 From: Lucca Bertoncini Date: Tue, 27 Jan 2026 11:27:33 -0800 Subject: [PATCH 2/6] Trigger benchmark CI From 613e503c0df791d808892903c1248fcd5ad8bc23 Mon Sep 17 00:00:00 2001 From: Lucca Bertoncini Date: Tue, 27 Jan 2026 11:28:02 -0800 Subject: [PATCH 3/6] Trigger benchmark workflow --- moonfish/move_ordering.py | 1 + 1 file changed, 1 insertion(+) diff --git a/moonfish/move_ordering.py b/moonfish/move_ordering.py index 546a1d9..2a61efd 100644 --- a/moonfish/move_ordering.py +++ b/moonfish/move_ordering.py @@ -125,3 +125,4 @@ def mvv_lva(board: Board, move: Move, phase: float) -> float: move_value += evaluate_capture(board, move, phase) return -move_value if board.turn else move_value + From a0730aa9346e3324a8d494f4d59fa23c0fe712cc Mon Sep 17 00:00:00 2001 From: Lucca Bertoncini Date: Tue, 27 Jan 2026 11:33:19 -0800 Subject: [PATCH 4/6] Trigger benchmark --- moonfish/engines/alpha_beta.py | 1 + 1 file changed, 1 insertion(+) diff --git a/moonfish/engines/alpha_beta.py b/moonfish/engines/alpha_beta.py index 48498b4..c42ce79 100644 --- a/moonfish/engines/alpha_beta.py +++ b/moonfish/engines/alpha_beta.py @@ -384,3 +384,4 @@ def search_move(self, board: Board) -> Move: )[1] assert best_move is not None, "Best move from root should not be None" return best_move +# Trigger From 1d986c8cf536b8cb2121abfcb4da19ce93c194b7 Mon Sep 17 00:00:00 2001 From: Lucca Bertoncini Date: Tue, 27 Jan 2026 11:34:24 -0800 Subject: [PATCH 5/6] Retrigger benchmark workflow --- moonfish/helper.py | 1 + 1 file changed, 1 insertion(+) diff --git a/moonfish/helper.py b/moonfish/helper.py index d7fd04d..c88f818 100644 --- a/moonfish/helper.py +++ b/moonfish/helper.py @@ -86,3 +86,4 @@ def find_best_move(board: Board, engine: ChessEngine) -> Move: except (ValueError, OSError, AttributeError, IndexError): best_move = engine.search_move(board) return best_move +# Trigger benchmark 1769542464 From e73ebd6971db787cf38b671a62cb676dcac2e222 Mon Sep 17 00:00:00 2001 From: Lucca Bertoncini Date: Tue, 27 Jan 2026 11:35:48 -0800 Subject: [PATCH 6/6] Clean up benchmark trigger commits --- moonfish/engines/alpha_beta.py | 1 - moonfish/helper.py | 1 - moonfish/move_ordering.py | 1 - 3 files changed, 3 deletions(-) diff --git a/moonfish/engines/alpha_beta.py b/moonfish/engines/alpha_beta.py index c42ce79..48498b4 100644 --- a/moonfish/engines/alpha_beta.py +++ b/moonfish/engines/alpha_beta.py @@ -384,4 +384,3 @@ def search_move(self, board: Board) -> Move: )[1] assert best_move is not None, "Best move from root should not be None" return best_move -# Trigger diff --git a/moonfish/helper.py b/moonfish/helper.py index c88f818..d7fd04d 100644 --- a/moonfish/helper.py +++ b/moonfish/helper.py @@ -86,4 +86,3 @@ def find_best_move(board: Board, engine: ChessEngine) -> Move: except (ValueError, OSError, AttributeError, IndexError): best_move = engine.search_move(board) return best_move -# Trigger benchmark 1769542464 diff --git a/moonfish/move_ordering.py b/moonfish/move_ordering.py index 2a61efd..546a1d9 100644 --- a/moonfish/move_ordering.py +++ b/moonfish/move_ordering.py @@ -125,4 +125,3 @@ def mvv_lva(board: Board, move: Move, phase: float) -> float: move_value += evaluate_capture(board, move, phase) return -move_value if board.turn else move_value -