diff options
Diffstat (limited to 'internal/casino/house_edge_craps.go')
| -rw-r--r-- | internal/casino/house_edge_craps.go | 136 |
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) +} |
