I have an assignment in a python course. We were instructed to make a 3D Battleships game for 2 players on the same computer. I've written a code that works, according to the assignment instructions, but I think it looks a little messy. I'm a beginner. Any suggestions to make the code simpler and tidier?
import numpy as np
from enum import Enum
class Input(Enum):
MISS = 'Miss'
HIT = 'Hit'
KILL = 'Kill'
END = 'End'
class Pieces:
""" all of the game pieces base class """
def __init__(self, item_id: int):
self.item_id = item_id
self.outline = np.array([0])
self.locs = None
@property
def shape(self):
return self.outline.shape
@property
def filled_outline(self):
return self.outline * self.item_id
def __repr__(self):
return f"{self.__class__.__name__}{self.item_id}"
class General(Pieces):
"""A number is submitted as input for compatibility, which is the game's determining piece"""
def __init__(self, item_id: int):
super().__init__(item_id)
self.outline = np.array([[1]], dtype=np.int16)
self.airplane = np.random.randint(3)
def hit(self, coord):
return Input.END
class Jet(Pieces):
""" Class for the airplane vessels """
def __init__(self, item_id: int):
super().__init__(item_id)
self.outline = np.array([[0, 1, 0], [1, 1, 1], [0, 1, 0], [0, 1, 0]], dtype=np.int16)
self.airplane = 2 # air
def hit(self, coord):
return Input.KILL
class Destroyer(Pieces):
""" class for ships 'Destroyers'"""
def __init__(self, item_id: int):
super().__init__(item_id)
self.outline = np.array([[1], [1], [1], [1]], dtype=np.int16)
self.airplane = 1 # Sea-level
def hit(self, cord: tuple):
cord = list(cord)
try:
self.locs.remove(cord)
except ValueError:
print(f"This cordination is not found or already targeted -{cord}")
return Input.MISS
else:
if len(self.locs) == 0:
return Input.KILL
else:
return Input.HIT
class Submarine(Pieces):
""" a Submarine vessel """
def __init__(self, item_id: int):
super().__init__(item_id)
self.outline = np.array([[1, 1, 1]], dtype=np.int16)
self.airplane = 0
def hit(self, coord):
return Input.KILL
class GamePieces(Enum):
""" the pieces type in the game"""
General = General
JETS = Jet
DESTROYERS = Destroyer
SUBMARINES = Submarine
class gameBoard(np.ndarray):
"""
A three dimensional board that holds the pieces.
"""
def __new__(subtype, shape: tuple=(10, 10, 3), dtype: np.dtype=np.object, *,
pieces: list):
if len(shape) != 3:
return ValueError(f"Shape received was {shape}, but it must have exactly 3 items.")
obj = super(gameBoard, subtype).__new__(subtype, shape, dtype)
obj.pieces = pieces
obj[:] = 0
return obj
def __array_finalize__(self, obj):
""" A unique method which is run after initialization """
if obj is None: return
default_pieces = [piece.value(item_id) for item_id, piece in enumerate(GamePieces)]
self.pieces = getattr(obj, 'pieces', default_pieces)
def pieces_places(self):
"""
Verifies that the piece was not put outside the board's bounds and that the cells were empty.
"""
for piece in self.pieces:
placed = False
trial = 0
while not placed and trial < 50:
row_idx = np.random.choice(self.shape[0])
col_idx = np.random.choice(self.shape[1])
cur_slice = (slice(row_idx, row_idx + piece.shape[0]),
slice(col_idx, col_idx + piece.shape[1]),
slice(piece.airplane, piece.airplane + 1))
try:
cur_subboard = self[cur_slice].copy()
assert cur_subboard.shape[:2] == piece.shape
assert np.all(cur_subboard == 0)
placed = True
except (IndexError, AssertionError):
pass
finally:
trial += 1
if trial >= 50:
raise UserWarning("Board is too small for all pieces.")
self[cur_slice] += np.atleast_3d(piece.filled_outline)
piece.locs = np.argwhere(self == piece.item_id).tolist()
self[self == piece.item_id] = piece
def check_if_hit(self, coord: tuple) -> Input:
"""
Receive a 3D coordinate of the location that was targeted.
Then check the board which piece is there and return the Input.
:param coord: Tuple of 3 coordinates
:return: Input
"""
cell = self[coord]
if cell != 0:
sig = cell.hit(coord)
if sig is Input.KILL:
self.pieces.remove(cell)
if len(self.pieces) == 1:
sig = Input.END
else:
sig = Input.MISS
return sig
class SubmarinesGame:
"""
Play a Submarines game for two players. Run it with the start() method.
Supply a tuple defining the 3D board size, and adictionary of unit names (from GamePieces) and values,
corresponding to the number of units you wish to have of that size.
At each turn you're prompted to enter a length-3 tuple with the coordinate you're targeting.
You'll be notified if you missed, hit or killed a unit. You can also write "show" to show the board,
and "quit" to stop the game.
"""
def __init__(self, board_shape: tuple=(10, 10, 3), pieces: dict=None):
self.board_shape = board_shape
self.pieces = pieces
self.piece_list_1 = []
self.piece_list_2 = []
self.__validate_input()
self.__generate_pieces_list()
self.board1 = gameBoard(self.board_shape, pieces=self.piece_list_1)
self.board2 = gameBoard(self.board_shape, pieces=self.piece_list_2)
self.players = ("Player 1", "Player 2")
self.boards = (self.board2, self.board1)
self.move = 0
def __validate_input(self):
assert len(self.board_shape) == 3
assert self.board_shape[2] == 3 # only 3D inputs
assert self.board_shape[0] >= 4 and self.board_shape[1] >= 4
if self.pieces is not None:
for piece, val in self.pieces.items():
assert piece in GamePieces
assert isinstance(val, int)
assert val >= 0 and val <= 50
assert GamePieces.General in self.pieces
assert self.pieces[GamePieces.General] == 1
def __generate_pieces_list(self):
""" Populate the list of pieces """
if self.pieces is not None:
item_id = 1
for piece, num in self.pieces.items():
for cur_instance in range(num):
self.piece_list_1.append(piece.value(item_id))
self.piece_list_2.append(piece.value(item_id))
item_id += 1
def __assert_coor_in_board(self, coord):
coor_list = []
for char in coord:
if char.isdigit():
coor_list.append(int(char))
coord = tuple(coor_list)
try:
assert len(coord) == 3
assert coord[0] < self.board1.shape[0] \
and coord[1] < self.board1.shape[1] \
and coord[2] < self.board1.shape[2]
except AssertionError:
print(f"Coordinate {coord} is located outside the board. Board shape is {self.board1.shape}.")
return False
else:
return coord
def __parse_input(self):
""" Helper function to parse the input from the user """
coor = input(f"{self.players[self.move % 2]} this is your turn. type you target! for example: (1,1,1) ")
if coor == 'quit':
raise SystemExit("Exiting")
if coor == 'show':
board = self.boards[(self.move-1) % 2]
print("Under-water:\n", board[..., 0])
print("Sea-level:\n", board[..., 1])
print("Air:\n", board[..., 2])
return False
coor = self.__assert_coor_in_board(coor)
return coor
def start(self):
""" Battleships game """
print("This is a game of Battleships\n")
print(f"The board dimensions are {self.board1.shape} and the pieces are assigned randomly.")
print("You can type 'show' to show your board, and 'quit' to exit the game.")
for board in self.boards:
board.pieces_places()
ret_Input = 0
while ret_Input != Input.END:
coor = False
while not coor:
coor = self.__parse_input()
ret_Input = self.boards[self.move % 2].check_if_hit(coor)
print(ret_Input)
self.move += 1
print(f"The game is over! The winner is {self.players[(self.move - 1) % 2]}.")
if __name__ == '__main__':
pieces = {GamePieces.General: 1,
GamePieces.JETS: 1,
GamePieces.DESTROYERS: 1,
GamePieces.SUBMARINES: 1}
subgame = SubmarinesGame(board_shape=(6, 6, 3), pieces=pieces)
subgame.start()