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

66 lines
2.5 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.
type ReplayAdapter<T> = {
progress?: ProgressReducer<T>
}
type ProgressReducer<T> = (events: MoveEvent<T>[]) => number // 0100
```
`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
```
## 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.