# lobby.py : gestion des lobbies (creation, classe, ready, demarrage) # les methodes retournent (conn_id, message), c'est main.py qui envoie import logging import random from dataclasses import dataclass, field logger = logging.getLogger(__name__) # pas de 0/O/I/1 pour eviter les confusions _LOBBY_CODE_CHARS = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789" _LOBBY_CODE_LENGTH = 6 MAX_PLAYERS = 3 VALID_CLASSES = {"kael", "seris", "aldric"} @dataclass class LobbyPlayer: conn_id: str username: str player_class: str | None = None is_ready: bool = False is_host: bool = False def to_dict(self) -> dict: return { "id": self.conn_id, "username": self.username, "class": self.player_class, "ready": self.is_ready, "host": self.is_host, } @dataclass class Lobby: code: str players: list[LobbyPlayer] = field(default_factory=list) started: bool = False def get_conn_ids(self) -> list[str]: return [p.conn_id for p in self.players] def get_player(self, conn_id): return next((p for p in self.players if p.conn_id == conn_id), None) def is_class_taken(self, class_name): return any(p.player_class == class_name for p in self.players) def all_ready(self): return ( len(self.players) == MAX_PLAYERS and all(p.is_ready and p.player_class for p in self.players) ) class LobbyManager: def __init__(self): self.lobbies: dict[str, Lobby] = {} self.conn_to_lobby: dict[str, str] = {} def _generate_code(self): while True: code = "".join(random.choices(_LOBBY_CODE_CHARS, k=_LOBBY_CODE_LENGTH)) if code not in self.lobbies: return code def _error(self, conn_id, code, message): return [(conn_id, {"type": "error", "code": code, "message": message})] def create_lobby(self, conn_id, username): # si on est deja dans un lobby on le libere d'abord, evite les blocages au retour menu leave_msgs = [] if conn_id in self.conn_to_lobby: leave_msgs = self.disconnect(conn_id) code = self._generate_code() player = LobbyPlayer(conn_id=conn_id, username=username, is_host=True) lobby = Lobby(code=code, players=[player]) self.lobbies[code] = lobby self.conn_to_lobby[conn_id] = code logger.info("Lobby %s créé par %s", code, username) return leave_msgs + [(conn_id, {"type": "lobby_created", "code": code})] def join_lobby(self, conn_id, username, code): code = code.upper() leave_msgs = [] if conn_id in self.conn_to_lobby: leave_msgs = self.disconnect(conn_id) lobby = self.lobbies.get(code) if not lobby: return self._error(conn_id, "lobby_not_found", f"Lobby {code} introuvable.") if lobby.started: return self._error(conn_id, "game_in_progress", "La partie a déjà commencé.") if len(lobby.players) >= MAX_PLAYERS: return self._error(conn_id, "lobby_full", "Le lobby est plein (3/3).") player = LobbyPlayer(conn_id=conn_id, username=username) lobby.players.append(player) self.conn_to_lobby[conn_id] = code messages = [ (conn_id, {"type": "lobby_joined", "code": code, "players": [p.to_dict() for p in lobby.players]}), ] # notifier les autres for p in lobby.players: if p.conn_id != conn_id: messages.append((p.conn_id, {"type": "player_joined", "player": player.to_dict()})) logger.info("%s a rejoint le lobby %s (%d/3)", username, code, len(lobby.players)) return leave_msgs + messages def select_class(self, conn_id, class_name): code = self.conn_to_lobby.get(conn_id) if not code: return self._error(conn_id, "not_in_lobby", "Tu n'es pas dans un lobby.") lobby = self.lobbies[code] player = lobby.get_player(conn_id) if not player: return self._error(conn_id, "not_in_lobby", "Joueur introuvable dans le lobby.") if class_name not in VALID_CLASSES: return self._error(conn_id, "invalid_class", f"Classe invalide : {class_name}.") # un joueur peut reselectionner sa propre classe sans erreur if lobby.is_class_taken(class_name) and player.player_class != class_name: return self._error(conn_id, "class_taken", f"La classe {class_name} est déjà prise.") player.player_class = class_name player.is_ready = False # reset du ready si on change de classe logger.info("%s a choisi la classe %s", player.username, class_name) return [(p.conn_id, {"type": "class_selected", "player_id": conn_id, "class": class_name}) for p in lobby.players] def set_ready(self, conn_id): code = self.conn_to_lobby.get(conn_id) if not code: return self._error(conn_id, "not_in_lobby", "Tu n'es pas dans un lobby.") lobby = self.lobbies[code] player = lobby.get_player(conn_id) if not player: return self._error(conn_id, "not_in_lobby", "Joueur introuvable.") if not player.player_class: return self._error(conn_id, "no_class_selected", "Choisis une classe avant de te mettre prêt.") player.is_ready = True logger.info("%s est prêt", player.username) return [(p.conn_id, {"type": "player_ready", "player_id": conn_id}) for p in lobby.players] def start_game(self, conn_id): code = self.conn_to_lobby.get(conn_id) if not code: return self._error(conn_id, "not_in_lobby", "Tu n'es pas dans un lobby.") lobby = self.lobbies[code] player = lobby.get_player(conn_id) if not player or not player.is_host: return self._error(conn_id, "not_host", "Seul le chef de partie peut démarrer.") if not lobby.all_ready(): return self._error( conn_id, "not_all_ready", f"Tous les joueurs doivent être prêts avec une classe ({len(lobby.players)}/3).", ) lobby.started = True logger.info("Partie démarrée dans le lobby %s", code) return [(p.conn_id, {"type": "game_starting", "countdown": 3}) for p in lobby.players] def leave_lobby(self, conn_id): # bouton Back : on quitte volontairement, meme logique que disconnect + confirmation if conn_id not in self.conn_to_lobby: return [] notifications = self.disconnect(conn_id) notifications.insert(0, (conn_id, {"type": "lobby_left"})) return notifications def disconnect(self, conn_id): code = self.conn_to_lobby.pop(conn_id, None) if not code: return [] lobby = self.lobbies.get(code) if not lobby: return [] player = lobby.get_player(conn_id) if not player: return [] lobby.players = [p for p in lobby.players if p.conn_id != conn_id] logger.info("%s a quitté le lobby %s", player.username, code) if not lobby.players: del self.lobbies[code] logger.info("Lobby %s supprimé (vide)", code) return [] # transferer l'host au premier restant if player.is_host: lobby.players[0].is_host = True return [(p.conn_id, {"type": "player_left", "player_id": conn_id}) for p in lobby.players]