diff options
| author | historia <[not public]> | 2026-09-15 15:35:52 -0400 |
|---|---|---|
| committer | historia <[not public]> | 2026-09-15 15:35:52 -0400 |
| commit | 0b0b25222b3131ada79ff78c28eb8bf07d977deb (patch) | |
| tree | 2ceaef5203595044f66a8f00064cd8e017f8f48d /internal/casino/credits.go | |
| parent | 396c3facb8287b9a34076cae8f3d7b300d366d99 (diff) | |
| download | thehouseoficarus-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.go | 84 |
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." +} |
