diff --git a/NOTE.md b/NOTE.md index 81d2710..6387271 100644 --- a/NOTE.md +++ b/NOTE.md @@ -2,7 +2,6 @@ A faire : 1. Principal : - - Historique - Paramètres (contenu : fps, volume dans le jeu, plein écran, ...) (bouton dans le jeu) - Documenter (Docstring, README, ...) @@ -17,6 +16,7 @@ A faire : Bug : - (incertain) Dans de rare cas (souvent en fermant brutalement la fenêtre) le processus ne s'arrête pas - Quitter pendant que l'on décide de si l'on doit charger ou non une ancienne sauvegarde fait crash l'adversaire + - Les champs invalides n'empêchent pas de faire l'action Autre : - Tester sur Linux diff --git a/source/core/Board.py b/source/core/Board.py index 1f78546..a2ac2f1 100644 --- a/source/core/Board.py +++ b/source/core/Board.py @@ -113,6 +113,20 @@ class Board: # 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 diff --git a/source/gui/position.py b/source/gui/position.py new file mode 100644 index 0000000..1f83369 --- /dev/null +++ b/source/gui/position.py @@ -0,0 +1,37 @@ +from typing import Callable + + +# pourcentage + + +def w_percent(value: int) -> Callable: # positionne en pourcentage la largeur + return lambda widget: int(widget.scene.window.width * (value / 100)) + + +def h_percent(value: int) -> Callable: # positionne en pourcentage la hauteur + return lambda widget: int(widget.scene.window.height * (value / 100)) + + +# pixel + + +def right(px: int) -> Callable: # positionne depuis la droite + return lambda widget: widget.scene.window.width - px + + +def up(px: int) -> Callable: # positionne depuis le haut + return lambda widget: widget.scene.window.height - px + + +def right_content(px: int) -> Callable: # positionne depuis la droite avec la taille du widget compris + return lambda widget: widget.scene.window.width - widget.width - px + + +def up_content(px: int) -> Callable: # positionne depuis le haut avec la taille du widget compris + return lambda widget: widget.scene.window.height - widget.height - px + + +# raccourci + +w_full = w_percent(100) +h_full = h_percent(100) diff --git a/source/gui/scene/Game.py b/source/gui/scene/Game.py index 65eb989..c0dcab8 100644 --- a/source/gui/scene/Game.py +++ b/source/gui/scene/Game.py @@ -8,10 +8,9 @@ from source.path import path_save, path_history from source.core.enums import BombState from source.core.error import InvalidBombPosition, PositionAlreadyShot from source.gui.scene import GameResult -from source.gui.scene.abc import Scene +from source.gui.scene.abc import BaseGame from source.gui import widget, texture, scene -from source.network.packet import PacketChat, PacketBombPlaced, PacketBoatPlaced, PacketBombState, PacketQuit, \ - PacketAskSave, PacketResponseSave, PacketBoatsData +from source.network.packet import * from source.type import Point2D from source.utils import StoppableThread @@ -19,55 +18,18 @@ if TYPE_CHECKING: from source.gui.window import Window -class Game(Scene): +class Game(BaseGame): def __init__(self, window: "Window", thread: StoppableThread, connection: socket.socket, - boats_length: list, - name_ally: str, - name_enemy: str, my_turn: bool, - grid_width: int = None, - grid_height: int = None, - board_ally_data: dict = None, - board_enemy_data: dict = None, - - history: list[bool, Point2D] = None, - **kwargs): super().__init__(window, **kwargs) self.thread = thread self.connection = connection - self.boats_length = boats_length - self.name_ally = name_ally - self.name_enemy = name_enemy - self.grid_width = grid_width - self.grid_height = grid_height - - self.background = self.add_widget( - widget.Image, - - x=0, y=0, width=1.0, height=1.0, - - image=texture.Background.game, - ) - - self.grid_ally = self.add_widget( - widget.GameGrid, - - x=75, y=0.25, width=0.35, height=0.5, - - boats_length=self.boats_length, - - grid_style=texture.Grid.Style1, - boat_style=texture.Grid.Boat.Style1, - rows=self.grid_height, columns=self.grid_width, - - board_data=board_ally_data - ) def board_ally_ready(widget): self.boat_ready_ally = True @@ -75,18 +37,6 @@ class Game(Scene): self.grid_ally.add_listener("on_all_boats_placed", board_ally_ready) - self.grid_enemy = self.add_widget( - widget.GameGrid, - - x=lambda widget: widget.scene.window.width - 75 - widget.width, y=0.25, width=0.35, height=0.5, - - grid_style=texture.Grid.Style1, - boat_style=texture.Grid.Boat.Style1, - rows=self.grid_height, columns=self.grid_width, - - board_data=board_enemy_data - ) - def board_enemy_bomb(widget, cell: Point2D): if not (self.boat_ready_ally and self.boat_ready_enemy): return if not self.my_turn: return @@ -96,46 +46,6 @@ class Game(Scene): self.grid_enemy.add_listener("on_request_place_bomb", board_enemy_bomb) - self.add_widget( - widget.Text, - - x=0.27, y=0.995, - - text=self.name_ally, - font_size=20, - anchor_x="center", anchor_y="center" - ) - - self.add_widget( - widget.Text, - - x=0.73, y=0.995, - - text=self.name_enemy, - font_size=20, - anchor_x="center", anchor_y="center" - ) - - self.score_ally = self.add_widget( - widget.Text, - - x=0.44, y=0.995, - - text="0", - font_size=25, - anchor_x="center", anchor_y="center" - ) - - self.score_enemy = self.add_widget( - widget.Text, - - x=0.56, y=0.995, - - text="0", - font_size=25, - anchor_x="center", anchor_y="center" - ) - self.chat_log = self.add_widget( widget.Text, @@ -185,19 +95,11 @@ class Game(Scene): self.button_save.add_listener("on_click_release", ask_save) - self.button_quit = self.add_widget( - widget.Button, - - x=0.85, y=0, width=0.15, height=0.1, - - label_text="Quitter", - - style=texture.Button.Style1 + self.button_quit.add_listener( + "on_click_release", + lambda *_: self.window.add_scene(scene.GameQuit, game_scene=self) ) - self.button_quit.add_listener("on_click_release", - lambda *_: self.window.add_scene(scene.GameQuit, game_scene=self)) - self.label_state = self.add_widget( widget.Text, @@ -212,9 +114,7 @@ class Game(Scene): self._boat_ready_ally: bool = False # does the player finished placing his boat ? self._boat_ready_enemy: bool = False # does the opponent finished placing his boat ? - self.history: list[tuple[bool, Point2D]] = [] if history is None else history # liste des bombes posées - - if len(boats_length) == 0: # s'il n'y a pas de bateau à placé + if len(self.boats_length) == 0: # s'il n'y a pas de bateau à placé self._boat_ready_ally = True # défini l'état de notre planche comme prête PacketBoatPlaced().send_connection(connection) # indique à l'adversaire que notre planche est prête @@ -242,10 +142,6 @@ class Game(Scene): "L'adversaire place ses bombes..." ) - def _refresh_score_text(self): - self.score_ally.text = str(self.grid_enemy.board.get_score()) - self.score_enemy.text = str(self.grid_ally.board.get_score()) - # property @property @@ -312,15 +208,19 @@ class Game(Scene): history=data["history"] ) + def get_save_suffix(self) -> str: + # Le suffix est un entier indiquant si c'est à notre tour ou non. + # Cet entier permet aux localhost de toujours pouvoir sauvegarder et charger sans problème. + ip_address, port = self.connection.getpeername() + return f"-{int(self.my_turn)}" if ip_address == "127.0.0.1" else "" + @property def save_path(self) -> Path: ip_address, port = self.connection.getpeername() - # Le nom du fichier est l'IP de l'opposent, suivi d'un entier indiquant si c'est à notre tour ou non. - # Cet entier permet aux localhost de toujours pouvoir sauvegarder et charger sans problème. return path_save / ( ip_address + - (f"-{int(self.my_turn)}" if ip_address == "127.0.0.1" else "") + + self.get_save_suffix() + ".bn-save" ) @@ -328,6 +228,7 @@ class Game(Scene): def history_path(self): return path_history / ( datetime.now().strftime("%Y-%m-%d_%H-%M-%S") + + self.get_save_suffix() + ".bn-history" ) @@ -387,13 +288,12 @@ class Game(Scene): else: # c'est à notre tour si l'opposant à loupé sa bombe self.my_turn = not bomb_state.success + # sauvegarde la bombe dans l'historique + self.history.append((False, packet.position)) # envoie le résultat à l'autre joueur PacketBombState(position=packet.position, bomb_state=bomb_state).send_connection(self.connection) - # sauvegarde la bombe dans l'historique - self.history.append((False, packet.position)) - self._refresh_score_text() # le score a changé, donc rafraichi son texte if bomb_state is BombState.WON: diff --git a/source/gui/scene/HistoryGame.py b/source/gui/scene/HistoryGame.py index e3952e8..9d2c601 100644 --- a/source/gui/scene/HistoryGame.py +++ b/source/gui/scene/HistoryGame.py @@ -1,26 +1,96 @@ +import json from pathlib import Path from typing import TYPE_CHECKING -from source.gui import widget -from source.gui.scene.abc import Scene - +from source.gui import widget, texture +from source.gui.position import h_percent, w_percent +from source.gui.scene.abc import BaseGame if TYPE_CHECKING: from source.gui.window import Window -class HistoryGame(Scene): +class HistoryGame(BaseGame): def __init__(self, window: "Window", history_path: Path, **kwargs): - super().__init__(window, **kwargs) + + with open(history_path, "r", encoding="utf8") as file: + history_data = json.load(file) + + super().__init__( + window, + boats_length=[], + name_ally=history_data["name_ally"], + name_enemy=history_data["name_enemy"], + board_ally_data=history_data["grid_ally"], + board_enemy_data=history_data["grid_enemy"], + history=history_data["history"], + **kwargs + ) self.history_path = history_path - self.add_widget( - widget.Text, + from source.gui.scene import MainMenu + self.button_quit.add_listener("on_click_release", lambda *_: window.set_scene(MainMenu)) - x=0.5, y=0.5, + self.move_number: int = 0 # numéro du mouvement en cours - anchor_x="center", anchor_y="center", + self.grid_ally.board.clear_bombs() + self.grid_ally.refresh_board() + self.grid_enemy.board.clear_bombs() + self.grid_enemy.refresh_board() - text=str(history_path) + self.previous = self.add_widget( + widget.Button, + x=w_percent(20), y=h_percent(10), width=w_percent(20), height=h_percent(10), + + label_text="Précédent", + + style=texture.Button.Style1 ) + + self.previous.add_listener("on_click_release", lambda *_: self.previous_move()) + + self.next = self.add_widget( + widget.Button, + x=w_percent(60), y=h_percent(10), width=w_percent(20), height=h_percent(10), + + label_text="Suivant", + + style=texture.Button.Style1 + ) + + self.next.add_listener("on_click_release", lambda *_: self.next_move()) + + self.text_move = self.add_widget( + widget.Text, + x=w_percent(50), y=h_percent(12), + + anchor_x="center", + + font_size=28, + ) + self._refresh_move_text() + + def _refresh_move_text(self): + self.text_move.text = f"{self.move_number} / {len(self.history)}" + self._refresh_score_text() + + def previous_move(self): + # si le mouvement est au minimum, ignore + if self.move_number <= 0: return + + self.move_number -= 1 + turn, cell = self.history[self.move_number] + (self.grid_enemy if turn else self.grid_ally).remove_bomb(cell) + + self._refresh_move_text() + + def next_move(self): + # si le mouvement est au maximum, ignore + if self.move_number >= len(self.history): return + + self.move_number += 1 + turn, cell = self.history[self.move_number-1] + (self.grid_enemy if turn else self.grid_ally).place_bomb(cell) + + self._refresh_move_text() diff --git a/source/gui/scene/MainMenu.py b/source/gui/scene/MainMenu.py index d31cfe4..237a888 100644 --- a/source/gui/scene/MainMenu.py +++ b/source/gui/scene/MainMenu.py @@ -1,5 +1,6 @@ from typing import TYPE_CHECKING +from source.gui.position import w_full, h_full, h_percent from source.gui.scene.abc import Scene from source.gui import widget, scene, texture @@ -14,7 +15,7 @@ class MainMenu(Scene): self.background = self.add_widget( widget.Image, - x=0.0, y=0.0, width=1.0, height=1.0, + x=0, y=0, width=w_full, height=h_full, image=texture.Background.main ) @@ -22,7 +23,7 @@ class MainMenu(Scene): self.title = self.add_widget( widget.Text, - x=50, y=0.85, + x=50, y=h_percent(85), text="Bataille Navale", font_size=50 diff --git a/source/gui/scene/abc/BaseGame.py b/source/gui/scene/abc/BaseGame.py new file mode 100644 index 0000000..5ae3b9b --- /dev/null +++ b/source/gui/scene/abc/BaseGame.py @@ -0,0 +1,121 @@ +from abc import ABC +from typing import TYPE_CHECKING + +from source.gui import widget, texture +from source.gui.position import right_content, h_percent, w_percent, w_full, h_full +from source.gui.scene.abc import Scene +from source.type import Point2D + +if TYPE_CHECKING: + from source.gui.window import Window + + +class BaseGame(Scene, ABC): + def __init__(self, window: "Window", + boats_length: list, + name_ally: str, + name_enemy: str, + + grid_width: int = None, + grid_height: int = None, + board_ally_data: dict = None, + board_enemy_data: dict = None, + + history: list[bool, Point2D] = None, + + **kwargs): + + super().__init__(window, **kwargs) + + self.boats_length = boats_length + self.name_ally = name_ally + self.name_enemy = name_enemy + self.history: list[tuple[bool, Point2D]] = [] if history is None else history # liste des bombes posées + + self.background = self.add_widget( + widget.Image, + + x=0, y=0, width=w_full, height=h_full, + + image=texture.Background.game, + ) + + self.grid_ally = self.add_widget( + widget.GameGrid, + + x=75, y=0.25, width=0.35, height=0.5, + + boats_length=self.boats_length, + + grid_style=texture.Grid.Style1, + boat_style=texture.Grid.Boat.Style1, + + rows=grid_height, columns=grid_width, + board_data=board_ally_data + ) + + self.grid_enemy = self.add_widget( + widget.GameGrid, + + x=right_content(75), y=h_percent(25), width=w_percent(35), height=h_percent(50), + + grid_style=texture.Grid.Style1, + boat_style=texture.Grid.Boat.Style1, + + rows=grid_height, columns=grid_width, + board_data=board_enemy_data + ) + + self.add_widget( + widget.Text, + + x=0.27, y=0.995, + + text=self.name_ally, + font_size=20, + anchor_x="center", anchor_y="center" + ) + + self.add_widget( + widget.Text, + + x=0.73, y=0.995, + + text=self.name_enemy, + font_size=20, + anchor_x="center", anchor_y="center" + ) + + self.score_ally = self.add_widget( + widget.Text, + + x=0.44, y=0.995, + + text="0", + font_size=25, + anchor_x="center", anchor_y="center" + ) + + self.score_enemy = self.add_widget( + widget.Text, + + x=0.56, y=0.995, + + text="0", + font_size=25, + anchor_x="center", anchor_y="center" + ) + + self.button_quit = self.add_widget( + widget.Button, + + x=0.85, y=0, width=0.15, height=0.1, + + label_text="Quitter", + + style=texture.Button.Style1 + ) + + def _refresh_score_text(self): + self.score_ally.text = str(self.grid_enemy.board.get_score()) + self.score_enemy.text = str(self.grid_ally.board.get_score()) diff --git a/source/gui/scene/abc/__init__.py b/source/gui/scene/abc/__init__.py index 8abb0fe..0c9dbfb 100644 --- a/source/gui/scene/abc/__init__.py +++ b/source/gui/scene/abc/__init__.py @@ -1 +1,2 @@ from .Scene import Scene +from .BaseGame import BaseGame diff --git a/source/gui/widget/GameGrid.py b/source/gui/widget/GameGrid.py index 9283a99..cd4e08e 100644 --- a/source/gui/widget/GameGrid.py +++ b/source/gui/widget/GameGrid.py @@ -94,7 +94,7 @@ class GameGrid(BoxWidget): self.add_listener("on_hover", lambda _, *args: self._refresh_cursor(*args)) self._refresh_size() - self.display_board(self.board) + self.refresh_board() def get_cell_from_rel(self, rel_x: int, rel_y: int) -> tuple[int, int]: """ @@ -158,6 +158,10 @@ class GameGrid(BoxWidget): def hide_cursor(self): self.cursor.width, self.cursor.height = 0, 0 + def refresh_board(self): + # rafraichi l'affichage de la grille + self.display_board(self.board) + def display_board(self, board: Board, preview: bool = False): # remplacer par l'utilisation de board.boats ? @@ -239,7 +243,7 @@ class GameGrid(BoxWidget): if len(self.boats_length) == 0: self.trigger_event("on_all_boats_placed") - self.display_board(self.board) # rafraichi l'affichage + self.refresh_board() # rafraichi l'affichage def preview_boat(self, cell: Point2D): if len(self.boats_length) == 0: return @@ -251,7 +255,7 @@ class GameGrid(BoxWidget): cell ) except InvalidBoatPosition: - self.display_board(self.board) # if the boat can't be placed, ignore + self.refresh_board() # if the boat can't be placed, ignore else: self.display_board(preview_board, preview=True) @@ -262,9 +266,14 @@ class GameGrid(BoxWidget): x, y = cell self.board.boats[y, x] = int(force_touched) - self.display_board(self.board) + self.refresh_board() return bomb_state + def remove_bomb(self, cell: Point2D): + # retire une bombe de la planche + self.board.remove_bomb(cell) + self.refresh_board() + 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)