package casino import ( "fmt" "math/rand" ) // tableGameDef is the single registration point for a multiplayer table game. // A game's validation, rule defaults, constructor, help topic, and rule text // live together here, so adding a game is one entry instead of edits to // many independent switches (TableConfig.Validate, TableConfig.Normalize, // newTable, RulesText, and the presentation router) that could silently drift // apart. The rule structs stay on TableConfig; this does not introduce a // config union. type tableGameDef struct { // houseEdge analyzes this game's own configured wagers; nil means unverified, // never zero. See house_edge.go for the loss/stake convention. houseEdge func(TableConfig, string) HouseEdge // 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 // ruleText names the shared rule text builder (TableConfig.RulesText // dispatch, keeping the "rules" wording with the game's registration). ruleText func(TableConfig) string // helpTopic names the help page "rules" opens at this table. A game with // per-edition pages derives them from its config (craps variants). helpTopic func(TableConfig) string } // 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{ GamePaiGow: { houseEdge: paiGowHouseEdge, validate: validatePaiGow, normalize: func(c TableConfig) TableConfig { c.PaiGow = c.PaiGow.Normalize() c.MaxPlayers = min(c.MaxPlayers, 6) c.MaxBet = min(c.MaxBet, pgMaxWager) return c }, ruleBlockName: "pai_gow", hasRuleBlock: func(c TableConfig) bool { return c.PaiGow != (PaiGowConfig{}) }, new: newPaiGowTable, ruleText: paiGowRules, helpTopic: func(TableConfig) string { return "pai gow poker" }, }, GameVideoPoker: { houseEdge: videoPokerHouseEdge, validate: func(c TableConfig) error { if c.MaxPlayers > 1 { return fmt.Errorf("video poker cabinets have one seat") } return c.VideoPoker.Validate() }, normalize: func(c TableConfig) TableConfig { c.VideoPoker = c.VideoPoker.Normalize(); c.MaxPlayers = 1; return c }, ruleBlockName: "video_poker", hasRuleBlock: func(c TableConfig) bool { return c.VideoPoker != (VideoPokerConfig{}) }, new: newVideoPokerTable, ruleText: func(c TableConfig) string { return pokerRules(c.VideoPoker) }, helpTopic: func(c TableConfig) string { return pokerHelpTopic(c.VideoPoker) }, }, GameCraps: { houseEdge: crapsHouseEdge, 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) }, ruleText: func(c TableConfig) string { return c.CrapsRulesText() }, helpTopic: func(c TableConfig) string { return c.Craps.helpTopic() }, }, GameBigSix: { houseEdge: bigSixHouseEdge, validate: func(c TableConfig) error { if c.BettingTicks != 0 && c.BettingTicks != 25 { return fmt.Errorf("Big 6 betting_ticks must be 25") } return nil }, normalize: func(c TableConfig) TableConfig { c.BettingTicks = 25; return c }, hasRuleBlock: func(TableConfig) bool { return false }, new: func(room int, c TableConfig, rng *rand.Rand) tableGame { return newBigSixTable(room, c, rng) }, ruleText: bigSixRules, helpTopic: func(TableConfig) string { return "big6" }, }, GameRoulette: { houseEdge: rouletteHouseEdge, 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 = 8 } 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) }, ruleText: rouletteRulesText, helpTopic: func(TableConfig) string { return "roulette" }, }, GameBlackjack: { houseEdge: blackjackHouseEdge, validate: validateBlackjackTable, normalize: normalizeBlackjackTable, ruleBlockName: "blackjack", hasRuleBlock: hasBlackjackRuleBlock, new: func(roomID int, cfg TableConfig, rng *rand.Rand) tableGame { return newBlackjackTable(roomID, cfg, rng) }, ruleText: func(c TableConfig) string { return c.BlackjackRulesText() }, helpTopic: func(TableConfig) string { return "blackjack" }, }, GameBaccarat: { houseEdge: baccaratHouseEdge, validate: validateBaccaratTable, normalize: normalizeBaccaratTable, ruleBlockName: "baccarat", hasRuleBlock: hasBaccaratRuleBlock, new: func(roomID int, cfg TableConfig, rng *rand.Rand) tableGame { return newBaccaratTable(roomID, cfg, rng) }, ruleText: func(c TableConfig) string { return c.BaccaratRulesText() }, helpTopic: func(c TableConfig) string { switch c.Baccarat.Normalize().Variant { case BaccaratSuper6: return "super 6 baccarat" case BaccaratEZ: return "ez baccarat" default: return "baccarat" } }, }, } } // 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, } // HelpTopic is the help page the "rules" command opens while seated at this // table: a variant's own short page where the game has one (Simplified Craps, // Super 6 …), the game's main page otherwise. func (c TableConfig) HelpTopic() string { if def, ok := tableGameDefFor(c.Game); ok && def.helpTopic != nil { return def.helpTopic(c) } return string(c.Game) } // rulesTextOnTable is the shared route from a table config to its rule text. func rulesTextOnTable(cfg TableConfig) string { if def, ok := tableGameDefFor(cfg.Game); ok && def.ruleText != nil { return def.ruleText(cfg) } return cfg.Label() } // rouletteRulesText is roulette's table rules text: the payout table is // drawn by the presentation layer (the payout reference needs the paying // game's felt), so the engine text covers the fixed odds and table facts. func rouletteRulesText(c TableConfig) string { rules := c.Label() + "\nStraight numbers pay 35:1; splits 17:1; streets and trios 11:1; corners 8:1; lines 5:1.\n" + "1to12/13to24/25to36 and rows 2:1; other outside bets 1:1; the American basket pays 6:1.\n" + "Combos: bet 10 on split 1 2 | bet 10 on street 1 2 3 | bet 10 on corner 1 2 4 5 |\n" + "bet 10 on line 1 2 3 4 5 6 | bet 10 on trio 0 1 2 | bet 10 on basket.\n" if c.Roulette.Racetrack { rules += "Racetrack (single zero): bet 10 on voisins | tiers | orphelins | jeu zero | neighbors 17.\n" } return rules + "Zero pockets lose all outside bets. One wager per spin. Use help roulette." }