SimpleGo/version 0.0

Sub-page of SimpleGo
#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
[Diagram]
Board example  
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
[Diagram]
Board example  
              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):
        """
[Diagram]
Board example  
            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.

[Diagram]
No empty neighbour: Continue checking other rules and other intersections.  
[Diagram]
Empty neighbour: Legal  

If any of neighbour intersection is empty, move is legal.

[Diagram]
Move colored neighbour block with 1 liberty: continue checking other neighbours  
[Diagram]
Move colored neighbour block with more than 1 liberty: Legal move  

If any of neighbour intersection belongs to same color and has more than 1 liberty, then move is legal.

[Diagram]
Opposite colored neighbour block with more than 1 liberty: continue checking other neighbours  
[Diagram]
Opposite colored neighbour block with 1 liberty: Legal move  

If any of neighbour intersection belongs to opposite color and has 1 liberty, then move is legal.

[Diagram]
None of rules applies.  

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.

[Diagram]
start: pos_list  
[Diagram]
start: seen_pos  

liberty_count: 0


[Diagram]
iteration 1: pos_list  
[Diagram]
iteration 1: seen_pos  

liberty_count: 2


[Diagram]
iteration 2: pos_list  
[Diagram]
iteration 2: seen_pos  

liberty_count: 3


[Diagram]
iteration 3: pos_list  
[Diagram]
iteration 3: seen_pos  

liberty_count: 3


[Diagram]
iteration 4: pos_list  
[Diagram]
iteration 4: seen_pos  

liberty_count: 4


[Diagram]
iteration 5: pos_list  
[Diagram]
iteration 5: seen_pos  

liberty_count: 4


[Diagram]
iteration 6: pos_list  
[Diagram]
iteration 6: seen_pos  

liberty_count: 4


[Diagram]
iteration 7: pos_list  
[Diagram]
iteration 7: seen_pos  

liberty_count: 5


[Diagram]
iteration 8: pos_list  
[Diagram]
iteration 8: seen_pos  

liberty_count: 5


[Diagram]
iteration 9: pos_list  
[Diagram]
iteration 9: seen_pos  

liberty_count: 5


[Diagram]
iteration 10: pos_list  
[Diagram]
iteration 10: seen_pos  

liberty_count: 5


[Diagram]
iteration 11: pos_list  
[Diagram]
iteration 11: seen_pos  

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()

SimpleGo/version 0.0 last edited by Aloril on October 17, 2005 - 13:04
RecentChanges · StartingPoints · About
Edit page ·Search · Related · Page info · Latest diff
[Welcome to Sensei's Library!]
RecentChanges
StartingPoints
About
RandomPage
Search position
Page history
Latest page diff
Partner sites:
Go Teaching Ladder
Goproblems.com
Login / Prefs
Tools
Sensei's Library