From cc33072eced3d5460512c09c42ec4531c807d7af Mon Sep 17 00:00:00 2001 From: historia <[not public]> Date: Sat, 19 Sep 2026 01:55:34 -0400 Subject: fix: casino stake preservation, world state reconciliation, casino game registration --- internal/casino/blackjack.go | 26 +++++++++++++++----------- 1 file changed, 15 insertions(+), 11 deletions(-) (limited to 'internal/casino/blackjack.go') diff --git a/internal/casino/blackjack.go b/internal/casino/blackjack.go index 3b08ba5..cb71034 100644 --- a/internal/casino/blackjack.go +++ b/internal/casino/blackjack.go @@ -592,14 +592,12 @@ func (t *blackjackTable) settle() []Event { if !p.HasBet { continue } - payoutFailed := false for _, hand := range p.hands { result, payout, settleErr := settleHand(hand, dealerTotal, dealerBlackjack, t.Rules) // One shared settlement path for every game: pay before // announcing, and leave the stakes pending on any failure so a // refund can never be paid on top of a payout that was not durable. if settleStakeOutcome(p.Wallet, payout, hand.stakes, settleErr) { - payoutFailed = true publicResult, _ := blackjackResultMessages(p.Name, result, payout) settlement := t.playerAction(p.Name, publicResult, payoutFailedMessage()) colorizeBlackjackSettlement(&settlement, result) @@ -618,7 +616,6 @@ func (t *blackjackTable) settle() []Event { insurancePayout, insuranceErr = MulPayout(p.insurance, 1+t.Rules.InsurancePayout) } if settleStakeOutcome(p.Wallet, insurancePayout, []string{p.insuranceStakeID}, insuranceErr) { - payoutFailed = true events = append(events, t.private(p.Name, payoutFailedMessage())) } else if insurancePayout > 0 { events = append(events, t.private(p.Name, fmt.Sprintf("Insurance pays %d credits.", insurancePayout))) @@ -626,11 +623,12 @@ func (t *blackjackTable) settle() []Event { events = append(events, t.private(p.Name, "Insurance pays 0 credits.")) } } - if p.Wallet != nil && !payoutFailed { - // The round resolved; the stakes are no longer pending and must - // never be refunded. - p.Wallet.ResolveStakes() - } + // Each hand's stakes were already consumed per-handle by + // settleStakeOutcome (a payout removes them in PayCredits; a loss + // resolves them), and the insurance stake likewise. Deliberately do + // NOT call the player-wide ResolveStakes here: it would also consume + // a stake left pending by an earlier failed payout, which must stay + // refundable (see the Wallet contract). } t.resetRound() for _, p := range t.players.ordered() { @@ -781,9 +779,15 @@ func (t *blackjackTable) markDisconnectedLocked(name string) []Event { } if p := t.players.get(name); p != nil && p.HasBet && p.Wallet != nil { if t.phase == blackjackPlaying || t.phase == blackjackInsurance { - // A disconnected player forfeits the round in flight: consume the - // stakes now so a later refund cannot return them. - p.Wallet.ResolveStakes() + // A disconnected player forfeits the round in flight: consume + // ONLY this round's stakes, by handle, so a stake left pending by + // an earlier failed payout stays refundable. + var ids []string + for _, hand := range p.hands { + ids = append(ids, hand.stakes...) + } + ids = append(ids, p.insuranceStakeID) + resolveStakeIDs(p.Wallet, ids) } else { // No card has been dealt for this stake yet, so refund it: nothing // the bettor paid for has been revealed or played. -- cgit v1.2.3