94 lines
3.8 KiB
Markdown
94 lines
3.8 KiB
Markdown
# 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<T>`
|
||
|
||
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<T> = {
|
||
progress?: ProgressReducer<T>
|
||
state?: StateReducer<T, S>
|
||
}
|
||
|
||
type ProgressReducer<T> = (events: MoveEvent<T>[]) => number // 0–100
|
||
type StateReducer<T, S> = (events: MoveEvent<T>[]) => S // full game state
|
||
```
|
||
|
||
`MoveEvent<T>` 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<T>[]` — 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<T>[]` — 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.
|