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

Commit acd6b20

Browse files
committed
feat(Battleship): added board module
1 parent a16eb67 commit acd6b20

1 file changed

Lines changed: 232 additions & 0 deletions

File tree

projects/Battleship/board.py

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

Comments
 (0)