diff options
Diffstat (limited to 'internal/casino/money_regression_test.go')
| -rw-r--r-- | internal/casino/money_regression_test.go | 72 |
1 files changed, 72 insertions, 0 deletions
diff --git a/internal/casino/money_regression_test.go b/internal/casino/money_regression_test.go index 43bf505..e6b3ef8 100644 --- a/internal/casino/money_regression_test.go +++ b/internal/casino/money_regression_test.go @@ -175,3 +175,75 @@ func TestShoeReshufflesOnlyAtRoundBoundaries(t *testing.T) { t.Fatalf("shuffleIfDue did not reshuffle at the boundary (%d cards)", got) } } + +// TestSlotsLeaveAfterFailedPayoutKeepsStakeOpen: a spin whose payout could +// not be persisted leaves its stake record OPEN as recovery evidence for +// the owed settlement. Standing up afterwards must not resolve (forfeit) +// that stake — doing so would erase the refund evidence and silently drop +// the owed win. +func TestSlotsLeaveAfterFailedPayoutKeepsStakeOpen(t *testing.T) { + machine := newMachine(1, MachineConfig{ID: "slots", Game: GameSlots, SpinTicks: 1}, newTestRand()) + wallet := &failingPayoutWallet{testWallet{credits: 100}} + machine.join("alice") + machine.setBet("alice", 10, wallet) + machine.spin = &SlotSpin{Payout: 20} + for i := 0; i < 20 && machine.phase != machineReady; i++ { + machine.tick() + } + if len(wallet.open) != 1 { + t.Fatal("setup: a failed payout must leave the stake open") + } + machine.leave("alice") + if len(wallet.open) != 1 { + t.Errorf("leaving after a failed payout erased the outstanding stake; want it preserved for recovery/refund") + } +} + +// TestSlotsShutdownDuringBonusRefundsStake: free spins keep the triggering +// stake OUTSTANDING until the bonus completes. A graceful shutdown mid-feature +// therefore refunds the stake; the remaining feature EV is forfeit, but the +// player never loses the wager they paid for the round. +func TestSlotsShutdownDuringBonusRefundsStake(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{ScatterCount: 3} // deterministic bonus trigger + for i := 0; i < 20 && !machine.inFreeSpins; i++ { + machine.tick() + } + if !machine.inFreeSpins || machine.freeSpins != 7 { + t.Fatalf("setup: expected the first of eight free spins (inFreeSpins=%v freeSpins=%d)", machine.inFreeSpins, machine.freeSpins) + } + machine.refundAll() + if got := wallet.credits + wallet.paid; got != 100 { + t.Errorf("shutdown during the bonus left %d of the 100-credit balance; want the stake refunded", got) + } +} + +// TestSlotsStakeResolvesOnlyAfterFullRound: without any failure and without +// a feature, a completed spin resolves its stake (no shutdown refund), and a +// bonus that RUNS TO COMPLETION also resolves the stake at the end. +func TestSlotsStakeResolvesOnlyAfterFullRound(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{ScatterCount: 3} // trigger, then run all free spins + for i := 0; i < 200 && (machine.phase != machineReady || machine.inFreeSpins); i++ { + machine.tick() + } + if machine.inFreeSpins || machine.freeSpins != 0 { + t.Fatalf("setup: bonus must have completed (inFreeSpins=%v freeSpins=%d)", machine.inFreeSpins, machine.freeSpins) + } + if len(wallet.open) != 0 { + t.Fatalf("stake still outstanding after the feature completed: %+v", wallet.open) + } + // A completed round has nothing outstanding: the shutdown refund must + // not pay anything extra. + paidBefore := wallet.paid + machine.refundAll() + if wallet.paid != paidBefore { + t.Errorf("refundAll double-paid a completed round: paid %d -> %d", paidBefore, wallet.paid) + } +} |
