Skip to content

๐Ÿƒ Machine Coding: Deck of Cards (Blackjack)

๐Ÿ“ Overview

A flexible, object-oriented framework for a standard deck of cards, specifically extended to handle the complex, dynamic scoring rules of Blackjack.

Why This Challenge?

  • Inheritance & Polymorphism: Tests your ability to design abstract base classes (Card, Hand) that safely delegate game-specific logic to concrete subclasses.
  • Algorithmic Edge Cases: Evaluates how you handle dynamic state changesโ€”specifically, the dual-value nature of the "Ace" in Blackjack (1 or 11).
  • Extensibility: Ensures your architecture can easily support other games (like Poker or Rummy) without rewriting the core Deck mechanics.

๐Ÿญ The Scenario & Requirements

๐Ÿ˜ก The Problem (The Villain)

Building a generic card game engine is inherently difficult because every game calculates values differently. Blackjack is notoriously messy because an "Ace" isn't just one static valueโ€”it's a quantum state of 1 or 11. If you hardcode this logic directly into a Card object, the system breaks the moment you want to use the same deck for a game of Poker.

๐Ÿฆธ The System (The Hero)

An extensible object-oriented framework. Base classes (Deck, Hand, Card) strictly handle generic mechanics: holding cards, shuffling, and dealing. Subclasses (BlackJackCard, BlackJackHand) inject game-specific domain logic, cleanly isolating the complex, branching math required to score multiple Aces.

๐Ÿ“œ Requirements & Constraints

  1. (Functional): Must simulate a standard 52-card deck with 4 suits.
  2. (Functional): The deck must support dealing cards one by one and shuffling only the remaining, undealt cards.
  3. (Functional): Blackjack scoring must correctly handle face cards (J, Q, K = 10) and the Ace duality.
  4. (Technical): A hand with multiple Aces (e.g., Ace, Ace, 9) must automatically calculate the highest possible score without busting (21).

๐Ÿ—๏ธ Design & Architecture

๐Ÿง  Thinking Process

To enforce the Single Responsibility Principle, we separate the physical representation of a card from its game-specific value:
1. Card (Abstract): Holds the raw state (e.g., Suit: Heart, Face Value: 1).
2. BlackJackCard: Overrides the abstract value property. Translates raw face values (11, 12, 13) into Blackjack values (10).
3. Hand & BlackJackHand: Hand is a generic container. BlackJackHand contains the complex evaluation logic to sum the cards, dynamically branching whenever it encounters an Ace.
4. Deck: Acts as the dealer's shoe. Ignorant of the game being played.

๐Ÿงฉ Class Diagram

(The Object-Oriented Blueprint. Who owns what?)

classDiagram
    direction TB
    class Deck {
        -List~Card~ cards
        -int deal_index
        +deal_card()
        +shuffle()
        +remaining_cards()
    }
    class Card {
        <<abstract>>
        +Suit suit
        #int _value
        +value()* int
    }
    class BlackJackCard {
        +value() int
        +is_ace() bool
    }
    class Hand {
        -List~Card~ cards
        +add_card(card)
        +score() int
    }
    class BlackJackHand {
        +score() int
        +possible_scores() List~int~
    }

    Deck o-- Card : contains
    Hand o-- Card : holds
    Card <|-- BlackJackCard
    Hand <|-- BlackJackHand

โš™๏ธ Design Patterns Applied

  • Template Method (Concept): The Card base class defines the structure, but forces subclasses to implement the actual value retrieval logic via @abstractmethod.
  • Strategy Pattern (via Subclassing): BlackJackHand provides a specific scoring strategy that overrides the basic cumulative summation found in the generic Hand class.

๐Ÿ’ป Solution Implementation

The Code
import random
from abc import ABC, abstractmethod
from enum import Enum
from typing import List, Optional


class Suit(Enum):
    """Represents the standard suits of a playing card."""
    HEART = 0
    DIAMOND = 1
    CLUB = 2
    SPADE = 3


class Card(ABC):
    """Abstract base class representing a generic playing card."""

    def __init__(self, value: int, suit: Suit) -> None:
        self._value: int = value
        self.suit: Suit = suit
        self.is_available: bool = True

    @property
    @abstractmethod
    def value(self) -> int:
        """Abstract property: specific games dictate how a card's value is resolved."""
        pass


class BlackJackCard(Card):
    """Represents a card implementing Blackjack-specific scoring rules."""

    def __init__(self, value: int, suit: Suit) -> None:
        """
        Initializes a Blackjack card.

        Args:
            value (int): The face value (1 for Ace, 11-13 for Face cards).
            suit (Suit): The suit of the card.

        Raises:
            ValueError: If the card value falls outside standard deck bounds.
        """
        if not (1 <= value <= 13):
            raise ValueError(f"Invalid card value: {value}")
        super().__init__(value, suit)

    def is_ace(self) -> bool:
        return self._value == 1

    def is_face_card(self) -> bool:
        """Jack = 11, Queen = 12, King = 13"""
        return 11 <= self._value <= 13

    @property
    def value(self) -> int:
        """Resolves the face value into a Blackjack integer score."""
        if self.is_ace():
            return 1  # Base value for Ace; BlackJackHand resolves the 1 vs 11 duality
        elif self.is_face_card():
            return 10
        return self._value


class Hand:
    """Base collection representing a player's current held cards."""

    def __init__(self, cards: Optional[List[Card]] = None) -> None:
        self.cards: List[Card] = cards if cards is not None else []

    def add_card(self, card: Card) -> None:
        self.cards.append(card)

    def score(self) -> int:
        """Standard cumulative scoring fallback."""
        return sum(card.value for card in self.cards)


class BlackJackHand(Hand):
    """Extended Hand that implements dynamic Blackjack scoring resolution."""

    BLACKJACK_TARGET = 21

    def __init__(self, cards: Optional[List[BlackJackCard]] = None) -> None:
        # Type hinting overridden purely for domain specificity
        super().__init__(cards)  # type: ignore

    def score(self) -> int:
        """
        Calculates the optimal Blackjack score.
        Selects the highest possible score under 21, or the lowest bust if unavoidable.
        """
        possible_scores = self.possible_scores()
        best_under = 0
        min_over = float('inf')

        for s in possible_scores:
            if s <= self.BLACKJACK_TARGET:
                best_under = max(best_under, s)
            else:
                min_over = min(min_over, s)

        return best_under if best_under > 0 else int(min_over)

    def possible_scores(self) -> List[int]:
        """Generates a branching list of all possible scores due to the Ace 1/11 duality."""
        scores = [0]
        for card in self.cards:
            new_scores = []
            for current_score in scores:
                new_scores.append(current_score + card.value)

                # Branch the score calculation if the card is a dual-value Ace
                if isinstance(card, BlackJackCard) and card.is_ace():
                    new_scores.append(current_score + 11)
            scores = new_scores
        return scores


class Deck:
    """Manages a collection of Cards, representing a dealer's shoe or deck."""

    def __init__(self, cards: List[Card]) -> None:
        self.cards: List[Card] = cards
        self.deal_index: int = 0

    def remaining_cards(self) -> int:
        """Calculates undealt inventory."""
        return len(self.cards) - self.deal_index

    def deal_card(self) -> Optional[Card]:
        """
        Pulls the next available card from the top of the deck.

        Returns:
            Card if available, otherwise None if the deck is exhausted.
        """
        if self.deal_index < len(self.cards):
            card = self.cards[self.deal_index]
            card.is_available = False
            self.deal_index += 1
            return card
        return None

    def shuffle(self) -> None:
        """Shuffles only the remaining, undealt cards in the shoe."""
        remaining = self.cards[self.deal_index:]
        random.shuffle(remaining)
        self.cards = self.cards[:self.deal_index] + remaining

๐Ÿ”ฌ Why This Works (Evaluation)

The hardest algorithmic constraint is scoring multiple Aces. Instead of writing messy, deeply nested if/else chains, BlackJackHand.possible_scores() utilizes a BFS-like branching array.

Every time it iterates over an Ace, it duplicates the current running totalsโ€”one branch adding 1, the other adding 11. The score() method then simply filters this array, selecting the highest valid total \(\le 21\), or the lowest busted total if a bust is unavoidable.


โš–๏ธ Trade-offs & Limitations

Decision Pros Cons / Limitations
BFS Branching for Aces 100% accurate for any edge case involving multiple Aces. \(O(2^N)\) complexity where N is the number of Aces.
Index-based Dealing deal_index += 1 is \(O(1)\) and preserves the deck's history. The underlying array is never shrunk; keeps dealt cards in memory.
Strong Typing (BlackJackCard) Prevents generic cards from being scored in a Blackjack hand. Requires creating parallel classes for every new game added to the system.

๐ŸŽค Interview Toolkit

  • Algorithmic Probe: "Your BFS Ace calculation is \(O(2^N)\). What if we played a variant where a hand could have 1,000 Aces? How do you optimize it?" -> (Instead of branching, use a mathematical approach: Count the total number of Aces. Add them all as 1s. Then, if the total sum is \(\le 11\), upgrade exactly one Ace to 11 (adding 10). This reduces the time and space complexity to \(O(N)\)).
  • Extensibility: "How would you add Texas Hold'em Poker to this system?" -> (Create a PokerCard mapping suits/faces, and a PokerHand that overrides score() to evaluate combinations like Flushes, Straights, and Pairs, leaving Deck completely untouched).
  • State Management: "How do you handle a multi-deck 'shoe' commonly used in casinos?" -> (Simply instantiate the Deck class by passing in \(N \times 52\) cards into its constructor list. The deal_index and shuffle() logic will scale natively).
  • Tic-Tac-Toe Game โ€” Another classic OOP game design focusing on rule validation and state matrices.
  • Snake and Ladder โ€” For modeling turn-based game progression and board entities.