first prototype
This commit is contained in:
@@ -0,0 +1,95 @@
|
||||
"""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)
|
||||
Reference in New Issue
Block a user