aboutsummaryrefslogtreecommitdiff
path: root/internal/game/casino.go
diff options
context:
space:
mode:
authorhistoria <[not public]>2026-09-20 15:35:58 -0400
committerhistoria <[not public]>2026-09-20 15:35:58 -0400
commit91b466b6923d3eb1df1658199960fe7aa6db36dc (patch)
treee8a5f308530037f9471e19e6f194177c784c36b6 /internal/game/casino.go
parentbab5dd653039d026eb3d4e46da7aafef7137fe7c (diff)
downloadthehouseoficarus-91b466b6923d3eb1df1658199960fe7aa6db36dc.tar.gz
feat: new placeholder world
Diffstat (limited to 'internal/game/casino.go')
-rw-r--r--internal/game/casino.go59
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)