refactor(Lobby/model): metadata, pydantic models

parent f3c9c952
from collections import defaultdict import json
from typing import List, Dict from typing import List, Dict, Optional
from pydantic.main import BaseModel
from socketio import AsyncServer from socketio import AsyncServer
from server.game.manager import ClientManager from server.game.manager import ClientManager
...@@ -9,14 +10,20 @@ from server.model.game import Game ...@@ -9,14 +10,20 @@ from server.model.game import Game
from server.model.players import Player, Announce from server.model.players import Player, Announce
class Metadata(BaseModel):
ready: bool = False
sid: str = ""
last_announce: Optional[Announce] = None
class LobbyManager(ClientManager): class LobbyManager(ClientManager):
""" A ClientManager that handles a lobby, then orchestrates games. """ A ClientManager that handles a lobby, then orchestrates games.
""" """
def __init__(self, sio: AsyncServer): def __init__(self, sio: AsyncServer):
super().__init__() super().__init__()
self.last_announces: Dict[Player, Announce] = {} self.lobby: Dict[str, Player] = {}
self.lobby: Dict[Player, bool] = {} self.metadata: Dict[str, Metadata] = {}
self.games: List[Game] = [] self.games: List[Game] = []
self.sio = sio self.sio = sio
...@@ -26,28 +33,49 @@ class LobbyManager(ClientManager): ...@@ -26,28 +33,49 @@ class LobbyManager(ClientManager):
@property @property
def players_ready(self): def players_ready(self):
return [k for k, v in self.lobby.items() if v] return [self.lobby[k] for k, m in self.metadata.items() if m.ready]
async def add_player(self, player: Player, is_ready: bool = False) -> None: async def add_player(self, player: Player, is_ready: bool = False) -> str:
self.lobby[player] = is_ready self.lobby[player.name] = player
self.metadata[player.name] = Metadata(ready=is_ready)
print(f"Added {player} to a lobby with {len(self.lobby)} players.") print(f"Added {player} to a lobby with {len(self.lobby)} players.")
await self.sio.emit('messageChannel', "PONG") return f"Bienvenu, {player}! Il y a {len(self.lobby)} joueurs en ligne."
def wants_to_play(self, player: Player): def wants_to_play(self, player: Player):
self.lobby[player] = True self.metadata[player.name].ready = True
print(f"{player} ready to play! ({self.nb_ready} people ready)") print(f"{player} ready to play! ({self.nb_ready} people ready)")
def announces(self, player: Player, announce: Announce): def announces(self, player: Player, announce: Announce):
# FIXME: Call this message on incoming "ANNOUNCE" # FIXME: Call this message on incoming "ANNOUNCE"
self.last_announces[player] = announce self.metadata[player.name].last_announce = announce
print(f"{player} ready to play! ({self.nb_ready} people ready)") print(f"{player} ready to play! ({self.nb_ready} people ready)")
def start_game(self, max_players=2): def start_game(self, max_players=2):
players = self.players_ready[:max_players] players = self.players_ready[:max_players]
self.games.append(Game(players, manager=self)) self.games.append(Game(players, manager=self))
for p in players:
self.metadata[p.name].ready = False
print(f"Game started : {' vs '.join([p.name for p in players])}") print(f"Game started : {' vs '.join([p.name for p in players])}")
def send(self, to: Player, message: MessageToPlayer, extra=None): def send(self, to: Player, message: MessageToPlayer, extra=None):
self.sio.send(message) self.sio.send(message)
pass pass
def handle_message(self, sid, data):
sender: Optional[Player] = None
sanitized = str(data)
print(f"Lobby| Received message from {sid}: {data}.")
for player in self.players:
if player.name in sanitized:
sender = player
if sender:
print(f"Lobby| Found sender: {sender.name}")
message = MessageToPlayer.Waiting
extras = [p.name for p in self.players]
body = {"message": message.name}
if extras:
body["extras"] = extras
return json.dumps(body)
from dataclasses import dataclass
from enum import Enum from enum import Enum
from typing import Optional from typing import Optional
from pydantic.main import BaseModel
class Value(Enum): class Value(Enum):
Two = 1 Two = 1
...@@ -26,8 +27,7 @@ class Color(Enum): ...@@ -26,8 +27,7 @@ class Color(Enum):
Diamonds = "♦" Diamonds = "♦"
@dataclass(frozen=True) class Card(BaseModel):
class Card:
value: Value value: Value
color: Optional[Color] = None color: Optional[Color] = None
...@@ -57,4 +57,4 @@ def lowest_value_and_rest(): ...@@ -57,4 +57,4 @@ def lowest_value_and_rest():
otherValues = list(Value) otherValues = list(Value)
otherValues.remove(lowValue) otherValues.remove(lowValue)
return lowValue, otherValues return lowValue, otherValues
\ No newline at end of file
from random import randrange, shuffle from random import randrange, shuffle
from typing import List
from server.model.card import Card, Value, Color from server.model.card import Card, Value, Color
...@@ -7,7 +6,7 @@ from server.model.card import Card, Value, Color ...@@ -7,7 +6,7 @@ from server.model.card import Card, Value, Color
class Deck: class Deck:
def __init__(self, cards=None): def __init__(self, cards=None):
if cards is None: if cards is None:
cards = [Card(v, c) for v in Value for c in Color] cards = [Card(value=v, color=c) for v in Value for c in Color]
else: else:
print("Deck init with cards:", cards, len(cards)) print("Deck init with cards:", cards, len(cards))
self.cards = cards self.cards = cards
......
from collections import Counter from collections import Counter
from typing import List from typing import List
from pydantic.main import BaseModel
from server.model.card import Card, Value from server.model.card import Card, Value
class Hand: class Hand(BaseModel):
def __init__(self, cards: List[Card] = None): cards: List[Card] = []
if cards is None:
cards = []
self.cards: List[Card] = cards
def __contains__(self, item: Card): def __contains__(self, item: Card):
return item in self.cards return item in self.cards
......
...@@ -5,44 +5,52 @@ from server.model.card import Value, Color, Card ...@@ -5,44 +5,52 @@ from server.model.card import Value, Color, Card
from server.model.hand import Hand from server.model.hand import Hand
def hand(cards: List[Card]) -> Hand:
return Hand(cards=cards)
def card(value: Value, color: Color) -> Card:
return Card(value=value, color=color)
def carre(value) -> Hand: def carre(value) -> Hand:
return Hand([ return hand([
Card(value, Color.Hearts), card(value, Color.Hearts),
Card(value, Color.Clubs), card(value, Color.Clubs),
Card(value, Color.Diamonds), card(value, Color.Diamonds),
Card(value, Color.Spades) card(value, Color.Spades)
]) ])
def full(value_aux: Value, def full(value_aux: Value,
value_par: Value) -> Hand: value_par: Value) -> Hand:
return Hand([ return hand([
Card(value_aux, Color.Hearts), card(value_aux, Color.Hearts),
Card(value_aux, Color.Clubs), card(value_aux, Color.Clubs),
Card(value_aux, Color.Diamonds), card(value_aux, Color.Diamonds),
Card(value_par, Color.Hearts), card(value_par, Color.Hearts),
Card(value_par, Color.Clubs) card(value_par, Color.Clubs)
]) ])
def brelan(value) -> Hand: def brelan(value) -> Hand:
return Hand([ return hand([
Card(value, Color.Hearts), card(value, Color.Hearts),
Card(value, Color.Clubs), card(value, Color.Clubs),
Card(value, Color.Diamonds) card(value, Color.Diamonds)
]) ])
def pair(value) -> Hand: def pair(value) -> Hand:
return Hand([ return hand([
Card(value, Color.Hearts), card(value, Color.Hearts),
Card(value, Color.Clubs) card(value, Color.Clubs)
]) ])
def single(low_value) -> Hand: def single(low_value) -> Hand:
return Hand([ return hand([
Card(low_value, Color.Hearts) card(low_value, Color.Hearts)
]) ])
...@@ -52,13 +60,13 @@ def double_pair(value: Value, other: Value = None): ...@@ -52,13 +60,13 @@ def double_pair(value: Value, other: Value = None):
other = Value.Two if value == Value.Three else Value.Three other = Value.Two if value == Value.Three else Value.Three
assert other != value assert other != value
return Hand([ return hand([
# Pair of value # Pair of value
Card(value, Color.Hearts), card(value, Color.Hearts),
Card(value, Color.Clubs), card(value, Color.Clubs),
# And pair of twos or threes # And pair of twos or threes
Card(other, Color.Hearts), card(other, Color.Hearts),
Card(other, Color.Clubs) card(other, Color.Clubs)
]) ])
...@@ -77,7 +85,7 @@ def all_options() -> List[Hand]: ...@@ -77,7 +85,7 @@ def all_options() -> List[Hand]:
for b in brelans: for b in brelans:
for p in pairs: for p in pairs:
if not any([c in b.cards for c in p.cards]): # Valid full if not any([c in b.cards for c in p.cards]): # Valid full
hands.append(Hand([*b.cards, *p.cards])) hands.append(hand([*b.cards, *p.cards]))
return hands return hands
......
from server.model.card import Value, Color, Card from server.model.card import Value, Color
from server.model.hands import pair, single, double_pair, brelan, full, carre from server.model.hands import pair, single, double_pair, brelan, full, carre, card
ACE_OF_HEARTS = Card(Value.Ace, Color.Hearts) ACE_OF_HEARTS = card(Value.Ace, Color.Hearts)
ACE_OF_SPADES = Card(Value.Ace, Color.Spades) ACE_OF_SPADES = card(Value.Ace, Color.Spades)
ACE_OF_DIAMONDS = Card(Value.Ace, Color.Diamonds) ACE_OF_DIAMONDS = card(Value.Ace, Color.Diamonds)
ACE_OF_CLUBS = Card(Value.Ace, Color.Clubs) ACE_OF_CLUBS = card(Value.Ace, Color.Clubs)
PAIR_ACE = pair(Value.Ace) PAIR_ACE = pair(Value.Ace)
SINGLE_ACE = single(Value.Ace) SINGLE_ACE = single(Value.Ace)
DOUBLE_PAIR_ACE = double_pair(Value.Ace) DOUBLE_PAIR_ACE = double_pair(Value.Ace)
......
...@@ -2,14 +2,15 @@ from abc import abstractmethod, ABC ...@@ -2,14 +2,15 @@ from abc import abstractmethod, ABC
from dataclasses import dataclass from dataclasses import dataclass
from typing import Optional from typing import Optional
from pydantic.main import BaseModel
from server.model.animals import random_animal_name from server.model.animals import random_animal_name
from server.model.card import Card from server.model.card import Card
from server.model.hand import Hand from server.model.hand import Hand
from server.model.hands import random_option from server.model.hands import random_option
@dataclass class Announce(BaseModel):
class Announce:
bet: Optional[Hand] = None bet: Optional[Hand] = None
@property @property
...@@ -55,7 +56,7 @@ class NaivePlayer(Player, ABC): ...@@ -55,7 +56,7 @@ class NaivePlayer(Player, ABC):
def announce(self, current_bet: Optional[Hand]) -> Announce: def announce(self, current_bet: Optional[Hand]) -> Announce:
if not current_bet or self.hand.value() > current_bet.value(): if not current_bet or self.hand.value() > current_bet.value():
return Announce(self.hand) return Announce(bet=self.hand)
else: else:
return Announce() return Announce()
...@@ -70,7 +71,7 @@ class RandomPlayer(Player, ABC): ...@@ -70,7 +71,7 @@ class RandomPlayer(Player, ABC):
if current_bet and not current_bet.is_menteur and current_bet.is_carre_as: if current_bet and not current_bet.is_menteur and current_bet.is_carre_as:
return Announce() return Announce()
else: else:
return Announce(random_option()) return Announce(bet=random_option())
class MenteurPlayer(Player, ABC): class MenteurPlayer(Player, ABC):
...@@ -81,4 +82,4 @@ class MenteurPlayer(Player, ABC): ...@@ -81,4 +82,4 @@ class MenteurPlayer(Player, ABC):
def announce(self, current_bet: Optional[Hand]) -> Announce: def announce(self, current_bet: Optional[Hand]) -> Announce:
hand = random_option() hand = random_option()
return Announce(hand) return Announce(bet=hand)
...@@ -29,10 +29,7 @@ class MockPlayer(Player): ...@@ -29,10 +29,7 @@ class MockPlayer(Player):
return len([m for (m, e) in self.messages if m is msg]) return len([m for (m, e) in self.messages if m is msg])
def announce(self, current_bet: Optional[Hand]) -> Announce: def announce(self, current_bet: Optional[Hand]) -> Announce:
if self.bets: return Announce(bet=self.bets.pop() if self.bets else CARRE_ACE)
return Announce(self.bets.pop())
else:
return Announce(CARRE_ACE)
def receive(self, message: MessageToPlayer, extra: Optional[Any] = None): def receive(self, message: MessageToPlayer, extra: Optional[Any] = None):
self.messages.append((message, extra)) self.messages.append((message, extra))
...@@ -92,8 +89,8 @@ class TestManager(TestCase): ...@@ -92,8 +89,8 @@ class TestManager(TestCase):
self.assertIn(MessageToPlayer.Win, self.j1.messages, "Win not told") self.assertIn(MessageToPlayer.Win, self.j1.messages, "Win not told")
self.assertIn(MessageToPlayer.Lose, self.j2.messages, "Lose not told") self.assertIn(MessageToPlayer.Lose, self.j2.messages, "Lose not told")
def assertGot(self, player: MockPlayer, type: MessageToPlayer, extra=None, msg: str = "message not received"): def assertGot(self, player: MockPlayer, message: MessageToPlayer, extra=None, msg: str = "message not received"):
self.assertIn(type, [t for (t, _) in player.messages], msg) self.assertIn(message, [t for (t, _) in player.messages], msg)
if extra: if extra:
matching = [m for (m, e) in player.messages if e == extra and m == type] matching = [m for (m, e) in player.messages if e == extra and m == message]
self.assertTrue(matching, f"No message {type} with extra {extra}") self.assertTrue(matching, f"No message {message} with extra {extra}")
...@@ -18,6 +18,7 @@ class ClientPlayer(Player): ...@@ -18,6 +18,7 @@ class ClientPlayer(Player):
def __init__(self, lobby: LobbyManager): def __init__(self, lobby: LobbyManager):
super().__init__() super().__init__()
self.lobby = lobby self.lobby = lobby
self.ready = False
async def announce(self, current_bet: Optional[Hand]) -> Announce: async def announce(self, current_bet: Optional[Hand]) -> Announce:
return lobby.last_announces[self] return lobby.last_announces[self]
...@@ -27,13 +28,14 @@ class ClientPlayer(Player): ...@@ -27,13 +28,14 @@ class ClientPlayer(Player):
async def connect(sid, environ): async def connect(sid, environ):
print("[WS] Connect ", sid, environ) print("[WS] Connect ", sid, environ)
player = ClientPlayer(lobby) player = ClientPlayer(lobby)
await lobby.add_player(player) reply: str = await lobby.add_player(player)
await sio.emit('messageChannel', reply, room=sid)
@sio.event @sio.event
async def message(sid, data): async def message(sid, data):
print("[WS] Message ", data) print("[WS] Message ", data)
await sio.emit('messageChannel', "OK", room=sid) await sio.emit('messageChannel', lobby.handle_message(sid, data), room=sid)
@sio.on("pingServer") @sio.on("pingServer")
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment