Bird
Raised Fist0

Design a Card Game Framework - Deck, Card, Hand, Rules Engine, Multiplayer

LLDmediumInterview freq: medium
Choose your preparation mode4 modes available
๐ŸŽฏ
Design a Card Game Framework
40 min ยท medium
medium

Design a card game framework that supports multiple players, customizable decks and hands, and a rules engine to validate moves and determine winners. The system must allow different card games to be implemented by extending the framework without modifying existing code.

  • Deck contains a collection of Card objects with suits and ranks.
  • Hand represents a player's current cards and supports adding/removing cards.
  • Rules Engine validates moves and determines game state transitions.
  • Supports multiple players playing concurrently with thread-safe state updates.
  • Framework must allow new card games to be added by extending rules and deck configurations.
๐Ÿ“‹
Guided Steps
1/5
Step 1
Design Card and Deck classes
Define Card and Deck classes capturing card properties and deck operations like shuffle and draw.
๐Ÿ’ก Hint
Use enum for suits and ranks to ensure type safety. Deck should hold a List<Card> and provide synchronized draw method for thread safety.
Use enum Suit and Rank for card properties - enums provide compile-time safety and exhaustive switch support. Deck uses private final List<Card> cards and public synchronized Card draw() to atomically remove top card.
Mid: Defines enums for Suit and Rank. Senior: Explains synchronized draw() prevents race conditions where multiple threads draw simultaneously causing inconsistent deck state.
โš ๏ธCommon Wrong Answer
Using String for suits/ranks leads to runtime bugs due to typos and no exhaustive switch checking. Non-synchronized draw() causes race conditions with multiple threads drawing same card.
Step 2
Design Hand class to hold player cards
Design Hand class to represent a player's cards with methods to add and remove cards.
๐Ÿ’ก Hint
Hand maintains a thread-safe collection of cards. Use synchronized methods or concurrent collections to avoid race conditions when multiple threads modify a hand.
Hand uses private final List<Card> cards with public synchronized boolean addCard(Card c) and synchronized boolean removeCard(Card c) to ensure atomic updates to hand state.
Mid: Implements addCard and removeCard methods. Senior: Explains synchronized methods prevent lost updates when multiple threads modify the same hand concurrently.
โš ๏ธCommon Wrong Answer
Using unsynchronized List for cards allows concurrent modification exceptions or lost updates when multiple threads add/remove cards.
Step 3
Design Rules Engine abstraction
Create an abstract RulesEngine class to define game rules and validate moves.
๐Ÿ’ก Hint
Use abstract class instead of interface because RulesEngine holds shared state like current game status. Abstract methods enforce subclasses to implement specific game logic.
abstract class RulesEngine holds protected gameState field and abstract boolean validateMove(Move m). Abstract class used because it holds instance state; interface cannot hold fields in Java.
Mid: Defines abstract validateMove method. Senior: Explains why abstract class is chosen over interface due to shared mutable state in gameState.
โš ๏ธCommon Wrong Answer
Using interface for RulesEngine prevents holding shared game state, forcing external state management and complicating design.
Step 4
Support multiple players with thread-safe game state
Design Player and Game classes to support multiple players and concurrent game play.
๐Ÿ’ก Hint
Game class holds List<Player> and RulesEngine instance. Use volatile for gameState to ensure visibility across threads. Synchronize methods that update shared state.
Game class uses private volatile GameState gameState to ensure visibility of updates across threads. Methods like public synchronized boolean playMove(Player p, Move m) atomically validate and update state.
Mid: Uses synchronized playMove method. Senior: Explains volatile ensures visibility of gameState changes and synchronized prevents race conditions during move validation and state update.
โš ๏ธCommon Wrong Answer
Not using volatile for gameState causes threads to see stale state leading to inconsistent game behavior. Unsynchronized playMove allows race conditions corrupting game state.
Step 5
Enable extensibility for new card games
Design framework to allow adding new card games by extending deck configurations and rules engine without modifying existing code.
๐Ÿ’ก Hint
Apply Open-Closed Principle by using abstract classes and inheritance. Use composition to inject different RulesEngine implementations into Game.
Use abstract class RulesEngine and composition in Game to inject different rules implementations. Deck is configurable via factory methods or subclassing. This enforces Open-Closed Principle by allowing extension without modification.
Mid: Uses inheritance and composition for extensibility. Senior: Explains how abstract class holding state enables subclassing with shared behavior, and composition decouples Game from specific rules implementations.
โš ๏ธCommon Wrong Answer
Hardcoding rules in Game class violates Open-Closed Principle, requiring code changes to add new games and risking regressions.
๐Ÿ”Œ
API Design Challenge
Design an API that allows clients to create different card games by configuring decks, hands, and rules, while supporting concurrent multi-player gameplay.
  • Use abstract classes and interfaces to define extensible components like RulesEngine.
  • Ensure thread safety in methods that modify shared state, such as drawing cards or playing moves.
  • Provide factory or builder patterns to create customized decks and game instances.
๐Ÿ“Š
Scoring Rubric
โ–ผ Show
Must Have - 60%
  • Use enum for Suit and Rank to prevent runtime bugs from invalid values.
  • Deck.draw() method synchronized to prevent race conditions during concurrent draws.
  • Hand.addCard() and removeCard() synchronized to ensure thread-safe modifications.
  • abstract class RulesEngine holds shared mutable state gameState; interface cannot hold fields in Java.
  • Game.gameState declared volatile to ensure visibility across threads.
  • Game.playMove() synchronized to atomically validate and update game state.
Good to Have - 30%
  • Composition relationship between Player and Hand with lifecycle ownership of Hand.
  • Open-Closed Principle enforced by abstract RulesEngine and composition in Game.
  • Factory or builder pattern for Deck customization.
  • Encapsulation of card collections
Bonus - 10%
    โœ…
    Model Answer
    โ–ผ Reveal

    This card game framework in Java supports multiple players, customizable decks and hands, and a rules engine to validate moves and manage game state. It ensures thread safety for concurrent play and allows extensibility for new games by subclassing and composition.

    • Use enum Suit and Rank to ensure compile-time safety and prevent invalid card values.
    • Deck.draw() is synchronized to prevent race conditions when multiple threads draw cards concurrently.
    • Hand methods addCard and removeCard are synchronized to avoid concurrent modification issues.
    • RulesEngine is an abstract class (not interface) to hold shared mutable gameState, enabling subclasses to implement specific rules.
    • Game.gameState is declared volatile to guarantee visibility of updates across threads.
    • Game.playMove() is synchronized to atomically validate moves and update game state, preventing race conditions.
    • Player holds a Hand instance via composition, ensuring lifecycle management.
    • Open-Closed Principle is enforced by allowing new games to extend RulesEngine and Deck configurations without modifying existing code.
    • Factory or builder patterns can be used to create customized Decks for different games, supporting extensibility.
    • Use enum Suit and Rank for card properties to ensure compile-time safety and IDE support.
    • Deck class holds private final List<Card> cards and provides public synchronized Card draw() for atomic card removal.
    • Hand class maintains private final List<Card> cards with synchronized addCard and removeCard methods to prevent concurrent modification issues.
    • abstract class RulesEngine holds shared mutable gameState and defines abstract validateMove method, enabling subclasses to implement specific game logic.
    • Game class uses volatile GameState gameState to guarantee visibility across threads and synchronized playMove method to atomically validate and update game state.
    • Player class represents a participant and holds a Hand instance; composition relationship with lifecycle ownership of Hand.
    • Open-Closed Principle enforced by abstract RulesEngine and composition in Game to inject different rules implementations without modifying existing code.
    • Use of synchronized methods and volatile fields prevents race conditions and visibility issues in concurrent multi-player scenarios.
    • Deck and Hand use encapsulation to protect internal card collections from external modification.
    • Factory or builder patterns can be used to create customized Decks for different games, supporting extensibility.

    Practice

    (1/5)
    1. In designing a Vehicle abstraction for a parking system, should Vehicle be an abstract class or an interface? Consider that vehicles have state like license plate and behavior like park().
    easy
    A. Abstract class, because it can hold shared state and provide default behavior implementations.
    B. Interface, because state can be stored externally and behavior is all that matters.
    C. Interface, because it allows multiple inheritance and defines behavior without state.
    D. Abstract class, because it forces all subclasses to implement all methods explicitly.

    Solution

    1. Step 1: Analyze state and behavior requirements

      Vehicles have shared state (e.g., license plate) and common behavior (e.g., park). Abstract classes can hold state and provide default method implementations.
    2. Step 2: Eliminate interface options

      Interfaces cannot hold state (prior to Java 8 default methods) and rely on external storage for state, which complicates design. Abstract class better encapsulates state and behavior.
    3. Final Answer:

      Option A -> Option A
    4. Quick Check:

      Shared state + default behavior -> abstract class [check]
    Hint: Stateful base class -> abstract class, not interface [check]
    Common Mistakes:
    2. You want to add a new WeekendFeeStrategy to the existing chain of responsibility-based parking fee system. Which change respects the Open-Closed Principle (OCP) and Dependency Inversion Principle (DIP)?
    medium
    A. Modify the existing FeeHandler class to add weekend fee logic inside its handleRequest() method.
    B. Create a subclass WeekendFeeHandler extending FeeHandler and override handleRequest() with weekend logic.
    C. Create a new WeekendFeeHandler implementing the FeeStrategy interface and add it to the chain without changing existing handlers.
    D. Add a boolean flag isWeekend to FeeHandler and branch logic based on it.

    Solution

    1. Step 1: Understand OCP and DIP

      OCP requires extending behavior without modifying existing code; DIP requires depending on abstractions, not concretions.
    2. Step 2: Evaluate options

      Modify the existing FeeHandler class to add weekend fee logic inside its handleRequest() method. modifies existing class, violating OCP; Create a new WeekendFeeHandler implementing the FeeStrategy interface and add it to the chain without changing existing handlers. adds new handler, respecting OCP and DIP; Add a boolean flag isWeekend to FeeHandler and branch logic based on it. adds flag inside existing handler, violating OCP; Create a subclass WeekendFeeHandler extending FeeHandler and override handleRequest() with weekend logic. subclasses but may cause tight coupling and violates DIP if chain depends on concrete classes.
    3. Final Answer:

      Option D -> Option D
    4. Quick Check:

      New handler class added without modifying existing code [check]
    Hint: Add new handler class, don't modify existing ones [check]
    Common Mistakes:
    3. The Wallet System currently calculates fees with a FeeStrategy interface. To add a WeekendFeeStrategy without modifying existing classes, which SOLID principle is being applied and what is the minimum required change?
    medium
    A. Open/Closed Principle: create WeekendFeeStrategy implementing FeeStrategy; no existing class changes.
    B. Dependency Inversion Principle: inject WeekendFeeStrategy into Account class by modifying Account.
    C. Single Responsibility Principle: modify FeeStrategy to handle weekends explicitly.
    D. Liskov Substitution Principle: override FeeStrategy methods in WeekendFeeStrategy and modify Transaction.

    Solution

    1. Step 1: Identify the SOLID principle for extensibility

      Open/Closed Principle (OCP) states software entities should be open for extension but closed for modification.
    2. Step 2: Apply OCP to fee calculation

      Adding WeekendFeeStrategy as a new class implementing FeeStrategy extends behavior without modifying existing classes, preserving OCP.
    3. Final Answer:

      Option A -> Option A
    4. Quick Check:

      New FeeStrategy implementation -> no existing class changes -> OCP [check]
    Hint: Add new strategy class, no existing code change -> OCP [check]
    Common Mistakes:
    4. In a parking lot system, if parkVehicle() is not synchronized, which exact sequence of Thread 1 and Thread 2 causes double-booking of the same spot?
    medium
    A. Thread 1 parks vehicle, Thread 2 parks vehicle immediately after without checking availability
    B. Thread 1 checks spot availability -> true, context switch; Thread 2 checks availability -> true; both park simultaneously
    C. Thread 1 parks vehicle, Thread 2 waits for Thread 1 to finish, then parks another vehicle
    D. Thread 1 and Thread 2 both fail to acquire lock and retry indefinitely

    Solution

    1. Step 1: Identify the check-then-act race condition

      Without synchronization, Thread 1 reads spot as available, then context switches before marking it occupied.
    2. Step 2: Thread 2 also reads spot as available and proceeds to park, causing double-booking

      Both threads believe the spot is free and park concurrently, violating mutual exclusion.
    3. Final Answer:

      Option B -> Option B
    4. Quick Check:

      Check-then-act gap without synchronization -> double-booking [check]
    Hint: Check availability -> context switch -> both park -> double-booking [check]
    Common Mistakes:
    5. ElectricCar needs to use ChargingSpots in the parking lot system. What is the minimum change to the current design to support this without modifying existing classes?
    hard
    A. Add a flag in ParkingLot to treat ElectricCars differently and hardcode charging spot logic.
    B. Modify Vehicle class to add a boolean isElectric field and add logic in ParkingSpot to check it.
    C. Create a new ElectricCar class and a new ChargingSpot class, then modify ParkingLot to handle both explicitly.
    D. Add ELECTRIC_CAR to VehicleType enum, CHARGING to SpotType enum, create ElectricCar subclass, and update SpotCompatibility map externally.

    Solution

    1. Step 1: Identify extensibility without modifying existing classes

      Adding new enum values and subclasses allows extension without changing existing class code, preserving OCP.
    2. Step 2: Update compatibility externally

      Updating SpotCompatibility map outside existing classes avoids modifying their code and prevents class explosion or god-class logic.
    3. Final Answer:

      Option D -> Option D
    4. Quick Check:

      Extend enums + subclass + external compatibility update -> minimal change [check]
    Hint: Extend enums and subclasses, update compatibility externally [check]
    Common Mistakes:
    Recommended

    Ready to prove it?

    Pass the test and this pattern moves unknown strong while earning +1 Grip toward your certificate.

    10 questions~5 mineasy hard