From 80c4cca827ff80c0508c27cd9b6a37ffa2ea17e5 Mon Sep 17 00:00:00 2001 From: InigoGutierrez Date: Wed, 11 Jan 2023 19:20:06 +0100 Subject: 100% coverage on imagoIO module. --- imago/engine/imagoIO.py | 50 ++++++++----- imago/engine/monteCarlo.py | 2 +- imago/engine/parseHelpers.py | 14 ++-- tests/test_imagoIO.py | 164 ++++++++++++++++++++++++++++++++++++++++++- tests/test_parseHelpers.py | 36 +++++----- 5 files changed, 221 insertions(+), 45 deletions(-) diff --git a/imago/engine/imagoIO.py b/imago/engine/imagoIO.py index c983949..1b63f18 100644 --- a/imago/engine/imagoIO.py +++ b/imago/engine/imagoIO.py @@ -17,6 +17,7 @@ def getCoordsText(row, col): class ImagoIO: """Recieves and handles commands.""" + def __init__(self, decisionAlgorithmId=None, outputStream=sys.stdin): self.commands_set = { self.protocol_version, @@ -37,13 +38,14 @@ class ImagoIO: 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("? %s" % text.replace('\n', '\n? '), file=self.outputStream) print(file=self.outputStream) @@ -71,14 +73,14 @@ class ImagoIO: else: self._responseError("unknown command") except Exception as err: - self._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: - self._responseError("Wrong number of arguments.") - self._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: @@ -116,13 +118,14 @@ class ImagoIO: It is wise to call clear_board after this command. """ if len(args) != 1: - self._responseError("Wrong number of arguments") - self._responseError("Usag. boardsize ") + self._responseError("Wrong number of arguments\n" + + "Usage: boardsize ") return size = int(args[0]) self.gameEngine.setBoardsize(size) 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. @@ -130,23 +133,25 @@ class ImagoIO: self.gameEngine.clearBoard() self._response() + def komi(self, args): """Sets a new value of komi.""" if len(args) != 1: - self._responseError("Wrong number of arguments") - self._responseError("Usage: komi ") + self._responseError("Wrong number of arguments\n" + + "Usage: komi ") return komi = float(args[0]) self.gameEngine.setKomi(komi) 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: - self._responseError("Wrong number of arguments") - self._responseError("Usage: fixed_handicap ") + self._responseError("Wrong number of arguments\n" + + "Usage: fixed_handicap ") return stones = float(args[0]) vertices = self.gameEngine.setFixedHandicap(stones) @@ -155,29 +160,36 @@ class ImagoIO: out += " " + getCoordsText(vertex[0], vertex[1]) 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: - self._responseError("Wrong number of arguments.") - self._responseError("Usage: play ") + self._responseError("Wrong number of arguments\n" + + "Usage: play ") return - move = parseHelpers.parseMove(args, self.gameEngine.gameState.size) - self.gameEngine.play(move.color, move.vertex) - self._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: - self._responseError("Wrong number of arguments.") - self._responseError("Usage: genmove ") + self._responseError("Wrong number of arguments\n" + + "Usage: genmove ") return color = parseHelpers.parseColor(args[0]) output = parseHelpers.vertexToString(self.gameEngine.genmove(color), @@ -185,8 +197,10 @@ class ImagoIO: self._response(output) self.gameEngine.gameState.getBoard().printBoard() + 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. """ self.gameEngine.undo() + self._response() diff --git a/imago/engine/monteCarlo.py b/imago/engine/monteCarlo.py index f4712e6..ee8380a 100644 --- a/imago/engine/monteCarlo.py +++ b/imago/engine/monteCarlo.py @@ -15,7 +15,7 @@ class MCTS(DecisionAlgorithm): self.simulationsPerExpansion = 10 self.debug = False - def forceNextMove(self, coords): + def forceNextMove(self, coords: tuple): """Selects given move as next move.""" for node in self.root.children: if (node.move.isPass and coords == "pass" or 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] diff --git a/tests/test_imagoIO.py b/tests/test_imagoIO.py index c3c6fda..4f2d7bd 100644 --- a/tests/test_imagoIO.py +++ b/tests/test_imagoIO.py @@ -5,36 +5,194 @@ import unittest import io import sys +from imago.data.enums import DecisionAlgorithms from imago.engine.imagoIO import ImagoIO +from imago.engine.parseHelpers import parseVertex class TestImagoIO(unittest.TestCase): """Test ImagoIO component.""" @unittest.mock.patch('imago.engine.imagoIO.input', create=True) - def testSimpleCommands(self, mocked_input): """Test simple commands.""" + self.maxDiff = None + mocked_input.side_effect = [ + '\n', 'name\n', 'version\n', 'protocol_version\n', + 'clear_board\n', + '\n', + 'known_command\n', + 'known_command name\n', + 'known_command version\n', + 'known_command wrongcommand\n', + '\n', + 'boardsize\n', + 'boardsize 10\n', + '\n', + 'komi\n', + 'komi 5.5\n', + '\n', + 'play\n', + 'play 1\n', + 'play 1 2\n', + 'play b a1\n', + '\n', + 'undo\n', + 'undo\n', + '\n', + 'wrongcommand\n', + '\n', 'quit\n' ] testout = io.StringIO() - imagoIO = ImagoIO(outputStream=testout) + imagoIO = ImagoIO( + decisionAlgorithmId=DecisionAlgorithms.MONTECARLO, + outputStream=testout + ) imagoIO.start() value = testout.getvalue() self.assertEqual( '= Imago\n\n' + '= 0.0.0\n\n' + - '= 2\n\n', + '= 2\n\n' + + '= \n\n' + + '? Wrong number of arguments\n' + + '? Usage: known_command COMMAND_NAME\n\n' + + '= true\n\n' + + '= true\n\n' + + '= false\n\n' + + '? Wrong number of arguments\n' + + '? Usage: boardsize \n\n' + + '= \n\n' + + '? Wrong number of arguments\n' + + '? Usage: komi \n\n' + + '= \n\n' + + '? Wrong number of arguments\n' + + '? Usage: play \n\n' + + '? Wrong number of arguments\n' + + '? Usage: play \n\n' + + '? Invalid move: Unknown color [1].\n\n' + + '= \n\n' + + '= \n\n' + + '= \n\n' + + '? unknown command\n\n', + value + ) + + testout.close() + + + @unittest.mock.patch('imago.engine.imagoIO.input', create=True) + def testListsCommands(self, mocked_input): + """Test command for listing all commands.""" + + mocked_input.side_effect = [ + 'list_commands\n', + 'quit\n' + ] + + testout = io.StringIO() + imagoIO = ImagoIO( + decisionAlgorithmId=DecisionAlgorithms.MONTECARLO, + outputStream=testout + ) + + commandsString = "\n".join(list(map( + lambda cmd: "%s - %s" % (cmd.__name__, cmd.__doc__), + imagoIO.commands_set))) + + imagoIO.start() + value = testout.getvalue() + self.assertEqual( + '= ' + + commandsString + + '\n\n\n', value ) testout.close() + + @unittest.mock.patch('imago.engine.imagoIO.input', create=True) + def testFixedHandicap(self, mocked_input): + """Test command for setting fixed handicap stones.""" + + mocked_input.side_effect = [ + 'fixed_handicap\n', + 'fixed_handicap 2\n', + 'quit\n' + ] + + testout = io.StringIO() + imagoIO = ImagoIO( + decisionAlgorithmId=DecisionAlgorithms.MONTECARLO, + outputStream=testout + ) + + imagoIO.start() + value = testout.getvalue() + self.assertEqual( + '? Wrong number of arguments\n' + + '? Usage: fixed_handicap \n\n' + + '= A1 A2\n\n', + value + ) + + testout.close() + + +# @unittest.mock.patch('imago.engine.imagoIO.input', create=True) +# def testGenmove(self, mocked_input): +# """Test command for generating a move.""" +# +# mocked_input.side_effect = [ +# 'genmove\n', +# 'genmove w w\n', +# 'genmove 2\n', +# 'quit\n' +# ] +# +# testout = io.StringIO() +# imagoIO = ImagoIO( +# decisionAlgorithmId=DecisionAlgorithms.MONTECARLO, +# outputStream=testout +# ) +# +# imagoIO.start() +# value = testout.getvalue() +# self.assertEqual( +# '? Wrong number of arguments\n' + +# '? Usage: genmove \n\n' + +# '? Wrong number of arguments\n' + +# '? Usage: genmove \n\n' + +# '? Error: Unknown color [2].\n\n', +# value +# ) +# +# mocked_input.side_effect = [ +# 'genmove w\n', +# 'quit\n'] +# +# testout = io.StringIO() +# imagoIO = ImagoIO( +# decisionAlgorithmId=DecisionAlgorithms.MONTECARLO, +# outputStream=testout +# ) +# imagoIO.start() +# value = testout.getvalue() +# vertexValue = value.split(' ')[1].split('\n')[0] +# +# # Test parsing vertex does not raise an error +# parseVertex(vertexValue, imagoIO.gameEngine.gameState.size) +# +# testout.close() + + if __name__ == '__main__': unittest.main() diff --git a/tests/test_parseHelpers.py b/tests/test_parseHelpers.py index 7bbf152..c1405fb 100644 --- a/tests/test_parseHelpers.py +++ b/tests/test_parseHelpers.py @@ -26,7 +26,7 @@ class TestParseHelpers(unittest.TestCase): ) parsedMove = parseHelpers.parseMove(["B", "t1"], TEST_BOARD_SIZE) - targetMove = parseHelpers.GtpMove(Player.BLACK, [18, 18]) + targetMove = parseHelpers.GtpMove(Player.BLACK, (18, 18)) self.assertEqual(parsedMove.color, targetMove.color) self.assertEqual(parsedMove.vertex, targetMove.vertex) @@ -54,26 +54,26 @@ class TestParseHelpers(unittest.TestCase): """Test correct inputs and their resulting coordinates for parseVertex.""" self.assertEqual(parseHelpers.parseVertex( "a1", TEST_BOARD_SIZE), - [18,0]) + (18,0)) self.assertEqual(parseHelpers.parseVertex( "b1", TEST_BOARD_SIZE), - [18,1]) + (18,1)) self.assertEqual(parseHelpers.parseVertex( "a2", TEST_BOARD_SIZE), - [17,0]) + (17,0)) self.assertEqual(parseHelpers.parseVertex( "b2", TEST_BOARD_SIZE), - [17,1]) + (17,1)) self.assertEqual(parseHelpers.parseVertex( "T1", TEST_BOARD_SIZE), - [18,18]) + (18,18)) self.assertEqual(parseHelpers.parseVertex( "T19", TEST_BOARD_SIZE), - [0,18]) + (0,18)) self.assertEqual(parseHelpers.parseVertex( "A19", TEST_BOARD_SIZE), - [0,0]) + (0,0)) self.assertEqual(parseHelpers.parseVertex( "pass", TEST_BOARD_SIZE), @@ -81,10 +81,14 @@ class TestParseHelpers(unittest.TestCase): def testVertexToString(self): """Test converting vertices to strings.""" - self.assertEqual(parseHelpers.vertexToString([0,0], TEST_BOARD_SIZE), "A19") - self.assertEqual(parseHelpers.vertexToString([1,0], TEST_BOARD_SIZE), "A18") - self.assertEqual(parseHelpers.vertexToString([2,0], TEST_BOARD_SIZE), "A17") - self.assertEqual(parseHelpers.vertexToString([0,1], TEST_BOARD_SIZE), "B19") + + # Try with vertices as tuples + self.assertEqual(parseHelpers.vertexToString((0,0), TEST_BOARD_SIZE), "A19") + self.assertEqual(parseHelpers.vertexToString((1,0), TEST_BOARD_SIZE), "A18") + self.assertEqual(parseHelpers.vertexToString((2,0), TEST_BOARD_SIZE), "A17") + self.assertEqual(parseHelpers.vertexToString((0,1), TEST_BOARD_SIZE), "B19") + + # Try with vertices as arrays self.assertEqual(parseHelpers.vertexToString([0,2], TEST_BOARD_SIZE), "C19") self.assertEqual(parseHelpers.vertexToString([0,18], TEST_BOARD_SIZE), "T19") self.assertEqual(parseHelpers.vertexToString([18,0], TEST_BOARD_SIZE), "A1") @@ -93,10 +97,10 @@ class TestParseHelpers(unittest.TestCase): self.assertEqual(parseHelpers.vertexToString("pass", TEST_BOARD_SIZE), "pass") wrongVertices = [ - [-1,0], - [0,-1], - [-1,-1], - [19,0], + (-1,0), + (0,-1), + (-1,-1), + (19,0), [0,19], [19,19], [0], -- cgit v1.2.1