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
|
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)
}
}
|