diff options
Diffstat (limited to 'internal/casino/machine.go')
| -rw-r--r-- | internal/casino/machine.go | 43 |
1 files changed, 33 insertions, 10 deletions
diff --git a/internal/casino/machine.go b/internal/casino/machine.go index caff1bf..f156fe7 100644 --- a/internal/casino/machine.go +++ b/internal/casino/machine.go @@ -35,6 +35,10 @@ type Machine struct { suspense bool autoBet bool autoCountdown int + // stakeFailed marks the in-flight stake whose payout could not be + // persisted: the stake record stays OPEN as recovery evidence, so the + // seat must not resolve (forfeit) it when the player leaves. + stakeFailed bool } func newMachine(roomID int, cfg MachineConfig, rng *rand.Rand) *Machine { @@ -107,6 +111,7 @@ func (m *Machine) beginSpinLocked(name string, amount int, wallet Wallet, public m.bet = amount m.spinBet = amount m.player.Wallet = wallet + m.stakeFailed = false m.spin = generateSlotSpinWithProfile(m.rng, m.spinBet, m.profile) m.reel, m.clock, m.suspense = 0, 0, false m.phase = machineSpinning @@ -209,6 +214,7 @@ func (m *Machine) autoBetTick() []Event { return m.private(walletErrorMessage(err, "Autobet stopped: you don't have enough chips and credits for that bet.")) } m.spinBet = m.bet + m.stakeFailed = false m.spin = generateSlotSpinWithProfile(m.rng, m.spinBet, m.profile) m.reel, m.clock, m.suspense = 0, 0, false m.phase = machineSpinning @@ -233,20 +239,17 @@ func (m *Machine) finishSpin() []Event { if err := m.player.Wallet.PayCredits(credits); err != nil { // The payout could not be made durable; recovery will re-apply // the owed amount from the open journal record. Do not announce - // the win as paid, stop any autobet chain, and leave the stake - // open so recovery settles it against the payout record. + // the win as paid, stop any autobet chain, and keep the stake + // OPEN (stakeFailed): resolving it here would erase the refund + // evidence for the owed settlement. payFailed = true + m.stakeFailed = true m.autoBet, m.autoCountdown = false, 0 failedEvents := m.private(payoutFailedMessage()) failedEvents[0].Color = "casino_payout" events = append(events, failedEvents...) } } - if !payFailed && m.player.Wallet != nil { - // The paid spin resolved (free spins that follow wager nothing): - // the stake is no longer outstanding. - m.player.Wallet.ResolveStakes() - } if credits > 0 && !payFailed { winEvents := m.private(fmt.Sprintf("\nYou win %d credits.", credits)) winEvents[0].Color = "casino_payout" @@ -270,6 +273,10 @@ func (m *Machine) finishSpin() []Event { events = append(events, m.public(fmt.Sprintf("Three scatter symbols trigger %d free spins!", m.profile.FreeSpinCount))...) } if m.freeSpins > 0 { + // A feature is in progress: the triggering stake stays OUTSTANDING + // until the bonus completes, so a graceful shutdown (or crash) while + // free spins remain refunds the stake through RefundOutstanding / + // startup recovery instead of silently eating the feature. m.freeSpins-- m.phase = machineSpinning m.spin = generateSlotSpinWithProfile(m.rng, m.spinBet, m.profile) @@ -277,6 +284,13 @@ func (m *Machine) finishSpin() []Event { events = append(events, m.public(fmt.Sprintf("%d free spins remain.", m.freeSpins))...) } else { m.inFreeSpins = false + if !payFailed && m.player.Wallet != nil { + // The paid round fully resolved (the feature either never + // started or just completed; free spins wager nothing): the + // stake is no longer outstanding. + m.player.Wallet.ResolveStakes() + m.stakeFailed = false + } if !m.autoBet { events = append(events, m.menuEvents("")...) } @@ -300,9 +314,12 @@ func (m *Machine) leave(name string) []Event { // resetSeat frees the machine, discarding any in-flight spin and autobet // state. A wager already taken for a spin in progress is forfeited: its -// stake is resolved so startup recovery does not refund it. +// stake is resolved so startup recovery does not refund it. The exception +// is a stake whose payout could not be persisted (stakeFailed): the open +// stake record is the only evidence of the owed settlement, so it is left +// unresolved for recovery (or the shutdown refund) to complete. func (m *Machine) resetSeat() { - if m.player != nil && m.player.Wallet != nil { + if m.player != nil && m.player.Wallet != nil && !m.stakeFailed { m.player.Wallet.ResolveStakes() } m.player = nil @@ -315,6 +332,7 @@ func (m *Machine) resetSeat() { m.inFreeSpins = false m.payoutRemainder = 0 m.autoBet, m.autoCountdown = false, 0 + m.stakeFailed = false } // markDisconnected removes the player from the machine, forfeiting any @@ -409,7 +427,12 @@ func (m *Machine) menuEvents(prefix string) []Event { // refundAll refunds any outstanding stake at graceful shutdown/eviction. // The wallet resolves every outstanding stake exactly once; a completed -// spin has no outstanding stake and is not refunded again. Locked. +// spin has no outstanding stake and is not refunded again. A stake whose +// payout failed to persist (stakeFailed) is refunded here as well — its +// owed settlement remains open separately and startup recovery re-applies +// it. A bonus in progress keeps its triggering stake outstanding, so a +// shutdown during the feature returns the stake (the remaining free-spin +// EV is forfeit, never the wager). Locked. func (m *Machine) refundAll() []Event { m.mu.Lock() defer m.mu.Unlock() |
