1039 lines
38 KiB
Python
1039 lines
38 KiB
Python
# -*- 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 = (
|
|
'<line x1="'
|
|
+ str(x - w)
|
|
+ '" y1="'
|
|
+ str(y)
|
|
+ '" x2="'
|
|
+ str(x + w)
|
|
+ '" y2="'
|
|
+ str(y)
|
|
+ '" stroke-width="3" stroke="black" />'
|
|
)
|
|
toret += (
|
|
'<line x1="'
|
|
+ str(x)
|
|
+ '" y1="'
|
|
+ str(y - w)
|
|
+ '" x2="'
|
|
+ str(x)
|
|
+ '" y2="'
|
|
+ str(y + w)
|
|
+ '" stroke-width="3" stroke="black" />'
|
|
)
|
|
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 = (
|
|
'<svg height="'
|
|
+ str(text_width + border * 2 + (nb_cells - 1) * width)
|
|
+ '" '
|
|
+ ' width="'
|
|
+ str(text_width + border * 2 + (nb_cells - 1) * width)
|
|
+ '" > '
|
|
)
|
|
|
|
# The ABCD... line
|
|
board += (
|
|
'<svg height="'
|
|
+ str(text_width)
|
|
+ '" width="'
|
|
+ str(text_width + border * 2 + (nb_cells - 1) * width)
|
|
+ '">'
|
|
)
|
|
letters = "ABCDEFGHJ"
|
|
il = 0
|
|
for i in range(
|
|
border + text_width - 5, text_width - 5 + border + nb_cells * width, width
|
|
):
|
|
board += (
|
|
'<text x="'
|
|
+ str(i)
|
|
+ '" y="18" font-size="24" font-color="black">'
|
|
+ letters[il]
|
|
+ "</text>"
|
|
)
|
|
il += 1
|
|
# board += '<rect x=0 y=0 width=20 height=10 stroke="black" />'
|
|
board += "</svg>"
|
|
|
|
# The line numbers
|
|
il = 0
|
|
board += (
|
|
'<svg width="'
|
|
+ str(text_width)
|
|
+ '" height="'
|
|
+ str(text_width + border * 2 + (nb_cells - 1) * width)
|
|
+ '">'
|
|
)
|
|
for i in range(
|
|
border + text_width + 7, text_width + 7 + border + nb_cells * width, width
|
|
):
|
|
board += (
|
|
'<text y="'
|
|
+ str(i)
|
|
+ '" x="0" font-size="24" font-color="black">'
|
|
+ str(9 - il)
|
|
+ "</text>"
|
|
)
|
|
il += 1
|
|
# board += '<rect x=0 y=0 width=20 height=10 stroke="black" />'
|
|
board += "</svg>"
|
|
|
|
# The board by itself
|
|
board += (
|
|
' <svg x="'
|
|
+ str(text_width)
|
|
+ '" y="'
|
|
+ str(text_width)
|
|
+ '" height="'
|
|
+ str(text_width + width * (nb_cells - 1) + 2 * border)
|
|
+ '" width="'
|
|
+ str(text_width + width * (nb_cells - 1) + 2 * border)
|
|
+ '" > '
|
|
+ '<rect x="0" y="0" width="'
|
|
+ str(width * (nb_cells - 1) + 2 * border)
|
|
+ '" height="'
|
|
+ str(width * (nb_cells - 1) + 2 * border)
|
|
+ '" fill="#B4927A" />\
|
|
<line x1="'
|
|
+ str(border)
|
|
+ '" y1="'
|
|
+ str(border)
|
|
+ '" x2="'
|
|
+ str(border)
|
|
+ '" y2="'
|
|
+ wmax
|
|
+ '" stroke-width="4" stroke="black"/>\
|
|
<line x1="'
|
|
+ wmax
|
|
+ '" y1="'
|
|
+ str(border)
|
|
+ '" x2="'
|
|
+ str(border)
|
|
+ '" y2="'
|
|
+ str(border)
|
|
+ '" stroke-width="4" stroke="black"/>\
|
|
<line x1="'
|
|
+ wmax
|
|
+ '" y1="'
|
|
+ wmax
|
|
+ '" x2="'
|
|
+ wmax
|
|
+ '" y2="'
|
|
+ str(border)
|
|
+ '" stroke-width="4" stroke="black"/>\
|
|
<line x1="'
|
|
+ str(border)
|
|
+ '" y1="'
|
|
+ wmax
|
|
+ '" x2="'
|
|
+ wmax
|
|
+ '" y2="'
|
|
+ wmax
|
|
+ '" stroke-width="4" stroke="black"/>'
|
|
)
|
|
|
|
board += self._draw_cross(border + 4 * width, border + 4 * width, width / 3)
|
|
board += self._draw_cross(border + 2 * width, border + 2 * width, width / 3)
|
|
board += self._draw_cross(border + 6 * width, border + 6 * width, width / 3)
|
|
board += self._draw_cross(border + 2 * width, border + 6 * width, width / 3)
|
|
board += self._draw_cross(border + 6 * width, border + 2 * width, width / 3)
|
|
|
|
for i in range(border + width, width * (nb_cells - 2) + 2 * border, width):
|
|
board += (
|
|
'<line x1="'
|
|
+ str(i)
|
|
+ '" y1="'
|
|
+ str(border)
|
|
+ '" x2="'
|
|
+ str(i)
|
|
+ '" y2="'
|
|
+ wmax
|
|
+ '" stroke-width="2" stroke="#444444"/>'
|
|
)
|
|
board += (
|
|
'<line y1="'
|
|
+ str(i)
|
|
+ '" x1="'
|
|
+ str(border)
|
|
+ '" y2="'
|
|
+ str(i)
|
|
+ '" x2="'
|
|
+ wmax
|
|
+ '" stroke-width="2" stroke="#444444"/>'
|
|
)
|
|
|
|
# The stones
|
|
|
|
pieces = [
|
|
(x, y, self._board[Board.flatten((x, y))])
|
|
for x in range(self._BOARDSIZE)
|
|
for y in range(self._BOARDSIZE)
|
|
if self._board[Board.flatten((x, y))] != Board._EMPTY
|
|
]
|
|
for x, y, c in pieces:
|
|
board += (
|
|
'<circle cx="'
|
|
+ str(border + width * x)
|
|
+ '" cy="'
|
|
+ str(border + width * (nb_cells - y - 1))
|
|
+ '" r="'
|
|
+ str(circle_width)
|
|
+ '" stroke="#333333" stroke-width="3" fill="'
|
|
+ ("black" if c == 1 else "white")
|
|
+ '" />'
|
|
)
|
|
|
|
board += "</svg></svg>"
|
|
#'\ <text x="100" y="100" font-size="30" font-color="black"> Hello </text>\
|
|
return board
|