import copy from tugo.gotypes import Player, Point from tugo.scoring import compute_game_result # tag::import_zobrist[] from tugo import zobrist # end::import_zobrist[] __all__ = [ 'Board', 'GameState', 'Move', ] class IllegalMoveError(Exception): pass # tag::fast_go_strings[] class GoString: def __init__(self, color, stones, liberties): self.color = color self.stones = frozenset(stones) self.liberties = frozenset(liberties) # <1> def without_liberty(self, point): # <2> new_liberties = self.liberties - set([point]) return GoString(self.color, self.stones, new_liberties) def with_liberty(self, point): new_liberties = self.liberties | set([point]) return GoString(self.color, self.stones, new_liberties) # <1> `stones` and `liberties` are now immutable `frozenset` instances # <2> The `without_liberty` methods replaces the previous `remove_liberty` method... # <3> ... and `with_liberty` replaces `add_liberty`. # end::fast_go_strings[] def merged_with(self, string): """Return a new string containing all stones in both strings.""" assert string.color == self.color combined_stones = self.stones | string.stones return GoString( self.color, combined_stones, (self.liberties | string.liberties) - combined_stones) @property def num_liberties(self): return len(self.liberties) def __eq__(self, other): return isinstance(other, GoString) and \ self.color == other.color and \ self.stones == other.stones and \ self.liberties == other.liberties def __deepcopy__(self, memodict={}): return GoString(self.color, self.stones, copy.deepcopy(self.liberties)) # tag::init_zobrist[] class Board: def __init__(self, num_rows, num_cols): self.num_rows = num_rows self.num_cols = num_cols self._grid = {} self._hash = zobrist.EMPTY_BOARD # end::init_zobrist[] def place_stone(self, player, point): assert self.is_on_grid(point) if self._grid.get(point) is not None: print('Illegal play on %s' % str(point)) assert self._grid.get(point) is None # 0. Examine the adjacent points. adjacent_same_color = [] adjacent_opposite_color = [] liberties = [] for neighbor in point.neighbors(): if not self.is_on_grid(neighbor): continue neighbor_string = self._grid.get(neighbor) if neighbor_string is None: liberties.append(neighbor) elif neighbor_string.color == player: if neighbor_string not in adjacent_same_color: adjacent_same_color.append(neighbor_string) else: if neighbor_string not in adjacent_opposite_color: adjacent_opposite_color.append(neighbor_string) new_string = GoString(player, [point], liberties) # tag::apply_zobrist[] new_string = GoString(player, [point], liberties) # <1> for same_color_string in adjacent_same_color: # <2> new_string = new_string.merged_with(same_color_string) for new_string_point in new_string.stones: self._grid[new_string_point] = new_string self._hash ^= zobrist.HASH_CODE[point, player] # <3> for other_color_string in adjacent_opposite_color: replacement = other_color_string.without_liberty(point) # <4> if replacement.num_liberties: self._replace_string(other_color_string.without_liberty(point)) else: self._remove_string(other_color_string) # <5> # <1> Until this line `place_stone` remains the same. # <2> You merge any adjacent strings of the same color. # <3> Next, you apply the hash code for this point and player # <4> Then you reduce liberties of any adjacent strings of the opposite color. # <5> If any opposite color strings now have zero liberties, remove them. # end::apply_zobrist[] # tag::unapply_zobrist[] def _replace_string(self, new_string): # <1> for point in new_string.stones: self._grid[point] = new_string def _remove_string(self, string): for point in string.stones: for neighbor in point.neighbors(): # <2> neighbor_string = self._grid.get(neighbor) if neighbor_string is None: continue if neighbor_string is not string: self._replace_string(neighbor_string.with_liberty(point)) self._grid[point] = None self._hash ^= zobrist.HASH_CODE[point, string.color] # <3> # <1> This new helper method updates our Go board grid. # <2> Removing a string can create liberties for other strings. # <3> With Zobrist hashing, you need to unapply the hash for this move. # end::unapply_zobrist[] def is_on_grid(self, point): return 1 <= point.row <= self.num_rows and \ 1 <= point.col <= self.num_cols def get(self, point): """Return the content of a point on the board. Returns None if the point is empty, or a Player if there is a stone on that point. """ string = self._grid.get(point) if string is None: return None return string.color def get_go_string(self, point): """Return the entire string of stones at a point. Returns None if the point is empty, or a GoString if there is a stone on that point. """ string = self._grid.get(point) if string is None: return None return string def __eq__(self, other): return isinstance(other, Board) and \ self.num_rows == other.num_rows and \ self.num_cols == other.num_cols and \ self._hash() == other._hash() def __deepcopy__(self, memodict={}): copied = Board(self.num_rows, self.num_cols) # Can do a shallow copy b/c the dictionary maps tuples # (immutable) to GoStrings (also immutable) copied._grid = copy.copy(self._grid) copied._hash = self._hash return copied # tag::return_zobrist[] def zobrist_hash(self): return self._hash # end::return_zobrist[] class Move: """Any action a player can play on a turn. Exactly one of is_play, is_pass, is_resign will be set. """ def __init__(self, point=None, is_pass=False, is_resign=False): assert (point is not None) ^ is_pass ^ is_resign self.point = point self.is_play = (self.point is not None) self.is_pass = is_pass self.is_resign = is_resign @classmethod def play(cls, point): """A move that places a stone on the board.""" return Move(point=point) @classmethod def pass_turn(cls): return Move(is_pass=True) @classmethod def resign(cls): return Move(is_resign=True) def __str__(self): if self.is_pass: return 'pass' if self.is_resign: return 'resign' return '(r %d, c %d)' % (self.point.row, self.point.col) # tag::init_state_zobrist[] class GameState: def __init__(self, board, next_player, previous, move): self.board = board self.next_player = next_player self.previous_state = previous if self.previous_state is None: self.previous_states = frozenset() else: self.previous_states = frozenset( previous.previous_states | {(previous.next_player, previous.board.zobrist_hash())}) self.last_move = move # end::init_state_zobrist[] def apply_move(self, move): """Return the new GameState after applying the move.""" if move.is_play: next_board = copy.deepcopy(self.board) next_board.place_stone(self.next_player, move.point) else: next_board = self.board return GameState(next_board, self.next_player.other, self, move) @classmethod def new_game(cls, board_size): if isinstance(board_size, int): board_size = (board_size, board_size) board = Board(*board_size) return GameState(board, Player.black, None, None) def is_move_self_capture(self, player, move): if not move.is_play: return False next_board = copy.deepcopy(self.board) next_board.place_stone(player, move.point) new_string = next_board.get_go_string(move.point) return new_string.num_liberties == 0 @property def situation(self): return (self.next_player, self.board) # tag::ko_zobrist[] def does_move_violate_ko(self, player, move): if not move.is_play: return False next_board = copy.deepcopy(self.board) next_board.place_stone(player, move.point) next_situation = (player.other, next_board.zobrist_hash()) return next_situation in self.previous_states # end::ko_zobrist[] def is_valid_move(self, move): if self.is_over(): return False if move.is_pass or move.is_resign: return True return ( self.board.get(move.point) is None and not self.is_move_self_capture(self.next_player, move) and not self.does_move_violate_ko(self.next_player, move)) def is_over(self): if self.last_move is None: return False if self.last_move.is_resign: return True second_last_move = self.previous_state.last_move if second_last_move is None: return False return self.last_move.is_pass and second_last_move.is_pass def legal_moves(self): moves = [] for row in range(1, self.board.num_rows + 1): for col in range(1, self.board.num_cols + 1): move = Move.play(Point(row, col)) if self.is_valid_move(move): moves.append(move) # These two moves are always legal. moves.append(Move.pass_turn()) moves.append(Move.resign()) return moves def winner(self): if not self.is_over(): return None if self.last_move.is_resign: return self.next_player game_result = compute_game_result(self) return game_result.winner