from gomill import sgf 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.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 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 node in self.sgf_game.get_main_sequence(): color, pos = node.get_move() if color is not None: if pos == 'pass': self.pass_move(color) else: self.play_move(self._sgf_to_move(pos), color, add_sgf=False) if self.sgf_game.get_winner() is not None: self.winner = self.sgf_game.get_winner() self.to_move = None 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 reset(self): """Reset the board to a pre-game state""" # 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.sgf_game = sgf.Sgf_game(self.board_size) 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 = board_size * board_size self.board = [Goban.EMPTY] * num_points def clear_hover(self): self.hover = None # For performance reasons (to help with drawing code) # any modified positions are returned in a list. # This is self._changed, which is modified by this # function and by _delete_group_r(). # This list will *also* contain the last move and any # previous or new ko positions. def play_move(self, pos, color=None, add_sgf=True): 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) 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 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 or 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): 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)