LESSON 0002 · The domain · Blackjack Ensemble course

Lesson 0002 — Reading the rules in code

The Domain Core: Card, Rank & Hand

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.

The win You'll be able to compute any hand's value in your head the way the code does, explain why an Ace is sometimes 11 and sometimes 1, and read Hand fluently — the class every game rule leans on.

1 · Rank — an enum that knows its value

Rank 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.

2 · Card — rank + suit, and a mutable face

public 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.

3 · Hand.value() — the soft-ace trick

Here 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;
}
The key idea Sum every card with the Ace as 1. Then, if there's an Ace and the total is ≤ 11, add 10 (promoting one Ace to 11). One Ace can be “soft” (11); a second Ace must stay 1, because 11+11 = 22 > 11 so the 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.”

4 · Quick recall — compute like the code

a. Hand = Ace + 5. What does value() return?

reveal

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?

reveal

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?

reveal

Yes

King=10, Ace promotes to 11 → 21, and there are exactly 2 cards. hasBlackjack() is true.

5 · Hands-on — write a failing test first (I'll check it live)

Task · in 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.

reveal the value & why

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.

Primary source

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.