Design a data structure for generic deck of cards
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
Cardimmutable (suit + rank never change) - Separate generic deck/hand behavior from game rules
- Let games subclass
Card,Deck, andHandwhen needed - Support round‑robin dealing and optional multi‑deck shoes
2. System Requirements
Functional Requirements:
- Create a standard 52-card deck with 4 suits and 13 ranks
- Support shuffling using randomization algorithms
- Draw individual cards from deck (remove and return)
- Deal specified number of cards to one or more players
- Track remaining cards in deck
- Support game-specific card value calculations (Blackjack, Poker, etc.)
- Represent cards as immutable value objects (suit and rank never change)
- Support hand abstraction to hold player's cards
- Enable deck extension for non-standard decks (Uno, Tarot, etc.)
- Handle empty deck scenario (exception or reshuffle)
- Support discard pile management
- Allow multi-deck scenarios (6-deck shoe for Blackjack)
- Display card representation (e.g., "Ace of Spades", "10♥")
Non-Functional Requirements:
- Immutability: Cards must be immutable to prevent accidental modification
- Extensibility: Adding new games requires only subclassing, no base class modification
- Performance: Shuffle operation O(n), draw operation O(1)
- Type Safety: Compile-time guarantees for game-specific card types
- 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:
Deckconstructor calls abstractinitializeDeck()- 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()returnsHandBlackjackDeck.createHand()returnsBlackjackHand
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:
SuitandRankare 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: Card, Deck, 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:
- Generic Components: We have generic components like
Card,Deck, andSuitthat can be used universally. - Specialized Classes: By subclassing these components, we tailor the data structures for specific games without altering the base structure.
- Extensibility: This design allows easy addition of new games by subclassing and modifying only relevant parts of the functionality.