diff options
| author | historia <[not public]> | 2026-10-03 07:11:58 -0400 |
|---|---|---|
| committer | historia <[not public]> | 2026-10-03 07:11:58 -0400 |
| commit | 0fb83bdf433b9b48240f4ac7a5d4c912f6b808cc (patch) | |
| tree | ac0af51cc2513b1ddd8a280f360af820d1f097af /internal/casino/big_six_test.go | |
| parent | 6232d41e5639f9c38e47b2bb65266e343ca26e8f (diff) | |
| download | thehouseoficarus-0fb83bdf433b9b48240f4ac7a5d4c912f6b808cc.tar.gz | |
feat: added big 6 wheel to the casino
Diffstat (limited to 'internal/casino/big_six_test.go')
| -rw-r--r-- | internal/casino/big_six_test.go | 293 |
1 files changed, 293 insertions, 0 deletions
diff --git a/internal/casino/big_six_test.go b/internal/casino/big_six_test.go new file mode 100644 index 0000000..cccad77 --- /dev/null +++ b/internal/casino/big_six_test.go @@ -0,0 +1,293 @@ +package casino + +import ( + "math" + "math/big" + "math/rand" + "strings" + "testing" +) + +func newBigSixTest() *bigSixTable { + return newBigSixTable(1, TableConfig{ID: "wheel", Game: GameBigSix}, rand.New(rand.NewSource(123))) +} +func forceBigSix(t *bigSixTable, spot string) []Event { + for i, s := range bigSixWheel { + if s == spot { + t.winningIndex = i + return t.settle() + } + } + panic("unknown test symbol") +} +func TestBigSixRTPAndRegistry(t *testing.T) { + cfg := TableConfig{ID: "wheel", Game: GameBigSix} + if err := cfg.Validate(); err != nil { + t.Fatal(err) + } + if cfg.Normalize().BettingTicks != 25 || cfg.HelpTopic() != "big6" || newTable(1, cfg, newTestRand()) == nil { + t.Fatal("registration") + } + counts := map[string]int{} + for _, s := range bigSixWheel { + counts[s]++ + } + if len(bigSixWheel) != 51 || counts["DOUBLE"] != 1 { + t.Fatal(counts) + } + for _, s := range BigSixSpots() { + if counts[s.Label] != s.Count { + t.Fatal(s) + } + if s.Pays == 0 { + continue + } + rtp := float64(s.Count*(s.Pays+1)) / float64(len(bigSixWheel)-2*counts["DOUBLE"]) + if rtp < .978 || rtp > .985 || math.Abs(rtp-48.0/49) > 1e-12 { + t.Fatal(s, rtp) + } + if e := cfg.HouseEdge(s.Label); len(e.Parts) != 1 || math.Abs(e.Parts[0].Loss-(1-rtp)) > 1e-12 { + t.Fatal(e) + } + } + if len(cfg.HouseEdge("DOUBLE").Parts) != 0 { + t.Fatal("DOUBLE is not a target") + } +} +func TestBigSixMultipleBetsAndDouble(t *testing.T) { + table := newBigSixTest() + table.join("a") + w := &recordingWallet{testWallet: testWallet{credits: 100}} + table.bet("a", "$1", 10, w) + table.bet("a", "sword", 2, w) + table.bet("a", "1", 3, w) + if table.phase != phaseWaiting || len(w.open) != 3 { + t.Fatal("bet started spin or lost wager") + } + table.action("a", "spin") + if table.phase != phasePlaying { + t.Fatal("spin") + } + forceBigSix(table, "DOUBLE") + forceBigSix(table, "DOUBLE") + if table.multiplier.String() != "4" || w.paid != 0 || len(w.open) != 3 { + t.Fatal("DOUBLE must ride all bets without cashing out") + } + table.bet("a", "2", 1, w) + table.action("a", "unbet") + if len(w.open) != 3 || len(table.players.get("a").bets) != 3 { + t.Fatal("bet/unbet accepted during respin") + } + events := forceBigSix(table, "1") + if w.paid != 104 || len(w.open) != 0 || len(w.payStakes) != 3 { + t.Fatal("wrong payout/ledger", w) + } + for _, e := range events { + if len(e.Settlement) > 0 { + if len(e.Settlement) != 3 || e.Settlement[0].Stake != 10 || e.Settlement[0].Returned != 80 || e.Settlement[0].Odds != "7:1" { + t.Fatal(e.Settlement) + } + } + } + if table.multiplier.String() != "1" || table.phase != phaseWaiting { + t.Fatal("not reset") + } +} +func TestBigSixUncappedDoubleAndSafeOverflow(t *testing.T) { + table := newBigSixTest() + table.join("a") + w := &testWallet{credits: 10} + table.bet("a", "1", 1, w) + table.action("a", "spin") + for i := 0; i < 130; i++ { + forceBigSix(table, "DOUBLE") + } + if table.multiplier.Cmp(new(big.Int).Lsh(big.NewInt(1), 130)) != 0 { + t.Fatal("DOUBLE capped or wrapped") + } + events := forceBigSix(table, "1") + if w.paid != 0 || len(w.open) != 1 { + t.Fatal("overflow must not pay or consume pending stake") + } + for _, e := range events { + if len(e.Settlement) > 0 { + t.Fatal("announced unbanked payout") + } + } + table.refundAll() + if len(w.open) != 0 { + t.Fatal("recovery did not refund") + } +} +func TestBigSixMultiplayerClockWithoutBets(t *testing.T) { + table := newBigSixTest() + table.join("a") + table.join("b") + if table.phase != phaseBetting || table.window.timer.remaining() != 25 { + t.Fatal("join must open betting") + } + table.action("a", "spin") + for i := 0; i < 24; i++ { + table.tick() + if table.phase != phaseBetting { + t.Fatal("early spin", i) + } + } + table.tick() + if table.phase != phasePlaying { + t.Fatal("empty round did not spin") + } + forceBigSix(table, "2") + if table.phase != phaseBetting || table.window.timer.remaining() != 25 { + t.Fatal("next window") + } +} +func TestBigSixAnimationGeometryAndTiming(t *testing.T) { + sawLong, sawVariation := false, false + length := 0 + for round := 0; round < 250; round++ { + table := newBigSixTest() + table.startSpin() + frames := append([]bigSixFrame(nil), table.frames...) + if length != 0 && length != len(frames) { + sawVariation = true + } + length = len(frames) + fast := 0 + prevStep, prevDelay := 10, 1 + for i, f := range frames { + if f.delay == 1 { + fast++ + } + if f.delay >= 8 { + sawLong = true + } + if f.move > prevStep || f.delay < prevDelay { + t.Fatalf("not slowing: %+v", frames) + } + if f.delay == 1 && (f.move < 4 || f.move > 8) || f.delay == 2 && (f.move < 2 || f.move > 4) || f.delay == 3 && (f.move < 1 || f.move > 2) { + t.Fatal(frames) + } + // Only the very last click may take a long pause; everything else + // is capped so the wheel never crawls. + if i < len(frames)-1 && f.delay > 4 { + t.Fatalf("wait uncapped before the last click: %+v", frames) + } + prevStep, prevDelay = f.move, f.delay + } + if fast < 3 || fast > 6 { + t.Fatal("fast count", fast) + } + // The final approach is all single-space clicks, with an optional + // stationary click at the very end. + approach := 0 + for i := len(frames) - 1; i >= 0 && frames[i].move <= 1; i-- { + approach++ + } + if approach < 3 { + t.Fatalf("wheel must click one space at least 3 times at rest: %+v", frames) + } + if frames[len(frames)-1].move > 1 { + t.Fatalf("final click must stay or advance one space: %+v", frames) + } + winner := table.winningIndex + position := table.position + for i, f := range frames { + for wait := 1; wait < f.delay; wait++ { + if e := table.tick(); len(e) != 0 { + t.Fatal("early frame") + } + } + events := table.tick() + position = (position + f.move) % len(bigSixWheel) + var s *BigSixSnapshot + for _, e := range events { + if v, ok := e.Snapshot.(*BigSixSnapshot); ok { + s = v + break + } + } + if s == nil { + t.Fatal("missing frame") + } + count := 11 + if i == len(frames)-1 { + count = 9 + if position != winner { + t.Fatal("outcome teleport") + } + } + if len(s.Pockets) != count { + t.Fatal("strip width", s) + } + for j, p := range s.Pockets { + if p != bigSixWheel[(position+j-count/2+51)%51] { + t.Fatal("invented neighbor") + } + } + } + } + if !sawLong || !sawVariation { + t.Fatal("no suspense variation") + } +} +func TestBigSixUnbetAndDisconnect(t *testing.T) { + for _, spinning := range []bool{false, true} { + table := newBigSixTest() + table.join("a") + w := &testWallet{credits: 100} + table.bet("a", "1", 10, w) + table.bet("a", "2", 20, w) + if spinning { + table.action("a", "spin") + forceBigSix(table, "DOUBLE") + } else { + table.action("a", "unbet") + if w.paid != 30 || len(w.open) != 0 || table.players.get("a").HasBet { + t.Fatal("unbet") + } + table.bet("a", "1", 10, w) + } + table.markDisconnected("a") + if len(w.open) != 0 || table.players.count() != 0 || table.phase != phaseWaiting { + t.Fatal("disconnect cleanup") + } + if spinning && w.paid != 0 { + t.Fatal("selective refund exploit") + } + } +} +func TestBigSixSettlementFailureAndDeparture(t *testing.T) { + table := newBigSixTest() + table.join("a") + w := &failingPayoutWallet{testWallet: testWallet{credits: 100}} + table.bet("a", "1", 10, w) + table.action("a", "spin") + events := forceBigSix(table, "1") + if len(w.open) != 1 { + t.Fatal("failed payout consumed stake") + } + found := false + for _, e := range events { + if strings.Contains(e.Message, "pending recovery") { + found = true + } + if len(e.Settlement) > 0 { + t.Fatal("fake receipt") + } + } + if !found { + t.Fatal("missing error") + } + table.refundAll() + table.bet("a", "1", 1, &testWallet{credits: 10}) + table.action("a", "leave_after") + table.action("a", "spin") + events = forceBigSix(table, "2") + if table.players.count() != 0 { + t.Fatal("queued departure") + } + if s, ok := events[0].Snapshot.(*BigSixSnapshot); !ok || len(s.Players) != 1 || s.Players[0] != "a" { + t.Fatal("departing player lost result visibility") + } +} |
