cozy-games/packages/replay/docs/adapter-interface.md

94 lines
3.8 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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 // 0100
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.