package casino import ( "math" "math/rand" "strings" "testing" ) func newCrazyTimeTest() *crazyTimeTable { return newCrazyTimeTable(1, TableConfig{ID: "crazy", Game: GameCrazyTime, MinBet: 1, MaxBet: 100000}, rand.New(rand.NewSource(99))) } func TestCrazyTimeRegistrationAndRTP(t *testing.T) { cfg := TableConfig{ID: "crazy", Game: GameCrazyTime} if err := cfg.Validate(); err != nil { t.Fatal(err) } if cfg.Normalize().MaxPlayers != 1 { t.Fatal("crazy time must be single-seat") } if cfg.HelpTopic() != "crazy time" { t.Fatalf("help topic %q", cfg.HelpTopic()) } if newTable(1, cfg, newTestRand()) == nil { t.Fatal("registration failed") } for _, spot := range crazyTimeSpots() { rtp := crazyTimeRTPTargetFor(spot.Label) if math.Abs(rtp-crazyTimeRTPTarget) > 1e-9 { t.Fatalf("%s RTP = %.12f, want %.12f", spot.Label, rtp, crazyTimeRTPTarget) } e := cfg.HouseEdge(spot.Label) if len(e.Parts) != 1 || math.Abs(e.Parts[0].Loss-(1-crazyTimeRTPTarget)) > 1e-9 { t.Fatalf("%s edge %+v", spot.Label, e) } dist := crazyTimeTopDists[spot.Label] if dist.total <= 0 { t.Fatalf("%s empty top dist", spot.Label) } for _, w := range dist.weights { if w < 0 { t.Fatalf("%s negative weight", spot.Label) } } mean := 0.0 for i, w := range dist.weights { mean += float64(dist.values[i]) * w } mean /= dist.total if math.Abs(mean-crazyTimeTopTarget(spot.Label)) > 1e-9 { t.Fatalf("%s top mean %.9f target %.9f", spot.Label, mean, crazyTimeTopTarget(spot.Label)) } } } func TestCrazyTimeWheelCountsAndPositiveBonuses(t *testing.T) { counts := map[string]int{} for _, s := range crazyTimeWheel { counts[s]++ } if len(crazyTimeWheel) != crazyTimePockets { t.Fatalf("wheel has %d pockets", len(crazyTimeWheel)) } total := 0 for _, spot := range crazyTimeSpots() { if counts[spot.Label] != spot.Count { t.Fatalf("%s count %d want %d", spot.Label, counts[spot.Label], spot.Count) } total += spot.Count } if total != crazyTimePockets { t.Fatalf("counts sum to %d", total) } // Every bonus distribution is strictly positive. for _, v := range ctHuntValues { if v < 1 { t.Fatalf("cash hunt value %d is not positive", v) } } for _, v := range ctPachinkoValues { if v < 1 { t.Fatalf("pachinko value %d is not positive", v) } } for _, v := range ctCoinLowValues { if v < 1 { t.Fatalf("coin low value %d is not positive", v) } } for _, v := range ctCoinHighValues { if v < 1 { t.Fatalf("coin high value %d is not positive", v) } } for _, v := range ctBonusWheelValues { if v < 1 { t.Fatalf("crazy wheel value %d is not positive", v) } } } func TestCrazyTimeBetSpinSettle(t *testing.T) { table := newCrazyTimeTest() w := &recordingWallet{testWallet: testWallet{credits: 1000}} table.join("a") table.bet("a", "1", 10, w) table.bet("a", "crazy time", 5, w) if len(table.bets) != 2 || len(w.open) != 2 { t.Fatalf("bets not committed: %+v open=%d", table.bets, len(w.open)) } // Capture the fixed outcome before the wheel settles and resets it. table.topSymbol, table.topMult = "2", 7 events := table.action("a", "spin") if table.phase != ctSpinning || len(events) == 0 { t.Fatal("spin did not start") } win := table.winSymbol topSymbol, topMult := table.topSymbol, table.topMult if win == "" { t.Fatal("no winning symbol") } // Drive the wheel to completion. for i := 0; i < 400 && table.phase == ctSpinning; i++ { table.tick() } // A bonus may still be animating; drive it out too. for i := 0; i < 1000 && table.phase == ctBonusing; i++ { table.tick() } mult := 1 if topSymbol == win { mult = topMult } want := 0 if win == "1" { want += 10 * 2 * mult } if win == "CRAZY TIME" { want += 5 * table.bonusValue * mult } if w.paid != want { t.Fatalf("paid %d want %d (win=%s top=%s x%d)", w.paid, want, win, topSymbol, topMult) } if len(w.open) != 0 { t.Fatalf("stakes left open: %d", len(w.open)) } } func TestCrazyTimeAllBonusesOnlyPayPositive(t *testing.T) { for _, kind := range []string{ctBonusCoinFlip, ctBonusCashHunt, ctBonusPachinko, ctBonusWheel} { for seed := int64(0); seed < 40; seed++ { table := newCrazyTimeTable(1, TableConfig{ID: "c", Game: GameCrazyTime, MinBet: 1, MaxBet: 100000}, rand.New(rand.NewSource(seed+1))) table.name = "a" table.phase = ctBonusing table.winSymbol = labelForBonus(kind) table.topSymbol, table.topMult = "10", 1 switch kind { case ctBonusCoinFlip: table.buildCoinFlipLocked() case ctBonusCashHunt: table.buildCashHuntLocked() case ctBonusPachinko: table.buildPachinkoLocked() case ctBonusWheel: table.buildBonusWheelLocked() } if table.bonusValue < 1 { t.Fatalf("%s produced %d", kind, table.bonusValue) } if len(table.prints) == 0 { t.Fatalf("%s produced no frames", kind) } } } } func labelForBonus(kind string) string { for _, s := range crazyTimeSpots() { if s.Bonus == kind { return s.Label } } return "" } func TestCrazyTimeUnbetRefunds(t *testing.T) { table := newCrazyTimeTest() w := &testWallet{credits: 100} table.join("a") table.bet("a", "5", 10, w) if w.credits != 90 { t.Fatalf("stake not debited: %d", w.credits) } table.action("a", "unbet") if w.paid != 10 || len(w.open) != 0 { t.Fatalf("unbet did not refund: paid=%d open=%d", w.paid, len(w.open)) } } func TestCrazyTimeAllBetAndRebet(t *testing.T) { table := newCrazyTimeTest() w := &testWallet{credits: 1000} table.join("a") events := table.bet("a", "all", 10, w) if len(table.bets) != 4 { t.Fatalf("ALL placed %d wagers, want 4", len(table.bets)) } if w.credits != 960 || len(w.open) != 4 { t.Fatalf("ALL debit: credits=%d open=%d", w.credits, len(w.open)) } for _, e := range events { if e.Menu != nil { t.Fatalf("bet returned a menu (wallet would print): %+v", e) } } // Settle on a losing number, then rebet every remembered minigame wager. table.winSymbol = "10" table.topSymbol, table.topMult = "1", 1 table.settleLocked() if len(table.bets) != 0 { t.Fatal("settlement did not clear the round") } before := w.credits events = table.rebetAll("a", w) if len(table.bets) != 4 { t.Fatalf("rebet placed %d wagers, want 4", len(table.bets)) } if w.credits != before-40 { t.Fatalf("rebet debit: credits=%d want %d", w.credits, before-40) } for _, e := range events { if e.Menu != nil { t.Fatalf("rebet returned a menu: %+v", e) } } } func TestCrazyTimeResolveSpot(t *testing.T) { cases := map[string]string{ "1": "1", "2": "2", "5": "5", "10": "10", "co": "COIN FLIP", "coin flip": "COIN FLIP", "coinflip": "COIN FLIP", "ca": "CASH HUNT", "cash hunt": "CASH HUNT", "p": "PACHINKO", "pach": "PACHINKO", "cr": "CRAZY TIME", "crazy time": "CRAZY TIME", } for in, want := range cases { got, ok := crazyTimeResolveSpot(in) if !ok || got != want { t.Fatalf("%q -> %q,%v want %q", in, got, ok, want) } } if _, ok := crazyTimeResolveSpot("c"); ok { t.Fatal("ambiguous c should not resolve") } } func TestCrazyTimeRulesText(t *testing.T) { rules := crazyTimeRules(TableConfig{}) if !strings.Contains(rules, "98%") || strings.Contains(rules, "%%") { t.Fatalf("rules text: %q", rules) } if !strings.Contains(rules, "CASH HUNT") || !strings.Contains(rules, "Top Slot") { t.Fatalf("rules text missing content: %q", rules) } } // TestCrazyTimeSettlementMath pins the payout arithmetic: a winning target // pays stake * total-return-multiplier * Top Slot multiplier, and a losing // target returns nothing. func TestCrazyTimeSettlementMath(t *testing.T) { cases := []struct { spot, win, topSym string bonus, topMult int stake, want int }{ {"1", "1", "1", 0, 5, 10, 10 * 2 * 5}, {"1", "10", "1", 0, 5, 10, 0}, {"CASH HUNT", "CASH HUNT", "CASH HUNT", 3, 4, 7, 7 * 3 * 4}, {"CASH HUNT", "CASH HUNT", "COIN FLIP", 500, 9, 2, 2 * 500}, {"CRAZY TIME", "CRAZY TIME", "CRAZY TIME", 64, 50, 1, 64 * 50}, } for _, tc := range cases { table := newCrazyTimeTable(1, TableConfig{ID: "c", Game: GameCrazyTime, MinBet: 1, MaxBet: 100000}, rand.New(rand.NewSource(3))) w := &testWallet{credits: MaxCredits / 2} table.name = "a" table.wallet = w wager, err := w.Wager(tc.stake) if err != nil { t.Fatal(err) } table.bets = []crazyTimeBet{{spot: tc.spot, wager: wager}} table.winSymbol = tc.win table.bonusValue = tc.bonus table.topSymbol, table.topMult = tc.topSym, tc.topMult before := w.paid table.settleLocked() if got := w.paid - before; got != tc.want { t.Fatalf("%s win=%s bonus=%d top=%s x%d stake=%d paid %d want %d", tc.spot, tc.win, tc.bonus, tc.topSym, tc.topMult, tc.stake, got, tc.want) } } } func TestCrazyTimeSpinDelaysFirstMove(t *testing.T) { table := newCrazyTimeTest() w := &testWallet{credits: 1000} table.join("a") table.bet("a", "1", 10, w) events := table.action("a", "spin") if len(events) == 0 || events[0].Snapshot == nil { t.Fatal("spin did not print the opening wheel") } // The spin command and the first engine tick share a cycle, so the opening // position must survive that tick before the wheel advances. if ev := table.tick(); len(ev) != 0 { t.Fatalf("first tick advanced the wheel immediately: %+v", ev) } if ev := table.tick(); len(ev) == 0 { t.Fatal("second tick did not advance the wheel") } }