aboutsummaryrefslogtreecommitdiff
path: root/internal/casino/machine.go
diff options
context:
space:
mode:
Diffstat (limited to 'internal/casino/machine.go')
-rw-r--r--internal/casino/machine.go43
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()