Chaturmind
LearnDSASystem DesignInterview PrepDevOpsEngineering GrowthBlog
Start learning
Chaturmind

Structured learning paths for engineers who want to go deep. Written by practitioners.

Learn

  • Java
  • DSA
  • System Design
  • Spring Boot
  • AI / ML
  • DevOps
  • Engineering Growth
  • Java Interview Prep

Company

  • About
  • Blog
  • Contact

Legal

  • Privacy Policy
  • Terms of Service

© 2026 Chaturmind. All rights reserved.

👥10,106visitors

Built for engineers who want to go deep.


← Low-Level Design & Design Patterns

OOP Fundamentals & SOLID Principles

  • Object-Oriented Design Refresher
  • Single Responsibility & Open/Closed
  • Liskov Substitution & Interface Segregation
  • Dependency Inversion

Creational Design Patterns

  • Singleton Pattern
  • Factory & Abstract Factory
  • Builder Pattern
  • Prototype Pattern

Structural Design Patterns

  • Adapter & Facade
  • Decorator Pattern
  • Composite & Proxy
  • Bridge & Flyweight

Behavioral Design Patterns

  • Strategy Pattern
  • Observer Pattern
  • Command Pattern
  • Template Method
  • State Pattern
  • Chain of Responsibility
  • Iterator & Mediator
  • Visitor Pattern

Machine Coding Exercises

  • Parking Lot — Requirements & Class Design
  • Parking Lot — Implementation
  • Elevator System — Requirements & Class Design
  • Elevator System — Implementation & Scheduling
  • Library Management System
  • Tic-Tac-Toe / Board Game Design
  • Chess Engine Design
  • Vending Machine Design
  • ATM System Design
  • In-Memory Rate Limiter
  • Thread-Safe LRU Cache
  • Connection Pool Design
  • Producer-Consumer Class Design
  • Movie Ticket Booking System
  • Hotel Reservation System
  • Airline Booking / Seat Selection System
  • Splitwise / Expense Sharing System
  • Design a Logging Framework
  • Design a Notification/Observer-Based Pub-Sub
  • Design a Cache with Pluggable Eviction Policy
  • Design Snake and Ladder
  • Design a Card Game Framework
  • Design an Inventory Management System
  • Restaurant Management System
  • Food Delivery Order Matching
  • Design a Shopping Cart & Checkout Flow
  • Design a Recommendation Engine
  • Design a Job Scheduler
  • Design a Circuit Breaker
  • Design a Distributed ID Generator
  • Design a Pub-Sub Message Broker
  • Design Twitter/X
  • Multi-Pattern Problem: Design a Ride Booking Class Model
  • Multi-Pattern Problem: Design a Food Ordering Class Model
  • Design an Authentication/Authorization Module
  • Design a Form/Survey Builder
  • Design a Meeting Room / Calendar Booking System
  • Design a Search/Filter Engine for an E-Commerce Catalog
  • Design a Voting/Polling System
  • Design a Retry Mechanism with Backoff
  • Design a Health Check Aggregator
  • Design a Feature Flag System
  • Design a Config Management Client
  • Design a Leader Election Algorithm
  • Design a Distributed Counter
  • Design a Bloom Filter
  • Design a Consistent Hashing Ring
  • Design a Distributed Tracing Library
  • Design a Metrics Collection Library
  • Design an Audit Logging Framework
  • Design a Plugin/Extension System
  • Design a Workflow Engine
  • Multi-Pattern Capstone: Design a Food Delivery App
Chaturmind
← Low-Level Design & Design Patterns

OOP Fundamentals & SOLID Principles

  • Object-Oriented Design Refresher
  • Single Responsibility & Open/Closed
  • Liskov Substitution & Interface Segregation
  • Dependency Inversion

Creational Design Patterns

  • Singleton Pattern
  • Factory & Abstract Factory
  • Builder Pattern
  • Prototype Pattern

Structural Design Patterns

  • Adapter & Facade
  • Decorator Pattern
  • Composite & Proxy
  • Bridge & Flyweight

Behavioral Design Patterns

  • Strategy Pattern
  • Observer Pattern
  • Command Pattern
  • Template Method
  • State Pattern
  • Chain of Responsibility
  • Iterator & Mediator
  • Visitor Pattern

Machine Coding Exercises

  • Parking Lot — Requirements & Class Design
  • Parking Lot — Implementation
  • Elevator System — Requirements & Class Design
  • Elevator System — Implementation & Scheduling
  • Library Management System
  • Tic-Tac-Toe / Board Game Design
  • Chess Engine Design
  • Vending Machine Design
  • ATM System Design
  • In-Memory Rate Limiter
  • Thread-Safe LRU Cache
  • Connection Pool Design
  • Producer-Consumer Class Design
  • Movie Ticket Booking System
  • Hotel Reservation System
  • Airline Booking / Seat Selection System
  • Splitwise / Expense Sharing System
  • Design a Logging Framework
  • Design a Notification/Observer-Based Pub-Sub
  • Design a Cache with Pluggable Eviction Policy
  • Design Snake and Ladder
  • Design a Card Game Framework
  • Design an Inventory Management System
  • Restaurant Management System
  • Food Delivery Order Matching
  • Design a Shopping Cart & Checkout Flow
  • Design a Recommendation Engine
  • Design a Job Scheduler
  • Design a Circuit Breaker
  • Design a Distributed ID Generator
  • Design a Pub-Sub Message Broker
  • Design Twitter/X
  • Multi-Pattern Problem: Design a Ride Booking Class Model
  • Multi-Pattern Problem: Design a Food Ordering Class Model
  • Design an Authentication/Authorization Module
  • Design a Form/Survey Builder
  • Design a Meeting Room / Calendar Booking System
  • Design a Search/Filter Engine for an E-Commerce Catalog
  • Design a Voting/Polling System
  • Design a Retry Mechanism with Backoff
  • Design a Health Check Aggregator
  • Design a Feature Flag System
  • Design a Config Management Client
  • Design a Leader Election Algorithm
  • Design a Distributed Counter
  • Design a Bloom Filter
  • Design a Consistent Hashing Ring
  • Design a Distributed Tracing Library
  • Design a Metrics Collection Library
  • Design an Audit Logging Framework
  • Design a Plugin/Extension System
  • Design a Workflow Engine
  • Multi-Pattern Capstone: Design a Food Delivery App
HomeLearnSystem DesignLow-Level Design & Design PatternsMachine Coding Exercises
✓ FreeIntermediate· 8 min read

Design a Card Game Framework

Deck/Card/Player/Dealer/GameRules classes, shuffling and dealing as discrete testable methods, and GameRules as an injected strategy for supporting multiple card games.

Published September 23, 2026


Design a Card Game Framework

Core classes

enum Suit { HEARTS, DIAMONDS, CLUBS, SPADES }
enum Rank { TWO, THREE, /* ... */ KING, ACE }
class Card { Suit suit; Rank rank; }

class Deck {
    private final List<Card> cards = new ArrayList<>();
    Deck() { for (Suit s : Suit.values()) for (Rank r : Rank.values()) cards.add(new Card(s, r)); } // standard 52-card build
    void shuffle() { Collections.shuffle(cards); }
    Card dealOne() { return cards.remove(cards.size() - 1); } // deal from the 'top' (end of list)
}

class Player { String name; List<Card> hand = new ArrayList<>(); }
class Dealer {
    Deck deck;
    void dealHands(List<Player> players, int cardsPerPlayer) {
        for (int i = 0; i < cardsPerPlayer; i++) {
            for (Player p : players) p.hand.add(deck.dealOne());
        }
    }
}

Advertisement

Shuffling and dealing as discrete, testable methods

Separating shuffle() and dealOne() into their own single-purpose methods (rather than one monolithic "setup the game" method) is what makes each independently testable: dealOne() can be unit-tested by asserting the deck shrinks by exactly one card and the returned card is no longer in the deck, with zero dependency on randomness — shuffle()'s randomness can be tested separately (e.g. asserting the deck still contains all 52 unique cards after shuffling, without asserting a specific order). Bundling shuffle-and-deal into one method would force every test of dealing logic to also account for shuffle's randomness, an avoidable coupling.

GameRules as an injected strategy

interface GameRules {
    int cardsPerPlayer();
    boolean isValidPlay(Card card, List<Card> currentHand, GameState state);
    Player determineWinner(List<Player> players, GameState state);
}

class PokerRules implements GameRules { /* 5 (or 2, for Hold'em) cards per player, hand-ranking win logic */ }
class BlackjackRules implements GameRules { /* 2 cards per player, closest-to-21-without-busting win logic */ }

class CardGame {
    private final Dealer dealer;
    private final GameRules rules; // injected — CardGame has zero knowledge of poker vs blackjack specifics

    void start(List<Player> players) {
        dealer.dealHands(players, rules.cardsPerPlayer());
    }
}

This is Strategy pattern applied to "what game is actually being played" — Deck, Card, Dealer, and CardGame's own orchestration logic are entirely game-agnostic; every game-specific rule (how many cards to deal, what counts as a valid play, how a winner is determined) lives behind the GameRules interface. Supporting a new card game means writing one new GameRules implementation, with zero changes to the shared framework — directly reinforcing the same pattern-recognition skill Strategy Pattern and Tic-Tac-Toe / Board Game Design's WinningStrategy build.

Shuffling correctly and testably

A card game framework lives or dies on the fairness of its shuffle. The correct algorithm is Fisher–Yates: walk the deck from the end, and swap each position with a uniformly random position at or before it:

final class Deck {
    private final List<Card> cards;
    private final RandomGenerator random;                       // injected: seeded in tests, secure in production

    Deck(List<Card> cards, RandomGenerator random) { this.cards = new ArrayList<>(cards); this.random = random; }

    static Deck standard(RandomGenerator random) {
        List<Card> all = new ArrayList<>(52);
        for (Suit s : Suit.values()) for (Rank r : Rank.values()) all.add(new Card(s, r));
        return new Deck(all, random);
    }

    void shuffle() {
        for (int i = cards.size() - 1; i > 0; i--) {
            int j = random.nextInt(i + 1);                      // 0..i inclusive
            Collections.swap(cards, i, j);
        }
    }

    Card draw() {
        if (cards.isEmpty()) throw new IllegalStateException("Deck is empty");
        return cards.remove(cards.size() - 1);                  // O(1) from the end of an ArrayList
    }

    int remaining() { return cards.size(); }
}
  • The common bug is random.nextInt(cards.size()) for every position (swapping with any card). It looks random, but produces some orderings more often than others. Fisher–Yates produces every one of the 52! orderings with equal probability. Collections.shuffle(list, random) implements the same algorithm.
  • Inject the randomness source. Tests pass a seeded generator (new SplittableRandom(42)) to get reproducible deals and verify game logic. A real-money game uses SecureRandom, because a predictable generator lets players reconstruct the deck.
  • Keep Card an immutable value (a record Card(Suit suit, Rank rank)), so cards can be shared and compared safely.

Evaluating hands: a comparable value object

Most card games need "which hand wins?". Model the evaluation result as a comparable value, so the game logic is just max():

enum Category { HIGH_CARD, PAIR, TWO_PAIR, THREE_OF_A_KIND, STRAIGHT, FLUSH, FULL_HOUSE, FOUR_OF_A_KIND, STRAIGHT_FLUSH }

record HandValue(Category category, List<Integer> tiebreakers) implements Comparable<HandValue> {
    public int compareTo(HandValue o) {
        int c = category.compareTo(o.category);
        if (c != 0) return c;
        for (int i = 0; i < Math.min(tiebreakers.size(), o.tiebreakers.size()); i++) {
            c = Integer.compare(tiebreakers.get(i), o.tiebreakers.get(i));
            if (c != 0) return c;
        }
        return 0;                                              // a genuine tie: split the pot
    }
}

final class PokerHandEvaluator {
    HandValue evaluate(List<Card> five) {
        Map<Rank, Long> counts = five.stream().collect(Collectors.groupingBy(Card::rank, Collectors.counting()));
        boolean flush = five.stream().map(Card::suit).distinct().count() == 1;
        List<Integer> ranks = five.stream().map(c -> c.rank().ordinal()).sorted(Comparator.reverseOrder()).toList();
        boolean straight = counts.size() == 5 && ranks.get(0) - ranks.get(4) == 4;   // (ace-low straight omitted for brevity)

        // group ranks by (count desc, rank desc): e.g. full house → [trips rank, pair rank]
        List<Integer> byGroup = counts.entrySet().stream()
                .sorted(Map.Entry.<Rank, Long>comparingByValue().reversed()
                        .thenComparing(Map.Entry.comparingByKey(Comparator.reverseOrder())))
                .map(e -> e.getKey().ordinal()).toList();
        List<Long> shape = counts.values().stream().sorted(Comparator.reverseOrder()).toList();

        if (straight && flush)           return new HandValue(Category.STRAIGHT_FLUSH, List.of(ranks.get(0)));
        if (shape.equals(List.of(4L, 1L))) return new HandValue(Category.FOUR_OF_A_KIND, byGroup);
        if (shape.equals(List.of(3L, 2L))) return new HandValue(Category.FULL_HOUSE, byGroup);
        if (flush)                       return new HandValue(Category.FLUSH, ranks);
        if (straight)                    return new HandValue(Category.STRAIGHT, List.of(ranks.get(0)));
        if (shape.get(0) == 3)           return new HandValue(Category.THREE_OF_A_KIND, byGroup);
        if (shape.equals(List.of(2L, 2L, 1L))) return new HandValue(Category.TWO_PAIR, byGroup);
        if (shape.get(0) == 2)           return new HandValue(Category.PAIR, byGroup);
        return new HandValue(Category.HIGH_CARD, ranks);
    }
}

The evaluator is game-specific, so it belongs behind GameRules (Poker supplies this one, and Blackjack supplies a points calculator where aces count as 1 or 11). The framework's CardGame only ever asks the rules object "who won?".

A turn loop that works for many games

final class CardGame {
    private final GameRules rules; private final Deck deck; private final List<Player> players;

    GameResult play() {
        deck.shuffle();
        rules.deal(deck, players);                               // how many cards, face up or down
        while (!rules.isOver(players, deck)) {
            for (Player p : players) {
                if (!rules.isActive(p)) continue;                 // folded, bust, already finished
                Action a = p.chooseAction(rules.legalActions(p));  // a human UI or a bot strategy
                rules.apply(p, a, deck);
                if (rules.isOver(players, deck)) break;
            }
        }
        return rules.result(players);
    }
}

legalActions is the key extension point. The framework never hard-codes "hit/stand" or "call/raise/fold", so a new game is a new GameRules implementation, and players (human or bot) choose only among actions the rules allow.

Follow-up questions this topic invites — and their answers

Q: Why deal from the end of the list (cards.remove(cards.size() - 1)) rather than the front? A: Removing from the end of an ArrayList is O(1) (no shifting of remaining elements); removing from the front is O(n) (every remaining card shifts down one position) — a minor but easy, free optimization once you're aware ArrayList's removal cost depends on position.

Q: How would isValidPlay() differ meaningfully between Poker and a game like Rummy? A: Poker's validity is mostly about betting/turn structure rather than card legality (any card in hand can generally be played per the betting round); Rummy's validity depends on forming actual valid melds/sequences from the hand — this difference is exactly what's isolated behind the GameRules interface, so CardGame's orchestration code never needs to know which validity model applies.

Q: Should Deck support more than one standard 52-card deck (e.g., games using multiple decks, or jokers)? A: Worth surfacing as a scoping question early (per the Object-Oriented Design Refresher's scoping guidance) — a small change to Deck's constructor (accepting a deck count, optionally including jokers as a configurable extra) accommodates this without restructuring the class, as long as it's anticipated rather than hard-coded to exactly 52 cards.

Q: Does GameState need to be its own class, or could game-specific state just live on Player/CardGame directly? A: A separate GameState class (holding things like the current pot in Poker, or the discard pile in Rummy) keeps game-specific mutable state cleanly separated from the game-agnostic Player/Dealer/Deck classes — bolting Poker-specific fields directly onto Player would leak game-specific concerns into a class meant to stay reusable across games.

Q: Why is swap(i, random(0..n-1)) for every i a biased shuffle? A: It makes nⁿ equally likely sequences of swaps, but there are n! orderings, and nⁿ isn't divisible by n!, so some orderings must occur more often than others. Fisher–Yates chooses from a shrinking range (0..i), producing exactly n! equally likely outcomes.

Q: Why inject the random generator into the deck? A: Tests can use a seeded generator to reproduce specific deals and verify rules deterministically, while production can use a cryptographically secure generator, so players can't predict upcoming cards.

Q: How do you compare two poker hands cleanly? A: Evaluate each hand into a comparable value, a category plus an ordered list of tie-breaking ranks, and compare those values. Comparing by category first, then tie-breakers in order, handles every case, including exact ties.

Advertisement

Previous

Design Snake and Ladder

Next

Design an Inventory Management System

AI Tutor

Lesson: Design a Card Game Framework

Quick actions

AI responses can be inaccurate. Verify critical information.