From 7b418ab5cb2145ff96f2f381f5d0d64c08c232de Mon Sep 17 00:00:00 2001 From: Nemo D'ACREMONT Date: Sat, 17 May 2025 15:36:19 +0200 Subject: [PATCH] feat: create the basic player using iddfs for go --- go_ia/.gitignore | 6 +- go_player/.gitignore | 3 + go_player/GnuGo.py | 157 +++++ go_player/Goban.py | 1038 ++++++++++++++++++++++++++++++++++ go_player/README.txt | 91 +++ go_player/gnugoPlayer.py | 60 ++ go_player/localGame.py | 95 ++++ go_player/moveSearch.py | 106 ++++ go_player/myPlayer.py | 77 +++ go_player/namedGame.py | 110 ++++ go_player/playerInterface.py | 50 ++ go_player/randomPlayer.py | 49 ++ go_player/requirements.txt | 1 + go_player/starter-go.py | 68 +++ 14 files changed, 1909 insertions(+), 2 deletions(-) create mode 100644 go_player/GnuGo.py create mode 100644 go_player/Goban.py create mode 100644 go_player/README.txt create mode 100644 go_player/gnugoPlayer.py create mode 100644 go_player/localGame.py create mode 100644 go_player/moveSearch.py create mode 100644 go_player/myPlayer.py create mode 100644 go_player/namedGame.py create mode 100644 go_player/playerInterface.py create mode 100644 go_player/randomPlayer.py create mode 100644 go_player/requirements.txt create mode 100644 go_player/starter-go.py 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 = ( + ' ' + ) + + # The ABCD... line + board += ( + '' + ) + letters = "ABCDEFGHJ" + il = 0 + for i in range( + border + text_width - 5, text_width - 5 + border + nb_cells * width, width + ): + board += ( + '' + + letters[il] + + "" + ) + il += 1 + # board += '' + board += "" + + # The line numbers + il = 0 + board += ( + '' + ) + for i in range( + border + text_width + 7, text_width + 7 + border + nb_cells * width, width + ): + board += ( + '' + + str(9 - il) + + "" + ) + il += 1 + # board += '' + board += "" + + # The board by itself + board += ( + ' ' + + '\ + \ + \ + \ + ' + ) + + 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 += ( + '' + ) + board += ( + '' + ) + + # 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 += ( + '' + ) + + 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)