package casino import ( "fmt" "math/rand" ) // tableGameDef is the single registration point for a multiplayer table game. // A game's validation, rule defaults, and constructor live together here, so // adding a game is one entry instead of edits to three independent switches // (TableConfig.Validate, TableConfig.Normalize, and newTable) that could // silently drift apart. The rule structs stay on TableConfig; this does not // introduce a config union. type tableGameDef struct { // validate checks a raw (pre-Normalize) table config. validate func(TableConfig) error // normalize applies this game's rule defaults to a table config. normalize func(TableConfig) TableConfig // ruleBlockName is the YAML rule block this game owns (for error messages), // and hasRuleBlock reports whether that block carries any explicit value. // Together they let TableConfig.Validate reject a block authored for a // different game without a hand-maintained switch. ruleBlockName string hasRuleBlock func(TableConfig) bool // new builds the game. It is called only after validate passes and the // shared table config has been normalized. new func(roomID int, cfg TableConfig, rng *rand.Rand) tableGame } // tableGames is populated in init, not in a var initializer: the constructors // transitively call TableConfig.Normalize, which consults tableGames, and Go // rejects that as an initialization cycle when the map is a literal. var tableGames map[GameName]tableGameDef func init() { tableGames = map[GameName]tableGameDef{ GameCraps: { validate: validateCraps, normalize: func(c TableConfig) TableConfig { c.Craps = c.Craps.Normalize(); return c }, ruleBlockName: "craps", hasRuleBlock: func(c TableConfig) bool { return c.Craps != (CrapsConfig{}) }, new: func(room int, c TableConfig, rng *rand.Rand) tableGame { return newCrapsTable(room, c, rng) }, }, GameRoulette: { validate: func(c TableConfig) error { if c.Roulette.SpinTicks < 0 || c.Roulette.SpinTicks > maxSpinTicks { return fmt.Errorf("roulette spin_ticks must be between 0 and %d", maxSpinTicks) } if c.Roulette.Racetrack && c.Roulette.DoubleZero { return fmt.Errorf("roulette racetrack is only available on single-zero tables; remove racetrack or set double_zero: false") } return nil }, normalize: func(c TableConfig) TableConfig { if c.Roulette.SpinTicks == 0 { c.Roulette.SpinTicks = 3 } return c }, ruleBlockName: "roulette", hasRuleBlock: func(c TableConfig) bool { return c.Roulette != (RouletteConfig{}) }, new: func(roomID int, cfg TableConfig, rng *rand.Rand) tableGame { return newRouletteTable(roomID, cfg, rng) }, }, GameBlackjack: { validate: validateBlackjackTable, normalize: normalizeBlackjackTable, ruleBlockName: "blackjack", hasRuleBlock: hasBlackjackRuleBlock, new: func(roomID int, cfg TableConfig, rng *rand.Rand) tableGame { return newBlackjackTable(roomID, cfg, rng) }, }, GameBaccarat: { validate: validateBaccaratTable, normalize: normalizeBaccaratTable, ruleBlockName: "baccarat", hasRuleBlock: hasBaccaratRuleBlock, new: func(roomID int, cfg TableConfig, rng *rand.Rand) tableGame { return newBaccaratTable(roomID, cfg, rng) }, }, } } // tableGameDefFor returns the registration for a game, or false when the game // has no multiplayer table implementation. func tableGameDefFor(game GameName) (tableGameDef, bool) { def, ok := tableGames[game] return def, ok } // slotVariants maps a machine variant name to the profile that variant plays. // This is the content seam for a new slot cabinet: a variant is a profile // lookup, not a new code path through Validate and the machine constructor. // The map is shared by MachineConfig.Validate and machineSlotProfile so the // accepted set and the played set cannot disagree. var slotVariants = map[string]func() SlotProfile{ "huff_and_even_more_puff": DefaultHuffAndPuffProfile, }