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

Commit a16eb67

Browse files
committed
feat(Battleship): added utility module
1 parent 46a3469 commit a16eb67

1 file changed

Lines changed: 282 additions & 0 deletions

File tree

projects/Battleship/utils.py

Lines changed: 282 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,282 @@
1+
from typing import List, Generator, Tuple
2+
import random
3+
4+
from ship import Ship
5+
6+
7+
def generate_row_ship_cells(board_size: int) -> Generator[List[Tuple[int, int]], None, None]:
8+
"""
9+
Generator function to yield coordinates of ships aligned in rows.
10+
11+
Args:
12+
board_size (int): The size of the game board.
13+
14+
Yields:
15+
List[Tuple[int, int]]: List of tuples representing the coordinates of a ship aligned in a row.
16+
"""
17+
for ship_size in range(1, board_size + 1):
18+
for i in range(board_size): # Rows
19+
for j in range(board_size - ship_size + 1): # Columns
20+
coordinates = [(i, col) for col in range(j, j + ship_size)]
21+
yield coordinates
22+
23+
24+
def generate_column_ship_cells(board_size: int):
25+
"""
26+
Generator function to yield coordinates of ships aligned in columns.
27+
28+
Args:
29+
board_size (int): The size of the game board.
30+
31+
Yields:
32+
List[Tuple[int, int]]: List of tuples representing the coordinates of a ship aligned in a column.
33+
"""
34+
for ship_size in range(1, board_size + 1):
35+
for j in range(board_size): # Columns
36+
for i in range(board_size - ship_size + 1): # Rows
37+
coordinates = [(row, j) for row in range(i, i + ship_size)]
38+
yield coordinates
39+
40+
41+
def generate_random_row_ship(board_size: int, ship_length: int) -> Ship:
42+
"""
43+
Generates a random ship aligned in a row with the specified length.
44+
45+
Args:
46+
board_size (int): The size of the game board.
47+
ship_length (int): The length of the ship.
48+
49+
Returns:
50+
Ship: A Ship object representing the randomly generated ship aligned in a row.
51+
"""
52+
row_ships = [Ship(cells) for cells in generate_row_ship_cells(board_size) if len(cells) == ship_length]
53+
return random.choice(row_ships)
54+
55+
56+
def generate_random_column_ship(board_size: int, ship_length: int) -> Ship:
57+
"""
58+
Generates a random ship aligned in a column with the specified length.
59+
60+
Args:
61+
board_size (int): The size of the game board.
62+
ship_length (int): The length of the ship.
63+
64+
Returns:
65+
Ship: A Ship object representing the randomly generated ship aligned in a column.
66+
"""
67+
column_ships = [Ship(cells) for cells in generate_column_ship_cells(board_size) if len(cells) == ship_length]
68+
return random.choice(column_ships)
69+
70+
71+
def generate_random_ships_arrangements(board_size: int) -> List[Ship]:
72+
"""
73+
Generates a list of ships with random arrangements on the game board.
74+
75+
Args:
76+
board_size (int): The size of the game board.
77+
78+
Returns:
79+
List[Ship]: A list of Ship objects representing the randomly generated ships.
80+
"""
81+
ships = []
82+
83+
i = board_size
84+
while i > 0:
85+
row_or_col = random.choice(['row', 'column'])
86+
if row_or_col == 'row':
87+
ship = generate_random_row_ship(board_size, i)
88+
else:
89+
ship = generate_random_column_ship(board_size, i)
90+
91+
if any(ship.is_ship_overlap(s) for s in ships):
92+
continue
93+
94+
ships.append(ship)
95+
i -= 1
96+
97+
n = len(ships)
98+
assert (n * (n + 1)) / 2 == len(
99+
[tup for ship in ships for tup in ship.coordinates]), "Invalid Mathematical Assumption"
100+
101+
return ships
102+
103+
104+
class PromptMixin:
105+
"""Mixin class providing static methods for prompting user input."""
106+
107+
@staticmethod
108+
def prompt_board_size(prompt_message: str,
109+
error_message: str,
110+
min_board_size: int = 5,
111+
max_board_size: int = 15
112+
) -> int:
113+
"""
114+
Prompt the user to input the size of the game board within specified bounds.
115+
116+
Args:
117+
prompt_message (str): Message to prompt the user for input.
118+
error_message (str): Message to display in case of invalid input.
119+
min_board_size (int, optional): Minimum allowed board size. Defaults to 5.
120+
max_board_size (int, optional): Maximum allowed board size. Defaults to 15.
121+
122+
Returns:
123+
int: Size of the game board input by the user.
124+
"""
125+
126+
while True:
127+
try:
128+
board_size = int(input(prompt_message))
129+
130+
if board_size < min_board_size or board_size > max_board_size:
131+
print(error_message)
132+
continue
133+
134+
return board_size
135+
136+
except ValueError:
137+
print(error_message)
138+
continue
139+
140+
@staticmethod
141+
def prompt_name(prompt_message: str, error_message: str = 'Invalid Input! Please try again.\n') -> str:
142+
"""
143+
Prompt the user to input a name.
144+
145+
Args:
146+
prompt_message (str): Message to prompt the user for input.
147+
error_message (str, optional): Message to display in case of invalid input. Defaults to 'Invalid Input!
148+
Please try again.\n'.
149+
150+
Returns:
151+
str: Name input by the user.
152+
"""
153+
154+
while True:
155+
input_name = input(prompt_message)
156+
157+
# Check for invalid user input
158+
if input_name.strip() == "":
159+
print(error_message)
160+
continue
161+
162+
return input_name
163+
164+
@staticmethod
165+
def boolean_prompt(prompt_message: str,
166+
error_message: str = 'Invalid Input! Please try again.\n',
167+
true_str: str = 'yes',
168+
false_str: str = 'no') -> bool:
169+
"""
170+
Prompt the user to input a boolean value.
171+
172+
Args:
173+
prompt_message (str): Message to prompt the user for input.
174+
error_message (str, optional): Message to display in case of invalid input.
175+
Defaults to 'Invalid Input! Please try again.\n'.
176+
true_str (str, optional): String representing a true value. Defaults to 'yes'.
177+
false_str (str, optional): String representing a false value. Defaults to 'no'.
178+
179+
Returns:
180+
bool: Boolean value input by the user.
181+
"""
182+
183+
while True:
184+
bool_inp = input(prompt_message)
185+
186+
# Check for Invalid User Input
187+
if bool_inp.lower() not in [true_str.lower(), false_str.lower()]:
188+
print(error_message)
189+
continue
190+
191+
return bool_inp.lower() == true_str.lower()
192+
193+
@staticmethod
194+
def attack_prompt(board_size: int,
195+
prompt_message: str,
196+
error_message: str = 'Invalid Target! Pleas try again.',
197+
) -> Tuple[int, ...]:
198+
"""
199+
Prompt the user to input coordinates for an attack on the game board.
200+
201+
Args:
202+
board_size (int): Size of the game board.
203+
prompt_message (str): Message to prompt the user for input.
204+
error_message (str, optional): Message to display in case of invalid input.
205+
Defaults to 'Invalid Target! Please try again.'.
206+
207+
Returns:
208+
Tuple[int, ...]: Tuple containing the coordinates input by the user for the attack.
209+
"""
210+
211+
while True:
212+
attack_input = input(prompt_message)
213+
214+
if len(attack_input.split()) != 2:
215+
print(error_message)
216+
continue
217+
218+
try:
219+
attack_tuple = tuple(map(int, attack_input.split()))
220+
221+
invalid_conditions = \
222+
attack_tuple[0] < 0 or \
223+
attack_tuple[0] >= board_size or \
224+
attack_tuple[1] < 0 or \
225+
attack_tuple[1] >= board_size
226+
227+
if invalid_conditions:
228+
print(f'Attack Coordinates must be in [0, {board_size})\n' +
229+
error_message)
230+
continue
231+
232+
return attack_tuple
233+
except ValueError:
234+
print(error_message)
235+
continue
236+
237+
238+
class CollectionUtilsMixin:
239+
"""Mixin class providing static methods for manipulating collections."""
240+
241+
@staticmethod
242+
def alternate_tuples(list1, list2):
243+
"""
244+
Alternates the elements of two lists into a single list.
245+
246+
Args:
247+
list1: First list.
248+
list2: Second list.
249+
250+
Returns:
251+
List: Combined list with alternating elements from list1 and list2.
252+
"""
253+
result = []
254+
max_len = max(len(list1), len(list2))
255+
256+
for i in range(max_len):
257+
if i < len(list1):
258+
result.append(list1[i])
259+
if i < len(list2):
260+
result.append(list2[i])
261+
262+
return result
263+
264+
@staticmethod
265+
def shuffle(board_size) -> List[Tuple[int, int]]:
266+
"""
267+
Shuffles the board cells for one player.
268+
269+
Useful for generating random move sequence for one player.
270+
271+
Args:
272+
board_size (int): Size of the game board.
273+
274+
Returns:
275+
List[Tuple[int, int]]: Shuffled list of tuples representing board cells.
276+
"""
277+
278+
# Given the board size, generate random unique sequence of move for one player until all board
279+
# cells are generated.
280+
out_list = [(i, j) for i in range(board_size) for j in range(board_size)]
281+
random.shuffle(out_list) # In-place modification for shuffle
282+
return out_list

0 commit comments

Comments
 (0)