Skip to main content

Overview

The Player class represents a player’s profile in the Blackjack system. It tracks the player’s identity, name, and cumulative game statistics including wins and losses. The model enforces business rules around player data and provides methods for updating player state.

Class Structure

public final class Player {
    private final PlayerId id;
    private final PlayerName name;
    private final int wins;
    private final int losses;
}

Fields

id
PlayerId
required
Unique identifier for the player. Generated automatically as a UUID when creating a new player. Cannot be null.
name
PlayerName
required
The player’s display name. Must be non-null, non-blank, and between 1-30 characters after trimming whitespace.
wins
int
required
Total number of games won by the player. Must be non-negative. Defaults to 0 for new players.
losses
int
required
Total number of games lost by the player. Must be non-negative. Defaults to 0 for new players.

Constructor

The main constructor enforces validation rules:
public Player(PlayerId id, PlayerName name, int wins, int losses) {
    this.id = Objects.requireNonNull(id, "id");
    this.name = Objects.requireNonNull(name, "name");
    if (wins < 0 || losses < 0) 
        throw new IllegalArgumentException("wins/losses cannot be negative");
    this.wins = wins;
    this.losses = losses;
}

Validation Rules

  • id must not be null
  • name must not be null
  • wins must be >= 0
  • losses must be >= 0
  • Throws IllegalArgumentException if validation fails

Factory Method

Create New Player

The recommended way to create a new player:
public static Player newPlayer(PlayerName name) {
    return new Player(PlayerId.newId(), name, 0, 0);
}
Example:
PlayerName name = new PlayerName("Alice");
Player player = Player.newPlayer(name);
This factory method:
  • Generates a new unique PlayerId
  • Initializes statistics to zero (0 wins, 0 losses)
  • Returns a ready-to-use Player instance

State Modification Methods

The Player class follows an immutable design pattern. All modification methods return a new Player instance rather than mutating the existing object.

Rename Player

Update the player’s display name:
public Player rename(PlayerName newName) {
    return new Player(id, newName, wins, losses);
}
Example:
Player player = Player.newPlayer(new PlayerName("Bob"));
Player renamed = player.rename(new PlayerName("Robert"));
// Original 'player' is unchanged, 'renamed' has new name

Register Win

Increment the win count:
public Player registerWin() {
    return new Player(id, name, wins + 1, losses);
}
Example:
Player player = Player.newPlayer(new PlayerName("Charlie"));
Player afterWin = player.registerWin();
// afterWin.wins() == 1, player.wins() == 0

Register Loss

Increment the loss count:
public Player registerLoss() {
    return new Player(id, name, wins, losses + 1);
}
Example:
Player player = Player.newPlayer(new PlayerName("Diana"));
Player afterLoss = player.registerLoss();
// afterLoss.losses() == 1, player.losses() == 0

Accessor Methods

public PlayerId id() { return id; }
public PlayerName name() { return name; }
public int wins() { return wins; }
public int losses() { return losses; }

Computed Property

Score Calculation

The player’s score is calculated as the difference between wins and losses:
public int score() { 
    return wins - losses; 
}
Examples:
  • 10 wins, 5 losses → score = 5
  • 3 wins, 8 losses → score = -5
  • 7 wins, 7 losses → score = 0
This metric provides a quick assessment of overall player performance.

Immutability

The Player class is designed to be immutable:
  • All fields are final
  • The class is declared final (cannot be subclassed)
  • No setter methods exist
  • All modification operations return new instances
  • Thread-safe by design

Benefits

  1. Thread Safety - Multiple threads can safely read Player objects
  2. Predictability - Objects cannot change unexpectedly
  3. Easier Testing - No hidden state mutations to track
  4. Event Sourcing - Perfect for recording state changes over time

Value Objects

PlayerId

Wrapper for player identifier:
public record PlayerId(String value) {
    public PlayerId {
        if (value == null || value.isBlank()) 
            throw new IllegalArgumentException("PlayerId cannot be blank");
    }
    public static PlayerId newId() { 
        return new PlayerId(UUID.randomUUID().toString()); 
    }
}
  • Stored as UUID string
  • Cannot be null or blank
  • Provides type safety over plain String

PlayerName

Wrapper for player name with validation:
public record PlayerName(String value) {
    public PlayerName {
        if (value == null) 
            throw new IllegalArgumentException("PlayerName cannot be null");
        String v = value.trim();
        if (v.isEmpty()) 
            throw new IllegalArgumentException("PlayerName cannot be blank");
        if (v.length() > 30) 
            throw new IllegalArgumentException("PlayerName too long (max 30)");
        value = v;
    }
}
Validation rules:
  • Cannot be null
  • Cannot be blank after trimming
  • Maximum 30 characters after trimming
  • Whitespace is automatically trimmed

Complete Example

// Create new player
PlayerName name = new PlayerName("Emma");
Player player = Player.newPlayer(name);
System.out.println(player.score()); // 0

// Play some games
player = player.registerWin();
player = player.registerWin();
player = player.registerLoss();

System.out.println(player.wins());   // 2
System.out.println(player.losses()); // 1
System.out.println(player.score());  // 1

// Rename player
player = player.rename(new PlayerName("Emily"));
System.out.println(player.name().value()); // "Emily"

// Stats are preserved after rename
System.out.println(player.score()); // Still 1

Usage in Game Flow

// 1. Player registration
Player player = Player.newPlayer(new PlayerName("Frank"));
playerRepository.save(player);

// 2. Start game
Game game = Game.newGame(player.id());

// 3. Play game...
game = game.hit();
game = game.stand();

// 4. Update player stats based on outcome
if (game.status() == GameStatus.PLAYER_WINS) {
    player = player.registerWin();
} else if (game.status() == GameStatus.DEALER_WINS) {
    player = player.registerLoss();
}
// Note: PUSH (tie) doesn't update statistics

// 5. Persist updated player
playerRepository.save(player);
  • Game - References Player via PlayerId
  • PlayerId - Unique identifier value object (documented above)
  • PlayerName - Name value object with validation (documented above)

Build docs developers (and LLMs) love