"""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