class Game(object): """ Attributes: playerOne The human player. playerTwo: The AI player. listeners: The game events listeners. """ log = Logger() def __init__(self, playerOne=None, playerTwo=None): """Inits an instance of the Game class. PlayerOne is always the human side of the game. Args: playerOne (Optional[model.player.Player]): The human player (default is None). playerTwo (Optional[model.player.Player]): The AI player (default is None). """ self.playerOne = playerOne self.playerTwo = playerTwo self.listeners = list() self.aiPlayer = self.playerOne if self.playerOne.isAi \ else self.playerTwo self._board = Board() self._uuid = None self._moves = list() self.status = Status.InProgress self.nextPlayer = self.playerOne dispatcher.connect(self._onAiMoveResponse, signal=Events.aiResponse) @staticmethod def create(playerOne, playerTwo): """Creates an instance of the Game class. Sets the UUID of the new game. Args: playerOne (model.player.Player) playerTwo (model.player.Player) Returns: An instance of the Game class where the attribute _uuid was assigned a value to. """ if playerOne is None: raise ValueError('playerOne') if playerTwo is None: raise ValueError('playerTwo') p = Game(playerOne, playerTwo) p.uuid = uuid.uuid4() return p def start(self): self.log.info('Starting the game {0}'.format(self.uuid)) # prepares to launch the AI script aiScriptPath = os.getcwd() + "/../ai/aiprocess.py" self.log.debug('AI script path is {0!s}'.format(aiScriptPath)) kwargs = dict() aiprotocol.makePipe(bytes(self.uuid), self.aiPlayer.symbol, self.aiPlayer.depth, sys.executable, aiScriptPath, **kwargs) def stop(self): pass def close(self): pass @property def boardData(self): return self._board.data @property def uuid(self): """Gets the UUID of the game.""" return self._uuid @uuid.setter def uuid(self, uuid): """Sets the UUID for this game.""" if uuid is None: raise ValueError("The game's uuid cannot be None or empty.") self._uuid = uuid def makeMove(self, row, col, symbol): """Handles the player's turn. Places a piece/symbol on the board at a given position. Args: row (int): The row. col (int): The column. symbol (int): The symbol. Returns: The updated status of the game (common.ipc.GameStatus). Raises: IndexError. """ self.log.debug('Invoke makeMove with %d, %d, %d' % (row, col, symbol)) if (row < 0) or (row >= Board.SIZE): raise IndexError('Wrong value for the row index: %d' % (row)) if (col < 0) or (col >= Board.SIZE): raise IndexError('Wrong value for the column index: %d' % (col)) gameStatus = GameStatus(data=self.boardData, turn=self.nextPlayer.symbol, status=self.status, error=Errors.NoError) # if self.isGameOver(): self.log.info('The game was over !') return gameStatus # if symbol != self.nextPlayer.symbol: self.log.warn("It is not the {0:d} turn's".format(symbol)) gameStatus.error = Errors.WrongTurn return gameStatus # if self.isLegalMove(row, col): self.log.debug('Place the symbol {0:d} at ({1:d}, {2:d})'. format(symbol, row, col)) self._board.set(row, col, symbol) self._moves.append((row, col, symbol)) self.status = self._computeGameStatus(row, col) if self.isGameOver(): self.log.info('The game is over : {0:d}'.format(self.status)) dispatcher.send(signal=Events.quit, uuid=self.uuid) else: self._updateNextSymbol() self.log.debug('Game.makeMove - next symbol is {0:d}'. format(self.nextPlayer.symbol)) gameStatus.turn = self.nextPlayer.symbol # if self.nextPlayer.symbol == self.aiPlayer.symbol: self._aiMove(row, col) else: self.log.error('Illegal move') gameStatus.error = Errors.IlegalMove gameStatus.data = self.boardData gameStatus.status = self.status return gameStatus def isLegalMove(self, row, col): """Checks if a piece can be placed on the board at a given position. Args: row (int): The row. col (int): The column. Returns: bool: True if the piece can be placed on the board, False otherwise. """ self.log.debug('Invoke isLegalMove with (%d, %d)' % (row, col)) symbol = self._board.get(row, col) self.log.debug('symbol = {0!s}'.format(symbol)) return (not self.isGameOver()) and (symbol == Symbol.Empty) def isGameOver(self): """Checks whether the game is over. Returns: bool: True if the game is over, False otherwise. """ self.log.debug('invoking isGameOver: status is {status:d}', status=self.status) return (self.status != Status.InProgress) def _aiMove(self, row, col): """Signals that AI player is to make the next move. Args row (int): The last row coordinate of the human move. col (int): The last column coordinate of the human move. """ dispatcher.send(signal=Events.aiMove, uuid=self.uuid, row=row, col=col) def _onAiMoveResponse(self, uuid, row, col): """Handles the Events.AiResponse signal.""" self.log.debug('_onAiMoveResponse: {uuid}, {row}, {col}', uuid=uuid, row=row, col=col) self.log.debug('_onAiMoveResponse: self.uuid = {uuid}', uuid=self.uuid) if str(uuid) != str(self.uuid): # it's not for this game: ignore the signal return self.log.debug('AI process responded with: row {row} and col {col}', row=row, col=col) # gameStatus = None if (row == -1) or (col == -1): self.log.debug('_onAiMoveResponse: no moves available') gameStatus = CopyGameStatus(GameStatus(status=Status.Tie)) else: gameStatus = CopyGameStatus(self.makeMove(row, col, self.aiPlayer.symbol)) # try to notify the client that the AI's turn has completed if self.listeners: for cbk in self.listeners: self.log.debug('calling onAiMoved on the remote object') d = cbk.callRemote('onAiMoved', row=row, col=col, results=gameStatus) d.addCallback(lambda _: self.log.debug('remote_onAiMoved succeeded')) d.addErrback(lambda reason: self.log.error('remote_onAiMoved failed: {reason}', reason=reason)) def _count(self, symbol, row, col, dirI, dirJ): """Counts the occurrences of a symbol along a given direction. While inside the bounds of the board go in forward and backward directions of the direction vector and increment the counter while still on the 'side' symbol. Args: symbol (int): The symbol. row (int): The row where the piece/symbol was placed on. col (int): The column where the piece/symbol was placed on. dirI (int): The row component of the direction vector. dirJ (int): The column component of the direction vector. Returns: int: The number of occurrences of the given symbol. """ count = 0 i = row j = col # forward direction while (i > -1) and (i < Board.SIZE) and \ (j > -1) and (j < Board.SIZE) and \ (self._board.get(i, j) == symbol): count += 1 i += dirI j += dirJ # backward direction i = row - dirI j = col - dirJ while (i > -1) and (i < Board.SIZE) and \ (j > -1) and (j < Board.SIZE) and \ (self._board.get(i, j) == symbol): count += 1 i -= dirI j -= dirJ self.log.debug('_count: count = {0:d}'.format(count)) return count def _computeGameStatus(self, row, col): """Checks if we have a winner or it's a tie. Args: row (int): The row. col (int): The column. Returns: The status of the game: Status.Tie, Status.X_Won or Status.O_Won. """ self.log.debug('_computeGameStatus: (%d, %d)' % (row, col)) if self._moves.count == (Board.SIZE ** 2): return Status.Tie symbol = self._board.get(row, col) self.log.debug('_computeGameStatus: symbol is %d' % (symbol)) if (self._count(symbol, row, col, 1, 0) == Board.SIZE): return self._winner(symbol) if (self._count(symbol, row, col, 0, 1) == Board.SIZE): return self._winner(symbol) if (self._count(symbol, row, col, 1, -1) == Board.SIZE): return self._winner(symbol) if (self._count(symbol, row, col, 1, 1) == Board.SIZE): return self._winner(symbol) return Status.InProgress def _winner(self, symbol): """ """ return Status.X_Won if symbol == Symbol.X else Status.O_Won def _updateNextSymbol(self): """Gets the player which can place a piece on the board. Args: Returns: Symbol: the player which makes the next move. """ self.nextPlayer = self.playerOne if self.nextPlayer == self.playerTwo \ else self.playerTwo
class Controller: """ Class controller. Plays a battleship game with official battleship rules between 2 human players. Used by GUI. === Private Attributes === # Variables to keep track of the game. _P1_Battle_Plan_Board: P1's ship locations _P1_Battle_Field_Board: P1's hits and misses _P2_Battle_Plan_Board: P2's ship locations _P2_Battle_Field_Board P2's hits and misses _whos_turn: who plays next """ def __init__(self): """ Initializes the controller with the appropriate boards for each player. """ self._P1_Battle_Plan_Board = Board(Board.P1) self._P1_Battle_Field_Board = Board(Board.P1) self._P2_Battle_Plan_Board = Board(Board.P2) self._P2_Battle_Field_Board = Board(Board.P2) self._whos_turn = Board.P1 # Player One starts def get_whos_turn(self): """ Return whose turn it is :return: P1 or P2 """ return self._whos_turn def place_ship(self, start: tuple[int, int], end: tuple[int, int], ship: str) -> bool: """ Place a ship on P1's or P2's Board. Return true if ship was placed. :param start: coordinate of the first piece of the ship :param end: coordinate of the last piece of the ship :param ship: the ship that's going to be placed. :return: True if ship was successfully placed False otherwise """ dx = 0 dy = 0 diff = 0 if start[0] == end[0]: diff = abs(end[1] - start[1]) if end[1] > start[1]: dy = 1 else: dy = -1 if start[1] == end[1]: diff = abs(end[0] - start[0]) dy = 0 if end[0] > start[0]: dx = 1 else: dx = -1 if (dx == 0 and dy == 0) or (diff != BattlePlan.get_ship_size(ship)): return False if self._whos_turn == Board.P1: return self._P1_Battle_Plan_Board.place_ship(start, ship, dx, dy) else: return self._P2_Battle_Plan_Board.place_ship(start, ship, dx, dy) def valid_move(self, player: str, coord: tuple[int, int]) -> bool: """ Check if coord is a valid move for player: inside the board and player has not hit there before (so that player does not hit twice the same place). :param player: :param coord: move location :return: True if valid, false otherwise """ if not self._P1_Battle_Field_Board.valid_coordinate(coord): return False # coord not inside board if player == Board.P1: if self._P1_Battle_Field_Board.get() != Board.EMPTY: return False return True else: if self._P2_Battle_Field_Board.get() != Board.EMPTY: return False return True def move_hit(self, player: str, coord: tuple[int, int]) -> bool: """ Coord is a VALID move Make a move for player Update number of hits Update Battle_Field_Board Return whether a ship was hit. :param coord: Where to hit on opponents Board :param player: Player that is hitting: either P1 or P2 :return: True if a ship was hit, False otherwise """ is_hit = False if player == Board.P1: if self._P2_Battle_Plan_Board.get(coord) != Board.EMPTY: self._P1_Battle_Field_Board.hit(coord) is_hit = True if player == Board.P2: if self._P1_Battle_Plan_Board.get(coord) != Board.EMPTY: self._P2_Battle_Field_Board.hit(coord) is_hit = True return is_hit def is_game_over(self) -> bool: """ Return whether the game is over or not Game is over if all the ships of the opponent were sunk ie, number of hits equals 17 :return: True or False """ return (self._P1_Battle_Field_Board.get_nb_of_hits() == 17) or\ (self._P2_Battle_Field_Board.get_nb_of_hits() == 17) def get_winner(self): """ Return the winner of the game :return: P1 or P2 """ if self._P1_Battle_Field_Board.get_nb_of_hits() == 17: return Board.P1 if self._P2_Battle_Field_Board.get_nb_of_hits() == 17: return Board.P2