from __future__ import annotations
from typing import TYPE_CHECKING, Optional, Sequence, TypeVar
from .cards import Card, Suit, new_shuffled_deck
from .event_listener_protocol import WhistEventListener
if TYPE_CHECKING:
from .bet import Bet
from .player_strategy_protocol import PlayerStrategy
T = TypeVar("T")
class _Player:
"""This is used to ensure that the rules of whist are obeyed by the
:class:`whist_backend.player_strategy_protocol.PlayerStrategy`s.
"""
def __init__(
self,
strategy: PlayerStrategy,
hand: list[Card],
revealed_card: Card,
player_id: int,
number_of_players: int,
) -> None:
self.strategy = strategy
self.strategy.notify_round_start(
hand, revealed_card.suit, player_id, revealed_card, number_of_players
)
self.hand = hand
self.played_cards: list[Card] = []
self.player_id = player_id
self.tricks_won = 0
def make_move(self, initial_suit: Optional[Suit]) -> Card:
allowed_cards = self.get_allowed_cards(initial_suit)
picked_card = self.strategy.make_move(allowed_cards)
assert picked_card in allowed_cards, (
f"{self.strategy.name} picked an invalid card (picked {picked_card} which"
f"is not one of {allowed_cards})."
)
self.played_cards.append(picked_card)
self.hand.remove(picked_card)
return picked_card
def get_allowed_cards(self, initial_suit: Optional[Suit]) -> list[Card]:
if cards_of_initial_suit := [
card for card in self.hand if card.suit == initial_suit
]:
return cards_of_initial_suit
else:
return self.hand
def make_bet(self, disallowed_bet: Optional[int] = None) -> Bet:
bet = self.strategy.make_bet(disallowed_bet)
assert (
bet.number_of_tricks != disallowed_bet
), f"{self.strategy.name} made an invalid bet."
return bet
def notify_trick_won(self, player_id: int) -> None:
if player_id == self.player_id:
self.tricks_won += 1
self.strategy.notify_trick_won(player_id)
def __str__(self) -> str:
cards_left = ",".join(str(card) for card in self.hand)
cards_played = ",".join(str(card) for card in self.played_cards)
return f"{self.strategy.name}: hand {cards_left}" + (
f" (played {cards_played})" if cards_played else ""
)
[docs]def deal_hands(
number_of_players: int, hand_size: int, deck: list[Card]
) -> list[list[Card]]:
return [[deck.pop() for _ in range(hand_size)] for _ in range(number_of_players)]
[docs]def determine_trump_suit_and_hand_sizes(card: Card) -> tuple[Suit, int]:
trump_suit = card.suit
card_value = card.rank.value
if card_value < 5:
return trump_suit, 10 - card_value
elif card_value > 10:
return trump_suit, 21 - card_value
else:
return trump_suit, card_value
[docs]def cyclic_shift(items: list[T], shift_amount: int) -> list[T]:
shift_amount %= len(items)
return items[shift_amount:] + items[:shift_amount]
[docs]def find_trick_winner(
cards_and_players: list[tuple[_Player, Card]], trump_suit: Suit, initial_suit: Suit
) -> _Player:
trump_suit_moves = [
move for move in cards_and_players if move[1].suit == trump_suit
]
initial_suit_moves = [
move for move in cards_and_players if move[1].suit == initial_suit
]
if trump_suit_moves:
return max(trump_suit_moves, key=lambda move: move[1].rank)[0]
else:
return max(initial_suit_moves, key=lambda move: move[1].rank)[0]
[docs]class RoundSimulator:
[docs] def __init__(
self,
strategies: Sequence[PlayerStrategy],
*,
event_listeners: Optional[list[WhistEventListener]] = None,
) -> None:
deck = new_shuffled_deck()
revealed_card = deck.pop()
self.trump_suit, self.hand_size = determine_trump_suit_and_hand_sizes(
revealed_card
)
number_of_players = len(strategies)
hands = deal_hands(number_of_players, self.hand_size, deck)
self.players = [
_Player(strategy, hand, revealed_card, player_id, number_of_players)
for player_id, (strategy, hand) in enumerate(zip(strategies, hands))
]
self.next_starting_player = 0
self.event_listeners = event_listeners or []
for event_listener in self.event_listeners:
event_listener.notify_round_start(
revealed_card, self.trump_suit, self.hand_size, self.players
)
[docs] def play_round(self) -> list[tuple[PlayerStrategy, int]]:
bets = self.gather_bets_for_round(self.players)
for _ in range(self.hand_size):
self.play_trick()
results = [
(player.strategy, bet.compute_score(player.tricks_won))
for player, bet in zip(self.players, bets)
]
for player in self.players:
player.strategy.notify_round_end(
[(i, score) for i, (_, score) in enumerate(results)]
)
return results
[docs] def make_move_on_player(
self, player: _Player, initial_suit: Optional[Suit]
) -> Card:
picked_card = player.make_move(initial_suit)
for other_player in self.players:
other_player.strategy.notify_new_move(player.player_id, picked_card)
for event_listener in self.event_listeners:
event_listener.notify_card_played(player, picked_card)
return picked_card
[docs] def on_trick_win(self, player: _Player) -> None:
for other_player in self.players:
other_player.notify_trick_won(player.player_id)
self.next_starting_player = player.player_id
for event_listener in self.event_listeners:
event_listener.notify_trick_won(player)
[docs] def ask_player_for_bet(
self, player: _Player, disallowed_bet: Optional[int] = None
) -> Bet:
bet = player.make_bet(disallowed_bet)
for other_player in self.players:
other_player.strategy.notify_new_bet(player.player_id, bet)
return bet
[docs] def gather_bets_for_round(self, player_order: list[_Player]) -> list[Bet]:
bets = [self.ask_player_for_bet(player) for player in player_order[:-1]]
disallowed_bet_for_last_player = self.hand_size - sum(
map(lambda bet: bet.number_of_tricks, bets)
)
bets.append(
self.ask_player_for_bet(player_order[-1], disallowed_bet_for_last_player)
)
for event_listener in self.event_listeners:
event_listener.notify_bets_made(zip(player_order, bets))
return bets
[docs] def play_trick(self) -> None:
for player in self.players:
player.strategy.notify_new_trick(self.next_starting_player)
for event_listener in self.event_listeners:
event_listener.notify_new_trick(self.next_starting_player)
player_order = cyclic_shift(self.players, self.next_starting_player)
cards_played = [self.make_move_on_player(player_order[0], None)]
initial_suit = cards_played[0].suit
cards_played.extend(
self.make_move_on_player(player, initial_suit)
for player in player_order[1:]
)
trick_winner = find_trick_winner(
list(zip(player_order, cards_played)), self.trump_suit, initial_suit
)
self.on_trick_win(trick_winner)