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) }