164 lines
5.5 KiB
Python
164 lines
5.5 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:
|
|
"""
|
|
Represent a board for the game.
|
|
Boat can be added and bomb can be placed.
|
|
"""
|
|
|
|
__slots__ = ("width", "height", "boats", "bombs")
|
|
|
|
def __init__(
|
|
self,
|
|
width: int = None,
|
|
height: int = None,
|
|
|
|
boats: np.array = None,
|
|
bombs: np.array = None) -> None:
|
|
|
|
if (width is None or height is None) and (boats is None or bombs is None):
|
|
raise ValueError(f"{self.__class__}: width and height or boats and bombs should be set.")
|
|
|
|
# associate the boats and the bombs to array
|
|
self.boats: np.array = np.zeros((height, width), dtype=np.ushort) if boats is None else boats
|
|
self.bombs: np.array = np.ones((height, width), dtype=np.bool_) if bombs is None else bombs
|
|
|
|
# récupère la hauteur et la largeur
|
|
self.height, self.width = self.boats.shape
|
|
|
|
def __repr__(self) -> str:
|
|
return f"<{self.__class__.__name__} width={self.width} height={self.height}>"
|
|
|
|
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 old board matrice sum
|
|
board_matrice = self.boats.copy()
|
|
board_matrice_sum_old: int = board_matrice.sum()
|
|
board_matrice_max = np.max(board_matrice)
|
|
|
|
# get the sum of the boat
|
|
boat_matrice: np.array = boat.get_matrice(board_matrice_max+1)
|
|
boat_matrice_sum: int = boat_matrice.sum()
|
|
|
|
# add the boat to the board matrice
|
|
try:
|
|
copy_array_offset(boat_matrice, board_matrice, offset=position)
|
|
except ValueError:
|
|
raise InvalidBoatPosition(boat, position)
|
|
|
|
# get the new board matrice sum
|
|
board_matrice_sum_new: int = board_matrice.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_matrice_sum_old + boat_matrice_sum != board_matrice_sum_new:
|
|
raise InvalidBoatPosition(boat, position)
|
|
|
|
# otherwise accept the boat in the boats dict
|
|
self.boats = board_matrice
|
|
|
|
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.width or y >= self.height: raise InvalidBombPosition(position)
|
|
|
|
# if this position have already been shot
|
|
if not self.bombs[y, x]: 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[y, x] = 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 remove_bomb(self, cell: Point2D):
|
|
"""
|
|
Retire une bombe de la matrice
|
|
:param cell: cellule de la bombe
|
|
"""
|
|
x, y = cell
|
|
self.bombs[y, x] = True
|
|
|
|
def clear_bombs(self):
|
|
"""
|
|
Retire toutes les bombes de la planche
|
|
"""
|
|
self.bombs = np.ones(self.bombs.shape)
|
|
|
|
def get_matrice(self) -> np.array:
|
|
"""
|
|
:return: the boats and bombs represented as a matrice
|
|
"""
|
|
|
|
return self.boats * self.bombs # Remove the position that have been bombed
|
|
|
|
def get_score(self) -> int:
|
|
"""
|
|
:return: le score du joueur. (Nombre de bateau cassé)
|
|
"""
|
|
|
|
boat_total: int = np.count_nonzero(self.boats)
|
|
boat_left: int = np.count_nonzero(self.get_matrice())
|
|
|
|
return boat_total - boat_left
|
|
|
|
def to_json(self) -> dict:
|
|
return {
|
|
"boats": self.boats.tolist(),
|
|
"bombs": self.bombs.tolist()
|
|
}
|
|
|
|
@classmethod
|
|
def from_json(cls, json_: dict) -> "Board":
|
|
return Board(
|
|
boats=np.array(json_["boats"], dtype=np.ushort),
|
|
bombs=np.array(json_["bombs"], dtype=np.bool_)
|
|
)
|
|
|
|
def __copy__(self):
|
|
return self.__class__(
|
|
boats=self.boats.copy(),
|
|
bombs=self.bombs.copy(),
|
|
)
|