diff options
| author | historia <[not public]> | 2026-09-19 01:55:34 -0400 |
|---|---|---|
| committer | historia <[not public]> | 2026-09-19 01:55:34 -0400 |
| commit | cc33072eced3d5460512c09c42ec4531c807d7af (patch) | |
| tree | b1b451fa87fa389cb8343a3645372f3b43f5523e /internal/casino/config_validate.go | |
| parent | 2c38386484cdb507c4687caceb37e0b963e33594 (diff) | |
| download | thehouseoficarus-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.go | 137 |
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...) } |
