# Replay adapter interface The replay engine is **game-agnostic**: it schedules and delivers the events in a [`@cozy-games/move-log`](../../move-log) envelope over time, but it never interprets what an event *means*. All game meaning enters through a **game adapter** — the seam defined here. This is the concrete realization of the progress-reducer item in [ADR 0002](../../../docs/decisions/0002-game-adapter-pattern.md). ## `ReplayAdapter` An adapter is a plain object supplied at construction: ```js new PlaybackClock(envelope, deps, adapter) ``` ```ts // Typed generically over the game's event vocabulary T (and state S). type ReplayAdapter = { progress?: ProgressReducer state?: StateReducer } type ProgressReducer = (events: MoveEvent[]) => number // 0–100 type StateReducer = (events: MoveEvent[]) => S // full game state ``` `MoveEvent` is the move-log record `{ seq, t, event, receivedTs? }`, where `event` is the game's own payload — opaque to the engine. ## `progress(events) → %` The only adapter method today. It maps the **ordered slice of events delivered so far** (every event whose offset ≤ the current playback position) to a completion percentage. - **Input:** `MoveEvent[]` — the played-so-far slice, in order. To compute a percentage the adapter typically needs a total (e.g. total safe cells); it owns that context, usually by closing over the board it was built from. The engine passes only the slice. - **Output:** a number in `[0, 100]`. The engine **clamps** the result into range and throws if the reducer returns a non-number, so an adapter can be permissive. - **When:** call `clock.progress()` at any time. It returns `null` if no adapter (or no `progress`) was supplied — the engine never invents a percentage. ```js // A minesweeper-style adapter, built over its board (illustrative): const adapter = { progress: (events) => { const revealed = events.filter(e => e.event.type === 'reveal').length return (revealed / totalSafeCells) * 100 } } const clock = new PlaybackClock(envelope, {}, adapter) clock.seek(1500) clock.progress() // → e.g. 42 ``` ## `state(events) → S` (full-board mode) The second reducer reconstructs the **complete game state** at a playback point from the ordered slice of events delivered so far. It powers full-board replay — rebuilding the whole board on seek, not just a percentage. - **Input:** `MoveEvent[]` — the played-so-far slice, in order. - **Output:** the game's own state type `S` (opaque to the engine). For mnswpr it's a 2D board snapshot: `{ rows, cols, phase, revealedSafe, cells }`. - **When:** `clock.state()` returns the current reconstruction, and `onState` streams `{ position, state }` as playback advances or seeks. ### Flag-gated Full-board mode is **off by default** and gated behind a runtime flag — the engine's minimal, documented feature-flag seam: ```js new PlaybackClock(envelope, deps, adapter, { fullBoard: true }) ``` When the flag is **off** (default), the mode is fully inert: `state()` returns `null`, `onState` never fires, and the state reducer is never invoked (no reconstruction cost). When **on** with a `state` reducer supplied, `state()` and `onState` reconstruct the board — and `seek(t)` rebuilds the exact state at `t`. ## Contract rules - **The engine calls the reducer; it never interprets events itself.** Engine source references only envelope types (`MoveEvent` / `MoveLog`) and the log's recording metadata (`seq`, `t`) — never an event's `.event` payload. This is enforced by a guard in `test/playback-clock.test.js`. - **The adapter owns all game meaning** — event vocabulary, progress math, and (as the contract grows) state reduction and terminal predicates per ADR 0002. - **Typed generically over `T`** so one engine serves every game.