aboutsummaryrefslogtreecommitdiff
path: root/internal/casino/house_edge_craps.go
blob: 06a6629ace3c3cd0c6da57b985377d6ea1d8bccf (plain)
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
package casino

import (
	"strconv"
	"strings"
)

func crapsHouseEdge(cfg TableConfig, bet string) HouseEdge {
	c := cfg.Craps.Normalize()
	switch bet {
	case "pass", "come", "dontpass", "dontcome":
		return edge(-crapsContractEV(c, bet, 0))
	case "odds pass", "odds come <n>", "odds dontpass", "odds dontcome <n>":
		return edge(0)
	case "buy <n>", "lay <n>":
		e := HouseEdge{Note: "5% commission on wins, before upward credit rounding; small stakes cost more"}
		for _, n := range []int{2, 3, 4, 5, 6, 8, 9, 10, 11, 12} {
			if !c.number(n) {
				continue
			}
			// Buy: 5% of stake times P(number before 7).
			// Lay: 5% of true-odds profit times P(7 before number).
			// Both reduce to .05 * ways(number)/(ways(number)+6).
			w := float64(diceWays(n))
			e.Parts = append(e.Parts, EdgePart{strconv.Itoa(n), .05 * w / (w + 6)})
		}
		return e
	case "hard 4 / hard 10":
		return edge(-crapsResolvedEV(c, "hard", 4))
	case "hard 6 / hard 8":
		return edge(-crapsResolvedEV(c, "hard", 6))
	case "eleven / ace-deuce":
		return edge(-crapsResolvedEV(c, "eleven", 0))
	case "aces / boxcars":
		return edge(-crapsResolvedEV(c, "aces", 0))
	case "hop <a> <b>":
		return HouseEdge{Parts: []EdgePart{{"unequal dice", -crapsResolvedEV(c, "hop", 12)}, {"doubles", -crapsResolvedEV(c, "hop", 11)}}}
	case "small / tall":
		return edge(1 - 35*crapsCollectProbability([]int{2, 3, 4, 5, 6}))
	case "all":
		return edge(1 - 176*crapsCollectProbability([]int{2, 3, 4, 5, 6, 8, 9, 10, 11, 12}))
	case "fire":
		return edge(1 - crapsFireReturn())
	case "field", "seven", "craps", "horn":
		return edge(-crapsResolvedEV(c, bet, 0))
	}
	if strings.HasPrefix(bet, "place ") {
		n, err := strconv.Atoi(strings.TrimPrefix(bet, "place "))
		if err == nil && c.number(n) {
			return edge(-crapsResolvedEV(c, "place", n))
		}
	}
	return unknownEdge("unrecognized craps target")
}

func diceWays(n int) int {
	if n < 2 || n > 12 {
		return 0
	}
	if n > 7 {
		return 13 - n
	}
	return n - 1
}

// Enumerate the actual outcome function, skipping neutral rolls. Off-on-
// come-out wagers have the same eventual EV when left working to resolution.
func crapsResolvedEV(c CrapsConfig, kind string, n int) float64 {
	sum, resolved := 0.0, 0.0
	for a := 1; a <= 6; a++ {
		for b := 1; b <= 6; b++ {
			ok, x, y := c.outcome(kind, n, a, b, 6)
			if ok {
				sum += float64(x) / float64(y)
				resolved++
			}
		}
	}
	return sum / resolved
}

// A contract starts at n=0 and travels once to its personal point. Self
// loops (neutral dice or high-point come-out 2/3) are eliminated exactly.
func crapsContractEV(c CrapsConfig, kind string, n int) float64 {
	sum, effective := 0.0, 0.0
	for a := 1; a <= 6; a++ {
		for b := 1; b <= 6; b++ {
			if c.variant() == "die_rich" && b != 1 {
				continue
			}
			bb := b
			if c.variant() == "die_rich" {
				bb = 0
			}
			ok, x, y := c.outcome(kind, n, a, bb, 6)
			if ok {
				sum += float64(x) / float64(y)
				effective++
				continue
			}
			if n == 0 && c.pointNumber(a+bb) {
				sum += crapsContractEV(c, kind, a+bb)
				effective++
			}
		}
	}
	return sum / effective
}

// Probability of collecting all listed totals before ANY seven. All other
// rolls are self loops. This also accounts for duplicate rolls correctly.
func crapsCollectProbability(numbers []int) float64 {
	full := (1 << len(numbers)) - 1
	memo := make(map[int]float64)
	var visit func(int) float64
	visit = func(mask int) float64 {
		if mask == full {
			return 1
		}
		if p, ok := memo[mask]; ok {
			return p
		}
		numerator, denominator := 0.0, 6.0
		for i, n := range numbers {
			if mask&(1<<i) != 0 {
				continue
			}
			w := float64(diceWays(n))
			numerator += w * visit(mask|(1<<i))
			denominator += w
		}
		memo[mask] = numerator / denominator
		return memo[mask]
	}
	return visit(0)
}