Files
marvinandClaude Opus 5.5 21a2b044d9 Trump card: draw from the stack, no trump for wizards and jesters
- Deal the hands from the shuffled stack and draw the trump card from it,
  so the trump card is in nobody's hand and cannot be played
- A wizard or jester as trump card means no trump suit (previously a
  wizard let the dealer choose); remove the trump selection phase, the
  ChooseTrump action, the trump picker UI and the AI trump actions
- Completed tricks carry their round's trump card, so the table keeps
  showing the matching trump card while the last trick of a round is
  displayed
- Card corners are no longer rotated, so 6 and 9 cannot be confused
- Trump label explains why there is no trump (wizard, jester, last round)
- Tests: trump card never dealt, special cards give no trump, tricks keep
  their trump card

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 10:28:53 +02:00

107 lines
3.3 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 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)