aboutsummaryrefslogtreecommitdiff
path: root/internal/casino/config_validate.go
blob: 13229bc963b42208e4b3d47cfee82de9956fd607 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
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...)
}