aboutsummaryrefslogtreecommitdiff
path: root/internal/casino/craps_variants_test.go
diff options
context:
space:
mode:
Diffstat (limited to 'internal/casino/craps_variants_test.go')
-rw-r--r--internal/casino/craps_variants_test.go308
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))
+ }
+ }
+}