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

This is a copy of the living page "SimpleGo/version 0.0" at Sensei's Library.
(OC) 2016 the Authors, published under the OpenContent License V1.0.
[Welcome to Sensei's Library!]
StartingPoints
ReferenceSection
About