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, )
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
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())
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)
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
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), )
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
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"
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