diff --git a/.save/127.0.0.1.bn-save b/.save/127.0.0.1.bn-save new file mode 100644 index 0000000..ecf1905 --- /dev/null +++ b/.save/127.0.0.1.bn-save @@ -0,0 +1,227 @@ +{ + "grid_ally": { + "columns": 8, + "rows": 8, + "boats": [ + [ + { + "length": 5, + "orientation": "H" + }, + [ + 0, + 0 + ] + ], + [ + { + "length": 4, + "orientation": "V" + }, + [ + 1, + 2 + ] + ], + [ + { + "length": 4, + "orientation": "H" + }, + [ + 3, + 4 + ] + ], + [ + { + "length": 3, + "orientation": "V" + }, + [ + 0, + 5 + ] + ], + [ + { + "length": 2, + "orientation": "H" + }, + [ + 2, + 6 + ] + ] + ], + "bombs": [ + [ + false, + false, + true, + true, + true, + true, + true, + true + ], + [ + true, + true, + false, + true, + true, + true, + true, + true + ], + [ + true, + false, + true, + false, + true, + true, + true, + true + ], + [ + true, + true, + true, + true, + true, + true, + true, + true + ], + [ + true, + true, + true, + true, + true, + true, + true, + true + ], + [ + true, + true, + false, + true, + true, + false, + true, + true + ], + [ + true, + false, + true, + true, + true, + true, + true, + true + ], + [ + true, + true, + true, + true, + true, + true, + true, + true + ] + ] + }, + "grid_enemy": { + "columns": 8, + "rows": 8, + "boats": [], + "bombs": [ + [ + true, + false, + true, + true, + true, + false, + true, + true + ], + [ + true, + true, + true, + true, + true, + true, + true, + true + ], + [ + false, + false, + false, + false, + true, + true, + false, + true + ], + [ + true, + true, + true, + true, + true, + true, + true, + true + ], + [ + true, + true, + false, + true, + true, + true, + true, + true + ], + [ + true, + true, + true, + true, + true, + true, + true, + true + ], + [ + true, + true, + true, + true, + true, + true, + true, + true + ], + [ + true, + true, + true, + true, + true, + true, + true, + true + ] + ] + } +} \ No newline at end of file diff --git a/NOTE.md b/NOTE.md index d27811c..8a581fc 100644 --- a/NOTE.md +++ b/NOTE.md @@ -4,15 +4,13 @@ A faire : 1. Principal : - Sauvegarde - Historique / Replay -- Documenter - +- Documenter 2. Visuel : - Rendre le texte de status plus visible - Police d'écriture - Changer les images, rajouter les fonds, ... - 3. Hypothétique : - Voir si les event listener intégré à pyglet sont plus pratiques que l'event propagation (?) - Faire une scène incluant par défaut les boutons "Retour" (?) diff --git a/source/__init__.py b/source/__init__.py index e69de29..73d14ce 100644 --- a/source/__init__.py +++ b/source/__init__.py @@ -0,0 +1,4 @@ +from pathlib import Path + +path_save: Path = Path(".save") +path_save.mkdir(exist_ok=True) diff --git a/source/core/Board.py b/source/core/Board.py index 3bf3760..bf13915 100644 --- a/source/core/Board.py +++ b/source/core/Board.py @@ -164,3 +164,5 @@ if __name__ == "__main__": print(board.bomb((4, 3))) print(board.bomb((4, 4))) print(board) + + print(board.to_json()) diff --git a/source/gui/scene/Game.py b/source/gui/scene/Game.py index 8145638..d3b2613 100644 --- a/source/gui/scene/Game.py +++ b/source/gui/scene/Game.py @@ -1,13 +1,15 @@ +import json import socket from typing import TYPE_CHECKING +from source import path_save 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 import widget, texture, scene -from source import core -from source.network.packet import PacketChat, PacketBombPlaced, PacketBoatPlaced, PacketBombState, PacketQuit +from source.network.packet import PacketChat, PacketBombPlaced, PacketBoatPlaced, PacketBombState, PacketQuit, \ + PacketAskSave, PacketResponseSave from source.type import Point2D from source.utils import StoppableThread @@ -164,6 +166,12 @@ class Game(Scene): style=texture.Button.Style1 ) + def ask_save(widget, x, y, button, modifiers): + PacketAskSave().send_connection(self.connection) + self.chat_new_message("System", "demande de sauvegarde envoyé.") + + self.button_save.add_listener("on_click_release", ask_save) + self.button_quit = self.add_widget( widget.Button, @@ -187,9 +195,6 @@ class Game(Scene): font_size=20 ) - self.board_ally = core.Board(rows=self.grid_height, columns=self.grid_width) - self.board_enemy = core.Board(rows=self.grid_height, columns=self.grid_width) - self._my_turn = my_turn # is it the player turn ? 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 ? @@ -266,6 +271,24 @@ class Game(Scene): # function + def to_json(self) -> dict: + return { + "grid_ally": self.grid_ally.board.to_json(), + "grid_enemy": self.grid_enemy.board.to_json(), + } + + def save(self, value: bool): + self.chat_new_message( + "System", + "Sauvegarde de la partie..." if value else "Sauvegarde de la partie refusé." + ) + if not value: return + + ip_address, _ = self.connection.getpeername() + + with open(path_save / (ip_address + ".bn-save"), "w", encoding="utf-8") as file: + json.dump(self.to_json(), file, ensure_ascii=False, indent=4) + def game_end(self, won: bool): self.window.add_scene(GameResult, game_scene=self, won=won) # affiche le résultat self.thread.stop() # coupe la connexion @@ -287,15 +310,13 @@ class Game(Scene): def network_on_bomb_placed(self, packet: PacketBombPlaced): try: # essaye de poser la bombe sur la grille alliée - bomb_state = self.grid_ally.board.bomb(packet.position) + bomb_state = self.grid_ally.place_bomb(packet.position) except (InvalidBombPosition, PositionAlreadyShot): # si une erreur se produit, signale l'erreur bomb_state = BombState.ERROR # l'opposant va rejouer, ce n'est donc pas notre tour self.my_turn = False else: - # si la bombe a bien été placé, affiche-la sur la grille visuel allié - self.grid_ally.place_bomb(packet.position, bomb_state.success) # c'est à notre tour si l'opposant à loupé sa bombe self.my_turn = not bomb_state.success @@ -324,7 +345,7 @@ class Game(Scene): self.boat_broken_ally += 1 # place la bombe sur la grille ennemie visuelle - self.grid_enemy.place_bomb(packet.position, packet.bomb_state.success) + self.grid_enemy.place_bomb(packet.position, force_touched=packet.bomb_state.success) if packet.bomb_state is BombState.WON: # si cette bombe a touché le dernier bateau, alors l'on a gagné @@ -336,6 +357,13 @@ class Game(Scene): from source.gui.scene import GameError self.window.set_scene(GameError, text="L'adversaire a quitté la partie.") + def network_on_ask_save(self, packet: PacketAskSave): + from source.gui.scene import GameSave + self.window.add_scene(GameSave, game_scene=self) + + def network_on_response_save(self, packet: PacketResponseSave): + self.save(value=packet.value) + # event def on_resize_after(self, width: int, height: int): diff --git a/source/gui/scene/GameLoad.py b/source/gui/scene/GameLoad.py new file mode 100644 index 0000000..3e5a786 --- /dev/null +++ b/source/gui/scene/GameLoad.py @@ -0,0 +1,47 @@ +from typing import TYPE_CHECKING + +from source.gui import widget, texture +from source.gui.scene.abc import Scene + + +if TYPE_CHECKING: + from source.gui.window import Window + + +class GameLoad(Scene): + def __init__(self, window: "Window", **kwargs): + super().__init__(window, **kwargs) + + self.label = self.add_widget( + widget.Text, + + x=0.5, y=0.5, width=1.0, + + anchor_x="center", + + text="Une ancienne partie contre cet adversaire a été sauvegardé.\nSouhaitez-vous la reprendre ?", + align="center", + multiline=True, + font_size=28, + ) + + self.refuse = self.add_widget( + widget.Button, + + x=20, y=20, width=0.2, height=0.1, + + label_text="Refuser", + + style=texture.Button.Style1 + ) + + self.accept = self.add_widget( + widget.Button, + + x=lambda widget: widget.scene.window.width - 20 - widget.width, y=20, width=0.2, height=0.1, + + label_text="Accepter", + + style=texture.Button.Style1 + ) + diff --git a/source/gui/scene/GameSave.py b/source/gui/scene/GameSave.py new file mode 100644 index 0000000..04c43ab --- /dev/null +++ b/source/gui/scene/GameSave.py @@ -0,0 +1,64 @@ +from typing import TYPE_CHECKING + +from source.gui import widget, texture +from source.gui.scene.abc.Popup import Popup +from source.network.packet import PacketResponseSave + +if TYPE_CHECKING: + from source.gui.window import Window + from source.gui.scene import Game + + +class GameSave(Popup): + def __init__(self, window: "Window", game_scene: "Game", **kwargs): + super().__init__(window, **kwargs) + + self.game_scene = game_scene + + self.background = self.add_widget( + widget.Image, + + x=0, y=0, width=1.0, height=1.0, + + image=texture.Popup.Style1.background + ) + + self.text = self.add_widget( + widget.Text, + + x=0.5, y=0.5, + + anchor_x="center", + + text="L'adversaire souhaite sauvegarder", + font_size=28, + align="center", + ) + + self.refuse = self.add_widget( + widget.Button, + x=0.20, y=0.20, width=0.2, height=0.1, + + label_text="Refuser", + + style=texture.Button.Style1 + ) + + self.refuse.add_listener("on_click_release", lambda *_: self.choose_save(value=False)) + + self.accept = self.add_widget( + widget.Button, + x=0.60, y=0.20, width=0.2, height=0.1, + + label_text="Accepter", + + style=texture.Button.Style1 + ) + + self.accept.add_listener("on_click_release", lambda *_: self.choose_save(value=True)) + + def choose_save(self, value: bool): + PacketResponseSave(value=value).send_connection(self.game_scene.connection) + self.window.remove_scene(self) + self.game_scene.save(value=value) + diff --git a/source/gui/scene/__init__.py b/source/gui/scene/__init__.py index 0aac951..3a350b0 100644 --- a/source/gui/scene/__init__.py +++ b/source/gui/scene/__init__.py @@ -2,6 +2,8 @@ from .GameResult import GameResult from .Game import Game from .GameQuit import GameQuit from .GameError import GameError +from .GameSave import GameSave +from .GameLoad import GameLoad from .Settings import Settings from .RoomHost import RoomHost from .RoomJoin import RoomJoin diff --git a/source/gui/widget/GameGrid.py b/source/gui/widget/GameGrid.py index fc96d12..f3a4bd7 100644 --- a/source/gui/widget/GameGrid.py +++ b/source/gui/widget/GameGrid.py @@ -5,7 +5,7 @@ import numpy as np import pyglet.shapes from source.core import Board, Boat -from source.core.enums import Orientation +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 @@ -230,15 +230,21 @@ class GameGrid(BoxWidget): 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, touched: bool): + def place_bomb(self, cell: Point2D, force_touched: bool = None) -> BombState: + bomb_state = self.board.bomb(cell) + self.cell_sprites[cell] = Sprite( - img=self.bomb_style.get("touched" if touched else "missed"), + img=self.bomb_style.get( + "touched" if (bomb_state.success if force_touched is None else force_touched) else "missed" + ), batch=self.scene.batch, **self._bomb_kwargs ) self._refresh_size() + 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) diff --git a/source/network/Client.py b/source/network/Client.py index 7b6964c..2f2cce8 100644 --- a/source/network/Client.py +++ b/source/network/Client.py @@ -33,6 +33,8 @@ class Client(StoppableThread): print(f"[Client] Connecté avec {connection}") + ... + settings: Any = PacketSettings.from_connection(connection) PacketUsername(username=self.username).send_data_connection(connection) packet_username = PacketUsername.from_connection(connection) diff --git a/source/network/Host.py b/source/network/Host.py index 3da41e9..1994b12 100644 --- a/source/network/Host.py +++ b/source/network/Host.py @@ -1,6 +1,7 @@ import socket from typing import TYPE_CHECKING +from source import path_save from source.gui import scene from source.network import game_network from source.utils import StoppableThread @@ -36,13 +37,19 @@ class Host(StoppableThread): while True: try: - connection, address = server.accept() # accepte la première connexion entrante + connection, (ip_address, port) = server.accept() # accepte la première connexion entrante break # sort de la boucle except socket.timeout: # en cas de timeout if self.stopped: return # vérifie si le thread n'est pas censé s'arrêter # sinon, réessaye - print(f"[Serveur] Connecté avec {address}") + print(f"[Serveur] Connecté avec {ip_address}") + + # check pour ancienne sauvegarde contre ce joueur + + ... + + # paramètres & jeu self.settings.send_data_connection(connection) packet_username = PacketUsername.from_connection(connection) diff --git a/source/network/game_network.py b/source/network/game_network.py index 2b03899..18ed6ec 100644 --- a/source/network/game_network.py +++ b/source/network/game_network.py @@ -27,6 +27,8 @@ def game_network( packet.PacketBombPlaced: game_scene.network_on_bomb_placed, packet.PacketBombState: game_scene.network_on_bomb_state, packet.PacketQuit: game_scene.network_on_quit, + packet.PacketAskSave: game_scene.network_on_ask_save, + packet.PacketResponseSave: game_scene.network_on_response_save, } try: diff --git a/source/network/packet/PacketAskSave.py b/source/network/packet/PacketAskSave.py new file mode 100644 index 0000000..915b919 --- /dev/null +++ b/source/network/packet/PacketAskSave.py @@ -0,0 +1,7 @@ +from source.network.packet.abc import SignalPacket + + +class PacketAskSave(SignalPacket): + """ + A packet that is sent when the player wish to save the game. + """ diff --git a/source/network/packet/PacketResponseSave.py b/source/network/packet/PacketResponseSave.py new file mode 100644 index 0000000..20d356e --- /dev/null +++ b/source/network/packet/PacketResponseSave.py @@ -0,0 +1,23 @@ +import struct +from dataclasses import field, dataclass + +from source.network.packet.abc import SimplePacket + + +@dataclass +class PacketResponseSave(SimplePacket): + """ + A packet that is sent when the player accept or refuse a requested save. + """ + + value: bool = field() # True si requête accepter, sinon False + + packet_format = ">?" + + def to_bytes(self) -> bytes: + return struct.pack(self.packet_format, self.value) + + @classmethod + def from_bytes(cls, data: bytes) -> "PacketResponseSave": + value, *_ = struct.unpack(cls.packet_format, data) + return cls(value=value) diff --git a/source/network/packet/__init__.py b/source/network/packet/__init__.py index a6f4745..9a6251d 100644 --- a/source/network/packet/__init__.py +++ b/source/network/packet/__init__.py @@ -2,6 +2,8 @@ from .PacketChat import PacketChat from .PacketBombPlaced import PacketBombPlaced from .PacketBombState import PacketBombState from .PacketBoatPlaced import PacketBoatPlaced +from .PacketAskSave import PacketAskSave +from .PacketResponseSave import PacketResponseSave from .PacketSettings import PacketSettings from .PacketUsername import PacketUsername from .PacketQuit import PacketQuit