Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 38 additions & 0 deletions games/bcook/blackjack-challenge/dealer.go
Original file line number Diff line number Diff line change
@@ -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"
}
20 changes: 15 additions & 5 deletions games/bcook/blackjack-challenge/layout.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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(); {
Expand Down
21 changes: 21 additions & 0 deletions games/bcook/blackjack-challenge/room.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
78 changes: 76 additions & 2 deletions games/bcook/blackjack-challenge/room_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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") {
Expand Down Expand Up @@ -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)
}
}
37 changes: 37 additions & 0 deletions games/bcook/blackjack/dealer.go
Original file line number Diff line number Diff line change
@@ -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"
}
20 changes: 15 additions & 5 deletions games/bcook/blackjack/layout.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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(); {
Expand Down
21 changes: 21 additions & 0 deletions games/bcook/blackjack/room.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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]
Expand Down
Loading
Loading