added the possibility to view in replay a previous game

This commit is contained in:
Faraphel 2023-03-07 14:52:05 +01:00
parent 15353991f6
commit b386fc03fa
9 changed files with 287 additions and 134 deletions

View file

@ -2,7 +2,6 @@ A faire :
1. Principal :
- Historique
- Paramètres (contenu : fps, volume dans le jeu, plein écran, ...) (bouton dans le jeu)
- Documenter (Docstring, README, ...)
@ -17,6 +16,7 @@ A faire :
Bug :
- (incertain) Dans de rare cas (souvent en fermant brutalement la fenêtre) le processus ne s'arrête pas
- Quitter pendant que l'on décide de si l'on doit charger ou non une ancienne sauvegarde fait crash l'adversaire
- Les champs invalides n'empêchent pas de faire l'action
Autre :
- Tester sur Linux

View file

@ -113,6 +113,20 @@ class Board:
# if the boat have been touched, but without sinking
return BombState.TOUCHED
def remove_bomb(self, cell: Point2D):
"""
Retire une bombe de la matrice
:param cell: cellule de la bombe
"""
x, y = cell
self.bombs[y, x] = True
def clear_bombs(self):
"""
Retire toutes les bombes de la planche
"""
self.bombs = np.ones(self.bombs.shape)
def get_matrice(self) -> np.array:
"""
:return: the boats and bombs represented as a matrice

37
source/gui/position.py Normal file
View file

@ -0,0 +1,37 @@
from typing import Callable
# pourcentage
def w_percent(value: int) -> Callable: # positionne en pourcentage la largeur
return lambda widget: int(widget.scene.window.width * (value / 100))
def h_percent(value: int) -> Callable: # positionne en pourcentage la hauteur
return lambda widget: int(widget.scene.window.height * (value / 100))
# pixel
def right(px: int) -> Callable: # positionne depuis la droite
return lambda widget: widget.scene.window.width - px
def up(px: int) -> Callable: # positionne depuis le haut
return lambda widget: widget.scene.window.height - px
def right_content(px: int) -> Callable: # positionne depuis la droite avec la taille du widget compris
return lambda widget: widget.scene.window.width - widget.width - px
def up_content(px: int) -> Callable: # positionne depuis le haut avec la taille du widget compris
return lambda widget: widget.scene.window.height - widget.height - px
# raccourci
w_full = w_percent(100)
h_full = h_percent(100)

View file

@ -8,10 +8,9 @@ from source.path import path_save, path_history
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.scene.abc import BaseGame
from source.gui import widget, texture, scene
from source.network.packet import PacketChat, PacketBombPlaced, PacketBoatPlaced, PacketBombState, PacketQuit, \
PacketAskSave, PacketResponseSave, PacketBoatsData
from source.network.packet import *
from source.type import Point2D
from source.utils import StoppableThread
@ -19,55 +18,18 @@ if TYPE_CHECKING:
from source.gui.window import Window
class Game(Scene):
class Game(BaseGame):
def __init__(self, window: "Window",
thread: StoppableThread,
connection: socket.socket,
boats_length: list,
name_ally: str,
name_enemy: str,
my_turn: bool,
grid_width: int = None,
grid_height: int = None,
board_ally_data: dict = None,
board_enemy_data: dict = None,
history: list[bool, Point2D] = None,
**kwargs):
super().__init__(window, **kwargs)
self.thread = thread
self.connection = connection
self.boats_length = boats_length
self.name_ally = name_ally
self.name_enemy = name_enemy
self.grid_width = grid_width
self.grid_height = grid_height
self.background = self.add_widget(
widget.Image,
x=0, y=0, width=1.0, height=1.0,
image=texture.Background.game,
)
self.grid_ally = self.add_widget(
widget.GameGrid,
x=75, y=0.25, width=0.35, height=0.5,
boats_length=self.boats_length,
grid_style=texture.Grid.Style1,
boat_style=texture.Grid.Boat.Style1,
rows=self.grid_height, columns=self.grid_width,
board_data=board_ally_data
)
def board_ally_ready(widget):
self.boat_ready_ally = True
@ -75,18 +37,6 @@ class Game(Scene):
self.grid_ally.add_listener("on_all_boats_placed", board_ally_ready)
self.grid_enemy = self.add_widget(
widget.GameGrid,
x=lambda widget: widget.scene.window.width - 75 - widget.width, y=0.25, width=0.35, height=0.5,
grid_style=texture.Grid.Style1,
boat_style=texture.Grid.Boat.Style1,
rows=self.grid_height, columns=self.grid_width,
board_data=board_enemy_data
)
def board_enemy_bomb(widget, cell: Point2D):
if not (self.boat_ready_ally and self.boat_ready_enemy): return
if not self.my_turn: return
@ -96,46 +46,6 @@ class Game(Scene):
self.grid_enemy.add_listener("on_request_place_bomb", board_enemy_bomb)
self.add_widget(
widget.Text,
x=0.27, y=0.995,
text=self.name_ally,
font_size=20,
anchor_x="center", anchor_y="center"
)
self.add_widget(
widget.Text,
x=0.73, y=0.995,
text=self.name_enemy,
font_size=20,
anchor_x="center", anchor_y="center"
)
self.score_ally = self.add_widget(
widget.Text,
x=0.44, y=0.995,
text="0",
font_size=25,
anchor_x="center", anchor_y="center"
)
self.score_enemy = self.add_widget(
widget.Text,
x=0.56, y=0.995,
text="0",
font_size=25,
anchor_x="center", anchor_y="center"
)
self.chat_log = self.add_widget(
widget.Text,
@ -185,19 +95,11 @@ class Game(Scene):
self.button_save.add_listener("on_click_release", ask_save)
self.button_quit = self.add_widget(
widget.Button,
x=0.85, y=0, width=0.15, height=0.1,
label_text="Quitter",
style=texture.Button.Style1
self.button_quit.add_listener(
"on_click_release",
lambda *_: self.window.add_scene(scene.GameQuit, game_scene=self)
)
self.button_quit.add_listener("on_click_release",
lambda *_: self.window.add_scene(scene.GameQuit, game_scene=self))
self.label_state = self.add_widget(
widget.Text,
@ -212,9 +114,7 @@ class Game(Scene):
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.history: list[tuple[bool, Point2D]] = [] if history is None else history # liste des bombes posées
if len(boats_length) == 0: # s'il n'y a pas de bateau à placé
if len(self.boats_length) == 0: # s'il n'y a pas de bateau à placé
self._boat_ready_ally = True # défini l'état de notre planche comme prête
PacketBoatPlaced().send_connection(connection) # indique à l'adversaire que notre planche est prête
@ -242,10 +142,6 @@ class Game(Scene):
"L'adversaire place ses bombes..."
)
def _refresh_score_text(self):
self.score_ally.text = str(self.grid_enemy.board.get_score())
self.score_enemy.text = str(self.grid_ally.board.get_score())
# property
@property
@ -312,15 +208,19 @@ class Game(Scene):
history=data["history"]
)
def get_save_suffix(self) -> str:
# Le suffix est un entier indiquant si c'est à notre tour ou non.
# Cet entier permet aux localhost de toujours pouvoir sauvegarder et charger sans problème.
ip_address, port = self.connection.getpeername()
return f"-{int(self.my_turn)}" if ip_address == "127.0.0.1" else ""
@property
def save_path(self) -> Path:
ip_address, port = self.connection.getpeername()
# Le nom du fichier est l'IP de l'opposent, suivi d'un entier indiquant si c'est à notre tour ou non.
# Cet entier permet aux localhost de toujours pouvoir sauvegarder et charger sans problème.
return path_save / (
ip_address +
(f"-{int(self.my_turn)}" if ip_address == "127.0.0.1" else "") +
self.get_save_suffix() +
".bn-save"
)
@ -328,6 +228,7 @@ class Game(Scene):
def history_path(self):
return path_history / (
datetime.now().strftime("%Y-%m-%d_%H-%M-%S") +
self.get_save_suffix() +
".bn-history"
)
@ -387,13 +288,12 @@ class Game(Scene):
else:
# c'est à notre tour si l'opposant à loupé sa bombe
self.my_turn = not bomb_state.success
# sauvegarde la bombe dans l'historique
self.history.append((False, packet.position))
# envoie le résultat à l'autre joueur
PacketBombState(position=packet.position, bomb_state=bomb_state).send_connection(self.connection)
# sauvegarde la bombe dans l'historique
self.history.append((False, packet.position))
self._refresh_score_text() # le score a changé, donc rafraichi son texte
if bomb_state is BombState.WON:

View file

@ -1,26 +1,96 @@
import json
from pathlib import Path
from typing import TYPE_CHECKING
from source.gui import widget
from source.gui.scene.abc import Scene
from source.gui import widget, texture
from source.gui.position import h_percent, w_percent
from source.gui.scene.abc import BaseGame
if TYPE_CHECKING:
from source.gui.window import Window
class HistoryGame(Scene):
class HistoryGame(BaseGame):
def __init__(self, window: "Window", history_path: Path, **kwargs):
super().__init__(window, **kwargs)
with open(history_path, "r", encoding="utf8") as file:
history_data = json.load(file)
super().__init__(
window,
boats_length=[],
name_ally=history_data["name_ally"],
name_enemy=history_data["name_enemy"],
board_ally_data=history_data["grid_ally"],
board_enemy_data=history_data["grid_enemy"],
history=history_data["history"],
**kwargs
)
self.history_path = history_path
self.add_widget(
widget.Text,
from source.gui.scene import MainMenu
self.button_quit.add_listener("on_click_release", lambda *_: window.set_scene(MainMenu))
x=0.5, y=0.5,
self.move_number: int = 0 # numéro du mouvement en cours
anchor_x="center", anchor_y="center",
self.grid_ally.board.clear_bombs()
self.grid_ally.refresh_board()
self.grid_enemy.board.clear_bombs()
self.grid_enemy.refresh_board()
text=str(history_path)
self.previous = self.add_widget(
widget.Button,
x=w_percent(20), y=h_percent(10), width=w_percent(20), height=h_percent(10),
label_text="Précédent",
style=texture.Button.Style1
)
self.previous.add_listener("on_click_release", lambda *_: self.previous_move())
self.next = self.add_widget(
widget.Button,
x=w_percent(60), y=h_percent(10), width=w_percent(20), height=h_percent(10),
label_text="Suivant",
style=texture.Button.Style1
)
self.next.add_listener("on_click_release", lambda *_: self.next_move())
self.text_move = self.add_widget(
widget.Text,
x=w_percent(50), y=h_percent(12),
anchor_x="center",
font_size=28,
)
self._refresh_move_text()
def _refresh_move_text(self):
self.text_move.text = f"{self.move_number} / {len(self.history)}"
self._refresh_score_text()
def previous_move(self):
# si le mouvement est au minimum, ignore
if self.move_number <= 0: return
self.move_number -= 1
turn, cell = self.history[self.move_number]
(self.grid_enemy if turn else self.grid_ally).remove_bomb(cell)
self._refresh_move_text()
def next_move(self):
# si le mouvement est au maximum, ignore
if self.move_number >= len(self.history): return
self.move_number += 1
turn, cell = self.history[self.move_number-1]
(self.grid_enemy if turn else self.grid_ally).place_bomb(cell)
self._refresh_move_text()

View file

@ -1,5 +1,6 @@
from typing import TYPE_CHECKING
from source.gui.position import w_full, h_full, h_percent
from source.gui.scene.abc import Scene
from source.gui import widget, scene, texture
@ -14,7 +15,7 @@ class MainMenu(Scene):
self.background = self.add_widget(
widget.Image,
x=0.0, y=0.0, width=1.0, height=1.0,
x=0, y=0, width=w_full, height=h_full,
image=texture.Background.main
)
@ -22,7 +23,7 @@ class MainMenu(Scene):
self.title = self.add_widget(
widget.Text,
x=50, y=0.85,
x=50, y=h_percent(85),
text="Bataille Navale",
font_size=50

View file

@ -0,0 +1,121 @@
from abc import ABC
from typing import TYPE_CHECKING
from source.gui import widget, texture
from source.gui.position import right_content, h_percent, w_percent, w_full, h_full
from source.gui.scene.abc import Scene
from source.type import Point2D
if TYPE_CHECKING:
from source.gui.window import Window
class BaseGame(Scene, ABC):
def __init__(self, window: "Window",
boats_length: list,
name_ally: str,
name_enemy: str,
grid_width: int = None,
grid_height: int = None,
board_ally_data: dict = None,
board_enemy_data: dict = None,
history: list[bool, Point2D] = None,
**kwargs):
super().__init__(window, **kwargs)
self.boats_length = boats_length
self.name_ally = name_ally
self.name_enemy = name_enemy
self.history: list[tuple[bool, Point2D]] = [] if history is None else history # liste des bombes posées
self.background = self.add_widget(
widget.Image,
x=0, y=0, width=w_full, height=h_full,
image=texture.Background.game,
)
self.grid_ally = self.add_widget(
widget.GameGrid,
x=75, y=0.25, width=0.35, height=0.5,
boats_length=self.boats_length,
grid_style=texture.Grid.Style1,
boat_style=texture.Grid.Boat.Style1,
rows=grid_height, columns=grid_width,
board_data=board_ally_data
)
self.grid_enemy = self.add_widget(
widget.GameGrid,
x=right_content(75), y=h_percent(25), width=w_percent(35), height=h_percent(50),
grid_style=texture.Grid.Style1,
boat_style=texture.Grid.Boat.Style1,
rows=grid_height, columns=grid_width,
board_data=board_enemy_data
)
self.add_widget(
widget.Text,
x=0.27, y=0.995,
text=self.name_ally,
font_size=20,
anchor_x="center", anchor_y="center"
)
self.add_widget(
widget.Text,
x=0.73, y=0.995,
text=self.name_enemy,
font_size=20,
anchor_x="center", anchor_y="center"
)
self.score_ally = self.add_widget(
widget.Text,
x=0.44, y=0.995,
text="0",
font_size=25,
anchor_x="center", anchor_y="center"
)
self.score_enemy = self.add_widget(
widget.Text,
x=0.56, y=0.995,
text="0",
font_size=25,
anchor_x="center", anchor_y="center"
)
self.button_quit = self.add_widget(
widget.Button,
x=0.85, y=0, width=0.15, height=0.1,
label_text="Quitter",
style=texture.Button.Style1
)
def _refresh_score_text(self):
self.score_ally.text = str(self.grid_enemy.board.get_score())
self.score_enemy.text = str(self.grid_ally.board.get_score())

View file

@ -1 +1,2 @@
from .Scene import Scene
from .BaseGame import BaseGame

View file

@ -94,7 +94,7 @@ class GameGrid(BoxWidget):
self.add_listener("on_hover", lambda _, *args: self._refresh_cursor(*args))
self._refresh_size()
self.display_board(self.board)
self.refresh_board()
def get_cell_from_rel(self, rel_x: int, rel_y: int) -> tuple[int, int]:
"""
@ -158,6 +158,10 @@ class GameGrid(BoxWidget):
def hide_cursor(self):
self.cursor.width, self.cursor.height = 0, 0
def refresh_board(self):
# rafraichi l'affichage de la grille
self.display_board(self.board)
def display_board(self, board: Board, preview: bool = False):
# remplacer par l'utilisation de board.boats ?
@ -239,7 +243,7 @@ class GameGrid(BoxWidget):
if len(self.boats_length) == 0:
self.trigger_event("on_all_boats_placed")
self.display_board(self.board) # rafraichi l'affichage
self.refresh_board() # rafraichi l'affichage
def preview_boat(self, cell: Point2D):
if len(self.boats_length) == 0: return
@ -251,7 +255,7 @@ class GameGrid(BoxWidget):
cell
)
except InvalidBoatPosition:
self.display_board(self.board) # if the boat can't be placed, ignore
self.refresh_board() # if the boat can't be placed, ignore
else: self.display_board(preview_board, preview=True)
@ -262,9 +266,14 @@ class GameGrid(BoxWidget):
x, y = cell
self.board.boats[y, x] = int(force_touched)
self.display_board(self.board)
self.refresh_board()
return bomb_state
def remove_bomb(self, cell: Point2D):
# retire une bombe de la planche
self.board.remove_bomb(cell)
self.refresh_board()
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)