aboutsummaryrefslogtreecommitdiff
path: root/imago/engine
diff options
context:
space:
mode:
Diffstat (limited to 'imago/engine')
-rw-r--r--imago/engine/core.py4
-rw-r--r--imago/engine/imagoIO.py153
-rw-r--r--imago/engine/keras/convNeuralNetwork.py2
-rw-r--r--imago/engine/keras/denseNeuralNetwork.py2
-rw-r--r--imago/engine/keras/keras.py6
-rw-r--r--imago/engine/keras/neuralNetwork.py15
-rw-r--r--imago/engine/monteCarlo.py58
-rw-r--r--imago/engine/parseHelpers.py14
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]