From 1f118d79e319da4e765502e9ca5b661772de94f1 Mon Sep 17 00:00:00 2001 From: historia <[not public]> Date: Fri, 18 Sep 2026 18:38:35 -0400 Subject: refactor: casino table core; fix: stake/migration/validation gaps --- internal/casino/machine.go | 30 +++++++++++++++++++----------- 1 file changed, 19 insertions(+), 11 deletions(-) (limited to 'internal/casino/machine.go') diff --git a/internal/casino/machine.go b/internal/casino/machine.go index 83e018f..f285518 100644 --- a/internal/casino/machine.go +++ b/internal/casino/machine.go @@ -30,6 +30,11 @@ type Machine struct { inFreeSpins bool spinBet int payoutRemainder float64 + // stakeID is the wallet handle of the wager committed to the current + // round (the triggering wager for a free-spin feature). Resolving by + // handle — not the player-wide ResolveStakes — keeps an older + // failed-payout stake open as recovery evidence when a new round starts. + stakeID string // featureBank holds the winnings of a paid round whose free-spin feature // is still running. They are paid as ONE settlement when the feature // ends: see finishSpin for why no payout may be journaled while the @@ -105,11 +110,13 @@ func (m *Machine) start(name string, wallet Wallet) []Event { } func (m *Machine) beginSpinLocked(amount int, wallet Wallet, publicMessage, privateMessage string) []Event { - if _, err := wallet.Wager(amount); err != nil { + wager, err := wallet.Wager(amount) + if err != nil { return m.private(walletErrorMessage(err, "You don't have enough chips and credits for that bet.")) } m.bet = amount m.spinBet = amount + m.stakeID = wager.ID m.player.Wallet = wallet m.stakeFailed = false m.spin = generateSlotSpinWithProfile(m.rng, m.spinBet, m.profile) @@ -209,9 +216,11 @@ func (m *Machine) autoBetTick() []Event { m.autoBet = false return m.private("Autobet stopped: no wager is set.") } - if _, err := m.player.Wallet.Wager(m.bet); err != nil { + if wager, err := m.player.Wallet.Wager(m.bet); err != nil { m.autoBet = false return m.private(walletErrorMessage(err, "Autobet stopped: you don't have enough chips and credits for that bet.")) + } else { + m.stakeID = wager.ID } m.spinBet = m.bet m.stakeFailed = false @@ -328,13 +337,15 @@ func (m *Machine) finishSpin() []Event { 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() + // started or just completed; free spins wager nothing): resolve + // ONLY this round's stake, so an older failed-payout stake stays + // open as recovery evidence. + m.player.Wallet.ResolveStake(m.stakeID) + m.stakeID = "" m.stakeFailed = false } if !m.autoBet { - events = append(events, m.menuEvents("")...) + events = append(events, m.privateMenu("")...) } } return events @@ -362,12 +373,13 @@ func (m *Machine) leave(name string) []Event { // unresolved for recovery (or the shutdown refund) to complete. func (m *Machine) resetSeat() { if m.player != nil && m.player.Wallet != nil && !m.stakeFailed { - m.player.Wallet.ResolveStakes() + m.player.Wallet.ResolveStake(m.stakeID) } m.player = nil m.bet = 0 m.spin = nil m.spinBet = 0 + m.stakeID = "" m.phase = machineReady m.reel, m.clock, m.suspense = 0, 0, false m.freeSpins = 0 @@ -496,10 +508,6 @@ func (m *Machine) privateMenu(prefix string) []Event { return events } -func (m *Machine) menuEvents(prefix string) []Event { - return m.privateMenu(prefix) -} - // refundAll refunds any outstanding stake at graceful shutdown/eviction and // then DISCARDS the round (the machine closes with the process). The wallet // resolves every outstanding stake exactly once; a completed spin has no -- cgit v1.2.3