116 lines
2.8 KiB
Python
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
|