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).PlayerLostGame recorded" test forces if (result.payout() > 0) win else lose. (Win-only code records the wrong event for a bust.)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.