diff --git a/moonfish/engines/alpha_beta.py b/moonfish/engines/alpha_beta.py index bc97538..1d6721e 100644 --- a/moonfish/engines/alpha_beta.py +++ b/moonfish/engines/alpha_beta.py @@ -15,6 +15,14 @@ NEG_INF = float("-inf") NULL_MOVE = Move.null() +# History table type: 64x64 array (from_square, to_square) -> score +HISTORY_TYPE = list[list[int]] + + +def new_history_table() -> HISTORY_TYPE: + """Create a fresh 64x64 history table initialized to zeros.""" + return [[0] * 64 for _ in range(64)] + class AlphaBeta: """ @@ -176,6 +184,7 @@ def negamax( cache: DictProxy | CACHE_KEY, alpha: float = NEG_INF, beta: float = INF, + history: HISTORY_TYPE | None = None, ) -> tuple[float | int, Move | None]: """ This functions receives a board, depth and a player; and it returns @@ -250,6 +259,7 @@ def negamax( cache, -beta, -beta + 1, + history, )[0] board.pop() if board_score >= beta: @@ -260,7 +270,7 @@ def negamax( # initializing best_score best_score = NEG_INF - moves = organize_moves(board) + moves = organize_moves(board, history) for move in moves: # make the move @@ -273,6 +283,7 @@ def negamax( cache, -beta, -alpha, + history, )[0] if board_score > self.config.checkmate_threshold: board_score -= 1 @@ -284,6 +295,9 @@ def negamax( # beta-cutoff if board_score >= beta: + # Update history heuristic for quiet moves that cause beta cutoff + if history is not None and not board.is_capture(move): + history[move.from_square][move.to_square] += depth * depth cache[cache_key] = (board_score, move) return board_score, move @@ -313,9 +327,13 @@ def search_move(self, board: Board) -> Move: self.nodes = 0 # create shared cache cache: CACHE_KEY = {} + # History table: tracks quiet moves that cause beta cutoffs + # Indexed by (from_square, to_square), higher = more likely to be good + history = new_history_table() best_move = self.negamax( - board, self.config.negamax_depth, self.config.null_move, cache + board, self.config.negamax_depth, self.config.null_move, cache, + history=history, )[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..0d87f8c 100644 --- a/moonfish/move_ordering.py +++ b/moonfish/move_ordering.py @@ -5,15 +5,17 @@ from moonfish.psqt import evaluate_capture, evaluate_piece, get_phase -def organize_moves(board: Board): +def organize_moves(board: Board, history: list[list[int]] | None = 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). + Quiet moves are sorted by history heuristic when available. Arguments: - board: chess board state + - history: optional 64x64 history table for quiet move ordering Returns: - legal_moves: list of all the possible moves for the current player. @@ -28,7 +30,16 @@ def organize_moves(board: Board): non_captures.append(move) random.shuffle(captures) - random.shuffle(non_captures) + + # Sort quiet moves by history heuristic if available + if history is not None: + non_captures.sort( + key=lambda m: history[m.from_square][m.to_square], + reverse=True, + ) + else: + random.shuffle(non_captures) + return captures + non_captures