diff options
| author | historia <[not public]> | 2026-09-29 16:50:40 -0400 |
|---|---|---|
| committer | historia <[not public]> | 2026-09-29 16:50:40 -0400 |
| commit | 00fe5f7ead43627fa283035b27365924b76c5a0f (patch) | |
| tree | 07546518a51aaa037d64813f40eb6dc4596bd9cb /internal/casino/roulette.go | |
| parent | bfe22e10b5b9f2c7039d1d67021229659a06a226 (diff) | |
| download | thehouseoficarus-00fe5f7ead43627fa283035b27365924b76c5a0f.tar.gz | |
feat: roulette bets street, corner, split, etc.
Diffstat (limited to 'internal/casino/roulette.go')
| -rw-r--r-- | internal/casino/roulette.go | 376 |
1 files changed, 375 insertions, 1 deletions
diff --git a/internal/casino/roulette.go b/internal/casino/roulette.go index 5784ac9..cae9d0d 100644 --- a/internal/casino/roulette.go +++ b/internal/casino/roulette.go @@ -56,9 +56,24 @@ func normalizeRouletteSpot(spot string) string { } // Returns the covered pockets and total-return multiplier (stake included). -// Zero never belongs to an outside bet. +// Zero never belongs to an outside bet. Combination bets (splits, streets, +// corners, six-lines, trios and the basket) are parsed from a compact number +// list: the leading keyword is optional because the covered set uniquely +// identifies the bet. func rouletteBet(spot string, double bool) ([]string, int) { spot = normalizeRouletteSpot(spot) + if pockets, mult := rouletteStraightBet(spot, double); mult > 0 { + return pockets, mult + } + if pockets, mult := rouletteOutsideBet(spot); mult > 0 { + return pockets, mult + } + return rouletteCombinationBet(spot, double) +} + +// rouletteStraightBet covers a single pocket: any number, 0, or 00 (American +// tables only). It pays 35:1, a 36x total return. +func rouletteStraightBet(spot string, double bool) ([]string, int) { if spot == "00" { if double { return []string{spot}, 36 @@ -68,6 +83,12 @@ func rouletteBet(spot string, double bool) ([]string, int) { if n, err := strconv.Atoi(spot); err == nil && n >= 0 && n <= 36 && strconv.Itoa(n) == spot { return []string{spot}, 36 } + return nil, 0 +} + +// rouletteOutsideBet covers the even-money, dozen, and row wagers, whose +// pocket counts all divide 36 exactly, so the return is 36/len. +func rouletteOutsideBet(spot string) ([]string, int) { var pockets []string for n := 1; n <= 36; n++ { hit := false @@ -107,6 +128,359 @@ func rouletteBet(spot string, double bool) ([]string, int) { return pockets, 36 / len(pockets) } +// rouletteComboKeywords maps the accepted names for a combination bet onto +// its canonical kind. Several names share a kind ("square" is a corner, +// "sixline"/"double street" is a line). +var rouletteComboKeywords = map[string]string{ + "split": "split", + "street": "street", + "corner": "corner", + "square": "corner", + "line": "line", + "sixline": "line", + "six-line": "line", + "doublestreet": "line", + "double-street": "line", + "trio": "trio", + "basket": "basket", +} + +// Payout returns are 36/covered for every combination except the five-number +// American basket, which pays a deliberately short 6:1 (7x total). +func rouletteCombinationBet(spot string, double bool) ([]string, int) { + kind, nums, ok := parseRouletteCombination(spot, double) + if !ok { + return nil, 0 + } + if kind == "" { + kind = inferRouletteCombination(nums) + } + switch kind { + case "split": + if len(nums) == 2 && rouletteAdjacent(nums[0], nums[1], double) { + return nums, 18 + } + case "street": + if rouletteIsStreet(nums) { + return nums, 12 + } + case "corner": + if rouletteIsCorner(nums, double) { + return nums, 9 + } + case "line": + if rouletteIsLine(nums) { + return nums, 6 + } + case "trio": + if rouletteIsTrio(nums, double) { + return nums, 12 + } + case "basket": + if pockets, mult, valid := rouletteBasket(nums, double); valid { + return pockets, mult + } + } + return nil, 0 +} + +// parseRouletteCombination strips an optional leading keyword and converts the +// remaining number tokens (separated by spaces, dashes, slashes, commas or +// backslashes) into canonical pocket names. Duplicate numbers are rejected. +func parseRouletteCombination(spot string, double bool) (string, []string, bool) { + fields := strings.Fields(strings.ToLower(spot)) + if len(fields) == 0 { + return "", nil, false + } + kind := "" + switch fields[0] { + case "split", "street", "corner", "square", "line", "sixline", "six-line", "doublestreet", "double-street", "trio", "basket": + kind = fields[0] + fields = fields[1:] + case "six": + if len(fields) > 1 && fields[1] == "line" { + kind, fields = "sixline", fields[2:] + } + case "double": + if len(fields) > 1 && fields[1] == "street" { + kind, fields = "doublestreet", fields[2:] + } + } + if canonical, ok := rouletteComboKeywords[kind]; ok { + kind = canonical + } else if kind != "" { + return "", nil, false + } + if kind == "basket" && len(fields) == 0 { + return "basket", nil, true + } + if len(fields) == 0 { + return "", nil, false + } + raw := strings.NewReplacer("-", " ", "/", " ", ",", " ", "\\", " ").Replace(strings.Join(fields, " ")) + seen := map[string]bool{} + var nums []string + for _, tok := range strings.Fields(raw) { + pocket, ok := roulettePocketToken(tok, double) + if !ok || seen[pocket] { + return "", nil, false + } + seen[pocket] = true + nums = append(nums, pocket) + } + if len(nums) == 0 { + return "", nil, false + } + return kind, nums, true +} + +// roulettePocketToken canonicalizes one number token: a plain 0-36 value or +// 00, rejecting non-canonical spellings such as "01", "+1" or "000". +func roulettePocketToken(tok string, double bool) (string, bool) { + if tok == "00" { + return "00", double + } + n, err := strconv.Atoi(tok) + if err != nil || n < 0 || n > 36 || strconv.Itoa(n) != tok { + return "", false + } + return strconv.Itoa(n), true +} + +// inferRouletteCombination identifies a bare number list by its size. Adjacency +// and geometry are validated after this, so an ambiguous count still fails. +func inferRouletteCombination(nums []string) string { + switch len(nums) { + case 2: + return "split" + case 3: + if rouletteIsStreet(nums) { + return "street" + } + return "trio" + case 4: + return "corner" + case 5: + return "basket" + case 6: + return "line" + } + return "" +} + +// rouletteNumberCell maps 1-36 onto its column and row on the felt: column +// 0-11 left to right, row 0-2 bottom to top (row 0 holds 1,4,7,...). +func rouletteNumberCell(n string) (col, row int, ok bool) { + v, err := strconv.Atoi(n) + if err != nil || v < 1 || v > 36 { + return 0, 0, false + } + return (v - 1) / 3, (v - 1) % 3, true +} + +// rouletteAdjacent reports whether two pockets share an edge: horizontally +// (same row, neighboring columns) or vertically (same column, neighboring +// rows). Zero and double zero use the felt's special zero edge. +func rouletteAdjacent(a, b string, double bool) bool { + if a == "0" || a == "00" || b == "0" || b == "00" { + return rouletteZeroAdjacent(a, b, double) + } + ac, ar, ok := rouletteNumberCell(a) + if !ok { + return false + } + bc, br, ok := rouletteNumberCell(b) + if !ok { + return false + } + dc, dr := ac-bc, ar-br + if dc < 0 { + dc = -dc + } + if dr < 0 { + dr = -dr + } + return (dc == 1 && dr == 0) || (dc == 0 && dr == 1) +} + +// The zero lane spans all three numbered rows, so 0 splits with 1, 2 and 3. +// On an American felt the two zeros are adjacent to each other as well. +func rouletteZeroAdjacent(a, b string, double bool) bool { + neighbors := func(other string) bool { + if other == "0" || other == "00" { + return double + } + return other == "1" || other == "2" || other == "3" + } + if a == "0" || a == "00" { + return neighbors(b) + } + return neighbors(a) +} + +func rouletteIsStreet(nums []string) bool { + if len(nums) != 3 { + return false + } + set := map[int]bool{} + for _, s := range nums { + v, err := strconv.Atoi(s) + if err != nil { + return false + } + set[v] = true + } + if len(set) != 3 { + return false + } + for n := 1; n <= 34; n += 3 { + if set[n] && set[n+1] && set[n+2] { + return true + } + } + return false +} + +func rouletteIsCorner(nums []string, double bool) bool { + if len(nums) != 4 { + return false + } + // The European "first four" (0,1,2,3) is the only zero corner. + if !double && rouletteIsFirstFour(nums) { + return true + } + cells := map[[2]int]bool{} + for _, s := range nums { + c, r, ok := rouletteNumberCell(s) + if !ok { + return false + } + cells[[2]int{c, r}] = true + } + if len(cells) != 4 { + return false + } + cols, rows := map[int]bool{}, map[int]bool{} + for cell := range cells { + cols[cell[0]] = true + rows[cell[1]] = true + } + if len(cols) != 2 || len(rows) != 2 { + return false + } + for c := range cols { + for r := range rows { + if !cells[[2]int{c, r}] { + return false + } + } + } + return true +} + +func rouletteIsFirstFour(nums []string) bool { + if len(nums) != 4 { + return false + } + set := map[string]bool{} + for _, s := range nums { + set[s] = true + } + return set["0"] && set["1"] && set["2"] && set["3"] +} + +// A six-line is two neighboring columns, i.e. six numbers across all three +// rows of two adjacent streets. +func rouletteIsLine(nums []string) bool { + if len(nums) != 6 { + return false + } + cols := map[int]bool{} + set := map[int]bool{} + for _, s := range nums { + c, _, ok := rouletteNumberCell(s) + if !ok { + return false + } + v, _ := strconv.Atoi(s) + cols[c] = true + set[v] = true + } + if len(cols) != 2 || len(set) != 6 { + return false + } + var c1, c2 int + first := true + for c := range cols { + if first { + c1, first = c, false + } else { + c2 = c + } + } + if c1 > c2 { + c1, c2 = c2, c1 + } + return c2-c1 == 1 +} + +// A trio is any three distinct pockets drawn from 0, 00 and the first street +// (1,2,3) that includes at least one zero. It pays the same 11:1 as a street. +func rouletteIsTrio(nums []string, double bool) bool { + if len(nums) != 3 { + return false + } + hasZero := false + for _, s := range nums { + switch s { + case "0": + hasZero = true + case "00": + if !double { + return false + } + hasZero = true + default: + v, err := strconv.Atoi(s) + if err != nil || v < 1 || v > 3 { + return false + } + } + } + return hasZero +} + +// The basket is the American five-number 0/00/1/2/3 bet, which pays a short +// 6:1. On a single-zero felt the name covers the European first four instead. +// An empty list means the bare "basket" keyword. +func rouletteBasket(nums []string, double bool) ([]string, int, bool) { + if len(nums) == 0 { + if double { + return []string{"0", "00", "1", "2", "3"}, 7, true + } + return []string{"0", "1", "2", "3"}, 9, true + } + if double { + set := map[string]bool{} + for _, s := range nums { + set[s] = true + } + if len(nums) != 5 { + return nil, 0, false + } + for _, need := range []string{"0", "00", "1", "2", "3"} { + if !set[need] { + return nil, 0, false + } + } + return []string{"0", "00", "1", "2", "3"}, 7, true + } + if rouletteIsFirstFour(nums) { + return []string{"0", "1", "2", "3"}, 9, true + } + return nil, 0, false +} + type roulettePlayer struct { Participant lastSpot string |
