← The API

Phaser AE

API docs

v3.0.0-2206 exports in 26 modulesreleased October 19, 2026

ContentsUndo, redo and replay

Need to get started? Install the Phaser AE CLI →

Tools

Undo, redo and replay

createHistory: undo, redo, seek and replay for deterministic rules, with refusals, checkpoints and validated saves

engine/kernel/src/history.ts

Constants

createHistory

const createHistory: CreateHistory

From @phaser-ae/kernel

Make a history over deterministic rules. See HistorySpec.

Types

Applied

type Applied<R> = { readonly ok: true; readonly result: R } | Refusal

From @phaser-ae/kernel

What a history reports for a move: accepted with whatever the rules returned, or refused.

CreateHistory

type CreateHistory = <S, M, R = unknown>(spec: HistorySpec<S, M, R>) => History<S, M, R>

From @phaser-ae/kernel

Make a history over deterministic rules. See HistorySpec.

History

interface History<S, M, R = unknown>

From @phaser-ae/kernel

A run of play that can be wound back and forth, saved and restored.

  • readonly state: S — The state as it now stands, after index moves. The live object: read it, and change it only through apply.
  • readonly seed: number — The seed this run started from.
  • readonly moves: ReadonlyArray<M> — The moves recorded, oldest first. Includes any ahead of index that a redo would reach.
  • readonly index: number — How many moves of moves have been applied: the playhead.
  • readonly canUndo: boolean — Is there anything to undo?
  • readonly canRedo: boolean — Is there anything to redo?
  • apply(move: M): Applied<R> — Apply a move and record it. Anything undone and not yet redone is discarded first, because play has branched. A refused move changes nothing and is not recorded.
  • undo(): boolean — Wind back one move. False when there was nothing to undo.
  • redo(): Applied<R> — Wind forward one undone move and report what the rules returned. Refused when there is nothing to redo.
  • seek(index: number): void — Wind to exactly index moves played, forwards or backwards. Clamped to the recorded range.
  • reset(seed?: number): void — Start again on a fresh state, from a new seed or the same one, with no moves.
  • snapshot(): HistorySave<M> — The run as a save. See HistorySave.
  • restore(save: HistorySave<M> | string): Applied<void> — Replace this run with a save, or its JSON text. The save is replayed into a fresh state first and checked; if it is not a history, a move is refused, or the fingerprint differs, this run is left exactly as it was.

HistorySave

interface HistorySave<M>

From @phaser-ae/kernel

A history as a save: plain JSON, safe to store, share or put in a bug report.

  • readonly format: 'phaser-ae-history' — Always 'phaser-ae-history'.
  • readonly version: 1 — Always 1.
  • readonly seed: number — The seed the run started from.
  • readonly moves: ReadonlyArray<M> — Every recorded move, oldest first, including any a redo would reach.
  • readonly index: number — How many of moves were applied: where the playhead was.
  • readonly fingerprint?: string — The state's fingerprint at index, when the spec has fingerprint.

HistorySpec

interface HistorySpec<S, M, R = unknown>

From @phaser-ae/kernel

How to build a run of play from nothing and push it forward one move.

  • create(seed: number): S — A fresh state from a seed. It must be pure in the seed: the same seed gives the same state every time, or undo lands somewhere the player has never been. Use createRng(seed) for any chance inside it.
  • apply(state: S, move: M): R | Refusal — Apply one move to state, in place. Return anything to accept it: the value comes back from apply and redo (a presenter animates from it). Return a Refusal, { ok: false, reason }, to refuse it, and leave the state unchanged. An exception is a refusal too, with its message as the reason.
  • seed?: number — The seed to start from. Default 0. Take it from game.rng (game.rng.int(0, 0xffffffff)) so a game replays.
  • checkpointEvery?: number — Keep a copy of the state every this many moves, so undo and seek replay from the nearest copy instead of from the start. Default 0: never, which suits rules that replay in microseconds. Copies are made with clone.
  • clone?(state: S): S — Copy a state, for checkpoints. Default structuredClone, which cannot copy functions or class methods.
  • limit?: number — The most moves to record. Past it a move is refused (history: limit reached) rather than an old one dropped, because dropping one would leave undo unable to rebuild the state. Default unlimited.
  • merge?(previous: M, next: M): M | undefined — Fold a move into the one before it, so they undo as one: return the combined move, or undefined to record them apart. Asked only after the new move was accepted, and only when it would be recorded straight after previous. The classic use is time: two { kind: 'advance', seconds } moves in a row become one, provided the rules step a fixed timestep so that advancing a then b lands where advancing a + b does.
  • fingerprint?(state: S): string — A short fingerprint of a state, such as a hash of its JSON. When given, a save carries the fingerprint of the state it ends in, and restore refuses a save whose replay ends anywhere else: a check that the rules really are deterministic and the save was not edited.
  • onChange?(history: History<S, M, R>): void — Called after anything changes the state or the moves: an apply, undo, redo, seek, reset or restore.

Refusal

interface Refusal

From @phaser-ae/kernel

A move the rules refused. Nothing is recorded, and the rules must leave the state exactly as it was.

  • readonly ok: false — Always false.
  • readonly reason: string — Why, in words a player could be shown.