diff --git a/gui/scene/FPSCounterScene.py b/gui/scene/FPSCounterScene.py index cb963bc..f10c5b4 100644 --- a/gui/scene/FPSCounterScene.py +++ b/gui/scene/FPSCounterScene.py @@ -20,4 +20,3 @@ class FPSCounterScene(Scene): def on_draw(self, window: Window) -> None: self.fps_display.draw() - diff --git a/requirements.txt b/requirements.txt index b5f1cd7..5ed99d8 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1 +1,2 @@ -pyglet \ No newline at end of file +pyglet +numpy diff --git a/src/Board.py b/src/Board.py new file mode 100644 index 0000000..024e247 --- /dev/null +++ b/src/Board.py @@ -0,0 +1,119 @@ +import numpy as np + +from src import Boat +from src.enum import Orientation, BombState +from src.error import InvalidBoatPosition, PositionAlreadyShot +from src.utils import copy_array_offset + + +class Board: + __slots__ = ("width", "height", "_boats", "_bombs") + + def __init__(self, width: int, height: int = None) -> None: + self.width: int = width + self.height: int = width if height is None else height + self._boats: dict[Boat, tuple[int, int]] = {} # associate the boats to their position + self._bombs: np.array = np.ones((self.width, self.height), dtype=np.bool_) + + 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: tuple[int, int]) -> 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_sum: int = boat.get_matrice().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 + copy_array_offset(boat.get_matrice(), board_mat, offset=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_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: tuple[int, int]) -> BombState: + """ + Hit a position on the board + :position: the position where to shoot + :raise: PositionAlreadyShot if the position have already been shot before + """ + # 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.width, self.height), 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 + + +if __name__ == "__main__": + board = Board(5) + board.add_boat(Boat(3, Orientation.VERTICAL), (0, 4)) + board.add_boat(Boat(4, Orientation.HORIZONTAL), (4, 1)) + print(board.bomb((4, 1))) + print(board.bomb((4, 2))) + print(board.bomb((4, 3))) + print(board.bomb((4, 4))) + print(board) + + diff --git a/src/Boat.py b/src/Boat.py new file mode 100644 index 0000000..4bc76ed --- /dev/null +++ b/src/Boat.py @@ -0,0 +1,29 @@ +import numpy as np + +from src.enum import Orientation + + +class Boat: + __slots__ = ("orientation", "length") + + def __init__(self, length: int, orientation: Orientation): + self.orientation = orientation + self.length = length + + def get_matrice(self, value: int = 1) -> np.array: + """ + :return: the boat represented as a matrice + """ + return np.full( + (1, self.length) if self.orientation == Orientation.HORIZONTAL else + (self.length, 1), + value, + dtype=np.ushort + ) + + def __repr__(self): + return f"<{self.__class__.__name__} orientation={self.orientation}, length={self.length}>" + + +if __name__ == "__main__": + print(Boat(5, Orientation.VERTICAL).get_matrice()) diff --git a/src/__init__.py b/src/__init__.py new file mode 100644 index 0000000..d63e215 --- /dev/null +++ b/src/__init__.py @@ -0,0 +1,2 @@ +from .Boat import Boat +from .Board import Board diff --git a/src/enum/__init__.py b/src/enum/__init__.py new file mode 100644 index 0000000..4efc86b --- /dev/null +++ b/src/enum/__init__.py @@ -0,0 +1,2 @@ +from .orientation import Orientation +from .bomb import BombState diff --git a/src/enum/bomb.py b/src/enum/bomb.py new file mode 100644 index 0000000..faccddd --- /dev/null +++ b/src/enum/bomb.py @@ -0,0 +1,8 @@ +from enum import Enum + + +class BombState(Enum): + NOTHING = 0 + TOUCHED = 1 + SUNKEN = 2 + WON = 3 diff --git a/src/enum/orientation.py b/src/enum/orientation.py new file mode 100644 index 0000000..d5a9fb1 --- /dev/null +++ b/src/enum/orientation.py @@ -0,0 +1,6 @@ +from enum import Enum + + +class Orientation(Enum): + HORIZONTAL = "H" + VERTICAL = "V" diff --git a/src/error/InvalidBoatPosition.py b/src/error/InvalidBoatPosition.py new file mode 100644 index 0000000..733dfd2 --- /dev/null +++ b/src/error/InvalidBoatPosition.py @@ -0,0 +1,6 @@ +from src import Boat + + +class InvalidBoatPosition(Exception): + def __init__(self, boat: Boat, position: tuple[int, int]): + super().__init__(f"The boat {boat} can't be placed at {position}.") diff --git a/src/error/PositionAlreadyShot.py b/src/error/PositionAlreadyShot.py new file mode 100644 index 0000000..140525b --- /dev/null +++ b/src/error/PositionAlreadyShot.py @@ -0,0 +1,4 @@ +class PositionAlreadyShot(Exception): + def __init__(self, position: tuple[int, int]): + super().__init__(f"The position {position} have already been shot.") + diff --git a/src/error/__init__.py b/src/error/__init__.py new file mode 100644 index 0000000..03b0c70 --- /dev/null +++ b/src/error/__init__.py @@ -0,0 +1,2 @@ +from .InvalidBoatPosition import InvalidBoatPosition +from .PositionAlreadyShot import PositionAlreadyShot diff --git a/src/utils/__init__.py b/src/utils/__init__.py new file mode 100644 index 0000000..22d9bd9 --- /dev/null +++ b/src/utils/__init__.py @@ -0,0 +1 @@ +from .copy_array_offset import copy_array_offset diff --git a/src/utils/copy_array_offset.py b/src/utils/copy_array_offset.py new file mode 100644 index 0000000..5cf7248 --- /dev/null +++ b/src/utils/copy_array_offset.py @@ -0,0 +1,13 @@ +import numpy as np + + +def copy_array_offset(src: np.array, dst: np.array, offset: tuple[int, int]) -> None: + """ + Copy a numpy array into another one with an offset + :src: source array + :dst: destination array + :offset: the offset where to copy the array + """ + row, column = offset + width, height = src.shape + dst[row:row + width, column:column + height] = src