class Reversi: """This class enforces the rules of the game of Reversi.""" def __init__(self, size, BlackAgent=RandomAgent, WhiteAgent=RandomAgent, **kwargs): self.size = size self.board = Board(self.size) self.board.init_starting_position() # game state is a 2-tuple of the board state, and which player's turn # it is. self.game_state = (self.board, BLACK) self.legal_cache = CacheDict() black_time = kwargs.get('black_time', 5) white_time = kwargs.get('white_time', 5) self.white_agent = WhiteAgent(self, WHITE, time=white_time, **kwargs) self.black_agent = BlackAgent(self, BLACK, time=black_time, **kwargs) # storing legal moves allows us to avoid needlessly recalculating them self.legal_white_moves = [] self.legal_black_moves = [] def play_game(self): self.update_legal_moves(self.get_state()) while not self.is_won(): self.print_board() game_state = self.get_state() turn_color = game_state[1] self.update_legal_moves(game_state) legal_moves = None if turn_color == BLACK: legal_moves = self.legal_black_moves elif turn_color == WHITE: legal_moves = self.legal_white_moves else: raise ValueError if not legal_moves: print('{} had no moves, and passed their turn.'.format( color_name[turn_color])) self.game_state = (game_state[0], opponent[turn_color]) continue else: picked = self.agent_pick_move(game_state, legal_moves) print('{} plays at {}'.format( color_name[turn_color], str(picked))) updated_game_state = self.apply_move( game_state, picked[0], picked[1]) self.game_state = updated_game_state self.print_board() # figure out who won black_count, white_count = self.board.get_stone_counts() winner = BLACK if black_count > white_count else WHITE return winner, white_count, black_count def print_board(self): print(str(self.get_board())) def agent_pick_move(self, game_state, legal_moves): picked = None color = game_state[1] while picked not in legal_moves: if color == WHITE: picked = self.white_agent.get_action( self.get_state(), legal_moves) elif color == BLACK: picked = self.black_agent.get_action( self.get_state(), legal_moves) else: raise ValueError if picked is None: return None elif picked not in legal_moves: print(str(picked) + ' is not a legal move!') return picked def legal_moves(self, game_state, force_cache=False): # Note: this is a very naive and inefficient way to find # all available moves by brute force. I am sure there is a # more clever way to do this. If you want better performance # from agents, this would probably be the first area to improve. if force_cache: return self.legal_cache.get(game_state) board = game_state[0] if board.is_full(): return [] cached = self.legal_cache.get(game_state) if cached is not None: return cached board_size = board.get_size() moves = [] # list of x,y positions valid for color for y in range(board_size): for x in range(board_size): if self.is_valid_move(game_state, x, y): moves.append((x, y)) self.legal_cache.update(game_state, moves) return moves @staticmethod def is_valid_move(game_state, x, y): board, color = game_state piece = board.board[y][x] if piece != EMPTY: return False enemy = opponent[color] # now check in all directions, including diagonal for dy in range(-1, 2): for dx in range(-1, 2): if dy == 0 and dx == 0: continue # there needs to be >= 1 opponent piece # in this given direction, followed by 1 of player's piece distance = 1 yp = (distance * dy) + y xp = (distance * dx) + x while board.is_in_bounds(xp, yp) and board.board[yp][xp] == enemy: distance += 1 yp = (distance * dy) + y xp = (distance * dx) + x if distance > 1 and board.is_in_bounds(xp, yp) and board.board[yp][xp] == color: return True return False def next_state(self, game_state, x, y): """Given a game_state and a position for a new piece, return a new game_state reflecting the change. Does not modify the input game_state.""" game_state_copy = copy.deepcopy(game_state) result = self.apply_move(game_state_copy, x, y) return result @staticmethod def apply_move(game_state, x, y): """Given a game_state (which includes info about whose turn it is) and an x,y position to place a piece, transform it into the game_state that follows this play.""" color = game_state[1] board = game_state[0] board.place_stone_at(color, x, y) # now flip all the stones in every direction opponent = BLACK if color == BLACK: opponent = WHITE # now check in all directions, including diagonal to_flip = [] for dy in range(-1, 2): for dx in range(-1, 2): if dy == 0 and dx == 0: continue # there needs to be >= 1 opponent piece # in this given direction, followed by 1 of player's piece distance = 1 yp = (distance * dy) + y xp = (distance * dx) + x flip_candidates = [] while board.is_in_bounds(xp, yp) and board.board[yp][xp] == opponent: flip_candidates.append((xp, yp)) distance += 1 yp = (distance * dy) + y xp = (distance * dx) + x if distance > 1 and board.is_in_bounds(xp, yp) and board.board[yp][xp] == color: to_flip.extend(flip_candidates) for each in to_flip: board.flip_stone(each[0], each[1]) # board.place_stone_at(color, each[0], each[1]) if game_state[1] == WHITE: game_state = (game_state[0], BLACK) elif game_state[1] == BLACK: game_state = (game_state[0], WHITE) return game_state def is_won(self): """The game is over when neither player can make a move.""" return self.get_board().is_full() or (len(self.legal_black_moves) == 0 and len(self.legal_white_moves) == 0) def winner(self, game_state): """Given a game_state, return the color of the winner if there is one, otherwise return False to indicate the game isn't won yet. Note that legal_moves() is a slow operation, so this method tries to call it as few times as possible.""" board = game_state[0] black_count, white_count = board.get_stone_counts() # a full board means no more moves can be made, game over. if board.is_full(): if black_count > white_count: return BLACK else: # tie goes to white return WHITE # a non-full board can still be game-over if neither player can move. black_legal = self.legal_moves((game_state[0], BLACK)) if black_legal: return False white_legal = self.legal_moves((game_state[0], WHITE)) if white_legal: return False # neither black nor white has valid moves if black_count > white_count: return BLACK else: # tie goes to white return WHITE def update_legal_moves(self, game_state): legal_moves = self.legal_moves(game_state) color = game_state[1] if color == WHITE: self.legal_white_moves = legal_moves elif color == BLACK: self.legal_black_moves = legal_moves else: raise ValueError def get_board(self): """Return the board from the current game_state.""" return self.game_state[0] def get_state(self): """Returns a tuple representing the board state.""" return self.game_state def __str__(self): return str(self.board)