from gomill import sgf # leftoffat: implementing navigation forwards and backward through the game. class Goban: """Represents the go board. Handles stone placement, captures, etc""" # enum values for the board array EMPTY='.' WHITE='w' BLACK='b' SCORE_BLACK='B' SCORE_WHITE='W' SCORE_DAME='d' SCORING='s' def __init__(self, board_size=19, file_name=None): # Build the board intersections self.board_size = board_size num_points = board_size * board_size self.board = [Goban.EMPTY] * num_points self.def_draw_codes = self._make_default_draw_codes() self.to_move = Goban.BLACK self.black_captures = 0 self.white_captures = 0 self.last_move = None self.passed_last = False self.ko = None self.hover = None self.elapsed_time = 0 self.winner = Goban.EMPTY self.move = 0 self.file_name = file_name # Track our game in an easily saveable format! self.sgf_game = None if self.file_name is not None: self.load_sgf(file_name) else: self.sgf_game = sgf.Sgf_game(self.board_size) def load_sgf(self, file_name): try: with open(file_name, 'r') as fn: self.sgf_game = sgf.Sgf_game.from_string(fn.read()) except IOError: # fixme - this should be convertable into a dialog box... perhaps it should throw an exception of its own print 'There was a problem loading the SGF file.' # Do initial layout self._place_initial_stones() for node in self.sgf_game.get_main_sequence(): self._play_node(node) if self.sgf_game.get_winner() is not None: self.winner = self.sgf_game.get_winner() self.to_move = None def _place_initial_stones(self): root = self.sgf_game.get_root() if root.has_setup_stones(): black, white, empty = root.get_setup_stones() for point in black: self.board[self._real_pos(self._sgf_to_move(point))] = Goban.BLACK for point in white: self.board[self._real_pos(self._sgf_to_move(point))] = Goban.WHITE for point in empty: self.board[self._real_pos(self._sgf_to_move(point))] = Goban.EMPTY # Play a single node from an sgf tree. Useful for warking the tree. def _play_node(self, node): color, pos = node.get_move() if color is not None: if pos is None: self.pass_move(color) else: self.play_move(self._sgf_to_move(pos), color, add_sgf=False) # fixme - add resignation detection # Play all moves up to and including node, which should be # an sgf.Tree_node def play_to_node(self, node): self.soft_reset() for n in self.sgf_game.get_main_sequence(): self._sgf_play_node(self, n) if n == node: return def play_to_move_n(self, n=None): ''' Play to the nth move. If n is None, play to the latest move ''' self.soft_reset() if n is None or len(self.sgf_game.get_main_sequence()) < n: n = len(self.sgf_game.get_main_sequence()) portion = self.sgf_game.get_main_sequence()[:n] for node in portion: self._play_node(node) def save_sgf(self, file_name=None): '''Saves the current game as an SGF file. If file_name is None, we use the previously specified filename. If there is no previously specified filename, we raise an exception.''' if self.file_name is None and file_name is not None: self.file_name = file_name if file_name is None: file_name = self.file_name if file_name is None: return # fixme - this should be an exception instead with open(file_name, 'w') as fn: fn.write(self.sgf_game.serialise()) def set_hover(self, pos): rpos = self._real_pos(pos) if rpos == self.hover: return if not self._valid_move(rpos) or self.to_move == Goban.EMPTY: self.clear_hover() return self.hover = rpos def soft_reset(self): """Reset the board to a pre-game state, but preserves move history. In other words, goes back to before move #1""" # Clear the board by setting it to the same size it currently is at self.set_board_size(self.board_size) self.to_move = Goban.BLACK self.black_captures = 0 self.white_captures = 0 self.last_move = None self.passed_last = False self.ko = None self.hover = None self.elapsed_time = 0 self.winner = Goban.EMPTY self.move = 0 def reset(self): '''Fully resets the game. The only thing preserved is the SGF filename, if it is set ''' self.sgf_game = sgf.Sgf_game(self.board_size) self.soft_reset() def set_board_size(self, new_size): """Set the board to a new size. This will also reset the board to a blank state, but will *not* reset captures, etc (call reset() first if you want that)""" self.board_size = new_size num_points = self.board_size * self.board_size self.board = [Goban.EMPTY] * num_points def clear_hover(self): self.hover = None def play_move(self, pos, color=None, add_sgf=True): ''' Plays a move pos: a tuple containing row and column to play on color: which color is playing. If not specified, the player whose turn it is is assumed add_sgf: if True, this is a new move and should be recorded in the move history return: To help with drawing code efficiency, any modified positions are returned in a list. This includes anything that the GUI may want to redraw in the wake of this move. ''' if add_sgf and self.move != len(self.sgf_game.get_main_sequence()): return if color is None: color = self.to_move if self.to_move == Goban.EMPTY: return None rpos = self._real_pos(pos) if not self._valid_move(rpos, color): return None self.board[rpos] = color self._changed = [] self._changed.append(rpos) if self.ko is not None: self._changed.append(self.ko) if self.last_move is not None: self._changed.append(self.last_move) self._capture(rpos) self.last_move = rpos self.passed_last = False if add_sgf: node = self.sgf_game.extend_main_sequence() node.set_move(color, self._pos_to_sgf(pos)) self.to_move = self._other_color(color) self.clear_hover() # If there is a new ko, send that back too if self.ko is not None: self._changed.append(self.ko) self.move += 1 return self._changed def pass_move(self, color=None): if color is None: color = self.to_move # If the game is over, fail silently if color is None: return self._changed = [] if self.ko is not None: self._changed.append(self.ko) if self.last_move is not None: self._changed.append(self.last_move) node = self.sgf_game.extend_main_sequence() node.set_move(color, None) if self.passed_last: self.to_move = Goban.EMPTY self.winner = Goban.SCORING self.auto_score() return range(len(self.board)) else: self.to_move = self._other_color(color) self.passed_last = True self.last_move = None self.ko = None return self._changed def resign(self, color=None): if color is None: color = self.to_move # If the game is over, fail silently if color is None: return self._changed = [] if self.ko is not None: self._changed.append(self.ko) if self.last_move is not None: self._changed.append(self.last_move) self.passed_last = False self.last_move = None self.ko = None self.winner = self._other_color(color) self.to_move = Goban.EMPTY return self._changed def _capture(self, pos, color=None): """Look for stones captured on the 4 sides of pos, remove them and increment capture counter. This pos must be a *real* position value, not an x,y tuple.""" if color is None: color = self.to_move # If we get here, we've definitely played a move, # clearing any existing ko point self.ko = None # Who are we capturing who = self._other_color(color) captures = 0 for p in self._neighbors(pos): if not self._on_board(p): continue if not self._num_liberties(p, who): captures += self._delete_group(p) if color == Goban.BLACK: self.black_captures += captures elif color == Goban.WHITE: self.white_captures += captures # Check for ko if captures == 1 and self._num_liberties(pos, color) == 1: # find the empty point for p in self._neighbors(pos): if self.board[p] == Goban.EMPTY: self.ko = p break def _valid_move(self, pos, color=None): if not self._on_board(pos): return False if color is None: color = self.to_move # Can't play atop another stone or on the ko point if self.board[pos] != Goban.EMPTY or pos == self.ko: return False # Temporarily place the stone self.board[pos] = color liberties = self._num_liberties(pos, color) opponent = self._other_color(color) kills_group = False for d in self._neighbors(pos): if not self._on_board(d): continue if self._num_liberties(d, opponent) == 0: kills_group = True break # Remove temporary stone self.board[pos] = Goban.EMPTY return liberties > 0 or kills_group # Recursively find whether there are liberties for the group # at pos. def _num_liberties(self, pos, who): if not self._on_board(pos) or self.board[pos] != who: return -1 bs = self.board_size * self.board_size checked = [False] * bs return self._num_liberties_r(pos, who, checked) def _num_liberties_r(self, pos, who, checked=None): if checked[pos]: return 0 else: checked[pos] = True square = self.board[pos] if square == Goban.EMPTY: return 1 elif square != who: return 0 else: liberties = 0 for d in self._neighbors(pos): liberties += self._num_liberties_r(d, who, checked) return liberties # We don't need to worry about crossing ourselves with the # recursion here, because we've already deleted the stone. def _delete_group(self, pos): if not self._on_board(pos): return who = self.board[pos] if who == Goban.EMPTY: return 0 return self._delete_group_r(pos, who) def _delete_group_r(self, pos, who): if not self._on_board(pos): return 0 if self.board[pos] != who: return 0 self.board[pos] = Goban.EMPTY self._changed.append(pos) num_deleted = 1 num_deleted += self._delete_group_r(pos + 1, who) num_deleted += self._delete_group_r(pos - 1, who) num_deleted += self._delete_group_r(pos + self.board_size, who) num_deleted += self._delete_group_r(pos - self.board_size, who) return num_deleted def draw_code(self, pos): if not self._on_board(pos): return None point = self.board[pos] code = None if point == Goban.EMPTY or point == Goban.SCORE_DAME: code = self.def_draw_codes[pos] elif point == Goban.BLACK: code = 'b' if pos == self.last_move: code += 'Cw' elif point == Goban.WHITE: code = 'w' if pos == self.last_move: code += 'Cb' elif point == Goban.SCORE_WHITE: code = self.def_draw_codes[pos] + 'ws' elif point == Goban.SCORE_BLACK: code = self.def_draw_codes[pos] + 'bs' if pos == self.ko: code += 'S' return code def auto_score(self): ''' Detects regions and assigns them to the appropriate player This may not always guess correctly, so we also have API that can manually change these things. After calling this function, the entire board should be redrawn. ''' for i in range(len(self.board)): if self.board[i] == Goban.EMPTY: bs = self.board_size * self.board_size checked = set() score = self._score_space(i, checked) for c in checked: self.board[c] = score def _score_space(self, pos, checked): if pos in checked: return None if self.board[pos] == Goban.BLACK: return Goban.SCORE_BLACK elif self.board[pos] == Goban.WHITE: return Goban.SCORE_WHITE checked.add(pos) possible = set() for i in self._neighbors(pos): score = self._score_space(i, checked) possible.add(score) if Goban.SCORE_DAME in possible or (Goban.SCORE_BLACK in possible and Goban.SCORE_WHITE in possible): return Goban.SCORE_DAME elif Goban.SCORE_BLACK in possible: return Goban.SCORE_BLACK elif Goban.SCORE_WHITE in possible: return Goban.SCORE_WHITE else: return None def _make_default_draw_codes(self): ret = [] for pos in range(len(self.board)): if pos == 0: ret.append('ul') elif pos == self.board_size - 1: ret.append('ur') elif pos == self.board_size * self.board_size - self.board_size: ret.append('dl') elif pos == self.board_size * self.board_size - 1: ret.append('dr') elif pos in [60, 66, 72, 174, 180, 186, 288, 294, 300]: ret.append('h') elif pos < self.board_size - 1: ret.append('u') elif pos % self.board_size == 0: ret.append('l') elif pos > (self.board_size * self.board_size - self.board_size - 1): ret.append('d') elif pos % self.board_size == 18: ret.append('r') else: ret.append('m') return ret def _real_pos(self, pos): x,y = pos return x * self.board_size + y def _on_board(self, pos): return pos >= 0 and pos < self.board_size * self.board_size def _other_color(self, color): if color == Goban.BLACK: return Goban.WHITE elif color == Goban.WHITE: return Goban.BLACK def _neighbors(self, pos): neighbors = [] if pos >= self.board_size: neighbors.append(pos - self.board_size) if pos <= self.board_size * self.board_size - self.board_size - 1: neighbors.append(pos + self.board_size) if pos % self.board_size != 0: neighbors.append(pos - 1) if (pos + 1) % self.board_size != 0: neighbors.append(pos + 1) return neighbors # Convert an sgf vector to a move tuple def _sgf_to_move(self, move): if move is None: return None x,y = move new_x = self.board_size - 1 - x return (new_x, y) # Convert a 1-dimensional position to an sgf move def _pos_to_sgf(self, pos): x = self.board_size - 1 + (pos / self.board_size) y = pos % self.board_size return (x,y)