package casino import ( "sync" "testing" "time" ) // rebet is called through Manager.Rebet and reads the seat map; it must take // the table lock like every other game method, or a future caller off the // owner goroutine would race the live seat state. func TestRebetTakesTheTableLock(t *testing.T) { blackjack := newBlackjackTable(1, TableConfig{ID: "blackjack", Game: GameBlackjack}, newTestRand()) blackjack.join("alice") assertRebetWaitsForLock(t, "blackjack", &blackjack.mu, func() { blackjack.rebet("alice") }) baccarat := newBaccaratTable(1, TableConfig{ID: "baccarat", Game: GameBaccarat}, newTestRand()) baccarat.join("alice") assertRebetWaitsForLock(t, "baccarat", &baccarat.mu, func() { baccarat.rebet("alice") }) } func assertRebetWaitsForLock(t *testing.T, name string, mu *sync.Mutex, rebet func()) { t.Helper() mu.Lock() done := make(chan struct{}) go func() { rebet() close(done) }() select { case <-done: mu.Unlock() t.Fatalf("%s rebet read the seat map without the table lock", name) case <-time.After(50 * time.Millisecond): } mu.Unlock() select { case <-done: case <-time.After(2 * time.Second): t.Fatalf("%s rebet did not return after the lock was released", name) } } // A lone connected player is deliberately never timed out by the action // window. The absolute turn ceiling must therefore end that turn, or a // hung-but-connected client holds the hand, the seat, and the stake forever // (leave refuses mid-round). func TestLoneBettorTurnCeilingSettlesTheHand(t *testing.T) { table := newBlackjackTable(1, TableConfig{ID: "blackjack", Game: GameBlackjack, Blackjack: BlackjackConfig{ActionTicks: 2}}, newTestRand()) table.shoe = tableauShoe( card{rank: "10"}, card{rank: "7"}, // dealer 17 card{rank: "10"}, card{rank: "6"}, // alice 16 ) wallet := &testWallet{credits: 100} table.join("alice") table.bet("alice", "", 10, wallet) if table.phase != blackjackPlaying { t.Fatalf("setup: expected a dealt hand (phase=%s)", table.phase) } // The action timer alone would have expired after two ticks, but the // lone bettor gets the full grace period: 10x the action window. ceiling := table.Rules.ActionTicks * loneTurnGrace for i := 0; i < ceiling-1; i++ { table.tick() } if table.phase != blackjackPlaying { t.Fatalf("lone bettor's hand ended before the turn ceiling (phase=%s, ceiling=%d)", table.phase, ceiling) } events := table.tick() if table.phase != blackjackWaiting { t.Fatalf("turn ceiling did not settle the hand: phase=%s events=%+v", table.phase, events) } if !hasMessage(events, "idle too long") { t.Fatalf("missing turn-ceiling message: %+v", events) } if len(wallet.open) != 0 { t.Fatalf("stake still committed after the ceiling stood the hand: %+v", wallet.open) } }