pygo/lib/goban.py

461 lines
13 KiB
Python

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
self.sgf_game = None
if self.file_name is not None:
self.load_sgf(file_name)
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(point)] = Goban.BLACK
for point in white:
self.board[self._real_pos(point)] = Goban.WHITE
for node in self.sgf_game.get_main_sequence():
color, pos = node.get_move()
if color is not None:
self.play_move(color, pos)
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
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):
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
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
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)
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
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