LEARNING RECORD · Blackjack Ensemble course
24 July 2026
Date: 2026-07-24 (prep for the 5pm ensemble, "Ensemble 152") Repo: blackjack-ensemble-blue @ 097684d (Ensemble 151) Workspace lesson produced: lessons/0004-paying-the-winners.html
Lesson 0002's hands-on task (add a two-ace test) went green on arrival — it exercised existing Hand.value() behaviour rather than driving new code. Tom's feedback: "it was helpful to run the test, but it didn't lead to me adding new functionality." Correct critique — that task taught reading, not TDD. The fix: make the next lesson a genuine RED-first cycle tied to the work he'll actually do next session.
Reading the repo — not guessing — the next feature is spelled out as pseudocode in GameService.execute() (lines ~93–98):
if (game.isGameOver()) {
gameMonitor.gameCompleted(game);
gameRepository.saveOutcome(game);
// loop through all playerIds in the Game:
// retrieved PlayerAccount from Repository
// playerAccount.win(payout, playerOutcome)
// repository.save(playerAccount)
}
placePlayerBets → account.bet(amount) → balance − bet.account.win(payout, outcome) → balance + payout.Game.playerResults() → PlayerResult{ playerId(), outcome(), payout() }, and PlayerAccount.win(payout, outcome) / lose(outcome) already exist. The gap is purely application-layer wiring.Rather than reason about correctness (the mistake that made 0002 feel weak), the whole cycle was run against a scratch git worktree off HEAD, then torn down so the real repo stayed pristine for the ensemble.
RED test added to GameServiceTest (player beats dealer, 2× payoff):
PlayerAccountRepository accounts = PlayerAccountRepository.withNextId(9);
PlayerAccount account = PlayerAccount.register("winner");
account.deposit(50); accounts.save(account);
GameService gameService = GameService.createForTest(new StubShuffler(), accounts);
Deck deck = new StubDeck(Rank.QUEEN, Rank.EIGHT, Rank.TEN, Rank.JACK); // player 20 vs dealer 18
gameService.createGame(List.of(PlayerId.of(9)), new Shoe(List.of(deck)));
gameService.placePlayerBets(List.of(new PlayerBet(PlayerId.of(9), Bet.of(11))));
gameService.initialDeal();
gameService.playerStands();
assertThat(accounts.find(PlayerId.of(9))).get().extracting(PlayerAccount::balance)
.isEqualTo(50 - 11 + 22); // 61
→ Failed for the right reason: expected: 61 but was: 39 (bet deducted, payout never added).
GREEN (minimal wiring in execute()):
for (PlayerResult result : game.playerResults()) {
playerAccountRepository.find(result.playerId()).ifPresent(account -> {
account.win(result.payout(), result.outcome());
playerAccountRepository.save(account);
});
}
→ GameServiceTest 8/8 green; full suite 245/245 green. (Full version with a payout() > 0 ? win : lose branch also verified 245 green.)
SinglePlayerOutcomeTest.playerBeatsDealer using StubDeck(QUEEN, EIGHT, TEN, JACK) → player QUEEN+TEN=20, dealer EIGHT+JACK=18. (The two-per-line layout in the stub factories is misleading — read it column-wise.)PlayerOutcome payoff is a multiplier on the total returned, not profit**, and the bet is already deducted at bet-time: BEATS/DEALER_BUSTED ×2, BLACKJACK ×2.5, PUSH ×1, BUSTED/LOSES ×0.win() vs lose(): win(payout, outcome) enqueues PlayerWonGame (balance += payout); lose(outcome) enqueues PlayerLostGame (balance unchanged — money already gone).Two things the first cut of Lesson 0004 got wrong, now fixed:
PlayerAccount.win() is already implemented AND unit-tested — PlayerAccountTest winEmitsPlayerWonGame + playerWonGameIncreasesBalance. So there is nothing to build/test on the domain win function; the only gap is the wiring in execute(). Don't re-test win through GameService.win(0, PLAYER_BUSTED) → balance += 0 → unchanged, so a balance test passes with no branch. The only thing distinguishing win(0) from lose() is the recorded event (PlayerWonGame vs PlayerLostGame). So the elegant home for the decision is the domain (e.g. PlayerAccount.settle(PlayerResult)), unit-tested via freshEvents(), leaving the service branch-free. The current repo state: execute() has a started loop over game.playerIds() that finds the account and does nothing; the win test is present but @Disabled.game.playerIds() to game.playerResults() and calling win() + save(). (tracer bullet)settle(...), unit-tested on freshEvents().placePlayerBets to stop faking with getFirst() and loop all bets; then delete the commented-out // if (playerAccountRepository != null) scaffold (the contract step).execute()), not by inventing plausible exercises.