feat/refactor/fix: Players, turn, deals

parent 1ad38c08
...@@ -49,4 +49,12 @@ class Card: ...@@ -49,4 +49,12 @@ class Card:
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 score(self) -> int: def score(self) -> int:
return int(self.value.value) return int(self.value.value)
\ No newline at end of file
def lowest_value_and_rest():
lowValue: Value = Value.Two
otherValues = list(Value)
otherValues.remove(lowValue)
return lowValue, otherValues
\ No newline at end of file
import itertools import itertools
from collections import defaultdict from collections import defaultdict
from dataclasses import dataclass
from typing import List, Dict, Optional from typing import List, Dict, Optional
from server.model.hand import Hand
from server.model.animals import random_animal_name
from server.model.card import Card from server.model.card import Card
from server.model.deck import Deck from server.model.deck import Deck
from server.model.hand import Hand
from server.model.players import Player
@dataclass
class Announce:
bet: Optional[Hand] = None
@property
def menteur(self):
return not self.bet
class Player:
def __init__(self, name: str = None):
if not name:
name = random_animal_name()
self.name: str = name
self.hand: Hand = Hand()
def __str__(self):
return f"Player {self.name}"
def give(self, card: Card):
self.hand.give(card)
def announce(self, current_bet: Optional[Hand]) -> Announce:
""" A naive player that only trusts what they sees:
bets his hand, or menteur if his hand is lower. """
if not current_bet or self.hand.value() > current_bet.value():
return Announce(self.hand)
else:
return Announce()
class Game: class Game:
...@@ -48,6 +16,7 @@ class Game: ...@@ -48,6 +16,7 @@ class Game:
self.deck: Deck = deck self.deck: Deck = deck
self.players: List[Player] = players self.players: List[Player] = players
self.defeats: Dict[Player, int] = defaultdict(lambda: 0) self.defeats: Dict[Player, int] = defaultdict(lambda: 0)
self.current_bet: Optional[Hand] = None
@property @property
def global_hand(self) -> Hand: def global_hand(self) -> Hand:
...@@ -64,6 +33,8 @@ class Game: ...@@ -64,6 +33,8 @@ class Game:
self.players.remove(loser) self.players.remove(loser)
else: else:
print(f"{loser} lost the round, now playing with {self.defeats[loser] + 1} cards!") 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] winner = self.players[0]
print(f"Game over - {winner.name} wins with {len(winner.hand)} cards!") print(f"Game over - {winner.name} wins with {len(winner.hand)} cards!")
...@@ -77,49 +48,95 @@ class Game: ...@@ -77,49 +48,95 @@ class Game:
# Distribution # Distribution
self.deck.reset() self.deck.reset()
for current_player in self.players: for current_player in self.players:
current_player.give(self.deck.random_card()) current_player.clear()
draw = []
for i in range(self.defeats[current_player] + 1):
draw.append(self.deck.random_card())
print(f"Drew {draw} for {current_player}.")
for d in draw:
current_player.give(d)
# Tour # Tour
current_bet: Optional[Hand] = 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):
print(f"| {current_player}'s turn >") loser = self.play_turn(current_player, last_player)
if not current_bet: # First player, has to bet something if loser is not None:
while not current_bet: return loser
announce = current_player.announce(current_bet)
if announce.bet:
current_bet = announce.bet
print(f"{current_player} starts the round: {current_bet}")
else: # Next player, announce or menteur
announce = current_player.announce(current_bet)
if announce.bet:
while announce.bet.value() < current_bet.value(): # Bad announce!
print(f"Invalid bet, {announce.bet} < {current_bet}!")
announce = current_player.announce(current_bet)
current_bet = announce.bet
print(f" {current_player} bets {current_bet}.")
else: # Menteur! Who lost the round?
menteur = self.is_menteur(current_bet)
print(f"{current_player} says Menteur... {'Bravo!' if menteur else 'FAIL!'}")
loser = last_player if menteur else current_player
print(f"{loser} lost the round!")
self.count_defeat(loser)
return loser
last_player = current_player last_player = current_player
# TODO: Put next first player first of list # TODO: Put next first player first of list
def is_menteur(self, bet: Hand): def is_menteur(self, bet: Hand):
"""
Is this bet a menteur?
:param bet:
:return:
"""
to_scan = [Card(c.value) for c in self.global_hand.cards.copy()] # Keep only values 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_find = [Card(c.value) for c in bet.cards]
menteur = False
for card in to_find: for card in to_find:
if card in to_scan: if card in to_scan:
to_scan.remove(card) to_scan.remove(card)
continue continue
else: else:
print(f"Missing a {card}!") print(f"Missing {card}!")
print(f"Didn't find {bet} in {self.global_hand}: MENTEUR!") menteur = True
return False # if menteur:
print(f"Didn't find {bet} in {self.global_hand}: MENTEUR!")
return menteur
def count_defeat(self, loser): def count_defeat(self, loser):
self.defeats[loser] += 1 self.defeats[loser] += 1
def add_player(self, player: Player):
"""
Adds a player to the game.
:return: True if the player was accepted (unique names).
"""
if any([s.name == player.name for s in self.players]):
return False
else:
self.players.append(player)
return True
def play_turn(self, current_player: Player, last_player: Player) -> Optional[Player]:
"""
Runs a turn, eventually returning a loser.
:param current_player:
:param last_player:
:return:
"""
print(f"| {current_player}'s turn >")
if not self.current_bet: # First player, has to bet something
while not self.current_bet: # Ask a valid bet
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}")
else: # Next player, announce or menteur
announce = current_player.announce(self.current_bet)
if announce.bet:
while announce.bet.value() < self.current_bet.value(): # Bad announce!
print(f"Invalid bet by {current_player}, {announce.bet} < {self.current_bet}!")
announce = current_player.announce(self.current_bet)
self.current_bet = announce.bet
# Valid bet:
print(f" {current_player} bets {self.current_bet}.")
else: # Menteur! Who lost the round?
print(f" {current_player} says Menteur!")
menteur = self.is_menteur(self.current_bet)
loser = last_player if menteur else current_player
self.count_defeat(loser)
print(f"{current_player} says Menteur... {'Bravo!' if menteur else 'FAIL!'}")
return loser
...@@ -22,4 +22,4 @@ class Deck: ...@@ -22,4 +22,4 @@ class Deck:
def reset(self): def reset(self):
self.cards.extend(self.defausse) self.cards.extend(self.defausse)
self.defausse.clear() self.defausse.clear()
shuffle(self.cards) shuffle(self.cards)
\ No newline at end of file
from collections import Counter from collections import Counter
from typing import List from typing import List
from server.model.card import Card from server.model.card import Card, Value
class Hand: class Hand:
...@@ -23,9 +23,17 @@ class Hand: ...@@ -23,9 +23,17 @@ class Hand:
return "|".join([str(c) for c in self.cards]) return "|".join([str(c) for c in self.cards])
def give(self, other: Card): def give(self, other: Card):
"""
Adds another card to this hand.
:raises ValueError if the card is already in this hand.
"""
if (other.value, other.color) in [(c.value, c.color) for c in self.cards]:
raise ValueError(f"TRICHEUR! {other} already in this hand: {self.cards}!")
self.cards.append(other) self.cards.append(other)
def value(self): def value(self):
""" Scores this hand according to the Poker Menteur rules."""
counter = Counter([c.value for c in self.cards]) counter = Counter([c.value for c in self.cards])
has_pair = False has_pair = False
...@@ -87,4 +95,12 @@ class Hand: ...@@ -87,4 +95,12 @@ class Hand:
analysis_log += f"\t-> score=\t{score}" analysis_log += f"\t-> score=\t{score}"
# print(analysis_log) # print(analysis_log)
return score return score
\ No newline at end of file
@property
def is_menteur(self) -> bool:
return len(self.cards) == 0
@property
def is_carre_as(self) -> bool:
return len(self.cards) == 4 and all([c.value == Value.Ace for c in self.cards])
from server.model.hand import Hand from random import choice
from typing import List
from server.model.card import Value, Color, Card from server.model.card import Value, Color, Card
from server.model.hand import Hand
def carre(value) -> Hand: def carre(value) -> Hand:
...@@ -57,3 +60,32 @@ def double_pair(value: Value, other: Value = None): ...@@ -57,3 +60,32 @@ def double_pair(value: Value, other: Value = None):
Card(other, Color.Hearts), Card(other, Color.Hearts),
Card(other, Color.Clubs) Card(other, Color.Clubs)
]) ])
def all_options() -> List[Hand]:
hands = []
carres = [carre(value) for value in Value]
brelans = [brelan(value) for value in Value]
pairs = [pair(value) for value in Value]
singles = [single(value) for value in Value]
hands.extend(carres)
hands.extend(brelans)
hands.extend(pairs)
hands.extend(singles)
for b in brelans:
for p in pairs:
if any([c in b.cards for c in p.cards]): # Invalid full
print(f"Invalid full: {b.cards}-{p.cards}")
else:
hands.append(Hand([*b.cards, *p.cards]))
return hands
options = all_options()
def random_option() -> Hand:
return choice(options)
from abc import abstractmethod, ABC
from dataclasses import dataclass
from typing import Optional
from server.model.animals import random_animal_name
from server.model.card import Card
from server.model.hand import Hand
from server.model.hands import random_option
@dataclass
class Announce:
bet: Optional[Hand] = None
@property
def menteur(self):
return not self.bet
class Player(ABC):
def __init__(self, name: str = None):
if not name:
name = random_animal_name()
self.name: str = name
self.hand: Hand = Hand()
def __str__(self):
return f"Player {self.name}"
def __repr__(self):
return str(self)
def give(self, card: Card):
self.hand.give(card)
def clear(self):
self.hand.cards.clear()
@abstractmethod
def announce(self, current_bet: Optional[Hand]) -> Announce:
"""
Announces a bet or Menteur, based on the current bet.
:param current_bet:
:return:
"""
return Announce()
class NaivePlayer(Player, ABC):
""" A naive player that only trusts what they sees:
bets his hand, or menteur if his hand is lower. """
def __str__(self):
return "Naive " + super().__str__()
def announce(self, current_bet: Optional[Hand]) -> Announce:
if not current_bet or self.hand.value() > current_bet.value():
return Announce(self.hand)
else:
return Announce()
class RandomPlayer(Player, ABC):
""" A weird player that never says menteur, always betting a random option. """
def __str__(self):
return "Random " + super().__str__()
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:
return Announce(random_option())
class MenteurPlayer(Player, ABC):
""" A crazy player that always says menteur. """
def __str__(self):
return "Menteur " + super().__str__()
def announce(self, current_bet: Optional[Hand]) -> Announce:
hand = random_option()
return Announce(hand)
from unittest import TestCase from unittest import TestCase
from server.model.data import Player from server.model.card import Value, Color, Card, lowest_value_and_rest
from server.model.deck import Deck from server.model.deck import Deck
from server.model.hand import Hand from server.model.hand import Hand
from server.model.card import Value, Color, Card
from server.model.hands import full, brelan, pair, single, double_pair, carre from server.model.hands import full, brelan, pair, single, double_pair, carre
from server.model.known import ACE_OF_HEARTS, PAIR_ACE, SINGLE_ACE, DOUBLE_PAIR_ACE, BRELAN_ACE, FULL_ACE from server.model.known import ACE_OF_HEARTS, PAIR_ACE, SINGLE_ACE, DOUBLE_PAIR_ACE, BRELAN_ACE, FULL_ACE
from server.model.players import RandomPlayer
class TestDeck(TestCase): class TestDeck(TestCase):
...@@ -30,7 +30,7 @@ class TestPlayer(TestCase): ...@@ -30,7 +30,7 @@ class TestPlayer(TestCase):
def setUp(self) -> None: def setUp(self) -> None:
super().setUp() super().setUp()
self.player = Player() self.player = RandomPlayer()
def testHand(self): def testHand(self):
self.assertEqual(0, len(self.player.hand), "Begin no cards") self.assertEqual(0, len(self.player.hand), "Begin no cards")
...@@ -42,18 +42,15 @@ class TestPlayer(TestCase): ...@@ -42,18 +42,15 @@ class TestPlayer(TestCase):
self.assertEqual(Value.Ace, self.player.hand[0].value, "Is Ace") self.assertEqual(Value.Ace, self.player.hand[0].value, "Is Ace")
self.assertEqual(Color.Hearts, self.player.hand[0].color, "of Hearts") self.assertEqual(Color.Hearts, self.player.hand[0].color, "of Hearts")
def testTricheur(self):
with self.assertRaises(ValueError, msg="Giving someone a card they already have is an error"):
self.player.give(ACE_OF_HEARTS)
self.player.give(ACE_OF_HEARTS)
def testDefeats(self): def testDefeats(self):
pass pass
def lowest_value_and_rest():
lowValue: Value = Value.Two
otherValues = list(Value)
otherValues.remove(lowValue)
return lowValue, otherValues
class TestHand(TestCase): class TestHand(TestCase):
def setUp(self) -> None: def setUp(self) -> None:
self.hand = Hand() self.hand = Hand()
...@@ -126,27 +123,6 @@ class TestHand(TestCase): ...@@ -126,27 +123,6 @@ class TestHand(TestCase):
self.assertGreater(high_score, pair_score, f"Brelan[{high_hand}] > Pair[Ace]") self.assertGreater(high_score, pair_score, f"Brelan[{high_hand}] > Pair[Ace]")
self.assertGreater(high_score, single_score, f"Brelan[{high_hand}] > Ace") self.assertGreater(high_score, single_score, f"Brelan[{high_hand}] > Ace")
def testFulls(self):
# AssertionError: 24060 not greater than 24060 :
# 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 [
Value.Three,
Value.Three,
Value.Three,
Value.Four,
Value.Four,
]])
full2 = Hand([Card(v) for v in [
Value.Two,
Value.Two,
Value.Two,
Value.Four,
Value.Four,
]])
self.assertGreater(full1.value(), full2.value(), "Full1 > full2 (threes >> twos)")
def testFull(self): def testFull(self):
low_value, other_values = lowest_value_and_rest() low_value, other_values = lowest_value_and_rest()
...@@ -202,3 +178,25 @@ class TestHand(TestCase): ...@@ -202,3 +178,25 @@ class TestHand(TestCase):
self.assertGreater(high_score, double_pair_score, f"Carre[{high_hand}] > Pair[Ace]") self.assertGreater(high_score, double_pair_score, f"Carre[{high_hand}] > Pair[Ace]")
self.assertGreater(high_score, pair_score, f"Carre[{high_hand}] > Pair[Ace]") self.assertGreater(high_score, pair_score, f"Carre[{high_hand}] > Pair[Ace]")
self.assertGreater(high_score, single_score, f"Carre[{high_hand}] > Ace") self.assertGreater(high_score, single_score, f"Carre[{high_hand}] > Ace")
# Specifics
def testFulls(self):
# AssertionError: 24060 not greater than 24060 :
# 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 [
Value.Three,
Value.Three,
Value.Three,
Value.Four,
Value.Four,
]])
full2 = Hand([Card(v) for v in [
Value.Two,
Value.Two,
Value.Two,
Value.Four,
Value.Four,
]])
self.assertGreater(full1.value(), full2.value(), "Full1 > full2 (threes >> twos)")
from unittest import TestCase from unittest import TestCase
from server.model.data import Game, Player from server.model.data import Game
from server.model.players import MenteurPlayer, NaivePlayer, RandomPlayer
class TestGame(TestCase): class TestGame(TestCase):
def setUp(self) -> None: def setUp(self) -> None:
super().setUp() super().setUp()
self.player1 = Player("PLN") self.player1 = RandomPlayer("PLN")
self.player2 = Player("Nassim") self.player2 = NaivePlayer("Nassim")
self.game = Game([self.player1, self.player2]) self.game = Game([self.player1, self.player2])
def test_global_hand(self) -> None: def test_global_hand(self) -> None:
...@@ -23,11 +24,23 @@ class TestGame(TestCase): ...@@ -23,11 +24,23 @@ class TestGame(TestCase):
self.assertTrue(card2 in self.game.global_hand, "Global hand should contain player2's first card") self.assertTrue(card2 in self.game.global_hand, "Global hand should contain player2's first card")
self.assertTrue(card3 in self.game.global_hand, "Global hand should contain player2's second card") self.assertTrue(card3 in self.game.global_hand, "Global hand should contain player2's second card")
def test_add_player(self):
self.game.add_player(RandomPlayer("Foo"))
self.assertEqual(3, len(self.game.players), "Should be added")
self.game.add_player(RandomPlayer("Foo"))
self.assertEqual(3, len(self.game.players), "Should not add duplicate")
def test_turn(self): def test_turn(self):
menteur = MenteurPlayer()
naive = NaivePlayer()
self.game = Game([menteur, naive])
self.game.new_turn() self.game.new_turn()
self.assertEqual(1, len(self.game.defeats.values()), "There should have been one defeat.") self.assertEqual(1, len(self.game.defeats.values()), "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.")
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): def test_full_game(self):
self.game.new_game() self.game.new_game()
......
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