Overview
The Hand class represents a collection of cards held by either the player or dealer in a Blackjack game. It encapsulates the core Blackjack scoring rules, including flexible Ace valuation (11 or 1) and special conditions like Blackjack and bust detection.
Class Structure
public class Hand {
private final List<Card> cards;
private Hand(List<Card> cards) {
this.cards = List.copyOf(cards);
}
}
Fields
Immutable list of cards in the hand. Protected through defensive copying - the internal list cannot be modified from outside the class.
Factory Methods
Create Empty Hand
public static Hand empty() {
return new Hand(List.of());
}
Creates a new hand with no cards. Used as the starting point when dealing cards.
Example:
Hand playerHand = Hand.empty();
Create from Card List
public static Hand fromCards(List<Card> cards) {
return new Hand(cards);
}
Creates a hand from an existing list of cards. Useful for rehydrating hands from storage.
Example:
List<Card> savedCards = Arrays.asList(
new Card(Rank.ACE, Suit.SPADES),
new Card(Rank.KING, Suit.HEARTS)
);
Hand restoredHand = Hand.fromCards(savedCards);
Methods
Add Card
Adds a single card to the hand:
public Hand add(Card card) {
var next = new ArrayList<>(cards);
next.add(card);
return new Hand(next);
}
Follows immutable design - returns a new Hand instance with the card added.
Example:
Hand hand = Hand.empty();
hand = hand.add(new Card(Rank.TEN, Suit.CLUBS));
hand = hand.add(new Card(Rank.SEVEN, Suit.DIAMONDS));
// hand now contains 2 cards with score of 17
Get Cards
public List<Card> cards() {
return List.copyOf(cards);
}
Returns an immutable copy of the cards in the hand. Safe to use without risk of external modification.
Example:
Hand hand = Hand.empty()
.add(new Card(Rank.QUEEN, Suit.HEARTS))
.add(new Card(Rank.FIVE, Suit.SPADES));
List<Card> cards = hand.cards();
System.out.println(cards.size()); // 2
Scoring Logic
The heart of the Hand class is its Blackjack scoring algorithm:
public int score() {
int total = 0;
int aces = 0;
for (Card c: cards) {
total += c.rank().getDefaultValue();
if (c.rank().isAce()) aces++;
}
while (total > 21 && aces > 0) {
total -= 10;
aces--;
}
return total;
}
Scoring Rules
-
Initial Calculation
- Sum all card values using their default values
- Count the number of Aces (initially valued at 11)
-
Ace Adjustment
- If total exceeds 21 and Aces are present
- Convert Aces from 11 to 1 (subtract 10 per Ace)
- Continue until score is ≤ 21 or all Aces are converted
-
Final Score
- Return the optimized total
Scoring Examples
Example 1: Simple Hand
Hand hand = Hand.empty()
.add(new Card(Rank.TEN, Suit.HEARTS))
.add(new Card(Rank.SEVEN, Suit.CLUBS));
System.out.println(hand.score()); // 17
Example 2: Blackjack (Natural 21)
Hand hand = Hand.empty()
.add(new Card(Rank.ACE, Suit.SPADES))
.add(new Card(Rank.KING, Suit.DIAMONDS));
System.out.println(hand.score()); // 21 (Ace as 11 + King as 10)
System.out.println(hand.isBlackjack()); // true
Example 3: Soft Hand (Ace as 11)
Hand hand = Hand.empty()
.add(new Card(Rank.ACE, Suit.HEARTS))
.add(new Card(Rank.SIX, Suit.CLUBS));
System.out.println(hand.score()); // 17 (Ace as 11 + 6)
// Called "soft 17" because Ace can still convert to 1 if another card is drawn
Example 4: Ace Adjustment (Ace as 1)
Hand hand = Hand.empty()
.add(new Card(Rank.ACE, Suit.DIAMONDS))
.add(new Card(Rank.NINE, Suit.HEARTS))
.add(new Card(Rank.EIGHT, Suit.SPADES));
System.out.println(hand.score()); // 18
// Initial: 11 + 9 + 8 = 28 (bust!)
// Adjusted: 1 + 9 + 8 = 18 (Ace converted to 1)
Example 5: Multiple Aces
Hand hand = Hand.empty()
.add(new Card(Rank.ACE, Suit.CLUBS))
.add(new Card(Rank.ACE, Suit.DIAMONDS))
.add(new Card(Rank.NINE, Suit.HEARTS));
System.out.println(hand.score()); // 21
// Initial: 11 + 11 + 9 = 31
// After 1st adjustment: 1 + 11 + 9 = 21
// One Ace as 1, one Ace as 11
Example 6: Bust
Hand hand = Hand.empty()
.add(new Card(Rank.KING, Suit.SPADES))
.add(new Card(Rank.QUEEN, Suit.HEARTS))
.add(new Card(Rank.FIVE, Suit.CLUBS));
System.out.println(hand.score()); // 25
System.out.println(hand.isBust()); // true
Special Conditions
Blackjack Detection
public boolean isBlackjack() {
return cards.size() == 2 && score() == 21;
}
Returns true if the hand is a natural Blackjack:
- Exactly 2 cards
- Total score of 21
Typical Blackjack combinations:
- Ace + Ten/Jack/Queen/King
Example:
Hand blackjack = Hand.empty()
.add(new Card(Rank.ACE, Suit.CLUBS))
.add(new Card(Rank.JACK, Suit.SPADES));
System.out.println(blackjack.isBlackjack()); // true
Hand not21With2Cards = Hand.empty()
.add(new Card(Rank.TEN, Suit.HEARTS))
.add(new Card(Rank.NINE, Suit.DIAMONDS));
System.out.println(not21With2Cards.isBlackjack()); // false (only 19)
Hand threeCardTwentyOne = Hand.empty()
.add(new Card(Rank.SEVEN, Suit.CLUBS))
.add(new Card(Rank.SEVEN, Suit.DIAMONDS))
.add(new Card(Rank.SEVEN, Suit.HEARTS));
System.out.println(threeCardTwentyOne.isBlackjack()); // false (3 cards)
Bust Detection
public boolean isBust() {
return score() > 21;
}
Returns true if the hand total exceeds 21 (after Ace adjustments).
Example:
Hand bustHand = Hand.empty()
.add(new Card(Rank.KING, Suit.HEARTS))
.add(new Card(Rank.QUEEN, Suit.CLUBS))
.add(new Card(Rank.TEN, Suit.DIAMONDS));
System.out.println(bustHand.score()); // 30
System.out.println(bustHand.isBust()); // true
Immutability
The Hand class follows immutable design principles:
- The
cards field is final
- All methods return new instances rather than modifying state
- Defensive copying prevents external modification
Immutability Example
Hand original = Hand.empty()
.add(new Card(Rank.FIVE, Suit.HEARTS));
Hand modified = original.add(new Card(Rank.NINE, Suit.CLUBS));
System.out.println(original.cards().size()); // 1 (unchanged)
System.out.println(modified.cards().size()); // 2 (new instance)
Usage in Game Logic
Player Hit
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;
}
Dealer Play
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;
}
Natural Blackjack Check
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;
}
Complete Example
// Initial deal
Hand playerHand = Hand.empty()
.add(new Card(Rank.SEVEN, Suit.HEARTS))
.add(new Card(Rank.NINE, Suit.CLUBS));
System.out.println("Initial hand: " + playerHand.score()); // 16
// Player decides to hit
playerHand = playerHand.add(new Card(Rank.FOUR, Suit.DIAMONDS));
System.out.println("After hit: " + playerHand.score()); // 20
if (!playerHand.isBust()) {
System.out.println("Still in the game!");
}
// Check for winning conditions
if (playerHand.score() == 21 && playerHand.isBlackjack()) {
System.out.println("Blackjack!");
} else if (playerHand.score() == 21) {
System.out.println("Twenty-one!");
}
// Display all cards
for (Card card : playerHand.cards()) {
System.out.println(card.rank() + " of " + card.suit());
}
- Card - Individual playing card in the hand
- Game - Uses Hand for both player and dealer
- Rank - Determines card values for scoring (see Card documentation)
- Deck - Source of cards added to hands