Sessions and snapshots #
This is the engine contract. Start with Architecture for code ownership and lifecycle; public formats are in records and APIs.
GameSession is the mutation boundary exported by src/core/game.ts. It owns
the game engine, command sequence, simulation clock, and cumulative active
playing time. session.view is the read-only interface used by the HUD and
renderers. The core does not depend on
the DOM or Three.js.
Commands and ownership #
const session = new GameSession(board, placements, seed, "timed", {
policy: "continuous",
initialTimeMs: 0,
});
session.dispatch({ type: "start", anchor: 12 }, 0);
session.dispatch({ type: "move", anchor: 300 }, 1000);
session.dispatch({ type: "place" }, 1200);
session.advanceTo(2000);
Commands include start, restart, pause, resume, move, rotate, flip, place,
place-at, use-reward, and free-mode select-piece/select-eraser. place-at performs aim and
placement together. Move targets identify a board cell and optionally a point
inside that cell. Placements are computed from the local canonical index;
commands cannot supply cell arrays, occupancy, scores, RNG state, or a fabricated
placement.
dispatch(command, atMs) allocates the next sequence number. An authority that
already assigns sequence numbers uses
receive({ sequence, atMs, command }). Sequences start at 1 and must be contiguous.
Times must be monotonic and within 0..MAX_SESSION_TIME_MS (a finite range below
Number.MAX_SAFE_INTEGER, with timer and catch-up headroom). Larger clock values
cannot reliably represent short deadlines and are rejected. Command shape, IDs,
and surface points are validated before time advances. A rejected envelope changes neither the clock,
sequence, nor game. A well-formed command that is unavailable in the current
phase consumes its sequence and returns accepted: true, applied: false.
applied means the command had an effect; an occupied placement can produce
rejection feedback without locking a piece. Use game outcomes and successful-lock
or clear counters when reacting to completed placements.
totalPlayingTimeMs counts time while the session is in playing or
resolving, excluding ready time, pauses, and time after game over. It is
owned by GameSession; command timestamps and clock advances account for
elapsed intervals without double counting. A successful start or restart resets
it to zero. Its snapshot value is finite and nonnegative, no larger than
Number.MAX_SAFE_INTEGER, and may be fractional because the browser clock can
have sub-millisecond precision.
The view has no dispatch or tick method. Its collections expose read-only operations, and its arrays, points, board data, and canonical placements are protected from mutation. Renderers receive a dispatch callback only when they are intended to control a local board. Test setup is isolated from this production interface.
Continuous timing #
The browser and simulation use continuous timing. The caller supplies
one logical monotonic clock in milliseconds. advanceTo processes all elapsed
piece and feedback deadlines. A command first advances to its timestamp, including
deadlines at that exact timestamp, then applies the action. A place or rotate
command arriving exactly when the piece expires therefore sees the resulting
phase, not the expired piece.
Deadlines use absolute timestamps and are preserved in snapshots. Different partitions of clock advancement produce the same state for the same ordered commands. A single advance or command gap over 24 hours is rejected; a host catching up a longer interval must advance in smaller chunks.
Pause freezes game timers while command order and session time continue. Resume restarts deadlines from the preserved remainder. A future battle server must decide who may pause or restart; this library does not authenticate players or grant those permissions. Hiding a browser tab or disconnecting a client is not a pause command. The authority should continue advancing its clock independently of connected displays.
The browser uses this same timing behavior: a delayed frame catches up every
elapsed deadline. New pieces keep the current board anchor rather than sampling
the camera at a render frame. Snapshots require the continuous policy;
published runs encode it in the gameplay rules version.
Complete snapshots and restore #
session.snapshot() returns detached JSON-safe data. Version 1 includes:
- Rules, explicit board polyhedron, subdivision and fingerprint, placement compatibility identifier, seed, and mode. Checkpoints without board metadata are rejected.
- RNG algorithm version, state and draw count, current piece, queue, and saved-piece inventory.
- Occupancy and color entries, score, misses, and cleared-face count.
- Anchor, surface point, heading, reflection, and canonical placement identity.
- Phase, pause state, current and suspended piece timers, and feedback state.
- Event counters, revision, command sequence, clock origin and time, latest
command timestamp, exact pending deadline, and required cumulative
totalPlayingTimeMs.
const stored = JSON.stringify(session.snapshot());
const result = replacementSession.restore(JSON.parse(stored));
if (!result.restored) throw new Error(result.reason);
The replacement must use the same seed, mode, timing policy, compatible board, and placement index. Restore validates detached data before committing it. Malformed or incompatible data leaves the running session unchanged. Input arrays are not retained by reference. Restore preserves sequence and time: continue with the next sequence number and the restored logical clock, rather than substituting a new process's uptime. The cumulative playing-time total is preserved when the clock is rebased. Snapshots that omit it or contain a non-finite, negative, or out-of-range value are rejected; snapshot version 1 has no migration path for omitted data.
Displays use view.renderRevision to invalidate cached geometry and colors.
Successful restore invalidates that local counter even when the restored
serialized revision equals the previously displayed revision. It is separate
from the revision preserved in checkpoints and replays.
Consumers that track event counters should reset their cursors after restore;
checkpoint restoration does not itself replay historical sound or clear events.
Free Play's eraser uses the same place/place-at commands, removing only the
aimed cell and its color. Empty targets do nothing. It awards no points and
consumes no draws or inventory. The required erasing checkpoint flag preserves
the tool; timed checkpoints cannot enable it.
restore(snapshot, { rebaseTimeMs }) can attach a validated checkpoint to a new
monotonic clock after a page reload. It preserves game timers and command
sequence while rebasing the session clock and any pending deadline. Ordinary
restore without this option retains the snapshot's original clock for exact
continuation on the same logical timeline.
Scenarios and challenges #
A board scenario is reusable starting-board content: its version, explicit polyhedron and subdivision, board fingerprint, occupied cell IDs, and the piece/color ID for each occupied cell. The current board is identified by its exact topology fingerprint, and scenario validation checks that every stored cell and piece ID belongs to that board. The scenario identity is derived from this canonical content. A saved name, record ID, and save time are repository metadata and do not affect identity.
A challenge definition combines a scenario with a positive starting level, the gameplay rules version, RNG algorithm version, and timing policy. Its identity accounts for each of these, so changing the layout, level, rules, RNG algorithm, or timing policy changes the challenge category. It excludes a run's seed and live checkpoint: those describe one attempt, not the scoring category. For score comparisons, use the challenge identity.
Starting a challenge creates a fresh timed session with the scenario occupancy and colors, the selected effective level, zero score/misses/clears, an empty saved-piece inventory, and a new RNG sequence. Clears in the run advance level from that starting level. Score multipliers and piece timers use the effective level. Restart resets the run seed sequence and counters while retaining the challenge layout and starting level.
A challenge checkpoint stores both the immutable challenge definition and the complete live session snapshot. The definition says what run was started; the session snapshot says where that run currently is, including its seed, RNG progress, queue, score, timers, and counters. Restoring requires a matching challenge setup and validates the checkpoint before changing the session. Ordinary snapshots without scenario/challenge setup remain valid. Every game snapshot requires its starting level, RNG version, and eraser state. Missing required fields are rejected without conversion. Checkpoints do not contain event or replay logs.
Restore is a trusted administrative operation for checkpoints, reconnects, and replays. Validation checks representation and internal consistency; it does not prove that a score or inventory was earned. Never expose arbitrary snapshot restore as a player command. Bump the rules compatibility version when changing semantics that would make old checkpoints behave differently.
Random sequence compatibility #
New game checkpoints and challenge definitions include
rngVersion: "mulberry32-v1". This identifies the exact seed normalization,
32-bit generator state transition, and conversion to a number in [0, 1) in
src/core/random.ts. The seed selects a sequence; the state and draw count mark
the saved position in that sequence. The version identifies how to interpret
all three. Reference-output tests pin the current sequence, including seed zero
and 32-bit wraparound.
Unknown, omitted, or malformed RNG versions are rejected before restoring state. The engine currently implements only this one algorithm; adding a future algorithm requires either explicit dispatch to retained implementations or rejecting unsupported versions. Changing the identifier alone does not implement backward compatibility.
Every challenge identity hashes its explicit RNG version along with the scenario, starting level, rules version, and timing policy. Scenario identities do not depend on randomness.
Keep three kinds of version distinct: the snapshot version describes the JSON shape; the rules version describes gameplay, including mapping random numbers to piece IDs and when draws occur; the RNG version describes the number generator. Change the RNG identifier if its output changes. Change the rules version if piece selection or draw consumption changes, even if the generator is unchanged.
JavaScript's integer bit operations and Math.imul specify the operations used
here, so this sequence does not depend on the CPU architecture. A port to another
language must reproduce those unsigned 32-bit operations and output conversion.
These fields add constant-size compatibility metadata, not a move history. They support continuing the correct sequence, but do not prove that a player earned their score or inventory. Full verification still needs the starting setup, seed, compatible rules and generator, and an action/time history.
Network authority #
Transport, authentication, match rules, and opponent displays remain separate from this engine. A server must assign authoritative timestamps and order; client-provided timestamps alone do not establish authority. Snapshot restore is trusted administration, never an ordinary player command. See the roadmap for the first multiplayer slice.