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 "testing"
type testWallet struct {
chips, credits, paid int
open []Wager
}
func (w *testWallet) Wager(amount int) (Wager, error) {
if w.chips+w.credits < amount {
return Wager{}, ErrInsufficientFunds
}
chips := amount
if chips > w.chips {
chips = w.chips
}
w.chips -= chips
credits := amount - chips
w.credits -= credits
wager := Wager{Total: amount, Chips: chips, Credits: credits, ID: string(rune('a' + len(w.open)))}
w.open = append(w.open, wager)
return wager, nil
}
// PayCredits mirrors the production wallet: it banks the payout and, in the
// same step, consumes the stakes the settlement resolves.
func (w *testWallet) PayCredits(amount int, stakeIDs ...string) error {
w.paid += amount
for _, id := range stakeIDs {
w.ResolveStake(id)
}
return nil
}
// ResolveStake settles one outstanding stake by handle, mirroring the
// production wallet.
func (w *testWallet) ResolveStake(stakeID string) {
kept := w.open[:0]
for _, wager := range w.open {
if wager.ID != stakeID {
kept = append(kept, wager)
}
}
w.open = kept
}
// ResolveStakes mirrors the production wallet: every outstanding stake is
// settled (no longer refundable).
func (w *testWallet) ResolveStakes() { w.open = nil }
// RefundOutstanding returns every outstanding stake to the balances and
// resolves the stakes, mirroring the production refund path: credits are
// paid through PayCredits (recorded in paid), chips return as chips.
func (w *testWallet) RefundOutstanding() (chips, credits int, err error) {
for _, wager := range w.open {
chips += wager.Chips
credits += wager.Credits
}
w.open = nil
w.chips += chips
if credits > 0 {
w.paid += credits
}
return chips, credits, nil
}
func TestMixedWagerUsesChipsFirst(t *testing.T) {
w := &testWallet{chips: 7, credits: 50}
wager, err := w.Wager(12)
if err != nil || wager.Chips != 7 || wager.Credits != 5 || w.chips != 0 || w.credits != 45 {
t.Fatalf("unexpected wager: %+v wallet=%+v", wager, w)
}
}
func TestJoinUnknownTableGameRejected(t *testing.T) {
m := NewManager(1)
events := m.Join(1, TableConfig{ID: "slots", Game: GameSlots}, "alice")
if len(events) != 1 || !contains(events[0].Message, "not available") {
t.Fatalf("joining an unsupported table game: %+v", events)
}
if m.Table(1, "slots") != nil {
t.Fatal("unsupported game was cached as a table")
}
}
func TestMachineAutobetCountdownAndStop(t *testing.T) {
m := NewManager(1)
cfg := MachineConfig{ID: "slots", Game: GameSlots, SpinTicks: 1}
m.JoinMachine(1, cfg, "alice")
w := &testWallet{credits: 100}
if events := m.SetMachineBet(1, "slots", "alice", 10, w); len(events) == 0 {
t.Fatalf("setting a bet did not start a spin: %+v", events)
}
for m.MachineBusy("alice") {
m.Tick()
}
if events := m.EnableMachineAutoBet(1, "slots", "alice", w); len(events) != 0 {
t.Fatalf("enabling autobet emitted events before countdown: %+v", events)
}
for i, want := range []string{"Autobet enabled!", `Type "stop" to stop playing`, "Betting... 10 credits."} {
events := m.Tick()
if len(events) == 0 || eventText(events) != want {
t.Fatalf("tick %d message=%q events=%+v, want %q", i+1, eventText(events), events, want)
}
}
if !m.MachineBusy("alice") {
t.Fatal("autobet did not start a spin after countdown")
}
if events, stopped := m.StopMachineAutoBet(1, "slots", "alice"); !stopped || len(events) != 1 {
t.Fatalf("stop autobet events=%+v stopped=%v", events, stopped)
}
if _, playing := m.MachineFor("alice"); !playing {
t.Fatal("stopping autobet removed the player from the machine")
}
}
func eventText(events []Event) string {
for _, event := range events {
if event.PrivateMessage != "" {
return event.PrivateMessage
}
if event.Message != "" {
return event.Message
}
}
return ""
}
// TestRekeyRoomsKeepsTableIdentityAndWhitespaceIDs: a room rekey must move
// the manager key and the shared table identity together, and must not
// truncate a table ID that legally contains whitespace.
func TestRekeyRoomsKeepsTableIdentityAndWhitespaceIDs(t *testing.T) {
m := NewManager(1)
const id = "high stakes bj"
cfg := TableConfig{ID: id, Game: GameBlackjack}
m.Join(1, cfg, "alice")
table := m.Table(1, id)
if table == nil {
t.Fatal("table was not created for a valid whitespace-containing ID")
}
m.RekeyRooms(map[int]int{1: 12})
if m.Table(1, id) != nil {
t.Fatal("table left under the old room key")
}
moved := m.Table(12, id)
if moved != table {
t.Fatalf("rekey lost the table (or truncated its key): Table(12, %q)=%p want %p", id, moved, table)
}
if moved.RoomID != 12 {
t.Fatalf("moved table RoomID = %d, want 12", moved.RoomID)
}
// The game shares the table's base, so events must route to the new room.
events := moved.join("bob")
if len(events) == 0 || events[0].RoomID != 12 {
t.Fatalf("post-rekey events route to the wrong room: %+v", events)
}
// The old, truncated key must not have been created.
if m.Table(12, "high") != nil {
t.Fatal("rekey created a truncated table key from a whitespace ID")
}
}
|