package casino import "testing" // TestMulPayoutPositiveBetNeverZero: a positive stake on a winning outcome // must not floor to zero (a 1-credit banker win at 0.95 paid nothing). func TestMulPayoutPositiveBetNeverZero(t *testing.T) { if got, err := MulPayout(1, 0.95); err != nil || got != 1 { t.Fatalf("MulPayout(1, 0.95) = %d, %v; want 1", got, err) } if got, err := MulPayout(1, 0.5); err != nil || got != 1 { t.Fatalf("MulPayout(1, 0.5) = %d, %v; want 1", got, err) } // A zero amount is still zero. if got, err := MulPayout(0, 0.95); err != nil || got != 0 { t.Fatalf("MulPayout(0, 0.95) = %d, %v; want 0", got, err) } } // TestCalculateRTPIncludesTriggerPayout: the runtime pays a paying scatter // like any other symbol, so the RTP model must include it. func TestCalculateRTPIncludesTriggerPayout(t *testing.T) { base := DefaultHuffAndPuffProfile() withPay := DefaultHuffAndPuffProfile() for i := range withPay.Symbols { if withPay.Symbols[i].ID == withPay.FreeSpinTrigger { withPay.Symbols[i].Payout = map[int]float64{3: 1, 4: 2, 5: 5} } } if CalculateRTP(withPay) <= CalculateRTP(base) { t.Fatalf("paying trigger did not raise RTP: %v <= %v", CalculateRTP(withPay), CalculateRTP(base)) } } // TestSlotProfileRejectsMissingTriggerSymbol: a named trigger that matches no // symbol silently disables the feature. func TestSlotProfileRejectsMissingTriggerSymbol(t *testing.T) { p := DefaultHuffAndPuffProfile() p.FreeSpinTrigger = "ghost" if err := p.Validate(); err == nil { t.Fatal("accepted a free_spin_trigger that names no symbol") } } // TestSlotProfileRejectsPaytableGap: a positive payout for a shorter run with // no payout for a longer run makes the longer run pay zero. func TestSlotProfileRejectsPaytableGap(t *testing.T) { p := DefaultHuffAndPuffProfile() for i := range p.Symbols { if p.Symbols[i].ID == "straw" { p.Symbols[i].Payout = map[int]float64{3: 0.5} } } if err := p.Validate(); err == nil { t.Fatal("accepted a paytable with a gap") } } // TestSlotProfileRejectsWeightlessTrigger: a trigger symbol with no reel // weight never appears, so the feature silently never fires. func TestSlotProfileRejectsWeightlessTrigger(t *testing.T) { p := DefaultHuffAndPuffProfile() for i := range p.Symbols { if p.Symbols[i].ID == p.FreeSpinTrigger { p.Symbols[i].Weight = 0 } } if err := p.Validate(); err == nil { t.Fatal("accepted a free_spin_trigger with no reel weight") } } // TestNonBettorDoesNotStartLoneBettorTimer: a spectator at the table must // not put a lone bettor on the clock. With only one live wager the action // timer and turn ceiling both stay stopped, so the hand waits for the player. func TestNonBettorDoesNotStartLoneBettorTimer(t *testing.T) { table := newBlackjackTable(1, TableConfig{ID: "blackjack", Game: GameBlackjack, MaxPlayers: 3, 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.join("bob") table.bet("alice", "", 10, wallet) // Let the betting window expire so the round deals with alice the only // bettor and bob seated as a non-bettor. for i := 0; i < 200 && table.phase != blackjackPlaying; i++ { table.tick() } if table.phase != blackjackPlaying { t.Fatalf("setup: round did not start (phase=%s)", table.phase) } for i := 0; i < 3; i++ { table.tick() } if table.ceiling.clock != 0 || table.timer.clock != 0 { t.Fatalf("a spectator started a timer for a lone bettor: ceiling=%d timer=%d", table.ceiling.clock, table.timer.clock) } table.markDisconnected("bob") if table.phase != blackjackPlaying || table.ceiling.clock != 0 || table.timer.clock != 0 { t.Fatalf("non-bettor disconnect changed the lone bettor's timers: phase=%s ceiling=%d timer=%d", table.phase, table.ceiling.clock, table.timer.clock) } }