L3-Bataille-Navale/source/gui/scene/Game.py

449 lines
14 KiB
Python

import json
import socket
from datetime import datetime, timedelta
from pathlib import Path
from typing import TYPE_CHECKING, Optional
from source 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 import widget, texture, scene
from source.network.packet import PacketChat, PacketBombPlaced, PacketBoatPlaced, PacketBombState, PacketQuit, \
PacketAskSave, PacketResponseSave, PacketBoatsData
from source.type import Point2D
from source.utils import StoppableThread
if TYPE_CHECKING:
from source.gui.window import Window
class Game(Scene):
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
PacketBoatPlaced().send_connection(connection)
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
PacketBombPlaced(position=cell).send_connection(connection)
self.my_turn = False
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,
x=10, y=45, width=0.4,
text="",
anchor_x="left", anchor_y="bottom",
multiline=True
)
self.chat_input = self.add_widget(
widget.Input,
x=10, y=10, width=0.5, height=30,
type_regex=".{0,60}",
style=texture.Button.Style1
)
def send_chat(widget):
text: str = widget.text
widget.text = ""
self.chat_new_message(self.name_ally, text)
PacketChat(message=text).send_connection(connection)
self.chat_input.add_listener("on_enter", send_chat)
self.button_save = self.add_widget(
widget.Button,
x=0.7, y=0, width=0.15, height=0.1,
label_text="Sauvegarder",
style=texture.Button.Style1
)
def ask_save(widget, x, y, button, modifiers):
if not (self._boat_ready_ally and self._boat_ready_enemy):
self.chat_new_message("System", "Veuillez poser vos bateaux avant de sauvegarder.", system=True)
return
PacketAskSave().send_connection(self.connection)
self.chat_new_message("System", "demande de sauvegarde envoyé.", system=True)
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.label_state = self.add_widget(
widget.Text,
x=0.5, y=0.15,
anchor_x="center",
font_size=20
)
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_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é
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
self.save_cooldown: Optional[datetime] = None
self._refresh_turn_text()
self._refresh_score_text()
# refresh
def _refresh_chat_box(self):
# supprime des messages jusqu'à ce que la boite soit plus petite que un quart de la fenêtre
while self.chat_log.label.content_height > (self.window.height / 4):
chat_logs: list[str] = self.chat_log.text.split("\n")
self.chat_log.text = "\n".join(chat_logs[1:])
# ajuste la boite de message pour être collé en bas
self.chat_log.label.y = self.chat_log.y + self.chat_log.label.content_height
def _refresh_turn_text(self):
self.label_state.text = (
"Placer vos bateaux" if not self.boat_ready_ally else
"L'adversaire place ses bateaux..." if not self.boat_ready_enemy else
"Placer vos bombes" if self.my_turn else
"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
def my_turn(self):
return self._my_turn
@my_turn.setter
def my_turn(self, my_turn: bool):
self._my_turn = my_turn
self._refresh_turn_text()
@property
def boat_ready_ally(self):
return self._boat_ready_ally
@boat_ready_ally.setter
def boat_ready_ally(self, boat_ready_ally: bool):
self._boat_ready_ally = boat_ready_ally
self._refresh_turn_text()
@property
def boat_ready_enemy(self):
return self._boat_ready_enemy
@boat_ready_enemy.setter
def boat_ready_enemy(self, boat_ready_enemy: bool):
self._boat_ready_enemy = boat_ready_enemy
self._refresh_turn_text()
# function
def to_json(self) -> dict:
return {
"name_ally": self.name_ally,
"name_enemy": self.name_enemy,
"my_turn": self.my_turn,
"grid_ally": self.grid_ally.board.to_json(),
"grid_enemy": self.grid_enemy.board.to_json(),
"history": self.history,
}
@classmethod
def from_json(cls,
data: dict,
window: "Window",
thread: StoppableThread,
connection: socket.socket) -> "Game":
return cls(
window=window,
thread=thread,
connection=connection,
boats_length=[],
name_ally=data["name_ally"],
name_enemy=data["name_enemy"],
my_turn=data["my_turn"],
board_ally_data=data["grid_ally"],
board_enemy_data=data["grid_enemy"],
history=data["history"]
)
@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 "") +
".bn-save"
)
@property
def history_path(self):
return path_history / (
datetime.now().strftime("%Y-%m-%d_%H-%M-%S") +
".bn-history"
)
def save_to_path(self, path: Path):
with open(path, "w", encoding="utf-8") as file:
json.dump(self.to_json(), file, ensure_ascii=False)
def save(self, value: bool):
self.chat_new_message(
"System",
"Sauvegarde de la partie..." if value else "Sauvegarde de la partie refusé.",
system=True
)
if not value: return
self.save_to_path(self.save_path)
def game_end(self, won: bool):
# envoie notre planche à l'adversaire
PacketBoatsData(boats=self.grid_ally.board.boats).send_data_connection(self.connection)
packet_boats = PacketBoatsData.from_connection(self.connection)
self.grid_enemy.board.boats = packet_boats.boats
# s'il existe une ancienne sauvegarde, efface la
self.save_path.unlink(missing_ok=True)
# sauvegarde cette partie dans l'historique
self.save_to_path(self.history_path)
self.window.add_scene(GameResult, game_scene=self, won=won) # affiche le résultat
self.thread.stop()
def chat_new_message(self, author: str, content: str, system: bool = False):
deco_left, deco_right = "<>" if system else "[]"
message: str = f"{deco_left}{author}{deco_right} - {content}"
self.chat_log.text += "\n" + message
self._refresh_chat_box()
# network
def network_on_chat(self, packet: PacketChat):
self.chat_new_message(self.name_enemy, packet.message)
def network_on_boat_placed(self, packet: PacketBoatPlaced):
self.boat_ready_enemy = True
def network_on_bomb_placed(self, packet: PacketBombPlaced):
try:
# essaye de poser la bombe sur la grille alliée
bomb_state = self.grid_ally.place_bomb(packet.position)
except (InvalidBombPosition, PositionAlreadyShot):
# si une erreur se produit, signale l'erreur
bomb_state = BombState.ERROR
# l'opposant va rejouer, ce n'est donc pas notre tour
self.my_turn = False
else:
# c'est à notre tour si l'opposant à loupé sa bombe
self.my_turn = not bomb_state.success
# 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:
# si l'ennemie à gagner, alors l'on a perdu
self.game_end(won=False)
def network_on_bomb_state(self, packet: PacketBombState):
if packet.bomb_state is BombState.ERROR:
# si une erreur est survenue, on rejoue
self.my_turn = True
return
# on rejoue uniquement si la bombe à toucher
self.my_turn = packet.bomb_state.success
self._refresh_score_text() # le score a changé, donc rafraichi son texte
# place la bombe sur la grille ennemie visuelle
self.grid_enemy.place_bomb(packet.position, force_touched=packet.bomb_state.success)
# sauvegarde la bombe dans l'historique
self.history.append((True, packet.position))
if packet.bomb_state is BombState.WON:
# si cette bombe a touché le dernier bateau, alors l'on a gagné
self.game_end(won=True)
def network_on_quit(self, packet: PacketQuit):
self.thread.stop() # coupe la connexion
from source.gui.scene import GameError
self.window.set_scene(GameError, text="L'adversaire a quitté la partie.")
def network_on_ask_save(self, packet: PacketAskSave):
now = datetime.now()
if self.save_cooldown is not None: # si un cooldown est défini
if self.save_cooldown + timedelta(seconds=40) >= now:
# si l'action à déjà été faite dans les 40 dernières secondes, ignore la requête de sauvegarde
return
self.save_cooldown = now
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
def on_resize_after(self, width: int, height: int):
self._refresh_chat_box()