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
|
package casino
import (
"math/rand"
"testing"
)
func TestBetRangeTextFormatsUnlimitedMax(t *testing.T) {
if got := betRangeText(1, maxBetUnlimited); got != "1 or more (no maximum)" {
t.Fatalf("unlimited range=%q", got)
}
if got := betRangeText(5, 100); got != "between 5 and 100" {
t.Fatalf("bounded range=%q", got)
}
}
func TestMachineSetBetUnlimitedMaxMessage(t *testing.T) {
m := newMachine(1, MachineConfig{ID: "slots", Game: GameSlots, MinBet: 1}, rand.New(rand.NewSource(7)))
m.player = &Participant{Name: "alice", Connected: true}
events := m.setBet("alice", 0, &testWallet{credits: 100})
if !hasMessage(events, "Slot machine bets must be 1 or more (no maximum).") {
t.Fatalf("unexpected bet-range message: %+v", events)
}
}
func TestMachineFreeSpinsUseProfileCount(t *testing.T) {
m := newMachine(1, MachineConfig{ID: "slots", Game: GameSlots}, rand.New(rand.NewSource(7)))
m.profile.FreeSpinCount = 5
m.player = &Participant{Name: "alice", Connected: true, Wallet: &testWallet{credits: 100}}
m.spinBet = 10
m.spin = &SlotSpin{ScatterCount: 3}
events := m.finishSpin()
if m.freeSpins != 4 {
t.Fatalf("freeSpins=%d, want 4 remaining after the first of 5 chained spins", m.freeSpins)
}
if m.phase != machineSpinning {
t.Fatalf("phase=%s, want spinning", m.phase)
}
if !hasMessage(events, "trigger 5 free spins") {
t.Fatalf("missing profile-count free spin message: %+v", events)
}
}
func TestMachineMenuEventCarriesStructuredMenu(t *testing.T) {
m := newMachine(1, MachineConfig{ID: "slots", Game: GameSlots}, rand.New(rand.NewSource(7)))
m.player = &Participant{Name: "alice", Connected: true}
events := m.privateMenu("You sit down at the slot machine.")
if len(events) != 1 || events[0].Menu == nil || events[0].Menu.Kind != MenuSlotCommands || len(events[0].Menu.Commands) == 0 {
t.Fatalf("slot menu event=%+v", events)
}
}
func TestMachineLeaveResetsPayoutRemainder(t *testing.T) {
m := newMachine(1, MachineConfig{ID: "slots", Game: GameSlots}, rand.New(rand.NewSource(7)))
m.player = &Participant{Name: "alice", Connected: true}
m.bet = 10
m.payoutRemainder = 0.5
m.leave("alice")
if m.payoutRemainder != 0 {
t.Fatalf("payoutRemainder=%v leaked to the next player", m.payoutRemainder)
}
}
func TestMachineLeaveStopsAutoBet(t *testing.T) {
m := newMachine(1, MachineConfig{ID: "slots", Game: GameSlots}, rand.New(rand.NewSource(7)))
m.player = &Participant{Name: "alice", Connected: true}
m.autoBet, m.autoCountdown = true, 2
m.leave("alice")
if m.autoBet || m.autoCountdown != 0 {
t.Fatalf("leave did not stop autobet: autoBet=%v countdown=%d", m.autoBet, m.autoCountdown)
}
if events := m.tick(); len(events) != 0 {
t.Fatalf("empty machine with stale autobet emitted events: %+v", events)
}
}
func TestMachineDisconnectFreesSeatAndForfeitsSpin(t *testing.T) {
m := newMachine(1, MachineConfig{ID: "slots", Game: GameSlots, SpinTicks: 1}, rand.New(rand.NewSource(7)))
w := &testWallet{credits: 100}
m.player = &Participant{Name: "alice", Connected: true, Wallet: w}
m.autoBet, m.autoCountdown = true, 2
m.spin = &SlotSpin{}
m.phase = machineSpinning
m.bet = 20
// Disconnect removes the player and discards the in-flight spin.
events := m.markDisconnected("alice")
if m.player != nil {
t.Fatal("disconnect did not free the seat")
}
if m.spin != nil || m.phase != machineReady {
t.Fatalf("in-flight spin was not discarded: spin=%v phase=%s", m.spin, m.phase)
}
if m.autoBet || m.autoCountdown != 0 {
t.Fatalf("autobet not cleared: autoBet=%v countdown=%d", m.autoBet, m.autoCountdown)
}
if m.bet != 0 || m.payoutRemainder != 0 {
t.Fatalf("wager state leaked: bet=%d remainder=%.2f", m.bet, m.payoutRemainder)
}
if len(events) == 0 || events[0].Type != EventLeft {
t.Fatalf("no public stand-up event: %+v", events)
}
// The seatel is immediately available.
m.join("bob")
if m.player == nil || m.player.Name != "bob" {
t.Fatalf("seat not available for the next player: player=%+v", m.player)
}
// The forfeited spin never pays out.
if w.paid != 0 {
t.Fatalf("forfeited spin paid credits: paid=%d", w.paid)
}
// The machine is idle after a disconnect.
if events := m.tick(); len(events) != 0 {
t.Fatalf("empty machine emitted events after disconnect: %+v", events)
}
}
func TestMachineFreeSpinsDoNotRetrigger(t *testing.T) {
m := newMachine(1, MachineConfig{ID: "slots", Game: GameSlots}, rand.New(rand.NewSource(7)))
m.player = &Participant{Name: "alice", Connected: true, Wallet: &testWallet{credits: 100}}
m.spinBet = 10
m.inFreeSpins = true
m.freeSpins = 1
m.spin = &SlotSpin{ScatterCount: 3}
events := m.finishSpin()
for _, e := range events {
if hasMessage([]Event{e}, "trigger") {
t.Fatalf("scatters during free spins retriggered: %+v", events)
}
}
// The last free spin resolved; the feature is now over.
m.spin = &SlotSpin{ScatterCount: 3}
m.finishSpin()
if m.freeSpins != 0 || m.inFreeSpins {
t.Fatalf("free-spin feature did not end: freeSpins=%d inFreeSpins=%v", m.freeSpins, m.inFreeSpins)
}
// Scatters on a paid spin trigger normally again.
m.phase = machineReady
m.spin = &SlotSpin{ScatterCount: 3}
events = m.finishSpin()
if m.freeSpins != m.profile.FreeSpinCount-1 || !m.inFreeSpins {
t.Fatalf("paid-spin trigger broken: freeSpins=%d inFreeSpins=%v", m.freeSpins, m.inFreeSpins)
}
if !hasMessage(events, "trigger") {
t.Fatalf("missing trigger message: %+v", events)
}
}
|