aboutsummaryrefslogtreecommitdiff
path: root/internal/casino/craps_rules.go
diff options
context:
space:
mode:
Diffstat (limited to 'internal/casino/craps_rules.go')
-rw-r--r--internal/casino/craps_rules.go205
1 files changed, 205 insertions, 0 deletions
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 <amount> on pass / dontpass / come / dontcome / field\n"+
+ "bet <amount> on place 6 / buy 4 / lay 10 / hard 8 / hop 2 3\n"+
+ "bet <amount> on seven / craps / eleven / ace-deuce / aces / boxcars / horn\n"+
+ "bet <amount> on odds pass / odds come 6 (also dontpass/dontcome)\n"+
+ "bet <amount> 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 <target>: 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
+}