diff options
| author | historia <[not public]> | 2026-09-29 16:50:40 -0400 |
|---|---|---|
| committer | historia <[not public]> | 2026-09-29 16:50:40 -0400 |
| commit | 00fe5f7ead43627fa283035b27365924b76c5a0f (patch) | |
| tree | 07546518a51aaa037d64813f40eb6dc4596bd9cb /internal/casino/roulette_test.go | |
| parent | bfe22e10b5b9f2c7039d1d67021229659a06a226 (diff) | |
| download | thehouseoficarus-00fe5f7ead43627fa283035b27365924b76c5a0f.tar.gz | |
feat: roulette bets street, corner, split, etc.
Diffstat (limited to 'internal/casino/roulette_test.go')
| -rw-r--r-- | internal/casino/roulette_test.go | 202 |
1 files changed, 202 insertions, 0 deletions
diff --git a/internal/casino/roulette_test.go b/internal/casino/roulette_test.go index a20b17c..160ff37 100644 --- a/internal/casino/roulette_test.go +++ b/internal/casino/roulette_test.go @@ -1,6 +1,7 @@ package casino import ( + "fmt" "math/rand" "strconv" "strings" @@ -66,6 +67,207 @@ func TestRouletteRowBetMapping(t *testing.T) { } } +// TestRouletteCombinationBets covers splits, streets, corners, six-lines, +// trios and the basket, including the geometry that must be rejected and the +// bare-number-list inference that lets the leading keyword be omitted. +func TestRouletteCombinationBets(t *testing.T) { + same := func(got []string, want ...string) bool { + if len(got) != len(want) { + return false + } + set := map[string]bool{} + for _, g := range got { + set[g] = true + } + for _, w := range want { + if !set[w] { + return false + } + } + return true + } + cases := []struct { + spot string + double bool + mult int + want []string + }{ + // Splits cover edge-neighbours only, horizontally or vertically. + {"split 1 2", false, 18, []string{"1", "2"}}, + {"1-2", false, 18, []string{"1", "2"}}, + {"1/4", false, 18, []string{"1", "4"}}, + {"split 1 5", false, 0, nil}, + {"split 3 4", false, 0, nil}, + {"split 0 3", false, 18, []string{"0", "3"}}, + // Streets are one three-row column. + {"street 1 2 3", false, 12, []string{"1", "2", "3"}}, + {"1-2-3", false, 12, []string{"1", "2", "3"}}, + {"street 2 3 4", false, 0, nil}, + // Corners are a two-by-two block. + {"corner 1 2 4 5", false, 9, []string{"1", "2", "4", "5"}}, + {"1-2-4-5", false, 9, []string{"1", "2", "4", "5"}}, + {"corner 1 2 3 4", false, 0, nil}, + // Six-lines are two adjacent streets. + {"line 1 2 3 4 5 6", false, 6, []string{"1", "2", "3", "4", "5", "6"}}, + {"1-2-3-4-5-6", false, 6, []string{"1", "2", "3", "4", "5", "6"}}, + {"line 1 2 3 4 5 7", false, 0, nil}, + // Trios pair a zero with the first street. + {"trio 0 1 2", false, 12, []string{"0", "1", "2"}}, + {"0-1-2", false, 12, []string{"0", "1", "2"}}, + {"trio 0 00 2", true, 12, []string{"0", "00", "2"}}, + {"trio 0 00 2", false, 0, nil}, + // The American basket is five numbers at a deliberately short 6:1. + {"basket", true, 7, []string{"0", "00", "1", "2", "3"}}, + {"basket 0 00 1 2 3", true, 7, []string{"0", "00", "1", "2", "3"}}, + {"basket 0 00 1 2", true, 0, nil}, + // Single zero falls back to the European first four. + {"basket", false, 9, []string{"0", "1", "2", "3"}}, + {"corner 0 1 2 3", false, 9, []string{"0", "1", "2", "3"}}, + // Zero-to-zero is an American split only. + {"split 0 00", true, 18, []string{"0", "00"}}, + {"split 0 00", false, 0, nil}, + } + for _, tc := range cases { + got, mult := rouletteBet(tc.spot, tc.double) + if mult != tc.mult { + t.Fatalf("%q double=%v: mult=%d, want %d", tc.spot, tc.double, mult, tc.mult) + } + if tc.mult == 0 { + continue + } + if !same(got, tc.want...) { + t.Fatalf("%q double=%v: pockets=%v, want %v", tc.spot, tc.double, got, tc.want) + } + // Every fair combination covers a divisor of 36; the American basket + // is the one intentional exception (5 x 7 = 35, not 36). + if tc.mult != 7 && len(got)*mult != 36 { + t.Fatalf("%q: coverage/payout %d/%d", tc.spot, len(got), mult) + } + } + // A bare list and its named form must reach the same bet. + for _, pair := range [][2]string{{"1-2", "split 1 2"}, {"1-2-3", "street 1 2 3"}, {"1-2-4-5", "corner 1 2 4 5"}, {"1-2-3-4-5-6", "line 1 2 3 4 5 6"}} { + bare, bareMult := rouletteBet(pair[0], false) + named, namedMult := rouletteBet(pair[1], false) + if bareMult != namedMult || !same(bare, named...) { + t.Fatalf("inference %q != %q: %v/%d vs %v/%d", pair[0], pair[1], bare, bareMult, named, namedMult) + } + } +} + +// TestRouletteCombinationOrderIndependent checks that every permutation of a +// combination's numbers selects the same bet. Street 3-1-2 must be the 1-2-3 +// street, and so on for splits, corners, lines, trios and the basket. The +// payout multiplier and the covered pockets must not depend on input order. +func TestRouletteCombinationOrderIndependent(t *testing.T) { + same := func(a, b []string) bool { + if len(a) != len(b) { + return false + } + set := map[string]bool{} + for _, x := range a { + set[x] = true + } + for _, x := range b { + if !set[x] { + return false + } + } + return true + } + var permute func([]string, func([]string)) + permute = func(items []string, visit func([]string)) { + var rec func(int) + rec = func(i int) { + if i == len(items) { + visit(append([]string(nil), items...)) + return + } + for j := i; j < len(items); j++ { + items[i], items[j] = items[j], items[i] + rec(i + 1) + items[i], items[j] = items[j], items[i] + } + } + rec(0) + } + cases := []struct { + kind string + nums []string + double bool + }{ + {"split", []string{"1", "2"}, false}, + {"split", []string{"0", "00"}, true}, + {"street", []string{"1", "2", "3"}, false}, + {"corner", []string{"1", "2", "4", "5"}, false}, + {"line", []string{"1", "2", "3", "4", "5", "6"}, false}, + {"trio", []string{"0", "1", "2"}, false}, + {"trio", []string{"0", "00", "2"}, true}, + {"basket", []string{"0", "00", "1", "2", "3"}, true}, + } + for _, tc := range cases { + canonical := tc.kind + " " + strings.Join(tc.nums, " ") + wantPockets, wantMult := rouletteBet(canonical, tc.double) + if wantMult == 0 { + t.Fatalf("canonical %q is invalid", canonical) + } + permute(append([]string(nil), tc.nums...), func(p []string) { + spot := tc.kind + " " + strings.Join(p, " ") + got, mult := rouletteBet(spot, tc.double) + if mult != wantMult || !same(got, wantPockets) { + t.Fatalf("%q: got %v/%d, want %v/%d", spot, got, mult, wantPockets, wantMult) + } + }) + } +} + +// TestRouletteCombinationSettlement drives a split through the real table +// phases, so coverage and payout are exercised through settle, not only by +// calling rouletteBet directly. +func TestRouletteCombinationSettlement(t *testing.T) { + table := rouletteFixture() + wheel := rouletteWheel(false) + index := rand.New(rand.NewSource(7)).Intn(len(wheel)) + pocket := wheel[index] + // Pick a real split covering the sampled pocket. + split := "" + for n := 1; n <= 36 && split == ""; n++ { + for _, m := range []int{n + 1, n + 3} { + if m > 36 { + continue + } + spot := fmt.Sprintf("split %d %d", n, m) + covered, mult := rouletteBet(spot, false) + if mult == 0 { + continue + } + for _, p := range covered { + if p == pocket { + split = spot + break + } + } + if split != "" { + break + } + } + } + if split == "" { + split = "split 0 1" + if _, mult := rouletteBet(split, false); mult == 0 { + t.Fatal("no split covers pocket ", pocket) + } + } + table.join("a") + w := &testWallet{chips: 100} + table.bet("a", split, 10, w) + for i := 0; i < 4; i++ { + table.tick() + } + if w.paid != 180 || len(w.open) != 0 { + t.Fatalf("split %q pocket %s: wallet %+v", split, pocket, w) + } +} + func rouletteFixture() *rouletteTable { return newRouletteTable(1, TableConfig{ID: "r", Game: GameRoulette, BettingTicks: 2, Roulette: RouletteConfig{SpinTicks: 2}}, rand.New(rand.NewSource(7))) } |
