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
|
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 TestRoomCasinoIDsAndLegacySlots(t *testing.T) {
tables := []TableConfig{{ID: "slots", Game: GameSlots}}
machines := []MachineConfig{{ID: "slots", Game: GameSlots}}
if err := ValidateRoomConfigs(tables, nil); err != nil {
t.Fatalf("legacy slots rejected: %v", err)
}
if ValidateRoomConfigs(tables, machines) == nil || ValidateRoomConfigs(nil, append(machines, machines...)) == nil {
t.Fatal("duplicate casino IDs accepted")
}
}
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")
}
}
|