Game sessions, commands, snapshots, and time #
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.
Timing #
Continuous #
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 and published runs require the
continuous policy; earlier frame-based saves and results are rejected.
Complete snapshots and restore #
session.snapshot() returns detached JSON-safe data. Version 1 includes:
- Rules, explicit board subdivision and fingerprint, placement compatibility identifier, seed, and mode. Checkpoints without subdivision 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 exposes every piece directly and neither earns nor uses saved pieces.
Selecting the eraser makes place/place-at remove just the aimed triangle and
its color; empty targets do nothing. Erasing awards no points, consumes no random
draws or inventory, and does not count as a placement, miss, or clear. Selecting
a shape returns to placement. The checkpoint field erasing preserves the
selected tool, and timed checkpoints cannot enable the eraser. Browser clicks
and taps use the same commands in both projections.
With either a shape or the eraser selected, dragging orbits/pans the board and
pinching or scrolling zooms. Erasing happens on a click or tap, not during a drag.
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, 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. Incomplete artifacts from earlier pre-release builds are rejected; this format does not migrate omitted fields. 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. The strict pre-release cleanup changes existing challenge IDs; no migration or old-ID compatibility is provided. 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.
Scope #
This establishes the local simulation boundary. Network transport, authentication, match rules, server deployment, and display-only opponent boards remain separate work. The server must assign authoritative timestamps and order; client-provided timestamps alone do not make a battle authoritative.
The browser runtime now scopes each HUD to its own root and disposes renderers, audio, listeners, observers, and animation frames when leaving the game page. See the SvelteKit migration for that lifecycle.
Before optimizing for multiple boards, profile the intended battle layout. Current potential costs include recoloring cells on broad render revisions, uploading the Lambert cell texture each frame, and recomputing HUD presentation and reward availability. These are inspection findings, not measured frame-rate regressions. Share immutable topology and placement indexes where possible; use measurements to decide whether board changes, previews, and animated time need separate update paths. Preserve invalidation for occupancy, inventory, pause, active rewards, restart, and phase changes.