From 1474af0e5f0d5dc9169ac124769d8cccc21d4cf8 Mon Sep 17 00:00:00 2001 From: Anna Wiggins Date: Fri, 13 Apr 2012 16:53:57 -0400 Subject: [PATCH] Factored the board code into a 'goban' library, and re-implemented it to use simple arrays and to be much, much simpler. Unfortunately, though, it doesn't quite work yet --- lib/goban.py | 273 +++++++++++++++++++++++++++++++++++++++++++++ pygo.py | 307 +++------------------------------------------------ 2 files changed, 286 insertions(+), 294 deletions(-) create mode 100644 lib/goban.py diff --git a/lib/goban.py b/lib/goban.py new file mode 100644 index 0000000..8edfc86 --- /dev/null +++ b/lib/goban.py @@ -0,0 +1,273 @@ +import pygame +from pygame.locals import * + + +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.ko = None + self.hover = None + self.elapsed_time = 0 + + + def set_hover(self, pos): + rpos = self._real_pos(pos) + + if not self._valid_move(rpos): + self.clear_hover() + return + + self.hover = rpos + + + def clear_hover(self): + self.hover = None + + + # fixme - somewhere in this or its component functions we need to + # identify ko points :) + def place_stone(self, pos): + rpos = self._real_pos(pos) + + if not self._valid_move(rpos): + return + + # fixme - update the GRAY status of affected squares + + self.board[rpos] = self.to_move + self._capture(rpos) + + self.to_move = self._other_color(self.to_move) + + + def _capture(self, pos): + """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.""" + + # Who are we capturing + who = self._other_color(self.to_move) + + for p in [pos + 1, pos - 1, pos + self.board_size, pos - self.board_size]: + if not self._on_board(p): + continue + + if not self._has_liberties(p, who): + if self.to_move == Goban.BLACK: + self.black_captures += self._delete_group(p) + elif self.to_move == Goban.WHITE: + self.white_captures += self._delete_group(p) + + + def _valid_move(self, pos): + if not self._on_board(pos): + return False + + # Can't play atop another stone + if self.board[pos] == Goban.EMPTY: + return True + + # Temporarily place the stone + self.board[pos] = self.to_move + + liberties = self._has_liberties(pos, self.to_move) + opponent = self._other_color(self.to_move) + + kills_group = False + for d in [pos + 1, pos - 1, pos + self.board_size, pos - self.board_size]: + if not self._on_board(d): + continue + + if self._has_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. Positive numbers are not necessarily accurate - + # treat this as a boolean + def _has_liberties(self, pos, who, checked=None): + if not self._on_board(pos): + return 0 + + if checked is None: + bs = board_size * board_size + checked = [False] * bs + + 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 [pos + 1, pos - 1, pos + self.board_size, pos - self.board_size]: + liberties += self._has_liberties(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. + # It would be more efficient to avoid going backwards, + # but a lot more complicated (see _has_liberties) + 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 + + + # fixme - needs to be evaluated with new data model + 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_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: + s = img_res[self.def_draw_codes[pos]] + elif point == Goban.BLACK: + s = img_res('b') + elif point == Goban.WHITE: + s = img_res('w') + + s = pygame.transform.scale(s, (inc, inc)) + ret.blit(s, ((pos % self.board_size) *inc, (pos / self.board_size) *inc)) + + if self.hover == point: + 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)) + + 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 - 19: + 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 - 20): + 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 - 1 + + + def _other_color(self, color): + if color == Goban.BLACK: + return Goban.WHITE + elif color == Goban.WHITE: + return Goban.BLACK diff --git a/pygo.py b/pygo.py index 524a388..9798cb0 100755 --- a/pygo.py +++ b/pygo.py @@ -3,9 +3,13 @@ # # A GTK Python GO client +import sys +sys.path.append('lib/') + import os import pygame from pygame.locals import * +import goban def load_png(name, alpha=None): @@ -54,288 +58,6 @@ def build_img_res(): return ret -class GobanSquare: - """A single square on the go board""" - - def __init__(self, pos): - self.x, self.y = pos - self.state = -1 - self.marked = False - self.checked = False # Used for recursive checks - - if (self.x, self.y) == (1,1): - self.default_draw_code = 'ul' - elif (self.x, self.y) == (1,19): - self.default_draw_code = 'ur' - elif (self.x, self.y) == (19,1): - self.default_draw_code = 'dl' - elif (self.x, self.y) == (19,19): - self.default_draw_code = 'dr' - elif (self.x, self.y) in [(4,4), (4,10), (4,16), (10,4), (10,10), (10,16), (16,4), (16,10), (16,16)]: - self.default_draw_code = 'h' - elif self.x == 1: - self.default_draw_code = 'u' - elif self.y == 1: - self.default_draw_code = 'l' - elif self.x == 19: - self.default_draw_code = 'd' - elif self.y == 19: - self.default_draw_code = 'r' - else: - self.default_draw_code = 'm' - - - def toggle_marked(self): - if self.marked: - self.marked = False - else: - self.marked = True - - - def get_draw_code(self): - ret = None - - if self.state == -1: - ret = self.default_draw_code - elif self.state == 0: - ret = 'b' - elif self.state == 1: - ret = 'w' - - if self.marked: - if self.state >=0: - ret = self.default_draw_code + 'T' - else: - ret = self.default_draw_code + 'C' - - return ret - - - -class Goban: - """Represents the go board. Handles stone placement, captures, etc""" - - def __init__(self): - # Build the 361 board intersections - self.board = [] - for i in range(19): - self.board.append([]) - for j in range(19): - self.board[i].append(GobanSquare((i+1, j+1))) - - self.turn = 0 - self.captures = [] - self.captures.append(0) - self.captures.append(0) - self.hover = None - self.elapsed_time = 0 - - - def set_hover(self, pos): - if not self._valid_move(pos): - self.clear_hover() - return - - x,y = pos - self.hover = self.board[x][y] - - - def clear_hover(self): - self.hover = None - - - def place_stone(self, pos): - if not self._valid_move(pos): - return - - x, y = pos - self.board[x][y].state = self.turn - self._capture(pos) - self.turn = (self.turn + 1) % 2 - - - def toggle_marked(self, pos): - x,y = pos - if x < 0 or x > 18 or y < 0 or y > 18: - return - - self.board[x][y].toggle_marked() - - - def _capture(self, pos): - x, y = pos - who = (self.turn + 1) % 2 - - for p in [(x, y+1), (x, y-1), (x+1, y), (x-1, y)]: - newx, newy = p - if newx < 0 or newx > 18 or newy < 0 or newy > 18 or self.board[newx][newy].state == self.turn: - continue - - if not self._has_liberties(p, who): - self.captures[self.turn] += self._delete_group(p) - - self._clear_checks() - - - def _valid_move(self, pos): - x, y = pos - if x < 0 or x > 18 or y < 0 or y > 18: - return False - - # Can't play atop another stone - if self.board[x][y].state != -1: - return False - - # Temporarily place the stone - self.board[x][y].state = self.turn - - liberties = self._has_liberties(pos, self.turn) - self._clear_checks() - - opponent = (self.turn + 1) % 2 - - kills_group = False - for d in [(x, y+1), (x, y-1), (x+1, y), (x-1, y)]: - new_x, new_y = d - if new_x < 0 or new_x > 18 or new_y < 0 or new_y > 18: - continue - - if self._has_liberties(d, opponent) == 0: - kills_group = True - break - self._clear_checks() - - # Remove temporary stone - self.board[x][y].state = -1 - - return liberties > 0 or kills_group - - - # Recursively find whether there are liberties for the group - # at pos. Positive numbers are not necessarily accurate - - # treat this as a boolean - def _has_liberties(self, pos, who, direction = None): - x,y = pos - if x < 0 or x > 18 or y < 0 or y > 18: - return 0 - - square = self.board[x][y] - - if square.checked: - return 0 - else: - square.checked = True - - if square.state == -1: - return 1 - elif square.state != who: - return 0 - else: - liberties = 0 - for d in [(x, y+1), (x-1, y), (x+1, y), (x, y-1)]: - liberties += self._has_liberties(d, who) - - return liberties - - - # We don't need to worry about crossing ourselves with the - # recursion here, because we've already deleted the stone. - # It would be more efficient to avoid going backwards, - # but a lot more complicated (see _has_liberties) - def _delete_group(self, pos): - x,y = pos - if x < 0 or x > 18 or y < 0 or y > 18: - return - - who = self.board[x][y].state - - if who == -1: - return - - self.board[x][y].state = -1 - - num_deleted = 1 - - num_deleted += self._delete_group_r((x, y+1), who) - num_deleted += self._delete_group_r((x, y-1), who) - num_deleted += self._delete_group_r((x+1, y), who) - num_deleted += self._delete_group_r((x-1, y), who) - - return num_deleted - - - def _delete_group_r(self, pos, who): - x,y = pos - if x < 0 or x > 18 or y < 0 or y > 18: - return 0 - - if self.board[x][y].state != who: - return 0 - - self.board[x][y].state = -1 - - num_deleted = 1 - - num_deleted += self._delete_group_r((x, y+1), who) - num_deleted += self._delete_group_r((x, y-1), who) - num_deleted += self._delete_group_r((x+1, y), who) - num_deleted += self._delete_group_r((x-1, y), who) - - return num_deleted - - - def _clear_checks(self): - for row in self.board: - for col in row: - col.checked = False - - - def draw_board(self, size, img_res): - ret = pygame.Surface((size,size)) - - inc = size / 19; - i = 0 - for row in self.board: - j = 0 - for square in row: - s = img_res[square.get_draw_code()] - s = pygame.transform.scale(s, (inc, inc)) - ret.blit(s, (j*inc,i*inc)) - - if self.hover == square: - c = img_res['bH'] - if self.turn == 1: - c = img_res['wH'] - c = pygame.transform.scale(c, (inc, inc)) - ret.blit(c, (j*inc,i*inc)) - - j += 1 - i += 1 - - 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.captures[0]), 1, (10, 10, 10)) - white_cap = font.render('White: {}'.format(self.captures[1]), 1, (10, 10, 10)) - turn = font.render('Turn: {}'.format(['Black', 'White'][self.turn]), 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 main(): # Basic screen init @@ -354,12 +76,12 @@ def main(): board_size = 800 board_inc = board_size / 19 - goban = Goban() + gb = goban.Goban() - board = goban.draw_board(board_size, img_res) + board = gb.draw_board(board_size, img_res) background.blit(board, (0,0)) - text = goban.draw_info() + text = gb.draw_info() background.blit(text, (815, 25)) screen.blit(background, (0, 0)) @@ -381,9 +103,9 @@ def main(): col = x / board_inc if x <= board_size: - goban.set_hover((row,col)) + gb.set_hover((row,col)) else: - goban.clear_hover() + gb.clear_hover() # Place a stone on left-click if event.type == MOUSEBUTTONDOWN: @@ -393,18 +115,15 @@ def main(): if x <= board_size: if event.button == 1: - goban.place_stone((row, col)) - - if event.button == 3: - goban.toggle_marked((row, col)) + gb.place_stone((row, col)) if event.type == USEREVENT: - goban.elapsed_time += 1 + gb.elapsed_time += 1 - board = goban.draw_board(board_size, img_res) + board = gb.draw_board(board_size, img_res) background.blit(board, (0,0)) - text = goban.draw_info() + text = gb.draw_info() background.blit(text, (815, 25)) screen.blit(background, (0, 0))