diff options
Diffstat (limited to 'internal/game/economy.go')
| -rw-r--r-- | internal/game/economy.go | 49 |
1 files changed, 34 insertions, 15 deletions
diff --git a/internal/game/economy.go b/internal/game/economy.go index 04508be..afdcdd8 100644 --- a/internal/game/economy.go +++ b/internal/game/economy.go @@ -353,13 +353,21 @@ func (g *Game) recoverEconomy() { players[name] = p return p } - hasLaterPayout := func(name string, seq int) bool { + // laterPayoutEvidence reports whether any non-void payout record was + // journaled for the player after seq — evidence the round resolved and + // its settlement was recorded — together with the total credits those + // records promise. A payout covering the stake's full value returned + // the stake inside the payout (win, push, or refund); a smaller total + // (e.g. the credit side of a crashed mixed refund) leaves the chip + // side of the stake outstanding. + laterPayoutEvidence := func(name string, seq int) (resolved bool, credits int) { for _, fr := range folded { if fr.rec.Kind == "payout" && fr.rec.Player == name && fr.rec.Status != "void" && fr.seq > seq { - return true + resolved = true + credits += fr.rec.Credit } } - return false + return resolved, credits } compensated := 0 @@ -413,13 +421,23 @@ func (g *Game) recoverEconomy() { paid++ compensated++ } - for _, other := range group { - if paid == 0 { - break + for idx, other := range group { + if idx < paid { + g.Economy.settle(other) + settled[other.ID] = true + continue } - g.Economy.settle(other) - settled[other.ID] = true - paid-- + // An unpaid sibling (the save failed, or the credit cap + // blocked re-application) stays owed. Re-anchor its + // checkpoints to the current durable balance so a later + // recovery re-applies it against a balance it can actually + // be paid from — without this, the balance a PAID sibling + // moved reads as this record's own durability and the + // liability is silently settled instead of paid. + other.CreditsBefore = p.Credits + other.CreditsAfter = p.Credits + other.Credit + settled[other.ID] = true // handled for the rest of this run + _ = g.Economy.append(*other) } case p.Credits == rec.CreditsAfter && len(group) == 1: g.Economy.settle(rec) @@ -439,7 +457,7 @@ func (g *Game) recoverEconomy() { } for k := len(openWagers) - 1; k >= 0; k-- { rec := &folded[openWagers[k]].rec - resolvedByPayout := hasLaterPayout(rec.Player, folded[openWagers[k]].seq) + resolvedByPayout, laterPayoutCredits := laterPayoutEvidence(rec.Player, folded[openWagers[k]].seq) p := loadPlayer(rec.Player) if p == nil { continue @@ -449,11 +467,12 @@ func (g *Game) recoverEconomy() { creditsNow, chipsNow := p.Credits, countChips(p) // Chips are recovered independently of the credit checkpoints. Any // state where the chip debit is durable but the chips have not come - // back (post-debit chip balance) still owes the chips, even when a - // later payout record already resolves the credit side. A durable - // chip refund always restores the balance to ChipsBefore, so this - // cannot double-return. - chipsOwed := rec.Chips > 0 && chipsNow == rec.ChipsAfter + // back (post-debit chip balance) still owes the chips — unless the + // resolving payout already returned the stake's full value in + // credits (a win, push, or refund includes the stake): returning + // the chips in kind on top would double-refund the round. + chipsOwed := rec.Chips > 0 && chipsNow == rec.ChipsAfter && + (!resolvedByPayout || laterPayoutCredits < rec.Chips+rec.Credit) if chipsOwed && !g.giveChips(p, rec.Chips) { // No inventory room for the chip return: keep the record open // for operator inspection rather than silently dropping the |
