aboutsummaryrefslogtreecommitdiff
path: root/internal/casino/config_validate.go
diff options
context:
space:
mode:
Diffstat (limited to 'internal/casino/config_validate.go')
-rw-r--r--internal/casino/config_validate.go53
1 files changed, 53 insertions, 0 deletions
diff --git a/internal/casino/config_validate.go b/internal/casino/config_validate.go
index 38d426d..a03cfbf 100644
--- a/internal/casino/config_validate.go
+++ b/internal/casino/config_validate.go
@@ -50,6 +50,12 @@ func (c TableConfig) Validate() error {
if !finiteNonnegative(b.BlackjackPayout) || !finiteNonnegative(b.InsurancePayout) {
return fmt.Errorf("blackjack_payout and insurance_payout must be finite and nonnegative")
}
+ if b.BlackjackPayout > maxBlackjackPayout {
+ return fmt.Errorf("blackjack_payout %v exceeds the %d:1 cap", b.BlackjackPayout, maxBlackjackPayout)
+ }
+ if b.InsurancePayout > maxInsurancePayout {
+ return fmt.Errorf("insurance_payout %v exceeds the %d:1 cap", b.InsurancePayout, maxInsurancePayout)
+ }
rules := b.Normalize()
if rules.AllowsSurrender() && c.Normalize().MinBet < minimumSurrenderBet {
return fmt.Errorf("surrender needs min_bet >= %d: a surrendered hand returns half the bet, which rounds to zero below that", minimumSurrenderBet)
@@ -70,6 +76,12 @@ func (c TableConfig) Validate() error {
if !finiteNonnegative(b.TiePayout) || !finiteNonnegative(b.BankerPayout) {
return fmt.Errorf("tie_payout and banker_payout must be finite and nonnegative")
}
+ if b.TiePayout > maxTiePayout {
+ return fmt.Errorf("tie_payout %v exceeds the %d:1 cap", b.TiePayout, maxTiePayout)
+ }
+ if b.BankerPayout > maxBankerPayout {
+ return fmt.Errorf("banker_payout %v exceeds the %v:1 cap", b.BankerPayout, maxBankerPayout)
+ }
// Non-blocking for the same reason as blackjack above.
return nil
default:
@@ -114,6 +126,22 @@ func validateShoe(decks int, shuffleAt float64) error {
// the whole stake.
const minimumSurrenderBet = 2
+// Payout-multiplier caps. Validation bounds authored multipliers so a typo
+// (e.g. blackjack_payout: 15) cannot silently create an absurd table and so
+// MulPayout's overflow backstop is not the only thing standing between a
+// configuration error and a MaxCredits payout.
+const (
+ maxBlackjackPayout = 5
+ maxInsurancePayout = 5
+ maxTiePayout = 10
+ maxBankerPayout = 0.99
+)
+
+// maxSlotSymbolLen is the fixed width of one reel cell in the renderer
+// (" " + symbol + padding + " "); a longer id would make the renderer's
+// width padding negative and panic.
+const maxSlotSymbolLen = 7
+
// blackjackRoundCards is the heuristic per-round card budget the shoe uses
// to decide whether to reshuffle before a round: an estimated eleven cards
// per hand times the most hands the table can have in play (the dealer's
@@ -141,6 +169,31 @@ func (p SlotProfile) Validate() error {
return fmt.Errorf("unsupported slot geometry %dx%d: the runtime evaluator supports %dx%d only",
profile.Rows, profile.Reels, slotGridRows, slotGridReels)
}
+ // Symbol IDs are looked up by exact string and rendered in fixed-width
+ // reel cells, so a duplicate ID silently pays that win twice, an over-long
+ // ID panics the renderer, and a zero total weight makes weightedSymbol
+ // unable to pick anything.
+ seenSymbols := make(map[string]bool, len(profile.Symbols))
+ totalWeight := 0
+ for _, symbol := range profile.Symbols {
+ if symbol.ID == "" {
+ return fmt.Errorf("slot symbol with an empty id")
+ }
+ if len(symbol.ID) > maxSlotSymbolLen {
+ return fmt.Errorf("slot symbol id %q exceeds the %d-character reel width", symbol.ID, maxSlotSymbolLen)
+ }
+ if seenSymbols[symbol.ID] {
+ return fmt.Errorf("duplicate slot symbol id %q", symbol.ID)
+ }
+ seenSymbols[symbol.ID] = true
+ if symbol.Weight < 0 {
+ return fmt.Errorf("slot symbol %q has negative reel weight", symbol.ID)
+ }
+ totalWeight += symbol.Weight
+ }
+ if totalWeight <= 0 {
+ return fmt.Errorf("slot symbols have no total reel weight, so no symbol can ever appear")
+ }
// A named trigger must exist, or the feature silently never fires while
// the RTP model also computes a zero trigger probability and reports green.
if profile.FreeSpinTrigger != "" {