TypeScript 97.6%
SQL 1.4%
JavaScript 0.5%
1import { describe, expect, it } from "vitest";2import { CryptoRng, SeededRng, runSpin, simulate, validateDefinition, evaluateWays, evaluateLines, defineGame, sym, wildSym, scatterSym, LINES_10 } from "../src";3import type { GameDefinition } from "../src";45const mini = defineGame({6 slug: "test-ways",7 name: "Test Ways",8 version: "1.0.0",9 tagline: "",10 description: "",11 theme: "",12 grid: { reels: 5, rows: 3 },13 payModel: { type: "ways" },14 rtp: 0.96,15 volatility: "medium",16 maxMultiplier: 1000,17 betDivisor: 1,18 symbols: [19 wildSym("W", "Wild", [0, 1, 1, 1, 0], { shape: "ring", color: "#fff" }),20 scatterSym("S", "Scatter", 1, { shape: "star", color: "#fff" }, { 3: 2 }),21 sym("A", "A", "premium", { 3: 1, 4: 2, 5: 5 }, 3, { shape: "gem", color: "#f00" }),22 sym("B", "B", "high", { 3: 0.5, 4: 1, 5: 2 }, 4, { shape: "gem", color: "#0f0" }),23 sym("C", "C", "mid", { 3: 0.3, 4: 0.5, 5: 1 }, 6, { shape: "gem", color: "#00f" }),24 sym("D", "D", "low", { 3: 0.1, 4: 0.2, 5: 0.5 }, 9, { shape: "gem", color: "#ff0" }),25 sym("E", "E", "low", { 3: 0.1, 4: 0.2, 5: 0.5 }, 9, { shape: "gem", color: "#0ff" }),26 ],27 wild: { id: "W" },28 scatter: { id: "S", triggers: { 3: 5 } },29 presentation: { palette: { primary: "#fff", secondary: "#fff", glow: "#fff", bg: "#000", surface: "#000" }, particles: "stars", ambience: "neon", frame: "metal", backdrop: "grid" },30});3132describe("RNG", () => {33 it("CryptoRng.int is uniform-ish and in range", () => {34 const rng = new CryptoRng();35 const counts = new Array(6).fill(0);36 for (let i = 0; i < 60_000; i++) counts[rng.int(6)]++;37 for (const c of counts) expect(Math.abs(c - 10_000)).toBeLessThan(600);38 });39 it("weighted respects weights", () => {40 const rng = new CryptoRng();41 let ones = 0;42 for (let i = 0; i < 20_000; i++) if (rng.weighted([1, 3]) === 1) ones++;43 expect(ones / 20_000).toBeGreaterThan(0.72);44 expect(ones / 20_000).toBeLessThan(0.78);45 });46 it("SeededRng is deterministic", () => {47 const a = new SeededRng(42);48 const b = new SeededRng(42);49 for (let i = 0; i < 100; i++) expect(a.int(1000)).toBe(b.int(1000));50 });51});5253describe("evaluation", () => {54 it("ways: counts ways with wilds", () => {55 const grid = [56 ["A", "D", "E"],57 ["A", "A", "W"],58 ["A", "E", "D"],59 ["D", "E", "C"],60 ["A", "A", "A"],61 ];62 const wins = evaluateWays(mini, grid, { bet: 100 });63 const a = wins.find((w) => w.symbol === "A")!;64 expect(a.count).toBe(3);65 expect(a.ways).toBe(1 * 3 * 1);66 expect(a.payout).toBe(300); // 1 × bet × 3 ways67 });68 it("lines: substitutes wilds and pays longest match", () => {69 const def: GameDefinition = { ...mini, payModel: { type: "lines", lines: LINES_10 }, betDivisor: 10 };70 const grid = [71 ["D", "B", "D"],72 ["D", "W", "D"],73 ["D", "B", "D"],74 ["D", "B", "D"],75 ["D", "C", "D"],76 ];77 const wins = evaluateLines(def, grid, { bet: 100 });78 const mid = wins.find((w) => w.line === 0)!; // middle row = line 079 expect(mid.symbol).toBe("B");80 expect(mid.count).toBe(4);81 expect(mid.payout).toBe(10); // 1 × 100 / 1082 });83});8485describe("engine", () => {86 it("runSpin is pure and returns a coherent outcome", () => {87 const rng = new SeededRng(7);88 const out = runSpin(mini, 100, { rng });89 expect(out.bet).toBe(100);90 expect(out.totalWin).toBeGreaterThanOrEqual(0);91 expect(out.steps.length).toBeGreaterThan(0);92 expect(out.steps[0].grid.length).toBe(5);93 expect(out.multiplier).toBeCloseTo(out.totalWin / 100, 6);94 const sum = out.steps.reduce((a, s) => a + s.win, 0);95 expect(Math.min(sum, mini.maxMultiplier * 100)).toBe(out.totalWin);96 });97 it("never exceeds maxMultiplier", () => {98 const def = { ...mini, maxMultiplier: 2 };99 const rng = new SeededRng(3);100 for (let i = 0; i < 2000; i++) expect(runSpin(def, 100, { rng }).totalWin).toBeLessThanOrEqual(200);101 });102 it("simulation reports stable RTP for a seeded run", () => {103 const r = simulate(mini, { spins: 20_000, seed: 1 });104 expect(r.spins).toBe(20_000);105 expect(r.observedRtp).toBeGreaterThan(0);106 expect(r.hitRate).toBeGreaterThan(0.05);107 expect(r.distribution.reduce((a, b) => a + b.count, 0)).toBe(20_000);108 });109 it("validateDefinition catches bad definitions", () => {110 const bad = { ...mini, rtp: 1.2, slug: "Bad Slug" };111 const issues = validateDefinition(bad);112 expect(issues.some((i) => i.message.includes("RTP"))).toBe(true);113 expect(issues.some((i) => i.message.includes("kebab"))).toBe(true);114 });115});116