96 lines
2.9 KiB
Python
96 lines
2.9 KiB
Python
"""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 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)
|