aboutsummaryrefslogtreecommitdiff
path: root/internal/casino/config_validate.go
diff options
context:
space:
mode:
authorhistoria <[not public]>2026-09-19 03:15:59 -0400
committerhistoria <[not public]>2026-09-19 03:15:59 -0400
commit0e60567a7a0addb9421769ee7be7853bfa8788cb (patch)
tree7645112db0e4eae95b8a83c1e4f9aab38bede655 /internal/casino/config_validate.go
parentcc33072eced3d5460512c09c42ec4531c807d7af (diff)
downloadthehouseoficarus-0e60567a7a0addb9421769ee7be7853bfa8788cb.tar.gz
refactor: simplifcation, change how rooms are stored in player yaml, remove legacy code
Diffstat (limited to 'internal/casino/config_validate.go')
-rw-r--r--internal/casino/config_validate.go77
1 files changed, 63 insertions, 14 deletions
diff --git a/internal/casino/config_validate.go b/internal/casino/config_validate.go
index 6181883..9ad1776 100644
--- a/internal/casino/config_validate.go
+++ b/internal/casino/config_validate.go
@@ -4,6 +4,7 @@ import (
"errors"
"fmt"
"math"
+ "sort"
"strings"
)
@@ -21,8 +22,8 @@ func (c MachineConfig) Validate() error {
return fmt.Errorf("unsupported slots variant %q", c.Variant)
}
}
- if c.SpinTicks < 0 {
- return fmt.Errorf("spin_ticks must not be negative")
+ if c.SpinTicks < 0 || c.SpinTicks > maxSpinTicks {
+ return fmt.Errorf("spin_ticks must be between 0 and %d", maxSpinTicks)
}
if !finiteNonnegative(c.Payout) || c.Payout > 1 {
return fmt.Errorf("payout must be finite and between 0 and 1 (0 uses the default)")
@@ -37,16 +38,40 @@ 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")
+ if c.MaxPlayers < 0 || c.MaxPlayers > maxTablePlayers {
+ return fmt.Errorf("max_players must be between 0 and %d", maxTablePlayers)
+ }
+ if c.BettingTicks < 0 || c.BettingTicks > maxBettingTicks {
+ return fmt.Errorf("betting_ticks must be between 0 and %d", maxBettingTicks)
}
def, ok := tableGameDefFor(c.Game)
if !ok {
return fmt.Errorf("unsupported table game %q", c.Game)
}
+ if foreign := foreignRuleBlocks(c); len(foreign) > 0 {
+ return fmt.Errorf("table game %q has a rule block for a different game (%s); remove it or set game to match", c.Game, strings.Join(foreign, ", "))
+ }
return def.validate(c)
}
+// foreignRuleBlocks names every game-specific rule block present on c that
+// does not belong to c.Game, in a deterministic order. Each game registers its
+// own block in tableGames, so a new game widens this automatically instead of
+// needing another hand-maintained switch.
+func foreignRuleBlocks(c TableConfig) []string {
+ var foreign []string
+ for game, def := range tableGames {
+ if game == c.Game || def.hasRuleBlock == nil || def.ruleBlockName == "" {
+ continue
+ }
+ if def.hasRuleBlock(c) {
+ foreign = append(foreign, def.ruleBlockName)
+ }
+ }
+ sort.Strings(foreign)
+ return foreign
+}
+
// normalizeBlackjackTable applies the blackjack rule defaults. It is the
// game's Normalize entry in tableGames.
func normalizeBlackjackTable(c TableConfig) TableConfig {
@@ -66,8 +91,8 @@ func validateBlackjackTable(c TableConfig) error {
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 b.MaxSplitHands < 0 || b.ActionTicks < 0 || b.ActionTicks > maxActionTicks {
+ return fmt.Errorf("max_split_hands must not be negative and action_ticks must be between 0 and %d", maxActionTicks)
}
if !finiteNonnegative(b.BlackjackPayout) || !finiteNonnegative(b.InsurancePayout) {
return fmt.Errorf("blackjack_payout and insurance_payout must be finite and nonnegative")
@@ -130,6 +155,22 @@ func finiteNonnegative(n float64) bool {
return !math.IsNaN(n) && !math.IsInf(n, 0) && n >= 0
}
+// hasBlackjackRuleBlock reports whether c carries any explicit blackjack rule.
+// Used by foreignRuleBlocks to reject a block authored for another game.
+func hasBlackjackRuleBlock(c TableConfig) bool {
+ b := c.Blackjack
+ return b.Decks != 0 || b.ShuffleAt != 0 || b.BlackjackPayout != 0 ||
+ b.DealerHitSoft17 != nil || b.MaxSplitHands != 0 || b.ResplitAces != nil ||
+ b.DoubleAfterSplit != nil || b.DoubleSplitBlackjack != nil ||
+ b.InsurancePayout != 0 || b.Surrender != nil || b.ActionTicks != 0 ||
+ b.SplitUnlikeTenValues != nil
+}
+
+func hasBaccaratRuleBlock(c TableConfig) bool {
+ b := c.Baccarat
+ return b.Decks != 0 || b.ShuffleAt != 0 || b.TiePayout != 0 || b.BankerPayout != 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.
@@ -158,6 +199,18 @@ const (
maxBankerPayout = 0.99
)
+// Authoring limits for tick-based windows. A typo such as betting_ticks:
+// 2000000 would otherwise publish a table that holds its betting window open
+// for days; while the window is open a committed stake cannot be withdrawn, so
+// the bettor is trapped. These are generous relative to normal play (a tick is
+// 600ms).
+const (
+ maxTablePlayers = 64
+ maxBettingTicks = 3600
+ maxActionTicks = 3600
+ maxSpinTicks = 3600
+)
+
// 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.
@@ -187,6 +240,9 @@ const baccaratRoundCards = 6
// the geometry here keeps that gate meaningful.
func (p SlotProfile) Validate() error {
profile := p.Normalize()
+ if len(profile.Symbols) == 0 {
+ return fmt.Errorf("slot profile has no symbols")
+ }
if profile.Rows != slotGridRows || profile.Reels != slotGridReels {
return fmt.Errorf("unsupported slot geometry %dx%d: the runtime evaluator supports %dx%d only",
profile.Rows, profile.Reels, slotGridRows, slotGridReels)
@@ -265,7 +321,7 @@ func machineSlotProfile(c MachineConfig) SlotProfile {
}
// ValidateRoomConfigs checks the shared room-local ID namespace as well as
-// individual definitions. Legacy slots under casino_tables remain supported.
+// individual definitions.
//
// It also enforces one table/machine per game per room: a room is addressed
// by game name ("play blackjack"), so a second entry for the same game (or
@@ -291,13 +347,6 @@ func ValidateRoomConfigs(tables []TableConfig, machines []MachineConfig) error {
addressable[key] = id
}
for _, c := range tables {
- if c.Game == GameSlots {
- // A slots "table" is instantiated as the room's slots machine.
- legacy := LegacySlotMachine(c)
- check("legacy slots", c.ID, legacy.Validate())
- checkAddressable(strings.ToLower(string(GameSlots)), c.ID)
- continue
- }
check("table", c.ID, c.Validate())
checkAddressable(strings.ToLower(string(c.Game)), c.ID)
}