aboutsummaryrefslogtreecommitdiff
path: root/internal/casino/roulette_geometry_test.go
diff options
context:
space:
mode:
authorhistoria <[not public]>2026-09-29 18:58:28 -0400
committerhistoria <[not public]>2026-09-29 18:58:28 -0400
commit624626b9a10ae47cf7d5f6e91765570ed759bd74 (patch)
tree9812f5d254b7d9cfe1d2fcb6da53362e974541b0 /internal/casino/roulette_geometry_test.go
parentff9002b96e35ad0808945e7c35791460c9ca8776 (diff)
downloadthehouseoficarus-624626b9a10ae47cf7d5f6e91765570ed759bd74.tar.gz
feat: better roulette racetrack graphics and payout table
Diffstat (limited to 'internal/casino/roulette_geometry_test.go')
-rw-r--r--internal/casino/roulette_geometry_test.go109
1 files changed, 109 insertions, 0 deletions
diff --git a/internal/casino/roulette_geometry_test.go b/internal/casino/roulette_geometry_test.go
new file mode 100644
index 0000000..37fec74
--- /dev/null
+++ b/internal/casino/roulette_geometry_test.go
@@ -0,0 +1,109 @@
+package casino
+
+import (
+ "fmt"
+ "testing"
+)
+
+func TestRouletteRejectsNonTouchingCombinations(t *testing.T) {
+ for _, tc := range []struct {
+ target string
+ double bool
+ }{
+ {"corner 1 3 4 6", false}, {"corner 1 2 7 8", false},
+ {"corner 1 3 34 36", true}, {"trio 0 1 3", false},
+ {"trio 0 1 3", true}, {"trio 0 2 3", true},
+ {"trio 00 1 2", true}, {"trio 0 00 1", true},
+ {"trio 0 00 3", true}, {"split 0 3", true},
+ {"split 00 1", true},
+ } {
+ if _, mult := rouletteBet(tc.target, tc.double); mult != 0 {
+ t.Errorf("accepted %q (double=%v)", tc.target, tc.double)
+ }
+ }
+ for _, double := range []bool{false, true} {
+ for c := 0; c < 11; c++ {
+ for r := 1; r <= 2; r++ {
+ n := 3*c + r
+ target := fmt.Sprintf("corner %d %d %d %d", n, n+1, n+3, n+4)
+ if _, mult := rouletteBet(target, double); mult != 9 {
+ t.Errorf("rejected %s", target)
+ }
+ }
+ }
+ for _, target := range []string{"split 0 1", "split 0 2", "trio 0 1 2"} {
+ if _, mult := rouletteBet(target, double); mult == 0 {
+ t.Errorf("rejected %s", target)
+ }
+ }
+ }
+ for _, target := range []string{"split 00 2", "split 00 3", "trio 00 2 3", "trio 0 00 2"} {
+ if _, mult := rouletteBet(target, true); mult == 0 {
+ t.Errorf("rejected %s", target)
+ }
+ }
+}
+
+// Sum the return over every equally likely outcome. Ordinary bets return
+// 36 stakes per wheel; the American five-number basket returns only 35.
+func TestRouletteWheelAndExpectedReturns(t *testing.T) {
+ for _, double := range []bool{false, true} {
+ wheel := WheelOrder(double)
+ want := 37
+ if double {
+ want = 38
+ }
+ seen := map[string]bool{}
+ for _, p := range wheel {
+ if _, valid := roulettePocketToken(p, double); !valid || seen[p] {
+ t.Fatalf("invalid wheel pocket %s", p)
+ }
+ seen[p] = true
+ }
+ if len(seen) != want {
+ t.Fatalf("wheel has %d pockets, want %d", len(seen), want)
+ }
+ targets := append(WheelOrder(double), "red", "black", "odd", "even", "low", "high", "1to12", "13to24", "25to36", "row1", "row2", "row3", "split 1 2", "street 1 2 3", "corner 1 2 4 5", "line 1 2 3 4 5 6", "trio 0 1 2", "basket")
+ for _, target := range targets {
+ wager, ok := rouletteWagerFor(target, double, false)
+ if !ok {
+ t.Fatalf("invalid target %s", target)
+ }
+ sum := 0
+ for _, p := range wheel {
+ share, _ := wager.covers(p)
+ payout, err := share.payout(1)
+ if err != nil {
+ t.Fatal(err)
+ }
+ sum += payout
+ }
+ wantReturn := 36
+ if double && target == "basket" {
+ wantReturn = 35
+ }
+ if sum != wantReturn {
+ t.Errorf("%s: total return %d, want %d", target, sum, wantReturn)
+ }
+ }
+ }
+}
+
+func TestRouletteSharePayoutCap(t *testing.T) {
+ shares := []rouletteShare{{36, 1}, {7, 1}, {36, 9}}
+ for _, bet := range rouletteCallBets {
+ for _, share := range bet.wager().shares {
+ shares = append(shares, share)
+ }
+ }
+ for _, share := range shares {
+ limit := ((MaxCredits+1)*share.den - 1) / share.num
+ got, err := share.payout(limit)
+ if err != nil || got > MaxCredits {
+ t.Fatalf("%+v at limit: %d, %v", share, got, err)
+ }
+ if _, err := share.payout(limit + 1); err == nil {
+ t.Fatalf("%+v accepted overflow", share)
+ }
+ }
+}