aboutsummaryrefslogtreecommitdiff
path: root/internal/casino/config_validate.go
diff options
context:
space:
mode:
authorhistoria <[not public]>2026-09-15 14:18:14 -0400
committerhistoria <[not public]>2026-09-15 14:18:14 -0400
commit396c3facb8287b9a34076cae8f3d7b300d366d99 (patch)
tree941f280d051fac474fa68ca08318a8d16adc8d0f /internal/casino/config_validate.go
parentb3568f03e4103dd46db2ccc680fbab0a6454d604 (diff)
downloadthehouseoficarus-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.go119
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...)
+}