Closes #6; Multiples games in parallel
release-tag / release-image (push) Successful in 22s

This commit is contained in:
2024-05-11 18:16:37 +02:00
parent dc02d9b22f
commit 93949062d6
10 changed files with 307 additions and 103 deletions
+2
View File
@@ -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
+31 -66
View File
@@ -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:
+12 -20
View File
@@ -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:
+49
View File
@@ -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(),
),
)
+149
View File
@@ -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)
+8 -6
View File
@@ -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()
+9 -8
View File
@@ -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
+14 -3
View File
@@ -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)
+32
View File
@@ -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
+1
View File
@@ -10,6 +10,7 @@ class MessageType(str, Enum):
LOGIN = "login"
READY = "ready"
DISCORD_LOGIN = "discord_login"
JOIN = "join"
CONTEXT = "context"
CREATE = "create"