diff --git a/source/gui/scene/Game.py b/source/gui/scene/Game.py index a1e61f7..f617405 100644 --- a/source/gui/scene/Game.py +++ b/source/gui/scene/Game.py @@ -7,8 +7,7 @@ from source.gui.scene.abc import Scene from source.gui import widget, texture from source.gui.widget.grid import GameGridAlly, GameGridEnemy from source import core -from source.network.SocketType import SocketType -from source.network.packet.Bomb import Bomb +from source.network.packet import PacketChat, PacketBombPlaced, PacketBoatPlaced from source.type import Point2D if TYPE_CHECKING: @@ -56,7 +55,7 @@ class Game(Scene): ) def board_ally_ready(): - connection.send(SocketType.BOAT_PLACED.value.to_bytes(1, "big")) + PacketBoatPlaced().send_connection(connection) self.grid_ally.add_listener("on_all_boats_placed", board_ally_ready) @@ -76,8 +75,7 @@ class Game(Scene): ) def board_enemy_bomb(cell: Point2D): - connection.send(SocketType.BOMB.value.to_bytes(1, "big")) - connection.send(Bomb(x=cell[0], y=cell[1]).to_bytes()) + PacketBombPlaced(position=cell).send_connection(connection) self.grid_enemy.add_listener("on_request_place_bomb", board_enemy_bomb) @@ -159,8 +157,7 @@ class Game(Scene): self.chat_log.text += "\n" + text self.chat_log.label.y = self.chat_log.y + self.chat_log.label.content_height - connection.send(SocketType["CHAT"].value.to_bytes(1, "big")) - connection.send(text.encode()) + PacketChat(message=text).send_connection(connection) self.chat_input.add_listener("on_enter", send_chat) @@ -205,5 +202,3 @@ class Game(Scene): self.batch_grid_cursor.draw() self.batch_label.draw() - - self.grid_enemy.draw() # DEBUG diff --git a/source/network/Client.py b/source/network/Client.py index b19033a..72b0e10 100644 --- a/source/network/Client.py +++ b/source/network/Client.py @@ -1,15 +1,7 @@ import socket -from queue import Queue from typing import TYPE_CHECKING -import pyglet.clock - -from source.core.enums import BombState -from source.core.error import PositionAlreadyShot, InvalidBombPosition -from source.gui import scene -from source.network.SocketType import SocketType -from source.network.packet.Bomb import Bomb -from source.network.packet.PacketBombState import PacketBombState +from source.network import game_network from source.utils import StoppableThread if TYPE_CHECKING: @@ -34,51 +26,4 @@ class Client(StoppableThread): print(f"[Client] Connecté avec {connection}") - def create_game_scene(dt: float, queue: Queue): - game_scene = self.window.set_scene(scene.Game, connection=connection) - queue.put(game_scene) - - queue = Queue() - pyglet.clock.schedule_once(create_game_scene, 0, queue) - game_scene = queue.get() - - while True: - data = None - - try: data = connection.recv(1) - except socket.timeout: pass - - if not data: - if self._stop: return # vérifie si le thread n'est pas censé s'arrêter - continue - - socket_type = SocketType(int.from_bytes(data, "big")) - - match socket_type: - case SocketType.CHAT: print(connection.recv(1024).decode()) - case SocketType.BOAT_PLACED: print("adversaire à posé ses bateaux") - case SocketType.BOMB: - bomb = Bomb.from_bytes(connection.recv(2)) - - try: bomb_state = game_scene.grid_ally.board.bomb((bomb.x, bomb.y)) - except (InvalidBombPosition, PositionAlreadyShot): pass # TODO: gérer les erreurs - - connection.send(SocketType.BOMB_STATE.value.to_bytes(1, "big")) - - packet_bomb_state = PacketBombState( - x=bomb.x, - y=bomb.y, - bomb_state=bomb_state - ) - connection.send(packet_bomb_state.to_bytes()) - - case SocketType.BOMB_STATE: - packet_bomb_state = PacketBombState.from_bytes(connection.recv(3)) - - touched = packet_bomb_state.bomb_state in [BombState.TOUCHED, BombState.SUNKEN, BombState.WON] - - pyglet.clock.schedule_once( - lambda dt: game_scene.grid_enemy.place_bomb((packet_bomb_state.x, packet_bomb_state.y), - touched), - 0 - ) + game_network(self, self.window, connection) diff --git a/source/network/Host.py b/source/network/Host.py index f9439cd..cc5217a 100644 --- a/source/network/Host.py +++ b/source/network/Host.py @@ -1,15 +1,7 @@ import socket -from queue import Queue from typing import TYPE_CHECKING -import pyglet - -from source.core.enums import BombState -from source.core.error import InvalidBombPosition, PositionAlreadyShot -from source.gui import scene -from source.network.SocketType import SocketType -from source.network.packet.Bomb import Bomb -from source.network.packet.PacketBombState import PacketBombState +from source.network import game_network from source.utils import StoppableThread if TYPE_CHECKING: @@ -38,56 +30,9 @@ class Host(StoppableThread): connection, address = server.accept() # accepte la première connexion entrante break # sort de la boucle except socket.timeout: # en cas de timeout - if self._stop: return # vérifie si le thread n'est pas censé s'arrêter + 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}") - def create_game_scene(dt: float, queue: Queue): - game_scene = self.window.set_scene(scene.Game, connection=connection) - queue.put(game_scene) - - queue = Queue() - pyglet.clock.schedule_once(create_game_scene, 0, queue) - game_scene = queue.get() - - while True: - data = None - - try: data = connection.recv(1) - except socket.timeout: pass - - if not data: - if self._stop: return # vérifie si le thread n'est pas censé s'arrêter - continue - - socket_type = SocketType(int.from_bytes(data, "big")) - - match socket_type: - case SocketType.CHAT: print(connection.recv(1024).decode()) - case SocketType.BOAT_PLACED: print("adversaire à posé ses bateaux") - case SocketType.BOMB: - bomb = Bomb.from_bytes(connection.recv(2)) - - try: bomb_state = game_scene.grid_ally.board.bomb((bomb.x, bomb.y)) - except (InvalidBombPosition, PositionAlreadyShot): pass # TODO: gérer les erreurs - - connection.send(SocketType.BOMB_STATE.value.to_bytes(1, "big")) - - packet_bomb_state = PacketBombState( - x=bomb.x, - y=bomb.y, - bomb_state=bomb_state - ) - connection.send(packet_bomb_state.to_bytes()) - - case SocketType.BOMB_STATE: - packet_bomb_state = PacketBombState.from_bytes(connection.recv(3)) - - touched = packet_bomb_state.bomb_state in [BombState.TOUCHED, BombState.SUNKEN, BombState.WON] - - pyglet.clock.schedule_once( - lambda dt: game_scene.grid_enemy.place_bomb((packet_bomb_state.x, packet_bomb_state.y), - touched), - 0 - ) + game_network(self, self.window, connection) diff --git a/source/network/SocketType.py b/source/network/SocketType.py deleted file mode 100644 index 20b41b4..0000000 --- a/source/network/SocketType.py +++ /dev/null @@ -1,8 +0,0 @@ -from enum import Enum - - -class SocketType(Enum): - CHAT = 0 - BOAT_PLACED = 1 - BOMB = 2 - BOMB_STATE = 3 diff --git a/source/network/__init__.py b/source/network/__init__.py index 4f77a77..87b026e 100644 --- a/source/network/__init__.py +++ b/source/network/__init__.py @@ -1,2 +1,4 @@ +from .game_network import game_network + from .Client import Client from .Host import Host diff --git a/source/network/game_network.py b/source/network/game_network.py new file mode 100644 index 0000000..f2cae54 --- /dev/null +++ b/source/network/game_network.py @@ -0,0 +1,45 @@ +import socket +from typing import Any + +from source.core.enums import BombState +from source.core.error import InvalidBombPosition, PositionAlreadyShot +from source.gui import scene +from source.network.packet.abc import Packet +from source.network import packet + +from source.gui.window import Window +from source.utils import StoppableThread +from source.utils.thread import in_pyglet_context + + +def game_network(thread: "StoppableThread", window: "Window", connection: socket.socket): + game_scene = in_pyglet_context(window.set_scene, scene.Game, connection=connection) + + while True: + data: Any = Packet.from_connection(connection) + + if data is None: + if thread.stopped: return # vérifie si le thread n'est pas censé s'arrêter + continue + + match type(data): + case packet.PacketChat: + print(data.message) + + case packet.PacketBoatPlaced: + print("adversaire à posé ses bateaux") + + case packet.PacketBombPlaced: + try: + bomb_state = game_scene.grid_ally.board.bomb(data.position) + except (InvalidBombPosition, PositionAlreadyShot): + bomb_state = BombState.ERROR + + packet.PacketBombState(position=data.position, bomb_state=bomb_state).send_connection(connection) + + case packet.PacketBombState: + if data.bomb_state is BombState.ERROR: continue + + touched = data.bomb_state in [BombState.TOUCHED, BombState.SUNKEN, BombState.WON] + + in_pyglet_context(game_scene.grid_enemy.place_bomb, data.position, touched) diff --git a/source/network/packet/Bomb.py b/source/network/packet/Bomb.py deleted file mode 100644 index 36769c5..0000000 --- a/source/network/packet/Bomb.py +++ /dev/null @@ -1,20 +0,0 @@ -from dataclasses import dataclass, field - - -@dataclass -class Bomb: - x: int = field() - y: int = field() - - def to_bytes(self) -> bytes: - return ( - self.x.to_bytes(1, "big") + - self.y.to_bytes(1, "big") - ) - - @classmethod - def from_bytes(cls, data: bytes): - return cls( - x=int.from_bytes(data[0:1], "big"), - y=int.from_bytes(data[1:2], "big"), - ) diff --git a/source/network/packet/Chat.py b/source/network/packet/Chat.py deleted file mode 100644 index e406eee..0000000 --- a/source/network/packet/Chat.py +++ /dev/null @@ -1,6 +0,0 @@ -from dataclasses import dataclass, field - - -@dataclass -class Chat: - message: str = field() diff --git a/source/network/packet/PacketBoatPlaced.py b/source/network/packet/PacketBoatPlaced.py new file mode 100644 index 0000000..ab09090 --- /dev/null +++ b/source/network/packet/PacketBoatPlaced.py @@ -0,0 +1,18 @@ +from dataclasses import dataclass +import socket + +from source.network.packet.abc import Packet + + +@dataclass +class PacketBoatPlaced(Packet): + def to_bytes(self): + return b"" + + @classmethod + def from_bytes(cls, data: bytes): + return cls() + + @classmethod + def from_connection(cls, connection: socket.socket) -> "PacketBoatPlaced": + return cls.from_bytes(connection.recv(0)) diff --git a/source/network/packet/PacketBombPlaced.py b/source/network/packet/PacketBombPlaced.py new file mode 100644 index 0000000..d33f2b6 --- /dev/null +++ b/source/network/packet/PacketBombPlaced.py @@ -0,0 +1,25 @@ +import socket +from dataclasses import dataclass, field + +from source.network.packet.abc import Packet +from source.type import Point2D + + +@dataclass +class PacketBombPlaced(Packet): + position: Point2D = field() + + def to_bytes(self): + x, y = self.position + return x.to_bytes(1, "big") + y.to_bytes(1, "big") + + @classmethod + def from_bytes(cls, data: bytes): + return cls(position=( + int.from_bytes(data[0:1], "big"), + int.from_bytes(data[1:2], "big"), + )) + + @classmethod + def from_connection(cls, connection: socket.socket) -> "PacketBombPlaced": + return cls.from_bytes(connection.recv(2)) diff --git a/source/network/packet/PacketBombState.py b/source/network/packet/PacketBombState.py index ae589bf..b09b089 100644 --- a/source/network/packet/PacketBombState.py +++ b/source/network/packet/PacketBombState.py @@ -1,25 +1,35 @@ +import socket from dataclasses import dataclass, field from source.core.enums import BombState +from source.network.packet.abc import Packet +from source.type import Point2D @dataclass -class PacketBombState: - x: int = field() - y: int = field() +class PacketBombState(Packet): + position: Point2D = field() bomb_state: BombState = field() - def to_bytes(self) -> bytes: + def to_bytes(self): + x, y = self.position + return ( - self.x.to_bytes(1, "big") + - self.y.to_bytes(1, "big") + + x.to_bytes(1, "big") + + y.to_bytes(1, "big") + self.bomb_state.value.to_bytes() ) @classmethod def from_bytes(cls, data: bytes): return cls( - x=int.from_bytes(data[0:1], "big"), - y=int.from_bytes(data[1:2], "big"), + position=( + int.from_bytes(data[0:1], "big"), + int.from_bytes(data[1:2], "big"), + ), bomb_state=BombState.from_bytes(data[2:3]) ) + + @classmethod + def from_connection(cls, connection: socket.socket) -> "PacketBombState": + return cls.from_bytes(connection.recv(3)) diff --git a/source/network/packet/PacketChat.py b/source/network/packet/PacketChat.py new file mode 100644 index 0000000..ba593bd --- /dev/null +++ b/source/network/packet/PacketChat.py @@ -0,0 +1,20 @@ +import socket +from dataclasses import dataclass, field + +from source.network.packet.abc import Packet + + +@dataclass +class PacketChat(Packet): + message: str = field() + + def to_bytes(self): + return self.message.encode("utf-8") + + @classmethod + def from_bytes(cls, data: bytes): + return cls(message=data.decode("utf-8")) + + @classmethod + def from_connection(cls, connection: socket.socket) -> "PacketChat": + return cls.from_bytes(connection.recv(256)) \ No newline at end of file diff --git a/source/network/packet/__init__.py b/source/network/packet/__init__.py new file mode 100644 index 0000000..7cae4ad --- /dev/null +++ b/source/network/packet/__init__.py @@ -0,0 +1,4 @@ +from .PacketChat import PacketChat +from .PacketBombPlaced import PacketBombPlaced +from .PacketBombState import PacketBombState +from .PacketBoatPlaced import PacketBoatPlaced diff --git a/source/network/packet/abc/Packet.py b/source/network/packet/abc/Packet.py new file mode 100644 index 0000000..e22079c --- /dev/null +++ b/source/network/packet/abc/Packet.py @@ -0,0 +1,42 @@ +import socket +from abc import abstractmethod, ABC +from typing import Type, Optional + + +class Packet(ABC): + packed_header: bytes + packet_id: int = 0 + + def __init_subclass__(cls, **kwargs): + cls.packet_header = Packet.packet_id.to_bytes(1, "big") + Packet.packet_id = Packet.packet_id + 1 + + @classmethod + def cls_from_header(cls, packet_header: bytes) -> Type["Packet"]: + return next(filter( + lambda subcls: subcls.packet_header == packet_header, + cls.__subclasses__() + )) + + @abstractmethod + def to_bytes(self) -> bytes: + pass + + @classmethod + @abstractmethod + def from_bytes(cls, data: bytes) -> "Packet": + pass + + def send_connection(self, connection: socket.socket) -> None: + connection.send(self.packet_header) + connection.send(self.to_bytes()) + + @classmethod + def from_connection(cls, connection: socket.socket) -> Optional[Type["Packet"]]: + packet_header: Optional[bytes] = None + try: packet_header = connection.recv(1) + except socket.timeout: pass + + if not packet_header: return None + + return cls.cls_from_header(packet_header).from_connection(connection) diff --git a/source/network/packet/abc/__init__.py b/source/network/packet/abc/__init__.py new file mode 100644 index 0000000..a7143d4 --- /dev/null +++ b/source/network/packet/abc/__init__.py @@ -0,0 +1 @@ +from .Packet import Packet diff --git a/source/utils/thread.py b/source/utils/thread.py index 0c49ca7..3679381 100644 --- a/source/utils/thread.py +++ b/source/utils/thread.py @@ -1,4 +1,8 @@ +from queue import Queue from threading import Thread +from typing import Any, Callable + +import pyglet class StoppableThread(Thread): @@ -9,7 +13,13 @@ class StoppableThread(Thread): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) - self._stop = False + self.stopped = False def stop(self) -> None: - self._stop = True + self.stopped = True + + +def in_pyglet_context(func: Callable, *args, **kwargs) -> Any: + queue = Queue() + pyglet.clock.schedule_once(lambda dt: queue.put(func(*args, **kwargs)), 0) + return queue.get()