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
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
|
package casino
import (
"fmt"
"strconv"
"strings"
)
// rouletteShare is a total-return fraction: a winning pocket pays
// stake*num/den credits, floor-rounded like a house paying in chips.
// Ordinary bets have den 1. Call bets split the stake across several chips,
// so their shares carry a denominator; a pocket covered by more than one
// component (for example 17 in Orphelins) sums those component returns into
// one share.
type rouletteShare struct {
num int
den int
}
// payout returns floor(stake*num/den) and refuses a wager whose return would
// exceed the economy's credit cap, without overflowing stake*num.
func (s rouletteShare) payout(stake int) (int, error) {
if stake <= 0 || s.num <= 0 || s.den <= 0 {
return 0, nil
}
if limit := ((MaxCredits+1)*s.den - 1) / s.num; stake > limit {
return MaxCredits, fmt.Errorf("payout overflow: %d * %d/%d exceeds %d", stake, s.num, s.den, MaxCredits)
}
return stake * s.num / s.den, nil
}
// rouletteWager is a resolved wager: every covered pocket and its total-return
// share. A wager can cover one pocket (straight), many pockets at one rate
// (outside or inside combination), or several chip components at different
// rates (the racetrack call bets).
type rouletteWager struct {
shares map[string]rouletteShare
}
func (w rouletteWager) covers(pocket string) (rouletteShare, bool) {
share, ok := w.shares[pocket]
return share, ok
}
// validate verifies that every possible winning pocket pays a representable
// amount for this stake before the wager is committed.
func (w rouletteWager) validate(stake int) error {
for _, share := range w.shares {
if _, err := share.payout(stake); err != nil {
return err
}
}
return nil
}
// rouletteComponent is one chip group inside a call bet: the pockets it covers
// and the total-return multiplier (stake included) paid when one of them hits.
type rouletteComponent struct {
pockets []string
chips int
mult int
}
// rouletteCallBet is a fixed French/racetrack bet. The series is the display
// order (wheel order) and parts are the actual chip placement.
type rouletteCallBet struct {
canonical string
name string
chips int
series []string
parts []rouletteComponent
}
// RacetrackBetView is one call bet for the presentation layer.
type RacetrackBetView struct {
Name string
Syntax string
Chips int
Series []string
}
// RacetrackBets returns the fixed French call bets in display order. The
// numbers are shown for reference; the actual wagers are the named bets.
func RacetrackBets() []RacetrackBetView {
views := make([]RacetrackBetView, 0, len(rouletteCallBets))
for _, bet := range rouletteCallBets {
views = append(views, RacetrackBetView{
Name: bet.name,
Syntax: bet.canonical,
Chips: bet.chips,
Series: append([]string(nil), bet.series...),
})
}
return views
}
// The four standard single-zero call bets. Chip placements and payouts follow
// the traditional layout: Voisins (9 chips), Tiers (6), Orphelins (5) and
// Jeu Zero (4). Every inside component returns 36/covered, so the combined
// wager keeps the wheel's normal house edge before whole-credit rounding.
var rouletteCallBets = []rouletteCallBet{
{
canonical: "voisins",
name: "Voisins du Zero",
chips: 9,
series: []string{"22", "18", "29", "7", "28", "12", "35", "3", "26", "0", "32", "15", "19", "4", "21", "2", "25"},
parts: []rouletteComponent{
{pockets: []string{"0", "2", "3"}, chips: 2, mult: 12}, // trio
{pockets: []string{"4", "7"}, chips: 1, mult: 18},
{pockets: []string{"12", "15"}, chips: 1, mult: 18},
{pockets: []string{"18", "21"}, chips: 1, mult: 18},
{pockets: []string{"19", "22"}, chips: 1, mult: 18},
{pockets: []string{"25", "26", "28", "29"}, chips: 2, mult: 9}, // corner
{pockets: []string{"32", "35"}, chips: 1, mult: 18},
},
},
{
canonical: "tiers",
name: "Tiers du Cylindre",
chips: 6,
series: []string{"27", "13", "36", "11", "30", "8", "23", "10", "5", "24", "16", "33"},
parts: []rouletteComponent{
{pockets: []string{"5", "8"}, chips: 1, mult: 18},
{pockets: []string{"10", "11"}, chips: 1, mult: 18},
{pockets: []string{"13", "16"}, chips: 1, mult: 18},
{pockets: []string{"23", "24"}, chips: 1, mult: 18},
{pockets: []string{"27", "30"}, chips: 1, mult: 18},
{pockets: []string{"33", "36"}, chips: 1, mult: 18},
},
},
{
canonical: "orphelins",
name: "Orphelins",
chips: 5,
series: []string{"17", "34", "6", "1", "20", "14", "31", "9"},
parts: []rouletteComponent{
{pockets: []string{"1"}, chips: 1, mult: 36},
{pockets: []string{"6", "9"}, chips: 1, mult: 18},
{pockets: []string{"14", "17"}, chips: 1, mult: 18},
{pockets: []string{"17", "20"}, chips: 1, mult: 18},
{pockets: []string{"31", "34"}, chips: 1, mult: 18},
},
},
{
canonical: "jeuzero",
name: "Jeu Zero",
chips: 4,
series: []string{"12", "35", "3", "26", "0", "32", "15"},
parts: []rouletteComponent{
{pockets: []string{"0", "3"}, chips: 1, mult: 18},
{pockets: []string{"12", "15"}, chips: 1, mult: 18},
{pockets: []string{"32", "35"}, chips: 1, mult: 18},
{pockets: []string{"26"}, chips: 1, mult: 36},
},
},
}
// rouletteCallAliases maps the many spellings players use onto a canonical
// call bet. Accents are stripped before lookup.
var rouletteCallAliases = map[string]string{
"voisins": "voisins",
"voisins du zero": "voisins",
"grands voisins": "voisins",
"grands voisins du zero": "voisins",
"vdz": "voisins",
"tiers": "tiers",
"tiers du cylindre": "tiers",
"le tiers du cylindre": "tiers",
"tdc": "tiers",
"small series": "tiers",
"orphelins": "orphelins",
"orphans": "orphelins",
"les orphelins": "orphelins",
"zero": "jeuzero",
"jeu zero": "jeuzero",
"jeuzero": "jeuzero",
}
// neighborsRadius is how many pockets on each side of the chosen number the
// neighbours bet includes: the number plus four either side, nine in total.
const neighborsRadius = 4
// wager converts a call bet's chip placement into per-pocket return shares,
// summing shares where components overlap (17 in Orphelins).
func (b rouletteCallBet) wager() rouletteWager {
shares := map[string]rouletteShare{}
for _, part := range b.parts {
for _, pocket := range part.pockets {
share := shares[pocket]
shares[pocket] = rouletteShare{num: share.num + part.chips*part.mult, den: b.chips}
}
}
return rouletteWager{shares: shares}
}
// normalizeRacetrackName lowercases, strips accents and collapses whitespace so
// "Voisins du Zéro" and "voisins du zero" resolve the same way.
func normalizeRacetrackName(spot string) string {
name := strings.ToLower(strings.TrimSpace(spot))
name = strings.NewReplacer("é", "e", "è", "e", "ê", "e", "à", "a", "ô", "o", "û", "u").Replace(name)
return strings.Join(strings.Fields(name), " ")
}
// rouletteRacetrackWager resolves a call bet or a neighbours bet for a
// single-zero table whose racetrack is enabled.
func rouletteRacetrackWager(spot string, double bool) (rouletteWager, bool) {
if double {
return rouletteWager{}, false
}
if canonical, ok := rouletteCallAliases[normalizeRacetrackName(spot)]; ok {
for _, bet := range rouletteCallBets {
if bet.canonical == canonical {
return bet.wager(), true
}
}
}
return rouletteNeighborsWager(spot, double)
}
// rouletteNeighborsWager covers the chosen pocket and the four pockets on each
// side of it in wheel order, one chip each (nine chips, a uniform 3:1 return).
// It accepts "neighbors 17", "neighbors of 17", "17 neighbors" and the British
// "neighbours" spelling.
func rouletteNeighborsWager(spot string, double bool) (rouletteWager, bool) {
if double {
return rouletteWager{}, false
}
keyword := false
number := ""
numbers := 0
for _, field := range strings.Fields(strings.ToLower(spot)) {
switch field {
case "neighbor", "neighbors", "neighbour", "neighbours":
keyword = true
continue
case "of", "the", "around":
continue
}
pocket, ok := roulettePocketToken(field, double)
if !ok {
return rouletteWager{}, false
}
number = pocket
numbers++
}
if !keyword || numbers != 1 || number == "" {
return rouletteWager{}, false
}
wheel := rouletteWheel(false)
index := -1
for i, pocket := range wheel {
if pocket == number {
index = i
break
}
}
if index < 0 {
return rouletteWager{}, false
}
shares := map[string]rouletteShare{}
for d := -neighborsRadius; d <= neighborsRadius; d++ {
pocket := wheel[(index+d+len(wheel))%len(wheel)]
shares[pocket] = rouletteShare{num: 36, den: 2*neighborsRadius + 1}
}
return rouletteWager{shares: shares}, true
}
// rouletteWagerFor resolves any roulette target into a wager. Call bets and
// neighbours are reachable only on a single-zero table with the racetrack
// enabled; every other target uses the ordinary uniform bets.
func rouletteWagerFor(spot string, double, racetrack bool) (rouletteWager, bool) {
if racetrack && !double {
if wager, ok := rouletteRacetrackWager(spot, double); ok {
return wager, true
}
}
pockets, mult := rouletteBet(spot, double)
if mult == 0 {
return rouletteWager{}, false
}
shares := make(map[string]rouletteShare, len(pockets))
for _, pocket := range pockets {
shares[pocket] = rouletteShare{num: mult, den: 1}
}
return rouletteWager{shares: shares}, true
}
// formatShare renders a share for diagnostics and tests without floating
// point.
func formatShare(share rouletteShare) string {
if share.den == 1 {
return strconv.Itoa(share.num)
}
return fmt.Sprintf("%d/%d", share.num, share.den)
}
|