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) } }