def set_position(self, lag: List[str], starting_fen=chess.STARTING_FEN): """ Sets the internal board state to that specified by the series of long algebraic notation moves given (starting from the specified start position). :param lag: List of long algebraic notation moves :param starting_fen: The FEN to start the board at :return: None """ self.board = SearchBoard(fen=starting_fen) for move in lag: self.board.push(chess.Move.from_uci(move))
def _zugzwang(board: SearchBoard) -> bool: """ Detects so called zugzwang positions, wherein not moving is actually more benificial than moving (often happens in the end game when trying to mate the king). Note that this method is only a heuristic and technically doesn't cover every case. :param board: Board to check :return: True if zugzwang, False otherwise """ # TODO improve zugzwang detection (see brucemo.com) return board.pieces_mask(chess.KNIGHT, board.turn) == 0 and \ board.pieces_mask(chess.BISHOP, board.turn) == 0 and \ board.pieces_mask(chess.ROOK, board.turn) == 0 and \ board.pieces_mask(chess.QUEEN, board.turn) == 0
def _compute_passed_isolated_pawns( self, board: SearchBoard, piece_list: PieceList) -> Tuple[int, int, int, int]: """ Computes the score for white and black passed and isolated pawns. :param board: Board to compute on :param piece_list: Piece list :return: white passed pawn score, black, white isolated pawn score, black """ white_pawns = board.pieces(chess.PAWN, chess.WHITE).mask black_pawns = board.pieces(chess.PAWN, chess.BLACK).mask white_passed, black_passed = 0, 0 white_isolated, black_isolated = 0, 0 for sq in piece_list.square_list(chess.PAWN, chess.WHITE): # Pawn present and it is passed (note that pawn is guaranteed to be here) if not self._white_passed_pawns_bbs[sq] & black_pawns: white_passed += self.pawn_passed[sq // 8] # Pawn present and is isolated if not self._isolated_pawns_bbs[sq] & white_pawns: white_isolated += self.pawn_isolated for sq in piece_list.square_list(chess.PAWN, chess.BLACK): if not self._black_passed_pawns_bbs[sq] & white_pawns: black_passed += self.pawn_passed[7 - (sq // 8)] if not self._isolated_pawns_bbs[sq] & black_pawns: black_isolated += self.pawn_isolated return white_passed, black_passed, white_isolated, black_isolated
def _probe_endgame_tablebase( self, board: SearchBoard) -> Tuple[Optional[chess.Move], Optional[int]]: with chess.gaviota.PythonTablebase() as tablebase: tablebase.add_directory(self.endgame_tablebase_filepath) curr_dtm = tablebase.get_dtm(board) # If playing for a stalemate then no point in probing if curr_dtm is not None and curr_dtm != 0: max_dtm = -1_000_000 max_dtm_move = None for move in board.legal_moves: board.push(move) outcome = board.outcome() considering_dtm = tablebase.get_dtm(board) no_draw = outcome is None or outcome.termination == chess.Termination.CHECKMATE mate_move = outcome is not None and outcome.termination == chess.Termination.CHECKMATE # The case where a DTM of 0 means that we are mating if 0 <= curr_dtm <= 3 and mate_move and considering_dtm == 0: board.pop() return move, considering_dtm else: # Otherwise, DTM of 0 means draw if curr_dtm > 0: optimizes = max_dtm < considering_dtm < 0 else: optimizes = considering_dtm > max_dtm and considering_dtm > 0 if considering_dtm is not None and no_draw and optimizes: max_dtm = considering_dtm max_dtm_move = move board.pop() return max_dtm_move, max_dtm return None, None
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 _count_piece_type(board: SearchBoard, piece_type: chess.PieceType) -> Tuple[int, int]: """ Counts how many occurrences of the given piece type are on the board. :param board: Board to consider :param piece_type: Piece type to count :return: The piece count for white, black """ return (count_bin_ones(board.pieces_mask(piece_type, chess.WHITE)), count_bin_ones(board.pieces_mask(piece_type, chess.BLACK)))
def test_boards(): with open(os.path.join("testing/mirror.epd"), "r", encoding="iso8859-1") as mirror: while True: line = mirror.readline() if line is None or line.strip() == "": break position_fen = line.split(" ")[0] fen = f"{position_fen} w KQkq - 0 1" board = SearchBoard(fen=fen) yield board
def _quiescence(self, alpha: int, beta: int, board: SearchBoard) -> int: self.search_info.nodes += 1 evaluation = self.evaluator.evaluate_board(board) if self._terminal_condition(board): return 0 score = evaluation.board_evaluation if board.searching_ply > self.max_depth - 1: return score if score >= beta: # Beta cutoff return beta if score > alpha: # Standing pattern alpha = score # From here, it is basically minimax alpha beta using only capture moves num_legal_moves = 0 for move in self.move_orderer(board, None, captures_only=True): if move in self.search_info.excluded_moves: continue num_legal_moves += 1 board.push(move) score = -self._quiescence(-beta, -alpha, board) board.pop() if self.search_info.stop: return 0 if score > alpha: if score >= beta: self.search_info.fail_high_first += 1 if num_legal_moves == 1 else 0 self.search_info.fail_high += 1 return beta alpha = score return alpha
def apply_heatmap_dynamic(self, board: SearchBoard, color: chess.Color) -> int: """ Applies a heatmap to a given board. This implementation dynamically locates pieces and thus it is slower than `apply_heatmap`, which should be used if possible. :param board: The board to apply the heatmap to :param color: The color to apply the heatmap to :return: The sum of the heatmap times 1 if a piece of the given color and `self.piece_type` """ piece_mask = board.pieces_mask(self.piece_type, color) heatmap = self._white_heatmap if color == chess.WHITE else self._black_heatmap return sum(v * (1 if piece_mask & (1 << i) else 0) for i, v in enumerate(heatmap))
def __init__(self, config_filepath: str, log_func: Callable[[str], None]): """ Creates an engine using the given searcher. :param config_filepath: File path to the config file :param log_func: The function to use to log data """ self.config_filepath = config_filepath self.board: SearchBoard = SearchBoard() with open(self.config_filepath, "r+") as config_f: self.config = json.load(config_f) self._parse_config() # Stop flag (for pondering) self._stop = False # Logging information self.log_func: Callable[[str], None] = log_func self._time_last_log: Optional[float] = None self._nodes_last_log: Optional[int] = None
def _compute_open_files(self, board: SearchBoard, piece_list: PieceList) -> Tuple[int, int]: """ Computes the score for occupying open files (for rooks and queens) :param board: Board to consider :param piece_list: Piece list of the board :return: White open file score, black open file score """ white_pawns = board.pieces(chess.PAWN, chess.WHITE).mask black_pawns = board.pieces(chess.PAWN, chess.BLACK).mask pawns = white_pawns | black_pawns open_file = lambda s: not (pawns & self._file_bbs[s % 8]) white_semiopen_file = lambda s: not (white_pawns & self._file_bbs[s % 8 ]) black_semiopen_file = lambda s: not (black_pawns & self._file_bbs[s % 8 ]) white, black = 0, 0 for sq in piece_list.square_list(chess.ROOK, chess.WHITE): if open_file(sq): white += self.rook_open_file elif white_semiopen_file(sq): white += self.rook_semiopen_file for sq in piece_list.square_list(chess.ROOK, chess.BLACK): if open_file(sq): black += self.rook_open_file elif black_semiopen_file(sq): black += self.rook_semiopen_file # Queens for sq in piece_list.square_list(chess.QUEEN, chess.WHITE): if open_file(sq): white += self.queen_open_file elif white_semiopen_file(sq): white += self.queen_semiopen_file for sq in piece_list.square_list(chess.QUEEN, chess.BLACK): if open_file(sq): black += self.queen_open_file elif black_semiopen_file(sq): black += self.queen_semiopen_file return white, black
def _init_search_info(self, board: SearchBoard = None, time_to_think: float = None, nodes: int = None) -> SearchBoard: """ Clears all relevant searching arrays and sets up the search information. :param board: The board to clear out too, if needed :param time_to_think: Time to think in seconds :param nodes: Number of nodes to search :return: The updated board, if passed in """ self.search_info = _SearchInfo(time_to_think=time_to_think, nodes=nodes) self.search_history: List[List[int]] = [[ 0 for s in range(len(chess.SQUARES)) ] for p in range(len(chess.SQUARES))] self.search_killers: List[List[chess.Move]] = [[ chess.Move.null() for d in range(self.max_depth) ] for _ in range(2)] if board: # Reset the board's information board.searching_ply = 0 return board
def evaluate_board(self, board: SearchBoard) -> BoardEvaluation: piece_list = board.generate_piece_list() # Pawns must also be checked if len(piece_list.square_list(chess.PAWN, chess.WHITE)) == 0 and \ len(piece_list.square_list(chess.PAWN, chess.BLACK)) == 0 and \ self._is_material_draw(piece_list): return BoardEvaluation(0, 0, board.turn, True, True, 0) # Compute all values (all in centipawns) white_materials, black_materials, num_men = self._compute_materials( self.piece_values, piece_list) white_positions = self._compute_positions(chess.WHITE, piece_list, white_materials, black_materials) black_positions = self._compute_positions(chess.BLACK, piece_list, white_materials, black_materials) passed_isolated = self._compute_passed_isolated_pawns( board, piece_list) white_passed, black_passed, white_isolated, black_isolated = passed_isolated white_open_files, black_open_files = self._compute_open_files( board, piece_list) white_bishop_pair = self.bishop_pair if len( piece_list.square_list(chess.BISHOP, chess.WHITE)) >= 2 else 0 black_bishop_pair = self.bishop_pair if len( piece_list.square_list(chess.BISHOP, chess.BLACK)) >= 2 else 0 # Sum it all up white = white_materials + white_positions + white_passed + white_isolated + white_open_files + white_bishop_pair black = black_materials + black_positions + black_passed + black_isolated + black_open_files + black_bishop_pair return BoardEvaluation( white, black, board.turn, self._is_endgame(chess.WHITE, white_materials, black_materials), self._is_endgame(chess.BLACK, white_materials, black_materials), num_men)
def search(self, board: SearchBoard, fixed_time: Optional[float], target_depth: int, target_nodes: Optional[int], on_depth_completed: Callable[[int, int, List[chess.Move], str], None] = None, log_func: Callable[[str], None] = None, use_opening_book: bool = True, use_endgame_tablebase: bool = True) -> List[chess.Move]: """ Performs iterative deepening minimax (negamax) search with alpha-beta pruning. Also handles checking opening and closing books. :param board: Board to search :param fixed_time: Time to use for the search, in seconds :param target_depth: Depth to search to :param target_nodes: Number of nodes to search :param on_depth_completed: A callback that is run after each successive depth is completed :param log_func: Logging function to use :param use_opening_book: Whether to use the opening book :param use_endgame_tablebase: Whether to use the endgame tablebase :return: The PV line found """ # Parse parameters if target_depth is None: target_depth = self.max_depth if log_func is None: log_func = lambda s: None board = self._init_search_info(board, time_to_think=fixed_time, nodes=target_nodes) # Check for opening book entry if use_opening_book: move = self._probe_opening_book(board) if move is not None: log_func("opening book hit") return [move] # Check endgame tables if use_endgame_tablebase: move, dtm = self._probe_endgame_tablebase(board) if move is not None: log_func(f"endgame table hit, DTM: {dtm}") return [move] # Clear the transposition table when there is a transition to endgame if len(board.move_stack) > 0: move = board.pop() prev_evaluation = self.evaluator.evaluate_board(board) board.push(move) evaluation = self.evaluator.evaluate_board(board) if evaluation.white_endgame != prev_evaluation.white_endgame or \ evaluation.black_endgame != prev_evaluation.black_endgame: board.clear_transposition_table() log_func( "transposition table cleared due to transition to endgame") # Set up the checkup daemon thread timer = call_repeatedly(0.5, self._checkup) # Iterative deepening prev_elapsed_time = 0 best_score = None max_depth = 0 err_log = dict() for curr_depth in range(1, target_depth + 1): st = time.time() self.search_info.curr_depth = curr_depth best_score = self._minimax_alpha_beta(-self.inf, self.inf, board, curr_depth, True) self._checkup() if self.search_info.stop: break max_depth = curr_depth ordering = self.search_info.fail_high_first / self.search_info.fail_high \ if self.search_info.fail_high > 0 else 0.0 # elapsed_time = time.time() - st # log_func(f"Depth {curr_depth} took time {elapsed_time}") # if curr_depth > 2: # err = abs(predicted_time_next_iteration - elapsed_time) # err_log[curr_depth] = err # if curr_depth > 1: # # Effective branching factor # time_ebf = elapsed_time / prev_elapsed_time # predicted_time_next_iteration = time_ebf * elapsed_time # log_func(f"Predicting that depth {curr_depth + 1} will take {round(predicted_time_next_iteration, 2)}s") # # # TODO finish up the timecontrol classes and delegate to that via callbacks # # # TODO this doesn't work when coming off of the transposition table, or maybe the error calculation doesn't work?? # # if self.search_info.end_time is not None: # # time_remaining = self.search_info.end_time - time.time() # # if time_remaining < 0.75 * predicted_time_next_iteration: # # break # # prev_elapsed_time = elapsed_time on_depth_completed(curr_depth, best_score, board.generate_pv_line(curr_depth)) # Stop the timer timer() log_func("Errors:") for d in range(1, target_depth + 1): if d in err_log: log_func(f"Depth {d}: {round(err_log[d], 2)}") return board.generate_pv_line(max_depth)
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
def _terminal_condition(board: SearchBoard) -> bool: return board.is_fifty_moves() or board.is_insufficient_material() or \ board.is_repetition(count=2) or board.is_repetition(count=4)
def new_game(self): """ Resets everything to be ready for a new game. :return: None """ self.board = SearchBoard()
class CoeusEngine: """ The engine largely just wraps the searcher to provide proper UCI protocol control and time control. This includes managing pondering. In order to use the engine, follow these steps: `engine.clear_mode()` `# set relevant engine parameters such as fixed_time, winc, etc.` `engine.set_mode(relevant mode booleans)` `engine.go(on_completed)` """ def __init__(self, config_filepath: str, log_func: Callable[[str], None]): """ Creates an engine using the given searcher. :param config_filepath: File path to the config file :param log_func: The function to use to log data """ self.config_filepath = config_filepath self.board: SearchBoard = SearchBoard() with open(self.config_filepath, "r+") as config_f: self.config = json.load(config_f) self._parse_config() # Stop flag (for pondering) self._stop = False # Logging information self.log_func: Callable[[str], None] = log_func self._time_last_log: Optional[float] = None self._nodes_last_log: Optional[int] = None def _parse_config(self): self.name = self.config["name"] self.version = self.config["version"] # Evaluator and searcher self.evaluator = evaluation.evaluator_from_config( self.config["evaluator_config"]) self.searcher = AlphaBetaMinimaxSearcher( self.config["searcher_config"], self.evaluator) # Ponder settings self._base_ponder_depth = self.config["base_ponder_depth"] self._ponder_pv_depth_offset = self.config["ponder_pv_depth_offset"] def _reset_logging(self): self._time_last_log = None self._nodes_last_log = None def _log_info(self): # Depth, nodes, and time elapsed_search_time_ms = int( 1000 * (time.time() - self.searcher.search_info.start_time)) self.log_func(f"info depth {self.searcher.search_info.curr_depth} " f"nodes {self.searcher.search_info.nodes} " f"time {elapsed_search_time_ms}") # Nodes per second if self._time_last_log and self._nodes_last_log: elapsed_time = time.time() - self._time_last_log nodes_since_last_log = self.searcher.search_info.nodes - self._nodes_last_log nps = nodes_since_last_log / elapsed_time self.log_func(f"info nps {nps}") self._time_last_log = time.time() self._nodes_last_log = self.searcher.search_info.nodes def _log_completed_depth(self, depth_completed: Optional[int], best_score: Optional[int], pv_line: Optional[List[chess.Move]], info_str: str = None): if depth_completed is not None and best_score is not None and pv_line is not None and len( pv_line) > 0: # Log the best score, depth, nodes, time, and PV line elapsed_search_time_ms = int( 1000 * (time.time() - self.searcher.search_info.start_time)) pv_line_str = " ".join(str(m) for m in pv_line) mate_in = self.searcher.mate_in(best_score) score_str = f"mate {mate_in}" if mate_in else f"cp {best_score}" self.log_func(f"info score {score_str} " f"depth {depth_completed} " f"nodes {self.searcher.search_info.nodes} " f"time {elapsed_search_time_ms} " f"pv {pv_line_str}") if info_str: self.log_func(f"info string {info_str}") def set_position(self, lag: List[str], starting_fen=chess.STARTING_FEN): """ Sets the internal board state to that specified by the series of long algebraic notation moves given (starting from the specified start position). :param lag: List of long algebraic notation moves :param starting_fen: The FEN to start the board at :return: None """ self.board = SearchBoard(fen=starting_fen) for move in lag: self.board.push(chess.Move.from_uci(move)) def new_game(self): """ Resets everything to be ready for a new game. :return: None """ self.board = SearchBoard() def stop(self): """ Stops the engine from the current search. :return: None """ self._stop = True self.searcher.search_info.stop = True def go(self, params: EngineGoParams, on_completed: Callable[[List[chess.Move]], Any] = None, log_time_quantum: float = 0.25, ponder: bool = False) -> List[chess.Move]: """ Finds the best move considering the time constraints and returns it. :param params: All parameters associated with a call to go, including the proper mode information :param on_completed: The completion callback (argument is the output) :param log_time_quantum: The time quantum that searching info will be logged every, in seconds :param ponder: Whether to ponder :return: The principal variation line found """ logger.debug(f"Searching from board:\n") logger.debug(f"\n{str(self.board)}") logger.debug(f"FEN: {self.board.fen()}") logger.debug(f"mode={params.mode}") self.log_func( f"info string {self.board.transposition_table_size()} transposition table entries" ) if params.mode == _TimeMode.INFINITE or ponder: fixed_time = None depth = None nodes = None elif params.mode == _TimeMode.TIME_CONTROL: moves_to_go = 30 if params.moves_to_go is None else params.moves_to_go time_left = params.wtime if self.board.turn == chess.WHITE else params.btime inc = params.winc if self.board.turn == chess.WHITE else params.binc if inc is None: inc = 0 # Time control calculation fixed_time = ((time_left // moves_to_go) + inc) / 1_000 depth = self.searcher.max_depth nodes = None elif params.mode == _TimeMode.FIXED_TIME: fixed_time = params.fixed_time / 1_000 depth = self.searcher.max_depth nodes = None elif params.mode == _TimeMode.DEPTH: fixed_time = None depth = params.target_depth nodes = None elif params.mode == _TimeMode.NODES: fixed_time = None depth = None nodes = params.target_nodes else: raise ValueError("The mode is not set properly!") # Set a separate timer to log information self._reset_logging() if ponder and abs(log_time_quantum - 0.25) < 1e-3: log_time_quantum = 10.0 timer = call_repeatedly(log_time_quantum, self._log_info) if ponder: # In ponder mode, use a kind of iterative deepening, namely: # - Search to a base depth plus an offset for the opponent's PV move # - Search to a base depth for all other of the opponent's moves # - Increase the base depth and repeat pondering_board: SearchBoard = self.board.copy_transposition_table_referenced( ) # Recall that this PV line is dynamically updated so we simply take the 1st element pv_line = pondering_board.generate_pv_line(depth=1) # Note that in theory the PV line could be messed up so verify that the entry is actual a legal move suspected_opponent_pv_move = pv_line[0] if len( pv_line) > 0 else None opponent_pv_move = None other_opponent_moves = [] for move in pondering_board.legal_moves: if suspected_opponent_pv_move == move: opponent_pv_move = suspected_opponent_pv_move else: other_opponent_moves.append(move) ponder_depth = self._base_ponder_depth kwargs = { "on_depth_completed": self._log_completed_depth, "log_func": lambda s: self.log_func(f"info string {s}"), "use_opening_book": False, "use_endgame_tablebase": False } while not self._stop and ponder_depth + self._ponder_pv_depth_offset < self.searcher.max_depth: if opponent_pv_move: if self._stop: self.stop() break self._reset_logging() pondering_board.push(opponent_pv_move) self.searcher.search( pondering_board, None, ponder_depth + self._ponder_pv_depth_offset, None, **kwargs) pondering_board.pop() for move in other_opponent_moves: if self._stop: self.stop() break self._reset_logging() pondering_board.push(move) self.searcher.search(pondering_board, None, ponder_depth, None, **kwargs) pondering_board.pop() ponder_depth += 1 # Simply set for returning pv_line = None else: pv_line = self.searcher.search( self.board, fixed_time, depth, nodes, on_depth_completed=self._log_completed_depth, log_func=lambda s: self.log_func(f"info string {s}"), use_opening_book=params.use_opening_book, use_endgame_tablebase=params.use_endgame_tablebase) # Stop the logging timer timer() # Reset stop flag self._stop = False if on_completed: on_completed(pv_line) return pv_line