"""Stateless rule helpers of the Wizard card game.""" from __future__ import annotations from collections.abc import Sequence from .cards import DECK_SIZE, Card, Suit MIN_PLAYERS = 3 MAX_PLAYERS = 6 # Points for a correct prediction and per trick (correct) / per trick off (wrong). CORRECT_BID_BONUS = 20 POINTS_PER_TRICK = 10 PENALTY_PER_TRICK_OFF = 10 def total_rounds(num_players: int) -> int: """Number of rounds played: 3/4/5/6 players play 20/15/12/10 rounds.""" validate_player_count(num_players) return DECK_SIZE // num_players def validate_player_count(num_players: int) -> None: if not MIN_PLAYERS <= num_players <= MAX_PLAYERS: raise ValueError( f"Wizard is played with {MIN_PLAYERS}-{MAX_PLAYERS} players, got {num_players}" ) def trump_suit(trump_card: Card | None) -> Suit | None: """The trump suit defined by the turned up trump card. Only a number card defines a trump suit. A wizard, a jester or no trump card at all (last round) means the round is played without trump. """ if trump_card is None or not trump_card.is_number: return None return trump_card.suit def lead_suit(trick: Sequence[Card]) -> Suit | None: """The suit that has to be followed in a trick. The first number card determines the suit. Jesters played before it are skipped. If a wizard is played before any number card, the trick has no suit to follow. """ for card in trick: if card.is_jester: continue if card.is_wizard: return None return card.suit return None def legal_cards(hand: Sequence[Card], trick: Sequence[Card]) -> list[Card]: """Cards of ``hand`` that may be played onto ``trick``. Wizards and jesters may always be played. Number cards must follow the lead suit if the player holds a number card of that suit. """ suit = lead_suit(trick) if suit is None: return list(hand) if not any(card.is_number and card.suit is suit for card in hand): return list(hand) return [card for card in hand if not card.is_number or card.suit is suit] def trick_winner(trick: Sequence[Card], trump: Suit | None) -> int: """Index (within ``trick``) of the card that wins the trick. - the first wizard wins - otherwise the highest trump wins - otherwise the highest card of the lead suit wins - if only jesters were played, the first jester wins """ if not trick: raise ValueError("Cannot determine the winner of an empty trick") for index, card in enumerate(trick): if card.is_wizard: return index if trump is not None: trumps = [(card.rank, index) for index, card in enumerate(trick) if card.is_number and card.suit is trump] if trumps: return max(trumps)[1] suit = lead_suit(trick) if suit is not None: followers = [(card.rank, index) for index, card in enumerate(trick) if card.is_number and card.suit is suit] return max(followers)[1] return 0 def round_score(bid: int, tricks: int) -> int: """Score change of a player at the end of a round.""" if bid == tricks: return CORRECT_BID_BONUS + POINTS_PER_TRICK * tricks return -PENALTY_PER_TRICK_OFF * abs(bid - tricks)