03 / 07
OOD / 03

Design a deck of cards

A standard 52-card deck, plus the bones of any card game. The trap is to overfit to Blackjack or Poker — the right design treats Game as a pluggable layer on top of Deck and Hand, so the deck doesn't know what game it's in.


1 · Clarifying questions

One game or any game?Any. The deck is reusable across games; one specific game implementation is the concrete demo.
Jokers?Default no. Allow a with_jokers flag on the deck for games that need them.
Multiple decks?Yes (Blackjack uses six). Deck takes a count.
Card value?Game-dependent. A King is worth 13 in War, 10 in Blackjack, "high" in Poker. Value is not a property of Card; it's looked up by the game.
Shuffle quality?Fisher-Yates. Not the "sort by random key" approach — it's biased.

2 · Entities

  • Suit enum: HEARTS, DIAMONDS, CLUBS, SPADES.
  • Rank enum: TWO..TEN, JACK, QUEEN, KING, ACE.
  • Card — immutable. (suit, rank).
  • Deck — owns a list of cards; supports shuffle() and deal().
  • Hand — a collection of cards held by a player.
  • Player — has a hand, can be dealt to.
  • Game (interface) — manages players + deck, defines rules.

3 · Class sketch

enum Suit:  HEARTS, DIAMONDS, CLUBS, SPADES
enum Rank:  TWO, THREE, ..., TEN, JACK, QUEEN, KING, ACE

@dataclass(frozen=True)
class Card:
  suit: Suit
  rank: Rank

class Deck:
  def __init__(num_decks=1, with_jokers=False):
    self.cards = [Card(s, r) for s in Suit for r in Rank] * num_decks
    if with_jokers: self.cards.extend([Joker(), Joker()] * num_decks)

  def shuffle(rng=secrets):
    # Fisher-Yates: for i = n-1..1, swap cards[i] with cards[randint(0, i)].
    for i in range(len(self.cards) - 1, 0, -1):
      j = rng.randint(0, i)
      self.cards[i], self.cards[j] = self.cards[j], self.cards[i]

  def deal(n=1) -> list[Card]:
    if n > len(self.cards): raise EmptyDeck()
    out, self.cards = self.cards[:n], self.cards[n:]
    return out

  def __len__(self): return len(self.cards)

class Hand:
  cards: list[Card]
  def add(card): self.cards.append(card)
  def remove(card): self.cards.remove(card)

class Player:
  name: str
  hand: Hand

class Game(ABC):
  players: list[Player]
  deck: Deck
  def play(): ...

class BlackjackGame(Game):
  def value_of(card) -> int | tuple[int, int]:  # Aces are 1 or 11
    if card.rank in {JACK, QUEEN, KING}: return 10
    if card.rank == ACE: return (1, 11)
    return card.rank.value
  def play(): ...                # deal initial 2, loop hit/stand, settle

class PokerGame(Game):
  RANK_ORDER = [...]
  def best_hand(cards) -> HandRank: ...  # high card .. royal flush
  def play(): ...

4 · Why Card doesn't carry a value

Putting value on the card binds the deck to one game. Card value is contextual — game decides. The deck stays game-agnostic; Game subclasses provide the value-of-card map.

Same logic for "ordering" — Poker's "Aces high" vs. War's "Aces low" lives in the game, not the card.

5 · Shuffling, done correctly

Fisher-Yates in place is O(n), produces a uniform permutation, and uses one pass. The naive alternatives all bias:

  • sort(by: lambda _: random()) — biased (the comparator isn't a total order on random values; sort behaviour is undefined).
  • for each card, swap with a random card (loop over all i, swap with random across 0..n-1) — produces a biased, non-uniform distribution; the correct bound is 0..i, not 0..n-1.

Use secrets (or any CSPRNG) for any game where money is on the line.

6 · Edge cases

  • Deal more than remaining. Raise; never silently return a short list.
  • Reshuffle mid-game. Some games shuffle the discard back in. Deck supports add(cards) for this; the game owns the policy.
  • Card equality. If you allow multiple decks, two different physical 7♥ cards may need to be distinguishable. Add a hidden deck_id field, or accept that equality is by face value only.
  • Determinism for tests. Allow injecting an RNG seed.

7 · Extensions

  • Networked play. Server holds the deck; clients see only their own hand. Card becomes serialisable.
  • Provably fair shuffle. Server commits to a shuffle hash before play, reveals the seed after. Used in online casino sites.
  • Game state persistence. Serialise Game (players, hands, deck position) so a paused game can resume.

8 · What gets graded

  • Did you separate card from card-value?
  • Did you use Fisher-Yates (or at least flag the biased alternatives)?
  • Did you propose a Game interface, or did you only model Blackjack?
  • Did you make Card immutable?
Found this useful?