|
| 1 | +from typing import List, Tuple, Generator |
| 2 | +from dataclasses import dataclass |
| 3 | + |
| 4 | +from exceptions import BoardException |
| 5 | +from ship import Ship |
| 6 | + |
| 7 | + |
| 8 | +@dataclass |
| 9 | +class BoardStatesPlayerPOV: |
| 10 | + """Cell's State Labels from Player's Point-of-View.""" |
| 11 | + missed: str = "M" |
| 12 | + hit: str = "H" |
| 13 | + ship: str = "S" |
| 14 | + unoccupied: str = "-" |
| 15 | + |
| 16 | + |
| 17 | +@dataclass |
| 18 | +class BoardStatesEnemyPOV: |
| 19 | + """Cell's State Labels from Enemy's Point-of-View.""" |
| 20 | + hit: str = "X" |
| 21 | + missed: str = "O" |
| 22 | + no_move: str = "-" |
| 23 | + |
| 24 | + |
| 25 | +class Board: |
| 26 | + """Represents the Battleship Board.""" |
| 27 | + |
| 28 | + def __init__(self, |
| 29 | + board_size: int, |
| 30 | + empty_label=" "): |
| 31 | + """ |
| 32 | + Initializes the Board object. |
| 33 | +
|
| 34 | + Args: |
| 35 | + board_size (int): The size of the game board. |
| 36 | + empty_label (str, optional): Label representing empty cells on the board. Defaults to " ". |
| 37 | +
|
| 38 | + Raises: |
| 39 | + BoardException: If an invalid board size is given. |
| 40 | + """ |
| 41 | + |
| 42 | + if board_size < 5 or board_size > 15: |
| 43 | + raise BoardException("Invalid given board size. Board size must be 5 to 15.") |
| 44 | + |
| 45 | + self.board_size = board_size |
| 46 | + self._empty_label = empty_label |
| 47 | + |
| 48 | + self._board = [[self._empty_label for _ in range(self.board_size)] for _ in range(self.board_size)] |
| 49 | + |
| 50 | + # Instantiate Labels for Player and Enemy POVs |
| 51 | + self._player_pov_labels = BoardStatesPlayerPOV() |
| 52 | + self._enemy_pov_labels = BoardStatesEnemyPOV() |
| 53 | + |
| 54 | + self.ships: List[Ship] = [] # List of Ships |
| 55 | + |
| 56 | + # Keep track of all enemy actions/moves for "Missed" and "No Move". |
| 57 | + # Hit can be obtained from `hit_cells` property. |
| 58 | + self._enemy_moves = { |
| 59 | + "hit": [], |
| 60 | + "missed": [] |
| 61 | + } |
| 62 | + |
| 63 | + @property |
| 64 | + def num_of_ships(self): |
| 65 | + """Returns the number of ships on the board.""" |
| 66 | + return len(self.ships) |
| 67 | + |
| 68 | + @property |
| 69 | + def dead_ships(self) -> List[Ship]: |
| 70 | + """Returns a list of destroyed ships.""" |
| 71 | + return [ship for ship in self.ships if ship.is_destroyed] |
| 72 | + |
| 73 | + @property |
| 74 | + def hit_cells(self) -> List[Tuple[int, int]]: |
| 75 | + """Returns a list of coordinates of cells hit by enemy.""" |
| 76 | + return [cell for ship in self.ships for cell in ship.hit_cells] |
| 77 | + |
| 78 | + @property |
| 79 | + def un_hit_cells(self) -> List[Tuple[int, int]]: |
| 80 | + """Returns a list of coordinates of un-hit cells.""" |
| 81 | + return [cell for ship in self.ships for cell in ship.un_hit_cells] |
| 82 | + |
| 83 | + @property |
| 84 | + def occupied_cells(self) -> List[Tuple[int, int]]: |
| 85 | + """Returns a list of coordinates of cells occupied by ships.""" |
| 86 | + return [cell for ship in self.ships for cell in ship.coordinates] |
| 87 | + |
| 88 | + @property |
| 89 | + def is_player_lost(self) -> bool: |
| 90 | + """Returns True if all player's ships are destroyed, otherwise False.""" |
| 91 | + return all(ship.is_destroyed for ship in self.ships) |
| 92 | + |
| 93 | + @property |
| 94 | + def valid_moves(self) -> List[Tuple[int, int]]: |
| 95 | + """Returns a list of available valid moves.""" |
| 96 | + all_cells_generator = ((i, j) for i in range(self.board_size) for j in range(self.board_size)) |
| 97 | + return [cell for cell in all_cells_generator if |
| 98 | + cell not in self._enemy_moves["hit"] + self._enemy_moves["missed"]] |
| 99 | + |
| 100 | + def generate_valid_moves(self) -> Generator[Tuple[int, int], None, None]: |
| 101 | + """Yields all the available valid moves.""" |
| 102 | + board = self.get_board_for_enemy() |
| 103 | + for i in range(self.board_size): |
| 104 | + for j in range(self.board_size): |
| 105 | + if board[i][j] == self._enemy_pov_labels.no_move: |
| 106 | + # yield board[i][j] |
| 107 | + yield i, j |
| 108 | + |
| 109 | + def get_board_for_player(self) -> List[List[str]]: |
| 110 | + """Gets the Board States from Player's POV.""" |
| 111 | + # Initialize an empty board. |
| 112 | + board = self._create_empty_board() |
| 113 | + |
| 114 | + # Fill with Ships' Positions |
| 115 | + for ship in self.ships: |
| 116 | + for row, col in ship.coordinates: |
| 117 | + board[row][col] = self._player_pov_labels.ship |
| 118 | + |
| 119 | + # Fill with Hit |
| 120 | + for (row, col) in self._enemy_moves["hit"]: |
| 121 | + board[row][col] = self._player_pov_labels.hit |
| 122 | + |
| 123 | + # Fill with Missed |
| 124 | + for (row, col) in self._enemy_moves["missed"]: |
| 125 | + board[row][col] = self._player_pov_labels.missed |
| 126 | + |
| 127 | + # Fill with Unoccupied |
| 128 | + for row in range(self.board_size): |
| 129 | + for col in range(self.board_size): |
| 130 | + if board[row][col] == self._empty_label: |
| 131 | + board[row][col] = self._player_pov_labels.unoccupied |
| 132 | + |
| 133 | + return board |
| 134 | + |
| 135 | + def get_board_for_enemy(self) -> List[List[str]]: |
| 136 | + """Gets the Board States from Enemy's POV.""" |
| 137 | + # Initialize an empty board. |
| 138 | + board = self._create_empty_board() |
| 139 | + |
| 140 | + # Fill with Hit |
| 141 | + for row, col in self._enemy_moves["hit"]: |
| 142 | + board[row][col] = self._enemy_pov_labels.hit |
| 143 | + |
| 144 | + # Fill with Missed |
| 145 | + for row, col in self._enemy_moves["missed"]: |
| 146 | + board[row][col] = self._enemy_pov_labels.missed |
| 147 | + |
| 148 | + # Fill with No Moves |
| 149 | + for row in range(self.board_size): |
| 150 | + for col in range(self.board_size): |
| 151 | + if board[row][col] == self._empty_label: |
| 152 | + board[row][col] = self._enemy_pov_labels.no_move |
| 153 | + |
| 154 | + return board |
| 155 | + |
| 156 | + def _create_empty_board(self) -> List[List[str]]: |
| 157 | + """Creates an empty board.""" |
| 158 | + return [[self._empty_label for _ in range(self.board_size)] for _ in range(self.board_size)] |
| 159 | + |
| 160 | + @staticmethod |
| 161 | + def print_board(board: List[List[str]]) -> None: |
| 162 | + """Prints a game board and it's states.""" |
| 163 | + board_size = len(board) |
| 164 | + # Print the Column Numbers for first row (0 to board_size - 1) |
| 165 | + print(f" ", end="") # Print the Initial Spaces |
| 166 | + for col in range(board_size): |
| 167 | + if col > 9: |
| 168 | + print(f" {col}", end="") # For 0 to 9 |
| 169 | + else: |
| 170 | + print(f" {col}", end="") # For 10 to 14 |
| 171 | + print() # new line |
| 172 | + |
| 173 | + for row in range(board_size): |
| 174 | + if row > 9: |
| 175 | + print(f"{row} ", end="") |
| 176 | + else: |
| 177 | + print(f" {row} ", end="") |
| 178 | + for col in range(board_size): |
| 179 | + if col > 9: |
| 180 | + print(f" {board[row][col]}", end="") |
| 181 | + else: |
| 182 | + print(f" {board[row][col]} ", end="") |
| 183 | + |
| 184 | + if col < board_size - 1: |
| 185 | + if col > 9: |
| 186 | + print(" |", end="") # Separate cells with | |
| 187 | + else: |
| 188 | + print("|", end="") # Separate cells with | |
| 189 | + print() # Move to the next line after printing the row |
| 190 | + |
| 191 | + def place_ship(self, coordinates: List[Tuple[int, int]]) -> None: |
| 192 | + """Places a ship on the board.""" |
| 193 | + # Check if player can still place a ship based on the board & ship size constraints. |
| 194 | + if self.num_of_ships > self.board_size: |
| 195 | + raise BoardException("Cannot place another ship on the board.") |
| 196 | + |
| 197 | + # Check if there's an invalid cell/coordinate to place a ship. |
| 198 | + for row, col in coordinates: |
| 199 | + if row < 0 or row >= self.board_size or col < 0 or col >= self.board_size: |
| 200 | + raise BoardException(f"Invalid cell to place a ship: ({row}, {col})") |
| 201 | + |
| 202 | + # Check if a cell is already occupied. |
| 203 | + if not all(not self._is_cell_occupied(*cell) for cell in coordinates): |
| 204 | + raise BoardException("Some of the given cells are already occupied.") |
| 205 | + |
| 206 | + # Create a New Ship |
| 207 | + new_ship = Ship(coordinates) |
| 208 | + |
| 209 | + # Add the ship to the list |
| 210 | + self.ships.append(new_ship) |
| 211 | + |
| 212 | + def _is_cell_occupied(self, row: int, col: int) -> bool: |
| 213 | + """Returns True if a cell is occupied by another ship, otherwise False.""" |
| 214 | + return any((row, col) in ship.coordinates for ship in self.ships) |
| 215 | + |
| 216 | + def which_ship(self, row: int, col: int) -> None | Ship: |
| 217 | + """Returns the Ship that occupies the cell. Otherwise, None.""" |
| 218 | + for ship in self.ships: |
| 219 | + if (row, col) in ship.coordinates: |
| 220 | + return ship |
| 221 | + return None |
| 222 | + |
| 223 | + def enemy_move(self, row: int, col: int) -> None: |
| 224 | + """Performs an enemy's move.""" |
| 225 | + ship: None | Ship = self.which_ship(row, col) |
| 226 | + |
| 227 | + if isinstance(ship, Ship): |
| 228 | + # Update states in the ship |
| 229 | + ship.hit_ship(row, col) |
| 230 | + self._enemy_moves["hit"].append((row, col)) |
| 231 | + else: |
| 232 | + self._enemy_moves["missed"].append((row, col)) |
0 commit comments