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""" netcode messages and model functions"""
from controller.client import ClientController from controller.client import ClientController
from controller.lobbyClient import LobbyClientController
from controller.game import GameController from controller.game import GameController
from controller.lobby import LobbyController
+22 -57
View File
@@ -14,6 +14,7 @@ from .auth import discord_process_code
if TYPE_CHECKING: if TYPE_CHECKING:
from controller import GameController from controller import GameController
from model import Game
def vibe_check(t): def vibe_check(t):
@@ -50,24 +51,13 @@ class ClientController:
websocket: WebSocketServerProtocol, websocket: WebSocketServerProtocol,
): ):
self.websocket = websocket self.websocket = websocket
self.on_message = self.login_context
self.game_controller = game_controller self.game_controller = game_controller
self.game_controller.clients.append(self)
self.on_message = lambda _: None
async def listen(self) -> None: async def close(self):
"""Hooks the client to the correct listen callbacks""" """Remove self from Game broadcasting list"""
await self.send( self.game_controller.clients.remove(self)
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 send(self, message: Message) -> None: async def send(self, message: Message) -> None:
"""Send a Message object to the connected client""" """Send a Message object to the connected client"""
@@ -95,45 +85,14 @@ class ClientController:
Message(type=notification_type, data_type="object", data=data) Message(type=notification_type, data_type="object", data=data)
) )
@vibe_check(LoginMessage) async def join_game(
async def login_context(self, message: LoginMessage) -> None: self, game: "Game", username: str, discord_id: str, discord_avatar: str
"""Handles all messages related to when the current client isn't yet logged in""" ) -> None:
username = message.data
if message.type == MessageType.REGISTER:
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( player = next(
( (p for p in game.players if p.discord_id == discord_id),
p
for p in self.game_controller.game.players
if p.discord_id == user["id"]
),
None, None,
) )
if player is None: if player is None:
if self.game_controller.game.started: if self.game_controller.game.started:
return await self.send_error( return await self.send_error(
@@ -141,19 +100,18 @@ class ClientController:
) )
player = await self.game_controller.game.create_player(username) player = await self.game_controller.game.create_player(username)
player.discord_id = user["id"] player.discord_id = discord_id
player.image = ( player.image = (
"https://cdn.discordapp.com/avatars/" "https://cdn.discordapp.com/avatars/"
+ user["id"] + discord_id
+ "/" + "/"
+ user["avatar"] + discord_avatar
+ ".webp" + ".webp"
) )
else:
return await self.send_error("invalid message type for context")
self.player = player self.player = player
self.player.team.listeners.append(self.send_announce_dict) self.player.team.listeners.append(self.send_announce_dict)
if self.game_controller.game.started: if self.game_controller.game.started:
self.on_message = self.game_context self.on_message = self.game_context
await self.send( await self.send(
@@ -188,6 +146,13 @@ class ClientController:
data=self.player.uuid, data=self.player.uuid,
) )
) )
await self.send(
Message(
type=MessageType.SET,
data_type="current_game",
data=self.player.game.uuid,
)
)
@vibe_check(Message) @vibe_check(Message)
async def pregame_context(self, message: Message) -> None: async def pregame_context(self, message: Message) -> None:
+12 -20
View File
@@ -1,5 +1,5 @@
import asyncio import asyncio
from typing import Callable, List, Any from typing import Callable, List, Any, TYPE_CHECKING
from websockets.exceptions import ConnectionClosedOK from websockets.exceptions import ConnectionClosedOK
from netcode import WSServer from netcode import WSServer
from netcode.ws_server import WebSocketServerProtocol from netcode.ws_server import WebSocketServerProtocol
@@ -9,27 +9,20 @@ from controller import (
ClientController, ClientController,
) )
if TYPE_CHECKING:
from controller import LobbyController
class GameController: class GameController:
server: WSServer lobby_controller: "LobbyController"
game: Game game: Game
clients: list[ClientController]
clients: List[ClientController] def __init__(self, game: Game, lobby_controller: "LobbyController") -> None:
self.lobby_controller: "LobbyController" = lobby_controller
def __init__(self, game: Game, server: WSServer) -> None:
self.server: WSServer = server
self.game: Game = game 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.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): async def send(self, message: Message, websocket: WebSocketServerProtocol):
try: try:
@@ -38,7 +31,8 @@ class GameController:
print("Connection Closed") print("Connection Closed")
async def broadcast(self, message: Message): 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( async def broadcast_announce_dict(
self, self,
@@ -98,6 +92,7 @@ class GameController:
) )
for player in self.game.players: for player in self.game.players:
# TODO : fix for coop
await self.announce_dict( await self.announce_dict(
MessageType.CREATE, MessageType.CREATE,
player.turn.serialize(), player.turn.serialize(),
@@ -115,9 +110,6 @@ class GameController:
websocket, websocket,
) )
async def guest_handler(self, websocket: WebSocketServerProtocol):
await self.announce_game(websocket)
async def start_game(self): async def start_game(self):
await self.game.start_game() await self.game.start_game()
for c in self.clients: 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 #!/usr/bin/env python
import asyncio import asyncio
from netcode.ws_server import WebSocketServerProtocol
from netcode import WSServer from netcode import WSServer
from controller import ClientController, GameController
from model import Game from controller import LobbyController
# from model import Game
gameServer = WSServer() gameServer = WSServer()
hero_game = Game() # hero_game = Game()
game_controller = GameController(hero_game, gameServer) # game_controller = GameController(hero_game, gameServer)
lobby = LobbyController(gameServer)
async def main(): async def main():
await hero_game.init_game() # await hero_game.init_game()
await gameServer.serve() 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, CardRole,
CardEffects, CardEffects,
CardType, CardType,
CardFaction, CardFaction,
CardParser, CardParser,
) )
from model.card_loader import load_cards from .card_loader import load_cards
from model.card import Card, Effect, EffectType, EffectTimes from .card import Card, Effect, EffectType, EffectTimes
from model.team import Team from .team import Team
from model.turn import Turn from .turn import Turn
from model.player import Player from .player import Player
from model.game import Game 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""" """Instance of the game, with players and a market"""
import asyncio import asyncio
from typing import List from typing import List, TYPE_CHECKING
import random import random
from model import Card, CardRole, CardParser, Player, Team, Serializable, Effect 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 from netcode.models import MessageType
if TYPE_CHECKING:
from model import Lobby
class Game(Serializable): class Game(Serializable):
"""Instance of the game, with players and a market""" """Instance of the game, with players and a market"""
@@ -30,7 +33,9 @@ class Game(Serializable):
current_turn: Team | None current_turn: Team | None
def __init__(self) -> None: lobby: "Lobby"
def __init__(self, lobby: "Lobby") -> None:
super().__init__() super().__init__()
self.market_stack: List[Card] = [] self.market_stack: List[Card] = []
self.market: List[Card | None] = [None for _ in range(5)] self.market: List[Card | None] = [None for _ in range(5)]
@@ -44,6 +49,9 @@ class Game(Serializable):
self.current_turn = None self.current_turn = None
self.lobby = lobby
self.lobby.games.append(self)
async def init_game(self): async def init_game(self):
await self.init_cards() await self.init_cards()
await self.distribute_market_cards() await self.distribute_market_cards()
@@ -137,7 +145,7 @@ class Game(Serializable):
"effects": [effect.uuid for effect in self.effects], "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): async def start_game(self):
self.started = True 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), (m for m in [f for f in pile if f is not None] if str(m.uuid) == card_id),
None, 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" LOGIN = "login"
READY = "ready" READY = "ready"
DISCORD_LOGIN = "discord_login" DISCORD_LOGIN = "discord_login"
JOIN = "join"
CONTEXT = "context" CONTEXT = "context"
CREATE = "create" CREATE = "create"