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
|
package casino
// Regressions for the money-safety fixes: table settlement must consume only
// its own round's stakes (never the player-wide ledger), a shutdown/eviction
// refund must still return a stake left pending after the round reset, and a
// disconnect during the slot payout window must settle a revealed win.
import "testing"
// TestBlackjackSecondRoundDoesNotResolveFailedPayoutStake: round 1's payout
// cannot be persisted, leaving its stake open for recovery. Round 2 then
// loses; the table must resolve only round 2's stake, not call the
// player-wide ResolveStakes and erase round 1's refund evidence.
func TestBlackjackSecondRoundDoesNotResolveFailedPayoutStake(t *testing.T) {
table := newBlackjackTable(1, TableConfig{ID: "bj", Game: GameBlackjack}, newTestRand())
// Draw order: round 1 dealer 10,7 (17) vs alice 10,8 (18, win);
// round 2 dealer 10,10 (20) vs alice 10,8 (18, loss).
table.shoe = tableauShoe(
card{rank: "10"}, card{rank: "7"}, card{rank: "10"}, card{rank: "8"},
card{rank: "10"}, card{rank: "10"}, card{rank: "10"}, card{rank: "8"},
)
wallet := &failingPayoutWallet{testWallet{credits: 100}}
table.join("alice")
table.bet("alice", "", 10, wallet)
table.action("alice", "stand")
if len(wallet.open) != 1 {
t.Fatalf("setup: a failed payout must leave round 1's stake open, open=%+v", wallet.open)
}
firstID := wallet.open[0].ID
table.bet("alice", "", 10, wallet)
table.action("alice", "stand")
if len(wallet.open) != 1 || wallet.open[0].ID != firstID {
t.Fatalf("round 2 erased the failed-payout stake: open=%+v, want only %q", wallet.open, firstID)
}
}
// TestBlackjackShutdownRefundsFailedPayoutAfterReset: once the round that
// held the stake has reset (HasBet cleared), a shutdown/eviction refund must
// still return the pending stake rather than skip the seated player.
func TestBlackjackShutdownRefundsFailedPayoutAfterReset(t *testing.T) {
table := newBlackjackTable(1, TableConfig{ID: "bj", Game: GameBlackjack}, newTestRand())
table.shoe = tableauShoe(
card{rank: "10"}, card{rank: "7"}, card{rank: "10"}, card{rank: "8"},
)
wallet := &failingPayoutWallet{testWallet{credits: 100}}
table.join("alice")
table.bet("alice", "", 10, wallet)
table.action("alice", "stand")
if table.phase != blackjackWaiting {
t.Fatalf("setup: the round should have reset (phase=%v)", table.phase)
}
if len(wallet.open) != 1 {
t.Fatalf("setup: a failed payout must leave the stake open, open=%+v", wallet.open)
}
table.refundAll()
if len(wallet.open) != 0 {
t.Fatalf("shutdown refund skipped the reset round's pending stake: open=%+v", wallet.open)
}
// The failed payout means the stake was never consumed, so the refund
// makes the player whole at 100 (the pre-fix code skipped the seated
// player and left them at 90).
if got := wallet.credits + wallet.paid; got != 100 {
t.Fatalf("refund returned balance %d, want 100 (stake returned once)", got)
}
}
// TestRoomAllowsDistinctSlotVariants: a room may host several variants of one
// game (each addressed by its variant), while two variant-less machines of the
// same game remain an unreachable duplicate.
func TestRoomAllowsDistinctSlotVariants(t *testing.T) {
slotVariants["test_second_variant"] = func() SlotProfile { return DefaultHuffAndPuffProfile() }
defer delete(slotVariants, "test_second_variant")
err := ValidateRoomConfigs(nil, []MachineConfig{
{ID: "slots_a", Game: GameSlots, Variant: "huff_and_even_more_puff"},
{ID: "slots_b", Game: GameSlots, Variant: "test_second_variant"},
})
if err != nil {
t.Fatalf("two distinct slot variants rejected: %v", err)
}
err = ValidateRoomConfigs(nil, []MachineConfig{
{ID: "slots_a", Game: GameSlots},
{ID: "slots_b", Game: GameSlots},
})
if err == nil {
t.Fatal("two variant-less machines of one game accepted despite being unaddressable")
}
}
// TestSlotsDisconnectDuringPayoutWindowSettlesRevealedWin: once every reel has
// stopped the result is visible to the player, so a disconnect during the
// cosmetic payout delay must pay the shown win and consume the stake rather
// than forfeit both.
func TestSlotsDisconnectDuringPayoutWindowSettlesRevealedWin(t *testing.T) {
machine := newMachine(1, MachineConfig{ID: "slots", Game: GameSlots, SpinTicks: 1}, newTestRand())
wallet := &testWallet{credits: 100}
machine.join("alice")
machine.setBet("alice", 10, wallet)
machine.spin = &SlotSpin{Payout: 20} // deterministic winning spin
for i := 0; i < 20 && machine.phase != machinePayout; i++ {
machine.tick()
}
if machine.phase != machinePayout {
t.Fatalf("setup: expected the payout window (phase=%v)", machine.phase)
}
machine.markDisconnected("alice")
if wallet.paid != 20 {
t.Fatalf("revealed win was not paid on disconnect: paid=%d, want 20", wallet.paid)
}
if len(wallet.open) != 0 {
t.Fatalf("resolved round left a stake outstanding: open=%+v", wallet.open)
}
if got := wallet.credits + wallet.paid; got != 110 {
t.Fatalf("balance after payout = %d, want 110", got)
}
}
|