aboutsummaryrefslogtreecommitdiff
path: root/internal/casino/hardening_fixes_batch2_test.go
blob: 13911432e971769cb5ea6aaccd69a2467893f951 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
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)
	}
}