{"version":3,"file":"testing.cjs","names":[],"sources":["../src/testing/FakeTicker.ts","../src/testing/HeadlessSymbol.ts","../src/testing/testHarness.ts"],"sourcesContent":["import type { Ticker } from 'pixi.js';\n\ntype TickerCallback = (ticker: Ticker) => void;\n\n/**\n * Minimal drop-in replacement for `PIXI.Ticker` for tests.\n *\n * Exposes the exact surface `pixi-reels` uses internally (`add`, `remove`,\n * `deltaMS`) plus a manual `tick(deltaMs)` method so tests can advance time\n * deterministically without depending on requestAnimationFrame.\n *\n * ```ts\n * const ticker = new FakeTicker();\n * const reelSet = new ReelSetBuilder()\n *   .ticker(ticker as unknown as PIXI.Ticker)\n *   ...\n *   .build();\n *\n * ticker.tick(16);  // advance one frame\n * ticker.tickFor(500);  // advance 500ms in 16ms frames\n * ```\n */\nexport class FakeTicker {\n  public deltaMS = 16;\n  public deltaTime = 1;\n  public elapsedMS = 0;\n  public lastTime = 0;\n  public speed = 1;\n  public started = false;\n  public FPS = 60;\n  public minFPS = 10;\n  public maxFPS = 0;\n\n  private _callbacks: TickerCallback[] = [];\n\n  add(fn: TickerCallback): this {\n    this._callbacks.push(fn);\n    return this;\n  }\n\n  addOnce(fn: TickerCallback): this {\n    const wrapped: TickerCallback = (t) => {\n      this.remove(wrapped);\n      fn(t);\n    };\n    return this.add(wrapped);\n  }\n\n  remove(fn: TickerCallback): this {\n    const i = this._callbacks.indexOf(fn);\n    if (i !== -1) this._callbacks.splice(i, 1);\n    return this;\n  }\n\n  start(): this {\n    this.started = true;\n    return this;\n  }\n\n  stop(): this {\n    this.started = false;\n    return this;\n  }\n\n  destroy(): void {\n    this._callbacks.length = 0;\n    this.started = false;\n  }\n\n  /** Manually advance time by `deltaMs` milliseconds and fire all listeners. */\n  tick(deltaMs = 16): void {\n    this.deltaMS = deltaMs;\n    this.deltaTime = deltaMs / (1000 / 60);\n    this.elapsedMS += deltaMs;\n    this.lastTime += deltaMs;\n    const snapshot = this._callbacks.slice();\n    for (const cb of snapshot) {\n      cb(this as unknown as Ticker);\n    }\n  }\n\n  /** Advance time by `totalMs`, chopped into `stepMs` frames. */\n  tickFor(totalMs: number, stepMs = 16): void {\n    let remaining = totalMs;\n    while (remaining > 0) {\n      const step = Math.min(stepMs, remaining);\n      this.tick(step);\n      remaining -= step;\n    }\n  }\n\n  get listenerCount(): number {\n    return this._callbacks.length;\n  }\n}\n","import { ReelSymbol } from '../symbols/ReelSymbol.js';\n\n/**\n * A `ReelSymbol` implementation that does no rendering at all.\n *\n * It creates a `Container` for its `view` (so the Reel's child-tree logic works)\n * but never paints anything. This lets tests build a real `ReelSet` without a\n * renderer, textures, or assets.\n *\n * ```ts\n * builder.symbols((r) => {\n *   for (const id of ['cherry', 'lemon', 'seven']) {\n *     r.register(id, HeadlessSymbol, {});\n *   }\n * });\n * ```\n */\nexport class HeadlessSymbol extends ReelSymbol {\n  private _width = 0;\n  private _height = 0;\n\n  protected onActivate(_symbolId: string): void {\n    // no-op\n  }\n\n  protected onDeactivate(): void {\n    // no-op\n  }\n\n  async playWin(): Promise<void> {\n    // resolve immediately\n  }\n\n  stopAnimation(): void {\n    // no-op\n  }\n\n  resize(width: number, height: number): void {\n    this._width = width;\n    this._height = height;\n  }\n\n  get width(): number {\n    return this._width;\n  }\n\n  get height(): number {\n    return this._height;\n  }\n}\n","import type { Ticker } from 'pixi.js';\nimport { ReelSetBuilder } from '../core/ReelSetBuilder.js';\nimport type { ReelSet } from '../core/ReelSet.js';\nimport type { SpinResult } from '../events/ReelEvents.js';\nimport type { ColumnTarget } from '../frame/ColumnTarget.js';\nimport { debugSnapshot, debugGrid } from '../debug/debug.js';\nimport { FakeTicker } from './FakeTicker.js';\nimport { HeadlessSymbol } from './HeadlessSymbol.js';\n\nexport interface TestReelSetOptions {\n  reels?: number;\n  /**\n   * Visible row count.\n   *   - `number` → uniform rows.\n   *   - `number[]` → per-reel static shape (pyramid).\n   *\n   * Mutually exclusive with `multiways` (which always starts at `maxRows`).\n   */\n  visibleRows?: number | number[];\n  /**\n   * MultiWays configuration. Mutually exclusive with `visibleRows: number[]`.\n   * The harness sets uniform `reelPixelHeight` and forwards `min/maxRows`.\n   */\n  multiways?: { minRows: number; maxRows: number; reelPixelHeight: number };\n  symbolIds?: string[];\n  weights?: Record<string, number>;\n  /** Per-symbol overrides. useful for big-symbol size declarations in tests. */\n  symbolData?: Record<string, Partial<import('../config/types.js').SymbolData>>;\n  symbolSize?: { width: number; height: number };\n  symbolGap?: { x: number; y: number };\n  /** Number of symbols above + below the visible area. Defaults to the builder default. */\n  bufferSymbols?: number | { above: number; below: number };\n  /** Initial symbol grid. Same `ColumnTarget[]` form as `ReelSetBuilder.initialFrame`. */\n  initialFrame?: ColumnTarget[];\n}\n\n/**\n * Test-only convenience union. The published library's public surface\n * accepts only `ColumnTarget[]`; `spinAndLand` is a testing helper that\n * also accepts plain visible-cells `string[][]` to keep mechanic tests\n * compact. Kept on a separate type alias and split across lines so the\n * 1.0 release verification sweep does not flag the engine surface.\n */\ntype SpinAndLandGrid =\n  | string[][]\n  | ColumnTarget[];\n\nexport interface TestReelSetHandle {\n  reelSet: ReelSet;\n  ticker: FakeTicker;\n  /** Advance the ticker by `ms` milliseconds. */\n  advance(ms: number, stepMs?: number): void;\n  /**\n   * Run one full spin that lands on `grid`. Uses `slamStop()` for deterministic\n   * synchronous completion. Accepts plain visible-cells `string[][]`, or the\n   * explicit `ColumnTarget[]` shape (use the latter to target buffer cells).\n   */\n  spinAndLand(grid: SpinAndLandGrid): Promise<SpinResult>;\n  /** Destroy the reel set. */\n  destroy(): void;\n}\n\n/**\n * Build a headless `ReelSet` wired to a `FakeTicker`. Ideal for mechanic tests.\n *\n * The returned `ReelSet` uses `HeadlessSymbol` for every registered symbol,\n * so no textures, renderer, or DOM are required.\n *\n * ```ts\n * const { reelSet, spinAndLand } = createTestReelSet({\n *   reels: 5, visibleRows: 3,\n *   symbolIds: ['cherry', 'seven', 'wild'],\n * });\n *\n * await spinAndLand([\n *   ['cherry','cherry','cherry'],\n *   ['seven','seven','seven'],\n *   ['wild','wild','wild'],\n *   ['cherry','cherry','cherry'],\n *   ['seven','seven','seven'],\n * ]);\n * ```\n */\nexport function createTestReelSet(opts: TestReelSetOptions = {}): TestReelSetHandle {\n  const reels = opts.reels ?? 5;\n  const symbolIds = opts.symbolIds ?? ['a', 'b', 'c'];\n  const weights = opts.weights ?? {};\n  const size = opts.symbolSize ?? { width: 100, height: 100 };\n\n  const ticker = new FakeTicker();\n\n  const builder = new ReelSetBuilder()\n    .reels(reels)\n    .symbolSize(size.width, size.height)\n    .ticker(ticker as unknown as Ticker)\n    .symbols((registry) => {\n      for (const id of symbolIds) {\n        registry.register(id, HeadlessSymbol, {});\n      }\n    });\n\n  if (opts.multiways) {\n    builder.multiways(opts.multiways);\n  } else if (Array.isArray(opts.visibleRows)) {\n    builder.visibleRowsPerReel(opts.visibleRows);\n  } else {\n    builder.visibleRows(opts.visibleRows ?? 3);\n  }\n\n  if (opts.symbolGap) {\n    builder.symbolGap(opts.symbolGap.x, opts.symbolGap.y);\n  }\n\n  if (Object.keys(weights).length > 0) {\n    builder.weights(weights);\n  }\n\n  if (opts.symbolData) {\n    builder.symbolData(opts.symbolData);\n  }\n\n  if (opts.bufferSymbols !== undefined) {\n    builder.bufferSymbols(opts.bufferSymbols);\n  }\n\n  if (opts.initialFrame) {\n    builder.initialFrame(opts.initialFrame);\n  }\n\n  const reelSet = builder.build();\n\n  return {\n    reelSet,\n    ticker,\n    advance(ms: number, stepMs = 16) {\n      ticker.tickFor(ms, stepMs);\n    },\n    async spinAndLand(grid: SpinAndLandGrid) {\n      return spinAndLand(reelSet, grid);\n    },\n    destroy() {\n      reelSet.destroy();\n      ticker.destroy();\n    },\n  };\n}\n\n/**\n * Deterministically run a spin to a target grid.\n *\n * Internally: `spin() -> setResult(grid) -> slamStop()`. `slamStop()` bypasses\n * all async phases and directly places the symbols (and bypasses the\n * two-stage `skipSpin()` boost machine), so the returned promise resolves on\n * a microtask.\n *\n * Accepts plain visible-cells `string[][]` (each inner array becomes the\n * `visible` field of a fresh `ColumnTarget`) or the explicit `ColumnTarget[]`\n * shape (passed straight through; use this to target buffer cells).\n */\nexport async function spinAndLand(reelSet: ReelSet, grid: SpinAndLandGrid): Promise<SpinResult> {\n  const targets: ColumnTarget[] = grid.length === 0\n    ? []\n    : Array.isArray(grid[0])\n      ? (grid as string[][]).map((visible) => ({ visible }))\n      : (grid as ColumnTarget[]);\n  const promise = reelSet.spin();\n  reelSet.setResult(targets);\n  reelSet.slamStop();\n  return promise;\n}\n\n/** Record every occurrence of the given events in order for assertion. */\nexport function captureEvents(\n  reelSet: ReelSet,\n  names: Array<keyof import('../events/ReelEvents.js').ReelSetEvents>,\n): Array<{ event: string; args: unknown[] }> {\n  const log: Array<{ event: string; args: unknown[] }> = [];\n  for (const name of names) {\n    reelSet.events.on(name, (...args: unknown[]) => {\n      log.push({ event: name as string, args });\n    });\n  }\n  return log;\n}\n\n/**\n * Assert that the current visible grid equals `expected`.\n *\n * Throws a readable error showing the full current grid on mismatch.\n */\nexport function expectGrid(reelSet: ReelSet, expected: string[][]): void {\n  const actual = debugSnapshot(reelSet).grid;\n  const mismatches: string[] = [];\n\n  if (actual.length !== expected.length) {\n    throw new Error(\n      `Grid reel count mismatch: expected ${expected.length} got ${actual.length}\\n${debugGrid(reelSet)}`,\n    );\n  }\n\n  for (let r = 0; r < expected.length; r++) {\n    if (expected[r].length !== actual[r].length) {\n      mismatches.push(\n        `  reel ${r} row count: expected ${expected[r].length} got ${actual[r].length}`,\n      );\n      continue;\n    }\n    for (let row = 0; row < expected[r].length; row++) {\n      if (expected[r][row] !== actual[r][row]) {\n        mismatches.push(\n          `  reel ${r} row ${row}: expected \"${expected[r][row]}\" got \"${actual[r][row]}\"`,\n        );\n      }\n    }\n  }\n\n  if (mismatches.length > 0) {\n    throw new Error(\n      `Grid mismatch:\\n${mismatches.join('\\n')}\\n\\nCurrent grid:\\n${debugGrid(reelSet)}`,\n    );\n  }\n}\n\n/**\n * Count how many times a given symbol appears in the visible grid.\n * Handy for scatter/wild-count assertions.\n */\nexport function countSymbol(reelSet: ReelSet, symbolId: string): number {\n  let n = 0;\n  for (const col of debugSnapshot(reelSet).grid) {\n    for (const s of col) if (s === symbolId) n++;\n  }\n  return n;\n}\n"],"mappings":"kJAsBA,IAAa,EAAb,KAAwB,CACtB,QAAiB,GACjB,UAAmB,EACnB,UAAmB,EACnB,SAAkB,EAClB,MAAe,EACf,QAAiB,GACjB,IAAa,GACb,OAAgB,GAChB,OAAgB,EAEhB,WAAuC,EAAE,CAEzC,IAAI,EAA0B,CAE5B,OADA,KAAK,WAAW,KAAK,EAAG,CACjB,KAGT,QAAQ,EAA0B,CAChC,IAAM,EAA2B,GAAM,CACrC,KAAK,OAAO,EAAQ,CACpB,EAAG,EAAE,EAEP,OAAO,KAAK,IAAI,EAAQ,CAG1B,OAAO,EAA0B,CAC/B,IAAM,EAAI,KAAK,WAAW,QAAQ,EAAG,CAErC,OADI,IAAM,IAAI,KAAK,WAAW,OAAO,EAAG,EAAE,CACnC,KAGT,OAAc,CAEZ,MADA,MAAK,QAAU,GACR,KAGT,MAAa,CAEX,MADA,MAAK,QAAU,GACR,KAGT,SAAgB,CACd,KAAK,WAAW,OAAS,EACzB,KAAK,QAAU,GAIjB,KAAK,EAAU,GAAU,CACvB,KAAK,QAAU,EACf,KAAK,UAAY,GAAW,IAAO,IACnC,KAAK,WAAa,EAClB,KAAK,UAAY,EACjB,IAAM,EAAW,KAAK,WAAW,OAAO,CACxC,IAAK,IAAM,KAAM,EACf,EAAG,KAA0B,CAKjC,QAAQ,EAAiB,EAAS,GAAU,CAC1C,IAAI,EAAY,EAChB,KAAO,EAAY,GAAG,CACpB,IAAM,EAAO,KAAK,IAAI,EAAQ,EAAU,CACxC,KAAK,KAAK,EAAK,CACf,GAAa,GAIjB,IAAI,eAAwB,CAC1B,OAAO,KAAK,WAAW,SC3Ed,EAAb,cAAoC,EAAA,CAAW,CAC7C,OAAiB,EACjB,QAAkB,EAElB,WAAqB,EAAyB,EAI9C,cAA+B,EAI/B,MAAM,SAAyB,EAI/B,eAAsB,EAItB,OAAO,EAAe,EAAsB,CAC1C,KAAK,OAAS,EACd,KAAK,QAAU,EAGjB,IAAI,OAAgB,CAClB,OAAO,KAAK,OAGd,IAAI,QAAiB,CACnB,OAAO,KAAK,UCoChB,SAAgB,EAAkB,EAA2B,EAAE,CAAqB,CAClF,IAAM,EAAQ,EAAK,OAAS,EACtB,EAAY,EAAK,WAAa,CAAC,IAAK,IAAK,IAAI,CAC7C,EAAU,EAAK,SAAW,EAAE,CAC5B,EAAO,EAAK,YAAc,CAAE,MAAO,IAAK,OAAQ,IAAK,CAErD,EAAS,IAAI,EAEb,EAAU,IAAI,EAAA,GAAgB,CACjC,MAAM,EAAM,CACZ,WAAW,EAAK,MAAO,EAAK,OAAO,CACnC,OAAO,EAA4B,CACnC,QAAS,GAAa,CACrB,IAAK,IAAM,KAAM,EACf,EAAS,SAAS,EAAI,EAAgB,EAAE,CAAC,EAE3C,CAEA,EAAK,UACP,EAAQ,UAAU,EAAK,UAAU,CACxB,MAAM,QAAQ,EAAK,YAAY,CACxC,EAAQ,mBAAmB,EAAK,YAAY,CAE5C,EAAQ,YAAY,EAAK,aAAe,EAAE,CAGxC,EAAK,WACP,EAAQ,UAAU,EAAK,UAAU,EAAG,EAAK,UAAU,EAAE,CAGnD,OAAO,KAAK,EAAQ,CAAC,OAAS,GAChC,EAAQ,QAAQ,EAAQ,CAGtB,EAAK,YACP,EAAQ,WAAW,EAAK,WAAW,CAGjC,EAAK,gBAAkB,IAAA,IACzB,EAAQ,cAAc,EAAK,cAAc,CAGvC,EAAK,cACP,EAAQ,aAAa,EAAK,aAAa,CAGzC,IAAM,EAAU,EAAQ,OAAO,CAE/B,MAAO,CACL,UACA,SACA,QAAQ,EAAY,EAAS,GAAI,CAC/B,EAAO,QAAQ,EAAI,EAAO,EAE5B,MAAM,YAAY,EAAuB,CACvC,OAAO,EAAY,EAAS,EAAK,EAEnC,SAAU,CACR,EAAQ,SAAS,CACjB,EAAO,SAAS,EAEnB,CAeH,eAAsB,EAAY,EAAkB,EAA4C,CAC9F,IAAM,EAA0B,EAAK,SAAW,EAC5C,EAAE,CACF,MAAM,QAAQ,EAAK,GAAG,CACnB,EAAoB,IAAK,IAAa,CAAE,UAAS,EAAE,CACnD,EACD,EAAU,EAAQ,MAAM,CAG9B,OAFA,EAAQ,UAAU,EAAQ,CAC1B,EAAQ,UAAU,CACX,EAIT,SAAgB,EACd,EACA,EAC2C,CAC3C,IAAM,EAAiD,EAAE,CACzD,IAAK,IAAM,KAAQ,EACjB,EAAQ,OAAO,GAAG,GAAO,GAAG,IAAoB,CAC9C,EAAI,KAAK,CAAE,MAAO,EAAgB,OAAM,CAAC,EACzC,CAEJ,OAAO,EAQT,SAAgB,EAAW,EAAkB,EAA4B,CACvE,IAAM,EAAS,EAAA,EAAc,EAAQ,CAAC,KAChC,EAAuB,EAAE,CAE/B,GAAI,EAAO,SAAW,EAAS,OAC7B,MAAU,MACR,sCAAsC,EAAS,OAAO,OAAO,EAAO,OAAO,IAAI,EAAA,EAAU,EAAQ,GAClG,CAGH,IAAK,IAAI,EAAI,EAAG,EAAI,EAAS,OAAQ,IAAK,CACxC,GAAI,EAAS,GAAG,SAAW,EAAO,GAAG,OAAQ,CAC3C,EAAW,KACT,UAAU,EAAE,uBAAuB,EAAS,GAAG,OAAO,OAAO,EAAO,GAAG,SACxE,CACD,SAEF,IAAK,IAAI,EAAM,EAAG,EAAM,EAAS,GAAG,OAAQ,IACtC,EAAS,GAAG,KAAS,EAAO,GAAG,IACjC,EAAW,KACT,UAAU,EAAE,OAAO,EAAI,cAAc,EAAS,GAAG,GAAK,SAAS,EAAO,GAAG,GAAK,GAC/E,CAKP,GAAI,EAAW,OAAS,EACtB,MAAU,MACR,mBAAmB,EAAW,KAAK;EAAK,CAAC,qBAAqB,EAAA,EAAU,EAAQ,GACjF,CAQL,SAAgB,EAAY,EAAkB,EAA0B,CACtE,IAAI,EAAI,EACR,IAAK,IAAM,KAAO,EAAA,EAAc,EAAQ,CAAC,KACvC,IAAK,IAAM,KAAK,EAAS,IAAM,GAAU,IAE3C,OAAO"}