aboutsummaryrefslogtreecommitdiff
path: root/internal/casino/roulette.go
diff options
context:
space:
mode:
Diffstat (limited to 'internal/casino/roulette.go')
-rw-r--r--internal/casino/roulette.go376
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