aboutsummaryrefslogtreecommitdiff
path: root/internal/casino/hardening_fixes_batch2_test.go
diff options
context:
space:
mode:
Diffstat (limited to 'internal/casino/hardening_fixes_batch2_test.go')
-rw-r--r--internal/casino/hardening_fixes_batch2_test.go108
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)
+ }
+}