diff options
Diffstat (limited to 'imago/engine')
-rw-r--r-- | imago/engine/core.py | 4 | ||||
-rw-r--r-- | imago/engine/imagoIO.py | 153 | ||||
-rw-r--r-- | imago/engine/keras/convNeuralNetwork.py | 2 | ||||
-rw-r--r-- | imago/engine/keras/denseNeuralNetwork.py | 2 | ||||
-rw-r--r-- | imago/engine/keras/keras.py | 6 | ||||
-rw-r--r-- | imago/engine/keras/neuralNetwork.py | 15 | ||||
-rw-r--r-- | imago/engine/monteCarlo.py | 58 | ||||
-rw-r--r-- | imago/engine/parseHelpers.py | 14 |
8 files changed, 149 insertions, 105 deletions
diff --git a/imago/engine/core.py b/imago/engine/core.py index 2bf7d5a..0f88dfd 100644 --- a/imago/engine/core.py +++ b/imago/engine/core.py @@ -4,14 +4,16 @@ from imago.data.enums import DecisionAlgorithms from imago.engine.createDecisionAlgorithm import create as createDA from imago.gameLogic.gameState import GameState + DEF_SIZE = 9 DEF_KOMI = 5.5 DEF_ALGORITHM = DecisionAlgorithms.KERAS + class GameEngine: """Plays the game of Go.""" - def __init__(self, decisionAlgorithmId = DEF_ALGORITHM): + def __init__(self, decisionAlgorithmId=DEF_ALGORITHM): self.komi = DEF_KOMI self.gameState = GameState(DEF_SIZE) self.daId = decisionAlgorithmId diff --git a/imago/engine/imagoIO.py b/imago/engine/imagoIO.py index 9b3e367..75a1a99 100644 --- a/imago/engine/imagoIO.py +++ b/imago/engine/imagoIO.py @@ -5,40 +5,23 @@ import sys from imago.engine import parseHelpers from imago.engine.core import GameEngine -def _response(text=""): - print("= %s" % text) - print() - -def _responseError(text=""): - print("? %s" % text) - print() - -def protocol_version(_): - """Version of the GTP Protocol""" - _response("2") - -def name(_): - """Name of the engine""" - _response("Imago") - -def version(_): - """Version of the engine""" - _response("0.0.0") def getCoordsText(row, col): """Returns a string representation of row and col. In GTP A1 is bottom left corner. """ - return "%s%d" % (chr(65+row), col+1) + return "%s%d" % (chr(65 + row), col + 1) + class ImagoIO: """Recieves and handles commands.""" - def __init__(self, decisionAlgorithmId = None): + + def __init__(self, decisionAlgorithmId=None, outputStream=sys.stdout): self.commands_set = { - protocol_version, - name, - version, + self.protocol_version, + self.name, + self.version, self.known_command, self.list_commands, self.boardsize, @@ -49,9 +32,22 @@ class ImagoIO: self.set_free_handicap, self.play, self.genmove, - self.undo + self.undo, + self.showboard } self.gameEngine = GameEngine(decisionAlgorithmId) + self.outputStream = outputStream + + + def _response(self, text=""): + print("= %s" % text, file=self.outputStream) + print(file=self.outputStream) + + + def _responseError(self, text=""): + print("? %s" % text.replace('\n', '\n? '), file=self.outputStream) + print(file=self.outputStream) + def start(self): """Starts reading commands interactively.""" @@ -63,7 +59,8 @@ class ImagoIO: continue if input_tokens[0] == "quit": - sys.exit(0) + self._response() + break command = None for comm in self.commands_set: @@ -72,109 +69,139 @@ class ImagoIO: if command is not None: arguments = input_tokens[1:] - #print("[DEBUG]:Selected command: %s; args: %s" % (command, arguments)) command(arguments) else: - _responseError("unknown command") + self._responseError("unknown command") except Exception as err: - _responseError("An uncontrolled error ocurred. The error was: %s" % err) + self._responseError("Error: %s" % err) + def known_command(self, args): - """True if command is known, false otherwise""" + """True if command is known, false otherwise.""" if len(args) != 1: - _responseError("Wrong number of arguments.") - _responseError("Usage: known_command COMMAND_NAME") + self._responseError("Wrong number of arguments\n" + + "Usage: known_command COMMAND_NAME") return out = "false" for c in self.commands_set: if c.__name__ == args[0]: out = "true" - _response(out) + self._response(out) + def list_commands(self, _): - """List of commands, one per row""" + """List of commands, one per row.""" output = "" for c in self.commands_set: output += ("%s - %s\n" % (c.__name__, c.__doc__)) - _response(output) + output = output[:-1] # Remove last newline character + self._response(output) + + + def protocol_version(self, _): + """Version of the GTP Protocol.""" + self._response("2") + + + def name(self, _): + """Name of the engine.""" + self._response("Imago") + + + def version(self, _): + """Version of the engine.""" + self._response("0.0.0") + def boardsize(self, args): """Changes the size of the board. Board state, number of stones and move history become arbitrary. - It is wise to call clear_board after this command. - """ + It is wise to call clear_board after this command.""" if len(args) != 1: - _responseError("Wrong number of arguments") - _responseError("Usag. boardsize <newSize>") + self._responseError("Wrong number of arguments\n" + + "Usage: boardsize <newSize>") return size = int(args[0]) self.gameEngine.setBoardsize(size) - _response() + self._response() + def clear_board(self, _): """The board is cleared, the number of captured stones reset to zero and the move - history reset to empty. - """ + history reset to empty.""" self.gameEngine.clearBoard() - _response() + self._response() + def komi(self, args): """Sets a new value of komi.""" if len(args) != 1: - _responseError("Wrong number of arguments") - _responseError("Usage: komi <newKomi>") + self._responseError("Wrong number of arguments\n" + + "Usage: komi <newKomi>") return komi = float(args[0]) self.gameEngine.setKomi(komi) - _response() + self._response() + def fixed_handicap(self, args): """Handicap stones are placed on the board on standard vertices. - These vertices follow the GTP specification. - """ + These vertices follow the GTP specification.""" if len(args) != 1: - _responseError("Wrong number of arguments") - _responseError("Usage: fixed_handicap <count>") + self._responseError("Wrong number of arguments\n" + + "Usage: fixed_handicap <count>") return stones = float(args[0]) vertices = self.gameEngine.setFixedHandicap(stones) out = getCoordsText(vertices[0][0], vertices[0][1]) for vertex in vertices[1:]: out += " " + getCoordsText(vertex[0], vertex[1]) - _response(out) + self._response(out) + def place_free_handicap(self, args): """Handicap stones are placed on the board by the AI criteria.""" #TODO + def set_free_handicap(self, args): """Handicap stones are placed on the board as requested.""" #TODO + def play(self, args): """A stone of the requested color is played at the requested vertex.""" if len(args) != 2: - _responseError("Wrong number of arguments.") - _responseError("Usage: play <color> <vertex>") + self._responseError("Wrong number of arguments\n" + + "Usage: play <color> <vertex>") return - move = parseHelpers.parseMove(args, self.gameEngine.gameState.size) - self.gameEngine.play(move.color, move.vertex) - _response() + try: + move = parseHelpers.parseMove(args, self.gameEngine.gameState.size) + self.gameEngine.play(move.color, move.vertex) + self._response() + except Exception as err: + self._responseError("Invalid move: %s" % err) + def genmove(self, args): """A stone of the requested color is played where the engine chooses.""" if len(args) != 1: - _responseError("Wrong number of arguments.") - _responseError("Usage: genmove <color>") + self._responseError("Wrong number of arguments\n" + + "Usage: genmove <color>") return color = parseHelpers.parseColor(args[0]) output = parseHelpers.vertexToString(self.gameEngine.genmove(color), - self.gameEngine.gameState.size) - _response(output) - self.gameEngine.gameState.getBoard().printBoard() + self.gameEngine.gameState.size) + self._response(output) + def undo(self, _): """The board configuration and number of captured stones are reset to the state - before the last move, which is removed from the move history. - """ + before the last move, which is removed from the move history.""" self.gameEngine.undo() + self._response() + + + def showboard(self, _): + """Prints a representation of the board state.""" + self._response('\n%s' % self.gameEngine.gameState.getBoard().toString()) diff --git a/imago/engine/keras/convNeuralNetwork.py b/imago/engine/keras/convNeuralNetwork.py index 638e2fe..7e6e63c 100644 --- a/imago/engine/keras/convNeuralNetwork.py +++ b/imago/engine/keras/convNeuralNetwork.py @@ -43,7 +43,7 @@ class ConvNeuralNetwork(NeuralNetwork): ), ]) - model.summary() + #model.summary() model.compile( optimizer=Adam(learning_rate=0.0001), diff --git a/imago/engine/keras/denseNeuralNetwork.py b/imago/engine/keras/denseNeuralNetwork.py index 6a350f7..4b4c0e0 100644 --- a/imago/engine/keras/denseNeuralNetwork.py +++ b/imago/engine/keras/denseNeuralNetwork.py @@ -29,7 +29,7 @@ class DenseNeuralNetwork(NeuralNetwork): ), ]) - model.summary() + #model.summary() model.compile( optimizer=Adam(learning_rate=0.0001), diff --git a/imago/engine/keras/keras.py b/imago/engine/keras/keras.py index 871f4a0..b3061e8 100644 --- a/imago/engine/keras/keras.py +++ b/imago/engine/keras/keras.py @@ -34,7 +34,7 @@ class Keras(DecisionAlgorithm): if coords == "pass": self.currentMove = self.currentMove.addPass() else: - self.currentMove = self.currentMove.addMoveByCoords(coords) + self.currentMove = self.currentMove.addMove(coords) def pickMove(self): """Returns a move to play.""" @@ -47,3 +47,7 @@ class Keras(DecisionAlgorithm): """Empties move history.""" boardSize = self.currentMove.board.getBoardHeight() self.currentMove = GameMove(GameBoard(boardSize, boardSize)) + + +if __name__ == '__main__': + unittest.main() diff --git a/imago/engine/keras/neuralNetwork.py b/imago/engine/keras/neuralNetwork.py index c414a78..9d0b853 100644 --- a/imago/engine/keras/neuralNetwork.py +++ b/imago/engine/keras/neuralNetwork.py @@ -7,6 +7,9 @@ import os.path import numpy from matplotlib import pyplot +# Disable TensorFlow importing warnings +os.environ['TF_CPP_MIN_LOG_LEVEL'] = '3' + from tensorflow.keras.models import load_model from tensorflow.keras.utils import plot_model @@ -28,12 +31,12 @@ class NeuralNetwork: self.model = self._loadModel(self.path) except FileNotFoundError: self.model = self._initModel(boardSize) - self.saveModelPlot("model.png") + #self.saveModelPlot("model.png") def _initModel(self, boardSize=DEF_BOARD_SIZE): raise NotImplementedError("Tried to directly use NeuralNetwork class. Use one of the subclasses instead.") - def trainModel(self, games): + def trainModel(self, games, epochs=20, verbose=2): trainMoves = [] targets = [] for game in games: @@ -48,14 +51,14 @@ class NeuralNetwork: y=targets, validation_split=0.1, batch_size=1, - epochs=20, + epochs=epochs, shuffle=False, - verbose=2 + verbose=verbose ) def _loadModel(self, modelPath): # Load model - if os.path.isfile(modelPath): + if os.path.isfile(modelPath) or os.path.isdir(modelPath): return load_model(modelPath) else: raise FileNotFoundError("Keras neural network model file not found at %s" @@ -145,7 +148,7 @@ class NeuralNetwork: return self.model.predict( x = sampleBoards, batch_size = 1, - verbose = 2) + verbose = 0) def _saveHeatmap(self, data, passChance): rows = len(data) diff --git a/imago/engine/monteCarlo.py b/imago/engine/monteCarlo.py index baaaba8..ee8380a 100644 --- a/imago/engine/monteCarlo.py +++ b/imago/engine/monteCarlo.py @@ -11,26 +11,30 @@ class MCTS(DecisionAlgorithm): def __init__(self, move): self.root = MCTSNode(move, None) + self.expansions = 5 + self.simulationsPerExpansion = 10 + self.debug = False - def forceNextMove(self, coords): + def forceNextMove(self, coords: tuple): """Selects given move as next move.""" - if coords == "pass": - raise NotImplementedError("Pass not implemented for MCTS algorithm.") for node in self.root.children: - if (node.move.getRow() == coords[0] - and node.move.getCol() == coords[1]): + if (node.move.isPass and coords == "pass" or + node.move.getRow() == coords[0] + and node.move.getCol() == coords[1]): self.root = node return self.root = self.root.expansionForCoords(coords) def pickMove(self): """ - Performs an exploratory cycle, updates the root to the best node and returns its - corresponding move.""" + Performs an exploratory cycle and returns the coordinates of the picked move.""" #NOTE: with only one selection-expansion the match is # completely random - for _ in range(5): - self.root.selection().expansion().simulation(10, 20) + for _ in range(self.expansions): + self.root\ + .selection()\ + .expansion()\ + .simulation(self.simulationsPerExpansion, 20, self.debug) selectedNode = self._selectBestNextNode() return selectedNode.move.coords @@ -63,6 +67,7 @@ class MCTSNode: self.parent = parent self.children = set() self.unexploredVertices = move.getPlayableVertices() + self.unexploredVertices.add("pass") def ucbForPlayer(self): """ @@ -133,17 +138,17 @@ class MCTSNode: """ Adds a move for the given coordinates as a new node to the children of this node.""" - newMove = self.move.addMove(coords[0], coords[1]) + newMove = self.move.addMove(coords) newNode = MCTSNode(newMove, self) self.children.add(newNode) - self.unexploredVertices.remove((coords[0], coords[1])) + self.unexploredVertices.remove(coords) return newNode - def simulation(self, nMatches, scoreDiffHeur): + def simulation(self, nMatches, scoreDiffHeur, debug=False): """Play random matches to accumulate reward information on the node.""" scoreAcc = 0 for _ in range(nMatches): - result = self._randomMatch(scoreDiffHeur) + result = self._randomMatch(scoreDiffHeur, debug) self.visits += 1 scoreDiff = result[0]-result[1] if scoreDiff != 0: @@ -155,10 +160,10 @@ class MCTSNode: node.visits += nMatches node = node.parent - def _randomMatch(self, scoreDiffHeur): + def _randomMatch(self, scoreDiffHeur, debug=False): """Play a random match and return the resulting score.""" - #IMPORTANT: the score heuristic doesn't work for the first move of the game, since - #the black player holds all except for one vertex! + #NOTE: IMPORTANT: the score heuristic doesn't work for the first move of the game, + # since the black player holds all except for one vertex! currentMove = self.move score = currentMove.board.score() while currentMove.getGameLength() < 5 or abs(score[0] - score[1]) < scoreDiffHeur: @@ -169,16 +174,19 @@ class MCTSNode: currentMove = currentMove.addPass() else: selectedMove = random.choice(list(sensibleMoves)) - currentMove = currentMove.addMoveByCoords(selectedMove) + currentMove = currentMove.addMove(selectedMove) score = currentMove.board.score() - print("Current move: %s" % (str(currentMove))) - print("Current move game length: ", currentMove.getGameLength()) - print("Score of the board: %d, %d (%d)" - % (score[0], - score[1], - score[0]-score[1]) - ) - currentMove.printBoard() + + if debug: + print("Current move: %s" % (str(currentMove))) + print("Current move game length: ", currentMove.getGameLength()) + print("Score of the board: %d, %d (%d)" + % (score[0], + score[1], + score[0]-score[1]) + ) + currentMove.printBoard() + return score def _printBoardInfo(self): diff --git a/imago/engine/parseHelpers.py b/imago/engine/parseHelpers.py index fc42845..64342d3 100644 --- a/imago/engine/parseHelpers.py +++ b/imago/engine/parseHelpers.py @@ -19,7 +19,7 @@ def parseMove(args, boardsize): """Converts the textual representation of a move to a move instance.""" if len(args) != 2: raise RuntimeError( - "Unable to transform string %s to move: Wrong format." + "Unable to transform string [%s] to move: Wrong format." % args) color = parseColor(args[0]) vertex = parseVertex(args[1], boardsize) @@ -32,7 +32,7 @@ def parseColor(text): return Player.WHITE if textUpper in VALID_BLACK_STRINGS: return Player.BLACK - raise RuntimeError("Unknown color %s." % text) + raise RuntimeError("Unknown color [%s]." % text) def parseVertex(text, boardSize): """Returns row and column of a vertex given its input string. A vertex can also be the @@ -47,7 +47,7 @@ def parseVertex(text, boardSize): if text == "PASS": return "pass" raise RuntimeError( - "Unable to transform string %s to vertex. Wrong format." + "Unable to transform string [%s] to vertex. Wrong format." % text) vertexCol = ord(text[0]) @@ -61,10 +61,10 @@ def parseVertex(text, boardSize): if (vertexCol < 0 or vertexRow < 0 or vertexCol >= boardSize or vertexRow >= boardSize): raise RuntimeError( - "Unable to transform string %s to vertex. Maps to [%d, %d], which is out of board bounds (size %d)" + "Unable to transform string [%s] to vertex. Maps to [%d, %d], which is out of board bounds (size [%d])" % (text, vertexCol, vertexRow, boardSize)) - return [vertexRow, vertexCol] + return (vertexRow, vertexCol) def vertexToString(vertex, boardSize): """Returns a string representing the vertex. @@ -76,11 +76,11 @@ def vertexToString(vertex, boardSize): return "pass" if len(vertex) != 2: raise RuntimeError( - "Unable to transform vertex %s to string. Too many elements." + "Unable to transform vertex [%s] to string. Too many elements." % str(vertex)) if vertex[0] >= boardSize or vertex[1] >= boardSize or vertex[0] < 0 or vertex[1] < 0: raise RuntimeError( - "Unable to transform vertex %s to string. Vertex out of board bounds (size %d)" + "Unable to transform vertex [%s] to string. Vertex out of board bounds (size [%d])" % (str(vertex), boardSize)) vertexRow = boardSize - vertex[0] |