diff --git a/games/bcook/blackjack-challenge/dealer.go b/games/bcook/blackjack-challenge/dealer.go new file mode 100644 index 0000000..66ea247 --- /dev/null +++ b/games/bcook/blackjack-challenge/dealer.go @@ -0,0 +1,38 @@ +package main + +import kit "github.com/shellcade/kit/v2" + +// The rotating dealer crew: every shoe belongs to one dealer, and the two +// retire together. A busy table reaches the cut card well inside the hand cap; +// a slow (heads-up) shoe hits the cap first. Either way the swap lands at the +// next betting window — as on a real floor, where dealers rotate off on a +// schedule and the incoming dealer starts from a fresh shuffle. +const handsPerDealer = 20 + +// dealerNames is the dealer roster, cycled in order from a seeded random +// start — star names, for The Star's table. Names stay <= 6 letters so the +// spaced-caps nameplate centred over the dealer's cards never crowds the +// (wide, 30-col) left rules signage. +var dealerNames = [...]string{"Vega", "Nova", "Orion", "Luna", "Rigel", "Stella", "Cass", "Astra"} + +// dealerName is the dealer currently working the table. +func (rm *room) dealerName() string { return dealerNames[rm.dealerIdx] } + +// needsNewDealer reports whether the current dealer's shoe is done: the cut +// card was reached (or a drained round forced a recycle), or the dealer has +// dealt their full shift of hands from a slow-burning shoe. +func (rm *room) needsNewDealer() bool { + return rm.sh.needsReshuffle() || rm.handsThisShoe >= handsPerDealer +} + +// rotateDealer retires the current dealer along with the spent shoe and seats +// the next one off the roster with a freshly shuffled shoe. The note announces +// the change on the felt through the betting window; the incoming dealer's +// first deal clears it. +func (rm *room) rotateDealer(r kit.Room) { + prev := rm.dealerName() + rm.dealerIdx = (rm.dealerIdx + 1) % len(dealerNames) + rm.sh.shuffle(r.Rand()) + rm.handsThisShoe = 0 + rm.dealerNote = prev + " steps away - " + rm.dealerName() + " takes the shoe" +} diff --git a/games/bcook/blackjack-challenge/layout.go b/games/bcook/blackjack-challenge/layout.go index f986c31..080c3e7 100644 --- a/games/bcook/blackjack-challenge/layout.go +++ b/games/bcook/blackjack-challenge/layout.go @@ -88,7 +88,9 @@ func (rm *room) compose(f *kit.Frame, v kit.Player) { // across the middle of the felt — keeping the centre clear for the cards. f.Text(dealerRow-1, 2, "blackjack pays 2:1 - ties lose", stDim) f.TextRight(dealerRow-1, kit.Cols-3, "dealer stands on 17", stDim) - center(f, dealerRow-1, "D E A L E R", stTitle) + // The centred label is the working dealer's nameplate (dealer.go): the crew + // rotates with the shoe, so the name up top says whose shoe this is. + center(f, dealerRow-1, rm.dealerName(), stTitle) rm.drawDealer(f) // Seats along the rail, centred as a group. @@ -117,7 +119,14 @@ func (rm *room) compose(f *kit.Frame, v kit.Player) { func (rm *room) drawDealer(f *kit.Frame) { if len(rm.dealer) == 0 { - center(f, dealerRow+1, "(waiting for bets)", stDim) + // A dealer changeover just happened: announce it where the cards will + // land, brightly, for this betting window (the next deal clears it). + // Otherwise the wait line names the dealer, anchoring the nameplate. + if rm.dealerNote != "" { + center(f, dealerRow+1, rm.dealerNote, stPhase) + } else { + center(f, dealerRow+1, "(dealer "+rm.dealerName()+" waits for bets)", stDim) + } return } // Size and centre the row to the cards actually on the table (the one dealt @@ -412,9 +421,10 @@ func (rm *room) drawActionBar(f *kit.Frame, v kit.Player, active *seat) { return default: // No hand left on turn: every player has resolved and the dealer is - // drawing out from its one face-up card. Name the moment so the slow - // draw-out reads as the dealer acting rather than a frozen table. - msg, st = "dealer plays...", stDim + // drawing out from its one face-up card. Name the moment (and the + // dealer, by name) so the slow draw-out reads as the dealer acting + // rather than a frozen table. + msg, st = rm.dealerName()+" plays...", stDim } case phResults: switch n := rm.unreadyCount(); { diff --git a/games/bcook/blackjack-challenge/room.go b/games/bcook/blackjack-challenge/room.go index 4f01f74..683fecd 100644 --- a/games/bcook/blackjack-challenge/room.go +++ b/games/bcook/blackjack-challenge/room.go @@ -129,6 +129,14 @@ type room struct { dealer hand joinSeq int + // The rotating dealer (dealer.go): dealerIdx names who is working the + // table, handsThisShoe counts the rounds dealt from the current shoe (the + // rotation cap for a shoe the cut card is slow to end), and dealerNote + // carries the changeover announcement through the next betting window. + dealerIdx int + handsThisShoe int + dealerNote string + // deadline is the current phase deadline (rendered as the countdown) and // what is the active pending one-shot. pendAt is the instant `what` fires; // for most phases it equals deadline, but pendSettle/pendBettingClose-grace @@ -162,6 +170,10 @@ func (rm *room) economyOff() bool { return rm.svc.Credits == nil } func (rm *room) OnStart(r kit.Room) { rm.sh = newShoe(r.Rand()) + // The opening dealer comes off the room seed, NOT r.Rand(): drawing from + // the RNG here would shift the card stream and change every seeded deal + // (the smoke script's choreography rides on exact cards from its seed). + rm.dealerIdx = int(((rm.cfg.Seed % int64(len(dealerNames))) + int64(len(dealerNames))) % int64(len(dealerNames))) if rm.economyOff() { rm.render(r) // out-of-service: no economy, no betting return @@ -373,6 +385,11 @@ func (rm *room) enterBetting(r kit.Room) { rm.dealer = nil rm.bettingClosing = false rm.clearSchedule() + // A spent shoe retires with its dealer: the cut card (or the handsPerDealer + // cap on a slow shoe) swaps in the next dealer, who brings a fresh shuffle. + if rm.needsNewDealer() { + rm.rotateDealer(r) + } for _, s := range rm.seats { s.hands = nil s.placed = false @@ -622,8 +639,12 @@ func loopTier(tiers []int, cur, budget int) int { func (rm *room) deal(r kit.Room) { if rm.sh.needsReshuffle() { + // Defensive: the changeover at enterBetting (rotateDealer) reshuffles + // before any spent shoe reaches a deal, so this never fires in play. rm.sh.shuffle(r.Rand()) } + rm.dealerNote = "" // the incoming dealer's first deal retires the announcement + rm.handsThisShoe++ // one more round on this dealer's shoe (rotation cap) rm.sh.beginRound() // everything dealt before this point is recyclable discards rng := r.Rand() rm.dealer = hand{rm.sh.draw(rng)} // ONE face-up card; the rest draw at the dealer's turn diff --git a/games/bcook/blackjack-challenge/room_test.go b/games/bcook/blackjack-challenge/room_test.go index 0726b78..8283d29 100644 --- a/games/bcook/blackjack-challenge/room_test.go +++ b/games/bcook/blackjack-challenge/room_test.go @@ -1453,8 +1453,8 @@ func TestRulesTaglineFlanksTheDealer(t *testing.T) { if !strings.Contains(dealerLabelRow, "dealer stands on 17") { t.Errorf("dealer rule not on the dealer label row: %q", dealerLabelRow) } - if !strings.Contains(dealerLabelRow, "D E A L E R") { - t.Errorf("DEALER label should remain centred between the rules: %q", dealerLabelRow) + if !strings.Contains(dealerLabelRow, rm.dealerName()) { + t.Errorf("dealer nameplate should sit centred between the rules: %q", dealerLabelRow) } // The old mid-felt tagline (row 9) must be clear now. if mid := kittest.String(f, 9); strings.Contains(mid, "blackjack pays") { @@ -1663,3 +1663,77 @@ func TestPairsMult(t *testing.T) { } } } + +// TestDealerRotatesWithSpentShoe asserts the dealer changeover rides the shoe: +// a shoe past its cut card retires the dealer at the next betting window — the +// roster advances, a fresh shuffle resets the shoe, the hand count restarts, +// and the changeover is announced on the felt. +func TestDealerRotatesWithSpentShoe(t *testing.T) { + a := mkPlayer("a") + rm, tr := newGame(t, a) + rm.OnJoin(tr, a) + was := rm.dealerIdx + rm.sh.pos = rm.sh.cut // cut card reached: the shoe is spent + rm.handsThisShoe = 7 + rm.enterBetting(tr) + if rm.dealerIdx != (was+1)%len(dealerNames) { + t.Fatalf("dealerIdx = %d, want roster advance from %d", rm.dealerIdx, was) + } + if rm.sh.pos != 0 { + t.Fatalf("shoe pos = %d, want 0 (incoming dealer brings a fresh shuffle)", rm.sh.pos) + } + if rm.handsThisShoe != 0 { + t.Fatalf("handsThisShoe = %d, want 0 after the changeover", rm.handsThisShoe) + } + want := dealerNames[was] + " steps away - " + rm.dealerName() + " takes the shoe" + if rm.dealerNote != want { + t.Fatalf("dealerNote = %q, want %q", rm.dealerNote, want) + } + // The announcement renders where the dealer's cards will land. + rm.render(tr) + if row := kittest.String(tr.LastFrame(a), dealerRow+1); !strings.Contains(row, want) { + t.Fatalf("changeover note not on the felt: %q", row) + } + // A fresh-shoe reopen (nobody bet) must NOT rotate again. + rm.enterBetting(tr) + if rm.dealerIdx != (was+1)%len(dealerNames) { + t.Fatalf("reopen rotated the dealer again: idx %d", rm.dealerIdx) + } +} + +// TestDealerRotatesAtHandCap asserts a slow-burning shoe still retires its +// dealer: handsPerDealer rounds dealt swaps the dealer (and the shoe with +// them) even though the cut card is nowhere near. +func TestDealerRotatesAtHandCap(t *testing.T) { + a := mkPlayer("a") + rm, tr := newGame(t, a) + rm.OnJoin(tr, a) + was := rm.dealerIdx + rm.handsThisShoe = handsPerDealer // dealt a full shift; shoe nowhere near the cut + rm.enterBetting(tr) + if rm.dealerIdx != (was+1)%len(dealerNames) { + t.Fatalf("hand cap did not rotate the dealer: idx %d, was %d", rm.dealerIdx, was) + } + if rm.handsThisShoe != 0 || rm.sh.pos != 0 { + t.Fatalf("changeover must reset the count and shoe: hands %d, pos %d", rm.handsThisShoe, rm.sh.pos) + } +} + +// TestDealCountsHandsAndClearsNote asserts each deal ticks the rotation +// counter and retires the changeover announcement. +func TestDealCountsHandsAndClearsNote(t *testing.T) { + a := mkPlayer("a") + rm, tr := newGame(t, a) + rm.OnJoin(tr, a) + s := rm.seats[a.AccountID] + fund(tr, s, 1000) + s.placed = true + rm.dealerNote = "Vega steps away - Nova takes the shoe" + rm.deal(tr) + if rm.handsThisShoe != 1 { + t.Fatalf("handsThisShoe = %d after one deal, want 1", rm.handsThisShoe) + } + if rm.dealerNote != "" { + t.Fatalf("dealerNote survived the deal: %q", rm.dealerNote) + } +} diff --git a/games/bcook/blackjack/dealer.go b/games/bcook/blackjack/dealer.go new file mode 100644 index 0000000..dc7e5d9 --- /dev/null +++ b/games/bcook/blackjack/dealer.go @@ -0,0 +1,37 @@ +package main + +import kit "github.com/shellcade/kit/v2" + +// The rotating dealer crew: every shoe belongs to one dealer, and the two +// retire together. A busy table reaches the cut card well inside the hand cap; +// a slow (heads-up) shoe hits the cap first. Either way the swap lands at the +// next betting window — as on a real floor, where dealers rotate off on a +// schedule and the incoming dealer starts from a fresh shuffle. +const handsPerDealer = 20 + +// dealerNames is the dealer roster, cycled in order from a seeded random +// start. Names stay <= 6 letters so the spaced-caps nameplate centred over the +// dealer's cards never crowds the flanking rules signage. +var dealerNames = [...]string{"Marge", "Dex", "Ruthie", "Sal", "Pearl", "Gus", "Vera", "Cliff"} + +// dealerName is the dealer currently working the table. +func (rm *room) dealerName() string { return dealerNames[rm.dealerIdx] } + +// needsNewDealer reports whether the current dealer's shoe is done: the cut +// card was reached (or a drained round forced a recycle), or the dealer has +// dealt their full shift of hands from a slow-burning shoe. +func (rm *room) needsNewDealer() bool { + return rm.sh.needsReshuffle() || rm.handsThisShoe >= handsPerDealer +} + +// rotateDealer retires the current dealer along with the spent shoe and seats +// the next one off the roster with a freshly shuffled shoe. The note announces +// the change on the felt through the betting window; the incoming dealer's +// first deal clears it. +func (rm *room) rotateDealer(r kit.Room) { + prev := rm.dealerName() + rm.dealerIdx = (rm.dealerIdx + 1) % len(dealerNames) + rm.sh.shuffle(r.Rand()) + rm.handsThisShoe = 0 + rm.dealerNote = prev + " steps away - " + rm.dealerName() + " takes the shoe" +} diff --git a/games/bcook/blackjack/layout.go b/games/bcook/blackjack/layout.go index 7f6ca0a..fc7d46b 100644 --- a/games/bcook/blackjack/layout.go +++ b/games/bcook/blackjack/layout.go @@ -88,7 +88,9 @@ func (rm *room) compose(f *kit.Frame, v kit.Player) { // across the middle of the felt — keeping the centre clear for the cards. f.Text(dealerRow-1, 2, "blackjack pays 3:2", stDim) f.TextRight(dealerRow-1, kit.Cols-3, "dealer stands on 17", stDim) - center(f, dealerRow-1, "D E A L E R", stTitle) + // The centred label is the working dealer's nameplate (dealer.go): the crew + // rotates with the shoe, so the name up top says whose shoe this is. + center(f, dealerRow-1, rm.dealerName(), stTitle) rm.drawDealer(f) // Seats along the rail, centred as a group. @@ -117,7 +119,14 @@ func (rm *room) compose(f *kit.Frame, v kit.Player) { func (rm *room) drawDealer(f *kit.Frame) { if len(rm.dealer) == 0 { - center(f, dealerRow+1, "(waiting for bets)", stDim) + // A dealer changeover just happened: announce it where the cards will + // land, brightly, for this betting window (the next deal clears it). + // Otherwise the wait line names the dealer, anchoring the nameplate. + if rm.dealerNote != "" { + center(f, dealerRow+1, rm.dealerNote, stPhase) + } else { + center(f, dealerRow+1, "(dealer "+rm.dealerName()+" waits for bets)", stDim) + } return } hide := -1 @@ -435,9 +444,10 @@ func (rm *room) drawActionBar(f *kit.Frame, v kit.Player, active *seat) { return default: // No hand left on turn: every player has resolved and the dealer is - // turning its hole card and drawing. Name the moment so the slow - // reveal reads as the dealer acting rather than a frozen table. - msg, st = "dealer plays...", stDim + // turning its hole card and drawing. Name the moment (and the + // dealer, by name) so the slow reveal reads as the dealer acting + // rather than a frozen table. + msg, st = rm.dealerName()+" plays...", stDim } case phResults: switch n := rm.unreadyCount(); { diff --git a/games/bcook/blackjack/room.go b/games/bcook/blackjack/room.go index 7561dcf..d3240a8 100644 --- a/games/bcook/blackjack/room.go +++ b/games/bcook/blackjack/room.go @@ -132,6 +132,14 @@ type room struct { dealerHole bool // hole card concealed joinSeq int + // The rotating dealer (dealer.go): dealerIdx names who is working the + // table, handsThisShoe counts the rounds dealt from the current shoe (the + // rotation cap for a shoe the cut card is slow to end), and dealerNote + // carries the changeover announcement through the next betting window. + dealerIdx int + handsThisShoe int + dealerNote string + // deadline is the current phase deadline (rendered as the countdown) and // what is the active pending one-shot. pendAt is the instant `what` fires; // for most phases it equals deadline, but pendSettle/pendBettingClose-grace @@ -165,6 +173,10 @@ func (rm *room) economyOff() bool { return rm.svc.Credits == nil } func (rm *room) OnStart(r kit.Room) { rm.sh = newShoe(r.Rand()) + // The opening dealer comes off the room seed, NOT r.Rand(): drawing from + // the RNG here would shift the card stream and change every seeded deal + // (the smoke script's choreography rides on exact cards from its seed). + rm.dealerIdx = int(((rm.cfg.Seed % int64(len(dealerNames))) + int64(len(dealerNames))) % int64(len(dealerNames))) if rm.economyOff() { rm.render(r) // out-of-service: no economy, no betting return @@ -380,6 +392,11 @@ func (rm *room) enterBetting(r kit.Room) { rm.dealerHole = false rm.bettingClosing = false rm.clearSchedule() + // A spent shoe retires with its dealer: the cut card (or the handsPerDealer + // cap on a slow shoe) swaps in the next dealer, who brings a fresh shuffle. + if rm.needsNewDealer() { + rm.rotateDealer(r) + } for _, s := range rm.seats { s.hands = nil s.placed = false @@ -632,8 +649,12 @@ func loopTier(tiers []int, cur, budget int) int { func (rm *room) deal(r kit.Room) { if rm.sh.needsReshuffle() { + // Defensive: the changeover at enterBetting (rotateDealer) reshuffles + // before any spent shoe reaches a deal, so this never fires in play. rm.sh.shuffle(r.Rand()) } + rm.dealerNote = "" // the incoming dealer's first deal retires the announcement + rm.handsThisShoe++ // one more round on this dealer's shoe (rotation cap) rm.sh.beginRound() // everything dealt before this point is recyclable discards rng := r.Rand() rm.dealer = hand{rm.sh.draw(rng), rm.sh.draw(rng)} // [up, hole] diff --git a/games/bcook/blackjack/room_test.go b/games/bcook/blackjack/room_test.go index 68de93a..691cd57 100644 --- a/games/bcook/blackjack/room_test.go +++ b/games/bcook/blackjack/room_test.go @@ -75,7 +75,7 @@ func TestPairsSideBetLoopsOnP(t *testing.T) { if s.pairsBet != 0 { t.Fatalf("pairs side bet defaults to %d, want 0 (off)", s.pairsBet) } - s.bal = 100000 // deep enough to afford every tier, so the loop wraps only at the top + s.bal = 100000 // deep enough to afford every tier, so the loop wraps only at the top rm.OnInput(tr, a, runeInput('p')) // P advances one tier if s.pairsBet != pairsTiers[1] { t.Fatalf("after P, pairsBet = %d, want %d", s.pairsBet, pairsTiers[1]) @@ -300,7 +300,7 @@ func TestSettleBehindBetWinFolds(t *testing.T) { rm.OnJoin(tr, b) sa, sb := rm.seats[a.AccountID], rm.seats[b.AccountID] sa.placed, sa.bet = true, 25 - staked(tr, sa, 925, 75) // a's open stake: main 25 + behind 50 escrowed + staked(tr, sa, 925, 75) // a's open stake: main 25 + behind 50 escrowed sa.hands = []*phand{{cards: hand{{2, suitSpade}, {3, suitHeart}}, bet: 25}} // a: 5, loses sb.placed, sb.bet = true, 25 sb.hands = []*phand{{cards: hand{{10, suitSpade}, {9, suitHeart}}, bet: 25}} // b: 19, beats dealer @@ -326,7 +326,7 @@ func TestSettleBehindRefundsWhenTargetLeft(t *testing.T) { rm.OnJoin(tr, b) sa := rm.seats[a.AccountID] sa.placed, sa.bet = true, 25 - staked(tr, sa, 900, 75) // main 25 + behind 50 escrowed; 900 bankroll left + staked(tr, sa, 900, 75) // main 25 + behind 50 escrowed; 900 bankroll left sa.hands = []*phand{{cards: hand{{10, suitSpade}, {9, suitHeart}}, bet: 25}} // 19, pushes sa.backs = map[string]*backBet{b.AccountID: {behind: 50}} delete(rm.seats, b.AccountID) // b left mid-round @@ -353,9 +353,9 @@ func TestSettleFoldsPairsResultIntoNet(t *testing.T) { s.placed = true s.bet = 50 s.pairsBet = 10 - staked(tr, s, 940, 60) // main 50 + pairs 10 escrowed - s.pairsWin = 70 // a mixed pair already resolved at deal... - s.grossThisRound = 70 // ...and folded into the open stake's gross + staked(tr, s, 940, 60) // main 50 + pairs 10 escrowed + s.pairsWin = 70 // a mixed pair already resolved at deal... + s.grossThisRound = 70 // ...and folded into the open stake's gross s.hands = []*phand{{cards: hand{{10, suitSpade}, {9, suitHeart}}, bet: 50}} // 19 rm.dealer = hand{{10, suitClub}, {9, suitDiamond}} // 19 -> hand pushes rm.settle(tr) @@ -1274,8 +1274,8 @@ func TestRulesTaglineFlanksTheDealer(t *testing.T) { if !strings.Contains(dealerLabelRow, "dealer stands on 17") { t.Errorf("dealer rule not on the dealer label row: %q", dealerLabelRow) } - if !strings.Contains(dealerLabelRow, "D E A L E R") { - t.Errorf("DEALER label should remain centred between the rules: %q", dealerLabelRow) + if !strings.Contains(dealerLabelRow, rm.dealerName()) { + t.Errorf("dealer nameplate should sit centred between the rules: %q", dealerLabelRow) } // The old mid-felt tagline (row 9) must be clear now. if mid := kittest.String(f, 9); strings.Contains(mid, "blackjack pays") { @@ -1528,3 +1528,77 @@ func TestBrokeSeatRebuysOnRInBetting(t *testing.T) { t.Fatal("re-bought seat still cannot place a bet") } } + +// TestDealerRotatesWithSpentShoe asserts the dealer changeover rides the shoe: +// a shoe past its cut card retires the dealer at the next betting window — the +// roster advances, a fresh shuffle resets the shoe, the hand count restarts, +// and the changeover is announced on the felt. +func TestDealerRotatesWithSpentShoe(t *testing.T) { + a := mkPlayer("a") + rm, tr := newGame(t, a) + rm.OnJoin(tr, a) + was := rm.dealerIdx + rm.sh.pos = rm.sh.cut // cut card reached: the shoe is spent + rm.handsThisShoe = 7 + rm.enterBetting(tr) + if rm.dealerIdx != (was+1)%len(dealerNames) { + t.Fatalf("dealerIdx = %d, want roster advance from %d", rm.dealerIdx, was) + } + if rm.sh.pos != 0 { + t.Fatalf("shoe pos = %d, want 0 (incoming dealer brings a fresh shuffle)", rm.sh.pos) + } + if rm.handsThisShoe != 0 { + t.Fatalf("handsThisShoe = %d, want 0 after the changeover", rm.handsThisShoe) + } + want := dealerNames[was] + " steps away - " + rm.dealerName() + " takes the shoe" + if rm.dealerNote != want { + t.Fatalf("dealerNote = %q, want %q", rm.dealerNote, want) + } + // The announcement renders where the dealer's cards will land. + rm.render(tr) + if row := kittest.String(tr.LastFrame(a), dealerRow+1); !strings.Contains(row, want) { + t.Fatalf("changeover note not on the felt: %q", row) + } + // A fresh-shoe reopen (nobody bet) must NOT rotate again. + rm.enterBetting(tr) + if rm.dealerIdx != (was+1)%len(dealerNames) { + t.Fatalf("reopen rotated the dealer again: idx %d", rm.dealerIdx) + } +} + +// TestDealerRotatesAtHandCap asserts a slow-burning shoe still retires its +// dealer: handsPerDealer rounds dealt swaps the dealer (and the shoe with +// them) even though the cut card is nowhere near. +func TestDealerRotatesAtHandCap(t *testing.T) { + a := mkPlayer("a") + rm, tr := newGame(t, a) + rm.OnJoin(tr, a) + was := rm.dealerIdx + rm.handsThisShoe = handsPerDealer // dealt a full shift; shoe nowhere near the cut + rm.enterBetting(tr) + if rm.dealerIdx != (was+1)%len(dealerNames) { + t.Fatalf("hand cap did not rotate the dealer: idx %d, was %d", rm.dealerIdx, was) + } + if rm.handsThisShoe != 0 || rm.sh.pos != 0 { + t.Fatalf("changeover must reset the count and shoe: hands %d, pos %d", rm.handsThisShoe, rm.sh.pos) + } +} + +// TestDealCountsHandsAndClearsNote asserts each deal ticks the rotation +// counter and retires the changeover announcement. +func TestDealCountsHandsAndClearsNote(t *testing.T) { + a := mkPlayer("a") + rm, tr := newGame(t, a) + rm.OnJoin(tr, a) + s := rm.seats[a.AccountID] + fund(tr, s, 1000) + s.placed = true + rm.dealerNote = "Marge steps away - Dex takes the shoe" + rm.deal(tr) + if rm.handsThisShoe != 1 { + t.Fatalf("handsThisShoe = %d after one deal, want 1", rm.handsThisShoe) + } + if rm.dealerNote != "" { + t.Fatalf("dealerNote survived the deal: %q", rm.dealerNote) + } +}