def generate_ordered_moves_v1(board: SearchBoard, pv_move: chess.Move, killer_moves: List[List[chess.Move]], search_history: List[List[int]], captures_only: bool = False) \ -> Generator[chess.Move, None, None]: """ Generates ordered moves using the following heuristics in this order: - principal variation move - most valuable victim, least valuable attacker captures (MVV LVA) - killer moves (beta cutoff but aren't captures) - search history (alpha cutoff but not beta cutoff and not a capture) :param board: The board to generate moves for :param pv_move: The principal variation move :param killer_moves: The killer moves table, a 2 x max_depth size array with the 0th index containing current killers and the 1st index containing next killers :param search_history: The search history table, a 64 x 64 size array that provides a heatmap of "relevant" (alpha cutoff meeting but not beta or capture) moves :param captures_only: True if only captures should be generated, False otherwise :return: """ scored_moves: List[_ScoredMove] = [] moves = board.generate_legal_captures( ) if captures_only else board.legal_moves for move in moves: if board.is_capture(move): attacker = board.piece_at(move.from_square).piece_type victim_piece = board.piece_at(move.to_square) if not victim_piece: # en passant victim = chess.PAWN else: victim = victim_piece.piece_type score = MVV_LVA_SCORES[victim][attacker] + 10_000_000 # "Quiet" moves elif killer_moves[0][ board.searching_ply] == move: # Current killer moves score = 9_000_000 elif killer_moves[1][board.searching_ply] == move: # Next killer moves score = 8_000_000 else: # Simply use search history heatmap score = search_history[move.from_square][move.to_square] # Principal variation should always be considered first if move == pv_move: score = 20_000_000 scored_moves.append(_ScoredMove(move, score)) return ordered_move_generator(scored_moves)
def _minimax_alpha_beta(self, alpha: int, beta: int, board: SearchBoard, depth: int, null_pruning: bool) -> int: # Check depth terminal condition if depth <= 0: return self._quiescence(alpha, beta, board) self.search_info.nodes += 1 if self._terminal_condition( board) and board.searching_ply > 0: # Stalemate/draw condition return 0 # Evaluate evaluation = self.evaluator.evaluate_board(board) if board.searching_ply > self.max_depth - 1: # Max depth return evaluation.board_evaluation # In-check test to "get out of check" in_check = board.is_check() depth += 1 if in_check else 0 # Prove transposition table and return early if we have a hit pv_move, score = board.probe_transposition_table( alpha, beta, depth, self.mate_value) if pv_move: return score # Null move pruning (must also verify that the side isn't in check and not in zugzwang scenario) if null_pruning and not in_check and board.searching_ply > 0 \ and not self._zugzwang(board) and depth >= self.reduction_factor + 1: board.push(chess.Move.null()) score = -self._minimax_alpha_beta( -beta, -beta + 1, board, depth - self.reduction_factor - 1, False) board.pop() if self.search_info.stop: return 0 # If the null move option improves on beta and isn't a mate then return it if score >= beta and abs(score) < self.mate_value: return beta old_alpha = alpha best_move = None best_score = -self.inf num_legal_moves = 0 found_pv = False for move in self.move_orderer(board, pv_move): if move in self.search_info.excluded_moves: continue num_legal_moves += 1 board.push(move) if found_pv: # PVS (principal variation search) score = -self._minimax_alpha_beta(-alpha - 1, -alpha, board, depth - 1, True) if alpha < score < beta: score = -self._minimax_alpha_beta(-beta, -alpha, board, depth - 1, True) else: score = -self._minimax_alpha_beta(-beta, -alpha, board, depth - 1, True) board.pop() if self.search_info.stop: return 0 capture = board.is_capture(move) if score > best_score: best_score = score best_move = move if score > alpha: # Alpha cutoff if score >= beta: # Beta cutoff self.search_info.fail_high_first += 1 if num_legal_moves == 1 else 0 self.search_info.fail_high += 1 if not capture: # Killer move (beta cutoff but not capture) self.search_killers[1][ board.searching_ply] = self.search_killers[0][ board.searching_ply] self.search_killers[0][board.searching_ply] = move board.store_transposition_table_entry( best_move, beta, depth, TranspositionTableFlag.BETA, self.mate_value) return beta found_pv = True alpha = score if not capture: # Alpha cutoff that isn't a capture self.search_history[best_move.from_square][ best_move.to_square] += depth if num_legal_moves == 0: # Checkmate cases if in_check: return -self.inf + board.searching_ply else: # Draw return 0 if alpha != old_alpha: # Principal variation board.store_transposition_table_entry(best_move, best_score, depth, TranspositionTableFlag.EXACT, self.mate_value) else: board.store_transposition_table_entry(best_move, alpha, depth, TranspositionTableFlag.ALPHA, self.mate_value) return alpha