Skip to main content

Overview

The Game class is the central domain model that encapsulates the complete state and business logic of a Blackjack game. It manages the game flow, player and dealer hands, deck state, and determines game outcomes according to standard Blackjack rules.

Class Structure

public final class Game {
    private final GameId id;
    private final PlayerId playerId;
    private final Deck deck;
    private final Hand playerHand;
    private final Hand dealerHand;
    private final GameStatus status;
}

Fields

id
GameId
required
Unique identifier for the game session. Generated automatically using UUID when a new game is created.
playerId
PlayerId
required
Identifier of the player participating in this game. Links the game to a specific player record.
deck
Deck
required
The deck of cards used in this game. Tracks remaining cards after each draw operation. Immutable - each draw returns a new Deck instance.
playerHand
Hand
required
The player’s current hand containing all dealt cards. Updated with each hit action.
dealerHand
Hand
required
The dealer’s current hand containing all dealt cards. Updated automatically when player stands.
status
GameStatus
required
Current state of the game. Possible values:
  • IN_PROGRESS - Game is active and awaiting player action
  • PLAYER_BUST - Player’s hand exceeded 21 (intermediate state)
  • DEALER_BUST - Dealer’s hand exceeded 21 (intermediate state)
  • PLAYER_WINS - Player won the game
  • DEALER_WINS - Dealer won the game
  • PUSH - Tie game (both hands have equal value)

Business Logic

Game Initialization

When a new game is created, the following occurs:
  1. A shuffled 52-card deck is prepared
  2. Four cards are drawn in alternating fashion:
    • Card 1 → Player
    • Card 2 → Dealer
    • Card 3 → Player
    • Card 4 → Dealer
  3. Natural Blackjacks (21 on initial deal) are automatically resolved
public static Game newGame(PlayerId playerId, Deck deck) {
    var d1 = deck.draw(); var c1 = d1.card(); deck = d1.nextDeck();
    var d2 = deck.draw(); var c2 = d2.card(); deck = d2.nextDeck();
    var d3 = deck.draw(); var c3 = d3.card(); deck = d3.nextDeck();
    var d4 = deck.draw(); var c4 = d4.card(); deck = d4.nextDeck();

    Hand player = Hand.empty().add(c1).add(c3);
    Hand dealer = Hand.empty().add(c2).add(c4);

    Game g = new Game(GameId.newId(), playerId, deck, player, dealer, GameStatus.IN_PROGRESS);
    return g.resolveNaturals();
}

Natural Blackjack Resolution

The game automatically checks for natural Blackjacks (21 on initial two cards):
  • Both have Blackjack → Game ends in PUSH (tie)
  • Player has Blackjack → Player wins immediately
  • Dealer has Blackjack → Dealer wins immediately
  • Neither has Blackjack → Game continues normally
private Game resolveNaturals() {
    boolean playerBJ = playerHand.isBlackjack();
    boolean dealerBJ = dealerHand.isBlackjack();
    if (playerBJ && dealerBJ) return withStatus(GameStatus.PUSH);
    if (playerBJ) return withStatus(GameStatus.PLAYER_WINS);
    if (dealerBJ) return withStatus(GameStatus.DEALER_WINS);
    return this;
}

Player Actions

Hit

Draws one additional card for the player:
public Game hit() {
    ensureInProgress();
    var draw = deck.draw();
    Hand nextPlayer = playerHand.add(draw.card());
    Game next = new Game(id, playerId, draw.nextDeck(), nextPlayer, dealerHand, status);
    
    if(nextPlayer.isBust()) return next.withStatus(GameStatus.PLAYER_BUST).finalizeOutcome();
    return next;
}
  • Throws InvalidMoveException if game is not IN_PROGRESS
  • Automatically checks for bust (score > 21)
  • If player busts, game transitions to DEALER_WINS

Stand

Player holds their current hand and triggers dealer play:
public Game stand() {
    ensureInProgress();
    Game afterDealer = dealerPlay();
    return afterDealer.finalizeOutcome();
}
  • Throws InvalidMoveException if game is not IN_PROGRESS
  • Dealer automatically draws cards following standard rules
  • Final outcome is determined by comparing scores

Dealer Logic

The dealer follows standard casino rules:
  • Must hit on any total of 16 or less
  • Must stand on any total of 17 or more
private Game dealerPlay(){
    Deck d = deck;
    Hand dealer = dealerHand;
    
    while (dealer.score() < 17) {
        var draw = d.draw();
        dealer = dealer.add(draw.card());
        d = draw.nextDeck();
    }
    
    Game g = new Game(id, playerId, d, playerHand, dealer, status);
    if (dealer.isBust()) return g.withStatus(GameStatus.DEALER_BUST);
    return g;
}

Outcome Resolution

Final game outcomes are determined by:
  1. Player Bust → Dealer wins
  2. Dealer Bust → Player wins
  3. Player score > Dealer score → Player wins
  4. Dealer score > Player score → Dealer wins
  5. Equal scores → Push (tie)
private Game finalizeOutcome() {
    if (status == GameStatus.PLAYER_BUST)
        return withStatus(GameStatus.DEALER_WINS);
    
    if (status == GameStatus.DEALER_BUST)
        return withStatus(GameStatus.PLAYER_WINS);
    
    int ps = playerHand.score();
    int ds = dealerHand.score();
    
    if (ps > ds) return withStatus(GameStatus.PLAYER_WINS);
    if (ds > ps) return withStatus(GameStatus.DEALER_WINS);
    return withStatus(GameStatus.PUSH);
}

Immutability

The Game class follows an immutable design pattern:
  • All fields are final
  • The class is declared final (cannot be subclassed)
  • All state-changing operations return a new Game instance
  • This ensures thread safety and prevents unintended side effects

Factory Methods

Create New Game

// Create with shuffled standard deck
Game game = Game.newGame(playerId);

// Create with custom deck (for testing)
Game game = Game.newGame(playerId, customDeck);

Rehydrate from Storage

// Reconstruct game from persisted state
Game game = Game.rehydrate(id, playerId, deck, playerHand, dealerHand, status);

Accessor Methods

public GameId id() { return id; }
public PlayerId playerId() { return playerId; }
public Deck deck() { return deck; }
public Hand playerHand() { return playerHand; }
public Hand dealerHand() { return dealerHand; }
public GameStatus status() { return status; }

Validation Rules

  • All fields are non-null (enforced via Objects.requireNonNull())
  • Player actions (hit, stand) can only be performed when status is IN_PROGRESS
  • Attempting actions on completed games throws InvalidMoveException

Example Usage

// Start a new game
PlayerId playerId = PlayerId.newId();
Game game = Game.newGame(playerId);

// Player hits
game = game.hit();

// Check if still in progress
if (game.status() == GameStatus.IN_PROGRESS) {
    // Player stands
    game = game.stand();
}

// Check final outcome
if (game.status() == GameStatus.PLAYER_WINS) {
    System.out.println("Player wins!");
}
  • GameStatus - Enumeration of possible game states (documented above)
  • Hand - Represents a collection of cards with scoring logic
  • Card - Individual playing card
  • Player - Player profile and statistics
  • Deck - Card deck management

Build docs developers (and LLMs) love