|
| 1 | +from typing import List, Tuple |
| 2 | +from abc import ABC, abstractmethod |
| 3 | +import random |
| 4 | + |
| 5 | +from utils import CollectionUtilsMixin, PrintMixin, PromptMixin |
| 6 | +from player import Player |
| 7 | +from board import Board |
| 8 | + |
| 9 | + |
| 10 | +class Game(ABC): |
| 11 | + """Abstract base class for the Battleship game.""" |
| 12 | + |
| 13 | + MAX_BOARD_SIZE = 15 |
| 14 | + MIN_BOARD_SIZE = 5 |
| 15 | + |
| 16 | + def __init__(self, |
| 17 | + board_size: int, |
| 18 | + player_1_name: str, |
| 19 | + player_2_name: str |
| 20 | + ): |
| 21 | + """ |
| 22 | + Initialize the game with the given parameters. |
| 23 | +
|
| 24 | + Args: |
| 25 | + board_size (int): The size of the game board. |
| 26 | + player_1_name (str): Name of the first player. |
| 27 | + player_2_name (str): Name of the second player. |
| 28 | +
|
| 29 | + To-Do for Implementer: |
| 30 | + - Initialize first player mover (i.e. self._current_player). |
| 31 | + - Implement place_ships() and use it to place the ships on the Board for each player. |
| 32 | + - Implement the game's main loop. This can be used for different UIs. |
| 33 | + """ |
| 34 | + |
| 35 | + self.player_1: Player = Player(player_1_name, Board(board_size)) |
| 36 | + self.player_2: Player = Player(player_2_name, Board(board_size)) |
| 37 | + self.board_size: int = board_size |
| 38 | + |
| 39 | + self._current_player: Player | None = None |
| 40 | + |
| 41 | + @property |
| 42 | + def current_player(self) -> Player | None: |
| 43 | + """Gets the current player.""" |
| 44 | + return self._current_player |
| 45 | + |
| 46 | + @current_player.setter |
| 47 | + def current_player(self, value): |
| 48 | + """Sets the current player.""" |
| 49 | + self._current_player = value |
| 50 | + |
| 51 | + @property |
| 52 | + def previous_player(self) -> Player | None: |
| 53 | + """Gets the previous player.""" |
| 54 | + return self.player_1 if self._current_player.name == self.player_2.name else self.player_2 |
| 55 | + |
| 56 | + def get_winner(self) -> Player | None: |
| 57 | + """ |
| 58 | + Get the winner of the game. |
| 59 | +
|
| 60 | + Returns: |
| 61 | + Player | None: The winning player or None if there is no winner yet. |
| 62 | + """ |
| 63 | + if self.player_1.enemy_board.is_player_lost: |
| 64 | + return self.player_1 |
| 65 | + elif self.player_2.enemy_board.is_player_lost: |
| 66 | + return self.player_2 |
| 67 | + else: |
| 68 | + return None |
| 69 | + |
| 70 | + def update_player(self) -> None: |
| 71 | + """Update the current player to the next one.""" |
| 72 | + self._current_player = self.player_1 if self._current_player.name == self.player_2.name else self.player_2 |
| 73 | + |
| 74 | + def make_current_player_attack(self, row: int, col: int) -> Player | None: |
| 75 | + """ |
| 76 | + Make the current player's attack. |
| 77 | +
|
| 78 | + Args: |
| 79 | + row (int): The row index of the attack. |
| 80 | + col (int): The column index of the attack. |
| 81 | +
|
| 82 | + Returns: |
| 83 | + Player | None: The winning player or None if there is no winner yet. |
| 84 | +
|
| 85 | + Raises: |
| 86 | + ValueError: If the first player to move was not initialized. |
| 87 | + """ |
| 88 | + if self.current_player is None: |
| 89 | + raise ValueError('First Player to move was not initialized.') |
| 90 | + |
| 91 | + # Make Current Player Attack. Note that exceptions will be thrown from .attack() method |
| 92 | + self.current_player.attack(row, col) |
| 93 | + |
| 94 | + # Check if there is a Winner |
| 95 | + winner_or_none = self.get_winner() |
| 96 | + if winner_or_none is not None: |
| 97 | + return winner_or_none |
| 98 | + |
| 99 | + # Update the Current Player |
| 100 | + self.update_player() |
| 101 | + |
| 102 | + @abstractmethod |
| 103 | + def run(self, *args, **kwargs): |
| 104 | + """Abstract method to run the game.""" |
| 105 | + pass |
| 106 | + |
| 107 | + @abstractmethod |
| 108 | + def place_ships(self, *args, **kwargs): |
| 109 | + """Abstract method to place ships on the board.""" |
| 110 | + pass |
| 111 | + |
| 112 | + |
| 113 | +class AlternatingGame(Game, CollectionUtilsMixin, PrintMixin): |
| 114 | + """ |
| 115 | + Battleship Game with given alternative moves. |
| 116 | +
|
| 117 | + Ad-hoc Setup |
| 118 | + ------------ |
| 119 | + game = AlternatingGame(...) |
| 120 | + game.place_ships("<player_1>", [[(...), (...), ...], [(...), (...), ...], ...]) |
| 121 | + game.place_ships("<player_2>", [[(...), (...), ...], [(...), (...), ...], ...]) |
| 122 | + game.set_moves([(...), (...), ...], [(...), (...), ...]) |
| 123 | + game.run(...) |
| 124 | + """ |
| 125 | + |
| 126 | + PLAYER_1_MOVES = None |
| 127 | + PLAYER_2_MOVES = None |
| 128 | + |
| 129 | + def set_moves(self, player_1_moves: List[Tuple[int, int]], player_2_moves: List[Tuple[int, int]]) -> None: |
| 130 | + """ |
| 131 | + Set the moves for players. |
| 132 | +
|
| 133 | + Args: |
| 134 | + player_1_moves (List[Tuple[int, int]]): List of moves for player 1. |
| 135 | + player_2_moves (List[Tuple[int, int]]): List of moves for player 2. |
| 136 | +
|
| 137 | + Raises: |
| 138 | + ValueError: If the moves for players are already set. |
| 139 | + """ |
| 140 | + if self.PLAYER_1_MOVES is not None or self.PLAYER_2_MOVES is not None: |
| 141 | + raise ValueError(f'The moves for {self.player_1.name} and {self.player_2.name} are already set.') |
| 142 | + |
| 143 | + # Check the validity of the given moves |
| 144 | + assert len(player_1_moves) == self.board_size ** 2, f'The number of moves must be {self.board_size ** 2}' |
| 145 | + assert len(player_2_moves) == self.board_size ** 2, f'The number of moves must be {self.board_size ** 2}' |
| 146 | + |
| 147 | + # Check for Duplicates and Out-of-Bound |
| 148 | + def all_cells_(): |
| 149 | + return [(i, j) for i in range(self.board_size) for j in range(self.board_size)] |
| 150 | + |
| 151 | + for player_moves in [player_1_moves, player_2_moves]: |
| 152 | + all_cells = all_cells_() |
| 153 | + for cell in player_moves: |
| 154 | + if cell not in all_cells: |
| 155 | + raise ValueError(f'The cell {cell} is invalid.') |
| 156 | + all_cells.remove(cell) |
| 157 | + if len(all_cells) != 0: |
| 158 | + raise ValueError('There may be duplicates in the given moves.') |
| 159 | + |
| 160 | + self.PLAYER_1_MOVES = player_1_moves |
| 161 | + self.PLAYER_2_MOVES = player_2_moves |
| 162 | + |
| 163 | + def run(self, initial_player_name: str): |
| 164 | + """ |
| 165 | + Run the alternating game. |
| 166 | +
|
| 167 | + Args: |
| 168 | + initial_player_name (str): Name of the player who will make the first attack move. |
| 169 | +
|
| 170 | + Raises: |
| 171 | + ValueError: If the given player does not belong to this game. |
| 172 | + """ |
| 173 | + if initial_player_name not in [self.player_1.name, self.player_2.name]: |
| 174 | + raise ValueError(f'The given player "{initial_player_name}" does not belong to this Game.') |
| 175 | + |
| 176 | + self.current_player = self.player_1 if initial_player_name == self.player_1.name else self.player_2 |
| 177 | + |
| 178 | + winner_or_none = self.get_winner() |
| 179 | + while winner_or_none is None: # Stop the loop when there is a winner |
| 180 | + move = None |
| 181 | + if self.current_player == self.player_1: |
| 182 | + move = self.PLAYER_1_MOVES.pop(0) |
| 183 | + elif self.current_player == self.player_2: |
| 184 | + move = self.PLAYER_2_MOVES.pop(0) |
| 185 | + |
| 186 | + assert move is not None, "The move was not updated." |
| 187 | + |
| 188 | + self.make_current_player_attack(*move) |
| 189 | + winner_or_none = self.get_winner() |
| 190 | + |
| 191 | + print('Winner:', winner_or_none.name) |
| 192 | + |
| 193 | + def place_ships(self, player_name: str, ships_coordinates: List[List[Tuple[int, int]]]) -> None: |
| 194 | + """ |
| 195 | + Place ships on the board for the specified player. |
| 196 | +
|
| 197 | + Args: |
| 198 | + player_name (str): The name of the player for whom the ships are to be placed. |
| 199 | + ships_coordinates (List[List[Tuple[int, int]]]): A list of ship coordinates to be placed on the board. |
| 200 | +
|
| 201 | + Raises: |
| 202 | + ValueError: If the given player does not belong to this ConcreteGame. |
| 203 | + """ |
| 204 | + if player_name not in [self.player_1.name, self.player_2.name]: |
| 205 | + raise ValueError('The given player does not belong to this ConcreteGame.') |
| 206 | + |
| 207 | + if player_name == self.player_1.name: |
| 208 | + for ship_coordinates in ships_coordinates: |
| 209 | + # Use player_2's enemy_board to place the ships for player_1 |
| 210 | + self.player_2.place_ship(ship_coordinates) |
| 211 | + elif player_name == self.player_2.name: |
| 212 | + for ship_coordinates in ships_coordinates: |
| 213 | + # Use player_1's enemy_board to place the ships for player_2 |
| 214 | + self.player_1.place_ship(ship_coordinates) |
| 215 | + |
| 216 | + |
| 217 | +class CLIGame(Game, PrintMixin, PromptMixin): |
| 218 | + """ |
| 219 | + CLI-based Battleship game. |
| 220 | +
|
| 221 | + This class represents a Battleship game that is played in the command-line interface (CLI). |
| 222 | + It inherits from the `Game` class and includes methods for setting up the game, running the game loop, |
| 223 | + and placing ships on the board. |
| 224 | +
|
| 225 | + Usage: |
| 226 | + game = CLIGame() |
| 227 | + game.run() |
| 228 | + """ |
| 229 | + |
| 230 | + def __init__(self): |
| 231 | + board_size = self.prompt_board_size(f'Enter Board Size >>> ', |
| 232 | + 'Invalid Board Size Input.\n') |
| 233 | + player_1_name = self.prompt_name("Please Enter the Name for Player 1 >>> ") |
| 234 | + self.play_with_random_player = self.boolean_prompt('Do you want to play with a bot? (yes/no) >>> ') |
| 235 | + player_2_name = self.prompt_name("Please Enter the Name for Player 2 >>> ") |
| 236 | + |
| 237 | + super().__init__(board_size, player_1_name, player_2_name) |
| 238 | + |
| 239 | + # Place the Ships Randomly |
| 240 | + print('Randomly placing ship ...') |
| 241 | + self.place_ships() |
| 242 | + |
| 243 | + # Select the First Mover Randomly |
| 244 | + self.current_player = random.choice([self.player_1, self.player_2]) |
| 245 | + print(f'Player {self.current_player.name} moves first.') |
| 246 | + |
| 247 | + def run(self): |
| 248 | + """Start the game loop and handle player moves until a winner is determined.""" |
| 249 | + |
| 250 | + while True: |
| 251 | + try: |
| 252 | + # Play with a Bot |
| 253 | + if self.play_with_random_player: # Assume that Player 2 is the Random Bot. |
| 254 | + # Check if current player is player_2 |
| 255 | + if self.current_player == self.player_2: |
| 256 | + # Make Random Valid Attack |
| 257 | + valid_moves = [move for move in self.current_player.enemy_board.generate_valid_moves()] |
| 258 | + attack_coordinate = random.choice(valid_moves) |
| 259 | + print(f'{self.player_2.name} (Bot) is attacking on {attack_coordinate} ...\n\n') |
| 260 | + winner_or_none = self.make_current_player_attack(*attack_coordinate) |
| 261 | + |
| 262 | + # Player 1's move |
| 263 | + else: |
| 264 | + attack_coordinate = \ |
| 265 | + self.attack_prompt(self.board_size, |
| 266 | + f'Please Enter the Attack Coordinates Admiral {self.current_player.name} >>> ', |
| 267 | + 'Invalid Attack Coordinate') |
| 268 | + winner_or_none = self.make_current_player_attack(*attack_coordinate) |
| 269 | + |
| 270 | + # Print Board State. When we make a move, it will automatically update for the next player |
| 271 | + self.print_player_board(self.previous_player, self.current_player) |
| 272 | + else: |
| 273 | + attack_coordinate = \ |
| 274 | + self.attack_prompt(self.board_size, |
| 275 | + f'Please Enter the Attack Coordinates Admiral {self.current_player.name} >>> ', |
| 276 | + 'Invalid Attack Coordinate') |
| 277 | + |
| 278 | + winner_or_none = self.make_current_player_attack(*attack_coordinate) |
| 279 | + |
| 280 | + # Print Board State. When we make a move, it will automatically update for the next player |
| 281 | + self.print_player_board(self.previous_player, self.current_player) |
| 282 | + |
| 283 | + if winner_or_none is not None: |
| 284 | + print(f'Player {winner_or_none.name} won the war!!!') |
| 285 | + break |
| 286 | + except Exception as e: |
| 287 | + print(e) |
| 288 | + continue |
| 289 | + |
| 290 | + def place_ships(self): |
| 291 | + """Randomly place ships for both players.""" |
| 292 | + self.player_1.generate_random_ship_arrangements() |
| 293 | + self.player_2.generate_random_ship_arrangements() |
| 294 | + self.player_1.generate_random_ship_arrangements() |
| 295 | + self.player_2.generate_random_ship_arrangements() |
0 commit comments