package casino import ( "fmt" "strings" "testing" ) var crapsVariants = []string{"bank", "simplified", "high_point", "crapless", "new_york", "die_rich", "diceless"} func variantFixture(v string) *crapsTable { t := crapsFixture() t.Config.Craps.Variant = v return t } func TestCrapsVariantValidationAndMenus(t *testing.T) { for _, v := range crapsVariants { tb := variantFixture(v) if err := tb.Config.Validate(); err != nil { t.Fatal(v, err) } if !strings.Contains(tb.Config.CrapsRulesText(), tb.Config.Craps.title()) { t.Fatal(v, "missing title") } if tb.view().Variant != v { t.Fatal(v, "snapshot") } } for _, v := range []string{"simplified", "high_point", "die_rich"} { tb := variantFixture(v) tb.Config.Craps.Odds = 2 if tb.Config.Validate() == nil { t.Fatal(v, "accepted unsupported odds") } } } func TestVariantAllOutcomes(t *testing.T) { for _, v := range crapsVariants { c := CrapsConfig{Variant: v, Field12: 2} for n := 0; n <= 12; n++ { if n != 0 && !c.pointNumber(n) { continue } for a := 1; a <= 6; a++ { for b := 0; b <= 6; b++ { if (v == "die_rich") != (b == 0) { continue } s := a + b done, profit := false, 0 switch v { case "simplified": done = true profit = [13]int{0, 0, 3, 1, 1, -1, -1, -1, -1, -1, 2, 2, 5}[s] case "high_point": if n == 0 { done = s >= 11 profit = 1 } else { done = true profit = -1 if s > n { profit = 1 } } case "die_rich": done = s == 1 || (n == 0 && s == 6) || (n != 0 && s == n) profit = 1 if s == 1 { profit = -1 } case "crapless": if n == 0 { done = s == 7 profit = 1 } else { done = s == 7 || s == n profit = 1 if s == 7 { profit = -1 } } default: if n == 0 { done = s == 2 || s == 3 || s == 7 || s == 11 || s == 12 profit = -1 if s == 7 || s == 11 { profit = 1 } } else { done = s == n || s == 7 profit = 1 if s == 7 { profit = -1 } } } got, x, y := c.outcome("pass", n, a, b, n) if got != done || (done && (x != profit || y != 1)) { t.Fatalf("%s point %d roll %d,%d: %v %d/%d want %v %d", v, n, a, b, got, x, y, done, profit) } } } } } } func TestVariantBetRestrictionsNoDebit(t *testing.T) { for _, v := range crapsVariants { for _, spot := range []string{"pass", "dontpass", "come", "dontcome", "field", "place 6", "buy 4", "lay 10", "hard 8", "hop 2 3", "seven", "craps", "eleven", "ace-deuce", "aces", "boxcars", "horn", "fire", "small", "tall", "all", "place 2", "buy 11", "lay 12"} { tb := variantFixture(v) tb.join("a") w := &testWallet{chips: 10000} kind, n, _ := parseCrapsTarget(spot) if kind == "come" || kind == "dontcome" { tb.point = 6 } allowed := tb.Config.Craps.allows(kind) if kind == "place" || kind == "buy" || kind == "lay" { allowed = allowed && tb.Config.Craps.number(n) } tb.bet("a", spot, 120, w) if (len(w.open) > 0) != allowed { t.Fatal(v, spot, "acceptance", allowed) } if !allowed && w.chips != 10000 { t.Fatal("rejected wager debited") } } } } func TestCraplessExtraPointOddsPayouts(t *testing.T) { for _, n := range []int{2, 3, 11, 12} { t.Run(fmt.Sprint(n), func(t *testing.T) { tb := variantFixture("crapless") tb.join("a") w := &testWallet{chips: 1000} a, b := 1, n-1 if n > 7 { a, b = n-6, 6 } tb.bet("a", "pass", 10, w) tb.resolveRoll(a, b) if tb.point != n || tb.players.get("a").bets[0].number != n { t.Fatal("point not established") } tb.bet("a", "odds pass", 11, w) if len(w.open) != 1 { t.Fatal("extra point default odds limit") } tb.bet("a", "odds pass", 10, w) projection := tb.projectionFor(tb.players.get("a")) x, _ := crapsTrueOdds(n) want := 20 + 10*(x+1) for _, r := range projection { if r.Total == n && r.Net != want-20 { t.Fatal("preview", r) } } tb.resolveRoll(a, b) if w.paid != want || len(w.open) != 0 || tb.point != 0 { t.Fatal(w.paid, want, tb.point) } }) } for _, c := range []struct { spot string amount, a, b, want int }{ {"place 2", 2, 1, 1, 13}, {"place 3", 4, 1, 2, 15}, {"place 11", 4, 5, 6, 15}, {"place 12", 2, 6, 6, 13}, {"buy 2", 20, 1, 1, 139}, {"buy 11", 20, 5, 6, 79}, {"lay 12", 120, 3, 4, 139}, {"lay 3", 60, 3, 4, 79}, } { tb := variantFixture("crapless") tb.point = 6 tb.join("a") w := &testWallet{chips: 1000} tb.bet("a", c.spot, c.amount, w) tb.resolveRoll(c.a, c.b) if w.paid != c.want { t.Fatal(c.spot, w.paid, c.want) } } } func TestVariantShooterLifecycleAndWallets(t *testing.T) { for _, v := range crapsVariants { t.Run(v, func(t *testing.T) { tb := variantFixture(v) tb.join("a") tb.join("b") w := &testWallet{chips: 1000} tb.bet("a", "pass", 10, w) a, b := 2, 2 if v == "die_rich" { a, b = 4, 0 } if v == "simplified" { a, b = 2, 3 } tb.resolveRoll(a, b) if v != "simplified" { if tb.point != 4 { t.Fatal("point", tb.point) } a, b = 3, 4 if v == "high_point" { a, b = 1, 2 } if v == "die_rich" { a, b = 1, 0 } tb.resolveRoll(a, b) } if tb.point != 0 || tb.queue[0] != "b" || len(w.open) != 0 || w.chips != 990 { t.Fatal("loss lifecycle", tb.point, tb.queue, w) } tb.bet("b", "pass", 10, w) tb.refundAll() tb.refundAll() if w.chips != 990 || len(w.open) != 0 { t.Fatal("shutdown refund") } tb.bet("b", "pass", 10, w) tb.markDisconnected("b") if w.chips != 980 || len(w.open) != 0 { t.Fatal("disconnect") } }) } } func TestHighPointAndDieRichPointWin(t *testing.T) { for _, v := range []string{"high_point", "die_rich"} { tb := variantFixture(v) tb.join("a") w := &testWallet{chips: 100} tb.bet("a", "pass", 10, w) if v == "high_point" { tb.resolveRoll(2, 2) tb.resolveRoll(2, 3) } else { tb.resolveRoll(4, 0) tb.resolveRoll(6, 0) if tb.point != 4 { t.Fatal("six must not win established point") } tb.resolveRoll(4, 0) } if w.paid != 20 || tb.point != 0 || tb.queue[0] != "a" { t.Fatal(v, w.paid, tb.point) } } } func TestCrapsBonusPreviewMatchesSettlement(t *testing.T) { for _, point := range []int{0, 6} { for a := 1; a <= 6; a++ { for b := a; b <= 6; b++ { tb := crapsFixture() tb.join("a") w := &testWallet{chips: 1000} for _, spot := range []string{"fire", "small", "tall", "all"} { tb.bet("a", spot, 1, w) } tb.point = point tb.made = map[int]bool{4: true, 5: true, 6: true, 8: true} tb.seen = map[int]bool{2: true, 3: true, 4: true, 5: true, 8: true, 9: true, 10: true, 11: true, 12: true} // Four made points must NOT advertise Fire profit except at seven-out. for _, row := range tb.projectionFor(tb.players.get("a")) { if row.Rolls != fmt.Sprintf("%d+%d", a, b) { continue } tb.resolveRoll(a, b) // Net = wallet change excluding returned unresolved commitments. live := 0 for _, bet := range tb.players.get("a").bets { live += bet.stake.Total } actual := w.paid - (4 - live) if row.Net != actual { t.Fatalf("point %d roll %d+%d: preview %d actual %d", point, a, b, row.Net, actual) } break } } } } } func TestVariantRandomSources(t *testing.T) { for _, v := range []string{"diceless", "die_rich"} { tb := variantFixture(v) seen := map[[2]int]bool{} for i := 0; i < 2000; i++ { tb.roll() d := tb.dice seen[d] = true if d[0] < 1 || d[0] > 6 || (v == "die_rich" && d[1] != 0) || (v == "diceless" && (d[1] < 1 || d[1] > 6)) { t.Fatal(v, d) } } want := 36 if v == "die_rich" { want = 6 } if len(seen) != want { t.Fatal(v, "source support", len(seen)) } } }