aboutsummaryrefslogtreecommitdiff
path: root/internal/casino/house_edge_craps.go
diff options
context:
space:
mode:
Diffstat (limited to 'internal/casino/house_edge_craps.go')
-rw-r--r--internal/casino/house_edge_craps.go136
1 files changed, 136 insertions, 0 deletions
diff --git a/internal/casino/house_edge_craps.go b/internal/casino/house_edge_craps.go
new file mode 100644
index 0000000..06a6629
--- /dev/null
+++ b/internal/casino/house_edge_craps.go
@@ -0,0 +1,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)
+}