diff options
| author | historia <[not public]> | 2026-09-15 19:55:21 -0400 |
|---|---|---|
| committer | historia <[not public]> | 2026-09-15 19:55:21 -0400 |
| commit | 6f3c43aca62a7d33a19f14f6ad7c16891952673a (patch) | |
| tree | 0b0665ef624ef442df87ec7131129aaecc3420bd /internal/casino/blackjack.go | |
| parent | cd95de60364aba8f5bdf8ec86f7234a9903eacc8 (diff) | |
| download | thehouseoficarus-6f3c43aca62a7d33a19f14f6ad7c16891952673a.tar.gz | |
fix: outstanding-stake ledger, owner backpressure, editor state preservation
Diffstat (limited to 'internal/casino/blackjack.go')
| -rw-r--r-- | internal/casino/blackjack.go | 61 |
1 files changed, 49 insertions, 12 deletions
diff --git a/internal/casino/blackjack.go b/internal/casino/blackjack.go index 7c01c34..d3b26af 100644 --- a/internal/casino/blackjack.go +++ b/internal/casino/blackjack.go @@ -380,6 +380,8 @@ func splitCompatible(a, b card, unlikeTens bool) bool { } func (t *blackjackTable) startRound() []Event { + // The cut-card reshuffle happens between rounds, never mid-hand. + t.shoe.shuffleIfDue() t.dealer = blackjackHand{cards: []card{t.shoe.draw(), t.shoe.draw()}} for _, p := range t.players.ordered() { if !p.HasBet { @@ -542,10 +544,27 @@ func (t *blackjackTable) settle() []Event { if !p.HasBet { continue } + payoutFailed := false for _, hand := range p.hands { result, payout := settleHand(hand, dealerTotal, dealerBlackjack, t.Rules) if payout > 0 && p.Wallet != nil { - p.Wallet.PayCredits(payout) + if err := p.Wallet.PayCredits(payout); err != nil { + // The payout could not be made durable. The journal + // record stays open: recovery re-applies the owed + // amount. The win is not announced as paid, and the + // stakes stay open so recovery settles them against + // the payout record. + payoutFailed = true + publicResult, _ := blackjackResultMessages(p.Name, result, payout) + settlement := t.playerAction(p.Name, publicResult, payoutFailedMessage()) + if result == "blackjack" { + settlement.Color = "casino_jackpot_win" + } else if result == "win" { + settlement.Color = "casino_big_win" + } + events = append(events, settlement) + continue + } } publicResult, privateResult := blackjackResultMessages(p.Name, result, payout) settlement := t.playerAction(p.Name, publicResult, privateResult) @@ -562,9 +581,20 @@ func (t *blackjackTable) settle() []Event { insurancePayout, _ = MulPayout(p.insurance, 1+t.Rules.InsurancePayout) } if insurancePayout > 0 { - p.Wallet.PayCredits(insurancePayout) + if err := p.Wallet.PayCredits(insurancePayout); err != nil { + payoutFailed = true + events = append(events, t.private(p.Name, payoutFailedMessage())) + } else { + events = append(events, t.private(p.Name, fmt.Sprintf("Insurance pays %d credits.", insurancePayout))) + } + } else { + events = append(events, t.private(p.Name, "Insurance pays 0 credits.")) } - events = append(events, t.private(p.Name, fmt.Sprintf("Insurance pays %d credits.", insurancePayout))) + } + if p.Wallet != nil && !payoutFailed { + // The round resolved; the stakes are no longer outstanding and + // startup recovery must not refund them. + p.Wallet.ResolveStakes() } } t.resetRound() @@ -701,6 +731,11 @@ func (t *blackjackTable) markDisconnectedLocked(name string) []Event { if index < 0 { return nil } + // A disconnected player forfeits the round in flight: resolve the + // stakes now so startup recovery does not refund them later. + if p := t.players.get(name); p != nil && p.HasBet && p.Wallet != nil { + p.Wallet.ResolveStakes() + } t.players.remove(name) events := []Event{t.event(fmt.Sprintf("%s stands up.", name), true)} if t.players.count() == 0 { @@ -730,7 +765,8 @@ func (t *blackjackTable) hasParticipant(name string) bool { } // refundAll resolves every committed wager at this table (graceful -// shutdown): stakes return to their players through the wallet. +// shutdown/eviction): the wallet returns every outstanding stake — opening +// bet, doubles, splits, and insurance — exactly once. func (t *blackjackTable) refundAll() []Event { t.mu.Lock() defer t.mu.Unlock() @@ -739,17 +775,18 @@ func (t *blackjackTable) refundAll() []Event { if !p.HasBet || p.Wallet == nil { continue } - name, refund := p.Name, p.Wager.Total - if refund <= 0 { - continue - } - if err := p.Wallet.PayCredits(refund); err != nil { - events = append(events, t.private(name, "Shutdown refund could not be persisted; the stake is recorded for recovery.")) + name := p.Name + chips, credits, err := p.Wallet.RefundOutstanding() + refund := chips + credits + if err != nil || refund <= 0 { + if err != nil { + events = append(events, t.private(name, "Shutdown refund could not be persisted; the stake is recorded for recovery.")) + } continue } events = append(events, t.playerAction(name, - fmt.Sprintf("%s is refunded %d credits as the casino closes.", name, refund), - fmt.Sprintf("The house refunds your %d credit stake for the shutdown.", refund))) + fmt.Sprintf("%s is refunded %d as the casino closes.", name, refund), + fmt.Sprintf("The house refunds your %d stake for the shutdown.", refund))) } return events } |
