diff options
Diffstat (limited to 'internal/casino/fix_regressions_test.go')
| -rw-r--r-- | internal/casino/fix_regressions_test.go | 121 |
1 files changed, 121 insertions, 0 deletions
diff --git a/internal/casino/fix_regressions_test.go b/internal/casino/fix_regressions_test.go new file mode 100644 index 0000000..dc615ec --- /dev/null +++ b/internal/casino/fix_regressions_test.go @@ -0,0 +1,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) + } +} |
