aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--doc/listings/test.sh2
-rw-r--r--doc/listings/testOutput.txt28
-rw-r--r--doc/tex/imago.tex10
-rw-r--r--doc/tex/systemAnalysis.tex31
-rw-r--r--imago/engine/imagoIO.py108
-rw-r--r--imago/engine/monteCarlo.py56
-rw-r--r--imago/gameLogic/gameMove.py9
-rw-r--r--imago/gameLogic/gameState.py13
-rw-r--r--tests/test_gameMove.py6
-rw-r--r--tests/test_gameState.py89
-rw-r--r--tests/test_imagoIO.py40
-rw-r--r--tests/test_monteCarlo.py48
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)