diff options
| author | historia <[not public]> | 2026-09-20 15:35:58 -0400 |
|---|---|---|
| committer | historia <[not public]> | 2026-09-20 15:35:58 -0400 |
| commit | 91b466b6923d3eb1df1658199960fe7aa6db36dc (patch) | |
| tree | e8a5f308530037f9471e19e6f194177c784c36b6 /internal/game/casino.go | |
| parent | bab5dd653039d026eb3d4e46da7aafef7137fe7c (diff) | |
| download | thehouseoficarus-91b466b6923d3eb1df1658199960fe7aa6db36dc.tar.gz | |
feat: new placeholder world
Diffstat (limited to 'internal/game/casino.go')
| -rw-r--r-- | internal/game/casino.go | 59 |
1 files changed, 35 insertions, 24 deletions
diff --git a/internal/game/casino.go b/internal/game/casino.go index 23b6c51..698cb78 100644 --- a/internal/game/casino.go +++ b/internal/game/casino.go @@ -19,14 +19,34 @@ import ( type casinoWallet struct { g *Game p *player.Player + // Optional per-wallet persistence seam for fault-injection tests. + saveCharacter func(*player.Player) error +} + +// save treats a completed replacement as applied, even if its durability is +// uncertain. Reverting memory then would disagree with the visible file and +// could overwrite a paid win or resurrect a resolved stake on the next save. +// Report the storage fault, but do not tell the game to retry/refund an +// already-applied mutation. Only pre-replacement failures require rollback. +func (w casinoWallet) save(operation string) error { + save := w.saveCharacter + if save == nil { + save = w.g.AccountStore.SaveCharacter + } + err := save(w.p) + if errors.Is(err, persistence.ErrDurabilityUncertain) { + fmt.Fprintf(os.Stderr, "casino %s: %s save durability uncertain: %v — keeping applied state\n", operation, w.p.Name, err) + return nil + } + return err } // Wager debits the stake and records it in PendingWagers in the SAME atomic // character save. The entry stays until the round resolves: a settlement // removes the stakes it returns (PayCredits) or consumes them // (ResolveStake/ResolveStakes), and startup refunds anything still listed -// (see recoverPendingWagers). A failed save reverts the in-memory debit and -// ledger entry, so a player is never charged for a stake that is not durable. +// (see recoverPendingWagers). A pre-replacement save failure reverts the +// debit and ledger entry; an uncertain replacement keeps both (see save). func (w casinoWallet) Wager(amount int) (casino.Wager, error) { if amount <= 0 { return casino.Wager{}, casino.ErrInsufficientFunds @@ -46,16 +66,7 @@ func (w casinoWallet) Wager(amount int) (casino.Wager, error) { w.p.Credits -= credits pending := player.PendingWager{ID: newPendingWagerID(), Chips: chips, Credits: credits} w.p.PendingWagers = append(w.p.PendingWagers, pending) - if err := w.g.AccountStore.SaveCharacter(w.p); err != nil { - if errors.Is(err, persistence.ErrDurabilityUncertain) { - // The character file was replaced but its directory entry may not - // be durable. The debit and the pending entry are in the visible - // file, so keep them: a surviving save refunds the stake at - // startup, and a lost one means the player still has the money. - // Either way the stake is never charged twice. - fmt.Fprintf(os.Stderr, "casino wager: %s save durability uncertain: %v — stake kept pending\n", w.p.Name, err) - return casino.Wager{Total: amount, Chips: chips, Credits: credits, ID: pending.ID}, nil - } + if err := w.save("wager"); err != nil { // Revert the in-memory mutation: the play must not proceed on a // stake that could not be made durable. if chips > 0 { @@ -78,15 +89,15 @@ func (w casinoWallet) Wager(amount int) (casino.Wager, error) { // passes the hand's stake IDs so the credit and the stake return are one // atomic step; a slot win passes its triggering stake for the same reason. // Either way a crash can never pay the payout AND refund the same stake. A -// failed save reverts both the credit and the ledger and reports persistence -// failure, so the caller never announces a win that is not durable. +// pre-replacement failure reverts both the credit and the ledger and reports +// failure. An uncertain replacement is applied and logged (see save). func (w casinoWallet) PayCredits(amount int, stakeIDs ...string) error { return w.payCredits(amount, stakeIDs) } // payCredits credits the payout and, when stakeIDs is non-empty, removes -// those stakes from the pending ledger in the same atomic save. On any -// failure the credit and the ledger are reverted together. +// those stakes from the pending ledger in the same atomic save. On a +// pre-replacement failure the credit and the ledger are reverted together. func (w casinoWallet) payCredits(amount int, stakeIDs []string) error { if amount <= 0 && len(stakeIDs) == 0 { return nil @@ -101,7 +112,7 @@ func (w casinoWallet) payCredits(amount int, stakeIDs []string) error { removed = takePendingWagers(w.p, stakeIDs) } w.p.Credits = sum - if err := w.g.AccountStore.SaveCharacter(w.p); err != nil { + if err := w.save("payout"); err != nil { w.p.Credits = creditsBefore w.p.PendingWagers = append(w.p.PendingWagers, removed...) return casino.ErrWalletPersistence @@ -118,7 +129,7 @@ func (w casinoWallet) ResolveStakes() { } before := append([]player.PendingWager(nil), w.p.PendingWagers...) w.p.PendingWagers = nil - if err := w.g.AccountStore.SaveCharacter(w.p); err != nil { + if err := w.save("resolve stakes"); err != nil { // Keep the in-memory ledger consistent with the durable file: the // stakes are still pending on disk, so restore them for a later // retry or startup recovery instead of silently diverging. @@ -139,7 +150,7 @@ func (w casinoWallet) ResolveStake(stakeID string) { if len(removed) == 0 { return } - if err := w.g.AccountStore.SaveCharacter(w.p); err != nil { + if err := w.save("resolve stake"); err != nil { // The durable file still lists the stake, so restore it in memory to // keep the two records consistent for a later retry/recovery. w.p.PendingWagers = append(w.p.PendingWagers, removed...) @@ -159,7 +170,7 @@ func (w casinoWallet) ResolveStakesByIDs(stakeIDs ...string) { if len(removed) == 0 { return } - if err := w.g.AccountStore.SaveCharacter(w.p); err != nil { + if err := w.save("resolve stakes"); err != nil { // The durable file still lists the stakes, so restore them in memory to // keep the two records consistent for a later retry/recovery. w.p.PendingWagers = append(w.p.PendingWagers, removed...) @@ -169,9 +180,9 @@ func (w casinoWallet) ResolveStakesByIDs(stakeIDs ...string) { // RefundOutstanding returns every pending stake to the player — chips in // kind, credits as credits — and clears the ledger in one save. It is used -// by graceful shutdown/eviction and by a pre-deal disconnect. On a failed -// save the in-memory return and ledger are reverted so a later attempt (or -// startup recovery) retries cleanly. +// by graceful shutdown/eviction and by a pre-deal disconnect. A pre-replacement +// failure reverts the return and ledger for retry; an uncertain replacement +// keeps both applied (see save). func (w casinoWallet) RefundOutstanding() (chips, credits int, err error) { before := append([]player.PendingWager(nil), w.p.PendingWagers...) for _, pending := range before { @@ -198,7 +209,7 @@ func (w casinoWallet) RefundOutstanding() (chips, credits int, err error) { return 0, 0, casino.ErrWalletPersistence } w.p.Credits = sum - if err := w.g.AccountStore.SaveCharacter(w.p); err != nil { + if err := w.save("refund"); err != nil { w.p.Credits = creditsBefore if chips > 0 { w.p.RemoveItem("chips", chips) |
