pygo/lib/goban.py

299 lines
8.3 KiB
Python

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 rpos == self.hover:
return
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
self.board[rpos] = self.to_move
self._capture(rpos)
self.to_move = self._other_color(self.to_move)
self.clear_hover()
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 self._neighbors(pos):
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 False
# 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 self._neighbors(pos):
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):
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._has_liberties_r(pos, who, checked)
def _has_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._has_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.
# 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
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 == 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))
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