L3-Bataille-Navale/source/core/Board.py

161 lines
5.7 KiB
Python

import numpy as np
from source.core import Boat
from source.core.enums import Orientation, BombState
from source.core.error import InvalidBoatPosition, PositionAlreadyShot, InvalidBombPosition
from source.type import Point2D
from source.utils import copy_array_offset
class Board:
__slots__ = ("_columns", "_rows", "_boats", "_bombs")
def __init__(
self,
rows: int,
columns: int = None,
boats: dict[Boat, Point2D] = None,
bombs: np.array = None
) -> None:
self._rows: int = rows
self._columns: int = rows if columns is None else columns
# associate the boats to their position
self._boats: dict[Boat, Point2D] = {} if boats is None else boats
# position that have been shot by a bomb
self._bombs: np.array = np.ones((self._rows, self._columns), dtype=np.bool_) if bombs is None else bombs
def __repr__(self) -> str:
return f"<{self.__class__.__name__} width={self._columns}, height={self._rows}>"
def __str__(self) -> str:
return str(self.get_matrice())
def add_boat(self, boat: Boat, position: Point2D) -> None:
"""
Add a boat to the board. Check before if the position is valid.
:boat: the boat to add
:position: the position where to add the boat
:raise: InvalidBoatPosition if the boat position is not valid
"""
# get the sum of the boat
boat_mat: np.array = boat.get_matrice()
boat_mat_sum: int = boat_mat.sum()
# get the old board matrice sum
board_mat: np.array = self.get_matrice()
board_mat_sum_old: int = board_mat.sum()
# add the boat to the board matrice
try: copy_array_offset(boat_mat, board_mat, offset=position)
except ValueError: raise InvalidBoatPosition(boat, position)
# get the new board matrice sum
board_mat_sum_new: int = board_mat.sum()
# if the sum of the old board plus the boat sum is different from the new board sum,
# then the boat have been incorrectly placed (overlapping, outside of bounds, ...)
if board_mat_sum_old + boat_mat_sum != board_mat_sum_new: raise InvalidBoatPosition(boat, position)
# otherwise accept the boat in the boats dict
self._boats[boat] = position
def remove_boat(self, boat: Boat) -> None:
"""
Remove a boat from the boat dict
"""
self._boats.pop(boat)
def bomb(self, position: Point2D) -> BombState:
"""
Hit a position on the board
:position: the position where to shoot
:raise: PositionAlreadyShot if the position have already been shot before
"""
# if the bomb is inside the board
x, y = position
if x >= self._columns or y >= self._rows: raise InvalidBombPosition(position)
# if this position have already been shot
if not self._bombs[position]: raise PositionAlreadyShot(position)
# get the old board matrice
board_mat_old_sum = self.get_matrice().sum()
# place the bomb (setting the position to False cause the matrice multiplication to remove the boat if any)
self._bombs[position] = False
# get the new board matrice
board_mat_new = self.get_matrice()
board_mat_new_sum = board_mat_new.sum()
# if the board sum is 0, then there is no boat left on the board
if board_mat_new_sum == 0: return BombState.WON
# get the difference between the old and new board sum.
# if the board sum changed, then the difference is the number of the boat that have been hit
boat_touched: int = board_mat_old_sum - board_mat_new_sum
# if no boat have been touched, ignore
if boat_touched == 0: return BombState.NOTHING
# if the boat have sinked (no more tile with the boat on it)
if not np.isin(boat_touched, board_mat_new): return BombState.SUNKEN
# if the boat have been touched, but without sinking
return BombState.TOUCHED
def get_matrice(self) -> np.array:
"""
:return: the boat represented as a matrice
"""
board = np.zeros((self._rows, self._columns), dtype=np.ushort)
for index, (boat, position) in enumerate(self._boats.items(), start=1):
# Paste the boat into the board at the correct position.
# The boat is represented by a number representing its order in the boats list
copy_array_offset(boat.get_matrice(value=index), board, offset=position)
board *= self._bombs # Remove the position that have been bombed
return board
def to_json(self) -> dict:
return {
"columns": self._columns,
"rows": self._rows,
"boats": [[boat.to_json(), position] for boat, position in self._boats.items()],
"bombs": self._bombs.tolist()
}
@classmethod
def from_json(cls, json_: dict) -> "Board":
return Board(
rows=json_["columns"],
columns=json_["rows"],
boats={Boat.from_json(boat_json): tuple(position) for boat_json, position in json_["boats"]},
bombs=np.array(json_["bombs"], dtype=np.bool_)
)
def __copy__(self):
return self.__class__(
rows=self._rows,
columns=self._columns,
boats=self._boats.copy(),
bombs=self._bombs.copy(),
)
if __name__ == "__main__":
board = Board(5)
board.add_boat(Boat(3, Orientation.VERTICAL), (4, 0))
board.add_boat(Boat(4, Orientation.HORIZONTAL), (1, 4))
print(board.bomb((4, 1)))
print(board.bomb((4, 2)))
print(board.bomb((4, 3)))
print(board.bomb((4, 4)))
print(board)