diff --git a/go_ia/.gitignore b/go_ia/.gitignore
index c619c3c..9798d66 100644
--- a/go_ia/.gitignore
+++ b/go_ia/.gitignore
@@ -1,8 +1,10 @@
__pycache__
/venv
/.venv
-/samples-8x8.json.gz
-/samples-8x8.json
+/*.json.gz
+/*.json
*.pt
*.gz
*.txt
+/rendu
+
diff --git a/go_player/.gitignore b/go_player/.gitignore
index 48d6580..44e24de 100644
--- a/go_player/.gitignore
+++ b/go_player/.gitignore
@@ -1,3 +1,6 @@
__pycache__
/venv
/.venv
+/go-package.tgz
+*.ipynb
+/chess
diff --git a/go_player/GnuGo.py b/go_player/GnuGo.py
new file mode 100644
index 0000000..1e862f2
--- /dev/null
+++ b/go_player/GnuGo.py
@@ -0,0 +1,157 @@
+import subprocess, sys
+
+""" Connection with the Go Text Protocol of GNU Go.
+You have to have gnugo installed, and in your exec path."""
+import random
+
+
+class GnuGo:
+
+ def query(self, s):
+ self._stdin.write(s + "\n")
+ ret = []
+ while True:
+ l = self._stdout.readline().rstrip()
+ if l == "":
+ break
+ ret.append(l)
+ if len(ret) == 1 and ret[0].startswith("="):
+ return ("OK", ret[0][1:])
+ elif len(ret) == 0:
+ return ("NOK", None)
+ else:
+ return ("NOK", ret[0])
+
+ def __str__(self):
+ self._stdin.write("showboard\n")
+ ret = []
+ while True:
+ l = self._stdout.readline().rstrip()
+ if l == "":
+ break
+ ret.append(l)
+ return "\n".join(ret[1:])
+
+ def finalScore(self):
+ self._stdin.write("final_score\n")
+ ret = []
+ while True:
+ l = self._stdout.readline().rstrip()
+ if l == "":
+ break
+ ret.append(l)
+ return ret[0][2:]
+
+ class Moves:
+
+ def __init__(self, gnugo):
+ self._nextplayer = "black"
+ self._gnugo = gnugo
+
+ def flip(self):
+ if self._nextplayer == "black":
+ self._nextplayer = "white"
+ else:
+ self._nextplayer = "black"
+
+ def player(self):
+ return self._nextplayer
+
+ def getbest(self):
+ status, toret = self._gnugo.query("reg_genmove " + self._nextplayer)
+ if status == "OK":
+ return toret.strip()
+ return "ERR"
+
+ def get_randomized_best(self):
+ status, toret = self._gnugo.query("experimental_score " + self._nextplayer)
+ if status != "OK":
+ return "ERR"
+ status, toret = self._gnugo.query("top_moves " + self._nextplayer)
+ if status != "OK":
+ return "ERR"
+ moves = []
+ scoremoves = []
+ cumulatedscore = []
+ cumul = 0
+ toread = toret.strip().split()
+ if len(toread) == 0:
+ return "PASS"
+ while len(toread) > 0:
+ m = toread.pop(0)
+ s = float(toread.pop(0))
+ moves.append(m)
+ scoremoves.append(s)
+ cumul += s
+ cumulatedscore.append(cumul)
+ r = random.uniform(0, cumul)
+ i = 0
+ while i < len(moves) and r > cumulatedscore[i]:
+ i += 1
+ if i >= len(moves):
+ i = len(moves) - 1
+ return moves[i]
+
+ def get_history(self):
+ status, toret = self._gnugo.query("move_history")
+ if status != "OK":
+ return "ERR"
+ toread = toret.strip().split()
+ return toread
+
+ def playthis(self, move):
+ status, toret = self._gnugo.query(
+ "play " + self._nextplayer + " " + str(move)
+ )
+ # print(status, toret)
+ self.flip()
+ return status
+
+ def __iter__(self):
+ return self
+
+ def __next__(self):
+ status, toret = self._gnugo.query("genmove " + self._nextplayer)
+ self.flip()
+ if status == "OK":
+ return toret.strip()
+ return "ERR"
+
+ def __init__(self, size, seed=12345678):
+ self._proc = subprocess.Popen(
+ [
+ "gnugo",
+ "--capture-all-dead",
+ "--chinese-rules",
+ "--monte-carlo",
+ "--boardsize",
+ str(size),
+ "--mode",
+ "gtp",
+ "--never-resign",
+ "--seed",
+ str(seed),
+ ],
+ bufsize=1,
+ stdin=subprocess.PIPE,
+ stdout=subprocess.PIPE,
+ universal_newlines=True,
+ )
+ self._stdin = self._proc.stdin
+ self._stdout = self._proc.stdout
+ self._size = size
+ self._nextplayer = "black"
+ (ok, _) = self.query("level 0")
+ assert ok == "OK"
+ (ok, _) = self.query("boardsize " + str(size))
+ assert ok == "OK"
+ (ok, _) = self.query("clear_board")
+ assert ok == "OK"
+ (ok, name) = self.query("name")
+ assert ok == "OK"
+ (ok, version) = self.query("version")
+ assert ok == "OK"
+ # print("Connection to", name.strip(), "(" + version.strip() + ")","Ok")
+ (ok, legal) = self.query("all_legal black")
+ assert ok == "OK"
+ # print("Legal moves: ", legal)
diff --git a/go_player/Goban.py b/go_player/Goban.py
new file mode 100644
index 0000000..e0ccab8
--- /dev/null
+++ b/go_player/Goban.py
@@ -0,0 +1,1038 @@
+# -*- 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
diff --git a/go_player/README.txt b/go_player/README.txt
new file mode 100644
index 0000000..a1b808f
--- /dev/null
+++ b/go_player/README.txt
@@ -0,0 +1,91 @@
+Goban.py
+---------
+
+Fichier contenant les règles du jeu de GO avec les fonctions et méthodes pour parcourir (relativement) efficacement
+l'arbre de jeu, à l'aide de legal_moves() et push()/pop() comme vu en cours.
+
+Ce fichier sera utilisé comme arbitre dans le tournoi. Vous avez maintenant les fonctions de score implantés dedans.
+Sauf problème, ce sera la methode result() qui donnera la vainqueur quand is_game_over() sera Vrai.
+
+Vous avez un décompte plus précis de la victoire dans final_go_score()
+
+Pour vous aider à parcourir le plateau de jeu, si b est un Board(), vous pouvez avoir accès à la couleur de la pierre
+posée en (x,y) en utilisant b[Board.flatten((x,y))]
+
+
+GnuGo.py
+--------
+
+Fichier contenant un ensemble de fonctions pour communiquer avec gnugo. Attention, il faut installer correctement (et
+à part gnugo sur votre machine). Je l'ai testé sur Linux uniquement mais cela doit fonctionner avec tous les autres
+systèmes (même s'ils sont moins bons :)).
+
+
+starter-go.py
+-------------
+
+Exemples de deux développements aléatoires (utilisant legal_moves et push/pop). Le premier utilise legal_moves et le
+second weak_legal_moves, qui ne garanti plus que le coup aléatoire soit vraiment légal (à cause des Ko).
+
+La première chose à faire est probablement de
+
+
+localGame.py
+------------
+
+Permet de lancer un match de myPlayer contre lui même, en vérifiant les coups avec une instanciation de Goban.py comme
+arbitre. Vous ne devez pas modifier ce fichier pour qu'il fonctionne, sans quoi je risque d'avoir des problèmes pour
+faire entrer votre IA dans le tournoi.
+
+
+playerInterface.py
+------------------
+
+Classe abstraite, décrite dans le sujet, permettant à votre joueur d'implanter correctement les fonctions pour être
+utilisé dans localGame et donc, dans le tournoi. Attention, il faut bien faire attention aux coups internes dans Goban
+(appelés "flat") et qui sont utilisés dans legal_moves/weak_legal_moves et push/pop des coups externes qui sont
+utilisés dans l'interface (les named moves). En interne, un coup est un indice dans un tableau 1 dimension
+-1, 0.._BOARDSIZE^2 et en externe (dans cette interface) les coups sont des chaines de caractères dans "A1", ..., "J9",
+"PASS". Il ne faut pas se mélanger les pinceaux.
+
+
+myPlayer.py
+-----------
+
+Fichier que vous devrez modifier pour y mettre votre IA pour le tournoi. En l'état actuel, il contient la copie du
+joueur randomPlayer.py
+
+
+randomPlayer.py
+---------------
+
+Un joueur aléatoire que vous pourrez conserver tel quel
+
+
+gnugoPlayer.py
+--------------
+
+Un joueur basé sur gnugo. Vous permet de vous mesurer à lui simplement.
+
+
+namedGame.py
+------------
+
+Permet de lancer deux joueurs différents l'un contre l'autre.
+Il attent en argument les deux modules des deux joueurs à importer.
+
+
+EXEMPLES DE LIGNES DE COMMANDES:
+================================
+
+python3 localGame.py
+--> Va lancer un match myPlayer.py contre myPlayer.py
+
+python3 namedGame.py myPlayer randomPlayer
+--> Va lancer un match entre votre joueur (NOIRS) et le randomPlayer
+ (BLANC)
+
+ python3 namedGame gnugoPlayer myPlayer
+ --> gnugo (level 0) contre votre joueur (très dur à battre)
+
+
diff --git a/go_player/gnugoPlayer.py b/go_player/gnugoPlayer.py
new file mode 100644
index 0000000..8db2b9d
--- /dev/null
+++ b/go_player/gnugoPlayer.py
@@ -0,0 +1,60 @@
+# -*- coding: utf-8 -*-
+
+import time
+import Goban
+from playerInterface import *
+import GnuGo
+
+
+class myPlayer(PlayerInterface):
+ """Antoher player example that simply act as a wrapper to my GnuGo.py interface. Allows to play against gnugo."""
+
+ def __init__(self):
+ self._board = Goban.Board()
+ self._gnugo = GnuGo.GnuGo(Goban.Board._BOARDSIZE)
+ self._moves = self._gnugo.Moves(self._gnugo)
+ self._mycolor = None
+
+ def getPlayerName(self):
+ return "Gnugo Player"
+
+ def getPlayerMove(self):
+ if self._board.is_game_over():
+ print("Referee told me to play but the game is over!")
+ return "PASS"
+ # gets the legal moves from Goban (just to write them)
+ board_moves = [Goban.Board.flat_to_name(m) for m in self._board.legal_moves()]
+ print(
+ "Board Legal Moves for player "
+ + Goban.Board.player_name(self._board._nextPlayer)
+ )
+ (ok, legal) = self._gnugo.query(
+ "all_legal " + Goban.Board.player_name(self._board._nextPlayer)
+ )
+ print("GNUGO Legal Moves are ", legal[1:])
+
+ move = self._moves.getbest()
+ print(
+ "I am playing ", move
+ ) # New here: allows to consider internal representations of
+ self._board.push(Goban.Board.name_to_flat(move))
+ self._moves.playthis(move)
+ # moves
+ print("My current board :")
+ self._board.prettyPrint()
+ return move
+
+ def playOpponentMove(self, move):
+ print("Opponent played ", move)
+ self._board.push(Goban.Board.name_to_flat(move))
+ self._moves.playthis(move)
+
+ def newGame(self, color):
+ self._mycolor = color
+ self._opponent = Goban.Board.flip(color)
+
+ def endGame(self, winner):
+ if self._mycolor == winner:
+ print("I won!!!")
+ else:
+ print("I lost :(!!")
diff --git a/go_player/localGame.py b/go_player/localGame.py
new file mode 100644
index 0000000..7b5990d
--- /dev/null
+++ b/go_player/localGame.py
@@ -0,0 +1,95 @@
+""""""
+
+import Goban
+import myPlayer
+import randomPlayer
+import time
+from io import StringIO
+import sys
+
+b = Goban.Board()
+
+players = []
+player1 = myPlayer.myPlayer()
+player1.newGame(Goban.Board._BLACK)
+players.append(player1)
+
+player2 = randomPlayer.myPlayer()
+player2.newGame(Goban.Board._WHITE)
+players.append(player2)
+
+totalTime = [0, 0] # total real time for each player
+nextplayer = 0
+nextplayercolor = Goban.Board._BLACK
+nbmoves = 1
+
+outputs = ["", ""]
+sysstdout = sys.stdout
+stringio = StringIO()
+wrongmovefrom = 0
+
+while not b.is_game_over():
+ print("Referee Board:")
+ b.prettyPrint()
+ print("Before move", nbmoves)
+ legals = (
+ b.legal_moves()
+ ) # legal moves are given as internal (flat) coordinates, not A1, A2, ...
+ print(
+ "Legal Moves: ", [b.move_to_str(m) for m in legals]
+ ) # I have to use this wrapper if I want to print them
+ nbmoves += 1
+ otherplayer = (nextplayer + 1) % 2
+ othercolor = Goban.Board.flip(nextplayercolor)
+
+ currentTime = time.time()
+ sys.stdout = stringio
+ move = players[
+ nextplayer
+ ].getPlayerMove() # The move must be given by "A1", ... "J8" string coordinates (not as an internal move)
+ sys.stdout = sysstdout
+ playeroutput = stringio.getvalue()
+ stringio.truncate(0)
+ stringio.seek(0)
+ print(("[Player " + str(nextplayer) + "] ").join(playeroutput.splitlines(True)))
+ outputs[nextplayer] += playeroutput
+ totalTime[nextplayer] += time.time() - currentTime
+ print(
+ "Player ",
+ nextplayercolor,
+ players[nextplayer].getPlayerName(),
+ "plays: " + move,
+ ) # changed
+
+ if not Goban.Board.name_to_flat(move) in legals:
+ print(otherplayer, nextplayer, nextplayercolor)
+ print("Problem: illegal move")
+ wrongmovefrom = nextplayercolor
+ break
+ b.push(
+ Goban.Board.name_to_flat(move)
+ ) # Here I have to internally flatten the move to be able to check it.
+ players[otherplayer].playOpponentMove(move)
+
+ nextplayer = otherplayer
+ nextplayercolor = othercolor
+
+print("The game is over")
+b.prettyPrint()
+result = b.result()
+print("Time:", totalTime)
+print("GO Score:", b.final_go_score())
+print("Winner: ", end="")
+if wrongmovefrom > 0:
+ if wrongmovefrom == b._WHITE:
+ print("BLACK")
+ elif wrongmovefrom == b._BLACK:
+ print("WHITE")
+ else:
+ print("ERROR")
+elif result == "1-0":
+ print("WHITE")
+elif result == "0-1":
+ print("BLACK")
+else:
+ print("DEUCE")
diff --git a/go_player/moveSearch.py b/go_player/moveSearch.py
new file mode 100644
index 0000000..c009618
--- /dev/null
+++ b/go_player/moveSearch.py
@@ -0,0 +1,106 @@
+from sys import stderr
+import time
+import math
+from typing import Any, Callable
+import Goban
+
+
+def _next_color(color):
+ return Goban.Board._BLACK if color == Goban.Board._WHITE else Goban.Board._WHITE
+
+# Returns heuristic, move
+def _alphabeta(
+ board: Goban.Board,
+ heuristic: Callable[[Goban.Board, Any], float],
+ color,
+ move,
+ alpha=-math.inf,
+ beta=math.inf,
+ depth: int = 3,
+) -> tuple[float, Any]:
+
+ wantMax = board.next_player != color
+ if depth == 0 or board.is_game_over():
+ return heuristic(board, color), move
+
+ if wantMax:
+ acc = -math.inf, None
+ for move in board.generate_legal_moves():
+ board.push(move)
+ value = (
+ _alphabeta(
+ board,
+ alpha=alpha,
+ beta=beta,
+ move=move,
+ heuristic=heuristic,
+ color=_next_color(color),
+ depth=depth - 1,
+ )[0],
+ move,
+ )
+ acc = max(
+ acc,
+ value,
+ key=lambda t: t[0],
+ )
+ board.pop()
+
+ if acc[0] >= beta:
+ break # beta cutoff
+ alpha = max(alpha, value[0])
+
+ else:
+ acc = math.inf, None
+ for move in board.generate_legal_moves():
+ board.push(move)
+ value = (
+ _alphabeta(
+ board,
+ alpha=alpha,
+ beta=beta,
+ move=move,
+ heuristic=heuristic,
+ color=_next_color(color),
+ depth=depth - 1,
+ )[0],
+ move,
+ )
+ acc = min(
+ acc,
+ value,
+ key=lambda t: t[0],
+ )
+ board.pop()
+
+ if acc[0] <= alpha:
+ break # alpha cutoff
+ beta = min(beta, value[0])
+
+ return acc
+
+
+def alphabeta(
+ board: Goban.Board,
+ heuristic: Callable[[Goban.Board, Any], float],
+ color,
+ depth: int = 3,
+):
+ _, move = _alphabeta(board, move=-1, heuristic=heuristic, color=color, depth=depth)
+ return move
+
+
+def IDDFS(board: Goban.Board, heuristic, color, duration: float, maxdepth=42):
+ st = time.time()
+ depth = 1
+ move = -1
+
+ while time.time() - st < duration and depth < maxdepth:
+ print("depth:", depth, time.time() - st, file=stderr)
+ move = _alphabeta(
+ board, heuristic, color, move=-1, alpha=-10, beta=10, depth=depth
+ )[1]
+ depth += 1
+
+ print(time.time() - st, duration, depth, file=stderr)
+ return move
diff --git a/go_player/myPlayer.py b/go_player/myPlayer.py
new file mode 100644
index 0000000..7f5a40b
--- /dev/null
+++ b/go_player/myPlayer.py
@@ -0,0 +1,77 @@
+# -*- coding: utf-8 -*-
+"""This is the file you have to modify for the tournament. Your default AI player must be called by this module, in the
+myPlayer class.
+
+Right now, this class contains the copy of the randomPlayer. But you have to change this!
+"""
+
+import time
+import Goban
+from random import choice
+from moveSearch import IDDFS, alphabeta
+from playerInterface import *
+
+
+class myPlayer(PlayerInterface):
+ """
+ Example of a random player for the go. The only tricky part is to be able to handle
+ the internal representation of moves given by legal_moves() and used by push() and
+ to translate them to the GO-move strings "A1", ..., "J8", "PASS". Easy!
+ """
+
+ def __init__(self):
+ self._board = Goban.Board()
+ self._mycolor = None
+ self.moveCount = 0
+
+ def getPlayerName(self):
+ return "xXx_7h3_5cRuM_M45T3r_xXx"
+
+ @staticmethod
+ def simple_heuristic(board, color):
+ # Simple stone difference heuristic
+ score = board.compute_score()
+ return (
+ score[0] - score[1] if color == Goban.Board._BLACK else score[1] - score[0]
+ )
+
+ def getPlayerMove(self):
+ if self._board.is_game_over():
+ print("Referee told me to play but the game is over!")
+ return "PASS"
+
+ if self.moveCount < 10:
+ max_depth = 1
+ elif self.moveCount < 20:
+ max_depth = 2
+ elif self.moveCount < 40:
+ max_depth = 3
+ else:
+ max_depth = 24
+
+ move = IDDFS(
+ self._board, self.simple_heuristic, self._mycolor, duration=1., maxdepth=max_depth
+ ) # IDDFS(self._board, self.simple_heuristic, self._mycolor, 1.)
+ self._board.push(move)
+
+ # New here: allows to consider internal representations of moves
+ # move is an internal representation. To communicate with the interface I need to change if to a string
+ self.moveCount += 1 if Goban.Board.flat_to_name(move) != "PASS" else 0
+ return Goban.Board.flat_to_name(move)
+
+ def playOpponentMove(self, move):
+ print("Opponent played ", move) # New here
+ # the board needs an internal represetation to push the move. Not a string
+ self.moveCount += 1 if move != "PASS" else 0
+ self._board.push(Goban.Board.name_to_flat(move))
+
+ def newGame(self, color):
+ self._mycolor = color
+ self._opponent = Goban.Board.flip(color)
+ self.moveCount = 0
+
+ def endGame(self, winner):
+ if self._mycolor == winner:
+ print("I won!!!")
+ else:
+ print("I lost :(!!")
diff --git a/go_player/namedGame.py b/go_player/namedGame.py
new file mode 100644
index 0000000..50b603b
--- /dev/null
+++ b/go_player/namedGame.py
@@ -0,0 +1,110 @@
+"""Sorry no comments :)."""
+
+import Goban
+import importlib
+import time
+from io import StringIO
+import sys
+
+
+def fileorpackage(name):
+ if name.endswith(".py"):
+ return name[:-3]
+ return name
+
+
+if len(sys.argv) > 2:
+ classNames = [fileorpackage(sys.argv[1]), fileorpackage(sys.argv[2])]
+elif len(sys.argv) > 1:
+ classNames = [fileorpackage(sys.argv[1]), "myPlayer"]
+else:
+ classNames = ["myPlayer", "myPlayer"]
+
+b = Goban.Board()
+
+players = []
+player1class = importlib.import_module(classNames[0])
+player1 = player1class.myPlayer()
+player1.newGame(Goban.Board._BLACK)
+players.append(player1)
+
+player2class = importlib.import_module(classNames[1])
+player2 = player2class.myPlayer()
+player2.newGame(Goban.Board._WHITE)
+players.append(player2)
+
+totalTime = [0, 0] # total real time for each player
+nextplayer = 0
+nextplayercolor = Goban.Board._BLACK
+nbmoves = 1
+
+outputs = ["", ""]
+sysstdout = sys.stdout
+stringio = StringIO()
+wrongmovefrom = 0
+
+while not b.is_game_over():
+ print("Referee Board:")
+ b.prettyPrint()
+ print("Before move", nbmoves)
+ legals = (
+ b.legal_moves()
+ ) # legal moves are given as internal (flat) coordinates, not A1, A2, ...
+ print(
+ "Legal Moves: ", [b.move_to_str(m) for m in legals]
+ ) # I have to use this wrapper if I want to print them
+ nbmoves += 1
+ otherplayer = (nextplayer + 1) % 2
+ othercolor = Goban.Board.flip(nextplayercolor)
+
+ currentTime = time.time()
+ sys.stdout = stringio
+ move = players[
+ nextplayer
+ ].getPlayerMove() # The move must be given by "A1", ... "J8" string coordinates (not as an internal move)
+ sys.stdout = sysstdout
+ playeroutput = stringio.getvalue()
+ stringio.truncate(0)
+ stringio.seek(0)
+ print(("[Player " + str(nextplayer) + "] ").join(playeroutput.splitlines(True)))
+ outputs[nextplayer] += playeroutput
+ totalTime[nextplayer] += time.time() - currentTime
+ print(
+ "Player ",
+ nextplayercolor,
+ players[nextplayer].getPlayerName(),
+ "plays: " + move,
+ ) # changed
+
+ if not Goban.Board.name_to_flat(move) in legals:
+ print(otherplayer, nextplayer, nextplayercolor)
+ print("Problem: illegal move")
+ wrongmovefrom = nextplayercolor
+ break
+ b.push(
+ Goban.Board.name_to_flat(move)
+ ) # Here I have to internally flatten the move to be able to check it.
+ players[otherplayer].playOpponentMove(move)
+
+ nextplayer = otherplayer
+ nextplayercolor = othercolor
+
+print("The game is over")
+b.prettyPrint()
+result = b.result()
+print("Time:", totalTime)
+print("GO Score:", b.final_go_score())
+print("Winner: ", end="")
+if wrongmovefrom > 0:
+ if wrongmovefrom == b._WHITE:
+ print("BLACK")
+ elif wrongmovefrom == b._BLACK:
+ print("WHITE")
+ else:
+ print("ERROR")
+elif result == "1-0":
+ print("WHITE")
+elif result == "0-1":
+ print("BLACK")
+else:
+ print("DEUCE")
diff --git a/go_player/playerInterface.py b/go_player/playerInterface.py
new file mode 100644
index 0000000..b3c6203
--- /dev/null
+++ b/go_player/playerInterface.py
@@ -0,0 +1,50 @@
+class PlayerInterface:
+ """Abstract class that must be implemented by you AI. Typically, a file "myPlayer.py" will implement it for your
+ AI to enter the tournament.
+
+ You may want to check to player implementations of this interface:
+ - the random player
+ - the gnugo player
+ """
+
+ def getPlayerName(self):
+ """Must return the name of your AI player."""
+ return "Not Defined"
+
+ def getPlayerMove(self):
+ """This is where you will put your AI. This function must return the move as a standard
+ move in GO, ie, "A1", "A2", ..., "D5", ..., "J8", "J9" or "PASS"
+
+ WARNING: In the Board class, legal_moves() and weak_legal_moves() are giving internal
+ coordinates only (to speed up the push/pop methods and the game tree traversal). However,
+ to communicate with this interface, you can't use these moves anymore here.
+
+ You have to use the helper function flat_to_name to translate the internal representation of moves
+ in the Goban.py file into a named move.
+
+ The result of this function must be one element of [Board.flat_to_name(m) for m in b.legal_moves()]
+ (it has to be legal, so at the end, weak_legal_moves() may not be sufficient here.)
+ """
+ return "PASS"
+
+ def playOpponentMove(self, move):
+ """Inform you that the oponent has played this move. You must play it with no
+ search (just update your local variables to take it into account)
+
+ The move is given as a GO move string, like "A1", ... "J9", "PASS"
+
+ WARNING: because the method Goban.push(m) needs a move represented as a flat move (integers),
+ you can not directly call this method with the given move here. You will typically call
+ b.push(Board.name_to_flat(move)) to translate the move into its flat (internal) representation.
+ """
+ pass
+
+ def newGame(self, color):
+ """Starts a new game, and give you your color. As defined in Goban.py : color=1
+ for BLACK, and color=2 for WHITE"""
+ pass
+
+ def endGame(self, color):
+ """You can get a feedback on the winner
+ This function gives you the color of the winner"""
+ pass
diff --git a/go_player/randomPlayer.py b/go_player/randomPlayer.py
new file mode 100644
index 0000000..2246938
--- /dev/null
+++ b/go_player/randomPlayer.py
@@ -0,0 +1,49 @@
+# -*- coding: utf-8 -*-
+"""This is the famous random player whici (almost) always looses."""
+
+import time
+import Goban
+from random import choice
+from playerInterface import *
+
+
+class myPlayer(PlayerInterface):
+ """Example of a random player for the go. The only tricky part is to be able to handle
+ the internal representation of moves given by legal_moves() and used by push() and
+ to translate them to the GO-move strings "A1", ..., "J8", "PASS". Easy!
+
+ """
+
+ def __init__(self):
+ self._board = Goban.Board()
+ self._mycolor = None
+
+ def getPlayerName(self):
+ return "Random Player"
+
+ def getPlayerMove(self):
+ if self._board.is_game_over():
+ print("Referee told me to play but the game is over!")
+ return "PASS"
+ moves = self._board.legal_moves() # Dont use weak_legal_moves() here!
+ move = choice(moves)
+ self._board.push(move)
+
+ # New here: allows to consider internal representations of moves
+ # move is an internal representation. To communicate with the interface I need to change if to a string
+ return Goban.Board.flat_to_name(move)
+
+ def playOpponentMove(self, move):
+ print("Opponent played ", move, "i.e. ", move) # New here
+ # the board needs an internal represetation to push the move. Not a string
+ self._board.push(Goban.Board.name_to_flat(move))
+
+ def newGame(self, color):
+ self._mycolor = color
+ self._opponent = Goban.Board.flip(color)
+
+ def endGame(self, winner):
+ if self._mycolor == winner:
+ print("I won!!!")
+ else:
+ print("I lost :(!!")
diff --git a/go_player/requirements.txt b/go_player/requirements.txt
new file mode 100644
index 0000000..24ce15a
--- /dev/null
+++ b/go_player/requirements.txt
@@ -0,0 +1 @@
+numpy
diff --git a/go_player/starter-go.py b/go_player/starter-go.py
new file mode 100644
index 0000000..70ca024
--- /dev/null
+++ b/go_player/starter-go.py
@@ -0,0 +1,68 @@
+import time
+import Goban
+from random import choice
+
+
+def randomMove(b):
+ """Renvoie un mouvement au hasard sur la liste des mouvements possibles. Pour avoir un choix au hasard, il faut
+ construire explicitement tous les mouvements. Or, generate_legal_moves() peut nous donner un itérateur (quand on
+ l'utilise avec pychess)."""
+ return choice(list(b.generate_legal_moves()))
+
+
+def deroulementRandom(b):
+ """Déroulement d'une partie de go au hasard des coups possibles. Cela va donner presque exclusivement
+ des parties très longues et sans gagnant. Cela illustre cependant comment on peut jouer avec la librairie
+ très simplement."""
+ print("----------")
+ b.prettyPrint()
+ if b.is_game_over():
+ print("Resultat : ", b.result())
+ return
+ b.push(randomMove(b))
+ deroulementRandom(b)
+ b.pop()
+
+
+board = Goban.Board()
+deroulementRandom(board)
+
+""" Exemple de déroulement random avec weak_legal_moves()"""
+
+
+def weakRandomMove(b):
+ """Renvoie un mouvement au hasard sur la liste des mouvements possibles mais attention, dans ce cas
+ weak_legal_moves() peut renvoyer des coups qui entrainent des super ko. Si on prend un coup au hasard
+ il y a donc un risque qu'il ne soit pas légal. Du coup, il faudra surveiller si push() nous renvoie
+ bien True et sinon, défaire immédiatement le coup par un pop() et essayer un autre coup.
+ """
+ return choice(b.weak_legal_moves())
+
+
+def weakDeroulementRandom(b):
+ """Déroulement d'une partie de go au hasard des coups possibles. Cela va donner presque exclusivement
+ des parties très longues. Cela illustre cependant comment on peut jouer avec la librairie
+ très simplement en utilisant les coups weak_legal_moves().
+
+ Ce petit exemple montre comment utiliser weak_legal_moves() plutot que legal_moves(). Vous y gagnerez en efficacité.
+ """
+
+ print("----------")
+ b.prettyPrint()
+ if b.is_game_over():
+ print("Resultat : ", b.result())
+ return
+
+ while True:
+ # push peut nous renvoyer faux si le coup demandé n'est pas valide à cause d'un superKo. Dans ce cas il faut
+ # faire un pop() avant de retenter un nouveau coup
+ valid = b.push(weakRandomMove(b))
+ if valid:
+ break
+ b.pop()
+ weakDeroulementRandom(b)
+ b.pop()
+
+
+board = Goban.Board()
+deroulementRandom(board)