diff options
Diffstat (limited to 'internal/casino/craps_test.go')
| -rw-r--r-- | internal/casino/craps_test.go | 403 |
1 files changed, 403 insertions, 0 deletions
diff --git a/internal/casino/craps_test.go b/internal/casino/craps_test.go new file mode 100644 index 0000000..5c7b0df --- /dev/null +++ b/internal/casino/craps_test.go @@ -0,0 +1,403 @@ +package casino + +import ( + "fmt" + "math/rand" + "strings" + "testing" + "time" +) + +func crapsFixture() *crapsTable { + return newCrapsTable(1, TableConfig{ID: "craps", Game: GameCraps, BettingTicks: 2}, rand.New(rand.NewSource(7))) +} +func TestCrapsAll36ComeOut(t *testing.T) { + counts := map[string][3]int{} + for _, kind := range []string{"pass", "dontpass", "come", "dontcome"} { + for a := 1; a <= 6; a++ { + for b := 1; b <= 6; b++ { + done, x, _ := crapsOutcome(kind, 0, a, b, 0, 2) + c := counts[kind] + if done { + if x > 0 { + c[0]++ + } else if x < 0 { + c[1]++ + } else { + c[2]++ + } + } + counts[kind] = c + } + } + } + if counts["pass"] != [3]int{8, 4, 0} || counts["come"] != counts["pass"] || counts["dontpass"] != [3]int{3, 8, 1} || counts["dontcome"] != counts["dontpass"] { + t.Fatal(counts) + } + for _, n := range []int{4, 5, 6, 8, 9, 10} { + for _, kind := range []string{"pass", "dontpass", "come", "dontcome"} { + for a := 1; a <= 6; a++ { + for b := 1; b <= 6; b++ { + done, x, _ := crapsOutcome(kind, n, a, b, 6, 2) + if done != (a+b == n || a+b == 7) { + t.Fatal(kind, n, a, b) + } + if done && ((x > 0) != ((a+b == n) != strings.HasPrefix(kind, "dont"))) { + t.Fatal("wrong winner", kind, n, a, b) + } + } + } + } + } +} +func TestCrapsPayouts(t *testing.T) { + cases := []struct { + spot string + amount, a, b, point, want int + }{ + {"field", 10, 6, 6, 6, 30}, {"field", 10, 1, 1, 6, 30}, {"field", 10, 3, 4, 6, 0}, + {"place 4", 10, 2, 2, 6, 28}, {"place 5", 10, 2, 3, 6, 24}, {"place 6", 12, 3, 3, 6, 26}, + {"buy 4", 20, 2, 2, 6, 59}, {"buy 5", 20, 2, 3, 6, 49}, {"buy 6", 20, 3, 3, 6, 43}, + {"lay 4", 40, 3, 4, 6, 59}, {"lay 5", 30, 3, 4, 6, 49}, {"lay 6", 30, 3, 4, 6, 53}, + {"hard 4", 10, 2, 2, 6, 80}, {"hard 6", 10, 3, 3, 6, 100}, {"hard 8", 10, 2, 6, 6, 0}, + {"seven", 10, 3, 4, 6, 50}, {"craps", 10, 1, 1, 6, 80}, {"eleven", 10, 5, 6, 6, 160}, + {"ace-deuce", 10, 1, 2, 6, 160}, {"aces", 10, 1, 1, 6, 310}, {"boxcars", 10, 6, 6, 6, 310}, + {"hop 2 3", 10, 3, 2, 6, 160}, {"hop 3 3", 10, 3, 3, 6, 310}, {"horn", 4, 1, 1, 6, 31}, {"horn", 4, 5, 6, 6, 16}, + } + for _, c := range cases { + t.Run(fmt.Sprintf("%s-%d%d", c.spot, c.a, c.b), func(t *testing.T) { + tb := crapsFixture() + tb.point = c.point + w := &testWallet{chips: 10000} + tb.join("a") + tb.bet("a", c.spot, c.amount, w) + if len(tb.players.get("a").bets) != 1 { + t.Fatal("rejected") + } + tb.resolveRoll(c.a, c.b) + if w.paid != c.want { + t.Fatalf("paid %d want %d", w.paid, c.want) + } + if len(w.open) != 0 { + t.Fatal("stake not consumed") + } + }) + } + tb := crapsFixture() + tb.Config.Craps.Field12 = 3 + w := &testWallet{chips: 100} + tb.join("a") + tb.bet("a", "field", 10, w) + tb.resolveRoll(6, 6) + if w.paid != 40 { + t.Fatal(w.paid) + } +} +func TestCrapsLineOddsAndQueue(t *testing.T) { + tb := crapsFixture() + w := &recordingWallet{testWallet: testWallet{chips: 1000}} + tb.join("a") + tb.join("b") + tb.bet("a", "pass", 10, w) + tb.resolveRoll(2, 2) + tb.bet("a", "odds pass", 31, w) + if len(w.open) != 1 { + t.Fatal("over limit odds accepted") + } + tb.bet("a", "odds pass", 30, w) + tb.resolveRoll(2, 2) + if w.paid != 110 || len(w.payStakes) != 2 || tb.point != 0 || tb.queue[0] != "a" { + t.Fatal(w.paid, w.payStakes, tb.point, tb.queue) + } + tb.join("c") + tb.bet("a", "pass", 10, w) + tb.resolveRoll(3, 3) + tb.resolveRoll(3, 4) + if strings.Join(tb.queue, ",") != "b,c,a" { + t.Fatal(tb.queue) + } + tb.leave("c") + tb.join("c") + if strings.Join(tb.queue, ",") != "b,a,c" { + t.Fatal(tb.queue) + } +} +func TestCrapsComeTravelAndComeOutOddsOff(t *testing.T) { + for _, win := range []bool{false, true} { + tb := crapsFixture() + tb.join("a") + w := &testWallet{chips: 1000} + tb.point = 4 + tb.bet("a", "come", 10, w) + tb.resolveRoll(3, 3) + tb.bet("a", "odds come 6", 50, w) + tb.resolveRoll(2, 2) + if win { + tb.resolveRoll(3, 3) + if w.paid != 70 { + t.Fatal("come odds should be returned, not win", w.paid) + } + } else { + tb.resolveRoll(3, 4) + if w.paid != 50 { + t.Fatal("come odds should not lose", w.paid) + } + } + if len(w.open) != 0 { + t.Fatal(w.open) + } + } +} +func TestCrapsWorkingRules(t *testing.T) { + for _, spot := range []string{"place 6", "buy 6", "hard 6"} { + tb := crapsFixture() + tb.join("a") + w := &testWallet{chips: 100} + tb.bet("a", spot, 30, w) + tb.resolveRoll(3, 4) + if w.paid != 0 || len(tb.players.get("a").bets) != 1 { + t.Fatal(spot) + } + tb.resolveRoll(3, 3) + if len(tb.players.get("a").bets) != 1 { + t.Fatal("come-out hit must be off", spot) + } + tb.resolveRoll(3, 4) + if len(tb.players.get("a").bets) != 0 { + t.Fatal("seven out", spot) + } + } + tb := crapsFixture() + tb.join("a") + w := &testWallet{chips: 100} + tb.bet("a", "dontpass", 10, w) + tb.resolveRoll(2, 2) + tb.bet("a", "odds dontpass", 60, w) + tb.resolveRoll(3, 4) + if w.paid != 110 { + t.Fatal(w.paid) + } +} +func TestCrapsFireDistinctPoints(t *testing.T) { + for count := 3; count <= 6; count++ { + tb := crapsFixture() + tb.join("a") + w := &testWallet{chips: 100} + tb.bet("a", "fire", 1, w) + points := [][2]int{{2, 2}, {2, 3}, {3, 3}, {4, 4}, {4, 5}, {5, 5}} + for _, dice := range points[:count] { + tb.resolveRoll(dice[0], dice[1]) + tb.resolveRoll(dice[0], dice[1]) + } + // Repeating an already made point must not increase Fire count. + tb.resolveRoll(2, 2) + tb.resolveRoll(2, 2) + tb.resolveRoll(3, 3) + tb.resolveRoll(3, 4) + want := map[int]int{3: 0, 4: 25, 5: 250, 6: 1000}[count] + if w.paid != want { + t.Fatalf("%d points: %d want %d", count, w.paid, want) + } + } +} +func TestCrapsATSAndSevenReset(t *testing.T) { + tb := crapsFixture() + tb.join("a") + w := &testWallet{chips: 100} + for _, spot := range []string{"small", "tall", "all"} { + tb.bet("a", spot, 1, w) + } + for _, d := range [][2]int{{1, 1}, {1, 2}, {2, 2}, {2, 3}, {3, 3}, {4, 4}, {4, 5}, {5, 5}, {5, 6}, {6, 6}} { + tb.resolveRoll(d[0], d[1]) + } + if w.paid != 246 || len(w.open) != 0 { + t.Fatal(w.paid, w.open) + } + tb = crapsFixture() + tb.join("a") + w = &testWallet{chips: 100} + tb.bet("a", "small", 1, w) + tb.resolveRoll(1, 1) + tb.resolveRoll(3, 4) + if w.paid != 0 || len(w.open) != 0 || len(tb.seen) != 0 { + t.Fatal("ATS must lose on come-out seven") + } + tb.bet("a", "small", 1, w) + if len(w.open) != 0 { + t.Fatal("late bonus allowed") + } +} +func TestCrapsTimersControlsAndRefunds(t *testing.T) { + tb := crapsFixture() + tb.join("a") + tb.join("b") + w := &testWallet{chips: 100} + tb.bet("a", "pass", 10, w) + tb.action("a", "roll") + if tb.dice != [2]int{} { + t.Fatal("early roll") + } + tb.tickElapsed(10 * time.Second) + if tb.phase != phasePlaying { + t.Fatal(tb.phase) + } + tb.action("b", "roll") + if tb.dice != [2]int{} { + t.Fatal("nonshooter roll") + } + tb.bet("a", "field", 10, w) + if len(w.open) != 2 { + t.Fatal("bets must remain open until the roll") + } + tb.action("a", "roll") + if tb.dice == [2]int{} { + t.Fatal("no roll") + } + tb = crapsFixture() + tb.join("a") + w = &testWallet{chips: 100} + tb.bet("a", "pass", 10, w) + tb.tickElapsed(120 * time.Second) + if tb.dice != [2]int{} { + t.Fatal("solo players must never be forced to roll") + } + tb.action("a", "roll") + if tb.dice == [2]int{} { + t.Fatal("solo manual roll stalled") + } + tb = crapsFixture() + tb.join("a") + w = &testWallet{chips: 100} + tb.bet("a", "pass", 10, w) + tb.resolveRoll(3, 3) + tb.action("a", "unbet pass") + if len(w.open) != 1 { + t.Fatal("contract removed") + } + tb.markDisconnected("a") + if len(w.open) != 0 || w.paid != 0 { + t.Fatal("disconnect refunded") + } + tb = crapsFixture() + tb.join("a") + w = &testWallet{chips: 100} + tb.bet("a", "pass", 10, w) + tb.refundAll() + tb.refundAll() + if w.chips != 100 || len(w.open) != 0 { + t.Fatal("refund not idempotent") + } + tb.bet("a", "place 6", 12, w) + tb.action("a", "unbet place 6") + if w.paid != 12 || tb.players.get("a").HasBet { + t.Fatal("optional removal") + } + tb.bet("a", "pass", 10, w) + tb.action("a", "leave_after") + tb.resolveRoll(3, 4) + if tb.hasParticipant("a") { + t.Fatal("queued departure stuck") + } +} +func TestCrapsValidation(t *testing.T) { + for _, r := range []CrapsConfig{{Odds: -1}, {Field12: 4}, {Variant: "crapless"}} { + if err := (TableConfig{ID: "c", Game: GameCraps, Craps: r}).Validate(); err == nil { + t.Fatal("accepted", r) + } + } + for _, r := range []CrapsConfig{{}, {Odds: 10, Field12: 3}, {Odds: 100}} { + if err := (TableConfig{ID: "c", Game: GameCraps, Craps: r}).Validate(); err != nil { + t.Fatal(err) + } + } + if err := (TableConfig{ID: "r", Game: GameRoulette, Craps: CrapsConfig{Field12: 3}}).Validate(); err == nil { + t.Fatal("wrong rule block accepted") + } + tb := crapsFixture() + tb.join("a") + w := &testWallet{chips: 1000} + for _, spot := range []string{"place 7", "hard 5", "hop 0 6", "horn", "place 6", "come", "odds pass", "unknown"} { + tb.bet("a", spot, 1, w) + } + if len(w.open) != 0 { + t.Fatal("invalid bets debited", w.open) + } +} + +func TestCrapsOddsLimitsAndReturns(t *testing.T) { + for _, uniform := range []int{0, 2, 10} { + for _, n := range []int{4, 5, 6, 8, 9, 10} { + for _, against := range []bool{false, true} { + tb := crapsFixture() + tb.Config.Craps.Odds = uniform + tb.join("a") + w := &testWallet{chips: 10000} + kind := "pass" + if against { + kind = "dontpass" + } + tb.bet("a", kind, 10, w) + a, b := n/2, n-n/2 + tb.resolveRoll(a, b) + limit := tb.Config.Craps.oddsLimit(n, against) * 10 + x, y := crapsTrueOdds(n) + if against { + x, y = y, x + } + amount := limit - limit%y + tb.bet("a", "odds "+kind, limit+y, w) + if len(w.open) != 1 { + t.Fatal("overlimit", uniform, n, against) + } + tb.bet("a", "odds "+kind, amount, w) + if len(w.open) != 2 { + t.Fatal("odds rejected", uniform, n, against) + } + if against { + tb.resolveRoll(3, 4) + } else { + tb.resolveRoll(a, b) + } + if w.paid != 20+amount+amount/y*x { + t.Fatal("wrong odds", uniform, n, against, w.paid) + } + } + } + } +} +func TestCrapsFailureRecoveryAndSnapshots(t *testing.T) { + tb := crapsFixture() + tb.join("a") + w := &failingPayoutWallet{testWallet: testWallet{chips: 100}} + tb.bet("a", "pass", 10, w) + snap := tb.view() + snap.Queue[0] = "changed" + snap.Bets[0].Amount = 999 + if tb.queue[0] != "a" || tb.players.get("a").bets[0].stake.Total != 10 { + t.Fatal("snapshot aliases live state") + } + events := tb.resolveRoll(3, 4) + found := false + for _, e := range events { + if strings.Contains(e.Message, payoutFailedMessage()) { + found = true + } + } + if !found || len(w.open) != 1 || w.paid != 0 || tb.players.get("a").HasBet { + t.Fatal("failed payout handling", events, w) + } + tb.refundAll() + tb.refundAll() + if w.chips != 100 || len(w.open) != 0 { + t.Fatal("pending recovery", w) + } + tb = crapsFixture() + tb.join("a") + good := &testWallet{chips: 100} + tb.bet("a", "place 6", 12, good) + tb.action("a", "leave_after") + tb.action("a", "unbet place 6") + if tb.hasParticipant("a") { + t.Fatal("unbet stranded queued departure") + } +} |
