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

163 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 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(),
)
if __name__ == "__main__":
board = Board(5, 10)
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)
print(board.to_json())