# -*- coding: utf-8 -*-
"""This is a class to play small games of GO, natively coded in Python.
I tried to use nice data structures to speed it up (union & find, Zobrist hashs,
numpy memory efficient ...)
Licence is MIT: you can do whatever you want with the code. But keep my name somewhere.
(c) Laurent SIMON 2019 -- 2024 V3
Known Limitations:
- No early detection of endgames (only stops when no stone can be put on the board, or superKo)
- Final scoring does not remove dead stones, and thus may differ from a more smart counting.
You may want to end the game only when all the areas are almost filled.
References and Code inspirations
--------------------------------
I looked around in the web for inspiration. One important source of inspiration (some of my python lines
may be directly inspired by him is the fantastic github repo and book (which I bought :)) of Max Pumperla
about Deep Learning and the game of Go
https://github.com/maxpumperla/deep_learning_and_the_game_of_go
I tried to be faster by using more non python data structures (limiting lists and sets), however :)
"""
from __future__ import print_function # Used to help cython work well
import numpy as np
import random
def getProperRandom():
"""Gets a proper 64 bits random number (ints in Python are not the ideal toy to play with int64)"""
return np.random.randint(np.iinfo(np.int64).max, dtype="int64")
_COIN_ = False
class Board:
"""GO Board class to implement your (simple) GO player."""
__VERSION__ = 3.0
_BLACK = 1
_WHITE = 2
_COIN = 3
_EMPTY = 0
_BOARDSIZE = 8 # Used in static methods, do not write it
_DEBUG = False
##########################################################
##########################################################
""" A set of functions to manipulate the moves from the
- internal representation, called "flat", in 1D (just integers)
- coord representation on the board (0,0)..(_BOARDSIZE, _BOARDSIZE)
- name representation (A1, A2, ... D5, D6, ..., PASS)"""
@staticmethod
def flatten(coord):
"""Static method that teturns the flatten (1D) coordinates given the 2D coordinates (x,y) on the board. It is a
simple helper function to get y*_BOARDSIZE + x.
Internally, all the moves are flatten. If you use legal_moves or weak_legal_moves, it will produce flatten
coordinates."""
if coord == (-1, -1):
return -1
return Board._BOARDSIZE * coord[1] + coord[0]
@staticmethod
def unflatten(fcoord):
if fcoord == -1:
return (-1, -1)
d = divmod(fcoord, Board._BOARDSIZE)
return d[1], d[0]
@staticmethod
def name_to_coord(s): # Note that there is no "I"!
if s == "PASS":
return (-1, -1)
indexLetters = {
"A": 0,
"B": 1,
"C": 2,
"D": 3,
"E": 4,
"F": 5,
"G": 6,
"H": 7,
"J": 8,
}
col = indexLetters[s[0]]
lin = int(s[1:]) - 1
return (col, lin)
@staticmethod
def name_to_flat(s):
return Board.flatten(Board.name_to_coord(s))
@staticmethod
def coord_to_name(coord):
if coord == (-1, -1):
return "PASS"
letterIndex = "ABCDEFGHJ" # Note that there is no "I"!
return letterIndex[coord[0]] + str(coord[1] + 1)
@staticmethod
def flat_to_name(fcoord):
if fcoord == -1:
return "PASS"
return Board.coord_to_name(Board.unflatten(fcoord))
##########################################################
##########################################################
"""Just a couple of helper functions about who has to play next"""
@staticmethod
def flip(player):
if player == Board._BLACK:
return Board._WHITE
return Board._BLACK
@staticmethod
def player_name(player):
if player == Board._BLACK:
return "black"
elif player == Board._WHITE:
return "white"
return "???"
##########################################################
##########################################################
def _reset(self):
self._nbWHITE = 0
self._nbBLACK = 0
self._capturedWHITE = 0
self._capturedBLACK = 0
self._nextPlayer = self._BLACK
self._board = np.zeros((Board._BOARDSIZE**2), dtype="int8")
self._empties = set(range(Board._BOARDSIZE**2))
if _COIN_:
if Board._BOARDSIZE == 9:
coins = [(2, 2), (6, 2), (2, 6), (6, 6), (4, 4)]
elif Board._BOARDSIZE == 7:
coins = [(1, 1), (5, 5), (1, 5), (5, 1)]
for c in coins:
self._board[Board.flatten(c)] = Board._COIN
self._empties.remove(Board.flatten(c))
self._lastPlayerHasPassed = False
self._gameOver = False
self._trailMoves = [] # data structure used to push/pop the moves
self._stringUnionFind = np.full((Board._BOARDSIZE**2), -1, dtype="int8")
self._stringLiberties = np.full((Board._BOARDSIZE**2), -1, dtype="int8")
self._stringSizes = np.full((Board._BOARDSIZE**2), -1, dtype="int8")
# Zobrist values for the hashes. I use np.int64 to be machine independant
self._positionHashes = np.empty((Board._BOARDSIZE**2, 2), dtype="int64")
random_state = np.random.get_state()
np.random.seed(123456789) # Fixes the seed for the generation of hashs
for x in range(Board._BOARDSIZE**2):
for c in range(2):
self._positionHashes[x][c] = getProperRandom()
self._currentHash = getProperRandom()
self._passHashB = getProperRandom()
self._passHashW = getProperRandom()
np.random.set_state(random_state)
self._seenHashes = set()
self._historyMoveNames = []
# Building fast structures for accessing neighborhood
self._neighbors = []
self._neighborsEntries = []
for nl in [
self._get_neighbors(fcoord) for fcoord in range(Board._BOARDSIZE**2)
]:
self._neighborsEntries.append(len(self._neighbors))
for n in nl:
self._neighbors.append(n)
self._neighbors.append(-1) # Sentinelle
self._neighborsEntries = np.array(self._neighborsEntries, dtype="int16")
self._neighbors = np.array(self._neighbors, dtype="int8")
def __init__(self, other=None, deepcopy=False):
"""Main constructor. Instantiate all non static variables."""
if other is None:
self._reset()
else:
self._shallow_copy(other)
if deepcopy: # Also copy the backtrack structure
self._trailMoves = other._trailMoves.deepcopy()
else:
self._trailMoves = []
##########################################################
##########################################################
""" Simple helper function to directly access the board.
if b is a Board(), you can ask for b[m] to get the value of the corresponding cell,
(0 for Empty, 1 for Black and 2 for White, see Board._BLACK,_WHITE,_EMPTY values)
If you want to have an access via coordinates on the board you can use it like
b[Board.flatten((x,y))]
"""
def __getitem__(self, key):
"""Helper access to the board, from flatten coordinates (in [0 .. Board.BOARDSIZE**2]).
Read Only array. If you want to add a stone on the board, you have to use
_put_stone()."""
return self._board[key]
def __len__(self):
return Board._BOARDSIZE**2
##########################################################
##########################################################
""" Main functions for generating legal moves """
def is_game_over(self):
"""Checks if the game is over, ie, if you can still put a stone somewhere"""
return self._gameOver
def legal_moves(self):
"""
Produce a list of moves, ie flatten moves. They are integers representing the coordinates on the board. To get
named Move (like A1, D5, ..., PASS) from these moves, you can use the function Board.flat_to_name(m).
This function only produce legal moves. That means that SuperKO are checked BEFORE trying to move (when
populating the returned list). This can
only be done by actually placing the stone, capturing strigns, ... to compute the hash of the board. This is
extremelly costly to check. Thus, you should use weak_legal_moves that does not check the superko and actually
check the return value of the push() function that can return False if the move was illegal due to superKo.
"""
moves = [
m
for m in self._empties
if not self._is_suicide(m, self._nextPlayer)
and not self._is_super_ko(m, self._nextPlayer)[0]
]
moves.append(-1) # We can always ask to pass
return moves
def weak_legal_moves(self):
"""
Produce a list of moves, ie flatten moves. They are integers representing the coordinates on the board. To get
named Move (like A1, D5, ..., PASS) from these moves, you can use the function Board.flat_to_name(m).
Can generate illegal moves, but only due to Super KO position. In this generator, KO are not checked.
If you use a move from this list, you have to check if push(m) was True or False and then immediatly pop
it if it is False (meaning the move was superKO."""
moves = [m for m in self._empties if not self._is_suicide(m, self._nextPlayer)]
moves.append(-1) # We can always ask to pass
return moves
def generate_legal_moves(self):
"""See legal_moves description. This is just a wrapper to this function, kept for compatibility."""
return self.legal_moves()
def move_to_str(self, m):
"""Transform the internal representation of a move into a string. Simple wrapper, but useful for
producing general code."""
return Board.flat_to_name(m)
def str_to_move(self, s):
"""Transform a move given as a string into an internal representation. Simple wrapper here, but may be
more complex in other games."""
return Board.name_to_flat(s)
def play_move(self, fcoord):
"""Main internal function to play a move.
Checks the superKo, put the stone then capture the other color's stones.
Returns True if the move was ok, and False otherwise. If False is returned, there was no side effect.
In particular, it checks the superKo that may not have been checked before.
You can call it directly but the push/pop mechanism will not be able to undo it. Thus in general,
only push/pop are called and this method is never directly used."""
if self._gameOver:
return True
if fcoord != -1: # pass otherwise
alreadySeen, tmpHash = self._is_super_ko(fcoord, self._nextPlayer)
if alreadySeen:
self._historyMoveNames.append(self.flat_to_name(fcoord))
return False
captured = self._put_stone(fcoord, self._nextPlayer)
# captured is the list of Strings that have 0 liberties
for fc in captured:
self._capture_string(fc)
assert tmpHash == self._currentHash
self._lastPlayerHasPassed = False
if self._nextPlayer == self._WHITE:
self._nbWHITE += 1
else:
self._nbBLACK += 1
else:
if self._lastPlayerHasPassed:
self._gameOver = True
else:
self._lastPlayerHasPassed = True
self._currentHash ^= (
self._passHashB if self._nextPlayer == Board._BLACK else self._passHashW
)
self._seenHashes.add(self._currentHash)
self._historyMoveNames.append(self.flat_to_name(fcoord))
self._nextPlayer = Board.flip(self._nextPlayer)
return True
def next_player(self):
return self._nextPlayer
##########################################################
##########################################################
""" Helper functions for pushing/poping moves. You may want to use them in your game tree traversal"""
def push(self, m):
"""
push: used to push a move on the board. More costly than play_move()
but you can pop it after. Helper for your search tree algorithm"""
assert not self._gameOver
self._pushBoard()
return self.play_move(m)
def push_lazy(self, m):
"""
Pushes the move without the backtrack memory. You can't go back. Used for sampling.
"""
assert not self._gameOver
return self.play_move(m)
def pop(self):
"""
pop: another helper function for you rsearch tree algorithm. If a move has been pushed,
you can undo it by calling pop
"""
hashtopop = self._currentHash
self._popBoard()
if hashtopop in self._seenHashes:
self._seenHashes.remove(hashtopop)
##########################################################
##########################################################
def _result(self):
"""
The scoring mechanism is fixed but really costly. It may be not a good idea to use it as a heuristics.
It is the chinese area scoring that computes the final result. It uses the same notation as in chess:
Returns:
- "1-0" if WHITE wins
- "0-1" if BLACK wins
- "1/2-1/2" if DEUCE
Known problems: dead stones are not removed, so the score only stricly apply the area rules. You may want
to keep playing to consolidate your area before computing the scores.
"""
score = self._count_areas()
score_black = self._nbBLACK + score[0]
score_white = self._nbWHITE + score[1]
return score_black, score_white
def result(self):
score_black, score_white = self._result()
if score_white > score_black:
return "1-0"
elif score_white < score_black:
return "0-1"
else:
return "1/2-1/2"
def result_number(self):
score_black, score_white = self._result()
if score_white > score_black:
return Board._WHITE
elif score_white < score_black:
return Board._BLACK
else:
return Board._EMPTY
def winner(self):
return self.result_number()
def compute_score(self):
"""Computes the score (chinese rules) and return the scores for (blacks, whites) in this order"""
score = self._count_areas()
return (self._nbBLACK + score[0], self._nbWHITE + score[1])
def diff_stones_board(self):
"""You can call it to get the difference of stones on the board (NBBLACKS - NBWHITES)"""
return self._nbBLACK - self._nbWHITE
def diff_stones_captured(self):
"""You can call it to get the difference of captured stones during the game(NBBLACKS - NBWHITES)"""
return self._capturedBLACK - self._capturedWHITE
def final_go_score(self):
"""Returns the final score in a more GO-like way."""
score_black, score_white = self.compute_score()
if score_white > score_black:
return "W+" + str(score_white - score_black)
elif score_white < score_black:
return "B+" + str(score_black - score_white)
else:
return "0"
def get_board(self):
"""Returns the numpy array representing the board. Don't write in it unless you know exactly what you are doing."""
return self._board
def _shallow_copy(self, other):
"""Copy everything but the backtrack structures (cannot pop after).
Use deepcopy if you want to copy everything, including the
backtrack capabilities"""
self._nbWHITE = other._nbWHITE
self._nbBLACK = other._nbBLACK
self._capturedWHITE = other._capturedWHITE
self._capturedBLACK = other._capturedBLACK
self._nextPlayer = other._nextPlayer
self._board = other._board.copy()
self._gameOver = other._gameOver
self._lastPlayerHasPassed = other._lastPlayerHasPassed
self._stringUnionFind = other._stringUnionFind.copy()
self._stringLiberties = other._stringLiberties.copy()
self._stringSizes = other._stringSizes.copy()
self._empties = other._empties.copy()
self._currentHash = other._currentHash
self._positionHashes = other._positionHashes
self._passHashB = other._passHashB
self._passHashW = other._passHashW
self._seenHashes = other._seenHashes.copy()
self._historyMoveNames = []
self._neighbors = other._neighbors
self._neighborsEntries = other._neighborsEntries
self._trailMoves = None # Can be overrided right after...
##########################################################
##########################################################
##########################################################
##########################################################
""" Internal functions only"""
def _pushBoard(self):
currentStatus = []
currentStatus.append(self._nbWHITE)
currentStatus.append(self._nbBLACK)
currentStatus.append(self._capturedWHITE)
currentStatus.append(self._capturedBLACK)
currentStatus.append(self._nextPlayer)
currentStatus.append(self._board.copy())
currentStatus.append(self._gameOver)
currentStatus.append(self._lastPlayerHasPassed)
currentStatus.append(self._stringUnionFind.copy())
currentStatus.append(self._stringLiberties.copy())
currentStatus.append(self._stringSizes.copy())
currentStatus.append(self._empties.copy())
currentStatus.append(self._currentHash)
self._trailMoves.append(currentStatus)
def _popBoard(self):
oldStatus = self._trailMoves.pop()
self._currentHash = oldStatus.pop()
self._empties = oldStatus.pop()
self._stringSizes = oldStatus.pop()
self._stringLiberties = oldStatus.pop()
self._stringUnionFind = oldStatus.pop()
self._lastPlayerHasPassed = oldStatus.pop()
self._gameOver = oldStatus.pop()
self._board = oldStatus.pop()
self._nextPlayer = oldStatus.pop()
self._capturedBLACK = oldStatus.pop()
self._capturedWHITE = oldStatus.pop()
self._nbBLACK = oldStatus.pop()
self._nbWHITE = oldStatus.pop()
self._historyMoveNames.pop()
def _getPositionHash(self, fcoord, color):
return self._positionHashes[fcoord][color - 1]
# Used only in init to build the neighborsEntries datastructure
def _get_neighbors(self, fcoord):
x, y = Board.unflatten(fcoord)
neighbors = ((x + 1, y), (x - 1, y), (x, y + 1), (x, y - 1))
return [Board.flatten(c) for c in neighbors if self._isOnBoard(c[0], c[1])]
# for union find structure, recover the number of the current string of stones
def _getStringOfStone(self, fcoord):
# In the union find structure, it is important to route all the nodes to the root
# when querying the node. But in Python, using the successive array is really costly
# so this is not so clear that we need to use the successive collection of nodes
# Moreover, not rerouting the nodes may help for backtracking on the structure
successives = []
while self._stringUnionFind[fcoord] != -1:
fcoord = self._stringUnionFind[fcoord]
successives.append(fcoord)
if len(successives) > 1:
for fc in successives[:-1]:
self._stringUnionFind[fc] = fcoord
return fcoord
def _merge_strings(self, str1, str2):
self._stringLiberties[str1] += self._stringLiberties[str2]
self._stringLiberties[str2] = -1
self._stringSizes[str1] += self._stringSizes[str2]
self._stringSizes[str2] = -1
assert self._stringUnionFind[str2] == -1
self._stringUnionFind[str2] = str1
def _put_stone(self, fcoord, color):
self._board[fcoord] = color
self._currentHash ^= self._getPositionHash(fcoord, color)
if self._DEBUG:
assert fcoord in self._empties
self._empties.remove(fcoord)
nbEmpty = 0
nbSameColor = 0
i = self._neighborsEntries[fcoord]
while self._neighbors[i] != -1:
n = self._board[self._neighbors[i]]
if n == Board._EMPTY:
nbEmpty += 1
elif n == color:
nbSameColor += 1
i += 1
# nbOtherColor = 4 - nbEmpty - nbSameColor
currentString = fcoord
self._stringLiberties[currentString] = nbEmpty
self._stringSizes[currentString] = 1
stringWithNoLiberties = [] # String to capture (if applies)
i = self._neighborsEntries[fcoord]
while self._neighbors[i] != -1:
fn = self._neighbors[i]
if self._board[fn] == color: # We may have to merge the strings
stringNumber = self._getStringOfStone(fn)
self._stringLiberties[stringNumber] -= 1
if currentString != stringNumber:
self._merge_strings(stringNumber, currentString)
currentString = stringNumber
elif self._board[fn] != Board._EMPTY: # Other color
stringNumber = self._getStringOfStone(fn)
self._stringLiberties[stringNumber] -= 1
if self._stringLiberties[stringNumber] == 0:
if (
stringNumber not in stringWithNoLiberties
): # We may capture more than one string
stringWithNoLiberties.append(stringNumber)
i += 1
return stringWithNoLiberties
def reset(self):
self._reset()
def _isOnBoard(self, x, y):
return x >= 0 and x < Board._BOARDSIZE and y >= 0 and y < Board._BOARDSIZE
def _is_an_eye(self, fcoord, color):
opponent = Board.flip(color)
i = self._neighborsEntries[fcoord]
while self._neighbors[i] != -1:
fn = self._neighbors[i]
if self._board[fn] == Board._EMPTY:
return False
if self._board[fn] == opponent:
return False
i += 1
return True
def _is_suicide(self, fcoord, color):
opponent = Board.flip(color)
i = self._neighborsEntries[fcoord]
libertiesFriends = {}
libertiesOpponents = {}
while self._neighbors[i] != -1:
fn = self._neighbors[i]
if self._board[fn] == Board._EMPTY:
return False
string = self._getStringOfStone(fn)
if self._board[fn] == color: # check that we don't kill the whole zone
if string not in libertiesFriends:
libertiesFriends[string] = self._stringLiberties[string] - 1
else:
libertiesFriends[string] -= 1
else:
if Board._DEBUG:
assert self._board[fn] == opponent
if string not in libertiesOpponents:
libertiesOpponents[string] = self._stringLiberties[string] - 1
else:
libertiesOpponents[string] -= 1
i += 1
for s in libertiesOpponents:
if libertiesOpponents[s] == 0:
return False # At least one capture right after this move, it is legal
if len(libertiesFriends) == 0: # No a single friend there...
return True
# Now checks that when we connect all the friends, we don't create
# a zone with 0 liberties
sumLibertiesFriends = 0
for s in libertiesFriends:
sumLibertiesFriends += libertiesFriends[s]
if sumLibertiesFriends == 0:
return True # At least one friend zone will be captured right after this move, it is unlegal
return False
# Checks if the move leads to an already seen board
# By doing this, it has to "simulate" the move, and thus
# it computes also the sets of strings to be removed by the move.
def _is_super_ko(self, fcoord, color):
# Check if it is a complex move (if it takes at least a stone)
tmpHash = self._currentHash ^ self._getPositionHash(fcoord, color)
assert self._currentHash == tmpHash ^ self._getPositionHash(fcoord, color)
i = self._neighborsEntries[fcoord]
libertiesOpponents = {}
opponent = Board.flip(color)
while self._neighbors[i] != -1:
fn = self._neighbors[i]
# print("superko looks at ", self.coord_to_name(fn), "for move", self.coord_to_name(fcoord))
if self._board[fn] == opponent:
s = self._getStringOfStone(fn)
# print("superko sees string", self.coord_to_name(s))
if s not in libertiesOpponents:
libertiesOpponents[s] = self._stringLiberties[s] - 1
else:
libertiesOpponents[s] -= 1
i += 1
for s in libertiesOpponents:
if libertiesOpponents[s] == 0:
# print("superko computation for move ", self.coord_to_name(fcoord), ":")
for fn in self._breadthSearchString(s):
# print(self.coord_to_name(fn)+" ", end="")
assert self._board[fn] == opponent
tmpHash ^= self._getPositionHash(fn, opponent)
# print()
if tmpHash in self._seenHashes:
return True, tmpHash
return False, tmpHash
def _breadthSearchString(self, fc):
color = self._board[fc]
assert color != Board._COIN
string = set([fc])
frontier = [fc]
while frontier:
current_fc = frontier.pop()
string.add(current_fc)
i = self._neighborsEntries[current_fc]
while self._neighbors[i] != -1:
fn = self._neighbors[i]
i += 1
if self._board[fn] == color and not fn in string:
frontier.append(fn)
return string
def _count_areas(self):
"""Costly function that computes the number of empty positions that only reach respectively BLACK and WHITE
stones (the third values is the number of places touching both colours)"""
to_check = self._empties.copy() # We need to check all the empty positions
only_blacks = 0
only_whites = 0
others = 0
while len(to_check) > 0:
s = to_check.pop()
ssize = 0
assert self._board[s] == Board._EMPTY
frontier = [s]
touched_blacks, touched_whites = 0, 0
currentstring = []
while frontier:
current = frontier.pop()
currentstring.append(current)
ssize += 1 # number of empty places in this loop
assert current not in to_check
i = self._neighborsEntries[current]
while self._neighbors[i] != -1:
n = self._neighbors[i]
i += 1
if self._board[n] == Board._EMPTY and n in to_check:
to_check.remove(n)
frontier.append(n)
elif self._board[n] == Board._BLACK:
touched_blacks += 1
elif self._board[n] == Board._WHITE:
touched_whites += 1
# here we have gathered all the informations about an empty area
assert len(currentstring) == ssize
assert (
(self._nbBLACK == 0 and self._nbWHITE == 0)
or touched_blacks > 0
or touched_whites > 0
)
if touched_blacks == 0 and touched_whites > 0:
only_whites += ssize
elif touched_whites == 0 and touched_blacks > 0:
only_blacks += ssize
else:
others += ssize
return (only_blacks, only_whites, others)
def _piece2str(self, c):
if c == Board._WHITE:
return "O"
elif c == Board._BLACK:
return "X"
elif c == Board._COIN:
return "-"
else:
return "."
def __str__(self):
"""WARNING: this print function does not reflect the classical coordinates. It represents the internal
values in the board."""
toreturn = ""
for i, c in enumerate(self._board):
toreturn += (
self._piece2str(c) + " "
) # +'('+str(i)+":"+str(self._stringUnionFind[i])+","+str(self._stringLiberties[i])+') '
if (i + 1) % Board._BOARDSIZE == 0:
toreturn += "\n"
toreturn += (
"Next player: "
+ ("BLACK" if self._nextPlayer == self._BLACK else "WHITE")
+ "\n"
)
toreturn += (
str(self._nbBLACK)
+ " blacks and "
+ str(self._nbWHITE)
+ " whites on board\n"
)
return toreturn
def pretty_print(self):
return self.prettyPrint()
def prettyPrint(self):
if Board._BOARDSIZE not in [5, 7, 9]:
print(self)
return
print()
print("To Move: ", "black" if self._nextPlayer == Board._BLACK else "white")
print("Last player has passed: ", "yes" if self._lastPlayerHasPassed else "no")
print()
print(" WHITE (O) has captured %d stones" % self._capturedBLACK)
print(" BLACK (X) has captured %d stones" % self._capturedWHITE)
print()
print(" WHITE (O) has %d stones" % self._nbWHITE)
print(" BLACK (X) has %d stones" % self._nbBLACK)
print()
if Board._BOARDSIZE == 9:
specialPoints = [(2, 2), (6, 2), (4, 4), (2, 6), (6, 6)]
headerline = " A B C D E F G H J"
elif Board._BOARDSIZE == 8:
specialPoints = [(2, 2), (5, 2), (5, 5), (2, 5)]
headerline = " A B C D E F G H"
elif Board._BOARDSIZE == 7:
specialPoints = [(2, 2), (4, 2), (3, 3), (2, 4), (4, 4)]
headerline = " A B C D E F G"
else:
specialPoints = [(1, 1), (3, 1), (2, 2), (1, 3), (3, 3)]
headerline = " A B C D E"
print(headerline)
for l in range(Board._BOARDSIZE):
line = Board._BOARDSIZE - l
print(" %d" % line, end="")
for c in range(Board._BOARDSIZE):
p = self._board[Board.flatten((c, Board._BOARDSIZE - l - 1))]
ch = "."
if p == Board._WHITE:
ch = "O"
elif p == Board._BLACK:
ch = "X"
elif p == Board._COIN:
ch = "-"
elif (l, c) in specialPoints:
ch = "+"
print(" " + ch, end="")
print(" %d" % line)
print(headerline)
print("hash = ", self._currentHash)
"""
Internally, the board has a redundant information by keeping track of strings of stones.
"""
def _capture_string(self, fc):
# The Union and Find data structure can efficiently handle
# the string number of which the stone belongs to. However,
# to recover all the stones, given a string number, we must
# search for them.
string = self._breadthSearchString(fc)
for s in string:
if self._nextPlayer == Board._WHITE:
self._capturedBLACK += 1
self._nbBLACK -= 1
else:
self._capturedWHITE += 1
self._nbWHITE -= 1
self._currentHash ^= self._getPositionHash(s, self._board[s])
self._board[s] = self._EMPTY
self._empties.add(s)
i = self._neighborsEntries[s]
while self._neighbors[i] != -1:
fn = self._neighbors[i]
if self._board[fn] != Board._EMPTY:
st = self._getStringOfStone(fn)
if st != s:
self._stringLiberties[st] += 1
i += 1
self._stringUnionFind[s] = -1
self._stringSizes[s] = -1
self._stringLiberties[s] = -1
""" Internal wrapper to full_play_move. Simply translate named move into
internal coordinates system"""
def _play_namedMove(self, m):
if m != "PASS":
return self.play_move(Board.name_to_flat(m))
else:
return self.play_move(-1)
def _draw_cross(self, x, y, w):
toret = (
''
)
toret += (
''
)
return toret
def svg(self):
"""Can be used to get a SVG representation of the board, to be used in a jupyter notebook"""
text_width = 20
nb_cells = self._BOARDSIZE
circle_width = 16
border = 20
width = 40
wmax = str(width * (nb_cells - 1) + border)
board = (
'"
#'\ Hello \
return board