aboutsummaryrefslogtreecommitdiff
path: root/internal/casino/credits.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/casino/credits.go
parent396c3facb8287b9a34076cae8f3d7b300d366d99 (diff)
downloadthehouseoficarus-0b0b25222b3131ada79ff78c28eb8bf07d977deb.tar.gz
feat: single-owner game state, transactional lifecycle, durable money on server stop
Diffstat (limited to 'internal/casino/credits.go')
-rw-r--r--internal/casino/credits.go84
1 files changed, 84 insertions, 0 deletions
diff --git a/internal/casino/credits.go b/internal/casino/credits.go
new file mode 100644
index 0000000..ce47d41
--- /dev/null
+++ b/internal/casino/credits.go
@@ -0,0 +1,84 @@
+package casino
+
+import (
+ "errors"
+ "fmt"
+ "math"
+)
+
+// Currency limits and checked arithmetic. All balances and payouts flow
+// through these helpers so overflow and unbounded multipliers fail loudly
+// instead of silently wrapping or producing absurd balances.
+const (
+ // MaxCredits is the largest credit balance the economy supports.
+ // Configured bets are additionally bounded by each game's MaxBet.
+ MaxCredits = 1_000_000_000_000 // 1e12
+ // DefaultMaxBet bounds wagers when a configuration does not set one.
+ // The explicit unlimited sentinel (MaxBet = MaxInt) remains accepted
+ // for authored configurations that deliberately want no cap.
+ DefaultMaxBet = 1_000_000
+)
+
+// maxBetUnlimited is the MaxBet sentinel meaning "no wager cap".
+const maxBetUnlimited = int(^uint(0) >> 1)
+
+// Wallet errors. ErrInsufficientFunds is normal gameplay (the table reports
+// it like a rejected bet); ErrWalletPersistence means the wager or payout
+// could not be made durable and MUST NOT be presented as successful play.
+var (
+ ErrInsufficientFunds = errors.New("insufficient funds")
+ ErrWalletPersistence = errors.New("wallet persistence failure")
+)
+
+// AddCredits adds two balances with an explicit cap, reporting overflow.
+// A negative addend is rejected (payouts never decrease a balance).
+func AddCredits(a, b int) (int, error) {
+ if b < 0 {
+ return a, errors.New("negative credit add")
+ }
+ sum := a + b
+ if b > 0 && sum < a || a > 0 && sum < b {
+ return MaxCredits, fmt.Errorf("credit overflow: %d + %d exceeds %d", a, b, MaxCredits)
+ }
+ if sum > MaxCredits {
+ return MaxCredits, fmt.Errorf("credit overflow: %d + %d exceeds %d", a, b, MaxCredits)
+ }
+ return sum, nil
+}
+
+// MulPayout scales an amount by a payout multiplier and converts to whole
+// credits (floor, like a house paying in chips). Overflow past MaxCredits
+// is an error, not a silent wrap. A non-finite or negative multiplier is
+// rejected.
+func MulPayout(amount int, multiplier float64) (int, error) {
+ if multiplier < 0 || math.IsInf(multiplier, 0) || math.IsNaN(multiplier) {
+ return 0, fmt.Errorf("invalid payout multiplier %v", multiplier)
+ }
+ if amount <= 0 {
+ return 0, nil
+ }
+ // Bound the product before conversion: amount * multiplier must fit
+ // within MaxCredits (multiplier > 1 may grow the stake).
+ if float64(amount) > float64(MaxCredits)/multiplier {
+ return MaxCredits, fmt.Errorf("payout overflow: %d * %v exceeds %d", amount, multiplier, MaxCredits)
+ }
+ payout := int(math.Floor(float64(amount) * multiplier))
+ if payout > MaxCredits {
+ return MaxCredits, fmt.Errorf("payout overflow: %d * %v exceeds %d", amount, multiplier, MaxCredits)
+ }
+ return payout, nil
+}
+
+// walletErrorMessage renders a wallet error for a player: insufficient
+// funds is normal gameplay; a persistence failure makes clear that the
+// house could not process the wager rather than implying a short balance.
+// context, when non-empty, is the game-specific insufficient-funds wording.
+func walletErrorMessage(err error, context ...string) string {
+ if err == ErrWalletPersistence {
+ return "The house could not process that transaction (persistence failure). Nothing was charged; try again shortly."
+ }
+ if len(context) > 0 && context[0] != "" {
+ return context[0]
+ }
+ return "You don't have enough chips and credits for that bet."
+}