#Simple Go playing program #Goals are: #1) Easy to understand: # If fully understanding GnuGo could be considered advanced, # then this should be beginner level Go playing program # Main focus is in understanding code, not in fancy stuff. # It should illustrate Go concepts using simple code. #2) Plays enough well to get solid rating at KGS #3) Small #4) Dual license: GPL and license used at Senseis
#Why at Senseis? #Goal is to illustrate Go programming and not to code another "GnuGo". #Senseis looks like good place to co-operatively write text and #create diagrams to illustrate algorithms. #So main focus is in explaining code. #Also possibility is to crosslink between concepts and documented code.
import re, string, time, random, sys from types import * from math import sqrt from copy import deepcopy
EMPTY = "." BLACK = "X" WHITE = "O"
colors = [BLACK, WHITE]
other_side = {BLACK: WHITE, WHITE: BLACK}
PASS_MOVE = (-1, -1)
x_coords_string = "ABCDEFGHJKLMNOPQRSTUVXYZ"
def move_as_string(move, board_size): """convert move tuple to string example: (2, 3) -> B3 """ if move==PASS_MOVE: return "PASS" x, y = move return x_coords_string[x-1] + str(y)
def string_as_move(m, size): """convert string to move tuple example: B3 -> (2, 3) """ if m=="PASS": return PASS_MOVE x = string.find(x_coords_string, m[0]) + 1 y = int(m[1:]) return x,y
class Board: """Go board: one position in board and relevant methods
Attributes: size: board size side: side to move: WHITE or BLACK captures: number of captures for WHITE and BLACK goban: actual board as dictionary: coordinates are keys and color is value
size: 3 side: WHITE goban: {(1, 3): EMPTY, (2, 3): WHITE, (3, 3): BLACK, (1, 2): WHITE, (2, 2): BLACK, (3, 2): BLACK, (1, 1): EMPTY, (2, 1): BLACK, (3, 1): EMPTY}
"""
def init(self, size): """Initialize board as empty: argument: size """ self.size = size self.side = BLACK #side to move self.captures[BLACK] = 0 self.captures[WHITE] = 0 self.goban = {} #actual board #Create and initialize board as empty size*size for pos in self.iterate_goban(): self.goban[pos] = EMPTY
def iterate_goban(self): """This goes trough all positions in goban Example usage: b = Board(2) for pos in b.iterate_goban(): print pos
Above code will print these values: (1, 1) (1, 2) (2, 1) (2, 2)
""" for x in range(1, self.size+1): for y in range(1, self.size+1): yield x, y
def iterate_neighbour(self, pos): """
This goes trough all neighbour positions: up, right, down, left Example usage: see legal_move method
"""
x, y = pos for x2,y2 in ((x,y+1), (x+1,y), (x,y-1), (x-1,y)): if 1<=x2<=self.size and 1<=y2<=self.size: yield (x2, y2)
def key(self): """This returns unique key for board. Returns board as string: all itersections are concated to one string in order iterate_goban() returns. Key can be used for example in super-ko detection """ stones = [] for pos in self.iterate_goban(): stones.append(self.goban[pos]) return string.join(stones, "")
def change_side(self): self.side = other_side[self.side]
def legal_move(self, move): """Test whether given move is legal. No previous position is considered. For ko and super-ko check see Game.legal_move. Returns truth value.
If move is in empty position in goban we check all neighbour intersections to see if move is legal.
If any of neighbour intersection belongs to same color and has more than 1 liberty, then move is legal.
If no rule applies, then move is illegal.
""" if move==PASS_MOVE: return True if move not in self.goban: return False if self.goban[move]!=EMPTY: return False for pos in self.iterate_neighbour(move): if self.goban[pos]==EMPTY: return True if self.goban[pos]==self.side and self.liberties(pos)>1: return True if self.goban[pos]==other_side[self.side] and self.liberties(pos)==1: return True return False
def liberties(self, pos): """Count liberties for group at given position. Returns number of liberties. This is simple flood algorith tha keeps track of stones and empty intersections visited. pos_list keeps track of stones we need to visit. pos_list starts with argument pos. We go trough each stone in pos_list. First we check whether we have already seen this position and skip it if it so. Then we go trough each neighbour intersection skipping those we have already seen. If intersection is empty we add to liberty_count and mark this as visited. If intersection belongs to same group we add it to stones to go trough (pos_list).
1 marks current intersection being processed. Square marks content of variables.
liberty_count: 6
""" seen_pos = {} liberty_count = 0 group_color = self.goban[pos] pos_list = [pos] while pos_list: pos2 = pos_list.pop() if pos2 in seen_pos: continue seen_pos[pos2] = True for pos3 in self.iterate_neighbour(pos2): if pos3 in seen_pos: continue if self.goban[pos3]==EMPTY: liberty_count = liberty_count + 1 seen_pos[pos3] = True continue if self.goban[pos3]==group_color and \ pos3 not in seen_pos and \ pos3 not in pos_list: pos_list.append(pos3) return liberty_count
def remove_group(self, pos): """Recursively remove given group from board and updating capture counts. First we remove this stone and then recursively call ourself to remove neighbour stones. """ remove_color = self.goban[pos] self.goban[pos] = EMPTY self.captures[remove_color] = self.captures[remove_color] + 1 for pos2 in self.iterate_neighbour(pos): if self.goban[pos2] == remove_color: self.remove_group(pos2)
def make_move(self, move): """Make move given in argument. Returns move or None if illegl. First we check given move for legality. Then we make move and remove captured opponent groups if there are any. """ if move==PASS_MOVE: self.change_side() return move if self.legal_move(move): self.goban[move] = self.side remove_color = other_side[self.side] for pos in self.iterate_neighbour(move): if self.goban[pos]==remove_color and self.liberties(pos)==0: self.remove_group(pos) self.change_side() return move return None
def str(self): """Convert position to string suitable for printing to screen. Returns board as string. """ s = self.side + " to move:\n" s = s + "Captured stones: " s = s + "White: " + str(self.captures[WHITE]) s = s + " Black: " + str(self.captures[BLACK]) + "\n" board_x_coords = " " + x_coords_string[:self.size] s = s + board_x_coords + "\n" s = s + " +" + "-"*self.size + "+\n" for y in range(self.size, 0, -1): if y < 10: board_y_coord = " " + str(y) else: board_y_coord = str(y) line = board_y_coord + "|" for x in range(1, self.size+1): line = line + self.goban[x,y] s = s + line + "|" + board_y_coord + "\n" s = s + " +" + "-"*self.size + "+\n" s = s + board_x_coords + "\n" return s
class Game: def init(self, size): """Initialize game: argument: size """ self.size = size self.current_board = Board(size) #past boards and moves self.board_history = [] self.move_history = [] #for super-ko detection self.position_seen = {} self.position_seen[self.current_board.key()] = True
def make_move_in_new_board(self, move): """This is utility method. This does not check legality. It returns move in copied board and also key of new board """ new_board = deepcopy(self.current_board) new_board.make_move(move) board_key = new_board.key() return new_board, board_key
def legal_move(self, move): """check whether move is legal return truth value first check move legality on current board then check for repetition (situational super-ko) """ if move==PASS_MOVE: return True if not self.current_board.legal_move(move): return False new_board, board_key = self.make_move_in_new_board(move) if board_key in self.position_seen: return False return True
def make_move(self, move): """make given move and return new board or return None if move is illegal First check move legality. Then make move and update history. """ if not self.legal_move(move): return None new_board, board_key = self.make_move_in_new_board(move) self.move_history.append(move) self.board_history.append(self.current_board) if move!=PASS_MOVE: self.position_seen[board_key] = True self.current_board = new_board return new_board
def undo_move(self): """undo latest move and return current board or return None if at beginning. Update repetition history and make previous position current. """ if not self.move_history: return None last_move = self.move_history.pop() if last_move!=PASS_MOVE: del self.position_seen[self.current_board.key()] self.current_board = self.board_history.pop() return self.current_board
def list_moves(self): """return all legal moves including pass move """ all_moves = [PASS_MOVE] for move in self.current_board.iterate_goban(): if self.legal_move(move): all_moves.append(move) return all_moves
def select_random_move(self): """return randomly selected move from all legal moves """ return random.choice(self.list_moves())
def generate_move(self): """generate move using random move generator """ return self.select_random_move()
def main(): size = 5 g = Game(size) while True: move = g.generate_move() g.make_move(move) print move_as_string(move, g.size) print g.current_board #if last 2 moves are pass moves: exit loop if len(g.move_history)>=2 and \ g.move_history[-1]==PASS_MOVE and \ g.move_history[-2]==PASS_MOVE: break
if name=="main": main()