Added prompt to save the game

This commit is contained in:
Faraphel 2023-03-04 12:46:58 +01:00
parent 5cbb28df72
commit 312210b323
15 changed files with 438 additions and 17 deletions

227
.save/127.0.0.1.bn-save Normal file
View file

@ -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
]
]
}
}

View file

@ -6,13 +6,11 @@ A faire :
- Historique / Replay - Historique / Replay
- Documenter - Documenter
2. Visuel : 2. Visuel :
- Rendre le texte de status plus visible - Rendre le texte de status plus visible
- Police d'écriture - Police d'écriture
- Changer les images, rajouter les fonds, ... - Changer les images, rajouter les fonds, ...
3. Hypothétique : 3. Hypothétique :
- Voir si les event listener intégré à pyglet sont plus pratiques que l'event propagation (?) - 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" (?) - Faire une scène incluant par défaut les boutons "Retour" (?)

View file

@ -0,0 +1,4 @@
from pathlib import Path
path_save: Path = Path(".save")
path_save.mkdir(exist_ok=True)

View file

@ -164,3 +164,5 @@ if __name__ == "__main__":
print(board.bomb((4, 3))) print(board.bomb((4, 3)))
print(board.bomb((4, 4))) print(board.bomb((4, 4)))
print(board) print(board)
print(board.to_json())

View file

@ -1,13 +1,15 @@
import json
import socket import socket
from typing import TYPE_CHECKING from typing import TYPE_CHECKING
from source import path_save
from source.core.enums import BombState from source.core.enums import BombState
from source.core.error import InvalidBombPosition, PositionAlreadyShot from source.core.error import InvalidBombPosition, PositionAlreadyShot
from source.gui.scene import GameResult from source.gui.scene import GameResult
from source.gui.scene.abc import Scene from source.gui.scene.abc import Scene
from source.gui import widget, texture, 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.type import Point2D
from source.utils import StoppableThread from source.utils import StoppableThread
@ -164,6 +166,12 @@ class Game(Scene):
style=texture.Button.Style1 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( self.button_quit = self.add_widget(
widget.Button, widget.Button,
@ -187,9 +195,6 @@ class Game(Scene):
font_size=20 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._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_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_ready_enemy: bool = False # does the opponent finished placing his boat ?
@ -266,6 +271,24 @@ class Game(Scene):
# function # 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): def game_end(self, won: bool):
self.window.add_scene(GameResult, game_scene=self, won=won) # affiche le résultat self.window.add_scene(GameResult, game_scene=self, won=won) # affiche le résultat
self.thread.stop() # coupe la connexion self.thread.stop() # coupe la connexion
@ -287,15 +310,13 @@ class Game(Scene):
def network_on_bomb_placed(self, packet: PacketBombPlaced): def network_on_bomb_placed(self, packet: PacketBombPlaced):
try: try:
# essaye de poser la bombe sur la grille alliée # 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): except (InvalidBombPosition, PositionAlreadyShot):
# si une erreur se produit, signale l'erreur # si une erreur se produit, signale l'erreur
bomb_state = BombState.ERROR bomb_state = BombState.ERROR
# l'opposant va rejouer, ce n'est donc pas notre tour # l'opposant va rejouer, ce n'est donc pas notre tour
self.my_turn = False self.my_turn = False
else: 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 # c'est à notre tour si l'opposant à loupé sa bombe
self.my_turn = not bomb_state.success self.my_turn = not bomb_state.success
@ -324,7 +345,7 @@ class Game(Scene):
self.boat_broken_ally += 1 self.boat_broken_ally += 1
# place la bombe sur la grille ennemie visuelle # 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: if packet.bomb_state is BombState.WON:
# si cette bombe a touché le dernier bateau, alors l'on a gagné # 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 from source.gui.scene import GameError
self.window.set_scene(GameError, text="L'adversaire a quitté la partie.") 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 # event
def on_resize_after(self, width: int, height: int): def on_resize_after(self, width: int, height: int):

View file

@ -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
)

View file

@ -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)

View file

@ -2,6 +2,8 @@ from .GameResult import GameResult
from .Game import Game from .Game import Game
from .GameQuit import GameQuit from .GameQuit import GameQuit
from .GameError import GameError from .GameError import GameError
from .GameSave import GameSave
from .GameLoad import GameLoad
from .Settings import Settings from .Settings import Settings
from .RoomHost import RoomHost from .RoomHost import RoomHost
from .RoomJoin import RoomJoin from .RoomJoin import RoomJoin

View file

@ -5,7 +5,7 @@ import numpy as np
import pyglet.shapes import pyglet.shapes
from source.core import Board, Boat 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.core.error import InvalidBoatPosition
from source.gui.sprite import Sprite from source.gui.sprite import Sprite
from source.gui.texture.abc import Style 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 except InvalidBoatPosition: self.display_board(self.board) # if the boat can't be placed, ignore
else: self.display_board(preview_board, preview=True) 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( 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, batch=self.scene.batch,
**self._bomb_kwargs **self._bomb_kwargs
) )
self._refresh_size() self._refresh_size()
return bomb_state
def on_click_release(self, rel_x: int, rel_y: int, button: int, modifiers: int): 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) cell = self.get_cell_from_rel(rel_x, rel_y)

View file

@ -33,6 +33,8 @@ class Client(StoppableThread):
print(f"[Client] Connecté avec {connection}") print(f"[Client] Connecté avec {connection}")
...
settings: Any = PacketSettings.from_connection(connection) settings: Any = PacketSettings.from_connection(connection)
PacketUsername(username=self.username).send_data_connection(connection) PacketUsername(username=self.username).send_data_connection(connection)
packet_username = PacketUsername.from_connection(connection) packet_username = PacketUsername.from_connection(connection)

View file

@ -1,6 +1,7 @@
import socket import socket
from typing import TYPE_CHECKING from typing import TYPE_CHECKING
from source import path_save
from source.gui import scene from source.gui import scene
from source.network import game_network from source.network import game_network
from source.utils import StoppableThread from source.utils import StoppableThread
@ -36,13 +37,19 @@ class Host(StoppableThread):
while True: while True:
try: 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 break # sort de la boucle
except socket.timeout: # en cas de timeout except socket.timeout: # en cas de timeout
if self.stopped: 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 # 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) self.settings.send_data_connection(connection)
packet_username = PacketUsername.from_connection(connection) packet_username = PacketUsername.from_connection(connection)

View file

@ -27,6 +27,8 @@ def game_network(
packet.PacketBombPlaced: game_scene.network_on_bomb_placed, packet.PacketBombPlaced: game_scene.network_on_bomb_placed,
packet.PacketBombState: game_scene.network_on_bomb_state, packet.PacketBombState: game_scene.network_on_bomb_state,
packet.PacketQuit: game_scene.network_on_quit, packet.PacketQuit: game_scene.network_on_quit,
packet.PacketAskSave: game_scene.network_on_ask_save,
packet.PacketResponseSave: game_scene.network_on_response_save,
} }
try: try:

View file

@ -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.
"""

View file

@ -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)

View file

@ -2,6 +2,8 @@ from .PacketChat import PacketChat
from .PacketBombPlaced import PacketBombPlaced from .PacketBombPlaced import PacketBombPlaced
from .PacketBombState import PacketBombState from .PacketBombState import PacketBombState
from .PacketBoatPlaced import PacketBoatPlaced from .PacketBoatPlaced import PacketBoatPlaced
from .PacketAskSave import PacketAskSave
from .PacketResponseSave import PacketResponseSave
from .PacketSettings import PacketSettings from .PacketSettings import PacketSettings
from .PacketUsername import PacketUsername from .PacketUsername import PacketUsername
from .PacketQuit import PacketQuit from .PacketQuit import PacketQuit