implemented a Board and a Boat for naval battle
This commit is contained in:
parent
d19ce65a72
commit
82fc77cc6e
13 changed files with 194 additions and 2 deletions
|
@ -20,4 +20,3 @@ class FPSCounterScene(Scene):
|
||||||
|
|
||||||
def on_draw(self, window: Window) -> None:
|
def on_draw(self, window: Window) -> None:
|
||||||
self.fps_display.draw()
|
self.fps_display.draw()
|
||||||
|
|
||||||
|
|
|
@ -1 +1,2 @@
|
||||||
pyglet
|
pyglet
|
||||||
|
numpy
|
||||||
|
|
119
src/Board.py
Normal file
119
src/Board.py
Normal file
|
@ -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)
|
||||||
|
|
||||||
|
|
29
src/Boat.py
Normal file
29
src/Boat.py
Normal file
|
@ -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())
|
2
src/__init__.py
Normal file
2
src/__init__.py
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
from .Boat import Boat
|
||||||
|
from .Board import Board
|
2
src/enum/__init__.py
Normal file
2
src/enum/__init__.py
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
from .orientation import Orientation
|
||||||
|
from .bomb import BombState
|
8
src/enum/bomb.py
Normal file
8
src/enum/bomb.py
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
from enum import Enum
|
||||||
|
|
||||||
|
|
||||||
|
class BombState(Enum):
|
||||||
|
NOTHING = 0
|
||||||
|
TOUCHED = 1
|
||||||
|
SUNKEN = 2
|
||||||
|
WON = 3
|
6
src/enum/orientation.py
Normal file
6
src/enum/orientation.py
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
from enum import Enum
|
||||||
|
|
||||||
|
|
||||||
|
class Orientation(Enum):
|
||||||
|
HORIZONTAL = "H"
|
||||||
|
VERTICAL = "V"
|
6
src/error/InvalidBoatPosition.py
Normal file
6
src/error/InvalidBoatPosition.py
Normal file
|
@ -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}.")
|
4
src/error/PositionAlreadyShot.py
Normal file
4
src/error/PositionAlreadyShot.py
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
class PositionAlreadyShot(Exception):
|
||||||
|
def __init__(self, position: tuple[int, int]):
|
||||||
|
super().__init__(f"The position {position} have already been shot.")
|
||||||
|
|
2
src/error/__init__.py
Normal file
2
src/error/__init__.py
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
from .InvalidBoatPosition import InvalidBoatPosition
|
||||||
|
from .PositionAlreadyShot import PositionAlreadyShot
|
1
src/utils/__init__.py
Normal file
1
src/utils/__init__.py
Normal file
|
@ -0,0 +1 @@
|
||||||
|
from .copy_array_offset import copy_array_offset
|
13
src/utils/copy_array_offset.py
Normal file
13
src/utils/copy_array_offset.py
Normal file
|
@ -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
|
Loading…
Reference in a new issue