diff options
author | InigoGutierrez <inigogf.95@gmail.com> | 2021-02-08 17:21:51 +0100 |
---|---|---|
committer | InigoGutierrez <inigogf.95@gmail.com> | 2021-02-08 17:21:51 +0100 |
commit | a9f645e19dd80f243c0e246e3dca9465207e60cb (patch) | |
tree | fb6e0fad605d078da8d2b0b77399009255e7f0c6 | |
parent | bcc55cedada03c66e92bfc3aac3b73245b89aaf8 (diff) | |
download | imago-a9f645e19dd80f243c0e246e3dca9465207e60cb.tar.gz imago-a9f645e19dd80f243c0e246e3dca9465207e60cb.zip |
logic: Made move legality checks independent.
-rw-r--r-- | imago/gameLogic/gameBoard.py | 134 | ||||
-rw-r--r-- | imago/gameLogic/gameMove.py | 30 | ||||
-rw-r--r-- | 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 |