aboutsummaryrefslogtreecommitdiff
path: root/internal/casino/config_validate.go
diff options
context:
space:
mode:
authorhistoria <[not public]>2026-09-19 01:55:34 -0400
committerhistoria <[not public]>2026-09-19 01:55:34 -0400
commitcc33072eced3d5460512c09c42ec4531c807d7af (patch)
treeb1b451fa87fa389cb8343a3645372f3b43f5523e /internal/casino/config_validate.go
parent2c38386484cdb507c4687caceb37e0b963e33594 (diff)
downloadthehouseoficarus-cc33072eced3d5460512c09c42ec4531c807d7af.tar.gz
fix: casino stake preservation, world state reconciliation, casino game registration
Diffstat (limited to 'internal/casino/config_validate.go')
-rw-r--r--internal/casino/config_validate.go137
1 files changed, 87 insertions, 50 deletions
diff --git a/internal/casino/config_validate.go b/internal/casino/config_validate.go
index 9b9759c..6181883 100644
--- a/internal/casino/config_validate.go
+++ b/internal/casino/config_validate.go
@@ -16,8 +16,10 @@ func (c MachineConfig) Validate() error {
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.Variant != "" {
+ if _, ok := slotVariants[c.Variant]; !ok {
+ return fmt.Errorf("unsupported slots variant %q", c.Variant)
+ }
}
if c.SpinTicks < 0 {
return fmt.Errorf("spin_ticks must not be negative")
@@ -38,55 +40,74 @@ func (c TableConfig) Validate() error {
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")
- }
- if b.BlackjackPayout > maxBlackjackPayout {
- return fmt.Errorf("blackjack_payout %v exceeds the %d:1 cap", b.BlackjackPayout, maxBlackjackPayout)
- }
- if b.InsurancePayout > maxInsurancePayout {
- return fmt.Errorf("insurance_payout %v exceeds the %d:1 cap", b.InsurancePayout, maxInsurancePayout)
- }
- rules := b.Normalize()
- if rules.AllowsSurrender() && c.Normalize().MinBet < minimumSurrenderBet {
- return fmt.Errorf("surrender needs min_bet >= %d: a surrendered hand returns half the bet, which rounds to zero below that", minimumSurrenderBet)
- }
- // Shoe coverage is NOT a configuration floor. blackjackRoundCards is
- // only a boundary heuristic (an all-ace hand can hold more than
- // eleven cards), so a hard floor would reject tables that play fine
- // and, because a room is refused whole on any error, would darken
- // every casino game in it. shoe.ensureForRound reshuffles at the
- // round boundary and draw()'s exhausted-shoe rebuild is a deliberate
- // shuffle-through instead.
- return nil
- 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")
- }
- if b.TiePayout > maxTiePayout {
- return fmt.Errorf("tie_payout %v exceeds the %d:1 cap", b.TiePayout, maxTiePayout)
- }
- if b.BankerPayout > maxBankerPayout {
- return fmt.Errorf("banker_payout %v exceeds the %v:1 cap", b.BankerPayout, maxBankerPayout)
- }
- // Non-blocking for the same reason as blackjack above.
- return nil
- default:
+ def, ok := tableGameDefFor(c.Game)
+ if !ok {
return fmt.Errorf("unsupported table game %q", c.Game)
}
+ return def.validate(c)
+}
+
+// normalizeBlackjackTable applies the blackjack rule defaults. It is the
+// game's Normalize entry in tableGames.
+func normalizeBlackjackTable(c TableConfig) TableConfig {
+ c.Blackjack = c.Blackjack.Normalize()
+ return c
+}
+
+// normalizeBaccaratTable applies the baccarat rule defaults. It is the game's
+// Normalize entry in tableGames.
+func normalizeBaccaratTable(c TableConfig) TableConfig {
+ c.Baccarat = c.Baccarat.Normalize()
+ return c
+}
+
+func validateBlackjackTable(c TableConfig) error {
+ 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")
+ }
+ if b.BlackjackPayout > maxBlackjackPayout {
+ return fmt.Errorf("blackjack_payout %v exceeds the %d:1 cap", b.BlackjackPayout, maxBlackjackPayout)
+ }
+ if b.InsurancePayout > maxInsurancePayout {
+ return fmt.Errorf("insurance_payout %v exceeds the %d:1 cap", b.InsurancePayout, maxInsurancePayout)
+ }
+ rules := b.Normalize()
+ if rules.AllowsSurrender() && c.Normalize().MinBet < minimumSurrenderBet {
+ return fmt.Errorf("surrender needs min_bet >= %d: a surrendered hand returns half the bet, which rounds to zero below that", minimumSurrenderBet)
+ }
+ // Shoe coverage is NOT a configuration floor. blackjackRoundCards is
+ // only a boundary heuristic (an all-ace hand can hold more than
+ // eleven cards), so a hard floor would reject tables that play fine
+ // and, because a room is refused whole on any error, would darken
+ // every casino game in it. shoe.ensureForRound reshuffles at the
+ // round boundary and draw()'s exhausted-shoe rebuild is a deliberate
+ // shuffle-through instead.
+ return nil
+}
+
+func validateBaccaratTable(c TableConfig) error {
+ 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")
+ }
+ if b.TiePayout > maxTiePayout {
+ return fmt.Errorf("tie_payout %v exceeds the %d:1 cap", b.TiePayout, maxTiePayout)
+ }
+ if b.BankerPayout > maxBankerPayout {
+ return fmt.Errorf("banker_payout %v exceeds the %v:1 cap", b.BankerPayout, maxBankerPayout)
+ }
+ // Non-blocking for the same reason as blackjack above.
+ return nil
}
func validateIdentityAndBets(id string, minBet, maxBet int) error {
@@ -234,6 +255,9 @@ func (p SlotProfile) Validate() error {
// validation and for newMachine.
func machineSlotProfile(c MachineConfig) SlotProfile {
profile := DefaultHuffAndPuffProfile()
+ if factory, ok := slotVariants[c.Variant]; ok {
+ profile = factory()
+ }
if c.Payout > 0 {
profile = ScaleToRTP(profile, c.Payout)
}
@@ -277,12 +301,25 @@ func ValidateRoomConfigs(tables []TableConfig, machines []MachineConfig) error {
check("table", c.ID, c.Validate())
checkAddressable(strings.ToLower(string(c.Game)), c.ID)
}
+ // A machine addresses by its variant when one is declared; a bare game
+ // name is addressable only when exactly one machine in the room declares
+ // it, so a room may host several variants of one game (and the validator
+ // no longer rejects the second variant as a duplicate game).
+ machineGameCount := make(map[string]int, len(machines))
+ for _, c := range machines {
+ machineGameCount[strings.ToLower(string(c.Game))]++
+ }
for _, c := range machines {
check("machine", c.ID, c.Validate())
- checkAddressable(strings.ToLower(string(c.Game)), c.ID)
if c.Variant != "" {
checkAddressable(strings.ToLower(c.Variant), c.ID)
}
+ // A variant-less machine is addressed by its game name, so two of them
+ // collide; a variant-ful machine only claims the bare game name when it
+ // is the sole machine that declares it.
+ if c.Variant == "" || machineGameCount[strings.ToLower(string(c.Game))] == 1 {
+ checkAddressable(strings.ToLower(string(c.Game)), c.ID)
+ }
}
return errors.Join(errs...)
}