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") ) // 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 == 0 && multiplier > 0 { // A positive stake on a winning outcome must never pay nothing: a // fractional multiplier (e.g. a 5% baccarat commission) at a 1-credit // bet floors to zero, turning a win into a total loss. payout = 1 } 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." } // payoutFailedMessage explains a settlement the wallet could not apply right // now: a persistence failure or a credit-cap block. Games must NOT announce // such a payout as paid. The stake stays pending, so it is returned by a // shutdown refund or at the next startup rather than paid as winnings. func payoutFailedMessage() string { return "The house could not process your payout right now. Your stake is safe and will be returned if the settlement cannot be completed." }