409 lines
11 KiB
Python
409 lines
11 KiB
Python
class Goban:
|
|
"""Represents the go board. Handles stone placement, captures, etc"""
|
|
|
|
# enum values for the board array
|
|
EMPTY=0
|
|
WHITE=1
|
|
BLACK=2
|
|
# GRAY_WHITE=3
|
|
# GRAY_BLACK=4
|
|
|
|
def __init__(self, board_size=19):
|
|
# 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
|
|
|
|
|
|
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
|
|
|
|
|
|
def play_move(self, pos, color=None):
|
|
if color is None:
|
|
color = self.to_move
|
|
|
|
if self.to_move == Goban.EMPTY:
|
|
return
|
|
|
|
rpos = self._real_pos(pos)
|
|
|
|
if not self._valid_move(rpos, color):
|
|
return
|
|
|
|
self.board[rpos] = color
|
|
self._capture(rpos)
|
|
self.last_move = rpos
|
|
self.passed_last = False
|
|
|
|
self.to_move = self._other_color(color)
|
|
self.clear_hover()
|
|
|
|
|
|
|
|
# fixme: need to handle post-game stuff here... scoring code
|
|
def pass_move(self, color=None):
|
|
if color is None:
|
|
color = self.to_move
|
|
|
|
if self.passed_last:
|
|
self.to_move = Goban.EMPTY
|
|
else:
|
|
self.to_move = self._other_color(color)
|
|
self.passed_last = True
|
|
|
|
self.last_move = None
|
|
self.ko = None
|
|
|
|
|
|
def resign(self, color=None):
|
|
if color is None:
|
|
color = self.to_move
|
|
|
|
self.passed_last = False
|
|
self.last_move = None
|
|
self.ko = None
|
|
self.winner = self._other_color(color)
|
|
self.to_move = Goban.EMPTY
|
|
|
|
|
|
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):
|
|
# 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)
|
|
|
|
if who == Goban.EMPTY:
|
|
return 0
|
|
|
|
self.board[x][y].state = Goban.EMPTY
|
|
|
|
|
|
|
|
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
|
|
|
|
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]
|
|
|
|
if point == Goban.EMPTY:
|
|
code = self.def_draw_codes[pos]
|
|
elif point == Goban.BLACK:
|
|
code = 'b'
|
|
elif point == Goban.WHITE:
|
|
code = 'w'
|
|
|
|
if pos == self.last_move:
|
|
code = code + 'T'
|
|
|
|
if pos == self.ko:
|
|
code = code + 'C'
|
|
|
|
return code
|
|
|
|
|
|
|
|
# def draw_board(self, size, img_res):
|
|
# ret = pygame.Surface((size,size))
|
|
|
|
# inc = size / self.board_size;
|
|
|
|
# for pos in range(len(self.board)):
|
|
# point = self.board[pos]
|
|
|
|
# if point == Goban.EMPTY:
|
|
# code = self.def_draw_codes[pos]
|
|
# elif point == Goban.BLACK:
|
|
# code = 'b'
|
|
# elif point == Goban.WHITE:
|
|
# code = 'w'
|
|
|
|
# if pos == self.last_move:
|
|
# code = code + 'T'
|
|
|
|
# if pos == self.ko:
|
|
# code = code + 'C'
|
|
|
|
# s = img_res[code]
|
|
# s = pygame.transform.scale(s, (inc, inc))
|
|
# ret.blit(s, ((pos % self.board_size) *inc, (pos / self.board_size) *inc))
|
|
|
|
# if self.hover == pos:
|
|
# c = img_res['bH']
|
|
# if self.to_move == Goban.WHITE:
|
|
# c = img_res['wH']
|
|
# c = pygame.transform.scale(c, (inc, inc))
|
|
# ret.blit(c, ((pos % self.board_size) *inc, (pos / self.board_size) *inc))
|
|
|
|
# return ret.convert_alpha()
|
|
|
|
|
|
|
|
# def draw_info(self):
|
|
# textbox = pygame.Surface((150, 300))
|
|
# textbox = textbox.convert()
|
|
# textbox.fill((250, 250, 250))
|
|
|
|
# font = pygame.font.Font(None, 24)
|
|
# # time = font.render('Time: {:02d}:{:02d}'.format(self.elapsed_time / 60, self.elapsed_time % 60), 1, (10, 10, 10))
|
|
# heading = font.render('Captures', 1, (10, 10, 10))
|
|
# black_cap = font.render('Black: {}'.format(self.black_captures), 1, (10, 10, 10))
|
|
# white_cap = font.render('White: {}'.format(self.white_captures), 1, (10, 10, 10))
|
|
# if self.to_move == Goban.BLACK:
|
|
# turn = font.render('To move: Black', 1, (10, 10, 10))
|
|
# elif self.to_move == Goban.WHITE:
|
|
# turn = font.render('To move: White', 1, (10, 10, 10))
|
|
# else:
|
|
# if self.winner == Goban.WHITE:
|
|
# turn = font.render('Winner: White', 1, (10, 10, 10))
|
|
# elif self.winner == Goban.BLACK:
|
|
# turn = font.render('Winner: Black', 1, (10, 10, 10))
|
|
# else:
|
|
# turn = font.render('Scoring', 1, (10, 10, 10))
|
|
|
|
# textbox.blit(heading, (0, 0))
|
|
# textbox.blit(black_cap, (0, 28))
|
|
# textbox.blit(white_cap, (0, 56))
|
|
# textbox.blit(turn, (0, 100))
|
|
# # textbox.blit(time, (0, 150))
|
|
|
|
# return textbox
|
|
|
|
|
|
|
|
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
|