From 0fcb03633e3008169173d76c8408f5410592b3eb Mon Sep 17 00:00:00 2001 From: historia <[not public]> Date: Wed, 30 Sep 2026 05:33:45 -0400 Subject: feat: bank craps added to casino --- internal/casino/craps_rules.go | 205 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 205 insertions(+) create mode 100644 internal/casino/craps_rules.go (limited to 'internal/casino/craps_rules.go') diff --git a/internal/casino/craps_rules.go b/internal/casino/craps_rules.go new file mode 100644 index 0000000..86aa9bb --- /dev/null +++ b/internal/casino/craps_rules.go @@ -0,0 +1,205 @@ +package casino + +import "fmt" + +// CrapsConfig intentionally supports bank craps only. Other games must supply +// their own outcome rules, not silently inherit bank-craps probabilities. +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") + } + if c.Craps.Variant != "" && c.Craps.Variant != "bank" { + return fmt.Errorf("only bank craps is implemented") + } + 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 4, 10: + return 2, 1 + case 5, 9: + return 3, 2 + default: + return 6, 5 + } +} +func (c CrapsConfig) oddsLimit(n int, against bool) int { + if c.Odds > 0 { + return c.Odds + } + if against { + return 6 + } + switch n { + case 4, 10: + return 3 + case 5, 9: + return 4 + default: + return 5 + } +} +func (c TableConfig) CrapsRulesText() string { + r := c.Craps.Normalize() + odds := "3-4-5x (don't: 6x laid)" + if r.Odds > 0 { + odds = fmt.Sprintf("%dx", r.Odds) + } + return fmt.Sprintf("Bank Craps | Free odds %s | Field 12 pays %d:1\n"+ + "bet on pass / dontpass / come / dontcome / field\n"+ + "bet on place 6 / buy 4 / lay 10 / hard 8 / hop 2 3\n"+ + "bet on seven / craps / eleven / ace-deuce / aces / boxcars / horn\n"+ + "bet on odds pass / odds come 6 (also dontpass/dontcome)\n"+ + "bet 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 up, 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"+ + "Exact payout units required; no fractional-credit rounding except commission.\n"+ + "Solo: roll anytime. Multiplayer: 10 seconds to bet, then 60 seconds for the shooter.\n"+ + "Bets remain open until the roll. Dice rotate on seven-out.\n"+ + "autoroll: repeat every 3 seconds on your turn (multiplayer waits 10 seconds).\n"+ + "unbet : remove non-contract bets/odds; pass/come contracts stay.\n"+ + "unbet all: return every optional bet and odds at once.\n"+ + "stop cancels autoroll without leaving; in manual mode stop/sitout wait for 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" { + switch n { + case 4, 10: + x, y = 9, 5 + case 5, 9: + x, y = 7, 5 + default: + x, y = 7, 6 + } + } + 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 +} -- cgit v1.2.3