From 93949062d6364317d72e8c5c5ebebe72d1cc9587 Mon Sep 17 00:00:00 2001 From: legonzaur Date: Sat, 11 May 2024 18:16:37 +0200 Subject: [PATCH] Closes #6; Multiples games in parallel --- src/controller/__init__.py | 2 + src/controller/client.py | 97 +++++++--------------- src/controller/game.py | 32 +++----- src/controller/lobby.py | 49 +++++++++++ src/controller/lobbyClient.py | 149 ++++++++++++++++++++++++++++++++++ src/main.py | 14 ++-- src/model/__init__.py | 17 ++-- src/model/game.py | 17 +++- src/model/lobby.py | 32 ++++++++ src/netcode/models.py | 1 + 10 files changed, 307 insertions(+), 103 deletions(-) create mode 100644 src/controller/lobby.py create mode 100644 src/controller/lobbyClient.py create mode 100644 src/model/lobby.py diff --git a/src/controller/__init__.py b/src/controller/__init__.py index 7c07eb6..7ab61e1 100644 --- a/src/controller/__init__.py +++ b/src/controller/__init__.py @@ -2,4 +2,6 @@ netcode messages and model functions""" from controller.client import ClientController +from controller.lobbyClient import LobbyClientController from controller.game import GameController +from controller.lobby import LobbyController diff --git a/src/controller/client.py b/src/controller/client.py index c5a3021..c879187 100644 --- a/src/controller/client.py +++ b/src/controller/client.py @@ -14,6 +14,7 @@ from .auth import discord_process_code if TYPE_CHECKING: from controller import GameController + from model import Game def vibe_check(t): @@ -50,24 +51,13 @@ class ClientController: websocket: WebSocketServerProtocol, ): self.websocket = websocket - self.on_message = self.login_context - self.game_controller = game_controller + self.game_controller.clients.append(self) + self.on_message = lambda _: None - async def listen(self) -> None: - """Hooks the client to the correct listen callbacks""" - await self.send( - Message( - type=MessageType.CONTEXT, - data_type="game_context_id", - data="login", - ) - ) - - await self.game_controller.announce_game(self.websocket) - - async for message in self.websocket: - await self.on_message(message) + async def close(self): + """Remove self from Game broadcasting list""" + self.game_controller.clients.remove(self) async def send(self, message: Message) -> None: """Send a Message object to the connected client""" @@ -95,65 +85,33 @@ class ClientController: Message(type=notification_type, data_type="object", data=data) ) - @vibe_check(LoginMessage) - async def login_context(self, message: LoginMessage) -> None: - """Handles all messages related to when the current client isn't yet logged in""" - username = message.data - if message.type == MessageType.REGISTER: + async def join_game( + self, game: "Game", username: str, discord_id: str, discord_avatar: str + ) -> None: + player = next( + (p for p in game.players if p.discord_id == discord_id), + None, + ) + + if player is None: if self.game_controller.game.started: return await self.send_error( "you cannot interact with a game that has already started" ) - if len(username) < 3: - return await self.send_error( - "username must be longer than 3 characters" - ) - - if self.game_controller.game.get_player(username) is not None: - return await self.send_error("username already exists") - player = await self.game_controller.game.create_player(username) - elif message.type == MessageType.LOGIN: - player2 = self.game_controller.game.get_player(username) - if player2 is None: - return await self.send_error("player doesn't exists") - - player = player2 - elif message.type == MessageType.DISCORD_LOGIN: - user = discord_process_code(message.data) - if user is None: - await self.send_error("discord login not successful") - return - username = user["global_name"] - player = next( - ( - p - for p in self.game_controller.game.players - if p.discord_id == user["id"] - ), - None, - ) - if player is None: - if self.game_controller.game.started: - return await self.send_error( - "you cannot interact with a game that has already started" - ) - player = await self.game_controller.game.create_player(username) - - player.discord_id = user["id"] - player.image = ( - "https://cdn.discordapp.com/avatars/" - + user["id"] - + "/" - + user["avatar"] - + ".webp" - ) - else: - return await self.send_error("invalid message type for context") + player.discord_id = discord_id + player.image = ( + "https://cdn.discordapp.com/avatars/" + + discord_id + + "/" + + discord_avatar + + ".webp" + ) self.player = player self.player.team.listeners.append(self.send_announce_dict) + if self.game_controller.game.started: self.on_message = self.game_context await self.send( @@ -188,6 +146,13 @@ class ClientController: data=self.player.uuid, ) ) + await self.send( + Message( + type=MessageType.SET, + data_type="current_game", + data=self.player.game.uuid, + ) + ) @vibe_check(Message) async def pregame_context(self, message: Message) -> None: diff --git a/src/controller/game.py b/src/controller/game.py index 545ec37..df4b5b4 100644 --- a/src/controller/game.py +++ b/src/controller/game.py @@ -1,5 +1,5 @@ import asyncio -from typing import Callable, List, Any +from typing import Callable, List, Any, TYPE_CHECKING from websockets.exceptions import ConnectionClosedOK from netcode import WSServer from netcode.ws_server import WebSocketServerProtocol @@ -9,27 +9,20 @@ from controller import ( ClientController, ) +if TYPE_CHECKING: + from controller import LobbyController + class GameController: - server: WSServer + lobby_controller: "LobbyController" game: Game + clients: list[ClientController] - clients: List[ClientController] - - def __init__(self, game: Game, server: WSServer) -> None: - self.server: WSServer = server + def __init__(self, game: Game, lobby_controller: "LobbyController") -> None: + self.lobby_controller: "LobbyController" = lobby_controller self.game: Game = game - self.server.on_new_connection_callback = self.on_new_connection + self.clients: list[ClientController] = [] self.game.listeners.append(self.broadcast_announce_dict) - self.clients: List[ClientController] = [] - - async def on_new_connection( - self, websocket: WebSocketServerProtocol, callback: Callable - ): - client = ClientController(self, websocket) - self.clients.append(client) - await asyncio.gather(client.listen(), callback()) - self.clients.remove(client) async def send(self, message: Message, websocket: WebSocketServerProtocol): try: @@ -38,7 +31,8 @@ class GameController: print("Connection Closed") async def broadcast(self, message: Message): - await self.server.broadcast(message.model_dump_json()) + for c in self.clients: + await c.send(message) async def broadcast_announce_dict( self, @@ -98,6 +92,7 @@ class GameController: ) for player in self.game.players: + # TODO : fix for coop await self.announce_dict( MessageType.CREATE, player.turn.serialize(), @@ -115,9 +110,6 @@ class GameController: websocket, ) - async def guest_handler(self, websocket: WebSocketServerProtocol): - await self.announce_game(websocket) - async def start_game(self): await self.game.start_game() for c in self.clients: diff --git a/src/controller/lobby.py b/src/controller/lobby.py new file mode 100644 index 0000000..1857e79 --- /dev/null +++ b/src/controller/lobby.py @@ -0,0 +1,49 @@ +import asyncio +from typing import Callable, List, Any +from websockets.exceptions import ConnectionClosedOK +from netcode import WSServer +from netcode.ws_server import WebSocketServerProtocol +from netcode.models import Message, MessageType +from model import Lobby +from controller import GameController, LobbyClientController + + +class LobbyController: + server: WSServer + lobby: Lobby + clients: List[LobbyClientController] + games: list[GameController] + + def __init__(self, server: WSServer) -> None: + self.server: WSServer = server + self.server.on_new_connection_callback = self.on_new_connection + self.clients: List[LobbyClientController] = [] + self.lobby = Lobby() + self.games: List[GameController] = [] + + async def on_new_connection( + self, websocket: WebSocketServerProtocol, callback: Callable + ): + client = LobbyClientController(self, websocket) + await asyncio.gather(client.listen(), callback()) + await client.close() + + async def remove_client(self, client: LobbyClientController): + self.clients.remove(client) + + async def broadcast_to_unconnected(self, message: Message): + for c in self.clients: + if c.game_client is None: + await c.send(message) + + async def create_game(self): + game = GameController(self.lobby.create_game(), self) + self.games.append(game) + await game.game.init_game() + await self.broadcast_to_unconnected( + Message( + type=MessageType.UPDATE, + data_type="object", + data=self.lobby.serialize(), + ), + ) diff --git a/src/controller/lobbyClient.py b/src/controller/lobbyClient.py new file mode 100644 index 0000000..e8637b3 --- /dev/null +++ b/src/controller/lobbyClient.py @@ -0,0 +1,149 @@ +"""Handles communication between one WebSocket tunnel and the game""" + +from typing import Callable, Any, TYPE_CHECKING, Union +from pydantic import ValidationError + +from websockets.server import WebSocketServerProtocol +from websockets.exceptions import ConnectionClosedOK + +from netcode.models import Message, LoginMessage, MessageType + +from .auth import discord_process_code + +from controller import ClientController + +if TYPE_CHECKING: + from controller import LobbyController + + +def vibe_check(t): + """Enforces the selected type for the message""" + + def decorator(func): + async def new_f(self, message): + try: + message = t.model_validate_json(message) + return await func(self, message) + except ValidationError as e: + print(e) + await self.send_error(e.errors()) + + return new_f + + return decorator + + +class LobbyClientController: + """Handles communication between one WebSocket client and the lobby""" + + websocket: WebSocketServerProtocol + lobby_controller: "LobbyController" + track_connection: Callable + + discord_uid: str | None + username: str | None + discord_avatar: str | None + + game_client: Union["ClientController", None] + + def __init__( + self, + lobby: "LobbyController", + websocket: WebSocketServerProtocol, + ): + self.lobby_controller = lobby + self.websocket = websocket + self.on_message = self.login_context + self.lobby_controller.clients.append(self) + self.discord_uid = None + self.username = None + self.discord_avatar = None + self.game_client = None + + async def listen(self) -> None: + """Hooks the client to the correct listen callbacks""" + await self.send( + Message( + type=MessageType.CONTEXT, + data_type="game_context_id", + data="login", + ) + ) + + async for message in self.websocket: + await self.on_message(message) + + async def close(self): + """Remove self from Lobby broadcasting list""" + if self.game_client is not None: + await self.game_client.close() + self.lobby_controller.clients.remove(self) + + async def send(self, message: Message) -> None: + """Send a Message object to the connected client""" + try: + await self.websocket.send(message.model_dump_json()) + except ConnectionClosedOK: + print("Connection Closed") + + async def send_error(self, error: str) -> None: + """Send an error string to the connected client""" + await self.send(Message(type=MessageType.ERROR, data_type="error", data=error)) + + @vibe_check(LoginMessage) + async def login_context(self, message: LoginMessage) -> None: + if message.type == MessageType.DISCORD_LOGIN: + user = discord_process_code(message.data) + if user is None: + return + if "id" in user: + self.discord_uid = user["id"] + self.username = user["username"] + self.discord_avatar = user["avatar"] + self.on_message = self.lobby_context + await self.send( + Message( + type=MessageType.CREATE, + data_type="object", + data=self.lobby_controller.lobby.serialize(), + ) + ) + await self.send( + Message( + type=MessageType.CONTEXT, + data_type="game_context_id", + data="lobby", + ) + ) + else: + print("user not connected") + + @vibe_check(Message) + async def lobby_context(self, message: Message) -> None: + if message.type == MessageType.CREATE: + if message.data_type == "game": + await self.lobby_controller.create_game() + if message.type == MessageType.JOIN: + if message.data_type == "game": + game_ctrler = next( + ( + g + for g in self.lobby_controller.games + if str(g.game.uuid) == message.data + ), + None, + ) + if game_ctrler is None: + return print("Game not found") + self.game_client = ClientController(game_ctrler, self.websocket) + self.on_message = self.game_context + await self.game_client.game_controller.announce_game(self.websocket) + await self.game_client.join_game( + game_ctrler.game, + self.username, + self.discord_uid, + self.discord_avatar, + ) + + async def game_context(self, message: Message) -> None: + await self.game_client.on_message(message) diff --git a/src/main.py b/src/main.py index 1f0dcb1..1697405 100644 --- a/src/main.py +++ b/src/main.py @@ -1,19 +1,21 @@ #!/usr/bin/env python import asyncio -from netcode.ws_server import WebSocketServerProtocol from netcode import WSServer -from controller import ClientController, GameController -from model import Game + +from controller import LobbyController + +# from model import Game gameServer = WSServer() -hero_game = Game() -game_controller = GameController(hero_game, gameServer) +# hero_game = Game() +# game_controller = GameController(hero_game, gameServer) +lobby = LobbyController(gameServer) async def main(): - await hero_game.init_game() + # await hero_game.init_game() await gameServer.serve() diff --git a/src/model/__init__.py b/src/model/__init__.py index 48c3e0f..a9921d4 100644 --- a/src/model/__init__.py +++ b/src/model/__init__.py @@ -1,15 +1,16 @@ -from model.serializable import Serializable +from .serializable import Serializable -from model.card_validator import ( +from .card_validator import ( CardRole, CardEffects, CardType, CardFaction, CardParser, ) -from model.card_loader import load_cards -from model.card import Card, Effect, EffectType, EffectTimes -from model.team import Team -from model.turn import Turn -from model.player import Player -from model.game import Game +from .card_loader import load_cards +from .card import Card, Effect, EffectType, EffectTimes +from .team import Team +from .turn import Turn +from .player import Player +from .game import Game +from .lobby import Lobby diff --git a/src/model/game.py b/src/model/game.py index 4642e06..7af72f3 100644 --- a/src/model/game.py +++ b/src/model/game.py @@ -1,7 +1,7 @@ """Instance of the game, with players and a market""" import asyncio -from typing import List +from typing import List, TYPE_CHECKING import random from model import Card, CardRole, CardParser, Player, Team, Serializable, Effect @@ -9,6 +9,9 @@ from model.card_loader import load_cards, create_card_effects, create_card from netcode.models import MessageType +if TYPE_CHECKING: + from model import Lobby + class Game(Serializable): """Instance of the game, with players and a market""" @@ -30,7 +33,9 @@ class Game(Serializable): current_turn: Team | None - def __init__(self) -> None: + lobby: "Lobby" + + def __init__(self, lobby: "Lobby") -> None: super().__init__() self.market_stack: List[Card] = [] self.market: List[Card | None] = [None for _ in range(5)] @@ -44,6 +49,9 @@ class Game(Serializable): self.current_turn = None + self.lobby = lobby + self.lobby.games.append(self) + async def init_game(self): await self.init_cards() await self.distribute_market_cards() @@ -137,7 +145,7 @@ class Game(Serializable): "effects": [effect.uuid for effect in self.effects], } - return {k: data[k] for k in data.keys() - ["loaded_cards"]} + return {k: data[k] for k in data.keys() - ["loaded_cards", "lobby"]} async def start_game(self): self.started = True @@ -176,3 +184,6 @@ class Game(Serializable): (m for m in [f for f in pile if f is not None] if str(m.uuid) == card_id), None, ) + + def end_game(self): + self.lobby.remove(self) diff --git a/src/model/lobby.py b/src/model/lobby.py new file mode 100644 index 0000000..50c7867 --- /dev/null +++ b/src/model/lobby.py @@ -0,0 +1,32 @@ +"""Instance of the game, with players and a market""" + +import asyncio +from typing import List +from model import Game, Serializable + +from model.card_loader import load_cards, create_card_effects, create_card + +from netcode.models import MessageType + + +class Lobby(Serializable): + """Instance of the game, with players and a market""" + + object_type = "lobby" + + games: list[Game] + + def __init__(self) -> None: + super().__init__() + self.games: List[Game] = [] + + def serialize(self) -> dict: + data = super().serialize() | { + "games": [game.uuid for game in self.games], + } + + return {k: data[k] for k in data.keys() - []} + + def create_game(self) -> Game: + game = Game(self) + return game diff --git a/src/netcode/models.py b/src/netcode/models.py index b2c65a4..0ac7b99 100644 --- a/src/netcode/models.py +++ b/src/netcode/models.py @@ -10,6 +10,7 @@ class MessageType(str, Enum): LOGIN = "login" READY = "ready" DISCORD_LOGIN = "discord_login" + JOIN = "join" CONTEXT = "context" CREATE = "create"