L3-Bataille-Navale/source/gui/widget/GameGrid.py

297 lines
10 KiB
Python

from copy import copy
from typing import TYPE_CHECKING, Type
import numpy as np
import pyglet.shapes
from source.core import Board, Boat
from source.core.enums import Orientation, BombState
from source.core.error import InvalidBoatPosition
from source.gui.sprite import Sprite
from source.gui.texture.abc import Style
from source.gui.widget.abc import BoxWidget
from source.type import Distance, ColorRGB, Point2D
from source.utils import dict_filter_prefix
if TYPE_CHECKING:
from source.gui.scene.abc import Scene
class GameGrid(BoxWidget):
"""
A widget that represent a game grid.
"""
def __init__(self, scene: "Scene",
grid_style: Type[Style],
boat_style: Type[Style],
x: Distance = 0,
y: Distance = 0,
width: Distance = None,
height: Distance = None,
preview_color: ColorRGB = (150, 255, 150),
boats_length: list[int] = None,
rows: int = None,
columns: int = None,
board_data: dict = None,
**kwargs):
if (rows is None or columns is None) and board_data is None:
raise ValueError(f"{self.__class__} object need to set rows and columns, or the board_data")
self.cell_sprites: dict[Point2D, tuple["Sprite", hash]] = {}
super().__init__(scene, x, y, width, height)
self.group_cursor = pyglet.graphics.Group(order=1)
self.group_line = pyglet.graphics.Group(order=2)
# the list of the size of the boats to place
self.boats_length = [] if boats_length is None else sorted(boats_length, reverse=True)
self.preview_color = preview_color
# créer la planche du jeu
self.board = Board(width=rows, height=columns) if board_data is None else Board.from_json(board_data)
self.rows = self.board.height
self.columns = self.board.width
self.orientation: Orientation = Orientation.HORIZONTAL
self._boat_kwargs = dict_filter_prefix("boat_", kwargs)
self._bomb_kwargs = dict_filter_prefix("bomb_", kwargs)
self.grid_style = grid_style
self.boat_style = boat_style
self.background = Sprite(
img=grid_style.get("background"),
batch=self.scene.batch,
**dict_filter_prefix("background_", kwargs)
)
self.lines: list[pyglet.shapes.Line] = [
pyglet.shapes.Line(
0, 0, 0, 0,
batch=self.scene.batch,
group=self.group_line,
**dict_filter_prefix("line_", kwargs)
)
for _ in range((self.columns - 1) + (self.rows - 1))
]
self.cursor = pyglet.shapes.Rectangle(
0, 0, 0, 0,
color=(0, 0, 0, 100),
batch=self.scene.batch,
group=self.group_cursor
)
self.add_listener("on_click_release", lambda _, *args: self.on_click_release(*args))
self.add_listener("on_hover_leave", lambda *_: self.hide_cursor())
self.add_listener("on_hover", lambda _, *args: self._refresh_cursor(*args))
self._refresh_size()
self.display_board(self.board)
def get_cell_from_rel(self, rel_x: int, rel_y: int) -> tuple[int, int]:
"""
Return the cell of the grid from a point relative position
"""
return (
int((rel_x-1) / self.cell_width),
int((rel_y-1) / self.cell_height)
)
# refresh
def _refresh_size(self):
self.background.x, self.background.y = self.xy
self.background.width, self.background.height = self.size
# lines
for column, line in enumerate(self.lines[:self.columns - 1], start=1):
line.x = self.x + self.cell_width * column
line.x2 = line.x
line.y = self.y
line.y2 = self.y2
for row, line in enumerate(self.lines[-self.rows + 1:], start=1):
line.x = self.x
line.x2 = self.x2
line.y = self.y + self.cell_height * row
line.y2 = line.y
# sprites
for (x, y), (sprite, hash_) in self.cell_sprites.items():
# calcul des décalages à cause de la rotation qui est faite par rapport à l'origine de l'image
offset_x = 0 if sprite.rotation <= 90 else self.cell_width
offset_y = self.cell_height if 90 <= sprite.rotation <= 180 else 0
width, height = (
(self.cell_width, self.cell_height) if sprite.rotation % 180 == 0 else
(self.cell_height, self.cell_width)
)
sprite.x = self.x + (x * self.cell_width) + offset_x
sprite.y = self.y + (y * self.cell_height) + offset_y
sprite.width = width
sprite.height = height
def _refresh_cursor(self, rel_x: int, rel_y: int):
cell_x, cell_y = self.get_cell_from_rel(rel_x, rel_y)
self.cursor.x = self.x + cell_x * self.width / self.columns
self.cursor.y = self.y + cell_y * self.height / self.rows
self.cursor.width, self.cursor.height = self.cell_size
self.preview_boat((cell_x, cell_y)) # display the preview of the boat on this cell
# function
def hide_cursor(self):
self.cursor.width, self.cursor.height = 0, 0
def display_board(self, board: Board, preview: bool = False):
# remplacer par l'utilisation de board.boats ?
matrice = board.boats
max_boat: int = np.max(matrice)
for (y, x), value in np.ndenumerate(matrice):
bombed: bool = not board.bombs[y, x] # cette case a déjà été attaqué si la valeur est "False".
if value == 0 and not bombed:
if (x, y) in self.cell_sprites:
self.cell_sprites.pop((x, y))
continue # ignore s'il n'y a ni bombe, ni bateau.
# calcul de la forme et de la rotation de cette cellule du bateau
form, rotation = (
# bombe
("touched", 0) if bombed and value != 0 else
("missed", 0) if bombed else
# corps
("body", 0) if 0 < y < (self.rows-1) and matrice[y-1, x] == matrice[y+1, x] == value else # colonne
("body", 1) if 0 < x < (self.columns-1) and matrice[y, x-1] == matrice[y, x+1] == value else # ligne
# bordure
("edge", 0) if 0 < y and matrice[y-1, x] == value else # bas
("edge", 1) if 0 < x and matrice[y, x-1] == value else # droite
("edge", 2) if y < (self.rows-1) and matrice[y+1, x] == value else # haut
("edge", 3) if x < (self.columns-1) and matrice[y, x+1] == value else # gauche
# aucune bordure (bateau de taille 1)
("solo", 0)
)
# si le bateau est le dernier placé et qu'on est en prévisualisation, change sa teinte.
color: ColorRGB = self.preview_color if preview and value == max_boat else (255, 255, 255)
hash_new = hash((form, rotation, color))
sprite_old, hash_old = self.cell_sprites.get((x, y), (None, None))
if hash_old == hash_new:
# si la texture n'a pas changé, ne rafraichi pas le sprite
continue
sprite = Sprite(
img=self.boat_style.get(form),
batch=self.scene.batch,
**self._boat_kwargs
)
sprite.rotation = rotation * 90
sprite.color = color
self.cell_sprites[x, y] = (sprite, hash_new)
self._refresh_size()
def swap_orientation(self):
self.orientation = (
Orientation.HORIZONTAL if self.orientation is Orientation.VERTICAL else
Orientation.VERTICAL
)
def place_boat(self, cell: Point2D):
if len(self.boats_length) == 0: return
try:
self.board.add_boat(
Boat(self.boats_length[0], orientation=self.orientation),
cell
)
except InvalidBoatPosition:
pass # if the boat can't be placed, ignore
else: # if the boat have been placed
self.boats_length.pop(0) # remove the boat from the list of boat to place
if len(self.boats_length) == 0:
self.trigger_event("on_all_boats_placed")
self.display_board(self.board) # rafraichi l'affichage
def preview_boat(self, cell: Point2D):
if len(self.boats_length) == 0: return
try:
preview_board = copy(self.board)
preview_board.add_boat(
Boat(self.boats_length[0], orientation=self.orientation),
cell
)
except InvalidBoatPosition:
self.display_board(self.board) # if the boat can't be placed, ignore
else: self.display_board(preview_board, preview=True)
def place_bomb(self, cell: Point2D, force_touched: bool = None) -> BombState:
bomb_state = self.board.bomb(cell)
if force_touched is not None:
x, y = cell
self.board.boats[y, x] = int(force_touched)
self.display_board(self.board)
return bomb_state
def on_click_release(self, rel_x: int, rel_y: int, button: int, modifiers: int):
cell = self.get_cell_from_rel(rel_x, rel_y)
match button:
case pyglet.window.mouse.RIGHT:
self.swap_orientation()
self.preview_boat(cell)
case pyglet.window.mouse.LEFT:
self.place_boat(cell)
self.trigger_event("on_request_place_bomb", cell)
# property
@property
def cell_width(self) -> float:
return self.width / self.columns
@property
def cell_height(self) -> float:
return self.height / self.rows
@property
def cell_size(self) -> tuple[float, float]:
return self.cell_width, self.cell_height
# event
def on_resize(self, width: int, height: int):
self._refresh_size()