diff options
| author | historia <[not public]> | 2026-09-30 16:16:17 -0400 |
|---|---|---|
| committer | historia <[not public]> | 2026-09-30 16:16:17 -0400 |
| commit | a4c09c2d38d1f3bc1d3ad4bdb0234bdea7c37e73 (patch) | |
| tree | 00c9567e620f331a59dd44f1f1fa779ecd94a845 /internal/casino/craps_variants_test.go | |
| parent | 724079f8de1ae69965c6267dca3d7fc3cc86afa2 (diff) | |
| download | thehouseoficarus-a4c09c2d38d1f3bc1d3ad4bdb0234bdea7c37e73.tar.gz | |
feat: additional craps variants added, standardizard casino command hints
Diffstat (limited to 'internal/casino/craps_variants_test.go')
| -rw-r--r-- | internal/casino/craps_variants_test.go | 308 |
1 files changed, 308 insertions, 0 deletions
diff --git a/internal/casino/craps_variants_test.go b/internal/casino/craps_variants_test.go new file mode 100644 index 0000000..687a496 --- /dev/null +++ b/internal/casino/craps_variants_test.go @@ -0,0 +1,308 @@ +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 = -1 + if s <= 4 || s >= 10 { + profit = 1 + } + 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)) + } + } +} |
