problemmediumooddesign-a-data-structure-for-generic-deck-of-cardsdesign a data structure for generic deck of cardsdesignadatastructureforgenericdeckofcardsood-for-card-gamesood for card gamesoodforcardgames

Design a data structure for generic deck of cards

MediumUpdated: Jan 1, 2026

1. Problem Statement

Build a simple, reusable deck‑of‑cards library that works for many games (Poker, Blackjack, etc.). Provide a standard 52‑card deck and the basic operations: shuffle, draw, deal to players, and check remaining cards.

Different games score cards differently. The design should make it easy to add game‑specific rules (e.g., Blackjack: face cards = 10, Ace = 1 or 11) without changing the generic types.

Core goals:

  • Keep Card immutable (suit + rank never change)
  • Separate generic deck/hand behavior from game rules
  • Let games subclass Card, Deck, and Hand when needed
  • Support round‑robin dealing and optional multi‑deck shoes

2. System Requirements

Functional Requirements:

  1. Create a standard 52-card deck with 4 suits and 13 ranks
  2. Support shuffling using randomization algorithms
  3. Draw individual cards from deck (remove and return)
  4. Deal specified number of cards to one or more players
  5. Track remaining cards in deck
  6. Support game-specific card value calculations (Blackjack, Poker, etc.)
  7. Represent cards as immutable value objects (suit and rank never change)
  8. Support hand abstraction to hold player's cards
  9. Enable deck extension for non-standard decks (Uno, Tarot, etc.)
  10. Handle empty deck scenario (exception or reshuffle)
  11. Support discard pile management
  12. Allow multi-deck scenarios (6-deck shoe for Blackjack)
  13. Display card representation (e.g., "Ace of Spades", "10♥")

Non-Functional Requirements:

  1. Immutability: Cards must be immutable to prevent accidental modification
  2. Extensibility: Adding new games requires only subclassing, no base class modification
  3. Performance: Shuffle operation O(n), draw operation O(1)
  4. Type Safety: Compile-time guarantees for game-specific card types
  5. Usability: Intuitive API matching card game mental models

Assumptions:

  • Single-threaded access initially (thread safety can be added if needed)
  • Standard card games use 52-card deck; non-standard games extend appropriately
  • Cards are drawn from top of deck by default
  • Shuffling uses pseudorandom number generator (sufficient for non-gambling applications)
  • Each card in a deck is unique (no duplicate Ace of Spades in one deck)

3. Use Case Diagram

Actors:

  • Player: Draws cards, holds hand
  • Dealer: Manages deck, deals cards to players
  • Game System: Evaluates hands, determines winners

Core Use Cases:

  • Initialize Deck
  • Shuffle Deck
  • Draw Card
  • Deal Hand to Player
  • Evaluate Hand Value
  • Display Card
  • Reshuffle Discard Pile
  • Create Multi-Deck Shoe
graph TB
    subgraph CardGameSystem["Card Game System"]
        UC1["Initialize Deck"]
        UC2["Shuffle Deck"]
        UC3["Draw Card"]
        UC4["Deal Hand"]
        UC5["Evaluate Hand Value"]
        UC6["Display Card"]
        UC7["Reshuffle Discards"]
        UC8["Create Multi-Deck Shoe"]
    end
    
    Player([Player])
    Dealer([Dealer])
    GameSystem([Game System])
    
    Player --> UC3
    Player --> UC6
    
    Dealer --> UC1
    Dealer --> UC2
    Dealer --> UC4
    Dealer --> UC7
    Dealer --> UC8
    
    GameSystem --> UC5
    
    UC1 -.->|triggers| UC2
    UC4 -.->|uses| UC3
    UC7 -.->|triggers| UC2
    
    style Player fill:#4CAF50,color:#fff
    style Dealer fill:#FF9800,color:#fff
    style GameSystem fill:#2196F3,color:#fff

4. Class Diagram

Core Classes:

  • Suit: Enum (HEARTS, DIAMONDS, CLUBS, SPADES) with symbols
  • Rank: Enum (ACE through KING) with numeric values
  • Card: Immutable base class with suit and rank
  • BlackjackCard: Card subclass with Blackjack-specific value logic
  • Deck: Abstract base managing cards collection with shuffle/draw/deal
  • StandardDeck: 52-card deck implementation
  • BlackjackDeck: Deck creating BlackjackCard instances
  • Hand: Player's held cards with evaluation logic
  • BlackjackHand: Hand with Blackjack total calculation (Ace optimization)
classDiagram
    class Suit {
        <<enumeration>>
        HEARTS
        DIAMONDS
        CLUBS
        SPADES
        +String symbol
        +getSymbol() String
    }
    
    class Rank {
        <<enumeration>>
        ACE
        TWO
        THREE
        FOUR
        FIVE
        SIX
        SEVEN
        EIGHT
        NINE
        TEN
        JACK
        QUEEN
        KING
        +int value
        +String displayName
        +getValue() int
        +getDisplayName() String
    }
    
    class Card {
        -Suit suit
        -Rank rank
        +Card(Suit, Rank)
        +getSuit() Suit
        +getRank() Rank
        +getValue() int
        +toString() String
        +equals(Object) boolean
        +hashCode() int
    }
    
    class BlackjackCard {
        +BlackjackCard(Suit, Rank)
        +getValue() int
        +getMinValue() int
        +getMaxValue() int
        +isAce() boolean
    }
    
    class Deck {
        <<abstract>>
        #List~Card~ cards
        #List~Card~ discardPile
        +Deck()
        +shuffle() void
        +draw() Card
        +drawMultiple(int) List~Card~
        +deal(int, int) List~Hand~
        +remainingCards() int
        +isEmpty() boolean
        +reset() void
        #initializeDeck()* void
    }
    
    class StandardDeck {
        +StandardDeck()
        #initializeDeck() void
    }
    
    class BlackjackDeck {
        +BlackjackDeck()
        +BlackjackDeck(int)
        #initializeDeck() void
    }
    
    class Hand {
        #List~Card~ cards
        +Hand()
        +addCard(Card) void
        +removeCard(Card) void
        +getCards() List~Card~
        +size() int
        +clear() void
        +toString() String
    }
    
    class BlackjackHand {
        +BlackjackHand()
        +getValue() int
        +isBusted() boolean
        +isBlackjack() boolean
    }
    
    Card --> Suit
    Card --> Rank
    Card <|-- BlackjackCard
    
    Deck "1" o-- "*" Card
    Deck <|-- StandardDeck
    Deck <|-- BlackjackDeck
    
    Hand "1" o-- "*" Card
    Hand <|-- BlackjackHand
    
    Deck ..> Hand : creates

5. Activity Diagrams

Deck Initialization and Shuffling

graph TD
    Start([Create Deck]) --> Init[Call constructor]
    Init --> Template[Call initializeDeck abstract method]
    Template --> Subclass[Subclass creates game-specific cards]
    Subclass --> Loop{For each suit}
    
    Loop -->|Each suit| Inner{For each rank}
    Inner -->|Each rank| Create[Create Card suit, rank]
    Create --> Add[Add card to deck]
    Add --> Inner
    Inner -->|All ranks| Loop
    
    Loop -->|All suits| Shuffle[Shuffle deck]
    Shuffle --> FisherYates[Fisher-Yates algorithm]
    FisherYates --> Iter{For i from n-1 to 1}
    Iter -->|Each position| Random[j = random 0 to i]
    Random --> Swap[Swap cards i and j]
    Swap --> Iter
    
    Iter -->|Complete| Ready([Deck ready])

Dealing Cards to Players

graph TD
    Start([Deal request: players=3, cards=5]) --> CheckDeck{Enough cards?}
    CheckDeck -->|No| Error[Throw InsufficientCardsException]
    
    CheckDeck -->|Yes| CreateHands[Create empty Hand for each player]
    CreateHands --> OuterLoop{For each card position 1 to 5}
    
    OuterLoop -->|Each position| InnerLoop{For each player}
    InnerLoop -->|Each player| Draw[Draw one card from deck]
    Draw --> AddToHand[Add card to player's hand]
    AddToHand --> InnerLoop
    
    InnerLoop -->|All players| OuterLoop
    OuterLoop -->|All positions| Return([Return List of Hands])

Blackjack Hand Evaluation

graph TD
    Start([Calculate Blackjack hand value]) --> Init[totalValue = 0, aceCount = 0]
    Init --> Loop{For each card in hand}
    
    Loop -->|Each card| CheckAce{Is Ace?}
    CheckAce -->|Yes| AddOne[totalValue += 1]
    AddOne --> IncrementAce[aceCount++]
    IncrementAce --> Loop
    
    CheckAce -->|No| AddValue[totalValue += card.getValue]
    AddValue --> Loop
    
    Loop -->|All cards processed| AceLoop{aceCount > 0?}
    AceLoop -->|Yes| CheckBust{totalValue + 10 <= 21?}
    CheckBust -->|Yes| Optimize[totalValue += 10]
    Optimize --> DecrementAce[aceCount--]
    DecrementAce --> AceLoop
    
    CheckBust -->|No| AceLoop
    AceLoop -->|No| Return([Return totalValue])

6. Java Implementation

Enums

/**
 * Card suits with Unicode symbols
 */
public enum Suit {
    HEARTS("♥"),
    DIAMONDS("♦"),
    CLUBS("♣"),
    SPADES("♠");
    
    private final String symbol;
    
    Suit(String symbol) {
        this.symbol = symbol;
    }
    
    public String getSymbol() {
        return symbol;
    }
}

/**
 * Card ranks from Ace to King
 */
public enum Rank {
    ACE(1, "A"),
    TWO(2, "2"),
    THREE(3, "3"),
    FOUR(4, "4"),
    FIVE(5, "5"),
    SIX(6, "6"),
    SEVEN(7, "7"),
    EIGHT(8, "8"),
    NINE(9, "9"),
    TEN(10, "10"),
    JACK(11, "J"),
    QUEEN(12, "Q"),
    KING(13, "K");
    
    private final int value;
    private final String displayName;
    
    Rank(int value, String displayName) {
        this.value = value;
        this.displayName = displayName;
    }
    
    public int getValue() {
        return value;
    }
    
    public String getDisplayName() {
        return displayName;
    }
    
    public boolean isFaceCard() {
        return this == JACK || this == QUEEN || this == KING;
    }
}

Card Classes

import java.util.Objects;

/**
 * Immutable playing card with suit and rank
 */
public class Card {
    private final Suit suit;
    private final Rank rank;
    
    public Card(Suit suit, Rank rank) {
        this.suit = Objects.requireNonNull(suit, "Suit cannot be null");
        this.rank = Objects.requireNonNull(rank, "Rank cannot be null");
    }
    
    public Suit getSuit() {
        return suit;
    }
    
    public Rank getRank() {
        return rank;
    }
    
    /**
     * Get numeric value (overridden by subclasses for game-specific logic)
     */
    public int getValue() {
        return rank.getValue();
    }
    
    @Override
    public String toString() {
        return rank.getDisplayName() + suit.getSymbol();
    }
    
    public String toLongString() {
        return rank.getDisplayName() + " of " + suit;
    }
    
    @Override
    public boolean equals(Object obj) {
        if (this == obj) return true;
        if (!(obj instanceof Card)) return false;
        Card other = (Card) obj;
        return suit == other.suit && rank == other.rank;
    }
    
    @Override
    public int hashCode() {
        return Objects.hash(suit, rank);
    }
}

/**
 * Blackjack-specific card with special value rules
 */
public class BlackjackCard extends Card {
    
    public BlackjackCard(Suit suit, Rank rank) {
        super(suit, rank);
    }
    
    /**
     * Blackjack value: Face cards = 10, Ace = 1 (optimization handled in Hand)
     */
    @Override
    public int getValue() {
        if (getRank().isFaceCard()) {
            return 10;
        }
        return getRank().getValue();
    }
    
    /**
     * Minimum value (Ace as 1)
     */
    public int getMinValue() {
        return getValue();
    }
    
    /**
     * Maximum value (Ace as 11, others same)
     */
    public int getMaxValue() {
        return isAce() ? 11 : getValue();
    }
    
    public boolean isAce() {
        return getRank() == Rank.ACE;
    }
}

Deck Classes

import java.util.*;

/**
 * Abstract base deck with template method for initialization
 */
public abstract class Deck {
    protected List<Card> cards;
    protected List<Card> discardPile;
    
    public Deck() {
        this.cards = new ArrayList<>();
        this.discardPile = new ArrayList<>();
        initializeDeck();
        shuffle();
    }
    
    /**
     * Template method: subclasses define deck composition
     */
    protected abstract void initializeDeck();
    
    /**
     * Fisher-Yates shuffle algorithm
     */
    public void shuffle() {
        Random random = new Random();
        for (int i = cards.size() - 1; i > 0; i--) {
            int j = random.nextInt(i + 1);
            Card temp = cards.get(i);
            cards.set(i, cards.get(j));
            cards.set(j, temp);
        }
    }
    
    /**
     * Draw one card from top of deck
     */
    public Card draw() {
        if (cards.isEmpty()) {
            throw new IllegalStateException("Cannot draw from empty deck");
        }
        return cards.remove(cards.size() - 1);
    }
    
    /**
     * Draw multiple cards
     */
    public List<Card> drawMultiple(int count) {
        if (count > cards.size()) {
            throw new IllegalArgumentException(
                "Cannot draw " + count + " cards, only " + cards.size() + " remaining");
        }
        
        List<Card> drawn = new ArrayList<>();
        for (int i = 0; i < count; i++) {
            drawn.add(draw());
        }
        return drawn;
    }
    
    /**
     * Deal cards to multiple players
     * @param numPlayers number of players
     * @param cardsPerPlayer cards to deal to each player
     * @return list of hands (one per player)
     */
    public List<Hand> deal(int numPlayers, int cardsPerPlayer) {
        int totalCards = numPlayers * cardsPerPlayer;
        if (totalCards > cards.size()) {
            throw new IllegalArgumentException(
                "Insufficient cards: need " + totalCards + ", have " + cards.size());
        }
        
        List<Hand> hands = new ArrayList<>();
        for (int i = 0; i < numPlayers; i++) {
            hands.add(createHand());
        }
        
        // Deal round-robin style
        for (int cardNum = 0; cardNum < cardsPerPlayer; cardNum++) {
            for (Hand hand : hands) {
                hand.addCard(draw());
            }
        }
        
        return hands;
    }
    
    /**
     * Factory method for creating appropriate Hand type
     */
    protected Hand createHand() {
        return new Hand();
    }
    
    /**
     * Add card to discard pile
     */
    public void discard(Card card) {
        discardPile.add(card);
    }
    
    /**
     * Reshuffle discard pile back into deck
     */
    public void reshuffleDiscards() {
        cards.addAll(discardPile);
        discardPile.clear();
        shuffle();
    }
    
    public int remainingCards() {
        return cards.size();
    }
    
    public boolean isEmpty() {
        return cards.isEmpty();
    }
    
    /**
     * Reset deck to initial state
     */
    public void reset() {
        cards.clear();
        discardPile.clear();
        initializeDeck();
        shuffle();
    }
    
    @Override
    public String toString() {
        return getClass().getSimpleName() + "[cards=" + cards.size() + 
               ", discards=" + discardPile.size() + "]";
    }
}

/**
 * Standard 52-card deck
 */
public class StandardDeck extends Deck {
    
    @Override
    protected void initializeDeck() {
        for (Suit suit : Suit.values()) {
            for (Rank rank : Rank.values()) {
                cards.add(new Card(suit, rank));
            }
        }
    }
}

/**
 * Blackjack deck (can support multi-deck shoe)
 */
public class BlackjackDeck extends Deck {
    private final int numDecks;
    
    public BlackjackDeck() {
        this(1);
    }
    
    public BlackjackDeck(int numDecks) {
        this.numDecks = numDecks;
        // Note: initializeDeck called by super() before this line
    }
    
    @Override
    protected void initializeDeck() {
        for (int deck = 0; deck < (numDecks == 0 ? 1 : numDecks); deck++) {
            for (Suit suit : Suit.values()) {
                for (Rank rank : Rank.values()) {
                    cards.add(new BlackjackCard(suit, rank));
                }
            }
        }
    }
    
    @Override
    protected Hand createHand() {
        return new BlackjackHand();
    }
}

Hand Classes

import java.util.*;

/**
 * Player's hand of cards
 */
public class Hand {
    protected List<Card> cards;
    
    public Hand() {
        this.cards = new ArrayList<>();
    }
    
    public void addCard(Card card) {
        cards.add(card);
    }
    
    public void removeCard(Card card) {
        cards.remove(card);
    }
    
    public List<Card> getCards() {
        return new ArrayList<>(cards); // Defensive copy
    }
    
    public int size() {
        return cards.size();
    }
    
    public void clear() {
        cards.clear();
    }
    
    @Override
    public String toString() {
        StringBuilder sb = new StringBuilder("[");
        for (int i = 0; i < cards.size(); i++) {
            sb.append(cards.get(i));
            if (i < cards.size() - 1) {
                sb.append(", ");
            }
        }
        sb.append("]");
        return sb.toString();
    }
}

/**
 * Blackjack hand with value calculation and Ace optimization
 */
public class BlackjackHand extends Hand {
    
    /**
     * Calculate hand value with Ace optimization
     */
    public int getValue() {
        int totalValue = 0;
        int aceCount = 0;
        
        // First pass: count base values and Aces
        for (Card card : cards) {
            if (card instanceof BlackjackCard) {
                BlackjackCard bjCard = (BlackjackCard) card;
                if (bjCard.isAce()) {
                    totalValue += 1; // Count Ace as 1 initially
                    aceCount++;
                } else {
                    totalValue += bjCard.getValue();
                }
            } else {
                totalValue += card.getValue();
            }
        }
        
        // Optimize Aces: convert one Ace from 1 to 11 if it doesn't bust
        while (aceCount > 0 && totalValue + 10 <= 21) {
            totalValue += 10; // Convert one Ace from 1 to 11
            aceCount--;
        }
        
        return totalValue;
    }
    
    public boolean isBusted() {
        return getValue() > 21;
    }
    
    public boolean isBlackjack() {
        return cards.size() == 2 && getValue() == 21;
    }
    
    @Override
    public String toString() {
        return super.toString() + " [Value: " + getValue() + "]";
    }
}

Demo Application

/**
 * Comprehensive demo of card game system
 */
public class CardGameDemo {
    
    public static void main(String[] args) {
        System.out.println("═══════════════════════════════════════════════");
        System.out.println("       CARD GAME SYSTEM DEMONSTRATION");
        System.out.println("═══════════════════════════════════════════════\n");
        
        // Demo 1: Standard Deck
        demoStandardDeck();
        
        // Demo 2: Blackjack Game
        demoBlackjack();
        
        // Demo 3: Multi-Deck Shoe
        demoMultiDeckShoe();
        
        System.out.println("\n═══════════════════════════════════════════════");
        System.out.println("       DEMONSTRATION COMPLETE");
        System.out.println("═══════════════════════════════════════════════");
    }
    
    private static void demoStandardDeck() {
        System.out.println("▶ DEMO 1: Standard 52-Card Deck");
        System.out.println("─────────────────────────────────────────────\n");
        
        StandardDeck deck = new StandardDeck();
        System.out.println("✓ Created deck: " + deck);
        System.out.println("  Remaining cards: " + deck.remainingCards());
        
        System.out.println("\n📤 Drawing 5 cards:");
        List<Card> drawnCards = deck.drawMultiple(5);
        for (int i = 0; i < drawnCards.size(); i++) {
            System.out.println("  Card " + (i + 1) + ": " + drawnCards.get(i).toLongString());
        }
        System.out.println("  Remaining cards: " + deck.remainingCards());
        
        System.out.println("\n🎲 Dealing poker game (4 players, 5 cards each):");
        deck.reset(); // Start fresh
        List<Hand> pokerHands = deck.deal(4, 5);
        for (int i = 0; i < pokerHands.size(); i++) {
            System.out.println("  Player " + (i + 1) + ": " + pokerHands.get(i));
        }
        System.out.println("  Remaining cards: " + deck.remainingCards());
        System.out.println();
    }
    
    private static void demoBlackjack() {
        System.out.println("▶ DEMO 2: Blackjack Game");
        System.out.println("─────────────────────────────────────────────\n");
        
        BlackjackDeck deck = new BlackjackDeck();
        System.out.println("✓ Created Blackjack deck: " + deck);
        
        System.out.println("\n🎰 Dealing Blackjack hands (2 players + dealer):");
        List<Hand> hands = deck.deal(3, 2);
        
        BlackjackHand player1 = (BlackjackHand) hands.get(0);
        BlackjackHand player2 = (BlackjackHand) hands.get(1);
        BlackjackHand dealer = (BlackjackHand) hands.get(2);
        
        System.out.println("  Player 1: " + player1);
        if (player1.isBlackjack()) {
            System.out.println("           🎉 BLACKJACK!");
        }
        
        System.out.println("  Player 2: " + player2);
        if (player2.isBlackjack()) {
            System.out.println("           🎉 BLACKJACK!");
        }
        
        System.out.println("  Dealer:   " + dealer);
        if (dealer.isBlackjack()) {
            System.out.println("           🎉 DEALER BLACKJACK!");
        }
        
        // Simulate player 1 hitting
        System.out.println("\n🎯 Player 1 hits:");
        Card hitCard = deck.draw();
        player1.addCard(hitCard);
        System.out.println("  Drew: " + hitCard.toLongString());
        System.out.println("  Player 1: " + player1);
        if (player1.isBusted()) {
            System.out.println("           💥 BUSTED!");
        }
        
        // Demonstrate Ace optimization
        System.out.println("\n♠️  Demonstrating Ace optimization:");
        BlackjackHand testHand = new BlackjackHand();
        testHand.addCard(new BlackjackCard(Suit.SPADES, Rank.ACE));
        testHand.addCard(new BlackjackCard(Suit.HEARTS, Rank.KING));
        System.out.println("  Hand: " + testHand + " (Ace counts as 11)");
        
        testHand.addCard(new BlackjackCard(Suit.DIAMONDS, Rank.FIVE));
        System.out.println("  After adding 5: " + testHand + " (Ace now counts as 1)");
        System.out.println();
    }
    
    private static void demoMultiDeckShoe() {
        System.out.println("▶ DEMO 3: Multi-Deck Shoe (6 decks)");
        System.out.println("─────────────────────────────────────────────\n");
        
        BlackjackDeck shoe = new BlackjackDeck(6);
        System.out.println("✓ Created 6-deck shoe: " + shoe);
        System.out.println("  Total cards: " + shoe.remainingCards());
        System.out.println("  Expected: " + (52 * 6) + " cards");
        
        System.out.println("\n🎲 Dealing multiple rounds:");
        for (int round = 1; round <= 3; round++) {
            if (shoe.remainingCards() < 6) {
                System.out.println("  ⚠️  Insufficient cards, reshuffling discards...");
                shoe.reshuffleDiscards();
            }
            
            List<Hand> hands = shoe.deal(2, 2);
            System.out.println("  Round " + round + ":");
            System.out.println("    Player: " + hands.get(0));
            System.out.println("    Dealer: " + hands.get(1));
            System.out.println("    Remaining: " + shoe.remainingCards() + " cards");
            
            // Discard hands
            for (Hand hand : hands) {
                for (Card card : hand.getCards()) {
                    shoe.discard(card);
                }
            }
        }
        
        System.out.println("\n📊 Final state:");
        System.out.println("  Cards in shoe: " + shoe.remainingCards());
        System.out.println();
    }
}

7. Design Patterns Applied

1. Immutable Value Object (Card)

Intent: Cards are immutable once created, preventing bugs from shared mutable state.

Implementation:

  • All fields final
  • No setters
  • Defensive copying in getters if needed

Benefits:

  • Thread-safe by default
  • Can safely share cards across hands
  • Hashable for collections
public class Card {
    private final Suit suit;  // Immutable
    private final Rank rank;  // Immutable
    // No setters!
}

2. Template Method (Deck Initialization)

Intent: Define skeleton of deck creation while letting subclasses customize card types.

Implementation:

  • Deck constructor calls abstract initializeDeck()
  • Subclasses implement specific card creation

Benefits:

  • Common shuffle/draw logic in base class
  • Game-specific initialization in subclasses
  • Enforces initialization contract
public abstract class Deck {
    public Deck() {
        initializeDeck();  // Calls subclass method
        shuffle();
    }
    protected abstract void initializeDeck();
}

3. Factory Method (Hand Creation)

Intent: Deck creates appropriate Hand type for the game.

Implementation:

  • Deck.createHand() returns Hand
  • BlackjackDeck.createHand() returns BlackjackHand

Benefits:

  • Decouples deck from specific hand types
  • Ensures matching hand type for game
protected Hand createHand() {
    return new Hand();
}
// BlackjackDeck overrides:
protected Hand createHand() {
    return new BlackjackHand();
}

4. Strategy Pattern (Shuffling - Potential)

Intent: Pluggable shuffle algorithms.

Could Implement:

interface ShuffleStrategy {
    void shuffle(List<Card> cards);
}

class FisherYatesStrategy implements ShuffleStrategy { ... }
class RiggedStrategy implements ShuffleStrategy { ... }

deck.setShuffleStrategy(new FisherYatesStrategy());

5. Enum Singleton (Suit, Rank)

Intent: Type-safe constants with behavior.

Implementation:

  • Suit and Rank are enums
  • Each enum value is a singleton

Benefits:

  • Type safety (can't pass invalid suit)
  • Built-in iteration (Suit.values())
  • Can add methods to enum values
public enum Rank {
    ACE(1, "A"),
    // ...
    public boolean isFaceCard() { ... }
}

6. Null Object Pattern (Empty Deck Handling)

Intent: Avoid null checks by providing empty behavior.

Could Implement:

class EmptyDeck extends Deck {
    @Override
    public Card draw() {
        return new NullCard(); // Special "no card" object
    }
}

8. Key Design Decisions

Decision 1: Cards Are Immutable

Rationale: Once created, card suit and rank never change.

Benefits:

  • Thread-safe
  • Safe to share across hands
  • Prevents accidental modification bugs

Implementation: Final fields, no setters

Decision 2: Separate Card Subclasses Per Game

Rationale: Different games interpret card values differently.

Benefits:

  • Game-specific logic encapsulated in card
  • BlackjackCard.getValue() returns 10 for face cards
  • Type safety for game-specific hands

Alternative Considered: Single Card class with strategy for value calculation (more complex)

Decision 3: Template Method for Deck Initialization

Rationale: Common deck operations (shuffle, draw) same across games, but card types differ.

Benefits:

  • Code reuse for common operations
  • Subclasses only implement initializeDeck()
  • Enforces initialization before shuffle
public Deck() {
    initializeDeck(); // Must be implemented
    shuffle();
}

Decision 4: Enums for Suit and Rank

Rationale: Fixed set of values with type safety.

Benefits:

  • Compile-time safety (can't pass invalid suit)
  • Built-in iteration (for (Suit s : Suit.values()))
  • Can add methods (isFaceCard(), getSymbol())

Alternative Considered: String constants (error-prone, no type safety)

Decision 5: Draw Removes Card from Deck

Rationale: Drawing consumes a card (standard card game semantics).

Benefits:

  • Matches real-world behavior
  • Automatically tracks remaining cards
  • Supports discard pile reshuffle
public Card draw() {
    return cards.remove(cards.size() - 1);
}

Decision 6: Fisher-Yates Shuffle Algorithm

Rationale: Efficient O(n) unbiased shuffle.

Benefits:

  • Uniform random permutation
  • In-place (no extra memory)
  • Industry standard

Implementation:

for (int i = n-1; i > 0; i--) {
    int j = random.nextInt(i + 1);
    swap(cards, i, j);
}

Decision 7: Hand Has List of Cards (Composition)

Rationale: Hand contains cards rather than inheriting from collection.

Benefits:

  • Can add game-specific methods (getValue(), isBusted())
  • Encapsulates cards list (defensive copying)
  • Clear semantics (Hand is not a List)

Alternative Considered: Hand extends ArrayList<Card> (exposes unwanted methods like set())

Decision 8: Deal Method Uses Round-Robin

Rationale: Deal one card to each player repeatedly (matches real dealing).

Benefits:

  • Fair distribution
  • Matches physical dealing process
  • Simple loop structure
for (int cardNum = 0; cardNum < cardsPerPlayer; cardNum++) {
    for (Hand hand : hands) {
        hand.addCard(draw());
    }
}

Decision 9: Blackjack Ace Optimization in Hand

Rationale: Ace value (1 or 11) depends on total hand, not individual card.

Benefits:

  • Card remains simple (Ace always returns 1)
  • Hand calculates optimal total
  • Supports multiple Aces correctly

Algorithm: Count Aces as 1, then add 10 for one Ace if total ≤21

Decision 10: Multi-Deck Support via Constructor Parameter

Rationale: Casinos use 6-8 deck shoes for Blackjack.

Benefits:

  • Simple multiplication of single deck
  • Reduces card counting effectiveness (realistic)

We’ll start by defining the basic structures: CardDeck, and Suit.

Basic Components

Suit Enum

public enum Suit {
	HEARTS, DIAMONDS, CLUBS, SPADES;
}

Card Class

A card is compose of suit and a value.

public class Card {

	private final Suit suit;

	private final int value; // Assuming Ace = 1, Jack = 11, Queen = 12, King = 13
	public Card(Suit suit, int value) {
		this.suit = suit;
		this.value = value;
	}

	public Suit getSuit() {
		return suit;
	}

	public int getValue() {
		return value;
	}

	@Override

	public String toString() {
		String[] valueNames = {
			"", "Ace", "2", "3", "4", "5", "6", "7", "8", "9", "10", "Jack", "Queen", "King"
		};
		return valueNames[value] + " of " + suit;
	}
}

Deck Class

A deck has 52 cards.

public class Deck {

	private final List<Card> cards;

	public Deck() {
		cards = new ArrayList<>();

		for (Suit suit: Suit.values()) {
			for (int i = 1; i <= 13; i++) {
				cards.add(new Card(suit, i));
			}
		}

		shuffle();
	}

	public void shuffle() {
		Collections.shuffle(cards);
	}

	public Card draw() {
		if (cards.isEmpty()) {
			throw new IllegalStateException("No more cards in the deck");
		}

		return cards.remove(cards.size() - 1);
	}

	public int remainingCards() {
		return cards.size();
	}
}

Subclassing the deck for specific games

To manage different rules or specialized decks, we can subclass the Deck and possibly Card classes.

Poker Game

For a Poker game, you may not need to subclass Card, but you may want a specialized Deck that defines poker-specific shuffling or dealing rules.

public class PokerDeck extends Deck {

	public PokerDeck() {
		super();
	}

	public List<Card> dealHand(int handSize) {
		List<Card> hand = new ArrayList<>();

		for (int i = 0; i < handSize; i++) {
			hand.add(draw());
		}

		return hand;
	}

	public static void main(String[] args) {
		PokerDeck pokerDeck = new PokerDeck();
		List<Card> hand = pokerDeck.dealHand(5);

		System.out.println("Poker hand:");

		for (Card card: hand) {
			System.out.println(card);
		}
	}
}

Blackjack Game

For Blackjack, you might want to enhance Card to include a more game-specific value computation (like treating an Ace as 1 or 11), and a specialized Deck for dealing.

Blackjack Card Class
public class BlackjackCard extends Card {

	public BlackjackCard(Suit suit, int value) {
		super(suit, value);
	}

	public int getBlackjackValue() {
		int value = getValue();

		if (value > 10) {
			return 10; // Face cards are worth 10
		}

		return value;
	}
}
Blackjack Deck Class
public class BlackjackDeck extends Deck {

	public BlackjackDeck() {
		cards = new ArrayList<>();

		for (Suit suit: Suit.values()) {
			for (int i = 1; i <= 13; i++) {
				cards.add(new BlackjackCard(suit, i));
			}
		}

		shuffle();
	}

	public List<BlackjackCard> dealHand(int handSize) {
		List<BlackjackCard> hand = new ArrayList<>();

		for (int i = 0; i < handSize; i++) {
			hand.add((BlackjackCard) draw()); // Cast to BlackjackCard
		}

		return hand;
	}

	public static void main(String[] args) {
		BlackjackDeck blackjackDeck = new BlackjackDeck();
		List<BlackjackCard> hand = blackjackDeck.dealHand(2);

		System.out.println("Blackjack hand:");

		for (BlackjackCard card: hand) {
			System.out.println(card + " (Value: " + card.getBlackjackValue() + ")");
		}
	}
}

Here is the mermaid diagram of same:

classDiagram
    class Suit {
    }

    class Card {
        +Suit suit
        +int value
        +Card(Suit suit, int value)
        +Suit getSuit()
        +int getValue()
        +String toString()
    }

    class Deck {
        -List~Card~ cards
        +Deck()
        +void shuffle()
        +Card draw()
        +int remainingCards()
    }

    class PokerDeck {
        +List~Card~ dealHand(int handSize)
    }

    class BlackjackCard {
        +BlackjackCard(Suit suit, int value)
        +int getBlackjackValue()
    }

    class BlackjackDeck {
        +List~BlackjackCard~ dealHand(int handSize)
    }
	
	Card o--> Suit
	Deck o--> Card

    Deck <|-- PokerDeck
    Card <|-- BlackjackCard
    Deck <|-- BlackjackDeck

Summary

Here’s how the design achieves the flexibility to handle various card games:

  1. Generic Components: We have generic components like CardDeck, and Suit that can be used universally.
  2. Specialized Classes: By subclassing these components, we tailor the data structures for specific games without altering the base structure.
  3. Extensibility: This design allows easy addition of new games by subclassing and modifying only relevant parts of the functionality.

Comments