diff options
Diffstat (limited to 'internal/casino/hardening_fixes_batch2_test.go')
| -rw-r--r-- | internal/casino/hardening_fixes_batch2_test.go | 108 |
1 files changed, 108 insertions, 0 deletions
diff --git a/internal/casino/hardening_fixes_batch2_test.go b/internal/casino/hardening_fixes_batch2_test.go new file mode 100644 index 0000000..1391143 --- /dev/null +++ b/internal/casino/hardening_fixes_batch2_test.go @@ -0,0 +1,108 @@ +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") + } +} + +// TestDisconnectingNonBettorDoesNotResetLoneTurnCeiling: a third party's +// seat event re-prompts the same turn and must not extend a hung lone +// bettor's absolute deadline. +func TestDisconnectingNonBettorDoesNotResetLoneTurnCeiling(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() + } + before := table.ceiling.clock + if before == 0 { + t.Fatal("setup: turn ceiling did not advance") + } + + table.markDisconnected("bob") + if table.ceiling.clock != before { + t.Fatalf("a non-bettor's disconnect reset the turn ceiling: %d -> %d", before, table.ceiling.clock) + } +} |
