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
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
|
package casino
import (
"fmt"
"math"
"reflect"
"sync"
"testing"
)
func TestMachineConfigValidation(t *testing.T) {
valid := MachineConfig{ID: "slots", Game: GameSlots}
if err := valid.Validate(); err != nil {
t.Fatal(err)
}
for name, change := range map[string]func(*MachineConfig){
"missing ID": func(c *MachineConfig) { c.ID = "" },
"ID whitespace": func(c *MachineConfig) { c.ID = " slots " },
"unknown game": func(c *MachineConfig) { c.Game = "roulette" },
"unknown variant": func(c *MachineConfig) { c.Variant = "typo" },
"negative bet": func(c *MachineConfig) { c.MinBet = -1 },
"negative max": func(c *MachineConfig) { c.MaxBet = -1 },
"inverted bets": func(c *MachineConfig) { c.MinBet, c.MaxBet = 10, 5 },
"negative timer": func(c *MachineConfig) { c.SpinTicks = -1 },
"negative payout": func(c *MachineConfig) { c.Payout = -1 },
"excess payout": func(c *MachineConfig) { c.Payout = 1.01 },
"NaN payout": func(c *MachineConfig) { c.Payout = math.NaN() },
"infinite payout": func(c *MachineConfig) { c.Payout = math.Inf(1) },
} {
t.Run(name, func(t *testing.T) {
c := valid
change(&c)
if err := c.Validate(); err == nil {
t.Fatalf("accepted invalid config: %+v", c)
}
m := NewManager(1)
if m.EnsureMachine(1, c) != nil {
t.Fatal("invalid machine instantiated")
}
if events := m.JoinMachine(1, c, "alice"); len(events) != 1 || !contains(events[0].Message, "not available") {
t.Fatalf("invalid machine join: %+v", events)
}
})
}
}
func TestTableConfigValidation(t *testing.T) {
for _, game := range []GameName{GameBlackjack, GameBaccarat} {
if err := (TableConfig{ID: "table", Game: game}).Validate(); err != nil {
t.Fatal(err)
}
}
for name, c := range map[string]TableConfig{
"unknown game": {ID: "table", Game: "roulette"},
"missing ID": {Game: GameBlackjack},
"negative timer": {ID: "table", Game: GameBlackjack, BettingTicks: -1},
"negative seats": {ID: "table", Game: GameBlackjack, MaxPlayers: -1},
"inverted bets": {ID: "table", Game: GameBlackjack, MinBet: 10, MaxBet: 5},
"negative decks": {ID: "table", Game: GameBlackjack, Blackjack: BlackjackConfig{Decks: -1}},
"huge shoe": {ID: "table", Game: GameBaccarat, Baccarat: BaccaratConfig{Decks: 1000000000}},
"NaN shuffle": {ID: "table", Game: GameBlackjack, Blackjack: BlackjackConfig{ShuffleAt: math.NaN()}},
"shuffle endpoint": {ID: "table", Game: GameBaccarat, Baccarat: BaccaratConfig{ShuffleAt: 1}},
"infinite payout": {ID: "table", Game: GameBlackjack, Blackjack: BlackjackConfig{BlackjackPayout: math.Inf(1)}},
"negative insurance": {ID: "table", Game: GameBlackjack, Blackjack: BlackjackConfig{InsurancePayout: -1}},
"NaN tie": {ID: "table", Game: GameBaccarat, Baccarat: BaccaratConfig{TiePayout: math.NaN()}},
"negative banker": {ID: "table", Game: GameBaccarat, Baccarat: BaccaratConfig{BankerPayout: -1}},
"negative splits": {ID: "table", Game: GameBlackjack, Blackjack: BlackjackConfig{MaxSplitHands: -1}},
"negative action timer": {ID: "table", Game: GameBlackjack, Blackjack: BlackjackConfig{ActionTicks: -1}},
} {
t.Run(name, func(t *testing.T) {
if c.Validate() == nil || NewManager(1).EnsureTable(1, c) != nil {
t.Fatalf("accepted invalid table: %+v", c)
}
})
}
}
func TestRoomCasinoIDsRejectDuplicates(t *testing.T) {
machines := []MachineConfig{{ID: "slots", Game: GameSlots}}
if ValidateRoomConfigs(nil, append(machines, machines...)) == nil {
t.Fatal("duplicate casino IDs accepted")
}
// Slots are machines only: declaring them under casino_tables is invalid.
if err := ValidateRoomConfigs([]TableConfig{{ID: "slots", Game: GameSlots}}, nil); err == nil {
t.Fatal("slots table accepted; slots must be declared under casino_machines")
}
}
func TestManagerGamesHaveIndependentRandomStreams(t *testing.T) {
m := NewManager(123)
var wg sync.WaitGroup
for i := 0; i < 8; i++ {
wg.Add(1)
go func(i int) {
defer wg.Done()
// Creation/shuffling can overlap spins on already-created games.
id := fmt.Sprintf("slots%d", i)
machine := m.EnsureMachine(1, MachineConfig{ID: id, Game: GameSlots})
m.EnsureTable(1, TableConfig{ID: fmt.Sprintf("cards%d", i), Game: GameBlackjack})
machine.join(id)
wallet := &testWallet{credits: 100}
for j := 0; j < 50; j++ {
machine.setBet(id, 1, wallet)
for machine.isBusy(id) {
machine.tick()
}
}
}(i)
}
wg.Wait()
}
func TestScaleToRTPDoesNotChangeSourceProfile(t *testing.T) {
source := DefaultHuffAndPuffProfile()
want := DefaultHuffAndPuffProfile()
first := ScaleToRTP(source, .9)
second := ScaleToRTP(source, .97)
if !reflect.DeepEqual(source, want) {
t.Fatal("scaling mutated the source profile")
}
if math.Abs(CalculateRTP(first)-.9) > 1e-10 || math.Abs(CalculateRTP(second)-.97) > 1e-10 {
t.Fatal("profiles share mutable paytables")
}
}
func TestSlotProfileEmptySymbolsRejected(t *testing.T) {
// A new slotVariants entry that forgets its symbol table must not validate
// and silently play the built-in cabinet.
if err := (SlotProfile{}).Validate(); err == nil {
t.Fatal("empty slot profile validated; expected an error")
}
}
func TestTableConfigRejectsForeignRuleBlock(t *testing.T) {
c := TableConfig{ID: "t", Game: GameBaccarat, Blackjack: BlackjackConfig{BlackjackPayout: 15}}
if err := c.Validate(); err == nil {
t.Fatal("baccarat table with a blackjack rule block validated; expected an error")
}
ok := TableConfig{ID: "t", Game: GameBlackjack}
if err := ok.Validate(); err != nil {
t.Fatalf("valid blackjack table rejected: %v", err)
}
}
func TestTableConfigTimerCaps(t *testing.T) {
cases := map[string]TableConfig{
"huge betting window": {ID: "t", Game: GameBaccarat, BettingTicks: maxBettingTicks + 1},
"huge action window": {ID: "t", Game: GameBlackjack, Blackjack: BlackjackConfig{ActionTicks: maxActionTicks + 1}},
}
for name, c := range cases {
t.Run(name, func(t *testing.T) {
if err := c.Validate(); err == nil {
t.Fatal("expected an error")
}
})
}
m := MachineConfig{ID: "m", Game: GameSlots, SpinTicks: maxSpinTicks + 1}
if err := m.Validate(); err == nil {
t.Fatal("expected an error for an oversized spin window")
}
}
|