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
Unique identifier for the player. Generated automatically as a UUID when creating a new player. Cannot be null.
The player’s display name. Must be non-null, non-blank, and between 1-30 characters after trimming whitespace.
Total number of games won by the player. Must be non-negative. Defaults to 0 for new players.
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
- Thread Safety - Multiple threads can safely read Player objects
- Predictability - Objects cannot change unexpectedly
- Easier Testing - No hidden state mutations to track
- 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)