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 minBet > 0 && maxBet == 0 && minBet > DefaultMaxBet { return fmt.Errorf("min_bet (%d) exceeds the default maximum bet (%d); set a larger max_bet", minBet, DefaultMaxBet) } 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...) }