Skip to content
This repository was archived by the owner on Apr 24, 2025. It is now read-only.

Commit cef722e

Browse files
committed
feat(Battleship): added abstract base class for the Game and created AlternatingGame & CLIGame concrete classes
feat(Battleship): added PrintMixin class for printing the board of a specified player
1 parent 34ea397 commit cef722e

2 files changed

Lines changed: 323 additions & 0 deletions

File tree

projects/Battleship/game.py

Lines changed: 295 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,295 @@
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()

projects/Battleship/utils.py

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22
import random
33

44
from ship import Ship
5+
from player import Player
6+
from board import Board
57

68

79
def generate_row_ship_cells(board_size: int) -> Generator[List[Tuple[int, int]], None, None]:
@@ -235,6 +237,32 @@ def attack_prompt(board_size: int,
235237
continue
236238

237239

240+
class PrintMixin:
241+
@staticmethod
242+
def print_player_board(player: Player, other_player: Player) -> None:
243+
"""
244+
Print the current battlefield situation and targets for the specified player.
245+
246+
Args:
247+
player (Player): The player whose perspective is being printed.
248+
other_player (Player): The opposing player.
249+
"""
250+
251+
player_board = other_player.enemy_board
252+
other_player_board = player.enemy_board
253+
254+
player_board_state = player_board.get_board_for_player()
255+
player_hit_or_miss_state = other_player_board.get_board_for_enemy()
256+
257+
print(f"{player.name} Battlefield Situation")
258+
Board.print_board(player_board_state)
259+
print()
260+
print(f"{player.name} Targets")
261+
Board.print_board(player_hit_or_miss_state)
262+
263+
print("\n")
264+
265+
238266
class CollectionUtilsMixin:
239267
"""Mixin class providing static methods for manipulating collections."""
240268

0 commit comments

Comments
 (0)