From 246452572f94f59c049ab86a5b8a53f8d91d7d75 Mon Sep 17 00:00:00 2001 From: Paul-Louis NECH Date: Sun, 19 Apr 2020 14:53:02 +0200 Subject: [PATCH] refactor: Async, announces waiting --- server/app.py | 9 ++++++++- server/game/lobby.py | 67 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++----------- server/game/message.py | 3 ++- server/model/card.py | 3 +++ server/model/game.py | 19 ++++++++++--------- server/model/players.py | 9 ++++----- server/test/test_data.py | 4 ++-- server/test/test_game.py | 8 ++++---- server/test/test_lobby.py | 10 +++++----- server/ws.py | 7 +++++++ 10 files changed, 101 insertions(+), 38 deletions(-) diff --git a/server/app.py b/server/app.py index c2e71f3..91831ce 100644 --- a/server/app.py +++ b/server/app.py @@ -5,7 +5,7 @@ from starlette.exceptions import HTTPException from starlette.responses import PlainTextResponse # Game state -from server.ws import sio +from server.ws import sio, lobby # Server app = FastAPI() @@ -33,6 +33,13 @@ async def hello_world(): return "Hello, gentle[wo]man" +@router.get("/reset") +async def reset( +): + lobby.reset() + return "Done." + + app.include_router(router) diff --git a/server/game/lobby.py b/server/game/lobby.py index 21c1600..ebe833f 100644 --- a/server/game/lobby.py +++ b/server/game/lobby.py @@ -15,6 +15,7 @@ class Metadata(BaseModel): ready: bool = False sid: str = "" last_announce: Optional[Announce] = None + fresh_announce: bool = True class LobbyManager(ClientManager): @@ -46,9 +47,11 @@ class LobbyManager(ClientManager): self.metadata[player.name].ready = True 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" - 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)") def start_game(self, max_players=2): @@ -59,37 +62,63 @@ class LobbyManager(ClientManager): 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): + data = {message: message.name} + if extra: + data["extra"] = extra self.sio.send(message) pass def handle_message(self, sid, data): - sender: Optional[Player] = None message = None - extras = None + extras = {"players": [p.name for p in self.players]} print(f"Lobby| Received message from {sid}: {data}.") - for name, metadata in self.metadata.items(): - if metadata.sid == sid: - sender = self.lobby[name] + sender = self.which_player(sid) if sender: print(f"Lobby| Found sender: {sender.name}") for option in MessageFromPlayer: if option.value in data: + if option == MessageFromPlayer.Waiting: 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: 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: # FIXME vraie annonce, pas juste carre d'as lol self.announces(player=sender, announce=Announce(bet=CARRE_ACE)) + game: Game = self.game_with(sender) # TODO: connect with current game, return appropriate message elif option == MessageFromPlayer.Menteur: self.announces(player=sender, announce=Announce()) @@ -99,3 +128,19 @@ class LobbyManager(ClientManager): if extras: body["extras"] = extras 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}) diff --git a/server/game/message.py b/server/game/message.py index 221cd52..da88060 100644 --- a/server/game/message.py +++ b/server/game/message.py @@ -3,7 +3,7 @@ from enum import Enum class MessageToPlayer(Enum): Waiting = "WAITING_ROOM" - Ready = "READY_ROOM" + ReadyToStart = "READY_ROOM" NewGame = "NEW_GAME" GiveHand = "GIVE_HAND" WaitTurn = "WAITING_TURN" @@ -13,6 +13,7 @@ class MessageToPlayer(Enum): Win = "WINNER" WinnerIs = "WINNER_IS" Lose = "LOSER" + Reset = "RESET" class MessageFromPlayer(Enum): diff --git a/server/model/card.py b/server/model/card.py index f40309d..f95fa43 100644 --- a/server/model/card.py +++ b/server/model/card.py @@ -48,6 +48,9 @@ class Card(BaseModel): def __str__(self) -> str: 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: return int(self.value.value) diff --git a/server/model/game.py b/server/model/game.py index 1d5279c..a68e687 100644 --- a/server/model/game.py +++ b/server/model/game.py @@ -37,13 +37,14 @@ class Game: @property 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() while len(self.players) > 1: - loser = self.new_turn() + loser = await self.new_turn() if self.defeats[loser] == 5: print(f"{loser} is eliminated!") @@ -57,7 +58,7 @@ class Game: self.message(MessageToPlayer.WinnerIs, extra=winner) 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. @@ -83,7 +84,7 @@ class Game: self.current_bet = None last_player = None 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: self.players.remove(loser) self.players.insert(0, loser) @@ -100,8 +101,8 @@ class Game: :return: """ - to_scan = [Card(c.value) for c in self.global_hand.cards.copy()] # Keep only values - to_find = [Card(c.value) for c in bet.cards] + to_scan = [Card(value=c.value) for c in self.global_hand.cards.copy()] # Keep only values + to_find = [Card(value=c.value) for c in bet.cards] menteur = False for card in to_find: @@ -130,7 +131,7 @@ class Game: self.players.append(player) 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. @@ -144,7 +145,7 @@ class Game: self.message(MessageToPlayer.YourTurn, current_player, extra=self.current_bet) while not self.current_bet: # Ask a valid bet # 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: self.current_bet = announce.bet print(f"{current_player} starts the round: {self.current_bet}") diff --git a/server/model/players.py b/server/model/players.py index 3a0f729..84cb5e0 100644 --- a/server/model/players.py +++ b/server/model/players.py @@ -1,5 +1,4 @@ from abc import abstractmethod, ABC -from dataclasses import dataclass from typing import Optional from pydantic.main import BaseModel @@ -38,7 +37,7 @@ class Player(ABC): self.hand.cards.clear() @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. :param current_bet: @@ -54,7 +53,7 @@ class NaivePlayer(Player, ABC): def __str__(self): 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(): return Announce(bet=self.hand) else: @@ -67,7 +66,7 @@ class RandomPlayer(Player, ABC): def __str__(self): 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: return Announce() else: @@ -80,6 +79,6 @@ class MenteurPlayer(Player, ABC): def __str__(self): return "Menteur " + super().__str__() - def announce(self, current_bet: Optional[Hand]) -> Announce: + async def announce(self, current_bet: Optional[Hand]) -> Announce: hand = random_option() return Announce(bet=hand) diff --git a/server/test/test_data.py b/server/test/test_data.py index e822c68..6fba3f2 100644 --- a/server/test/test_data.py +++ b/server/test/test_data.py @@ -185,14 +185,14 @@ class TestHand(TestCase): # Full[Three of ♥|Three of ♣|Three 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.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, diff --git a/server/test/test_game.py b/server/test/test_game.py index 19cdb2b..85f7cb7 100644 --- a/server/test/test_game.py +++ b/server/test/test_game.py @@ -30,11 +30,11 @@ class TestGame(TestCase): self.game.add_player(RandomPlayer("Foo")) self.assertEqual(3, len(self.game.players), "Should not add duplicate") - def test_turn(self): + async def test_turn(self): menteur = MenteurPlayer() naive = NaivePlayer() 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.assertTrue(1 in self.game.defeats.values(), "A player should have one defeat.") @@ -42,8 +42,8 @@ class TestGame(TestCase): 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.") - def test_full_game(self): - self.game.new_game() + async def test_full_game(self): + await self.game.new_game() 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.") diff --git a/server/test/test_lobby.py b/server/test/test_lobby.py index 9475cd2..ad09f45 100644 --- a/server/test/test_lobby.py +++ b/server/test/test_lobby.py @@ -28,7 +28,7 @@ class MockPlayer(Player): def count(self, msg: MessageToPlayer) -> int: 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) def receive(self, message: MessageToPlayer, extra: Optional[Any] = None): @@ -62,15 +62,15 @@ class TestManager(TestCase): deck=Deck(cards=cards), manager=self.manager) - def test_turn_messages(self): - self.game.new_turn() + async def test_turn_messages(self): + 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.j2, MessageToPlayer.LoseRound, extra=self.j1.name, msg=f"J2 should hear round lost by j1.") for player in [self.j1, self.j2]: self.assertGot(player, MessageToPlayer.LoseRound, msg="End of round not announced") - def test_game_messages(self): - self.game.new_game() + async def test_game_messages(self): + await self.game.new_game() 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])}") diff --git a/server/ws.py b/server/ws.py index 1eb22a2..80e84a4 100644 --- a/server/ws.py +++ b/server/ws.py @@ -1,8 +1,10 @@ +from time import sleep from typing import Optional import socketio from server.game.lobby import LobbyManager +from server.game.message import MessageToPlayer from server.model.hand import Hand from server.model.players import Player, Announce @@ -21,6 +23,10 @@ class ClientPlayer(Player): self.ready = False 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] @@ -47,3 +53,4 @@ async def ping_server(sid, data): @sio.event def disconnect(sid): print('[WS] Disconnect ', sid) + -- libgit2 0.27.0