From 65ac3a6b050dcb88688cdc2654b1ed6693e9a160 Mon Sep 17 00:00:00 2001 From: InigoGutierrez Date: Mon, 12 Jun 2023 19:43:40 +0200 Subject: Submitted version. --- doc/Makefile | 2 +- doc/listings/testOutput.txt | 53 +++++------ doc/tex/appendixes.tex | 48 ++++++---- doc/tex/imago.tex | 25 +++-- doc/tex/implementation.tex | 83 ++++++++++++++++- imago/engine/core.py | 1 + imago/engine/imagoIO.py | 22 ++--- imago/engine/keras/convNeuralNetwork.py | 2 +- imago/engine/keras/denseNeuralNetwork.py | 2 +- imago/engine/keras/initialDenseNeuralNetwork.py | 28 ------ imago/engine/keras/keras.py | 4 + imago/engine/keras/neuralNetwork.py | 15 +-- imago/gameLogic/gameBoard.py | 9 +- imago/gameLogic/gameData.old | 52 +++++++++++ imago/gameLogic/gameData.py | 51 ---------- imago/sgfParser/astNode.py | 119 ++++++++++++------------ tests/test_gameBoard.py | 28 ++++++ tests/test_gameState.py | 2 +- tests/test_imagoIO.py | 9 +- tests/test_monteCarlo.py | 2 +- tests/test_neuralNetwork.py | 68 ++++++++++++++ tests/test_sgf.py | 27 ++++++ 22 files changed, 423 insertions(+), 229 deletions(-) delete mode 100644 imago/engine/keras/initialDenseNeuralNetwork.py create mode 100644 imago/gameLogic/gameData.old delete mode 100644 imago/gameLogic/gameData.py create mode 100644 tests/test_sgf.py diff --git a/doc/Makefile b/doc/Makefile index cf6c166..472de8e 100644 --- a/doc/Makefile +++ b/doc/Makefile @@ -9,7 +9,7 @@ diagramImgs = diagrams/planningWorkPlanEngine.png diagrams/planningWorkPlanGame. imgs = img/imago.jpg img/models/denseModel.png img/models/convModel.png -listings = listings/denseModel.txt listings/convModel.txt listings/denseTraining.txt listings/convTraining.txt listings/trainCommand.sh +listings = listings/denseModel.txt listings/convModel.txt listings/denseTraining.txt listings/convTraining.txt listings/trainCommand.sh listings/testOutput.txt all: $(docName).pdf diff --git a/doc/listings/testOutput.txt b/doc/listings/testOutput.txt index 7d1cfda..e6fb2d6 100644 --- a/doc/listings/testOutput.txt +++ b/doc/listings/testOutput.txt @@ -1,29 +1,24 @@ -Invalid Move! Move outside board bounds. -Name Stmts Miss Cover Missing -------------------------------------------------------------------------------- -imago/data/enums.py 17 0 100% -imago/engine/core.py 39 8 79% 43, 50-52, 60-62, 68 -imago/engine/createDecisionAlgorithm.py 11 5 55% 13-21 -imago/engine/decisionAlgorithm.py 9 4 56% 6, 10, 14, 18 -imago/engine/imagoIO.py 103 10 90% 75-76, 190-198, 205 -imago/engine/keras/convNeuralNetwork.py 12 12 0% 3-54 -imago/engine/keras/denseNeuralNetwork.py 12 12 0% 3-40 -imago/engine/keras/initialDenseNeuralNetwork.py 11 11 0% 3-28 -imago/engine/keras/keras.py 28 15 46% 16-27, 34-37, 41, 48-49 -imago/engine/keras/neuralNetwork.py 137 112 18% 23-31, 34, 37-46, 58-61, 66-69, 72-80, 85-95, 100-112, 117-139, 142-145, 151-186, 195-203, 206 -imago/engine/monteCarlo.py 110 8 93% 128, 181-190, 194-195 -imago/engine/parseHelpers.py 48 0 100% -imago/gameLogic/gameBoard.py 199 26 87% 115, 177, 202, 269, 278-303, 311 -imago/gameLogic/gameData.py 24 24 0% 3-51 -imago/gameLogic/gameMove.py 95 13 86% 21, 27, 34, 131-134, 138-142, 146 -imago/gameLogic/gameState.py 42 0 100% -imago/scripts/monteCarloSimulation.py 17 17 0% 3-25 -imago/sgfParser/astNode.py 125 125 0% 1-156 -imago/sgfParser/parsetab.py 18 18 0% 5-28 -imago/sgfParser/sgf.py 6 6 0% 3-13 -imago/sgfParser/sgflex.py 31 31 0% 5-71 -imago/sgfParser/sgfyacc.py 41 41 0% 5-71 -------------------------------------------------------------------------------- -TOTAL 1135 498 56% - -8 empty files skipped. +Name Stmts Miss Cover Missing +------------------------------------------------------------------------ +imago/data/enums.py 17 0 100% +imago/engine/core.py 39 7 82% 43, 50-52, 60-62 +imago/engine/createDecisionAlgorithm.py 11 5 55% 13-21 +imago/engine/decisionAlgorithm.py 9 4 56% 6, 10, 14, 18 +imago/engine/imagoIO.py 107 9 92% 76-77, 189-196, 208 +imago/engine/keras/convNeuralNetwork.py 11 0 100% +imago/engine/keras/denseNeuralNetwork.py 11 0 100% +imago/engine/keras/keras.py 30 3 90% 48-49, 53 +imago/engine/keras/neuralNetwork.py 137 1 99% 141 +imago/engine/monteCarlo.py 110 7 94% 128, 181-188, 194-195 +imago/engine/parseHelpers.py 48 0 100% +imago/gameLogic/gameBoard.py 205 7 97% 116, 180, 205, 272, 284, 306, 312 +imago/gameLogic/gameMove.py 95 9 91% 21, 27, 34, 138-142, 146 +imago/gameLogic/gameState.py 41 0 100% +imago/scripts/monteCarloSimulation.py 17 17 0% 3-25 +imago/sgfParser/astNode.py 70 7 90% 10, 14, 22, 45, 59, 67, 157 +imago/sgfParser/parsetab.py 18 0 100% +imago/sgfParser/sgf.py 6 0 100% +imago/sgfParser/sgflex.py 31 9 71% 37-38, 42-43, 47-48, 64-65, 71 +imago/sgfParser/sgfyacc.py 41 14 66% 35-36, 51, 59-68, 71 +------------------------------------------------------------------------ +TOTAL 1054 99 91% diff --git a/doc/tex/appendixes.tex b/doc/tex/appendixes.tex index f9f189e..b4533d6 100644 --- a/doc/tex/appendixes.tex +++ b/doc/tex/appendixes.tex @@ -76,7 +76,7 @@ input or because of the \gls{ko} rule. \subsubsection{The engine: the \texttt{imagocli.py} interface} -\texttt{imagocli.py} is a text interface which follows the \acrshort{gtp} +\texttt{imagocli.py} is a text interface which follows the \acrfull{gtp} specification. It can be executed in a shell as: { @@ -87,8 +87,8 @@ specification. It can be executed in a shell as: \par } -The \acrshort{ai} to be run can be passes as an argument to the \texttt{-e} -option. The available arguments are: +If desired, the \acrshort{ai} to be run can be passed as an argument to the +\texttt{-e} option, but it is not necessary. The available arguments are: \begin{itemize} @@ -111,11 +111,11 @@ be executed as: \par } -If no arguments are provided, the default configuration is to use the Monte -Carlo Tree Search algorithm. +If no arguments are provided the default configuration is to use the Monte Carlo +Tree Search algorithm. When executed interactively and before any input is provided it just waits for -input, with no prompt whatsoever. This is in compliance with the \acrshort{gtp} +input with no prompt whatsoever. This is in compliance with the \acrshort{gtp} specification. These are the commands that the program knows and a short description of what @@ -125,8 +125,8 @@ each does: \item \texttt{list\_commands}: Shows a list of the commands the engine knows. - \item \texttt{known\_command}: Receives an argument and tells wether it is a - known command or not. + \item \texttt{known\_command}: Receives an argument and tells whether it is + a known command or not. \item \texttt{name}: Shows the name of the program. \item \texttt{version}: Shows the version of the program. \item \texttt{protocol\_version}: Shows the implemented \acrshort{gtp} @@ -320,13 +320,23 @@ The costs are calculated based on a programmer salary of 20€/hour. \midrule Game preliminary research & 15 & 300 \\ \midrule - Game implementation & 55 & 1100 \\ + Game implementation & 95 & 1900 \\ \midrule - Game unit testing & 50 & 1000 \\ + Game unit testing & 90 & 1800 \\ \midrule - Game system testing & 5 & 100 \\ + Game system testing & 15 & 300 \\ \midrule - \textbf{Total} & \textbf{125} & \textbf{2500} \\ + Engine preliminary research & 15 & 300 \\ + \midrule + Engine implementation & 75 & 1500 \\ + \midrule + Algorithms implementations & 135 & 2700 \\ + \midrule + Engine testing & 75 & 1500 \\ + \midrule + Results analysis & 30 & 600 \\ + \midrule + \textbf{Total} & \textbf{545} & \textbf{10900} \\ \bottomrule \end{tabular} } @@ -354,11 +364,11 @@ The costs are calculated based on a programmer salary of 20€/hour. \toprule \textbf{Category} & \textbf{Cost (€)} \\ \midrule - Work & 2500 \\ + Work & 10900 \\ \midrule Materials & 600 \\ \midrule - \textbf{Total} & \textbf{3100} \\ + \textbf{Total} & \textbf{11500} \\ \bottomrule \end{tabular} } @@ -372,17 +382,17 @@ The costs are calculated based on a programmer salary of 20€/hour. \toprule \textbf{Task} & \textbf{Cost (€)} \\ \midrule - Game preliminary research & 300 \\ + Game system development & 2200 \\ \midrule - Game implementation & 1100 \\ + Engine development & 4500 \\ \midrule - Game unit testing & 1000 \\ + Testing & 3600 \\ \midrule - Game system testing & 100 \\ + Result analysis & 600 \\ \midrule Materials & 600 \\ \midrule - \textbf{Total} & \textbf{3100} \\ + \textbf{Total} & \textbf{11500} \\ \bottomrule \end{tabular} } diff --git a/doc/tex/imago.tex b/doc/tex/imago.tex index 4287330..ca72c7a 100644 --- a/doc/tex/imago.tex +++ b/doc/tex/imago.tex @@ -51,12 +51,13 @@ \includegraphics[width=0.3\textwidth]{img/logoEII.png} \end{center}~\\[10pt] \program\\ - \large An AI player of the game of Go + \large An AI player of the game of Go\\ + \large (Juego Go basado en inteligencia artificial)\\ } \author{Íñigo Gutiérrez Fernández} -\date{} +\date{Oviedo, June 2023} \maketitle @@ -71,12 +72,20 @@ \clearpage \begin{abstract} - The game of Go presents a complex problem for machine learning by virtue of - containing a very wide and deep decision tree. This project has tried to - tackle the problem by using different decision algorithms and also provides - a full implementation of the game. These tools could be used by players and - developers as a foundation for other machine learning projects or to simply - keep studying the game. + With a history of more than 3000 years, the game of Go presents a complex + problem for machine learning by virtue of containing a very wide and deep + decision tree. Finally, in 2016, computer scientists from DeepMind were able + to create an artificial intelligence capable of defeating profesional + players of the game with a combination of old and new strategies. This + project has tried to follow their steps and tackle the problem by using + different decision algorithms, such as Monte Carlo Tree Search and neural + networks, and also provides a full implementation of the game, playable on + its own or available as a library for the engine developed for this project + and potentially others to come. The resulting strength of the developed + algorithms is no match to that of a profesional player, but it shows the + possibilities achievable just with the limited resources employed on this + project. These tools could be used by players and developers as a foundation + for other machine learning projects or to simply keep studying the game. \end{abstract} \clearpage diff --git a/doc/tex/implementation.tex b/doc/tex/implementation.tex index 4970c14..d3cd0c3 100644 --- a/doc/tex/implementation.tex +++ b/doc/tex/implementation.tex @@ -98,6 +98,9 @@ A version control tool widely used in software development environments allows to store and manage snapshots of a project, navigate through them and diverge into different development branches, among other useful features. +The source code of this document and of the rest of the project is publicly +available at \url{https://git.taamas.xyz/Taamas/imago}. + \subsubsection{Documentation Tools} \paragraph{\LaTeX} @@ -260,11 +263,13 @@ The script used to run the tests is shown on \lref{lst:test} and its output on \subsubsection{Usability Testing} -A human user was asked to interact with the interfaces of \program{} and -presented with a questionary. The profile of this user is of someone who has -played some Go matches and knows the fundamentals of the game but is a beginner, -and who has little experience with computers outside of their usage as office -tools and internet browsers. Here are their answers. +Two human users were asked to interact with the interfaces of \program{} and +presented with a questionary. + +The profile of the first user is of someone who has played some Go matches and +knows the fundamentals of the game but is a beginner, and who has little +experience with computers outside of their usage as office tools and internet +browsers. Here are their answers. \vspace{\interclassSpace} @@ -330,3 +335,71 @@ tools and internet browsers. Here are their answers. Yes.\\ \bottomrule \end{tabular} + +The profile of the second user is of someone who has experience with computers +and works as a software developer, but who has just the bare minimum knowledge +of the game of Go. Here are their answers. + +\vspace{\interclassSpace} + +\begin{tabular}{p{0.4\linewidth}p{0.6\linewidth}} + \toprule + \multicolumn{2}{c}{\textbf{Playing against a human}} \\ + \midrule + \textbf{Question} & \textbf{Answer} \\ + \midrule + Were you able to start the interface? & + Yes, I was able to. \\ + \midrule + How hard was the interface of the game to understand? & + I think six out of ten. Some people won't understand what a command is and + without a visual interface they could feel confused about it.\\ + \bottomrule +\end{tabular} + +\vspace{\interclassSpace} + +\begin{tabular}{p{0.4\linewidth}p{0.6\linewidth}} + \toprule + \multicolumn{2}{c}{\textbf{Playing against the engine}} \\ + \midrule + \textbf{Question} & \textbf{Answer} \\ + \midrule + Were you able to start the interface? & + Yes, I was. \\ + \midrule + How hard was the interface of the game to understand? & + I think seven out of ten. I was expecting to follow some steps so I could + execute it at the same time I was reading the manual, but in the end the + most practical thing was read everything before execute commands.\\ + \midrule + How strong did you find the engine? & + It followed good paths to win the game, I didn't feel random moves during + the game by its side.\\ + \bottomrule +\end{tabular} + +\vspace{\interclassSpace} + +\begin{tabular}{p{0.4\linewidth}p{0.6\linewidth}} + \toprule + \multicolumn{2}{c}{\textbf{Playing against the interface through a + third-party}} \\ + \midrule + \textbf{Question} & \textbf{Answer} \\ + \midrule + Were you able to start the interface? & + Yes, I was. \\ + \midrule + Did you find any problems when setting up the engine? & + No, I didn't.\\ + \midrule + Do you think this tool has value for studying Go? & + I think it is a good tool for a group of players, so they can practise and + even train the AI if they want to.\\ + \bottomrule +\end{tabular} + +The results of these usability tests were useful mostly to update the manual and +make it easier to understand and follow and also to address some problems with +the interfaces that raised up during the testing. diff --git a/imago/engine/core.py b/imago/engine/core.py index 47c8f16..0f88dfd 100644 --- a/imago/engine/core.py +++ b/imago/engine/core.py @@ -4,6 +4,7 @@ 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 diff --git a/imago/engine/imagoIO.py b/imago/engine/imagoIO.py index ae1210b..75a1a99 100644 --- a/imago/engine/imagoIO.py +++ b/imago/engine/imagoIO.py @@ -6,7 +6,6 @@ from imago.engine import parseHelpers from imago.engine.core import GameEngine - def getCoordsText(row, col): """Returns a string representation of row and col. In GTP A1 is bottom left corner. @@ -91,33 +90,33 @@ class ImagoIO: 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__)) + output = output[:-1] # Remove last newline character self._response(output) def protocol_version(self, _): - """Version of the GTP Protocol""" + """Version of the GTP Protocol.""" self._response("2") def name(self, _): - """Name of the engine""" + """Name of the engine.""" self._response("Imago") def version(self, _): - """Version of the engine""" + """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: self._responseError("Wrong number of arguments\n" + "Usage: boardsize ") @@ -129,8 +128,7 @@ class ImagoIO: 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() self._response() @@ -148,8 +146,7 @@ class ImagoIO: 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: self._responseError("Wrong number of arguments\n" + "Usage: fixed_handicap ") @@ -200,8 +197,7 @@ class ImagoIO: 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() 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/initialDenseNeuralNetwork.py b/imago/engine/keras/initialDenseNeuralNetwork.py deleted file mode 100644 index dfe8379..0000000 --- a/imago/engine/keras/initialDenseNeuralNetwork.py +++ /dev/null @@ -1,28 +0,0 @@ -"""Dense neural network.""" - -from tensorflow.keras.models import Sequential -from tensorflow.keras.layers import Dense -from tensorflow.keras.optimizers import Adam - -from imago.engine.keras.neuralNetwork import NeuralNetwork - -defaultModelFile = 'models/imagoDenseKerasModel.h5' - -class DenseNeuralNetwork(NeuralNetwork): - - def _initModel(self, boardSize=NeuralNetwork.DEF_BOARD_SIZE): - model = Sequential([ - Dense(units=16, activation='relu', input_shape=(boardSize,boardSize)), - Dense(units=32, activation='relu'), - Dense(units=boardSize, activation='softmax') - ]) - - model.summary() - - model.compile( - optimizer=Adam(learning_rate=0.0001), - loss='categorical_crossentropy', - metrics=['accuracy'] - ) - - return model diff --git a/imago/engine/keras/keras.py b/imago/engine/keras/keras.py index 3d60b8e..b3061e8 100644 --- a/imago/engine/keras/keras.py +++ b/imago/engine/keras/keras.py @@ -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/gameLogic/gameBoard.py b/imago/gameLogic/gameBoard.py index 170a7cb..d0e22a6 100644 --- a/imago/gameLogic/gameBoard.py +++ b/imago/gameLogic/gameBoard.py @@ -110,9 +110,12 @@ class GameBoard: containing the vertices where stones were captured. """ - if (row < 0 or row >= self.getBoardHeight() - or col < 0 or col >= self.getBoardWidth()): - raise RuntimeError("[ERROR] Move and capture: out of bounds (%d, %d)" % (row, col)) + try: + if (row < 0 or row >= self.getBoardHeight() + or col < 0 or col >= self.getBoardWidth()): + raise RuntimeError("[ERROR] Move and capture: out of bounds (%d, %d)" % (row, col)) + except Exception as err: + raise RuntimeError("[ERROR] Move and capture: Wrong input: %s" % err) self.board[row][col] = player diff --git a/imago/gameLogic/gameData.old b/imago/gameLogic/gameData.old new file mode 100644 index 0000000..f23723f --- /dev/null +++ b/imago/gameLogic/gameData.old @@ -0,0 +1,52 @@ +"""Invariable data pertaining a match.""" + +#class GameData: + """Invariable data pertaining a match.""" + +# Obsolete method of storing properties +# def __init__(self, +# name = None, +# size = 19, +# annotator = None, +# date = None, +# blackRank = "?", +# whiteRank = "?", +# blackName = None, +# whiteName = None, +# blackTeam = None, +# whiteTeam = None, +# copyrightInfo = None, +# event = None, +# gameComment = None, +# openingInfo = None, +# timeInfo = None, +# overtimeInfo = None, +# placeInfo = None, +# result = "?", +# roundInfo = None, +# rules = None, +# source = None, +# user = None +# ): +# self.name = name +# self.size = size +# self.annotator = annotator +# self.date = date +# self.blackRank = blackRank +# self.whiteRank = whiteRank +# self.blackName = blackName +# self.whiteName = whiteName +# self.blackTeam = blackTeam +# self.whiteTeam = whiteTeam +# self.copyrightInfo = copyrightInfo +# self.event = event +# self.gameComment = gameComment +# self.openingInfo = openingInfo +# self.timeInfo = timeInfo +# self.overtimeInfo = overtimeInfo +# self.placeInfo = placeInfo +# self.result = result +# self.roundInfo = roundInfo +# self.rules = rules +# self.source = source +# self.user = user diff --git a/imago/gameLogic/gameData.py b/imago/gameLogic/gameData.py deleted file mode 100644 index 6cc4d7e..0000000 --- a/imago/gameLogic/gameData.py +++ /dev/null @@ -1,51 +0,0 @@ -"""Invariable data pertaining a match.""" - -class GameData: - """Invariable data pertaining a match.""" - - def __init__(self, - name = None, - size = 19, - annotator = None, - date = None, - blackRank = "?", - whiteRank = "?", - blackName = None, - whiteName = None, - blackTeam = None, - whiteTeam = None, - copyrightInfo = None, - event = None, - gameComment = None, - openingInfo = None, - timeInfo = None, - overtimeInfo = None, - placeInfo = None, - result = "?", - roundInfo = None, - rules = None, - source = None, - user = None - ): - self.name = name - self.size = size - self.annotator = annotator - self.date = date - self.blackRank = blackRank - self.whiteRank = whiteRank - self.blackName = blackName - self.whiteName = whiteName - self.blackTeam = blackTeam - self.whiteTeam = whiteTeam - self.copyrightInfo = copyrightInfo - self.event = event - self.gameComment = gameComment - self.openingInfo = openingInfo - self.timeInfo = timeInfo - self.overtimeInfo = overtimeInfo - self.placeInfo = placeInfo - self.result = result - self.roundInfo = roundInfo - self.rules = rules - self.source = source - self.user = user diff --git a/imago/sgfParser/astNode.py b/imago/sgfParser/astNode.py index 41629ce..5071483 100644 --- a/imago/sgfParser/astNode.py +++ b/imago/sgfParser/astNode.py @@ -1,4 +1,4 @@ -from imago.gameLogic.gameData import GameData +#from imago.gameLogic.gameData import GameData from imago.gameLogic.gameMove import GameMove from imago.gameLogic.gameBoard import GameBoard from imago.data.enums import Player @@ -22,64 +22,6 @@ class ASTNode: children = children[0].children children.append(move) - def toGameTree(self): - """Converts this node and its subtree into a GameTree""" - gameData = GameData() - for prop in self.props: - if prop.name == "GM": # Type of game, 1 is Go - if prop.value != 1: - print("ERROR") # TODO: Error handling - if prop.name == "SZ": # Size of board. [19] for squared, [10:12] for rect. - gameData.size = prop.value - if prop.name == "AN": # Annotator - gameData.annotator = prop.value - if prop.name == "BR": # Rank of black player - gameData.blackRank = prop.value - if prop.name == "WR": # Rank of white player - gameData.whiteRank = prop.value - if prop.name == "PB": # Name of black player - gameData.blackName = prop.value - if prop.name == "PW": # Name of white player - gameData.whiteName = prop.value - if prop.name == "BT": # Name of black team - gameData.blackTeam = prop.value - if prop.name == "WT": # Name of white team - gameData.whiteTeam = prop.value - if prop.name == "CP": # Copyright information - gameData.copyright = prop.value - if prop.name == "DT": # Date - gameData.date = prop.value - if prop.name == "EV": # Event information - gameData.event = prop.value - if prop.name == "GN": # Game nae - gameData.name = prop.value - if prop.name == "GC": # Extra game comment - gameData.gameComment = prop.value - if prop.name == "ON": # Description of opening played - gameData.openingInfo = prop.value - if prop.name == "OT": # Overtime method - gameData.overtimeInfo = prop.value - if prop.name == "PC": # Place where the game took place - gameData.place = prop.value - if prop.name == "RE": # Result of the game - gameData.result = prop.value - if prop.name == "RO": # Round number and type - gameData.roundInfo = prop.value - if prop.name == "RU": # Rules used for the game - gameData.rules = prop.value - if prop.name == "SO": # Source of the game - gameData.source = prop.source - if prop.name == "TM": # Time limit in seconds - gameData.timeInfo = prop.source - if prop.name == "US": # User or program which entered the game - gameData.user = prop.source - - firstMoves = [] - for child in self.children: - firstMoves.append(child.toGameMoveTree(size)) - - return GameTree(firstMoves, gameData) - def toGameMoveTree(self, previousMove=None): if previousMove is None: # Game root node @@ -138,6 +80,65 @@ def textToCoords(text): # Poner en PropertyMove, subclase de Property row = ord(text[1]) - ord('a') return [row, col] + # Obsolete method of storing properties + #def toGameTree(self): + # """Converts this node and its subtree into a GameTree""" + # gameData = GameData() + # for prop in self.props: + # if prop.name == "GM": # Type of game, 1 is Go + # if prop.value != 1: + # print("ERROR") # TODO: Error handling + # if prop.name == "SZ": # Size of board, [19] for squared, [10:12] for rect. + # gameData.size = prop.value + # if prop.name == "AN": # Annotator + # gameData.annotator = prop.value + # if prop.name == "BR": # Rank of black player + # gameData.blackRank = prop.value + # if prop.name == "WR": # Rank of white player + # gameData.whiteRank = prop.value + # if prop.name == "PB": # Name of black player + # gameData.blackName = prop.value + # if prop.name == "PW": # Name of white player + # gameData.whiteName = prop.value + # if prop.name == "BT": # Name of black team + # gameData.blackTeam = prop.value + # if prop.name == "WT": # Name of white team + # gameData.whiteTeam = prop.value + # if prop.name == "CP": # Copyright information + # gameData.copyright = prop.value + # if prop.name == "DT": # Date + # gameData.date = prop.value + # if prop.name == "EV": # Event information + # gameData.event = prop.value + # if prop.name == "GN": # Game nae + # gameData.name = prop.value + # if prop.name == "GC": # Extra game comment + # gameData.gameComment = prop.value + # if prop.name == "ON": # Description of opening played + # gameData.openingInfo = prop.value + # if prop.name == "OT": # Overtime method + # gameData.overtimeInfo = prop.value + # if prop.name == "PC": # Place where the game took place + # gameData.place = prop.value + # if prop.name == "RE": # Result of the game + # gameData.result = prop.value + # if prop.name == "RO": # Round number and type + # gameData.roundInfo = prop.value + # if prop.name == "RU": # Rules used for the game + # gameData.rules = prop.value + # if prop.name == "SO": # Source of the game + # gameData.source = prop.source + # if prop.name == "TM": # Time limit in seconds + # gameData.timeInfo = prop.source + # if prop.name == "US": # User or program which entered the game + # gameData.user = prop.source + # + # firstMoves = [] + # for child in self.children: + # firstMoves.append(child.toGameMoveTree()) + # + # return GameTree(firstMoves, gameData) + class Property: """Property of a Node""" diff --git a/tests/test_gameBoard.py b/tests/test_gameBoard.py index 8a7b127..c7808ac 100644 --- a/tests/test_gameBoard.py +++ b/tests/test_gameBoard.py @@ -114,5 +114,33 @@ class TestGameBoard(unittest.TestCase): board.board[9][0] = Player.WHITE self.assertEqual((9, 21), board.score()) + def testToString(self): + """Test formatting of the board as a string.""" + + board = GameBoard(9, 9) + self.assertEqual(' A B C D E F G H J \n\ +9 · · · · · · · · · \n\ +8 · · · · · · · · · \n\ +7 · · · · · · · · · \n\ +6 · · · · · · · · · \n\ +5 · · · · · · · · · \n\ +4 · · · · · · · · · \n\ +3 · · · · · · · · · \n\ +2 · · · · · · · · · \n\ +1 · · · · · · · · · ', board.toString()) + + board.moveAndCapture(2, 6, Player.BLACK) + board.moveAndCapture(5, 4, Player.WHITE) + self.assertEqual(' A B C D E F G H J \n\ +9 · · · · · · · · · \n\ +8 · · · · · · · · · \n\ +7 · · · · · · B · · \n\ +6 · · · · · · · · · \n\ +5 · · · · · · · · · \n\ +4 · · · · W · · · · \n\ +3 · · · · · · · · · \n\ +2 · · · · · · · · · \n\ +1 · · · · · · · · · ', board.toString()) + if __name__ == '__main__': unittest.main() diff --git a/tests/test_gameState.py b/tests/test_gameState.py index 638e269..1c6b997 100644 --- a/tests/test_gameState.py +++ b/tests/test_gameState.py @@ -73,7 +73,7 @@ class TestGameState(unittest.TestCase): ]) - self.assertFalse(state.playMove(-1, -1)) + self.assertRaises(Exception, state.playMove, -1, -1) self.assertEqual(state.getBoard().getBoard(), [ [Player.EMPTY, Player.EMPTY, Player.EMPTY], diff --git a/tests/test_imagoIO.py b/tests/test_imagoIO.py index 4f2d7bd..499fdb2 100644 --- a/tests/test_imagoIO.py +++ b/tests/test_imagoIO.py @@ -81,7 +81,8 @@ class TestImagoIO(unittest.TestCase): '= \n\n' + '= \n\n' + '= \n\n' + - '? unknown command\n\n', + '? unknown command\n\n' + + '= \n\n', value ) @@ -112,7 +113,8 @@ class TestImagoIO(unittest.TestCase): self.assertEqual( '= ' + commandsString + - '\n\n\n', + '\n\n' + + '= \n\n', value ) @@ -140,7 +142,8 @@ class TestImagoIO(unittest.TestCase): self.assertEqual( '? Wrong number of arguments\n' + '? Usage: fixed_handicap \n\n' + - '= A1 A2\n\n', + '= A1 A2\n\n' + + '= \n\n', value ) diff --git a/tests/test_monteCarlo.py b/tests/test_monteCarlo.py index 496c073..9d1fcfc 100644 --- a/tests/test_monteCarlo.py +++ b/tests/test_monteCarlo.py @@ -66,7 +66,7 @@ class TestMonteCarlo(unittest.TestCase): self.assertEqual(thirdMoveCoords, nextNode.move.coords) #def testSimulation(self): - # """Test calculation of group liberties.""" + # """Test Monte Carlo simulation.""" # board = GameBoard(TEST_BOARD_SIZE, TEST_BOARD_SIZE) # move = GameMove(board) # node = MCTSNode(move, None) diff --git a/tests/test_neuralNetwork.py b/tests/test_neuralNetwork.py index dfcbd7a..42ba4a1 100644 --- a/tests/test_neuralNetwork.py +++ b/tests/test_neuralNetwork.py @@ -1,8 +1,16 @@ """Tests for neural network module.""" +import os +import shutil import unittest +from imago.data.enums import DecisionAlgorithms +from imago.sgfParser.sgf import loadGameTree +from imago.gameLogic.gameState import GameState from imago.engine.keras.neuralNetwork import NeuralNetwork +from imago.engine.keras.denseNeuralNetwork import DenseNeuralNetwork +from imago.engine.keras.convNeuralNetwork import ConvNeuralNetwork +from imago.engine.keras.keras import Keras class TestNeuralNetwork(unittest.TestCase): """Test neural network module.""" @@ -13,3 +21,63 @@ class TestNeuralNetwork(unittest.TestCase): self.assertRaises(NotImplementedError, NeuralNetwork, "non/existing/file") + + def testNetworks(self): + """Test creation of initial model for dense neural network""" + + testModel = 'testModel' + testModelPlot = 'testModelPlot' + + games = [] + for file in [ + '../collections/minigo/matches/1.sgf', + '../collections/minigo/matches/2.sgf', + '../collections/minigo/matches/3.sgf' + ]: + games.append(loadGameTree(file)) + matches = [game.getMainLineOfPlay() for game in games] + + nn = DenseNeuralNetwork(modelPath=testModel, boardSize=9) + nn.trainModel(matches, epochs=1, verbose=0) + + game = GameState(9) + nn.pickMove(game.lastMove, game.getCurrentPlayer()) + + nn.saveModel(testModel) + self.assertTrue(os.path.isdir(testModel)) + shutil.rmtree(testModel, ignore_errors=True) + + nn.saveModel() + self.assertTrue(os.path.isdir(testModel)) + nn = DenseNeuralNetwork(modelPath=testModel, boardSize=9) + + nn.saveModelPlot(testModelPlot) + self.assertTrue(os.path.isfile(testModelPlot)) + + shutil.rmtree(testModel, ignore_errors=True) + os.remove(testModelPlot) + + nn = ConvNeuralNetwork(testModel, boardSize=9) + + def testKeras(self): + """Test keras model loading.""" + + gameState = GameState(9) + move = gameState.lastMove + + keras = Keras(move) + keras.forceNextMove("pass") + + keras = Keras(move, DecisionAlgorithms.DENSE) + keras.forceNextMove((3,3)) + + keras = Keras(move, DecisionAlgorithms.CONV) + self.assertRaises(RuntimeError, keras.forceNextMove, "wrongmove") + pickedCoords = keras.pickMove() + self.assertTrue(len(pickedCoords) == 2 or pickedCoords == "pass") + + self.assertRaises(RuntimeError, Keras, move, DecisionAlgorithms.MONTECARLO) + + +if __name__ == '__main__': + unittest.main() diff --git a/tests/test_sgf.py b/tests/test_sgf.py new file mode 100644 index 0000000..1266429 --- /dev/null +++ b/tests/test_sgf.py @@ -0,0 +1,27 @@ +"""Tests for processing of SGF files.""" + +import unittest + +from imago.sgfParser.sgfyacc import parser + +TESTING_SGF = 'tests/testingSGF.sgf' + +class TestSGF(unittest.TestCase): + """Test processing SGF files.""" + + def testToGameTree(self): + """Test converting file to GameTree""" + + file = open(TESTING_SGF, "r") + text = file.read() + file.close() + + astNode = parser.parse(text) + + astNode.toGameMoveTree() + + astNode.toString() + + +if __name__ == '__main__': + unittest.main() -- cgit v1.2.1