LESSON 0002 · The domain · Blackjack Ensemble course
Lesson 0002 — Reading the rules in code
The domain is where Blackjack's rules live as plain Java — no Spring, no HTTP. You'll read the three classes at its heart and understand the one genuinely clever method: Hand.value() and its soft-ace trick.
Hand fluently — the class every game rule leans on.
Rank — an enum that knows its valueRank pairs each card rank with its point value and display string:
public enum Rank {
ACE(1, "A"), TWO(2, "2"), ... TEN(10, "10"),
JACK(10, "J"), QUEEN(10, "Q"), KING(10, "K");
public int value() { return value; }
public String display() { return display; }
}
Two things to notice: face cards (J/Q/K) all carry value 10, and ACE carries a base value of 1. The “sometimes 11” logic is not here — it lives in Hand, because whether an Ace counts as 11 depends on the whole hand.
Card — rank + suit, and a mutable facepublic class Card {
private final Suit suit;
private final Rank rank;
private Face face = Face.UP; // UP or DOWN — the dealer's hole card is DOWN
public int rankValue() { return rank.value(); }
public void flip() { ... } // toggles UP/DOWN
}
suit and rank are final (a card's identity never changes), but face is mutable because a card gets turned over during play. Card also defines equals/hashCode on suit + rank — so two ACE of HEARTS are equal regardless of face. That's why tests can write new Card(Suit.HEARTS, Rank.ACE) and compare.
Hand.value() — the soft-ace trickHere is the actual method, verbatim:
public int value() {
int handValue = cards.stream()
.mapToInt(Card::rankValue) // Ace counts as 1 here
.sum();
boolean hasAce = cards.stream()
.anyMatch(card -> card.rank() == Rank.ACE);
// promote ONE ace from 1 → 11 (add 10) only if it won't bust
if (hasAce && handValue <= 11) {
handValue += 10;
}
return handValue;
}
if won't fire twice. Elegant, and driven entirely by tests like HandValueAceTest.
Two neighbours built on value():
boolean isBusted() { return value() > 21; }
boolean hasBlackjack() { return valueEquals(21) && cards.size() == 2; }
Blackjack is 21 in exactly two cards — matching the Glossary's “natural.”
a. Hand = Ace + 5. What does value() return?
16
Sum with Ace=1 → 6. Has an Ace and 6 ≤ 11, so +10 → 16 (the “soft 16”).
b. Hand = Ace + 8 + 3. What does value() return?
12
Sum with Ace=1 → 12. Has an Ace but 12 > 11, so no +10. The Ace stays 1. (This is exactly a case in HandValueAceTest.)
c. Hand = Ace + King. Is it Blackjack?
Yes
King=10, Ace promotes to 11 → 21, and there are exactly 2 cards. hasBlackjack() is true.
HandValueAceTest · red → green (green expected!)
You'll practise the TDD rhythm on code that already works — so the “green” confirms your understanding. Open src/test/java/…/domain/HandValueAceTest.java. Add one test that pins down the two-ace case:
@Test
void handWithTwoAcesCountsOneAsElevenAndOneAsOne() {
Hand hand = createHand(Rank.ACE, Rank.ACE);
assertThat(hand.valueEquals(11 + 1)) // predict: what number?
.isTrue();
}
Predict first: what does Ace + Ace evaluate to? Write your guess down, then run just this test with ⌃⇧R.
12
Sum with both Aces = 1 → 2. Has an Ace and 2 ≤ 11, so +10 once → 12. The if fires a single time, so only one Ace becomes 11. Your assertion 11 + 1 = 12 should go green.
Then push further, test-first: add handWithThreeCardsAceSevenSevenStaysHard (Ace + 7 + 7). Predict the value, write the assertion, watch it pass. Notice you're using tests to document the rule.
Say “done” and I'll read your two new tests, run HandValueAceTest, and check your predictions against the real output — then we go to the testability patterns that make the rest of the game this easy to test.
The rules themselves: the repo's own Glossary.md and README.md. For the “tell, don't ask” style Hand embodies (methods like beats, pushes, isBusted instead of exposing data), see Ted Young's writing at tedyoung.me. Java enums: dev.java — Enums.
Want to see how Hand is used inside Game (dealer vs player, beats/pushes)? Ask me — I'll trace a full round through the domain.