diff --git a/NOTE.md b/NOTE.md index 1570e50..30a4601 100644 --- a/NOTE.md +++ b/NOTE.md @@ -5,6 +5,8 @@ A faire : - Faire une scène incluant par défaut les boutons "Retour" (?) - Police d'écriture +- Voir si les event listener intégré à pyglet sont plus pratique que l'event propagation + Bug : - / @@ -17,4 +19,4 @@ Autre : Bonus ultime : -- Envoyer la texture de la grille à l'adversaire \ No newline at end of file +- Envoyer la texture de la grille à l'adversaire (???) \ No newline at end of file diff --git a/source/core/Board.py b/source/core/Board.py index 745e04d..5d1187a 100644 --- a/source/core/Board.py +++ b/source/core/Board.py @@ -81,13 +81,13 @@ class Board: if x >= self._columns or y >= self._rows: raise InvalidBombPosition(position) # if this position have already been shot - if not self._bombs[position]: raise PositionAlreadyShot(position) + if not self._bombs[y, x]: raise PositionAlreadyShot(position) # get the old board matrice board_mat_old_sum = self.get_matrice().sum() # place the bomb (setting the position to False cause the matrice multiplication to remove the boat if any) - self._bombs[position] = False + self._bombs[y, x] = False # get the new board matrice board_mat_new = self.get_matrice() diff --git a/source/core/enums/bomb.py b/source/core/enums/bomb.py index eaba00a..d714b08 100644 --- a/source/core/enums/bomb.py +++ b/source/core/enums/bomb.py @@ -8,3 +8,10 @@ class BombState(Enum): WON = 3 ERROR = 10 + + def to_bytes(self) -> bytes: + return self.value.to_bytes(1, "big") + + @classmethod + def from_bytes(cls, data: bytes): + return cls(int.from_bytes(data, "big")) diff --git a/source/gui/scene/Game.py b/source/gui/scene/Game.py index 9ec23e3..a1e61f7 100644 --- a/source/gui/scene/Game.py +++ b/source/gui/scene/Game.py @@ -8,6 +8,8 @@ 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.type import Point2D if TYPE_CHECKING: from source.gui.window import Window @@ -53,6 +55,11 @@ class Game(Scene): boat_batch=self.batch_grid_boat, ) + def board_ally_ready(): + connection.send(SocketType.BOAT_PLACED.value.to_bytes(1, "big")) + + self.grid_ally.add_listener("on_all_boats_placed", board_ally_ready) + self.grid_enemy = self.add_widget( GameGridEnemy, @@ -68,6 +75,12 @@ class Game(Scene): bomb_batch=self.batch_grid_bomb ) + 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()) + + self.grid_enemy.add_listener("on_request_place_bomb", board_enemy_bomb) + self.name_ally = self.add_widget( widget.Text, @@ -119,10 +132,10 @@ class Game(Scene): self.chat_log = self.add_widget( widget.Text, - x=10, y=70, width=0.5, + x=10, y=35, width=0.5, - text="FARAPHEL - HELLO BILLY\nLEO - HELLO BOLLO", - anchor_x="left", anchor_y="baseline", + text="", + anchor_x="left", multiline=True, batch=self.batch_label, @@ -131,7 +144,7 @@ class Game(Scene): self.chat_input = self.add_widget( widget.Input, - x=10, y=10, width=0.5, height=50, + x=10, y=10, width=0.5, height=30, style=texture.Button.Style1, @@ -140,8 +153,14 @@ class Game(Scene): ) def send_chat(): + text = self.chat_input.text + self.chat_input.text = "" + + 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(self.chat_input.text.encode()) + connection.send(text.encode()) self.chat_input.add_listener("on_enter", send_chat) diff --git a/source/gui/scene/RoomCreate.py b/source/gui/scene/RoomCreate.py index 35a7cb5..bdfd8b4 100644 --- a/source/gui/scene/RoomCreate.py +++ b/source/gui/scene/RoomCreate.py @@ -1,11 +1,10 @@ from typing import TYPE_CHECKING import pyglet -import requests -from source import network -from source.gui.scene.abc import Scene from source.gui import widget, texture +from source.gui.scene import RoomHost +from source.gui.scene.abc import Scene if TYPE_CHECKING: from source.gui.window import Window @@ -15,16 +14,9 @@ class RoomCreate(Scene): def __init__(self, window: "Window", **kwargs): super().__init__(window, **kwargs) - """r = requests.get('https://api.ipify.org') - r.raise_for_status() - ip_address: str = r.content.decode('utf8') - port: int = 52321""" - - ip_address = "127.0.0.1" - port = 52321 - - self.batch_button_background = pyglet.graphics.Batch() self.batch_label = pyglet.graphics.Batch() + self.batch_input_background = pyglet.graphics.Batch() + self.batch_button_background = pyglet.graphics.Batch() self.back = self.add_widget( widget.Button, @@ -38,39 +30,74 @@ class RoomCreate(Scene): label_batch=self.batch_label ) - self.back.add_listener("on_click_release", self.button_back_callback) - - self.label_ip = self.add_widget( - widget.Text, - - x=0.5, y=0.55, - - anchor_x="center", anchor_y="center", - text=f"Votre IP - {ip_address}:{port}", - font_size=20, - - batch=self.batch_label - ) - - self.description = self.add_widget( - widget.Text, - - x=0.5, y=0.45, - - anchor_x="center", anchor_y="center", - text="En attente d'un second joueur...", - - batch=self.batch_label - ) - - self.thread = network.Host(window=self.window, daemon=True, username="Host") - self.thread.start() - - def button_back_callback(self, *_): - self.thread.stop() from source.gui.scene import MainMenu - self.window.set_scene(MainMenu) + self.back.add_listener("on_click_release", lambda *_: self.window.set_scene(MainMenu)) + + self.add_widget( + widget.Text, + + x=0.1, y=0.9, + anchor_x="center", anchor_y="center", + text=f"Largeur de la grille", + + batch=self.batch_label + ) + + input_width = self.add_widget( + widget.Input, + + x=0.2, y=0.86, width=0.1, height=0.08, + + regex=r"\d+", + + style=texture.Input.Style1, + + label_text="8", + + background_batch=self.batch_input_background, + label_batch=self.batch_label + ) + + self.add_widget( + widget.Text, + + x=0.1, y=0.8, + anchor_x="center", anchor_y="center", + text=f"Longueur de la grille", + + batch=self.batch_label + ) + + input_height = self.add_widget( + widget.Input, + + x=0.2, y=0.76, width=0.1, height=0.08, + + regex=r"\d+", + + style=texture.Input.Style1, + + label_text="8", + + background_batch=self.batch_input_background, + label_batch=self.batch_label + ) + + 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, + + label_text="Continuer", + + style=texture.Button.Style1, + + background_batch=self.batch_button_background, + label_batch=self.batch_label + ) + + self.start.add_listener("on_click_release", lambda *_: self.window.set_scene(RoomHost)) def on_draw(self): + self.batch_input_background.draw() self.batch_button_background.draw() self.batch_label.draw() diff --git a/source/gui/scene/RoomHost.py b/source/gui/scene/RoomHost.py new file mode 100644 index 0000000..d2e86e6 --- /dev/null +++ b/source/gui/scene/RoomHost.py @@ -0,0 +1,76 @@ +from typing import TYPE_CHECKING + +import pyglet +import requests + +from source import network +from source.gui.scene.abc import Scene +from source.gui import widget, texture + +if TYPE_CHECKING: + from source.gui.window import Window + + +class RoomHost(Scene): + def __init__(self, window: "Window", **kwargs): + super().__init__(window, **kwargs) + + """r = requests.get('https://api.ipify.org') + r.raise_for_status() + ip_address: str = r.content.decode('utf8') + port: int = 52321""" + + ip_address = "127.0.0.1" + port = 52321 + + self.batch_button_background = pyglet.graphics.Batch() + self.batch_label = pyglet.graphics.Batch() + + self.back = self.add_widget( + widget.Button, + x=20, y=20, width=0.2, height=0.1, + + label_text="Retour", + + style=texture.Button.Style1, + + background_batch=self.batch_button_background, + label_batch=self.batch_label + ) + + self.back.add_listener("on_click_release", self.button_back_callback) + + self.label_ip = self.add_widget( + widget.Text, + + x=0.5, y=0.55, + + anchor_x="center", anchor_y="center", + text=f"Votre IP - {ip_address}:{port}", + font_size=20, + + batch=self.batch_label + ) + + self.description = self.add_widget( + widget.Text, + + x=0.5, y=0.45, + + anchor_x="center", anchor_y="center", + text="En attente d'un second joueur...", + + batch=self.batch_label + ) + + self.thread = network.Host(window=self.window, daemon=True, username="Host") + self.thread.start() + + def button_back_callback(self, *_): + self.thread.stop() + from source.gui.scene import MainMenu + self.window.set_scene(MainMenu) + + def on_draw(self): + self.batch_button_background.draw() + self.batch_label.draw() diff --git a/source/gui/scene/__init__.py b/source/gui/scene/__init__.py index ca9e8a8..b87473b 100644 --- a/source/gui/scene/__init__.py +++ b/source/gui/scene/__init__.py @@ -1,6 +1,7 @@ from .Game import Game from .Settings import Settings -from .RoomCreate import RoomCreate +from .RoomHost import RoomHost from .RoomJoin import RoomJoin +from .RoomCreate import RoomCreate from .MainMenu import MainMenu diff --git a/source/gui/texture/Grid.py b/source/gui/texture/Grid.py index f4b9a8e..7ea380e 100644 --- a/source/gui/texture/Grid.py +++ b/source/gui/texture/Grid.py @@ -20,8 +20,8 @@ class Grid: class Bomb: class Style1(Style): _animation = sorted( - (path_bomb / "animation").iterdir(), - key=lambda path: int(path.stem) + (path_bomb / "animation").iterdir(), + key=lambda path: int(path.stem) ) missed = [*_animation, path_bomb / "missed.png"], 0.03, False diff --git a/source/gui/widget/Text.py b/source/gui/widget/Text.py index 6a04c99..b3fe039 100644 --- a/source/gui/widget/Text.py +++ b/source/gui/widget/Text.py @@ -32,6 +32,14 @@ class Text(BoxWidget): self._refresh_size() + @property + def text(self): + return self.label.text + + @text.setter + def text(self, text: str): + self.label.text = text + def _refresh_size(self): self.label.x, self.label.y = self.xy self.label.width, self.label.height = self.size diff --git a/source/gui/widget/grid/GameGridAlly.py b/source/gui/widget/grid/GameGridAlly.py index 130b597..14effee 100644 --- a/source/gui/widget/grid/GameGridAlly.py +++ b/source/gui/widget/grid/GameGridAlly.py @@ -6,7 +6,6 @@ import numpy as np from source.core.enums import Orientation from source.core.error import InvalidBoatPosition -from source.gui import texture from source.gui.sprite import Sprite from source.gui.texture.abc import Style from source.gui.widget.grid.abc import GameGrid @@ -126,6 +125,8 @@ class GameGridAlly(GameGrid): 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) diff --git a/source/gui/widget/grid/GameGridEnemy.py b/source/gui/widget/grid/GameGridEnemy.py index 7fff668..40fbfc0 100644 --- a/source/gui/widget/grid/GameGridEnemy.py +++ b/source/gui/widget/grid/GameGridEnemy.py @@ -40,11 +40,9 @@ class GameGridEnemy(GameGrid): sprite.width = self.cell_width sprite.height = self.cell_height - def place_bomb(self, cell: Point2D): - from random import randint - + def place_bomb(self, cell: Point2D, touched: bool): self.cell_sprites[cell] = Sprite( - img=self.bomb_style.get("touched" if randint(0, 1) else "missed"), + img=self.bomb_style.get("touched" if touched else "missed"), **self._bomb_kwargs ) @@ -54,7 +52,7 @@ class GameGridEnemy(GameGrid): cell = self.get_cell_from_rel(rel_x, rel_y) if button == pyglet.window.mouse.LEFT: - self.place_bomb(cell) + self.trigger_event("on_request_place_bomb", cell) def draw(self): self.background.draw() diff --git a/source/network/Client.py b/source/network/Client.py index 538b36f..b19033a 100644 --- a/source/network/Client.py +++ b/source/network/Client.py @@ -1,10 +1,15 @@ 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.utils import StoppableThread if TYPE_CHECKING: @@ -29,7 +34,13 @@ class Client(StoppableThread): print(f"[Client] Connecté avec {connection}") - pyglet.clock.schedule_once(lambda dt: self.window.set_scene(scene.Game, connection=connection), 0) + 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 @@ -43,7 +54,31 @@ class Client(StoppableThread): socket_type = SocketType(int.from_bytes(data, "big")) - print(socket_type) - 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 + ) diff --git a/source/network/Host.py b/source/network/Host.py index 8c0b329..f9439cd 100644 --- a/source/network/Host.py +++ b/source/network/Host.py @@ -1,10 +1,15 @@ 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.utils import StoppableThread if TYPE_CHECKING: @@ -38,7 +43,13 @@ class Host(StoppableThread): print(f"[Serveur] Connecté avec {address}") - pyglet.clock.schedule_once(lambda dt: self.window.set_scene(scene.Game, connection=connection), 0) + 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 @@ -52,7 +63,31 @@ class Host(StoppableThread): socket_type = SocketType(int.from_bytes(data, "big")) - print(socket_type) - 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 + ) diff --git a/source/network/SocketType.py b/source/network/SocketType.py index ed212c1..20b41b4 100644 --- a/source/network/SocketType.py +++ b/source/network/SocketType.py @@ -5,3 +5,4 @@ class SocketType(Enum): CHAT = 0 BOAT_PLACED = 1 BOMB = 2 + BOMB_STATE = 3 diff --git a/source/network/packet/Bomb.py b/source/network/packet/Bomb.py new file mode 100644 index 0000000..36769c5 --- /dev/null +++ b/source/network/packet/Bomb.py @@ -0,0 +1,20 @@ +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 new file mode 100644 index 0000000..e406eee --- /dev/null +++ b/source/network/packet/Chat.py @@ -0,0 +1,6 @@ +from dataclasses import dataclass, field + + +@dataclass +class Chat: + message: str = field() diff --git a/source/network/packet/PacketBombState.py b/source/network/packet/PacketBombState.py new file mode 100644 index 0000000..ae589bf --- /dev/null +++ b/source/network/packet/PacketBombState.py @@ -0,0 +1,25 @@ +from dataclasses import dataclass, field + +from source.core.enums import BombState + + +@dataclass +class PacketBombState: + x: int = field() + y: int = field() + bomb_state: BombState = field() + + def to_bytes(self) -> bytes: + return ( + self.x.to_bytes(1, "big") + + self.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"), + bomb_state=BombState.from_bytes(data[2:3]) + )