diff options
-rw-r--r-- | doc/listings/test.sh | 2 | ||||
-rw-r--r-- | doc/listings/testOutput.txt | 28 | ||||
-rw-r--r-- | doc/tex/imago.tex | 10 | ||||
-rw-r--r-- | doc/tex/systemAnalysis.tex | 31 | ||||
-rw-r--r-- | imago/engine/imagoIO.py | 108 | ||||
-rw-r--r-- | imago/engine/monteCarlo.py | 56 | ||||
-rw-r--r-- | imago/gameLogic/gameMove.py | 9 | ||||
-rw-r--r-- | imago/gameLogic/gameState.py | 13 | ||||
-rw-r--r-- | tests/test_gameMove.py | 6 | ||||
-rw-r--r-- | tests/test_gameState.py | 89 | ||||
-rw-r--r-- | tests/test_imagoIO.py | 40 | ||||
-rw-r--r-- | tests/test_monteCarlo.py | 48 |
12 files changed, 341 insertions, 99 deletions
diff --git a/doc/listings/test.sh b/doc/listings/test.sh new file mode 100644 index 0000000..b929249 --- /dev/null +++ b/doc/listings/test.sh @@ -0,0 +1,2 @@ +#!/bin/sh +coverage run --source=imago -m unittest discover tests/ && coverage report -m --skip-empty diff --git a/doc/listings/testOutput.txt b/doc/listings/testOutput.txt new file mode 100644 index 0000000..2bfa52f --- /dev/null +++ b/doc/listings/testOutput.txt @@ -0,0 +1,28 @@ +Name Stmts Miss Cover Missing +------------------------------------------------------------------------------- +imago/data/enums.py 17 0 100% +imago/engine/core.py 39 39 0% 3-68 +imago/engine/createDecisionAlgorithm.py 11 11 0% 3-21 +imago/engine/decisionAlgorithm.py 9 4 56% 6, 10, 14, 18 +imago/engine/imagoIO.py 105 105 0% 3-186 +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 28 0% 3-49 +imago/engine/keras/neuralNetwork.py 137 137 0% 3-206 +imago/engine/monteCarlo.py 107 78 27% 17-24, 32-35, 41-49, 53-54, 71, 77-79, 84-88, 92-93, 110-125, 129-130, 136-140, 144-156, 162-182, 186-187 +imago/engine/parseHelpers.py 48 0 100% +imago/gameLogic/gameBoard.py 199 55 72% 115, 128, 136-139, 177, 188, 192, 202, 211-212, 216, 224, 228, 230, 235-241, 267-274, 278-303, 307-311 +imago/gameLogic/gameData.py 24 24 0% 3-51 +imago/gameLogic/gameMove.py 93 29 69% 21, 27, 33-35, 51-56, 73, 91, 117, 121-125, 130-133, 137-141, 145 +imago/gameLogic/gameState.py 42 22 48% 17-21, 25, 29, 38, 43-49, 53, 57, 61, 66-71 +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 1132 805 29% + +8 empty files skipped. diff --git a/doc/tex/imago.tex b/doc/tex/imago.tex index 4b292f1..7978a0c 100644 --- a/doc/tex/imago.tex +++ b/doc/tex/imago.tex @@ -38,16 +38,6 @@ \newcommand{\lref}[1]{Listing~\ref{#1}} %\newcommand{\uurl}[1]{\underline{\url{#1}}} -%\newcommand{\acronim}[2] -%{ -% \iftoggle{#1} -% {#1} -% {#1 (#2)\toggletrue{#1}} -%} -% -%\newtoggle{SGF} -%\newcommand{\acrSGF}[0]{\acronim{SGF}{Smart Game Format}} - \newcommand{\tabitem}{~~\llap{\textbullet}~~} \begin{document} diff --git a/doc/tex/systemAnalysis.tex b/doc/tex/systemAnalysis.tex index bbae66e..ba5fbf1 100644 --- a/doc/tex/systemAnalysis.tex +++ b/doc/tex/systemAnalysis.tex @@ -878,6 +878,17 @@ The engine interface reads the input for generating a move as stated by the \subsection{Testing Plan Specification} +The Testing Plan will include four types of tests: + +\begin{itemize} + + \item Unitary Testing: for isolated code elements. + \item Integration Testing: for the collaboration between components. + \item System Testing: for the product as a whole. + \item Usability Testing: for the experience of users with the product. + +\end{itemize} + \subsubsection{Unitary Testing} Tests for the Python code are developed using the unittest \cite{python_unittest} @@ -888,4 +899,24 @@ The coverage of unit testing is checked with Coverage.py \cite{python_coverage}, which can by itself run the unittest tests and generate coverage reports based on the results. +The script used to run the tests is shown on \lref{lst:test} and its output on +\lref{lst:testOutput}. + % Maybe put an example report here? +\begin{listing}[h] + \inputminted{bash}{listings/test.sh} + \caption{Dense neural network model.} + \label{lst:test} +\end{listing} + +\begin{listing}[h] + \inputminted[fontsize=\footnotesize]{text}{listings/testOutput.txt} + \caption{Unitary testing output.} + \label{lst:testOutput} +\end{listing} + +\subsubsection{Integration Testing} + +\subsubsection{System Testing} + +\subsubsection{Usability Testing} diff --git a/imago/engine/imagoIO.py b/imago/engine/imagoIO.py index 9a89095..c983949 100644 --- a/imago/engine/imagoIO.py +++ b/imago/engine/imagoIO.py @@ -6,30 +6,6 @@ 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. @@ -41,11 +17,11 @@ def getCoordsText(row, col): class ImagoIO: """Recieves and handles commands.""" - def __init__(self, decisionAlgorithmId=None): + def __init__(self, decisionAlgorithmId=None, outputStream=sys.stdin): self.commands_set = { - protocol_version, - name, - version, + self.protocol_version, + self.name, + self.version, self.known_command, self.list_commands, self.boardsize, @@ -59,6 +35,17 @@ class ImagoIO: self.undo } 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, file=self.outputStream) + print(file=self.outputStream) + def start(self): """Starts reading commands interactively.""" @@ -70,7 +57,8 @@ class ImagoIO: continue if input_tokens[0] == "quit": - sys.exit(0) + #sys.exit(0) + break command = None for comm in self.commands_set: @@ -81,28 +69,46 @@ class ImagoIO: arguments = input_tokens[1:] 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("An uncontrolled error ocurred. The error was: %s" % err) + def known_command(self, args): """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.") + self._responseError("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""" output = "" for c in self.commands_set: output += ("%s - %s\n" % (c.__name__, c.__doc__)) - _response(output) + 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. @@ -110,44 +116,44 @@ class ImagoIO: 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") + self._responseError("Usag. 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. """ 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") + self._responseError("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. """ if len(args) != 1: - _responseError("Wrong number of arguments") - _responseError("Usage: fixed_handicap <count>") + self._responseError("Wrong number of arguments") + self._responseError("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.""" @@ -160,23 +166,23 @@ class ImagoIO: 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.") + self._responseError("Usage: play <color> <vertex>") return move = parseHelpers.parseMove(args, self.gameEngine.gameState.size) self.gameEngine.play(move.color, move.vertex) - _response() + self._response() 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.") + self._responseError("Usage: genmove <color>") return color = parseHelpers.parseColor(args[0]) output = parseHelpers.vertexToString(self.gameEngine.genmove(color), self.gameEngine.gameState.size) - _response(output) + self._response(output) self.gameEngine.gameState.getBoard().printBoard() def undo(self, _): diff --git a/imago/engine/monteCarlo.py b/imago/engine/monteCarlo.py index baaaba8..f4712e6 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): """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/gameLogic/gameMove.py b/imago/gameLogic/gameMove.py index c1c7a05..b6f2947 100644 --- a/imago/gameLogic/gameMove.py +++ b/imago/gameLogic/gameMove.py @@ -84,14 +84,15 @@ class GameMove: vertices.add((row, col)) return vertices - def addMoveByCoords(self, coords): + def addMove(self, coords): """Adds a move to the next moves list creating its board from this move's board plus a new stone at the specified coordinates. """ - return self.addMove(coords[0], coords[1]) + if coords == "pass": + return self.addPass() + return self.addMoveByCoords(coords[0], coords[1]) - - def addMove(self, row, col): + def addMoveByCoords(self, row, col): """Adds a move to the next moves list creating its board from this move's board plus a new stone at the specified row and column. """ diff --git a/imago/gameLogic/gameState.py b/imago/gameLogic/gameState.py index 72b91b4..3e8c1a5 100644 --- a/imago/gameLogic/gameState.py +++ b/imago/gameLogic/gameState.py @@ -40,6 +40,10 @@ class GameState: def playMoveForPlayer(self, row, col, player): """Execute a move on the board for the given player.""" + # Check a last move already exists + if self.lastMove is None: + raise RuntimeError("Last move of the GameState is None.") + prevBoards = self.lastMove.getThisAndPrevBoards() playable, message = self.lastMove.board.isPlayable(row, col, player, prevBoards) if playable: @@ -50,22 +54,17 @@ class GameState: def playPass(self): """Passes the turn for the player who should make a move.""" - self.lastMove.addPass() + self.lastMove = self.lastMove.addPass() def passForPlayer(self, player): """Passes the turn for the given player.""" - self.lastMove.addPassForPlayer(player) + self.lastMove = self.lastMove.addPassForPlayer(player) def undo(self): """Sets the move before the last move as the new last move.""" self.lastMove = self.lastMove.previousMove def _addMove(self, player, row, col): - - # Check a last move already exists - if self.lastMove is None: - raise RuntimeError("Last move of the GameState is None.") - # Add and return the new move self.lastMove = self.lastMove.addMoveForPlayer(row, col, player) return self.lastMove diff --git a/tests/test_gameMove.py b/tests/test_gameMove.py index 6569c5b..a7edfab 100644 --- a/tests/test_gameMove.py +++ b/tests/test_gameMove.py @@ -18,13 +18,13 @@ class TestGameMove(unittest.TestCase): self.assertIsNone(firstMove.coords) - secondMove = firstMove.addMove(1, 2) + secondMove = firstMove.addMoveByCoords(1, 2) self.assertIsNone(firstMove.coords) self.assertEqual(secondMove.coords[0], 1) self.assertEqual(secondMove.coords[1], 2) - thirdMove = secondMove.addMove(5, 7) + thirdMove = secondMove.addMoveByCoords(5, 7) self.assertIsNone(firstMove.coords) self.assertIsNone(thirdMove.previousMove.previousMove.coords) @@ -66,7 +66,7 @@ class TestGameMove(unittest.TestCase): (2,0), (2,1), (2,2))) ) - secondMove = firstMove.addMove(1, 2) + secondMove = firstMove.addMoveByCoords(1, 2) self.assertSetEqual( secondMove.getPlayableVertices(), set(((0,0), (0,1), (0,2), diff --git a/tests/test_gameState.py b/tests/test_gameState.py new file mode 100644 index 0000000..638e269 --- /dev/null +++ b/tests/test_gameState.py @@ -0,0 +1,89 @@ +"""Tests for the input/output component.""" + +import unittest + +from imago.data.enums import Player +from imago.gameLogic.gameState import GameState + +class TestGameState(unittest.TestCase): + """Test GameState component.""" + + def testCurrentPlayer(self): + """Test simple commands.""" + size = 9 + state = GameState(size) + + self.assertEqual(state.getCurrentPlayer(), Player.BLACK) + self.assertEqual(state.getPlayerCode(), 'B') + + state.playMove(0, 0) + self.assertEqual(state.getCurrentPlayer(), Player.WHITE) + self.assertEqual(state.getPlayerCode(), 'W') + + def testPlays(self): + """Test simple commands.""" + size = 3 + state = GameState(size) + + self.assertEqual(state.getBoard().getBoard(), + [ + [Player.EMPTY, Player.EMPTY, Player.EMPTY], + [Player.EMPTY, Player.EMPTY, Player.EMPTY], + [Player.EMPTY, Player.EMPTY, Player.EMPTY] + ]) + + state.playMove(1, 1) + self.assertEqual(state.getBoard().getBoard(), + [ + [Player.EMPTY, Player.EMPTY, Player.EMPTY], + [Player.EMPTY, Player.BLACK, Player.EMPTY], + [Player.EMPTY, Player.EMPTY, Player.EMPTY] + ]) + + state.playPass() + self.assertEqual(state.getBoard().getBoard(), + [ + [Player.EMPTY, Player.EMPTY, Player.EMPTY], + [Player.EMPTY, Player.BLACK, Player.EMPTY], + [Player.EMPTY, Player.EMPTY, Player.EMPTY] + ]) + + state.undo() + self.assertEqual(state.getBoard().getBoard(), + [ + [Player.EMPTY, Player.EMPTY, Player.EMPTY], + [Player.EMPTY, Player.BLACK, Player.EMPTY], + [Player.EMPTY, Player.EMPTY, Player.EMPTY] + ]) + + state.undo() + self.assertEqual(state.getBoard().getBoard(), + [ + [Player.EMPTY, Player.EMPTY, Player.EMPTY], + [Player.EMPTY, Player.EMPTY, Player.EMPTY], + [Player.EMPTY, Player.EMPTY, Player.EMPTY] + ]) + + state.passForPlayer(Player.WHITE) + self.assertEqual(state.getBoard().getBoard(), + [ + [Player.EMPTY, Player.EMPTY, Player.EMPTY], + [Player.EMPTY, Player.EMPTY, Player.EMPTY], + [Player.EMPTY, Player.EMPTY, Player.EMPTY] + ]) + + + self.assertFalse(state.playMove(-1, -1)) + self.assertEqual(state.getBoard().getBoard(), + [ + [Player.EMPTY, Player.EMPTY, Player.EMPTY], + [Player.EMPTY, Player.EMPTY, Player.EMPTY], + [Player.EMPTY, Player.EMPTY, Player.EMPTY] + ]) + + state.lastMove = None + self.assertEqual(state.getCurrentPlayer(), Player.BLACK) + self.assertRaises(RuntimeError, state.playMove, 0, 0) + +if __name__ == '__main__': + unittest.main() diff --git a/tests/test_imagoIO.py b/tests/test_imagoIO.py new file mode 100644 index 0000000..c3c6fda --- /dev/null +++ b/tests/test_imagoIO.py @@ -0,0 +1,40 @@ +"""Tests for the input/output component.""" + +import unittest + +import io +import sys + +from imago.engine.imagoIO import ImagoIO + +class TestImagoIO(unittest.TestCase): + """Test ImagoIO component.""" + + @unittest.mock.patch('imago.engine.imagoIO.input', create=True) + + def testSimpleCommands(self, mocked_input): + """Test simple commands.""" + + mocked_input.side_effect = [ + 'name\n', + 'version\n', + 'protocol_version\n', + 'quit\n' + ] + + testout = io.StringIO() + imagoIO = ImagoIO(outputStream=testout) + + imagoIO.start() + value = testout.getvalue() + self.assertEqual( + '= Imago\n\n' + + '= 0.0.0\n\n' + + '= 2\n\n', + value + ) + + testout.close() + +if __name__ == '__main__': + unittest.main() diff --git a/tests/test_monteCarlo.py b/tests/test_monteCarlo.py index b217cf9..496c073 100644 --- a/tests/test_monteCarlo.py +++ b/tests/test_monteCarlo.py @@ -2,6 +2,7 @@ import unittest +from imago.engine.decisionAlgorithm import DecisionAlgorithm from imago.gameLogic.gameState import GameState from imago.engine.monteCarlo import MCTS from imago.engine.monteCarlo import MCTSNode @@ -17,6 +18,53 @@ class TestMonteCarlo(unittest.TestCase): tree = MCTS(state.lastMove) #print(tree.pickMove().toString()) + def testForceNextMove(self): + """Test forcing next move.""" + + # Next move before expansion (no children nodes) + state = GameState(TEST_BOARD_SIZE) + tree = MCTS(state.lastMove) + self.assertEqual(set(), tree.root.children) + tree.forceNextMove((0, 1)) + self.assertEqual(set(), tree.root.children) + + # Next move after expansion (with children nodes) + tree.expansions = 2 + tree.simulationsPerExpansion = 2 + tree.pickMove() + self.assertEqual(tree.expansions, len(tree.root.children)) + nextMoveCoords = list(tree.root.children)[0].move.coords + tree.forceNextMove(nextMoveCoords) + + def testPass(self): + """Test passing as next move.""" + state = GameState(TEST_BOARD_SIZE) + tree = MCTS(state.lastMove) + self.assertFalse(tree.root.move.isPass) + tree.forceNextMove("pass") + self.assertTrue(tree.root.move.isPass) + + def testClearBoard(self): + """Test clearing board returns root to original and retains information.""" + state = GameState(TEST_BOARD_SIZE) + tree = MCTS(state.lastMove) + + firstMoveCoords = (0,0) + secondMoveCoords = (1,0) + thirdMoveCoords = (0,1) + + tree.forceNextMove(firstMoveCoords) + tree.forceNextMove(secondMoveCoords) + tree.forceNextMove(thirdMoveCoords) + tree.clearBoard() + + nextNode = list(tree.root.children)[0] + self.assertEqual(firstMoveCoords, nextNode.move.coords) + nextNode = list(nextNode.children)[0] + self.assertEqual(secondMoveCoords, nextNode.move.coords) + nextNode = list(nextNode.children)[0] + self.assertEqual(thirdMoveCoords, nextNode.move.coords) + #def testSimulation(self): # """Test calculation of group liberties.""" # board = GameBoard(TEST_BOARD_SIZE, TEST_BOARD_SIZE) |