refactor: Async, announces waiting

parent 357a4209
...@@ -5,7 +5,7 @@ from starlette.exceptions import HTTPException ...@@ -5,7 +5,7 @@ from starlette.exceptions import HTTPException
from starlette.responses import PlainTextResponse from starlette.responses import PlainTextResponse
# Game state # Game state
from server.ws import sio from server.ws import sio, lobby
# Server # Server
app = FastAPI() app = FastAPI()
...@@ -33,6 +33,13 @@ async def hello_world(): ...@@ -33,6 +33,13 @@ async def hello_world():
return "Hello, gentle[wo]man" return "Hello, gentle[wo]man"
@router.get("/reset")
async def reset(
):
lobby.reset()
return "Done."
app.include_router(router) app.include_router(router)
......
...@@ -15,6 +15,7 @@ class Metadata(BaseModel): ...@@ -15,6 +15,7 @@ class Metadata(BaseModel):
ready: bool = False ready: bool = False
sid: str = "" sid: str = ""
last_announce: Optional[Announce] = None last_announce: Optional[Announce] = None
fresh_announce: bool = True
class LobbyManager(ClientManager): class LobbyManager(ClientManager):
...@@ -46,9 +47,11 @@ class LobbyManager(ClientManager): ...@@ -46,9 +47,11 @@ class LobbyManager(ClientManager):
self.metadata[player.name].ready = 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): async def announces(self, player: Player, announce: Announce):
# FIXME: Call this message on incoming "ANNOUNCE" # FIXME: Call this message on incoming "ANNOUNCE"
self.metadata[player.name].last_announce = announce meta = self.metadata[player.name]
meta.last_announce = announce
meta.fresh_announce = True
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):
...@@ -59,37 +62,63 @@ class LobbyManager(ClientManager): ...@@ -59,37 +62,63 @@ class LobbyManager(ClientManager):
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 reset(self):
players = len(self.players)
games = len(self.games)
msg = f"Resetting! sorry for the {players} players / {games} games..."
print(msg)
for p in self.players:
m = self.metadata[p.name]
self.send(p, MessageToPlayer.Reset)
self.sio.disconnect(m.sid)
self.lobby.clear()
self.metadata.clear()
self.games.clear()
print(f"Reset done, {players} players, {games} games.")
return msg
def send(self, to: Player, message: MessageToPlayer, extra=None): def send(self, to: Player, message: MessageToPlayer, extra=None):
data = {message: message.name}
if extra:
data["extra"] = extra
self.sio.send(message) self.sio.send(message)
pass pass
def handle_message(self, sid, data): def handle_message(self, sid, data):
sender: Optional[Player] = None
message = None message = None
extras = None extras = {"players": [p.name for p in self.players]}
print(f"Lobby| Received message from {sid}: {data}.") print(f"Lobby| Received message from {sid}: {data}.")
for name, metadata in self.metadata.items(): sender = self.which_player(sid)
if metadata.sid == sid:
sender = self.lobby[name]
if sender: if sender:
print(f"Lobby| Found sender: {sender.name}") print(f"Lobby| Found sender: {sender.name}")
for option in MessageFromPlayer: for option in MessageFromPlayer:
if option.value in data: if option.value in data:
if option == MessageFromPlayer.Waiting: if option == MessageFromPlayer.Waiting:
self.metadata[sender.name].ready = False self.metadata[sender.name].ready = False
extras = [p.name for p in self.players]
print(f"MSG|Player ready {extras}") print(f"MSG|Player {sender.name} waiting, while {extras} ready.")
if len(self.players_ready) > 1:
message = MessageToPlayer.ReadyToStart
elif option == MessageFromPlayer.Ready: elif option == MessageFromPlayer.Ready:
self.wants_to_play(player=sender) self.wants_to_play(player=sender)
message = MessageToPlayer.Ready
extras = [p.name for p in self.players_ready] print(f"MSG|Players ready: {extras}.")
message = MessageToPlayer.ReadyToStart \
if len(self.players_ready) > 1 else MessageToPlayer.Waiting
extras["playersReady"] = [p.name for p in self.players_ready]
elif option == MessageFromPlayer.Bet: elif option == MessageFromPlayer.Bet:
# FIXME vraie annonce, pas juste carre d'as lol # FIXME vraie annonce, pas juste carre d'as lol
self.announces(player=sender, self.announces(player=sender,
announce=Announce(bet=CARRE_ACE)) announce=Announce(bet=CARRE_ACE))
game: Game = self.game_with(sender)
# TODO: connect with current game, return appropriate message # TODO: connect with current game, return appropriate message
elif option == MessageFromPlayer.Menteur: elif option == MessageFromPlayer.Menteur:
self.announces(player=sender, announce=Announce()) self.announces(player=sender, announce=Announce())
...@@ -99,3 +128,19 @@ class LobbyManager(ClientManager): ...@@ -99,3 +128,19 @@ class LobbyManager(ClientManager):
if extras: if extras:
body["extras"] = extras body["extras"] = extras
return json.dumps(body) return json.dumps(body)
def which_player(self, sid) -> Player:
sender: Optional[Player] = None
for name, metadata in self.metadata.items():
if metadata.sid == sid:
sender = self.lobby[name]
return sender
def game_with(self, player: Player) -> Game:
return [g for g in self.games if player in g.players][0]
def send_waiting_for(self, player: Player):
game = self.game_with(player)
self.send(player, MessageToPlayer.YourTurn, extra={"bet": game.current_bet})
for p in [p for p in game.players if p != player]:
self.send(p, MessageToPlayer.Waiting, extra={"waitingFor": p.name})
...@@ -3,7 +3,7 @@ from enum import Enum ...@@ -3,7 +3,7 @@ from enum import Enum
class MessageToPlayer(Enum): class MessageToPlayer(Enum):
Waiting = "WAITING_ROOM" Waiting = "WAITING_ROOM"
Ready = "READY_ROOM" ReadyToStart = "READY_ROOM"
NewGame = "NEW_GAME" NewGame = "NEW_GAME"
GiveHand = "GIVE_HAND" GiveHand = "GIVE_HAND"
WaitTurn = "WAITING_TURN" WaitTurn = "WAITING_TURN"
...@@ -13,6 +13,7 @@ class MessageToPlayer(Enum): ...@@ -13,6 +13,7 @@ class MessageToPlayer(Enum):
Win = "WINNER" Win = "WINNER"
WinnerIs = "WINNER_IS" WinnerIs = "WINNER_IS"
Lose = "LOSER" Lose = "LOSER"
Reset = "RESET"
class MessageFromPlayer(Enum): class MessageFromPlayer(Enum):
......
...@@ -48,6 +48,9 @@ class Card(BaseModel): ...@@ -48,6 +48,9 @@ class Card(BaseModel):
def __str__(self) -> str: def __str__(self) -> str:
return f"{self.value.name} of {self.color.value if self.color else '?'}" return f"{self.value.name} of {self.color.value if self.color else '?'}"
def __hash__(self) -> int:
return hash(self.value) + 100 * hash(self.color)
def score(self) -> int: def score(self) -> int:
return int(self.value.value) return int(self.value.value)
......
...@@ -37,13 +37,14 @@ class Game: ...@@ -37,13 +37,14 @@ class Game:
@property @property
def global_hand(self) -> Hand: def global_hand(self) -> Hand:
return Hand([c for p in self.players for c in p.hand]) all_cards = [c for p in self.players for c in p.hand.cards]
return Hand(cards=all_cards)
def new_game(self): async def new_game(self):
self.deck.reset() self.deck.reset()
while len(self.players) > 1: while len(self.players) > 1:
loser = self.new_turn() loser = await self.new_turn()
if self.defeats[loser] == 5: if self.defeats[loser] == 5:
print(f"{loser} is eliminated!") print(f"{loser} is eliminated!")
...@@ -57,7 +58,7 @@ class Game: ...@@ -57,7 +58,7 @@ class Game:
self.message(MessageToPlayer.WinnerIs, extra=winner) self.message(MessageToPlayer.WinnerIs, extra=winner)
print(f"Game over - {winner.name} wins with {len(winner.hand)} cards!") print(f"Game over - {winner.name} wins with {len(winner.hand)} cards!")
def new_turn(self) -> Player: async def new_turn(self) -> Player:
""" """
Runs a turn of the game. Runs a turn of the game.
...@@ -83,7 +84,7 @@ class Game: ...@@ -83,7 +84,7 @@ class Game:
self.current_bet = None self.current_bet = None
last_player = None last_player = None
for current_player in itertools.cycle(self.players): for current_player in itertools.cycle(self.players):
loser = self.play_turn(current_player, last_player) loser = await self.play_turn(current_player, last_player)
if loser is not None: if loser is not None:
self.players.remove(loser) self.players.remove(loser)
self.players.insert(0, loser) self.players.insert(0, loser)
...@@ -100,8 +101,8 @@ class Game: ...@@ -100,8 +101,8 @@ class Game:
:return: :return:
""" """
to_scan = [Card(c.value) for c in self.global_hand.cards.copy()] # Keep only values to_scan = [Card(value=c.value) for c in self.global_hand.cards.copy()] # Keep only values
to_find = [Card(c.value) for c in bet.cards] to_find = [Card(value=c.value) for c in bet.cards]
menteur = False menteur = False
for card in to_find: for card in to_find:
...@@ -130,7 +131,7 @@ class Game: ...@@ -130,7 +131,7 @@ class Game:
self.players.append(player) self.players.append(player)
return True return True
def play_turn(self, current_player: Player, last_player: Player) -> Optional[Player]: async def play_turn(self, current_player: Player, last_player: Player) -> Optional[Player]:
""" """
Runs a turn, eventually returning a loser. Runs a turn, eventually returning a loser.
...@@ -144,7 +145,7 @@ class Game: ...@@ -144,7 +145,7 @@ class Game:
self.message(MessageToPlayer.YourTurn, current_player, extra=self.current_bet) self.message(MessageToPlayer.YourTurn, current_player, extra=self.current_bet)
while not self.current_bet: # Ask a valid bet while not self.current_bet: # Ask a valid bet
# FIXME: Wait for player announce? Maybe just sleep 10? # FIXME: Wait for player announce? Maybe just sleep 10?
announce = current_player.announce(self.current_bet) announce = await current_player.announce(self.current_bet)
if announce.bet: if announce.bet:
self.current_bet = announce.bet self.current_bet = announce.bet
print(f"{current_player} starts the round: {self.current_bet}") print(f"{current_player} starts the round: {self.current_bet}")
......
from abc import abstractmethod, ABC from abc import abstractmethod, ABC
from dataclasses import dataclass
from typing import Optional from typing import Optional
from pydantic.main import BaseModel from pydantic.main import BaseModel
...@@ -38,7 +37,7 @@ class Player(ABC): ...@@ -38,7 +37,7 @@ class Player(ABC):
self.hand.cards.clear() self.hand.cards.clear()
@abstractmethod @abstractmethod
def announce(self, current_bet: Optional[Hand]) -> Announce: async def announce(self, current_bet: Optional[Hand]) -> Announce:
""" """
Announces a bet or Menteur, based on the current bet. Announces a bet or Menteur, based on the current bet.
:param current_bet: :param current_bet:
...@@ -54,7 +53,7 @@ class NaivePlayer(Player, ABC): ...@@ -54,7 +53,7 @@ class NaivePlayer(Player, ABC):
def __str__(self): def __str__(self):
return "Naive " + super().__str__() return "Naive " + super().__str__()
def announce(self, current_bet: Optional[Hand]) -> Announce: async 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(bet=self.hand) return Announce(bet=self.hand)
else: else:
...@@ -67,7 +66,7 @@ class RandomPlayer(Player, ABC): ...@@ -67,7 +66,7 @@ class RandomPlayer(Player, ABC):
def __str__(self): def __str__(self):
return "Random " + super().__str__() return "Random " + super().__str__()
def announce(self, current_bet: Optional[Hand]) -> Announce: async def announce(self, current_bet: Optional[Hand]) -> Announce:
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:
...@@ -80,6 +79,6 @@ class MenteurPlayer(Player, ABC): ...@@ -80,6 +79,6 @@ class MenteurPlayer(Player, ABC):
def __str__(self): def __str__(self):
return "Menteur " + super().__str__() return "Menteur " + super().__str__()
def announce(self, current_bet: Optional[Hand]) -> Announce: async def announce(self, current_bet: Optional[Hand]) -> Announce:
hand = random_option() hand = random_option()
return Announce(bet=hand) return Announce(bet=hand)
...@@ -185,14 +185,14 @@ class TestHand(TestCase): ...@@ -185,14 +185,14 @@ class TestHand(TestCase):
# Full[Three of ♥|Three of ♣|Three of ♠|Four of ♥|Four of ♣] # Full[Three of ♥|Three of ♣|Three of ♠|Four of ♥|Four of ♣]
# > Full[Two of ♥|Two of ♣|Two of ♠|Four of ♥|Four of ♣]] # > Full[Two of ♥|Two of ♣|Two of ♠|Four of ♥|Four of ♣]]
full1 = Hand([Card(v) for v in [ full1 = Hand(cards=[Card(value=v) for v in [
Value.Three, Value.Three,
Value.Three, Value.Three,
Value.Three, Value.Three,
Value.Four, Value.Four,
Value.Four, Value.Four,
]]) ]])
full2 = Hand([Card(v) for v in [ full2 = Hand(cards=[Card(value=v) for v in [
Value.Two, Value.Two,
Value.Two, Value.Two,
Value.Two, Value.Two,
......
...@@ -30,11 +30,11 @@ class TestGame(TestCase): ...@@ -30,11 +30,11 @@ class TestGame(TestCase):
self.game.add_player(RandomPlayer("Foo")) self.game.add_player(RandomPlayer("Foo"))
self.assertEqual(3, len(self.game.players), "Should not add duplicate") self.assertEqual(3, len(self.game.players), "Should not add duplicate")
def test_turn(self): async def test_turn(self):
menteur = MenteurPlayer() menteur = MenteurPlayer()
naive = NaivePlayer() naive = NaivePlayer()
self.game = Game([menteur, naive]) self.game = Game([menteur, naive])
self.game.new_turn() await self.game.new_turn()
self.assertEqual(1, len([v for v in self.game.defeats.values() if v > 0]), "There should have been one defeat.") self.assertEqual(1, len([v for v in self.game.defeats.values() if v > 0]), "There should have been one defeat.")
self.assertTrue(1 in self.game.defeats.values(), "A player should have one defeat.") self.assertTrue(1 in self.game.defeats.values(), "A player should have one defeat.")
...@@ -42,8 +42,8 @@ class TestGame(TestCase): ...@@ -42,8 +42,8 @@ class TestGame(TestCase):
loser = [p for p in self.game.players if self.game.defeats[p]][0] loser = [p for p in self.game.players if self.game.defeats[p]][0]
self.assertEqual(self.game.players[0], loser, "The loser should be first to play.") self.assertEqual(self.game.players[0], loser, "The loser should be first to play.")
def test_full_game(self): async def test_full_game(self):
self.game.new_game() await self.game.new_game()
self.assertGreater(len(self.game.defeats.values()), 0, "There should be at least one player with defeats.") self.assertGreater(len(self.game.defeats.values()), 0, "There should be at least one player with defeats.")
self.assertTrue(5 in self.game.defeats.values(), "A player should have lost five times.") self.assertTrue(5 in self.game.defeats.values(), "A player should have lost five times.")
...@@ -28,7 +28,7 @@ class MockPlayer(Player): ...@@ -28,7 +28,7 @@ class MockPlayer(Player):
def count(self, msg: MessageToPlayer) -> int: def count(self, msg: MessageToPlayer) -> int:
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: async def announce(self, current_bet: Optional[Hand]) -> Announce:
return Announce(bet=self.bets.pop() if self.bets else CARRE_ACE) return Announce(bet=self.bets.pop() if self.bets else CARRE_ACE)
def receive(self, message: MessageToPlayer, extra: Optional[Any] = None): def receive(self, message: MessageToPlayer, extra: Optional[Any] = None):
...@@ -62,15 +62,15 @@ class TestManager(TestCase): ...@@ -62,15 +62,15 @@ class TestManager(TestCase):
deck=Deck(cards=cards), deck=Deck(cards=cards),
manager=self.manager) manager=self.manager)
def test_turn_messages(self): async def test_turn_messages(self):
self.game.new_turn() await self.game.new_turn()
self.assertGot(self.j1, MessageToPlayer.LoseRound, extra=self.j1.name, msg=f"J1 should hear round lost.") self.assertGot(self.j1, MessageToPlayer.LoseRound, extra=self.j1.name, msg=f"J1 should hear round lost.")
self.assertGot(self.j2, MessageToPlayer.LoseRound, extra=self.j1.name, msg=f"J2 should hear round lost by j1.") self.assertGot(self.j2, MessageToPlayer.LoseRound, extra=self.j1.name, msg=f"J2 should hear round lost by j1.")
for player in [self.j1, self.j2]: for player in [self.j1, self.j2]:
self.assertGot(player, MessageToPlayer.LoseRound, msg="End of round not announced") self.assertGot(player, MessageToPlayer.LoseRound, msg="End of round not announced")
def test_game_messages(self): async def test_game_messages(self):
self.game.new_game() await self.game.new_game()
self.assertEqual(len([m for m in self.j1.messages if m is MessageToPlayer.LoseRound]), 5, self.assertEqual(len([m for m in self.j1.messages if m is MessageToPlayer.LoseRound]), 5,
f"{self.j1} should lose 5 rounds: {'|'.join([str(m) for m in self.j1.messages])}") f"{self.j1} should lose 5 rounds: {'|'.join([str(m) for m in self.j1.messages])}")
......
from time import sleep
from typing import Optional from typing import Optional
import socketio import socketio
from server.game.lobby import LobbyManager from server.game.lobby import LobbyManager
from server.game.message import MessageToPlayer
from server.model.hand import Hand from server.model.hand import Hand
from server.model.players import Player, Announce from server.model.players import Player, Announce
...@@ -21,6 +23,10 @@ class ClientPlayer(Player): ...@@ -21,6 +23,10 @@ class ClientPlayer(Player):
self.ready = False self.ready = False
async def announce(self, current_bet: Optional[Hand]) -> Announce: async def announce(self, current_bet: Optional[Hand]) -> Announce:
announce = None
while not self.lobby.metadata[self.name].fresh_announce:
lobby.send_waiting_for(self)
sleep(2)
return lobby.last_announces[self] return lobby.last_announces[self]
...@@ -47,3 +53,4 @@ async def ping_server(sid, data): ...@@ -47,3 +53,4 @@ async def ping_server(sid, data):
@sio.event @sio.event
def disconnect(sid): def disconnect(sid):
print('[WS] Disconnect ', sid) print('[WS] Disconnect ', sid)
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