Files
Wizard/wizard_server/game_logic/wizard_game/rules.py
T
2026-09-27 09:52:00 +02:00

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)