package casino import "fmt" // CrapsConfig selects explicit house rules and the shared wager limits. type CrapsConfig struct { Odds int `yaml:"odds,omitempty"` // 0 = 3-4-5x; positive = uniform Nx Field12 int `yaml:"field_12,omitempty"` Variant string `yaml:"variant,omitempty"` } func (c CrapsConfig) Normalize() CrapsConfig { if c.Field12 == 0 { c.Field12 = 2 } return c } func validateCraps(c TableConfig) error { if c.Craps.Odds < 0 || c.Craps.Odds > 1000000 { return fmt.Errorf("craps odds must be 0 (3-4-5x) or 1..1000000") } if c.Craps.Field12 != 0 && c.Craps.Field12 != 2 && c.Craps.Field12 != 3 { return fmt.Errorf("craps field_12 must be 2 or 3") } switch c.Craps.variant() { case "bank", "simplified", "high_point", "crapless", "new_york", "die_rich", "diceless": default: return fmt.Errorf("unknown craps variant %q", c.Craps.Variant) } if !c.Craps.allows("odds") && c.Craps.Odds != 0 { return fmt.Errorf("this craps variant does not support odds") } return nil } func crapsNumber(n int) bool { return n == 4 || n == 5 || n == 6 || n == 8 || n == 9 || n == 10 } func crapsTrueOdds(n int) (int, int) { switch n { case 2, 12: return 6, 1 case 3, 11: return 3, 1 case 4, 10: return 2, 1 case 5, 9: return 3, 2 default: return 6, 5 } } // crapsVigUnit is the smallest stake whose true-odds profit AND 5% commission // are both whole credits. Buy bets pay 5% of the stake, so 20 is the base; // lay bets pay 5% of the profit, so the unit scales with the lay odds. func crapsVigUnit(kind string, n int) int { if kind == "buy" { return 20 } num, den := crapsTrueOdds(n) // Lay profit is stake*den/num; commission is stake*den/(20*num). d := 20 * num return d / gcd(den, d) } func gcd(a, b int) int { for b != 0 { a, b = b, a%b } return a } // roundUpTo returns the smallest multiple of unit that is at least n. A stake // that misses the exact payout/commission unit is nudged up rather than // rejected, since single-credit differences are noise at table stakes. func roundUpTo(n, unit int) int { if unit <= 1 { return n } return ((n + unit - 1) / unit) * unit } func (c CrapsConfig) oddsLimit(n int, against bool) int { if c.Odds > 0 { return c.Odds } if c.variant() == "crapless" && !crapsNumber(n) { return 1 } if against { return 6 } switch n { case 4, 10: return 3 case 5, 9: return 4 default: return 5 } } // oddsDisplay is the felt label for the free-odds cap: a uniform Nx cap, or // the classic 3-4-5x schedule when the config leaves Odds unset. func (c CrapsConfig) oddsDisplay() string { if c.Odds > 0 { return fmt.Sprintf("%dx", c.Odds) } return "3x-4x-5x" } func (c TableConfig) CrapsRulesText() string { r := c.Craps.Normalize() if r.variant() != "bank" && r.variant() != "diceless" { return r.variantRulesText() } prefix := "" if r.variant() == "diceless" { prefix = "Diceless Craps — bank rules using cards, not dice. Two independent six-card packs labelled 1-6; draw one from each, replace and reshuffle BOTH after EVERY roll. All 36 ordered pairs are equally likely. No finite shoe or card counting.\n" } odds := "3-4-5x (don't: 6x laid)" if r.Odds > 0 { odds = fmt.Sprintf("%dx", r.Odds) } return prefix + fmt.Sprintf("Bank Craps | Free odds %s | Field 12 pays %d:1\n"+ "bet N on pass / dontpass / come / dontcome / field\n"+ "bet N on place 6 / buy 4 / lay 10 / hard 8 / hop 2 3\n"+ "bet N on seven / craps / eleven / ace-deuce / aces / boxcars / horn\n"+ "bet N on odds pass / odds come 6 (also dontpass/dontcome)\n"+ "bet N on fire / small / tall / all (before shooter's first roll only)\n"+ "Pass/come 1:1; don't bars 12. Place 4/10 9:5, 5/9 7:5, 6/8 7:6.\n"+ "Buy/lay true odds; 5%% commission on buy STAKE / lay WIN, rounded down, wins only.\n"+ "Field 2 pays 2:1; 3/4/9/10/11 pay 1:1. Hard 4/10 7:1; 6/8 9:1.\n"+ "Seven 4:1; craps 7:1; 11/3 15:1; 2/12 30:1; hop easy 15:1, hard 30:1.\n"+ "Horn splits equally across 2/3/11/12. Fire: 4/5/6 distinct made points pay 24/249/999:1.\n"+ "Small (2-6) / Tall (8-12) pay 34:1; All pays 175:1. Any 7 resets ATS.\n"+ "Place/buy/hard and come odds are OFF on come-out; lay and don't odds work.\n"+ "Stakes round up to the next exact payout/vig unit; commission never rounds up.\n"+ "Solo: roll anytime. Multiplayer: 10 ticks to bet, then 60 ticks for the shooter.\n"+ "Bets remain open until the roll. Dice rotate on seven-out.\n"+ "autoroll: repeat every 3 ticks on your turn (multiplayer waits 10 ticks).\n"+ "unbet: remove every non-contract bet and all free odds; pass/come contracts stay.\n"+ "stop cancels autoroll without leaving; in manual mode stop waits for live bets.\n"+ "Disconnect forfeits live bets.\n"+ "Winning bets return stake and are removed (no automatic pressing).", odds, r.Field12) } // outcome returns resolved and profit numerator/denominator; -1 means loss. func crapsOutcome(kind string, n, a, b, point, field12 int) (bool, int, int) { s := a + b hard := a == b switch kind { case "pass", "come", "dontpass", "dontcome": against := kind == "dontpass" || kind == "dontcome" if n == 0 { if against { switch s { case 2, 3: return true, 1, 1 case 7, 11: return true, -1, 1 case 12: return true, 0, 1 } } else { switch s { case 7, 11: return true, 1, 1 case 2, 3, 12: return true, -1, 1 } } return false, 0, 1 } if s != 7 && s != n { return false, 0, 1 } if (s == n) != against { return true, 1, 1 } return true, -1, 1 case "place", "buy", "lay": if point == 0 && kind != "lay" { return false, 0, 1 } if s != 7 && s != n { return false, 0, 1 } if (s == 7) != (kind == "lay") { return true, -1, 1 } x, y := crapsTrueOdds(n) if kind == "lay" { x, y = y, x } if kind == "place" { x, y = crapsPlaceOdds(n) } return true, x, y case "hard": if point == 0 { return false, 0, 1 } if s == 7 || (s == n && !hard) { return true, -1, 1 } if s == n { if n == 4 || n == 10 { return true, 7, 1 } return true, 9, 1 } return false, 0, 1 case "field": switch s { case 2: return true, 2, 1 case 12: return true, field12, 1 case 3, 4, 9, 10, 11: return true, 1, 1 } return true, -1, 1 case "seven": if s == 7 { return true, 4, 1 } case "craps": if s == 2 || s == 3 || s == 12 { return true, 7, 1 } case "eleven", "ace-deuce": if (kind == "eleven" && s == 11) || (kind == "ace-deuce" && s == 3) { return true, 15, 1 } case "aces", "boxcars": if (kind == "aces" && s == 2) || (kind == "boxcars" && s == 12) { return true, 30, 1 } case "hop": if a*10+b == n || b*10+a == n { if hard { return true, 30, 1 } return true, 15, 1 } case "horn": switch s { case 2, 12: return true, 27, 4 case 3, 11: return true, 3, 1 } default: return false, 0, 1 } return true, -1, 1 }