aboutsummaryrefslogtreecommitdiff
path: root/internal/game/economy.go
diff options
context:
space:
mode:
Diffstat (limited to 'internal/game/economy.go')
-rw-r--r--internal/game/economy.go62
1 files changed, 37 insertions, 25 deletions
diff --git a/internal/game/economy.go b/internal/game/economy.go
index 3505a86..9ce4404 100644
--- a/internal/game/economy.go
+++ b/internal/game/economy.go
@@ -885,6 +885,24 @@ func pruneOpenPayouts(j *economyJournal, p *player.Player) bool {
// written later proves the round resolved (a win/push/refund already
// returned the stake inside that payout) → settled with no refund.
// Anything else stays open for operator inspection.
+// reanchorOwedPayout rewrites an open payout record's checkpoints to the
+// current durable balance so a later recovery reads it as still owed (the
+// balance sits at the record's before-checkpoint). It is used when a recovery
+// run could not pay every sibling in an identical-signature group: the paid
+// siblings moved the durable balance, so leaving the unpaid remainder at its
+// original checkpoints would let a later run mistake the balance for its own
+// durability and settle the liability without paying it. The post-checkpoint
+// respects the economy cap: when the credit cannot fit, before == after, so
+// the record keeps reading as owed at this balance instead of claiming an
+// impossible post-state.
+func reanchorOwedPayout(rec *economyRecord, credits, credit int) {
+ rec.CreditsBefore = credits
+ rec.CreditsAfter = credits
+ if after, err := addCreditsChecked(credits, credit); err == nil {
+ rec.CreditsAfter = after
+ }
+}
+
func (g *Game) recoverEconomy() {
if g.Economy == nil {
return
@@ -1099,35 +1117,29 @@ func (g *Game) recoverEconomy() {
settled[other.ID] = true
continue
}
- if capBlocked {
- // The credit cap blocked re-application. Re-anchor the
- // record's 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.
- // The re-anchored post-state must respect the cap: when it
- // would exceed it, no reachable balance can pay the record,
- // so the current balance is recorded as both checkpoints —
- // the record then keeps reading as owed at this balance
- // (retrying the re-application) instead of claiming an
- // impossible post-state that a coincidental balance could
- // be mistaken for as this record's own durability.
- other.CreditsBefore = p.Credits
- other.CreditsAfter = p.Credits
- if after, err := addCreditsChecked(p.Credits, other.Credit); err == nil {
- other.CreditsAfter = after
- }
+ if capBlocked || paid > 0 {
+ // Either the credit cap blocked re-application, or an
+ // earlier sibling WAS paid before this record's save failed
+ // transiently. In both cases the durable balance already
+ // moved away from this record's original before-checkpoint,
+ // so re-anchor the checkpoints to the current durable balance
+ // — without this, the balance a PAID sibling moved reads as
+ // this record's own durability and the liability is silently
+ // settled instead of paid (and compaction then erases the
+ // settled sibling evidence, so a later run sees len(group)==1
+ // and settles the remainder as "already durable").
+ reanchorOwedPayout(other, p.Credits, other.Credit)
settled[other.ID] = true // handled for the rest of this run
_ = g.Economy.append(*other)
continue
}
- // A TRANSIENT save failure blocked re-application and the
- // balance still has room below the cap: keep the record open
- // with its ORIGINAL checkpoints. Re-anchoring here would make
- // the record unreadable against the unchanged durable balance
- // (before-checkpoint mismatch) and permanently ambiguous; the
- // original checkpoints let a later recovery retry cleanly.
+ // A TRANSIENT save failure blocked the FIRST re-application
+ // (paid == 0) and the balance still sits at the original
+ // before-checkpoint: keep the record open with its ORIGINAL
+ // checkpoints so a later recovery reads it as owed and retries
+ // cleanly. Re-anchoring here would make the record unreadable
+ // against the unchanged durable balance (before-checkpoint
+ // mismatch) and permanently ambiguous.
settled[other.ID] = true // handled for the rest of this run
}
}