aboutsummaryrefslogtreecommitdiff
path: root/internal/casino/fix_regressions_test.go
blob: dc615ec7b24860aeda3aa5fdce0193c1bc1adce2 (plain)
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)
	}
}