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 ", "odds dontpass", "odds dontcome ": return edge(0) case "buy ", "lay ": 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 ": 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<