Exemplo n.º 1
0
    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
Exemplo n.º 2
0
    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
Exemplo n.º 3
0
    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)
Exemplo n.º 4
0
    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