aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorInigoGutierrez <inigogf.95@gmail.com>2021-02-08 17:21:51 +0100
committerInigoGutierrez <inigogf.95@gmail.com>2021-02-08 17:21:51 +0100
commita9f645e19dd80f243c0e246e3dca9465207e60cb (patch)
treefb6e0fad605d078da8d2b0b77399009255e7f0c6
parentbcc55cedada03c66e92bfc3aac3b73245b89aaf8 (diff)
downloadimago-a9f645e19dd80f243c0e246e3dca9465207e60cb.tar.gz
imago-a9f645e19dd80f243c0e246e3dca9465207e60cb.zip
logic: Made move legality checks independent.
-rw-r--r--imago/gameLogic/gameBoard.py134
-rw-r--r--imago/gameLogic/gameMove.py30
-rw-r--r--imago/gameLogic/gameState.py72
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