diff --git a/NOTE.md b/NOTE.md index 3838bf9..e4d1abb 100644 --- a/NOTE.md +++ b/NOTE.md @@ -1,5 +1,6 @@ A faire : -- Ecran de configuration de la partie +- Limite des bateaux, taille des pseudo, ... + - Nom dans les options - Faire marcher le tchat - Sauvegarde / Quitter diff --git a/source/gui/scene/Game.py b/source/gui/scene/Game.py index 9c5a508..52bcecf 100644 --- a/source/gui/scene/Game.py +++ b/source/gui/scene/Game.py @@ -15,10 +15,26 @@ if TYPE_CHECKING: class Game(Scene): - def __init__(self, window: "Window", connection: socket.socket, **kwargs): + def __init__(self, window: "Window", + connection: socket.socket, + + boat_sizes: list, + name_ally: str, + name_enemy: str, + grid_width: int, + grid_height: int, + my_turn: bool, + + **kwargs): super().__init__(window, **kwargs) self.connection = connection + self.boat_sizes = boat_sizes + self.name_ally = name_ally + self.name_enemy = name_enemy + self.grid_width = grid_width + self.grid_height = grid_height + self.my_turn = my_turn # is it the player turn ? self.batch_label = pyglet.graphics.Batch() self.batch_button_background = pyglet.graphics.Batch() @@ -42,12 +58,12 @@ class Game(Scene): x=75, y=0.25, width=0.35, height=0.5, - boats_length=[5, 5, 4, 3, 2], + boats_length=self.boat_sizes, grid_style=texture.Grid.Style1, boat_style=texture.Grid.Boat.Style1, bomb_style=texture.Grid.Bomb.Style1, - rows=8, columns=8, + rows=self.grid_height, columns=self.grid_width, background_batch=self.batch_grid_background, line_batch=self.batch_grid_line, @@ -74,7 +90,7 @@ class Game(Scene): grid_style=texture.Grid.Style1, boat_style=texture.Grid.Boat.Style1, bomb_style=texture.Grid.Bomb.Style1, - rows=8, columns=8, + rows=self.grid_height, columns=self.grid_width, background_batch=self.batch_grid_background, line_batch=self.batch_grid_line, @@ -91,24 +107,24 @@ class Game(Scene): self.grid_enemy.add_listener("on_request_place_bomb", board_enemy_bomb) - self.name_ally = self.add_widget( + self.add_widget( widget.Text, x=0.27, y=0.995, - text="Raphael", + text=self.name_ally, font_size=20, anchor_x="center", anchor_y="center", batch=self.batch_label, ) - self.name_enemy = self.add_widget( + self.add_widget( widget.Text, x=0.73, y=0.995, - text="Leo", + text=self.name_enemy, font_size=20, anchor_x="center", anchor_y="center", @@ -212,10 +228,9 @@ class Game(Scene): batch=self.batch_label ) - self.board_ally = core.Board(rows=8, columns=8) - self.board_enemy = core.Board(rows=8, columns=8) + 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: bool = False # 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 ? self._boat_broken_ally: int = 0 diff --git a/source/gui/scene/RoomCreate.py b/source/gui/scene/RoomCreate.py index bdfd8b4..570a587 100644 --- a/source/gui/scene/RoomCreate.py +++ b/source/gui/scene/RoomCreate.py @@ -5,6 +5,7 @@ import pyglet from source.gui import widget, texture from source.gui.scene import RoomHost from source.gui.scene.abc import Scene +from source.network.packet import PacketSettings if TYPE_CHECKING: from source.gui.window import Window @@ -17,6 +18,7 @@ class RoomCreate(Scene): self.batch_label = pyglet.graphics.Batch() self.batch_input_background = pyglet.graphics.Batch() self.batch_button_background = pyglet.graphics.Batch() + self.batch_checkbox = pyglet.graphics.Batch() self.back = self.add_widget( widget.Button, @@ -33,6 +35,35 @@ class RoomCreate(Scene): from source.gui.scene import MainMenu self.back.add_listener("on_click_release", lambda *_: self.window.set_scene(MainMenu)) + # Username + + self.add_widget( + widget.Text, + + x=0.1, y=0.5, + + anchor_x="center", anchor_y="center", + + text="Pseudonyme", + + batch=self.batch_label + ) + + self.input_username = self.add_widget( + widget.Input, + + x=0.2, y=0.45, width=0.15, height=0.1, + + style=texture.Input.Style1, + + label_text="Host", + + background_batch=self.batch_input_background, + label_batch=self.batch_label + ) + + # Grid configuration + self.add_widget( widget.Text, @@ -43,7 +74,7 @@ class RoomCreate(Scene): batch=self.batch_label ) - input_width = self.add_widget( + self.input_width = self.add_widget( widget.Input, x=0.2, y=0.86, width=0.1, height=0.08, @@ -68,7 +99,7 @@ class RoomCreate(Scene): batch=self.batch_label ) - input_height = self.add_widget( + self.input_height = self.add_widget( widget.Input, x=0.2, y=0.76, width=0.1, height=0.08, @@ -83,6 +114,141 @@ class RoomCreate(Scene): label_batch=self.batch_label ) + # Tour + + self.checkbox_host_start = self.add_widget( + widget.Checkbox, + + x=0.4, y=0.8, width=0.05, height=0.1, + + style=texture.Checkbox.Style1, + + state=True, + + batch=self.batch_checkbox + ) + + self.add_widget( + widget.Text, + + x=0.46, y=0.85, + + anchor_y="center", + + text="Premier tour pour l'hôte", + + batch=self.batch_label + ) + + # taille et quantité des bateaux + + self.boat_size: int = 1 + self.boat_size_amount: dict[int, int] = {2: 1, 3: 1, 4: 2, 5: 1} + + def update_boat_size_text(): + self.label_boat_size.text = f"Taille: {self.boat_size}" + self.input_boat_amount.text = str(self.boat_size_amount.get(self.boat_size, 0)) + + self.label_boat_recap.text = "" + for size, amount in self.boat_size_amount.items(): + self.label_boat_recap.text += f"Taille: {size}, Quantité: {amount}\n" + + self.button_boat_size_previous = self.add_widget( + widget.Button, + + x=0.7, y=0.8, width=0.03, height=0.1, + + label_text="<", + label_font_size=25, + + style=texture.Button.Style1, + + background_batch=self.batch_button_background, + label_batch=self.batch_label + ) + + def previous_boat_size(): + if self.boat_size <= 1: return + self.boat_size -= 1 + update_boat_size_text() + + self.button_boat_size_previous.add_listener("on_click_release", lambda *_: previous_boat_size()) + + self.label_boat_size = self.add_widget( + widget.Text, + + x=0.8, y=0.85, + + anchor_x="center", anchor_y="center", + + batch=self.batch_label, + ) + + self.button_boat_size_next = self.add_widget( + widget.Button, + + x=0.87, y=0.8, width=0.03, height=0.1, + + label_text=">", + label_font_size=25, + + style=texture.Button.Style1, + + background_batch=self.batch_button_background, + label_batch=self.batch_label + ) + + def next_boat_size(): + if self.boat_size >= min(int(self.input_width.text), int(self.input_height.text)): return + self.boat_size += 1 + update_boat_size_text() + + self.button_boat_size_next.add_listener("on_click_release", lambda *_: next_boat_size()) + + self.input_boat_amount = self.add_widget( + widget.Input, + + x=0.7, y=0.68, width=0.2, height=0.08, + + regex=r"\d+", + + style=texture.Input.Style1, + + label_text="8", + + background_batch=self.batch_input_background, + label_batch=self.batch_label + ) + + def change_boat_amount(): + quantity = int(self.input_boat_amount.text) + + if quantity > 0: + self.boat_size_amount[self.boat_size] = quantity + + elif self.boat_size in self.boat_size_amount: + self.boat_size_amount.pop(self.boat_size) + + update_boat_size_text() + + self.input_boat_amount.add_listener("on_valid_text", lambda *_: change_boat_amount()) + + self.label_boat_recap = self.add_widget( + widget.Text, + + x=0.7, y=0.60, width=0.2, height=0.1, + + multiline=True, + + batch=self.batch_label + ) + + update_boat_size_text() + + # TODO: si on diminue la taille de la grille après avoir mis des bateaux de plus longue taille, faire un check + + # Démarrer + self.start = self.add_widget( widget.Button, x=lambda widget: widget.scene.window.width - 20 - widget.width, y=20, width=0.2, height=0.1, @@ -95,9 +261,21 @@ class RoomCreate(Scene): label_batch=self.batch_label ) - self.start.add_listener("on_click_release", lambda *_: self.window.set_scene(RoomHost)) + self.start.add_listener("on_click_release", lambda *_: self.confirm()) + + def confirm(self): + settings = PacketSettings( + username=self.input_username.text, + grid_width=int(self.input_width.text), + grid_height=int(self.input_height.text), + host_start=self.checkbox_host_start.state, + boat_size=[size for size, quantity in self.boat_size_amount.items() for _ in range(quantity)] + ) + + self.window.set_scene(RoomHost, settings=settings) def on_draw(self): self.batch_input_background.draw() self.batch_button_background.draw() + self.batch_checkbox.draw() self.batch_label.draw() diff --git a/source/gui/scene/RoomHost.py b/source/gui/scene/RoomHost.py index d2e86e6..6d75eb1 100644 --- a/source/gui/scene/RoomHost.py +++ b/source/gui/scene/RoomHost.py @@ -9,10 +9,11 @@ from source.gui import widget, texture if TYPE_CHECKING: from source.gui.window import Window + from source.network.packet import PacketSettings class RoomHost(Scene): - def __init__(self, window: "Window", **kwargs): + def __init__(self, window: "Window", settings: "PacketSettings", **kwargs): super().__init__(window, **kwargs) """r = requests.get('https://api.ipify.org') @@ -63,7 +64,7 @@ class RoomHost(Scene): batch=self.batch_label ) - self.thread = network.Host(window=self.window, daemon=True, username="Host") + self.thread = network.Host(window=self.window, daemon=True, settings=settings) self.thread.start() def button_back_callback(self, *_): diff --git a/source/gui/scene/RoomJoin.py b/source/gui/scene/RoomJoin.py index 5996c7e..6affb95 100644 --- a/source/gui/scene/RoomJoin.py +++ b/source/gui/scene/RoomJoin.py @@ -32,9 +32,25 @@ class RoomJoin(Scene): self.back.add_listener("on_click_release", self.button_back_callback) + # Pseudo + + self.entry_username = self.add_widget( + widget.Input, + x=0.4, y=0.55, width=0.2, height=0.1, + + style=texture.Input.Style1, + + label_text="Client", + + background_batch=self.batch_input_background, + label_batch=self.batch_label + ) + + # IP / Port + self.entry_ip = self.add_widget( widget.Input, - x=0.4, y=0.5, width=0.13, height=0.1, + x=0.4, y=0.45, width=0.13, height=0.1, regex=r"\d{1,3}(\.\d{1,3}){3}", @@ -48,7 +64,7 @@ class RoomJoin(Scene): self.entry_port = self.add_widget( widget.Input, - x=0.53, y=0.5, width=0.07, height=0.1, + x=0.53, y=0.45, width=0.07, height=0.1, regex=r"\d{0,5}", @@ -60,7 +76,7 @@ class RoomJoin(Scene): self.connect = self.add_widget( widget.Button, - x=0.4, y=0.4, width=0.2, height=0.1, + x=0.4, y=0.35, width=0.2, height=0.1, label_text="Se connecter", @@ -77,7 +93,7 @@ class RoomJoin(Scene): window=self.window, ip_address=self.entry_ip.text, daemon=True, - username="Client" + username=self.entry_username.text ).start() def button_back_callback(self, widget, *_): diff --git a/source/gui/widget/Input.py b/source/gui/widget/Input.py index 41be4ad..a9a808d 100644 --- a/source/gui/widget/Input.py +++ b/source/gui/widget/Input.py @@ -121,6 +121,9 @@ class Input(BoxWidget): if self.regex is not None: # si il y a un regex de validation, applique le pour vérifier le texte self.invalid = self.regex.fullmatch(self.text) is None + if not self.invalid: + self.trigger_event("on_valid_text") + def on_resize(self, width: int, height: int): self._refresh_size() diff --git a/source/network/Client.py b/source/network/Client.py index a5e4482..feb0e94 100644 --- a/source/network/Client.py +++ b/source/network/Client.py @@ -1,8 +1,12 @@ import socket -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, Any +from source.gui import scene from source.network import game_network +from source.network.packet import PacketUsername +from source.network.packet.abc import Packet from source.utils import StoppableThread +from source.utils.thread import in_pyglet_context if TYPE_CHECKING: from source.gui.window import Window @@ -30,4 +34,25 @@ class Client(StoppableThread): print(f"[Client] Connecté avec {connection}") - game_network(self, self.window, connection, False) + settings: Any = Packet.from_connection(connection) + PacketUsername(username=self.username).send_connection(connection) + + game_scene = in_pyglet_context( + self.window.set_scene, + scene.Game, + + connection=connection, + + boat_sizes=settings.boat_size, + name_ally=self.username, + name_enemy=settings.username, + grid_width=settings.grid_width, + grid_height=settings.grid_height, + my_turn=not settings.host_start + ) + + game_network( + thread=self, + connection=connection, + game_scene=game_scene, + ) diff --git a/source/network/Host.py b/source/network/Host.py index 85db6b0..6c7c2fa 100644 --- a/source/network/Host.py +++ b/source/network/Host.py @@ -1,11 +1,15 @@ import socket -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, Any +from source.gui import scene from source.network import game_network +from source.network.packet.abc import Packet from source.utils import StoppableThread +from source.utils.thread import in_pyglet_context if TYPE_CHECKING: from source.gui.window import Window + from source.network.packet import PacketSettings class Host(StoppableThread): @@ -13,11 +17,11 @@ class Host(StoppableThread): The thread executed on the person who create a room. """ - def __init__(self, window: "Window", username: str, port: int = 52321, **kw): + def __init__(self, window: "Window", settings: "PacketSettings", port: int = 52321, **kw): super().__init__(**kw) self.window = window - self.username = username + self.settings = settings self.port = port def run(self) -> None: @@ -39,4 +43,25 @@ class Host(StoppableThread): print(f"[Serveur] Connecté avec {address}") - game_network(self, self.window, connection, True) + self.settings.send_connection(connection) + packet_username: Any = Packet.from_connection(connection) + + game_scene = in_pyglet_context( + self.window.set_scene, + scene.Game, + + connection=connection, + + boat_sizes=self.settings.boat_size, + name_ally=self.settings.username, + name_enemy=packet_username.username, + grid_width=self.settings.grid_width, + grid_height=self.settings.grid_height, + my_turn=self.settings.host_start + ) + + game_network( + thread=self, + connection=connection, + game_scene=game_scene + ) diff --git a/source/network/game_network.py b/source/network/game_network.py index 073ea12..4dd2134 100644 --- a/source/network/game_network.py +++ b/source/network/game_network.py @@ -3,26 +3,26 @@ from typing import Any from source.core.enums import BombState from source.core.error import InvalidBombPosition, PositionAlreadyShot -from source.gui import scene +from source.gui.scene import Game 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, host: bool): +def game_network( + thread: "StoppableThread", + connection: socket.socket, + game_scene: Game, +): """ Run the networking to make the game work and react with the other player + :param game_scene: the scene of the game :param thread: the thread where this function is called. - :param window: the window of the game :param connection: the connection with the other player """ - game_scene = in_pyglet_context(window.set_scene, scene.Game, connection=connection) - game_scene.my_turn = host - while True: data: Any = Packet.from_connection(connection) diff --git a/source/network/packet/PacketSettings.py b/source/network/packet/PacketSettings.py new file mode 100644 index 0000000..478289f --- /dev/null +++ b/source/network/packet/PacketSettings.py @@ -0,0 +1,42 @@ +import struct +from dataclasses import dataclass, field + +from source.network.packet.abc import Packet + + +@dataclass +class PacketSettings(Packet): + username: str = field() + grid_width: int = field() + grid_height: int = field() + host_start: bool = field() + boat_size: list = field() + + packet_size: int = 51 + packet_format: str = ">16sBB?32B" + + def to_bytes(self): + boat_size = self.boat_size + ([0] * (32 - len(self.boat_size))) + + return struct.pack( + self.packet_format, + + self.username.encode("utf-8"), + self.grid_width, + self.grid_height, + self.host_start, + + *boat_size + ) + + @classmethod + def from_bytes(cls, data: bytes): + username, grid_width, grid_height, host_start, *boat_size = struct.unpack(cls.packet_format, data) + + return cls( + username=username.replace(b"\x00", b"").decode("utf-8"), + grid_width=grid_width, + grid_height=grid_height, + host_start=host_start, + boat_size=list(filter(lambda value: value != 0, boat_size)) + ) diff --git a/source/network/packet/PacketUsername.py b/source/network/packet/PacketUsername.py new file mode 100644 index 0000000..d4ae651 --- /dev/null +++ b/source/network/packet/PacketUsername.py @@ -0,0 +1,17 @@ +from dataclasses import dataclass, field + +from source.network.packet.abc import Packet + + +@dataclass +class PacketUsername(Packet): + username: str = field() + + packet_size: int = 16 + + def to_bytes(self): + return self.username.encode("utf-8") + + @classmethod + def from_bytes(cls, data: bytes): + return cls(username=data.decode("utf-8")) diff --git a/source/network/packet/__init__.py b/source/network/packet/__init__.py index 7cae4ad..c74e280 100644 --- a/source/network/packet/__init__.py +++ b/source/network/packet/__init__.py @@ -2,3 +2,5 @@ from .PacketChat import PacketChat from .PacketBombPlaced import PacketBombPlaced from .PacketBombState import PacketBombState from .PacketBoatPlaced import PacketBoatPlaced +from .PacketSettings import PacketSettings +from .PacketUsername import PacketUsername diff --git a/source/network/packet/abc/Packet.py b/source/network/packet/abc/Packet.py index 5dc2d62..9b4e345 100644 --- a/source/network/packet/abc/Packet.py +++ b/source/network/packet/abc/Packet.py @@ -2,6 +2,8 @@ import socket from abc import abstractmethod, ABC from typing import Type, Optional +# TODO: struct.calcsize() au lieu de packet_size + class Packet(ABC): packed_header: bytes