diff --git a/server/app.py b/server/app.py index 97d03ae..c2e71f3 100644 --- a/server/app.py +++ b/server/app.py @@ -1,18 +1,12 @@ -from http.client import HTTPException - import socketio from fastapi import FastAPI, APIRouter from fastapi.exceptions import RequestValidationError from starlette.exceptions import HTTPException from starlette.responses import PlainTextResponse -from server.game.lobby import LobbyManager -from server.model.data import Game # Game state -from server.model.players import RandomPlayer from server.ws import sio - # Server app = FastAPI() router = APIRouter() @@ -39,15 +33,6 @@ async def hello_world(): return "Hello, gentle[wo]man" -@router.post("/join") -async def join(player_name: str): - if game.add_player(RandomPlayer(player_name) # TODO: Let user play - ): - return "Welcome %s, %i players currently waiting!" % (player_name, len(game.players)) - else: - raise HTTPException(status_code=403, detail=f"{player_name} already connected, choose another name.") - - app.include_router(router) diff --git a/server/game/lobby.py b/server/game/lobby.py index cde5843..1e7de37 100644 --- a/server/game/lobby.py +++ b/server/game/lobby.py @@ -5,7 +5,7 @@ from socketio import AsyncServer from server.game.manager import ClientManager from server.game.message import MessageToPlayer -from server.model.data import Game +from server.model.game import Game from server.model.players import Player, Announce @@ -38,6 +38,7 @@ class LobbyManager(ClientManager): print(f"{player} ready to play! ({self.nb_ready} people ready)") def announces(self, player: Player, announce: Announce): + # FIXME: Call this message on incoming "ANNOUNCE" self.last_announces[player] = announce print(f"{player} ready to play! ({self.nb_ready} people ready)") diff --git a/server/game/manager.py b/server/game/manager.py index dff5c22..7e8d2c1 100644 --- a/server/game/manager.py +++ b/server/game/manager.py @@ -9,6 +9,7 @@ class ClientManager(ABC): """ A listener of game state that notifies clients. """ + def __init__(self): self.players: List[Player] = [] @@ -18,5 +19,8 @@ class ClientManager(ABC): self.send(p, message) @abstractmethod - def send(self, to: Player, message: MessageToPlayer, extra=None): + def send(self, + to: Player, + message: MessageToPlayer, + extra=None): raise NotImplementedError("Send a message to clients ") diff --git a/server/game/message.py b/server/game/message.py index 544a263..bd037b1 100644 --- a/server/game/message.py +++ b/server/game/message.py @@ -4,9 +4,13 @@ from enum import Enum class MessageToPlayer(Enum): Waiting = "WAITING_ROOM" Ready = "READY_ROOM" + GiveHand = "GIVE_HAND" WaitTurn = "WAITING_TURN" YourTurn = "YOUR_TURN" + Announce = "ANNOUNCE" + LoseRound = "LOSE_ROUND" Win = "WINNER" + WinnerIs = "WINNER_IS" Lose = "LOSER" diff --git a/server/model/deck.py b/server/model/deck.py index ee6bc9b..bc84067 100644 --- a/server/model/deck.py +++ b/server/model/deck.py @@ -1,11 +1,16 @@ from random import randrange, shuffle +from typing import List from server.model.card import Card, Value, Color class Deck: - def __init__(self): - self.cards = [Card(v, c) for v in Value for c in Color] + def __init__(self, cards=None): + if cards is None: + cards = [Card(v, c) for v in Value for c in Color] + else: + print("Deck init with cards:", cards, len(cards)) + self.cards = cards self.defausse = [] def __len__(self): diff --git a/server/model/data.py b/server/model/game.py similarity index 78% rename from server/model/data.py rename to server/model/game.py index 8803794..1531171 100644 --- a/server/model/data.py +++ b/server/model/game.py @@ -3,10 +3,11 @@ from collections import defaultdict from typing import List, Dict, Optional from server.game.manager import ClientManager -from server.model.card import Card +from server.game.message import MessageToPlayer +from server.model.card import Card, Value from server.model.deck import Deck from server.model.hand import Hand -from server.model.players import Player +from server.model.players import Player, Announce class Game: @@ -22,6 +23,17 @@ class Game: self.players: List[Player] = players self.defeats: Dict[Player, int] = defaultdict(lambda: 0) self.current_bet: Optional[Hand] = None + self.manager = manager + + def message(self, message: MessageToPlayer, + *to: Player, + extra=None + ) -> None: + if self.manager: + if not to: + to = self.players + for player in to: + self.manager.send(player, message, extra) @property def global_hand(self) -> Hand: @@ -35,13 +47,14 @@ class Game: if self.defeats[loser] == 5: print(f"{loser} is eliminated!") + self.message(MessageToPlayer.Lose, loser) self.players.remove(loser) else: print(f"{loser} lost the round, now playing with {self.defeats[loser] + 1} cards!") - self.players.remove(loser) - self.players.insert(0, loser) winner = self.players[0] + self.message(MessageToPlayer.Win, winner) + self.message(MessageToPlayer.WinnerIs, extra=winner) print(f"Game over - {winner.name} wins with {len(winner.hand)} cards!") def new_turn(self) -> Player: @@ -52,11 +65,19 @@ class Game: """ # Distribution self.deck.reset() + + self.message(MessageToPlayer.WaitTurn) for current_player in self.players: current_player.clear() - for i in range(self.defeats[current_player] + 1): - current_player.give(self.deck.random_card()) - print(f"Drew {current_player.hand} for {current_player}.") + count_player_cards = self.defeats[current_player] + 1 + print(f"Drawing {count_player_cards} card(s) for {current_player}: ", end="") + for i in range(count_player_cards): + card = self.deck.random_card() + current_player.give(card) + print(f"{card}") + + self.message(MessageToPlayer.GiveHand, current_player, extra=current_player.hand) + print(f"Cards sent.") # Tour self.current_bet = None @@ -64,6 +85,9 @@ class Game: for current_player in itertools.cycle(self.players): loser = self.play_turn(current_player, last_player) if loser is not None: + self.players.remove(loser) + self.players.insert(0, loser) + self.message(MessageToPlayer.LoseRound, loser) return loser last_player = current_player @@ -117,14 +141,26 @@ class Game: print(f"| {current_player}'s turn >") if not self.current_bet: # First player, has to bet something + 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) if announce.bet: self.current_bet = announce.bet print(f"{current_player} starts the round: {self.current_bet}") + self.message(MessageToPlayer.Announce, extra={"player": current_player, "announce": announce}) + else: + print(f"You cannot say Menteur on first round, {current_player}!") else: # Next player, announce or menteur - announce = current_player.announce(self.current_bet) + + # Wait, is the announce the last possible one? + if len(self.current_bet.cards) == 4 and all([c.value is Value.Ace for c in self.current_bet.cards]): + print("CARRE D'AS!") + announce = Announce() # MENTEUR obligatoire + else: + self.message(MessageToPlayer.YourTurn, current_player, extra=self.current_bet) + announce = current_player.announce(self.current_bet) if announce.bet: while announce.bet.value() < self.current_bet.value(): # Bad announce! @@ -134,6 +170,7 @@ class Game: # Valid bet: print(f" {current_player} bets {self.current_bet}.") + self.message(MessageToPlayer.Announce, extra={"player": current_player, "announce": announce}) else: # Menteur! Who lost the round? menteur = self.is_menteur(self.current_bet) diff --git a/server/model/hands.py b/server/model/hands.py index a4f2cac..b916eba 100644 --- a/server/model/hands.py +++ b/server/model/hands.py @@ -19,7 +19,7 @@ def full(value_aux: Value, return Hand([ Card(value_aux, Color.Hearts), Card(value_aux, Color.Clubs), - Card(value_aux, Color.Spades), + Card(value_aux, Color.Diamonds), Card(value_par, Color.Hearts), Card(value_par, Color.Clubs) ]) diff --git a/server/requirements.txt b/server/requirements.txt index f536c03..4617764 100644 --- a/server/requirements.txt +++ b/server/requirements.txt @@ -1,2 +1,3 @@ fastapi==0.53.2 uvicorn==0.11.3 +python-socketio==4.5.1 diff --git a/server/test/test_game.py b/server/test/test_game.py index 235249e..19cdb2b 100644 --- a/server/test/test_game.py +++ b/server/test/test_game.py @@ -1,6 +1,6 @@ from unittest import TestCase -from server.model.data import Game +from server.model.game import Game from server.model.players import MenteurPlayer, NaivePlayer, RandomPlayer @@ -38,8 +38,8 @@ class TestGame(TestCase): 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.") - 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.") def test_full_game(self): diff --git a/server/test/test_lobby.py b/server/test/test_lobby.py new file mode 100644 index 0000000..ee6d1a7 --- /dev/null +++ b/server/test/test_lobby.py @@ -0,0 +1,93 @@ +from typing import Optional, List +from unittest import TestCase + +from server.game.manager import ClientManager +from server.game.message import MessageToPlayer +from server.model.card import Value +from server.model.deck import Deck +from server.model.game import Game +from server.model.hand import Hand +from server.model.hands import pair, brelan +from server.model.known import CARRE_ACE +from server.model.players import Announce, Player + + +class MockPlayer(Player): + + def __init__(self, + name: str = None, + bets: List[Optional[Hand]] = None + ): + super().__init__(name) + + if bets is None: + bets = [] + self.bets: List[Optional[Hand]] = bets + self.messages: List[MessageToPlayer] = [] + + def count(self, msg: MessageToPlayer) -> int: + return len([m for m in self.messages if m is msg]) + + def announce(self, current_bet: Optional[Hand]) -> Announce: + if self.bets: + return Announce(self.bets.pop()) + else: + return Announce(CARRE_ACE) + + def receive(self, message: MessageToPlayer): + self.messages.append(message) + + def print_msgs(self) -> str: + '|'.join([str(m) for m in self.messages]) + + +class MockManager(ClientManager): + def __init__(self, players: List[MockPlayer]): + super().__init__() + self.players = players + + def send(self, to: Player, message: MessageToPlayer, extra=None): + if isinstance(to, MockPlayer): + to.receive(message) + print(f"Sent {message} to {to}.") + + +class TestManager(TestCase): + + def setUp(self) -> None: + self.j1 = MockPlayer("j1", [pair(Value.Ace)]) + self.j2 = MockPlayer("j2", [brelan(Value.Two)]) + self.manager = MockManager([self.j1, self.j2]) + cards = [] + cards.extend(self.j1.bets[0].cards) + cards.extend(self.j2.bets[0].cards) + self.game = Game(players=[self.j1, self.j2], + deck=Deck(cards=cards), + manager=self.manager) + + def test_turn_messages(self): + self.game.new_turn() + self.assertEqual(self.j1.count(MessageToPlayer.LoseRound), 6, f"j1 should lose 6 rounds.") + self.assertEqual(self.j2.count(MessageToPlayer.LoseRound), 6, f"j2 should lose 6 rounds.") + + for player in [self.j1, self.j2]: + self.assertIn(MessageToPlayer.Lose, player.messages, "Loser not announced") + + def test_game_messages(self): + 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])}") + + self.assertEqual(self.j1.count(MessageToPlayer.YourTurn), 6, + f"{self.j1} should play 6 rounds: {self.j1.print_msgs()}") + + self.assertEqual(self.j2.count(MessageToPlayer.YourTurn), 1, + f"{self.j2} should play 1 rounds: {self.j2.print_msgs()}") + + self.assertEqual(len([m for m in self.j2.messages if m is MessageToPlayer.LoseRound]), 0, + f"{self.j2} should lose 0 rounds: {'|'.join([str(m) for m in self.j2.messages])}") + for player in [self.j1, self.j2]: + self.assertEqual(player.count(MessageToPlayer.Announce), 7, + f"{player} should see 7 announces: {player.print_msgs()}") + self.assertIn(MessageToPlayer.WinnerIs, player.messages, "Winner not announced") + self.assertIn(MessageToPlayer.Win, player.messages, "Win not told")