From 65adea3cf601525c32279a5ae36a23c1cde6a387 Mon Sep 17 00:00:00 2001 From: InigoGutierrez Date: Fri, 29 Jan 2021 15:06:27 +0100 Subject: Started implementing tests for gameBoard. --- imago/gameLogic/gameBoard.py | 24 ++++++++++++++---------- tests/test_gameBoard.py | 30 ++++++++++++++++++++++++++++++ 2 files changed, 44 insertions(+), 10 deletions(-) create mode 100644 tests/test_gameBoard.py diff --git a/imago/gameLogic/gameBoard.py b/imago/gameLogic/gameBoard.py index 265b646..bf6c9e4 100644 --- a/imago/gameLogic/gameBoard.py +++ b/imago/gameLogic/gameBoard.py @@ -4,7 +4,7 @@ from copy import deepcopy from imago.data.enums import Player -def getNewBoard(size): +def _getNewBoard(size): """Return a new board.""" board = [] for row in range(size): @@ -14,9 +14,10 @@ def getNewBoard(size): return board class GameBoard: + """Logic and state related to the board.""" def __init__(self, size): - self.board = getNewBoard(size) + self.board = _getNewBoard(size) self.capturesBlack = 0 self.capturesWhite = 0 self.lastStone = None @@ -30,13 +31,16 @@ class GameBoard: newBoard.board = deepcopy(self.board) return newBoard + def getGroupLibertiesCount(self, row, col): + return len(self.getGroupLiberties(row, col)) + def getGroupLiberties(self, row, col): - """Returns the empty vertexes adjacent to the group occupying a cell (its - liberties) or -1 if the cell is empty. + """Returns the empty vertexes adjacent to the group occupying a vertex (its + liberties) as a set. An empty set is returned if the vertex is empty. """ groupColor = self.board[row][col] if groupColor == Player.EMPTY: - return -1 + return {} emptyCells = set() exploredCells = set() self.__exploreLiberties(row, col, groupColor, emptyCells, exploredCells) @@ -114,25 +118,25 @@ class GameBoard: if (self.board[row-1][col] != player and self.board[row-1][col] != Player.EMPTY and len(self.getGroupLiberties(row-1, col)) == 0): - captured += self.captureGroup(row-1, col) + captured += self.__captureGroup(row-1, col) if row < len(self.board)-1: if (self.board[row+1][col] != player and self.board[row+1][col] != Player.EMPTY and len(self.getGroupLiberties(row+1, col)) == 0): - captured += self.captureGroup(row+1, col) + captured += self.__captureGroup(row+1, col) if col > 0: if (self.board[row][col-1] != player and self.board[row][col-1] != Player.EMPTY and len(self.getGroupLiberties(row, col-1)) == 0): - captured += self.captureGroup(row, col-1) + captured += self.__captureGroup(row, col-1) if col < len(self.board[0])-1: if (self.board[row][col+1] != player and self.board[row][col+1] != Player.EMPTY and len(self.getGroupLiberties(row, col+1)) == 0): - captured += self.captureGroup(row, col+1) + captured += self.__captureGroup(row, col+1) return captured - def captureGroup(self, row, col): + def __captureGroup(self, row, col): """Removes all the stones from the group occupying the given cell and returns the number of removed stones. """ diff --git a/tests/test_gameBoard.py b/tests/test_gameBoard.py new file mode 100644 index 0000000..2fa1037 --- /dev/null +++ b/tests/test_gameBoard.py @@ -0,0 +1,30 @@ +"""Tests for gameBoard module.""" + +import unittest + +from imago.data.enums import Player +from imago.gameLogic.gameBoard import GameBoard + +#from imago.data.enums import Player + +TEST_BOARD_SIZE = 19 + +class TestGameBoard(unittest.TestCase): + """Test gameBoard module.""" + + def testGetGroupLiberties(self): + """Test calculation of group liberties.""" + board = GameBoard(TEST_BOARD_SIZE) + + #Empty cell liberties + self.assertEqual(board.getGroupLiberties(0,0), {}) + self.assertEqual(board.getGroupLibertiesCount(0,0), 0) + + # Lone stone liberties + board.board[3][3] = Player.WHITE + self.assertEqual(board.getGroupLiberties(3,3), + {(2,3), (3,2), (4,3), (3,4)}) + self.assertEqual(board.getGroupLibertiesCount(3,3), 4) + +if __name__ == '__main__': + unittest.main() -- cgit v1.2.1 From bcc55cedada03c66e92bfc3aac3b73245b89aaf8 Mon Sep 17 00:00:00 2001 From: InigoGutierrez Date: Fri, 5 Feb 2021 19:10:43 +0100 Subject: Removed shebang from non-executable files. --- imago/engine/core.py | 2 -- imago/engine/imagoIO.py | 2 -- 2 files changed, 4 deletions(-) diff --git a/imago/engine/core.py b/imago/engine/core.py index 28166b8..0960319 100644 --- a/imago/engine/core.py +++ b/imago/engine/core.py @@ -1,5 +1,3 @@ -#!/usr/bin/python - """Imago GTP engine""" from random import randrange diff --git a/imago/engine/imagoIO.py b/imago/engine/imagoIO.py index b929788..f676b69 100644 --- a/imago/engine/imagoIO.py +++ b/imago/engine/imagoIO.py @@ -1,5 +1,3 @@ -#!/usr/bin/python - """Imago GTP engine input output""" import sys -- cgit v1.2.1 From a9f645e19dd80f243c0e246e3dca9465207e60cb Mon Sep 17 00:00:00 2001 From: InigoGutierrez Date: Mon, 8 Feb 2021 17:21:51 +0100 Subject: logic: Made move legality checks independent. --- imago/gameLogic/gameBoard.py | 134 +++++++++++++++++++++++++++++++++---------- imago/gameLogic/gameMove.py | 30 +++++++--- imago/gameLogic/gameState.py | 72 +++++++++-------------- 3 files changed, 155 insertions(+), 81 deletions(-) diff --git a/imago/gameLogic/gameBoard.py b/imago/gameLogic/gameBoard.py index bf6c9e4..42b50d9 100644 --- a/imago/gameLogic/gameBoard.py +++ b/imago/gameLogic/gameBoard.py @@ -4,27 +4,36 @@ from copy import deepcopy from imago.data.enums import Player -def _getNewBoard(size): +def _getNewBoard(height, width): """Return a new board.""" board = [] - for row in range(size): + for row in range(height): board.append([]) - for _ in range(size): + for _ in range(width): board[row].append(Player.EMPTY) return board class GameBoard: """Logic and state related to the board.""" - def __init__(self, size): - self.board = _getNewBoard(size) + def __init__(self, height, width): + self.board = _getNewBoard(height, width) self.capturesBlack = 0 self.capturesWhite = 0 self.lastStone = None + def getBoardHeight(self): + """Returns the number of rows in the board.""" + return len(self.board) + + def getBoardWidth(self): + """Returns the number of columns of the first row of the board. This number should + be the same for all the rows.""" + return len(self.board[0]) + def getDeepCopy(self): """Returns a copy GameBoard.""" - newBoard = GameBoard(len(self.board)) + newBoard = GameBoard(self.getBoardHeight(), self.getBoardWidth()) newBoard.capturesBlack = self.capturesBlack newBoard.capturesWhite = self.capturesWhite newBoard.lastStone = self.lastStone @@ -32,6 +41,7 @@ class GameBoard: return newBoard def getGroupLibertiesCount(self, row, col): + """Returns the number of liberties of a group.""" return len(self.getGroupLiberties(row, col)) def getGroupLiberties(self, row, col): @@ -40,7 +50,7 @@ class GameBoard: """ groupColor = self.board[row][col] if groupColor == Player.EMPTY: - return {} + return set() emptyCells = set() exploredCells = set() self.__exploreLiberties(row, col, groupColor, emptyCells, exploredCells) @@ -67,11 +77,11 @@ class GameBoard: self.__exploreLiberties(row-1, col, groupColor, emptyCells, exploredCells) # Right - if col < len(self.board[0])-1: + if col < self.getBoardWidth()-1: self.__exploreLiberties(row, col+1, groupColor, emptyCells, exploredCells) # Down - if row < len(self.board)-1: + if row < self.getBoardHeight()-1: self.__exploreLiberties(row+1, col, groupColor, emptyCells, exploredCells) # Left @@ -97,55 +107,121 @@ class GameBoard: self.__exploreGroup(row-1, col, groupColor, cells) # Right - if col < len(self.board[0])-1: + if col < self.getBoardWidth()-1: self.__exploreGroup(row, col+1, groupColor, cells) # Down - if row < len(self.board)-1: + if row < self.getBoardHeight()-1: self.__exploreGroup(row+1, col, groupColor, cells) # Left if col > 0: self.__exploreGroup(row, col-1, groupColor, cells) - - def moveCapture(self, row, col, player): - """Checks surrounding captures of a move, removes them and returns the number of - stones captured. + def moveAndCapture(self, row, col, player): + """Checks surrounding captures of a move, removes them and returns a set + containing the vertices where stones were captured. """ - captured = 0 + captured = set() + if row > 0: if (self.board[row-1][col] != player and self.board[row-1][col] != Player.EMPTY and len(self.getGroupLiberties(row-1, col)) == 0): - captured += self.__captureGroup(row-1, col) - if row < len(self.board)-1: + captured.add(self.__captureGroup(row-1, col)) + + if row < self.getBoardHeight()-1: if (self.board[row+1][col] != player and self.board[row+1][col] != Player.EMPTY and len(self.getGroupLiberties(row+1, col)) == 0): - captured += self.__captureGroup(row+1, col) + captured.add(self.__captureGroup(row+1, col)) + if col > 0: if (self.board[row][col-1] != player and self.board[row][col-1] != Player.EMPTY and len(self.getGroupLiberties(row, col-1)) == 0): - captured += self.__captureGroup(row, col-1) - if col < len(self.board[0])-1: + captured.add(self.__captureGroup(row, col-1)) + + if col < self.getBoardWidth()-1: if (self.board[row][col+1] != player and self.board[row][col+1] != Player.EMPTY and len(self.getGroupLiberties(row, col+1)) == 0): - captured += self.__captureGroup(row, col+1) + captured.add(self.__captureGroup(row, col+1)) + return captured def __captureGroup(self, row, col): - """Removes all the stones from the group occupying the given cell and returns the - number of removed stones. + """Removes all the stones from the group occupying the given cell and returns a + set containing them. """ cellsToCapture = self.getGroupCells(row, col) - count = 0 for cell in cellsToCapture: self.board[cell[0]][cell[1]] = Player.EMPTY - count += 1 - return count + return cellsToCapture + + def isMoveInBoardBounds(self, row, col): + """Returns True if move is inside board bounds, false otherwise.""" + return 0 <= row < self.getBoardHeight() and 0 <= col < self.getBoardWidth() + + def isCellEmpty(self, row, col): + """Returns True if cell is empty, false otherwise.""" + return self.board[row][col] == Player.EMPTY + + def isMoveSuicidal(self, row, col, player): + """Returns True if move is suicidal.""" + + # Check vertex is empty + if not self.isCellEmpty(row, col): + raise RuntimeError("Cell to play should be empty when checking for suicide.") + + # Play and capture + self.board[row][col] = player + groupLiberties = self.getGroupLibertiesCount(row, col) + captured = self.moveAndCapture(row, col, player) + + # If move didn't capture anything and its group is left without liberties, it's + # suicidal + if len(captured) == 0 and groupLiberties == 0: + # Restore captured stones + for vertex in captured: + self.board[vertex[0]][vertex[1]] = Player.otherPlayer(player) + self.board[row][col] = Player.EMPTY + # Remove played stone + return True + + def isMoveKoIllegal(self, row, col, player, prevBoards): + """Returns True if move is illegal because of ko.""" + + # Check vertex is empty + if not self.isCellEmpty(row, col): + raise RuntimeError("Cell to play should be empty when checking for ko.") + + illegal = False + # Temporarily place stone to play for comparisons + self.board[row][col] = player + # Check previous boards + for prevBoard in prevBoards: + # A ko is possible in boards where the stone to play exists + if prevBoard.board[row][col] == player: + if self.equals(prevBoard): + illegal = True + + # Remove temporarily placed stone + self.board[row][col] = Player.EMPTY + return illegal + + def equals(self, otherBoard): + """Returns true if this board is equal to another board. Only takes into account + dimensions and placed stones. + """ + if ( self.getBoardHeight() != otherBoard.getBoardHeight() + or self.getBoardWidth() != otherBoard.getBoardWidth() ): + return False + for row in range(self.getBoardHeight()): + for col in range(self.getBoardWidth()): + if self.board[row][col] != otherBoard[row][col]: + return False + return True def printBoard(self): """Print the board.""" @@ -154,7 +230,7 @@ class GameBoard: # Print column names rowText = " " * (rowTitlePadding + 2) - for col in range(len(self.board[0])): + for col in range(self.getBoardWidth()): rowText += colTitle + " " colTitle = chr(ord(colTitle)+1) if colTitle == "I": # Skip I @@ -162,7 +238,7 @@ class GameBoard: print(rowText) # Print rows - rowTitle = len(self.board) + rowTitle = self.getBoardHeight() for row in self.board: rowText = "" for col in row: diff --git a/imago/gameLogic/gameMove.py b/imago/gameLogic/gameMove.py index bd210b4..dc93909 100644 --- a/imago/gameLogic/gameMove.py +++ b/imago/gameLogic/gameMove.py @@ -1,19 +1,35 @@ """Information about one move.""" class GameMove: + """Stores information about a move. A move in this context is one position of the Game + Tree: the board can be empty, or the move can consist of more than one added or + removed stones.""" - def __init__(self, player, row, col, makesKo, board): + def __init__(self, player, board): self.player = player - self.row = row - self.col = col - self.makesKo = makesKo self.board = board self.nextMoves = [] self.previousMove = None - def addMove(self, player, row, col, makesKo, board): - """Adds a move to the next moves list.""" - newMove = GameMove(player, row, col, makesKo, board) + def getRow(self): + """Returns the row of the vertex the move was played on.""" + return self.board.lastStone[0] + + def getCol(self): + """Returns the column of the vertex the move was played on.""" + return self.board.lastStone[1] + + def addMove(self, row, col, player): + """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. + """ + newBoard = self.board.getDeepCopy() + newBoard.board[row][col] = player + return self.addMoveWithBoard(player, newBoard) + + def addMoveWithBoard(self, player, board): + """Adds a move to the next moves list containing the provided board.""" + newMove = GameMove(player, board) newMove.previousMove = self self.nextMoves.append(newMove) return newMove diff --git a/imago/gameLogic/gameState.py b/imago/gameLogic/gameState.py index 7a96962..0108887 100644 --- a/imago/gameLogic/gameState.py +++ b/imago/gameLogic/gameState.py @@ -10,14 +10,17 @@ class GameState: def __init__(self, size): self.size = size - self.gameTree = None - self.lastMove = None - self.initState() + self.gameTree = GameTree() + newBoard = GameBoard(self.size, self.size) + self.lastMove = GameMove(Player.EMPTY, newBoard) + self.gameTree.firstMoves.append(self.lastMove) def getCurrentPlayer(self): """Gets the player who should make the next move.""" if self.lastMove is None: return Player.BLACK + if self.lastMove.player is Player.EMPTY: + return Player.BLACK return Player.otherPlayer(self.lastMove.player) def getPlayerCode(self): @@ -27,7 +30,7 @@ class GameState: def getBoard(self): """Returns the board as of the last move.""" if self.lastMove is None: - return GameBoard(self.size) + return GameBoard(self.size, self.size) return self.lastMove.board def playMove(self, row, col): @@ -42,64 +45,43 @@ class GameState: print("Invalid move!") return False - newBoard = self.getBoard().getDeepCopy() - - newBoard.board[row][col] = player - - groupLiberties = newBoard.getGroupLiberties(row, col) - # Check suicide - killed = newBoard.moveCapture(row, col, player) - if killed == 0 and len(groupLiberties) == 0: + if self.getBoard().isMoveSuicidal(row, col, player): print("Invalid move! (Suicide)") return False # Check ko - if self.lastMove is not None: - illegalKoVertex = self.lastMove.makesKo - if illegalKoVertex is not None: - if row == illegalKoVertex[0] and col == illegalKoVertex[1]: - print("Invalid move! (Ko)") - return False + prevBoards = [] + checkedMove = self.lastMove + while checkedMove is not None: + prevBoards.append(checkedMove.board) + checkedMove = checkedMove.previousMove + if self.getBoard().isMoveKoIllegal(row, col, player, prevBoards): + print("Invalid move! (Ko)") + return False # Move is legal - - # Check if move makes ko - makesKo = None - if killed == 1 and len(groupLiberties) == 1: - makesKo = groupLiberties[0] - - self.__addMove(player, row, col, makesKo, newBoard) + self.__addMove(player, row, col) return True def undo(self): """Sets the move before the last move as the new last move.""" self.lastMove = self.lastMove.previousMove - def initState(self): - """Starts current player, captured stones, board and game tree.""" - self.capturesBlack = 0 - self.capturesWhite = 0 - self.gameTree = GameTree() - self.lastMove = None - - def clearBoard(self): - """Clears the board, captured stones and game tree.""" - self.initState() - def prevalidateMove(self, row, col): - """Returns True if move is valid, False if not.""" - if (row < 0 or row >= self.size - or col < 0 or col >= self.size): + """Returns True if move is inside bounds and cell is empty, False if not.""" + if not self.getBoard().isMoveInBoardBounds(row, col): return False - if self.getBoard().board[row][col] != Player.EMPTY: + if not self.getBoard().isCellEmpty(row, col): return False return True - def __addMove(self, player, row, col, makesKo, newBoard): + def __addMove(self, player, row, col): + + # Check a last move already exists if self.lastMove is None: - self.lastMove = GameMove(player, row, col, makesKo, newBoard) - self.gameTree.firstMoves.append(self.lastMove) - else: - self.lastMove = self.lastMove.addMove(player, row, col, makesKo, newBoard) + raise RuntimeError("Last move of the GameState is None.") + + # Add and return the new move + self.lastMove = self.lastMove.addMove(player, row, col) return self.lastMove -- cgit v1.2.1 From 5cf270c29aec83bd81121e6b72f24e2c35340e38 Mon Sep 17 00:00:00 2001 From: InigoGutierrez Date: Tue, 9 Feb 2021 20:40:13 +0100 Subject: logic: Fixed ko checks in gameBoard.py --- imago/gameLogic/gameBoard.py | 45 ++++++++++++++++++++++++++++---------------- imago/gameLogic/gameMove.py | 29 ++++++++++++++++++++++++---- imago/gameLogic/gameState.py | 8 ++------ 3 files changed, 56 insertions(+), 26 deletions(-) diff --git a/imago/gameLogic/gameBoard.py b/imago/gameLogic/gameBoard.py index 42b50d9..327728b 100644 --- a/imago/gameLogic/gameBoard.py +++ b/imago/gameLogic/gameBoard.py @@ -122,31 +122,34 @@ class GameBoard: """Checks surrounding captures of a move, removes them and returns a set containing the vertices where stones were captured. """ + + self.board[row][col] = player + captured = set() if row > 0: if (self.board[row-1][col] != player and self.board[row-1][col] != Player.EMPTY and len(self.getGroupLiberties(row-1, col)) == 0): - captured.add(self.__captureGroup(row-1, col)) + captured.update(self.__captureGroup(row-1, col)) if row < self.getBoardHeight()-1: if (self.board[row+1][col] != player and self.board[row+1][col] != Player.EMPTY and len(self.getGroupLiberties(row+1, col)) == 0): - captured.add(self.__captureGroup(row+1, col)) + captured.update(self.__captureGroup(row+1, col)) if col > 0: if (self.board[row][col-1] != player and self.board[row][col-1] != Player.EMPTY and len(self.getGroupLiberties(row, col-1)) == 0): - captured.add(self.__captureGroup(row, col-1)) + captured.update(self.__captureGroup(row, col-1)) if col < self.getBoardWidth()-1: if (self.board[row][col+1] != player and self.board[row][col+1] != Player.EMPTY and len(self.getGroupLiberties(row, col+1)) == 0): - captured.add(self.__captureGroup(row, col+1)) + captured.update(self.__captureGroup(row, col+1)) return captured @@ -174,20 +177,23 @@ class GameBoard: if not self.isCellEmpty(row, col): raise RuntimeError("Cell to play should be empty when checking for suicide.") - # Play and capture + # Temporarily play and capture self.board[row][col] = player groupLiberties = self.getGroupLibertiesCount(row, col) captured = self.moveAndCapture(row, col, player) + illegal = False # If move didn't capture anything and its group is left without liberties, it's # suicidal if len(captured) == 0 and groupLiberties == 0: - # Restore captured stones - for vertex in captured: - self.board[vertex[0]][vertex[1]] = Player.otherPlayer(player) - self.board[row][col] = Player.EMPTY - # Remove played stone - return True + illegal = True + + # Restore captured stones + for vertex in captured: + self.board[vertex[0]][vertex[1]] = Player.otherPlayer(player) + # Remove temporarily played stone + self.board[row][col] = Player.EMPTY + return illegal def isMoveKoIllegal(self, row, col, player, prevBoards): """Returns True if move is illegal because of ko.""" @@ -197,8 +203,8 @@ class GameBoard: raise RuntimeError("Cell to play should be empty when checking for ko.") illegal = False - # Temporarily place stone to play for comparisons - self.board[row][col] = player + # Temporarily play and capture for comparisons + captured = self.moveAndCapture(row, col, player) # Check previous boards for prevBoard in prevBoards: # A ko is possible in boards where the stone to play exists @@ -206,7 +212,10 @@ class GameBoard: if self.equals(prevBoard): illegal = True - # Remove temporarily placed stone + # Restore captured stones + for vertex in captured: + self.board[vertex[0]][vertex[1]] = Player.otherPlayer(player) + # Remove temporarily played stone self.board[row][col] = Player.EMPTY return illegal @@ -219,7 +228,7 @@ class GameBoard: return False for row in range(self.getBoardHeight()): for col in range(self.getBoardWidth()): - if self.board[row][col] != otherBoard[row][col]: + if self.board[row][col] != otherBoard.board[row][col]: return False return True @@ -227,9 +236,13 @@ class GameBoard: """Print the board.""" colTitle = 'A' rowTitlePadding = 2 + if self.getBoardHeight() >= 10: + firstRowPadding = 2 + else: + firstRowPadding = 1 # Print column names - rowText = " " * (rowTitlePadding + 2) + rowText = " " * (rowTitlePadding + firstRowPadding) for col in range(self.getBoardWidth()): rowText += colTitle + " " colTitle = chr(ord(colTitle)+1) diff --git a/imago/gameLogic/gameMove.py b/imago/gameLogic/gameMove.py index dc93909..b2db34f 100644 --- a/imago/gameLogic/gameMove.py +++ b/imago/gameLogic/gameMove.py @@ -1,5 +1,7 @@ """Information about one move.""" +from imago.data.enums import Player + class GameMove: """Stores information about a move. A move in this context is one position of the Game Tree: the board can be empty, or the move can consist of more than one added or @@ -19,15 +21,34 @@ class GameMove: """Returns the column of the vertex the move was played on.""" return self.board.lastStone[1] - def addMove(self, row, col, player): + def getThisAndPrevBoards(self): + """Returns an array with all the boards of this and previous moves.""" + prevBoards = [] + checkedMove = self.previousMove + while checkedMove is not None: + prevBoards.append(checkedMove.board) + checkedMove = checkedMove.previousMove + return prevBoards + + def addMove(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. + """ + if self.player == Player.EMPTY: + player = Player.BLACK + else: + player = Player.otherPlayer(self.player) + return self.addMoveForPlayer(row, col, player) + + def addMoveForPlayer(self, row, col, player): """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. """ newBoard = self.board.getDeepCopy() - newBoard.board[row][col] = player - return self.addMoveWithBoard(player, newBoard) + newBoard.moveAndCapture(row, col, player) + return self.addMoveForPlayerAndBoard(player, newBoard) - def addMoveWithBoard(self, player, board): + def addMoveForPlayerAndBoard(self, player, board): """Adds a move to the next moves list containing the provided board.""" newMove = GameMove(player, board) newMove.previousMove = self diff --git a/imago/gameLogic/gameState.py b/imago/gameLogic/gameState.py index 0108887..cc65c75 100644 --- a/imago/gameLogic/gameState.py +++ b/imago/gameLogic/gameState.py @@ -51,11 +51,7 @@ class GameState: return False # Check ko - prevBoards = [] - checkedMove = self.lastMove - while checkedMove is not None: - prevBoards.append(checkedMove.board) - checkedMove = checkedMove.previousMove + prevBoards = self.lastMove.getThisAndPrevBoards() if self.getBoard().isMoveKoIllegal(row, col, player, prevBoards): print("Invalid move! (Ko)") return False @@ -83,5 +79,5 @@ class GameState: raise RuntimeError("Last move of the GameState is None.") # Add and return the new move - self.lastMove = self.lastMove.addMove(player, row, col) + self.lastMove = self.lastMove.addMoveForPlayer(row, col, player) return self.lastMove -- cgit v1.2.1 From 3c8124e26898e58ea835194a255da0c04b2ecfac Mon Sep 17 00:00:00 2001 From: InigoGutierrez Date: Wed, 10 Feb 2021 19:25:34 +0100 Subject: logic: Reorganized move evaluation. GameBoard able to know if a move is valid, given the previous boards. GameMove no longer stores player, it gets it from board's last move. --- imago/gameLogic/gameBoard.py | 19 +++++++++++++++++++ imago/gameLogic/gameMove.py | 19 ++++++++++++++++--- imago/gameLogic/gameState.py | 36 ++++++++---------------------------- 3 files changed, 43 insertions(+), 31 deletions(-) diff --git a/imago/gameLogic/gameBoard.py b/imago/gameLogic/gameBoard.py index 327728b..c6a6007 100644 --- a/imago/gameLogic/gameBoard.py +++ b/imago/gameLogic/gameBoard.py @@ -31,6 +31,12 @@ class GameBoard: be the same for all the rows.""" return len(self.board[0]) + def getLastPlayer(self): + """Returns the player who placed the last stone.""" + if self.lastStone is None: + return Player.EMPTY + return self.board[self.lastStone[0]][self.lastStone[1]] + def getDeepCopy(self): """Returns a copy GameBoard.""" newBoard = GameBoard(self.getBoardHeight(), self.getBoardWidth()) @@ -124,6 +130,7 @@ class GameBoard: """ self.board[row][col] = player + self.lastStone = [row, col] captured = set() @@ -219,6 +226,18 @@ class GameBoard: self.board[row][col] = Player.EMPTY return illegal + def isPlayable(self, row, col, player, prevBoards): + """Determines if a move is playable.""" + if not self.isMoveInBoardBounds(row, col): + return False, "Move outside board bounds." + if not self.isCellEmpty(row, col): + return False, "Vertex is not empty." + if self.isMoveSuicidal(row, col, player): + return False, "Move is suicidal." + if self.isMoveKoIllegal(row, col, player, prevBoards): + return False, "Illegal by ko rule." + return True, "" + def equals(self, otherBoard): """Returns true if this board is equal to another board. Only takes into account dimensions and placed stones. diff --git a/imago/gameLogic/gameMove.py b/imago/gameLogic/gameMove.py index b2db34f..90ec4bf 100644 --- a/imago/gameLogic/gameMove.py +++ b/imago/gameLogic/gameMove.py @@ -8,7 +8,6 @@ class GameMove: removed stones.""" def __init__(self, player, board): - self.player = player self.board = board self.nextMoves = [] self.previousMove = None @@ -21,6 +20,10 @@ class GameMove: """Returns the column of the vertex the move was played on.""" return self.board.lastStone[1] + def getLastPlayer(self): + """Returns the player who placed the last stone.""" + return self.board.getLastPlayer() + def getThisAndPrevBoards(self): """Returns an array with all the boards of this and previous moves.""" prevBoards = [] @@ -30,14 +33,24 @@ class GameMove: checkedMove = checkedMove.previousMove return prevBoards + def getPlayableVertices(self): + """Returns a set with the playable vertices.""" + playables = set() + player = Player.otherPlayer(self.getLastPlayer()) + prevBoards = self.getThisAndPrevBoards() + for row in range(self.board.getBoardHeight()): + for col in range(self.board.getBoardWidth()): + if self.board.isPlayable(row, col, player, prevBoards): + playables.add((row, col)) + def addMove(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. """ - if self.player == Player.EMPTY: + if self.getLastPlayer() == Player.EMPTY: player = Player.BLACK else: - player = Player.otherPlayer(self.player) + player = Player.otherPlayer(self.getLastPlayer()) return self.addMoveForPlayer(row, col, player) def addMoveForPlayer(self, row, col, player): diff --git a/imago/gameLogic/gameState.py b/imago/gameLogic/gameState.py index cc65c75..e4dd629 100644 --- a/imago/gameLogic/gameState.py +++ b/imago/gameLogic/gameState.py @@ -19,9 +19,9 @@ class GameState: """Gets the player who should make the next move.""" if self.lastMove is None: return Player.BLACK - if self.lastMove.player is Player.EMPTY: + if self.lastMove.getLastPlayer() is Player.EMPTY: return Player.BLACK - return Player.otherPlayer(self.lastMove.player) + return Player.otherPlayer(self.lastMove.getLastPlayer()) def getPlayerCode(self): """Gets a string representation of the current player.""" @@ -40,38 +40,18 @@ class GameState: def playMoveForPlayer(self, row, col, player): """Execute a move on the board for the given player.""" - # Check valid move - if not self.prevalidateMove(row, col): - print("Invalid move!") - return False - - # Check suicide - if self.getBoard().isMoveSuicidal(row, col, player): - print("Invalid move! (Suicide)") - return False - - # Check ko prevBoards = self.lastMove.getThisAndPrevBoards() - if self.getBoard().isMoveKoIllegal(row, col, player, prevBoards): - print("Invalid move! (Ko)") - return False - - # Move is legal - self.__addMove(player, row, col) - return True + playable, message = self.lastMove.board.isPlayable(row, col, player, prevBoards) + if playable: + self.__addMove(player, row, col) + return True + print("Invalid Move! %s" % message) + return False def undo(self): """Sets the move before the last move as the new last move.""" self.lastMove = self.lastMove.previousMove - def prevalidateMove(self, row, col): - """Returns True if move is inside bounds and cell is empty, False if not.""" - if not self.getBoard().isMoveInBoardBounds(row, col): - return False - if not self.getBoard().isCellEmpty(row, col): - return False - return True - def __addMove(self, player, row, col): # Check a last move already exists -- cgit v1.2.1 From e8a3007e25c32ed8014b5e524849dfb38e9bef13 Mon Sep 17 00:00:00 2001 From: InigoGutierrez Date: Wed, 10 Feb 2021 19:26:55 +0100 Subject: logic: Added monteCarlo.py as an incomplete implementation of MCTS. --- imago/engine/monteCarlo.py | 59 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 59 insertions(+) create mode 100644 imago/engine/monteCarlo.py diff --git a/imago/engine/monteCarlo.py b/imago/engine/monteCarlo.py new file mode 100644 index 0000000..13f5c47 --- /dev/null +++ b/imago/engine/monteCarlo.py @@ -0,0 +1,59 @@ +"""Monte Carlo Tree Search module.""" + +class MCTS: + """Monte Carlo tree.""" + + def __init__(self, root): + self.root = root + + def selection(self): + """Select the most promising node with unexplored children.""" + bestUCB = 0 + bestNode = None + bestUCB, bestNode = self._selectionRec(self.root, bestUCB, bestNode) + return bestNode + + def __selectionRec(self, node, bestUCB, bestNode): + + # Check if node has unexplored children and better UCB than previously explored + if len(node.unexploredVertices) > 0: + ucb = node.ucb() + if ucb > bestUCB: + bestUCB = ucb + bestNode = node + + # Recursively search children for better UCB + for child in node.children: + bestUCB, bestNode = self._selectionRec(child, bestUCB, bestNode) + + return bestUCB, bestNode + + def expansion(self, node): + # Get a random unexplored vertex and remove it from the set + newVertex = node.unexploredVertices.pop() + newNode = MCTSNode(newVertex[0], newVertex[1], node) + parent.children.add(self) + return newNode + + def simulation(self, node): + + def backup(self, node): + + +class MCTSNode: + """Monte Carlo tree node.""" + + def __init__(self, move, parent): + self.visits = 0 + self.score = 0 + self.move = move + self.parent = parent + self.children = set() + self.unexploredVertices = move.getPlayableVertices() + + def ucb(self): + """Returns Upper Confidence Bound of node""" + # meanVictories + 1/visits + mean = self.score / self.visits + adjust = 1/self.visits + return mean + adjust -- cgit v1.2.1 From 4a39a8fd07e49db5feb0c403b784423f0b673f97 Mon Sep 17 00:00:00 2001 From: InigoGutierrez Date: Sat, 12 Jun 2021 10:58:21 +0200 Subject: First MonteCarlo match simulations. --- doc/diagrams/GameBoard.pumlc | 28 +++++++ doc/diagrams/GameMove.pumlc | 20 +++-- doc/diagrams/GameState.pumlc | 16 ++-- doc/diagrams/GameTree.pumlc | 4 +- doc/diagrams/gameRepresentation.puml | 9 ++- doc/diagrams/gtpEngine.puml | 15 +++- doc/tex/implementation.tex | 34 +++++--- doc/tex/previousWorks.tex | 15 ++-- doc/tex/tfg.tex | 4 +- go.py | 4 +- imago/engine/core.py | 19 ++++- imago/engine/monteCarlo.py | 91 ++++++++++++++------- imago/engine/parseHelpers.py | 9 ++- imago/gameLogic/gameBoard.py | 148 ++++++++++++++++++----------------- imago/gameLogic/gameMove.py | 68 ++++++++++++---- imago/gameLogic/gameState.py | 10 ++- tests/test_gameBoard.py | 84 +++++++++++++++++++- tests/test_gameMove.py | 78 ++++++++++++++++++ tests/test_monteCarlo.py | 22 ++++++ 19 files changed, 513 insertions(+), 165 deletions(-) create mode 100644 doc/diagrams/GameBoard.pumlc create mode 100644 tests/test_gameMove.py create mode 100644 tests/test_monteCarlo.py diff --git a/doc/diagrams/GameBoard.pumlc b/doc/diagrams/GameBoard.pumlc new file mode 100644 index 0000000..7a57b2d --- /dev/null +++ b/doc/diagrams/GameBoard.pumlc @@ -0,0 +1,28 @@ +@startuml + +class GameBoard { + int[][] board + int capturesBlack + int capturesWhite + + getBoard(self) + getBoardHeight(self) + getBoardWidth(self) + getDeepCopy(self) + getGroupLiberties(self, row, col) + getGroupLibertiesCount(self, row, col) + getGroupCells(self, row, col) + getGroupCellsCount(self, row, col) + moveAndCapture(self, row, col, player) + isMoveInBoardBounds(self, row, col) + isCellEmpty(self, row, col) + isCellEye(self, row, col) + isMoveSuicidal(self, row, col, player) + isMoveKoIllegal(self, row, col, player, prevBoards) + isPlayable(self, row, col, player, prevBoards) + score(self) + equals(self, otherBoard) + printBoard(self) +} + +@enduml diff --git a/doc/diagrams/GameMove.pumlc b/doc/diagrams/GameMove.pumlc index 7dcb5e3..0f75edd 100644 --- a/doc/diagrams/GameMove.pumlc +++ b/doc/diagrams/GameMove.pumlc @@ -1,13 +1,23 @@ @startuml class GameMove { - int player - int row - int col - int[2] makesKo - int[] board + GameBoard board GameMove[] nextMoves GameMove previousMove + boolean isPass + int[2] coords + + getRow(self) + getCol(self) + getPlayer(self) + getNextPlayer(self) + getGameLength(self) + getThisAndPrevBoards(self) + getPlayableVertices(self) + addMove(self, row, col) + addMoveForPlayer(self, row, col, player) + addPass(self) + printBoard(self) } @enduml diff --git a/doc/diagrams/GameState.pumlc b/doc/diagrams/GameState.pumlc index db39d8f..38e1397 100644 --- a/doc/diagrams/GameState.pumlc +++ b/doc/diagrams/GameState.pumlc @@ -1,14 +1,18 @@ @startuml class GameState { - int player - int capturesBlack - int capturesWhite - List board - List prevBoards # for ko + int size GameTree gameTree GameMove lastMove - GameData gameData + 'GameData gameData + + getCurrentPlayer(self) + getPlayerCode(self) + getBoard(self) + playMove(self, row, col) + playMoveForPlayer(self, row, col, player) + playPass(self) + undo(self) } @enduml diff --git a/doc/diagrams/GameTree.pumlc b/doc/diagrams/GameTree.pumlc index 85859d8..10b9251 100644 --- a/doc/diagrams/GameTree.pumlc +++ b/doc/diagrams/GameTree.pumlc @@ -1,8 +1,8 @@ @startuml class GameTree { - GameNode[] firstMoves - GameData gameData + GameMove[] firstMoves + 'GameData gameData } @enduml diff --git a/doc/diagrams/gameRepresentation.puml b/doc/diagrams/gameRepresentation.puml index 9a869f1..db5d5a5 100644 --- a/doc/diagrams/gameRepresentation.puml +++ b/doc/diagrams/gameRepresentation.puml @@ -5,10 +5,13 @@ !include GameState.pumlc !include GameTree.pumlc !include GameMove.pumlc +!include GameBoard.pumlc -GameState --> GameTree -GameState --> GameMove: Current move +GameState -> GameTree +GameState --> GameMove: Last move GameTree *--> GameMove -GameMove -> GameMove +GameMove --> GameMove: Previous move +GameMove *--> GameMove: Next moves +GameBoard <- GameMove @enduml diff --git a/doc/diagrams/gtpEngine.puml b/doc/diagrams/gtpEngine.puml index c4caf32..5b098da 100644 --- a/doc/diagrams/gtpEngine.puml +++ b/doc/diagrams/gtpEngine.puml @@ -2,13 +2,20 @@ !include skinparams.puml -class EngineCore { -} - class IO { processComand() } +class EngineCore { + setBoardsize() + clearBoard() + setKomi() + setFixedHandicap() + play() + genmove() + undo() +} + !include GameState.pumlc 'class EngineBoard { @@ -22,7 +29,7 @@ class EngineAI { genmove(board) } -EngineCore --> IO +IO --> EngineCore EngineCore --> GameState EngineCore --> EngineAI diff --git a/doc/tex/implementation.tex b/doc/tex/implementation.tex index 9e42313..28fd0ec 100644 --- a/doc/tex/implementation.tex +++ b/doc/tex/implementation.tex @@ -2,9 +2,23 @@ \subsection{Engine} -An engine implementing GTP.\@ It is designed to be used by a software controller +An implementation of GTP, that is, the piece of software which offers the GTP +interface to other applications.\@ It is designed to be used by a software controller but can also be directly run, mostly for debugging purposes. Its design is shown -in \fref{fig:engine} +in \fref{fig:engine}. The core of the engine is related with three components, +each with a separate responsibility: + +\begin{itemize} + \item The IO component is the one called from other applications and offers + the text interface. It reads and processes input and calls corresponding + commands from the core of the engine. + \item The EngineBoard component stores the state of the match, recording + information such as the history of boards positions and whose turn goes + next. The engine core uses it for these state-storing purposes. + \item The EngineAI component is responsible of analyzing the match and + generate moves. The engine core uses it when a decision has to be made + by the AI, such as when a move needs to be generated by the engine. +\end{itemize} \begin{figure}[h] \begin{center} @@ -28,14 +42,16 @@ moves. One module to read and write SGF files. Modules are shown in \subsection{Representation of a match} -Strictly said, a match is composed of a series of moves. But since game review -and variants exploration is an important part of Go learing, \program{} allows -for navigation back and forth through the board states of a match and for new +A regular Go match is composed of a list of moves. But since game review and +variants exploration is an important part of Go learning, \program{} allows for +navigation back and forth through the board states of a match and for new variants to be created from each of these board states. Therefore, a match is -represented as a tree of moves. The state of the game must also be present so -liberties, captures and legality of moves can be addressed, so it is represented -with its own class, which holds a reference both to the game tree and the -current move. This classes and their relationship can be seen in +represented as a tree of moves. The state of the board at any given move must +also be stored so liberties, captures count and legality of moves can be +addressed, so it is represented with its own class, which holds a reference both +to the game tree and the current move. Moves depend on a representation of the +game board to have access to its current layout and count of captured stones. +These classes and their relationships can be seen in \fref{fig:gameRepresentation}. \begin{figure}[h] diff --git a/doc/tex/previousWorks.tex b/doc/tex/previousWorks.tex index 6ba5dce..6e503a3 100644 --- a/doc/tex/previousWorks.tex +++ b/doc/tex/previousWorks.tex @@ -2,12 +2,13 @@ \subsection{SGF} -SGF (\textit{Smart Go Format} or \textit{Smart Game Format}) is a text file -format specification for records of games or even collections of them. It was -devised for Go but it supports other games with similar turns structure. It -supports move variations, annotations, setups and game metadata. By supporting -SGF, our application can be used to analyse existing games registered by other -applications, such as those played on online Go servers. +SGF (\textit{Smart Go Format} or, in a more general context, \textit{Smart Game +Format}) is a text file format specification for records of games or collections +of them. It was devised for Go but it supports other games with similar +turn-based structure. It supports move variations, annotations, setups and game +metadata. By supporting SGF our application can be used to analyse existing +games registered by other applications, such as those played on online Go +servers. The SGF specification can be found at \url{https://www.red-bean.com/sgf/user_guide/index.html} @@ -17,5 +18,5 @@ The SGF specification can be found at GTP (\textit{Go Text Protocol}) is a text based protocol for communication with computer go programs. [ref https://www.lysator.liu.se/~gunnar/gtp/] It is the protocol used by GNU Go and the more modern and powerful KataGo. By supporting -GTP, our application can be used with existing GUIs and other programs, making +GTP our application can be used with existing GUIs and other programs, making it easier to use it with the tools users are already familiar with. diff --git a/doc/tex/tfg.tex b/doc/tex/tfg.tex index 9248748..a14708d 100644 --- a/doc/tex/tfg.tex +++ b/doc/tex/tfg.tex @@ -23,7 +23,7 @@ %\renewcommand{\contentsname}{Contenidos} %\renewcommand{\figurename}{Figura} -\newcommand{\program}{Go-AI} +\newcommand{\program}{Imago} \newcommand{\inputtex}[1]{\input{tex/#1}} \newcommand{\fref}[1]{Fig.~\ref{#1}} @@ -33,7 +33,7 @@ \frenchspacing -\title{\program} +\title{\program: An AI capable of playing the game of Go} \author{Íñigo Gutiérrez Fernández} diff --git a/go.py b/go.py index 548df38..96f7052 100755 --- a/go.py +++ b/go.py @@ -7,8 +7,8 @@ from imago.gameLogic.gameState import GameState if __name__ == "__main__": - #GAMESTATE = GameState(5) - GAMESTATE = GameState(19) + GAMESTATE = GameState(5) + #GAMESTATE = GameState(19) while 1: diff --git a/imago/engine/core.py b/imago/engine/core.py index 0960319..48af241 100644 --- a/imago/engine/core.py +++ b/imago/engine/core.py @@ -5,7 +5,7 @@ from random import randrange from imago.data.enums import Player from imago.gameLogic.gameState import GameState -DEF_SIZE = 19 +DEF_SIZE = 7 DEF_KOMI = 5.5 class GameEngine: @@ -33,7 +33,7 @@ class GameEngine: self.komi = komi def setFixedHandicap(self, stones): - """Sets handicap stones in fixed vertexes.""" + """Sets handicap stones in fixed vertices.""" if stones < 1 or stones > 9: raise Exception("Wrong number of handicap stones") # TODO: Set handicap stones @@ -41,22 +41,35 @@ class GameEngine: def play(self, color, vertex): """Plays in the vertex passed as argument""" + if vertex == "pass": + self.gameState.passForPlayer(color) + return row = vertex[0] col = vertex[1] self.gameState.playMoveForPlayer(row, col, color) def genmove(self, color): """The key of this TFG.""" + + # Get valid vertices to play at validCells = [] board = self.gameState.getBoard().board size = self.gameState.size for row in range(size): for col in range(size): if board[row][col] == Player.EMPTY: - validCells.append([row, col]) + # Don't play on eyes! + if ( self.gameState.getBoard().getGroupCellsCount(row, col) != 1 + and self.gameState.getBoard().isCellEye(row, col) == Player.EMPTY ): + validCells.append([row, col]) + # Pass if no valid vertices + # Select a random vertex randIndex = randrange(0, len(validCells)) move = validCells[randIndex] self.gameState.playMoveForPlayer(move[0], move[1], color) + # NOTA: Esto usa gameState para hacer play, y en monteCarlo.py se usa GameMove + # para hacer add. Incoherente. Idealmente monteCarlo.py usaría un GameState en vez + # de acceder a los GameMove y gestionar un árbol... return move def undo(self): diff --git a/imago/engine/monteCarlo.py b/imago/engine/monteCarlo.py index 13f5c47..6e5d54d 100644 --- a/imago/engine/monteCarlo.py +++ b/imago/engine/monteCarlo.py @@ -8,37 +8,11 @@ class MCTS: def selection(self): """Select the most promising node with unexplored children.""" - bestUCB = 0 - bestNode = None - bestUCB, bestNode = self._selectionRec(self.root, bestUCB, bestNode) + bestNode = self.root.selectionRec(self.root) return bestNode - def __selectionRec(self, node, bestUCB, bestNode): - - # Check if node has unexplored children and better UCB than previously explored - if len(node.unexploredVertices) > 0: - ucb = node.ucb() - if ucb > bestUCB: - bestUCB = ucb - bestNode = node - - # Recursively search children for better UCB - for child in node.children: - bestUCB, bestNode = self._selectionRec(child, bestUCB, bestNode) - - return bestUCB, bestNode - - def expansion(self, node): - # Get a random unexplored vertex and remove it from the set - newVertex = node.unexploredVertices.pop() - newNode = MCTSNode(newVertex[0], newVertex[1], node) - parent.children.add(self) - return newNode - - def simulation(self, node): - def backup(self, node): - + """Update nodes backbards up to root.""" class MCTSNode: """Monte Carlo tree node.""" @@ -53,7 +27,66 @@ class MCTSNode: def ucb(self): """Returns Upper Confidence Bound of node""" - # meanVictories + 1/visits + # UCB = meanVictories + 1/visits mean = self.score / self.visits adjust = 1/self.visits return mean + adjust + + def selectionRec(self, bestNode): + """Searches this node and its children for the node with the best UCB value.""" + + # Check if node has unexplored children and better UCB than previously explored + if len(self.unexploredVertices) > 0: + if self.ucb() > bestNode.ucb(): + bestNode = self + + # Recursively search children for better UCB + for child in self.children: + bestNode = child.selectionRec(bestNode) + + return bestNode + + def expansion(self): + """Pick an unexplored vertex from this node and add it as a new MCTSNode.""" + newVertex = self.unexploredVertices.pop() # Random? + newMove = self.move.addMove(newVertex[0], newVertex[1]) + newNode = MCTSNode(newMove, self) + self.children.add(newNode) + return newNode + + def simulation(self): + """Play random matches to accumulate reward information on the node.""" + matches = 10 + for _ in range(matches): + result = self._randomMatch() + self.visits += 1 + scoreDiff = result[0]-result[1] + self.score += scoreDiff / abs(scoreDiff) + self._printBoardInfo() + + def _randomMatch(self): + """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! + currentMove = self.move + scoreHeuristic = 15 + score = currentMove.board.score() + while currentMove.getGameLength() < 5 or abs(score[0] - score[1]) < scoreHeuristic: + validMoves = currentMove.getPlayableVertices() + selectedMove = validMoves.pop() + currentMove = currentMove.addMove(selectedMove[0], selectedMove[1]) + print("Current move: %d, %d" % (currentMove.getRow(), currentMove.getCol())) + print("Current move game length: ", currentMove.getGameLength()) + score = currentMove.board.score() + print("Score of the board: %d, %d (%d)" + % (score[0], + score[1], + score[0]-score[1]) + ) + currentMove.printBoard() + return score + + def _printBoardInfo(self): + """Prints the visits and score for debugging purposes.""" + print("Visits: %d" % self.visits) + print("Score: %d" % self.score) diff --git a/imago/engine/parseHelpers.py b/imago/engine/parseHelpers.py index 32f0753..59e0496 100644 --- a/imago/engine/parseHelpers.py +++ b/imago/engine/parseHelpers.py @@ -21,7 +21,7 @@ class ParseCodes(Enum): QUIT = enumAuto() def parseMove(args, boardsize): - """Converts the textual input of a move to a move instance.""" + """Converts the textual representation of a move to a move instance.""" if len(args) != 2: print("[ERROR] - Wrong n of args for move") return ParseCodes.ERROR @@ -40,7 +40,8 @@ def parseColor(text): return ParseCodes.ERROR def parseVertex(text, boardSize): - """Returns row and column of a vertex given its input string. + """Returns row and column of a vertex given its input string. A vertex can also be the + string "pass". GTP uses A1 style notation: columns are letters left to right, rows are number bottom to top. @@ -48,6 +49,8 @@ def parseVertex(text, boardSize): text = text.upper() if not re.match("^[A-HJ-Z][1-9][0-9]*$", text): + if text == "PASS": + return "pass" return ParseCodes.ERROR vertexCol = ord(text[0]) @@ -70,6 +73,8 @@ def vertexToString(vertex, boardSize): GTP uses A1 style notation: columns are letters left to right, rows are number bottom to top. """ + if vertex == "pass": + return "pass" if len(vertex) != 2: return ParseCodes.ERROR if vertex[0] >= boardSize or vertex[1] >= boardSize or vertex[0] < 0 or vertex[1] < 0: diff --git a/imago/gameLogic/gameBoard.py b/imago/gameLogic/gameBoard.py index c6a6007..eaa1545 100644 --- a/imago/gameLogic/gameBoard.py +++ b/imago/gameLogic/gameBoard.py @@ -20,7 +20,10 @@ class GameBoard: self.board = _getNewBoard(height, width) self.capturesBlack = 0 self.capturesWhite = 0 - self.lastStone = None + + def getBoard(self): + """Gets the matrix representing the board.""" + return self.board def getBoardHeight(self): """Returns the number of rows in the board.""" @@ -31,27 +34,16 @@ class GameBoard: be the same for all the rows.""" return len(self.board[0]) - def getLastPlayer(self): - """Returns the player who placed the last stone.""" - if self.lastStone is None: - return Player.EMPTY - return self.board[self.lastStone[0]][self.lastStone[1]] - def getDeepCopy(self): """Returns a copy GameBoard.""" newBoard = GameBoard(self.getBoardHeight(), self.getBoardWidth()) newBoard.capturesBlack = self.capturesBlack newBoard.capturesWhite = self.capturesWhite - newBoard.lastStone = self.lastStone newBoard.board = deepcopy(self.board) return newBoard - def getGroupLibertiesCount(self, row, col): - """Returns the number of liberties of a group.""" - return len(self.getGroupLiberties(row, col)) - def getGroupLiberties(self, row, col): - """Returns the empty vertexes adjacent to the group occupying a vertex (its + """Returns the empty vertices adjacent to the group occupying a vertex (its liberties) as a set. An empty set is returned if the vertex is empty. """ groupColor = self.board[row][col] @@ -62,6 +54,10 @@ class GameBoard: self.__exploreLiberties(row, col, groupColor, emptyCells, exploredCells) return emptyCells + def getGroupLibertiesCount(self, row, col): + """Returns the number of liberties of a group.""" + return len(self.getGroupLiberties(row, col)) + def __exploreLiberties(self, row, col, groupColor, emptyCells, exploredCells): """Adds surrounding empty cells to array if they have not been added yet and explores adjacent occupied cells of the same group. @@ -78,51 +74,36 @@ class GameBoard: emptyCells.add((row, col)) return - # Up - if row > 0: - self.__exploreLiberties(row-1, col, groupColor, emptyCells, exploredCells) - - # Right - if col < self.getBoardWidth()-1: - self.__exploreLiberties(row, col+1, groupColor, emptyCells, exploredCells) - - # Down - if row < self.getBoardHeight()-1: - self.__exploreLiberties(row+1, col, groupColor, emptyCells, exploredCells) - - # Left - if col > 0: - self.__exploreLiberties(row, col-1, groupColor, emptyCells, exploredCells) + for side in ((-1,0), (1,0), (0,-1), (0,1)): + rowToExplore = row + side[0] + colToExplore = col + side[1] + if self.isMoveInBoardBounds(rowToExplore, colToExplore): + self.__exploreLiberties(rowToExplore, colToExplore, groupColor, + emptyCells, exploredCells) def getGroupCells(self, row, col): - """Returns a set containing the cells occupied by the group in the given cell.""" + """ + Returns a set containing the cells occupied by the group in the given cell. + This is also valid if the cell is empty.""" groupColor = self.board[row][col] - if groupColor == Player.EMPTY: - return 0 cells = set() self.__exploreGroup(row, col, groupColor, cells) return cells + def getGroupCellsCount(self, row, col): + """Returns the number of cells of a group.""" + return len(self.getGroupCells(row, col)) + def __exploreGroup(self, row, col, groupColor, cells): if self.board[row][col] != groupColor or (row, col) in cells: return cells.add((row, col)) - # Up - if row > 0: - self.__exploreGroup(row-1, col, groupColor, cells) - - # Right - if col < self.getBoardWidth()-1: - self.__exploreGroup(row, col+1, groupColor, cells) - - # Down - if row < self.getBoardHeight()-1: - self.__exploreGroup(row+1, col, groupColor, cells) - - # Left - if col > 0: - self.__exploreGroup(row, col-1, groupColor, cells) + for side in ((-1,0), (1,0), (0,-1), (0,1)): + rowToExplore = row + side[0] + colToExplore = col + side[1] + if self.isMoveInBoardBounds(rowToExplore, colToExplore): + self.__exploreGroup(rowToExplore, colToExplore, groupColor, cells) def moveAndCapture(self, row, col, player): """Checks surrounding captures of a move, removes them and returns a set @@ -130,33 +111,17 @@ class GameBoard: """ self.board[row][col] = player - self.lastStone = [row, col] captured = set() - if row > 0: - if (self.board[row-1][col] != player - and self.board[row-1][col] != Player.EMPTY - and len(self.getGroupLiberties(row-1, col)) == 0): - captured.update(self.__captureGroup(row-1, col)) - - if row < self.getBoardHeight()-1: - if (self.board[row+1][col] != player - and self.board[row+1][col] != Player.EMPTY - and len(self.getGroupLiberties(row+1, col)) == 0): - captured.update(self.__captureGroup(row+1, col)) - - if col > 0: - if (self.board[row][col-1] != player - and self.board[row][col-1] != Player.EMPTY - and len(self.getGroupLiberties(row, col-1)) == 0): - captured.update(self.__captureGroup(row, col-1)) - - if col < self.getBoardWidth()-1: - if (self.board[row][col+1] != player - and self.board[row][col+1] != Player.EMPTY - and len(self.getGroupLiberties(row, col+1)) == 0): - captured.update(self.__captureGroup(row, col+1)) + for side in ((-1,0), (1,0), (0,-1), (0,1)): + rowToExplore = row + side[0] + colToExplore = col + side[1] + if self.isMoveInBoardBounds(rowToExplore, colToExplore): + if (self.board[rowToExplore][colToExplore] != player + and self.board[rowToExplore][colToExplore] != Player.EMPTY + and self.getGroupLibertiesCount(rowToExplore, colToExplore) == 0): + captured.update(self.__captureGroup(rowToExplore, colToExplore)) return captured @@ -177,6 +142,29 @@ class GameBoard: """Returns True if cell is empty, false otherwise.""" return self.board[row][col] == Player.EMPTY + def isCellEye(self, row, col): + """Returns the surrounding color if the cell is part of an eye and Player.EMTPY + otherwise. + """ + # if isCellEmpty && all adjacent to group are same color + if not self.isCellEmpty(row, col): + return Player.EMPTY + groupCells = self.getGroupCells(row, col) + surroundingColor = Player.EMPTY + # Check surrounding cells of each cell in the group + for cell in groupCells: + for side in ((-1,0), (1,0), (0,-1), (0,1)): + rowChecked = cell[0]+side[0] + colChecked = cell[1]+side[1] + if self.isMoveInBoardBounds(rowChecked, colChecked): + otherColor = self.board[rowChecked][colChecked] + if otherColor != Player.EMPTY: + if surroundingColor == Player.EMPTY: + surroundingColor = otherColor + elif surroundingColor != otherColor: + return Player.EMPTY + return surroundingColor + def isMoveSuicidal(self, row, col, player): """Returns True if move is suicidal.""" @@ -238,6 +226,26 @@ class GameBoard: return False, "Illegal by ko rule." return True, "" + def score(self): + """Gets the current score given by the already surrounded territory for Japanese + rules. The format of the returned score is (black, white). + """ + scores = [] + for player in Player: + while len(scores) <= player.value: + scores.append(0) + checkedVertices = set() + for row in range(0, self.getBoardHeight()): + for col in range(0, self.getBoardWidth()): + if not (row, col) in checkedVertices: + group = self.getGroupCells(row, col) + for cell in group: + checkedVertices.add(cell) + surroundingColor = self.isCellEye(row, col) + if surroundingColor != Player.EMPTY: + scores[surroundingColor.value] += len(group) + return (scores[Player.BLACK.value], scores[Player.WHITE.value]) + def equals(self, otherBoard): """Returns true if this board is equal to another board. Only takes into account dimensions and placed stones. diff --git a/imago/gameLogic/gameMove.py b/imago/gameLogic/gameMove.py index 90ec4bf..224e120 100644 --- a/imago/gameLogic/gameMove.py +++ b/imago/gameLogic/gameMove.py @@ -7,22 +7,52 @@ class GameMove: Tree: the board can be empty, or the move can consist of more than one added or removed stones.""" - def __init__(self, player, board): + def __init__(self, board, coords=None, isPass=False): self.board = board self.nextMoves = [] self.previousMove = None + self.isPass = isPass + self.coords = coords def getRow(self): """Returns the row of the vertex the move was played on.""" - return self.board.lastStone[0] + if self.coords is None: + return None + return self.coords[0] def getCol(self): """Returns the column of the vertex the move was played on.""" - return self.board.lastStone[1] + if self.coords is None: + return None + return self.coords[1] - def getLastPlayer(self): - """Returns the player who placed the last stone.""" - return self.board.getLastPlayer() + def getPlayer(self): + """Returns the player who placed the last stone or passed.""" + if self.isPass: + if self.previousMove is None: + return Player.BLACK + return Player.otherPlayer(self.previousMove.getPlayer()) + + if self.coords is None: # Not pass and no coordinates: root move of the tree + return Player.EMPTY + + return self.board.getBoard()[self.getRow()][self.getCol()] + + def getNextPlayer(self): + """Returns the player who should place the next stone.""" + selfPlayer = self.getPlayer() + if selfPlayer == Player.EMPTY: + return Player.BLACK + return Player.otherPlayer(selfPlayer) + + def getGameLength(self): + """Returns the number of (actual player-made) moves since the game started.""" + acc = 0 + prevMove = self.previousMove + while prevMove is not None: + acc += 1 + prevMove = prevMove.previousMove + return acc def getThisAndPrevBoards(self): """Returns an array with all the boards of this and previous moves.""" @@ -36,21 +66,23 @@ class GameMove: def getPlayableVertices(self): """Returns a set with the playable vertices.""" playables = set() - player = Player.otherPlayer(self.getLastPlayer()) + player = self.getNextPlayer() prevBoards = self.getThisAndPrevBoards() for row in range(self.board.getBoardHeight()): for col in range(self.board.getBoardWidth()): - if self.board.isPlayable(row, col, player, prevBoards): + isPlayable, _ = self.board.isPlayable(row, col, player, prevBoards) + if isPlayable: playables.add((row, col)) + return playables def addMove(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. """ - if self.getLastPlayer() == Player.EMPTY: + if self.getPlayer() == Player.EMPTY: player = Player.BLACK else: - player = Player.otherPlayer(self.getLastPlayer()) + player = Player.otherPlayer(self.getPlayer()) return self.addMoveForPlayer(row, col, player) def addMoveForPlayer(self, row, col, player): @@ -59,11 +91,19 @@ class GameMove: """ newBoard = self.board.getDeepCopy() newBoard.moveAndCapture(row, col, player) - return self.addMoveForPlayerAndBoard(player, newBoard) + newMove = GameMove( newBoard, (row, col) ) + newMove.previousMove = self + self.nextMoves.append(newMove) + return newMove - def addMoveForPlayerAndBoard(self, player, board): - """Adds a move to the next moves list containing the provided board.""" - newMove = GameMove(player, board) + def addPass(self): + """Adds a pass move to the next moves list.""" + newBoard = self.board.getDeepCopy() + newMove = GameMove(newBoard, True) newMove.previousMove = self self.nextMoves.append(newMove) return newMove + + def printBoard(self): + """Prints the board as of this move.""" + self.board.printBoard() diff --git a/imago/gameLogic/gameState.py b/imago/gameLogic/gameState.py index e4dd629..55bac5f 100644 --- a/imago/gameLogic/gameState.py +++ b/imago/gameLogic/gameState.py @@ -12,16 +12,16 @@ class GameState: self.size = size self.gameTree = GameTree() newBoard = GameBoard(self.size, self.size) - self.lastMove = GameMove(Player.EMPTY, newBoard) + self.lastMove = GameMove(newBoard) self.gameTree.firstMoves.append(self.lastMove) def getCurrentPlayer(self): """Gets the player who should make the next move.""" if self.lastMove is None: return Player.BLACK - if self.lastMove.getLastPlayer() is Player.EMPTY: + if self.lastMove.getPlayer() is Player.EMPTY: return Player.BLACK - return Player.otherPlayer(self.lastMove.getLastPlayer()) + return Player.otherPlayer(self.lastMove.getPlayer()) def getPlayerCode(self): """Gets a string representation of the current player.""" @@ -48,6 +48,10 @@ class GameState: print("Invalid Move! %s" % message) return False + def playPass(self): + """Passes the turn for the given player.""" + self.lastMove.addPass() + def undo(self): """Sets the move before the last move as the new last move.""" self.lastMove = self.lastMove.previousMove diff --git a/tests/test_gameBoard.py b/tests/test_gameBoard.py index 2fa1037..3a4d5a0 100644 --- a/tests/test_gameBoard.py +++ b/tests/test_gameBoard.py @@ -5,8 +5,6 @@ import unittest from imago.data.enums import Player from imago.gameLogic.gameBoard import GameBoard -#from imago.data.enums import Player - TEST_BOARD_SIZE = 19 class TestGameBoard(unittest.TestCase): @@ -14,10 +12,10 @@ class TestGameBoard(unittest.TestCase): def testGetGroupLiberties(self): """Test calculation of group liberties.""" - board = GameBoard(TEST_BOARD_SIZE) + board = GameBoard(TEST_BOARD_SIZE, TEST_BOARD_SIZE) #Empty cell liberties - self.assertEqual(board.getGroupLiberties(0,0), {}) + self.assertEqual(board.getGroupLiberties(0,0), set()) self.assertEqual(board.getGroupLibertiesCount(0,0), 0) # Lone stone liberties @@ -26,5 +24,83 @@ class TestGameBoard(unittest.TestCase): {(2,3), (3,2), (4,3), (3,4)}) self.assertEqual(board.getGroupLibertiesCount(3,3), 4) + def testIsCellEye(self): + """Tests the isCellEye method.""" + board = GameBoard(TEST_BOARD_SIZE, TEST_BOARD_SIZE) + + # Empty board is eye + self.assertEqual(Player.EMPTY, board.isCellEye(0, 0)) + self.assertEqual(Player.EMPTY, board.isCellEye(3, 3)) + self.assertEqual(Player.EMPTY, board.isCellEye(TEST_BOARD_SIZE-1, TEST_BOARD_SIZE-1)) + + # Board with 1 stone is eye + board.board[5][6] = Player.WHITE + self.assertEqual(Player.WHITE, board.isCellEye(3, 3)) + + # Board with 2 stones of different color is not eye + board.board[9][9] = Player.BLACK + self.assertEqual(Player.EMPTY, board.isCellEye(3, 3)) + + # Surrounded cell is eye + board.board[6][5] = Player.WHITE + board.board[6][7] = Player.WHITE + board.board[7][6] = Player.WHITE + + self.assertEqual(Player.WHITE, board.isCellEye(6, 6)) + + # Surrounded cell with 2 different colors is not eye + board.board[6][5] = Player.BLACK + self.assertEqual(Player.EMPTY, board.isCellEye(6, 6)) + + def testScore(self): + """Tests the score method.""" + board = GameBoard(TEST_BOARD_SIZE, TEST_BOARD_SIZE) + + # Empty board has no score. + self.assertEqual((0, 0), board.score()) + + # Board with 1 black stone has totalVertices-1 points for black. + board.board[3][3] = Player.BLACK + self.assertEqual((TEST_BOARD_SIZE*TEST_BOARD_SIZE-1, 0), board.score()) + + # Board with 2 black stones has totalVertices-2 points for black. + board.board[5][5] = Player.BLACK + self.assertEqual((TEST_BOARD_SIZE*TEST_BOARD_SIZE-2, 0), board.score()) + + # Board with lone stones of different colors has no score. + board.board[7][7] = Player.WHITE + self.assertEqual((0, 0), board.score()) + + # Black group with surrounded territory. + board.board[2][3] = Player.BLACK + board.board[1][3] = Player.BLACK + board.board[0][3] = Player.BLACK + board.board[3][2] = Player.BLACK + board.board[3][1] = Player.BLACK + board.board[3][0] = Player.BLACK + self.assertEqual((9, 0), board.score()) + + # White group besides black group. + board.board[6][7] = Player.WHITE + board.board[5][7] = Player.WHITE + board.board[5][6] = Player.WHITE + board.board[5][5] = Player.WHITE + board.board[5][4] = Player.WHITE + board.board[5][3] = Player.WHITE + board.board[5][2] = Player.WHITE + board.board[5][1] = Player.WHITE + board.board[5][0] = Player.WHITE + board.board[8][7] = Player.WHITE + board.board[9][7] = Player.WHITE + board.board[9][6] = Player.WHITE + board.board[9][5] = Player.WHITE + board.board[9][4] = Player.WHITE + board.board[9][3] = Player.WHITE + board.board[9][2] = Player.WHITE + board.board[9][1] = Player.WHITE + board.board[9][0] = Player.WHITE + self.assertEqual((9, 21), board.score()) + + if __name__ == '__main__': unittest.main() diff --git a/tests/test_gameMove.py b/tests/test_gameMove.py new file mode 100644 index 0000000..6569c5b --- /dev/null +++ b/tests/test_gameMove.py @@ -0,0 +1,78 @@ +"""Tests for gameMove module.""" + +import unittest + +from imago.data.enums import Player +from imago.gameLogic.gameBoard import GameBoard +from imago.gameLogic.gameMove import GameMove + +TEST_BOARD_SIZE = 19 + +class TestGameMove(unittest.TestCase): + """Test gameMove module.""" + + def testAddMove(self): + """Test adding new moves to existing moves.""" + board = GameBoard(TEST_BOARD_SIZE, TEST_BOARD_SIZE) + firstMove = GameMove(board) + + self.assertIsNone(firstMove.coords) + + secondMove = firstMove.addMove(1, 2) + + self.assertIsNone(firstMove.coords) + self.assertEqual(secondMove.coords[0], 1) + self.assertEqual(secondMove.coords[1], 2) + + thirdMove = secondMove.addMove(5, 7) + + self.assertIsNone(firstMove.coords) + self.assertIsNone(thirdMove.previousMove.previousMove.coords) + + self.assertEqual(secondMove.coords[0], 1) + self.assertEqual(secondMove.coords[1], 2) + self.assertEqual(thirdMove.previousMove.coords[0], 1) + self.assertEqual(thirdMove.previousMove.coords[1], 2) + + self.assertEqual(thirdMove.coords[0], 5) + self.assertEqual(thirdMove.coords[1], 7) + self.assertEqual(firstMove + .nextMoves[0] + .nextMoves[0] + .coords[0], 5) + self.assertEqual(firstMove + .nextMoves[0] + .nextMoves[0] + .coords[1], 7) + + self.assertEqual(firstMove.board.getBoard()[1][2], Player.EMPTY) + self.assertEqual(secondMove.board.getBoard()[1][2], Player.BLACK) + self.assertEqual(thirdMove.board.getBoard()[1][2], Player.BLACK) + + self.assertEqual(firstMove.board.getBoard()[5][7], Player.EMPTY) + self.assertEqual(secondMove.board.getBoard()[5][7], Player.EMPTY) + self.assertEqual(thirdMove.board.getBoard()[5][7], Player.WHITE) + + def testGetPlayableVertices(self): + """Test getting the set of valid moves.""" + boardSize = 3 + board = GameBoard(boardSize, boardSize) + + firstMove = GameMove(board) + self.assertSetEqual( + firstMove.getPlayableVertices(), + set(((0,0), (0,1), (0,2), + (1,0), (1,1), (1,2), + (2,0), (2,1), (2,2))) + ) + + secondMove = firstMove.addMove(1, 2) + self.assertSetEqual( + secondMove.getPlayableVertices(), + set(((0,0), (0,1), (0,2), + (1,0), (1,1), + (2,0), (2,1), (2,2))) + ) + +if __name__ == '__main__': + unittest.main() diff --git a/tests/test_monteCarlo.py b/tests/test_monteCarlo.py new file mode 100644 index 0000000..8fe6f47 --- /dev/null +++ b/tests/test_monteCarlo.py @@ -0,0 +1,22 @@ +"""Tests for the MonteCarlo algorithm.""" + +import unittest + +from imago.gameLogic.gameBoard import GameBoard +from imago.gameLogic.gameMove import GameMove +from imago.engine.monteCarlo import MCTSNode + +TEST_BOARD_SIZE = 19 + +class TestMonteCarlo(unittest.TestCase): + """Test MonteCarlo algorithm.""" + + def testSimulation(self): + """Test calculation of group liberties.""" + board = GameBoard(TEST_BOARD_SIZE, TEST_BOARD_SIZE) + move = GameMove(board) + node = MCTSNode(move, None) + node.simulation() + +if __name__ == '__main__': + unittest.main() -- cgit v1.2.1 From 25e4e5fb3a77c8da65a2e7b42b7d77ba8f1b21a1 Mon Sep 17 00:00:00 2001 From: InigoGutierrez Date: Thu, 17 Jun 2021 20:13:41 +0200 Subject: Engine now uses MCTS algorithm. --- imago/engine/core.py | 31 ++++------- imago/engine/monteCarlo.py | 119 +++++++++++++++++++++++++++++++++---------- imago/gameLogic/gameBoard.py | 10 ++++ imago/gameLogic/gameMove.py | 44 +++++++++++++--- imago/gameLogic/gameState.py | 6 ++- tests/test_monteCarlo.py | 26 ++++++---- 6 files changed, 170 insertions(+), 66 deletions(-) diff --git a/imago/engine/core.py b/imago/engine/core.py index 48af241..3e52f8c 100644 --- a/imago/engine/core.py +++ b/imago/engine/core.py @@ -3,6 +3,7 @@ from random import randrange from imago.data.enums import Player +from imago.engine.monteCarlo import MCTS from imago.gameLogic.gameState import GameState DEF_SIZE = 7 @@ -14,6 +15,7 @@ class GameEngine: def __init__(self): self.komi = DEF_KOMI self.gameState = GameState(DEF_SIZE) + self.mcts = MCTS(self.gameState.lastMove) def setBoardsize(self, newSize): """Changes the size of the board. @@ -47,30 +49,15 @@ class GameEngine: row = vertex[0] col = vertex[1] self.gameState.playMoveForPlayer(row, col, color) + self.mcts.forceNextMove(vertex) def genmove(self, color): - """The key of this TFG.""" - - # Get valid vertices to play at - validCells = [] - board = self.gameState.getBoard().board - size = self.gameState.size - for row in range(size): - for col in range(size): - if board[row][col] == Player.EMPTY: - # Don't play on eyes! - if ( self.gameState.getBoard().getGroupCellsCount(row, col) != 1 - and self.gameState.getBoard().isCellEye(row, col) == Player.EMPTY ): - validCells.append([row, col]) - # Pass if no valid vertices - # Select a random vertex - randIndex = randrange(0, len(validCells)) - move = validCells[randIndex] - self.gameState.playMoveForPlayer(move[0], move[1], color) - # NOTA: Esto usa gameState para hacer play, y en monteCarlo.py se usa GameMove - # para hacer add. Incoherente. Idealmente monteCarlo.py usaría un GameState en vez - # de acceder a los GameMove y gestionar un árbol... - return move + """Returns a list representing coordinates of the board in the form (row, col).""" + coords = self.mcts.pickMove().coords + #TODO: The move should NOT be played in its generation. This method is just for + #suggesting a move. + self.gameState.playMoveForPlayer(coords[0], coords[1], color) + return coords def undo(self): """The board configuration and number of captured stones are reset to the state diff --git a/imago/engine/monteCarlo.py b/imago/engine/monteCarlo.py index 6e5d54d..2a531b6 100644 --- a/imago/engine/monteCarlo.py +++ b/imago/engine/monteCarlo.py @@ -1,18 +1,49 @@ """Monte Carlo Tree Search module.""" +import sys +import random + +from imago.data.enums import Player + class MCTS: """Monte Carlo tree.""" - def __init__(self, root): - self.root = root + def __init__(self, move): + self.root = MCTSNode(move, None) - def selection(self): - """Select the most promising node with unexplored children.""" - bestNode = self.root.selectionRec(self.root) - return bestNode + def forceNextMove(self, coords): + """Selects given move as next move.""" + for node in self.root.children: + if (node.move.getRow() == coords[0] + and node.move.getCol() == coords[1]): + self.root = node + return + self.root = self.root.expansionForCoords(coords) - def backup(self, node): - """Update nodes backbards up to root.""" + def pickMove(self): + """ + Performs an exploratory cycle, updates the root to the best node and returns its + corresponding move.""" + #NOTE: with only one selection-expansion the match is + # completely random + for _ in range(5): + self.root.selection().expansion().simulation(10, 20) + self.root = self.selectBestNextNode() + return self.root.move + + def selectBestNextNode(self): + """Returns best ucb node available for the current player.""" + + # Assumes at least one expansion has occured + bestUCB = -sys.maxsize - 1 + bestNode = None + for node in self.root.children: + ucb = node.ucbForPlayer() + if ucb > bestUCB: + bestUCB = ucb + bestNode = node + + return bestNode class MCTSNode: """Monte Carlo tree node.""" @@ -28,56 +59,92 @@ class MCTSNode: def ucb(self): """Returns Upper Confidence Bound of node""" # UCB = meanVictories + 1/visits + if self.visits == 0: + return 0 mean = self.score / self.visits adjust = 1/self.visits return mean + adjust + def ucbForPlayer(self): + """ + Returns Upper Confidence Bound of node changing the symbol if the move is for the + wite player.""" + + # Account for white player score being negative + if self.move.getPlayer() == Player.WHITE: + return self.ucb() * -1 + return self.ucb() + + def selection(self): + """Select the most promising node with unexplored children.""" + bestNode = self.selectionRec(self) + return bestNode + def selectionRec(self, bestNode): """Searches this node and its children for the node with the best UCB value.""" # Check if node has unexplored children and better UCB than previously explored if len(self.unexploredVertices) > 0: - if self.ucb() > bestNode.ucb(): + if self.ucbForPlayer() > bestNode.ucbForPlayer(): bestNode = self # Recursively search children for better UCB for child in self.children: - bestNode = child.selectionRec(bestNode) + bestChildNode = child.selectionRec(bestNode) + if bestChildNode.ucbForPlayer() > bestNode.ucbForPlayer(): + bestNode = bestChildNode return bestNode def expansion(self): """Pick an unexplored vertex from this node and add it as a new MCTSNode.""" - newVertex = self.unexploredVertices.pop() # Random? - newMove = self.move.addMove(newVertex[0], newVertex[1]) + newVertex = random.choice(list(self.unexploredVertices)) + return self.expansionForCoords(newVertex) + + def expansionForCoords(self, coords): + """ + 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]) newNode = MCTSNode(newMove, self) self.children.add(newNode) + self.unexploredVertices.remove((coords[0], coords[1])) return newNode - def simulation(self): + def simulation(self, nMatches, scoreDiffHeur): """Play random matches to accumulate reward information on the node.""" - matches = 10 - for _ in range(matches): - result = self._randomMatch() + scoreAcc = 0 + for _ in range(nMatches): + result = self._randomMatch(scoreDiffHeur) self.visits += 1 scoreDiff = result[0]-result[1] - self.score += scoreDiff / abs(scoreDiff) - self._printBoardInfo() + if scoreDiff != 0: + scoreAcc += scoreDiff / abs(scoreDiff) + # Backup + node = self + while node is not None: + node.score += scoreAcc + node.visits += nMatches + node = node.parent - def _randomMatch(self): + def _randomMatch(self, scoreDiffHeur): """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! currentMove = self.move - scoreHeuristic = 15 score = currentMove.board.score() - while currentMove.getGameLength() < 5 or abs(score[0] - score[1]) < scoreHeuristic: - validMoves = currentMove.getPlayableVertices() - selectedMove = validMoves.pop() - currentMove = currentMove.addMove(selectedMove[0], selectedMove[1]) - print("Current move: %d, %d" % (currentMove.getRow(), currentMove.getCol())) - print("Current move game length: ", currentMove.getGameLength()) + while currentMove.getGameLength() < 5 or abs(score[0] - score[1]) < scoreDiffHeur: + if currentMove.isPass and currentMove.previousMove.isPass: + return score + sensibleMoves = currentMove.getSensibleVertices() + if len(sensibleMoves) == 0: + currentMove = currentMove.addPass() + else: + selectedMove = random.choice(list(sensibleMoves)) + currentMove = currentMove.addMoveByCoords(selectedMove) score = currentMove.board.score() + print("Current move: %s" % (currentMove.toString())) + print("Current move game length: ", currentMove.getGameLength()) print("Score of the board: %d, %d (%d)" % (score[0], score[1], diff --git a/imago/gameLogic/gameBoard.py b/imago/gameLogic/gameBoard.py index eaa1545..74098ed 100644 --- a/imago/gameLogic/gameBoard.py +++ b/imago/gameLogic/gameBoard.py @@ -226,6 +226,16 @@ class GameBoard: return False, "Illegal by ko rule." return True, "" + def isSensible(self, row, col, player, prevBoards): + """Determines if a move is playable and sensible.""" + playable, playableText = self.isPlayable(row, col, player, prevBoards) + if not playable: + return playable, playableText + if ( self.getGroupCellsCount(row, col) == 1 + and self.isCellEye(row, col) == player ): + return False, "Move fills own eye.""" + return True, "" + def score(self): """Gets the current score given by the already surrounded territory for Japanese rules. The format of the returned score is (black, white). diff --git a/imago/gameLogic/gameMove.py b/imago/gameLogic/gameMove.py index 224e120..6d2d464 100644 --- a/imago/gameLogic/gameMove.py +++ b/imago/gameLogic/gameMove.py @@ -7,12 +7,13 @@ class GameMove: Tree: the board can be empty, or the move can consist of more than one added or removed stones.""" - def __init__(self, board, coords=None, isPass=False): + def __init__(self, board, coords=None, isPass=False, playerWhoPassed=None): self.board = board self.nextMoves = [] self.previousMove = None self.isPass = isPass self.coords = coords + self.playerWhoPassed = playerWhoPassed def getRow(self): """Returns the row of the vertex the move was played on.""" @@ -31,7 +32,7 @@ class GameMove: if self.isPass: if self.previousMove is None: return Player.BLACK - return Player.otherPlayer(self.previousMove.getPlayer()) + return self.playerWhoPassed if self.coords is None: # Not pass and no coordinates: root move of the tree return Player.EMPTY @@ -65,15 +66,30 @@ class GameMove: def getPlayableVertices(self): """Returns a set with the playable vertices.""" - playables = set() + return self._getVerticesByFilter(self.board.isPlayable) + + def getSensibleVertices(self): + """Returns a set with the sensible vertices.""" + return self._getVerticesByFilter(self.board.isSensible) + + def _getVerticesByFilter(self, filterFunction): + """Returns a set with the vertices which fill a requirement.""" + vertices = set() player = self.getNextPlayer() prevBoards = self.getThisAndPrevBoards() for row in range(self.board.getBoardHeight()): for col in range(self.board.getBoardWidth()): - isPlayable, _ = self.board.isPlayable(row, col, player, prevBoards) - if isPlayable: - playables.add((row, col)) - return playables + valid, _ = filterFunction(row, col, player, prevBoards) + if valid: + vertices.add((row, col)) + return vertices + + def addMoveByCoords(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]) + def addMove(self, row, col): """Adds a move to the next moves list creating its board from this move's board @@ -98,12 +114,24 @@ class GameMove: def addPass(self): """Adds a pass move to the next moves list.""" + return self.addPassForPlayer(self.getNextPlayer()) + + def addPassForPlayer(self, player): + """Adds a pass move for the given player to the next moves list.""" newBoard = self.board.getDeepCopy() - newMove = GameMove(newBoard, True) + newMove = GameMove(newBoard, isPass=True, playerWhoPassed=player) newMove.previousMove = self self.nextMoves.append(newMove) return newMove + def toString(self): + """Returns the coordinates of the move as a string.""" + if self.isPass: + return "Pass" + if self.coords is None: + return "Root move" + return "(%d, %d)" % (self.getRow(), self.getCol()) + def printBoard(self): """Prints the board as of this move.""" self.board.printBoard() diff --git a/imago/gameLogic/gameState.py b/imago/gameLogic/gameState.py index 55bac5f..d596ba1 100644 --- a/imago/gameLogic/gameState.py +++ b/imago/gameLogic/gameState.py @@ -49,9 +49,13 @@ class GameState: return False def playPass(self): - """Passes the turn for the given player.""" + """Passes the turn for the player who should make a move.""" self.lastMove.addPass() + def passForPlayer(self, player): + """Passes the turn for the given player.""" + self.lastMove.addPassForPlayer(player) + def undo(self): """Sets the move before the last move as the new last move.""" self.lastMove = self.lastMove.previousMove diff --git a/tests/test_monteCarlo.py b/tests/test_monteCarlo.py index 8fe6f47..283c657 100644 --- a/tests/test_monteCarlo.py +++ b/tests/test_monteCarlo.py @@ -2,21 +2,29 @@ import unittest -from imago.gameLogic.gameBoard import GameBoard -from imago.gameLogic.gameMove import GameMove +from imago.gameLogic.gameState import GameState +from imago.engine.monteCarlo import MCTS from imago.engine.monteCarlo import MCTSNode -TEST_BOARD_SIZE = 19 +TEST_BOARD_SIZE = 9 class TestMonteCarlo(unittest.TestCase): """Test MonteCarlo algorithm.""" - def testSimulation(self): - """Test calculation of group liberties.""" - board = GameBoard(TEST_BOARD_SIZE, TEST_BOARD_SIZE) - move = GameMove(board) - node = MCTSNode(move, None) - node.simulation() + def testPickMove(self): + """Test picking a move.""" + state = GameState(TEST_BOARD_SIZE) + tree = MCTS(state) + print(tree.pickMove().toString()) + + #def testSimulation(self): + # """Test calculation of group liberties.""" + # board = GameBoard(TEST_BOARD_SIZE, TEST_BOARD_SIZE) + # move = GameMove(board) + # node = MCTSNode(move, None) + # nMatches = 100 + # scoreDiffHeur = 15 + # node.simulation(nMatches, scoreDiffHeur) if __name__ == '__main__': unittest.main() -- cgit v1.2.1 From 08bd7d1bd0f9a564cff7ee7f616d62f234a54057 Mon Sep 17 00:00:00 2001 From: InigoGutierrez Date: Fri, 18 Jun 2021 20:37:16 +0200 Subject: imagocli.py works with the Sabaki GUI. --- imago/engine/core.py | 7 +++---- imago/engine/imagoIO.py | 24 +++++++++++++++++------- imago/engine/monteCarlo.py | 5 +++++ imago/gameLogic/gameBoard.py | 4 ++++ imago/gameLogic/gameState.py | 12 ++++++++---- 5 files changed, 37 insertions(+), 15 deletions(-) diff --git a/imago/engine/core.py b/imago/engine/core.py index 3e52f8c..44da3b8 100644 --- a/imago/engine/core.py +++ b/imago/engine/core.py @@ -1,8 +1,5 @@ """Imago GTP engine""" -from random import randrange - -from imago.data.enums import Player from imago.engine.monteCarlo import MCTS from imago.gameLogic.gameState import GameState @@ -23,12 +20,14 @@ class GameEngine: It is wise to call clear_board after this command. """ self.gameState = GameState(newSize) + self.mcts = MCTS(self.gameState.lastMove) def clearBoard(self): """The board is cleared, the number of captured stones reset to zero and the move history reset to empty. """ self.gameState.clearBoard() + self.mcts.clearBoard() def setKomi(self, komi): """Sets a new value of komi.""" @@ -56,7 +55,7 @@ class GameEngine: coords = self.mcts.pickMove().coords #TODO: The move should NOT be played in its generation. This method is just for #suggesting a move. - self.gameState.playMoveForPlayer(coords[0], coords[1], color) + #self.gameState.playMoveForPlayer(coords[0], coords[1], color) return coords def undo(self): diff --git a/imago/engine/imagoIO.py b/imago/engine/imagoIO.py index f676b69..371c447 100644 --- a/imago/engine/imagoIO.py +++ b/imago/engine/imagoIO.py @@ -5,17 +5,21 @@ import sys from imago.engine import parseHelpers from imago.engine.core import GameEngine +def _response(text=""): + print("= %s" % text) + print() + def protocol_version(_): """Version of the GTP Protocol""" - print("2") + _response("2") def name(_): """Name of the engine""" - print("Imago") + _response("Imago") def version(_): """Version of the engine""" - print("0.0.0") + _response("0.0.0") def getCoordsText(row, col): """Returns a string representation of row and col. @@ -75,12 +79,14 @@ class ImagoIO: for c in self.commands_set: if c.__name__ == args[0]: out = "true" - print(out) + _response(out) def list_commands(self, _): """List of commands, one per row""" + output = "" for c in self.commands_set: - print("%s - %s" % (c.__name__, c.__doc__)) + output += ("%s - %s\n" % (c.__name__, c.__doc__)) + _response(output) def boardsize(self, args): """Changes the size of the board. @@ -92,12 +98,14 @@ class ImagoIO: sys.exit(1) size = int(args[0]) self.gameEngine.setBoardsize(size) + _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() def komi(self, args): """Sets a new value of komi.""" @@ -106,6 +114,7 @@ class ImagoIO: sys.exit(1) komi = float(args[0]) self.gameEngine.setKomi(komi) + _response() def fixed_handicap(self, args): """Handicap stones are placed on the board on standard vertices. @@ -119,7 +128,7 @@ class ImagoIO: out = getCoordsText(vertices[0][0], vertices[0][1]) for vertex in vertices[1:]: out += " " + getCoordsText(vertex[0], vertex[1]) - print(out) + _response(out) def place_free_handicap(self, args): """Handicap stones are placed on the board by the AI criteria.""" @@ -136,6 +145,7 @@ class ImagoIO: sys.exit(1) move = parseHelpers.parseMove(args, self.gameEngine.gameState.size) self.gameEngine.play(move.color, move.vertex) + _response() def genmove(self, args): """A stone of the requested color is played where the engine chooses.""" @@ -145,7 +155,7 @@ class ImagoIO: color = parseHelpers.parseColor(args[0]) output = parseHelpers.vertexToString(self.gameEngine.genmove(color), self.gameEngine.gameState.size) - print(output) + _response(output) self.gameEngine.gameState.getBoard().printBoard() def undo(self, _): diff --git a/imago/engine/monteCarlo.py b/imago/engine/monteCarlo.py index 2a531b6..4540cb8 100644 --- a/imago/engine/monteCarlo.py +++ b/imago/engine/monteCarlo.py @@ -45,6 +45,11 @@ class MCTS: return bestNode + def clearBoard(self): + """Empties move history.""" + while self.root.parent is not None: + self.root = self.root.parent + class MCTSNode: """Monte Carlo tree node.""" diff --git a/imago/gameLogic/gameBoard.py b/imago/gameLogic/gameBoard.py index 74098ed..b22dfd7 100644 --- a/imago/gameLogic/gameBoard.py +++ b/imago/gameLogic/gameBoard.py @@ -110,6 +110,10 @@ class GameBoard: containing the vertices where stones were captured. """ + if (row < 0 or row >= self.getBoardHeight() + or col < 0 or col >= self.getBoardWidth()): + print("[ERROR] Move and capture: out of bounds (%d, %d)" % (row, col)) + self.board[row][col] = player captured = set() diff --git a/imago/gameLogic/gameState.py b/imago/gameLogic/gameState.py index d596ba1..9aafd11 100644 --- a/imago/gameLogic/gameState.py +++ b/imago/gameLogic/gameState.py @@ -10,10 +10,7 @@ class GameState: def __init__(self, size): self.size = size - self.gameTree = GameTree() - newBoard = GameBoard(self.size, self.size) - self.lastMove = GameMove(newBoard) - self.gameTree.firstMoves.append(self.lastMove) + self.clearBoard() def getCurrentPlayer(self): """Gets the player who should make the next move.""" @@ -33,6 +30,13 @@ class GameState: return GameBoard(self.size, self.size) return self.lastMove.board + def clearBoard(self): + """Empties the board and moves tree.""" + self.gameTree = GameTree() + newBoard = GameBoard(self.size, self.size) + self.lastMove = GameMove(newBoard) + self.gameTree.firstMoves.append(self.lastMove) + def playMove(self, row, col): """Execute a move on the board for the current player and switches players.""" return self.playMoveForPlayer(row, col, self.getCurrentPlayer()) -- cgit v1.2.1 From 6968123fadde3b864882851e2cfc99b2e724dec7 Mon Sep 17 00:00:00 2001 From: InigoGutierrez Date: Wed, 23 Jun 2021 22:25:55 +0200 Subject: Docs: Written initial planification and system analysis sections. --- doc/Makefile | 4 +- doc/diagrams/analysisClasses.puml | 27 ++++ doc/diagrams/modules.puml | 2 +- doc/diagrams/planificationWorkPlanEngine.puml | 30 ++++ doc/diagrams/planificationWorkPlanGame.puml | 18 +++ doc/diagrams/skinparamsGantt.puml | 14 ++ doc/diagrams/useCases.puml | 18 +++ doc/tex/biber.bib | 22 +++ doc/tex/planification.tex | 162 +++++++++++++++++++++ doc/tex/previousWorks.tex | 24 ++-- doc/tex/systemAnalysis.tex | 200 ++++++++++++++++++++++++++ doc/tex/tfg.tex | 74 ++++++++-- 12 files changed, 569 insertions(+), 26 deletions(-) create mode 100644 doc/diagrams/analysisClasses.puml create mode 100644 doc/diagrams/planificationWorkPlanEngine.puml create mode 100644 doc/diagrams/planificationWorkPlanGame.puml create mode 100644 doc/diagrams/skinparamsGantt.puml create mode 100644 doc/diagrams/useCases.puml create mode 100644 doc/tex/biber.bib create mode 100644 doc/tex/planification.tex create mode 100644 doc/tex/systemAnalysis.tex diff --git a/doc/Makefile b/doc/Makefile index 954ae3c..a631921 100644 --- a/doc/Makefile +++ b/doc/Makefile @@ -3,8 +3,8 @@ docName = tfg outputFolder = out -texFiles = tex/tfg.tex tex/introduction.tex tex/previousWorks.tex tex/interface.tex tex/implementation.tex -diagramImgs = diagrams/gameRepresentation.png diagrams/modules.png diagrams/sgfModule.png diagrams/gtpEngine.png +texFiles = tex/tfg.tex tex/introduction.tex tex/planification.tex tex/interface.tex tex/implementation.tex tex/systemAnalysis.tex tex/biber.bib +diagramImgs = diagrams/gameRepresentation.png diagrams/gtpEngine.png diagrams/modules.png diagrams/planificationWorkPlanEngine.png diagrams/planificationWorkPlanGame.png diagrams/sgfModule.png diagrams/useCases.png diagrams/analysisClasses.png all: $(docName).pdf diff --git a/doc/diagrams/analysisClasses.puml b/doc/diagrams/analysisClasses.puml new file mode 100644 index 0000000..5f1ccde --- /dev/null +++ b/doc/diagrams/analysisClasses.puml @@ -0,0 +1,27 @@ +@startuml + +!include skinparams.puml + +package GameModule { + class GameIO + class GameTree + class GameMove +} + +GameIO -> GameTree +GameTree -> GameMove + +package EngineModule { + class EngineIO + class EngineLogic + interface DecisionAlgorithm + class DecisionAlgorithm1 + class DecisionAlgorithm2 +} + +EngineIO -> EngineLogic +EngineLogic -> DecisionAlgorithm +DecisionAlgorithm <|.. DecisionAlgorithm1 +DecisionAlgorithm <|.. DecisionAlgorithm2 + +@enduml diff --git a/doc/diagrams/modules.puml b/doc/diagrams/modules.puml index 064be1d..5b8d78c 100644 --- a/doc/diagrams/modules.puml +++ b/doc/diagrams/modules.puml @@ -1,6 +1,6 @@ @startuml -!include ./skinparams.puml +!include skinparams.puml !include GameState.pumlc !include SGF.pumlc diff --git a/doc/diagrams/planificationWorkPlanEngine.puml b/doc/diagrams/planificationWorkPlanEngine.puml new file mode 100644 index 0000000..53bd5ea --- /dev/null +++ b/doc/diagrams/planificationWorkPlanEngine.puml @@ -0,0 +1,30 @@ +@startgantt + +!include skinparams.puml +!include skinparamsGantt.puml + +'printscale weekly +Sunday are closed + +Project starts 2021-01-04 + +-- Engine Implementation -- +[Engine modelling] as [EM] starts 2021-01-04 +[Engine modelling] as [EM] lasts 1 week +[Engine implementation] as [EI] lasts 5 weeks +[Engine testing] as [ET] lasts 5 weeks + +-- Algorithms Implementations -- +[Algorithm research] as [AR] lasts 1 week +[Monte Carlo implementation] as [MCI] lasts 3 weeks +[Extra algorithms research] as [EAR] lasts 2 weeks +[Extra algorithms implementation] as [EAI] lasts 4 weeks + +[EM] -> [AR] +[AR] -> [MCI] +[AR] -> [EI] +[AR] -> [ET] +[EI] -> [EAR] +[EAR] -> [EAI] + +@endgantt diff --git a/doc/diagrams/planificationWorkPlanGame.puml b/doc/diagrams/planificationWorkPlanGame.puml new file mode 100644 index 0000000..427564a --- /dev/null +++ b/doc/diagrams/planificationWorkPlanGame.puml @@ -0,0 +1,18 @@ +@startgantt + +!include skinparams.puml +!include skinparamsGantt.puml + +'printscale weekly +Sunday are closed + +Project starts 2020-11-02 + +-- Game Implementation -- +[Domain modelling] as [DM] lasts 6 days +[Domain implementation] as [DI] lasts 30 days +[Domain testing] as [DT] lasts 30 days +[DM] -> [DI] +[DM] -> [DT] + +@endgantt diff --git a/doc/diagrams/skinparamsGantt.puml b/doc/diagrams/skinparamsGantt.puml new file mode 100644 index 0000000..5355d5f --- /dev/null +++ b/doc/diagrams/skinparamsGantt.puml @@ -0,0 +1,14 @@ +@startuml + + + +@enduml diff --git a/doc/diagrams/useCases.puml b/doc/diagrams/useCases.puml new file mode 100644 index 0000000..e3da3f8 --- /dev/null +++ b/doc/diagrams/useCases.puml @@ -0,0 +1,18 @@ +@startuml + +!include skinparams.puml + +actor "Human Player" as player +actor "Human User" as user +actor "GUI Program" as gui + +usecase "Play a match" as play +usecase "Generate a move" as genMove +usecase "Use as backend for machine player" as backend + +player --> play +user --> genMove +gui --> genMove +gui --> backend + +@enduml diff --git a/doc/tex/biber.bib b/doc/tex/biber.bib new file mode 100644 index 0000000..ddebe23 --- /dev/null +++ b/doc/tex/biber.bib @@ -0,0 +1,22 @@ +@online{sl_go, + title = {Go}, + organization = {Sensei's Library}, + date = {2019}, + urldate = {2021}, + url = {https://senseis.xmp.net/?go} +} + +@article{natureAlphaGo2016, + author = {David Silver and Aja Huang and Chris J. Maddison and Arthur Guez and Laurent Sifre and George van den Driessche and Julian Schrittwieser and Ioannis Antonoglou and Veda Panneershelvam and Marc Lanctot and Sander Dieleman and Dominik Grewe and John Nham and Nal Kalchbrenner and Ilya Sutskever and Timothy Lillicrap and Madeleine Leach and Koray Kavukcuoglu and Thore Graepel and Demis Hassabis}, + title = {Mastering the game of Go with deep neural networks and tree search}, + journaltitle = {Nature}, + date = {2016-01-27}, + url = {https://www.nature.com/articles/nature16961} +} + +@online{plantillaRedondo, + author = {J. M. Redondo}, + title = {Documentos-modelo para Trabajos de Fin de Grado/Master de la Escuela de Informática de Oviedo}, + date = {2019-06-17}, + url = {https://www.researchgate.net/publication/327882831_Plantilla_de_Proyectos_de_Fin_de_Carrera_de_la_Escuela_de_Informatica_de_Oviedo} +} diff --git a/doc/tex/planification.tex b/doc/tex/planification.tex new file mode 100644 index 0000000..9a95e8b --- /dev/null +++ b/doc/tex/planification.tex @@ -0,0 +1,162 @@ +\section{Planning} + +\subsection{Driving needs} + +As one of the deepest and most studied games in the world, Go presents a very +interesting problem for artificial intelligence. Implementing not only the +game's simple but subtle rules, but a system capable of playing it with a +satisfying level of skill, is a task worth of pursuing as an exercise on +software design, algorithmics and AI research. + +On the practical level, this project can be a foundation for the development of +different Go analysis algorithms by providing an existing engine to house them, +which can be of interest to Go players and software scientists alike. + +\subsection{Reach} + +Presented here are the ideal targets of the project. + +\begin{itemize} + \item An implementation of the game of Go, that is, a system for holding the + moves and variants of a match (a tree of moves) and the logic for the + game's rules. + \item An engine capable of analyzing board positions and generating strong + moves. + \item Either a GUI specifically developed for the project or an + implementation of an existing protocol so the engine can be used with + existing tools and GUIs. +\end{itemize} + +\subsection{Project stages} + +The project will be organized in several stages based on the different +components and needs. + +\subsubsection{Game implementation} + +The rules of the game must be implemented, ideally in a way they can be tested +by direct human play. This system will at its bare minimum represent the +Japanese Go rules (area scoring, no superko rule, no suicide moves). + +\subsubsection{Engine implementation} + +The key of this project is to create some kind of system able to generate strong +moves based on any given board configuration: this will be such system. It will +implement an existing protocol so it can be used with other compatible tools. It +has to be able to receive game updates and configuration and to output moves for +either player. It should also be modular enough so different algorithms can be +selected and tested against each other as an experimental search for the best of +them. + +\subsubsection{Artificial Intelligence algorithms} + +Different algorithms for the engine to use should be implemented and tested. The +results of this development and testing process should be presented as part of +the final version of the project. + +\subsection{Logistics} + +The project will be developed by Íñigo Gutiérrez Fernández, student of the +Computer Software Engineering at the University of Oviedo, with supervision from +Vicente García Díaz, Associate Professor in the Department of Computer Science +at the University of Oviedo. + +The used material consists of a development and testing machine owned by the +student with specifications stated later on the project duration. + +\subsection{Work plan} + +The sole developer will be the student, who is currently working as a Junior +Software Engineer on a 35 hour per week schedule and no university +responsibilities other than this project. Taking this into account, a sensible +initial assumption is that he will be able to work 3 hours a day, Monday to +Saturday. Gantt diagrams for the planned working schedule are shown as +Fig.~\ref{fig:planificationWorkPlanGame} and +Fig.~\ref{fig:planificationWorkPlanEngine}. + +\begin{figure}[h] + \begin{center} + \includegraphics[width=\textwidth]{diagrams/planificationWorkPlanGame.png} + \caption{Initial work plan for implementing the game. + }\label{fig:planificationWorkPlanGame} + \end{center} +\end{figure} + +\begin{figure}[h] + \begin{center} + \includegraphics[width=\textwidth]{diagrams/planificationWorkPlanEngine.png} + \caption{Initial work plan for implementing the engine and algorithms. + }\label{fig:planificationWorkPlanEngine} + \end{center} +\end{figure} + +\subsection{Previous works} + +\subsubsection{Existing engines} + +\paragraph{AlphaGo} + +AlphaGo is a Go play and analysis engine developed by DeepMind Technologies, a +company owned by Google. It revolutionized the world of Go in 2015 and 2016 when +it respectively became the first AI to win against a professional Go player and +then won against Lee Sedol, a Korean player of the highest professional rank and +one of the strongest players in the world at the time. Its source code is +closed, but a paper \parencite{natureAlphaGo2016} written by the team and +published on Nature is available on +https://storage.googleapis.com/deepmind-media/alphago/AlphaGoNaturePaper.pdf. + +The unprecedented success of AlphaGo served as inspiration for many AI projects, +including this one. + +\paragraph{KataGo} + +KataGo is an open source project based on the AlphaGo paper that also achieved +superhuman strength of play. The availability of its implementation and +documentation presents a great resource for this project. + +\paragraph{GnuGo} + +GnuGo is a software capable of playing Go part of the GNU project. Although not +a strong engine anymore, it is interesting for historic reasons as the free +software engine for which the GTP protocol was first defined. + +\subsubsection{GTP} + +GTP (\textit{Go Text Protocol}) is a text based protocol for communication with +computer go programs. [ref https://www.lysator.liu.se/~gunnar/gtp/] It is the +protocol used by GNU Go and the more modern and powerful KataGo. By supporting +GTP the engine developed for this project can be used with existing GUIs and +other programs, making it easier to use it with the tools users are already +familiar with. + +\subsubsection{Sabaki} + +Sabaki is a go board software compatible with GTP engines. It can serve as a GUI +for the engine developed in this project and as an example of the advantages of +following a standardized protocol. + +\subsection{Technological Infrastructure} + +\subsubsection{Programming language} + +The resulting product of this project will be one or more pieces of software +able to be run locally on a personal computer. The programming language of +choice is Python, for various reasons: + +\begin{itemize} + + \item Has established a reputation on scientific fields and more + specifically on AI research and development. + \item Interpreters are available for many platforms, which allows the most + people possible to access the product. + \item Although not too deeply, it has been used by the developer student + during its degree including in AI and game theory contexts. + +\end{itemize} + +\subsubsection{Interface} + +Both the game and the engine will offer a text interface. For the game this +allows for quick human testing. For the engine it is mandated by the engine, +since GTP is a text based protocol for programs using text interfaces. +Independent programs compatible with this interface can be used as a GUI. diff --git a/doc/tex/previousWorks.tex b/doc/tex/previousWorks.tex index 6e503a3..bff0c82 100644 --- a/doc/tex/previousWorks.tex +++ b/doc/tex/previousWorks.tex @@ -1,17 +1,17 @@ \section{Previous works} -\subsection{SGF} - -SGF (\textit{Smart Go Format} or, in a more general context, \textit{Smart Game -Format}) is a text file format specification for records of games or collections -of them. It was devised for Go but it supports other games with similar -turn-based structure. It supports move variations, annotations, setups and game -metadata. By supporting SGF our application can be used to analyse existing -games registered by other applications, such as those played on online Go -servers. - -The SGF specification can be found at -\url{https://www.red-bean.com/sgf/user_guide/index.html} +%\subsection{SGF} +% +%SGF (\textit{Smart Go Format} or, in a more general context, \textit{Smart Game +%Format}) is a text file format specification for records of games or collections +%of them. It was devised for Go but it supports other games with similar +%turn-based structure. It supports move variations, annotations, setups and game +%metadata. By supporting SGF our application can be used to analyse existing +%games registered by other applications, such as those played on online Go +%servers. +% +%The SGF specification can be found at +%\url{https://www.red-bean.com/sgf/user_guide/index.html} \subsection{GTP} diff --git a/doc/tex/systemAnalysis.tex b/doc/tex/systemAnalysis.tex new file mode 100644 index 0000000..d92f537 --- /dev/null +++ b/doc/tex/systemAnalysis.tex @@ -0,0 +1,200 @@ +\section{System Analysis} + +%\subsection{System reach determination} + +\subsection{System Requirements} + +The requirements for the system are expressed here in a nested list way, each of +them with a textual and numeric reference so they are traceable. The functional +requirements are exposed first, followed by the other kinds of requisites needed +for the system. + +\setlist[enumerate,2]{label*=\arabic*.} +\setlist[enumerate,3]{label*=\arabic*.} + +\subsubsection{Functional Requirements} + +\paragraph{Game Requirements} + +\setlist[enumerate,1]{label=FRG \arabic*.} + +\begin{enumerate} + + \item The state of the board can be shown to the user. + \begin{enumerate} + \item A text representation of each cell is printed. + \begin{enumerate} + \item A different character is used for each different state + of a cell. + \end{enumerate} + \item The coordinates system is shown around the board. + \begin{enumerate} + \item Columns are shown as capital letters left to right + starting with ``A'' and skipping ``I''. + \item Rows are shown as numbers starting with 1 on the + lowest row and increasing upwards. + \end{enumerate} + \end{enumerate} + + \item Movements can be introduced to be played on the board. + \begin{enumerate} + \item A move is introduced as the textual representation of the + coordinates of the vertex to play on or as ``pass''. + \begin{enumerate} + \item The text introduced for the move must follow the + regular expression \texttt{([A-Z][0-9]+|pass)} + \item If the move is not valid, it must be notified to the + user and another move asked for. + \end{enumerate} + \end{enumerate} + + \item The board will behave according to the Japanese rules of Go. + +\end{enumerate} + +\paragraph{Engine Requirements} + +\setlist[enumerate,1]{label=FRE \arabic*.} + +\begin{enumerate} + + \item Coordinates of the board representing valid moves must be printed. + +\end{enumerate} + +%\subsubsection{Security Requirements} +% +%\setlist[enumerate,1]{label=SR \arabic*.} + +\subsubsection{Usability Requirements} + +\setlist[enumerate,1]{label=UR \arabic*.} + +\begin{enumerate} + + \item The engine executable will include a help option with the different + modes of execution. + +\end{enumerate} + +\subsubsection{User Requirements} + +\setlist[enumerate,1]{label=USR \arabic*.} + +\begin{enumerate} + + \item For understanding the workings of the application the user needs to be + familiar with the basics of the game of Go. + + \item For directly using the engine the user needs to be familiar with + command line interfaces. + +\end{enumerate} + +\subsubsection{Technological Requirements} + +\setlist[enumerate,1]{label=TR \arabic*.} + +\begin{enumerate} + + \item The game program will be a python file able to be executed by the + python interpreter. + + \item The program will make use of standard input and standard output for + communication. + \begin{enumerate} + \item Standard input will be used for reading moves. + \item Standard output will be used for showing the board and for + messages directed to the user. + \end{enumerate} + +\end{enumerate} + +\subsubsection{Response Time Requirements} + +\setlist[enumerate,1]{label=RTR \arabic*.} + +\begin{enumerate} + + \item The maximum thinking time of the engine will be configurable. + \begin{enumerate} + \item It will be possible to pass the maximum time as a launch + argument. + \item It will be possible to store the maximum time as a setting + in a configuration file + \end{enumerate} + +\end{enumerate} + + +\setlist[enumerate,1]{label=\arabic*.} + +\subsection{System Actors} + +There are various actors who will interact with the system, both human and +non-human. + +\begin{itemize} + + \item The human player who interacts with the playing interface. + \item The human user who interacts with the engine. + \item A GUI software which uses the engine to generate moves. + +\end{itemize} + +\subsection{Use Cases} + +\begin{figure}[h] + \begin{center} + \includegraphics[width=\textwidth]{diagrams/useCases.png} + \caption{Use cases.}\label{fig:useCases} + \end{center} +\end{figure} + +The different actors and use cases are represented on \fref{fig:useCases}. Each +use case is explained next. + +\paragraph{Play a match} + +The game interface reads the moves presented by the player and shows their +result on the board. + +\paragraph{Generate moves} + +The engine interface reads the input for generating a move as stated by the +GTP protocol and outputs the coordinates of the board to play. + +\paragraph{Use as backend for machine player} + +The engine is used as the backend for generating moves for a machine player. + +\subsection{Subsystems} + +\subsubsection{Subsystems description} + +There will be two main subsystems. + +% TODO: Are there really two different subsystems? They look very coupled, since +% the engine will use some classes of the game. This section is more suited for +% independently run systems which communicate through some common interface. + +The first, called the Game System, will be in charge of storing all the state +information regarding a Go match, such as the history of moves, the possible +variations, the state of the board at any given time or the current number of +captured stones. + +The second, called the Engine System, will implement the GTP interface and use +the Game System to analyze positions and generate moves via decision algorithms. + +\subsection{Class analysis} + +The classes resulting from the analysis phase are shown in +\fref{fig:analysisClasses}. + +\begin{figure}[h] + \begin{center} + \includegraphics[width=\textwidth]{diagrams/analysisClasses.png} + \caption{General classes obtained from the analysis + phase.}\label{fig:analysisClasses} + \end{center} +\end{figure} diff --git a/doc/tex/tfg.tex b/doc/tex/tfg.tex index a14708d..9bf2189 100644 --- a/doc/tex/tfg.tex +++ b/doc/tex/tfg.tex @@ -5,27 +5,24 @@ \usepackage{booktabs} \usepackage{hyperref} \usepackage{csquotes} +\usepackage{enumitem} \usepackage[backend=biber, style=numeric-comp]{biblatex} -\addbibresource{/home/taamas/docs/biber.bib} +\addbibresource{tex/biber.bib} \geometry{left=4.5cm,top=2cm,bottom=2cm,right=4.5cm} -\hypersetup{colorlinks=true, +\hypersetup{colorlinks=false, linkcolor=black, - filecolor=magenta, + filecolor=black, urlcolor=black, bookmarks=true } \urlstyle{mono} -%\renewcommand{\contentsname}{Contenidos} -%\renewcommand{\figurename}{Figura} - \newcommand{\program}{Imago} -\newcommand{\inputtex}[1]{\input{tex/#1}} \newcommand{\fref}[1]{Fig.~\ref{#1}} %\newcommand{\uurl}[1]{\underline{\url{#1}}} @@ -45,15 +42,70 @@ This is the abstract. \end{abstract} +\clearpage + +\section*{Acknowledgements} + +TODO: Acknowledgements + +To Vicente García Díaz, for directing me in this project. + +To José Manuel Redondo López\cite{plantillaRedondo}, for making a very +complete template on which the structure of this documentation is extensively +based. + +\clearpage + +\section*{Copyright notice} + +\begin{displayquote} + + Copyright (C) 2021 \textbf{ÍÑIGO GUTIÉRREZ FERNÁNDEZ}. + + \textit{Permission is granted to copy, distribute and/or modify this + document under the terms of the GNU Free Documentation License, Version 1.3 + or any later version published by the Free Software Foundation; with the + Copyright notice as an Invariant Section, no Front- Cover Texts, and no + Back-Cover Texts. A copy of the license is included in the section + entitled ``GNU Free Documentation License''.} + +\end{displayquote} + +Although this document uses the GNU Free Documentation License to keep its +contained information free, bear in mind that it isn't software documentation or +a manual or guide of any kind, and serves just as the documentation for the +author's Degree's Final Project. + +This document is based on a template by José Manuel Redondo López, associate +professor of the University of Oviedo. The copyright notice he asks for +inclusion to use this template is included here. + +\begin{displayquote} + + Copyright (C) 2019 \textbf{JOSÉ MANUEL REDONDO LÓPEZ}.\cite{plantillaRedondo} + + \textit{Permission is granted to copy, distribute and/or modify this + document under the terms of the GNU Free Documentation License, Version 1.3 + or any later version published by the Free Software Foundation; with no + Invariant Sections, no Front- Cover Texts, and no Back-Cover Texts. A copy + of the license is included in the section entitled ``GNU Free + Documentation License''.} + +\end{displayquote} + +\clearpage + \tableofcontents -\inputtex{introduction.tex} +\input{tex/introduction.tex} + +\input{tex/planification.tex} -\inputtex{previousWorks.tex} +\input{tex/systemAnalysis.tex} -\inputtex{interface.tex} +%\input{tex/interface.tex} -\inputtex{implementation.tex} +%\input{tex/implementation.tex} \printbibliography{} -- cgit v1.2.1 From 2a9a758d0bc968db45035a331844e03e3e66e8c7 Mon Sep 17 00:00:00 2001 From: InigoGutierrez Date: Wed, 30 Jun 2021 20:25:16 +0200 Subject: docs: Added first class diagrams to system analysis section. --- doc/diagrams/analysisClasses.puml | 10 ++- doc/tex/systemAnalysis.tex | 181 ++++++++++++++++++++++++++++++++++++++ doc/tex/tfg.tex | 4 + 3 files changed, 192 insertions(+), 3 deletions(-) diff --git a/doc/diagrams/analysisClasses.puml b/doc/diagrams/analysisClasses.puml index 5f1ccde..6adc849 100644 --- a/doc/diagrams/analysisClasses.puml +++ b/doc/diagrams/analysisClasses.puml @@ -4,24 +4,28 @@ package GameModule { class GameIO + class GameState class GameTree class GameMove } -GameIO -> GameTree +GameIO -> GameState +GameState -> GameTree GameTree -> GameMove package EngineModule { class EngineIO class EngineLogic interface DecisionAlgorithm - class DecisionAlgorithm1 class DecisionAlgorithm2 + class DecisionAlgorithm1 } -EngineIO -> EngineLogic +EngineIO --> EngineLogic EngineLogic -> DecisionAlgorithm DecisionAlgorithm <|.. DecisionAlgorithm1 DecisionAlgorithm <|.. DecisionAlgorithm2 +EngineLogic --> GameTree + @enduml diff --git a/doc/tex/systemAnalysis.tex b/doc/tex/systemAnalysis.tex index d92f537..7753018 100644 --- a/doc/tex/systemAnalysis.tex +++ b/doc/tex/systemAnalysis.tex @@ -188,6 +188,8 @@ the Game System to analyze positions and generate moves via decision algorithms. \subsection{Class analysis} +\subsubsection{Class diagram} + The classes resulting from the analysis phase are shown in \fref{fig:analysisClasses}. @@ -198,3 +200,182 @@ The classes resulting from the analysis phase are shown in phase.}\label{fig:analysisClasses} \end{center} \end{figure} + +\subsubsection{Class description} + +\newcommand{\interclassSpace}{30pt} + +\indent + +\begin{tabular}{p{\linewidth}} + \toprule + \textbf{EngineIO} \\ + \midrule + \textbf{Description} \\ + Offers the interface with the engine. \\ + \midrule + \textbf{Responsibilities} \\ + % TODO: Single responsibility would be better? + \tabitem{Read input.} \\ + \tabitem{Do some preprocessing.} \\ + \tabitem{Forward commands to the engine logic component.} \\ + \midrule + \textbf{Proposed attributes} \\ + \midrule + \textbf{Proposed methods} \\ + \tabitem{\textbf{start()}: Starts reading standard input in a loop.} \\ + \bottomrule +\end{tabular} + +\vspace{\interclassSpace} + +\begin{tabular}{p{\linewidth}} + \toprule + \textbf{EngineLogic} \\ + \midrule + \textbf{Description} \\ + Does the core logic and connects the different components of the engine. \\ + \midrule + \textbf{Responsibilities} \\ + \tabitem{Processes the commands and arguments forwarded by the IO + component.} \\ + \tabitem{Handles the logic of the game by using components from the game + module.} \\ + \tabitem{Calls a decision algorithm to generate moves.} \\ + \midrule + \textbf{Proposed attributes} \\ + \midrule + \textbf{Proposed methods} \\ + \bottomrule +\end{tabular} + +\vspace{\interclassSpace} + +\begin{tabular}{p{\linewidth}} + \toprule + \textbf{DecisionAlgorithm} \\ + \midrule + \textbf{Description} \\ + Interface for the decision algorithms to be used by the engine. \\ + \midrule + \textbf{Responsibilities} \\ + \tabitem{Analyzing game states and generating moves.} \\ + \midrule + \textbf{Proposed attributes} \\ + \textit{Depends on the algorithm.} \\ + \midrule + \textbf{Proposed methods} \\ + \tabitem{\textbf{genmove()}: Gives the coordinates of a move to play.} \\ + \bottomrule +\end{tabular} + +\vspace{\interclassSpace} + +\begin{tabular}{p{\linewidth}} + \toprule + \textbf{GameIO} \\ + \midrule + \textbf{Description} \\ + Offers the interface with the game. \\ + \midrule + \textbf{Responsibilities} \\ + \tabitem{Read input.} \\ + \tabitem{Do some preprocessing.} \\ + \tabitem{Forward commands to the game state component.} \\ + \midrule + \textbf{Proposed attributes} \\ + \midrule + \textbf{Proposed methods} \\ + \tabitem{\textbf{start()}: Starts reading standard input in a loop.} \\ + \bottomrule +\end{tabular} + +\vspace{\interclassSpace} + +\begin{tabular}{p{\linewidth}} + \toprule + \textbf{GameState} \\ + \midrule + \textbf{Description} \\ + Stores the state of a match. \\ + \midrule + \textbf{Responsibilities} \\ + \tabitem{Store state information.} \\ + \tabitem{Offer methods to manipulate the game state.} \\ + \midrule + \textbf{Proposed attributes} \\ + \midrule + \textbf{Proposed methods} \\ + \tabitem{\textbf{getCurrentPlayer()}: Returns the player who should play + next.} \\ + \tabitem{\textbf{playMove()}: Play a move in the specified coordinates + for the specified player.} \\ + \tabitem{\textbf{playMoveForPlayer()}: Play a move in the specified coordinates + for the player who should play next.} \\ + \tabitem{\textbf{undo()}: Reset the state to how it was just before the + last move was played.} \\ + \bottomrule +\end{tabular} + +\vspace{\interclassSpace} + +\begin{tabular}{p{\linewidth}} + \toprule + \textbf{GameTree} \\ + \midrule + \textbf{Description} \\ + Stores the moves and variations in a match. \\ + \midrule + \textbf{Responsibilities} \\ + \tabitem{Store the base node of the tree of moves of a match.} \\ + \midrule + \textbf{Proposed attributes} \\ + \tabitem{\textbf{GameMove root}: First move of the match (normally a + symbolic move representing the empty board before the actual first move of a + player).} \\ + \midrule + \textbf{Proposed methods} \\ + \bottomrule +\end{tabular} + +\vspace{\interclassSpace} + +\begin{tabular}{p{\linewidth}} + \toprule + \textbf{GameMove} \\ + \midrule + \textbf{Description} \\ + Stores information about a specific move and its relationships to the + previous and next moves. \\ + \midrule + \textbf{Responsibilities} \\ + \tabitem{Store information about a move (board, player, coordinates\ldots).} \\ + \midrule + \textbf{Proposed attributes} \\ + \tabitem{\textbf{GameBoard board}: The board as of this move.} \\ + \tabitem{\textbf{GameMove[] nextMoves}: The list of moves played after this + one. Different moves represent different game variations.} \\ + \tabitem{\textbf{GameMove previousMove}: The move before this one.} \\ + \tabitem{\textbf{boolean isPass}: True if the move is a pass and not a stone + placement.} \\ + \tabitem{\textbf{int[] coords}: The coordinates of the board the move was + played at. Have no meaning if \textbf{isPass} is true.} \\ + \midrule + \textbf{Proposed methods} \\ + \tabitem{\textbf{getRow()}: Returns the row the move was played at.} \\ + \tabitem{\textbf{getCol()}: Returns the col the move was played at.} \\ + \tabitem{\textbf{getPlayer()}: Returns the player who played the move.} \\ + \tabitem{\textbf{getNextPlayer()}: Returns the player who should play after + this move.} \\ + \tabitem{\textbf{getGameLength()}: Returns the number of moves the game has + had.} \\ + \tabitem{\textbf{getPlayableVertices()}: Returns the legal vertices for the + next move.} \\ + \tabitem{\textbf{addMove()}: Inserts a new children move for the given + coordinates and for the player who should make the next move.} \\ + \tabitem{\textbf{addMoveForPlayer()}: Inserts a new children move for the given + coordinates and player.} \\ + \bottomrule +\end{tabular} + +\vspace{\interclassSpace} diff --git a/doc/tex/tfg.tex b/doc/tex/tfg.tex index 9bf2189..7296cb9 100644 --- a/doc/tex/tfg.tex +++ b/doc/tex/tfg.tex @@ -7,6 +7,9 @@ \usepackage{csquotes} \usepackage{enumitem} +\usepackage{chngcntr} +\counterwithin{figure}{section} + \usepackage[backend=biber, style=numeric-comp]{biblatex} \addbibresource{tex/biber.bib} @@ -26,6 +29,7 @@ \newcommand{\fref}[1]{Fig.~\ref{#1}} %\newcommand{\uurl}[1]{\underline{\url{#1}}} +\newcommand{\tabitem}{~~\llap{\textbullet}~~} \begin{document} \frenchspacing -- cgit v1.2.1 From 79a2fa6eba1648bf2beaaf534d1fa3247c0d6a01 Mon Sep 17 00:00:00 2001 From: InigoGutierrez Date: Wed, 1 Jun 2022 20:14:15 +0200 Subject: Some redesigning. --- doc/Makefile | 4 +- doc/diagrams/analysisClasses.puml | 17 +-- doc/diagrams/planificationWorkPlanGame.puml | 6 + doc/diagrams/useCase_useAsBackend.puml | 23 ++++ doc/tex/biber.bib | 60 ++++++-- doc/tex/planification.tex | 51 ++++--- doc/tex/systemAnalysis.tex | 204 ++++++++++++++++++++++++++-- doc/tex/systemDesign.tex | 2 + doc/tex/tfg.tex | 22 ++- imago/engine/monteCarlo.py | 28 +++- imago/gameLogic/gameBoard.py | 14 +- imago/gameLogic/gameState.py | 6 +- imago/gameLogic/gameTree.py | 8 +- tests/test_monteCarlo.py | 4 +- tests/test_parseHelpers.py | 7 + 15 files changed, 368 insertions(+), 88 deletions(-) create mode 100644 doc/diagrams/useCase_useAsBackend.puml create mode 100644 doc/tex/systemDesign.tex diff --git a/doc/Makefile b/doc/Makefile index a631921..4153756 100644 --- a/doc/Makefile +++ b/doc/Makefile @@ -3,8 +3,8 @@ docName = tfg outputFolder = out -texFiles = tex/tfg.tex tex/introduction.tex tex/planification.tex tex/interface.tex tex/implementation.tex tex/systemAnalysis.tex tex/biber.bib -diagramImgs = diagrams/gameRepresentation.png diagrams/gtpEngine.png diagrams/modules.png diagrams/planificationWorkPlanEngine.png diagrams/planificationWorkPlanGame.png diagrams/sgfModule.png diagrams/useCases.png diagrams/analysisClasses.png +texFiles = tex/tfg.tex tex/introduction.tex tex/planification.tex tex/interface.tex tex/implementation.tex tex/systemAnalysis.tex tex/systemDesign.tex tex/biber.bib +diagramImgs = diagrams/gameRepresentation.png diagrams/gtpEngine.png diagrams/modules.png diagrams/planificationWorkPlanEngine.png diagrams/planificationWorkPlanGame.png diagrams/sgfModule.png diagrams/useCases.png diagrams/analysisClasses.png diagrams/useCase_useAsBackend.png all: $(docName).pdf diff --git a/doc/diagrams/analysisClasses.puml b/doc/diagrams/analysisClasses.puml index 6adc849..4a286f7 100644 --- a/doc/diagrams/analysisClasses.puml +++ b/doc/diagrams/analysisClasses.puml @@ -5,27 +5,28 @@ package GameModule { class GameIO class GameState - class GameTree + class GameBoard class GameMove } GameIO -> GameState -GameState -> GameTree -GameTree -> GameMove +GameState -> GameMove +GameMove -> GameBoard package EngineModule { class EngineIO class EngineLogic interface DecisionAlgorithm - class DecisionAlgorithm2 - class DecisionAlgorithm1 + class MonteCarloTreeSearch + class OtherDecisionAlgorithm } EngineIO --> EngineLogic EngineLogic -> DecisionAlgorithm -DecisionAlgorithm <|.. DecisionAlgorithm1 -DecisionAlgorithm <|.. DecisionAlgorithm2 +DecisionAlgorithm <|.. MonteCarloTreeSearch +DecisionAlgorithm <|.. OtherDecisionAlgorithm -EngineLogic --> GameTree +MonteCarloTreeSearch --> GameMove +OtherDecisionAlgorithm --> GameMove @enduml diff --git a/doc/diagrams/planificationWorkPlanGame.puml b/doc/diagrams/planificationWorkPlanGame.puml index 427564a..42b0821 100644 --- a/doc/diagrams/planificationWorkPlanGame.puml +++ b/doc/diagrams/planificationWorkPlanGame.puml @@ -8,10 +8,16 @@ Sunday are closed Project starts 2020-11-02 +-- Preliminary investigation -- +[Previous works investigation] as [PWI] lasts 7 days +[Engines investigation] as [EI] lasts 7 days + -- Game Implementation -- [Domain modelling] as [DM] lasts 6 days [Domain implementation] as [DI] lasts 30 days [Domain testing] as [DT] lasts 30 days + +[PWI] -> [DM] [DM] -> [DI] [DM] -> [DT] diff --git a/doc/diagrams/useCase_useAsBackend.puml b/doc/diagrams/useCase_useAsBackend.puml new file mode 100644 index 0000000..3f1cece --- /dev/null +++ b/doc/diagrams/useCase_useAsBackend.puml @@ -0,0 +1,23 @@ +@startuml + +!include skinparams.puml + +actor "GUI Program" as program + +boundary "Set board state" as setState +control "State set to represent the needed board" as setStateEngine +entity "Board state" as state +boundary "Move is asked for" as ask +control "Engine thinks next move" as think +boundary "Move is suggested" as suggestion + +program -> setState +setState -> setStateEngine +setStateEngine -> state +state -> ask +program -> ask +ask -> think +think -> suggestion +program -> suggestion + +@enduml diff --git a/doc/tex/biber.bib b/doc/tex/biber.bib index ddebe23..ef09d98 100644 --- a/doc/tex/biber.bib +++ b/doc/tex/biber.bib @@ -1,11 +1,3 @@ -@online{sl_go, - title = {Go}, - organization = {Sensei's Library}, - date = {2019}, - urldate = {2021}, - url = {https://senseis.xmp.net/?go} -} - @article{natureAlphaGo2016, author = {David Silver and Aja Huang and Chris J. Maddison and Arthur Guez and Laurent Sifre and George van den Driessche and Julian Schrittwieser and Ioannis Antonoglou and Veda Panneershelvam and Marc Lanctot and Sander Dieleman and Dominik Grewe and John Nham and Nal Kalchbrenner and Ilya Sutskever and Timothy Lillicrap and Madeleine Leach and Koray Kavukcuoglu and Thore Graepel and Demis Hassabis}, title = {Mastering the game of Go with deep neural networks and tree search}, @@ -14,9 +6,61 @@ url = {https://www.nature.com/articles/nature16961} } +@online{gtp, + author = {Gunnar Farnebäck}, + title = {GTP --- Go Text Protocol}, + date = {2002}, + urldate = {2021}, + url = {https://www.lysator.liu.se/~gunnar/gtp} +} + +@online{katago, + author = {David J Wu ("lightvector")}, + title = {KataGo}, + date = {2021}, + urldate = {2021}, + url = {https://github.com/lightvector/KataGo} +} + +@online{gnugo, + title = {GNU Go}, + organization = {Free Software Foundation}, + date = {2019}, + urldate = {2021}, + url = {https://www.gnu.org/software/gnugo} +} + +@online{sabaki, + author = {Yichuan Shen}, + title = {Sabaki --- An elegant Go board and SGF editor for a more civilized age.}, + date = {2019}, + urldate = {2021}, + url = {https://sabaki.yichuanshen.de} +} + +@online{sl_go, + title = {Go}, + organization = {Sensei's Library}, + date = {2019}, + urldate = {2021}, + url = {https://senseis.xmp.net/?go} +} + @online{plantillaRedondo, author = {J. M. Redondo}, title = {Documentos-modelo para Trabajos de Fin de Grado/Master de la Escuela de Informática de Oviedo}, date = {2019-06-17}, url = {https://www.researchgate.net/publication/327882831_Plantilla_de_Proyectos_de_Fin_de_Carrera_de_la_Escuela_de_Informatica_de_Oviedo} } + +@online{python_unittest, + title = {unittest --- Unit testing framework}, + urldate = {2021}, + url = {https://docs.python.org/3/library/unittest.html} +} + +@online{python_coverage, + title = {Coverage.py}, + urldate = {2021}, + url = {https://coverage.readthedocs.io} +} diff --git a/doc/tex/planification.tex b/doc/tex/planification.tex index 9a95e8b..5c9b253 100644 --- a/doc/tex/planification.tex +++ b/doc/tex/planification.tex @@ -57,17 +57,17 @@ the final version of the project. \subsection{Logistics} The project will be developed by Íñigo Gutiérrez Fernández, student of the -Computer Software Engineering at the University of Oviedo, with supervision from -Vicente García Díaz, Associate Professor in the Department of Computer Science -at the University of Oviedo. +Computer Software Engineering Degree at the University of Oviedo, with +supervision from Vicente García Díaz, Associate Professor in the Department of +Computer Science at the University of Oviedo. The used material consists of a development and testing machine owned by the -student with specifications stated later on the project duration. +student with specifications stated later on the project plan. \subsection{Work plan} The sole developer will be the student, who is currently working as a Junior -Software Engineer on a 35 hour per week schedule and no university +Software Engineer on a 35 hour per week schedule and with no university responsibilities other than this project. Taking this into account, a sensible initial assumption is that he will be able to work 3 hours a day, Monday to Saturday. Gantt diagrams for the planned working schedule are shown as @@ -96,9 +96,9 @@ Fig.~\ref{fig:planificationWorkPlanEngine}. \paragraph{AlphaGo} -AlphaGo is a Go play and analysis engine developed by DeepMind Technologies, a -company owned by Google. It revolutionized the world of Go in 2015 and 2016 when -it respectively became the first AI to win against a professional Go player and +A Go play and analysis engine developed by DeepMind Technologies, a company +owned by Google. It revolutionized the world of Go in 2015 and 2016 when it +respectively became the first AI to win against a professional Go player and then won against Lee Sedol, a Korean player of the highest professional rank and one of the strongest players in the world at the time. Its source code is closed, but a paper \parencite{natureAlphaGo2016} written by the team and @@ -108,28 +108,27 @@ https://storage.googleapis.com/deepmind-media/alphago/AlphaGoNaturePaper.pdf. The unprecedented success of AlphaGo served as inspiration for many AI projects, including this one. -\paragraph{KataGo} +\paragraph{KataGo~\cite{katago}} -KataGo is an open source project based on the AlphaGo paper that also achieved -superhuman strength of play. The availability of its implementation and -documentation presents a great resource for this project. +An open source project based on the AlphaGo paper that also achieved superhuman +strength of play. The availability of its implementation and documentation +presents a great resource for this project. -\paragraph{GnuGo} +\paragraph{GnuGo~\cite{gnugo}} -GnuGo is a software capable of playing Go part of the GNU project. Although not -a strong engine anymore, it is interesting for historic reasons as the free -software engine for which the GTP protocol was first defined. +A software capable of playing Go part of the GNU project. Although not a strong +engine anymore, it is interesting for historic reasons as the free software +engine for which the GTP protocol was first defined. -\subsubsection{GTP} +\subsubsection{GTP~\cite{gtp}} -GTP (\textit{Go Text Protocol}) is a text based protocol for communication with -computer go programs. [ref https://www.lysator.liu.se/~gunnar/gtp/] It is the -protocol used by GNU Go and the more modern and powerful KataGo. By supporting -GTP the engine developed for this project can be used with existing GUIs and -other programs, making it easier to use it with the tools users are already -familiar with. +GTP (\textit{Go Text Protocol}) is a text based protocol for +communication with computer go programs. It is the protocol used by GNU Go and +the more modern and powerful KataGo. By supporting GTP the engine developed for +this project can be used with existing GUIs and other programs, making it easier +to use it with the tools users are already familiar with. -\subsubsection{Sabaki} +\subsubsection{Sabaki~\cite{sabaki}} Sabaki is a go board software compatible with GTP engines. It can serve as a GUI for the engine developed in this project and as an example of the advantages of @@ -145,7 +144,7 @@ choice is Python, for various reasons: \begin{itemize} - \item Has established a reputation on scientific fields and more + \item It has established a reputation on scientific fields and more specifically on AI research and development. \item Interpreters are available for many platforms, which allows the most people possible to access the product. @@ -157,6 +156,6 @@ choice is Python, for various reasons: \subsubsection{Interface} Both the game and the engine will offer a text interface. For the game this -allows for quick human testing. For the engine it is mandated by the engine, +allows for quick human testing. For the engine it is mandated by the protocol, since GTP is a text based protocol for programs using text interfaces. Independent programs compatible with this interface can be used as a GUI. diff --git a/doc/tex/systemAnalysis.tex b/doc/tex/systemAnalysis.tex index 7753018..5868422 100644 --- a/doc/tex/systemAnalysis.tex +++ b/doc/tex/systemAnalysis.tex @@ -5,9 +5,9 @@ \subsection{System Requirements} The requirements for the system are expressed here in a nested list way, each of -them with a textual and numeric reference so they are traceable. The functional -requirements are exposed first, followed by the other kinds of requisites needed -for the system. +them with a textual and numeric reference for them to be traceable. The +functional requirements are exposed first, followed by the other kinds of +requisites needed for the system. \setlist[enumerate,2]{label*=\arabic*.} \setlist[enumerate,3]{label*=\arabic*.} @@ -58,7 +58,47 @@ for the system. \begin{enumerate} - \item Coordinates of the board representing valid moves must be printed. + \item The engine implements the GTP (\textit{Go Text Protocol}) for its + interface. + \begin{enumerate} + \item Commands are read from standard input. + \item Responses are provided via standard output. + \item There exist commands to set up the conditions of the match. + \begin{enumerate} + \item The size of the board can be set. + \item The komi can be set. + \end{enumerate} + \item There exist commands to manipulate the internal representation + of the match. + \begin{enumerate} + \item It is possible to indicate a move being played. + \item It is possible to clear the board. + \end{enumerate} + \item There exists a command to generate a move. + \begin{enumerate} + \item The generated move must be a playable move. + \item Generating a move does not change the internal + representation of the match. + \end{enumerate} + \item There exist commands to ask for information about the engine. + \begin{enumerate} + \item It is possible to ask for the protocol version + implemented. + \item It is possible to ask for the name of the engine. + \item It is possible to ask for the version of the engine. + \item It is possible to ask whether a specific command is + known to the engine. + \item It is possible to ask for a list of the known commands. + \end{enumerate} + \item There exists a command to stop the engine. + \end{enumerate} + + \item The engine can be executed from the command line. + \begin{enumerate} + \item The engine can be executed directly from an interactive shell. + \item The engine can be executed by another program to be used as + backend. + \end{enumerate} \end{enumerate} @@ -121,7 +161,7 @@ for the system. \item It will be possible to pass the maximum time as a launch argument. \item It will be possible to store the maximum time as a setting - in a configuration file + in a configuration file. \end{enumerate} \end{enumerate} @@ -170,22 +210,36 @@ The engine is used as the backend for generating moves for a machine player. \subsection{Subsystems} -\subsubsection{Subsystems description} - There will be two main subsystems. % TODO: Are there really two different subsystems? They look very coupled, since % the engine will use some classes of the game. This section is more suited for % independently run systems which communicate through some common interface. +% ...Or maybe not. From the template: "Subsystems are groupings of packages and +% classes with a common objective. Examples of subsystems are the classes which +% handle the database, classes joining a group of related services..." + +\subsubsection{Game System} The first, called the Game System, will be in charge of storing all the state information regarding a Go match, such as the history of moves, the possible variations, the state of the board at any given time or the current number of captured stones. +This system will include a command-line interface with which to play Go matches +between human players to show and test its capabilities. + +\subsubsection{Engine System} + The second, called the Engine System, will implement the GTP interface and use the Game System to analyze positions and generate moves via decision algorithms. +This system can be directly called to manually set up game states and ask for +moves or can be called by other programs to be used as backend for playing +matches against a computer player. + +%\subsubsection{Interface between subsystems} + \subsection{Class analysis} \subsubsection{Class diagram} @@ -262,7 +316,7 @@ The classes resulting from the analysis phase are shown in \tabitem{Analyzing game states and generating moves.} \\ \midrule \textbf{Proposed attributes} \\ - \textit{Depends on the algorithm.} \\ + \textit{(Depends on the algorithm.)} \\ \midrule \textbf{Proposed methods} \\ \tabitem{\textbf{genmove()}: Gives the coordinates of a move to play.} \\ @@ -321,20 +375,27 @@ The classes resulting from the analysis phase are shown in \begin{tabular}{p{\linewidth}} \toprule - \textbf{GameTree} \\ + \textbf{GameBoard} \\ \midrule \textbf{Description} \\ - Stores the moves and variations in a match. \\ + Stores the state of a board position and handles its logic. \\ \midrule \textbf{Responsibilities} \\ - \tabitem{Store the base node of the tree of moves of a match.} \\ + \tabitem{Store the vertices of a board position.} \\ + \tabitem{Logic related to a board position.} \\ \midrule \textbf{Proposed attributes} \\ - \tabitem{\textbf{GameMove root}: First move of the match (normally a - symbolic move representing the empty board before the actual first move of a - player).} \\ + \tabitem{\textbf{Player[][] board}: An array of the stones on the board.} \\ \midrule \textbf{Proposed methods} \\ + \tabitem{\textbf{getGroupLiberties()}: Returns a set with the empty vertices + adjacent to the group occupying a vertex.} \\ + \tabitem{\textbf{getGroupLibertiesCount()}: Returns the number of liberties + of the group occupying a vertex.} \\ + \tabitem{\textbf{getGroupVertices()}: Returns a set with the vertices of the + group occupying a vertex.} \\ + \tabitem{\textbf{getGroupVerticesCount()}: Returns the number of stones of + the group occupying a vertex.} \\ \bottomrule \end{tabular} @@ -379,3 +440,118 @@ The classes resulting from the analysis phase are shown in \end{tabular} \vspace{\interclassSpace} + +\subsection{Use case analysis and scenarios} + +\indent + +\begin{tabular}{lp{0.7\linewidth}} + \toprule + \multicolumn{2}{c}{\textbf{Play a match (Make a move?)}} \\ + \midrule + \textbf{Preconditions} & The game interface has been started. \\ + \midrule + \textbf{Postconditions} & Description of postconditions \\ + \midrule + \textbf{Actors} & Actors \\ + \midrule + \textbf{Description} & Description \\ + \midrule + \textbf{Secondary scenarios} & Secondary scenarios \\ + \midrule + \textbf{Exceptions} & Exceptions \\ + \midrule + \textbf{Notes} & + ---\\ + \bottomrule +\end{tabular} + +\vspace{\interclassSpace} + +\begin{tabular}{lp{0.7\linewidth}} + \toprule + \multicolumn{2}{c}{\textbf{Generate a move}} \\ + \midrule + \textbf{Preconditions} & The game engine has been started. \newline + Optionally, some moves have already been played. \\ + \midrule + \textbf{Postconditions} & A move is suggested via the engine output. \\ + \midrule + \textbf{Actors} & Human user and GUI program. \\ + \midrule + \textbf{Description} & + 1. The user or program enters the player to generate the move + for.\newline + 2. The suggested move is outputted by the engine, either as + coordinates or as an indication to pass. + \\ + \midrule + \textbf{Secondary scenarios} & + \textbf{The move is illegal}: An error message is shown. Go back to step 1 of + main scenario. \\ + \midrule + \textbf{Exceptions} & + \textbf{The input is wrong}: An error message is shown. Go back to step 1 of + main scenario.\\ + \midrule + \textbf{Notes} & + ---\\ + \bottomrule +\end{tabular} + +\vspace{\interclassSpace} + +\subsubsection{Use as backend for machine player} + +\begin{figure}[h] + \begin{center} + \includegraphics[width=\textwidth]{diagrams/useCase_useAsBackend.png} + \caption{Use as backend for machine player} + \end{center} +\end{figure} + +\begin{tabular}{lp{0.7\linewidth}} + \toprule + \multicolumn{2}{c}{\textbf{Use as backend for machine player}} \\ + \midrule + \textbf{Preconditions} & The game engine has been configured as engine for + the software. \\ + \midrule + \textbf{Postconditions} & A match has been played against the engine. \\ + \midrule + \textbf{Actors} & GUI program. \\ + \midrule + \textbf{Description} & + 1. The program gives commands to the engine. The specific commands will + vary from program to program.\newline + 2. The engine suggest moves to the program.\newline + 3. The moves are shown by the program as if made by another player.\\ + \midrule + \textbf{Secondary scenarios} & + ---\\ + \midrule + \textbf{Exceptions} & + ---\\ + \midrule + \textbf{Notes} & + ---\\ + \bottomrule +\end{tabular} + +\subsection{Testing Plan Specification} + +\subsubsection{Unitary Testing} + +Tests for the python code are developed using the unittest\cite{python_unittest} +testing framework. It has been chosen by virtue of being thoroughly documented +and widely used. + +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. + +% Maybe put an example report here? + +\subsubsection{Integration Testing} + +\subsubsection{System Testing} diff --git a/doc/tex/systemDesign.tex b/doc/tex/systemDesign.tex new file mode 100644 index 0000000..5988ae0 --- /dev/null +++ b/doc/tex/systemDesign.tex @@ -0,0 +1,2 @@ +\section{System Design} + diff --git a/doc/tex/tfg.tex b/doc/tex/tfg.tex index 7296cb9..6e2c472 100644 --- a/doc/tex/tfg.tex +++ b/doc/tex/tfg.tex @@ -10,7 +10,7 @@ \usepackage{chngcntr} \counterwithin{figure}{section} -\usepackage[backend=biber, style=numeric-comp]{biblatex} +\usepackage[backend=biber, style=numeric, sorting=none]{biblatex} \addbibresource{tex/biber.bib} \geometry{left=4.5cm,top=2cm,bottom=2cm,right=4.5cm} @@ -54,9 +54,8 @@ TODO: Acknowledgements To Vicente García Díaz, for directing me in this project. -To José Manuel Redondo López\cite{plantillaRedondo}, for making a very -complete template on which the structure of this documentation is extensively -based. +To José Manuel Redondo López\cite{plantillaRedondo}, for making an extensive +template on which the structure of this documentation is extensively based. \clearpage @@ -69,7 +68,7 @@ based. \textit{Permission is granted to copy, distribute and/or modify this document under the terms of the GNU Free Documentation License, Version 1.3 or any later version published by the Free Software Foundation; with the - Copyright notice as an Invariant Section, no Front- Cover Texts, and no + Copyright notice as an Invariant Section, no Front-Cover Texts, and no Back-Cover Texts. A copy of the license is included in the section entitled ``GNU Free Documentation License''.} @@ -91,7 +90,7 @@ inclusion to use this template is included here. \textit{Permission is granted to copy, distribute and/or modify this document under the terms of the GNU Free Documentation License, Version 1.3 or any later version published by the Free Software Foundation; with no - Invariant Sections, no Front- Cover Texts, and no Back-Cover Texts. A copy + Invariant Sections, no Front-Cover Texts, and no Back-Cover Texts. A copy of the license is included in the section entitled ``GNU Free Documentation License''.} @@ -107,10 +106,19 @@ inclusion to use this template is included here. \input{tex/systemAnalysis.tex} +\input{tex/systemDesign.tex} + %\input{tex/interface.tex} %\input{tex/implementation.tex} -\printbibliography{} +\clearpage + +% References (bibliography) + +\setcounter{secnumdepth}{0} +\section{References} +\printbibliography[type=article,title={Cited articles},heading=subbibintoc]{} +\printbibliography[type=online,title={Online resources},heading=subbibintoc]{} \end{document} diff --git a/imago/engine/monteCarlo.py b/imago/engine/monteCarlo.py index 4540cb8..5c916b0 100644 --- a/imago/engine/monteCarlo.py +++ b/imago/engine/monteCarlo.py @@ -74,9 +74,13 @@ class MCTSNode: """ Returns Upper Confidence Bound of node changing the symbol if the move is for the wite player.""" + return self.ucbForSpecificPlayer(self.move.getPlayer()) - # Account for white player score being negative - if self.move.getPlayer() == Player.WHITE: + def ucbForSpecificPlayer(self, player): + """ + Returns Upper Confidence Bound of node from the perspective of the given + player.""" + if player == Player.WHITE: return self.ucb() * -1 return self.ucb() @@ -86,17 +90,31 @@ class MCTSNode: return bestNode def selectionRec(self, bestNode): - """Searches this node and its children for the node with the best UCB value.""" + """Searches this node and its children for the node with the best UCB value for + the current player.""" + + #TODO: En cada nivel debe escoger como mejor el que sea mejor para quien le toque + # jugar, ya que esa es la partida esperada. Esto es lo que hacía con + # ucbForPlayer() + + # ¿Cada nuevo nodo expandido tiene una puntuación inicial? ¿Se le hacen + # exploraciones? + + # Backpropagation: ¿Es correcto acumular la puntuación de cada nodo hacia + # arriba? ¿Sería mejor acumular hacia arriba el valor del mejor nodo? ¿O del + # juego perfecto? Estos dos últimos valores no son el mismo. + + player = self.move.getPlayer() # Check if node has unexplored children and better UCB than previously explored if len(self.unexploredVertices) > 0: - if self.ucbForPlayer() > bestNode.ucbForPlayer(): + if self.ucbForSpecificPlayer(player) > bestNode.ucbForSpecificPlayer(player): bestNode = self # Recursively search children for better UCB for child in self.children: bestChildNode = child.selectionRec(bestNode) - if bestChildNode.ucbForPlayer() > bestNode.ucbForPlayer(): + if bestChildNode.ucbForSpecificPlayer(player) > bestNode.ucbForSpecificPlayer(player): bestNode = bestChildNode return bestNode diff --git a/imago/gameLogic/gameBoard.py b/imago/gameLogic/gameBoard.py index b22dfd7..d6114b9 100644 --- a/imago/gameLogic/gameBoard.py +++ b/imago/gameLogic/gameBoard.py @@ -81,7 +81,7 @@ class GameBoard: self.__exploreLiberties(rowToExplore, colToExplore, groupColor, emptyCells, exploredCells) - def getGroupCells(self, row, col): + def getGroupVertices(self, row, col): """ Returns a set containing the cells occupied by the group in the given cell. This is also valid if the cell is empty.""" @@ -90,9 +90,9 @@ class GameBoard: self.__exploreGroup(row, col, groupColor, cells) return cells - def getGroupCellsCount(self, row, col): + def getGroupVerticesCount(self, row, col): """Returns the number of cells of a group.""" - return len(self.getGroupCells(row, col)) + return len(self.getGroupVertices(row, col)) def __exploreGroup(self, row, col, groupColor, cells): if self.board[row][col] != groupColor or (row, col) in cells: @@ -133,7 +133,7 @@ class GameBoard: """Removes all the stones from the group occupying the given cell and returns a set containing them. """ - cellsToCapture = self.getGroupCells(row, col) + cellsToCapture = self.getGroupVertices(row, col) for cell in cellsToCapture: self.board[cell[0]][cell[1]] = Player.EMPTY return cellsToCapture @@ -153,7 +153,7 @@ class GameBoard: # if isCellEmpty && all adjacent to group are same color if not self.isCellEmpty(row, col): return Player.EMPTY - groupCells = self.getGroupCells(row, col) + groupCells = self.getGroupVertices(row, col) surroundingColor = Player.EMPTY # Check surrounding cells of each cell in the group for cell in groupCells: @@ -235,7 +235,7 @@ class GameBoard: playable, playableText = self.isPlayable(row, col, player, prevBoards) if not playable: return playable, playableText - if ( self.getGroupCellsCount(row, col) == 1 + if ( self.getGroupVerticesCount(row, col) == 1 and self.isCellEye(row, col) == player ): return False, "Move fills own eye.""" return True, "" @@ -252,7 +252,7 @@ class GameBoard: for row in range(0, self.getBoardHeight()): for col in range(0, self.getBoardWidth()): if not (row, col) in checkedVertices: - group = self.getGroupCells(row, col) + group = self.getGroupVertices(row, col) for cell in group: checkedVertices.add(cell) surroundingColor = self.isCellEye(row, col) diff --git a/imago/gameLogic/gameState.py b/imago/gameLogic/gameState.py index 9aafd11..5232a43 100644 --- a/imago/gameLogic/gameState.py +++ b/imago/gameLogic/gameState.py @@ -1,7 +1,6 @@ """Storing state of the game.""" from imago.data.enums import Player -from imago.gameLogic.gameTree import GameTree from imago.gameLogic.gameMove import GameMove from imago.gameLogic.gameBoard import GameBoard, cellToString @@ -10,6 +9,7 @@ class GameState: def __init__(self, size): self.size = size + self.lastMove = None self.clearBoard() def getCurrentPlayer(self): @@ -26,16 +26,12 @@ class GameState: def getBoard(self): """Returns the board as of the last move.""" - if self.lastMove is None: - return GameBoard(self.size, self.size) return self.lastMove.board def clearBoard(self): """Empties the board and moves tree.""" - self.gameTree = GameTree() newBoard = GameBoard(self.size, self.size) self.lastMove = GameMove(newBoard) - self.gameTree.firstMoves.append(self.lastMove) def playMove(self, row, col): """Execute a move on the board for the current player and switches players.""" diff --git a/imago/gameLogic/gameTree.py b/imago/gameLogic/gameTree.py index 4b88c4d..5955252 100644 --- a/imago/gameLogic/gameTree.py +++ b/imago/gameLogic/gameTree.py @@ -2,7 +2,7 @@ # This class will not be necessary if it just keeps being a list of moves -class GameTree: - - def __init__(self): - self.firstMoves = [] +#class GameTree: +# +# def __init__(self): +# self.firstMoves = [] diff --git a/tests/test_monteCarlo.py b/tests/test_monteCarlo.py index 283c657..b217cf9 100644 --- a/tests/test_monteCarlo.py +++ b/tests/test_monteCarlo.py @@ -14,8 +14,8 @@ class TestMonteCarlo(unittest.TestCase): def testPickMove(self): """Test picking a move.""" state = GameState(TEST_BOARD_SIZE) - tree = MCTS(state) - print(tree.pickMove().toString()) + tree = MCTS(state.lastMove) + #print(tree.pickMove().toString()) #def testSimulation(self): # """Test calculation of group liberties.""" diff --git a/tests/test_parseHelpers.py b/tests/test_parseHelpers.py index cf1fa9f..d03f253 100644 --- a/tests/test_parseHelpers.py +++ b/tests/test_parseHelpers.py @@ -20,6 +20,7 @@ class TestParseHelpers(unittest.TestCase): self.assertEqual(parseHelpers.parseMove(["B", "A1", "W"], TEST_BOARD_SIZE), parseHelpers.ParseCodes.ERROR) + parsedMove = parseHelpers.parseMove(["B", "t1"], TEST_BOARD_SIZE) targetMove = parseHelpers.GtpMove(Player.BLACK, [18, 18]) @@ -67,6 +68,10 @@ class TestParseHelpers(unittest.TestCase): "A19", TEST_BOARD_SIZE), [0,0]) + self.assertEqual(parseHelpers.parseVertex( + "pass", TEST_BOARD_SIZE), + "pass") + def testVertexToString(self): """Test converting vertices to strings.""" self.assertEqual(parseHelpers.vertexToString([0,0], TEST_BOARD_SIZE), "A19") @@ -78,6 +83,8 @@ class TestParseHelpers(unittest.TestCase): self.assertEqual(parseHelpers.vertexToString([18,0], TEST_BOARD_SIZE), "A1") self.assertEqual(parseHelpers.vertexToString([18,18], TEST_BOARD_SIZE), "T1") + self.assertEqual(parseHelpers.vertexToString("pass", TEST_BOARD_SIZE), "pass") + self.assertEqual(parseHelpers.vertexToString([-1,0], TEST_BOARD_SIZE), parseHelpers.ParseCodes.ERROR) self.assertEqual(parseHelpers.vertexToString([0,-1], TEST_BOARD_SIZE), -- cgit v1.2.1 From 157303459b32de07f6989ba9131a38fc113754de Mon Sep 17 00:00:00 2001 From: InigoGutierrez Date: Fri, 10 Jun 2022 21:31:47 +0200 Subject: Structuring the different decision algorithms. --- doc/diagrams/DecisionAlgorithm.pumlc | 8 +++++ doc/diagrams/Keras.pumlc | 9 ++++++ doc/diagrams/NeuralNetwork.pumlc | 8 +++++ doc/diagrams/analysisClasses.puml | 20 ++++++------ doc/diagrams/keras.puml | 13 ++++++++ doc/diagrams/skinparams.puml | 3 ++ imago/engine/core.py | 17 ++++++----- imago/engine/decisionAlgorithm.py | 14 +++++++++ imago/engine/decisionAlgorithmFactory.py | 23 ++++++++++++++ imago/engine/keras.py | 16 ++++++++++ imago/engine/keras/neuralNetwork.py | 52 ++++++++++++++++++++++++++++++++ imago/engine/monteCarlo.py | 11 ++++--- imago/scripts/__init__.py | 0 imago/scripts/monteCarloSimulation.py | 25 +++++++++++++++ imago/scripts/monteCarloSimulation.sh | 24 +++++++++++++++ results.txt | 41 +++++++++++++++++++++++++ simulationMatchesPyplot.py | 27 +++++++++++++++++ 17 files changed, 290 insertions(+), 21 deletions(-) create mode 100644 doc/diagrams/DecisionAlgorithm.pumlc create mode 100644 doc/diagrams/Keras.pumlc create mode 100644 doc/diagrams/NeuralNetwork.pumlc create mode 100644 doc/diagrams/keras.puml create mode 100644 imago/engine/decisionAlgorithm.py create mode 100644 imago/engine/decisionAlgorithmFactory.py create mode 100644 imago/engine/keras.py create mode 100644 imago/engine/keras/neuralNetwork.py create mode 100644 imago/scripts/__init__.py create mode 100644 imago/scripts/monteCarloSimulation.py create mode 100755 imago/scripts/monteCarloSimulation.sh create mode 100644 results.txt create mode 100755 simulationMatchesPyplot.py diff --git a/doc/diagrams/DecisionAlgorithm.pumlc b/doc/diagrams/DecisionAlgorithm.pumlc new file mode 100644 index 0000000..aada4f0 --- /dev/null +++ b/doc/diagrams/DecisionAlgorithm.pumlc @@ -0,0 +1,8 @@ +@startuml + +interface DecisionAlgorithm { + {abstract} forceNextMove(self, coords) + {abstract} pickMove(self) +} + +@enduml diff --git a/doc/diagrams/Keras.pumlc b/doc/diagrams/Keras.pumlc new file mode 100644 index 0000000..daca149 --- /dev/null +++ b/doc/diagrams/Keras.pumlc @@ -0,0 +1,9 @@ +@startuml + +class Keras { + forceNextMove(self, coords) + pickMove(self) + loadNetwork(self) +} + +@enduml diff --git a/doc/diagrams/NeuralNetwork.pumlc b/doc/diagrams/NeuralNetwork.pumlc new file mode 100644 index 0000000..30f783e --- /dev/null +++ b/doc/diagrams/NeuralNetwork.pumlc @@ -0,0 +1,8 @@ +@startuml + +class NeuralNetwork { + load(self, path) + save(self, path) +} + +@enduml diff --git a/doc/diagrams/analysisClasses.puml b/doc/diagrams/analysisClasses.puml index 4a286f7..d051cf9 100644 --- a/doc/diagrams/analysisClasses.puml +++ b/doc/diagrams/analysisClasses.puml @@ -7,24 +7,24 @@ package GameModule { class GameState class GameBoard class GameMove -} -GameIO -> GameState -GameState -> GameMove -GameMove -> GameBoard + GameIO -> GameState + GameState -> GameMove + GameMove -> GameBoard +} package EngineModule { class EngineIO class EngineLogic - interface DecisionAlgorithm + !include DecisionAlgorithm.pumlc class MonteCarloTreeSearch class OtherDecisionAlgorithm -} -EngineIO --> EngineLogic -EngineLogic -> DecisionAlgorithm -DecisionAlgorithm <|.. MonteCarloTreeSearch -DecisionAlgorithm <|.. OtherDecisionAlgorithm + EngineIO --> EngineLogic + EngineLogic -> DecisionAlgorithm + DecisionAlgorithm <|.. MonteCarloTreeSearch + DecisionAlgorithm <|.. OtherDecisionAlgorithm +} MonteCarloTreeSearch --> GameMove OtherDecisionAlgorithm --> GameMove diff --git a/doc/diagrams/keras.puml b/doc/diagrams/keras.puml new file mode 100644 index 0000000..98776d1 --- /dev/null +++ b/doc/diagrams/keras.puml @@ -0,0 +1,13 @@ +@startuml + +!include skinparams.puml + +!include DecisionAlgorithm.pumlc +!include Keras.pumlc +!include NeuralNetwork.pumlc + +DecisionAlgorithm <|.. Keras + +Keras -> NeuralNetwork + +@enduml diff --git a/doc/diagrams/skinparams.puml b/doc/diagrams/skinparams.puml index cde3da7..2a9e58e 100644 --- a/doc/diagrams/skinparams.puml +++ b/doc/diagrams/skinparams.puml @@ -1,5 +1,8 @@ @startuml +'Old style +skin rose + skinparam { monochrome true shadowing false diff --git a/imago/engine/core.py b/imago/engine/core.py index 44da3b8..09ee51d 100644 --- a/imago/engine/core.py +++ b/imago/engine/core.py @@ -1,18 +1,21 @@ """Imago GTP engine""" -from imago.engine.monteCarlo import MCTS +from imago.engine.decisionAlgorithmFactory import DecisionAlgorithms, DecisionAlgorithmFactory from imago.gameLogic.gameState import GameState DEF_SIZE = 7 DEF_KOMI = 5.5 +DEF_ALGORITHM = DecisionAlgorithms.MONTECARLO class GameEngine: """Plays the game of Go.""" - def __init__(self): + def __init__(self, decisionAlgorithmId = DEF_ALGORITHM): self.komi = DEF_KOMI self.gameState = GameState(DEF_SIZE) - self.mcts = MCTS(self.gameState.lastMove) + self.daId = decisionAlgorithmId + self.daFactory = DecisionAlgorithmFactory() + self.da = self.daFactory.create(self.daId, self.gameState.lastMove) def setBoardsize(self, newSize): """Changes the size of the board. @@ -20,14 +23,14 @@ class GameEngine: It is wise to call clear_board after this command. """ self.gameState = GameState(newSize) - self.mcts = MCTS(self.gameState.lastMove) + self.da = self.da.__init__(self.gameState.lastMove) def clearBoard(self): """The board is cleared, the number of captured stones reset to zero and the move history reset to empty. """ self.gameState.clearBoard() - self.mcts.clearBoard() + self.da.clearBoard() def setKomi(self, komi): """Sets a new value of komi.""" @@ -48,11 +51,11 @@ class GameEngine: row = vertex[0] col = vertex[1] self.gameState.playMoveForPlayer(row, col, color) - self.mcts.forceNextMove(vertex) + self.da.forceNextMove(vertex) def genmove(self, color): """Returns a list representing coordinates of the board in the form (row, col).""" - coords = self.mcts.pickMove().coords + coords = self.da.pickMove().coords #TODO: The move should NOT be played in its generation. This method is just for #suggesting a move. #self.gameState.playMoveForPlayer(coords[0], coords[1], color) diff --git a/imago/engine/decisionAlgorithm.py b/imago/engine/decisionAlgorithm.py new file mode 100644 index 0000000..67fc3ef --- /dev/null +++ b/imago/engine/decisionAlgorithm.py @@ -0,0 +1,14 @@ +"""Decision algorithm interface.""" + +class DecisionAlgorithm: + + def __init__(self, move): + pass + + def forceNextMove(self, coords): + """Selects given move as next move.""" + pass + + def pickMove(self): + """Returns a move to play.""" + pass diff --git a/imago/engine/decisionAlgorithmFactory.py b/imago/engine/decisionAlgorithmFactory.py new file mode 100644 index 0000000..c40c687 --- /dev/null +++ b/imago/engine/decisionAlgorithmFactory.py @@ -0,0 +1,23 @@ +"""Decision algorithm factory.""" + +from enum import Enum, auto as enumAuto +from imago.engine.monteCarlo import MCTS + +class DecisionAlgorithms(Enum): + RANDOM = enumAuto() + MONTECARLO = enumAuto() + KERAS = enumAuto() + +class DecisionAlgorithmFactory: + + def create(self, algorithm, move): + """Creates an instance of the requested algorithm.""" + +# if algorithm == DecisionAlgorithms.RANDOM: +# -- + + if algorithm == DecisionAlgorithms.MONTECARLO: + return MCTS(move) + +# if algorithm == DecisionAlgorithms.KERAS: +# -- diff --git a/imago/engine/keras.py b/imago/engine/keras.py new file mode 100644 index 0000000..5445d92 --- /dev/null +++ b/imago/engine/keras.py @@ -0,0 +1,16 @@ +"""Keras neural network module.""" + +from imago.engine.decisionAlgorithm import DecisionAlgorithm + +class Keras(DecisionAlgorithm): + + def __init__(self, move): + pass + + def forceNextMove(self, coords): + """Selects given move as next move.""" + pass + + def pickMove(self): + """Returns a move to play.""" + pass diff --git a/imago/engine/keras/neuralNetwork.py b/imago/engine/keras/neuralNetwork.py new file mode 100644 index 0000000..7a712f2 --- /dev/null +++ b/imago/engine/keras/neuralNetwork.py @@ -0,0 +1,52 @@ +"""Keras neural network.""" + +import os.path + +import tensorflow as tf +from tensorflow import keras +from tensorflow.keras.models import Sequential +from tensorflow.keras.layers import Activation, Dense +from tensorflow.keras.optimizers import Adam +from tensorflow.keras.metrics import categorical_crossentropy + +modelFile = 'models/medical_trial_model.h5' + +class NeuralNetwork: + def __init__(self, boardSize): + self.path = modelFile + + def _initModel(self, modelPath, boardSize=9): + # Save full model + model = Sequential([ + Dense(units=16, activation='relu', input_shape=(boardSize,boardSize)), + Dense(units=32, activation='relu'), + Dense(units=2, activation='softmax') + ]) + + model.summary() + + model.compile( + optimizer=Adam(learning_rate=0.0001), + loss='sparse_categorical_crossentropy', + metrics=['accuracy'] + ) + + model.fit( + x=scaled_train_samples, + y=train_labels, + validation_split=0.1, + batch_size=10, + epochs=30, + shuffle=True, + verbose=2 + ) + + model.save(modelPath) + + def _loadModel(self, modelPath): + # Load model + if os.path.isfile(modelPath): + from tensorflow.keras.models import load_model + model = load_model(modelPath) + else: + raise Exception("Keras neural network file not found at %s" % modelPath) diff --git a/imago/engine/monteCarlo.py b/imago/engine/monteCarlo.py index 5c916b0..3f28d8d 100644 --- a/imago/engine/monteCarlo.py +++ b/imago/engine/monteCarlo.py @@ -4,8 +4,9 @@ import sys import random from imago.data.enums import Player +from imago.engine.decisionAlgorithm import DecisionAlgorithm -class MCTS: +class MCTS(DecisionAlgorithm): """Monte Carlo tree.""" def __init__(self, move): @@ -15,7 +16,7 @@ class MCTS: """Selects given move as next move.""" for node in self.root.children: if (node.move.getRow() == coords[0] - and node.move.getCol() == coords[1]): + and node.move.getCol() == coords[1]): self.root = node return self.root = self.root.expansionForCoords(coords) @@ -108,13 +109,15 @@ class MCTSNode: # Check if node has unexplored children and better UCB than previously explored if len(self.unexploredVertices) > 0: - if self.ucbForSpecificPlayer(player) > bestNode.ucbForSpecificPlayer(player): + if self.ucbForSpecificPlayer( + player) > bestNode.ucbForSpecificPlayer(player): bestNode = self # Recursively search children for better UCB for child in self.children: bestChildNode = child.selectionRec(bestNode) - if bestChildNode.ucbForSpecificPlayer(player) > bestNode.ucbForSpecificPlayer(player): + if bestChildNode.ucbForSpecificPlayer( + player) > bestNode.ucbForSpecificPlayer(player): bestNode = bestChildNode return bestNode diff --git a/imago/scripts/__init__.py b/imago/scripts/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/imago/scripts/monteCarloSimulation.py b/imago/scripts/monteCarloSimulation.py new file mode 100644 index 0000000..395a87e --- /dev/null +++ b/imago/scripts/monteCarloSimulation.py @@ -0,0 +1,25 @@ +#!/usr/bin/python + +"""Runs MonteCarlo simulations.""" + +import sys + +from imago.gameLogic.gameBoard import GameBoard +from imago.gameLogic.gameMove import GameMove +from imago.engine.monteCarlo import MCTSNode + +if __name__ == "__main__": + + if len(sys.argv) != 4: + print("[ERROR] Usage: monteCarloSimulation.py ") + sys.exit(1) + + size = int(sys.argv[1]) + scoreDiffHeur = int(sys.argv[2]) + nMatches = int(sys.argv[3]) + + board = GameBoard(size, size) + move = GameMove(board) + node = MCTSNode(move, None) + node.simulation(nMatches, scoreDiffHeur) + print("Score: %d" % node.score) diff --git a/imago/scripts/monteCarloSimulation.sh b/imago/scripts/monteCarloSimulation.sh new file mode 100755 index 0000000..8aa9e68 --- /dev/null +++ b/imago/scripts/monteCarloSimulation.sh @@ -0,0 +1,24 @@ +#!/bin/sh + +# Stores statistical data about MonteCarlo simulations + +outputDir="out" + +sampleSize=50 +nMatches=10 + +mkdir -p "$outputDir" + +for size in 9 13 19; do + for scoreDiffHeur in 10 30 60; do + filename="size${size}_scoreDiffHeur${scoreDiffHeur}.dat" + logFile="size${size}_scoreDiffHeur${scoreDiffHeur}_fullOutput.log" + rm "$logFile" + for i in $(seq "$sampleSize"); do + python ./monteCarloSimulation.py "$size" "$scoreDiffHeur" "$nMatches" | + tee -a "${outputDir}/${logFile}" | + grep -E '^Score:' | + cut -d' ' -f2 + done >"${outputDir}/${filename}" + done +done diff --git a/results.txt b/results.txt new file mode 100644 index 0000000..aa1a8a3 --- /dev/null +++ b/results.txt @@ -0,0 +1,41 @@ +Visits: 10 +Score: 2 +Visits: 10 +Score: -4 +Visits: 10 +Score: 2 +Visits: 10 +Score: -2 +Visits: 10 +Score: -4 +Visits: 10 +Score: 0 +Visits: 10 +Score: 4 +Visits: 10 +Score: -2 +Visits: 10 +Score: 0 +Visits: 10 +Score: -4 +---------- +Visits: 50 +Score: -4 +Visits: 50 +Score: 0 +Visits: 50 +Score: -6 +Visits: 50 +Score: 0 +Visits: 50 +Score: -12 +Visits: 50 +Score: -6 +Visits: 50 +Score: 10 +Visits: 50 +Score: 0 +Visits: 50 +Score: 8 +Visits: 50 +Score: -2 diff --git a/simulationMatchesPyplot.py b/simulationMatchesPyplot.py new file mode 100755 index 0000000..809e1a1 --- /dev/null +++ b/simulationMatchesPyplot.py @@ -0,0 +1,27 @@ +#!/usr/bin/python + +"""Uses pyplot to create graphs of the results of MonteCarlo simulations""" + +import matplotlib.pyplot as plt +import numpy as np + +data = np.loadtxt("out/size9_scoreDiffHeur10.dat") + +dic = {} +for value in data: + if value in dic.keys(): + dic[value] += 1 + else: + dic[value] = 1 + +names = list(dic.keys()) +values = list(dic.values()) + +fig, axs = plt.subplots(1, 3, figsize=(9, 3), sharey=True) +axs[0].bar(names, values) +axs[1].scatter(names, values) +axs[2].plot(names, values) +axs[2].axis([-10, 10, 0, max(values)+2]) +fig.suptitle('Categorical Plotting') + +plt.show() -- cgit v1.2.1 From 6f4f8a2dd258355c870bc94d9ef9c13d5aaad894 Mon Sep 17 00:00:00 2001 From: InigoGutierrez Date: Sun, 12 Jun 2022 22:30:34 +0200 Subject: Debugging SGF module. --- imago/engine/keras/neuralNetwork.py | 27 +++++++++------ imago/sgfParser/astNode.py | 1 - imago/sgfParser/sgf.py | 11 +++--- imago/sgfParser/sgflex.py | 2 ++ imago/sgfParser/sgfyacc.py | 36 +++++++++++--------- sgfyacc.py | 67 +++++++++++++++++++++++++++++++++++++ 6 files changed, 112 insertions(+), 32 deletions(-) create mode 100755 sgfyacc.py diff --git a/imago/engine/keras/neuralNetwork.py b/imago/engine/keras/neuralNetwork.py index 7a712f2..d0b7798 100644 --- a/imago/engine/keras/neuralNetwork.py +++ b/imago/engine/keras/neuralNetwork.py @@ -4,19 +4,23 @@ import os.path import tensorflow as tf from tensorflow import keras -from tensorflow.keras.models import Sequential +from tensorflow.keras.models import Sequential, load_model from tensorflow.keras.layers import Activation, Dense from tensorflow.keras.optimizers import Adam from tensorflow.keras.metrics import categorical_crossentropy -modelFile = 'models/medical_trial_model.h5' +modelFile = 'models/imagoKerasModel.h5' +DEF_BOARD_SIZE = 9 class NeuralNetwork: - def __init__(self, boardSize): + def __init__(self, modelPath="", boardSize=DEF_BOARD_SIZE): self.path = modelFile + if modelPath != "": + self.model = self._loadModel(modelPath) + else: + self.model = self._initModel(boardSize) - def _initModel(self, modelPath, boardSize=9): - # Save full model + def _initModel(self, boardSize=DEF_BOARD_SIZE): model = Sequential([ Dense(units=16, activation='relu', input_shape=(boardSize,boardSize)), Dense(units=32, activation='relu'), @@ -31,7 +35,10 @@ class NeuralNetwork: metrics=['accuracy'] ) - model.fit( + return model + + def _trainModel(self, boardSize=DEF_BOARD_SIZE): + self.model.fit( x=scaled_train_samples, y=train_labels, validation_split=0.1, @@ -41,12 +48,12 @@ class NeuralNetwork: verbose=2 ) - model.save(modelPath) - def _loadModel(self, modelPath): # Load model if os.path.isfile(modelPath): - from tensorflow.keras.models import load_model - model = load_model(modelPath) + return load_model(modelPath) else: raise Exception("Keras neural network file not found at %s" % modelPath) + + def _saveModel(self, modelPath): + self.model.save(modelPath) diff --git a/imago/sgfParser/astNode.py b/imago/sgfParser/astNode.py index ec28c2a..e92223b 100644 --- a/imago/sgfParser/astNode.py +++ b/imago/sgfParser/astNode.py @@ -1,4 +1,3 @@ -from imago.gameLogic.gameTree import GameTree from imago.gameLogic.gameData import GameData from imago.gameLogic.gameMove import GameMove from imago.gameLogic.gameState import Player diff --git a/imago/sgfParser/sgf.py b/imago/sgfParser/sgf.py index b5c0be2..3263650 100644 --- a/imago/sgfParser/sgf.py +++ b/imago/sgfParser/sgf.py @@ -1,11 +1,10 @@ """Module for reading and writing of SGF files.""" -from imago.gameLogic.gameTree import GameTree from imago.gameLogic.gameMove import GameMove -# def loadGameTree(filename): +def loadGameTree(filename): # PLY? -# """Parses a GameTree instance from a source SGF file.""" -# sgf = open(filename, "r") -# -# a = GameTree() + """Parses a GameTree instance from a source SGF file.""" + sgf = open(filename, "r") + + a = GameTree() diff --git a/imago/sgfParser/sgflex.py b/imago/sgfParser/sgflex.py index 07fb404..e102447 100755 --- a/imago/sgfParser/sgflex.py +++ b/imago/sgfParser/sgflex.py @@ -53,6 +53,8 @@ def t_DOUBLE(t): def t_PROPVALUE(t): r'\[[^\]]*\]' + import pdb + pdb.set_trace() t.value = t.value.strip('[').strip(']') return t diff --git a/imago/sgfParser/sgfyacc.py b/imago/sgfParser/sgfyacc.py index bd00df6..e48e6b9 100755 --- a/imago/sgfParser/sgfyacc.py +++ b/imago/sgfParser/sgfyacc.py @@ -1,4 +1,4 @@ -#!/bin/python +#!/usr/bin/python # -------------------------------------- # sgyacc.py @@ -7,8 +7,8 @@ import ply.yacc as yacc -from sgflex import tokens -from astNode import ASTNode, Property +from imago.sgfParser.sgflex import tokens +from imago.sgfParser.astNode import ASTNode, Property def p_tree(p): '''tree : LPAREN node RPAREN @@ -47,15 +47,21 @@ def p_error(_): """Error rule for syntax errors""" print("Syntax error in input!") -# Build the parser -parser = yacc.yacc() - -while True: - try: - s = input('calc > ') - except EOFError: - break - if not s: - continue - result = parser.parse(s) - print(result.toString()) +def main(): + + # Build the parser + parser = yacc.yacc() + + s = "" + while True: + try: + s = input('calc > ') + except EOFError: + break + if not s: + continue + result = parser.parse(s) + print(result.toString()) + +if __name__ == '__main__': + main() diff --git a/sgfyacc.py b/sgfyacc.py new file mode 100755 index 0000000..e48e6b9 --- /dev/null +++ b/sgfyacc.py @@ -0,0 +1,67 @@ +#!/usr/bin/python + +# -------------------------------------- +# sgyacc.py +""" Parser for SGF """ +# -------------------------------------- + +import ply.yacc as yacc + +from imago.sgfParser.sgflex import tokens +from imago.sgfParser.astNode import ASTNode, Property + +def p_tree(p): + '''tree : LPAREN node RPAREN + | LPAREN tree RPAREN''' + p[0] = p[2] + +def p_node_sequence(p): + '''node : node node''' + p[1].addToSequence(p[2]) + p[0] = p[1] + +def p_node_tree(p): + '''node : node tree''' + p[1].children.append(p[2]) + p[0] = p[1] + +def p_node(p): + 'node : SCOLON' + p[0] = ASTNode() + +def p_node_prop(p): + 'node : node property' + p[1].props[p[2].name] = p[2].value + p[0] = p[1] + +def p_property(p): + 'property : PROPID PROPVALUE' + p[0] = Property(p[1], p[2]) + +def p_property_value(p): + 'property : property PROPVALUE' + p[1].addValue(p[2]) + p[0] = p[1] + +def p_error(_): + """Error rule for syntax errors""" + print("Syntax error in input!") + +def main(): + + # Build the parser + parser = yacc.yacc() + + s = "" + while True: + try: + s = input('calc > ') + except EOFError: + break + if not s: + continue + result = parser.parse(s) + print(result.toString()) + +if __name__ == '__main__': + main() -- cgit v1.2.1 From a44e15834f6891cdd850a1aaa60d0c43b88485ef Mon Sep 17 00:00:00 2001 From: InigoGutierrez Date: Mon, 13 Jun 2022 20:53:38 +0200 Subject: SGF parser working, although not perfectly. --- imago/sgfParser/astNode.py | 21 ++++++++------- imago/sgfParser/sgf.py | 3 ++- imago/sgfParser/sgflex.py | 24 +++++------------ imago/sgfParser/sgfyacc.py | 13 ++++----- sgfyacc.py | 67 ---------------------------------------------- 5 files changed, 27 insertions(+), 101 deletions(-) delete mode 100755 sgfyacc.py diff --git a/imago/sgfParser/astNode.py b/imago/sgfParser/astNode.py index e92223b..e2b74a2 100644 --- a/imago/sgfParser/astNode.py +++ b/imago/sgfParser/astNode.py @@ -97,21 +97,21 @@ class ASTNode: newMove.previousMove = gameMove return gameMove -def textToCoords(text): # Poner en PropertyMove, subclase de Property - col = ord(text[1]) - ord('a') - row = ord(text[0]) - ord('a') - return [row, col] - - - def toString(self): + def toString(self, debug=False): """Returns a depth-first representation of the tree.""" out = '(' + str(self.props) + ')' - out += "in" + if debug: out += "in" for node in self.children: out = out + node.toString() - out += "out" + if debug: out += "out" return out +def textToCoords(text): # Poner en PropertyMove, subclase de Property + col = ord(text[1]) - ord('a') + row = ord(text[0]) - ord('a') + return [row, col] + + class Property: """Property of a Node""" def __init__(self, name, value): @@ -124,3 +124,6 @@ class Property: self.value.append(value) else: self.value = [self.value, value] + + def toString(self): + return("Property(%s, %s)" % (self.name, self.value)) diff --git a/imago/sgfParser/sgf.py b/imago/sgfParser/sgf.py index 3263650..56135de 100644 --- a/imago/sgfParser/sgf.py +++ b/imago/sgfParser/sgf.py @@ -1,10 +1,11 @@ """Module for reading and writing of SGF files.""" from imago.gameLogic.gameMove import GameMove +from imago.sgfParser.sgfyacc import parser def loadGameTree(filename): # PLY? """Parses a GameTree instance from a source SGF file.""" sgf = open(filename, "r") - a = GameTree() + return parser.parse(sgf).toGameMoveTree() diff --git a/imago/sgfParser/sgflex.py b/imago/sgfParser/sgflex.py index e102447..c7dbf38 100755 --- a/imago/sgfParser/sgflex.py +++ b/imago/sgfParser/sgflex.py @@ -1,4 +1,4 @@ -#!/bin/python +#!/usr/bin/python # -------------------------------------- # sgflex.py @@ -12,8 +12,6 @@ tokens = ( 'LPAREN', 'RPAREN', 'SCOLON', - 'LSQBRACKET', - 'RSQBRACKET', 'PROPID', 'UCLETTER', 'DIGIT', @@ -29,8 +27,6 @@ tokens = ( t_LPAREN = r'\(' t_RPAREN = r'\)' t_SCOLON = r';' -t_LSQBRACKET = r'\[' -t_RSQBRACKET = r'\]' t_PROPID = r'[A-Z][A-Z]?' t_UCLETTER = r'[A-Z]' t_DIGIT = r'[0-9]' @@ -53,14 +49,12 @@ def t_DOUBLE(t): def t_PROPVALUE(t): r'\[[^\]]*\]' - import pdb - pdb.set_trace() t.value = t.value.strip('[').strip(']') return t # Rule to track line numbers def t_newline(t): - r'\n+' + r'\n' t.lexer.lineno += len(t.value) t_ignore = ' \t' @@ -70,14 +64,8 @@ def t_error(t): print("Illegal character '%s'" % t.value[0]) t.lexer.skip(1) -#data=''' -#B[aa ] -#W[AB]W[ab] -#B[bb] -#''' +lexer = lex.lex() # Used by the parser +#lexer = lex.lex(debug=True) -lexer = lex.lex() -#lexer.input(data) - -#for token in lexer: -# print(token) +if __name__ == '__main__': + lex.runmain() diff --git a/imago/sgfParser/sgfyacc.py b/imago/sgfParser/sgfyacc.py index e48e6b9..a07d179 100755 --- a/imago/sgfParser/sgfyacc.py +++ b/imago/sgfParser/sgfyacc.py @@ -43,14 +43,15 @@ def p_property_value(p): p[1].addValue(p[2]) p[0] = p[1] -def p_error(_): +def p_error(p): """Error rule for syntax errors""" - print("Syntax error in input!") + print("Syntax error in input - %s" % p) -def main(): +# Build the parser +parser = yacc.yacc() +#parser = yacc.yacc(start='property') - # Build the parser - parser = yacc.yacc() +def main(): s = "" while True: @@ -60,7 +61,7 @@ def main(): break if not s: continue - result = parser.parse(s) + result = parser.parse(s, debug=True) print(result.toString()) if __name__ == '__main__': diff --git a/sgfyacc.py b/sgfyacc.py deleted file mode 100755 index e48e6b9..0000000 --- a/sgfyacc.py +++ /dev/null @@ -1,67 +0,0 @@ -#!/usr/bin/python - -# -------------------------------------- -# sgyacc.py -""" Parser for SGF """ -# -------------------------------------- - -import ply.yacc as yacc - -from imago.sgfParser.sgflex import tokens -from imago.sgfParser.astNode import ASTNode, Property - -def p_tree(p): - '''tree : LPAREN node RPAREN - | LPAREN tree RPAREN''' - p[0] = p[2] - -def p_node_sequence(p): - '''node : node node''' - p[1].addToSequence(p[2]) - p[0] = p[1] - -def p_node_tree(p): - '''node : node tree''' - p[1].children.append(p[2]) - p[0] = p[1] - -def p_node(p): - 'node : SCOLON' - p[0] = ASTNode() - -def p_node_prop(p): - 'node : node property' - p[1].props[p[2].name] = p[2].value - p[0] = p[1] - -def p_property(p): - 'property : PROPID PROPVALUE' - p[0] = Property(p[1], p[2]) - -def p_property_value(p): - 'property : property PROPVALUE' - p[1].addValue(p[2]) - p[0] = p[1] - -def p_error(_): - """Error rule for syntax errors""" - print("Syntax error in input!") - -def main(): - - # Build the parser - parser = yacc.yacc() - - s = "" - while True: - try: - s = input('calc > ') - except EOFError: - break - if not s: - continue - result = parser.parse(s) - print(result.toString()) - -if __name__ == '__main__': - main() -- cgit v1.2.1 From faea06ac8d0dc92edb9ca0f2b6aacf79f640ace7 Mon Sep 17 00:00:00 2001 From: InigoGutierrez Date: Wed, 15 Jun 2022 20:58:07 +0200 Subject: SGF module working and mapping SGF files to GameMove tree. --- imago/sgfParser/astNode.py | 50 +++++++++++++++++++++++++++++----------------- imago/sgfParser/sgf.py | 9 ++++++--- imago/sgfParser/sgfyacc.py | 5 ++++- testSGF.py | 19 ++++++++++++++++++ 4 files changed, 61 insertions(+), 22 deletions(-) create mode 100755 testSGF.py diff --git a/imago/sgfParser/astNode.py b/imago/sgfParser/astNode.py index e2b74a2..f87d9c1 100644 --- a/imago/sgfParser/astNode.py +++ b/imago/sgfParser/astNode.py @@ -1,6 +1,6 @@ from imago.gameLogic.gameData import GameData from imago.gameLogic.gameMove import GameMove -from imago.gameLogic.gameState import Player +from imago.gameLogic.gameBoard import GameBoard class ASTNode: """Abstract Syntax Tree Node of SGF parser""" @@ -12,7 +12,7 @@ class ASTNode: if props: self.props = props else: - self.props = {} + self.props = [] self.leaf = leaf def addToSequence(self, move): @@ -76,27 +76,41 @@ class ASTNode: firstMoves = [] for child in self.children: - firstMoves.append(child.toGameMoveTree) + firstMoves.append(child.toGameMoveTree(size)) return GameTree(firstMoves, gameData) - def toGameMoveTree(self): - player = 0 - coords = [] - for prop in self.props: - if prop.name == "B": # White move - player = Player.BLACK - coords = textToCoords(prop.value) - if prop.name == "W": # White move - player = Player.WHITE - coords = textToCoords(prop.value) - gameMove = GameMove(player, coords[0], coords[1]) + def toGameMoveTree(self, previousMove=None): + coords = None + if previousMove is None: + # Game root node + size = int(self.getPropertyValue("SZ")) + board = GameBoard(size, size) + gameMove = GameMove(board) + else: + for prop in self.props: + if prop.name == "B" or prop.name == "W": + coords = textToCoords(prop.value) + gameMove = previousMove.addMoveByCoords(coords) for child in self.children: - newMove = child.toGameMoveTree() - gameMove.nextMoves.append(newMove) + newMove = child.toGameMoveTree(gameMove) newMove.previousMove = gameMove return gameMove + def hasProperty(self, propertyName): + """Returns True if the node contains a property with the given name.""" + for prop in self.props: + if prop.name == propertyName: + return True + return False + + def getPropertyValue(self, propertyName): + """Returns the value of the given property for this node.""" + for prop in self.props: + if prop.name == propertyName: + return prop.value + raise RuntimeError("ASTNode has no property %s" % propertyName) + def toString(self, debug=False): """Returns a depth-first representation of the tree.""" out = '(' + str(self.props) + ')' @@ -107,8 +121,8 @@ class ASTNode: return out def textToCoords(text): # Poner en PropertyMove, subclase de Property - col = ord(text[1]) - ord('a') - row = ord(text[0]) - ord('a') + col = ord(text[0]) - ord('a') + row = ord(text[1]) - ord('a') return [row, col] diff --git a/imago/sgfParser/sgf.py b/imago/sgfParser/sgf.py index 56135de..4b11cfb 100644 --- a/imago/sgfParser/sgf.py +++ b/imago/sgfParser/sgf.py @@ -1,11 +1,14 @@ """Module for reading and writing of SGF files.""" -from imago.gameLogic.gameMove import GameMove from imago.sgfParser.sgfyacc import parser def loadGameTree(filename): # PLY? """Parses a GameTree instance from a source SGF file.""" - sgf = open(filename, "r") + file = open(filename, "r") - return parser.parse(sgf).toGameMoveTree() + text = file.read() + + file.close() + + return parser.parse(text).toGameMoveTree() diff --git a/imago/sgfParser/sgfyacc.py b/imago/sgfParser/sgfyacc.py index a07d179..d18f2a8 100755 --- a/imago/sgfParser/sgfyacc.py +++ b/imago/sgfParser/sgfyacc.py @@ -31,7 +31,10 @@ def p_node(p): def p_node_prop(p): 'node : node property' - p[1].props[p[2].name] = p[2].value + if p[1].hasProperty(p[2].name): + print("Syntax error: node already contains a property named - %s" % p[2].name) + raise SyntaxError + p[1].props.append(p[2]) p[0] = p[1] def p_property(p): diff --git a/testSGF.py b/testSGF.py new file mode 100755 index 0000000..6291033 --- /dev/null +++ b/testSGF.py @@ -0,0 +1,19 @@ +#!/usr/bin/python + +import sys +from imago.sgfParser.sgf import loadGameTree + +"""Gets a game from an SGF file.""" + +def main(): + filename = sys.argv[1] + move = loadGameTree(filename) + while move is not None: + move.printBoard() + if len(move.nextMoves) > 0: + move = move.nextMoves[0] + else: + move = None + +if __name__ == '__main__': + main() -- cgit v1.2.1 From e9af1d809f25f6499a9aeb43264ce809118e63e8 Mon Sep 17 00:00:00 2001 From: InigoGutierrez Date: Sat, 18 Jun 2022 14:44:38 +0200 Subject: Testing working neural network. --- imago/engine/decisionAlgorithm.py | 4 +- imago/engine/keras/__init__.py | 0 imago/engine/keras/neuralNetwork.py | 83 +++++++++++++++++++++++++++++++------ imago/gameLogic/gameBoard.py | 2 +- imago/gameLogic/gameMove.py | 8 ++++ testKeras.py | 24 +++++++++++ testSGF.py | 5 ++- 7 files changed, 108 insertions(+), 18 deletions(-) create mode 100644 imago/engine/keras/__init__.py create mode 100755 testKeras.py diff --git a/imago/engine/decisionAlgorithm.py b/imago/engine/decisionAlgorithm.py index 67fc3ef..e3f4b15 100644 --- a/imago/engine/decisionAlgorithm.py +++ b/imago/engine/decisionAlgorithm.py @@ -7,8 +7,8 @@ class DecisionAlgorithm: def forceNextMove(self, coords): """Selects given move as next move.""" - pass + raise NotImplementedError("Method forceNextMove not implemented on type %s" % type(self)) def pickMove(self): """Returns a move to play.""" - pass + raise NotImplementedError("Method pickMove not implemented on type %s" % type(self)) diff --git a/imago/engine/keras/__init__.py b/imago/engine/keras/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/imago/engine/keras/neuralNetwork.py b/imago/engine/keras/neuralNetwork.py index d0b7798..26cb19e 100644 --- a/imago/engine/keras/neuralNetwork.py +++ b/imago/engine/keras/neuralNetwork.py @@ -1,50 +1,67 @@ """Keras neural network.""" import os.path +import numpy +from copy import deepcopy import tensorflow as tf from tensorflow import keras from tensorflow.keras.models import Sequential, load_model -from tensorflow.keras.layers import Activation, Dense +from tensorflow.keras.layers import Dense, Flatten from tensorflow.keras.optimizers import Adam from tensorflow.keras.metrics import categorical_crossentropy -modelFile = 'models/imagoKerasModel.h5' +from imago.data.enums import Player + +defaultModelFile = 'models/imagoKerasModel.h5' DEF_BOARD_SIZE = 9 +PLAYER_ID = 1 +OPPONENT_ID = -1 class NeuralNetwork: def __init__(self, modelPath="", boardSize=DEF_BOARD_SIZE): - self.path = modelFile + self.path = defaultModelFile if modelPath != "": + self.path = modelPath + try: self.model = self._loadModel(modelPath) - else: + except FileNotFoundError: self.model = self._initModel(boardSize) def _initModel(self, boardSize=DEF_BOARD_SIZE): model = Sequential([ Dense(units=16, activation='relu', input_shape=(boardSize,boardSize)), Dense(units=32, activation='relu'), - Dense(units=2, activation='softmax') + Dense(units=boardSize, activation='softmax') ]) model.summary() model.compile( optimizer=Adam(learning_rate=0.0001), - loss='sparse_categorical_crossentropy', + loss='categorical_crossentropy', metrics=['accuracy'] ) return model - def _trainModel(self, boardSize=DEF_BOARD_SIZE): + def trainModel(self, games): + trainMoves = [] + targets = [] + for game in games: + for move in self._movesToTrainMoves(game): + trainMoves.append(move) + for target in self._movesToTargets(game): + targets.append(target) + trainMoves = numpy.array(trainMoves) + targets = numpy.array(targets) self.model.fit( - x=scaled_train_samples, - y=train_labels, + x=trainMoves, + y=targets, validation_split=0.1, batch_size=10, epochs=30, - shuffle=True, + shuffle=False, verbose=2 ) @@ -53,7 +70,47 @@ class NeuralNetwork: if os.path.isfile(modelPath): return load_model(modelPath) else: - raise Exception("Keras neural network file not found at %s" % modelPath) + raise FileNotFoundError("Keras neural network file not found at %s" % modelPath) + + def saveModel(self, modelPath=""): + if modelPath != "": + self.model.save(modelPath) + else: + self.model.save(self.path) + + def _movesToTrainMoves(self, moves): + trainMoves = [] + for move in moves: + trainMove = self._moveToTrainMove(move) + trainMoves.append(trainMove) + return trainMoves + + def _moveToTrainMove(self, move): + player = move.getPlayer() + return self._boardToPlayerContext(move.board.board, player) + + def _boardToPlayerContext(self, board, player): + """Converts to a board where PLAYER_ID marks the stones who made the move and + OPPONENT_ID marks the stones of the other player.""" + boardRows = len(board) + boardCols = len(board[0]) + contextBoard = numpy.zeros( (boardRows, boardCols), dtype = int) + for row in range(boardRows): + for col in range(boardCols): + if board[row][col] != Player.EMPTY: + if board[row][col] == player: + contextBoard[row][col] = PLAYER_ID + else: + contextBoard[row][col] = OPPONENT_ID + return contextBoard.tolist() - def _saveModel(self, modelPath): - self.model.save(modelPath) + def _movesToTargets(self, moves): + targets = [] + for move in moves: + target = numpy.zeros( + (move.board.getBoardHeight(), + move.board.getBoardWidth()), + dtype = float) + target[move.getRow()][move.getCol()] = 1 + targets.append(target.tolist()) + return targets diff --git a/imago/gameLogic/gameBoard.py b/imago/gameLogic/gameBoard.py index d6114b9..9054a18 100644 --- a/imago/gameLogic/gameBoard.py +++ b/imago/gameLogic/gameBoard.py @@ -112,7 +112,7 @@ class GameBoard: if (row < 0 or row >= self.getBoardHeight() or col < 0 or col >= self.getBoardWidth()): - print("[ERROR] Move and capture: out of bounds (%d, %d)" % (row, col)) + raise RuntimeError("[ERROR] Move and capture: out of bounds (%d, %d)" % (row, col)) self.board[row][col] = player diff --git a/imago/gameLogic/gameMove.py b/imago/gameLogic/gameMove.py index 6d2d464..cdd2fd6 100644 --- a/imago/gameLogic/gameMove.py +++ b/imago/gameLogic/gameMove.py @@ -124,6 +124,14 @@ class GameMove: self.nextMoves.append(newMove) return newMove + def getMainLineOfPlay(self, previousMoves=[]): + """Returns, recursively, this move and its first children, so a list of moves is + obtained. This typically represents the played sequence of a match.""" + previousMoves.append(self) + if len(self.nextMoves) == 0: + return previousMoves + return self.nextMoves[0].getMainLineOfPlay(previousMoves) + def toString(self): """Returns the coordinates of the move as a string.""" if self.isPass: diff --git a/testKeras.py b/testKeras.py new file mode 100755 index 0000000..3b0b3f5 --- /dev/null +++ b/testKeras.py @@ -0,0 +1,24 @@ +#!/usr/bin/python + +"""Starts training a keras neural network.""" + +import sys + +from imago.sgfParser.sgf import loadGameTree +from imago.engine.keras.neuralNetwork import NeuralNetwork + +def main(): + games = [] + for file in sys.argv[1:]: + print(file) + games.append(loadGameTree(file)) + + matches = [game.getMainLineOfPlay() for game in games] + + modelFile = "models/testModel.h5" + nn = NeuralNetwork(modelFile, 9) + nn.trainModel(matches) + nn.saveModel() + +if __name__ == '__main__': + main() diff --git a/testSGF.py b/testSGF.py index 6291033..08a1bee 100755 --- a/testSGF.py +++ b/testSGF.py @@ -1,9 +1,10 @@ #!/usr/bin/python +"""Gets a game from an SGF file.""" + import sys -from imago.sgfParser.sgf import loadGameTree -"""Gets a game from an SGF file.""" +from imago.sgfParser.sgf import loadGameTree def main(): filename = sys.argv[1] -- cgit v1.2.1 From 6527b7d98843a13bd5499ec9aaf9e9f409bf8d2f Mon Sep 17 00:00:00 2001 From: InigoGutierrez Date: Sat, 18 Jun 2022 16:22:06 +0200 Subject: Handling pass annotation in sgfParser.astNode --- imago/sgfParser/astNode.py | 21 +++++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/imago/sgfParser/astNode.py b/imago/sgfParser/astNode.py index f87d9c1..ff0c517 100644 --- a/imago/sgfParser/astNode.py +++ b/imago/sgfParser/astNode.py @@ -1,6 +1,7 @@ from imago.gameLogic.gameData import GameData from imago.gameLogic.gameMove import GameMove from imago.gameLogic.gameBoard import GameBoard +from imago.data.enums import Player class ASTNode: """Abstract Syntax Tree Node of SGF parser""" @@ -67,7 +68,7 @@ class ASTNode: gameData.roundInfo = prop.value if prop.name == "RU": # Rules used for the game gameData.rules = prop.value - if prop.name == "SO": # Source of the gamw + if prop.name == "SO": # Source of the game gameData.source = prop.source if prop.name == "TM": # Time limit in seconds gameData.timeInfo = prop.source @@ -81,20 +82,28 @@ class ASTNode: return GameTree(firstMoves, gameData) def toGameMoveTree(self, previousMove=None): - coords = None if previousMove is None: # Game root node size = int(self.getPropertyValue("SZ")) board = GameBoard(size, size) gameMove = GameMove(board) else: + coords = [] + player = None for prop in self.props: if prop.name == "B" or prop.name == "W": - coords = textToCoords(prop.value) - gameMove = previousMove.addMoveByCoords(coords) + if prop.value != "tt" and prop.value != "": + coords = textToCoords(prop.value) + if prop.name == "B": + player = Player.BLACK + else: + player = Player.WHITE + if len(coords) == 2: + gameMove = previousMove.addMoveForPlayer(coords[0], coords[1], player) + else: + gameMove = previousMove.addPassForPlayer(player) for child in self.children: - newMove = child.toGameMoveTree(gameMove) - newMove.previousMove = gameMove + child.toGameMoveTree(previousMove=gameMove) return gameMove def hasProperty(self, propertyName): -- cgit v1.2.1 From 8519597eb00f838ca7beca5324ab27435adb4d36 Mon Sep 17 00:00:00 2001 From: InigoGutierrez Date: Sun, 19 Jun 2022 19:46:22 +0200 Subject: Fixed keras algorithm playing on occupied vertices. --- imago/engine/core.py | 10 +++--- imago/engine/decisionAlgorithm.py | 4 +++ imago/engine/decisionAlgorithmFactory.py | 8 +++-- imago/engine/imagoIO.py | 4 +-- imago/engine/keras.py | 16 --------- imago/engine/keras/keras.py | 27 ++++++++++++++ imago/engine/keras/neuralNetwork.py | 60 ++++++++++++++++++++++++++----- imago/engine/monteCarlo.py | 6 ++-- imago/gameLogic/gameMove.py | 2 +- imagocli.py | 18 +++++++++- models/testModel.h5 | Bin 0 -> 43120 bytes 11 files changed, 116 insertions(+), 39 deletions(-) delete mode 100644 imago/engine/keras.py create mode 100644 imago/engine/keras/keras.py create mode 100644 models/testModel.h5 diff --git a/imago/engine/core.py b/imago/engine/core.py index 09ee51d..810b851 100644 --- a/imago/engine/core.py +++ b/imago/engine/core.py @@ -5,7 +5,7 @@ from imago.gameLogic.gameState import GameState DEF_SIZE = 7 DEF_KOMI = 5.5 -DEF_ALGORITHM = DecisionAlgorithms.MONTECARLO +DEF_ALGORITHM = DecisionAlgorithms.KERAS class GameEngine: """Plays the game of Go.""" @@ -23,7 +23,7 @@ class GameEngine: It is wise to call clear_board after this command. """ self.gameState = GameState(newSize) - self.da = self.da.__init__(self.gameState.lastMove) + self.da = self.daFactory.create(self.daId, self.gameState.lastMove) def clearBoard(self): """The board is cleared, the number of captured stones reset to zero and the move @@ -55,10 +55,8 @@ class GameEngine: def genmove(self, color): """Returns a list representing coordinates of the board in the form (row, col).""" - coords = self.da.pickMove().coords - #TODO: The move should NOT be played in its generation. This method is just for - #suggesting a move. - #self.gameState.playMoveForPlayer(coords[0], coords[1], color) + coords = self.da.pickMove() + self.play(color, [coords[0], coords[1]]) return coords def undo(self): diff --git a/imago/engine/decisionAlgorithm.py b/imago/engine/decisionAlgorithm.py index e3f4b15..5727d25 100644 --- a/imago/engine/decisionAlgorithm.py +++ b/imago/engine/decisionAlgorithm.py @@ -12,3 +12,7 @@ class DecisionAlgorithm: def pickMove(self): """Returns a move to play.""" raise NotImplementedError("Method pickMove not implemented on type %s" % type(self)) + + def clearBoard(self): + """Empties move history.""" + raise NotImplementedError("Method clearBoard not implemented on type %s" % type(self)) diff --git a/imago/engine/decisionAlgorithmFactory.py b/imago/engine/decisionAlgorithmFactory.py index c40c687..bd86864 100644 --- a/imago/engine/decisionAlgorithmFactory.py +++ b/imago/engine/decisionAlgorithmFactory.py @@ -2,6 +2,7 @@ from enum import Enum, auto as enumAuto from imago.engine.monteCarlo import MCTS +from imago.engine.keras.keras import Keras class DecisionAlgorithms(Enum): RANDOM = enumAuto() @@ -19,5 +20,8 @@ class DecisionAlgorithmFactory: if algorithm == DecisionAlgorithms.MONTECARLO: return MCTS(move) -# if algorithm == DecisionAlgorithms.KERAS: -# -- + if algorithm == DecisionAlgorithms.KERAS: + return Keras(move) + + else: + return MCTS(move) diff --git a/imago/engine/imagoIO.py b/imago/engine/imagoIO.py index 371c447..6ada674 100644 --- a/imago/engine/imagoIO.py +++ b/imago/engine/imagoIO.py @@ -30,7 +30,7 @@ def getCoordsText(row, col): class ImagoIO: """Recieves and handles commands.""" - def __init__(self): + def __init__(self, decisionAlgorithmId = None): self.commands_set = { protocol_version, name, @@ -47,7 +47,7 @@ class ImagoIO: self.genmove, self.undo } - self.gameEngine = GameEngine() + self.gameEngine = GameEngine(decisionAlgorithmId) def start(self): """Starts reading commands interactively.""" diff --git a/imago/engine/keras.py b/imago/engine/keras.py deleted file mode 100644 index 5445d92..0000000 --- a/imago/engine/keras.py +++ /dev/null @@ -1,16 +0,0 @@ -"""Keras neural network module.""" - -from imago.engine.decisionAlgorithm import DecisionAlgorithm - -class Keras(DecisionAlgorithm): - - def __init__(self, move): - pass - - def forceNextMove(self, coords): - """Selects given move as next move.""" - pass - - def pickMove(self): - """Returns a move to play.""" - pass diff --git a/imago/engine/keras/keras.py b/imago/engine/keras/keras.py new file mode 100644 index 0000000..a6aa913 --- /dev/null +++ b/imago/engine/keras/keras.py @@ -0,0 +1,27 @@ +"""Keras neural network module.""" + +from imago.gameLogic.gameMove import GameMove +from imago.gameLogic.gameBoard import GameBoard +from imago.engine.decisionAlgorithm import DecisionAlgorithm +from imago.engine.keras.neuralNetwork import NeuralNetwork + +MODEL_FILE = "models/testModel.h5" + +class Keras(DecisionAlgorithm): + + def __init__(self, move): + self.currentMove = move + self.boardSize = move.board.getBoardHeight() + self.nn = NeuralNetwork(MODEL_FILE, self.boardSize) + + def forceNextMove(self, coords): + """Selects given move as next move.""" + self.currentMove = self.currentMove.addMoveByCoords(coords) + + def pickMove(self): + """Returns a move to play.""" + return self.nn.pickMove(self.currentMove) + + def clearBoard(self): + """Empties move history.""" + self.currentMove = GameMove(GameBoard(self.boardSize, self.boardSize)) diff --git a/imago/engine/keras/neuralNetwork.py b/imago/engine/keras/neuralNetwork.py index 26cb19e..41b8b23 100644 --- a/imago/engine/keras/neuralNetwork.py +++ b/imago/engine/keras/neuralNetwork.py @@ -1,15 +1,14 @@ """Keras neural network.""" +import sys import os.path + import numpy -from copy import deepcopy +from matplotlib import pyplot -import tensorflow as tf -from tensorflow import keras from tensorflow.keras.models import Sequential, load_model -from tensorflow.keras.layers import Dense, Flatten +from tensorflow.keras.layers import Dense from tensorflow.keras.optimizers import Adam -from tensorflow.keras.metrics import categorical_crossentropy from imago.data.enums import Player @@ -20,6 +19,7 @@ OPPONENT_ID = -1 class NeuralNetwork: def __init__(self, modelPath="", boardSize=DEF_BOARD_SIZE): + self.boardSize = boardSize self.path = defaultModelFile if modelPath != "": self.path = modelPath @@ -59,8 +59,8 @@ class NeuralNetwork: x=trainMoves, y=targets, validation_split=0.1, - batch_size=10, - epochs=30, + batch_size=1, + epochs=20, shuffle=False, verbose=2 ) @@ -94,7 +94,7 @@ class NeuralNetwork: OPPONENT_ID marks the stones of the other player.""" boardRows = len(board) boardCols = len(board[0]) - contextBoard = numpy.zeros( (boardRows, boardCols), dtype = int) + contextBoard = numpy.zeros((boardRows, boardCols), dtype = int) for row in range(boardRows): for col in range(boardCols): if board[row][col] != Player.EMPTY: @@ -114,3 +114,47 @@ class NeuralNetwork: target[move.getRow()][move.getCol()] = 1 targets.append(target.tolist()) return targets + + def pickMove(self, gameMove): + prediction = self._predict(gameMove)[0] + #self.showHeatmap(prediction) + playableVertices = gameMove.getPlayableVertices() + highest = sys.float_info.min + hRow = -1 + hCol = -1 + for row in range(self.boardSize): + for col in range(self.boardSize): + #print("[%d, %d] %s" % (row, col, (row, col) in playableVertices)) + if prediction[row][col] > highest and (row, col) in playableVertices: + hRow = row + hCol = col + highest = prediction[row][col] + return [hRow, hCol] + + def _predict(self, gameMove): + board = gameMove.board.board + player = gameMove.getPlayer() + sampleBoard = self._boardToPlayerContext(board, player) + sampleBoard = numpy.array([sampleBoard]) + return self.model.predict( + x = sampleBoard, + batch_size = 1, + verbose = 2) + + def showHeatmap(self, data): + fig, ax = pyplot.subplots() + im = ax.imshow(data) + + # Show all ticks and label them with the respective list entries + ax.set_xticks(numpy.arange(9)) + ax.set_yticks(numpy.arange(9)) + + # Loop over data dimensions and create text annotations. + for i in range(9): + for j in range(9): + text = ax.text(j, i, data[i, j], + ha="center", va="center", color="w") + + ax.set_title("Harvest of local farmers (in tons/year)") + fig.tight_layout() + pyplot.show() diff --git a/imago/engine/monteCarlo.py b/imago/engine/monteCarlo.py index 3f28d8d..2113fdd 100644 --- a/imago/engine/monteCarlo.py +++ b/imago/engine/monteCarlo.py @@ -29,8 +29,8 @@ class MCTS(DecisionAlgorithm): # completely random for _ in range(5): self.root.selection().expansion().simulation(10, 20) - self.root = self.selectBestNextNode() - return self.root.move + selectedNode = self.selectBestNextNode() + return selectedNode.move.coords def selectBestNextNode(self): """Returns best ucb node available for the current player.""" @@ -169,7 +169,7 @@ class MCTSNode: selectedMove = random.choice(list(sensibleMoves)) currentMove = currentMove.addMoveByCoords(selectedMove) score = currentMove.board.score() - print("Current move: %s" % (currentMove.toString())) + print("Current move: %s" % (str(currentMove))) print("Current move game length: ", currentMove.getGameLength()) print("Score of the board: %d, %d (%d)" % (score[0], diff --git a/imago/gameLogic/gameMove.py b/imago/gameLogic/gameMove.py index cdd2fd6..c1c7a05 100644 --- a/imago/gameLogic/gameMove.py +++ b/imago/gameLogic/gameMove.py @@ -132,7 +132,7 @@ class GameMove: return previousMoves return self.nextMoves[0].getMainLineOfPlay(previousMoves) - def toString(self): + def __str__(self): """Returns the coordinates of the move as a string.""" if self.isPass: return "Pass" diff --git a/imagocli.py b/imagocli.py index e8e4580..150587e 100755 --- a/imagocli.py +++ b/imagocli.py @@ -2,8 +2,24 @@ """Run the Imago engine.""" +import sys + from imago.engine.imagoIO import ImagoIO +from imago.engine.decisionAlgorithmFactory import DecisionAlgorithms if __name__ == "__main__": - io = ImagoIO() + + decisionAlgorithm = None + if len(sys.argv) >= 3: + if sys.argv[1] == "-e": + if sys.argv[2] == "montecarlo": + decisionAlgorithm = DecisionAlgorithms.MONTECARLO + if sys.argv[2] == "keras": + decisionAlgorithm = DecisionAlgorithms.KERAS + + if decisionAlgorithm is None: + io = ImagoIO() + else: + io = ImagoIO(decisionAlgorithm) + io.start() diff --git a/models/testModel.h5 b/models/testModel.h5 new file mode 100644 index 0000000..6bc5fe2 Binary files /dev/null and b/models/testModel.h5 differ -- cgit v1.2.1 From 3c20c94d6644966ba0c9fe5f68f41a6480f3fef5 Mon Sep 17 00:00:00 2001 From: InigoGutierrez Date: Sun, 19 Jun 2022 20:14:12 +0200 Subject: Lowest representable float is -sys.float_info.max, not sys.float_info.min. --- imago/engine/keras/neuralNetwork.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/imago/engine/keras/neuralNetwork.py b/imago/engine/keras/neuralNetwork.py index 41b8b23..1ec0efd 100644 --- a/imago/engine/keras/neuralNetwork.py +++ b/imago/engine/keras/neuralNetwork.py @@ -119,7 +119,7 @@ class NeuralNetwork: prediction = self._predict(gameMove)[0] #self.showHeatmap(prediction) playableVertices = gameMove.getPlayableVertices() - highest = sys.float_info.min + highest = -sys.float_info.max hRow = -1 hCol = -1 for row in range(self.boardSize): -- cgit v1.2.1 From 206f2cf2592f1ea36d28e1026c5bfb0a6432fbd2 Mon Sep 17 00:00:00 2001 From: InigoGutierrez Date: Wed, 22 Jun 2022 21:31:40 +0200 Subject: Created subclasses for different neural networks. --- imago/engine/keras/convNeuralNetwork.py | 51 ++++++++++++++++++++++++++++++++ imago/engine/keras/denseNeuralNetwork.py | 28 ++++++++++++++++++ imago/engine/keras/keras.py | 8 +++-- imago/engine/keras/neuralNetwork.py | 31 ++++++------------- testKeras.py | 7 +++-- 5 files changed, 98 insertions(+), 27 deletions(-) create mode 100644 imago/engine/keras/convNeuralNetwork.py create mode 100644 imago/engine/keras/denseNeuralNetwork.py diff --git a/imago/engine/keras/convNeuralNetwork.py b/imago/engine/keras/convNeuralNetwork.py new file mode 100644 index 0000000..2534db6 --- /dev/null +++ b/imago/engine/keras/convNeuralNetwork.py @@ -0,0 +1,51 @@ +"""Convolutional neural network.""" + +from tensorflow.keras.models import Sequential +from tensorflow.keras.layers import Conv2D, Conv2DTranspose, Reshape, Dense +from tensorflow.keras.optimizers import Adam + +from imago.engine.keras.neuralNetwork import NeuralNetwork + +defaultModelFile = 'models/imagoConvKerasModel.h5' + +class ConvNeuralNetwork(NeuralNetwork): + + def _initModel(self, boardSize=NeuralNetwork.DEF_BOARD_SIZE): + model = Sequential([ + Conv2D( + filters=32, + kernel_size=(3), + activation='relu', + padding='same', + input_shape=(boardSize,boardSize,1) + ), + Conv2D( + filters=64, + kernel_size=(3), + activation='relu', + padding='same' + ), + #Conv2DTranspose( + # filters=64, + # kernel_size=(3), + # activation='relu', + # padding='same' + #), + Dense( + units=1, + activation='softmax' + ), + Reshape( + (boardSize,boardSize) + ), + ]) + + model.summary() + + model.compile( + optimizer=Adam(learning_rate=0.0001), + loss='categorical_crossentropy', + metrics=['accuracy'] + ) + + return model diff --git a/imago/engine/keras/denseNeuralNetwork.py b/imago/engine/keras/denseNeuralNetwork.py new file mode 100644 index 0000000..ff2efa8 --- /dev/null +++ b/imago/engine/keras/denseNeuralNetwork.py @@ -0,0 +1,28 @@ +"""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=32, activation='sigmoid', input_shape=(boardSize,boardSize)), + Dense(units=64, 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 a6aa913..80b6647 100644 --- a/imago/engine/keras/keras.py +++ b/imago/engine/keras/keras.py @@ -3,16 +3,18 @@ from imago.gameLogic.gameMove import GameMove from imago.gameLogic.gameBoard import GameBoard from imago.engine.decisionAlgorithm import DecisionAlgorithm -from imago.engine.keras.neuralNetwork import NeuralNetwork +from imago.engine.keras.denseNeuralNetwork import DenseNeuralNetwork +from imago.engine.keras.convNeuralNetwork import ConvNeuralNetwork -MODEL_FILE = "models/testModel.h5" +MODEL_FILE = "" # Use network's default model file class Keras(DecisionAlgorithm): def __init__(self, move): self.currentMove = move self.boardSize = move.board.getBoardHeight() - self.nn = NeuralNetwork(MODEL_FILE, self.boardSize) + #self.nn = NeuralNetwork(MODEL_FILE, self.boardSize) + self.nn = ConvNeuralNetwork(MODEL_FILE, self.boardSize) def forceNextMove(self, coords): """Selects given move as next move.""" diff --git a/imago/engine/keras/neuralNetwork.py b/imago/engine/keras/neuralNetwork.py index 1ec0efd..a52a3af 100644 --- a/imago/engine/keras/neuralNetwork.py +++ b/imago/engine/keras/neuralNetwork.py @@ -6,18 +6,18 @@ import os.path import numpy from matplotlib import pyplot -from tensorflow.keras.models import Sequential, load_model -from tensorflow.keras.layers import Dense -from tensorflow.keras.optimizers import Adam +from tensorflow.keras.models import load_model from imago.data.enums import Player defaultModelFile = 'models/imagoKerasModel.h5' -DEF_BOARD_SIZE = 9 -PLAYER_ID = 1 -OPPONENT_ID = -1 class NeuralNetwork: + + DEF_BOARD_SIZE = 9 + PLAYER_ID = 1 + OPPONENT_ID = -1 + def __init__(self, modelPath="", boardSize=DEF_BOARD_SIZE): self.boardSize = boardSize self.path = defaultModelFile @@ -29,21 +29,7 @@ class NeuralNetwork: self.model = self._initModel(boardSize) def _initModel(self, boardSize=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 + raise NotImplementedError("Tried to directly use NeuralNetwork class. Use one of the subclasses instead.") def trainModel(self, games): trainMoves = [] @@ -70,7 +56,8 @@ class NeuralNetwork: if os.path.isfile(modelPath): return load_model(modelPath) else: - raise FileNotFoundError("Keras neural network file not found at %s" % modelPath) + raise FileNotFoundError("Keras neural network model file not found at %s" + % modelPath) def saveModel(self, modelPath=""): if modelPath != "": diff --git a/testKeras.py b/testKeras.py index 3b0b3f5..0f25b21 100755 --- a/testKeras.py +++ b/testKeras.py @@ -6,6 +6,7 @@ import sys from imago.sgfParser.sgf import loadGameTree from imago.engine.keras.neuralNetwork import NeuralNetwork +from imago.engine.keras.convNeuralNetwork import ConvNeuralNetwork def main(): games = [] @@ -15,8 +16,10 @@ def main(): matches = [game.getMainLineOfPlay() for game in games] - modelFile = "models/testModel.h5" - nn = NeuralNetwork(modelFile, 9) + modelFile = "" + boardsize = 9 + nn = NeuralNetwork(modelFile, boardsize) + #nn = ConvNeuralNetwork(modelFile, boardsize) nn.trainModel(matches) nn.saveModel() -- cgit v1.2.1 From 259a6e8a1ff030060ca624d07f37f3ffd0c2295d Mon Sep 17 00:00:00 2001 From: InigoGutierrez Date: Wed, 22 Jun 2022 21:33:19 +0200 Subject: Fixed using wrong network for testing. --- testKeras.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/testKeras.py b/testKeras.py index 0f25b21..0f518d0 100755 --- a/testKeras.py +++ b/testKeras.py @@ -5,7 +5,7 @@ import sys from imago.sgfParser.sgf import loadGameTree -from imago.engine.keras.neuralNetwork import NeuralNetwork +from imago.engine.keras.denseNeuralNetwork import DenseNeuralNetwork from imago.engine.keras.convNeuralNetwork import ConvNeuralNetwork def main(): @@ -18,7 +18,7 @@ def main(): modelFile = "" boardsize = 9 - nn = NeuralNetwork(modelFile, boardsize) + nn = DenseNeuralNetwork(modelFile, boardsize) #nn = ConvNeuralNetwork(modelFile, boardsize) nn.trainModel(matches) nn.saveModel() -- cgit v1.2.1 From f57799a2b45cf7ad76ca9cff32cb4d1d86c2f2c4 Mon Sep 17 00:00:00 2001 From: InigoGutierrez Date: Sat, 25 Jun 2022 01:47:38 +0200 Subject: Working convolutional neural network. --- imago/engine/keras/convNeuralNetwork.py | 35 +++++---- imago/engine/keras/keras.py | 13 ++-- imago/engine/keras/neuralNetwork.py | 133 ++++++++++++++++++++++---------- 3 files changed, 117 insertions(+), 64 deletions(-) diff --git a/imago/engine/keras/convNeuralNetwork.py b/imago/engine/keras/convNeuralNetwork.py index 2534db6..9d97586 100644 --- a/imago/engine/keras/convNeuralNetwork.py +++ b/imago/engine/keras/convNeuralNetwork.py @@ -1,43 +1,46 @@ """Convolutional neural network.""" from tensorflow.keras.models import Sequential -from tensorflow.keras.layers import Conv2D, Conv2DTranspose, Reshape, Dense +from tensorflow.keras.layers import Conv2D, MaxPool2D, Flatten, Dense from tensorflow.keras.optimizers import Adam from imago.engine.keras.neuralNetwork import NeuralNetwork -defaultModelFile = 'models/imagoConvKerasModel.h5' - class ConvNeuralNetwork(NeuralNetwork): + NETWORK_ID = "convNeuralNetwork" + DEFAULT_MODEL_FILE = 'models/imagoConvKerasModel.h5' + def _initModel(self, boardSize=NeuralNetwork.DEF_BOARD_SIZE): model = Sequential([ Conv2D( filters=32, - kernel_size=(3), + kernel_size=(3,3), activation='relu', padding='same', - input_shape=(boardSize,boardSize,1) + input_shape=(boardSize,boardSize,2) + ), + MaxPool2D( + pool_size=(2,2), + strides=2, + padding='valid' ), Conv2D( filters=64, - kernel_size=(3), + kernel_size=(3,3), activation='relu', padding='same' ), - #Conv2DTranspose( - # filters=64, - # kernel_size=(3), - # activation='relu', - # padding='same' - #), + MaxPool2D( + pool_size=(2,2), + strides=2, + padding='valid' + ), + Flatten(), Dense( - units=1, + units=81, activation='softmax' ), - Reshape( - (boardSize,boardSize) - ), ]) model.summary() diff --git a/imago/engine/keras/keras.py b/imago/engine/keras/keras.py index 80b6647..4f39818 100644 --- a/imago/engine/keras/keras.py +++ b/imago/engine/keras/keras.py @@ -3,18 +3,19 @@ from imago.gameLogic.gameMove import GameMove from imago.gameLogic.gameBoard import GameBoard from imago.engine.decisionAlgorithm import DecisionAlgorithm -from imago.engine.keras.denseNeuralNetwork import DenseNeuralNetwork -from imago.engine.keras.convNeuralNetwork import ConvNeuralNetwork +from imago.engine.keras.neuralNetwork import NeuralNetwork -MODEL_FILE = "" # Use network's default model file +MODEL_FILE = "/home/taamas/IIS/TFG/imago/models/imagoConvKerasModel.h5" class Keras(DecisionAlgorithm): def __init__(self, move): self.currentMove = move self.boardSize = move.board.getBoardHeight() - #self.nn = NeuralNetwork(MODEL_FILE, self.boardSize) - self.nn = ConvNeuralNetwork(MODEL_FILE, self.boardSize) + self.nn = NeuralNetwork( + MODEL_FILE, + self.boardSize + ) def forceNextMove(self, coords): """Selects given move as next move.""" @@ -22,7 +23,7 @@ class Keras(DecisionAlgorithm): def pickMove(self): """Returns a move to play.""" - return self.nn.pickMove(self.currentMove) + return self.nn.pickMove(self.currentMove, self.currentMove.getNextPlayer()) def clearBoard(self): """Empties move history.""" diff --git a/imago/engine/keras/neuralNetwork.py b/imago/engine/keras/neuralNetwork.py index a52a3af..d0eb4ae 100644 --- a/imago/engine/keras/neuralNetwork.py +++ b/imago/engine/keras/neuralNetwork.py @@ -1,32 +1,34 @@ """Keras neural network.""" import sys +import os import os.path import numpy from matplotlib import pyplot from tensorflow.keras.models import load_model +from tensorflow.keras.utils import plot_model from imago.data.enums import Player -defaultModelFile = 'models/imagoKerasModel.h5' - class NeuralNetwork: DEF_BOARD_SIZE = 9 - PLAYER_ID = 1 - OPPONENT_ID = -1 + + NETWORK_ID = "neuralNetwork" + DEFAULT_MODEL_FILE = "models/imagoKerasModel.h5" def __init__(self, modelPath="", boardSize=DEF_BOARD_SIZE): self.boardSize = boardSize - self.path = defaultModelFile + self.path = self.DEFAULT_MODEL_FILE if modelPath != "": self.path = modelPath try: - self.model = self._loadModel(modelPath) + self.model = self._loadModel(self.path) except FileNotFoundError: self.model = self._initModel(boardSize) + self.saveModelPlot() def _initModel(self, boardSize=DEF_BOARD_SIZE): raise NotImplementedError("Tried to directly use NeuralNetwork class. Use one of the subclasses instead.") @@ -60,6 +62,7 @@ class NeuralNetwork: % modelPath) def saveModel(self, modelPath=""): + """Saves the neural network model at the given path.""" if modelPath != "": self.model.save(modelPath) else: @@ -68,80 +71,126 @@ class NeuralNetwork: def _movesToTrainMoves(self, moves): trainMoves = [] for move in moves: - trainMove = self._moveToTrainMove(move) + if len(move.nextMoves) == 0: + continue + player = move.nextMoves[0].getPlayer() + board = move.board.board + trainMove = self._boardToPlayerContext(board, player) trainMoves.append(trainMove) return trainMoves - def _moveToTrainMove(self, move): - player = move.getPlayer() - return self._boardToPlayerContext(move.board.board, player) - def _boardToPlayerContext(self, board, player): - """Converts to a board where PLAYER_ID marks the stones who made the move and - OPPONENT_ID marks the stones of the other player.""" + """Converts the board to a 3D matrix with two representations of the board, one + marking the player's stones and the oter marking the opponent's stones.""" boardRows = len(board) boardCols = len(board[0]) - contextBoard = numpy.zeros((boardRows, boardCols), dtype = int) + contextBoard = numpy.zeros((boardRows, boardCols, 2), dtype = float) for row in range(boardRows): for col in range(boardCols): if board[row][col] != Player.EMPTY: if board[row][col] == player: - contextBoard[row][col] = PLAYER_ID + contextBoard[row][col][0] = 1 else: - contextBoard[row][col] = OPPONENT_ID - return contextBoard.tolist() + contextBoard[row][col][1] = 1 + return contextBoard def _movesToTargets(self, moves): + """Converts the moves to 2D matrices with values zero except for a one on the + played vertex.""" targets = [] for move in moves: - target = numpy.zeros( - (move.board.getBoardHeight(), - move.board.getBoardWidth()), - dtype = float) - target[move.getRow()][move.getCol()] = 1 + if len(move.nextMoves) == 0: + continue + target = numpy.zeros(self.boardSize * self.boardSize, dtype = float) + target[move.nextMoves[0].getRow() * self.boardSize + move.nextMoves[0].getCol()] = 1 targets.append(target.tolist()) return targets - def pickMove(self, gameMove): - prediction = self._predict(gameMove)[0] - #self.showHeatmap(prediction) + def pickMove(self, gameMove, player): + """Uses the model's predict function to pick the highest valued vertex to play.""" + + predictionVector = self._predict(gameMove, player)[0] + prediction = numpy.zeros((self.boardSize, self.boardSize)) + for row in range(self.boardSize): + for col in range(self.boardSize): + prediction[row][col] = predictionVector[row * self.boardSize + col] + self.saveHeatmap(prediction) + + # Search the highest valued vertex which is also playable playableVertices = gameMove.getPlayableVertices() highest = -sys.float_info.max hRow = -1 hCol = -1 for row in range(self.boardSize): for col in range(self.boardSize): - #print("[%d, %d] %s" % (row, col, (row, col) in playableVertices)) if prediction[row][col] > highest and (row, col) in playableVertices: hRow = row hCol = col highest = prediction[row][col] + return [hRow, hCol] - def _predict(self, gameMove): + def _predict(self, gameMove, player): board = gameMove.board.board - player = gameMove.getPlayer() - sampleBoard = self._boardToPlayerContext(board, player) - sampleBoard = numpy.array([sampleBoard]) + sampleBoards = self._boardToPlayerContext(board, player) + sampleBoards = numpy.array([sampleBoards]) return self.model.predict( - x = sampleBoard, + x = sampleBoards, batch_size = 1, verbose = 2) - def showHeatmap(self, data): + def saveHeatmap(self, data): + rows = len(data) + cols = len(data[0]) + fig, ax = pyplot.subplots() - im = ax.imshow(data) + im = ax.imshow(data, cmap="YlGn") # Show all ticks and label them with the respective list entries - ax.set_xticks(numpy.arange(9)) - ax.set_yticks(numpy.arange(9)) + ax.set_xticks(numpy.arange(cols)) + ax.set_xticklabels(self._getLetterLabels(cols)) + ax.set_yticks(numpy.arange(rows)) + ax.set_yticklabels(numpy.arange(rows, 0, -1)) # Loop over data dimensions and create text annotations. - for i in range(9): - for j in range(9): - text = ax.text(j, i, data[i, j], - ha="center", va="center", color="w") - - ax.set_title("Harvest of local farmers (in tons/year)") + textColorThreshold = 0.35 + for row in range(rows): + for col in range(cols): + textColor = ("k" if data[row, col] < textColorThreshold else "w") + ax.text(col, row, "%.2f"%(data[row, col]), + ha="center", va="center", color=textColor) + + ax.set_title("Heat map of move likelihood") fig.tight_layout() - pyplot.show() + pyplot.savefig("heatmaps/heatmap_%s_%s_%d.png" % + ( + self.NETWORK_ID, + self.path.replace('/','-'), + len([file for file in os.listdir("heatmaps")]) + ) + ) + + def _getLetterLabels(self, count): + labels = [] + letter = 'A' + for _ in range(count): + labels.append(letter) + letter = chr(ord(letter) + 1) + # Skip I + if letter == 'I': + letter = 'J' + return labels + + def saveModelPlot(self): + plot_model( + self.model, + to_file="model.png", + show_shapes=True, + show_dtype=True, + show_layer_names=True, + rankdir="TB", + expand_nested=True, + dpi=96, + layer_range=None, + show_layer_activations=True, + ) -- cgit v1.2.1 From 77764bee49c93a5d08d134cf7919b7f84a997e20 Mon Sep 17 00:00:00 2001 From: InigoGutierrez Date: Sat, 25 Jun 2022 19:56:48 +0200 Subject: Back to the documentation after creating neural networks. --- .gitignore | 11 +++ doc/Makefile | 5 +- doc/diagrams/interfaces.puml | 20 ++++++ doc/diagrams/planificationWorkPlanEngine.puml | 46 +++++++++---- doc/diagrams/planificationWorkPlanGame.puml | 22 +++--- doc/diagrams/useCases.puml | 8 ++- doc/tex/biber.bib | 8 +++ doc/tex/implementation.tex | 8 +-- doc/tex/interface.tex | 6 -- doc/tex/planification.tex | 35 ++++++++-- doc/tex/systemAnalysis.tex | 99 ++++++++++++++++++++++----- doc/tex/tfg.tex | 17 +++-- 12 files changed, 220 insertions(+), 65 deletions(-) create mode 100644 doc/diagrams/interfaces.puml delete mode 100644 doc/tex/interface.tex diff --git a/.gitignore b/.gitignore index 750838b..46fefef 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,10 @@ *.sgf +# images +*.png +*.jpg +*.xcf + # doc *.pdf doc/out/ @@ -12,3 +17,9 @@ __pycache__/ # ply parser.out parsetab.py + +# logs +*.log + +# NN models +models/ diff --git a/doc/Makefile b/doc/Makefile index 4153756..d2799a8 100644 --- a/doc/Makefile +++ b/doc/Makefile @@ -3,8 +3,9 @@ docName = tfg outputFolder = out -texFiles = tex/tfg.tex tex/introduction.tex tex/planification.tex tex/interface.tex tex/implementation.tex tex/systemAnalysis.tex tex/systemDesign.tex tex/biber.bib -diagramImgs = diagrams/gameRepresentation.png diagrams/gtpEngine.png diagrams/modules.png diagrams/planificationWorkPlanEngine.png diagrams/planificationWorkPlanGame.png diagrams/sgfModule.png diagrams/useCases.png diagrams/analysisClasses.png diagrams/useCase_useAsBackend.png +texFiles = tex/tfg.tex tex/introduction.tex tex/planification.tex tex/implementation.tex tex/systemAnalysis.tex tex/systemDesign.tex tex/biber.bib + +diagramImgs = diagrams/gameRepresentation.png diagrams/gtpEngine.png diagrams/modules.png diagrams/planificationWorkPlanEngine.png diagrams/planificationWorkPlanGame.png diagrams/sgfModule.png diagrams/useCases.png diagrams/analysisClasses.png diagrams/useCase_useAsBackend.png diagrams/interfaces.png all: $(docName).pdf diff --git a/doc/diagrams/interfaces.puml b/doc/diagrams/interfaces.puml new file mode 100644 index 0000000..3ade81e --- /dev/null +++ b/doc/diagrams/interfaces.puml @@ -0,0 +1,20 @@ +@startuml + +!include ./skinparams.puml + +component Game +component Engine +component Trainer + +interface "Game text interface" as GTI +interface "Engine text interface" as ETI +interface "Neural network model" as NNM +interface "SGF files" as SGF + +Game -- GTI +Engine -- ETI +Engine -- NNM +Trainer -- NNM +Trainer -- SGF + +@enduml diff --git a/doc/diagrams/planificationWorkPlanEngine.puml b/doc/diagrams/planificationWorkPlanEngine.puml index 53bd5ea..fcdb3ba 100644 --- a/doc/diagrams/planificationWorkPlanEngine.puml +++ b/doc/diagrams/planificationWorkPlanEngine.puml @@ -4,27 +4,43 @@ !include skinparamsGantt.puml 'printscale weekly +Saturday are closed Sunday are closed -Project starts 2021-01-04 +Project starts 2021-01-11 + +-- Preliminary research -- +[Previous works research] as [PWR] lasts 1 week +[Algorithms research] as [AR] lasts 2 weeks -- Engine Implementation -- -[Engine modelling] as [EM] starts 2021-01-04 [Engine modelling] as [EM] lasts 1 week -[Engine implementation] as [EI] lasts 5 weeks -[Engine testing] as [ET] lasts 5 weeks +[Engine implementation] as [EI] lasts 4 weeks -- Algorithms Implementations -- -[Algorithm research] as [AR] lasts 1 week -[Monte Carlo implementation] as [MCI] lasts 3 weeks -[Extra algorithms research] as [EAR] lasts 2 weeks -[Extra algorithms implementation] as [EAI] lasts 4 weeks - -[EM] -> [AR] -[AR] -> [MCI] -[AR] -> [EI] -[AR] -> [ET] -[EI] -> [EAR] -[EAR] -> [EAI] +[Monte Carlo implementation] as [MCI] lasts 4 weeks +[Neural networks research] as [NNR] lasts 2 weeks +[Neural networks implementation] as [NNI] lasts 3 weeks + +-- Testing -- +[Engine unit testing] as [EUT] lasts 4 weeks +[System testing] as [ST] lasts 1 week + +-- Analysis -- +[Algorithms comparison] as [AC] lasts 2 weeks + +[PWR] -> [AR] +[AR] -> [EM] + +[EM] -> [MCI] +[EM] -> [EI] +[EM] -> [EUT] + +[MCI] -> [NNR] +[NNR] -> [NNI] + +[NNI] -> [ST] + +[ST] -> [AC] @endgantt diff --git a/doc/diagrams/planificationWorkPlanGame.puml b/doc/diagrams/planificationWorkPlanGame.puml index 42b0821..fa09384 100644 --- a/doc/diagrams/planificationWorkPlanGame.puml +++ b/doc/diagrams/planificationWorkPlanGame.puml @@ -4,21 +4,27 @@ !include skinparamsGantt.puml 'printscale weekly +Saturday are closed Sunday are closed Project starts 2020-11-02 --- Preliminary investigation -- -[Previous works investigation] as [PWI] lasts 7 days -[Engines investigation] as [EI] lasts 7 days +-- Preliminary research -- +[Previous works research] as [PWR] lasts 1 week -- Game Implementation -- -[Domain modelling] as [DM] lasts 6 days -[Domain implementation] as [DI] lasts 30 days -[Domain testing] as [DT] lasts 30 days +[Domain modelling] as [DM] lasts 1 week +[Domain implementation] as [DI] lasts 6 weeks + +-- Testing -- +[Unit testing] as [UT] lasts 6 weeks +[System testing] as [ST] lasts 3 days + +[PWR] -> [DM] -[PWI] -> [DM] [DM] -> [DI] -[DM] -> [DT] +[DM] -> [UT] + +[DI] -> [ST] @endgantt diff --git a/doc/diagrams/useCases.puml b/doc/diagrams/useCases.puml index e3da3f8..022bd4c 100644 --- a/doc/diagrams/useCases.puml +++ b/doc/diagrams/useCases.puml @@ -3,16 +3,18 @@ !include skinparams.puml actor "Human Player" as player -actor "Human User" as user actor "GUI Program" as gui +actor "Human User" as user usecase "Play a match" as play -usecase "Generate a move" as genMove usecase "Use as backend for machine player" as backend +usecase "Generate a move" as genMove +usecase "Train a neural network" as train player --> play +gui --> backend user --> genMove gui --> genMove -gui --> backend +user --> train @enduml diff --git a/doc/tex/biber.bib b/doc/tex/biber.bib index ef09d98..7dd5251 100644 --- a/doc/tex/biber.bib +++ b/doc/tex/biber.bib @@ -14,6 +14,14 @@ url = {https://www.lysator.liu.se/~gunnar/gtp} } +@online{sgf, + author = {Arno Hollosi}, + title = {SGF File Format FF[4]}, + date = {2021}, + urldate = {2022}, + url = {https://www.red-bean.com/sgf} +} + @online{katago, author = {David J Wu ("lightvector")}, title = {KataGo}, diff --git a/doc/tex/implementation.tex b/doc/tex/implementation.tex index 28fd0ec..297ba0e 100644 --- a/doc/tex/implementation.tex +++ b/doc/tex/implementation.tex @@ -3,10 +3,10 @@ \subsection{Engine} An implementation of GTP, that is, the piece of software which offers the GTP -interface to other applications.\@ It is designed to be used by a software controller -but can also be directly run, mostly for debugging purposes. Its design is shown -in \fref{fig:engine}. The core of the engine is related with three components, -each with a separate responsibility: +interface to other applications.\@ It is designed to be used by a software +controller but can also be directly run, mostly for debugging purposes. Its +design is shown in \fref{fig:engine}. The core of the engine is related with +three components, each with a separate responsibility: \begin{itemize} \item The IO component is the one called from other applications and offers diff --git a/doc/tex/interface.tex b/doc/tex/interface.tex deleted file mode 100644 index 9caa78d..0000000 --- a/doc/tex/interface.tex +++ /dev/null @@ -1,6 +0,0 @@ -\section{Interface} - -\subsection{Importing and exporting games} - -The format chosen to read and write games is SGF (Smart Game Format). It is a -widely used text format which allows for variants, comments and other metadata. diff --git a/doc/tex/planification.tex b/doc/tex/planification.tex index 5c9b253..942973f 100644 --- a/doc/tex/planification.tex +++ b/doc/tex/planification.tex @@ -21,10 +21,12 @@ Presented here are the ideal targets of the project. moves and variants of a match (a tree of moves) and the logic for the game's rules. \item An engine capable of analyzing board positions and generating strong - moves. + moves via various decision algorithms. \item Either a GUI specifically developed for the project or an implementation of an existing protocol so the engine can be used with existing tools and GUIs. + \item A way for processing existing records of games, which are usually + recorded in the SGF format. \end{itemize} \subsection{Project stages} @@ -70,7 +72,7 @@ The sole developer will be the student, who is currently working as a Junior Software Engineer on a 35 hour per week schedule and with no university responsibilities other than this project. Taking this into account, a sensible initial assumption is that he will be able to work 3 hours a day, Monday to -Saturday. Gantt diagrams for the planned working schedule are shown as +Friday. Gantt diagrams for the planned working schedule are shown as Fig.~\ref{fig:planificationWorkPlanGame} and Fig.~\ref{fig:planificationWorkPlanEngine}. @@ -120,7 +122,9 @@ A software capable of playing Go part of the GNU project. Although not a strong engine anymore, it is interesting for historic reasons as the free software engine for which the GTP protocol was first defined. -\subsubsection{GTP~\cite{gtp}} +\subsubsection{Existing standards} + +\paragraph{GTP~\cite{gtp}} GTP (\textit{Go Text Protocol}) is a text based protocol for communication with computer go programs. It is the protocol used by GNU Go and @@ -128,6 +132,13 @@ the more modern and powerful KataGo. By supporting GTP the engine developed for this project can be used with existing GUIs and other programs, making it easier to use it with the tools users are already familiar with. +\paragraph{SGF~\cite{sgf}} + +SGF (\textit{Smart Game Format}) is a text format widely used for storing +records of Go matches which allows for variants, comments and other metadata. +Many popular playing tools use it. By supporting SGF vast existing collections +of games can be used to train the decision algorithms based on neural networks. + \subsubsection{Sabaki~\cite{sabaki}} Sabaki is a go board software compatible with GTP engines. It can serve as a GUI @@ -148,7 +159,7 @@ choice is Python, for various reasons: specifically on AI research and development. \item Interpreters are available for many platforms, which allows the most people possible to access the product. - \item Although not too deeply, it has been used by the developer student + \item Although not very deeply, it has been used by the developer student during its degree including in AI and game theory contexts. \end{itemize} @@ -159,3 +170,19 @@ Both the game and the engine will offer a text interface. For the game this allows for quick human testing. For the engine it is mandated by the protocol, since GTP is a text based protocol for programs using text interfaces. Independent programs compatible with this interface can be used as a GUI. + +There is also the need of an interface with SGF files so existing games can be +processed by the trainer. + +Both the engine and the trainer will need to interface with the files storing +the neural network models. + +The systems' interfaces are shown in Fig.~\ref{fig:interfaces}. + +\begin{figure}[h] + \begin{center} + \includegraphics[width=\textwidth]{diagrams/interfaces.png} + \caption{Interfaces of the three components of the project. + }\label{fig:interfaces} + \end{center} +\end{figure} diff --git a/doc/tex/systemAnalysis.tex b/doc/tex/systemAnalysis.tex index 5868422..d0eb0b5 100644 --- a/doc/tex/systemAnalysis.tex +++ b/doc/tex/systemAnalysis.tex @@ -20,6 +20,20 @@ requisites needed for the system. \begin{enumerate} + \item The game program is interactive. + + \item Movements can be introduced to be played on the board. + \begin{enumerate} + \item A move is introduced as the textual representation of the + coordinates of the vertex to play on or as ``pass''. + \begin{enumerate} + \item The text introduced for the move must follow the + regular expression \texttt{([A-Z][0-9]+|pass)} + \item If the move is not valid it must be notified to the + user and another move asked for. + \end{enumerate} + \end{enumerate} + \item The state of the board can be shown to the user. \begin{enumerate} \item A text representation of each cell is printed. @@ -36,18 +50,6 @@ requisites needed for the system. \end{enumerate} \end{enumerate} - \item Movements can be introduced to be played on the board. - \begin{enumerate} - \item A move is introduced as the textual representation of the - coordinates of the vertex to play on or as ``pass''. - \begin{enumerate} - \item The text introduced for the move must follow the - regular expression \texttt{([A-Z][0-9]+|pass)} - \item If the move is not valid, it must be notified to the - user and another move asked for. - \end{enumerate} - \end{enumerate} - \item The board will behave according to the Japanese rules of Go. \end{enumerate} @@ -58,6 +60,8 @@ requisites needed for the system. \begin{enumerate} + \item The engine program is interactive. + \item The engine implements the GTP (\textit{Go Text Protocol}) for its interface. \begin{enumerate} @@ -102,6 +106,34 @@ requisites needed for the system. \end{enumerate} +\paragraph{Trainer Requirements} + +\setlist[enumerate,1]{label=FRT \arabic*.} + +\begin{enumerate} + + \item The trainer program is non-interactive. + + \item The trainer can be executed from the command line. + \begin{enumerate} + \item The trainer can be executed directly from an interactive shell. + \end{enumerate} + + \item The trainer can interact with stored neural network models. + \begin{enumerate} + \item The trainer can read stored models to continue training them. + \item The trainer can store model files after their training. + \end{enumerate} + + \item The trainer can import existing games. + \begin{enumerate} + \item Records of games stored as SGF can be imported. + \item Files containing records of games are provided as arguments to + the trainer. + \end{enumerate} + +\end{enumerate} + %\subsubsection{Security Requirements} % %\setlist[enumerate,1]{label=SR \arabic*.} @@ -129,6 +161,9 @@ requisites needed for the system. \item For directly using the engine the user needs to be familiar with command line interfaces. + \item For directly using the trainer the user needs to know the different + network models available. + \end{enumerate} \subsubsection{Technological Requirements} @@ -140,12 +175,34 @@ requisites needed for the system. \item The game program will be a python file able to be executed by the python interpreter. - \item The program will make use of standard input and standard output for - communication. + \item The game program will make use of standard input and standard output + for communication. \begin{enumerate} \item Standard input will be used for reading moves. - \item Standard output will be used for showing the board and for - messages directed to the user. + \item Standard output will be used for showing the board. + \item Standard output will be used for messages directed to the user. + \end{enumerate} + + \item The engine program will be a python file able to be executed by the + python interpreter. + + \item The engine program will make use of standard input and standard output + for communication. + \begin{enumerate} + \item Standard input will be used for reading commands. + \item Standard output will be used for showing the result of + commands. + \end{enumerate} + + \item The trainer program will be a python file able to be executed by the + python interpreter. + + \item The engine program will make use of standard input and standard output + for communication. + \begin{enumerate} + \item Standard input will be used for reading commands. + \item Standard output will be used for showing the result of + commands. \end{enumerate} \end{enumerate} @@ -156,6 +213,7 @@ requisites needed for the system. \begin{enumerate} +%TODO: Check and update this to something feasible \item The maximum thinking time of the engine will be configurable. \begin{enumerate} \item It will be possible to pass the maximum time as a launch @@ -166,7 +224,6 @@ requisites needed for the system. \end{enumerate} - \setlist[enumerate,1]{label=\arabic*.} \subsection{System Actors} @@ -206,7 +263,13 @@ GTP protocol and outputs the coordinates of the board to play. \paragraph{Use as backend for machine player} -The engine is used as the backend for generating moves for a machine player. +The engine is used as the backend for generating moves for a machine player, +this is, for automated play, either against a human who is using the GUI or +against another machine player. + +\paragraph{Train a neural network} + +A neural network is trained over a given list of moves. \subsection{Subsystems} diff --git a/doc/tex/tfg.tex b/doc/tex/tfg.tex index 6e2c472..5af6278 100644 --- a/doc/tex/tfg.tex +++ b/doc/tex/tfg.tex @@ -13,7 +13,7 @@ \usepackage[backend=biber, style=numeric, sorting=none]{biblatex} \addbibresource{tex/biber.bib} -\geometry{left=4.5cm,top=2cm,bottom=2cm,right=4.5cm} +\geometry{left=4cm,top=2cm,bottom=2cm,right=4cm} \hypersetup{colorlinks=false, linkcolor=black, @@ -42,6 +42,14 @@ \maketitle +\thispagestyle{empty} + +\begin{figure}[h] + \begin{center} + \includegraphics[width=0.6\textwidth]{img/imago.jpg} + \end{center} +\end{figure} + \begin{abstract} This is the abstract. \end{abstract} @@ -85,7 +93,8 @@ inclusion to use this template is included here. \begin{displayquote} - Copyright (C) 2019 \textbf{JOSÉ MANUEL REDONDO LÓPEZ}.\cite{plantillaRedondo} + Copyright (C) 2019 \textbf{JOSÉ MANUEL REDONDO + LÓPEZ}.\cite{plantillaRedondo} \textit{Permission is granted to copy, distribute and/or modify this document under the terms of the GNU Free Documentation License, Version 1.3 @@ -108,9 +117,7 @@ inclusion to use this template is included here. \input{tex/systemDesign.tex} -%\input{tex/interface.tex} - -%\input{tex/implementation.tex} +\input{tex/implementation.tex} \clearpage -- cgit v1.2.1 From a19a30cfa998b631093aba82a17e26cad6043d04 Mon Sep 17 00:00:00 2001 From: InigoGutierrez Date: Mon, 27 Jun 2022 00:19:00 +0200 Subject: Updating documentation of system analysis. --- doc/diagrams/analysisClasses.puml | 50 ++++++--- doc/tex/planification.tex | 7 +- doc/tex/systemAnalysis.tex | 230 ++++++++++++++++++++++++++++++++------ doc/tex/tfg.tex | 9 +- imago/engine/keras/keras.py | 13 ++- imago/engine/monteCarlo.py | 44 ++++---- imago/sgfParser/sgf.py | 1 - 7 files changed, 277 insertions(+), 77 deletions(-) diff --git a/doc/diagrams/analysisClasses.puml b/doc/diagrams/analysisClasses.puml index d051cf9..8273930 100644 --- a/doc/diagrams/analysisClasses.puml +++ b/doc/diagrams/analysisClasses.puml @@ -1,32 +1,56 @@ @startuml -!include skinparams.puml +'!include skinparams.puml -package GameModule { +() Player +package "Game module" { class GameIO class GameState class GameBoard class GameMove - GameIO -> GameState - GameState -> GameMove - GameMove -> GameBoard + Player -> GameIO + GameIO ..> GameState + GameState ..> GameMove + GameMove ..> GameBoard } -package EngineModule { +() "Engine user" as EU +() "Model files" as MF +package "Engine module" { class EngineIO class EngineLogic - !include DecisionAlgorithm.pumlc + interface DecisionAlgorithm class MonteCarloTreeSearch - class OtherDecisionAlgorithm + class MCTSNode + class Keras + class NeuralNetwork - EngineIO --> EngineLogic - EngineLogic -> DecisionAlgorithm + EU -> EngineIO + EngineIO ..> EngineLogic + EngineLogic ..> DecisionAlgorithm DecisionAlgorithm <|.. MonteCarloTreeSearch - DecisionAlgorithm <|.. OtherDecisionAlgorithm + DecisionAlgorithm <|.. Keras + MonteCarloTreeSearch ..> MCTSNode + Keras ..> NeuralNetwork + NeuralNetwork --> MF } -MonteCarloTreeSearch --> GameMove -OtherDecisionAlgorithm --> GameMove +() "SGF files" as SGF +package "Training module" { + class Trainer + class Parser + class ASTNode + + Parser -> SGF + Trainer ..> Parser + Parser ..> ASTNode +} + +DecisionAlgorithm .> GameMove + +ASTNode .> GameMove + +Trainer .> NeuralNetwork @enduml diff --git a/doc/tex/planification.tex b/doc/tex/planification.tex index 942973f..df72486 100644 --- a/doc/tex/planification.tex +++ b/doc/tex/planification.tex @@ -182,7 +182,10 @@ The systems' interfaces are shown in Fig.~\ref{fig:interfaces}. \begin{figure}[h] \begin{center} \includegraphics[width=\textwidth]{diagrams/interfaces.png} - \caption{Interfaces of the three components of the project. - }\label{fig:interfaces} + \caption{Interfaces of the three components of the project.} + The Engine and Trainer components are shown to share the Neural network + model interface because they will interact with the same files (the + files generated by the Trainer will be used by the Engine). + \label{fig:interfaces} \end{center} \end{figure} diff --git a/doc/tex/systemAnalysis.tex b/doc/tex/systemAnalysis.tex index d0eb0b5..044f2ee 100644 --- a/doc/tex/systemAnalysis.tex +++ b/doc/tex/systemAnalysis.tex @@ -1,6 +1,28 @@ \section{System Analysis} -%\subsection{System reach determination} +\subsection{System reach determination} + +These are the main goals the final product must reach. + +\begin{enumerate} + + \item The implementation, analysis and comparison of different decision + algorithms for genarating moves. This is the main goal and the following + ones are derived from the need of reaching it. + + \item A library for representing the game of Go. It can be used for the + decision algorithms to keep the state of the game and can also be used + in an interactive application for a user to play the game and test the + code. + + \item An engine program as a way of presenting an interface for using these + algorithms. The engine will use the GTP so it can be used with an + existing GUI or other tools. + + \item A parser for SGF files so they can be processed in the training of + neural networks. + +\end{enumerate} \subsection{System Requirements} @@ -144,6 +166,7 @@ requisites needed for the system. \begin{enumerate} + %TODO: Implement this \item The engine executable will include a help option with the different modes of execution. @@ -269,39 +292,45 @@ against another machine player. \paragraph{Train a neural network} -A neural network is trained over a given list of moves. +A neural network is trained by providing records of games. \subsection{Subsystems} -There will be two main subsystems. - -% TODO: Are there really two different subsystems? They look very coupled, since -% the engine will use some classes of the game. This section is more suited for -% independently run systems which communicate through some common interface. -% ...Or maybe not. From the template: "Subsystems are groupings of packages and -% classes with a common objective. Examples of subsystems are the classes which -% handle the database, classes joining a group of related services..." +There will be three main subsystems. \subsubsection{Game System} -The first, called the Game System, will be in charge of storing all the state -information regarding a Go match, such as the history of moves, the possible -variations, the state of the board at any given time or the current number of -captured stones. +The Game System will be in charge of storing all the state information regarding +a Go match, such as the history of moves, the possible variations, the state of +the board at any given time or the current number of captured stones. This system will include a command-line interface with which to play Go matches between human players to show and test its capabilities. \subsubsection{Engine System} -The second, called the Engine System, will implement the GTP interface and use -the Game System to analyze positions and generate moves via decision algorithms. +The Engine System will implement the GTP interface and use the Game System to +analyse positions and generate moves via decision algorithms. This system can be directly called to manually set up game states and ask for -moves or can be called by other programs to be used as backend for playing -matches against a computer player. +moves or can be called by other programs which use GTP to be used as backend for +playing matches against a computer player. + +\subsubsection{Training System} -%\subsubsection{Interface between subsystems} +The Training System will process SGF files storing records of games, train the +neural network models over those games and store the result. These models can +then be imported by the engine and be used to generate moves. + +\subsubsection{Interface between subsystems} + +The Training System depends on the NeuralNetwork interface of the Engine System +and uses it to train and store the neural network models. + +Both the Engine and Training systems depend on the GameMove class of the Game +System. The Engine System uses it to store the state of a game and provide it +to the decision algorithms. The Training System uses it to create the internal +representation of a game resulting from the processing of an SGF file. \subsection{Class analysis} @@ -322,7 +351,9 @@ The classes resulting from the analysis phase are shown in \newcommand{\interclassSpace}{30pt} -\indent +\paragraph{Engine System} + +\indent \\ \begin{tabular}{p{\linewidth}} \toprule @@ -368,6 +399,14 @@ The classes resulting from the analysis phase are shown in \vspace{\interclassSpace} +\newcommand{\decisionAlgorithmMethods}{ + \tabitem{\textbf{pickMove()}: Gives a move to play.} \\ + \tabitem{\textbf{forceNextMove(coords)}: Notifies the system of a played + move so it can update its state accordingly.} \\ + \tabitem{\textbf{clearBoard()}: Empties the move history. The algorithm will + now generate moves as from a new game.} \\ +} + \begin{tabular}{p{\linewidth}} \toprule \textbf{DecisionAlgorithm} \\ @@ -382,7 +421,7 @@ The classes resulting from the analysis phase are shown in \textit{(Depends on the algorithm.)} \\ \midrule \textbf{Proposed methods} \\ - \tabitem{\textbf{genmove()}: Gives the coordinates of a move to play.} \\ + \decisionAlgorithmMethods \bottomrule \end{tabular} @@ -390,25 +429,123 @@ The classes resulting from the analysis phase are shown in \begin{tabular}{p{\linewidth}} \toprule - \textbf{GameIO} \\ + \textbf{MonteCarloTreeSearch} \\ \midrule \textbf{Description} \\ - Offers the interface with the game. \\ + Implements the Monte Carlo Tree Search algorithm for exploring the tree of + the game and deciding on a move. \\ \midrule \textbf{Responsibilities} \\ - \tabitem{Read input.} \\ - \tabitem{Do some preprocessing.} \\ - \tabitem{Forward commands to the game state component.} \\ + \tabitem{Analyzing game states and generating moves.} \\ \midrule \textbf{Proposed attributes} \\ + \tabitem{\textbf{root}: The root node of a tree representing of the current + game state and the explored possible moves from it.} \\ \midrule \textbf{Proposed methods} \\ - \tabitem{\textbf{start()}: Starts reading standard input in a loop.} \\ + \decisionAlgorithmMethods \bottomrule \end{tabular} \vspace{\interclassSpace} +\begin{tabular}{p{\linewidth}} + \toprule + \textbf{MCTSNode} \\ + \midrule + \textbf{Description} \\ + A node of the tree used by the Monte Carlo Tree Search algorithm. \\ + \midrule + \textbf{Responsibilities} \\ + \tabitem{Storing a specific state of a match.} \\ + \midrule + \textbf{Proposed attributes} \\ + \tabitem{\textbf{visits}: How many times the node has been visited.} \\ + \tabitem{\textbf{score}: The number of explorations of the node resulting in + victory.} \\ + \tabitem{\textbf{move}: A GameMove for accessing game state and logic.} \\ + \tabitem{\textbf{parent}: This node's parent in the tree.} \\ + \tabitem{\textbf{children}: The nodes following from this node in the tree.} + \\ + \tabitem{\textbf{unexploredVertices}: The plays which could be explored from + this node.} \\ + \midrule + \textbf{Proposed methods} \\ + \tabitem{\textbf{ucbForPlayer()}: Computes the Upper Confidence Bound of the + node from the perspective of the player making the move stored in the node.} + \\ + \tabitem{\textbf{selection()}: Monte Carlo Tree Search selection step. + Selects the most promising node which still has some unexplored children.} + \\ + \tabitem{\textbf{expansion()}: Monte Carlo Tree Search expansion step. Picks + an unexplored vertex from the node and adds it as a new MCTSNode.} \\ + \tabitem{\textbf{expansionForCoords()}: Performs an expansion for the given + coordinates. This represents forcing a move on the algorithm.} \\ + \tabitem{\textbf{simulation()}: Play random matches to accumulate reward + information on the node.} \\ + \bottomrule +\end{tabular} + +\vspace{\interclassSpace} + +\begin{tabular}{p{\linewidth}} + \toprule + \textbf{Keras} \\ + \midrule + \textbf{Description} \\ + Implements the DecisionAlgorithm interface to give access to a neural + network. \\ + \midrule + \textbf{Responsibilities} \\ + \tabitem{Analyzing game states and generating moves.} \\ + \midrule + \textbf{Proposed attributes} \\ + \tabitem{\textbf{currentMove}: A GameMove for accessing game state and + logic.} \\ + \tabitem{\textbf{neuralNetwork}: A NeuralNetwork instance for generating + moves.} \\ + \midrule + \textbf{Proposed methods} \\ + \decisionAlgorithmMethods + \bottomrule +\end{tabular} + +\begin{tabular}{p{\linewidth}} + \toprule + \textbf{NeuralNetwork} \\ + \midrule + \textbf{Description} \\ + Manages the neural networks used by the engine. \\ + \midrule + \textbf{Responsibilities} \\ + \tabitem{Analyzing game states and generating moves.} \\ + \tabitem{Generating a new neural network.} \\ + \tabitem{Loading a model file to use an existing trained neural network.} \\ + \midrule + \textbf{Proposed attributes} \\ + \tabitem{\textbf{currentMove}: A GameMove for accessing game state and + logic.} \\ + \tabitem{\textbf{neuralNetwork}: A NeuralNetwork instance for generating + moves.} \\ + \midrule + \textbf{Proposed methods} \\ + \tabitem{\textbf{pickMove()}: Uses the current internal model to pick a move + given a game state.} \\ + \tabitem{\textbf{trainModel()}: Receives a list of games, with each game + being a list of moves, and trains the network on them.} \\ + \tabitem{\textbf{saveModel()}: Saves the current internal neural network + model.} \\ + \tabitem{\textbf{saveHeatmap()}: Saves an image of a heatmap of move + likelihood.} \\ + \tabitem{\textbf{saveModelPlot()}: Saves an image of a plot of the model + configuration.} \\ + \bottomrule +\end{tabular} + +\paragraph{Game System} + +\indent \\ + \begin{tabular}{p{\linewidth}} \toprule \textbf{GameState} \\ @@ -476,14 +613,17 @@ The classes resulting from the analysis phase are shown in \tabitem{Store information about a move (board, player, coordinates\ldots).} \\ \midrule \textbf{Proposed attributes} \\ - \tabitem{\textbf{GameBoard board}: The board as of this move.} \\ - \tabitem{\textbf{GameMove[] nextMoves}: The list of moves played after this + \tabitem{\textbf{board}: The board as of this move.} \\ + \tabitem{\textbf{nextMoves}: The list of moves played after this one. Different moves represent different game variations.} \\ - \tabitem{\textbf{GameMove previousMove}: The move before this one.} \\ - \tabitem{\textbf{boolean isPass}: True if the move is a pass and not a stone + \tabitem{\textbf{previousMove}: The move before this one.} \\ + \tabitem{\textbf{isPass}: True if the move is a pass and not a stone placement.} \\ - \tabitem{\textbf{int[] coords}: The coordinates of the board the move was - played at. Have no meaning if \textbf{isPass} is true.} \\ + \tabitem{\textbf{coords}: The coordinates of the board the move was + played at. Has no meaning if \textbf{isPass} is true.} \\ + \tabitem{\textbf{playerWhoPassed}: The player who made this move. Has no + meaning if \textbf{isPass} is false, since the player can be obtained from + the coordinates of the move when it is not a pass.} \\ \midrule \textbf{Proposed methods} \\ \tabitem{\textbf{getRow()}: Returns the row the move was played at.} \\ @@ -504,6 +644,30 @@ The classes resulting from the analysis phase are shown in \vspace{\interclassSpace} +%TODO: Finish the classes of the Game System + +\paragraph{Training System} + +\indent \\ + +\begin{tabular}{p{\linewidth}} + \toprule + \textbf{Trainer} \\ + \midrule + \textbf{Description} \\ + . \\ + \midrule + \textbf{Responsibilities} \\ + \tabitem{.} \\ + \midrule + \textbf{Proposed attributes} \\ + \tabitem{\textbf{}: .} \\ + \midrule + \textbf{Proposed methods} \\ + \tabitem{\textbf{}: .} \\ + \bottomrule +\end{tabular} + \subsection{Use case analysis and scenarios} \indent diff --git a/doc/tex/tfg.tex b/doc/tex/tfg.tex index 5af6278..22ee0e9 100644 --- a/doc/tex/tfg.tex +++ b/doc/tex/tfg.tex @@ -34,7 +34,8 @@ \frenchspacing -\title{\program: An AI capable of playing the game of Go} +\title{\program\\ +\large An AI player of the game of Go} \author{Íñigo Gutiérrez Fernández} @@ -50,6 +51,8 @@ \end{center} \end{figure} +\clearpage + \begin{abstract} This is the abstract. \end{abstract} @@ -109,6 +112,10 @@ inclusion to use this template is included here. \tableofcontents +\listoffigures + +\clearpage + \input{tex/introduction.tex} \input{tex/planification.tex} diff --git a/imago/engine/keras/keras.py b/imago/engine/keras/keras.py index 4f39818..0668cd1 100644 --- a/imago/engine/keras/keras.py +++ b/imago/engine/keras/keras.py @@ -11,10 +11,9 @@ class Keras(DecisionAlgorithm): def __init__(self, move): self.currentMove = move - self.boardSize = move.board.getBoardHeight() - self.nn = NeuralNetwork( + self.neuralNetwork = NeuralNetwork( MODEL_FILE, - self.boardSize + move.board.getBoardHeight() ) def forceNextMove(self, coords): @@ -23,8 +22,12 @@ class Keras(DecisionAlgorithm): def pickMove(self): """Returns a move to play.""" - return self.nn.pickMove(self.currentMove, self.currentMove.getNextPlayer()) + return self.neuralNetwork.pickMove( + self.currentMove, + self.currentMove.getNextPlayer() + ) def clearBoard(self): """Empties move history.""" - self.currentMove = GameMove(GameBoard(self.boardSize, self.boardSize)) + boardSize = self.currentMove.board.getBoardHeight() + self.currentMove = GameMove(GameBoard(boardSize, boardSize)) diff --git a/imago/engine/monteCarlo.py b/imago/engine/monteCarlo.py index 2113fdd..81bee1d 100644 --- a/imago/engine/monteCarlo.py +++ b/imago/engine/monteCarlo.py @@ -29,10 +29,10 @@ class MCTS(DecisionAlgorithm): # completely random for _ in range(5): self.root.selection().expansion().simulation(10, 20) - selectedNode = self.selectBestNextNode() + selectedNode = self._selectBestNextNode() return selectedNode.move.coords - def selectBestNextNode(self): + def _selectBestNextNode(self): """Returns best ucb node available for the current player.""" # Assumes at least one expansion has occured @@ -62,35 +62,35 @@ class MCTSNode: self.children = set() self.unexploredVertices = move.getPlayableVertices() - def ucb(self): - """Returns Upper Confidence Bound of node""" - # UCB = meanVictories + 1/visits - if self.visits == 0: - return 0 - mean = self.score / self.visits - adjust = 1/self.visits - return mean + adjust - def ucbForPlayer(self): """ Returns Upper Confidence Bound of node changing the symbol if the move is for the wite player.""" - return self.ucbForSpecificPlayer(self.move.getPlayer()) + return self._ucbForSpecificPlayer(self.move.getPlayer()) - def ucbForSpecificPlayer(self, player): + def _ucbForSpecificPlayer(self, player): """ Returns Upper Confidence Bound of node from the perspective of the given player.""" if player == Player.WHITE: - return self.ucb() * -1 - return self.ucb() + return self._ucb() * -1 + return self._ucb() + + def _ucb(self): + """Returns Upper Confidence Bound of node""" + # UCB = meanVictories + 1/visits + if self.visits == 0: + return 0 + mean = self.score / self.visits + adjust = 1/self.visits + return mean + adjust def selection(self): """Select the most promising node with unexplored children.""" - bestNode = self.selectionRec(self) + bestNode = self._selectionRec(self) return bestNode - def selectionRec(self, bestNode): + def _selectionRec(self, bestNode): """Searches this node and its children for the node with the best UCB value for the current player.""" @@ -109,15 +109,15 @@ class MCTSNode: # Check if node has unexplored children and better UCB than previously explored if len(self.unexploredVertices) > 0: - if self.ucbForSpecificPlayer( - player) > bestNode.ucbForSpecificPlayer(player): + if self._ucbForSpecificPlayer( + player) > bestNode._ucbForSpecificPlayer(player): bestNode = self # Recursively search children for better UCB for child in self.children: - bestChildNode = child.selectionRec(bestNode) - if bestChildNode.ucbForSpecificPlayer( - player) > bestNode.ucbForSpecificPlayer(player): + bestChildNode = child._selectionRec(bestNode) + if bestChildNode._ucbForSpecificPlayer( + player) > bestNode._ucbForSpecificPlayer(player): bestNode = bestChildNode return bestNode diff --git a/imago/sgfParser/sgf.py b/imago/sgfParser/sgf.py index 4b11cfb..154cbbe 100644 --- a/imago/sgfParser/sgf.py +++ b/imago/sgfParser/sgf.py @@ -3,7 +3,6 @@ from imago.sgfParser.sgfyacc import parser def loadGameTree(filename): -# PLY? """Parses a GameTree instance from a source SGF file.""" file = open(filename, "r") -- cgit v1.2.1 From e501f0e116bb9f4a897158a8a203e2da97374cff Mon Sep 17 00:00:00 2001 From: InigoGutierrez Date: Mon, 27 Jun 2022 00:49:20 +0200 Subject: Changed work plans diagrams to weekly scale. --- doc/diagrams/planificationWorkPlanEngine.puml | 2 +- doc/diagrams/planificationWorkPlanGame.puml | 4 ++-- doc/diagrams/skinparamsGantt.puml | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/doc/diagrams/planificationWorkPlanEngine.puml b/doc/diagrams/planificationWorkPlanEngine.puml index fcdb3ba..9caad40 100644 --- a/doc/diagrams/planificationWorkPlanEngine.puml +++ b/doc/diagrams/planificationWorkPlanEngine.puml @@ -3,7 +3,7 @@ !include skinparams.puml !include skinparamsGantt.puml -'printscale weekly +printscale weekly Saturday are closed Sunday are closed diff --git a/doc/diagrams/planificationWorkPlanGame.puml b/doc/diagrams/planificationWorkPlanGame.puml index fa09384..ffaf72c 100644 --- a/doc/diagrams/planificationWorkPlanGame.puml +++ b/doc/diagrams/planificationWorkPlanGame.puml @@ -3,7 +3,7 @@ !include skinparams.puml !include skinparamsGantt.puml -'printscale weekly +printscale weekly zoom 2 Saturday are closed Sunday are closed @@ -18,7 +18,7 @@ Project starts 2020-11-02 -- Testing -- [Unit testing] as [UT] lasts 6 weeks -[System testing] as [ST] lasts 3 days +[System testing] as [ST] lasts 1 week [PWR] -> [DM] diff --git a/doc/diagrams/skinparamsGantt.puml b/doc/diagrams/skinparamsGantt.puml index 5355d5f..38a4b9b 100644 --- a/doc/diagrams/skinparamsGantt.puml +++ b/doc/diagrams/skinparamsGantt.puml @@ -3,10 +3,10 @@ -- cgit v1.2.1 From 32ba965ef1312856ef1ef1a90bb945f9fdba5537 Mon Sep 17 00:00:00 2001 From: InigoGutierrez Date: Mon, 27 Jun 2022 00:50:24 +0200 Subject: Updated Fig references to use the macro. --- doc/tex/planification.tex | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/doc/tex/planification.tex b/doc/tex/planification.tex index df72486..945c610 100644 --- a/doc/tex/planification.tex +++ b/doc/tex/planification.tex @@ -73,8 +73,8 @@ Software Engineer on a 35 hour per week schedule and with no university responsibilities other than this project. Taking this into account, a sensible initial assumption is that he will be able to work 3 hours a day, Monday to Friday. Gantt diagrams for the planned working schedule are shown as -Fig.~\ref{fig:planificationWorkPlanGame} and -Fig.~\ref{fig:planificationWorkPlanEngine}. +\fref{fig:planificationWorkPlanGame} and +\fref{fig:planificationWorkPlanEngine}. \begin{figure}[h] \begin{center} @@ -177,15 +177,12 @@ processed by the trainer. Both the engine and the trainer will need to interface with the files storing the neural network models. -The systems' interfaces are shown in Fig.~\ref{fig:interfaces}. +The systems' interfaces are shown in \fref{fig:interfaces}. \begin{figure}[h] \begin{center} \includegraphics[width=\textwidth]{diagrams/interfaces.png} \caption{Interfaces of the three components of the project.} - The Engine and Trainer components are shown to share the Neural network - model interface because they will interact with the same files (the - files generated by the Trainer will be used by the Engine). \label{fig:interfaces} \end{center} \end{figure} -- cgit v1.2.1 From d5d3ffad63ff3c274430725dd519191ec0f33029 Mon Sep 17 00:00:00 2001 From: InigoGutierrez Date: Wed, 29 Jun 2022 01:31:51 +0200 Subject: Updated use cases documentation. --- doc/Makefile | 2 +- doc/diagrams/useCase_generateAMove.puml | 24 +++ doc/diagrams/useCase_playAMatch.puml | 20 +++ doc/diagrams/useCase_useAsBackend.puml | 35 +++-- doc/diagrams/useCases.puml | 2 - doc/tex/biber.bib | 6 + doc/tex/previousWorks.tex | 24 +-- doc/tex/systemAnalysis.tex | 251 +++++++++++++++++++++++--------- 8 files changed, 268 insertions(+), 96 deletions(-) create mode 100644 doc/diagrams/useCase_generateAMove.puml create mode 100644 doc/diagrams/useCase_playAMatch.puml diff --git a/doc/Makefile b/doc/Makefile index d2799a8..e849a01 100644 --- a/doc/Makefile +++ b/doc/Makefile @@ -5,7 +5,7 @@ outputFolder = out texFiles = tex/tfg.tex tex/introduction.tex tex/planification.tex tex/implementation.tex tex/systemAnalysis.tex tex/systemDesign.tex tex/biber.bib -diagramImgs = diagrams/gameRepresentation.png diagrams/gtpEngine.png diagrams/modules.png diagrams/planificationWorkPlanEngine.png diagrams/planificationWorkPlanGame.png diagrams/sgfModule.png diagrams/useCases.png diagrams/analysisClasses.png diagrams/useCase_useAsBackend.png diagrams/interfaces.png +diagramImgs = diagrams/gameRepresentation.png diagrams/gtpEngine.png diagrams/modules.png diagrams/planificationWorkPlanEngine.png diagrams/planificationWorkPlanGame.png diagrams/sgfModule.png diagrams/useCases.png diagrams/analysisClasses.png diagrams/useCase_generateAMove.png diagrams/useCase_useAsBackend.png diagrams/useCase_playAMatch.png diagrams/interfaces.png all: $(docName).pdf diff --git a/doc/diagrams/useCase_generateAMove.puml b/doc/diagrams/useCase_generateAMove.puml new file mode 100644 index 0000000..fa76edb --- /dev/null +++ b/doc/diagrams/useCase_generateAMove.puml @@ -0,0 +1,24 @@ +@startuml + +!include skinparams.puml + +actor "GUI program / Human user" as user + +boundary "Engine CLI" as cli +control "Play a stone" as playStone +control "Think next move" as think +entity "Board state" as state + +loop until desired board is set + user -> cli : play stone + cli -> playStone + playStone -> state + cli <- state +end + +user -> cli : ask for move +cli -> think +think -> state +cli <- state : Show move + +@enduml diff --git a/doc/diagrams/useCase_playAMatch.puml b/doc/diagrams/useCase_playAMatch.puml new file mode 100644 index 0000000..65d1517 --- /dev/null +++ b/doc/diagrams/useCase_playAMatch.puml @@ -0,0 +1,20 @@ +@startuml + +!include skinparams.puml + +actor "Player" as player + +boundary "Game CLI" as cli +control "Play a stone" as playStone +control "Show board" as showBoard +entity "Board state" as state + +loop until game ends + player -> cli + cli -> playStone + playStone -> state + showBoard <- state + cli <- showBoard +end + +@enduml diff --git a/doc/diagrams/useCase_useAsBackend.puml b/doc/diagrams/useCase_useAsBackend.puml index 3f1cece..9076769 100644 --- a/doc/diagrams/useCase_useAsBackend.puml +++ b/doc/diagrams/useCase_useAsBackend.puml @@ -2,22 +2,31 @@ !include skinparams.puml +actor "Opponent" as opponent actor "GUI Program" as program -boundary "Set board state" as setState -control "State set to represent the needed board" as setStateEngine +boundary "Engine CLI" as cli +control "Play a stone" as playStone +control "Think next move" as think entity "Board state" as state -boundary "Move is asked for" as ask -control "Engine thinks next move" as think -boundary "Move is suggested" as suggestion -program -> setState -setState -> setStateEngine -setStateEngine -> state -state -> ask -program -> ask -ask -> think -think -> suggestion -program -> suggestion +loop until starting board is set + program -> cli : play stone + cli -> playStone + playStone -> state + cli <- state +end + +loop until game ends + program -> cli : ask for move + cli -> think + think -> state + cli <- state : Show move + opponent -> program : give input + program -> cli : play stone + cli -> playStone + playStone -> state + cli <- state +end @enduml diff --git a/doc/diagrams/useCases.puml b/doc/diagrams/useCases.puml index 022bd4c..8d4aa71 100644 --- a/doc/diagrams/useCases.puml +++ b/doc/diagrams/useCases.puml @@ -9,12 +9,10 @@ actor "Human User" as user usecase "Play a match" as play usecase "Use as backend for machine player" as backend usecase "Generate a move" as genMove -usecase "Train a neural network" as train player --> play gui --> backend user --> genMove gui --> genMove -user --> train @enduml diff --git a/doc/tex/biber.bib b/doc/tex/biber.bib index 7dd5251..055ca8f 100644 --- a/doc/tex/biber.bib +++ b/doc/tex/biber.bib @@ -72,3 +72,9 @@ urldate = {2021}, url = {https://coverage.readthedocs.io} } + +@online{matplotlib_heatmaps, + title = {Creating annotated heatmaps — Matplotlib 3.5.2 documentation}, + urldate = {2022}, + url = {https://matplotlib.org/stable/gallery/images_contours_and_fields/image_annotated_heatmap.html} +} diff --git a/doc/tex/previousWorks.tex b/doc/tex/previousWorks.tex index bff0c82..6e503a3 100644 --- a/doc/tex/previousWorks.tex +++ b/doc/tex/previousWorks.tex @@ -1,17 +1,17 @@ \section{Previous works} -%\subsection{SGF} -% -%SGF (\textit{Smart Go Format} or, in a more general context, \textit{Smart Game -%Format}) is a text file format specification for records of games or collections -%of them. It was devised for Go but it supports other games with similar -%turn-based structure. It supports move variations, annotations, setups and game -%metadata. By supporting SGF our application can be used to analyse existing -%games registered by other applications, such as those played on online Go -%servers. -% -%The SGF specification can be found at -%\url{https://www.red-bean.com/sgf/user_guide/index.html} +\subsection{SGF} + +SGF (\textit{Smart Go Format} or, in a more general context, \textit{Smart Game +Format}) is a text file format specification for records of games or collections +of them. It was devised for Go but it supports other games with similar +turn-based structure. It supports move variations, annotations, setups and game +metadata. By supporting SGF our application can be used to analyse existing +games registered by other applications, such as those played on online Go +servers. + +The SGF specification can be found at +\url{https://www.red-bean.com/sgf/user_guide/index.html} \subsection{GTP} diff --git a/doc/tex/systemAnalysis.tex b/doc/tex/systemAnalysis.tex index 044f2ee..e4962d3 100644 --- a/doc/tex/systemAnalysis.tex +++ b/doc/tex/systemAnalysis.tex @@ -249,51 +249,6 @@ requisites needed for the system. \setlist[enumerate,1]{label=\arabic*.} -\subsection{System Actors} - -There are various actors who will interact with the system, both human and -non-human. - -\begin{itemize} - - \item The human player who interacts with the playing interface. - \item The human user who interacts with the engine. - \item A GUI software which uses the engine to generate moves. - -\end{itemize} - -\subsection{Use Cases} - -\begin{figure}[h] - \begin{center} - \includegraphics[width=\textwidth]{diagrams/useCases.png} - \caption{Use cases.}\label{fig:useCases} - \end{center} -\end{figure} - -The different actors and use cases are represented on \fref{fig:useCases}. Each -use case is explained next. - -\paragraph{Play a match} - -The game interface reads the moves presented by the player and shows their -result on the board. - -\paragraph{Generate moves} - -The engine interface reads the input for generating a move as stated by the -GTP protocol and outputs the coordinates of the board to play. - -\paragraph{Use as backend for machine player} - -The engine is used as the backend for generating moves for a machine player, -this is, for automated play, either against a human who is using the GUI or -against another machine player. - -\paragraph{Train a neural network} - -A neural network is trained by providing records of games. - \subsection{Subsystems} There will be three main subsystems. @@ -510,6 +465,8 @@ The classes resulting from the analysis phase are shown in \bottomrule \end{tabular} +\vspace{\interclassSpace} + \begin{tabular}{p{\linewidth}} \toprule \textbf{NeuralNetwork} \\ @@ -542,6 +499,8 @@ The classes resulting from the analysis phase are shown in \bottomrule \end{tabular} +\vspace{\interclassSpace} + \paragraph{Game System} \indent \\ @@ -585,7 +544,7 @@ The classes resulting from the analysis phase are shown in \tabitem{Logic related to a board position.} \\ \midrule \textbf{Proposed attributes} \\ - \tabitem{\textbf{Player[][] board}: An array of the stones on the board.} \\ + \tabitem{\textbf{board}: An array of the stones on the board.} \\ \midrule \textbf{Proposed methods} \\ \tabitem{\textbf{getGroupLiberties()}: Returns a set with the empty vertices @@ -644,6 +603,40 @@ The classes resulting from the analysis phase are shown in \vspace{\interclassSpace} +\begin{tabular}{p{\linewidth}} + \toprule + \textbf{GameBoard} \\ + \midrule + \textbf{Description} \\ + Represents a board. Contains played stones and the amount of captures made + by each player. \\ + \midrule + \textbf{Responsibilities} \\ + \tabitem{Store a specific layout of stones in the board.} \\ + \midrule + \textbf{Proposed attributes} \\ + \tabitem{\textbf{board}: An array containing the stone layout.} \\ + \tabitem{\textbf{capturesBlack}: The stones captured by black before the + position.} \\ + \tabitem{\textbf{capturesWhite}: The stones captured by white before the + position.} \\ + \midrule + \textbf{Proposed methods} \\ + \tabitem{\textbf{getBoardHeight()}: Returns the number of rows of the board.} \\ + \tabitem{\textbf{getBoardWidth()}: Returns the number of columns of the board.} \\ + \tabitem{\textbf{getGroupLiberties()}: Returns a list with the empty + vertices adjacent to the group occupying a vertex.} \\ + \tabitem{\textbf{getGroupVertices()}: Returns a list with the vertices + occupied by the group occupying a vertex.} \\ + \tabitem{\textbf{moveAndCapture()}: Makes a move and captures the + corresponding stones if the move results in the capture of a group.} \\ + \tabitem{\textbf{score()}: Gets the current score based on the already + surrounded territory. This follows Japanese rules.} \\ + \bottomrule +\end{tabular} + +\vspace{\interclassSpace} + %TODO: Finish the classes of the Game System \paragraph{Training System} @@ -655,46 +648,162 @@ The classes resulting from the analysis phase are shown in \textbf{Trainer} \\ \midrule \textbf{Description} \\ - . \\ + Provides the neural networks with moves to train on. \\ + \midrule + \textbf{Responsibilities} \\ + \tabitem{Obtain moves from stored records of matches.} \\ + \tabitem{Provide neural networks with moves to train on.} \\ + \midrule + \textbf{Proposed attributes} \\ + %TODO: Explain why this is empty + \midrule + \textbf{Proposed methods} \\ + %TODO: Explain why this is empty + \bottomrule +\end{tabular} + +\vspace{\interclassSpace} + +\begin{tabular}{p{\linewidth}} + \toprule + \textbf{Parser} \\ + \midrule + \textbf{Description} \\ + Reads SGF files and converts them to a tree of GameMove from the Game + System. \\ + \midrule + \textbf{Responsibilities} \\ + \tabitem{Read SGF files.} \\ + \tabitem{Convert the content of the SGF files to a tree of GameMove.} \\ + \midrule + \textbf{Proposed attributes} \\ + %TODO: Explain why this is empty + \midrule + \textbf{Proposed methods} \\ + %TODO: Explain why this is empty + \bottomrule +\end{tabular} + +\vspace{\interclassSpace} + +\begin{tabular}{p{\linewidth}} + \toprule + \textbf{ASTNode} \\ + \midrule + \textbf{Description} \\ + Makes up the tree resulting from the parsing of an SGF file.\\ \midrule \textbf{Responsibilities} \\ - \tabitem{.} \\ + \tabitem{Obtain a GameMove tree from itself and its children.} \\ \midrule \textbf{Proposed attributes} \\ - \tabitem{\textbf{}: .} \\ + \tabitem{\textbf{children}: The nodes following from itself.} \\ + \tabitem{\textbf{props}: The properties of the tree read from an SGF file.} + \\ \midrule \textbf{Proposed methods} \\ - \tabitem{\textbf{}: .} \\ + \tabitem{\textbf{toGameTree()}: Returns a GameMove tree corresponding to the + tree following from this node.} \\ \bottomrule \end{tabular} +\vspace{\interclassSpace} + +\subsection{System Actors} + +There are various actors who will interact with the system, both human and +non-human. + +\begin{itemize} + + \item The human player who interacts with the playing interface. + \item The human user who interacts with the engine. + \item A GUI software which uses the engine to generate moves. + +\end{itemize} + +\subsection{Use Cases} + +\begin{figure}[h] + \begin{center} + \includegraphics[width=\textwidth]{diagrams/useCases.png} + \caption{Use cases.} + \label{fig:useCases} + \end{center} +\end{figure} + +The different actors and use cases are represented on \fref{fig:useCases}. Each +use case is explained next. + +\paragraph{Play a match} + +The game interface reads the moves presented by the player and shows their +result on the board. + +\paragraph{Use as backend for machine player} + +The engine is used as the backend for generating moves for a machine player, +this is, for automated play, either against a human who is using the GUI or +against another machine player. + +\paragraph{Generate a move} + +The engine interface reads the input for generating a move as stated by the +GTP protocol and outputs the coordinates of the board to play. + \subsection{Use case analysis and scenarios} -\indent +\begin{figure}[h] + \begin{center} + \includegraphics[width=\textwidth]{diagrams/useCase_playAMatch.png} + \caption{Use case: Play a match.} + \label{fig:useCase_playAMatch} + \end{center} +\end{figure} \begin{tabular}{lp{0.7\linewidth}} \toprule - \multicolumn{2}{c}{\textbf{Play a match (Make a move?)}} \\ + \multicolumn{2}{c}{\textbf{Play a match}} \\ \midrule \textbf{Preconditions} & The game interface has been started. \\ \midrule - \textbf{Postconditions} & Description of postconditions \\ + \textbf{Postconditions} & The program terminates after a match has been + played. \\ \midrule - \textbf{Actors} & Actors \\ + \textbf{Actors} & Human player \\ \midrule - \textbf{Description} & Description \\ + \textbf{Description} & + 1. The user enters the move to make.\newline + 2. The result of playing that move is outputted by the program.\newline + 3. Stop the program if the game has ended or go back to 1 if not. \\ \midrule - \textbf{Secondary scenarios} & Secondary scenarios \\ + \textbf{Secondary scenarios} & + \textbf{The move is illegal}: An error message is shown. Go back to step 1 of + main scenario. \\ \midrule - \textbf{Exceptions} & Exceptions \\ + \textbf{Exceptions} & + \textbf{The input is wrong}: An error message is shown. Go back to step 1 of + main scenario. \\ \midrule \textbf{Notes} & - ---\\ + This scenario does not pretend to be a complete recreation of a go match. It + will be playable, but its main purpose is to see the Game implementation in + action.\newline + A robustness diagram for this scenario is shown in + \fref{fig:useCase_playAMatch}.\\ \bottomrule \end{tabular} \vspace{\interclassSpace} +\begin{figure}[h] + \begin{center} + \includegraphics[width=\textwidth]{diagrams/useCase_generateAMove.png} + \caption{Use case: Generate a move.} + \label{fig:useCase_generateAMove} + \end{center} +\end{figure} + \begin{tabular}{lp{0.7\linewidth}} \toprule \multicolumn{2}{c}{\textbf{Generate a move}} \\ @@ -719,21 +828,21 @@ The classes resulting from the analysis phase are shown in \midrule \textbf{Exceptions} & \textbf{The input is wrong}: An error message is shown. Go back to step 1 of - main scenario.\\ + main scenario. \\ \midrule \textbf{Notes} & - ---\\ + A robustness diagram for this scenario is shown in + \fref{fig:useCase_generateAMove}.\\ \bottomrule \end{tabular} \vspace{\interclassSpace} -\subsubsection{Use as backend for machine player} - \begin{figure}[h] \begin{center} \includegraphics[width=\textwidth]{diagrams/useCase_useAsBackend.png} - \caption{Use as backend for machine player} + \caption{Use case: Use as backend for machine player.} + \label{fig:useCase_useAsBackend} \end{center} \end{figure} @@ -749,10 +858,13 @@ The classes resulting from the analysis phase are shown in \textbf{Actors} & GUI program. \\ \midrule \textbf{Description} & - 1. The program gives commands to the engine. The specific commands will - vary from program to program.\newline - 2. The engine suggest moves to the program.\newline - 3. The moves are shown by the program as if made by another player.\\ + 1. The program gives commands to the engine to set up the game. The + specific commands will vary from program to program.\newline + 2. The program asks the engine for a move.\newline + 3. The engine suggest a move to the program.\newline + 4. The moves are shown by the program as if made by a player.\newline + 5. The opponent gives a move to the program.\newline + 6. Repeat from step 2 until the game ends. \\ \midrule \textbf{Secondary scenarios} & ---\\ @@ -761,10 +873,13 @@ The classes resulting from the analysis phase are shown in ---\\ \midrule \textbf{Notes} & - ---\\ + A robustness diagram for this scenario is shown in + \fref{fig:useCase_useAsBackend}.\\ \bottomrule \end{tabular} +\vspace{\interclassSpace} + \subsection{Testing Plan Specification} \subsubsection{Unitary Testing} -- cgit v1.2.1 From 15faec71e4c3e972c522d6b0c81fe0b1ec7f7811 Mon Sep 17 00:00:00 2001 From: InigoGutierrez Date: Wed, 29 Jun 2022 01:36:07 +0200 Subject: Added error messages to engine IO and pass move to neural networks. --- imago/engine/core.py | 3 +- imago/engine/decisionAlgorithmFactory.py | 2 +- imago/engine/imagoIO.py | 75 +++++++++++++++---------- imago/engine/keras/convNeuralNetwork.py | 2 +- imago/engine/keras/initialDenseNeuralNetwork.py | 28 +++++++++ imago/engine/keras/keras.py | 5 +- imago/engine/keras/neuralNetwork.py | 63 ++++++++++++++------- imago/engine/monteCarlo.py | 2 + imago/engine/parseHelpers.py | 16 ++++-- imago/sgfParser/astNode.py | 8 ++- 10 files changed, 143 insertions(+), 61 deletions(-) create mode 100644 imago/engine/keras/initialDenseNeuralNetwork.py diff --git a/imago/engine/core.py b/imago/engine/core.py index 810b851..4028bb3 100644 --- a/imago/engine/core.py +++ b/imago/engine/core.py @@ -47,6 +47,7 @@ class GameEngine: """Plays in the vertex passed as argument""" if vertex == "pass": self.gameState.passForPlayer(color) + self.da.forceNextMove(vertex) return row = vertex[0] col = vertex[1] @@ -56,7 +57,7 @@ class GameEngine: def genmove(self, color): """Returns a list representing coordinates of the board in the form (row, col).""" coords = self.da.pickMove() - self.play(color, [coords[0], coords[1]]) + self.play(color, coords) return coords def undo(self): diff --git a/imago/engine/decisionAlgorithmFactory.py b/imago/engine/decisionAlgorithmFactory.py index bd86864..094a816 100644 --- a/imago/engine/decisionAlgorithmFactory.py +++ b/imago/engine/decisionAlgorithmFactory.py @@ -5,7 +5,7 @@ from imago.engine.monteCarlo import MCTS from imago.engine.keras.keras import Keras class DecisionAlgorithms(Enum): - RANDOM = enumAuto() + #RANDOM = enumAuto() MONTECARLO = enumAuto() KERAS = enumAuto() diff --git a/imago/engine/imagoIO.py b/imago/engine/imagoIO.py index 6ada674..9b3e367 100644 --- a/imago/engine/imagoIO.py +++ b/imago/engine/imagoIO.py @@ -9,6 +9,10 @@ def _response(text=""): print("= %s" % text) print() +def _responseError(text=""): + print("? %s" % text) + print() + def protocol_version(_): """Version of the GTP Protocol""" _response("2") @@ -51,30 +55,36 @@ class ImagoIO: def start(self): """Starts reading commands interactively.""" - while True: - input_tokens = input().split() - - if input_tokens[0] == "quit": - sys.exit(0) - - command = None - for comm in self.commands_set: - if comm.__name__ == input_tokens[0]: - command = comm - - if command is not None: - arguments = input_tokens[1:] - #print("[DEBUG]:Selected command: %s; args: %s" % (command, arguments)) - command(arguments) - else: - print("unknown command") + try: + while True: + input_tokens = input().split() + + if len(input_tokens) == 0: + continue + + if input_tokens[0] == "quit": + sys.exit(0) + + command = None + for comm in self.commands_set: + if comm.__name__ == input_tokens[0]: + command = comm + + if command is not None: + arguments = input_tokens[1:] + #print("[DEBUG]:Selected command: %s; args: %s" % (command, arguments)) + command(arguments) + else: + _responseError("unknown command") + except Exception as err: + _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: - print ("Wrong number of args.") - print ("Usage: known_command COMMAND_NAME") - sys.exit(0) + _responseError("Wrong number of arguments.") + _responseError("Usage: known_command COMMAND_NAME") + return out = "false" for c in self.commands_set: if c.__name__ == args[0]: @@ -94,8 +104,9 @@ class ImagoIO: It is wise to call clear_board after this command. """ if len(args) != 1: - print("Error - Wrong n of args") - sys.exit(1) + _responseError("Wrong number of arguments") + _responseError("Usag. boardsize ") + return size = int(args[0]) self.gameEngine.setBoardsize(size) _response() @@ -110,8 +121,9 @@ class ImagoIO: def komi(self, args): """Sets a new value of komi.""" if len(args) != 1: - print("Error - Wrong n of args") - sys.exit(1) + _responseError("Wrong number of arguments") + _responseError("Usage: komi ") + return komi = float(args[0]) self.gameEngine.setKomi(komi) _response() @@ -121,8 +133,9 @@ class ImagoIO: These vertices follow the GTP specification. """ if len(args) != 1: - print("Error - Wrong n of args") - sys.exit(1) + _responseError("Wrong number of arguments") + _responseError("Usage: fixed_handicap ") + return stones = float(args[0]) vertices = self.gameEngine.setFixedHandicap(stones) out = getCoordsText(vertices[0][0], vertices[0][1]) @@ -141,8 +154,9 @@ class ImagoIO: def play(self, args): """A stone of the requested color is played at the requested vertex.""" if len(args) != 2: - print("Error - Wrong n of args") - sys.exit(1) + _responseError("Wrong number of arguments.") + _responseError("Usage: play ") + return move = parseHelpers.parseMove(args, self.gameEngine.gameState.size) self.gameEngine.play(move.color, move.vertex) _response() @@ -150,8 +164,9 @@ class ImagoIO: def genmove(self, args): """A stone of the requested color is played where the engine chooses.""" if len(args) != 1: - print("Error - Wrong n of args") - sys.exit(1) + _responseError("Wrong number of arguments.") + _responseError("Usage: genmove ") + return color = parseHelpers.parseColor(args[0]) output = parseHelpers.vertexToString(self.gameEngine.genmove(color), self.gameEngine.gameState.size) diff --git a/imago/engine/keras/convNeuralNetwork.py b/imago/engine/keras/convNeuralNetwork.py index 9d97586..638e2fe 100644 --- a/imago/engine/keras/convNeuralNetwork.py +++ b/imago/engine/keras/convNeuralNetwork.py @@ -38,7 +38,7 @@ class ConvNeuralNetwork(NeuralNetwork): ), Flatten(), Dense( - units=81, + units=82, activation='softmax' ), ]) diff --git a/imago/engine/keras/initialDenseNeuralNetwork.py b/imago/engine/keras/initialDenseNeuralNetwork.py new file mode 100644 index 0000000..dfe8379 --- /dev/null +++ b/imago/engine/keras/initialDenseNeuralNetwork.py @@ -0,0 +1,28 @@ +"""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 0668cd1..00b06d7 100644 --- a/imago/engine/keras/keras.py +++ b/imago/engine/keras/keras.py @@ -18,7 +18,10 @@ class Keras(DecisionAlgorithm): def forceNextMove(self, coords): """Selects given move as next move.""" - self.currentMove = self.currentMove.addMoveByCoords(coords) + if coords == "pass": + self.currentMove = self.currentMove.addPass() + else: + self.currentMove = self.currentMove.addMoveByCoords(coords) def pickMove(self): """Returns a move to play.""" diff --git a/imago/engine/keras/neuralNetwork.py b/imago/engine/keras/neuralNetwork.py index d0eb4ae..7eddb9d 100644 --- a/imago/engine/keras/neuralNetwork.py +++ b/imago/engine/keras/neuralNetwork.py @@ -81,7 +81,7 @@ class NeuralNetwork: def _boardToPlayerContext(self, board, player): """Converts the board to a 3D matrix with two representations of the board, one - marking the player's stones and the oter marking the opponent's stones.""" + marking the player's stones and the other marking the opponent's stones.""" boardRows = len(board) boardCols = len(board[0]) contextBoard = numpy.zeros((boardRows, boardCols, 2), dtype = float) @@ -95,14 +95,19 @@ class NeuralNetwork: return contextBoard def _movesToTargets(self, moves): - """Converts the moves to 2D matrices with values zero except for a one on the - played vertex.""" + """Converts the moves to 2D matrices with values zero except for a one indicating + the played move.""" targets = [] + targetsSize = self.boardSize * self.boardSize + 1 # Each vertex + 1 for pass for move in moves: if len(move.nextMoves) == 0: continue - target = numpy.zeros(self.boardSize * self.boardSize, dtype = float) - target[move.nextMoves[0].getRow() * self.boardSize + move.nextMoves[0].getCol()] = 1 + target = numpy.zeros(targetsSize, dtype = float) + nextMove = move.nextMoves[0] + if nextMove.isPass: + target[-1] = 1 + else: + target[nextMove.getRow() * self.boardSize + nextMove.getCol()] = 1 targets.append(target.tolist()) return targets @@ -110,11 +115,12 @@ class NeuralNetwork: """Uses the model's predict function to pick the highest valued vertex to play.""" predictionVector = self._predict(gameMove, player)[0] - prediction = numpy.zeros((self.boardSize, self.boardSize)) + predictionBoard = numpy.zeros((self.boardSize, self.boardSize)) for row in range(self.boardSize): for col in range(self.boardSize): - prediction[row][col] = predictionVector[row * self.boardSize + col] - self.saveHeatmap(prediction) + predictionBoard[row][col] = predictionVector[row * self.boardSize + col] + predictionPass = predictionVector[-1] + self.saveHeatmap(predictionBoard, predictionPass) # Search the highest valued vertex which is also playable playableVertices = gameMove.getPlayableVertices() @@ -123,11 +129,13 @@ class NeuralNetwork: hCol = -1 for row in range(self.boardSize): for col in range(self.boardSize): - if prediction[row][col] > highest and (row, col) in playableVertices: + if predictionBoard[row][col] > highest and (row, col) in playableVertices: hRow = row hCol = col - highest = prediction[row][col] + highest = predictionBoard[row][col] + if highest < predictionPass: + return "pass" return [hRow, hCol] def _predict(self, gameMove, player): @@ -139,29 +147,42 @@ class NeuralNetwork: batch_size = 1, verbose = 2) - def saveHeatmap(self, data): + def saveHeatmap(self, data, passChance): rows = len(data) cols = len(data[0]) - fig, ax = pyplot.subplots() - im = ax.imshow(data, cmap="YlGn") + fig, (axBoard, axPass) = pyplot.subplots(1, 2, gridspec_kw={'width_ratios': [9, 1]}) + imBoard = axBoard.imshow(data, cmap="YlGn") + axPass.imshow([[passChance]], cmap="YlGn", norm=imBoard.norm) - # Show all ticks and label them with the respective list entries - ax.set_xticks(numpy.arange(cols)) - ax.set_xticklabels(self._getLetterLabels(cols)) - ax.set_yticks(numpy.arange(rows)) - ax.set_yticklabels(numpy.arange(rows, 0, -1)) + # Tick and label the board + axBoard.set_xticks(numpy.arange(cols)) + axBoard.set_xticklabels(self._getLetterLabels(cols)) + axBoard.set_yticks(numpy.arange(rows)) + axBoard.set_yticklabels(numpy.arange(rows, 0, -1)) + + # Label the pass chance + axPass.set_xticks([0]) + axPass.set_yticks([]) + axPass.set_xticklabels(["Pass"]) # Loop over data dimensions and create text annotations. - textColorThreshold = 0.35 + textColorThreshold = data.max() / 2 for row in range(rows): for col in range(cols): textColor = ("k" if data[row, col] < textColorThreshold else "w") - ax.text(col, row, "%.2f"%(data[row, col]), + axBoard.text(col, row, "%.2f"%(data[row, col]), ha="center", va="center", color=textColor) - ax.set_title("Heat map of move likelihood") + textColor = ("k" if passChance < textColorThreshold else "w") + axPass.text(0, 0, "%.2f"%(passChance), + ha="center", va="center", color=textColor) + + pyplot.suptitle("Heat map of move likelihood") + #axBoard.set_title("Heat map of move likelihood") fig.tight_layout() + + #pyplot.show() pyplot.savefig("heatmaps/heatmap_%s_%s_%d.png" % ( self.NETWORK_ID, diff --git a/imago/engine/monteCarlo.py b/imago/engine/monteCarlo.py index 81bee1d..0587cfc 100644 --- a/imago/engine/monteCarlo.py +++ b/imago/engine/monteCarlo.py @@ -14,6 +14,8 @@ class MCTS(DecisionAlgorithm): 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]): diff --git a/imago/engine/parseHelpers.py b/imago/engine/parseHelpers.py index 59e0496..fd5ea63 100644 --- a/imago/engine/parseHelpers.py +++ b/imago/engine/parseHelpers.py @@ -51,7 +51,9 @@ def parseVertex(text, boardSize): if not re.match("^[A-HJ-Z][1-9][0-9]*$", text): if text == "PASS": return "pass" - return ParseCodes.ERROR + raise RuntimeError( + "Unable to transform string %s to vertex. Wrong format." + % text) vertexCol = ord(text[0]) # Column 'I' does not exist @@ -63,7 +65,9 @@ def parseVertex(text, boardSize): if (vertexCol < 0 or vertexRow < 0 or vertexCol >= boardSize or vertexRow >= boardSize): - return ParseCodes.ERROR + raise RuntimeError( + "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] @@ -76,9 +80,13 @@ def vertexToString(vertex, boardSize): if vertex == "pass": return "pass" if len(vertex) != 2: - return ParseCodes.ERROR + raise RuntimeError( + "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: - return ParseCodes.ERROR + raise RuntimeError( + "Unable to transform vertex %s to string. Vertex out of board bounds (size %d)" + % (str(vertex), boardSize)) vertexRow = boardSize - vertex[0] vertexCol = ord('A') + vertex[1] diff --git a/imago/sgfParser/astNode.py b/imago/sgfParser/astNode.py index ff0c517..41629ce 100644 --- a/imago/sgfParser/astNode.py +++ b/imago/sgfParser/astNode.py @@ -5,7 +5,7 @@ from imago.data.enums import Player class ASTNode: """Abstract Syntax Tree Node of SGF parser""" - def __init__(self, children=None, leaf=None, props=None): + def __init__(self, children=None, props=None): if children: self.children = children else: @@ -14,7 +14,6 @@ class ASTNode: self.props = props else: self.props = [] - self.leaf = leaf def addToSequence(self, move): """Appends a move to the last of the sequence started by this move""" @@ -104,6 +103,11 @@ class ASTNode: gameMove = previousMove.addPassForPlayer(player) for child in self.children: child.toGameMoveTree(previousMove=gameMove) + # Add a couple passes at the end of the match since they are not usually recorded + if len(self.children) == 0 and not gameMove.isPass: + nextMove = gameMove.addPass() + nextMove.addPass() + return gameMove def hasProperty(self, propertyName): -- cgit v1.2.1 From 080ef02ab7abc46eae1e2a550d26fdcb6c87450f Mon Sep 17 00:00:00 2001 From: InigoGutierrez Date: Wed, 29 Jun 2022 14:32:57 +0200 Subject: Updated some tests. --- go.py | 9 ++++--- imago/engine/core.py | 2 +- imago/engine/parseHelpers.py | 19 +++++-------- imago/gameLogic/gameBoard.py | 6 ++--- tests/test_enums.py | 15 +++++++++++ tests/test_gameBoard.py | 20 +++++++++++--- tests/test_parseHelpers.py | 63 ++++++++++++++++++++++++-------------------- 7 files changed, 82 insertions(+), 52 deletions(-) create mode 100644 tests/test_enums.py diff --git a/go.py b/go.py index 96f7052..d4efc1b 100755 --- a/go.py +++ b/go.py @@ -2,7 +2,7 @@ """Play Go!""" -from imago.engine.parseHelpers import ParseCodes, parseVertex +from imago.engine.parseHelpers import parseVertex from imago.gameLogic.gameState import GameState if __name__ == "__main__": @@ -10,14 +10,15 @@ if __name__ == "__main__": GAMESTATE = GameState(5) #GAMESTATE = GameState(19) - while 1: + while True: GAMESTATE.getBoard().printBoard() move = input("Move (" + GAMESTATE.getPlayerCode() + "): ") - move = parseVertex(move, GAMESTATE.size) - if move == ParseCodes.ERROR: + try: + move = parseVertex(move, GAMESTATE.size) + except Exception as err: print("Invalid move syntax. Example of move: A1") continue diff --git a/imago/engine/core.py b/imago/engine/core.py index 4028bb3..4e383d4 100644 --- a/imago/engine/core.py +++ b/imago/engine/core.py @@ -3,7 +3,7 @@ from imago.engine.decisionAlgorithmFactory import DecisionAlgorithms, DecisionAlgorithmFactory from imago.gameLogic.gameState import GameState -DEF_SIZE = 7 +DEF_SIZE = 9 DEF_KOMI = 5.5 DEF_ALGORITHM = DecisionAlgorithms.KERAS diff --git a/imago/engine/parseHelpers.py b/imago/engine/parseHelpers.py index fd5ea63..fc42845 100644 --- a/imago/engine/parseHelpers.py +++ b/imago/engine/parseHelpers.py @@ -15,29 +15,24 @@ VALID_BLACK_STRINGS = { "BLACK" } -class ParseCodes(Enum): - """Return values of the move parser.""" - ERROR = enumAuto() - QUIT = enumAuto() - def parseMove(args, boardsize): """Converts the textual representation of a move to a move instance.""" if len(args) != 2: - print("[ERROR] - Wrong n of args for move") - return ParseCodes.ERROR + raise RuntimeError( + "Unable to transform string %s to move: Wrong format." + % args) color = parseColor(args[0]) vertex = parseVertex(args[1], boardsize) return GtpMove(color, vertex) def parseColor(text): """Returns color of a move given its input string.""" - text = text.upper() - if text in VALID_WHITE_STRINGS: + textUpper = text.upper() + if textUpper in VALID_WHITE_STRINGS: return Player.WHITE - if text in VALID_BLACK_STRINGS: + if textUpper in VALID_BLACK_STRINGS: return Player.BLACK - print("[ERROR] - Unknown color.") - return ParseCodes.ERROR + 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 diff --git a/imago/gameLogic/gameBoard.py b/imago/gameLogic/gameBoard.py index 9054a18..611c4cb 100644 --- a/imago/gameLogic/gameBoard.py +++ b/imago/gameLogic/gameBoard.py @@ -51,14 +51,14 @@ class GameBoard: return set() emptyCells = set() exploredCells = set() - self.__exploreLiberties(row, col, groupColor, emptyCells, exploredCells) + self._exploreLiberties(row, col, groupColor, emptyCells, exploredCells) return emptyCells def getGroupLibertiesCount(self, row, col): """Returns the number of liberties of a group.""" return len(self.getGroupLiberties(row, col)) - def __exploreLiberties(self, row, col, groupColor, emptyCells, exploredCells): + def _exploreLiberties(self, row, col, groupColor, emptyCells, exploredCells): """Adds surrounding empty cells to array if they have not been added yet and explores adjacent occupied cells of the same group. """ @@ -78,7 +78,7 @@ class GameBoard: rowToExplore = row + side[0] colToExplore = col + side[1] if self.isMoveInBoardBounds(rowToExplore, colToExplore): - self.__exploreLiberties(rowToExplore, colToExplore, groupColor, + self._exploreLiberties(rowToExplore, colToExplore, groupColor, emptyCells, exploredCells) def getGroupVertices(self, row, col): diff --git a/tests/test_enums.py b/tests/test_enums.py new file mode 100644 index 0000000..73f4009 --- /dev/null +++ b/tests/test_enums.py @@ -0,0 +1,15 @@ +"""Tests for enums module.""" + +import unittest + +from imago.data.enums import Player + +class TestEnums(unittest.TestCase): + """Test parseHelpers module.""" + + def testOtherPlayer(self): + """Test method to get the other player""" + + self.assertEqual(Player.otherPlayer(Player.BLACK), Player.WHITE) + self.assertEqual(Player.otherPlayer(Player.WHITE), Player.BLACK) + self.assertEqual(Player.otherPlayer(''), Player.EMPTY) diff --git a/tests/test_gameBoard.py b/tests/test_gameBoard.py index 3a4d5a0..8a7b127 100644 --- a/tests/test_gameBoard.py +++ b/tests/test_gameBoard.py @@ -10,20 +10,33 @@ TEST_BOARD_SIZE = 19 class TestGameBoard(unittest.TestCase): """Test gameBoard module.""" - def testGetGroupLiberties(self): - """Test calculation of group liberties.""" + def testGetGroupCounts(self): + """Test calculation of group stones and liberties.""" board = GameBoard(TEST_BOARD_SIZE, TEST_BOARD_SIZE) #Empty cell liberties self.assertEqual(board.getGroupLiberties(0,0), set()) self.assertEqual(board.getGroupLibertiesCount(0,0), 0) - # Lone stone liberties + # Lone stone board.board[3][3] = Player.WHITE + self.assertEqual(board.getGroupVertices(3,3), + {(3,3)}) + self.assertEqual(board.getGroupVerticesCount(3,3), 1) self.assertEqual(board.getGroupLiberties(3,3), {(2,3), (3,2), (4,3), (3,4)}) self.assertEqual(board.getGroupLibertiesCount(3,3), 4) + # L group (don't compute twice liberty inside L) + board.board[3][4] = Player.WHITE + board.board[2][4] = Player.WHITE + self.assertEqual(board.getGroupVertices(3,3), + {(3,3), (3,4), (2,4)}) + self.assertEqual(board.getGroupVerticesCount(3,3), 3) + self.assertEqual(board.getGroupLiberties(3,3), + {(2,3), (3,2), (4,3), (4, 4), (3,5), (2,5), (1,4)}) + self.assertEqual(board.getGroupLibertiesCount(3,3), 7) + def testIsCellEye(self): """Tests the isCellEye method.""" board = GameBoard(TEST_BOARD_SIZE, TEST_BOARD_SIZE) @@ -101,6 +114,5 @@ class TestGameBoard(unittest.TestCase): board.board[9][0] = Player.WHITE self.assertEqual((9, 21), board.score()) - if __name__ == '__main__': unittest.main() diff --git a/tests/test_parseHelpers.py b/tests/test_parseHelpers.py index d03f253..7bbf152 100644 --- a/tests/test_parseHelpers.py +++ b/tests/test_parseHelpers.py @@ -13,16 +13,19 @@ class TestParseHelpers(unittest.TestCase): def testParseMove(self): """Test parsing of GtpMove""" - self.assertEqual(parseHelpers.parseMove(["B"], TEST_BOARD_SIZE), - parseHelpers.ParseCodes.ERROR) - self.assertEqual(parseHelpers.parseMove(["A1"], TEST_BOARD_SIZE), - parseHelpers.ParseCodes.ERROR) - self.assertEqual(parseHelpers.parseMove(["B", "A1", "W"], TEST_BOARD_SIZE), - parseHelpers.ParseCodes.ERROR) - + wrongMoves = [ + ["B"], + ["A1"], + ["B", "A1", "W"] + ] + for move in wrongMoves: + self.assertRaises( + RuntimeError, + parseHelpers.parseMove, + move, TEST_BOARD_SIZE + ) parsedMove = parseHelpers.parseMove(["B", "t1"], TEST_BOARD_SIZE) - targetMove = parseHelpers.GtpMove(Player.BLACK, [18, 18]) self.assertEqual(parsedMove.color, targetMove.color) self.assertEqual(parsedMove.vertex, targetMove.vertex) @@ -33,15 +36,19 @@ class TestParseHelpers(unittest.TestCase): self.assertEqual(parseHelpers.parseColor("B"), Player.BLACK) self.assertEqual(parseHelpers.parseColor("w"), Player.WHITE) self.assertEqual(parseHelpers.parseColor("W"), Player.WHITE) - self.assertEqual(parseHelpers.parseColor("bw"), parseHelpers.ParseCodes.ERROR) - self.assertEqual(parseHelpers.parseColor("wb"), parseHelpers.ParseCodes.ERROR) + + self.assertRaises(RuntimeError, parseHelpers.parseColor, "bw") + self.assertRaises(RuntimeError, parseHelpers.parseColor, "wb") def testParseVertexWrongInputs(self): """Test wrong inputs for parseVertex.""" inputs = ( "a", "1", "1a", "aa1", "a0", "u1", "a"+str(TEST_BOARD_SIZE+1) ) for text in inputs: - self.assertEqual(parseHelpers.parseVertex(text, TEST_BOARD_SIZE), - parseHelpers.ParseCodes.ERROR) + self.assertRaises( + RuntimeError, + parseHelpers.parseVertex, + text, TEST_BOARD_SIZE + ) def testParseVertexCorrectInputs(self): """Test correct inputs and their resulting coordinates for parseVertex.""" @@ -85,22 +92,22 @@ class TestParseHelpers(unittest.TestCase): self.assertEqual(parseHelpers.vertexToString("pass", TEST_BOARD_SIZE), "pass") - self.assertEqual(parseHelpers.vertexToString([-1,0], TEST_BOARD_SIZE), - parseHelpers.ParseCodes.ERROR) - self.assertEqual(parseHelpers.vertexToString([0,-1], TEST_BOARD_SIZE), - parseHelpers.ParseCodes.ERROR) - self.assertEqual(parseHelpers.vertexToString([-1,-1], TEST_BOARD_SIZE), - parseHelpers.ParseCodes.ERROR) - self.assertEqual(parseHelpers.vertexToString([19,0], TEST_BOARD_SIZE), - parseHelpers.ParseCodes.ERROR) - self.assertEqual(parseHelpers.vertexToString([0,19], TEST_BOARD_SIZE), - parseHelpers.ParseCodes.ERROR) - self.assertEqual(parseHelpers.vertexToString([19,19], TEST_BOARD_SIZE), - parseHelpers.ParseCodes.ERROR) - self.assertEqual(parseHelpers.vertexToString([0], TEST_BOARD_SIZE), - parseHelpers.ParseCodes.ERROR) - self.assertEqual(parseHelpers.vertexToString([0,0,0], TEST_BOARD_SIZE), - parseHelpers.ParseCodes.ERROR) + wrongVertices = [ + [-1,0], + [0,-1], + [-1,-1], + [19,0], + [0,19], + [19,19], + [0], + [0,0,0] + ] + for vertex in wrongVertices: + self.assertRaises( + RuntimeError, + parseHelpers.vertexToString, + vertex, TEST_BOARD_SIZE + ) if __name__ == '__main__': unittest.main() -- cgit v1.2.1 From c25a5d482937fc861e9d1cfc1ff36e479fcb2fb4 Mon Sep 17 00:00:00 2001 From: InigoGutierrez Date: Wed, 29 Jun 2022 14:33:37 +0200 Subject: Writing implementation section. --- doc/tex/biber.bib | 36 +++++++++++++++++++++++++ doc/tex/implementation.tex | 66 ++++++++++++++++++++++++++++++++++++++++++++++ doc/tex/planification.tex | 10 +++++-- doc/tex/tfg.tex | 2 +- 4 files changed, 111 insertions(+), 3 deletions(-) diff --git a/doc/tex/biber.bib b/doc/tex/biber.bib index 055ca8f..f22e058 100644 --- a/doc/tex/biber.bib +++ b/doc/tex/biber.bib @@ -46,6 +46,12 @@ url = {https://sabaki.yichuanshen.de} } +@online{keras, + title = {Keras: the Python deep learning API}, + urldate = {2022}, + url = {https://keras.io} +} + @online{sl_go, title = {Go}, organization = {Sensei's Library}, @@ -78,3 +84,33 @@ urldate = {2022}, url = {https://matplotlib.org/stable/gallery/images_contours_and_fields/image_annotated_heatmap.html} } + +@online{python, + title = {Welcome to Python.org}, + urldate = {2022}, + url = {https://www.python.org} +} + +@online{numpy, + title = {NumPy}, + urldate = {2022}, + url = {https://numpy.org} +} + +@online{matplotlib, + title = {Matplotlib — Visualization with Python}, + urldate = {2022}, + url = {https://matplotlib.org} +} + +@online{ply, + title = {PLY (Python Lex-Yacc)}, + urldate = {2022}, + url = {http://www.dabeaz.com/ply} +} + +@online{neovim, + title = {Home - Neovim}, + urldate = {2022}, + url = {http://neovim.io} +} diff --git a/doc/tex/implementation.tex b/doc/tex/implementation.tex index 297ba0e..40dfb93 100644 --- a/doc/tex/implementation.tex +++ b/doc/tex/implementation.tex @@ -1,5 +1,71 @@ \section{Implementation} +\subsection{Development environment} + +\subsubsection{Language} + +The programming language of choice is Python\cite{python}. The rationale behind +this decision has been stated on Section \ref{sec:programmingLanguage}. It also +allows easy use of the Keras library for implementing neural networks. + +Various python libraries have been used to easy the development process or +assist in the analysis of results. These are: + +\paragraph{Keras/Tensorflow\cite{keras}} + +Tensorflow is a platform for machine learning which provides a diverse range of +tools, one of which is a python library for machine learning. + +Keras is a high-level API for Tensorflow allowing for the easy definition of +neural networks. It permits easily testing and comparing different network +layouts. + +\paragraph{NumPy\cite{numpy}} + +NumPy is a scientific package for python providing a lot of mathematical tools. +The most interesting for this project are its capabilities to create and +transform matrices. + +\paragraph{Matplotlib\cite{matplotlib}} + +Matplotlib is a python library for creating graphs and other visualizations. It +is used to show the likelihood of moves the neural networks of the project +create from a board configuration. + +\paragraph{PLY\cite{ply}} + +PLY is a tool for generating compilers in Python. It is an implementation of the +lex and yacc utilities, allowing to create lexers and parsers. It is used in the +project to create the SGF parser which transform SGF files to internal +representations of Go matches. + +\paragraph{Other utility libraries} + +These are some utility libraries commonly used for frequent programming tasks: + +\begin{itemize} + \item \textbf{sys}, to stop the execution of the program or access system info such + as primitives maximum values. + \item \textbf{os}, to interact with files. + \item \textbf{re}, to check strings with regular expressions. + \item \textbf{random}, to get random values, for example to obtain a random + item from a list. + \item \textbf{copy}, to obtain deep copies of multidimensional arrays. +\end{itemize} + +\subsubsection{Development tools} + +\paragraph{Neovim\cite{neovim}} + +\begin{itemize} + + \item Extensions + \item Extensions + +\end{itemize} + +% Old stuff starts here + \subsection{Engine} An implementation of GTP, that is, the piece of software which offers the GTP diff --git a/doc/tex/planification.tex b/doc/tex/planification.tex index 945c610..00695a1 100644 --- a/doc/tex/planification.tex +++ b/doc/tex/planification.tex @@ -145,13 +145,19 @@ Sabaki is a go board software compatible with GTP engines. It can serve as a GUI for the engine developed in this project and as an example of the advantages of following a standardized protocol. +\subsubsection{Keras~\cite{keras}} + +Keras is a deep learning API for Python allowing for the high-level definition +of neural networks. This permits easily testing and comparing different network +layouts. + \subsection{Technological Infrastructure} -\subsubsection{Programming language} +\subsubsection{Programming language}\label{sec:programmingLanguage} The resulting product of this project will be one or more pieces of software able to be run locally on a personal computer. The programming language of -choice is Python, for various reasons: +choice is Python\cite{python}, for various reasons: \begin{itemize} diff --git a/doc/tex/tfg.tex b/doc/tex/tfg.tex index 22ee0e9..f387064 100644 --- a/doc/tex/tfg.tex +++ b/doc/tex/tfg.tex @@ -122,7 +122,7 @@ inclusion to use this template is included here. \input{tex/systemAnalysis.tex} -\input{tex/systemDesign.tex} +%\input{tex/systemDesign.tex} \input{tex/implementation.tex} -- cgit v1.2.1 From 6724aeb3ba98c1b9f042344734c2d683e79dfc64 Mon Sep 17 00:00:00 2001 From: InigoGutierrez Date: Wed, 29 Jun 2022 23:23:09 +0200 Subject: Made full class diagram. --- .gitignore | 1 + doc/Makefile | 8 +- doc/diagrams/ASTNode.pumlc | 12 ++- doc/diagrams/ConvNeuralNetwork.pumlc | 7 ++ doc/diagrams/DecisionAlgorithm.pumlc | 5 +- doc/diagrams/DenseNeuralNetwork.pumlc | 7 ++ doc/diagrams/GameBoard.pumlc | 37 ++++---- doc/diagrams/GameEngine.pumlc | 18 ++++ doc/diagrams/GameMove.pumlc | 29 ++++--- doc/diagrams/GameState.pumlc | 18 ++-- doc/diagrams/ImagoIO.pumlc | 10 +++ doc/diagrams/Keras.pumlc | 3 + doc/diagrams/MCTS.pumlc | 11 +++ doc/diagrams/MCTSNode.pumlc | 18 ++++ doc/diagrams/NeuralNetwork.pumlc | 9 +- doc/diagrams/Property.pumlc | 10 +++ doc/diagrams/SGF.pumlc | 5 +- doc/diagrams/analysisClasses.puml | 2 +- doc/diagrams/engineModule.puml | 32 +++++++ doc/diagrams/fullClasses.puml | 16 ++++ doc/diagrams/gameModule.puml | 18 ++++ doc/diagrams/gtpEngine.puml | 36 -------- doc/diagrams/skinparams.puml | 7 ++ doc/diagrams/trainingModule.puml | 20 +++++ doc/tex/biber.bib | 18 ++++ doc/tex/implementation.tex | 140 +++++++++++-------------------- doc/tex/systemDesign.tex | 93 ++++++++++++++++++++ doc/tex/tfg.tex | 10 ++- go.py | 2 - imago/data/enums.py | 5 ++ imago/engine/decisionAlgorithmFactory.py | 5 -- imago/engine/keras/neuralNetwork.py | 10 +-- imago/gameLogic/gameState.py | 4 +- 33 files changed, 429 insertions(+), 197 deletions(-) create mode 100644 doc/diagrams/ConvNeuralNetwork.pumlc create mode 100644 doc/diagrams/DenseNeuralNetwork.pumlc create mode 100644 doc/diagrams/GameEngine.pumlc create mode 100644 doc/diagrams/ImagoIO.pumlc create mode 100644 doc/diagrams/MCTS.pumlc create mode 100644 doc/diagrams/MCTSNode.pumlc create mode 100644 doc/diagrams/Property.pumlc create mode 100644 doc/diagrams/engineModule.puml create mode 100644 doc/diagrams/fullClasses.puml create mode 100644 doc/diagrams/gameModule.puml delete mode 100644 doc/diagrams/gtpEngine.puml create mode 100644 doc/diagrams/trainingModule.puml diff --git a/.gitignore b/.gitignore index 46fefef..47e0340 100644 --- a/.gitignore +++ b/.gitignore @@ -9,6 +9,7 @@ *.pdf doc/out/ doc/diagrams/*.png +_minted-tfg/ # src __pycache__/ diff --git a/doc/Makefile b/doc/Makefile index e849a01..497e9b1 100644 --- a/doc/Makefile +++ b/doc/Makefile @@ -3,17 +3,17 @@ docName = tfg outputFolder = out -texFiles = tex/tfg.tex tex/introduction.tex tex/planification.tex tex/implementation.tex tex/systemAnalysis.tex tex/systemDesign.tex tex/biber.bib +texFiles = tex/tfg.tex tex/introduction.tex tex/planification.tex tex/implementation.tex tex/systemAnalysis.tex tex/biber.bib Makefile -diagramImgs = diagrams/gameRepresentation.png diagrams/gtpEngine.png diagrams/modules.png diagrams/planificationWorkPlanEngine.png diagrams/planificationWorkPlanGame.png diagrams/sgfModule.png diagrams/useCases.png diagrams/analysisClasses.png diagrams/useCase_generateAMove.png diagrams/useCase_useAsBackend.png diagrams/useCase_playAMatch.png diagrams/interfaces.png +diagramImgs = diagrams/gameRepresentation.png diagrams/planificationWorkPlanEngine.png diagrams/planificationWorkPlanGame.png diagrams/sgfModule.png diagrams/useCases.png diagrams/analysisClasses.png diagrams/useCase_generateAMove.png diagrams/useCase_useAsBackend.png diagrams/useCase_playAMatch.png diagrams/interfaces.png diagrams/engineModule.png diagrams/modules.png diagrams/fullClasses.png all: $(docName).pdf $(docName).pdf: $(texFiles) $(diagramImgs) [ -d $(outputFolder) ] || mkdir $(outputFolder) - xelatex -output-directory $(outputFolder) tex/$(docName).tex + xelatex -shell-escape -output-directory $(outputFolder) tex/$(docName).tex biber $(outputFolder)/$(docName) - xelatex -output-directory $(outputFolder) tex/$(docName).tex + xelatex -shell-escape -output-directory $(outputFolder) tex/$(docName).tex mv $(outputFolder)/$(docName).pdf . .puml.png: diff --git a/doc/diagrams/ASTNode.pumlc b/doc/diagrams/ASTNode.pumlc index 05c13ac..945b24d 100644 --- a/doc/diagrams/ASTNode.pumlc +++ b/doc/diagrams/ASTNode.pumlc @@ -1,10 +1,14 @@ @startuml class ASTNode { - ASTNode[] children - Property properties - void addtoSequence() - GameTree toGameTree() + ASTNode[] children + Property[] props + + addtoSequence() + toGameTree() + toGameMoveTree(previousMove) + hasProperty(propertyName) + getPropertyValue(propertyName) } @enduml diff --git a/doc/diagrams/ConvNeuralNetwork.pumlc b/doc/diagrams/ConvNeuralNetwork.pumlc new file mode 100644 index 0000000..2254e5d --- /dev/null +++ b/doc/diagrams/ConvNeuralNetwork.pumlc @@ -0,0 +1,7 @@ +@startuml + +class ConvNeuralNetwork { + +} + +@enduml diff --git a/doc/diagrams/DecisionAlgorithm.pumlc b/doc/diagrams/DecisionAlgorithm.pumlc index aada4f0..c3e9e8a 100644 --- a/doc/diagrams/DecisionAlgorithm.pumlc +++ b/doc/diagrams/DecisionAlgorithm.pumlc @@ -1,8 +1,9 @@ @startuml interface DecisionAlgorithm { - {abstract} forceNextMove(self, coords) - {abstract} pickMove(self) + {abstract} forceNextMove(coords) + {abstract} pickMove() + {abstract} clearBoard() } @enduml diff --git a/doc/diagrams/DenseNeuralNetwork.pumlc b/doc/diagrams/DenseNeuralNetwork.pumlc new file mode 100644 index 0000000..a9e7d1c --- /dev/null +++ b/doc/diagrams/DenseNeuralNetwork.pumlc @@ -0,0 +1,7 @@ +@startuml + +class DenseNeuralNetwork { + +} + +@enduml diff --git a/doc/diagrams/GameBoard.pumlc b/doc/diagrams/GameBoard.pumlc index 7a57b2d..ebeedd7 100644 --- a/doc/diagrams/GameBoard.pumlc +++ b/doc/diagrams/GameBoard.pumlc @@ -5,24 +5,25 @@ class GameBoard { int capturesBlack int capturesWhite - getBoard(self) - getBoardHeight(self) - getBoardWidth(self) - getDeepCopy(self) - getGroupLiberties(self, row, col) - getGroupLibertiesCount(self, row, col) - getGroupCells(self, row, col) - getGroupCellsCount(self, row, col) - moveAndCapture(self, row, col, player) - isMoveInBoardBounds(self, row, col) - isCellEmpty(self, row, col) - isCellEye(self, row, col) - isMoveSuicidal(self, row, col, player) - isMoveKoIllegal(self, row, col, player, prevBoards) - isPlayable(self, row, col, player, prevBoards) - score(self) - equals(self, otherBoard) - printBoard(self) + getBoard() + getBoardHeight() + getBoardWidth() + getDeepCopy() + getGroupLiberties(row, col) + getGroupLibertiesCount(row, col) + getGroupVertices(row, col) + getGroupVerticesCount(row, col) + moveAndCapture(row, col, player) + isMoveInBoardBounds(row, col) + isCellEmpty(row, col) + isCellEye(row, col) + isMoveSuicidal(row, col, player) + isMoveKoIllegal(row, col, player, prevBoards) + isPlayable(row, col, player, prevBoards) + isSensible(row, col, player, prevBoards) + score() + equals(otherBoard) + printBoard() } @enduml diff --git a/doc/diagrams/GameEngine.pumlc b/doc/diagrams/GameEngine.pumlc new file mode 100644 index 0000000..62b892e --- /dev/null +++ b/doc/diagrams/GameEngine.pumlc @@ -0,0 +1,18 @@ +@startuml + +class GameEngine { + int komi + GameState gameState + Enum daId + DecisionAlgorithm da + + setBoardsize(newSize) + clearBoard() + setKomi(komi) + setFixedHandicap(stones) + play(color, vertex) + genmove(color) + undo() +} + +@enduml diff --git a/doc/diagrams/GameMove.pumlc b/doc/diagrams/GameMove.pumlc index 0f75edd..a1d0d73 100644 --- a/doc/diagrams/GameMove.pumlc +++ b/doc/diagrams/GameMove.pumlc @@ -5,19 +5,24 @@ class GameMove { GameMove[] nextMoves GameMove previousMove boolean isPass - int[2] coords + int[] coords + Player playerWhoPassed - getRow(self) - getCol(self) - getPlayer(self) - getNextPlayer(self) - getGameLength(self) - getThisAndPrevBoards(self) - getPlayableVertices(self) - addMove(self, row, col) - addMoveForPlayer(self, row, col, player) - addPass(self) - printBoard(self) + getRow() + getCol() + getPlayer() + getNextPlayer() + getGameLength() + getThisAndPrevBoards() + getPlayableVertices() + getSensibleVertices() + addMove(row, col) + addMoveBcoords(coords) + addMoveForPlayer(row, col, player) + addPass() + addPassForPlayer() + getMainLineOfPlay() + printBoard() } @enduml diff --git a/doc/diagrams/GameState.pumlc b/doc/diagrams/GameState.pumlc index 38e1397..a913855 100644 --- a/doc/diagrams/GameState.pumlc +++ b/doc/diagrams/GameState.pumlc @@ -2,17 +2,17 @@ class GameState { int size - GameTree gameTree GameMove lastMove - 'GameData gameData - getCurrentPlayer(self) - getPlayerCode(self) - getBoard(self) - playMove(self, row, col) - playMoveForPlayer(self, row, col, player) - playPass(self) - undo(self) + getCurrentPlayer() + getPlayerCode() + getBoard() + clearBoard() + playMove(row, col) + playMoveForPlayer(row, col, player) + playPass() + playPassForPlayer(player) + undo() } @enduml diff --git a/doc/diagrams/ImagoIO.pumlc b/doc/diagrams/ImagoIO.pumlc new file mode 100644 index 0000000..848a173 --- /dev/null +++ b/doc/diagrams/ImagoIO.pumlc @@ -0,0 +1,10 @@ +@startuml + +class ImagoIO { + function[] commands_set + GameEngine gameEngine + + start() +} + +@enduml diff --git a/doc/diagrams/Keras.pumlc b/doc/diagrams/Keras.pumlc index daca149..1fa40b2 100644 --- a/doc/diagrams/Keras.pumlc +++ b/doc/diagrams/Keras.pumlc @@ -1,6 +1,9 @@ @startuml class Keras { + GameMove currentMove + NeuralNetwork neuralNetwork + forceNextMove(self, coords) pickMove(self) loadNetwork(self) diff --git a/doc/diagrams/MCTS.pumlc b/doc/diagrams/MCTS.pumlc new file mode 100644 index 0000000..ff00c62 --- /dev/null +++ b/doc/diagrams/MCTS.pumlc @@ -0,0 +1,11 @@ +@startuml + +class MCTS { + MCTSNode root + + forceNextMove(coords) + pickMove() + clearBoard() +} + +@enduml diff --git a/doc/diagrams/MCTSNode.pumlc b/doc/diagrams/MCTSNode.pumlc new file mode 100644 index 0000000..6ae8f35 --- /dev/null +++ b/doc/diagrams/MCTSNode.pumlc @@ -0,0 +1,18 @@ +@startuml + +class MCTSNode { + int visits + int score + GameMove move + MCTSNode parent + MCTSNode[] children + (int, int)[] unexploredVertices + + ucbForPlayer() + selection() + expansion() + expansionForCoords(coords) + simulation(nMatches, scoreDiffHeur) +} + +@enduml diff --git a/doc/diagrams/NeuralNetwork.pumlc b/doc/diagrams/NeuralNetwork.pumlc index 30f783e..f6c0e1c 100644 --- a/doc/diagrams/NeuralNetwork.pumlc +++ b/doc/diagrams/NeuralNetwork.pumlc @@ -1,8 +1,13 @@ @startuml class NeuralNetwork { - load(self, path) - save(self, path) + int boardSize + string path + + trainModel(games) + saveModel(modelPath) + pickMove(gameMove, player) + saveModelPlot(path) } @enduml diff --git a/doc/diagrams/Property.pumlc b/doc/diagrams/Property.pumlc new file mode 100644 index 0000000..bb06b47 --- /dev/null +++ b/doc/diagrams/Property.pumlc @@ -0,0 +1,10 @@ +@startuml + +class Property { + string name + object value + + addValue(value) +} + +@enduml diff --git a/doc/diagrams/SGF.pumlc b/doc/diagrams/SGF.pumlc index 2c30202..5ab248a 100644 --- a/doc/diagrams/SGF.pumlc +++ b/doc/diagrams/SGF.pumlc @@ -1,8 +1,7 @@ @startuml -class SGF { - GameTree loadGameTree(file) - void saveGameTree(file) +object SGF { + loadGameTree(file) } @enduml diff --git a/doc/diagrams/analysisClasses.puml b/doc/diagrams/analysisClasses.puml index 8273930..7685ea1 100644 --- a/doc/diagrams/analysisClasses.puml +++ b/doc/diagrams/analysisClasses.puml @@ -1,6 +1,6 @@ @startuml -'!include skinparams.puml +!include skinparams.puml () Player package "Game module" { diff --git a/doc/diagrams/engineModule.puml b/doc/diagrams/engineModule.puml new file mode 100644 index 0000000..c6f3a3e --- /dev/null +++ b/doc/diagrams/engineModule.puml @@ -0,0 +1,32 @@ +@startuml + +!include skinparams.puml + +package "Engine module" { + + !include ImagoIO.pumlc + !include GameEngine.pumlc + !include DecisionAlgorithm.pumlc + !include MCTS.pumlc + !include MCTSNode.pumlc + !include Keras.pumlc + !include NeuralNetwork.pumlc + !include DenseNeuralNetwork.pumlc + !include ConvNeuralNetwork.pumlc + + ImagoIO ..> GameEngine + GameEngine ..> DecisionAlgorithm + + DecisionAlgorithm <|.. MCTS + MCTSNode <. MCTS + MCTSNode -> MCTSNode + MCTSNode o--> MCTSNode + + DecisionAlgorithm <|.. Keras + Keras ..> NeuralNetwork + NeuralNetwork <|-- DenseNeuralNetwork + NeuralNetwork <|-- ConvNeuralNetwork + +} + +@enduml diff --git a/doc/diagrams/fullClasses.puml b/doc/diagrams/fullClasses.puml new file mode 100644 index 0000000..d7fe4d8 --- /dev/null +++ b/doc/diagrams/fullClasses.puml @@ -0,0 +1,16 @@ +@startuml + +!include skinparams.puml + +!include gameModule.puml +!include engineModule.puml +!include trainingModule.puml + +GameEngine --> GameState + +MCTSNode --> GameMove +Keras --> GameMove + +ASTNode --> GameMove + +@enduml diff --git a/doc/diagrams/gameModule.puml b/doc/diagrams/gameModule.puml new file mode 100644 index 0000000..9a60d3f --- /dev/null +++ b/doc/diagrams/gameModule.puml @@ -0,0 +1,18 @@ +@startuml + +!include skinparams.puml + +package "Game module" { + + !include GameState.pumlc + !include GameMove.pumlc + !include GameBoard.pumlc + + GameState ..> GameMove + GameMove -> GameMove : Previous move + GameMove o--> GameMove : Next moves + GameMove ..> GameBoard + +} + +@enduml diff --git a/doc/diagrams/gtpEngine.puml b/doc/diagrams/gtpEngine.puml deleted file mode 100644 index 5b098da..0000000 --- a/doc/diagrams/gtpEngine.puml +++ /dev/null @@ -1,36 +0,0 @@ -@startuml - -!include skinparams.puml - -class IO { - processComand() -} - -class EngineCore { - setBoardsize() - clearBoard() - setKomi() - setFixedHandicap() - play() - genmove() - undo() -} - -!include GameState.pumlc - -'class EngineBoard { -' setSize() -' setKomi() -' play() -' undo() -'} - -class EngineAI { - genmove(board) -} - -IO --> EngineCore -EngineCore --> GameState -EngineCore --> EngineAI - -@enduml diff --git a/doc/diagrams/skinparams.puml b/doc/diagrams/skinparams.puml index 2a9e58e..c94ad2d 100644 --- a/doc/diagrams/skinparams.puml +++ b/doc/diagrams/skinparams.puml @@ -9,4 +9,11 @@ skinparam { linetype polyline } +'skinparam { +' shadowing false +' ActorBorderColor #339933 +' ActorBackgroundColor #88FF88 +' linetype polyline +'} + @enduml diff --git a/doc/diagrams/trainingModule.puml b/doc/diagrams/trainingModule.puml new file mode 100644 index 0000000..81d5d72 --- /dev/null +++ b/doc/diagrams/trainingModule.puml @@ -0,0 +1,20 @@ +@startuml + +!include skinparams.puml + +package "Training module" { + + !include SGF.pumlc + !include sgflex.pumlc + !include sgfyacc.pumlc + !include ASTNode.pumlc + !include Property.pumlc + + SGF ..> sgfyacc + sgfyacc .> sgflex + sgfyacc ..> ASTNode + ASTNode .> Property + +} + +@enduml diff --git a/doc/tex/biber.bib b/doc/tex/biber.bib index f22e058..8884962 100644 --- a/doc/tex/biber.bib +++ b/doc/tex/biber.bib @@ -109,8 +109,26 @@ url = {http://www.dabeaz.com/ply} } +@online{vim, + title = {welcome home : vim online}, + urldate = {2022}, + url = {https://www.vim.org} +} + @online{neovim, title = {Home - Neovim}, urldate = {2022}, url = {http://neovim.io} } + +@online{latex, + title = {LaTeX - A document preparation system}, + urldate = {2022}, + url = {https://www.latex-project.org} +} + +@online{puml, + title = {Open-source tool that uses simple textual descriptions to draw beautiful UML diagrams.}, + urldate = {2022}, + url = {https://plantuml.com} +} diff --git a/doc/tex/implementation.tex b/doc/tex/implementation.tex index 40dfb93..f88d57f 100644 --- a/doc/tex/implementation.tex +++ b/doc/tex/implementation.tex @@ -22,20 +22,20 @@ layouts. \paragraph{NumPy\cite{numpy}} -NumPy is a scientific package for python providing a lot of mathematical tools. -The most interesting for this project are its capabilities to create and -transform matrices. +A scientific package for python providing a lot of mathematical tools. The most +interesting for this project are its capabilities to create and transform +matrices. \paragraph{Matplotlib\cite{matplotlib}} -Matplotlib is a python library for creating graphs and other visualizations. It -is used to show the likelihood of moves the neural networks of the project -create from a board configuration. +A python library for creating graphs and other visualizations. It is used to +show the likelihood of moves the neural networks of the project create from a +board configuration. \paragraph{PLY\cite{ply}} -PLY is a tool for generating compilers in Python. It is an implementation of the -lex and yacc utilities, allowing to create lexers and parsers. It is used in the +A tool for generating compilers in Python. It is an implementation of the lex +and yacc utilities, allowing to create lexers and parsers. It is used in the project to create the SGF parser which transform SGF files to internal representations of Go matches. @@ -44,103 +44,61 @@ representations of Go matches. These are some utility libraries commonly used for frequent programming tasks: \begin{itemize} - \item \textbf{sys}, to stop the execution of the program or access system info such + \item \textbf{sys}: to stop the execution of the program or access system info such as primitives maximum values. - \item \textbf{os}, to interact with files. - \item \textbf{re}, to check strings with regular expressions. - \item \textbf{random}, to get random values, for example to obtain a random + \item \textbf{os}: to interact with files. + \item \textbf{re}: to check strings with regular expressions. + \item \textbf{random}: to get random values, for example to obtain a random item from a list. - \item \textbf{copy}, to obtain deep copies of multidimensional arrays. + \item \textbf{copy}: to obtain deep copies of multidimensional arrays. \end{itemize} \subsubsection{Development tools} \paragraph{Neovim\cite{neovim}} -\begin{itemize} +A text editor based on Vim\cite{vim}, providing its same functionality with +useful additions and defaults for modern computers and terminal emulators. With +some extensions and configuration it can become a powerful development +environment with a very fluid usage experience. That, and the fact that the +developer considers Vim's modal editing the best writing experience possible on +a computer, have made Neovim the editor of choice. - \item Extensions +\begin{itemize} + %TODO: Write about neovim extensions + \item FZF \item Extensions \end{itemize} -% Old stuff starts here +\subsubsection{Documentation tools} -\subsection{Engine} +\paragraph{\LaTeX\cite{latex}} -An implementation of GTP, that is, the piece of software which offers the GTP -interface to other applications.\@ It is designed to be used by a software -controller but can also be directly run, mostly for debugging purposes. Its -design is shown in \fref{fig:engine}. The core of the engine is related with -three components, each with a separate responsibility: +A typesetting system widely used in the investigation field, among others. It +allows for documentation like this text to be written in plain text and then +compiled to PDF or other formats, which permits keeping the source files of the +documentation small and organized plus other benefits of plain text such as +being able to use version control. -\begin{itemize} - \item The IO component is the one called from other applications and offers - the text interface. It reads and processes input and calls corresponding - commands from the core of the engine. - \item The EngineBoard component stores the state of the match, recording - information such as the history of boards positions and whose turn goes - next. The engine core uses it for these state-storing purposes. - \item The EngineAI component is responsible of analyzing the match and - generate moves. The engine core uses it when a decision has to be made - by the AI, such as when a move needs to be generated by the engine. -\end{itemize} +\paragraph{PlantUML\cite{puml}} + +A program which creates diagrams from plain text files. PlantUML supports syntax +for many different sorts of diagrams, mainly but not only UML. It has been used +to generate the diagrams used in this text. + +\paragraph{Make} + +A tool for specifying and handling dependencies on a build system. It reads a +file, typically named ``Makefile'', containing which files are needed and on +which other files each of them depends, and then generates those files or +updates them if they already exist but their source files are newer than them. + +It has been used to generate this text from \LaTeX{} and PlantUML source files. +The contents of the Makefile with which this document has been compiled are +shown in \flist{code:makefile}. -\begin{figure}[h] - \begin{center} - \includegraphics[width=\textwidth]{diagrams/gtpEngine.png} - \caption{Design of the GTP engine.}\label{fig:engine} - \end{center} -\end{figure} - -\subsection{Modules} - -One module to store the state of the game and the game tree. One module to parse -moves. One module to read and write SGF files. Modules are shown in -\fref{fig:modules}. - -\begin{figure}[h] - \begin{center} - \includegraphics[width=\textwidth]{diagrams/modules.png} - \caption{Modules.}\label{fig:modules} - \end{center} -\end{figure} - -\subsection{Representation of a match} - -A regular Go match is composed of a list of moves. But since game review and -variants exploration is an important part of Go learning, \program{} allows for -navigation back and forth through the board states of a match and for new -variants to be created from each of these board states. Therefore, a match is -represented as a tree of moves. The state of the board at any given move must -also be stored so liberties, captures count and legality of moves can be -addressed, so it is represented with its own class, which holds a reference both -to the game tree and the current move. Moves depend on a representation of the -game board to have access to its current layout and count of captured stones. -These classes and their relationships can be seen in -\fref{fig:gameRepresentation}. - -\begin{figure}[h] - \begin{center} - \includegraphics[width=0.7\textwidth]{diagrams/gameRepresentation.png} - \caption{A game is represented as a tree of moves.}\label{fig:gameRepresentation} - \end{center} -\end{figure} - -\subsection{SGF} - -To parse SGF files a lexer and parser have been implemented using PLY.\@ The -result of the parsing is an AST (Annotated Syntax Tree) reflecting the contents -of the text input, each node with zero or more properties, and with the ability -to convert themselves and their corresponding subtree into a GameTree. This is -done for the root node, since from the SGF specification there are some -properties only usable in the root node, like those which specify general game -information and properties such as rank of players or komi. These components are -shown in \fref{fig:sgfModule}. - -\begin{figure}[h] - \begin{center} - \includegraphics[width=\textwidth]{diagrams/sgfModule.png} - \caption{Components of the SGF file parsing module.}\label{fig:sgfModule} - \end{center} -\end{figure} +\begin{listing}[h] + \inputminted{make}{Makefile} + \caption{Documentation Makefile}\label{code:makefile} +\end{listing} diff --git a/doc/tex/systemDesign.tex b/doc/tex/systemDesign.tex index 5988ae0..ccd1c48 100644 --- a/doc/tex/systemDesign.tex +++ b/doc/tex/systemDesign.tex @@ -1,2 +1,95 @@ \section{System Design} +\subsection{Class diagram} + +The full class diagram is shown in \fref{fig:fullClasses} + +\begin{figure}[h] + \begin{center} + \includegraphics[width=\textwidth]{diagrams/fullClasses.png} + \caption{Full class diagram.} + \label{fig:fullClasses} + \end{center} +\end{figure} + +% From here + + +\subsection{Engine} + +An implementation of GTP, that is, the piece of software which offers the GTP +interface to other applications.\@ It is designed to be used by a software +controller but can also be directly run, mostly for debugging purposes. Its +design is shown in \fref{fig:engine}. The core of the engine is related with +three components, each with a separate responsibility: + +\begin{itemize} + \item The IO component is the one called from other applications and offers + the text interface. It reads and processes input and calls corresponding + commands from the core of the engine. + \item The EngineBoard component stores the state of the match, recording + information such as the history of boards positions and whose turn goes + next. The engine core uses it for these state-storing purposes. + \item The EngineAI component is responsible of analyzing the match and + generate moves. The engine core uses it when a decision has to be made + by the AI, such as when a move needs to be generated by the engine. +\end{itemize} + +\begin{figure}[h] + \begin{center} + \includegraphics[width=\textwidth]{diagrams/engineModule.png} + \caption{Design of the GTP engine.}\label{fig:engine} + \end{center} +\end{figure} + +\subsection{Modules} + +One module to store the state of the game and the game tree. One module to parse +moves. One module to read and write SGF files. Modules are shown in +\fref{fig:modules}. + +\begin{figure}[h] + \begin{center} + \includegraphics[width=\textwidth]{diagrams/modules.png} + \caption{Modules.}\label{fig:modules} + \end{center} +\end{figure} + +\subsection{Representation of a match} + +A regular Go match is composed of a list of moves. But since game review and +variants exploration is an important part of Go learning, \program{} allows for +navigation back and forth through the board states of a match and for new +variants to be created from each of these board states. Therefore, a match is +represented as a tree of moves. The state of the board at any given move must +also be stored so liberties, captures count and legality of moves can be +addressed, so it is represented with its own class, which holds a reference both +to the game tree and the current move. Moves depend on a representation of the +game board to have access to its current layout and count of captured stones. +These classes and their relationships can be seen in +\fref{fig:gameRepresentation}. + +\begin{figure}[h] + \begin{center} + \includegraphics[width=0.7\textwidth]{diagrams/gameRepresentation.png} + \caption{A game is represented as a tree of moves.}\label{fig:gameRepresentation} + \end{center} +\end{figure} + +\subsection{SGF} + +To parse SGF files a lexer and parser have been implemented using PLY.\@ The +result of the parsing is an AST (Annotated Syntax Tree) reflecting the contents +of the text input, each node with zero or more properties, and with the ability +to convert themselves and their corresponding subtree into a GameTree. This is +done for the root node, since from the SGF specification there are some +properties only usable in the root node, like those which specify general game +information and properties such as rank of players or komi. These components are +shown in \fref{fig:sgfModule}. + +\begin{figure}[h] + \begin{center} + \includegraphics[width=\textwidth]{diagrams/sgfModule.png} + \caption{Components of the SGF file parsing module.}\label{fig:sgfModule} + \end{center} +\end{figure} diff --git a/doc/tex/tfg.tex b/doc/tex/tfg.tex index f387064..4c64bb2 100644 --- a/doc/tex/tfg.tex +++ b/doc/tex/tfg.tex @@ -13,6 +13,9 @@ \usepackage[backend=biber, style=numeric, sorting=none]{biblatex} \addbibresource{tex/biber.bib} +\usepackage{minted} % Code importing and formatting +\setminted{linenos, breaklines} + \geometry{left=4cm,top=2cm,bottom=2cm,right=4cm} \hypersetup{colorlinks=false, @@ -27,6 +30,7 @@ \newcommand{\program}{Imago} \newcommand{\fref}[1]{Fig.~\ref{#1}} +\newcommand{\flist}[1]{Listing~\ref{#1}} %\newcommand{\uurl}[1]{\underline{\url{#1}}} \newcommand{\tabitem}{~~\llap{\textbullet}~~} @@ -110,10 +114,14 @@ inclusion to use this template is included here. \clearpage +\setcounter{secnumdepth}{3} +\setcounter{tocdepth}{4} \tableofcontents \listoffigures +\listoflistings + \clearpage \input{tex/introduction.tex} @@ -122,7 +130,7 @@ inclusion to use this template is included here. \input{tex/systemAnalysis.tex} -%\input{tex/systemDesign.tex} +\input{tex/systemDesign.tex} \input{tex/implementation.tex} diff --git a/go.py b/go.py index d4efc1b..7cb8555 100755 --- a/go.py +++ b/go.py @@ -24,8 +24,6 @@ if __name__ == "__main__": print() - player = str(GAMESTATE.getPlayerCode()) - moveRow = move[0] moveCol = move[1] diff --git a/imago/data/enums.py b/imago/data/enums.py index 9d963ec..56fe2cc 100644 --- a/imago/data/enums.py +++ b/imago/data/enums.py @@ -16,3 +16,8 @@ class Player(Enum): if player == cls.WHITE: return cls.BLACK return cls.EMPTY + +class DecisionAlgorithms(Enum): + #RANDOM = enumAuto() + MONTECARLO = enumAuto() + KERAS = enumAuto() diff --git a/imago/engine/decisionAlgorithmFactory.py b/imago/engine/decisionAlgorithmFactory.py index 094a816..25517fa 100644 --- a/imago/engine/decisionAlgorithmFactory.py +++ b/imago/engine/decisionAlgorithmFactory.py @@ -4,11 +4,6 @@ from enum import Enum, auto as enumAuto from imago.engine.monteCarlo import MCTS from imago.engine.keras.keras import Keras -class DecisionAlgorithms(Enum): - #RANDOM = enumAuto() - MONTECARLO = enumAuto() - KERAS = enumAuto() - class DecisionAlgorithmFactory: def create(self, algorithm, move): diff --git a/imago/engine/keras/neuralNetwork.py b/imago/engine/keras/neuralNetwork.py index 7eddb9d..c414a78 100644 --- a/imago/engine/keras/neuralNetwork.py +++ b/imago/engine/keras/neuralNetwork.py @@ -28,7 +28,7 @@ class NeuralNetwork: self.model = self._loadModel(self.path) except FileNotFoundError: self.model = self._initModel(boardSize) - self.saveModelPlot() + 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.") @@ -120,7 +120,7 @@ class NeuralNetwork: for col in range(self.boardSize): predictionBoard[row][col] = predictionVector[row * self.boardSize + col] predictionPass = predictionVector[-1] - self.saveHeatmap(predictionBoard, predictionPass) + self._saveHeatmap(predictionBoard, predictionPass) # Search the highest valued vertex which is also playable playableVertices = gameMove.getPlayableVertices() @@ -147,7 +147,7 @@ class NeuralNetwork: batch_size = 1, verbose = 2) - def saveHeatmap(self, data, passChance): + def _saveHeatmap(self, data, passChance): rows = len(data) cols = len(data[0]) @@ -202,10 +202,10 @@ class NeuralNetwork: letter = 'J' return labels - def saveModelPlot(self): + def saveModelPlot(self, path): plot_model( self.model, - to_file="model.png", + to_file=path, show_shapes=True, show_dtype=True, show_layer_names=True, diff --git a/imago/gameLogic/gameState.py b/imago/gameLogic/gameState.py index 5232a43..72b91b4 100644 --- a/imago/gameLogic/gameState.py +++ b/imago/gameLogic/gameState.py @@ -43,7 +43,7 @@ class GameState: prevBoards = self.lastMove.getThisAndPrevBoards() playable, message = self.lastMove.board.isPlayable(row, col, player, prevBoards) if playable: - self.__addMove(player, row, col) + self._addMove(player, row, col) return True print("Invalid Move! %s" % message) return False @@ -60,7 +60,7 @@ class GameState: """Sets the move before the last move as the new last move.""" self.lastMove = self.lastMove.previousMove - def __addMove(self, player, row, col): + def _addMove(self, player, row, col): # Check a last move already exists if self.lastMove is None: -- cgit v1.2.1 From 4cc55348c8dbb1902a1246fba66237d5c59f0349 Mon Sep 17 00:00:00 2001 From: InigoGutierrez Date: Fri, 1 Jul 2022 15:40:57 +0200 Subject: Finished writing documentation. --- .gitignore | 2 +- doc/Makefile | 12 +- doc/diagrams/gameRepresentation.puml | 17 -- doc/diagrams/skinparams.puml | 4 +- doc/listings/convModel.txt | 23 ++ doc/listings/convTraining.txt | 40 +++ doc/listings/denseModel.txt | 17 ++ doc/listings/denseTraining.txt | 40 +++ doc/listings/trainCommand.sh | 1 + doc/tex/biber.bib | 22 ++ doc/tex/conclusions.tex | 128 ++++++++++ doc/tex/imago.tex | 171 +++++++++++++ doc/tex/implementation.tex | 35 ++- doc/tex/introduction.tex | 2 +- doc/tex/planification.tex | 45 ++-- doc/tex/previousWorks.tex | 22 -- doc/tex/results.tex | 416 +++++++++++++++++++++++++++++++ doc/tex/systemAnalysis.tex | 55 ++-- doc/tex/systemDesign.tex | 327 ++++++++++++++++++++---- doc/tex/tfg.tex | 146 ----------- imago/data/enums.py | 3 +- imago/engine/core.py | 8 +- imago/engine/createDecisionAlgorithm.py | 21 ++ imago/engine/decisionAlgorithmFactory.py | 22 -- imago/engine/keras/denseNeuralNetwork.py | 28 ++- imago/engine/keras/keras.py | 19 +- imago/engine/monteCarlo.py | 4 +- imagocli.py | 6 +- testKeras.py | 27 -- train.py | 27 ++ 30 files changed, 1321 insertions(+), 369 deletions(-) delete mode 100644 doc/diagrams/gameRepresentation.puml create mode 100644 doc/listings/convModel.txt create mode 100644 doc/listings/convTraining.txt create mode 100644 doc/listings/denseModel.txt create mode 100644 doc/listings/denseTraining.txt create mode 100644 doc/listings/trainCommand.sh create mode 100644 doc/tex/conclusions.tex create mode 100644 doc/tex/imago.tex delete mode 100644 doc/tex/previousWorks.tex create mode 100644 doc/tex/results.tex delete mode 100644 doc/tex/tfg.tex create mode 100644 imago/engine/createDecisionAlgorithm.py delete mode 100644 imago/engine/decisionAlgorithmFactory.py delete mode 100755 testKeras.py create mode 100755 train.py diff --git a/.gitignore b/.gitignore index 47e0340..0a4eeda 100644 --- a/.gitignore +++ b/.gitignore @@ -9,7 +9,7 @@ *.pdf doc/out/ doc/diagrams/*.png -_minted-tfg/ +_minted-imago/ # src __pycache__/ diff --git a/doc/Makefile b/doc/Makefile index 497e9b1..284000e 100644 --- a/doc/Makefile +++ b/doc/Makefile @@ -1,15 +1,19 @@ .SUFFIXES: .puml .png -docName = tfg +docName = imago outputFolder = out -texFiles = tex/tfg.tex tex/introduction.tex tex/planification.tex tex/implementation.tex tex/systemAnalysis.tex tex/biber.bib Makefile +texFiles = tex/$(docName).tex tex/introduction.tex tex/planification.tex tex/systemAnalysis.tex tex/systemDesign.tex tex/implementation.tex tex/results.tex tex/conclusions.tex tex/biber.bib -diagramImgs = diagrams/gameRepresentation.png diagrams/planificationWorkPlanEngine.png diagrams/planificationWorkPlanGame.png diagrams/sgfModule.png diagrams/useCases.png diagrams/analysisClasses.png diagrams/useCase_generateAMove.png diagrams/useCase_useAsBackend.png diagrams/useCase_playAMatch.png diagrams/interfaces.png diagrams/engineModule.png diagrams/modules.png diagrams/fullClasses.png +diagramImgs = diagrams/planificationWorkPlanEngine.png diagrams/planificationWorkPlanGame.png diagrams/useCases.png diagrams/analysisClasses.png diagrams/useCase_generateAMove.png diagrams/useCase_useAsBackend.png diagrams/useCase_playAMatch.png diagrams/interfaces.png diagrams/gameModule.png diagrams/engineModule.png diagrams/trainingModule.png diagrams/modules.png diagrams/fullClasses.png + +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 all: $(docName).pdf -$(docName).pdf: $(texFiles) $(diagramImgs) +$(docName).pdf: $(texFiles) $(diagramImgs) $(imgs) $(listings) Makefile [ -d $(outputFolder) ] || mkdir $(outputFolder) xelatex -shell-escape -output-directory $(outputFolder) tex/$(docName).tex biber $(outputFolder)/$(docName) diff --git a/doc/diagrams/gameRepresentation.puml b/doc/diagrams/gameRepresentation.puml deleted file mode 100644 index db5d5a5..0000000 --- a/doc/diagrams/gameRepresentation.puml +++ /dev/null @@ -1,17 +0,0 @@ -@startuml - -!include skinparams.puml - -!include GameState.pumlc -!include GameTree.pumlc -!include GameMove.pumlc -!include GameBoard.pumlc - -GameState -> GameTree -GameState --> GameMove: Last move -GameTree *--> GameMove -GameMove --> GameMove: Previous move -GameMove *--> GameMove: Next moves -GameBoard <- GameMove - -@enduml diff --git a/doc/diagrams/skinparams.puml b/doc/diagrams/skinparams.puml index c94ad2d..d9dce03 100644 --- a/doc/diagrams/skinparams.puml +++ b/doc/diagrams/skinparams.puml @@ -1,10 +1,10 @@ @startuml 'Old style -skin rose +'skin rose skinparam { - monochrome true + 'monochrome true shadowing false linetype polyline } diff --git a/doc/listings/convModel.txt b/doc/listings/convModel.txt new file mode 100644 index 0000000..5c90975 --- /dev/null +++ b/doc/listings/convModel.txt @@ -0,0 +1,23 @@ +Model: "sequential" +_________________________________________________________________ + Layer (type) Output Shape Param # +================================================================= + conv2d (Conv2D) (None, 9, 9, 32) 608 + + max_pooling2d (MaxPooling2D (None, 4, 4, 32) 0 + ) + + conv2d_1 (Conv2D) (None, 4, 4, 64) 18496 + + max_pooling2d_1 (MaxPooling (None, 2, 2, 64) 0 + 2D) + + flatten (Flatten) (None, 256) 0 + + dense (Dense) (None, 82) 21074 + +================================================================= +Total params: 40,178 +Trainable params: 40,178 +Non-trainable params: 0 +_________________________________________________________________ diff --git a/doc/listings/convTraining.txt b/doc/listings/convTraining.txt new file mode 100644 index 0000000..6108abc --- /dev/null +++ b/doc/listings/convTraining.txt @@ -0,0 +1,40 @@ +Epoch 1/20 +39501/39501 - 279s - loss: 3.7391 - accuracy: 0.1064 - val_loss: 3.1316 - val_accuracy: 0.2023 - 279s/epoch - 7ms/step +Epoch 2/20 +39501/39501 - 241s - loss: 2.6512 - accuracy: 0.3046 - val_loss: 2.0729 - val_accuracy: 0.4484 - 241s/epoch - 6ms/step +Epoch 3/20 +39501/39501 - 214s - loss: 1.6459 - accuracy: 0.6014 - val_loss: 1.2040 - val_accuracy: 0.7143 - 214s/epoch - 5ms/step +Epoch 4/20 +39501/39501 - 228s - loss: 0.9016 - accuracy: 0.8417 - val_loss: 0.6430 - val_accuracy: 0.8735 - 228s/epoch - 6ms/step +Epoch 5/20 +39501/39501 - 230s - loss: 0.4704 - accuracy: 0.9380 - val_loss: 0.3520 - val_accuracy: 0.9378 - 230s/epoch - 6ms/step +Epoch 6/20 +39501/39501 - 222s - loss: 0.2735 - accuracy: 0.9520 - val_loss: 0.2329 - val_accuracy: 0.9549 - 222s/epoch - 6ms/step +Epoch 7/20 +39501/39501 - 215s - loss: 0.2117 - accuracy: 0.9495 - val_loss: 0.1837 - val_accuracy: 0.9583 - 215s/epoch - 5ms/step +Epoch 8/20 +39501/39501 - 215s - loss: 0.1797 - accuracy: 0.9533 - val_loss: 0.1787 - val_accuracy: 0.9556 - 215s/epoch - 5ms/step +Epoch 9/20 +39501/39501 - 225s - loss: 0.1607 - accuracy: 0.9553 - val_loss: 0.1952 - val_accuracy: 0.9446 - 225s/epoch - 6ms/step +Epoch 10/20 +39501/39501 - 249s - loss: 0.1486 - accuracy: 0.9572 - val_loss: 0.1544 - val_accuracy: 0.9597 - 249s/epoch - 6ms/step +Epoch 11/20 +39501/39501 - 208s - loss: 0.1380 - accuracy: 0.9586 - val_loss: 0.1467 - val_accuracy: 0.9651 - 208s/epoch - 5ms/step +Epoch 12/20 +39501/39501 - 210s - loss: 0.1321 - accuracy: 0.9592 - val_loss: 0.1313 - val_accuracy: 0.9665 - 210s/epoch - 5ms/step +Epoch 13/20 +39501/39501 - 204s - loss: 0.1276 - accuracy: 0.9598 - val_loss: 0.1282 - val_accuracy: 0.9665 - 204s/epoch - 5ms/step +Epoch 14/20 +39501/39501 - 193s - loss: 0.1222 - accuracy: 0.9604 - val_loss: 0.1174 - val_accuracy: 0.9686 - 193s/epoch - 5ms/step +Epoch 15/20 +39501/39501 - 183s - loss: 0.1182 - accuracy: 0.9607 - val_loss: 0.1747 - val_accuracy: 0.9433 - 183s/epoch - 5ms/step +Epoch 16/20 +39501/39501 - 166s - loss: 0.1147 - accuracy: 0.9611 - val_loss: 0.1186 - val_accuracy: 0.9679 - 166s/epoch - 4ms/step +Epoch 17/20 +39501/39501 - 163s - loss: 0.1119 - accuracy: 0.9616 - val_loss: 0.1112 - val_accuracy: 0.9699 - 163s/epoch - 4ms/step +Epoch 18/20 +39501/39501 - 168s - loss: 0.1095 - accuracy: 0.9618 - val_loss: 0.1020 - val_accuracy: 0.9706 - 168s/epoch - 4ms/step +Epoch 19/20 +39501/39501 - 161s - loss: 0.1072 - accuracy: 0.9625 - val_loss: 0.1058 - val_accuracy: 0.9699 - 161s/epoch - 4ms/step +Epoch 20/20 +39501/39501 - 173s - loss: 0.1052 - accuracy: 0.9624 - val_loss: 0.1031 - val_accuracy: 0.9727 - 173s/epoch - 4ms/step diff --git a/doc/listings/denseModel.txt b/doc/listings/denseModel.txt new file mode 100644 index 0000000..006e321 --- /dev/null +++ b/doc/listings/denseModel.txt @@ -0,0 +1,17 @@ +Model: "sequential" +_________________________________________________________________ + Layer (type) Output Shape Param # +================================================================= + dense (Dense) (None, 9, 9, 81) 243 + + dense_1 (Dense) (None, 9, 9, 81) 6642 + + flatten (Flatten) (None, 6561) 0 + + dense_2 (Dense) (None, 82) 538084 + +================================================================= +Total params: 544,969 +Trainable params: 544,969 +Non-trainable params: 0 +_________________________________________________________________ diff --git a/doc/listings/denseTraining.txt b/doc/listings/denseTraining.txt new file mode 100644 index 0000000..0bfcb51 --- /dev/null +++ b/doc/listings/denseTraining.txt @@ -0,0 +1,40 @@ +Epoch 1/20 +148338/148338 - 1151s - loss: 1.1130 - accuracy: 0.6942 - val_loss: 0.6097 - val_accuracy: 0.8249 - 1151s/epoch - 8ms/step +Epoch 2/20 +148338/148338 - 1084s - loss: 0.5366 - accuracy: 0.8572 - val_loss: 0.4744 - val_accuracy: 0.8617 - 1084s/epoch - 7ms/step +Epoch 3/20 +148338/148338 - 1071s - loss: 0.4250 - accuracy: 0.8895 - val_loss: 0.4161 - val_accuracy: 0.8813 - 1071s/epoch - 7ms/step +Epoch 4/20 +148338/148338 - 1118s - loss: 0.3678 - accuracy: 0.9066 - val_loss: 0.3493 - val_accuracy: 0.9024 - 1118s/epoch - 8ms/step +Epoch 5/20 +148338/148338 - 1092s - loss: 0.3320 - accuracy: 0.9170 - val_loss: 0.3103 - val_accuracy: 0.9185 - 1092s/epoch - 7ms/step +Epoch 6/20 +148338/148338 - 1097s - loss: 0.3078 - accuracy: 0.9241 - val_loss: 0.3132 - val_accuracy: 0.9145 - 1097s/epoch - 7ms/step +Epoch 7/20 +148338/148338 - 1074s - loss: 0.2899 - accuracy: 0.9293 - val_loss: 0.2779 - val_accuracy: 0.9257 - 1074s/epoch - 7ms/step +Epoch 8/20 +148338/148338 - 1114s - loss: 0.2762 - accuracy: 0.9330 - val_loss: 0.2709 - val_accuracy: 0.9246 - 1114s/epoch - 8ms/step +Epoch 9/20 +148338/148338 - 1111s - loss: 0.2660 - accuracy: 0.9351 - val_loss: 0.2577 - val_accuracy: 0.9319 - 1111s/epoch - 7ms/step +Epoch 10/20 +148338/148338 - 1104s - loss: 0.2563 - accuracy: 0.9374 - val_loss: 0.2446 - val_accuracy: 0.9388 - 1104s/epoch - 7ms/step +Epoch 11/20 +148338/148338 - 1084s - loss: 0.2489 - accuracy: 0.9394 - val_loss: 0.2441 - val_accuracy: 0.9348 - 1084s/epoch - 7ms/step +Epoch 12/20 +148338/148338 - 1088s - loss: 0.2419 - accuracy: 0.9407 - val_loss: 0.2538 - val_accuracy: 0.9337 - 1088s/epoch - 7ms/step +Epoch 13/20 +148338/148338 - 1090s - loss: 0.2365 - accuracy: 0.9416 - val_loss: 0.2538 - val_accuracy: 0.9312 - 1090s/epoch - 7ms/step +Epoch 14/20 +148338/148338 - 1063s - loss: 0.2314 - accuracy: 0.9430 - val_loss: 0.2484 - val_accuracy: 0.9308 - 1063s/epoch - 7ms/step +Epoch 15/20 +148338/148338 - 1111s - loss: 0.2271 - accuracy: 0.9436 - val_loss: 0.2373 - val_accuracy: 0.9359 - 1111s/epoch - 7ms/step +Epoch 16/20 +148338/148338 - 1124s - loss: 0.2235 - accuracy: 0.9443 - val_loss: 0.2542 - val_accuracy: 0.9257 - 1124s/epoch - 8ms/step +Epoch 17/20 +148338/148338 - 1074s - loss: 0.2202 - accuracy: 0.9451 - val_loss: 0.2368 - val_accuracy: 0.9327 - 1074s/epoch - 7ms/step +Epoch 18/20 +148338/148338 - 1120s - loss: 0.2181 - accuracy: 0.9453 - val_loss: 0.2462 - val_accuracy: 0.9286 - 1120s/epoch - 8ms/step +Epoch 19/20 +148338/148338 - 1121s - loss: 0.2159 - accuracy: 0.9460 - val_loss: 0.2375 - val_accuracy: 0.9316 - 1121s/epoch - 8ms/step +Epoch 20/20 +148338/148338 - 1115s - loss: 0.2154 - accuracy: 0.9458 - val_loss: 0.2273 - val_accuracy: 0.9352 - 1115s/epoch - 8ms/step diff --git a/doc/listings/trainCommand.sh b/doc/listings/trainCommand.sh new file mode 100644 index 0000000..a5f09c4 --- /dev/null +++ b/doc/listings/trainCommand.sh @@ -0,0 +1 @@ +./train.py $(ls ../collections/minigo/matches/*.sgf | shuf | head -n 50) diff --git a/doc/tex/biber.bib b/doc/tex/biber.bib index 8884962..d0a714e 100644 --- a/doc/tex/biber.bib +++ b/doc/tex/biber.bib @@ -132,3 +132,25 @@ urldate = {2022}, url = {https://plantuml.com} } + +@online{sl_sword, + title = {9x9 Tengen Openings at Sensei's Library}, + urldate = {2022}, + url = {https://senseis.xmp.net/?9x9TengenOpenings} +} + +@online{ogsLifeAndDeath, + author = {sunspark}, + title = {Cho Chikun's Encyclopedia of Life and Death - Elementary: 1 / 900}, + date = {2016}, + urldate = {2022}, + url = {https://online-go.com/puzzle/2824} +} + +@online{fsf_free, + author = {Free Software Foundation}, + title = {What is Free Software? - GNU Project - Free Software Foundation}, + date = {2022}, + urldate = {2022}, + url = {https://www.gnu.org/philosophy/free-sw.html} +} diff --git a/doc/tex/conclusions.tex b/doc/tex/conclusions.tex new file mode 100644 index 0000000..ebbf7fd --- /dev/null +++ b/doc/tex/conclusions.tex @@ -0,0 +1,128 @@ +\section{Conclusions} + +Some problems found during the project, an evaluation of the results and what +could be done in the future to improve the system are discussed here. + +\subsection{Problems with the Implementation of the Game} + +While Go has a simple set of rules, they lead to many borderline cases which +must be specifically addressed. For example, the ko rule obliges to check the +previous board positions after each move so no boards are repeated. + +These problems have been solved by designing the tree of moves of a match so +each move stores the full board layout after it has been played. This of course +increases the memory needed to run the application, but has been chosen over the +alternative of having to recreate the board parsing the tree backwards for each +move to check if ko is occurring, if a move makes a capture and which stones +have already been captured, etc. It is the old problem of memory versus +processing, which in this case has been answered with memory. Which one would be +the best solution would require a deep analysis out of the scope of the project. + +\subsection{Problems with the Monte Carlo Tree Search Algorithm} + +The implementation Monte Carlo Tree Search algorithm was not precise by itself +when asked for moves. This could be attributed to various factors: + +\begin{itemize} + + \item The way the score is counted makes it difficult to evaluate who is + winning at the beginning or middle of the game, so the score of a move + has to be evaluated by running playing simulations. These simulations + are not precise on analyzing the result since the moves in them are + played at random. + + \item The configuration used, 5 explorations with 10 simulations each for + cycle, does not allow for many good moves to be found. + +\end{itemize} + +To solve these problems, either a good score evaluation engine has to be +implemented, which is another machine learning problem by itself, or the +explorations and simulations of the engine tuned up to values unable to be run +on the development hardware. + +\subsection{Problems with Neural Networks} + +Some concerns with the design and results of the neural networks are discussed +on this section. + +\subsubsection{Finding the correct design} + +When approaching neural networks design with a basic understanding of their +inner workings it is easy to get lost in all the different factors that can be +tuned. The number and variant of layers, their number of nodes, their activation +function, the learning rate; they can all be tweaked and doing so in the favour +of the design requires a good knowledge of what is being done. + +The basic approach was to design the input and output shapes and then adjust the +network based on that. At first, the desired output was a 9x9 two-dimensional +matrix with the probabilities of playing on each move. A heatmap generated from +a failed solution of this approach can be seen on \fref{fig:oldHM}, where the +network distributed the probability for each row instead of the whole board. The +fitting of shapes of input, internal layers and output has found to be tricky at +first. + +\begin{figure}[h] + \begin{center} + \includegraphics[width=\textwidth]{img/matches/oldLineByLineHeatmap.png} + \caption{A heatmap from an old design of the dense neural network.} + \label{fig:oldHM} + Note that each row adds up to one, meaning that the engine is selecting + the best move for each row and not for the whole board. + \end{center} +\end{figure} + +Finally, based on the design of classification networks where each possible +class is mapped to a value in the one-dimensional output vector, it was decided +that the output was not a two-dimensional matrix but a one-dimensional vector. +This also helped address the possibility of passing, treating it as just another +possible move that was previously left out because of not knowing how to fit it +on the matrix of possible moves. This vector is then reshaped to a matrix +representing the board when needed, for example to generate the heatmaps used +for evaluation. + +\subsubsection{Mistakes on Networks' Play Patterns} + +One notable mistake made by the networks, specially the convolutional network, +was passing to much. Passing is considered just another move, so the networks +have no grasp that they should not pass until there are no more valuable moves +to be made. A new design problem could be to create a specific passing policy. + +Another observation is that the engines seem to follow traditional openings and +make reasonable decisions at the start of the game, with some interesting +variations. But as the game progresses their moves make less sense. This could +be because they have seen many examples of starting moves but complications at +the middle and end game are much more diverse and the networks had not been +trained on similar situations. + +\subsection{Viable Extensions} + +There are several ways this project could be improved, but were left out by lack +of resources or time. Some interesting ones are explored on this section. + +\subsubsection{Joining Monte Carlo Tree Search and Neural Networks} + +The tree search algorithm is better at the end of the game, where the tree to +explore is much more manageable. On the other hand, the neural networks are +better at the beginning, where the positions are more common and patterns are +simpler. The logical next step would be to join both: the network could select +the best moves to be made, and the tree search algorithm could explore them to +select the best and grow its explored tree in meaningful directions. + +This is also what was done by AlphaGo. By seeing the results obtained in this +project it makes sense they made that decision. + +\subsubsection{Train the Engine by Itself} + +By training a game engine on human matches, it can at best get as good as the +human players it has learned from. + +Top engines learn not from humans but by playing against themselves. This would +be an interesting problem to tackle, if requiring a new approach to the design +of the networks and a lot of training resources. + +\subsubsection{Presenting the Engine to the World} + +Some Go servers allow for bot players to be registered. This would provide a +source of training against players from all around the world and also an +evaluation over time of the engine strength. diff --git a/doc/tex/imago.tex b/doc/tex/imago.tex new file mode 100644 index 0000000..8242467 --- /dev/null +++ b/doc/tex/imago.tex @@ -0,0 +1,171 @@ +\documentclass[12pt]{article} + +\usepackage{geometry} +\usepackage{graphicx} +\usepackage{booktabs} +\usepackage{hyperref} +\usepackage{csquotes} +\usepackage{enumitem} +\usepackage[indent=20pt]{parskip} % Space between paragraphs +\usepackage{indentfirst} % Indent first paragraph of sections + +\geometry{left=3cm,top=2cm,bottom=2cm,right=3cm} + +\usepackage{chngcntr} + +\hypersetup{colorlinks=false, + linkcolor=black, + filecolor=black, + urlcolor=black, + bookmarks=true +} + +\urlstyle{mono} + +\usepackage[backend=biber, style=numeric, sorting=none]{biblatex} +\addbibresource{tex/biber.bib} + +\usepackage{minted} % Code importing and formatting +\setminted{linenos, breaklines} + +\newcommand{\program}{Imago} + +\newcommand{\fref}[1]{Fig.~\ref{#1}} +\newcommand{\flist}[1]{Listing~\ref{#1}} +%\newcommand{\uurl}[1]{\underline{\url{#1}}} + +\newcommand{\tabitem}{~~\llap{\textbullet}~~} + +\begin{document} + +\frenchspacing + +\title{ + \begin{center} + \includegraphics[width=0.4\textwidth]{img/logoUniovi.png}\hfill + \includegraphics[width=0.3\textwidth]{img/logoEII.png} + \end{center}~\\[10pt] + \program\\ + \large An AI player of the game of Go +} + +\author{Íñigo Gutiérrez Fernández} + +\date{} + +\maketitle + +\thispagestyle{empty} + +\begin{figure}[h] + \begin{center} + \includegraphics[width=0.6\textwidth]{img/imago.jpg} + \end{center} +\end{figure} + +\clearpage + +\begin{abstract} + This is the abstract. +\end{abstract} + +\clearpage + +\section*{Acknowledgements} + +TODO: Acknowledgements + +To Vicente García Díaz, for directing me in this project. + +To José Manuel Redondo López\cite{plantillaRedondo}, for making an extensive +template on which the structure of this documentation is extensively based. + +\clearpage + +\section*{Copyright notice} + +\begin{displayquote} + + Copyright (C) 2021 \textbf{ÍÑIGO GUTIÉRREZ FERNÁNDEZ}. + + \textit{Permission is granted to copy, distribute and/or modify this + document under the terms of the GNU Free Documentation License, Version 1.3 + or any later version published by the Free Software Foundation; with the + Copyright notice as an Invariant Section, no Front-Cover Texts, and no + Back-Cover Texts. A copy of the license is included in the section + entitled ``GNU Free Documentation License''.} + +\end{displayquote} + +Although this document uses the GNU Free Documentation License to keep its +contained information free, bear in mind that it isn't software documentation or +a manual or guide of any kind, and serves just as the documentation for the +author's Degree's Final Project. + +This document is based on a template by José Manuel Redondo López, associate +professor of the University of Oviedo. The copyright notice he asks for +inclusion to use this template is included here. + +\begin{displayquote} + + Copyright (C) 2019 \textbf{JOSÉ MANUEL REDONDO + LÓPEZ}.\cite{plantillaRedondo} + + \textit{Permission is granted to copy, distribute and/or modify this + document under the terms of the GNU Free Documentation License, Version 1.3 + or any later version published by the Free Software Foundation; with no + Invariant Sections, no Front-Cover Texts, and no Back-Cover Texts. A copy + of the license is included in the section entitled ``GNU Free + Documentation License''.} + +\end{displayquote} + +\clearpage + +\setcounter{secnumdepth}{3} +\setcounter{tocdepth}{4} +\tableofcontents + +\clearpage + +\counterwithin{figure}{section} +\renewcommand{\listfigurename}{Figures} +\listoffigures + +\clearpage + +\renewcommand{\listoflistingscaption}{Listings} +\counterwithin{listing}{section} +\listoflistings + +\clearpage + +\input{tex/introduction.tex} +\clearpage + +\input{tex/planification.tex} +\clearpage + +\input{tex/systemAnalysis.tex} +\clearpage + +\input{tex/systemDesign.tex} +\clearpage + +\input{tex/implementation.tex} +\clearpage + +\input{tex/results.tex} +\clearpage + +\input{tex/conclusions.tex} +\clearpage + +% References (bibliography) + +\setcounter{secnumdepth}{0} +\section{References} +\printbibliography[type=article,title={Cited articles},heading=subbibintoc]{} +\printbibliography[type=online,title={Online resources},heading=subbibintoc]{} + +\end{document} diff --git a/doc/tex/implementation.tex b/doc/tex/implementation.tex index f88d57f..d46e805 100644 --- a/doc/tex/implementation.tex +++ b/doc/tex/implementation.tex @@ -1,6 +1,23 @@ \section{Implementation} -\subsection{Development environment} +\subsection{Development Hardware} + +The development and evaluation machine is a Satellite L50-C laptop. Its +specifications are shown as a list for readability. + +\begin{itemize} + \item \textbf{CPU}: Intel i5-6200U, 2.800GHz + \item \textbf{Integrated GPU}: Intel Skylake GT2 + \item \textbf{Dedicated GPU}: Nvidia Geforce 930M + \item \textbf{RAM}: 8 GiB +\end{itemize} + +\subsection{Development Software} + +The tools selected for the development of the project and the documentation are +listed and explained on this section. All the tools used are either +free\cite{fsf_free} or open source software. The development machine runs 64 +bits Arch Linux as its operating system. \subsubsection{Language} @@ -53,7 +70,7 @@ These are some utility libraries commonly used for frequent programming tasks: \item \textbf{copy}: to obtain deep copies of multidimensional arrays. \end{itemize} -\subsubsection{Development tools} +\subsubsection{Development Tools} \paragraph{Neovim\cite{neovim}} @@ -64,14 +81,14 @@ environment with a very fluid usage experience. That, and the fact that the developer considers Vim's modal editing the best writing experience possible on a computer, have made Neovim the editor of choice. -\begin{itemize} - %TODO: Write about neovim extensions - \item FZF - \item Extensions - -\end{itemize} +%TODO: Write about neovim extensions +%\begin{itemize} +% \item FZF +% \item Extensions +% +%\end{itemize} -\subsubsection{Documentation tools} +\subsubsection{Documentation Tools} \paragraph{\LaTeX\cite{latex}} diff --git a/doc/tex/introduction.tex b/doc/tex/introduction.tex index 6defbd9..2c710ea 100644 --- a/doc/tex/introduction.tex +++ b/doc/tex/introduction.tex @@ -12,5 +12,5 @@ game at heart, Go has nonetheless been interpreted as a stylized representation of fighting a war, settling a frontier, cornering a market, thrashing out an argument, or even of fortune-telling and prophecy. Go has always been one of the - most played games in the world. \parencite{sl_go} + most played games in the world.\cite{sl_go} \end{displayquote} diff --git a/doc/tex/planification.tex b/doc/tex/planification.tex index 00695a1..f0f7535 100644 --- a/doc/tex/planification.tex +++ b/doc/tex/planification.tex @@ -1,6 +1,6 @@ \section{Planning} -\subsection{Driving needs} +\subsection{Driving Needs} As one of the deepest and most studied games in the world, Go presents a very interesting problem for artificial intelligence. Implementing not only the @@ -29,18 +29,18 @@ Presented here are the ideal targets of the project. recorded in the SGF format. \end{itemize} -\subsection{Project stages} +\subsection{Project Stages} The project will be organized in several stages based on the different components and needs. -\subsubsection{Game implementation} +\subsubsection{Game Implementation} The rules of the game must be implemented, ideally in a way they can be tested by direct human play. This system will at its bare minimum represent the Japanese Go rules (area scoring, no superko rule, no suicide moves). -\subsubsection{Engine implementation} +\subsubsection{Engine Implementation} The key of this project is to create some kind of system able to generate strong moves based on any given board configuration: this will be such system. It will @@ -50,7 +50,7 @@ either player. It should also be modular enough so different algorithms can be selected and tested against each other as an experimental search for the best of them. -\subsubsection{Artificial Intelligence algorithms} +\subsubsection{Artificial Intelligence Algorithms} Different algorithms for the engine to use should be implemented and tested. The results of this development and testing process should be presented as part of @@ -66,7 +66,7 @@ Computer Science at the University of Oviedo. The used material consists of a development and testing machine owned by the student with specifications stated later on the project plan. -\subsection{Work plan} +\subsection{Work Plan} The sole developer will be the student, who is currently working as a Junior Software Engineer on a 35 hour per week schedule and with no university @@ -92,9 +92,9 @@ Friday. Gantt diagrams for the planned working schedule are shown as \end{center} \end{figure} -\subsection{Previous works} +\subsection{Previous Works} -\subsubsection{Existing engines} +\subsubsection{Existing Engines} \paragraph{AlphaGo} @@ -103,28 +103,28 @@ owned by Google. It revolutionized the world of Go in 2015 and 2016 when it respectively became the first AI to win against a professional Go player and then won against Lee Sedol, a Korean player of the highest professional rank and one of the strongest players in the world at the time. Its source code is -closed, but a paper \parencite{natureAlphaGo2016} written by the team and +closed, but a paper\cite{natureAlphaGo2016} written by the team and published on Nature is available on https://storage.googleapis.com/deepmind-media/alphago/AlphaGoNaturePaper.pdf. The unprecedented success of AlphaGo served as inspiration for many AI projects, including this one. -\paragraph{KataGo~\cite{katago}} +\paragraph{KataGo\cite{katago}} An open source project based on the AlphaGo paper that also achieved superhuman strength of play. The availability of its implementation and documentation presents a great resource for this project. -\paragraph{GnuGo~\cite{gnugo}} +\paragraph{GnuGo\cite{gnugo}} A software capable of playing Go part of the GNU project. Although not a strong engine anymore, it is interesting for historic reasons as the free software engine for which the GTP protocol was first defined. -\subsubsection{Existing standards} +\subsubsection{Existing Standards} -\paragraph{GTP~\cite{gtp}} +\paragraph{GTP\cite{gtp}} GTP (\textit{Go Text Protocol}) is a text based protocol for communication with computer go programs. It is the protocol used by GNU Go and @@ -132,20 +132,23 @@ the more modern and powerful KataGo. By supporting GTP the engine developed for this project can be used with existing GUIs and other programs, making it easier to use it with the tools users are already familiar with. -\paragraph{SGF~\cite{sgf}} +\paragraph{SGF\cite{sgf}} -SGF (\textit{Smart Game Format}) is a text format widely used for storing -records of Go matches which allows for variants, comments and other metadata. -Many popular playing tools use it. By supporting SGF vast existing collections -of games can be used to train the decision algorithms based on neural networks. +SGF (\textit{Smart Go Format} or, in a more general context, \textit{Smart Game +Format}) is a text format widely used for storing records of Go matches which +allows for variants, comments and other metadata. It was devised for Go but it +supports other games with similar turn-based structure. Many popular playing +tools use it. By supporting SGF vast existing collections of games, such as +those played on online Go servers, can be used to train the decision algorithms +based on neural networks. -\subsubsection{Sabaki~\cite{sabaki}} +\subsubsection{Sabaki\cite{sabaki}} Sabaki is a go board software compatible with GTP engines. It can serve as a GUI for the engine developed in this project and as an example of the advantages of following a standardized protocol. -\subsubsection{Keras~\cite{keras}} +\subsubsection{Keras\cite{keras}} Keras is a deep learning API for Python allowing for the high-level definition of neural networks. This permits easily testing and comparing different network @@ -153,7 +156,7 @@ layouts. \subsection{Technological Infrastructure} -\subsubsection{Programming language}\label{sec:programmingLanguage} +\subsubsection{Programming Language}\label{sec:programmingLanguage} The resulting product of this project will be one or more pieces of software able to be run locally on a personal computer. The programming language of diff --git a/doc/tex/previousWorks.tex b/doc/tex/previousWorks.tex deleted file mode 100644 index 6e503a3..0000000 --- a/doc/tex/previousWorks.tex +++ /dev/null @@ -1,22 +0,0 @@ -\section{Previous works} - -\subsection{SGF} - -SGF (\textit{Smart Go Format} or, in a more general context, \textit{Smart Game -Format}) is a text file format specification for records of games or collections -of them. It was devised for Go but it supports other games with similar -turn-based structure. It supports move variations, annotations, setups and game -metadata. By supporting SGF our application can be used to analyse existing -games registered by other applications, such as those played on online Go -servers. - -The SGF specification can be found at -\url{https://www.red-bean.com/sgf/user_guide/index.html} - -\subsection{GTP} - -GTP (\textit{Go Text Protocol}) is a text based protocol for communication with -computer go programs. [ref https://www.lysator.liu.se/~gunnar/gtp/] It is the -protocol used by GNU Go and the more modern and powerful KataGo. By supporting -GTP our application can be used with existing GUIs and other programs, making -it easier to use it with the tools users are already familiar with. diff --git a/doc/tex/results.tex b/doc/tex/results.tex new file mode 100644 index 0000000..3c586e4 --- /dev/null +++ b/doc/tex/results.tex @@ -0,0 +1,416 @@ +\newcommand{\boards}[5]{ +\begin{figure}[h] + \begin{center} + \includegraphics[width=0.4\textwidth]{#1} + \includegraphics[width=0.4\textwidth]{#2} + \caption{#3} + \label{#4} + #5 + \end{center} +\end{figure} +} + +\newcommand{\moveHeatmap}[9]{ +\begin{figure}[h] + \begin{center} + \includegraphics[width=0.35\textwidth]{#1} + \includegraphics[width=0.5\textwidth]{#2} + \\[\smallskipamount] + \includegraphics[width=0.35\textwidth]{#3} + \includegraphics[width=0.5\textwidth]{#4} + \\[\smallskipamount] + \includegraphics[width=0.35\textwidth]{#5} + \includegraphics[width=0.5\textwidth]{#6} + \caption{#7} + \label{#8} + #9 + \end{center} +\end{figure} +} + +\section{Results} + +\subsection{Monte Carlo Tree Search Evaluation} + +The Monte Carlo Algorithm tries to explore the tree of possibilities as +efficiently as possible. With this approach, it can be expected to fail when +alone on a problem such big as the game of Go. Nonetheless, there are some areas +where it can be useful. It will be evaluated by its capabilities while playing +games but also when presented with go problems. + +The Monte Carlo algorithm has been set to do 5 explorations with 10 simulations +each when it is asked for a move. In the hardware used this makes it think for +40 seconds on an empty 9x9 board. + +\subsubsection{Monte Carlo Tree Search against itself} + +\boards +{img/matches/mctsVSmcts01.jpg} +{img/matches/mctsVSmcts02.jpg} +{Monte Carlo Tree Search against itself.} +{fig:mctsVSmcts} +{Both boards are from the same match. The first shows only the first 12 moves +for clarity. The second shows the finished game.} + +When set to play against itself the algorithm shows what looks like random play. +Some moves could be debated as sensible, but many of them are on the second or +fist line, which so early on the game are the two worst places to play at. + +It seems clear that this algorithm, at least with this specific configuration, +can't handle the size of an empty Go board, even on the 9x9 size. + +A record of the game is shown in \fref{fig:mctsVSmcts}. + +\subsubsection{Monte Carlo Tree Search against Go problems} + +\boards +{img/problems/mctsProblem01.jpg} +{img/problems/mctsProblem02.jpg} +{Monte Carlo against the first of Cho Chikun's problems.} +{fig:mctsProblem01} + +Since tree exploration on smaller boards or advanced games with little empty +spaces should be easier the algorithm has also been tested on some Go problems. + +A Go problem or tsumego is a predefined layout of the board, or of a section of +the board, for which the player must find some beneficial move. Life and death +problems are a subset of tsumegos in which the survival of a group depends on +finding the correct sequence to save or kill the group. One collection of such +tsumegos is \textit{Cho Chikun's Encyclopedia of Life and Death}, part of which +are available on OGS\cite{ogsLifeAndDeath}, an online go server. + +The first of these problems and what the algorithm suggested as moves is shown +in \fref{fig:mctsProblem01}. + +Black makes the first move, which means the solution is to find some favorable +outcome for black, which in this case is killing the white group. The white +group has a critical point on B1. If white plays on B1 they make two eyes and +live, but if black plays there first white can't make two eyes and dies, so B1 +is the solution to the tsumego. This is one of the easiest Go problems. + +The algorithm neglects this solution. While asked five times to generate a move +for the starting position it suggested B1 only once. + +But notably, after making another move, it consistently suggested B1 for white, +which is the solution now that white has to play. So in the end it was able to +solve the tsumego, probably because after making a move it had already explored +part of the tree but it was difficult that it explored the solution for the +first move. + +The engine was tested against other tsumegos but it was not able to solve them, +so no more are shown here. + +\subsection{Neural Network Training} + +Each network has been training by receiving batches of SGF files which are first +converted to lists of moves by the Training System and then provided to the +train function of the networks. This has been executed with the following +command: + +\inputminted[fontsize=\small]{bash}{listings/trainCommand.sh} + +Which lists the contents of a folder containing multiple SGF files, shuffles the +list, takes some of them and passes them to train.py as arguments. The combined +list of game positions and moves made from them in all the games selected make +up the sample of the training. The number of files selected can of course be +adapted to the situation. + +The networks were configured to train for 20 epochs, that is, processing the +full batch and updating the weights on the network 20 times. 10\% of the sample +were used as validation. This means they were not used for training but to +check the accuracy and loss of the network after training with the rest of +the batch. This technique of validation can help detect overfitting, which +happens when a network does a very good job on the population it has been +trained on but it fails when receiving unseen data. + +The training shows loss decrementing and accuracy incrementing on each epoch for +both training and validation samples, which suggests there is learning happening +and no overfitting occurring. + +The number of files to train on has been selected by trying to keep the +resulting training time manageable for the developer. It was found that +incrementing the files per batch did not increment the training time linearly, +as with batches of 10 games an epoch of training could be completed in one +minute, in three minutes batches of 20 games, and in up to an hour with batches +of 100 games. + +The outputs from this training process can be seen on \flist{code:denseTraining} +and \flist{code:convTraining} for the dense network and the convolutional +network respectively. + +\begin{listing}[h] + \inputminted[fontsize=\scriptsize]{text}{listings/denseTraining.txt} + \caption{Dense neural network training log.} + \label{code:denseTraining} +\end{listing} + +\begin{listing}[h] + \inputminted[fontsize=\scriptsize]{text}{listings/convTraining.txt} + \caption{Convolutional neural network training log.} + \label{code:convTraining} +\end{listing} + +\subsection{Neural Network Evaluation} + +One way of studying the results of the training could be to programmatically +test if the network is able to predict human moves when provided with a game +file. This has the drawback of not directly testing the strength of the network, +since it would be tested on predicting the moves of some human player, whose +level could greatly vary, and not on its playing capabilities alone. This also +has already been tested to some extent by the validation portion of the samples +providing for training, and the results show the network increasing its accuracy +of predicting human moves through the training epochs. + +Another way is to make the network play either against itself, another network, +or a human player, and analyze the score it provides to each possible play. The +output of the network is a vector with the likelihood of making each move, so +we can take this values as how strongly the engine suggests each of them. + +\subsection{Neural Networks Against Themselves} + +The case of neural networks playing against themselves is interesting in that it +shows moves with high certainty, that is, the network strongly suggests a +specific move. It can be thought that this would be the most common line of play +the engine has seen, with one certain move leading into another. + +This happened for both the dense neural network and the convolutional network. + +\subsubsection{Dense network Against Itself} + +The initial moves of a match of the dense network against itself, along with +heatmaps showing the output from the network (which chance it gives for each +move), can be seen on Figs.~\ref{fig:denseVSdense01}, \ref{fig:denseVSdense02}, +\ref{fig:denseVSdense03} and \ref{fig:denseVSdense04}. + +The dense network starts on the center of the board, which is one of the +standard openings in the 9x9 board. It starts on a very good track, but we must +acknowledge that the empty board is a position present on every go match it has +trained on and so it should know it well. It probably means the center was the +most played opening in the sample. It is interesting to check the heatmap of +this move, since the selected move has only a score of 0.27. Other common +openings on the 9x9 are represented on the vertices with some significant score. + +The heatmap of the response to the center play is also interesting. The four +highest scored moves are equivalent, since the first move has not broken any +symmetry. The move they represent, two spaces from the first stone, is also the +consensual response. The three moves following in score are also equivalent +between them. + +The first white stone has broken the symmetry in one direction, and so the +second black move is suggested with more likelihood than the previous one. This +is also a very common sequence of play. + +The following moves are a white attack followed by black's sensible response, +and a strong white extension (arguably to the wrong side, but the minutia of why +the other side would be better is probably not grasped by the engine). The +following moves could be called looking for complication, which is a strategy. + +Overall some moves could be criticised but the engine is showing at least a +basic knowledge of the game's strategy. + +\subsubsection{Convolutional Network Against Itself} + +The same information on a match of the convolutional network against itself can +be seen on Figs.~\ref{fig:convVSconv01}, \ref{fig:convVSconv02}, +\ref{fig:convVSconv03} and \ref{fig:convVSconv04}. + +The convolutional network also likes to start on the center of the board and is +more certain of the move than the dense network. Its next two plays are also the +same, but the match starts to diverge from the fourth move. + +Instead of attacking the black stone on the side, white plays directly under at +B5, keeping the edge of symmetry. This is strategically important because while +symmetry is present playing on either side makes no difference, while playing +after the opponent has broken symmetry gives the opportunity to respond and +capitalize on the now better side to play. + +Black responds to the white stone by attacking it at B4 and white then extends +to the more open side with F7. + +The match up to this point is discussed on Sensei's Library as an interesting +sequence deriving from the center first stone\cite{sl_sword}. The discussion +there states that white should have extended to the other side. + +The following moves are some sensible continuations, with approaches and +responses on each side. They are probably not the best moves, but the idea +behind them can be read from the board. + +\subsubsection{Dense Network Against Convolutional} + +The first 12 and the last 6 moves of a match of the dense network against the +convolutional network can be seen on be seen on Figs.~\ref{fig:denseVSconv01}, +\ref{fig:denseVSconv02}, \ref{fig:denseVSconv03}, \ref{fig:denseVSconv04}, +\ref{fig:denseVSconv05} and \ref{fig:denseVSconv06}. + +The dense network is given black. The convolutional network plays then as white. + +The first three moves are known territory for both networks, since both play the +same sequence when matched against themselves. + +After white plays under the black stone as the fourth move, black makes a maybe +too careful extension, but it seemed very sure about it. Now white has some +doubts. It seems to like the extensions to both sides, if a bit more to the more +open side, a preference both engines have previously shown at this point of the +match. The move it prefers the most, though, would be to play again under black. +So, as this move is impossible, it defers to the extension to the open side with +F3. + +Black now makes the mistake of overattacking the lone white stone on B5 with B6, +and white capitalizes on it by keeping extending with D3. B5 is still a move +with a not negligible score, but it is no longer the highest. + +The following moves are questionable, with black keeping building a very tight +group and getting no territory and white attacking too directly with a lone +stone into that strong group. + +In the final moves of the game we see that black has built a living group on the +right side and things are looking good for it on the rest of the board. White +has passed a bunch of times, starting too early, which means it gives too high +an importance to passing. Since the networks learned only by seeing moves in +response to boards, it has not learnt that it should not pass until there is no +more to gain. But it is also important to note that passing was not the most +likely move for the engine to make, it just happens that the move it would like +to make is already taken and the highest chance available move is to pass. + +\moveHeatmap +{img/matches/denseVSdense_board_01.jpg} +{img/matches/denseVSdense_heatmap_01.png} +{img/matches/denseVSdense_board_02.jpg} +{img/matches/denseVSdense_heatmap_02.png} +{img/matches/denseVSdense_board_03.jpg} +{img/matches/denseVSdense_heatmap_03.png} +{Dense network against itself, moves 1 through 3.} +{fig:denseVSdense01} + +\moveHeatmap +{img/matches/denseVSdense_board_04.jpg} +{img/matches/denseVSdense_heatmap_04.png} +{img/matches/denseVSdense_board_05.jpg} +{img/matches/denseVSdense_heatmap_05.png} +{img/matches/denseVSdense_board_06.jpg} +{img/matches/denseVSdense_heatmap_06.png} +{Dense network against itself, moves 4 through 6.} +{fig:denseVSdense02} + +\moveHeatmap +{img/matches/denseVSdense_board_07.jpg} +{img/matches/denseVSdense_heatmap_07.png} +{img/matches/denseVSdense_board_08.jpg} +{img/matches/denseVSdense_heatmap_08.png} +{img/matches/denseVSdense_board_09.jpg} +{img/matches/denseVSdense_heatmap_09.png} +{Dense network against itself, moves 7 through 9.} +{fig:denseVSdense03} + +\moveHeatmap +{img/matches/denseVSdense_board_10.jpg} +{img/matches/denseVSdense_heatmap_10.png} +{img/matches/denseVSdense_board_11.jpg} +{img/matches/denseVSdense_heatmap_11.png} +{img/matches/denseVSdense_board_12.jpg} +{img/matches/denseVSdense_heatmap_12.png} +{Dense network against itself, moves 10 through 12.} +{fig:denseVSdense04} + +\moveHeatmap +{img/matches/convVSconv_board_01.jpg} +{img/matches/convVSconv_heatmap_01.png} +{img/matches/convVSconv_board_02.jpg} +{img/matches/convVSconv_heatmap_02.png} +{img/matches/convVSconv_board_03.jpg} +{img/matches/convVSconv_heatmap_03.png} +{Convolutional network against itself, moves 1 through 3.} +{fig:convVSconv01} + +\moveHeatmap +{img/matches/convVSconv_board_04.jpg} +{img/matches/convVSconv_heatmap_04.png} +{img/matches/convVSconv_board_05.jpg} +{img/matches/convVSconv_heatmap_05.png} +{img/matches/convVSconv_board_06.jpg} +{img/matches/convVSconv_heatmap_06.png} +{Convolutional network against itself, moves 4 through 6.} +{fig:convVSconv02} + +\moveHeatmap +{img/matches/convVSconv_board_07.jpg} +{img/matches/convVSconv_heatmap_07.png} +{img/matches/convVSconv_board_08.jpg} +{img/matches/convVSconv_heatmap_08.png} +{img/matches/convVSconv_board_09.jpg} +{img/matches/convVSconv_heatmap_09.png} +{Convolutional network against itself, moves 7 through 9.} +{fig:convVSconv03} + +\moveHeatmap +{img/matches/convVSconv_board_10.jpg} +{img/matches/convVSconv_heatmap_10.png} +{img/matches/convVSconv_board_11.jpg} +{img/matches/convVSconv_heatmap_11.png} +{img/matches/convVSconv_board_12.jpg} +{img/matches/convVSconv_heatmap_12.png} +{Convolutional network against itself, moves 10 through 12.} +{fig:convVSconv04} + +\moveHeatmap +{img/matches/denseVSconv_board_01.jpg} +{img/matches/denseVSconv_heatmap_01.png} +{img/matches/denseVSconv_board_02.jpg} +{img/matches/denseVSconv_heatmap_02.png} +{img/matches/denseVSconv_board_03.jpg} +{img/matches/denseVSconv_heatmap_03.png} +{Dense network against convolutional, moves 1 through 3.} +{fig:denseVSconv01} +{The dense network plays as black.} + +\moveHeatmap +{img/matches/denseVSconv_board_04.jpg} +{img/matches/denseVSconv_heatmap_04.png} +{img/matches/denseVSconv_board_05.jpg} +{img/matches/denseVSconv_heatmap_05.png} +{img/matches/denseVSconv_board_06.jpg} +{img/matches/denseVSconv_heatmap_06.png} +{Dense network against convolutional, moves 4 through 6.} +{fig:denseVSconv02} + +\moveHeatmap +{img/matches/denseVSconv_board_07.jpg} +{img/matches/denseVSconv_heatmap_07.png} +{img/matches/denseVSconv_board_08.jpg} +{img/matches/denseVSconv_heatmap_08.png} +{img/matches/denseVSconv_board_09.jpg} +{img/matches/denseVSconv_heatmap_09.png} +{Dense network against convolutional, moves 7 through 9.} +{fig:denseVSconv03} + +\moveHeatmap +{img/matches/denseVSconv_board_10.jpg} +{img/matches/denseVSconv_heatmap_10.png} +{img/matches/denseVSconv_board_11.jpg} +{img/matches/denseVSconv_heatmap_11.png} +{img/matches/denseVSconv_board_12.jpg} +{img/matches/denseVSconv_heatmap_12.png} +{Dense network against convolutional, moves 10 through 12.} +{fig:denseVSconv04} + +\moveHeatmap +{img/matches/denseVSconv_board_72.jpg} +{img/matches/denseVSconv_heatmap_72.png} +{img/matches/denseVSconv_board_73.jpg} +{img/matches/denseVSconv_heatmap_73.png} +{img/matches/denseVSconv_board_74.jpg} +{img/matches/denseVSconv_heatmap_74.png} +{Dense network against convolutional, moves 72 through 74.} +{fig:denseVSconv05} + +\moveHeatmap +{img/matches/denseVSconv_board_75.jpg} +{img/matches/denseVSconv_heatmap_75.png} +{img/matches/denseVSconv_board_76.jpg} +{img/matches/denseVSconv_heatmap_76.png} +{img/matches/denseVSconv_board_77.jpg} +{img/matches/denseVSconv_heatmap_77.png} +{Dense network against convolutional, moves 75 through 77.} +{fig:denseVSconv06} + +% Needed line so the processing of the last command doesn't find an EOF diff --git a/doc/tex/systemAnalysis.tex b/doc/tex/systemAnalysis.tex index e4962d3..a3d66de 100644 --- a/doc/tex/systemAnalysis.tex +++ b/doc/tex/systemAnalysis.tex @@ -1,6 +1,6 @@ \section{System Analysis} -\subsection{System reach determination} +\subsection{System Reach Determination} These are the main goals the final product must reach. @@ -277,7 +277,7 @@ The Training System will process SGF files storing records of games, train the neural network models over those games and store the result. These models can then be imported by the engine and be used to generate moves. -\subsubsection{Interface between subsystems} +\subsubsection{Interface Between Subsystems} The Training System depends on the NeuralNetwork interface of the Engine System and uses it to train and store the neural network models. @@ -287,9 +287,9 @@ System. The Engine System uses it to store the state of a game and provide it to the decision algorithms. The Training System uses it to create the internal representation of a game resulting from the processing of an SGF file. -\subsection{Class analysis} +\subsection{Class Analysis} -\subsubsection{Class diagram} +\subsubsection{Class Diagram} The classes resulting from the analysis phase are shown in \fref{fig:analysisClasses}. @@ -302,15 +302,14 @@ The classes resulting from the analysis phase are shown in \end{center} \end{figure} -\subsubsection{Class description} +\subsubsection{Class Description} \newcommand{\interclassSpace}{30pt} \paragraph{Engine System} - \indent \\ -\begin{tabular}{p{\linewidth}} +\begin{tabular}{p{0.9\linewidth}} \toprule \textbf{EngineIO} \\ \midrule @@ -318,7 +317,6 @@ The classes resulting from the analysis phase are shown in Offers the interface with the engine. \\ \midrule \textbf{Responsibilities} \\ - % TODO: Single responsibility would be better? \tabitem{Read input.} \\ \tabitem{Do some preprocessing.} \\ \tabitem{Forward commands to the engine logic component.} \\ @@ -332,7 +330,7 @@ The classes resulting from the analysis phase are shown in \vspace{\interclassSpace} -\begin{tabular}{p{\linewidth}} +\begin{tabular}{p{0.9\linewidth}} \toprule \textbf{EngineLogic} \\ \midrule @@ -362,7 +360,7 @@ The classes resulting from the analysis phase are shown in now generate moves as from a new game.} \\ } -\begin{tabular}{p{\linewidth}} +\begin{tabular}{p{0.9\linewidth}} \toprule \textbf{DecisionAlgorithm} \\ \midrule @@ -382,7 +380,7 @@ The classes resulting from the analysis phase are shown in \vspace{\interclassSpace} -\begin{tabular}{p{\linewidth}} +\begin{tabular}{p{0.9\linewidth}} \toprule \textbf{MonteCarloTreeSearch} \\ \midrule @@ -404,7 +402,7 @@ The classes resulting from the analysis phase are shown in \vspace{\interclassSpace} -\begin{tabular}{p{\linewidth}} +\begin{tabular}{p{0.9\linewidth}} \toprule \textbf{MCTSNode} \\ \midrule @@ -443,7 +441,7 @@ The classes resulting from the analysis phase are shown in \vspace{\interclassSpace} -\begin{tabular}{p{\linewidth}} +\begin{tabular}{p{0.9\linewidth}} \toprule \textbf{Keras} \\ \midrule @@ -467,7 +465,7 @@ The classes resulting from the analysis phase are shown in \vspace{\interclassSpace} -\begin{tabular}{p{\linewidth}} +\begin{tabular}{p{0.9\linewidth}} \toprule \textbf{NeuralNetwork} \\ \midrule @@ -505,7 +503,7 @@ The classes resulting from the analysis phase are shown in \indent \\ -\begin{tabular}{p{\linewidth}} +\begin{tabular}{p{0.9\linewidth}} \toprule \textbf{GameState} \\ \midrule @@ -532,7 +530,7 @@ The classes resulting from the analysis phase are shown in \vspace{\interclassSpace} -\begin{tabular}{p{\linewidth}} +\begin{tabular}{p{0.9\linewidth}} \toprule \textbf{GameBoard} \\ \midrule @@ -560,7 +558,7 @@ The classes resulting from the analysis phase are shown in \vspace{\interclassSpace} -\begin{tabular}{p{\linewidth}} +\begin{tabular}{p{0.9\linewidth}} \toprule \textbf{GameMove} \\ \midrule @@ -603,7 +601,7 @@ The classes resulting from the analysis phase are shown in \vspace{\interclassSpace} -\begin{tabular}{p{\linewidth}} +\begin{tabular}{p{0.9\linewidth}} \toprule \textbf{GameBoard} \\ \midrule @@ -637,13 +635,11 @@ The classes resulting from the analysis phase are shown in \vspace{\interclassSpace} -%TODO: Finish the classes of the Game System - \paragraph{Training System} \indent \\ -\begin{tabular}{p{\linewidth}} +\begin{tabular}{p{0.9\linewidth}} \toprule \textbf{Trainer} \\ \midrule @@ -658,13 +654,14 @@ The classes resulting from the analysis phase are shown in %TODO: Explain why this is empty \midrule \textbf{Proposed methods} \\ - %TODO: Explain why this is empty + \tabitem{\textbf{loadGameTree()}: Reads a file and generates a GameMove tree + from its contents.} \\ \bottomrule \end{tabular} \vspace{\interclassSpace} -\begin{tabular}{p{\linewidth}} +\begin{tabular}{p{0.9\linewidth}} \toprule \textbf{Parser} \\ \midrule @@ -686,7 +683,7 @@ The classes resulting from the analysis phase are shown in \vspace{\interclassSpace} -\begin{tabular}{p{\linewidth}} +\begin{tabular}{p{0.9\linewidth}} \toprule \textbf{ASTNode} \\ \midrule @@ -751,17 +748,17 @@ against another machine player. The engine interface reads the input for generating a move as stated by the GTP protocol and outputs the coordinates of the board to play. -\subsection{Use case analysis and scenarios} +\subsection{Use Case Analysis and Scenarios} \begin{figure}[h] \begin{center} - \includegraphics[width=\textwidth]{diagrams/useCase_playAMatch.png} + \includegraphics[width=0.8\textwidth]{diagrams/useCase_playAMatch.png} \caption{Use case: Play a match.} \label{fig:useCase_playAMatch} \end{center} \end{figure} -\begin{tabular}{lp{0.7\linewidth}} +\begin{tabular}{lp{0.6\linewidth}} \toprule \multicolumn{2}{c}{\textbf{Play a match}} \\ \midrule @@ -804,7 +801,7 @@ GTP protocol and outputs the coordinates of the board to play. \end{center} \end{figure} -\begin{tabular}{lp{0.7\linewidth}} +\begin{tabular}{lp{0.6\linewidth}} \toprule \multicolumn{2}{c}{\textbf{Generate a move}} \\ \midrule @@ -846,7 +843,7 @@ GTP protocol and outputs the coordinates of the board to play. \end{center} \end{figure} -\begin{tabular}{lp{0.7\linewidth}} +\begin{tabular}{lp{0.6\linewidth}} \toprule \multicolumn{2}{c}{\textbf{Use as backend for machine player}} \\ \midrule diff --git a/doc/tex/systemDesign.tex b/doc/tex/systemDesign.tex index ccd1c48..80a2ccb 100644 --- a/doc/tex/systemDesign.tex +++ b/doc/tex/systemDesign.tex @@ -1,95 +1,324 @@ \section{System Design} -\subsection{Class diagram} +\subsection{Class Diagram} -The full class diagram is shown in \fref{fig:fullClasses} +The full class diagram is shown in \fref{fig:fullClasses}. \begin{figure}[h] \begin{center} - \includegraphics[width=\textwidth]{diagrams/fullClasses.png} - \caption{Full class diagram.} - \label{fig:fullClasses} + \makebox[0pt]{\begin{minipage}{1.2\textwidth} + \includegraphics[width=\textwidth]{diagrams/fullClasses.png} + \caption{Full class diagram.} + \label{fig:fullClasses} + \end{minipage}} \end{center} \end{figure} -% From here +The design of each system of the diagram is explained after this section +together with diagrams for each subsystem, since the full class diagram can be +too big to be comfortably analyzed. +\subsection{Game} + +\begin{figure}[h] + \begin{center} + \includegraphics[width=0.55\textwidth]{diagrams/gameModule.png} + \caption{Design of the implementation of the game of Go.} + \label{fig:game} + A game is represented as a tree of moves. + \end{center} +\end{figure} + +A regular Go match is composed of a list of moves. But since game review and +variants exploration is an important part of Go learning, \program{} and most +playing and analysis existing programs allow for navigation back and forth +through the board states of a match and for new variants to be created from each +of these board states. Therefore, a match is represented as a tree of moves. The +GameMove class has the information about a specific move and also a reference to +the previous move and to a list of following moves, implementing this tree +structure and allowing for navigating both forward and backwards in the move +history. + +The state of the board at any given move must be stored so liberties, captures +count and legality of moves can be addressed, so it is represented with the +GameState class, which holds a reference to the current move. + +Moves depend on a representation of the game board to have access to its current +layout and count of captured stones. There are also many logic operations needed +to be performed on the board, such as getting the stones in a group, counting +their liberties or checking if a move is playable. The layout of the board and +these operations are implemented as the GameBoard class. + +A game can be started by the executable \texttt{go.py}. + +These classes and their relationships can be seen in \fref{fig:game}. \subsection{Engine} +\begin{figure}[h] + \begin{center} + \includegraphics[width=0.8\textwidth]{diagrams/engineModule.png} + \caption{Design of the GTP engine.}\label{fig:engine} + \end{center} +\end{figure} + An implementation of GTP, that is, the piece of software which offers the GTP -interface to other applications.\@ It is designed to be used by a software +interface to other applications. It is designed to be used by a software controller but can also be directly run, mostly for debugging purposes. Its design is shown in \fref{fig:engine}. The core of the engine is related with three components, each with a separate responsibility: \begin{itemize} - \item The IO component is the one called from other applications and offers - the text interface. It reads and processes input and calls corresponding - commands from the core of the engine. - \item The EngineBoard component stores the state of the match, recording - information such as the history of boards positions and whose turn goes - next. The engine core uses it for these state-storing purposes. - \item The EngineAI component is responsible of analyzing the match and - generate moves. The engine core uses it when a decision has to be made - by the AI, such as when a move needs to be generated by the engine. + + \item The ImagoIO component is the one imported from executables or other + applications and offers the text interface. It reads and processes input + and calls corresponding commands from the core of the engine. + + \item The GameEngine contains the logic of the commands available from the + IO component. It uses a GameState to keep a record of the game and uses + a DecisionAlgorithm to generate moves. + + \item The DecisionAlgorithm component is responsible of analyzing the match + and generate moves. The engine core uses it when a decision has to be + made by the AI, such as when a move needs to be generated by the engine. + +\end{itemize} + +Two implementations of DecisionAlgorithm have been made: one for the Monte +Carlo Tree Search algorithm (on the MCTS class) and the other for neural +networks (on the Keras class). + +The Keras class also makes use of the NeuralNetwork class, which offers +functions for creating, training, saving and using neural network models. The +designs of the network are implemented in the subclasses DenseNeuralNetwork and +ConvNeuralNetwork as examples of dense and convolutional networks, respectively. + +The engine can be started with the executable \texttt{imagocli.py}. + +\subsubsection{Monte Carlo Tree Search Explained} + +Monte Carlo Tree Search is an algorithm that can be useful for exploring +decision trees. It was used by AlphaGo in conjunction with neural networks as +explained in the AlphaGo 2016 paper\cite{natureAlphaGo2016}. + +The algorithm assigns a score to each explored node based on how likely the +player who makes the corresponding move is to win and updates this score on each +exploration cycle. + +The exploration of the tree has 4 steps: + +\begin{enumerate} + + \item \textbf{Selection}: The most promising move with unexplored children + is selected for exploration. Unexplored children are viable moves which + are not yet part of the tree. + + \item \textbf{Expansion}: An unexplored children of the selected move is + added to the tree. This children is selected at random. + + \item \textbf{Simulation}: The score of the new move is evaluated by playing + random matches from it. + + \item \textbf{Backpropagation}: The score of the new move, as well as its + previous moves up to the root of the tree, is updated based on the + results of the simulation. + +\end{enumerate} + +The suggested move is the children of the current move with the best score from +the perspective of the player which has to make the move. + +The implementation of the algorithm will use the existing GameMove class from +the Game System to access the game logic it needs, such as to get the possible +children from a node or to simulate random games. + +\subsubsection{Neural Networks Explained} + +A neural network is composed of nodes or ``neurons''. Each node contains a value +named weight. During execution the node receives a numeric input, multiplies it +for its weight and applies a function called activation function to the result. + +Nodes are organized in layers so that a network contains several layers and each +layer one or more neurons. The input to the network forms the input layer, which +contents are forwarded to the second layer. The second layer applies the weight +and activation function as discussed before in each of its nodes and the result +is forwarded to the next layer, and so on. At the end the last layer, called the +output layer, contains the result of the input evaluation. Each layer can have a +unique activation function for all its nodes and a different strategy of +connecting to the previous and next layers. + +Several kinds of layers have been used in this project: + +\begin{itemize} + + \item \textbf{Dense layers}, which connects each of its nodes to each of the + nodes of the previous layers. + + \item \textbf{Convolutional layers}, which process their input by applying + a filter function to adjacent values. In the case of this project, the + board is filtered by grouping its vertices in 3x3 squares. The aim of + these layers is to detect patterns in the input, such as curves, edges + or more complex shapes, so they are used a lot on neural networks + processing images. They are used in this project because a configuration + of the Go board is not that different from a two-dimensional image. + + \item \textbf{Max pooling layers}, which process their input in a similar + way to convolutional layers, but reducing the size of the input by + keeping the highest value in each of the groups they process. This + reduction accentuates the patterns detected by convolutional layers and + helps on the detection of bigger, more complex patterns. + + \item \textbf{Flatten layers}, which just change the shape of the input so + that all the values are on one dimension. + \end{itemize} +Combinations of these layers have been used to define two neural networks. + +First, a network using mainly dense layers as an example of a more general +purpose design of a network. + +Then, a network with convolutional and max pooling layers to compare the +approach used on image processing to the more general one and studying its +utility on the analysis of the Go board. + +These networks have been implemented on the DenseNeuralNetwork and +ConvNeuralNetwork classes, respectively. + +The networks have been designed to process boards of size 9x9, which is the +introductory size to the game. It is the easiest both for the hardware to handle +and for the analysis of results while keeping able to support meaningful +matches. + +Both networks have the same design for their input and output. + +Their input is a three-dimensional matrix of size 9x9x2 with values either 0 or +1. It represents two views of the board, one with ones as the stones of a player +and the other with ones as the stones of the other player. + +Their output is a vector with 82 elements of type float. Classification networks +typically use a vector of probabilities with one element for each class they are +trying to classify. Here the classes are the 81 positions of the 9x9 board and +the pass move, hence 82 total elements. Each element signifies the chance of +playing that move for the input board position, so the element with the highest +value represents the suggested move. + +\subsubsection{Dense Neural Network Design} + +\begin{listing}[h] + \inputminted{text}{listings/denseModel.txt} + \caption{Dense neural network model.} + \label{code:denseModel} +\end{listing} + \begin{figure}[h] \begin{center} - \includegraphics[width=\textwidth]{diagrams/engineModule.png} - \caption{Design of the GTP engine.}\label{fig:engine} + \includegraphics[width=0.7\textwidth]{img/models/denseModel.png} + \caption{Dense neural network model.} + \label{fig:denseNN} \end{center} \end{figure} -\subsection{Modules} +This network first uses two dense layers with 81 nodes each. This number has +been selected so each node can have as input each of the vertices of the board. +A flatten layer acts then to make the output one-dimensional, and a final dense +layer provides the vector containing the likelihood of each possible move. -One module to store the state of the game and the game tree. One module to parse -moves. One module to read and write SGF files. Modules are shown in -\fref{fig:modules}. +The design of this network is shown in \flist{code:denseModel} and +\fref{fig:denseNN} as provided by Keras' summary and plot\_model functions +respectively. + +\subsubsection{Convolutional Neural Network Design} + +\begin{listing}[h] + \inputminted{text}{listings/convModel.txt} + \caption{Convolutional neural network model.} + \label{code:convModel} +\end{listing} \begin{figure}[h] \begin{center} - \includegraphics[width=\textwidth]{diagrams/modules.png} - \caption{Modules.}\label{fig:modules} + \includegraphics[width=0.7\textwidth]{img/models/convModel.png} + \caption{Convolutional neural network model.} + \label{fig:convNN} \end{center} \end{figure} -\subsection{Representation of a match} +This network uses two pairs of convolutional and max pooling layers with the aim +of being trained to recognize patterns on the board. A flatten layer acts then +to make the output one-dimensional, and a final dense layer provides the vector +containing the likelihood of each possible move. -A regular Go match is composed of a list of moves. But since game review and -variants exploration is an important part of Go learning, \program{} allows for -navigation back and forth through the board states of a match and for new -variants to be created from each of these board states. Therefore, a match is -represented as a tree of moves. The state of the board at any given move must -also be stored so liberties, captures count and legality of moves can be -addressed, so it is represented with its own class, which holds a reference both -to the game tree and the current move. Moves depend on a representation of the -game board to have access to its current layout and count of captured stones. -These classes and their relationships can be seen in -\fref{fig:gameRepresentation}. +The design of this network is shown in \flist{code:convModel} and +\fref{fig:convNN} as provided by Keras' summary and plot\_model functions +respectively. + +\subsection{Training} \begin{figure}[h] \begin{center} - \includegraphics[width=0.7\textwidth]{diagrams/gameRepresentation.png} - \caption{A game is represented as a tree of moves.}\label{fig:gameRepresentation} + \includegraphics[width=\textwidth]{diagrams/trainingModule.png} + \caption{Components of the SGF file parsing module.} + \label{fig:trainingModule} + Components not showing a capital C are not classes, as in they not + follow the object-oriented paradigm and do not implement any classes, + only functions. \end{center} \end{figure} -\subsection{SGF} +Neural networks can be powerful machine learning algorithms, but they have to be +trained first so they can provide meaningful results. For a Go AI it makes sense +to have its algorithms trained on Go games. There exists a common text format to +store Go games: SGF. If the system is able to process SGF files, it can provide +the games stored on them to the neural networks for training. And so the need +for an SGF parser arises. To parse SGF files a lexer and parser have been implemented using PLY.\@ The result of the parsing is an AST (Annotated Syntax Tree) reflecting the contents of the text input, each node with zero or more properties, and with the ability -to convert themselves and their corresponding subtree into a GameTree. This is -done for the root node, since from the SGF specification there are some +to convert themselves and their corresponding subtree into a GameMove tree. This +is done for the root node, since from the SGF specification there are some properties only usable in the root node, like those which specify general game -information and properties such as rank of players or komi. These components are -shown in \fref{fig:sgfModule}. +information and properties such as rank of players or komi. -\begin{figure}[h] - \begin{center} - \includegraphics[width=\textwidth]{diagrams/sgfModule.png} - \caption{Components of the SGF file parsing module.}\label{fig:sgfModule} - \end{center} -\end{figure} +Here follows an explanation of the role and motivation before each component of +the Training module to show how these previous concerns have been addressed and +solved. These components are shown in \fref{fig:trainingModule}. + +\begin{itemize} + + \item \textbf{SGF}: Provides a high-level method to convert a path to a SGF + file to a GameMove tree. + + \item \textbf{sgfyacc}: The implementation of a SGF parser using PLY. Takes + the tokens generated by \textbf{sgflex} and creates an ASTNode tree from + them. + + \item \textbf{sgflex}: The implementation of a SGF lexer using PLY.\@ Takes + text input and generates the tokens of the SGF language from them. + + \item \textbf{ASTNode}: The AST resulting from the parsing of a SGF file. + Has a method to convert it to a tree of GameMove and so obtain the + contents of the SGF in the internal representation used by the project's + systems. + + \item \textbf{Property}: The representation of a property of an ASTNode + tree. Each property is made of a name and one or more values and this + class helps handling this specific situation. + +The training can be started with the executable \texttt{train.py}. + +\end{itemize} + +%\subsection{Modules} +% +%One module to store the state of the game and the game tree. One module to parse +%moves. One module to read and write SGF files. Modules are shown in +%\fref{fig:modules}. +% +%\begin{figure}[h] +% \begin{center} +% \includegraphics[width=\textwidth]{diagrams/modules.png} +% \caption{Modules.}\label{fig:modules} +% \end{center} +%\end{figure} diff --git a/doc/tex/tfg.tex b/doc/tex/tfg.tex deleted file mode 100644 index 4c64bb2..0000000 --- a/doc/tex/tfg.tex +++ /dev/null @@ -1,146 +0,0 @@ -\documentclass{article} - -\usepackage{geometry} -\usepackage{graphicx} -\usepackage{booktabs} -\usepackage{hyperref} -\usepackage{csquotes} -\usepackage{enumitem} - -\usepackage{chngcntr} -\counterwithin{figure}{section} - -\usepackage[backend=biber, style=numeric, sorting=none]{biblatex} -\addbibresource{tex/biber.bib} - -\usepackage{minted} % Code importing and formatting -\setminted{linenos, breaklines} - -\geometry{left=4cm,top=2cm,bottom=2cm,right=4cm} - -\hypersetup{colorlinks=false, - linkcolor=black, - filecolor=black, - urlcolor=black, - bookmarks=true -} - -\urlstyle{mono} - -\newcommand{\program}{Imago} - -\newcommand{\fref}[1]{Fig.~\ref{#1}} -\newcommand{\flist}[1]{Listing~\ref{#1}} -%\newcommand{\uurl}[1]{\underline{\url{#1}}} - -\newcommand{\tabitem}{~~\llap{\textbullet}~~} -\begin{document} - -\frenchspacing - -\title{\program\\ -\large An AI player of the game of Go} - -\author{Íñigo Gutiérrez Fernández} - -\date{} - -\maketitle - -\thispagestyle{empty} - -\begin{figure}[h] - \begin{center} - \includegraphics[width=0.6\textwidth]{img/imago.jpg} - \end{center} -\end{figure} - -\clearpage - -\begin{abstract} - This is the abstract. -\end{abstract} - -\clearpage - -\section*{Acknowledgements} - -TODO: Acknowledgements - -To Vicente García Díaz, for directing me in this project. - -To José Manuel Redondo López\cite{plantillaRedondo}, for making an extensive -template on which the structure of this documentation is extensively based. - -\clearpage - -\section*{Copyright notice} - -\begin{displayquote} - - Copyright (C) 2021 \textbf{ÍÑIGO GUTIÉRREZ FERNÁNDEZ}. - - \textit{Permission is granted to copy, distribute and/or modify this - document under the terms of the GNU Free Documentation License, Version 1.3 - or any later version published by the Free Software Foundation; with the - Copyright notice as an Invariant Section, no Front-Cover Texts, and no - Back-Cover Texts. A copy of the license is included in the section - entitled ``GNU Free Documentation License''.} - -\end{displayquote} - -Although this document uses the GNU Free Documentation License to keep its -contained information free, bear in mind that it isn't software documentation or -a manual or guide of any kind, and serves just as the documentation for the -author's Degree's Final Project. - -This document is based on a template by José Manuel Redondo López, associate -professor of the University of Oviedo. The copyright notice he asks for -inclusion to use this template is included here. - -\begin{displayquote} - - Copyright (C) 2019 \textbf{JOSÉ MANUEL REDONDO - LÓPEZ}.\cite{plantillaRedondo} - - \textit{Permission is granted to copy, distribute and/or modify this - document under the terms of the GNU Free Documentation License, Version 1.3 - or any later version published by the Free Software Foundation; with no - Invariant Sections, no Front-Cover Texts, and no Back-Cover Texts. A copy - of the license is included in the section entitled ``GNU Free - Documentation License''.} - -\end{displayquote} - -\clearpage - -\setcounter{secnumdepth}{3} -\setcounter{tocdepth}{4} -\tableofcontents - -\listoffigures - -\listoflistings - -\clearpage - -\input{tex/introduction.tex} - -\input{tex/planification.tex} - -\input{tex/systemAnalysis.tex} - -\input{tex/systemDesign.tex} - -\input{tex/implementation.tex} - -\clearpage - -% References (bibliography) - -\setcounter{secnumdepth}{0} -\section{References} -\printbibliography[type=article,title={Cited articles},heading=subbibintoc]{} -\printbibliography[type=online,title={Online resources},heading=subbibintoc]{} - -\end{document} diff --git a/imago/data/enums.py b/imago/data/enums.py index 56fe2cc..c790384 100644 --- a/imago/data/enums.py +++ b/imago/data/enums.py @@ -18,6 +18,7 @@ class Player(Enum): return cls.EMPTY class DecisionAlgorithms(Enum): - #RANDOM = enumAuto() MONTECARLO = enumAuto() KERAS = enumAuto() + DENSE = enumAuto() + CONV = enumAuto() diff --git a/imago/engine/core.py b/imago/engine/core.py index 4e383d4..2bf7d5a 100644 --- a/imago/engine/core.py +++ b/imago/engine/core.py @@ -1,6 +1,7 @@ """Imago GTP engine""" -from imago.engine.decisionAlgorithmFactory import DecisionAlgorithms, DecisionAlgorithmFactory +from imago.data.enums import DecisionAlgorithms +from imago.engine.createDecisionAlgorithm import create as createDA from imago.gameLogic.gameState import GameState DEF_SIZE = 9 @@ -14,8 +15,7 @@ class GameEngine: self.komi = DEF_KOMI self.gameState = GameState(DEF_SIZE) self.daId = decisionAlgorithmId - self.daFactory = DecisionAlgorithmFactory() - self.da = self.daFactory.create(self.daId, self.gameState.lastMove) + self.da = createDA(self.daId, self.gameState.lastMove) def setBoardsize(self, newSize): """Changes the size of the board. @@ -23,7 +23,7 @@ class GameEngine: It is wise to call clear_board after this command. """ self.gameState = GameState(newSize) - self.da = self.daFactory.create(self.daId, self.gameState.lastMove) + self.da = createDA(self.daId, self.gameState.lastMove) def clearBoard(self): """The board is cleared, the number of captured stones reset to zero and the move diff --git a/imago/engine/createDecisionAlgorithm.py b/imago/engine/createDecisionAlgorithm.py new file mode 100644 index 0000000..d7fafbf --- /dev/null +++ b/imago/engine/createDecisionAlgorithm.py @@ -0,0 +1,21 @@ +"""Create decision algorithms""" + +from imago.data.enums import DecisionAlgorithms +from imago.engine.monteCarlo import MCTS +from imago.engine.keras.keras import Keras + +def create(algorithm, move): + """Creates an instance of the requested algorithm.""" + + if algorithm == DecisionAlgorithms.MONTECARLO: + return MCTS(move) + + if algorithm == DecisionAlgorithms.KERAS: + return Keras(move) + + if algorithm == DecisionAlgorithms.DENSE\ + or algorithm == DecisionAlgorithms.CONV: + return Keras(move, algorithm) + + else: + return MCTS(move) diff --git a/imago/engine/decisionAlgorithmFactory.py b/imago/engine/decisionAlgorithmFactory.py deleted file mode 100644 index 25517fa..0000000 --- a/imago/engine/decisionAlgorithmFactory.py +++ /dev/null @@ -1,22 +0,0 @@ -"""Decision algorithm factory.""" - -from enum import Enum, auto as enumAuto -from imago.engine.monteCarlo import MCTS -from imago.engine.keras.keras import Keras - -class DecisionAlgorithmFactory: - - def create(self, algorithm, move): - """Creates an instance of the requested algorithm.""" - -# if algorithm == DecisionAlgorithms.RANDOM: -# -- - - if algorithm == DecisionAlgorithms.MONTECARLO: - return MCTS(move) - - if algorithm == DecisionAlgorithms.KERAS: - return Keras(move) - - else: - return MCTS(move) diff --git a/imago/engine/keras/denseNeuralNetwork.py b/imago/engine/keras/denseNeuralNetwork.py index ff2efa8..6a350f7 100644 --- a/imago/engine/keras/denseNeuralNetwork.py +++ b/imago/engine/keras/denseNeuralNetwork.py @@ -1,28 +1,40 @@ """Dense neural network.""" from tensorflow.keras.models import Sequential -from tensorflow.keras.layers import Dense +from tensorflow.keras.layers import Dense, Flatten from tensorflow.keras.optimizers import Adam from imago.engine.keras.neuralNetwork import NeuralNetwork -defaultModelFile = 'models/imagoDenseKerasModel.h5' - class DenseNeuralNetwork(NeuralNetwork): + NETWORK_ID = "denseNeuralNetwork" + DEFAULT_MODEL_FILE = "models/imagoDenseKerasModel.h5" + def _initModel(self, boardSize=NeuralNetwork.DEF_BOARD_SIZE): model = Sequential([ - Dense(units=32, activation='sigmoid', input_shape=(boardSize,boardSize)), - Dense(units=64, activation='relu'), - Dense(units=boardSize, activation='softmax') + Dense( + units=81, + activation="relu", + input_shape=(boardSize,boardSize,2) + ), + Dense( + units=81, + activation="relu" + ), + Flatten(), + Dense( + units=82, + activation="softmax" + ), ]) model.summary() model.compile( optimizer=Adam(learning_rate=0.0001), - loss='categorical_crossentropy', - metrics=['accuracy'] + loss="categorical_crossentropy", + metrics=["accuracy"] ) return model diff --git a/imago/engine/keras/keras.py b/imago/engine/keras/keras.py index 00b06d7..871f4a0 100644 --- a/imago/engine/keras/keras.py +++ b/imago/engine/keras/keras.py @@ -1,18 +1,31 @@ """Keras neural network module.""" +from imago.data.enums import DecisionAlgorithms from imago.gameLogic.gameMove import GameMove from imago.gameLogic.gameBoard import GameBoard from imago.engine.decisionAlgorithm import DecisionAlgorithm from imago.engine.keras.neuralNetwork import NeuralNetwork -MODEL_FILE = "/home/taamas/IIS/TFG/imago/models/imagoConvKerasModel.h5" +DENSE_MODEL_FILE = "/home/taamas/IIS/TFG/imago/models/imagoDenseKerasModel.h5" +CONV_MODEL_FILE = "/home/taamas/IIS/TFG/imago/models/imagoConvKerasModel.h5" +DEF_MODEL_FILE = CONV_MODEL_FILE class Keras(DecisionAlgorithm): - def __init__(self, move): + def __init__(self, move, network=None): self.currentMove = move + modelFile = DEF_MODEL_FILE + if network == DecisionAlgorithms.DENSE: + modelFile = DENSE_MODEL_FILE + elif network == DecisionAlgorithms.CONV: + modelFile = CONV_MODEL_FILE + elif network != None: + raise RuntimeError( + "Unknown decision algorithm code for neural network: %s" % network + ) + self.neuralNetwork = NeuralNetwork( - MODEL_FILE, + modelFile, move.board.getBoardHeight() ) diff --git a/imago/engine/monteCarlo.py b/imago/engine/monteCarlo.py index 0587cfc..baaaba8 100644 --- a/imago/engine/monteCarlo.py +++ b/imago/engine/monteCarlo.py @@ -37,7 +37,7 @@ class MCTS(DecisionAlgorithm): def _selectBestNextNode(self): """Returns best ucb node available for the current player.""" - # Assumes at least one expansion has occured + # Assumes at least one expansion has occurred bestUCB = -sys.maxsize - 1 bestNode = None for node in self.root.children: @@ -148,7 +148,7 @@ class MCTSNode: scoreDiff = result[0]-result[1] if scoreDiff != 0: scoreAcc += scoreDiff / abs(scoreDiff) - # Backup + # Backpropagation node = self while node is not None: node.score += scoreAcc diff --git a/imagocli.py b/imagocli.py index 150587e..5f14380 100755 --- a/imagocli.py +++ b/imagocli.py @@ -4,8 +4,8 @@ import sys +from imago.data.enums import DecisionAlgorithms from imago.engine.imagoIO import ImagoIO -from imago.engine.decisionAlgorithmFactory import DecisionAlgorithms if __name__ == "__main__": @@ -16,6 +16,10 @@ if __name__ == "__main__": decisionAlgorithm = DecisionAlgorithms.MONTECARLO if sys.argv[2] == "keras": decisionAlgorithm = DecisionAlgorithms.KERAS + if sys.argv[2] == "dense": + decisionAlgorithm = DecisionAlgorithms.DENSE + if sys.argv[2] == "conv": + decisionAlgorithm = DecisionAlgorithms.CONV if decisionAlgorithm is None: io = ImagoIO() diff --git a/testKeras.py b/testKeras.py deleted file mode 100755 index 0f518d0..0000000 --- a/testKeras.py +++ /dev/null @@ -1,27 +0,0 @@ -#!/usr/bin/python - -"""Starts training a keras neural network.""" - -import sys - -from imago.sgfParser.sgf import loadGameTree -from imago.engine.keras.denseNeuralNetwork import DenseNeuralNetwork -from imago.engine.keras.convNeuralNetwork import ConvNeuralNetwork - -def main(): - games = [] - for file in sys.argv[1:]: - print(file) - games.append(loadGameTree(file)) - - matches = [game.getMainLineOfPlay() for game in games] - - modelFile = "" - boardsize = 9 - nn = DenseNeuralNetwork(modelFile, boardsize) - #nn = ConvNeuralNetwork(modelFile, boardsize) - nn.trainModel(matches) - nn.saveModel() - -if __name__ == '__main__': - main() diff --git a/train.py b/train.py new file mode 100755 index 0000000..0f518d0 --- /dev/null +++ b/train.py @@ -0,0 +1,27 @@ +#!/usr/bin/python + +"""Starts training a keras neural network.""" + +import sys + +from imago.sgfParser.sgf import loadGameTree +from imago.engine.keras.denseNeuralNetwork import DenseNeuralNetwork +from imago.engine.keras.convNeuralNetwork import ConvNeuralNetwork + +def main(): + games = [] + for file in sys.argv[1:]: + print(file) + games.append(loadGameTree(file)) + + matches = [game.getMainLineOfPlay() for game in games] + + modelFile = "" + boardsize = 9 + nn = DenseNeuralNetwork(modelFile, boardsize) + #nn = ConvNeuralNetwork(modelFile, boardsize) + nn.trainModel(matches) + nn.saveModel() + +if __name__ == '__main__': + main() -- cgit v1.2.1 From 1eb1c9c538bb9380d07efe1175ced8febb78365b Mon Sep 17 00:00:00 2001 From: InigoGutierrez Date: Fri, 1 Jul 2022 15:45:39 +0200 Subject: Made default board size 9 for go.py. --- go.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/go.py b/go.py index 7cb8555..cf0a673 100755 --- a/go.py +++ b/go.py @@ -7,8 +7,7 @@ from imago.gameLogic.gameState import GameState if __name__ == "__main__": - GAMESTATE = GameState(5) - #GAMESTATE = GameState(19) + GAMESTATE = GameState(9) while True: -- cgit v1.2.1 From e2da0305174dd37efb1b7f25195e474447128ad7 Mon Sep 17 00:00:00 2001 From: InigoGutierrez Date: Fri, 1 Jul 2022 15:51:21 +0200 Subject: Updated README --- README.md | 19 ++++++++----------- 1 file changed, 8 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index ccd8f6c..3e6a01d 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # Imago -A Go AI. +An AI player of the game of Go. ## The project @@ -10,22 +10,19 @@ Software Engineering of the University of Oviedo. ## Implementation Imago is written in Python and the source code is inside the `imago` folder. The -implementation is on an early stage and includes core game logic, a basic GTP -engine and a placeholder AI function which plays on random empty vertices. +implementation includes core game logic and a GTP engine able to play using +either Monte Carlo Tree Search or a neural network trained on human matches. A game of go with no AI can be played by running the `go.py` script. This is useful to test the core game logic. The GTP engine can be started by the `imagocli.py` script. Following the GTP specification, known commands can be listed by entering `list_commands` on the GTP engine's interface. -Tests are stored in the `tests` folder which as of now contains an example -tests file. The tests can be run with the `test.sh` script which uses the -Python package `coverage` to get coverage statistics. +Tests are stored in the `tests` folder. The tests can be run with the `test.sh` +script which uses the Python package `coverage` to get coverage statistics. ## Documentation -The source code for a work in progress documentation is laid out inside the -`doc` folder, including a Makefile to compile it. This documentation compiling -process depends on `xelatex`, `biber`, `plantuml` and some `LaTeX` packages. -This documentation is fully subject to change in any moment of development and -it probably already contradicts the actual implementation. +The source code for the documentation is laid out inside the `doc` folder, +including a Makefile to compile it. The documentation compiling process depends +on `xelatex`, `biber`, `plantuml` and some `LaTeX` packages. -- cgit v1.2.1 From 5a10940c33a3eabd13438adb2ba9724e63379925 Mon Sep 17 00:00:00 2001 From: InigoGutierrez Date: Fri, 1 Jul 2022 16:04:44 +0200 Subject: Updated abstract, acknowledgements and introduction. --- doc/tex/imago.tex | 15 +++++++++++---- doc/tex/introduction.tex | 6 ++++++ doc/tex/systemAnalysis.tex | 4 ---- 3 files changed, 17 insertions(+), 8 deletions(-) diff --git a/doc/tex/imago.tex b/doc/tex/imago.tex index 8242467..cc77673 100644 --- a/doc/tex/imago.tex +++ b/doc/tex/imago.tex @@ -66,20 +66,27 @@ \clearpage \begin{abstract} - This is the 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. \end{abstract} \clearpage \section*{Acknowledgements} -TODO: Acknowledgements - -To Vicente García Díaz, for directing me in this project. +To Vicente García Díaz, for helping me learning to program on my first year at +the school and directing me in this project on my last. To José Manuel Redondo López\cite{plantillaRedondo}, for making an extensive template on which the structure of this documentation is extensively based. +To all the people who have provided their time, support, ideas and company, all +fundamental for this project. + \clearpage \section*{Copyright notice} diff --git a/doc/tex/introduction.tex b/doc/tex/introduction.tex index 2c710ea..fe5c205 100644 --- a/doc/tex/introduction.tex +++ b/doc/tex/introduction.tex @@ -14,3 +14,9 @@ argument, or even of fortune-telling and prophecy. Go has always been one of the most played games in the world.\cite{sl_go} \end{displayquote} + +As old and deep as Go is it has recently lived a revolution by the appearance of +artificial intelligences with superhuman strength. While not expecting to +achieve what a full team of developers and computer scientists at Google did, +this project aims to evaluate how an engine able to play the game of Go could be +created, implement such an engine and evaluate the results of the whole process. diff --git a/doc/tex/systemAnalysis.tex b/doc/tex/systemAnalysis.tex index a3d66de..c3c6aa8 100644 --- a/doc/tex/systemAnalysis.tex +++ b/doc/tex/systemAnalysis.tex @@ -890,7 +890,3 @@ which can by itself run the unittest tests and generate coverage reports based on the results. % Maybe put an example report here? - -\subsubsection{Integration Testing} - -\subsubsection{System Testing} -- cgit v1.2.1 From 865ce763ce3aaf1dd87187687dfd68e27ea8ba9b Mon Sep 17 00:00:00 2001 From: InigoGutierrez Date: Thu, 25 Aug 2022 20:35:38 +0200 Subject: Capitalized Go and moved 2 sections from planification to introduction. --- doc/tex/imago.tex | 10 ++++++++++ doc/tex/introduction.tex | 29 +++++++++++++++++++++++++++++ doc/tex/planification.tex | 36 +++++------------------------------- doc/tex/results.tex | 6 +++--- doc/tex/systemAnalysis.tex | 2 +- 5 files changed, 48 insertions(+), 35 deletions(-) diff --git a/doc/tex/imago.tex b/doc/tex/imago.tex index cc77673..5f0b781 100644 --- a/doc/tex/imago.tex +++ b/doc/tex/imago.tex @@ -34,6 +34,16 @@ \newcommand{\flist}[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/introduction.tex b/doc/tex/introduction.tex index fe5c205..2f34b7b 100644 --- a/doc/tex/introduction.tex +++ b/doc/tex/introduction.tex @@ -20,3 +20,32 @@ artificial intelligences with superhuman strength. While not expecting to achieve what a full team of developers and computer scientists at Google did, this project aims to evaluate how an engine able to play the game of Go could be created, implement such an engine and evaluate the results of the whole process. + +\subsection{Driving Needs} + +As one of the deepest and most studied games in the world, Go presents a very +interesting problem for artificial intelligence. Implementing not only the +game's simple but subtle rules, but a system capable of playing it with a +satisfying level of skill, is a task worth of pursuing as an exercise on +software design, algorithmics and AI research. + +On the practical level, this project can be a foundation for the development of +different Go analysis algorithms by providing an existing engine to house them, +which can be of interest to Go players and software scientists alike. + +\subsection{Reach} + +Presented here are the ideal targets of the project. + +\begin{itemize} + \item An implementation of the game of Go, that is, a system for holding the + moves and variants of a match (a tree of moves) and the logic for the + game's rules. + \item An engine capable of analyzing board positions and generating strong + moves via various decision algorithms. + \item Either a GUI specifically developed for the project or an + implementation of an existing protocol so the engine can be used with + existing tools and GUIs. + \item A way for processing existing records of games, which are usually + recorded in the SGF format. +\end{itemize} diff --git a/doc/tex/planification.tex b/doc/tex/planification.tex index f0f7535..9112f45 100644 --- a/doc/tex/planification.tex +++ b/doc/tex/planification.tex @@ -1,33 +1,7 @@ -\section{Planning} +\section{Planification} -\subsection{Driving Needs} - -As one of the deepest and most studied games in the world, Go presents a very -interesting problem for artificial intelligence. Implementing not only the -game's simple but subtle rules, but a system capable of playing it with a -satisfying level of skill, is a task worth of pursuing as an exercise on -software design, algorithmics and AI research. - -On the practical level, this project can be a foundation for the development of -different Go analysis algorithms by providing an existing engine to house them, -which can be of interest to Go players and software scientists alike. - -\subsection{Reach} - -Presented here are the ideal targets of the project. - -\begin{itemize} - \item An implementation of the game of Go, that is, a system for holding the - moves and variants of a match (a tree of moves) and the logic for the - game's rules. - \item An engine capable of analyzing board positions and generating strong - moves via various decision algorithms. - \item Either a GUI specifically developed for the project or an - implementation of an existing protocol so the engine can be used with - existing tools and GUIs. - \item A way for processing existing records of games, which are usually - recorded in the SGF format. -\end{itemize} +This section explains the aim of the project, its reach, the existing work it is +based on and an initial planification. \subsection{Project Stages} @@ -127,7 +101,7 @@ engine for which the GTP protocol was first defined. \paragraph{GTP\cite{gtp}} GTP (\textit{Go Text Protocol}) is a text based protocol for -communication with computer go programs. It is the protocol used by GNU Go and +communication with computer Go programs. It is the protocol used by GNU Go and the more modern and powerful KataGo. By supporting GTP the engine developed for this project can be used with existing GUIs and other programs, making it easier to use it with the tools users are already familiar with. @@ -144,7 +118,7 @@ based on neural networks. \subsubsection{Sabaki\cite{sabaki}} -Sabaki is a go board software compatible with GTP engines. It can serve as a GUI +Sabaki is a Go board software compatible with GTP engines. It can serve as a GUI for the engine developed in this project and as an example of the advantages of following a standardized protocol. diff --git a/doc/tex/results.tex b/doc/tex/results.tex index 3c586e4..9c1e52c 100644 --- a/doc/tex/results.tex +++ b/doc/tex/results.tex @@ -36,7 +36,7 @@ The Monte Carlo Algorithm tries to explore the tree of possibilities as efficiently as possible. With this approach, it can be expected to fail when alone on a problem such big as the game of Go. Nonetheless, there are some areas where it can be useful. It will be evaluated by its capabilities while playing -games but also when presented with go problems. +games but also when presented with Go problems. The Monte Carlo algorithm has been set to do 5 explorations with 10 simulations each when it is asked for a move. In the hardware used this makes it think for @@ -77,7 +77,7 @@ the board, for which the player must find some beneficial move. Life and death problems are a subset of tsumegos in which the survival of a group depends on finding the correct sequence to save or kill the group. One collection of such tsumegos is \textit{Cho Chikun's Encyclopedia of Life and Death}, part of which -are available on OGS\cite{ogsLifeAndDeath}, an online go server. +are available on OGS\cite{ogsLifeAndDeath}, an online Go server. The first of these problems and what the algorithm suggested as moves is shown in \fref{fig:mctsProblem01}. @@ -184,7 +184,7 @@ move), can be seen on Figs.~\ref{fig:denseVSdense01}, \ref{fig:denseVSdense02}, The dense network starts on the center of the board, which is one of the standard openings in the 9x9 board. It starts on a very good track, but we must -acknowledge that the empty board is a position present on every go match it has +acknowledge that the empty board is a position present on every Go match it has trained on and so it should know it well. It probably means the center was the most played opening in the sample. It is interesting to check the heatmap of this move, since the selected move has only a score of 0.27. Other common diff --git a/doc/tex/systemAnalysis.tex b/doc/tex/systemAnalysis.tex index c3c6aa8..e7fbc73 100644 --- a/doc/tex/systemAnalysis.tex +++ b/doc/tex/systemAnalysis.tex @@ -783,7 +783,7 @@ GTP protocol and outputs the coordinates of the board to play. main scenario. \\ \midrule \textbf{Notes} & - This scenario does not pretend to be a complete recreation of a go match. It + This scenario does not pretend to be a complete recreation of a Go match. It will be playable, but its main purpose is to see the Game implementation in action.\newline A robustness diagram for this scenario is shown in -- cgit v1.2.1 From 5cd1cf7ec3fa11e7137b1a82eaa64baa877e80b4 Mon Sep 17 00:00:00 2001 From: InigoGutierrez Date: Thu, 25 Aug 2022 20:44:27 +0200 Subject: Changed "planification" (does not exist) to "planning". --- doc/Makefile | 4 +- doc/diagrams/planificationWorkPlanEngine.puml | 46 ------- doc/diagrams/planificationWorkPlanGame.puml | 30 ----- doc/diagrams/planningWorkPlanEngine.puml | 46 +++++++ doc/diagrams/planningWorkPlanGame.puml | 30 +++++ doc/tex/imago.tex | 2 +- doc/tex/planification.tex | 171 -------------------------- doc/tex/planning.tex | 171 ++++++++++++++++++++++++++ 8 files changed, 250 insertions(+), 250 deletions(-) delete mode 100644 doc/diagrams/planificationWorkPlanEngine.puml delete mode 100644 doc/diagrams/planificationWorkPlanGame.puml create mode 100644 doc/diagrams/planningWorkPlanEngine.puml create mode 100644 doc/diagrams/planningWorkPlanGame.puml delete mode 100644 doc/tex/planification.tex create mode 100644 doc/tex/planning.tex diff --git a/doc/Makefile b/doc/Makefile index 284000e..8d37476 100644 --- a/doc/Makefile +++ b/doc/Makefile @@ -3,9 +3,9 @@ docName = imago outputFolder = out -texFiles = tex/$(docName).tex tex/introduction.tex tex/planification.tex tex/systemAnalysis.tex tex/systemDesign.tex tex/implementation.tex tex/results.tex tex/conclusions.tex tex/biber.bib +texFiles = tex/$(docName).tex tex/introduction.tex tex/planning.tex tex/systemAnalysis.tex tex/systemDesign.tex tex/implementation.tex tex/results.tex tex/conclusions.tex tex/biber.bib -diagramImgs = diagrams/planificationWorkPlanEngine.png diagrams/planificationWorkPlanGame.png diagrams/useCases.png diagrams/analysisClasses.png diagrams/useCase_generateAMove.png diagrams/useCase_useAsBackend.png diagrams/useCase_playAMatch.png diagrams/interfaces.png diagrams/gameModule.png diagrams/engineModule.png diagrams/trainingModule.png diagrams/modules.png diagrams/fullClasses.png +diagramImgs = diagrams/planningWorkPlanEngine.png diagrams/planningWorkPlanGame.png diagrams/useCases.png diagrams/analysisClasses.png diagrams/useCase_generateAMove.png diagrams/useCase_useAsBackend.png diagrams/useCase_playAMatch.png diagrams/interfaces.png diagrams/gameModule.png diagrams/engineModule.png diagrams/trainingModule.png diagrams/modules.png diagrams/fullClasses.png imgs = img/imago.jpg img/models/denseModel.png img/models/convModel.png diff --git a/doc/diagrams/planificationWorkPlanEngine.puml b/doc/diagrams/planificationWorkPlanEngine.puml deleted file mode 100644 index 9caad40..0000000 --- a/doc/diagrams/planificationWorkPlanEngine.puml +++ /dev/null @@ -1,46 +0,0 @@ -@startgantt - -!include skinparams.puml -!include skinparamsGantt.puml - -printscale weekly -Saturday are closed -Sunday are closed - -Project starts 2021-01-11 - --- Preliminary research -- -[Previous works research] as [PWR] lasts 1 week -[Algorithms research] as [AR] lasts 2 weeks - --- Engine Implementation -- -[Engine modelling] as [EM] lasts 1 week -[Engine implementation] as [EI] lasts 4 weeks - --- Algorithms Implementations -- -[Monte Carlo implementation] as [MCI] lasts 4 weeks -[Neural networks research] as [NNR] lasts 2 weeks -[Neural networks implementation] as [NNI] lasts 3 weeks - --- Testing -- -[Engine unit testing] as [EUT] lasts 4 weeks -[System testing] as [ST] lasts 1 week - --- Analysis -- -[Algorithms comparison] as [AC] lasts 2 weeks - -[PWR] -> [AR] -[AR] -> [EM] - -[EM] -> [MCI] -[EM] -> [EI] -[EM] -> [EUT] - -[MCI] -> [NNR] -[NNR] -> [NNI] - -[NNI] -> [ST] - -[ST] -> [AC] - -@endgantt diff --git a/doc/diagrams/planificationWorkPlanGame.puml b/doc/diagrams/planificationWorkPlanGame.puml deleted file mode 100644 index ffaf72c..0000000 --- a/doc/diagrams/planificationWorkPlanGame.puml +++ /dev/null @@ -1,30 +0,0 @@ -@startgantt - -!include skinparams.puml -!include skinparamsGantt.puml - -printscale weekly zoom 2 -Saturday are closed -Sunday are closed - -Project starts 2020-11-02 - --- Preliminary research -- -[Previous works research] as [PWR] lasts 1 week - --- Game Implementation -- -[Domain modelling] as [DM] lasts 1 week -[Domain implementation] as [DI] lasts 6 weeks - --- Testing -- -[Unit testing] as [UT] lasts 6 weeks -[System testing] as [ST] lasts 1 week - -[PWR] -> [DM] - -[DM] -> [DI] -[DM] -> [UT] - -[DI] -> [ST] - -@endgantt diff --git a/doc/diagrams/planningWorkPlanEngine.puml b/doc/diagrams/planningWorkPlanEngine.puml new file mode 100644 index 0000000..9caad40 --- /dev/null +++ b/doc/diagrams/planningWorkPlanEngine.puml @@ -0,0 +1,46 @@ +@startgantt + +!include skinparams.puml +!include skinparamsGantt.puml + +printscale weekly +Saturday are closed +Sunday are closed + +Project starts 2021-01-11 + +-- Preliminary research -- +[Previous works research] as [PWR] lasts 1 week +[Algorithms research] as [AR] lasts 2 weeks + +-- Engine Implementation -- +[Engine modelling] as [EM] lasts 1 week +[Engine implementation] as [EI] lasts 4 weeks + +-- Algorithms Implementations -- +[Monte Carlo implementation] as [MCI] lasts 4 weeks +[Neural networks research] as [NNR] lasts 2 weeks +[Neural networks implementation] as [NNI] lasts 3 weeks + +-- Testing -- +[Engine unit testing] as [EUT] lasts 4 weeks +[System testing] as [ST] lasts 1 week + +-- Analysis -- +[Algorithms comparison] as [AC] lasts 2 weeks + +[PWR] -> [AR] +[AR] -> [EM] + +[EM] -> [MCI] +[EM] -> [EI] +[EM] -> [EUT] + +[MCI] -> [NNR] +[NNR] -> [NNI] + +[NNI] -> [ST] + +[ST] -> [AC] + +@endgantt diff --git a/doc/diagrams/planningWorkPlanGame.puml b/doc/diagrams/planningWorkPlanGame.puml new file mode 100644 index 0000000..ffaf72c --- /dev/null +++ b/doc/diagrams/planningWorkPlanGame.puml @@ -0,0 +1,30 @@ +@startgantt + +!include skinparams.puml +!include skinparamsGantt.puml + +printscale weekly zoom 2 +Saturday are closed +Sunday are closed + +Project starts 2020-11-02 + +-- Preliminary research -- +[Previous works research] as [PWR] lasts 1 week + +-- Game Implementation -- +[Domain modelling] as [DM] lasts 1 week +[Domain implementation] as [DI] lasts 6 weeks + +-- Testing -- +[Unit testing] as [UT] lasts 6 weeks +[System testing] as [ST] lasts 1 week + +[PWR] -> [DM] + +[DM] -> [DI] +[DM] -> [UT] + +[DI] -> [ST] + +@endgantt diff --git a/doc/tex/imago.tex b/doc/tex/imago.tex index 5f0b781..c2a5319 100644 --- a/doc/tex/imago.tex +++ b/doc/tex/imago.tex @@ -160,7 +160,7 @@ inclusion to use this template is included here. \input{tex/introduction.tex} \clearpage -\input{tex/planification.tex} +\input{tex/planning.tex} \clearpage \input{tex/systemAnalysis.tex} diff --git a/doc/tex/planification.tex b/doc/tex/planification.tex deleted file mode 100644 index 9112f45..0000000 --- a/doc/tex/planification.tex +++ /dev/null @@ -1,171 +0,0 @@ -\section{Planification} - -This section explains the aim of the project, its reach, the existing work it is -based on and an initial planification. - -\subsection{Project Stages} - -The project will be organized in several stages based on the different -components and needs. - -\subsubsection{Game Implementation} - -The rules of the game must be implemented, ideally in a way they can be tested -by direct human play. This system will at its bare minimum represent the -Japanese Go rules (area scoring, no superko rule, no suicide moves). - -\subsubsection{Engine Implementation} - -The key of this project is to create some kind of system able to generate strong -moves based on any given board configuration: this will be such system. It will -implement an existing protocol so it can be used with other compatible tools. It -has to be able to receive game updates and configuration and to output moves for -either player. It should also be modular enough so different algorithms can be -selected and tested against each other as an experimental search for the best of -them. - -\subsubsection{Artificial Intelligence Algorithms} - -Different algorithms for the engine to use should be implemented and tested. The -results of this development and testing process should be presented as part of -the final version of the project. - -\subsection{Logistics} - -The project will be developed by Íñigo Gutiérrez Fernández, student of the -Computer Software Engineering Degree at the University of Oviedo, with -supervision from Vicente García Díaz, Associate Professor in the Department of -Computer Science at the University of Oviedo. - -The used material consists of a development and testing machine owned by the -student with specifications stated later on the project plan. - -\subsection{Work Plan} - -The sole developer will be the student, who is currently working as a Junior -Software Engineer on a 35 hour per week schedule and with no university -responsibilities other than this project. Taking this into account, a sensible -initial assumption is that he will be able to work 3 hours a day, Monday to -Friday. Gantt diagrams for the planned working schedule are shown as -\fref{fig:planificationWorkPlanGame} and -\fref{fig:planificationWorkPlanEngine}. - -\begin{figure}[h] - \begin{center} - \includegraphics[width=\textwidth]{diagrams/planificationWorkPlanGame.png} - \caption{Initial work plan for implementing the game. - }\label{fig:planificationWorkPlanGame} - \end{center} -\end{figure} - -\begin{figure}[h] - \begin{center} - \includegraphics[width=\textwidth]{diagrams/planificationWorkPlanEngine.png} - \caption{Initial work plan for implementing the engine and algorithms. - }\label{fig:planificationWorkPlanEngine} - \end{center} -\end{figure} - -\subsection{Previous Works} - -\subsubsection{Existing Engines} - -\paragraph{AlphaGo} - -A Go play and analysis engine developed by DeepMind Technologies, a company -owned by Google. It revolutionized the world of Go in 2015 and 2016 when it -respectively became the first AI to win against a professional Go player and -then won against Lee Sedol, a Korean player of the highest professional rank and -one of the strongest players in the world at the time. Its source code is -closed, but a paper\cite{natureAlphaGo2016} written by the team and -published on Nature is available on -https://storage.googleapis.com/deepmind-media/alphago/AlphaGoNaturePaper.pdf. - -The unprecedented success of AlphaGo served as inspiration for many AI projects, -including this one. - -\paragraph{KataGo\cite{katago}} - -An open source project based on the AlphaGo paper that also achieved superhuman -strength of play. The availability of its implementation and documentation -presents a great resource for this project. - -\paragraph{GnuGo\cite{gnugo}} - -A software capable of playing Go part of the GNU project. Although not a strong -engine anymore, it is interesting for historic reasons as the free software -engine for which the GTP protocol was first defined. - -\subsubsection{Existing Standards} - -\paragraph{GTP\cite{gtp}} - -GTP (\textit{Go Text Protocol}) is a text based protocol for -communication with computer Go programs. It is the protocol used by GNU Go and -the more modern and powerful KataGo. By supporting GTP the engine developed for -this project can be used with existing GUIs and other programs, making it easier -to use it with the tools users are already familiar with. - -\paragraph{SGF\cite{sgf}} - -SGF (\textit{Smart Go Format} or, in a more general context, \textit{Smart Game -Format}) is a text format widely used for storing records of Go matches which -allows for variants, comments and other metadata. It was devised for Go but it -supports other games with similar turn-based structure. Many popular playing -tools use it. By supporting SGF vast existing collections of games, such as -those played on online Go servers, can be used to train the decision algorithms -based on neural networks. - -\subsubsection{Sabaki\cite{sabaki}} - -Sabaki is a Go board software compatible with GTP engines. It can serve as a GUI -for the engine developed in this project and as an example of the advantages of -following a standardized protocol. - -\subsubsection{Keras\cite{keras}} - -Keras is a deep learning API for Python allowing for the high-level definition -of neural networks. This permits easily testing and comparing different network -layouts. - -\subsection{Technological Infrastructure} - -\subsubsection{Programming Language}\label{sec:programmingLanguage} - -The resulting product of this project will be one or more pieces of software -able to be run locally on a personal computer. The programming language of -choice is Python\cite{python}, for various reasons: - -\begin{itemize} - - \item It has established a reputation on scientific fields and more - specifically on AI research and development. - \item Interpreters are available for many platforms, which allows the most - people possible to access the product. - \item Although not very deeply, it has been used by the developer student - during its degree including in AI and game theory contexts. - -\end{itemize} - -\subsubsection{Interface} - -Both the game and the engine will offer a text interface. For the game this -allows for quick human testing. For the engine it is mandated by the protocol, -since GTP is a text based protocol for programs using text interfaces. -Independent programs compatible with this interface can be used as a GUI. - -There is also the need of an interface with SGF files so existing games can be -processed by the trainer. - -Both the engine and the trainer will need to interface with the files storing -the neural network models. - -The systems' interfaces are shown in \fref{fig:interfaces}. - -\begin{figure}[h] - \begin{center} - \includegraphics[width=\textwidth]{diagrams/interfaces.png} - \caption{Interfaces of the three components of the project.} - \label{fig:interfaces} - \end{center} -\end{figure} diff --git a/doc/tex/planning.tex b/doc/tex/planning.tex new file mode 100644 index 0000000..7f619f5 --- /dev/null +++ b/doc/tex/planning.tex @@ -0,0 +1,171 @@ +\section{Planning} + +This section explains the aim of the project, its reach, the existing work it is +based on and an initial planning. + +\subsection{Project Stages} + +The project will be organized in several stages based on the different +components and needs. + +\subsubsection{Game Implementation} + +The rules of the game must be implemented, ideally in a way they can be tested +by direct human play. This system will at its bare minimum represent the +Japanese Go rules (area scoring, no superko rule, no suicide moves). + +\subsubsection{Engine Implementation} + +The key of this project is to create some kind of system able to generate strong +moves based on any given board configuration: this will be such system. It will +implement an existing protocol so it can be used with other compatible tools. It +has to be able to receive game updates and configuration and to output moves for +either player. It should also be modular enough so different algorithms can be +selected and tested against each other as an experimental search for the best of +them. + +\subsubsection{Artificial Intelligence Algorithms} + +Different algorithms for the engine to use should be implemented and tested. The +results of this development and testing process should be presented as part of +the final version of the project. + +\subsection{Logistics} + +The project will be developed by Íñigo Gutiérrez Fernández, student of the +Computer Software Engineering Degree at the University of Oviedo, with +supervision from Vicente García Díaz, Associate Professor in the Department of +Computer Science at the University of Oviedo. + +The used material consists of a development and testing machine owned by the +student with specifications stated later on the project plan. + +\subsection{Work Plan} + +The sole developer will be the student, who is currently working as a Junior +Software Engineer on a 35 hour per week schedule and with no university +responsibilities other than this project. Taking this into account, a sensible +initial assumption is that he will be able to work 3 hours a day, Monday to +Friday. Gantt diagrams for the planned working schedule are shown as +\fref{fig:planningWorkPlanGame} and +\fref{fig:planningWorkPlanEngine}. + +\begin{figure}[h] + \begin{center} + \includegraphics[width=\textwidth]{diagrams/planningWorkPlanGame.png} + \caption{Initial work plan for implementing the game. + }\label{fig:planningWorkPlanGame} + \end{center} +\end{figure} + +\begin{figure}[h] + \begin{center} + \includegraphics[width=\textwidth]{diagrams/planningWorkPlanEngine.png} + \caption{Initial work plan for implementing the engine and algorithms. + }\label{fig:planningWorkPlanEngine} + \end{center} +\end{figure} + +\subsection{Previous Works} + +\subsubsection{Existing Engines} + +\paragraph{AlphaGo} + +A Go play and analysis engine developed by DeepMind Technologies, a company +owned by Google. It revolutionized the world of Go in 2015 and 2016 when it +respectively became the first AI to win against a professional Go player and +then won against Lee Sedol, a Korean player of the highest professional rank and +one of the strongest players in the world at the time. Its source code is +closed, but a paper\cite{natureAlphaGo2016} written by the team and +published on Nature is available on +https://storage.googleapis.com/deepmind-media/alphago/AlphaGoNaturePaper.pdf. + +The unprecedented success of AlphaGo served as inspiration for many AI projects, +including this one. + +\paragraph{KataGo\cite{katago}} + +An open source project based on the AlphaGo paper that also achieved superhuman +strength of play. The availability of its implementation and documentation +presents a great resource for this project. + +\paragraph{GnuGo\cite{gnugo}} + +A software capable of playing Go part of the GNU project. Although not a strong +engine anymore, it is interesting for historic reasons as the free software +engine for which the GTP protocol was first defined. + +\subsubsection{Existing Standards} + +\paragraph{GTP\cite{gtp}} + +GTP (\textit{Go Text Protocol}) is a text based protocol for +communication with computer Go programs. It is the protocol used by GNU Go and +the more modern and powerful KataGo. By supporting GTP the engine developed for +this project can be used with existing GUIs and other programs, making it easier +to use it with the tools users are already familiar with. + +\paragraph{SGF\cite{sgf}} + +SGF (\textit{Smart Go Format} or, in a more general context, \textit{Smart Game +Format}) is a text format widely used for storing records of Go matches which +allows for variants, comments and other metadata. It was devised for Go but it +supports other games with similar turn-based structure. Many popular playing +tools use it. By supporting SGF vast existing collections of games, such as +those played on online Go servers, can be used to train the decision algorithms +based on neural networks. + +\subsubsection{Sabaki\cite{sabaki}} + +Sabaki is a Go board software compatible with GTP engines. It can serve as a GUI +for the engine developed in this project and as an example of the advantages of +following a standardized protocol. + +\subsubsection{Keras\cite{keras}} + +Keras is a deep learning API for Python allowing for the high-level definition +of neural networks. This permits easily testing and comparing different network +layouts. + +\subsection{Technological Infrastructure} + +\subsubsection{Programming Language}\label{sec:programmingLanguage} + +The resulting product of this project will be one or more pieces of software +able to be run locally on a personal computer. The programming language of +choice is Python\cite{python}, for various reasons: + +\begin{itemize} + + \item It has established a reputation on scientific fields and more + specifically on AI research and development. + \item Interpreters are available for many platforms, which allows the most + people possible to access the product. + \item Although not very deeply, it has been used by the developer student + during its degree including in AI and game theory contexts. + +\end{itemize} + +\subsubsection{Interface} + +Both the game and the engine will offer a text interface. For the game this +allows for quick human testing. For the engine it is mandated by the protocol, +since GTP is a text based protocol for programs using text interfaces. +Independent programs compatible with this interface can be used as a GUI. + +There is also the need of an interface with SGF files so existing games can be +processed by the trainer. + +Both the engine and the trainer will need to interface with the files storing +the neural network models. + +The systems' interfaces are shown in \fref{fig:interfaces}. + +\begin{figure}[h] + \begin{center} + \includegraphics[width=\textwidth]{diagrams/interfaces.png} + \caption{Interfaces of the three components of the project.} + \label{fig:interfaces} + \end{center} +\end{figure} -- cgit v1.2.1 From 9e0e14a9fceb6de53a69bc2b247ce284717ca25f Mon Sep 17 00:00:00 2001 From: InigoGutierrez Date: Thu, 25 Aug 2022 21:01:41 +0200 Subject: Added space before cites. --- doc/tex/imago.tex | 4 ++-- doc/tex/implementation.tex | 20 ++++++++++---------- doc/tex/introduction.tex | 2 +- doc/tex/planning.tex | 16 ++++++++-------- doc/tex/results.tex | 4 ++-- doc/tex/systemAnalysis.tex | 4 ++-- doc/tex/systemDesign.tex | 2 +- 7 files changed, 26 insertions(+), 26 deletions(-) diff --git a/doc/tex/imago.tex b/doc/tex/imago.tex index c2a5319..d80c19a 100644 --- a/doc/tex/imago.tex +++ b/doc/tex/imago.tex @@ -91,7 +91,7 @@ To Vicente García Díaz, for helping me learning to program on my first year at the school and directing me in this project on my last. -To José Manuel Redondo López\cite{plantillaRedondo}, for making an extensive +To José Manuel Redondo López \cite{plantillaRedondo}, for making an extensive template on which the structure of this documentation is extensively based. To all the people who have provided their time, support, ideas and company, all @@ -126,7 +126,7 @@ inclusion to use this template is included here. \begin{displayquote} Copyright (C) 2019 \textbf{JOSÉ MANUEL REDONDO - LÓPEZ}.\cite{plantillaRedondo} + LÓPEZ}. \cite{plantillaRedondo} \textit{Permission is granted to copy, distribute and/or modify this document under the terms of the GNU Free Documentation License, Version 1.3 diff --git a/doc/tex/implementation.tex b/doc/tex/implementation.tex index d46e805..e449e7c 100644 --- a/doc/tex/implementation.tex +++ b/doc/tex/implementation.tex @@ -16,19 +16,19 @@ specifications are shown as a list for readability. The tools selected for the development of the project and the documentation are listed and explained on this section. All the tools used are either -free\cite{fsf_free} or open source software. The development machine runs 64 +free \cite{fsf_free} or open source software. The development machine runs 64 bits Arch Linux as its operating system. \subsubsection{Language} -The programming language of choice is Python\cite{python}. The rationale behind +The programming language of choice is Python \cite{python}. The rationale behind this decision has been stated on Section \ref{sec:programmingLanguage}. It also allows easy use of the Keras library for implementing neural networks. Various python libraries have been used to easy the development process or assist in the analysis of results. These are: -\paragraph{Keras/Tensorflow\cite{keras}} +\paragraph{Keras/Tensorflow \cite{keras}} Tensorflow is a platform for machine learning which provides a diverse range of tools, one of which is a python library for machine learning. @@ -37,19 +37,19 @@ Keras is a high-level API for Tensorflow allowing for the easy definition of neural networks. It permits easily testing and comparing different network layouts. -\paragraph{NumPy\cite{numpy}} +\paragraph{NumPy \cite{numpy}} A scientific package for python providing a lot of mathematical tools. The most interesting for this project are its capabilities to create and transform matrices. -\paragraph{Matplotlib\cite{matplotlib}} +\paragraph{Matplotlib \cite{matplotlib}} A python library for creating graphs and other visualizations. It is used to show the likelihood of moves the neural networks of the project create from a board configuration. -\paragraph{PLY\cite{ply}} +\paragraph{PLY \cite{ply}} A tool for generating compilers in Python. It is an implementation of the lex and yacc utilities, allowing to create lexers and parsers. It is used in the @@ -72,9 +72,9 @@ These are some utility libraries commonly used for frequent programming tasks: \subsubsection{Development Tools} -\paragraph{Neovim\cite{neovim}} +\paragraph{Neovim \cite{neovim}} -A text editor based on Vim\cite{vim}, providing its same functionality with +A text editor based on Vim \cite{vim}, providing its same functionality with useful additions and defaults for modern computers and terminal emulators. With some extensions and configuration it can become a powerful development environment with a very fluid usage experience. That, and the fact that the @@ -90,7 +90,7 @@ a computer, have made Neovim the editor of choice. \subsubsection{Documentation Tools} -\paragraph{\LaTeX\cite{latex}} +\paragraph{\LaTeX \cite{latex}} A typesetting system widely used in the investigation field, among others. It allows for documentation like this text to be written in plain text and then @@ -98,7 +98,7 @@ compiled to PDF or other formats, which permits keeping the source files of the documentation small and organized plus other benefits of plain text such as being able to use version control. -\paragraph{PlantUML\cite{puml}} +\paragraph{PlantUML \cite{puml}} A program which creates diagrams from plain text files. PlantUML supports syntax for many different sorts of diagrams, mainly but not only UML. It has been used diff --git a/doc/tex/introduction.tex b/doc/tex/introduction.tex index 2f34b7b..099f412 100644 --- a/doc/tex/introduction.tex +++ b/doc/tex/introduction.tex @@ -12,7 +12,7 @@ game at heart, Go has nonetheless been interpreted as a stylized representation of fighting a war, settling a frontier, cornering a market, thrashing out an argument, or even of fortune-telling and prophecy. Go has always been one of the - most played games in the world.\cite{sl_go} + most played games in the world. \cite{sl_go} \end{displayquote} As old and deep as Go is it has recently lived a revolution by the appearance of diff --git a/doc/tex/planning.tex b/doc/tex/planning.tex index 7f619f5..63c958f 100644 --- a/doc/tex/planning.tex +++ b/doc/tex/planning.tex @@ -77,20 +77,20 @@ owned by Google. It revolutionized the world of Go in 2015 and 2016 when it respectively became the first AI to win against a professional Go player and then won against Lee Sedol, a Korean player of the highest professional rank and one of the strongest players in the world at the time. Its source code is -closed, but a paper\cite{natureAlphaGo2016} written by the team and +closed, but a paper \cite{natureAlphaGo2016} written by the team and published on Nature is available on https://storage.googleapis.com/deepmind-media/alphago/AlphaGoNaturePaper.pdf. The unprecedented success of AlphaGo served as inspiration for many AI projects, including this one. -\paragraph{KataGo\cite{katago}} +\paragraph{KataGo \cite{katago}} An open source project based on the AlphaGo paper that also achieved superhuman strength of play. The availability of its implementation and documentation presents a great resource for this project. -\paragraph{GnuGo\cite{gnugo}} +\paragraph{GnuGo \cite{gnugo}} A software capable of playing Go part of the GNU project. Although not a strong engine anymore, it is interesting for historic reasons as the free software @@ -98,7 +98,7 @@ engine for which the GTP protocol was first defined. \subsubsection{Existing Standards} -\paragraph{GTP\cite{gtp}} +\paragraph{GTP \cite{gtp}} GTP (\textit{Go Text Protocol}) is a text based protocol for communication with computer Go programs. It is the protocol used by GNU Go and @@ -106,7 +106,7 @@ the more modern and powerful KataGo. By supporting GTP the engine developed for this project can be used with existing GUIs and other programs, making it easier to use it with the tools users are already familiar with. -\paragraph{SGF\cite{sgf}} +\paragraph{SGF \cite{sgf}} SGF (\textit{Smart Go Format} or, in a more general context, \textit{Smart Game Format}) is a text format widely used for storing records of Go matches which @@ -116,13 +116,13 @@ tools use it. By supporting SGF vast existing collections of games, such as those played on online Go servers, can be used to train the decision algorithms based on neural networks. -\subsubsection{Sabaki\cite{sabaki}} +\subsubsection{Sabaki \cite{sabaki}} Sabaki is a Go board software compatible with GTP engines. It can serve as a GUI for the engine developed in this project and as an example of the advantages of following a standardized protocol. -\subsubsection{Keras\cite{keras}} +\subsubsection{Keras \cite{keras}} Keras is a deep learning API for Python allowing for the high-level definition of neural networks. This permits easily testing and comparing different network @@ -134,7 +134,7 @@ layouts. The resulting product of this project will be one or more pieces of software able to be run locally on a personal computer. The programming language of -choice is Python\cite{python}, for various reasons: +choice is Python \cite{python}, for various reasons: \begin{itemize} diff --git a/doc/tex/results.tex b/doc/tex/results.tex index 9c1e52c..3cd682f 100644 --- a/doc/tex/results.tex +++ b/doc/tex/results.tex @@ -77,7 +77,7 @@ the board, for which the player must find some beneficial move. Life and death problems are a subset of tsumegos in which the survival of a group depends on finding the correct sequence to save or kill the group. One collection of such tsumegos is \textit{Cho Chikun's Encyclopedia of Life and Death}, part of which -are available on OGS\cite{ogsLifeAndDeath}, an online Go server. +are available on OGS \cite{ogsLifeAndDeath}, an online Go server. The first of these problems and what the algorithm suggested as moves is shown in \fref{fig:mctsProblem01}. @@ -228,7 +228,7 @@ Black responds to the white stone by attacking it at B4 and white then extends to the more open side with F7. The match up to this point is discussed on Sensei's Library as an interesting -sequence deriving from the center first stone\cite{sl_sword}. The discussion +sequence deriving from the center first stone \cite{sl_sword}. The discussion there states that white should have extended to the other side. The following moves are some sensible continuations, with approaches and diff --git a/doc/tex/systemAnalysis.tex b/doc/tex/systemAnalysis.tex index e7fbc73..3b6a0a3 100644 --- a/doc/tex/systemAnalysis.tex +++ b/doc/tex/systemAnalysis.tex @@ -881,11 +881,11 @@ GTP protocol and outputs the coordinates of the board to play. \subsubsection{Unitary Testing} -Tests for the python code are developed using the unittest\cite{python_unittest} +Tests for the python code are developed using the unittest \cite{python_unittest} testing framework. It has been chosen by virtue of being thoroughly documented and widely used. -The coverage of unit testing is checked with Coverage.py\cite{python_coverage}, +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. diff --git a/doc/tex/systemDesign.tex b/doc/tex/systemDesign.tex index 80a2ccb..7975276 100644 --- a/doc/tex/systemDesign.tex +++ b/doc/tex/systemDesign.tex @@ -99,7 +99,7 @@ The engine can be started with the executable \texttt{imagocli.py}. Monte Carlo Tree Search is an algorithm that can be useful for exploring decision trees. It was used by AlphaGo in conjunction with neural networks as -explained in the AlphaGo 2016 paper\cite{natureAlphaGo2016}. +explained in the AlphaGo 2016 paper \cite{natureAlphaGo2016}. The algorithm assigns a score to each explored node based on how likely the player who makes the corresponding move is to win and updates this score on each -- cgit v1.2.1 From 61dbd424248485981e55649a5939b0b59273f622 Mon Sep 17 00:00:00 2001 From: InigoGutierrez Date: Thu, 25 Aug 2022 21:05:38 +0200 Subject: Removed explicit and reduntant url reference. --- doc/tex/planning.tex | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/doc/tex/planning.tex b/doc/tex/planning.tex index 63c958f..269645a 100644 --- a/doc/tex/planning.tex +++ b/doc/tex/planning.tex @@ -77,9 +77,8 @@ owned by Google. It revolutionized the world of Go in 2015 and 2016 when it respectively became the first AI to win against a professional Go player and then won against Lee Sedol, a Korean player of the highest professional rank and one of the strongest players in the world at the time. Its source code is -closed, but a paper \cite{natureAlphaGo2016} written by the team and -published on Nature is available on -https://storage.googleapis.com/deepmind-media/alphago/AlphaGoNaturePaper.pdf. +closed, but a paper written by the team has been +published on Nature \cite{natureAlphaGo2016}. The unprecedented success of AlphaGo served as inspiration for many AI projects, including this one. -- cgit v1.2.1 From 9701d4572b27f3c1a02b34c84b70ce5bf71ddeda Mon Sep 17 00:00:00 2001 From: InigoGutierrez Date: Thu, 25 Aug 2022 21:20:16 +0200 Subject: Capitalized Python. --- doc/tex/implementation.tex | 8 ++++---- doc/tex/systemAnalysis.tex | 14 +++++++------- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/doc/tex/implementation.tex b/doc/tex/implementation.tex index e449e7c..24a231c 100644 --- a/doc/tex/implementation.tex +++ b/doc/tex/implementation.tex @@ -25,13 +25,13 @@ The programming language of choice is Python \cite{python}. The rationale behind this decision has been stated on Section \ref{sec:programmingLanguage}. It also allows easy use of the Keras library for implementing neural networks. -Various python libraries have been used to easy the development process or +Various Python libraries have been used to easy the development process or assist in the analysis of results. These are: \paragraph{Keras/Tensorflow \cite{keras}} Tensorflow is a platform for machine learning which provides a diverse range of -tools, one of which is a python library for machine learning. +tools, one of which is a Python library for machine learning. Keras is a high-level API for Tensorflow allowing for the easy definition of neural networks. It permits easily testing and comparing different network @@ -39,13 +39,13 @@ layouts. \paragraph{NumPy \cite{numpy}} -A scientific package for python providing a lot of mathematical tools. The most +A scientific package for Python providing a lot of mathematical tools. The most interesting for this project are its capabilities to create and transform matrices. \paragraph{Matplotlib \cite{matplotlib}} -A python library for creating graphs and other visualizations. It is used to +A Python library for creating graphs and other visualizations. It is used to show the likelihood of moves the neural networks of the project create from a board configuration. diff --git a/doc/tex/systemAnalysis.tex b/doc/tex/systemAnalysis.tex index 3b6a0a3..c84f95f 100644 --- a/doc/tex/systemAnalysis.tex +++ b/doc/tex/systemAnalysis.tex @@ -195,8 +195,8 @@ requisites needed for the system. \begin{enumerate} - \item The game program will be a python file able to be executed by the - python interpreter. + \item The game program will be a Python file able to be executed by the + Python interpreter. \item The game program will make use of standard input and standard output for communication. @@ -206,8 +206,8 @@ requisites needed for the system. \item Standard output will be used for messages directed to the user. \end{enumerate} - \item The engine program will be a python file able to be executed by the - python interpreter. + \item The engine program will be a Python file able to be executed by the + Python interpreter. \item The engine program will make use of standard input and standard output for communication. @@ -217,8 +217,8 @@ requisites needed for the system. commands. \end{enumerate} - \item The trainer program will be a python file able to be executed by the - python interpreter. + \item The trainer program will be a Python file able to be executed by the + Python interpreter. \item The engine program will make use of standard input and standard output for communication. @@ -881,7 +881,7 @@ GTP protocol and outputs the coordinates of the board to play. \subsubsection{Unitary Testing} -Tests for the python code are developed using the unittest \cite{python_unittest} +Tests for the Python code are developed using the unittest \cite{python_unittest} testing framework. It has been chosen by virtue of being thoroughly documented and widely used. -- cgit v1.2.1 From d1a53544e82d54797a8073081538999a16e7b4ca Mon Sep 17 00:00:00 2001 From: InigoGutierrez Date: Thu, 25 Aug 2022 21:32:21 +0200 Subject: Fixed errata. --- doc/tex/conclusions.tex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/tex/conclusions.tex b/doc/tex/conclusions.tex index ebbf7fd..9b6fd00 100644 --- a/doc/tex/conclusions.tex +++ b/doc/tex/conclusions.tex @@ -84,7 +84,7 @@ for evaluation. \subsubsection{Mistakes on Networks' Play Patterns} One notable mistake made by the networks, specially the convolutional network, -was passing to much. Passing is considered just another move, so the networks +was passing too much. Passing is considered just another move, so the networks have no grasp that they should not pass until there are no more valuable moves to be made. A new design problem could be to create a specific passing policy. -- cgit v1.2.1 From 95a42188701405ce15b77ae80832c670e307fbb9 Mon Sep 17 00:00:00 2001 From: InigoGutierrez Date: Tue, 13 Sep 2022 16:16:08 +0200 Subject: Configuring nvim LSP. --- imago/engine/core.py | 3 ++- imago/engine/imagoIO.py | 14 ++++++++++---- 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/imago/engine/core.py b/imago/engine/core.py index 2bf7d5a..47c8f16 100644 --- a/imago/engine/core.py +++ b/imago/engine/core.py @@ -8,10 +8,11 @@ DEF_SIZE = 9 DEF_KOMI = 5.5 DEF_ALGORITHM = DecisionAlgorithms.KERAS + class GameEngine: """Plays the game of Go.""" - def __init__(self, decisionAlgorithmId = DEF_ALGORITHM): + def __init__(self, decisionAlgorithmId=DEF_ALGORITHM): self.komi = DEF_KOMI self.gameState = GameState(DEF_SIZE) self.daId = decisionAlgorithmId diff --git a/imago/engine/imagoIO.py b/imago/engine/imagoIO.py index 9b3e367..9a89095 100644 --- a/imago/engine/imagoIO.py +++ b/imago/engine/imagoIO.py @@ -5,36 +5,43 @@ import sys from imago.engine import parseHelpers from imago.engine.core import GameEngine + def _response(text=""): print("= %s" % text) print() + def _responseError(text=""): print("? %s" % text) print() + def protocol_version(_): """Version of the GTP Protocol""" _response("2") + def name(_): """Name of the engine""" _response("Imago") + def version(_): """Version of the engine""" _response("0.0.0") + def getCoordsText(row, col): """Returns a string representation of row and col. In GTP A1 is bottom left corner. """ - return "%s%d" % (chr(65+row), col+1) + return "%s%d" % (chr(65 + row), col + 1) + class ImagoIO: """Recieves and handles commands.""" - def __init__(self, decisionAlgorithmId = None): + def __init__(self, decisionAlgorithmId=None): self.commands_set = { protocol_version, name, @@ -72,7 +79,6 @@ class ImagoIO: if command is not None: arguments = input_tokens[1:] - #print("[DEBUG]:Selected command: %s; args: %s" % (command, arguments)) command(arguments) else: _responseError("unknown command") @@ -169,7 +175,7 @@ class ImagoIO: return color = parseHelpers.parseColor(args[0]) output = parseHelpers.vertexToString(self.gameEngine.genmove(color), - self.gameEngine.gameState.size) + self.gameEngine.gameState.size) _response(output) self.gameEngine.gameState.getBoard().printBoard() -- cgit v1.2.1 From e74f404f9cb5f0d5752178ab8baf4055c0a10f84 Mon Sep 17 00:00:00 2001 From: InigoGutierrez Date: Wed, 26 Oct 2022 20:43:34 +0200 Subject: Adding examples of protocols. --- .gitignore | 2 + doc/tex/biber.bib | 146 ++++++++++++++++++++++++--------------------- doc/tex/imago.tex | 8 +-- doc/tex/implementation.tex | 2 +- doc/tex/planning.tex | 78 +++++++++++++++++++++--- doc/tex/results.tex | 4 +- doc/tex/systemDesign.tex | 4 +- 7 files changed, 157 insertions(+), 87 deletions(-) diff --git a/.gitignore b/.gitignore index 0a4eeda..1fd18b7 100644 --- a/.gitignore +++ b/.gitignore @@ -1,8 +1,10 @@ *.sgf # images +img/ *.png *.jpg +*.svg *.xcf # doc diff --git a/doc/tex/biber.bib b/doc/tex/biber.bib index d0a714e..324c0ad 100644 --- a/doc/tex/biber.bib +++ b/doc/tex/biber.bib @@ -7,27 +7,35 @@ } @online{gtp, - author = {Gunnar Farnebäck}, - title = {GTP --- Go Text Protocol}, - date = {2002}, - urldate = {2021}, - url = {https://www.lysator.liu.se/~gunnar/gtp} + author = {Gunnar Farnebäck}, + title = {GTP --- Go Text Protocol}, + date = {2002}, + urldate = {2021}, + url = {https://www.lysator.liu.se/~gunnar/gtp} } @online{sgf, - author = {Arno Hollosi}, - title = {SGF File Format FF[4]}, - date = {2021}, - urldate = {2022}, - url = {https://www.red-bean.com/sgf} + author = {Arno Hollosi}, + title = {SGF File Format FF[4]}, + date = {2021}, + urldate = {2022}, + url = {https://www.red-bean.com/sgf} +} + +@online{alphaGo, + author = {DeepMind}, + title = {AlphaGo}, + date = {2022}, + urldate = {2022}, + url = {https://www.deepmind.com/research/highlighted-research/alphago} } @online{katago, - author = {David J Wu ("lightvector")}, - title = {KataGo}, - date = {2021}, - urldate = {2021}, - url = {https://github.com/lightvector/KataGo} + author = {David J Wu ("lightvector")}, + title = {KataGo}, + date = {2021}, + urldate = {2021}, + url = {https://github.com/lightvector/KataGo} } @online{gnugo, @@ -39,17 +47,17 @@ } @online{sabaki, - author = {Yichuan Shen}, - title = {Sabaki --- An elegant Go board and SGF editor for a more civilized age.}, - date = {2019}, - urldate = {2021}, - url = {https://sabaki.yichuanshen.de} + author = {Yichuan Shen}, + title = {Sabaki --- An elegant Go board and SGF editor for a more civilized age.}, + date = {2019}, + urldate = {2021}, + url = {https://sabaki.yichuanshen.de} } @online{keras, - title = {Keras: the Python deep learning API}, - urldate = {2022}, - url = {https://keras.io} + title = {Keras: the Python deep learning API}, + urldate = {2022}, + url = {https://keras.io} } @online{sl_go, @@ -68,89 +76,89 @@ } @online{python_unittest, - title = {unittest --- Unit testing framework}, - urldate = {2021}, - url = {https://docs.python.org/3/library/unittest.html} + title = {unittest --- Unit testing framework}, + urldate = {2021}, + url = {https://docs.python.org/3/library/unittest.html} } @online{python_coverage, - title = {Coverage.py}, - urldate = {2021}, - url = {https://coverage.readthedocs.io} + title = {Coverage.py}, + urldate = {2021}, + url = {https://coverage.readthedocs.io} } @online{matplotlib_heatmaps, - title = {Creating annotated heatmaps — Matplotlib 3.5.2 documentation}, - urldate = {2022}, - url = {https://matplotlib.org/stable/gallery/images_contours_and_fields/image_annotated_heatmap.html} + title = {Creating annotated heatmaps — Matplotlib 3.5.2 documentation}, + urldate = {2022}, + url = {https://matplotlib.org/stable/gallery/images_contours_and_fields/image_annotated_heatmap.html} } @online{python, - title = {Welcome to Python.org}, - urldate = {2022}, - url = {https://www.python.org} + title = {Welcome to Python.org}, + urldate = {2022}, + url = {https://www.python.org} } @online{numpy, - title = {NumPy}, - urldate = {2022}, - url = {https://numpy.org} + title = {NumPy}, + urldate = {2022}, + url = {https://numpy.org} } @online{matplotlib, - title = {Matplotlib — Visualization with Python}, - urldate = {2022}, - url = {https://matplotlib.org} + title = {Matplotlib — Visualization with Python}, + urldate = {2022}, + url = {https://matplotlib.org} } @online{ply, - title = {PLY (Python Lex-Yacc)}, - urldate = {2022}, - url = {http://www.dabeaz.com/ply} + title = {PLY (Python Lex-Yacc)}, + urldate = {2022}, + url = {http://www.dabeaz.com/ply} } @online{vim, - title = {welcome home : vim online}, - urldate = {2022}, - url = {https://www.vim.org} + title = {welcome home : vim online}, + urldate = {2022}, + url = {https://www.vim.org} } @online{neovim, - title = {Home - Neovim}, - urldate = {2022}, - url = {http://neovim.io} + title = {Home - Neovim}, + urldate = {2022}, + url = {http://neovim.io} } @online{latex, - title = {LaTeX - A document preparation system}, - urldate = {2022}, - url = {https://www.latex-project.org} + title = {LaTeX - A document preparation system}, + urldate = {2022}, + url = {https://www.latex-project.org} } @online{puml, - title = {Open-source tool that uses simple textual descriptions to draw beautiful UML diagrams.}, - urldate = {2022}, - url = {https://plantuml.com} + title = {Open-source tool that uses simple textual descriptions to draw beautiful UML diagrams.}, + urldate = {2022}, + url = {https://plantuml.com} } @online{sl_sword, - title = {9x9 Tengen Openings at Sensei's Library}, - urldate = {2022}, - url = {https://senseis.xmp.net/?9x9TengenOpenings} + title = {9x9 Tengen Openings at Sensei's Library}, + urldate = {2022}, + url = {https://senseis.xmp.net/?9x9TengenOpenings} } @online{ogsLifeAndDeath, - author = {sunspark}, - title = {Cho Chikun's Encyclopedia of Life and Death - Elementary: 1 / 900}, - date = {2016}, - urldate = {2022}, - url = {https://online-go.com/puzzle/2824} + author = {sunspark}, + title = {Cho Chikun's Encyclopedia of Life and Death - Elementary: 1 / 900}, + date = {2016}, + urldate = {2022}, + url = {https://online-go.com/puzzle/2824} } @online{fsf_free, - author = {Free Software Foundation}, - title = {What is Free Software? - GNU Project - Free Software Foundation}, - date = {2022}, - urldate = {2022}, - url = {https://www.gnu.org/philosophy/free-sw.html} + author = {Free Software Foundation}, + title = {What is Free Software? - GNU Project - Free Software Foundation}, + date = {2022}, + urldate = {2022}, + url = {https://www.gnu.org/philosophy/free-sw.html} } diff --git a/doc/tex/imago.tex b/doc/tex/imago.tex index d80c19a..fbf0291 100644 --- a/doc/tex/imago.tex +++ b/doc/tex/imago.tex @@ -31,12 +31,12 @@ \newcommand{\program}{Imago} \newcommand{\fref}[1]{Fig.~\ref{#1}} -\newcommand{\flist}[1]{Listing~\ref{#1}} +\newcommand{\lref}[1]{Listing~\ref{#1}} %\newcommand{\uurl}[1]{\underline{\url{#1}}} \newcommand{\acronim}[2] { - \iftoggle{#1} + \iftoggle{#1} {#1} {#1 (#2)\toggletrue{#1}} } @@ -172,8 +172,8 @@ inclusion to use this template is included here. \input{tex/implementation.tex} \clearpage -\input{tex/results.tex} -\clearpage +%\input{tex/results.tex} +%\clearpage \input{tex/conclusions.tex} \clearpage diff --git a/doc/tex/implementation.tex b/doc/tex/implementation.tex index 24a231c..ffb2802 100644 --- a/doc/tex/implementation.tex +++ b/doc/tex/implementation.tex @@ -113,7 +113,7 @@ updates them if they already exist but their source files are newer than them. It has been used to generate this text from \LaTeX{} and PlantUML source files. The contents of the Makefile with which this document has been compiled are -shown in \flist{code:makefile}. +shown in \lref{code:makefile}. \begin{listing}[h] \inputminted{make}{Makefile} diff --git a/doc/tex/planning.tex b/doc/tex/planning.tex index 269645a..1450e8a 100644 --- a/doc/tex/planning.tex +++ b/doc/tex/planning.tex @@ -48,7 +48,9 @@ responsibilities other than this project. Taking this into account, a sensible initial assumption is that he will be able to work 3 hours a day, Monday to Friday. Gantt diagrams for the planned working schedule are shown as \fref{fig:planningWorkPlanGame} and -\fref{fig:planningWorkPlanEngine}. +\fref{fig:planningWorkPlanEngine}. This planning predicts 6 months of +development, from November 2020 to April 2021. With the planned schedule of 3 +hours a day on weekdays this amounts to 375 hours. \begin{figure}[h] \begin{center} @@ -70,30 +72,61 @@ Friday. Gantt diagrams for the planned working schedule are shown as \subsubsection{Existing Engines} -\paragraph{AlphaGo} +\begin{figure}[h] + \begin{center} + \includegraphics[width=0.5\textwidth]{img/Alphago_logo_Reversed.jpg} + \caption{AlphaGo logo. By Google DeepMind - Google DeepMind AlphaGo + Logo, Public Domain, + https://commons.wikimedia.org/w/index.php?curid=47169369 + }\label{fig:alphaGoLogo} + \end{center} +\end{figure} + +\paragraph{AlphaGo \cite{alphaGo}} A Go play and analysis engine developed by DeepMind Technologies, a company owned by Google. It revolutionized the world of Go in 2015 and 2016 when it respectively became the first AI to win against a professional Go player and then won against Lee Sedol, a Korean player of the highest professional rank and one of the strongest players in the world at the time. Its source code is -closed, but a paper written by the team has been -published on Nature \cite{natureAlphaGo2016}. +closed, but a paper written by the team has been published on Nature +\cite{natureAlphaGo2016}. The logo of the project is shown on +\fref{fig:alphaGoLogo}. The unprecedented success of AlphaGo served as inspiration for many AI projects, including this one. +\begin{figure}[h] + \begin{center} + \includegraphics[width=0.5\textwidth]{img/katago.png} + \caption{KataGo logo. + https://katagotraining.org/static/images/katago.png + }\label{fig:kataGoLogo} + \end{center} +\end{figure} + \paragraph{KataGo \cite{katago}} An open source project based on the AlphaGo paper that also achieved superhuman strength of play. The availability of its implementation and documentation -presents a great resource for this project. +presents a great resource for this project. The logo of the project is shown on +\fref{fig:kataGoLogo}. + +\begin{figure}[h] + \begin{center} + \includegraphics[width=0.5\textwidth]{img/gnuGoLogo.jpg} + \caption{GnuGo logo. + https://www.gnu.org/software/gnugo/logo-36.jpg + }\label{fig:gnuGoLogo} + \end{center} +\end{figure} \paragraph{GnuGo \cite{gnugo}} A software capable of playing Go part of the GNU project. Although not a strong engine anymore, it is interesting for historic reasons as the free software -engine for which the GTP protocol was first defined. +engine for which the GTP protocol was first defined. The logo of the project is shown on +\fref{fig:gnuGoLogo}. \subsubsection{Existing Standards} @@ -113,19 +146,46 @@ allows for variants, comments and other metadata. It was devised for Go but it supports other games with similar turn-based structure. Many popular playing tools use it. By supporting SGF vast existing collections of games, such as those played on online Go servers, can be used to train the decision algorithms -based on neural networks. +based on neural networks. An example of a SGF file can be seen on +\lref{lst:sgfExample}. + +\begin{listing}[h] + \inputminted[breakafter=\]]{text}{listings/sgfExample.sgf} + \caption{SGF example. Describes a tsumego (Go problem) setup and two + variants, one commented as "Correct" and other commented as "Incorrect".} + \label{lst:sgfExample} +\end{listing} + +\begin{figure}[h] + \begin{center} + \includegraphics[width=0.5\textwidth]{img/sabaki.jpg} + \caption{Sabaki screenshot. + https://sabaki.yichuanshen.de/img/screenshot.png + }\label{fig:sabaki} + \end{center} +\end{figure} \subsubsection{Sabaki \cite{sabaki}} Sabaki is a Go board software compatible with GTP engines. It can serve as a GUI for the engine developed in this project and as an example of the advantages of -following a standardized protocol. +following a standardized protocol. Part of its graphical interface is shown on +\fref{fig:sabaki}. + +\begin{figure}[h] + \begin{center} + \includegraphics[width=0.5\textwidth]{img/kerasLogo.jpg} + \caption{Keras logo. + https://keras.io/img/logo.png + }\label{fig:kerasLogo} + \end{center} +\end{figure} \subsubsection{Keras \cite{keras}} Keras is a deep learning API for Python allowing for the high-level definition of neural networks. This permits easily testing and comparing different network -layouts. +layouts. The logo of the project is shown on \fref{fig:kerasLogo}. \subsection{Technological Infrastructure} diff --git a/doc/tex/results.tex b/doc/tex/results.tex index 3cd682f..e37c0b2 100644 --- a/doc/tex/results.tex +++ b/doc/tex/results.tex @@ -134,8 +134,8 @@ as with batches of 10 games an epoch of training could be completed in one minute, in three minutes batches of 20 games, and in up to an hour with batches of 100 games. -The outputs from this training process can be seen on \flist{code:denseTraining} -and \flist{code:convTraining} for the dense network and the convolutional +The outputs from this training process can be seen on \lref{code:denseTraining} +and \lref{code:convTraining} for the dense network and the convolutional network respectively. \begin{listing}[h] diff --git a/doc/tex/systemDesign.tex b/doc/tex/systemDesign.tex index 7975276..f54032f 100644 --- a/doc/tex/systemDesign.tex +++ b/doc/tex/systemDesign.tex @@ -224,7 +224,7 @@ been selected so each node can have as input each of the vertices of the board. A flatten layer acts then to make the output one-dimensional, and a final dense layer provides the vector containing the likelihood of each possible move. -The design of this network is shown in \flist{code:denseModel} and +The design of this network is shown in \lref{code:denseModel} and \fref{fig:denseNN} as provided by Keras' summary and plot\_model functions respectively. @@ -249,7 +249,7 @@ of being trained to recognize patterns on the board. A flatten layer acts then to make the output one-dimensional, and a final dense layer provides the vector containing the likelihood of each possible move. -The design of this network is shown in \flist{code:convModel} and +The design of this network is shown in \lref{code:convModel} and \fref{fig:convNN} as provided by Keras' summary and plot\_model functions respectively. -- cgit v1.2.1 From 3b78e9f3751dfc7c77a7d137d0dbab04afcb23f5 Mon Sep 17 00:00:00 2001 From: InigoGutierrez Date: Fri, 28 Oct 2022 19:09:24 +0200 Subject: Adding glossary. --- doc/Makefile | 1 + doc/tex/conclusions.tex | 4 +-- doc/tex/glossary.tex | 36 +++++++++++++++++++++++++++ doc/tex/imago.tex | 25 ++++++++++++------- doc/tex/introduction.tex | 4 +-- doc/tex/planning.tex | 61 ++++++++++++++++++++++++++-------------------- doc/tex/systemAnalysis.tex | 2 +- doc/tex/systemDesign.tex | 2 +- 8 files changed, 92 insertions(+), 43 deletions(-) create mode 100644 doc/tex/glossary.tex diff --git a/doc/Makefile b/doc/Makefile index 8d37476..c9b473e 100644 --- a/doc/Makefile +++ b/doc/Makefile @@ -17,6 +17,7 @@ $(docName).pdf: $(texFiles) $(diagramImgs) $(imgs) $(listings) Makefile [ -d $(outputFolder) ] || mkdir $(outputFolder) xelatex -shell-escape -output-directory $(outputFolder) tex/$(docName).tex biber $(outputFolder)/$(docName) + makeglossaries -d $(outputFolder) $(docName) xelatex -shell-escape -output-directory $(outputFolder) tex/$(docName).tex mv $(outputFolder)/$(docName).pdf . diff --git a/doc/tex/conclusions.tex b/doc/tex/conclusions.tex index 9b6fd00..16118e0 100644 --- a/doc/tex/conclusions.tex +++ b/doc/tex/conclusions.tex @@ -6,14 +6,14 @@ could be done in the future to improve the system are discussed here. \subsection{Problems with the Implementation of the Game} While Go has a simple set of rules, they lead to many borderline cases which -must be specifically addressed. For example, the ko rule obliges to check the +must be specifically addressed. For example, the \gls{ko} rule obliges to check the previous board positions after each move so no boards are repeated. These problems have been solved by designing the tree of moves of a match so each move stores the full board layout after it has been played. This of course increases the memory needed to run the application, but has been chosen over the alternative of having to recreate the board parsing the tree backwards for each -move to check if ko is occurring, if a move makes a capture and which stones +move to check if \gls{ko} is occurring, if a move makes a capture and which stones have already been captured, etc. It is the old problem of memory versus processing, which in this case has been answered with memory. Which one would be the best solution would require a deep analysis out of the scope of the project. diff --git a/doc/tex/glossary.tex b/doc/tex/glossary.tex new file mode 100644 index 0000000..8b43e94 --- /dev/null +++ b/doc/tex/glossary.tex @@ -0,0 +1,36 @@ +\newglossaryentry{ko} +{ + name=ko, + description={A rule which prevents plays that would repeat the previous + board position} +} + +\newglossaryentry{superko} +{ + name=superko, + description={An extension to the ko rule to prevent any previous board + position and not just the last one} +} + +\newglossaryentry{komi} +{ + name=komi, + description={Points given to the white player to compensate for playing + second} +} + +\newglossaryentry{suicide} +{ + name=suicide move, + description={A typically prohibited move which would cause the player's + group to have zero liberties} +} + +\newglossaryentry{tsumego} +{ + name=tsumego, + description={A Go problem} +} + +\newacronym{sgf}{SGF}{Smart Game Format} +\newacronym{gtp}{GTP}{Go Text Protocol} diff --git a/doc/tex/imago.tex b/doc/tex/imago.tex index fbf0291..4b292f1 100644 --- a/doc/tex/imago.tex +++ b/doc/tex/imago.tex @@ -28,21 +28,25 @@ \usepackage{minted} % Code importing and formatting \setminted{linenos, breaklines} +\usepackage[acronym, toc]{glossaries} +\makeglossaries +\input{tex/glossary.tex} + \newcommand{\program}{Imago} \newcommand{\fref}[1]{Fig.~\ref{#1}} \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{\acronim}[2] +%{ +% \iftoggle{#1} +% {#1} +% {#1 (#2)\toggletrue{#1}} +%} +% +%\newtoggle{SGF} +%\newcommand{\acrSGF}[0]{\acronim{SGF}{Smart Game Format}} \newcommand{\tabitem}{~~\llap{\textbullet}~~} @@ -178,6 +182,9 @@ inclusion to use this template is included here. \input{tex/conclusions.tex} \clearpage +\printglossaries +\clearpage + % References (bibliography) \setcounter{secnumdepth}{0} diff --git a/doc/tex/introduction.tex b/doc/tex/introduction.tex index 099f412..5528bd5 100644 --- a/doc/tex/introduction.tex +++ b/doc/tex/introduction.tex @@ -43,9 +43,7 @@ Presented here are the ideal targets of the project. game's rules. \item An engine capable of analyzing board positions and generating strong moves via various decision algorithms. - \item Either a GUI specifically developed for the project or an - implementation of an existing protocol so the engine can be used with - existing tools and GUIs. + \item Compatibility with existing GUIs. \item A way for processing existing records of games, which are usually recorded in the SGF format. \end{itemize} diff --git a/doc/tex/planning.tex b/doc/tex/planning.tex index 1450e8a..2caace3 100644 --- a/doc/tex/planning.tex +++ b/doc/tex/planning.tex @@ -12,7 +12,7 @@ components and needs. The rules of the game must be implemented, ideally in a way they can be tested by direct human play. This system will at its bare minimum represent the -Japanese Go rules (area scoring, no superko rule, no suicide moves). +Japanese Go rules (area scoring, no \gls{superko} rule, no \gls{suicide} moves). \subsubsection{Engine Implementation} @@ -125,29 +125,35 @@ presents a great resource for this project. The logo of the project is shown on A software capable of playing Go part of the GNU project. Although not a strong engine anymore, it is interesting for historic reasons as the free software -engine for which the GTP protocol was first defined. The logo of the project is shown on -\fref{fig:gnuGoLogo}. +engine for which the \acrshort{gtp} protocol was first defined. The logo of the +project is shown on \fref{fig:gnuGoLogo}. \subsubsection{Existing Standards} -\paragraph{GTP \cite{gtp}} +\paragraph{\acrshort{gtp} \cite{gtp}} -GTP (\textit{Go Text Protocol}) is a text based protocol for +\acrshort{gtp} (\textit{\acrlong{gtp}}) is a text based protocol for communication with computer Go programs. It is the protocol used by GNU Go and -the more modern and powerful KataGo. By supporting GTP the engine developed for -this project can be used with existing GUIs and other programs, making it easier -to use it with the tools users are already familiar with. - -\paragraph{SGF \cite{sgf}} - -SGF (\textit{Smart Go Format} or, in a more general context, \textit{Smart Game -Format}) is a text format widely used for storing records of Go matches which -allows for variants, comments and other metadata. It was devised for Go but it -supports other games with similar turn-based structure. Many popular playing -tools use it. By supporting SGF vast existing collections of games, such as -those played on online Go servers, can be used to train the decision algorithms -based on neural networks. An example of a SGF file can be seen on -\lref{lst:sgfExample}. +the more modern and powerful KataGo. By supporting \acrshort{gtp} the engine +developed for this project can be used with existing GUIs and other programs, +making it easier to use it with the tools users are already familiar with. + +%TODO +%\begin{listing}[h] +% \inputminted{text}{listings/gtpExample.sgf} +% \caption{GTP session example.} +% \label{lst:gtpExample} +%\end{listing} + +\paragraph{\acrshort{sgf} \cite{sgf}} + +\acrshort{sgf} (\textit{\acrlong{sgf}}) is a text format widely used for storing +records of Go matches which allows for variants, comments and other metadata. It +was devised for Go but it supports other games with similar turn-based +structure. Many popular playing tools use it. By supporting \acrshort{sgf} vast +existing collections of games, such as those played on online Go servers, can be +used to train the decision algorithms based on neural networks. An example of a +\acrshort{sgf} file can be seen on \lref{lst:sgfExample}. \begin{listing}[h] \inputminted[breakafter=\]]{text}{listings/sgfExample.sgf} @@ -167,10 +173,10 @@ based on neural networks. An example of a SGF file can be seen on \subsubsection{Sabaki \cite{sabaki}} -Sabaki is a Go board software compatible with GTP engines. It can serve as a GUI -for the engine developed in this project and as an example of the advantages of -following a standardized protocol. Part of its graphical interface is shown on -\fref{fig:sabaki}. +Sabaki is a Go board software compatible with \acrshort{gtp} engines. It can +serve as a GUI for the engine developed in this project and as an example of the +advantages of following a standardized protocol. Part of its graphical interface +is shown on \fref{fig:sabaki}. \begin{figure}[h] \begin{center} @@ -210,11 +216,12 @@ choice is Python \cite{python}, for various reasons: Both the game and the engine will offer a text interface. For the game this allows for quick human testing. For the engine it is mandated by the protocol, -since GTP is a text based protocol for programs using text interfaces. -Independent programs compatible with this interface can be used as a GUI. +since \acrshort{gtp} is a text based protocol for programs using text +interfaces. Independent programs compatible with this interface can be used as a +GUI. -There is also the need of an interface with SGF files so existing games can be -processed by the trainer. +There is also the need of an interface with \acrshort{sgf} files so existing +games can be processed by the trainer. Both the engine and the trainer will need to interface with the files storing the neural network models. diff --git a/doc/tex/systemAnalysis.tex b/doc/tex/systemAnalysis.tex index c84f95f..3668f4e 100644 --- a/doc/tex/systemAnalysis.tex +++ b/doc/tex/systemAnalysis.tex @@ -92,7 +92,7 @@ requisites needed for the system. \item There exist commands to set up the conditions of the match. \begin{enumerate} \item The size of the board can be set. - \item The komi can be set. + \item The \gls{komi} can be set. \end{enumerate} \item There exist commands to manipulate the internal representation of the match. diff --git a/doc/tex/systemDesign.tex b/doc/tex/systemDesign.tex index f54032f..3e82698 100644 --- a/doc/tex/systemDesign.tex +++ b/doc/tex/systemDesign.tex @@ -279,7 +279,7 @@ of the text input, each node with zero or more properties, and with the ability to convert themselves and their corresponding subtree into a GameMove tree. This is done for the root node, since from the SGF specification there are some properties only usable in the root node, like those which specify general game -information and properties such as rank of players or komi. +information and properties such as rank of players or \gls{komi}. Here follows an explanation of the role and motivation before each component of the Training module to show how these previous concerns have been addressed and -- cgit v1.2.1 From e8b9bf589e698b51e55ae59693b5bb0293f86a26 Mon Sep 17 00:00:00 2001 From: InigoGutierrez Date: Tue, 1 Nov 2022 18:06:40 +0100 Subject: Added SGF and GTP instances as acronyms. --- doc/tex/implementation.tex | 2 +- doc/tex/introduction.tex | 2 +- doc/tex/planning.tex | 4 ++-- doc/tex/results.tex | 4 ++-- doc/tex/systemAnalysis.tex | 29 ++++++++++++++--------------- doc/tex/systemDesign.tex | 28 ++++++++++++++-------------- 6 files changed, 34 insertions(+), 35 deletions(-) diff --git a/doc/tex/implementation.tex b/doc/tex/implementation.tex index ffb2802..f89e54c 100644 --- a/doc/tex/implementation.tex +++ b/doc/tex/implementation.tex @@ -53,7 +53,7 @@ board configuration. A tool for generating compilers in Python. It is an implementation of the lex and yacc utilities, allowing to create lexers and parsers. It is used in the -project to create the SGF parser which transform SGF files to internal +project to create the \acrshort{sgf} parser which transform \acrshort{sgf} files to internal representations of Go matches. \paragraph{Other utility libraries} diff --git a/doc/tex/introduction.tex b/doc/tex/introduction.tex index 5528bd5..8b5430c 100644 --- a/doc/tex/introduction.tex +++ b/doc/tex/introduction.tex @@ -45,5 +45,5 @@ Presented here are the ideal targets of the project. moves via various decision algorithms. \item Compatibility with existing GUIs. \item A way for processing existing records of games, which are usually - recorded in the SGF format. + recorded in the \acrshort{sgf} format. \end{itemize} diff --git a/doc/tex/planning.tex b/doc/tex/planning.tex index 2caace3..0e8749f 100644 --- a/doc/tex/planning.tex +++ b/doc/tex/planning.tex @@ -141,7 +141,7 @@ making it easier to use it with the tools users are already familiar with. %TODO %\begin{listing}[h] % \inputminted{text}{listings/gtpExample.sgf} -% \caption{GTP session example.} +% \caption{\acrshort{gtp} session example.} % \label{lst:gtpExample} %\end{listing} @@ -157,7 +157,7 @@ used to train the decision algorithms based on neural networks. An example of a \begin{listing}[h] \inputminted[breakafter=\]]{text}{listings/sgfExample.sgf} - \caption{SGF example. Describes a tsumego (Go problem) setup and two + \caption{\acrshort{sgf} example. Describes a tsumego (Go problem) setup and two variants, one commented as "Correct" and other commented as "Incorrect".} \label{lst:sgfExample} \end{listing} diff --git a/doc/tex/results.tex b/doc/tex/results.tex index e37c0b2..5433041 100644 --- a/doc/tex/results.tex +++ b/doc/tex/results.tex @@ -102,14 +102,14 @@ so no more are shown here. \subsection{Neural Network Training} -Each network has been training by receiving batches of SGF files which are first +Each network has been training by receiving batches of \acrshort{sgf} files which are first converted to lists of moves by the Training System and then provided to the train function of the networks. This has been executed with the following command: \inputminted[fontsize=\small]{bash}{listings/trainCommand.sh} -Which lists the contents of a folder containing multiple SGF files, shuffles the +Which lists the contents of a folder containing multiple \acrshort{sgf} files, shuffles the list, takes some of them and passes them to train.py as arguments. The combined list of game positions and moves made from them in all the games selected make up the sample of the training. The number of files selected can of course be diff --git a/doc/tex/systemAnalysis.tex b/doc/tex/systemAnalysis.tex index 3668f4e..bbae66e 100644 --- a/doc/tex/systemAnalysis.tex +++ b/doc/tex/systemAnalysis.tex @@ -16,10 +16,10 @@ These are the main goals the final product must reach. code. \item An engine program as a way of presenting an interface for using these - algorithms. The engine will use the GTP so it can be used with an + algorithms. The engine will use the \acrshort{gtp} so it can be used with an existing GUI or other tools. - \item A parser for SGF files so they can be processed in the training of + \item A parser for \acrshort{sgf} files so they can be processed in the training of neural networks. \end{enumerate} @@ -84,8 +84,7 @@ requisites needed for the system. \item The engine program is interactive. - \item The engine implements the GTP (\textit{Go Text Protocol}) for its - interface. + \item The engine implements the \acrfull{gtp} for its interface. \begin{enumerate} \item Commands are read from standard input. \item Responses are provided via standard output. @@ -149,7 +148,7 @@ requisites needed for the system. \item The trainer can import existing games. \begin{enumerate} - \item Records of games stored as SGF can be imported. + \item Records of games stored as \acrshort{sgf} can be imported. \item Files containing records of games are provided as arguments to the trainer. \end{enumerate} @@ -264,16 +263,16 @@ between human players to show and test its capabilities. \subsubsection{Engine System} -The Engine System will implement the GTP interface and use the Game System to +The Engine System will implement the \acrshort{gtp} interface and use the Game System to analyse positions and generate moves via decision algorithms. This system can be directly called to manually set up game states and ask for -moves or can be called by other programs which use GTP to be used as backend for +moves or can be called by other programs which use \acrshort{gtp} to be used as backend for playing matches against a computer player. \subsubsection{Training System} -The Training System will process SGF files storing records of games, train the +The Training System will process \acrshort{sgf} files storing records of games, train the neural network models over those games and store the result. These models can then be imported by the engine and be used to generate moves. @@ -285,7 +284,7 @@ and uses it to train and store the neural network models. Both the Engine and Training systems depend on the GameMove class of the Game System. The Engine System uses it to store the state of a game and provide it to the decision algorithms. The Training System uses it to create the internal -representation of a game resulting from the processing of an SGF file. +representation of a game resulting from the processing of an \acrshort{sgf} file. \subsection{Class Analysis} @@ -666,12 +665,12 @@ The classes resulting from the analysis phase are shown in \textbf{Parser} \\ \midrule \textbf{Description} \\ - Reads SGF files and converts them to a tree of GameMove from the Game + Reads \acrshort{sgf} files and converts them to a tree of GameMove from the Game System. \\ \midrule \textbf{Responsibilities} \\ - \tabitem{Read SGF files.} \\ - \tabitem{Convert the content of the SGF files to a tree of GameMove.} \\ + \tabitem{Read \acrshort{sgf} files.} \\ + \tabitem{Convert the content of the \acrshort{sgf} files to a tree of GameMove.} \\ \midrule \textbf{Proposed attributes} \\ %TODO: Explain why this is empty @@ -688,14 +687,14 @@ The classes resulting from the analysis phase are shown in \textbf{ASTNode} \\ \midrule \textbf{Description} \\ - Makes up the tree resulting from the parsing of an SGF file.\\ + Makes up the tree resulting from the parsing of an \acrshort{sgf} file.\\ \midrule \textbf{Responsibilities} \\ \tabitem{Obtain a GameMove tree from itself and its children.} \\ \midrule \textbf{Proposed attributes} \\ \tabitem{\textbf{children}: The nodes following from itself.} \\ - \tabitem{\textbf{props}: The properties of the tree read from an SGF file.} + \tabitem{\textbf{props}: The properties of the tree read from an \acrshort{sgf} file.} \\ \midrule \textbf{Proposed methods} \\ @@ -746,7 +745,7 @@ against another machine player. \paragraph{Generate a move} The engine interface reads the input for generating a move as stated by the -GTP protocol and outputs the coordinates of the board to play. +\acrshort{gtp} protocol and outputs the coordinates of the board to play. \subsection{Use Case Analysis and Scenarios} diff --git a/doc/tex/systemDesign.tex b/doc/tex/systemDesign.tex index 3e82698..f0762e8 100644 --- a/doc/tex/systemDesign.tex +++ b/doc/tex/systemDesign.tex @@ -58,11 +58,11 @@ These classes and their relationships can be seen in \fref{fig:game}. \begin{figure}[h] \begin{center} \includegraphics[width=0.8\textwidth]{diagrams/engineModule.png} - \caption{Design of the GTP engine.}\label{fig:engine} + \caption{Design of the \acrshort{gtp} engine.}\label{fig:engine} \end{center} \end{figure} -An implementation of GTP, that is, the piece of software which offers the GTP +An implementation of \acrshort{gtp}, that is, the piece of software which offers the \acrshort{gtp} interface to other applications. It is designed to be used by a software controller but can also be directly run, mostly for debugging purposes. Its design is shown in \fref{fig:engine}. The core of the engine is related with @@ -258,7 +258,7 @@ respectively. \begin{figure}[h] \begin{center} \includegraphics[width=\textwidth]{diagrams/trainingModule.png} - \caption{Components of the SGF file parsing module.} + \caption{Components of the \acrshort{sgf} file parsing module.} \label{fig:trainingModule} Components not showing a capital C are not classes, as in they not follow the object-oriented paradigm and do not implement any classes, @@ -269,15 +269,15 @@ respectively. Neural networks can be powerful machine learning algorithms, but they have to be trained first so they can provide meaningful results. For a Go AI it makes sense to have its algorithms trained on Go games. There exists a common text format to -store Go games: SGF. If the system is able to process SGF files, it can provide +store Go games: \acrshort{sgf}. If the system is able to process \acrshort{sgf} files, it can provide the games stored on them to the neural networks for training. And so the need -for an SGF parser arises. +for an \acrshort{sgf} parser arises. -To parse SGF files a lexer and parser have been implemented using PLY.\@ The +To parse \acrshort{sgf} files a lexer and parser have been implemented using PLY.\@ The result of the parsing is an AST (Annotated Syntax Tree) reflecting the contents of the text input, each node with zero or more properties, and with the ability to convert themselves and their corresponding subtree into a GameMove tree. This -is done for the root node, since from the SGF specification there are some +is done for the root node, since from the \acrshort{sgf} specification there are some properties only usable in the root node, like those which specify general game information and properties such as rank of players or \gls{komi}. @@ -287,19 +287,19 @@ solved. These components are shown in \fref{fig:trainingModule}. \begin{itemize} - \item \textbf{SGF}: Provides a high-level method to convert a path to a SGF + \item \textbf{\acrshort{sgf}}: Provides a high-level method to convert a path to a \acrshort{sgf} file to a GameMove tree. - \item \textbf{sgfyacc}: The implementation of a SGF parser using PLY. Takes + \item \textbf{sgfyacc}: The implementation of a \acrshort{sgf} parser using PLY. Takes the tokens generated by \textbf{sgflex} and creates an ASTNode tree from them. - \item \textbf{sgflex}: The implementation of a SGF lexer using PLY.\@ Takes - text input and generates the tokens of the SGF language from them. + \item \textbf{sgflex}: The implementation of a \acrshort{sgf} lexer using PLY.\@ Takes + text input and generates the tokens of the \acrshort{sgf} language from them. - \item \textbf{ASTNode}: The AST resulting from the parsing of a SGF file. + \item \textbf{ASTNode}: The AST resulting from the parsing of a \acrshort{sgf} file. Has a method to convert it to a tree of GameMove and so obtain the - contents of the SGF in the internal representation used by the project's + contents of the \acrshort{sgf} in the internal representation used by the project's systems. \item \textbf{Property}: The representation of a property of an ASTNode @@ -313,7 +313,7 @@ The training can be started with the executable \texttt{train.py}. %\subsection{Modules} % %One module to store the state of the game and the game tree. One module to parse -%moves. One module to read and write SGF files. Modules are shown in +%moves. One module to read and write \acrshort{sgf} files. Modules are shown in %\fref{fig:modules}. % %\begin{figure}[h] -- cgit v1.2.1 From 824d5e3b6954407a694f5739cbeb40f66324c8d7 Mon Sep 17 00:00:00 2001 From: InigoGutierrez Date: Wed, 14 Dec 2022 15:41:49 +0100 Subject: Developing Unit Testing. --- doc/listings/test.sh | 2 + doc/listings/testOutput.txt | 28 +++++++++++ doc/tex/imago.tex | 10 ---- doc/tex/systemAnalysis.tex | 31 +++++++++++++ imago/engine/imagoIO.py | 108 +++++++++++++++++++++++-------------------- imago/engine/monteCarlo.py | 56 ++++++++++++---------- imago/gameLogic/gameMove.py | 9 ++-- imago/gameLogic/gameState.py | 13 +++--- tests/test_gameMove.py | 6 +-- tests/test_gameState.py | 89 +++++++++++++++++++++++++++++++++++ tests/test_imagoIO.py | 40 ++++++++++++++++ tests/test_monteCarlo.py | 48 +++++++++++++++++++ 12 files changed, 341 insertions(+), 99 deletions(-) create mode 100644 doc/listings/test.sh create mode 100644 doc/listings/testOutput.txt create mode 100644 tests/test_gameState.py create mode 100644 tests/test_imagoIO.py 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 ") + self._responseError("Wrong number of arguments") + self._responseError("Usag. boardsize ") 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 ") + self._responseError("Wrong number of arguments") + self._responseError("Usage: komi ") 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 ") + self._responseError("Wrong number of arguments") + self._responseError("Usage: fixed_handicap ") 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 ") + self._responseError("Wrong number of arguments.") + self._responseError("Usage: play ") 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 ") + self._responseError("Wrong number of arguments.") + self._responseError("Usage: genmove ") 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) -- cgit v1.2.1 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 From f9f150a644422714b16380e490db51989da53c61 Mon Sep 17 00:00:00 2001 From: InigoGutierrez Date: Tue, 21 Feb 2023 11:17:47 +0100 Subject: Added copy of GNU Free Documentation License. --- doc/tex/imago.tex | 5 +- doc/tex/license.tex | 535 +++++++++++++++++++++++++++++++++++++++++++++++++++ doc/tex/planning.tex | 2 +- doc/tex/results.tex | 12 +- 4 files changed, 546 insertions(+), 8 deletions(-) create mode 100644 doc/tex/license.tex diff --git a/doc/tex/imago.tex b/doc/tex/imago.tex index 7978a0c..8067043 100644 --- a/doc/tex/imago.tex +++ b/doc/tex/imago.tex @@ -115,7 +115,7 @@ author's Degree's Final Project. This document is based on a template by José Manuel Redondo López, associate professor of the University of Oviedo. The copyright notice he asks for -inclusion to use this template is included here. +inclusion of to use this template is included here. \begin{displayquote} @@ -181,5 +181,8 @@ inclusion to use this template is included here. \section{References} \printbibliography[type=article,title={Cited articles},heading=subbibintoc]{} \printbibliography[type=online,title={Online resources},heading=subbibintoc]{} +\clearpage + +\input{tex/license.tex} \end{document} diff --git a/doc/tex/license.tex b/doc/tex/license.tex new file mode 100644 index 0000000..d0e8c3a --- /dev/null +++ b/doc/tex/license.tex @@ -0,0 +1,535 @@ +\begin{center} + \section{GNU Free Documentation License} +\end{center} + + \begin{center} + + Version 1.3, 3 November 2008 + + + Copyright \copyright{} 2000, 2001, 2002, 2007, 2008 Free Software Foundation, Inc. + + \bigskip + + \texttt{} + + \bigskip + + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. +\end{center} + + +\begin{center} +{\bf\large Preamble} +\end{center} + +The purpose of this License is to make a manual, textbook, or other +functional and useful document ``free'' in the sense of freedom: to +assure everyone the effective freedom to copy and redistribute it, +with or without modifying it, either commercially or noncommercially. +Secondarily, this License preserves for the author and publisher a way +to get credit for their work, while not being considered responsible +for modifications made by others. + +This License is a kind of ``copyleft'', which means that derivative +works of the document must themselves be free in the same sense. It +complements the GNU General Public License, which is a copyleft +license designed for free software. + +We have designed this License in order to use it for manuals for free +software, because free software needs free documentation: a free +program should come with manuals providing the same freedoms that the +software does. But this License is not limited to software manuals; +it can be used for any textual work, regardless of subject matter or +whether it is published as a printed book. We recommend this License +principally for works whose purpose is instruction or reference. + + +\begin{center} +{\Large\textbf{1. APPLICABILITY AND DEFINITIONS}} +\end{center} + +This License applies to any manual or other work, in any medium, that +contains a notice placed by the copyright holder saying it can be +distributed under the terms of this License. Such a notice grants a +world-wide, royalty-free license, unlimited in duration, to use that +work under the conditions stated herein. The ``\textbf{Document}'', below, +refers to any such manual or work. Any member of the public is a +licensee, and is addressed as ``\textbf{you}''. You accept the license if you +copy, modify or distribute the work in a way requiring permission +under copyright law. + +A ``\textbf{Modified Version}'' of the Document means any work containing the +Document or a portion of it, either copied verbatim, or with +modifications and/or translated into another language. + +A ``\textbf{Secondary Section}'' is a named appendix or a front-matter section of +the Document that deals exclusively with the relationship of the +publishers or authors of the Document to the Document's overall subject +(or to related matters) and contains nothing that could fall directly +within that overall subject. (Thus, if the Document is in part a +textbook of mathematics, a Secondary Section may not explain any +mathematics.) The relationship could be a matter of historical +connection with the subject or with related matters, or of legal, +commercial, philosophical, ethical or political position regarding +them. + +The ``\textbf{Invariant Sections}'' are certain Secondary Sections whose titles +are designated, as being those of Invariant Sections, in the notice +that says that the Document is released under this License. If a +section does not fit the above definition of Secondary then it is not +allowed to be designated as Invariant. The Document may contain zero +Invariant Sections. If the Document does not identify any Invariant +Sections then there are none. + +The ``\textbf{Cover Texts}'' are certain short passages of text that are listed, +as Front-Cover Texts or Back-Cover Texts, in the notice that says that +the Document is released under this License. A Front-Cover Text may +be at most 5 words, and a Back-Cover Text may be at most 25 words. + +A ``\textbf{Transparent}'' copy of the Document means a machine-readable copy, +represented in a format whose specification is available to the +general public, that is suitable for revising the document +straightforwardly with generic text editors or (for images composed of +pixels) generic paint programs or (for drawings) some widely available +drawing editor, and that is suitable for input to text formatters or +for automatic translation to a variety of formats suitable for input +to text formatters. A copy made in an otherwise Transparent file +format whose markup, or absence of markup, has been arranged to thwart +or discourage subsequent modification by readers is not Transparent. +An image format is not Transparent if used for any substantial amount +of text. A copy that is not ``Transparent'' is called ``\textbf{Opaque}''. + +Examples of suitable formats for Transparent copies include plain +ASCII without markup, Texinfo input format, LaTeX input format, SGML +or XML using a publicly available DTD, and standard-conforming simple +HTML, PostScript or PDF designed for human modification. Examples of +transparent image formats include PNG, XCF and JPG. Opaque formats +include proprietary formats that can be read and edited only by +proprietary word processors, SGML or XML for which the DTD and/or +processing tools are not generally available, and the +machine-generated HTML, PostScript or PDF produced by some word +processors for output purposes only. + +The ``\textbf{Title Page}'' means, for a printed book, the title page itself, +plus such following pages as are needed to hold, legibly, the material +this License requires to appear in the title page. For works in +formats which do not have any title page as such, ``Title Page'' means +the text near the most prominent appearance of the work's title, +preceding the beginning of the body of the text. + +The ``\textbf{publisher}'' means any person or entity that distributes +copies of the Document to the public. + +A section ``\textbf{Entitled XYZ}'' means a named subunit of the Document whose +title either is precisely XYZ or contains XYZ in parentheses following +text that translates XYZ in another language. (Here XYZ stands for a +specific section name mentioned below, such as ``\textbf{Acknowledgements}'', +``\textbf{Dedications}'', ``\textbf{Endorsements}'', or ``\textbf{History}''.) +To ``\textbf{Preserve the Title}'' +of such a section when you modify the Document means that it remains a +section ``Entitled XYZ'' according to this definition. + +The Document may include Warranty Disclaimers next to the notice which +states that this License applies to the Document. These Warranty +Disclaimers are considered to be included by reference in this +License, but only as regards disclaiming warranties: any other +implication that these Warranty Disclaimers may have is void and has +no effect on the meaning of this License. + + +\begin{center} +{\Large\bf 2. VERBATIM COPYING\par} +\end{center} + +You may copy and distribute the Document in any medium, either +commercially or noncommercially, provided that this License, the +copyright notices, and the license notice saying this License applies +to the Document are reproduced in all copies, and that you add no other +conditions whatsoever to those of this License. You may not use +technical measures to obstruct or control the reading or further +copying of the copies you make or distribute. However, you may accept +compensation in exchange for copies. If you distribute a large enough +number of copies you must also follow the conditions in section~3. + +You may also lend copies, under the same conditions stated above, and +you may publicly display copies. + + +\begin{center} +{\Large\bf 3. COPYING IN QUANTITY\par} +\end{center} + + +If you publish printed copies (or copies in media that commonly have +printed covers) of the Document, numbering more than 100, and the +Document's license notice requires Cover Texts, you must enclose the +copies in covers that carry, clearly and legibly, all these Cover +Texts: Front-Cover Texts on the front cover, and Back-Cover Texts on +the back cover. Both covers must also clearly and legibly identify +you as the publisher of these copies. The front cover must present +the full title with all words of the title equally prominent and +visible. You may add other material on the covers in addition. +Copying with changes limited to the covers, as long as they preserve +the title of the Document and satisfy these conditions, can be treated +as verbatim copying in other respects. + +If the required texts for either cover are too voluminous to fit +legibly, you should put the first ones listed (as many as fit +reasonably) on the actual cover, and continue the rest onto adjacent +pages. + +If you publish or distribute Opaque copies of the Document numbering +more than 100, you must either include a machine-readable Transparent +copy along with each Opaque copy, or state in or with each Opaque copy +a computer-network location from which the general network-using +public has access to download using public-standard network protocols +a complete Transparent copy of the Document, free of added material. +If you use the latter option, you must take reasonably prudent steps, +when you begin distribution of Opaque copies in quantity, to ensure +that this Transparent copy will remain thus accessible at the stated +location until at least one year after the last time you distribute an +Opaque copy (directly or through your agents or retailers) of that +edition to the public. + +It is requested, but not required, that you contact the authors of the +Document well before redistributing any large number of copies, to give +them a chance to provide you with an updated version of the Document. + + +\begin{center} +{\Large\bf 4. MODIFICATIONS\par} +\end{center} + +You may copy and distribute a Modified Version of the Document under +the conditions of sections 2 and 3 above, provided that you release +the Modified Version under precisely this License, with the Modified +Version filling the role of the Document, thus licensing distribution +and modification of the Modified Version to whoever possesses a copy +of it. In addition, you must do these things in the Modified Version: + +\begin{itemize} +\item[A.] + Use in the Title Page (and on the covers, if any) a title distinct + from that of the Document, and from those of previous versions + (which should, if there were any, be listed in the History section + of the Document). You may use the same title as a previous version + if the original publisher of that version gives permission. + +\item[B.] + List on the Title Page, as authors, one or more persons or entities + responsible for authorship of the modifications in the Modified + Version, together with at least five of the principal authors of the + Document (all of its principal authors, if it has fewer than five), + unless they release you from this requirement. + +\item[C.] + State on the Title page the name of the publisher of the + Modified Version, as the publisher. + +\item[D.] + Preserve all the copyright notices of the Document. + +\item[E.] + Add an appropriate copyright notice for your modifications + adjacent to the other copyright notices. + +\item[F.] + Include, immediately after the copyright notices, a license notice + giving the public permission to use the Modified Version under the + terms of this License, in the form shown in the Addendum below. + +\item[G.] + Preserve in that license notice the full lists of Invariant Sections + and required Cover Texts given in the Document's license notice. + +\item[H.] + Include an unaltered copy of this License. + +\item[I.] + Preserve the section Entitled ``History'', Preserve its Title, and add + to it an item stating at least the title, year, new authors, and + publisher of the Modified Version as given on the Title Page. If + there is no section Entitled ``History'' in the Document, create one + stating the title, year, authors, and publisher of the Document as + given on its Title Page, then add an item describing the Modified + Version as stated in the previous sentence. + +\item[J.] + Preserve the network location, if any, given in the Document for + public access to a Transparent copy of the Document, and likewise + the network locations given in the Document for previous versions + it was based on. These may be placed in the ``History'' section. + You may omit a network location for a work that was published at + least four years before the Document itself, or if the original + publisher of the version it refers to gives permission. + +\item[K.] + For any section Entitled ``Acknowledgements'' or ``Dedications'', + Preserve the Title of the section, and preserve in the section all + the substance and tone of each of the contributor acknowledgements + and/or dedications given therein. + +\item[L.] + Preserve all the Invariant Sections of the Document, + unaltered in their text and in their titles. Section numbers + or the equivalent are not considered part of the section titles. + +\item[M.] + Delete any section Entitled ``Endorsements''. Such a section + may not be included in the Modified Version. + +\item[N.] + Do not retitle any existing section to be Entitled ``Endorsements'' + or to conflict in title with any Invariant Section. + +\item[O.] + Preserve any Warranty Disclaimers. +\end{itemize} + +If the Modified Version includes new front-matter sections or +appendices that qualify as Secondary Sections and contain no material +copied from the Document, you may at your option designate some or all +of these sections as invariant. To do this, add their titles to the +list of Invariant Sections in the Modified Version's license notice. +These titles must be distinct from any other section titles. + +You may add a section Entitled ``Endorsements'', provided it contains +nothing but endorsements of your Modified Version by various +parties---for example, statements of peer review or that the text has +been approved by an organization as the authoritative definition of a +standard. + +You may add a passage of up to five words as a Front-Cover Text, and a +passage of up to 25 words as a Back-Cover Text, to the end of the list +of Cover Texts in the Modified Version. Only one passage of +Front-Cover Text and one of Back-Cover Text may be added by (or +through arrangements made by) any one entity. If the Document already +includes a cover text for the same cover, previously added by you or +by arrangement made by the same entity you are acting on behalf of, +you may not add another; but you may replace the old one, on explicit +permission from the previous publisher that added the old one. + +The author(s) and publisher(s) of the Document do not by this License +give permission to use their names for publicity for or to assert or +imply endorsement of any Modified Version. + + +\begin{center} +{\Large\bf 5. COMBINING DOCUMENTS\par} +\end{center} + + +You may combine the Document with other documents released under this +License, under the terms defined in section~4 above for modified +versions, provided that you include in the combination all of the +Invariant Sections of all of the original documents, unmodified, and +list them all as Invariant Sections of your combined work in its +license notice, and that you preserve all their Warranty Disclaimers. + +The combined work need only contain one copy of this License, and +multiple identical Invariant Sections may be replaced with a single +copy. If there are multiple Invariant Sections with the same name but +different contents, make the title of each such section unique by +adding at the end of it, in parentheses, the name of the original +author or publisher of that section if known, or else a unique number. +Make the same adjustment to the section titles in the list of +Invariant Sections in the license notice of the combined work. + +In the combination, you must combine any sections Entitled ``History'' +in the various original documents, forming one section Entitled +``History''; likewise combine any sections Entitled ``Acknowledgements'', +and any sections Entitled ``Dedications''. You must delete all sections +Entitled ``Endorsements''. + +\begin{center} +{\Large\bf 6. COLLECTIONS OF DOCUMENTS\par} +\end{center} + +You may make a collection consisting of the Document and other documents +released under this License, and replace the individual copies of this +License in the various documents with a single copy that is included in +the collection, provided that you follow the rules of this License for +verbatim copying of each of the documents in all other respects. + +You may extract a single document from such a collection, and distribute +it individually under this License, provided you insert a copy of this +License into the extracted document, and follow this License in all +other respects regarding verbatim copying of that document. + + +\begin{center} +{\Large\bf 7. AGGREGATION WITH INDEPENDENT WORKS\par} +\end{center} + + +A compilation of the Document or its derivatives with other separate +and independent documents or works, in or on a volume of a storage or +distribution medium, is called an ``aggregate'' if the copyright +resulting from the compilation is not used to limit the legal rights +of the compilation's users beyond what the individual works permit. +When the Document is included in an aggregate, this License does not +apply to the other works in the aggregate which are not themselves +derivative works of the Document. + +If the Cover Text requirement of section~3 is applicable to these +copies of the Document, then if the Document is less than one half of +the entire aggregate, the Document's Cover Texts may be placed on +covers that bracket the Document within the aggregate, or the +electronic equivalent of covers if the Document is in electronic form. +Otherwise they must appear on printed covers that bracket the whole +aggregate. + + +\begin{center} +{\Large\bf 8. TRANSLATION\par} +\end{center} + + +Translation is considered a kind of modification, so you may +distribute translations of the Document under the terms of section~4. +Replacing Invariant Sections with translations requires special +permission from their copyright holders, but you may include +translations of some or all Invariant Sections in addition to the +original versions of these Invariant Sections. You may include a +translation of this License, and all the license notices in the +Document, and any Warranty Disclaimers, provided that you also include +the original English version of this License and the original versions +of those notices and disclaimers. In case of a disagreement between +the translation and the original version of this License or a notice +or disclaimer, the original version will prevail. + +If a section in the Document is Entitled ``Acknowledgements'', +``Dedications'', or ``History'', the requirement (section~4) to Preserve +its Title (section~1) will typically require changing the actual +title. + + +\begin{center} +{\Large\bf 9. TERMINATION\par} +\end{center} + + +You may not copy, modify, sublicense, or distribute the Document +except as expressly provided under this License. Any attempt +otherwise to copy, modify, sublicense, or distribute it is void, and +will automatically terminate your rights under this License. + +However, if you cease all violation of this License, then your license +from a particular copyright holder is reinstated (a) provisionally, +unless and until the copyright holder explicitly and finally +terminates your license, and (b) permanently, if the copyright holder +fails to notify you of the violation by some reasonable means prior to +60 days after the cessation. + +Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + +Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, receipt of a copy of some or all of the same material does +not give you any rights to use it. + + +\begin{center} +{\Large\bf 10. FUTURE REVISIONS OF THIS LICENSE\par} +\end{center} + + +The Free Software Foundation may publish new, revised versions +of the GNU Free Documentation License from time to time. Such new +versions will be similar in spirit to the present version, but may +differ in detail to address new problems or concerns. See +\texttt{https://www.gnu.org/licenses/}. + +Each version of the License is given a distinguishing version number. +If the Document specifies that a particular numbered version of this +License ``or any later version'' applies to it, you have the option of +following the terms and conditions either of that specified version or +of any later version that has been published (not as a draft) by the +Free Software Foundation. If the Document does not specify a version +number of this License, you may choose any version ever published (not +as a draft) by the Free Software Foundation. If the Document +specifies that a proxy can decide which future versions of this +License can be used, that proxy's public statement of acceptance of a +version permanently authorizes you to choose that version for the +Document. + + +\begin{center} +{\Large\bf 11. RELICENSING\par} +\end{center} + + +``Massive Multiauthor Collaboration Site'' (or ``MMC Site'') means any +World Wide Web server that publishes copyrightable works and also +provides prominent facilities for anybody to edit those works. A +public wiki that anybody can edit is an example of such a server. A +``Massive Multiauthor Collaboration'' (or ``MMC'') contained in the +site means any set of copyrightable works thus published on the MMC +site. + +``CC-BY-SA'' means the Creative Commons Attribution-Share Alike 3.0 +license published by Creative Commons Corporation, a not-for-profit +corporation with a principal place of business in San Francisco, +California, as well as future copyleft versions of that license +published by that same organization. + +``Incorporate'' means to publish or republish a Document, in whole or +in part, as part of another Document. + +An MMC is ``eligible for relicensing'' if it is licensed under this +License, and if all works that were first published under this License +somewhere other than this MMC, and subsequently incorporated in whole +or in part into the MMC, (1) had no cover texts or invariant sections, +and (2) were thus incorporated prior to November 1, 2008. + +The operator of an MMC Site may republish an MMC contained in the site +under CC-BY-SA on the same site at any time before August 1, 2009, +provided the MMC is eligible for relicensing. + + +\begin{center} +{\Large\bf ADDENDUM: How to use this License for your documents\par} +\end{center} + +To use this License in a document you have written, include a copy of +the License in the document and put the following copyright and +license notices just after the title page: + +\bigskip +\begin{quote} + Copyright \copyright{} YEAR YOUR NAME. + Permission is granted to copy, distribute and/or modify this document + under the terms of the GNU Free Documentation License, Version 1.3 + or any later version published by the Free Software Foundation; + with no Invariant Sections, no Front-Cover Texts, and no Back-Cover Texts. + A copy of the license is included in the section entitled ``GNU + Free Documentation License''. +\end{quote} +\bigskip + +If you have Invariant Sections, Front-Cover Texts and Back-Cover Texts, +replace the ``with \dots\ Texts.''\ line with this: + +\bigskip +\begin{quote} + with the Invariant Sections being LIST THEIR TITLES, with the + Front-Cover Texts being LIST, and with the Back-Cover Texts being LIST. +\end{quote} +\bigskip + +If you have Invariant Sections without Cover Texts, or some other +combination of the three, merge those two alternatives to suit the +situation. + +If your document contains nontrivial examples of program code, we +recommend releasing these examples in parallel under your choice of +free software license, such as the GNU General Public License, +to permit their use in free software. diff --git a/doc/tex/planning.tex b/doc/tex/planning.tex index 0e8749f..5d73ec3 100644 --- a/doc/tex/planning.tex +++ b/doc/tex/planning.tex @@ -157,7 +157,7 @@ used to train the decision algorithms based on neural networks. An example of a \begin{listing}[h] \inputminted[breakafter=\]]{text}{listings/sgfExample.sgf} - \caption{\acrshort{sgf} example. Describes a tsumego (Go problem) setup and two + \caption{\acrshort{sgf} example. Describes a \gls{tsumego} (Go problem) setup and two variants, one commented as "Correct" and other commented as "Incorrect".} \label{lst:sgfExample} \end{listing} diff --git a/doc/tex/results.tex b/doc/tex/results.tex index 5433041..fac2df9 100644 --- a/doc/tex/results.tex +++ b/doc/tex/results.tex @@ -72,11 +72,11 @@ A record of the game is shown in \fref{fig:mctsVSmcts}. Since tree exploration on smaller boards or advanced games with little empty spaces should be easier the algorithm has also been tested on some Go problems. -A Go problem or tsumego is a predefined layout of the board, or of a section of +A Go problem or \gls{tsumego} is a predefined layout of the board, or of a section of the board, for which the player must find some beneficial move. Life and death -problems are a subset of tsumegos in which the survival of a group depends on +problems are a subset of \gls{tsumego}s in which the survival of a group depends on finding the correct sequence to save or kill the group. One collection of such -tsumegos is \textit{Cho Chikun's Encyclopedia of Life and Death}, part of which +\gls{tsumego}s is \textit{Cho Chikun's Encyclopedia of Life and Death}, part of which are available on OGS \cite{ogsLifeAndDeath}, an online Go server. The first of these problems and what the algorithm suggested as moves is shown @@ -86,18 +86,18 @@ Black makes the first move, which means the solution is to find some favorable outcome for black, which in this case is killing the white group. The white group has a critical point on B1. If white plays on B1 they make two eyes and live, but if black plays there first white can't make two eyes and dies, so B1 -is the solution to the tsumego. This is one of the easiest Go problems. +is the solution to the \gls{tsumego}. This is one of the easiest Go problems. The algorithm neglects this solution. While asked five times to generate a move for the starting position it suggested B1 only once. But notably, after making another move, it consistently suggested B1 for white, which is the solution now that white has to play. So in the end it was able to -solve the tsumego, probably because after making a move it had already explored +solve the \gls{tsumego}, probably because after making a move it had already explored part of the tree but it was difficult that it explored the solution for the first move. -The engine was tested against other tsumegos but it was not able to solve them, +The engine was tested against other \gls{tsumego}s but it was not able to solve them, so no more are shown here. \subsection{Neural Network Training} -- cgit v1.2.1 From 2d895e4abb26eccefe6b4bc201fd60eb79600e3e Mon Sep 17 00:00:00 2001 From: InigoGutierrez Date: Sat, 13 May 2023 19:44:54 +0200 Subject: Added visualization of SGF example. --- doc/Makefile | 2 +- doc/listings/testOutput.txt | 21 +++++------ doc/tex/appendixes.tex | 51 ++++++++++++++++++++++++++ doc/tex/glossary.tex | 8 ++--- doc/tex/imago.tex | 11 +++--- doc/tex/planning.tex | 57 +++++++++++++++++------------ doc/tex/systemAnalysis.tex | 58 ++++++++++++++++-------------- doc/tex/systemDesign.tex | 88 ++++++++++++++++++++++++--------------------- tests/test_enums.py | 2 +- tests/test_neuralNetwork.py | 15 ++++++++ 10 files changed, 205 insertions(+), 108 deletions(-) create mode 100644 doc/tex/appendixes.tex create mode 100644 tests/test_neuralNetwork.py diff --git a/doc/Makefile b/doc/Makefile index c9b473e..554b0c5 100644 --- a/doc/Makefile +++ b/doc/Makefile @@ -3,7 +3,7 @@ docName = imago outputFolder = out -texFiles = tex/$(docName).tex tex/introduction.tex tex/planning.tex tex/systemAnalysis.tex tex/systemDesign.tex tex/implementation.tex tex/results.tex tex/conclusions.tex tex/biber.bib +texFiles = tex/$(docName).tex tex/introduction.tex tex/planning.tex tex/systemAnalysis.tex tex/systemDesign.tex tex/implementation.tex tex/results.tex tex/appendixes.tex tex/conclusions.tex tex/glossary.tex tex/license.tex tex/biber.bib diagramImgs = diagrams/planningWorkPlanEngine.png diagrams/planningWorkPlanGame.png diagrams/useCases.png diagrams/analysisClasses.png diagrams/useCase_generateAMove.png diagrams/useCase_useAsBackend.png diagrams/useCase_playAMatch.png diagrams/interfaces.png diagrams/gameModule.png diagrams/engineModule.png diagrams/trainingModule.png diagrams/modules.png diagrams/fullClasses.png diff --git a/doc/listings/testOutput.txt b/doc/listings/testOutput.txt index 2bfa52f..7d1cfda 100644 --- a/doc/listings/testOutput.txt +++ b/doc/listings/testOutput.txt @@ -1,21 +1,22 @@ +Invalid Move! Move outside board bounds. 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/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 105 105 0% 3-186 +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 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/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 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/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 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/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 @@ -23,6 +24,6 @@ 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% +TOTAL 1135 498 56% 8 empty files skipped. diff --git a/doc/tex/appendixes.tex b/doc/tex/appendixes.tex new file mode 100644 index 0000000..89c413c --- /dev/null +++ b/doc/tex/appendixes.tex @@ -0,0 +1,51 @@ +\section{Appendixes} + +Additional information is included here, after the main sections regarding the +project. + +\subsection{Budget} + +Here are tables regarding the costs of resources and development for the +project. + +\subsubsection{Work resources} + +The costs are calculated based on a programmer salary of 20€/hour. + +\begin{table}[h] + \makebox[\linewidth]{ + \begin{tabular}{l r r} + \toprule + \textbf{Task} & \textbf{Time (hours)} & \textbf{Cost (€)} \\ + \midrule + Game preliminary research & 15 & 300 \\ + \midrule + Game implementation & 55 & 1100 \\ + \midrule + Game unit testing & 50 & 1000 \\ + \midrule + Game system testing & 5 & 100 \\ + \midrule + \textbf{Total} & \textbf{125} & \textbf{2500} \\ + \bottomrule + \end{tabular} + } +\end{table} + +%TODO: Finish budgets + +\subsubsection{Material resources} + +\begin{table}[h] + \makebox[\linewidth]{ + \begin{tabular}{l r} + \toprule + \textbf{Resource} & \textbf{Cost (€)} \\ + \midrule + Development computer & 600 \\ + \bottomrule + \end{tabular} + } +\end{table} + +\subsubsection{Totals} diff --git a/doc/tex/glossary.tex b/doc/tex/glossary.tex index 8b43e94..b1cea02 100644 --- a/doc/tex/glossary.tex +++ b/doc/tex/glossary.tex @@ -1,15 +1,15 @@ \newglossaryentry{ko} { name=ko, - description={A rule which prevents plays that would repeat the previous - board position} + description={A rule forbidding plays that would repeat the previous board + position} } \newglossaryentry{superko} { name=superko, - description={An extension to the ko rule to prevent any previous board - position and not just the last one} + description={A modification to the ko rule forbidding plays that would + repeat any previous board position and not just the last one} } \newglossaryentry{komi} diff --git a/doc/tex/imago.tex b/doc/tex/imago.tex index 8067043..0447d70 100644 --- a/doc/tex/imago.tex +++ b/doc/tex/imago.tex @@ -151,8 +151,8 @@ inclusion of to use this template is included here. \clearpage -\input{tex/introduction.tex} -\clearpage +%\input{tex/introduction.tex} +%\clearpage \input{tex/planning.tex} \clearpage @@ -169,8 +169,11 @@ inclusion of to use this template is included here. %\input{tex/results.tex} %\clearpage -\input{tex/conclusions.tex} -\clearpage +%\input{tex/appendixes.tex} +%\clearpage + +%\input{tex/conclusions.tex} +%\clearpage \printglossaries \clearpage diff --git a/doc/tex/planning.tex b/doc/tex/planning.tex index 5d73ec3..17d413f 100644 --- a/doc/tex/planning.tex +++ b/doc/tex/planning.tex @@ -1,7 +1,7 @@ \section{Planning} -This section explains the aim of the project, its reach, the existing work it is -based on and an initial planning. +This section explains the aim of the project, its reach and the existing work it +is based on, and presents an initial planning. \subsection{Project Stages} @@ -123,20 +123,20 @@ presents a great resource for this project. The logo of the project is shown on \paragraph{GnuGo \cite{gnugo}} -A software capable of playing Go part of the GNU project. Although not a strong -engine anymore, it is interesting for historic reasons as the free software -engine for which the \acrshort{gtp} protocol was first defined. The logo of the +A software capable of playing Go and part of the GNU project. Although not a +strong engine anymore, it is interesting for historic reasons as the free +software engine for which the \acrfull{gtp} was first defined. The logo of the project is shown on \fref{fig:gnuGoLogo}. \subsubsection{Existing Standards} \paragraph{\acrshort{gtp} \cite{gtp}} -\acrshort{gtp} (\textit{\acrlong{gtp}}) is a text based protocol for -communication with computer Go programs. It is the protocol used by GNU Go and -the more modern and powerful KataGo. By supporting \acrshort{gtp} the engine -developed for this project can be used with existing GUIs and other programs, -making it easier to use it with the tools users are already familiar with. +The \acrfull{gtp} is a text based protocol for communication with computer Go +programs. It is the protocol used by GNU Go and the more modern and powerful +KataGo. By supporting \acrshort{gtp} the engine developed for this project can +be used with existing GUIs and other programs, making it easier to be used with +the tools target users are already familiar with. %TODO %\begin{listing}[h] @@ -147,21 +147,34 @@ making it easier to use it with the tools users are already familiar with. \paragraph{\acrshort{sgf} \cite{sgf}} -\acrshort{sgf} (\textit{\acrlong{sgf}}) is a text format widely used for storing -records of Go matches which allows for variants, comments and other metadata. It -was devised for Go but it supports other games with similar turn-based -structure. Many popular playing tools use it. By supporting \acrshort{sgf} vast -existing collections of games, such as those played on online Go servers, can be -used to train the decision algorithms based on neural networks. An example of a -\acrshort{sgf} file can be seen on \lref{lst:sgfExample}. +The \acrfull{sgf} is a text format widely used for storing records of Go matches +which allows for variants, comments and other metadata. It was devised for Go +but it supports other games with similar turn-based structure. Many popular +playing tools use it. By supporting \acrshort{sgf} vast existing collections of +games, such as those played on online Go servers, can be used to train the +decision algorithms based on neural networks. An example of a \acrshort{sgf} +file can be seen on \lref{lst:sgfExample}. The game state described in this file +is shown visually in \fref{fig:sgfExample}. \begin{listing}[h] \inputminted[breakafter=\]]{text}{listings/sgfExample.sgf} - \caption{\acrshort{sgf} example. Describes a \gls{tsumego} (Go problem) setup and two - variants, one commented as "Correct" and other commented as "Incorrect".} - \label{lst:sgfExample} + \caption{\acrshort{sgf} example. Describes a \gls{tsumego} (Go problem) + setup and two variants, one commented as "Correct" and other commented as + "Incorrect". + }\label{lst:sgfExample} \end{listing} +\begin{figure}[h] + \begin{center} + \includegraphics[width=0.5\textwidth]{img/sgfExample.png} + \caption{Screenshot of Sabaki showing the \gls{tsumego} described in the + \acrshort{sgf} example from \lref{lst:sgfExample}. Note that Sabaki + marks the two continuations described in the \acrshort{sgf} file with + two transparent grey dots. + }\label{fig:sgfExample} + \end{center} +\end{figure} + \begin{figure}[h] \begin{center} \includegraphics[width=0.5\textwidth]{img/sabaki.jpg} @@ -217,8 +230,8 @@ choice is Python \cite{python}, for various reasons: Both the game and the engine will offer a text interface. For the game this allows for quick human testing. For the engine it is mandated by the protocol, since \acrshort{gtp} is a text based protocol for programs using text -interfaces. Independent programs compatible with this interface can be used as a -GUI. +interfaces. Independent programs compatible with this interface will be able to +be used in conjunction with this engine, for example to serve as a GUI. There is also the need of an interface with \acrshort{sgf} files so existing games can be processed by the trainer. diff --git a/doc/tex/systemAnalysis.tex b/doc/tex/systemAnalysis.tex index ba5fbf1..5263d1f 100644 --- a/doc/tex/systemAnalysis.tex +++ b/doc/tex/systemAnalysis.tex @@ -7,7 +7,7 @@ These are the main goals the final product must reach. \begin{enumerate} \item The implementation, analysis and comparison of different decision - algorithms for genarating moves. This is the main goal and the following + algorithms for generating moves. This is the main goal and the following ones are derived from the need of reaching it. \item A library for representing the game of Go. It can be used for the @@ -278,13 +278,14 @@ then be imported by the engine and be used to generate moves. \subsubsection{Interface Between Subsystems} -The Training System depends on the NeuralNetwork interface of the Engine System -and uses it to train and store the neural network models. +The Training System depends on the \texttt{NeuralNetwork} interface of the +Engine System and uses it to train and store the neural network models. -Both the Engine and Training systems depend on the GameMove class of the Game -System. The Engine System uses it to store the state of a game and provide it -to the decision algorithms. The Training System uses it to create the internal -representation of a game resulting from the processing of an \acrshort{sgf} file. +Both the Engine and Training systems depend on the \texttt{GameMove} class of +the Game System. The Engine System uses it to store the state of a game and +provide it to the decision algorithms. The Training System uses it to create the +internal representation of a game resulting from the processing of an +\acrshort{sgf} file. \subsection{Class Analysis} @@ -415,7 +416,7 @@ The classes resulting from the analysis phase are shown in \tabitem{\textbf{visits}: How many times the node has been visited.} \\ \tabitem{\textbf{score}: The number of explorations of the node resulting in victory.} \\ - \tabitem{\textbf{move}: A GameMove for accessing game state and logic.} \\ + \tabitem{\textbf{move}: A \texttt{GameMove} for accessing game state and logic.} \\ \tabitem{\textbf{parent}: This node's parent in the tree.} \\ \tabitem{\textbf{children}: The nodes following from this node in the tree.} \\ @@ -430,7 +431,7 @@ The classes resulting from the analysis phase are shown in Selects the most promising node which still has some unexplored children.} \\ \tabitem{\textbf{expansion()}: Monte Carlo Tree Search expansion step. Picks - an unexplored vertex from the node and adds it as a new MCTSNode.} \\ + an unexplored vertex from the node and adds it as a new \texttt{MCTSNode}.} \\ \tabitem{\textbf{expansionForCoords()}: Performs an expansion for the given coordinates. This represents forcing a move on the algorithm.} \\ \tabitem{\textbf{simulation()}: Play random matches to accumulate reward @@ -445,17 +446,17 @@ The classes resulting from the analysis phase are shown in \textbf{Keras} \\ \midrule \textbf{Description} \\ - Implements the DecisionAlgorithm interface to give access to a neural - network. \\ + Implements the \texttt{DecisionAlgorithm} interface to give access to a + neural network. \\ \midrule \textbf{Responsibilities} \\ \tabitem{Analyzing game states and generating moves.} \\ \midrule \textbf{Proposed attributes} \\ - \tabitem{\textbf{currentMove}: A GameMove for accessing game state and + \tabitem{\textbf{currentMove}: A \texttt{GameMove} for accessing game state and logic.} \\ - \tabitem{\textbf{neuralNetwork}: A NeuralNetwork instance for generating - moves.} \\ + \tabitem{\textbf{neuralNetwork}: A \texttt{NeuralNetwork} instance for + generating moves.} \\ \midrule \textbf{Proposed methods} \\ \decisionAlgorithmMethods @@ -477,10 +478,10 @@ The classes resulting from the analysis phase are shown in \tabitem{Loading a model file to use an existing trained neural network.} \\ \midrule \textbf{Proposed attributes} \\ - \tabitem{\textbf{currentMove}: A GameMove for accessing game state and + \tabitem{\textbf{currentMove}: A \texttt{GameMove} for accessing game state and logic.} \\ - \tabitem{\textbf{neuralNetwork}: A NeuralNetwork instance for generating - moves.} \\ + \tabitem{\textbf{neuralNetwork}: A \texttt{NeuralNetwork} instance for + generating moves.} \\ \midrule \textbf{Proposed methods} \\ \tabitem{\textbf{pickMove()}: Uses the current internal model to pick a move @@ -653,8 +654,8 @@ The classes resulting from the analysis phase are shown in %TODO: Explain why this is empty \midrule \textbf{Proposed methods} \\ - \tabitem{\textbf{loadGameTree()}: Reads a file and generates a GameMove tree - from its contents.} \\ + \tabitem{\textbf{loadGameTree()}: Reads a file and generates a + \texttt{GameMove} tree from its contents.} \\ \bottomrule \end{tabular} @@ -665,12 +666,13 @@ The classes resulting from the analysis phase are shown in \textbf{Parser} \\ \midrule \textbf{Description} \\ - Reads \acrshort{sgf} files and converts them to a tree of GameMove from the Game - System. \\ + Reads \acrshort{sgf} files and converts them to a tree of \texttt{GameMove} + from the Game System. \\ \midrule \textbf{Responsibilities} \\ \tabitem{Read \acrshort{sgf} files.} \\ - \tabitem{Convert the content of the \acrshort{sgf} files to a tree of GameMove.} \\ + \tabitem{Convert the content of the \acrshort{sgf} files to a tree of + \texttt{GameMove}.} \\ \midrule \textbf{Proposed attributes} \\ %TODO: Explain why this is empty @@ -690,7 +692,7 @@ The classes resulting from the analysis phase are shown in Makes up the tree resulting from the parsing of an \acrshort{sgf} file.\\ \midrule \textbf{Responsibilities} \\ - \tabitem{Obtain a GameMove tree from itself and its children.} \\ + \tabitem{Obtain a \texttt{GameMove} tree from itself and its children.} \\ \midrule \textbf{Proposed attributes} \\ \tabitem{\textbf{children}: The nodes following from itself.} \\ @@ -698,8 +700,8 @@ The classes resulting from the analysis phase are shown in \\ \midrule \textbf{Proposed methods} \\ - \tabitem{\textbf{toGameTree()}: Returns a GameMove tree corresponding to the - tree following from this node.} \\ + \tabitem{\textbf{toGameTree()}: Returns a \texttt{GameMove} tree + corresponding to the tree following from this node.} \\ \bottomrule \end{tabular} @@ -905,7 +907,7 @@ The script used to run the tests is shown on \lref{lst:test} and its output on % Maybe put an example report here? \begin{listing}[h] \inputminted{bash}{listings/test.sh} - \caption{Dense neural network model.} + \caption{Script to run the tests.} \label{lst:test} \end{listing} @@ -920,3 +922,7 @@ The script used to run the tests is shown on \lref{lst:test} and its output on \subsubsection{System Testing} \subsubsection{Usability Testing} + +% Game playing + +% Using the engine with an existing GUI diff --git a/doc/tex/systemDesign.tex b/doc/tex/systemDesign.tex index f0762e8..7a5db7b 100644 --- a/doc/tex/systemDesign.tex +++ b/doc/tex/systemDesign.tex @@ -34,20 +34,20 @@ variants exploration is an important part of Go learning, \program{} and most playing and analysis existing programs allow for navigation back and forth through the board states of a match and for new variants to be created from each of these board states. Therefore, a match is represented as a tree of moves. The -GameMove class has the information about a specific move and also a reference to +\texttt{GameMove} class has the information about a specific move and also a reference to the previous move and to a list of following moves, implementing this tree structure and allowing for navigating both forward and backwards in the move history. The state of the board at any given move must be stored so liberties, captures count and legality of moves can be addressed, so it is represented with the -GameState class, which holds a reference to the current move. +\texttt{GameState} class, which holds a reference to the current move. Moves depend on a representation of the game board to have access to its current layout and count of captured stones. There are also many logic operations needed to be performed on the board, such as getting the stones in a group, counting their liberties or checking if a move is playable. The layout of the board and -these operations are implemented as the GameBoard class. +these operations are implemented as the \texttt{GameBoard} class. A game can be started by the executable \texttt{go.py}. @@ -74,24 +74,26 @@ three components, each with a separate responsibility: applications and offers the text interface. It reads and processes input and calls corresponding commands from the core of the engine. - \item The GameEngine contains the logic of the commands available from the - IO component. It uses a GameState to keep a record of the game and uses - a DecisionAlgorithm to generate moves. + \item The \texttt{GameEngine} contains the logic of the commands available + from the IO component. It uses a \texttt{GameState} to keep a record of + the game and uses a \texttt{DecisionAlgorithm} to generate moves. - \item The DecisionAlgorithm component is responsible of analyzing the match - and generate moves. The engine core uses it when a decision has to be - made by the AI, such as when a move needs to be generated by the engine. + \item The \texttt{DecisionAlgorithm} component is responsible of analyzing + the match and generate moves. The engine core uses it when a decision + has to be made by the AI, such as when a move needs to be generated by + the engine. \end{itemize} -Two implementations of DecisionAlgorithm have been made: one for the Monte -Carlo Tree Search algorithm (on the MCTS class) and the other for neural -networks (on the Keras class). +Two implementations of \texttt{DecisionAlgorithm} have been made: one for the +Monte Carlo Tree Search algorithm (on the \texttt{MCTS} class) and the other for +neural networks (on the \texttt{Keras} class). -The Keras class also makes use of the NeuralNetwork class, which offers -functions for creating, training, saving and using neural network models. The -designs of the network are implemented in the subclasses DenseNeuralNetwork and -ConvNeuralNetwork as examples of dense and convolutional networks, respectively. +The \texttt{Keras} class also makes use of the \texttt{NeuralNetwork} class, +which offers functions for creating, training, saving and using neural network +models. The designs of the network are implemented in the subclasses +\texttt{DenseNeuralNetwork} and \texttt{ConvNeuralNetwork} as examples of dense +and convolutional networks, respectively. The engine can be started with the executable \texttt{imagocli.py}. @@ -128,9 +130,9 @@ The exploration of the tree has 4 steps: The suggested move is the children of the current move with the best score from the perspective of the player which has to make the move. -The implementation of the algorithm will use the existing GameMove class from -the Game System to access the game logic it needs, such as to get the possible -children from a node or to simulate random games. +The implementation of the algorithm will use the existing \texttt{GameMove} +class from the Game System to access the game logic it needs, such as to get the +possible children from a node or to simulate random games. \subsubsection{Neural Networks Explained} @@ -182,8 +184,8 @@ Then, a network with convolutional and max pooling layers to compare the approach used on image processing to the more general one and studying its utility on the analysis of the Go board. -These networks have been implemented on the DenseNeuralNetwork and -ConvNeuralNetwork classes, respectively. +These networks have been implemented on the \texttt{DenseNeuralNetwork} and \\ +\texttt{ConvNeuralNetwork} classes, respectively. The networks have been designed to process boards of size 9x9, which is the introductory size to the game. It is the easiest both for the hardware to handle @@ -273,13 +275,14 @@ store Go games: \acrshort{sgf}. If the system is able to process \acrshort{sgf} the games stored on them to the neural networks for training. And so the need for an \acrshort{sgf} parser arises. -To parse \acrshort{sgf} files a lexer and parser have been implemented using PLY.\@ The -result of the parsing is an AST (Annotated Syntax Tree) reflecting the contents -of the text input, each node with zero or more properties, and with the ability -to convert themselves and their corresponding subtree into a GameMove tree. This -is done for the root node, since from the \acrshort{sgf} specification there are some -properties only usable in the root node, like those which specify general game -information and properties such as rank of players or \gls{komi}. +To parse \acrshort{sgf} files a lexer and parser have been implemented using +PLY.\@ The result of the parsing is an AST (Annotated Syntax Tree) reflecting +the contents of the text input, each node with zero or more properties, and with +the ability to convert themselves and their corresponding subtree into a +\texttt{GameMove} tree. This is done for the root node, since from the +\acrshort{sgf} specification there are some properties only usable in the root +node, like those which specify general game information and properties such as +rank of players or \gls{komi}. Here follows an explanation of the role and motivation before each component of the Training module to show how these previous concerns have been addressed and @@ -288,23 +291,24 @@ solved. These components are shown in \fref{fig:trainingModule}. \begin{itemize} \item \textbf{\acrshort{sgf}}: Provides a high-level method to convert a path to a \acrshort{sgf} - file to a GameMove tree. + file to a \texttt{GameMove} tree. \item \textbf{sgfyacc}: The implementation of a \acrshort{sgf} parser using PLY. Takes - the tokens generated by \textbf{sgflex} and creates an ASTNode tree from - them. + the tokens generated by \textbf{sgflex} and creates an \texttt{ASTNode} + tree from them. - \item \textbf{sgflex}: The implementation of a \acrshort{sgf} lexer using PLY.\@ Takes - text input and generates the tokens of the \acrshort{sgf} language from them. + \item \textbf{sgflex}: The implementation of a \acrshort{sgf} lexer using + PLY.\@ Takes text input and generates the tokens of the \acrshort{sgf} + language from them. - \item \textbf{ASTNode}: The AST resulting from the parsing of a \acrshort{sgf} file. - Has a method to convert it to a tree of GameMove and so obtain the - contents of the \acrshort{sgf} in the internal representation used by the project's - systems. + \item \textbf{ASTNode}: The AST resulting from the parsing of a + \acrshort{sgf} file. Has a method to convert it to a tree of + \texttt{GameMove} and so obtain the contents of the \acrshort{sgf} in + the internal representation used by the project's systems. - \item \textbf{Property}: The representation of a property of an ASTNode - tree. Each property is made of a name and one or more values and this - class helps handling this specific situation. + \item \textbf{Property}: The representation of a property of an + \texttt{ASTNode} tree. Each property is made of a name and one or more + values and this class helps handling this specific situation. The training can be started with the executable \texttt{train.py}. @@ -322,3 +326,7 @@ The training can be started with the executable \texttt{train.py}. % \caption{Modules.}\label{fig:modules} % \end{center} %\end{figure} + +%\subsection{Technical Testing Plan Specification} + +%TODO diff --git a/tests/test_enums.py b/tests/test_enums.py index 73f4009..21b2057 100644 --- a/tests/test_enums.py +++ b/tests/test_enums.py @@ -5,7 +5,7 @@ import unittest from imago.data.enums import Player class TestEnums(unittest.TestCase): - """Test parseHelpers module.""" + """Test enums module.""" def testOtherPlayer(self): """Test method to get the other player""" diff --git a/tests/test_neuralNetwork.py b/tests/test_neuralNetwork.py new file mode 100644 index 0000000..dfcbd7a --- /dev/null +++ b/tests/test_neuralNetwork.py @@ -0,0 +1,15 @@ +"""Tests for neural network module.""" + +import unittest + +from imago.engine.keras.neuralNetwork import NeuralNetwork + +class TestNeuralNetwork(unittest.TestCase): + """Test neural network module.""" + + def testLoadBaseClass(self): + """Test error when creating model with the base NeuralNetwork class""" + + self.assertRaises(NotImplementedError, + NeuralNetwork, + "non/existing/file") -- cgit v1.2.1 From 88fbf5f8919211cfa06116a76f42fb26ec9f2e18 Mon Sep 17 00:00:00 2001 From: InigoGutierrez Date: Fri, 2 Jun 2023 13:02:08 +0200 Subject: Second revision and added rules of the game. --- doc/Makefile | 2 +- doc/diagrams/useCase_trainTheEngine.puml | 24 ++++ doc/diagrams/useCases.puml | 2 + doc/tex/appendixes.tex | 42 +++++- doc/tex/biber.bib | 26 ++++ doc/tex/conclusions.tex | 19 +-- doc/tex/glossary.tex | 21 ++- doc/tex/imago.tex | 31 ++-- doc/tex/implementation.tex | 234 ++++++++++++++++++++++++------ doc/tex/introduction.tex | 42 +++++- doc/tex/planning.tex | 116 ++++++++------- doc/tex/results.tex | 112 ++++++++------- doc/tex/systemAnalysis.tex | 152 ++++++++++++++------ doc/tex/systemDesign.tex | 240 ++++++++++++++++++++++++------- go.py | 30 +++- imago/gameLogic/gameState.py | 2 +- train.py | 4 + 17 files changed, 817 insertions(+), 282 deletions(-) create mode 100644 doc/diagrams/useCase_trainTheEngine.puml diff --git a/doc/Makefile b/doc/Makefile index 554b0c5..cf6c166 100644 --- a/doc/Makefile +++ b/doc/Makefile @@ -5,7 +5,7 @@ outputFolder = out texFiles = tex/$(docName).tex tex/introduction.tex tex/planning.tex tex/systemAnalysis.tex tex/systemDesign.tex tex/implementation.tex tex/results.tex tex/appendixes.tex tex/conclusions.tex tex/glossary.tex tex/license.tex tex/biber.bib -diagramImgs = diagrams/planningWorkPlanEngine.png diagrams/planningWorkPlanGame.png diagrams/useCases.png diagrams/analysisClasses.png diagrams/useCase_generateAMove.png diagrams/useCase_useAsBackend.png diagrams/useCase_playAMatch.png diagrams/interfaces.png diagrams/gameModule.png diagrams/engineModule.png diagrams/trainingModule.png diagrams/modules.png diagrams/fullClasses.png +diagramImgs = diagrams/planningWorkPlanEngine.png diagrams/planningWorkPlanGame.png diagrams/useCases.png diagrams/analysisClasses.png diagrams/useCase_generateAMove.png diagrams/useCase_useAsBackend.png diagrams/useCase_trainTheEngine.png diagrams/useCase_playAMatch.png diagrams/interfaces.png diagrams/gameModule.png diagrams/engineModule.png diagrams/trainingModule.png diagrams/modules.png diagrams/fullClasses.png imgs = img/imago.jpg img/models/denseModel.png img/models/convModel.png diff --git a/doc/diagrams/useCase_trainTheEngine.puml b/doc/diagrams/useCase_trainTheEngine.puml new file mode 100644 index 0000000..5ae5e1e --- /dev/null +++ b/doc/diagrams/useCase_trainTheEngine.puml @@ -0,0 +1,24 @@ +@startuml + +!include skinparams.puml + +actor "Human user" as user + +boundary "Training CLI" as cli +control "Read files" as read +control "Create initial model" as initial +entity "Neural network" as nn +control "Train network" as train +boundary "Model file" as model + +user -> cli : provide input files +cli -> read +read -> initial +initial -> nn +loop until final epoch reached + train <- nn : expose to training + train -> nn : update +end +nn -> model : save neural network model + +@enduml diff --git a/doc/diagrams/useCases.puml b/doc/diagrams/useCases.puml index 8d4aa71..42cb471 100644 --- a/doc/diagrams/useCases.puml +++ b/doc/diagrams/useCases.puml @@ -9,10 +9,12 @@ actor "Human User" as user usecase "Play a match" as play usecase "Use as backend for machine player" as backend usecase "Generate a move" as genMove +usecase "Train the engine" as train player --> play gui --> backend user --> genMove gui --> genMove +user --> train @enduml diff --git a/doc/tex/appendixes.tex b/doc/tex/appendixes.tex index 89c413c..581f764 100644 --- a/doc/tex/appendixes.tex +++ b/doc/tex/appendixes.tex @@ -32,8 +32,6 @@ The costs are calculated based on a programmer salary of 20€/hour. } \end{table} -%TODO: Finish budgets - \subsubsection{Material resources} \begin{table}[h] @@ -49,3 +47,43 @@ The costs are calculated based on a programmer salary of 20€/hour. \end{table} \subsubsection{Totals} + +\begin{table}[h] + \makebox[\linewidth]{ + \begin{tabular}{l r} + \toprule + \textbf{Category} & \textbf{Cost (€)} \\ + \midrule + Work & 2500 \\ + \midrule + Materials & 600 \\ + \midrule + \textbf{Total} & \textbf{3100} \\ + \bottomrule + \end{tabular} + } +\end{table} + +\subsection{Budget for the client} + +\begin{table}[h] + \makebox[\linewidth]{ + \begin{tabular}{l r} + \toprule + \textbf{Task} & \textbf{Cost (€)} \\ + \midrule + Game preliminary research & 300 \\ + \midrule + Game implementation & 1100 \\ + \midrule + Game unit testing & 1000 \\ + \midrule + Game system testing & 100 \\ + \midrule + Materials & 600 \\ + \midrule + \textbf{Total} & \textbf{3100} \\ + \bottomrule + \end{tabular} + } +\end{table} diff --git a/doc/tex/biber.bib b/doc/tex/biber.bib index 324c0ad..a058686 100644 --- a/doc/tex/biber.bib +++ b/doc/tex/biber.bib @@ -54,6 +54,12 @@ url = {https://sabaki.yichuanshen.de} } +@online{tensorflow, + title = {TensorFlow}, + urldate = {2023}, + url = {https://www.tensorflow.org/} +} + @online{keras, title = {Keras: the Python deep learning API}, urldate = {2022}, @@ -117,6 +123,12 @@ url = {http://www.dabeaz.com/ply} } +@online{git, + title = {Git}, + urldate = {2023}, + url = {https://git-scm.com/} +} + @online{vim, title = {welcome home : vim online}, urldate = {2022}, @@ -147,6 +159,13 @@ url = {https://senseis.xmp.net/?9x9TengenOpenings} } +@book{choLifeAndDeath, + author = {Cho Chikun}, + title = {Cho Chikun Life and Death Dictionary}, + year = {1996}, + editor = {Nihon Ki-in (the Japanese Go Association)} +} + @online{ogsLifeAndDeath, author = {sunspark}, title = {Cho Chikun's Encyclopedia of Life and Death - Elementary: 1 / 900}, @@ -162,3 +181,10 @@ urldate = {2022}, url = {https://www.gnu.org/philosophy/free-sw.html} } + +@online{arch, + title = {Arch Linux}, + date = {2023}, + urldate = {2023}, + url = {https://archlinux.org/} +} diff --git a/doc/tex/conclusions.tex b/doc/tex/conclusions.tex index 16118e0..a6bc434 100644 --- a/doc/tex/conclusions.tex +++ b/doc/tex/conclusions.tex @@ -6,8 +6,8 @@ could be done in the future to improve the system are discussed here. \subsection{Problems with the Implementation of the Game} While Go has a simple set of rules, they lead to many borderline cases which -must be specifically addressed. For example, the \gls{ko} rule obliges to check the -previous board positions after each move so no boards are repeated. +must be specifically addressed. As an example, the \gls{ko} rule obliges to +check the previous board positions after each move so no boards are repeated. These problems have been solved by designing the tree of moves of a match so each move stores the full board layout after it has been played. This of course @@ -20,8 +20,8 @@ the best solution would require a deep analysis out of the scope of the project. \subsection{Problems with the Monte Carlo Tree Search Algorithm} -The implementation Monte Carlo Tree Search algorithm was not precise by itself -when asked for moves. This could be attributed to various factors: +The implemented Monte Carlo Tree Search algorithm was not precise by itself when +asked for moves. This could be attributed to various factors: \begin{itemize} @@ -31,7 +31,7 @@ when asked for moves. This could be attributed to various factors: are not precise on analyzing the result since the moves in them are played at random. - \item The configuration used, 5 explorations with 10 simulations each for + \item The configuration used, 5 explorations with 10 simulations each per cycle, does not allow for many good moves to be found. \end{itemize} @@ -48,7 +48,7 @@ on this section. \subsubsection{Finding the correct design} -When approaching neural networks design with a basic understanding of their +When approaching neural networks design with just basic understanding of their inner workings it is easy to get lost in all the different factors that can be tuned. The number and variant of layers, their number of nodes, their activation function, the learning rate; they can all be tweaked and doing so in the favour @@ -93,7 +93,8 @@ make reasonable decisions at the start of the game, with some interesting variations. But as the game progresses their moves make less sense. This could be because they have seen many examples of starting moves but complications at the middle and end game are much more diverse and the networks had not been -trained on similar situations. +trained on similar situations. These stages of the game are probably where a +strong Monte Carlo Tree Search algorithm would help the most. \subsection{Viable Extensions} @@ -109,8 +110,8 @@ simpler. The logical next step would be to join both: the network could select the best moves to be made, and the tree search algorithm could explore them to select the best and grow its explored tree in meaningful directions. -This is also what was done by AlphaGo. By seeing the results obtained in this -project it makes sense they made that decision. +This is also what was done by AlphaGo. Going by the results obtained in this +project it makes sense they went with that design. \subsubsection{Train the Engine by Itself} diff --git a/doc/tex/glossary.tex b/doc/tex/glossary.tex index b1cea02..d14dc02 100644 --- a/doc/tex/glossary.tex +++ b/doc/tex/glossary.tex @@ -22,8 +22,8 @@ \newglossaryentry{suicide} { name=suicide move, - description={A typically prohibited move which would cause the player's - group to have zero liberties} + description={A typically prohibited move which doesn't capture any stone + and results in the player's group having zero liberties} } \newglossaryentry{tsumego} @@ -32,5 +32,22 @@ description={A Go problem} } +\newglossaryentry{criticalPoint} +{ + name=critical point, + description={A position on the board which makes a group either live or die + depending on who plays there first} +} + +\newglossaryentry{eye} +{ + name=eye, + description={An area surrounded by a group. If a group has at least two eyes + it can never be captured, since the opponent would have to play in all the + eyes simultaneously to capture it} +} + +\newacronym{ai}{AI}{Artificial Intelligence} \newacronym{sgf}{SGF}{Smart Game Format} \newacronym{gtp}{GTP}{Go Text Protocol} +\newacronym{ast}{AST}{Annotated Syntax Tree} diff --git a/doc/tex/imago.tex b/doc/tex/imago.tex index 0447d70..f8e3101 100644 --- a/doc/tex/imago.tex +++ b/doc/tex/imago.tex @@ -151,8 +151,8 @@ inclusion of to use this template is included here. \clearpage -%\input{tex/introduction.tex} -%\clearpage +\input{tex/introduction.tex} +\clearpage \input{tex/planning.tex} \clearpage @@ -166,16 +166,29 @@ inclusion of to use this template is included here. \input{tex/implementation.tex} \clearpage -%\input{tex/results.tex} -%\clearpage +\input{tex/results.tex} +\clearpage + +\input{tex/appendixes.tex} +\clearpage + +\input{tex/conclusions.tex} +\clearpage + +% Glossary and acronyms -%\input{tex/appendixes.tex} -%\clearpage +\printglossary[title=Glossary of Go Terminology] +\makeatletter +\def\@currentlabelname{\@glotype@main@title} +\label{glossary} +\makeatother -%\input{tex/conclusions.tex} -%\clearpage +\printglossary[type=\acronymtype] +\makeatletter +\def\@currentlabelname{\@glotype@acronymtype@title} +\label{acronyms} +\makeatother -\printglossaries \clearpage % References (bibliography) diff --git a/doc/tex/implementation.tex b/doc/tex/implementation.tex index f89e54c..73e4cfd 100644 --- a/doc/tex/implementation.tex +++ b/doc/tex/implementation.tex @@ -1,8 +1,11 @@ \section{Implementation} +This section is about the specific tools and process used to implement +\program{}. + \subsection{Development Hardware} -The development and evaluation machine is a Satellite L50-C laptop. Its +The development and evaluation machine is a Toshiba Satellite L50-C laptop. Its specifications are shown as a list for readability. \begin{itemize} @@ -15,9 +18,9 @@ specifications are shown as a list for readability. \subsection{Development Software} The tools selected for the development of the project and the documentation are -listed and explained on this section. All the tools used are either -free \cite{fsf_free} or open source software. The development machine runs 64 -bits Arch Linux as its operating system. +listed and explained on this section. All the tools used are either free +\cite{fsf_free} or open source software. The development machine runs 64 bits +Arch Linux \cite{arch} as its operating system. \subsubsection{Language} @@ -28,58 +31,58 @@ allows easy use of the Keras library for implementing neural networks. Various Python libraries have been used to easy the development process or assist in the analysis of results. These are: -\paragraph{Keras/Tensorflow \cite{keras}} +\paragraph{Keras/TensorFlow} -Tensorflow is a platform for machine learning which provides a diverse range of -tools, one of which is a Python library for machine learning. +TensorFlow \cite{tensorflow} is a platform for machine learning which provides a +diverse range of tools, one of which is a Python library for machine learning. -Keras is a high-level API for Tensorflow allowing for the easy definition of -neural networks. It permits easily testing and comparing different network -layouts. +Keras \cite{keras} is a high-level API for TensorFlow allowing for the easy +definition of neural networks. It permits easily testing and comparing different +network layouts. -\paragraph{NumPy \cite{numpy}} +\paragraph{NumPy} -A scientific package for Python providing a lot of mathematical tools. The most -interesting for this project are its capabilities to create and transform -matrices. +A scientific package for Python providing a lot of mathematical tools +\cite{numpy}. The most interesting for this project are its capabilities to +create and transform matrices. -\paragraph{Matplotlib \cite{matplotlib}} +\paragraph{Matplotlib} -A Python library for creating graphs and other visualizations. It is used to -show the likelihood of moves the neural networks of the project create from a -board configuration. +A Python library for creating graphs and other visualizations \cite{matplotlib}. +It is used to show the likelihood of moves the neural networks of the project +create from a board configuration. -\paragraph{PLY \cite{ply}} +\paragraph{PLY} -A tool for generating compilers in Python. It is an implementation of the lex -and yacc utilities, allowing to create lexers and parsers. It is used in the -project to create the \acrshort{sgf} parser which transform \acrshort{sgf} files to internal -representations of Go matches. +A tool for generating compilers in Python \cite{ply}. It is an implementation of +the lex and yacc utilities, allowing to create lexers and parsers. It is used in +the project to create the \acrshort{sgf} parser which transform \acrshort{sgf} +files to internal representations of Go matches. \paragraph{Other utility libraries} -These are some utility libraries commonly used for frequent programming tasks: +These are some utility libraries commonly used for frequent programming tasks. \begin{itemize} - \item \textbf{sys}: to stop the execution of the program or access system info such + \item \textbf{sys}: To stop the execution of the program or access system info such as primitives maximum values. - \item \textbf{os}: to interact with files. - \item \textbf{re}: to check strings with regular expressions. - \item \textbf{random}: to get random values, for example to obtain a random - item from a list. - \item \textbf{copy}: to obtain deep copies of multidimensional arrays. + \item \textbf{os}: To interact with files. + \item \textbf{re}: To check strings with regular expressions. + \item \textbf{random}: For randomness, for example to obtain a random item + from a list. + \item \textbf{copy}: To obtain deep copies of multidimensional arrays. \end{itemize} \subsubsection{Development Tools} -\paragraph{Neovim \cite{neovim}} +\paragraph{Neovim} A text editor based on Vim \cite{vim}, providing its same functionality with -useful additions and defaults for modern computers and terminal emulators. With -some extensions and configuration it can become a powerful development -environment with a very fluid usage experience. That, and the fact that the -developer considers Vim's modal editing the best writing experience possible on -a computer, have made Neovim the editor of choice. +useful additions and defaults for modern computers and terminal emulators +\cite{neovim}. With some extensions and configuration it becomes a powerful +development environment with a very fluid usage experience. That, and the +preference of the developer of Vim's modal editing as the best writing +experience possible on a computer, have made Neovim the editor of choice. %TODO: Write about neovim extensions %\begin{itemize} @@ -88,21 +91,28 @@ a computer, have made Neovim the editor of choice. % %\end{itemize} +\paragraph{Git} + +A version control tool widely used in software development environments +\cite{git}. If the reader is not already familiar with it, it suffices to say it +allows to store and manage snapshots of a project, navigate through them and +diverge into different development branches, among other useful features. + \subsubsection{Documentation Tools} -\paragraph{\LaTeX \cite{latex}} +\paragraph{\LaTeX} -A typesetting system widely used in the investigation field, among others. It -allows for documentation like this text to be written in plain text and then -compiled to PDF or other formats, which permits keeping the source files of the -documentation small and organized plus other benefits of plain text such as -being able to use version control. +A typesetting system widely used in the investigation field, among others +\cite{latex}. It allows for documentation like this text to be written in plain +text and then compiled to PDF or other formats, which permits keeping the source +files of the documentation small and organized plus other benefits of plain text +such as being able to be used in conjunction with version control tools. -\paragraph{PlantUML \cite{puml}} +\paragraph{PlantUML} -A program which creates diagrams from plain text files. PlantUML supports syntax -for many different sorts of diagrams, mainly but not only UML. It has been used -to generate the diagrams used in this text. +A program which creates diagrams from plain text files \cite{puml}. PlantUML +supports syntax for many different sorts of diagrams, mainly but not only UML. +It has been used to generate the diagrams used in this text. \paragraph{Make} @@ -115,7 +125,137 @@ It has been used to generate this text from \LaTeX{} and PlantUML source files. The contents of the Makefile with which this document has been compiled are shown in \lref{code:makefile}. -\begin{listing}[h] +\begin{listing}[p] \inputminted{make}{Makefile} \caption{Documentation Makefile}\label{code:makefile} \end{listing} + +\subsection{Execution of the Testing Plan} + +Part of the implementation process is to execute the testing plan. The results +of this execution are provided in this section. + +\subsubsection{Execution of the Unitary Testing} + +The script used to run the tests is shown on \lref{lst:test} and its output on +\lref{lst:testOutput}. + +\begin{listing}[p] + \inputminted{bash}{listings/test.sh} + \caption{Script to run the tests and list the result.} + \label{lst:test} +\end{listing} + +\begin{listing}[p] + \inputminted[fontsize=\footnotesize]{text}{listings/testOutput.txt} + \caption{Unitary testing output.} + \label{lst:testOutput} +\end{listing} + +\subsubsection{Execution of the Integration Testing} + +\vspace{\interclassSpace} + +\begin{tabular}{p{0.2\linewidth}p{0.3\linewidth}p{0.4\linewidth}} + \toprule + \multicolumn{3}{c}{\textbf{Engine and Game modules}} \\ + \midrule + \textbf{Test} & \textbf{Expected behaviour} & \textbf{Results} \\ + \midrule + The GTP interface of the engine is used to play a match & + The module handles the game and can show its state. & + The engine runs correctly and is capable of keeping track of and showing the + state of a match, which shows it is making good use of the Game module. \\ + \bottomrule +\end{tabular} + +\vspace{\interclassSpace} + +\begin{tabular}{p{0.2\linewidth}p{0.3\linewidth}p{0.4\linewidth}} + \toprule + \multicolumn{3}{c}{\textbf{Training and Engine module}} \\ + \midrule + \textbf{Test} & \textbf{Expected behaviour} & \textbf{Results} \\ + \midrule + The training process is started & + The training uses the network defined on the Engine module. & + The training output shows the network in training follows the design defined + on the Engine module. \\ + \bottomrule +\end{tabular} + +\subsubsection{Execution of the System Testing} + +\vspace{\interclassSpace} + +\begin{tabular}{p{0.2\linewidth}p{0.3\linewidth}p{0.4\linewidth}} + \toprule + \multicolumn{3}{c}{\textbf{Game interface}} \\ + \midrule + \textbf{Test} & \textbf{Expected behaviour} & \textbf{Results} \\ + \midrule + Play a game of Go with no engine & + The game can be played until the end. & + The interface continues asking for moves until both players pass + consecutively, which ends the game and the execution. \\ + \midrule + Provide a wrong move & + The interface shows it is wrong and the game continues without a change of + state. & + As expected, the interface shows a message signaling that the move is wrong + and showing an example of a move. \\ + \midrule + Close the game & + The interface closes. & + A message is shown signaling that the game is ending and the engine closes. + \\ + \bottomrule +\end{tabular} + +\vspace{\interclassSpace} + +\begin{tabular}{p{0.2\linewidth}p{0.3\linewidth}p{0.4\linewidth}} + \toprule + \multicolumn{3}{c}{\textbf{Engine interface}} \\ + \midrule + \textbf{Test} & \textbf{Expected behaviour} & \textbf{Results} \\ + \midrule + Ask for the available commands & + The interface outputs the available commands. & + The list of available commands is printed one per line. \\ + \midrule + Provide a move & + The state of the engine updates with the new move. & + No output is given, but after providing the \texttt{showboard} command it is + shown that the move has been registered. \\ + \midrule + Ask for a move & + The engine suggests a move without changing the state of the current game. & + After the necessary time for generating the move, it is printed. The + \texttt{showboard} is then used to check the state of the game has not + changed. \\ + \bottomrule +\end{tabular} + +\vspace{\interclassSpace} + +\begin{tabular}{p{0.2\linewidth}p{0.3\linewidth}p{0.4\linewidth}} + \toprule + \multicolumn{3}{c}{\textbf{Training interface}} \\ + \midrule + \textbf{Test} & \textbf{Expected behaviour} & \textbf{Results} \\ + \midrule + Provide some games to train on & + A neural network model is created. & + First, all the training files used are printed, and then, as expected, the + training process commences. A model is created or updated in the + \texttt{models} folder. \\ + \midrule + Start the training without providing games & + An error message is shown and the execution terminated. & + As expected, a message is shown asking for SGF files to be provided as + arguments and the execution is terminated.\\ + \bottomrule +\end{tabular} + +\subsubsection{Usability Testing} diff --git a/doc/tex/introduction.tex b/doc/tex/introduction.tex index 8b5430c..22dd98a 100644 --- a/doc/tex/introduction.tex +++ b/doc/tex/introduction.tex @@ -27,7 +27,7 @@ As one of the deepest and most studied games in the world, Go presents a very interesting problem for artificial intelligence. Implementing not only the game's simple but subtle rules, but a system capable of playing it with a satisfying level of skill, is a task worth of pursuing as an exercise on -software design, algorithmics and AI research. +software design, algorithmics and \acrfull{ai} research. On the practical level, this project can be a foundation for the development of different Go analysis algorithms by providing an existing engine to house them, @@ -43,7 +43,43 @@ Presented here are the ideal targets of the project. game's rules. \item An engine capable of analyzing board positions and generating strong moves via various decision algorithms. - \item Compatibility with existing GUIs. + \item An interface compatible with existing GUIs. \item A way for processing existing records of games, which are usually - recorded in the \acrshort{sgf} format. + recorded in the \acrfull{sgf}. +\end{itemize} + +\subsection{Rules of Go} + +Some understanding of the basics of the game is necessary to process this +document. Luckily for the reader, the rules of Go are pretty simple. If the +reader prefers, there is an interactive tutorial at +\texttt{https://online-go.com/learn-to-play-go/} going over the fundamentals and +introducing basic strategy for managing the stones which is already useful and +needed for the first games. Either way, the rules are sumarized as follows: + +\begin{itemize} + +\item There are two players. One plays as black, the other as white. Black plays + first. + +\item The player with the biggest score when the game ends wins. The score + consists of surrounded territory and captured enemy stones. Surrounded + territory is defined as the areas of empty space connected orthogonally + only to stones of one color. Each empty space on a surrounded area and each + captured enemy stone score one point. + +\item As their turn, a player can either place a stone of their color in an + empty space of the board or pass. The game ends when both players pass + consecutively. + +\item Stones of the same color orthogonally adjacent to one another are + considered connected. When one group of connected stones has no more + orthogonally adjacent empty spaces it is considered as captured and its + stones are removed from the board. + +\item Additionally, to prevent endlessly repeating plays, it is forbidden to + make a move which resets the board to the previous position. This is called + the \Gls{ko} rule, is of strategic relevance outside the scope of a basic + introduction to the game, and doesn't always come up. + \end{itemize} diff --git a/doc/tex/planning.tex b/doc/tex/planning.tex index 17d413f..4057268 100644 --- a/doc/tex/planning.tex +++ b/doc/tex/planning.tex @@ -12,7 +12,8 @@ components and needs. The rules of the game must be implemented, ideally in a way they can be tested by direct human play. This system will at its bare minimum represent the -Japanese Go rules (area scoring, no \gls{superko} rule, no \gls{suicide} moves). +Japanese Go rules (area scoring, no \gls{superko} rule, no \gls{suicide} moves. +These terms are explained in the \nameref{glossary} of this document). \subsubsection{Engine Implementation} @@ -70,6 +71,9 @@ hours a day on weekdays this amounts to 375 hours. \subsection{Previous Works} +This section lists and describes the existing projects which inspired and can be +of use in the development of \program{}. + \subsubsection{Existing Engines} \begin{figure}[h] @@ -82,19 +86,19 @@ hours a day on weekdays this amounts to 375 hours. \end{center} \end{figure} -\paragraph{AlphaGo \cite{alphaGo}} +\paragraph{AlphaGo} -A Go play and analysis engine developed by DeepMind Technologies, a company -owned by Google. It revolutionized the world of Go in 2015 and 2016 when it -respectively became the first AI to win against a professional Go player and -then won against Lee Sedol, a Korean player of the highest professional rank and -one of the strongest players in the world at the time. Its source code is -closed, but a paper written by the team has been published on Nature -\cite{natureAlphaGo2016}. The logo of the project is shown on -\fref{fig:alphaGoLogo}. +A Go play and analysis engine \cite{alphaGo} developed by DeepMind Technologies, +a company owned by Google. It revolutionized the world of Go in 2015 and 2016 +when it respectively became the first \acrshort{ai} to win against a +professional Go player and then won against Lee Sedol, a Korean player of the +highest professional rank and one of the strongest players in the world at the +time. Its source code is closed, but a paper written by the team has been +published on Nature \cite{natureAlphaGo2016}. The logo of the project is shown +on \fref{fig:alphaGoLogo}. -The unprecedented success of AlphaGo served as inspiration for many AI projects, -including this one. +The unprecedented success of AlphaGo served as inspiration for many +\acrshort{ai} projects, including this one. \begin{figure}[h] \begin{center} @@ -105,12 +109,12 @@ including this one. \end{center} \end{figure} -\paragraph{KataGo \cite{katago}} +\paragraph{KataGo} -An open source project based on the AlphaGo paper that also achieved superhuman -strength of play. The availability of its implementation and documentation -presents a great resource for this project. The logo of the project is shown on -\fref{fig:kataGoLogo}. +An open source project \cite{katago} based on the AlphaGo paper that also +achieved superhuman strength of play. The availability of its implementation and +documentation presents a great resource for this project. The logo of the +project is shown on \fref{fig:kataGoLogo}. \begin{figure}[h] \begin{center} @@ -121,22 +125,22 @@ presents a great resource for this project. The logo of the project is shown on \end{center} \end{figure} -\paragraph{GnuGo \cite{gnugo}} +\paragraph{GnuGo} -A software capable of playing Go and part of the GNU project. Although not a -strong engine anymore, it is interesting for historic reasons as the free -software engine for which the \acrfull{gtp} was first defined. The logo of the -project is shown on \fref{fig:gnuGoLogo}. +A software \cite{gnugo} capable of playing Go and part of the GNU project. +Although not a strong engine anymore, it is interesting for historic reasons as +the free software engine for which the \acrfull{gtp} was first defined. The logo +of the project is shown on \fref{fig:gnuGoLogo}. \subsubsection{Existing Standards} -\paragraph{\acrshort{gtp} \cite{gtp}} +\paragraph{\acrshort{gtp}} -The \acrfull{gtp} is a text based protocol for communication with computer Go -programs. It is the protocol used by GNU Go and the more modern and powerful -KataGo. By supporting \acrshort{gtp} the engine developed for this project can -be used with existing GUIs and other programs, making it easier to be used with -the tools target users are already familiar with. +The \acrfull{gtp} \cite{gtp} is a text based protocol for communication with +computer Go programs. It is the protocol used by GNU Go and the more modern and +powerful KataGo. By supporting \acrshort{gtp} the engine developed for this +project can be used with existing GUIs and other programs, making it easier to +be used with the tools target users are already familiar with. %TODO %\begin{listing}[h] @@ -145,16 +149,16 @@ the tools target users are already familiar with. % \label{lst:gtpExample} %\end{listing} -\paragraph{\acrshort{sgf} \cite{sgf}} +\paragraph{\acrshort{sgf}} -The \acrfull{sgf} is a text format widely used for storing records of Go matches -which allows for variants, comments and other metadata. It was devised for Go -but it supports other games with similar turn-based structure. Many popular -playing tools use it. By supporting \acrshort{sgf} vast existing collections of -games, such as those played on online Go servers, can be used to train the -decision algorithms based on neural networks. An example of a \acrshort{sgf} -file can be seen on \lref{lst:sgfExample}. The game state described in this file -is shown visually in \fref{fig:sgfExample}. +The \acrfull{sgf} \cite{sgf} is a text format widely used for storing records of +Go matches which allows for variants, comments and other metadata. It was +devised for Go but it supports other games with similar turn-based structure. +Many popular playing tools use it. By supporting \acrshort{sgf} vast existing +collections of games, such as those played on online Go servers, can be used to +train the decision algorithms based on neural networks. An example of a +\acrshort{sgf} file can be seen on \lref{lst:sgfExample}. The game state +described in this file is shown visually in \fref{fig:sgfExample}. \begin{listing}[h] \inputminted[breakafter=\]]{text}{listings/sgfExample.sgf} @@ -168,9 +172,9 @@ is shown visually in \fref{fig:sgfExample}. \begin{center} \includegraphics[width=0.5\textwidth]{img/sgfExample.png} \caption{Screenshot of Sabaki showing the \gls{tsumego} described in the - \acrshort{sgf} example from \lref{lst:sgfExample}. Note that Sabaki - marks the two continuations described in the \acrshort{sgf} file with - two transparent grey dots. + \acrshort{sgf} example from \lref{lst:sgfExample}. Note that the two + continuations described in the \acrshort{sgf} file are marked with two + transparent grey dots. }\label{fig:sgfExample} \end{center} \end{figure} @@ -184,12 +188,12 @@ is shown visually in \fref{fig:sgfExample}. \end{center} \end{figure} -\subsubsection{Sabaki \cite{sabaki}} +\subsubsection{Sabaki} -Sabaki is a Go board software compatible with \acrshort{gtp} engines. It can -serve as a GUI for the engine developed in this project and as an example of the -advantages of following a standardized protocol. Part of its graphical interface -is shown on \fref{fig:sabaki}. +Sabaki \cite{sabaki} is a Go board software compatible with \acrshort{gtp} +engines. It can serve as a GUI for the engine developed in this project and as +an example of the advantages of following a standardized protocol. Part of its +graphical interface is shown on \fref{fig:sabaki}. \begin{figure}[h] \begin{center} @@ -200,14 +204,24 @@ is shown on \fref{fig:sabaki}. \end{center} \end{figure} -\subsubsection{Keras \cite{keras}} +\subsubsection{Keras} + +Keras \cite{keras} is a deep learning API for Python allowing for the high-level +definition of neural networks. This permits easily testing and comparing +different network layouts. The logo of the project is shown on +\fref{fig:kerasLogo}. -Keras is a deep learning API for Python allowing for the high-level definition -of neural networks. This permits easily testing and comparing different network -layouts. The logo of the project is shown on \fref{fig:kerasLogo}. +\subsubsection{PLY} + +PLY \cite{ply} is a Python implementation of the compiler construction tools lex +and yacc. It will be useful for implementing the \acrshort{sgf} parser needed to +process records of Go matches. \subsection{Technological Infrastructure} +This section establishes the technological needs of the project and proposes +solutions to them. + \subsubsection{Programming Language}\label{sec:programmingLanguage} The resulting product of this project will be one or more pieces of software @@ -217,11 +231,11 @@ choice is Python \cite{python}, for various reasons: \begin{itemize} \item It has established a reputation on scientific fields and more - specifically on AI research and development. + specifically on \acrshort{ai} research and development. \item Interpreters are available for many platforms, which allows the most people possible to access the product. \item Although not very deeply, it has been used by the developer student - during its degree including in AI and game theory contexts. + during its degree including in \acrshort{ai} and game theory contexts. \end{itemize} diff --git a/doc/tex/results.tex b/doc/tex/results.tex index fac2df9..e166873 100644 --- a/doc/tex/results.tex +++ b/doc/tex/results.tex @@ -30,13 +30,16 @@ \section{Results} +This section shows an analysis of the successes and failures of the final +product and the strength of play of its various algorithms. + \subsection{Monte Carlo Tree Search Evaluation} The Monte Carlo Algorithm tries to explore the tree of possibilities as efficiently as possible. With this approach, it can be expected to fail when -alone on a problem such big as the game of Go. Nonetheless, there are some areas -where it can be useful. It will be evaluated by its capabilities while playing -games but also when presented with Go problems. +alone on a problem such as big as the game of Go. Nonetheless, there are some +areas where it can be useful. It will be evaluated by its capabilities while +playing games but also when presented with Go problems. The Monte Carlo algorithm has been set to do 5 explorations with 10 simulations each when it is asked for a move. In the hardware used this makes it think for @@ -57,7 +60,8 @@ Some moves could be debated as sensible, but many of them are on the second or fist line, which so early on the game are the two worst places to play at. It seems clear that this algorithm, at least with this specific configuration, -can't handle the size of an empty Go board, even on the 9x9 size. +can't handle the tree of possibilities of an empty Go board even at the 9x9 +size. A record of the game is shown in \fref{fig:mctsVSmcts}. @@ -72,56 +76,63 @@ A record of the game is shown in \fref{fig:mctsVSmcts}. Since tree exploration on smaller boards or advanced games with little empty spaces should be easier the algorithm has also been tested on some Go problems. -A Go problem or \gls{tsumego} is a predefined layout of the board, or of a section of -the board, for which the player must find some beneficial move. Life and death -problems are a subset of \gls{tsumego}s in which the survival of a group depends on -finding the correct sequence to save or kill the group. One collection of such -\gls{tsumego}s is \textit{Cho Chikun's Encyclopedia of Life and Death}, part of which -are available on OGS \cite{ogsLifeAndDeath}, an online Go server. +A Go problem or \gls{tsumego} is a predefined layout of the board, or of a +section of the board, for which the player must find some beneficial move. Life +and death problems are a subset of \gls{tsumego}s in which the aim is to find +the correct sequence to save or kill a group. One collection of such +\gls{tsumego}s is \textit{Cho Chikun Life and Death Dictionary} +\cite{choLifeAndDeath}, part of which are available on OGS +\cite{ogsLifeAndDeath}, an online Go server. The first of these problems and what the algorithm suggested as moves is shown in \fref{fig:mctsProblem01}. Black makes the first move, which means the solution is to find some favorable outcome for black, which in this case is killing the white group. The white -group has a critical point on B1. If white plays on B1 they make two eyes and -live, but if black plays there first white can't make two eyes and dies, so B1 -is the solution to the \gls{tsumego}. This is one of the easiest Go problems. +group has a \gls{criticalPoint} on B1. If white plays on B1 they make two +\glspl{eye} and live, but if black plays there first white can't make two +\glspl{eye} and dies, so B1 is the solution to the \gls{tsumego}. This is one of +the easiest Go problems. The algorithm neglects this solution. While asked five times to generate a move for the starting position it suggested B1 only once. But notably, after making another move, it consistently suggested B1 for white, -which is the solution now that white has to play. So in the end it was able to -solve the \gls{tsumego}, probably because after making a move it had already explored -part of the tree but it was difficult that it explored the solution for the -first move. +which is now the correct move for white. So in the end it was able to solve the +\gls{tsumego}, probably because after making a move it had already explored part +of the tree but it was difficult that it explored the solution for the first +move, that is, in its first exploration cycle. -The engine was tested against other \gls{tsumego}s but it was not able to solve them, -so no more are shown here. +The engine was tested against other \gls{tsumego}s but it was not able to solve +them, so no more are shown here. This was the one simple enough for an engine of +this characteristics and running on the testing hardware to solve. \subsection{Neural Network Training} -Each network has been training by receiving batches of \acrshort{sgf} files which are first -converted to lists of moves by the Training System and then provided to the -train function of the networks. This has been executed with the following -command: +Each network has been training by receiving batches of \acrshort{sgf} files +which are first converted to lists of moves by the Training System and then +provided to the train function of the networks. This has been executed with the +following command: \inputminted[fontsize=\small]{bash}{listings/trainCommand.sh} -Which lists the contents of a folder containing multiple \acrshort{sgf} files, shuffles the -list, takes some of them and passes them to train.py as arguments. The combined -list of game positions and moves made from them in all the games selected make -up the sample of the training. The number of files selected can of course be -adapted to the situation. +Which lists the contents of a folder containing multiple \acrshort{sgf} files, +shuffles the list, takes some of them and passes them to train.py as arguments. +The combined list of game positions and moves made from them in all the games +selected make up the sample of the training. The number of files selected can of +course be adapted to the situation. The networks were configured to train for 20 epochs, that is, processing the full batch and updating the weights on the network 20 times. 10\% of the sample -were used as validation. This means they were not used for training but to -check the accuracy and loss of the network after training with the rest of -the batch. This technique of validation can help detect overfitting, which -happens when a network does a very good job on the population it has been -trained on but it fails when receiving unseen data. +was used as validation. This means they were not used for training but to check +the accuracy and loss of the network after training with the rest of the batch. +This technique of validation can help detect overfitting, which happens when a +network does a very good job on the population it has been trained on but it +fails when receiving unseen data. + +The outputs from this training process can be seen on \lref{code:denseTraining} +and \lref{code:convTraining} for the dense network and the convolutional +network respectively. The training shows loss decrementing and accuracy incrementing on each epoch for both training and validation samples, which suggests there is learning happening @@ -134,10 +145,6 @@ as with batches of 10 games an epoch of training could be completed in one minute, in three minutes batches of 20 games, and in up to an hour with batches of 100 games. -The outputs from this training process can be seen on \lref{code:denseTraining} -and \lref{code:convTraining} for the dense network and the convolutional -network respectively. - \begin{listing}[h] \inputminted[fontsize=\scriptsize]{text}{listings/denseTraining.txt} \caption{Dense neural network training log.} @@ -163,8 +170,8 @@ of predicting human moves through the training epochs. Another way is to make the network play either against itself, another network, or a human player, and analyze the score it provides to each possible play. The -output of the network is a vector with the likelihood of making each move, so -we can take this values as how strongly the engine suggests each of them. +output of the network is a vector with the likelihood of making each move, so we +can take these values as how strongly the engine suggests each of them. \subsection{Neural Networks Against Themselves} @@ -185,10 +192,10 @@ move), can be seen on Figs.~\ref{fig:denseVSdense01}, \ref{fig:denseVSdense02}, The dense network starts on the center of the board, which is one of the standard openings in the 9x9 board. It starts on a very good track, but we must acknowledge that the empty board is a position present on every Go match it has -trained on and so it should know it well. It probably means the center was the -most played opening in the sample. It is interesting to check the heatmap of -this move, since the selected move has only a score of 0.27. Other common -openings on the 9x9 are represented on the vertices with some significant score. +trained on, so it should know it well. It probably means the center was the most +played opening in the sample. It is interesting to check the heatmap of this +move, since the selected move has only a score of 0.27. Other common openings on +the 9x9 are represented on the vertices with some significant score. The heatmap of the response to the center play is also interesting. The four highest scored moves are equivalent, since the first move has not broken any @@ -201,12 +208,13 @@ second black move is suggested with more likelihood than the previous one. This is also a very common sequence of play. The following moves are a white attack followed by black's sensible response, -and a strong white extension (arguably to the wrong side, but the minutia of why +and a strong white extension (arguably to the wrong side, but the details of why the other side would be better is probably not grasped by the engine). The -following moves could be called looking for complication, which is a strategy. +following moves could be classified as ``looking for complication'', which is a +strategy. -Overall some moves could be criticised but the engine is showing at least a -basic knowledge of the game's strategy. +Overall some moves can be criticised but the engine is showing at least a basic +knowledge of the game's strategy. \subsubsection{Convolutional Network Against Itself} @@ -219,10 +227,10 @@ more certain of the move than the dense network. Its next two plays are also the same, but the match starts to diverge from the fourth move. Instead of attacking the black stone on the side, white plays directly under at -B5, keeping the edge of symmetry. This is strategically important because while -symmetry is present playing on either side makes no difference, while playing -after the opponent has broken symmetry gives the opportunity to respond and -capitalize on the now better side to play. +B5, keeping the edge of symmetry. This is strategically important because as +long as symmetry is present playing on either side makes no difference, while +playing after the opponent has broken symmetry gives the opportunity to respond +and capitalize on the now better side to play. Black responds to the white stone by attacking it at B4 and white then extends to the more open side with F7. @@ -233,7 +241,7 @@ there states that white should have extended to the other side. The following moves are some sensible continuations, with approaches and responses on each side. They are probably not the best moves, but the idea -behind them can be read from the board. +behind them can be grasped from the board. \subsubsection{Dense Network Against Convolutional} diff --git a/doc/tex/systemAnalysis.tex b/doc/tex/systemAnalysis.tex index 5263d1f..811a8f3 100644 --- a/doc/tex/systemAnalysis.tex +++ b/doc/tex/systemAnalysis.tex @@ -1,5 +1,9 @@ \section{System Analysis} +This section provides a preliminary analysis of the needs of the project, such +as its requirements, components and use cases. It also includes the +specification of the testing plan. + \subsection{System Reach Determination} These are the main goals the final product must reach. @@ -12,8 +16,8 @@ These are the main goals the final product must reach. \item A library for representing the game of Go. It can be used for the decision algorithms to keep the state of the game and can also be used - in an interactive application for a user to play the game and test the - code. + in an interactive application for a human user to play the game and test + the code. \item An engine program as a way of presenting an interface for using these algorithms. The engine will use the \acrshort{gtp} so it can be used with an @@ -50,7 +54,7 @@ requisites needed for the system. coordinates of the vertex to play on or as ``pass''. \begin{enumerate} \item The text introduced for the move must follow the - regular expression \texttt{([A-Z][0-9]+|pass)} + regular expression \texttt{([A-HJ-Z][0-9]+)|(pass)} \item If the move is not valid it must be notified to the user and another move asked for. \end{enumerate} @@ -149,8 +153,8 @@ requisites needed for the system. \item The trainer can import existing games. \begin{enumerate} \item Records of games stored as \acrshort{sgf} can be imported. - \item Files containing records of games are provided as arguments to - the trainer. + \item Files containing records of games can be provided as arguments + to the trainer. \end{enumerate} \end{enumerate} @@ -178,13 +182,13 @@ requisites needed for the system. \begin{enumerate} \item For understanding the workings of the application the user needs to be - familiar with the basics of the game of Go. + familiar with the rules of the game of Go. \item For directly using the engine the user needs to be familiar with command line interfaces. - \item For directly using the trainer the user needs to know the different - network models available. + \item For directly using the trainer the user needs to know about the + different network models available. \end{enumerate} @@ -194,7 +198,7 @@ requisites needed for the system. \begin{enumerate} - \item The game program will be a Python file able to be executed by the + \item The game program will be a Python script able to be executed by the Python interpreter. \item The game program will make use of standard input and standard output @@ -205,7 +209,7 @@ requisites needed for the system. \item Standard output will be used for messages directed to the user. \end{enumerate} - \item The engine program will be a Python file able to be executed by the + \item The engine program will be a Python script able to be executed by the Python interpreter. \item The engine program will make use of standard input and standard output @@ -216,11 +220,11 @@ requisites needed for the system. commands. \end{enumerate} - \item The trainer program will be a Python file able to be executed by the + \item The trainer program will be a Python script able to be executed by the Python interpreter. - \item The engine program will make use of standard input and standard output - for communication. + \item The trainer program will make use of standard input and standard + output for communication. \begin{enumerate} \item Standard input will be used for reading commands. \item Standard output will be used for showing the result of @@ -289,12 +293,15 @@ internal representation of a game resulting from the processing of an \subsection{Class Analysis} +This section describes the needed classes expected as a result of the analysis +of the project. + \subsubsection{Class Diagram} The classes resulting from the analysis phase are shown in \fref{fig:analysisClasses}. -\begin{figure}[h] +\begin{figure}[p] \begin{center} \includegraphics[width=\textwidth]{diagrams/analysisClasses.png} \caption{General classes obtained from the analysis @@ -716,13 +723,14 @@ non-human. \item The human player who interacts with the playing interface. \item The human user who interacts with the engine. + \item The human user who starts the training. \item A GUI software which uses the engine to generate moves. \end{itemize} \subsection{Use Cases} -\begin{figure}[h] +\begin{figure}[p] \begin{center} \includegraphics[width=\textwidth]{diagrams/useCases.png} \caption{Use cases.} @@ -738,20 +746,25 @@ use case is explained next. The game interface reads the moves presented by the player and shows their result on the board. +\paragraph{Generate a move} + +The engine interface reads the input for generating a move as stated by the +\acrshort{gtp} protocol and outputs the coordinates of the board to play. + +\paragraph{Train the engine} + +The training system is started to process matches stored as \acrshort{sgf} files +and to train a neural network. + \paragraph{Use as backend for machine player} The engine is used as the backend for generating moves for a machine player, this is, for automated play, either against a human who is using the GUI or against another machine player. -\paragraph{Generate a move} - -The engine interface reads the input for generating a move as stated by the -\acrshort{gtp} protocol and outputs the coordinates of the board to play. - \subsection{Use Case Analysis and Scenarios} -\begin{figure}[h] +\begin{figure}[p] \begin{center} \includegraphics[width=0.8\textwidth]{diagrams/useCase_playAMatch.png} \caption{Use case: Play a match.} @@ -759,6 +772,8 @@ The engine interface reads the input for generating a move as stated by the \end{center} \end{figure} +\vspace{\interclassSpace} + \begin{tabular}{lp{0.6\linewidth}} \toprule \multicolumn{2}{c}{\textbf{Play a match}} \\ @@ -794,7 +809,7 @@ The engine interface reads the input for generating a move as stated by the \vspace{\interclassSpace} -\begin{figure}[h] +\begin{figure}[p] \begin{center} \includegraphics[width=\textwidth]{diagrams/useCase_generateAMove.png} \caption{Use case: Generate a move.} @@ -836,7 +851,49 @@ The engine interface reads the input for generating a move as stated by the \vspace{\interclassSpace} -\begin{figure}[h] +\begin{figure}[p] + \begin{center} + \includegraphics[width=\textwidth]{diagrams/useCase_trainTheEngine.png} + \caption{Use case: Train the engine.} + \label{fig:useCase_trainTheEngine} + \end{center} +\end{figure} + +\begin{tabular}{lp{0.6\linewidth}} + \toprule + \multicolumn{2}{c}{\textbf{Train the engine}} \\ + \midrule + \textbf{Preconditions} & Some SGF files have been acquired to be used as + training games. \\ + \midrule + \textbf{Postconditions} & A neural network model has been created or updated. \\ + \midrule + \textbf{Actors} & Human user. \\ + \midrule + \textbf{Description} & + 1. The user starts the trainer.\newline + 2. The trainer processes the input files.\newline + 3. The trainer trains a model with the games described in the input + files. \\ + \midrule + \textbf{Secondary scenarios} & + --- \\ + \midrule + \textbf{Exceptions} & + \textbf{No input provided}: An error message is shown. Go back to step 1 of + main scenario. \newline + \textbf{The input is not formatted properly}: An error message is shown. Go + back to step 1 of main scenario. \\ + \midrule + \textbf{Notes} & + A robustness diagram for this scenario is shown in + \fref{fig:useCase_trainTheEngine}.\\ + \bottomrule +\end{tabular} + +\vspace{\interclassSpace} + +\begin{figure}[p] \begin{center} \includegraphics[width=\textwidth]{diagrams/useCase_useAsBackend.png} \caption{Use case: Use as backend for machine player.} @@ -893,36 +950,37 @@ The Testing Plan will include four types of tests: \subsubsection{Unitary Testing} -Tests for the Python code are developed using the unittest \cite{python_unittest} -testing framework. It has been chosen by virtue of being thoroughly documented -and widely used. +A unitary testing suite provides test coverage at a base level, checking the +correct behaviour of each specific code piece. -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. +\subsubsection{Integration Testing} -The script used to run the tests is shown on \lref{lst:test} and its output on -\lref{lst:testOutput}. +The interaction between components will be tested to check their correct +behaviour on the various situations where they have to work together: -% Maybe put an example report here? -\begin{listing}[h] - \inputminted{bash}{listings/test.sh} - \caption{Script to run the tests.} - \label{lst:test} -\end{listing} +\begin{itemize} +\item The Engine module is able to use the Game module to handle a match. +\item The Training module is able to use the Engine module and its definitions + of neural networks. +\end{itemize} -\begin{listing}[h] - \inputminted[fontsize=\footnotesize]{text}{listings/testOutput.txt} - \caption{Unitary testing output.} - \label{lst:testOutput} -\end{listing} +\subsubsection{System Testing} -\subsubsection{Integration Testing} +The working of the system as a whole has to be tested. Mainly: -\subsubsection{System Testing} +\begin{itemize} -\subsubsection{Usability Testing} + \item A game of Go can be played by a human by using the Game module. + \item A game of Go can be played against the computer by using the + \acrshort{gtp} interface of the Engine module. + \item The \acrshort{gtp} interface is compatible with at least one existing + GUI that claims compatibility with this protocol. + \item A training session can be started and it produces some results to be + evaluated independent of this test step. -% Game playing +\end{itemize} + +\subsubsection{Usability Testing} -% Using the engine with an existing GUI +Some people representing the final user will be confronted with the interfaces +of the system to test its usability. diff --git a/doc/tex/systemDesign.tex b/doc/tex/systemDesign.tex index 7a5db7b..78fb2dc 100644 --- a/doc/tex/systemDesign.tex +++ b/doc/tex/systemDesign.tex @@ -1,5 +1,8 @@ \section{System Design} +This section explains the design of the component systems of \program{}, the +algorithms used and how they are to be implemented. + \subsection{Class Diagram} The full class diagram is shown in \fref{fig:fullClasses}. @@ -15,7 +18,7 @@ The full class diagram is shown in \fref{fig:fullClasses}. \end{figure} The design of each system of the diagram is explained after this section -together with diagrams for each subsystem, since the full class diagram can be +along with diagrams for each subsystem, since the full class diagram can be too big to be comfortably analyzed. \subsection{Game} @@ -30,14 +33,14 @@ too big to be comfortably analyzed. \end{figure} A regular Go match is composed of a list of moves. But since game review and -variants exploration is an important part of Go learning, \program{} and most +exploration of variants is an important part of Go learning, \program{} and most playing and analysis existing programs allow for navigation back and forth through the board states of a match and for new variants to be created from each of these board states. Therefore, a match is represented as a tree of moves. The -\texttt{GameMove} class has the information about a specific move and also a reference to -the previous move and to a list of following moves, implementing this tree -structure and allowing for navigating both forward and backwards in the move -history. +\texttt{GameMove} class has the information about a specific move and also a +reference to the previous move and to a list of following moves, implementing +this tree structure and allowing for navigating both forward and backwards in +the move history. The state of the board at any given move must be stored so liberties, captures count and legality of moves can be addressed, so it is represented with the @@ -62,11 +65,11 @@ These classes and their relationships can be seen in \fref{fig:game}. \end{center} \end{figure} -An implementation of \acrshort{gtp}, that is, the piece of software which offers the \acrshort{gtp} -interface to other applications. It is designed to be used by a software -controller but can also be directly run, mostly for debugging purposes. Its -design is shown in \fref{fig:engine}. The core of the engine is related with -three components, each with a separate responsibility: +This will be an implementation of \acrshort{gtp}, that is, the piece of software +which offers the \acrshort{gtp} interface to other applications. It is designed +to be used by a software controller but can also be directly run, mostly for +debugging purposes. Its design is shown in \fref{fig:engine}. The core of the +engine is related with three components, each with a separate responsibility: \begin{itemize} @@ -80,8 +83,8 @@ three components, each with a separate responsibility: \item The \texttt{DecisionAlgorithm} component is responsible of analyzing the match and generate moves. The engine core uses it when a decision - has to be made by the AI, such as when a move needs to be generated by - the engine. + has to be made by the \acrshort{ai}, such as when a move needs to be + generated by the engine. \end{itemize} @@ -100,30 +103,33 @@ The engine can be started with the executable \texttt{imagocli.py}. \subsubsection{Monte Carlo Tree Search Explained} Monte Carlo Tree Search is an algorithm that can be useful for exploring -decision trees. It was used by AlphaGo in conjunction with neural networks as +decision trees. It has a history of use in Go engines, not being able to reach +strong levels of play until it was paired with neural networks by AlphaGo as explained in the AlphaGo 2016 paper \cite{natureAlphaGo2016}. -The algorithm assigns a score to each explored node based on how likely the -player who makes the corresponding move is to win and updates this score on each -exploration cycle. +The algorithm assigns a score to each explored node of the game tree based on +how likely the player who makes the corresponding move is to win and updates +this score on each exploration cycle. The exploration of the tree has 4 steps: \begin{enumerate} - \item \textbf{Selection}: The most promising move with unexplored children - is selected for exploration. Unexplored children are viable moves which - are not yet part of the tree. + \item \textbf{Selection}: The most promising move with at least one + unexplored children is selected for exploration. Unexplored children are + viable moves which are not yet part of the tree. \item \textbf{Expansion}: An unexplored children of the selected move is added to the tree. This children is selected at random. \item \textbf{Simulation}: The score of the new move is evaluated by playing - random matches from it. + different matches from it. How this matches are played varies: they can + be totally random, but here is where AlphaGo introduces one of its + neural networks so these simulation matches are more valuable. - \item \textbf{Backpropagation}: The score of the new move, as well as its - previous moves up to the root of the tree, is updated based on the - results of the simulation. + \item \textbf{Backpropagation}: The scores of the new move and its previous + moves up to the root of the tree are updated based on the results of the + simulation. \end{enumerate} @@ -132,7 +138,7 @@ the perspective of the player which has to make the move. The implementation of the algorithm will use the existing \texttt{GameMove} class from the Game System to access the game logic it needs, such as to get the -possible children from a node or to simulate random games. +available children from a node or to simulate random games. \subsubsection{Neural Networks Explained} @@ -156,13 +162,13 @@ Several kinds of layers have been used in this project: \item \textbf{Dense layers}, which connects each of its nodes to each of the nodes of the previous layers. - \item \textbf{Convolutional layers}, which process their input by applying - a filter function to adjacent values. In the case of this project, the + \item \textbf{Convolutional layers}, which process their input by applying a + filter function to adjacent values. In the case of this project, the board is filtered by grouping its vertices in 3x3 squares. The aim of these layers is to detect patterns in the input, such as curves, edges - or more complex shapes, so they are used a lot on neural networks - processing images. They are used in this project because a configuration - of the Go board is not that different from a two-dimensional image. + or more complex shapes, so they are used a lot on image processing. They + are used in this project because a configuration of the Go board is not + that different from a two-dimensional image. \item \textbf{Max pooling layers}, which process their input in a similar way to convolutional layers, but reducing the size of the input by @@ -178,19 +184,19 @@ Several kinds of layers have been used in this project: Combinations of these layers have been used to define two neural networks. First, a network using mainly dense layers as an example of a more general -purpose design of a network. +purpose and baseline design of a network. Then, a network with convolutional and max pooling layers to compare the -approach used on image processing to the more general one and studying its -utility on the analysis of the Go board. +approach used on image processing to the more general one and study its utility +on the analysis of the Go board. These networks have been implemented on the \texttt{DenseNeuralNetwork} and \\ \texttt{ConvNeuralNetwork} classes, respectively. The networks have been designed to process boards of size 9x9, which is the introductory size to the game. It is the easiest both for the hardware to handle -and for the analysis of results while keeping able to support meaningful -matches. +and for the analysis of results while still being big enough to support +meaningful matches. Both networks have the same design for their input and output. @@ -269,16 +275,16 @@ respectively. \end{figure} Neural networks can be powerful machine learning algorithms, but they have to be -trained first so they can provide meaningful results. For a Go AI it makes sense -to have its algorithms trained on Go games. There exists a common text format to -store Go games: \acrshort{sgf}. If the system is able to process \acrshort{sgf} files, it can provide -the games stored on them to the neural networks for training. And so the need -for an \acrshort{sgf} parser arises. - -To parse \acrshort{sgf} files a lexer and parser have been implemented using -PLY.\@ The result of the parsing is an AST (Annotated Syntax Tree) reflecting -the contents of the text input, each node with zero or more properties, and with -the ability to convert themselves and their corresponding subtree into a +trained first so they can provide meaningful results. For a Go \acrshort{ai} it +makes sense to have its algorithms trained on Go games. There exists a common +text format to store Go games: \acrshort{sgf}. If the system is able to process +\acrshort{sgf} files, it can provide the games stored on them to the neural +networks for training. And so the need for an \acrshort{sgf} parser arises. + +To parse \acrshort{sgf} files a lexer and parser have been implemented using PLY +\cite{ply}. The result of the parsing is an \acrfull{ast} reflecting the +contents of the text input, each node with zero or more properties, and with the +ability to convert themselves and their corresponding subtree into a \texttt{GameMove} tree. This is done for the root node, since from the \acrshort{sgf} specification there are some properties only usable in the root node, like those which specify general game information and properties such as @@ -290,23 +296,23 @@ solved. These components are shown in \fref{fig:trainingModule}. \begin{itemize} - \item \textbf{\acrshort{sgf}}: Provides a high-level method to convert a path to a \acrshort{sgf} + \item \textbf{\texttt{\acrshort{sgf}}}: Provides a high-level method to convert a path to a \acrshort{sgf} file to a \texttt{GameMove} tree. - \item \textbf{sgfyacc}: The implementation of a \acrshort{sgf} parser using PLY. Takes - the tokens generated by \textbf{sgflex} and creates an \texttt{ASTNode} + \item \textbf{\texttt{sgfyacc}}: The implementation of a \acrshort{sgf} parser using PLY. Takes + the tokens generated by \texttt{sgflex} and creates an \texttt{ASTNode} tree from them. - \item \textbf{sgflex}: The implementation of a \acrshort{sgf} lexer using + \item \textbf{\texttt{sgflex}}: The implementation of a \acrshort{sgf} lexer using PLY.\@ Takes text input and generates the tokens of the \acrshort{sgf} language from them. - \item \textbf{ASTNode}: The AST resulting from the parsing of a + \item \textbf{\texttt{ASTNode}}: The AST resulting from the parsing of a \acrshort{sgf} file. Has a method to convert it to a tree of \texttt{GameMove} and so obtain the contents of the \acrshort{sgf} in the internal representation used by the project's systems. - \item \textbf{Property}: The representation of a property of an + \item \textbf{\texttt{Property}}: The representation of a property of an \texttt{ASTNode} tree. Each property is made of a name and one or more values and this class helps handling this specific situation. @@ -327,6 +333,134 @@ The training can be started with the executable \texttt{train.py}. % \end{center} %\end{figure} -%\subsection{Technical Testing Plan Specification} +\subsection{Technical Testing Plan Specification} + +This section lists and explains the exact testing plan. + +\subsubsection{Unitary Testing} + +Tests for the Python code will be developed using the unittest +\cite{python_unittest} testing framework. It has been chosen by virtue of being +thoroughly documented and widely used. + +The coverage of unit testing will be checked with Coverage.py +\cite{python_coverage}, which can by itself run the unittest tests and generate +coverage reports based on the results. + +\subsubsection{Integration Testing} + +\vspace{\interclassSpace} + +\begin{tabular}{p{0.4\linewidth}p{0.5\linewidth}} + \toprule + \multicolumn{2}{c}{\textbf{Engine and Game modules}} \\ + \midrule + \textbf{Test} & \textbf{Expected behaviour} \\ + \midrule + The GTP interface of the engine is used to play a match & The module handles + the game and can show its state. \\ + \bottomrule +\end{tabular} + +\vspace{\interclassSpace} + +\begin{tabular}{p{0.4\linewidth}p{0.5\linewidth}} + \toprule + \multicolumn{2}{c}{\textbf{Training and Engine module}} \\ + \midrule + \textbf{Test} & \textbf{Expected behaviour} \\ + \midrule + The training process is started & The training uses the network defined on + the Engine module. \\ + \bottomrule +\end{tabular} + +\subsubsection{System Testing} + +These are the tests to check the correct working of the system as a whole. The +tests are grouped by the interface they are run against. + +\vspace{\interclassSpace} + +\begin{tabular}{p{0.4\linewidth}p{0.5\linewidth}} + \toprule + \multicolumn{2}{c}{\textbf{Game interface}} \\ + \midrule + \textbf{Test} & \textbf{Expected behaviour} \\ + \midrule + Play a game of Go with no engine & The game can be played until the end. \\ + \midrule + Provide a wrong move & The interface shows it is wrong and the game + continues without a change of state. \\ + \midrule + Close the game & The interface closes. \\ + \bottomrule +\end{tabular} + +\vspace{\interclassSpace} + +\begin{tabular}{p{0.4\linewidth}p{0.5\linewidth}} + \toprule + \multicolumn{2}{c}{\textbf{Engine interface}} \\ + \midrule + \textbf{Test} & \textbf{Expected behaviour} \\ + \midrule + Ask for the available commands & The interface outputs the available + commands. \\ + \midrule + Provide a move & The state of the engine updates with the new move. \\ + \midrule + Ask for a move & The engine suggests a move without changing the state of + the current game. \\ + \bottomrule +\end{tabular} + +\vspace{\interclassSpace} + +\begin{tabular}{p{0.4\linewidth}p{0.5\linewidth}} + \toprule + \multicolumn{2}{c}{\textbf{Training interface}} \\ + \midrule + \textbf{Test} & \textbf{Expected behaviour} \\ + \midrule + Provide some games to train on & A neural network model is created. \\ + \midrule + Start the training without providing games & An error message is shown and + the execution terminated. \\ + \bottomrule +\end{tabular} + +\subsubsection{Usability Testing} + +Two different human users will be exposed to the interfaces of the project and +asked to answer a questionary about their experience. The aim of this process is +to identify and address any problems end users could have when using the +system. + +As the training of the neural networks is part of the preparation of the system, +its usability will not be tested for end users. + +These are the questions provided to the testers. + +\begin{itemize} -%TODO + \item Playing a game against a human: + \begin{itemize} + \item Were you able to start the interface? + \item How hard to understand was the interface of the game? + \end{itemize} + + \item Playing a game against the engine: + \begin{itemize} + \item Were you able to start the interface? + \item How hard to understand was the interface of the game? + \item How strong did you find the engine? + \end{itemize} + + \item Playing a game against the interface through a third-party GUI: + \begin{itemize} + \item Were you able to start the interface? + \item Did you find any problems when setting up the engine? + \end{itemize} + +\end{itemize} diff --git a/go.py b/go.py index cf0a673..c6553df 100755 --- a/go.py +++ b/go.py @@ -2,14 +2,17 @@ """Play Go!""" +import sys + from imago.engine.parseHelpers import parseVertex from imago.gameLogic.gameState import GameState -if __name__ == "__main__": +def runGame(): GAMESTATE = GameState(9) + gameEnded = False - while True: + while not gameEnded: GAMESTATE.getBoard().printBoard() @@ -23,7 +26,24 @@ if __name__ == "__main__": print() - moveRow = move[0] - moveCol = move[1] + if move == "pass": + + GAMESTATE.playPass() + if GAMESTATE.lastMove.previousMove.isPass: + print("Both players passed: end of the game.") + gameEnded = True + + else: - GAMESTATE.playMove(moveRow, moveCol) + moveRow = move[0] + moveCol = move[1] + GAMESTATE.playMove(moveRow, moveCol) + +if __name__ == "__main__": + + try: + runGame() + except KeyboardInterrupt as err: + print() + print("Quitting: End of the game.") + sys.exit(0) diff --git a/imago/gameLogic/gameState.py b/imago/gameLogic/gameState.py index 3e8c1a5..8821384 100644 --- a/imago/gameLogic/gameState.py +++ b/imago/gameLogic/gameState.py @@ -49,7 +49,7 @@ class GameState: if playable: self._addMove(player, row, col) return True - print("Invalid Move! %s" % message) + print("Invalid Move. %s" % message) return False def playPass(self): diff --git a/train.py b/train.py index 0f518d0..306d6e5 100755 --- a/train.py +++ b/train.py @@ -14,6 +14,10 @@ def main(): print(file) games.append(loadGameTree(file)) + if len(games) == 0: + print("Error: No game files provided. Provide some SGF files as arguments.") + sys.exit(0) + matches = [game.getMainLineOfPlay() for game in games] modelFile = "" -- cgit v1.2.1 From 12431ce4c4ed52fcfc2180bcffdfec33e72b73ba Mon Sep 17 00:00:00 2001 From: InigoGutierrez Date: Tue, 6 Jun 2023 20:09:08 +0200 Subject: Written user manuals. --- doc/listings/goInterface/01-start.txt | 11 ++ doc/listings/goInterface/02-firstMove.txt | 22 +++ doc/listings/goInterface/03-fullGame.txt | 97 ++++++++++ doc/listings/goInterface/04-ko.txt | 72 +++++++ doc/listings/imagocliInterface/01-start.txt | 40 ++++ doc/tex/appendixes.tex | 279 +++++++++++++++++++++++++++- doc/tex/glossary.tex | 5 +- doc/tex/imago.tex | 7 +- doc/tex/introduction.tex | 4 +- doc/tex/planning.tex | 6 +- doc/tex/systemAnalysis.tex | 12 +- doc/tex/systemDesign.tex | 2 +- imago/engine/imagoIO.py | 13 +- imago/gameLogic/gameBoard.py | 14 +- imagocli.py | 9 +- 15 files changed, 558 insertions(+), 35 deletions(-) create mode 100644 doc/listings/goInterface/01-start.txt create mode 100644 doc/listings/goInterface/02-firstMove.txt create mode 100644 doc/listings/goInterface/03-fullGame.txt create mode 100644 doc/listings/goInterface/04-ko.txt create mode 100644 doc/listings/imagocliInterface/01-start.txt diff --git a/doc/listings/goInterface/01-start.txt b/doc/listings/goInterface/01-start.txt new file mode 100644 index 0000000..0aa8dfd --- /dev/null +++ b/doc/listings/goInterface/01-start.txt @@ -0,0 +1,11 @@ + A B C D E F G H J +9 · · · · · · · · · +8 · · · · · · · · · +7 · · · · · · · · · +6 · · · · · · · · · +5 · · · · · · · · · +4 · · · · · · · · · +3 · · · · · · · · · +2 · · · · · · · · · +1 · · · · · · · · · +Move (B): diff --git a/doc/listings/goInterface/02-firstMove.txt b/doc/listings/goInterface/02-firstMove.txt new file mode 100644 index 0000000..2fc37a6 --- /dev/null +++ b/doc/listings/goInterface/02-firstMove.txt @@ -0,0 +1,22 @@ + A B C D E F G H J +9 · · · · · · · · · +8 · · · · · · · · · +7 · · · · · · · · · +6 · · · · · · · · · +5 · · · · · · · · · +4 · · · · · · · · · +3 · · · · · · · · · +2 · · · · · · · · · +1 · · · · · · · · · +Move (B): e6 + A B C D E F G H J +9 · · · · · · · · · +8 · · · · · · · · · +7 · · · · · · · · · +6 · · · · B · · · · +5 · · · · · · · · · +4 · · · · · · · · · +3 · · · · · · · · · +2 · · · · · · · · · +1 · · · · · · · · · +Move (W): diff --git a/doc/listings/goInterface/03-fullGame.txt b/doc/listings/goInterface/03-fullGame.txt new file mode 100644 index 0000000..a975329 --- /dev/null +++ b/doc/listings/goInterface/03-fullGame.txt @@ -0,0 +1,97 @@ + A B C D E F G H J +9 · · · · · · · · · +8 · · · · · · · · · +7 · · · · · · · · · +6 · · · · · · · · · +5 · · · · · · · · · +4 · · · · · · · · · +3 · · · · · · · · · +2 · · · · · · · · · +1 · · · · · · · · · +Move (B): e6 + + A B C D E F G H J +9 · · · · · · · · · +8 · · · · · · · · · +7 · · · · · · · · · +6 · · · · B · · · · +5 · · · · · · · · · +4 · · · · · · · · · +3 · · · · · · · · · +2 · · · · · · · · · +1 · · · · · · · · · +Move (W): e4 + + A B C D E F G H J +9 · · · · · · · · · +8 · · · · · · · · · +7 · · · · · · · · · +6 · · · · B · · · · +5 · · · · · · · · · +4 · · · · W · · · · +3 · · · · · · · · · +2 · · · · · · · · · +1 · · · · · · · · · +Move (B): g5 + + A B C D E F G H J +9 · · · · · · · · · +8 · · · · · · · · · +7 · · · · · · · · · +6 · · · · B · · · · +5 · · · · · · B · · +4 · · · · W · · · · +3 · · · · · · · · · +2 · · · · · · · · · +1 · · · · · · · · · +Move (W): c5 + + A B C D E F G H J +9 · · · · · · · · · +8 · · · · · · · · · +7 · · · · · · · · · +6 · · · · B · · · · +5 · · W · · · B · · +4 · · · · W · · · · +3 · · · · · · · · · +2 · · · · · · · · · +1 · · · · · · · · · +Move (B): g3 + + A B C D E F G H J +9 · · · · · · · · · +8 · · · · · · · · · +7 · · · · · · · · · +6 · · · · B · · · · +5 · · W · · · B · · +4 · · · · W · · · · +3 · · · · · · B · · +2 · · · · · · · · · +1 · · · · · · · · · +Move (W): g7 + + A B C D E F G H J +9 · · · · · · · · · +8 · · · · · · · · · +7 · · · · · · W · · +6 · · · · B · · · · +5 · · W · · · B · · +4 · · · · W · · · · +3 · · · · · · B · · +2 · · · · · · · · · +1 · · · · · · · · · +Move (B): pass + + A B C D E F G H J +9 · · · · · · · · · +8 · · · · · · · · · +7 · · · · · · W · · +6 · · · · B · · · · +5 · · W · · · B · · +4 · · · · W · · · · +3 · · · · · · B · · +2 · · · · · · · · · +1 · · · · · · · · · +Move (W): pass + +Both players passed: end of the game. diff --git a/doc/listings/goInterface/04-ko.txt b/doc/listings/goInterface/04-ko.txt new file mode 100644 index 0000000..b2edf55 --- /dev/null +++ b/doc/listings/goInterface/04-ko.txt @@ -0,0 +1,72 @@ + A B C D E F G H J +9 B · B · · · · · · +8 W B · · · · · · · +7 · · · · · · · · · +6 · · · · · · · · · +5 · · · · · · · · · +4 · · · · · · · · · +3 · · · · · · · · · +2 · · · · · · · · · +1 · · · · · · · · W +Move (W): b9 + + A B C D E F G H J +9 · W B · · · · · · +8 W B · · · · · · · +7 · · · · · · · · · +6 · · · · · · · · · +5 · · · · · · · · · +4 · · · · · · · · · +3 · · · · · · · · · +2 · · · · · · · · · +1 · · · · · · · · W +Move (B): a9 + +Invalid Move. Illegal by ko rule. + A B C D E F G H J +9 · W B · · · · · · +8 W B · · · · · · · +7 · · · · · · · · · +6 · · · · · · · · · +5 · · · · · · · · · +4 · · · · · · · · · +3 · · · · · · · · · +2 · · · · · · · · · +1 · · · · · · · · W +Move (B): z9 +Invalid move syntax. Example of move: A1 + A B C D E F G H J +9 · W B · · · · · · +8 W B · · · · · · · +7 · · · · · · · · · +6 · · · · · · · · · +5 · · · · · · · · · +4 · · · · · · · · · +3 · · · · · · · · · +2 · · · · · · · · · +1 · · · · · · · · W +Move (B): a0 +Invalid move syntax. Example of move: A1 + A B C D E F G H J +9 · W B · · · · · · +8 W B · · · · · · · +7 · · · · · · · · · +6 · · · · · · · · · +5 · · · · · · · · · +4 · · · · · · · · · +3 · · · · · · · · · +2 · · · · · · · · · +1 · · · · · · · · W +Move (B): a10 +Invalid move syntax. Example of move: A1 + A B C D E F G H J +9 · W B · · · · · · +8 W B · · · · · · · +7 · · · · · · · · · +6 · · · · · · · · · +5 · · · · · · · · · +4 · · · · · · · · · +3 · · · · · · · · · +2 · · · · · · · · · +1 · · · · · · · · W +Move (B): diff --git a/doc/listings/imagocliInterface/01-start.txt b/doc/listings/imagocliInterface/01-start.txt new file mode 100644 index 0000000..b1c72f0 --- /dev/null +++ b/doc/listings/imagocliInterface/01-start.txt @@ -0,0 +1,40 @@ +name += Imago + +version += 0.0.0 + +protocol_version += 2 + +known_command nonexistentcommand += false + +known_command name += true + +nonexistentcommand +? unknown command + +play b e6 += + +genmove w += C8 + +showboard += + A B C D E F G H J +9 · · · · · · · · · +8 · · W · · · · · · +7 · · · · · · · · · +6 · · · · B · · · · +5 · · · · · · · · · +4 · · · · · · · · · +3 · · · · · · · · · +2 · · · · · · · · · +1 · · · · · · · · · + +quit += + diff --git a/doc/tex/appendixes.tex b/doc/tex/appendixes.tex index 581f764..ec6c2aa 100644 --- a/doc/tex/appendixes.tex +++ b/doc/tex/appendixes.tex @@ -3,6 +3,275 @@ Additional information is included here, after the main sections regarding the project. +\subsection{User manual} + +This manual explains to the end user how to use the software. + +\subsubsection{The game: the \texttt{go.py} interface} + +\texttt{go.py} is a simple executable to interact with the Game system of this +project. It makes use of no artificial intelligence whatsoever, its aim just +being allowing for human play. It can be executed in a shell as: + +{ + \centering + \begin{minipage}{0.4\textwidth} + \texttt{python go.py} + \end{minipage} + \par +} + +or by any other means of executing a python file with access to its input and +output streams. The executable receives no arguments and has no options. + +When executed the user is presented with the following interface: + +{ + \centering + \begin{minipage}{0.4\textwidth} + \inputminted{text}{listings/goInterface/01-start.txt} + \end{minipage} + \par +} + +The state of the board (empty for now) is shown and the user is prompt for a +move. The color to make the move is marked between brackets: \texttt{B} for +Black, \texttt{W} for White. + +A move can now be provided for the Black player, such as \texttt{e6}. + +{ + \centering + \begin{minipage}{0.4\textwidth} + \inputminted{text}{listings/goInterface/02-firstMove.txt} + \end{minipage} + \par +} + +The interface shows again the state of the board. The game can continue until +the move ``pass'' is provided for both players. + +{ + \centering + \begin{minipage}{\textwidth} + \begin{multicols}{2} + \inputminted[fontsize=\small]{text}{listings/goInterface/03-fullGame.txt} + \end{multicols} + \end{minipage} + \par +} + +The game will also show captured stones and notify illegal moves, such as wrong +input or because of the \gls{ko} rule. + +{ + \centering + \begin{minipage}{\textwidth} + \begin{multicols}{2} + \inputminted[fontsize=\small]{text}{listings/goInterface/04-ko.txt} + \end{multicols} + \end{minipage} + \par +} + +\subsubsection{The engine: the \texttt{imagocli.py} interface} + +\texttt{imagocli.py} is a text interface which follows the \acrshort{gtp} +specification. It can be executed in a shell as: + +{ + \centering + \begin{minipage}{0.4\textwidth} + \texttt{python imagocli.py} + \end{minipage} + \par +} + +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} +specification. + +There are the commands that the program knows and a short description of what +each does: + +\begin{itemize} + + \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{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} + version number. + \item \texttt{boardsize}: Changes the size of the board. Results in an + arbitrary internal state unless \texttt{clear\_board} is called after + it. + \item \texttt{clear\_board}: The board is cleared of stones, the record of + captured stones resets to zero and the move history resets to empty. + \item \texttt{komi}: Sets the value for \gls{komi}. + \item \texttt{fixed\_handicap}: The given number of handicap stones are + placed following the protocol specification, which follows traditional + placement of handicap. + \item \texttt{place\_free\_handicap}: The given number of handicap stones + are placed following the AI criteria. + \item \texttt{set\_free\_handicap}: The given number of handicap stones are + placed on the requested vertices. + \item \texttt{play}: A stone of the requested color is played at the + requested vertex. + \item \texttt{genmove}: A stone of the requested color is played following + the AI criteria. The played move is printed. + \item \texttt{undo}: The state is restored to before the last move, which is + removed from the move history. + \item \texttt{showboard}: Prints a text representation of the board state. + +\end{itemize} + +Here is an example of a session. + +{ + \centering + \begin{minipage}{\textwidth} + \begin{multicols}{2} + \inputminted[fontsize=\small]{text}{listings/imagocliInterface/01-start.txt} + \end{multicols} + \end{minipage} + \par +} + +Note how responses from the program begin with an equals symbol and a space but +with a question mark replacing the space to mark errors, like when providing an +unknown command (exemplified here with \texttt{nonexistentcommand} in line 16). + +This session consists first of some information asked to the engine: its name, +its version and the version of \acrshort{gtp} it implements. Then exemplifies +how to check if a command is implemented and an error answer. Then, the main +commands to interact with the game and the \acrshort{ai} are executed: +\texttt{play}, to provide an initial explicit move, and \texttt{genmove}, to +obtain an \acrshort{ai}-generated move. Finally, a representation of the board +state after these two moves is obtained with \texttt{showboard} and the engine +is closed with \texttt{quit}. + +\subsubsection{Example of \acrshort{gui} use: configuring Sabaki} + +Since the main aim of implementing a \acrshort{gtp} interface is having it be +compatible with existing tools, an explanation of how to use in in conjunction +with one such established tool is provided. + +Sabaki \cite{sabaki} is a Go board software compatible with \acrshort{gtp} +engines. It can be downloaded from \texttt{https://sabaki.yichuanshen.de/}. When +started, it shows a screen such as the one in \fref{fig:sabakiStart}. + +\begin{figure}[h] + \begin{center} + \includegraphics[width=0.8\textwidth]{img/sabakiManual/01-initialScreen.jpg} + \caption{Sabaki after being started. + }\label{fig:sabakiStart} + \end{center} +\end{figure} + +This initial screen contains a 19x19 board ready to be played on by local human +players. The stones can be placed by clicking on the desired vertices of the +board and, as would be expected, the interface swaps between players after each +move. An example of the screen after some initial moves can be seen on +\fref{fig:sabakiExampleMoves}. + +\begin{figure}[h] + \begin{center} + \includegraphics[width=0.8\textwidth]{img/sabakiManual/02-examplePlaying.jpg} + \caption{Playing some moves on the default board. + }\label{fig:sabakiExampleMoves} + \end{center} +\end{figure} + +To set Sabaki to work with \texttt{imagocli.py} first open the engines sidebar +by clicking on ``Engines'', then ``Show Engines Sidebar''. The window should now +look like the one shown on \fref{fig:sabakiEnginesSidebar}, with the engines +sidebar open at the left but with nothing shown on it yet. + +\begin{figure}[h] + \begin{center} + \includegraphics[width=0.8\textwidth]{img/sabakiManual/03-enginesSidebar.jpg} + \caption{Opened the engines sidebar (on the left). + }\label{fig:sabakiEnginesSidebar} + \end{center} +\end{figure} + +Click on the symbol on the top left of the engines sidebar, the one with a +triangle inside of a circle (the play button), and on ``Manage Engines...'' on +the opened floating dialog. The engine management window will open, as shown on +\fref{fig:sabakiEngineManagement}. + +\begin{figure}[h] + \begin{center} + \includegraphics[width=0.8\textwidth]{img/sabakiManual/04-engineManagement.jpg} + \caption{The engine management window. + }\label{fig:sabakiEngineManagement} + \end{center} +\end{figure} + +Engines are listed on the big box at the center of the window, but if none has +yet been configured nothing will be shown there. Click ``Add'' to create the +first engine entry. Give it a name by writing on the first line of the newly +created entry, then write the path to the \texttt{imagocli.py} executable +besides the folder symbol, at the text box prompting for the path to the engine. +Arguments and initial commands to provide to the engine can be configured here, +but none of them will be needed for the default configuration of \program{}. An +example of the configured engine is shown on \fref{fig:sabakiConfiguredEngine}. + +\begin{figure}[h] + \begin{center} + \includegraphics[width=0.8\textwidth]{img/sabakiManual/05-configuredEngine.jpg} + \caption{\texttt{imagocli.py} configured as engine. + }\label{fig:sabakiConfiguredEngine} + \end{center} +\end{figure} + +The engine management window can now be closed by clicking on ``Close'' at its +bottom right. Click again on the play button, the one on the top left of the +engines sidebar, and select the newly created engine entry. The engine will now +be started. By default, Sabaki will provide it the \texttt{name}, +\texttt{version}, \texttt{protocol\_version} and \texttt{list\_commands} +commands, and the text interface will be shown on the engines sidebar. To play +against the engine, click on ``File'', then ``New'', or just use the keyboard +shortcut Ctrl+N. A window will be shown where a new game can be configured. The +most important settings are making the engine be one (or both) of the players +and setting the board size to 9x9, since this is where the engine plays best. +For example, to set the engine as the white player, click on the arrow next to +the white player's name and select the engine, represented by the name given to +it before), on the floating menu. The size of the board can be set on the +``Board Size'' setting. Other options allow to set a name for the other player, +a name for the match, the \gls{komi} and the handicap stones, if any. When +ready, click ``Ok'' on the bottom right of the window. An example configuration +can be seen on \fref{fig:sabakiConfiguringAMatch}. If the engine doesn't respond +to the moves, which should be clear since its status is shown on the engines +sidebar, try adding it to the match by clicking on ``Attach Engine'' and then on +its name on the new match window, instead of directly on its name. This creates +a new entry for the engine on the engines sidebar. Multiple instances of the +same or different engines can be running at the same time; to remove one, just +right click on its name and click on ``Detach'' on the floating menu. + +\begin{figure}[h] + \begin{center} + \includegraphics[width=0.8\textwidth]{img/sabakiManual/06-configuringAMatch.jpg} + \caption{Configuring a match against the engine. + }\label{fig:sabakiConfiguringAMatch} + \end{center} +\end{figure} + +The engine can now be played against: when black (the human) makes a move, it +will respond as white. Some initial moves against it can be seen on +\fref{fig:sabakiAgainstTheEngine}. Note the interaction between Sabaki and the +\acrshort{gtp} interface on the engines sidebar. + +\begin{figure}[h] + \begin{center} + \includegraphics[width=0.8\textwidth]{img/sabakiManual/07-playingAgainstImago.png} + \caption{Playing some moves against \program{}. + }\label{fig:sabakiAgainstTheEngine} + \end{center} +\end{figure} + \subsection{Budget} Here are tables regarding the costs of resources and development for the @@ -12,7 +281,7 @@ project. The costs are calculated based on a programmer salary of 20€/hour. -\begin{table}[h] +\begin{table}[H] \makebox[\linewidth]{ \begin{tabular}{l r r} \toprule @@ -34,7 +303,7 @@ The costs are calculated based on a programmer salary of 20€/hour. \subsubsection{Material resources} -\begin{table}[h] +\begin{table}[H] \makebox[\linewidth]{ \begin{tabular}{l r} \toprule @@ -48,7 +317,7 @@ The costs are calculated based on a programmer salary of 20€/hour. \subsubsection{Totals} -\begin{table}[h] +\begin{table}[H] \makebox[\linewidth]{ \begin{tabular}{l r} \toprule @@ -64,9 +333,9 @@ The costs are calculated based on a programmer salary of 20€/hour. } \end{table} -\subsection{Budget for the client} +\subsubsection{Budget for the client} -\begin{table}[h] +\begin{table}[H] \makebox[\linewidth]{ \begin{tabular}{l r} \toprule diff --git a/doc/tex/glossary.tex b/doc/tex/glossary.tex index d14dc02..25c0488 100644 --- a/doc/tex/glossary.tex +++ b/doc/tex/glossary.tex @@ -48,6 +48,7 @@ } \newacronym{ai}{AI}{Artificial Intelligence} -\newacronym{sgf}{SGF}{Smart Game Format} -\newacronym{gtp}{GTP}{Go Text Protocol} \newacronym{ast}{AST}{Annotated Syntax Tree} +\newacronym{gtp}{GTP}{Go Text Protocol} +\newacronym{gui}{GUI}{Graphical User Interface} +\newacronym{sgf}{SGF}{Smart Game Format} diff --git a/doc/tex/imago.tex b/doc/tex/imago.tex index f8e3101..4287330 100644 --- a/doc/tex/imago.tex +++ b/doc/tex/imago.tex @@ -8,6 +8,7 @@ \usepackage{enumitem} \usepackage[indent=20pt]{parskip} % Space between paragraphs \usepackage{indentfirst} % Indent first paragraph of sections +\usepackage{multicol} % Multiple columns \geometry{left=3cm,top=2cm,bottom=2cm,right=3cm} @@ -192,11 +193,7 @@ inclusion of to use this template is included here. \clearpage % References (bibliography) - -\setcounter{secnumdepth}{0} -\section{References} -\printbibliography[type=article,title={Cited articles},heading=subbibintoc]{} -\printbibliography[type=online,title={Online resources},heading=subbibintoc]{} +\printbibliography[heading=bibintoc]{} \clearpage \input{tex/license.tex} diff --git a/doc/tex/introduction.tex b/doc/tex/introduction.tex index 22dd98a..c50d700 100644 --- a/doc/tex/introduction.tex +++ b/doc/tex/introduction.tex @@ -18,7 +18,7 @@ As old and deep as Go is it has recently lived a revolution by the appearance of artificial intelligences with superhuman strength. While not expecting to achieve what a full team of developers and computer scientists at Google did, -this project aims to evaluate how an engine able to play the game of Go could be +this project aims to analyze how an engine able to play the game of Go could be created, implement such an engine and evaluate the results of the whole process. \subsection{Driving Needs} @@ -43,7 +43,7 @@ Presented here are the ideal targets of the project. game's rules. \item An engine capable of analyzing board positions and generating strong moves via various decision algorithms. - \item An interface compatible with existing GUIs. + \item An interface compatible with existing \acrshort{gui}s. \item A way for processing existing records of games, which are usually recorded in the \acrfull{sgf}. \end{itemize} diff --git a/doc/tex/planning.tex b/doc/tex/planning.tex index 4057268..790a988 100644 --- a/doc/tex/planning.tex +++ b/doc/tex/planning.tex @@ -139,7 +139,7 @@ of the project is shown on \fref{fig:gnuGoLogo}. The \acrfull{gtp} \cite{gtp} is a text based protocol for communication with computer Go programs. It is the protocol used by GNU Go and the more modern and powerful KataGo. By supporting \acrshort{gtp} the engine developed for this -project can be used with existing GUIs and other programs, making it easier to +project can be used with existing \acrshort{gui}s and other programs, making it easier to be used with the tools target users are already familiar with. %TODO @@ -191,7 +191,7 @@ described in this file is shown visually in \fref{fig:sgfExample}. \subsubsection{Sabaki} Sabaki \cite{sabaki} is a Go board software compatible with \acrshort{gtp} -engines. It can serve as a GUI for the engine developed in this project and as +engines. It can serve as a \acrshort{gui} for the engine developed in this project and as an example of the advantages of following a standardized protocol. Part of its graphical interface is shown on \fref{fig:sabaki}. @@ -245,7 +245,7 @@ Both the game and the engine will offer a text interface. For the game this allows for quick human testing. For the engine it is mandated by the protocol, since \acrshort{gtp} is a text based protocol for programs using text interfaces. Independent programs compatible with this interface will be able to -be used in conjunction with this engine, for example to serve as a GUI. +be used in conjunction with this engine, for example to serve as a \acrshort{gui}. There is also the need of an interface with \acrshort{sgf} files so existing games can be processed by the trainer. diff --git a/doc/tex/systemAnalysis.tex b/doc/tex/systemAnalysis.tex index 811a8f3..1f0c997 100644 --- a/doc/tex/systemAnalysis.tex +++ b/doc/tex/systemAnalysis.tex @@ -21,7 +21,7 @@ These are the main goals the final product must reach. \item An engine program as a way of presenting an interface for using these algorithms. The engine will use the \acrshort{gtp} so it can be used with an - existing GUI or other tools. + existing \acrshort{gui} or other tools. \item A parser for \acrshort{sgf} files so they can be processed in the training of neural networks. @@ -724,7 +724,7 @@ non-human. \item The human player who interacts with the playing interface. \item The human user who interacts with the engine. \item The human user who starts the training. - \item A GUI software which uses the engine to generate moves. + \item A \acrshort{gui} software which uses the engine to generate moves. \end{itemize} @@ -759,7 +759,7 @@ and to train a neural network. \paragraph{Use as backend for machine player} The engine is used as the backend for generating moves for a machine player, -this is, for automated play, either against a human who is using the GUI or +this is, for automated play, either against a human who is using the \acrshort{gui} or against another machine player. \subsection{Use Case Analysis and Scenarios} @@ -826,7 +826,7 @@ against another machine player. \midrule \textbf{Postconditions} & A move is suggested via the engine output. \\ \midrule - \textbf{Actors} & Human user and GUI program. \\ + \textbf{Actors} & Human user and \acrshort{gui} program. \\ \midrule \textbf{Description} & 1. The user or program enters the player to generate the move @@ -910,7 +910,7 @@ against another machine player. \midrule \textbf{Postconditions} & A match has been played against the engine. \\ \midrule - \textbf{Actors} & GUI program. \\ + \textbf{Actors} & \acrshort{gui} program. \\ \midrule \textbf{Description} & 1. The program gives commands to the engine to set up the game. The @@ -974,7 +974,7 @@ The working of the system as a whole has to be tested. Mainly: \item A game of Go can be played against the computer by using the \acrshort{gtp} interface of the Engine module. \item The \acrshort{gtp} interface is compatible with at least one existing - GUI that claims compatibility with this protocol. + \acrshort{gui} that claims compatibility with this protocol. \item A training session can be started and it produces some results to be evaluated independent of this test step. diff --git a/doc/tex/systemDesign.tex b/doc/tex/systemDesign.tex index 78fb2dc..3542cb5 100644 --- a/doc/tex/systemDesign.tex +++ b/doc/tex/systemDesign.tex @@ -457,7 +457,7 @@ These are the questions provided to the testers. \item How strong did you find the engine? \end{itemize} - \item Playing a game against the interface through a third-party GUI: + \item Playing a game against the interface through a third-party \acrshort{gui}: \begin{itemize} \item Were you able to start the interface? \item Did you find any problems when setting up the engine? diff --git a/imago/engine/imagoIO.py b/imago/engine/imagoIO.py index 1b63f18..ae1210b 100644 --- a/imago/engine/imagoIO.py +++ b/imago/engine/imagoIO.py @@ -18,7 +18,7 @@ class ImagoIO: """Recieves and handles commands.""" - def __init__(self, decisionAlgorithmId=None, outputStream=sys.stdin): + def __init__(self, decisionAlgorithmId=None, outputStream=sys.stdout): self.commands_set = { self.protocol_version, self.name, @@ -33,7 +33,8 @@ class ImagoIO: self.set_free_handicap, self.play, self.genmove, - self.undo + self.undo, + self.showboard } self.gameEngine = GameEngine(decisionAlgorithmId) self.outputStream = outputStream @@ -59,7 +60,7 @@ class ImagoIO: continue if input_tokens[0] == "quit": - #sys.exit(0) + self._response() break command = None @@ -195,7 +196,6 @@ class ImagoIO: output = parseHelpers.vertexToString(self.gameEngine.genmove(color), self.gameEngine.gameState.size) self._response(output) - self.gameEngine.gameState.getBoard().printBoard() def undo(self, _): @@ -204,3 +204,8 @@ class ImagoIO: """ self.gameEngine.undo() self._response() + + + def showboard(self, _): + """Prints a representation of the board state.""" + self._response('\n%s' % self.gameEngine.gameState.getBoard().toString()) diff --git a/imago/gameLogic/gameBoard.py b/imago/gameLogic/gameBoard.py index 611c4cb..170a7cb 100644 --- a/imago/gameLogic/gameBoard.py +++ b/imago/gameLogic/gameBoard.py @@ -273,8 +273,8 @@ class GameBoard: return False return True - def printBoard(self): - """Print the board.""" + def toString(self): + """Returns a representation of the state of the board as a string.""" colTitle = 'A' rowTitlePadding = 2 if self.getBoardHeight() >= 10: @@ -289,7 +289,7 @@ class GameBoard: colTitle = chr(ord(colTitle)+1) if colTitle == "I": # Skip I colTitle = "J" - print(rowText) + output = rowText # Print rows rowTitle = self.getBoardHeight() @@ -297,11 +297,17 @@ class GameBoard: rowText = "" for col in row: rowText += cellToString(col) + " " - print(str(rowTitle) + " " * rowTitlePadding + rowText) + output += '\n%d%s%s' % (rowTitle, " " * rowTitlePadding, rowText) rowTitle -= 1 if rowTitle == 9: rowTitlePadding += 1 + return output + + def printBoard(self): + """Print the board.""" + print(self.toString()) + def cellToString(code): """Returns the text representation of a cell.""" if code == Player.WHITE: diff --git a/imagocli.py b/imagocli.py index 5f14380..d660195 100755 --- a/imagocli.py +++ b/imagocli.py @@ -14,12 +14,15 @@ if __name__ == "__main__": if sys.argv[1] == "-e": if sys.argv[2] == "montecarlo": decisionAlgorithm = DecisionAlgorithms.MONTECARLO - if sys.argv[2] == "keras": + elif sys.argv[2] == "keras": decisionAlgorithm = DecisionAlgorithms.KERAS - if sys.argv[2] == "dense": + elif sys.argv[2] == "dense": decisionAlgorithm = DecisionAlgorithms.DENSE - if sys.argv[2] == "conv": + elif sys.argv[2] == "conv": decisionAlgorithm = DecisionAlgorithms.CONV + else: + print('Invalid algorithm "%s"' % sys.argv[2]) + sys.exit(1) if decisionAlgorithm is None: io = ImagoIO() -- cgit v1.2.1 From a005228a986b17732ae7cccbedde450533cfe1f1 Mon Sep 17 00:00:00 2001 From: InigoGutierrez Date: Fri, 9 Jun 2023 13:12:05 +0200 Subject: First usability test. --- doc/listings/goInterface/02-firstMove.txt | 1 + doc/listings/goInterface/03-fullGame.txt | 1 - doc/listings/goInterface/04-ko.txt | 5 +- doc/tex/appendixes.tex | 127 +++++++++++++++++++----------- doc/tex/implementation.tex | 71 +++++++++++++++++ doc/tex/systemDesign.tex | 13 +-- go.py | 12 ++- imago/engine/keras/keras.py | 2 +- imago/gameLogic/gameState.py | 2 +- 9 files changed, 172 insertions(+), 62 deletions(-) diff --git a/doc/listings/goInterface/02-firstMove.txt b/doc/listings/goInterface/02-firstMove.txt index 2fc37a6..3fb46d6 100644 --- a/doc/listings/goInterface/02-firstMove.txt +++ b/doc/listings/goInterface/02-firstMove.txt @@ -9,6 +9,7 @@ 2 · · · · · · · · · 1 · · · · · · · · · Move (B): e6 + A B C D E F G H J 9 · · · · · · · · · 8 · · · · · · · · · diff --git a/doc/listings/goInterface/03-fullGame.txt b/doc/listings/goInterface/03-fullGame.txt index a975329..4e01b75 100644 --- a/doc/listings/goInterface/03-fullGame.txt +++ b/doc/listings/goInterface/03-fullGame.txt @@ -93,5 +93,4 @@ Move (B): pass 2 · · · · · · · · · 1 · · · · · · · · · Move (W): pass - Both players passed: end of the game. diff --git a/doc/listings/goInterface/04-ko.txt b/doc/listings/goInterface/04-ko.txt index b2edf55..f1ef2dd 100644 --- a/doc/listings/goInterface/04-ko.txt +++ b/doc/listings/goInterface/04-ko.txt @@ -21,8 +21,8 @@ Move (W): b9 2 · · · · · · · · · 1 · · · · · · · · W Move (B): a9 - Invalid Move. Illegal by ko rule. + A B C D E F G H J 9 · W B · · · · · · 8 W B · · · · · · · @@ -35,6 +35,7 @@ Invalid Move. Illegal by ko rule. 1 · · · · · · · · W Move (B): z9 Invalid move syntax. Example of move: A1 + A B C D E F G H J 9 · W B · · · · · · 8 W B · · · · · · · @@ -47,6 +48,7 @@ Invalid move syntax. Example of move: A1 1 · · · · · · · · W Move (B): a0 Invalid move syntax. Example of move: A1 + A B C D E F G H J 9 · W B · · · · · · 8 W B · · · · · · · @@ -59,6 +61,7 @@ Invalid move syntax. Example of move: A1 1 · · · · · · · · W Move (B): a10 Invalid move syntax. Example of move: A1 + A B C D E F G H J 9 · W B · · · · · · 8 W B · · · · · · · diff --git a/doc/tex/appendixes.tex b/doc/tex/appendixes.tex index ec6c2aa..f9f189e 100644 --- a/doc/tex/appendixes.tex +++ b/doc/tex/appendixes.tex @@ -34,7 +34,7 @@ When executed the user is presented with the following interface: \par } -The state of the board (empty for now) is shown and the user is prompt for a +The state of the board (empty for now) is shown and the user is prompted for a move. The color to make the move is marked between brackets: \texttt{B} for Black, \texttt{W} for White. @@ -87,11 +87,38 @@ 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: + +\begin{itemize} + + \item \texttt{montecarlo}: The Monte Carlo Tree Search algorithm is used. + \item \texttt{keras}: The default Keras neural network, the convolutional + one, is used. + \item \texttt{dense}: The dense neural network is used. + \item \texttt{conv}: The convolutional neural network is used. + +\end{itemize} + +So, if for example the dense neural network is the one to use, the program would +be executed as: + +{ + \centering + \begin{minipage}{0.4\textwidth} + \texttt{python imagocli.py -e dense} + \end{minipage} + \par +} + +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} specification. -There are the commands that the program knows and a short description of what +These are the commands that the program knows and a short description of what each does: \begin{itemize} @@ -155,18 +182,26 @@ is closed with \texttt{quit}. \subsubsection{Example of \acrshort{gui} use: configuring Sabaki} Since the main aim of implementing a \acrshort{gtp} interface is having it be -compatible with existing tools, an explanation of how to use in in conjunction +compatible with existing tools, an explanation of how to use it in conjunction with one such established tool is provided. +\begin{figure}[p] + \begin{center} + \includegraphics[width=0.8\textwidth]{img/sabakiManual/01-initialScreen.jpg} + \caption{Sabaki after being started. + }\label{fig:sabakiStart} + \end{center} +\end{figure} + Sabaki \cite{sabaki} is a Go board software compatible with \acrshort{gtp} engines. It can be downloaded from \texttt{https://sabaki.yichuanshen.de/}. When started, it shows a screen such as the one in \fref{fig:sabakiStart}. -\begin{figure}[h] +\begin{figure}[p] \begin{center} - \includegraphics[width=0.8\textwidth]{img/sabakiManual/01-initialScreen.jpg} - \caption{Sabaki after being started. - }\label{fig:sabakiStart} + \includegraphics[width=0.8\textwidth]{img/sabakiManual/02-examplePlaying.jpg} + \caption{Playing some moves on the default board. + }\label{fig:sabakiExampleMoves} \end{center} \end{figure} @@ -176,11 +211,11 @@ board and, as would be expected, the interface swaps between players after each move. An example of the screen after some initial moves can be seen on \fref{fig:sabakiExampleMoves}. -\begin{figure}[h] +\begin{figure}[p] \begin{center} - \includegraphics[width=0.8\textwidth]{img/sabakiManual/02-examplePlaying.jpg} - \caption{Playing some moves on the default board. - }\label{fig:sabakiExampleMoves} + \includegraphics[width=0.8\textwidth]{img/sabakiManual/03-enginesSidebar.jpg} + \caption{Opened the engines sidebar (on the left). + }\label{fig:sabakiEnginesSidebar} \end{center} \end{figure} @@ -191,9 +226,9 @@ sidebar open at the left but with nothing shown on it yet. \begin{figure}[h] \begin{center} - \includegraphics[width=0.8\textwidth]{img/sabakiManual/03-enginesSidebar.jpg} - \caption{Opened the engines sidebar (on the left). - }\label{fig:sabakiEnginesSidebar} + \includegraphics[width=0.8\textwidth]{img/sabakiManual/04-engineManagement.jpg} + \caption{The engine management window. + }\label{fig:sabakiEngineManagement} \end{center} \end{figure} @@ -202,11 +237,11 @@ triangle inside of a circle (the play button), and on ``Manage Engines...'' on the opened floating dialog. The engine management window will open, as shown on \fref{fig:sabakiEngineManagement}. -\begin{figure}[h] +\begin{figure}[p] \begin{center} - \includegraphics[width=0.8\textwidth]{img/sabakiManual/04-engineManagement.jpg} - \caption{The engine management window. - }\label{fig:sabakiEngineManagement} + \includegraphics[width=0.8\textwidth]{img/sabakiManual/05-configuredEngine.jpg} + \caption{\texttt{imagocli.py} configured as engine. + }\label{fig:sabakiConfiguredEngine} \end{center} \end{figure} @@ -215,15 +250,17 @@ yet been configured nothing will be shown there. Click ``Add'' to create the first engine entry. Give it a name by writing on the first line of the newly created entry, then write the path to the \texttt{imagocli.py} executable besides the folder symbol, at the text box prompting for the path to the engine. -Arguments and initial commands to provide to the engine can be configured here, -but none of them will be needed for the default configuration of \program{}. An -example of the configured engine is shown on \fref{fig:sabakiConfiguredEngine}. +Alternatively, click on the folder symbol and a file explorer will open where it +will be possible to graphically select the executable file. Arguments and +initial commands to provide to the engine can be configured here, but none of +them will be needed for the default configuration of \program{}. An example of +the configured engine is shown on \fref{fig:sabakiConfiguredEngine}. -\begin{figure}[h] +\begin{figure}[p] \begin{center} - \includegraphics[width=0.8\textwidth]{img/sabakiManual/05-configuredEngine.jpg} - \caption{\texttt{imagocli.py} configured as engine. - }\label{fig:sabakiConfiguredEngine} + \includegraphics[width=0.8\textwidth]{img/sabakiManual/06-configuringAMatch.jpg} + \caption{Configuring a match against the engine. + }\label{fig:sabakiConfiguringAMatch} \end{center} \end{figure} @@ -239,23 +276,23 @@ most important settings are making the engine be one (or both) of the players and setting the board size to 9x9, since this is where the engine plays best. For example, to set the engine as the white player, click on the arrow next to the white player's name and select the engine, represented by the name given to -it before), on the floating menu. The size of the board can be set on the -``Board Size'' setting. Other options allow to set a name for the other player, -a name for the match, the \gls{komi} and the handicap stones, if any. When -ready, click ``Ok'' on the bottom right of the window. An example configuration -can be seen on \fref{fig:sabakiConfiguringAMatch}. If the engine doesn't respond -to the moves, which should be clear since its status is shown on the engines -sidebar, try adding it to the match by clicking on ``Attach Engine'' and then on -its name on the new match window, instead of directly on its name. This creates -a new entry for the engine on the engines sidebar. Multiple instances of the -same or different engines can be running at the same time; to remove one, just -right click on its name and click on ``Detach'' on the floating menu. - -\begin{figure}[h] +it before, on the floating menu. The size of the board can be set on the ``Board +Size'' setting. Other options allow to set a name for the other player, a name +for the match, the \gls{komi} and the handicap stones, if any. When ready, click +``Ok'' on the bottom right of the window. An example configuration can be seen +on \fref{fig:sabakiConfiguringAMatch}. If the engine doesn't respond to the +moves, which should be clear since its status is shown on the engines sidebar, +try adding it to the match by clicking on ``Attach Engine'' and then on its name +on the new match window, instead of directly on its name. This creates a new +entry for the engine on the engines sidebar. Multiple instances of the same or +different engines can be running at the same time; to remove one, just right +click on its name and click on ``Detach'' on the floating menu. + +\begin{figure}[p] \begin{center} - \includegraphics[width=0.8\textwidth]{img/sabakiManual/06-configuringAMatch.jpg} - \caption{Configuring a match against the engine. - }\label{fig:sabakiConfiguringAMatch} + \includegraphics[width=0.8\textwidth]{img/sabakiManual/07-playingAgainstImago.png} + \caption{Playing some moves against \program{}. + }\label{fig:sabakiAgainstTheEngine} \end{center} \end{figure} @@ -264,13 +301,7 @@ will respond as white. Some initial moves against it can be seen on \fref{fig:sabakiAgainstTheEngine}. Note the interaction between Sabaki and the \acrshort{gtp} interface on the engines sidebar. -\begin{figure}[h] - \begin{center} - \includegraphics[width=0.8\textwidth]{img/sabakiManual/07-playingAgainstImago.png} - \caption{Playing some moves against \program{}. - }\label{fig:sabakiAgainstTheEngine} - \end{center} -\end{figure} +\clearpage \subsection{Budget} diff --git a/doc/tex/implementation.tex b/doc/tex/implementation.tex index 73e4cfd..4970c14 100644 --- a/doc/tex/implementation.tex +++ b/doc/tex/implementation.tex @@ -259,3 +259,74 @@ The script used to run the tests is shown on \lref{lst:test} and its output on \end{tabular} \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. + +\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. \\ + \midrule + How hard was the interface of the game to understand? & + It was easy and intuitive because you just entered the command to start and + then you only had to tell it where you wanted to play the stones. It felt + easy to me.\\ + \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. \\ + \midrule + How hard was the interface of the game to understand? & + It was easy to understand because it just was enering the commands and the + application did what it had to do, but it was more difficult than the + previous one because I'm not used to do these things with commands. It is + less intuitive than the previous one regarding playing against the machine + because you don't see each move as you write it, having to ask it to show + them instead.\\ + \midrule + How strong did you find the engine? & + It was not so aggresive as playing against a human who knows the game. It + doesn't play to harm its opponent.\\ + \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. \\ + \midrule + Did you find any problems when setting up the engine? & + No, it was well explained step by step, although the images could be better + lined up with the text. Anyway, the explanations were clear and easy to + follow.\\ + \midrule + Do you think this tool has value for studying Go? & + Yes.\\ + \bottomrule +\end{tabular} diff --git a/doc/tex/systemDesign.tex b/doc/tex/systemDesign.tex index 3542cb5..e166955 100644 --- a/doc/tex/systemDesign.tex +++ b/doc/tex/systemDesign.tex @@ -265,7 +265,7 @@ respectively. \begin{figure}[h] \begin{center} - \includegraphics[width=\textwidth]{diagrams/trainingModule.png} + \includegraphics[width=0.7\textwidth]{diagrams/trainingModule.png} \caption{Components of the \acrshort{sgf} file parsing module.} \label{fig:trainingModule} Components not showing a capital C are not classes, as in they not @@ -444,23 +444,24 @@ These are the questions provided to the testers. \begin{itemize} - \item Playing a game against a human: + \item Playing against a human: \begin{itemize} \item Were you able to start the interface? - \item How hard to understand was the interface of the game? + \item How hard was the interface of the game to understand? \end{itemize} - \item Playing a game against the engine: + \item Playing against the engine: \begin{itemize} \item Were you able to start the interface? - \item How hard to understand was the interface of the game? + \item How hard was the interface of the game to understand? \item How strong did you find the engine? \end{itemize} - \item Playing a game against the interface through a third-party \acrshort{gui}: + \item Playing against the interface through a third-party \acrshort{gui}: \begin{itemize} \item Were you able to start the interface? \item Did you find any problems when setting up the engine? + \item Do you think this tool has value for studying Go? \end{itemize} \end{itemize} diff --git a/go.py b/go.py index c6553df..7580c2b 100755 --- a/go.py +++ b/go.py @@ -22,10 +22,9 @@ def runGame(): move = parseVertex(move, GAMESTATE.size) except Exception as err: print("Invalid move syntax. Example of move: A1") + print() continue - print() - if move == "pass": GAMESTATE.playPass() @@ -34,10 +33,14 @@ def runGame(): gameEnded = True else: - moveRow = move[0] moveCol = move[1] - GAMESTATE.playMove(moveRow, moveCol) + try: + GAMESTATE.playMove(moveRow, moveCol) + except Exception as err: + print(err) + + print() if __name__ == "__main__": @@ -46,4 +49,5 @@ if __name__ == "__main__": except KeyboardInterrupt as err: print() print("Quitting: End of the game.") + print() sys.exit(0) diff --git a/imago/engine/keras/keras.py b/imago/engine/keras/keras.py index 871f4a0..3d60b8e 100644 --- a/imago/engine/keras/keras.py +++ b/imago/engine/keras/keras.py @@ -34,7 +34,7 @@ class Keras(DecisionAlgorithm): if coords == "pass": self.currentMove = self.currentMove.addPass() else: - self.currentMove = self.currentMove.addMoveByCoords(coords) + self.currentMove = self.currentMove.addMove(coords) def pickMove(self): """Returns a move to play.""" diff --git a/imago/gameLogic/gameState.py b/imago/gameLogic/gameState.py index 8821384..c577713 100644 --- a/imago/gameLogic/gameState.py +++ b/imago/gameLogic/gameState.py @@ -49,7 +49,7 @@ class GameState: if playable: self._addMove(player, row, col) return True - print("Invalid Move. %s" % message) + raise Exception("Invalid Move. %s" % message) return False def playPass(self): -- cgit v1.2.1 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