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

116 lines
2.8 KiB
Python

"""Cards and deck of the Wizard card game.
The deck consists of 60 cards:
- 52 number cards: four suits (red, yellow, green, blue) with ranks 1-13
- 4 wizards (highest cards)
- 4 jesters (lowest cards)
Every card has a stable integer id in the range 0-59 which is used for
serialization and as an index for machine learning encodings:
- 0-51: number cards, ``suit_index * 13 + (rank - 1)``
- 52-55: wizards
- 56-59: jesters
"""
from __future__ import annotations
import random
from dataclasses import dataclass
from enum import Enum
class Suit(str, Enum):
RED = "red"
YELLOW = "yellow"
GREEN = "green"
BLUE = "blue"
@property
def index(self) -> int:
return SUITS.index(self)
SUITS: tuple[Suit, ...] = (Suit.RED, Suit.YELLOW, Suit.GREEN, Suit.BLUE)
class CardKind(str, Enum):
NUMBER = "number"
WIZARD = "wizard"
JESTER = "jester"
NUM_RANKS = 13
NUM_NUMBER_CARDS = len(SUITS) * NUM_RANKS
NUM_WIZARDS = 4
NUM_JESTERS = 4
DECK_SIZE = NUM_NUMBER_CARDS + NUM_WIZARDS + NUM_JESTERS
WIZARD_ID_OFFSET = NUM_NUMBER_CARDS
JESTER_ID_OFFSET = NUM_NUMBER_CARDS + NUM_WIZARDS
@dataclass(frozen=True, slots=True)
class Card:
id: int
kind: CardKind
suit: Suit | None = None
rank: int = 0
@property
def is_wizard(self) -> bool:
return self.kind is CardKind.WIZARD
@property
def is_jester(self) -> bool:
return self.kind is CardKind.JESTER
@property
def is_number(self) -> bool:
return self.kind is CardKind.NUMBER
def to_dict(self) -> dict:
return {
"id": self.id,
"kind": self.kind.value,
"suit": self.suit.value if self.suit else None,
"rank": self.rank,
}
def __str__(self) -> str:
if self.is_wizard:
return f"Wizard{self.id - WIZARD_ID_OFFSET + 1}"
if self.is_jester:
return f"Jester{self.id - JESTER_ID_OFFSET + 1}"
assert self.suit is not None
return f"{self.suit.value.capitalize()}{self.rank}"
def __repr__(self) -> str:
return f"Card({self})"
def _build_deck() -> tuple[Card, ...]:
cards: list[Card] = []
for suit in SUITS:
for rank in range(1, NUM_RANKS + 1):
cards.append(Card(len(cards), CardKind.NUMBER, suit, rank))
for _ in range(NUM_WIZARDS):
cards.append(Card(len(cards), CardKind.WIZARD))
for _ in range(NUM_JESTERS):
cards.append(Card(len(cards), CardKind.JESTER))
return tuple(cards)
ALL_CARDS: tuple[Card, ...] = _build_deck()
def card_by_id(card_id: int) -> Card:
if not 0 <= card_id < DECK_SIZE:
raise ValueError(f"Invalid card id: {card_id}")
return ALL_CARDS[card_id]
def new_shuffled_deck(rng: random.Random) -> list[Card]:
deck = list(ALL_CARDS)
rng.shuffle(deck)
return deck