import assert from "node:assert/strict"; import test from "node:test"; import { FixedStepClock, NetworkClock, ReplayDivergenceError, SpatialGridIndex, createBinaryProtocol, createJsonCodec, deterministicHash, defineGame, defineMultiplayerGame, defineNetworkedGame, withLagCompensation, withReplayTransport, withSpatialReplication, withTimeTravel, } from "../dist/index.js"; test("spatial replication indexes, prioritizes, budgets, and projects per viewer", () => { const grid = new SpatialGridIndex(10); grid.insert({ id: "negative", x: -4, z: -7 }); grid.insert({ id: "near", x: 6, z: 2 }); grid.insert({ id: "far", x: 40, z: 40 }); assert.deepEqual( grid.query({ x: 0, z: 0 }, 10).map(({ id }) => id).sort(), ["near", "negative"], ); const codec = createJsonCodec(); const base = defineMultiplayerGame({ clock: { ticksPerSecond: 10, snapshotsPerSecond: 10 }, authority: { createInitialState: () => ({ entities: [ { id: 1, x: 0, z: 0 }, { id: 2, x: 10, z: 0 }, { id: 3, x: 20, z: 0 }, { id: 4, x: 200, z: 0 }, ], }), cloneState: (state) => ({ entities: state.entities.map((entity) => ({ ...entity })) }), applyInput() {}, step() {}, }, prediction: { createInitialState: () => ({ entities: [], events: [] }), cloneState: (state) => ({ entities: state.entities.map((entity) => ({ ...entity })), events: [...state.events] }), applyInput() {}, step() {}, applyEvent(state, event) { state.events.push(event); }, }, visibility: { createSnapshot: (authority) => ({ entities: authority.entities.map((entity) => ({ ...entity })), events: [], }), perceive: (_authority, event) => event, }, input: { validate: () => true }, encoding: { input: codec, clientState: codec, perception: codec }, }); const game = withSpatialReplication(base, { cellSize: 16, bandwidthBudgetBytesPerSecond: 1_000, viewerPosition: (authority, playerId) => authority.entities.find(({ id }) => id === playerId) ?? null, sources: [ { category: "entities", maximumDistance: 100, entities: (authority) => authority.entities, estimatedBytes: 40, required: (entity, playerId) => entity.id === playerId, }, ], projectSnapshot(snapshot, selection) { return { ...snapshot, entities: snapshot.entities.filter((entity) => selection.has("entities", entity.id)), }; }, perceive(_authority, event, perception, selection) { return selection.has("entities", event.sourceId) ? perception : null; }, }); const server = game.createServer(); server.addPlayer(1); assert.deepEqual( server.createSnapshot(1, 0).state.entities.map(({ id }) => id), [1, 2], ); const selection = game.spatialReplication.select(server.currentState, { playerId: 1, tick: server.tick, }); assert.equal(selection.budgetBytes, 100); assert.equal(selection.usedBytes, 80); assert.equal(selection.candidateCount, 3); assert.equal(selection.droppedCount, 1); assert.deepEqual(server.createPerceptions(1, [{ sourceId: 2 }]), [{ sourceId: 2 }]); assert.deepEqual(server.createPerceptions(1, [{ sourceId: 3 }]), []); }); test("the developer-facing API compiles named sections into a networked game", () => { const codec = createJsonCodec(); const game = defineMultiplayerGame({ clock: { ticksPerSecond: 12, snapshotsPerSecond: 4 }, authority: { createInitialState: () => ({ value: 0 }), cloneState: (state) => ({ ...state }), applyInput(state, input) { state.value += input.amount; }, step() {}, }, prediction: { createInitialState: () => ({ value: 0 }), cloneState: (state) => ({ ...state }), applyInput(state, input) { state.value += input.amount; }, step() {}, }, visibility: { createSnapshot: (authority) => ({ value: authority.value }), }, input: { validate: (input) => Number.isFinite(input.amount), }, encoding: { input: codec, clientState: codec, }, }); assert.equal(game.tickRateHz, 12); assert.equal(game.snapshotRateHz, 4); const server = game.createServer(); server.addPlayer(1); assert.equal( server.submitInput(1, { sequence: 1, targetTick: 1, observedTick: 0, input: { amount: 3 }, }).accepted, true, ); server.step(); assert.equal(server.currentState.value, 3); assert.deepEqual(codec.decode(codec.encode({ unicode: "flux ⚡" })), { unicode: "flux ⚡", }); }); const testGame = defineGame({ tickRateHz: 10, snapshotRateHz: 5, createInitialState: () => ({ position: 0, velocity: 0 }), cloneState: (state) => ({ ...state }), validateInput: (input) => Number.isFinite(input.axis) && Math.abs(input.axis) <= 1, applyInput: (state, input) => { state.velocity = input.axis; }, step: (state, { deltaSeconds }) => { state.position += state.velocity * deltaSeconds; }, validateState: (state) => Number.isFinite(state.position) && Number.isFinite(state.velocity), validateClientState: (authoritative, candidate) => Math.abs(authoritative.position - candidate.position) < 0.000_001, }); test("defineGame creates matching authoritative and predicted runtimes", () => { const server = testGame.createServer(); const client = testGame.createClient(); server.addPlayer(7); client.initialize(7, server.createSnapshot(0)); const packet = client.createInput({ axis: 1 }, 1); assert.deepEqual(server.submitInput(7, packet), { accepted: true }); client.step(); const result = server.step(); assert.equal(result.snapshotDue, false); assert.deepEqual(result.acknowledgements, [ { playerId: 7, sequence: packet.sequence }, ]); client.acknowledge(packet.sequence); client.reconcile(server.createSnapshot(100)); assert.equal(client.currentState.position, server.currentState.position); const valid = server.submitStateReport(7, client.createStateReport()); assert.equal(valid?.valid, true); const invalid = server.submitStateReport(7, { tick: server.tick, state: { position: 99, velocity: 0 }, }); assert.equal(invalid?.valid, false); assert.deepEqual( server.submitInput(7, { sequence: packet.sequence + 1, targetTick: server.tick + 1, input: { axis: 2 }, }), { accepted: false, reason: "invalid" }, ); }); test("fixed-step clocks cap catch-up work", () => { const clock = new FixedStepClock({ rateHz: 10, maxCatchUpSteps: 3 }); let steps = 0; assert.equal(clock.advance(0, () => steps++), 0); assert.equal(clock.advance(100, () => steps++), 1); assert.equal(clock.advance(1_000, () => steps++), 3); assert.equal(steps, 4); }); test("network clock estimates RTT, offset, and input lead", () => { const clock = new NetworkClock(); const ping = clock.createPing(100); const stats = clock.receivePong( { ...ping, serverReceivedAt: 160, serverSentAt: 162, }, 202, ); assert.equal(stats.roundTripTime, 100); assert.equal(stats.clockOffset, 10); assert.equal(clock.toServerTime(300), 310); assert.equal(clock.recommendedInputLeadTicks(60), 4); }); test("binary protocol round-trips generic inputs, state, and control frames", () => { const numberCodec = { encode(value) { const payload = new Uint8Array(8); new DataView(payload.buffer).setFloat64(0, value, true); return payload; }, decode(payload) { return new DataView( payload.buffer, payload.byteOffset, payload.byteLength, ).getFloat64(0, true); }, }; const protocol = createBinaryProtocol({ input: numberCodec, state: numberCodec, event: numberCodec, }); const input = { kind: "input", packet: { sequence: 4, targetTick: 20, observedTick: 16, input: 0.75, }, }; assert.deepEqual(protocol.decodeClient(protocol.encodeClient(input)), input); const welcome = { kind: "welcome", playerId: 9, snapshot: { tick: 18, serverTime: 42.5, state: 3.25 }, }; assert.deepEqual(protocol.decodeServer(protocol.encodeServer(welcome)), welcome); const rejection = { kind: "reject-input", sequence: 44 }; assert.deepEqual( protocol.decodeServer(protocol.encodeServer(rejection)), rejection, ); const replayStart = { kind: "replay-start", ticketId: 12, perspectiveId: 9, fromTick: 100, toTick: 160, frameCount: 2, playbackRate: 0.75, }; assert.deepEqual( protocol.decodeServer(protocol.encodeServer(replayStart)), replayStart, ); const replayFrame = { kind: "replay-frame", ticketId: 12, tick: 120, state: 3.5, events: [4.5, 5.5], }; assert.deepEqual( protocol.decodeServer(protocol.encodeServer(replayFrame)), replayFrame, ); const truncatedReplayFrame = protocol.encodeServer(replayFrame).slice(0, -1); assert.throws(() => protocol.decodeServer(truncatedReplayFrame), RangeError); const replayEnd = { kind: "replay-end", ticketId: 12 }; assert.deepEqual( protocol.decodeServer(protocol.encodeServer(replayEnd)), replayEnd, ); }); test("networked games keep authority private and derive viewer perceptions", () => { const jsonCodec = { encode: (value) => new TextEncoder().encode(JSON.stringify(value)), decode: (payload) => JSON.parse(new TextDecoder().decode(payload)), }; const privateGame = defineNetworkedGame({ tickRateHz: 10, snapshotRateHz: 5, server: { createInitialState: () => ({ publicValues: new Map(), secrets: new Map(), }), cloneState: (state) => ({ publicValues: new Map(state.publicValues), secrets: new Map(state.secrets), }), addPlayer(state, { playerId }) { state.publicValues.set(playerId, 0); state.secrets.set(playerId, `secret-${playerId}`); }, applyInput(state, input, { playerId, emit }) { state.publicValues.set(playerId, input); emit({ source: playerId, privateValue: state.secrets.get(playerId) }); }, step() {}, }, client: { createInitialState: () => ({ ownValue: 0, cues: [] }), cloneState: (state) => ({ ...state, cues: [...state.cues] }), applyInput(state, input) { state.ownValue = input; }, step() {}, applyEvent(state, event) { state.cues.push(event); }, }, replication: { createSnapshot(authority, { playerId }) { return { ownValue: authority.publicValues.get(playerId) ?? 0, cues: [], }; }, validateClientState(authority, candidate, { playerId }) { return candidate.ownValue === authority.publicValues.get(playerId); }, perceive(_authority, event, { playerId }) { return event.source === playerId ? null : "heard-an-event"; }, }, validateInput: (input) => Number.isFinite(input), codecs: { input: jsonCodec, state: jsonCodec, event: jsonCodec }, }); const server = privateGame.createServer(); server.addPlayer(1); server.addPlayer(2); const snapshot = server.createSnapshot(1, 0); assert.deepEqual(snapshot.state, { ownValue: 0, cues: [] }); assert.equal("secrets" in snapshot.state, false); assert.equal( server.submitInput(2, { sequence: 1, targetTick: 1, input: 5 }).accepted, true, ); const result = server.step(); assert.deepEqual(server.createPerceptions(1, result.events), [ "heard-an-event", ]); assert.deepEqual(server.createPerceptions(2, result.events), []); const client = privateGame.createClient(); client.initialize(1, snapshot); client.receiveEvent("heard-an-event", result.tick); assert.deepEqual(client.currentState.cues, ["heard-an-event"]); const eventFrame = privateGame.protocol.encodeServer({ kind: "event", tick: result.tick, event: "heard-an-event", }); assert.deepEqual(privateGame.protocol.decodeServer(eventFrame), { kind: "event", tick: result.tick, event: "heard-an-event", }); }); const lagJsonCodec = { encode: (value) => new TextEncoder().encode(JSON.stringify(value)), decode: (payload) => JSON.parse(new TextDecoder().decode(payload)), }; const lagBaseGame = defineNetworkedGame({ tickRateHz: 10, snapshotRateHz: 5, server: { createInitialState: () => ({ positions: {}, resolutions: [] }), cloneState: (state) => ({ positions: { ...state.positions }, resolutions: state.resolutions.map((entry) => ({ ...entry })), }), addPlayer(state, { playerId }) { state.positions[playerId] = 0; }, applyInput(state, input, { playerId }) { if (input.moveTo !== undefined) state.positions[playerId] = input.moveTo; }, step() {}, }, client: { createInitialState: () => ({ positions: {}, resolutions: [] }), cloneState: (state) => structuredClone(state), applyInput() {}, step() {}, }, replication: { createSnapshot: (state) => structuredClone(state), }, validateInput: (input) => (input.moveTo === undefined || Number.isFinite(input.moveTo)) && (input.action === undefined || ["rewind", "current", "projectile", "strict"].includes(input.action)), codecs: { input: lagJsonCodec, state: lagJsonCodec, event: lagJsonCodec }, }); const lagPolicyGame = withLagCompensation(lagBaseGame, { historySeconds: 2, captureState: (state) => ({ positions: { ...state.positions } }), cloneHistoricalState: (state) => ({ positions: { ...state.positions } }), classifyAction: (input) => input.action ? { type: input.action, targetId: input.targetId ?? 2 } : null, cloneAction: (action) => ({ ...action }), actions: { rewind: { mode: "rewind", maximumRewindMs: 1_000, resolve: recordLagResolution, }, current: { mode: "current", resolve: recordLagResolution, }, projectile: { mode: "fast-forward", maximumRewindMs: 1_000, resolve: recordLagResolution, }, strict: { mode: "rewind", maximumRewindMs: 100, outOfWindow: "reject", resolve: recordLagResolution, }, }, }); const lagReplayGame = withTimeTravel(lagPolicyGame, { createSeed: () => 0, hashState: deterministicHash, checkpointIntervalTicks: 2, verificationIntervalTicks: 1, }); function recordLagResolution(context) { context.currentState.resolutions.push({ type: context.action.type, requestedTick: context.requestedTick, resolvedTick: context.resolvedTick, currentTick: context.currentTick, rewindTicks: context.rewindTicks, catchUpTicks: context.catchUpTicks, historicalPosition: context.historicalState.positions[context.action.targetId], currentPosition: context.currentState.positions[context.action.targetId], }); } test("lag-compensation HOF applies an independent policy per action", () => { const server = lagPolicyGame.createServer(); server.addPlayer(1); server.addPlayer(2); server.submitInput(2, { sequence: 1, targetTick: 1, observedTick: 0, input: { moveTo: 5 }, }); server.step(); server.step(); server.submitInput(1, { sequence: 1, targetTick: 3, observedTick: 0, input: { action: "rewind", targetId: 2 }, }); server.step(); server.submitInput(1, { sequence: 2, targetTick: 4, observedTick: 0, input: { action: "current", targetId: 2 }, }); server.step(); server.submitInput(1, { sequence: 3, targetTick: 5, observedTick: 2, input: { action: "projectile", targetId: 2 }, }); server.step(); server.submitInput(1, { sequence: 4, targetTick: 6, observedTick: 0, input: { action: "strict", targetId: 2 }, }); server.step(); assert.equal(server.currentState.positions[2], 5); assert.deepEqual(server.currentState.resolutions, [ { type: "rewind", requestedTick: 0, resolvedTick: 0, currentTick: 3, rewindTicks: 3, catchUpTicks: 0, historicalPosition: 0, currentPosition: 5, }, { type: "current", requestedTick: 0, resolvedTick: 4, currentTick: 4, rewindTicks: 0, catchUpTicks: 0, historicalPosition: 5, currentPosition: 5, }, { type: "projectile", requestedTick: 2, resolvedTick: 2, currentTick: 5, rewindTicks: 3, catchUpTicks: 3, historicalPosition: 5, currentPosition: 5, }, ]); }); test("lag-compensated actions replay deterministically through checkpoints", () => { const server = lagReplayGame.createServer(); server.addPlayer(1); server.addPlayer(2); server.submitInput(2, { sequence: 1, targetTick: 1, observedTick: 0, input: { moveTo: 5 }, }); server.step(); server.step(); server.submitInput(1, { sequence: 1, targetTick: 3, observedTick: 0, input: { action: "rewind", targetId: 2 }, }); for (let tick = 2; tick < 6; tick += 1) server.step(); const replay = lagReplayGame.createReplay(server.exportRecording()); assert.deepEqual(replay.seek(6).state, server.currentState); assert.equal(replay.currentState.resolutions[0].historicalPosition, 0); }); const replayCodec = { encode: (value) => new TextEncoder().encode(JSON.stringify(value)), decode: (payload) => JSON.parse(new TextDecoder().decode(payload)), }; const replayDefinition = defineNetworkedGame({ tickRateHz: 10, snapshotRateHz: 5, server: { createInitialState: () => ({ seed: 0, secret: "uninitialized", positions: new Map(), velocities: new Map(), }), cloneState: (state) => ({ ...state, positions: new Map(state.positions), velocities: new Map(state.velocities), }), addPlayer(state, { playerId, emit }) { state.positions.set(playerId, state.seed); state.velocities.set(playerId, 0); emit({ type: "joined", playerId }); }, removePlayer(state, { playerId }) { state.positions.delete(playerId); state.velocities.delete(playerId); }, applyInput(state, input, { playerId, emit }) { state.velocities.set(playerId, input.axis); emit({ type: "moved", playerId }); }, step(state, { deltaSeconds }) { for (const [playerId, velocity] of state.velocities) { state.positions.set( playerId, (state.positions.get(playerId) ?? 0) + velocity * deltaSeconds, ); } }, }, client: { createInitialState: () => ({ ownPosition: 0, cues: [] }), cloneState: (state) => ({ ...state, cues: [...state.cues] }), applyInput(state, input) { state.ownPosition += input.axis / 10; }, step() {}, applyEvent(state, event) { state.cues.push(event); }, }, replication: { createSnapshot(authority, { playerId }) { return { ownPosition: authority.positions.get(playerId) ?? 0, cues: [], }; }, perceive(_authority, event, { playerId }) { return event.playerId === playerId ? null : `heard-${event.type}`; }, }, validateInput: (input) => Number.isFinite(input.axis) && Math.abs(input.axis) <= 1, codecs: { input: replayCodec, state: replayCodec, event: replayCodec, }, }); const replayGame = withTimeTravel(replayDefinition, { createSeed: () => 1, initializeState(state, seed) { state.seed = seed; state.secret = `secret-${seed}`; }, hashState: deterministicHash, checkpointIntervalTicks: 2, verificationIntervalTicks: 1, }); const replayTransportGame = withReplayTransport(replayGame, { historySeconds: 0.6, captureRateHz: 5, createTickets(event, { tick }) { return event.type === "joined" && event.playerId === 2 ? { requesterId: 1, perspectiveId: 2, fromTick: 0, toTick: tick, } : null; }, authorizeReplay: ({ requesterId, perspectiveId }) => requesterId === 1 && perspectiveId === 2, }); test("time-travel HOF records, seeks, verifies, filters, and branches", () => { const server = replayGame.createServer({ replaySeed: 7 }); server.addPlayer(1); server.addPlayer(2); assert.equal( server.submitInput(1, { sequence: 1, targetTick: 1, input: { axis: 1 }, }).accepted, true, ); assert.equal( server.submitInput(2, { sequence: 1, targetTick: 1, input: { axis: -0.5 }, }).accepted, true, ); for (let tick = 0; tick < 3; tick += 1) server.step(); assert.equal( server.submitInput(1, { sequence: 2, targetTick: 5, input: { axis: 0.25 }, }).accepted, true, ); for (let tick = 3; tick < 6; tick += 1) server.step(); const recording = server.exportRecording(); assert.equal(recording.seed, 7); assert.equal(recording.durationTicks, 6); assert.deepEqual( recording.checkpoints.map(({ tick }) => tick), [0, 2, 4, 6], ); const replay = replayGame.createReplay(recording); const finalFrame = replay.seek(6); assert.deepEqual(finalFrame.state, server.currentState); assert.equal(finalFrame.hash, deterministicHash(server.currentState)); const privateView = replay.viewAs(1, 123); assert.equal(privateView.tick, 6); assert.equal("secret" in privateView.state, false); assert.deepEqual(Object.keys(privateView.state).sort(), ["cues", "ownPosition"]); replay.seek(1); assert.deepEqual(replay.perceptionsAs(1), ["heard-joined", "heard-moved"]); const rewindPosition = replay.currentState.positions.get(1); replay.seek(6); assert.notEqual(replay.currentState.positions.get(1), rewindPosition); const branch = replay.branch(2); assert.equal( branch.submitInput(1, { sequence: 2, targetTick: 3, input: { axis: -1 }, }).accepted, true, ); while (branch.tick < 6) branch.step(); assert.notEqual( deterministicHash(branch.currentState), deterministicHash(server.currentState), ); const branchReplay = replayGame.createReplay(branch.exportRecording()); assert.deepEqual(branchReplay.seek(6).state, branch.currentState); assert.deepEqual(replay.seek(6).state, server.currentState); }); test("time-travel replay pinpoints deterministic divergence", () => { const server = replayGame.createServer({ replaySeed: 4 }); server.addPlayer(1); server.submitInput(1, { sequence: 1, targetTick: 1, input: { axis: 1 }, }); for (let tick = 0; tick < 3; tick += 1) server.step(); const recording = server.exportRecording(); const corrupted = { ...recording, verifications: recording.verifications.map((entry) => entry.tick === 3 ? { ...entry, hash: "corrupted" } : entry, ), }; const replay = replayGame.createReplay(corrupted); assert.throws(() => replay.seek(3), ReplayDivergenceError); }); test("rolling authority recordings stay bounded and remain seekable", () => { const server = replayGame.createServer({ replaySeed: 8, recordingHistoryTicks: 4, }); server.addPlayer(1); for (let tick = 0; tick < 10; tick += 1) server.step(); const recording = server.exportRecording(); assert.equal(recording.startTick, 6); assert.equal(recording.durationTicks, 10); assert.ok(recording.verifications.every(({ tick }) => tick >= 6)); assert.ok(recording.checkpoints.every(({ tick }) => tick >= 6)); const replay = replayGame.createReplay(recording); assert.equal(replay.startTick, 6); assert.deepEqual(replay.seek(10).state, server.currentState); assert.throws(() => replay.seek(5), RangeError); }); test("replay transport issues only authorized projected frames from a bounded ring", () => { const server = replayTransportGame.createServer({ replaySeed: 11 }); server.addPlayer(1); server.addPlayer(2); for (let tick = 0; tick < 12; tick += 1) server.step(); const automaticTickets = server.drainReplayTickets(1); assert.equal(automaticTickets.length, 1); assert.equal(automaticTickets[0].perspectiveId, 2); assert.equal( server.issueReplay({ requesterId: 1, perspectiveId: 1, fromTick: 0, toTick: 12, }), null, ); const ticket = server.issueReplay({ requesterId: 1, perspectiveId: 2, fromTick: 0, toTick: 12, playbackRate: 0.5, }); assert.ok(ticket); assert.equal(ticket.requesterId, 1); assert.equal(ticket.perspectiveId, 2); assert.equal(ticket.fromTick, 6); assert.equal(ticket.toTick, 12); assert.equal(ticket.playbackRate, 0.5); assert.ok(ticket.frames.length > 1); assert.ok( ticket.frames.every( ({ state }) => !Object.hasOwn(state, "secret") && Object.keys(state).sort().join(",") === "cues,ownPosition", ), ); assert.deepEqual(server.drainReplayTickets(1), [ticket]); assert.deepEqual(server.drainReplayTickets(1), []); assert.equal(server.exportRecording().startTick, 6); });