aboutsummaryrefslogtreecommitdiff
path: root/internal/game/casino.go
diff options
context:
space:
mode:
authorhistoria <[not public]>2026-09-15 15:35:52 -0400
committerhistoria <[not public]>2026-09-15 15:35:52 -0400
commit0b0b25222b3131ada79ff78c28eb8bf07d977deb (patch)
tree2ceaef5203595044f66a8f00064cd8e017f8f48d /internal/game/casino.go
parent396c3facb8287b9a34076cae8f3d7b300d366d99 (diff)
downloadthehouseoficarus-0b0b25222b3131ada79ff78c28eb8bf07d977deb.tar.gz
feat: single-owner game state, transactional lifecycle, durable money on server stop
Diffstat (limited to 'internal/game/casino.go')
-rw-r--r--internal/game/casino.go67
1 files changed, 57 insertions, 10 deletions
diff --git a/internal/game/casino.go b/internal/game/casino.go
index eb6fa4a..7442178 100644
--- a/internal/game/casino.go
+++ b/internal/game/casino.go
@@ -18,29 +18,76 @@ type casinoWallet struct {
p *player.Player
}
-func (w casinoWallet) Wager(amount int) (casino.Wager, bool) {
+// Wager debits the stake durably. Order of operations: durable journal
+// record FIRST (with balance checkpoints), then the in-memory mutation,
+// then the character save, then the settlement marker. A failed save
+// reverts the in-memory mutation and voids the record: no charge is kept
+// in memory, and the error distinguishes persistence failure from
+// insufficient funds.
+func (w casinoWallet) Wager(amount int) (casino.Wager, error) {
if amount <= 0 {
- return casino.Wager{}, false
+ return casino.Wager{}, casino.ErrInsufficientFunds
}
- chips := countChips(w.p)
+ creditsBefore := w.p.Credits
+ chipsBefore := countChips(w.p)
+ chips := chipsBefore
if chips > amount {
chips = amount
}
credits := amount - chips
- if w.p.Credits < credits {
- return casino.Wager{}, false
+ if creditsBefore < credits {
+ return casino.Wager{}, casino.ErrInsufficientFunds
+ }
+ rec, err := w.g.Economy.start("wager", w.p, chips, credits,
+ creditsBefore, creditsBefore-credits, chipsBefore, chipsBefore-chips)
+ if err != nil {
+ return casino.Wager{}, casino.ErrWalletPersistence
}
if chips > 0 {
w.p.RemoveItem("chips", chips)
}
w.p.Credits -= credits
- w.g.AccountStore.SaveCharacter(w.p)
- return casino.Wager{Total: amount, Chips: chips, Credits: credits}, true
+ if err := w.g.AccountStore.SaveCharacter(w.p); err != nil {
+ // Revert the in-memory mutation: the play must not proceed on a
+ // stake that could not be made durable.
+ if chips > 0 {
+ w.g.giveChips(w.p, chips)
+ }
+ w.p.Credits += credits
+ w.g.Economy.void(rec)
+ return casino.Wager{}, casino.ErrWalletPersistence
+ }
+ w.g.Economy.settle(rec)
+ return casino.Wager{Total: amount, Chips: chips, Credits: credits}, nil
}
-func (w casinoWallet) PayCredits(amount int) {
- w.p.Credits += amount
- w.g.AccountStore.SaveCharacter(w.p)
+// PayCredits applies a payout durably. The journal record is written first;
+// if the save fails, the record stays open and startup recovery re-pays it
+// (the player is owed the settlement).
+func (w casinoWallet) PayCredits(amount int) error {
+ if amount <= 0 {
+ return nil
+ }
+ creditsBefore := w.p.Credits
+ chipsBefore := countChips(w.p)
+ creditsAfter, err := addCreditsChecked(creditsBefore, amount)
+ if err != nil {
+ return err
+ }
+ rec, err := w.g.Economy.start("payout", w.p, 0, amount,
+ creditsBefore, creditsAfter, chipsBefore, chipsBefore)
+ if err != nil {
+ return casino.ErrWalletPersistence
+ }
+ w.p.Credits = creditsAfter
+ if err := w.g.AccountStore.SaveCharacter(w.p); err != nil {
+ w.p.Credits = creditsBefore
+ // Keep the record open: recovery re-pays on next startup, since a
+ // reported win must be durable.
+ return casino.ErrWalletPersistence
+ }
+ w.g.Economy.settle(rec)
+ return nil
}
// casinoAffordable is the most a player can wager: chips are spent first,