Beispiel #1
0
def test_pawn_stopped_from_advancing_two_ranks_by_knight():
    board = chess.Board(
        "q3kb2/1pp2pp1/4p1Pr/1n1Q3p/P2P1P2/1n2P1Nb/1PP2K2/1RB2BR1 w - - 0 28"
    )
    assert simulate_move(board, chess.Move.from_uci("b2b4")) == (
        chess.Move.null(),
        None,
    )
Beispiel #2
0
def certain_win(boards: List[chess.Board]) -> Optional[chess.Move]:
    for requested_move in move_actions(boards[0]):
        for board in boards:
            op_king_square = board.king(not board.turn)
            if requested_move.to_square != op_king_square:
                break  # this can't possibly be a king-capture
            _, capture_square = simulate_move(board, requested_move)
            if capture_square != op_king_square:
                break  # this isn't a king-capture
        else:
            return requested_move
Beispiel #3
0
 def op_move(self, capture_square: Optional[chess.Square]):
     new_boards = {}
     for board in self.boards:
         for requested_move in possible_requested_moves(board):
             taken_move, simulated_capture_square = simulate_move(
                 board, requested_move
             )
             if simulated_capture_square == capture_square:
                 new_board = board.copy(stack=False)
                 new_board.push(taken_move)
                 new_boards[board_fingerprint(new_board)] = new_board
     self.boards = list(new_boards.values())
Beispiel #4
0
 def move(
     self,
     requested_move: chess.Move,
     taken_move: chess.Move,
     capture_square: Optional[chess.Square],
 ):
     self.boards = [
         board
         for board in self.boards
         if simulate_move(board, requested_move) == (taken_move, capture_square)
     ]
     for board in self.boards:
         board.push(taken_move)
Beispiel #5
0
 async def move(
     self,
     requested_move: chess.Move,
     taken_move: chess.Move,
     capture_square: Optional[chess.Square],
 ):
     new_boards = []
     for board in self.boards:
         if simulate_move(board,
                          requested_move) == (taken_move, capture_square):
             board.push(taken_move)
             new_boards.append(board)
         await asyncio.sleep(0)
     self.boards = new_boards
Beispiel #6
0
def test_simulate_move(
    move_history: str,
    requested_move: str,
    expected_taken_move: str,
    expected_capture_square: Optional[str],
):
    board = chess.Board()
    for move in move_history.split():
        move = chess.Move.from_uci(move)
        assert move == chess.Move.null() or board.is_pseudo_legal(
            move
        ), f"Move history is invalid! Move {move} is not pseudo-legal on board\n{board}"
        board.push(move)
    assert simulate_move(board, chess.Move.from_uci(requested_move)) == (
        chess.Move.from_uci(expected_taken_move),
        None
        if expected_capture_square is None
        else chess.parse_square(expected_capture_square),
    )
Beispiel #7
0
def vote(possible_requested_moves, boards, engine):
    # This is just one of the many ways to aggregate the perfect-information recommendations over
    # each possible board into a move decision. Additionally, the general approach of aggregating
    # recommendations over MHT hypotheses is not necessarily the best strategy.
    # Imperfect-information-native approaches that don't involve MHT (like
    # counterfactual-regret-minimization) should theoretically outperform MHT approaches, though
    # through the first two competitions MHT bots such as StrangeFish and Oracle continue to be top
    # performers. (Also, MHT can be helpful in CFR and similar approaches, for example by
    # identifying dominated actions.)
    #
    # In this function, we choose a move to request by letting stockfish place votes over a random
    # subset of the MHT boards and selecting a winner by ranked-choice-voting. We must separately
    # handle unusual board configurations (i.e. the opponent king can be captured, or we are in
    # checkmate when it is our turn to move) and we let stockfish rank move options on the rest.
    # Because sometimes multiple move requests would be amended to the same taken move, we allow
    # choices to have a tied rank and nominate all such move requests with the same rank as the
    # taken move suggested by stockfish. In the unusual case where we have multiple options for
    # capturing the opponent king, this also gives us a way to nominate all those options as equal
    # first choices.
    votes = []
    random.shuffle(boards)
    for board in tqdm(boards[:1200]):
        my_ranked_votes = []
        votes.append(my_ranked_votes)
        # All requested moves that result in the voted-for taken moves are counted equally.
        move_lookup = defaultdict(list)
        for requested_move in possible_requested_moves:
            taken_move, _ = simulate_move(board, requested_move)
            move_lookup[taken_move].append(requested_move)
        # Boards where the king can be captured cannot be scored by stockfish.
        # Instead, vote equally for all possible king capture moves.
        op_king_square = board.king(not board.turn)
        king_attackers = board.attackers(board.turn, op_king_square)
        if king_attackers:
            my_ranked_votes.append([])
            for attacker in king_attackers:
                taken_move = chess.Move(attacker, op_king_square)
                requested_moves = move_lookup[taken_move]
                my_ranked_votes[0] += requested_moves
        else:
            board.clear_stack()
            results = engine.analyse(board,
                                     limit=chess.engine.Limit(depth=8),
                                     multipv=4)
            for result in results:
                try:
                    taken_move = result["pv"][0]
                    my_ranked_votes.append(move_lookup[taken_move])
                except KeyError:
                    pass  # No moves were suggested because we are in checkmate on this board.
    # Ranked-choice-voting is an iterative algorithm that scores candidates by the number of
    # first-choice votes they receive. If a candidate receives a majority, it is selected.
    # Otherwise, the lowest-scoring candidate is eliminated and the process repeats. Because this
    # version allows tied ranking, the total number of votes can exceed the number of voters.
    while True:
        if not votes:
            return chess.Move.null()
        threshold = len(votes) // 2
        first_choice_votes = defaultdict(int)
        for vote in votes:
            for move in vote[0]:
                first_choice_votes[move] += 1
        max_move, max_num_votes = max(first_choice_votes.items(),
                                      key=lambda x: x[1])
        if max_num_votes >= threshold:
            return max_move
        min_move, min_num_votes = min(first_choice_votes.items(),
                                      key=lambda x: x[1])
        revised_votes = []
        for vote in votes:
            revised_vote = []
            for group in vote:
                revised_group = [move for move in group if move != min_move]
                if revised_group:
                    revised_vote.append(revised_group)
            if revised_vote:
                revised_votes.append(revised_vote)
        votes = revised_votes
Beispiel #8
0
    def __init__(self, history_string: str):

        pygame.init()
        pygame.display.set_caption("Reconchess MHT Replay")
        pygame.display.set_icon(
            pygame.transform.scale(
                PIECE_IMAGES[chess.Piece(chess.KING, chess.WHITE)], (32, 32)))

        self.header_font = pygame.font.SysFont(pygame.font.get_default_font(),
                                               28)
        self.body_font = pygame.font.SysFont(pygame.font.get_default_font(),
                                             20)
        self.body_spacing = 20
        self.board_font = pygame.font.SysFont(pygame.font.get_default_font(),
                                              20)

        self.background_color = (45, 48, 50)
        self.header_color = (250, 250, 250)
        self.body_color = (160, 160, 160)

        self.square_size = 40
        self.margin = 10
        self.board_size = self.square_size * 8
        self.width = self.square_size * 16 + self.margin * 3
        self.height = self.width
        self.screen = pygame.display.set_mode((self.width, self.height))
        self.screen.fill(self.background_color)

        self.action_index = 0

        self.history = tuple(history_string.strip().lower().split())
        self.history_string = " ".join(
            self.history)  # to clean up possible multiple spaces
        self.num_actions = len(self.history)
        self.num_moves = self.num_actions // 2
        self.num_moves_by_white = (self.num_moves + 1) // 2
        self.num_moves_by_black = self.num_moves // 2

        board = chess.Board()
        self.views: List[View] = [
            View(board, self.board_font, self.board_size)
        ]

        # Compute the true board states synchronously since that is fast
        history_iter = iter(self.history)
        try:
            while True:
                # Sense step
                next(history_iter)
                # Move step
                requested_move = chess.Move.from_uci(next(history_iter))
                taken_move, capture_square = simulate_move(
                    board, requested_move)
                board.push(taken_move)
                self.views.append(View(board, self.board_font,
                                       self.board_size))

        except StopIteration:
            pass

        self.winner = not board.turn
        self.win_reason = "timeout" if board.king(
            board.turn) else "king capture"
Beispiel #9
0
    async def update_mht(self):
        history_iter = iter(self.history)
        board = chess.Board()
        active, waiting = AsyncMultiHypothesisTracker(
        ), AsyncMultiHypothesisTracker()
        turn_index = 0
        num_boards = [1, 1]

        requested_move = (
            taken_move) = capture_square = piece_moved = piece_captured = None
        x = y = 10
        view = self.views[0]

        surface_sense = pygame.Surface([self.square_size * 3] * 2,
                                       pygame.SRCALPHA)
        surface_sense.fill((205, 205, 255, 85))

        surface_capture = pygame.Surface([self.square_size] * 2,
                                         pygame.SRCALPHA)
        surface_capture.fill((255, 0, 0, 50))

        # TODO: Split white and black MHT views in case one explodes
        try:
            while True:
                view = self.views[turn_index]
                if board.turn == chess.WHITE:
                    view.surface_white = draw_boards(active.boards,
                                                     self.board_size,
                                                     self.board_font)
                    view.surface_black = draw_boards(waiting.boards,
                                                     self.board_size,
                                                     self.board_font)
                else:
                    view.surface_white = draw_boards(waiting.boards,
                                                     self.board_size,
                                                     self.board_font)
                    view.surface_black = draw_boards(active.boards,
                                                     self.board_size,
                                                     self.board_font)
                # Shade capture square
                if capture_square is not None:
                    x = self.square_size * chess.square_file(capture_square)
                    y = self.board_size - self.square_size * (
                        chess.square_rank(capture_square) + 1)
                    view.surface_true.blit(surface_capture, (x, y))
                    view.surface_white.blit(surface_capture, (x, y))
                    view.surface_black.blit(surface_capture, (x, y))
                view.updated_at = time.monotonic()
                await asyncio.sleep(0)

                # Update info
                view.surface_info.fill(self.background_color)
                if turn_index == 0:
                    info = ["White to sense on turn 1"]
                else:
                    info = [
                        f"{chess.COLOR_NAMES[not board.turn].capitalize()} "
                        f"requested to move {chess.PIECE_NAMES[piece_moved.piece_type]} "
                        f"{requested_move} on turn "
                        f"{(turn_index - 1) // 2 + 1}, which",
                        f"    resulted in move {taken_move} and " +
                        (f"the capture of the {chess.PIECE_NAMES[piece_captured.piece_type]} at "
                         f"{chess.SQUARE_NAMES[capture_square]}"
                         if piece_captured else "no capture"),
                        f"# possible boards for {chess.COLOR_NAMES[not board.turn]}: "
                        f"{num_boards[not board.turn]:,.0f} -> {len(waiting.boards):,.0f} "
                        f"(Δ = {len(waiting.boards) - num_boards[not board.turn]:+,.0f})",
                        f"# possible boards for {chess.COLOR_NAMES[board.turn]}: "
                        f"{num_boards[board.turn]:,.0f} -> {len(active.boards):,.0f} "
                        f"(Δ = {len(active.boards) - num_boards[board.turn]:+,.0f})",
                        "",
                        f"{chess.COLOR_NAMES[board.turn].capitalize()} to sense on turn {turn_index // 2 + 1}",
                    ]
                x = y = 10
                for line in info:
                    view.surface_info.blit(
                        self.body_font.render(line, True, self.body_color),
                        (x, y))
                    y += 20
                num_boards[board.turn] = len(active.boards)
                num_boards[not board.turn] = len(waiting.boards)
                await asyncio.sleep(0)

                # Sense step
                square = next(history_iter)
                square = None if square == "00" else chess.parse_square(square)
                result = simulate_sense(board, square)
                await active.sense(square, result)
                view.surface_after_sense = draw_boards(active.boards,
                                                       self.board_size,
                                                       self.board_font)
                # Shade sensed squares
                if square is not None:
                    x = self.square_size * (chess.square_file(square) - 1)
                    y = self.board_size - self.square_size * (
                        chess.square_rank(square) + 2)
                    view.surface_after_sense.blit(surface_sense, (x, y))
                view.updated_at = time.monotonic()
                await asyncio.sleep(0)

                # Update info
                view.surface_info_after_sense.fill(self.background_color)
                info = [
                    f"{chess.COLOR_NAMES[board.turn].capitalize()} " +
                    (f"sensed at {chess.SQUARE_NAMES[square]}"
                     if square is not None else "did not sense") +
                    f" on turn {turn_index // 2 + 1}",
                    f"# possible boards for {chess.COLOR_NAMES[board.turn]}: "
                    f"{num_boards[board.turn]:,.0f} -> {len(active.boards):,.0f} "
                    f"(Δ = {len(active.boards) - num_boards[board.turn]:+,.0f})",
                    "",
                    f"{chess.COLOR_NAMES[board.turn].capitalize()} to move on turn {(turn_index + 1) // 2 + 1}",
                ]
                x = y = 10
                for line in info:
                    view.surface_info_after_sense.blit(
                        self.body_font.render(line, True, self.body_color),
                        (x, y))
                    y += 20
                num_boards[board.turn] = len(active.boards)
                await asyncio.sleep(0)

                # Move step
                requested_move = chess.Move.from_uci(next(history_iter))
                taken_move, capture_square = simulate_move(
                    board, requested_move)
                piece_moved = (board.piece_at(requested_move.from_square)
                               if requested_move else None)
                piece_captured = (None if capture_square is None else
                                  board.piece_at(capture_square))
                await active.move(requested_move, taken_move, capture_square)
                await waiting.op_move(capture_square)
                board.push(taken_move)
                turn_index += 1
                active, waiting = waiting, active

        except StopIteration:
            pass

        info = [
            "",
            f"{chess.COLOR_NAMES[self.winner].capitalize()} wins by {self.win_reason}!",
        ]
        for line in info:
            (view.surface_info_after_sense if self.num_actions %
             2 else view.surface_info).blit(
                 self.body_font.render(line, True, self.body_color), (x, y))
            y += 20