1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
|
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() },
},
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."
}
|