diff options
| author | historia <[not public]> | 2026-09-15 14:18:14 -0400 |
|---|---|---|
| committer | historia <[not public]> | 2026-09-15 14:18:14 -0400 |
| commit | 396c3facb8287b9a34076cae8f3d7b300d366d99 (patch) | |
| tree | 941f280d051fac474fa68ca08318a8d16adc8d0f /internal/casino/config_validate.go | |
| parent | b3568f03e4103dd46db2ccc680fbab0a6454d604 (diff) | |
| download | thehouseoficarus-396c3facb8287b9a34076cae8f3d7b300d366d99.tar.gz | |
fix: atomic saves, casino RNG/config isolation, tick lifecycle
Diffstat (limited to 'internal/casino/config_validate.go')
| -rw-r--r-- | internal/casino/config_validate.go | 119 |
1 files changed, 119 insertions, 0 deletions
diff --git a/internal/casino/config_validate.go b/internal/casino/config_validate.go new file mode 100644 index 0000000..13229bc --- /dev/null +++ b/internal/casino/config_validate.go @@ -0,0 +1,119 @@ +package casino + +import ( + "errors" + "fmt" + "math" + "strings" +) + +// Validate checks raw configuration before Normalize supplies omitted (zero) +// defaults. Invalid explicit values must not silently become house rules. +func (c MachineConfig) Validate() error { + if err := validateIdentityAndBets(c.ID, c.MinBet, c.MaxBet); err != nil { + return err + } + if c.Game != GameSlots { + return fmt.Errorf("unsupported machine game %q", c.Game) + } + if c.Variant != "" && c.Variant != "huff_and_even_more_puff" { + return fmt.Errorf("unsupported slots variant %q", c.Variant) + } + if c.SpinTicks < 0 { + return fmt.Errorf("spin_ticks must not be negative") + } + if !finiteNonnegative(c.Payout) || c.Payout > 1 { + return fmt.Errorf("payout must be finite and between 0 and 1 (0 uses the default)") + } + return nil +} + +func (c TableConfig) Validate() error { + if err := validateIdentityAndBets(c.ID, c.MinBet, c.MaxBet); err != nil { + return err + } + if c.MaxPlayers < 0 || c.BettingTicks < 0 { + return fmt.Errorf("max_players and betting_ticks must not be negative") + } + switch c.Game { + case GameBlackjack: + b := c.Blackjack + if err := validateShoe(b.Decks, b.ShuffleAt); err != nil { + return err + } + if b.MaxSplitHands < 0 || b.ActionTicks < 0 { + return fmt.Errorf("max_split_hands and action_ticks must not be negative") + } + if !finiteNonnegative(b.BlackjackPayout) || !finiteNonnegative(b.InsurancePayout) { + return fmt.Errorf("blackjack_payout and insurance_payout must be finite and nonnegative") + } + case GameBaccarat: + b := c.Baccarat + if err := validateShoe(b.Decks, b.ShuffleAt); err != nil { + return err + } + if !finiteNonnegative(b.TiePayout) || !finiteNonnegative(b.BankerPayout) { + return fmt.Errorf("tie_payout and banker_payout must be finite and nonnegative") + } + default: + return fmt.Errorf("unsupported table game %q", c.Game) + } + return nil +} + +func validateIdentityAndBets(id string, minBet, maxBet int) error { + if id == "" || strings.TrimSpace(id) != id { + return fmt.Errorf("id must be nonempty with no surrounding whitespace") + } + if minBet < 0 || maxBet < 0 { + return fmt.Errorf("min_bet and max_bet must not be negative") + } + if maxBet > 0 && maxBet < max(1, minBet) { + return fmt.Errorf("max_bet must be at least min_bet") + } + return nil +} + +func finiteNonnegative(n float64) bool { + return !math.IsNaN(n) && !math.IsInf(n, 0) && n >= 0 +} + +func validateShoe(decks int, shuffleAt float64) error { + // A generous resource limit prevents malformed content from allocating an + // unbounded shoe. Zero retains the existing eight-deck default. + if decks < 0 || decks > 64 { + return fmt.Errorf("decks must be between 0 and 64 (0 uses the default)") + } + if !finiteNonnegative(shuffleAt) || shuffleAt >= 1 { + return fmt.Errorf("shuffle_at must be finite and in [0, 1)") + } + return nil +} + +// ValidateRoomConfigs checks the shared room-local ID namespace as well as +// individual definitions. Legacy slots under casino_tables remain supported. +func ValidateRoomConfigs(tables []TableConfig, machines []MachineConfig) error { + seen := make(map[string]bool) + var errs []error + check := func(kind, id string, err error) { + if seen[id] { + errs = append(errs, fmt.Errorf("duplicate casino id %q", id)) + } + seen[id] = true + if err != nil { + errs = append(errs, fmt.Errorf("%s %q: %w", kind, id, err)) + } + } + for _, c := range tables { + if c.Game == GameSlots { + legacy := MachineConfig{ID: c.ID, Game: GameSlots, MinBet: c.MinBet, MaxBet: c.MaxBet} + check("legacy slots", c.ID, legacy.Validate()) + } else { + check("table", c.ID, c.Validate()) + } + } + for _, c := range machines { + check("machine", c.ID, c.Validate()) + } + return errors.Join(errs...) +} |
