Intermediate / Game loop
Introduce Fixed Simulation Steps with Bounded Catch-Up

The shared HoloViral loop advances each engine once per animation callback using a capped variable delta. That is easy to read and works for these simple rules. A fixed-step loop makes a different tradeoff: simulation updates use a constant interval, while rendering follows the browser's repaint schedule.
What you'll build
We will design an optional fixed-step scheduler around the collector's existing update(dt) interface. This is an extension, not the loop currently used by the demo. Its most important feature is a bounded catch-up policy rather than an unlimited attempt to replay every delayed update.
Prerequisites
Understand the current createLoop function and why movement multiplies speed by elapsed seconds. Keep the collector's rules unchanged while replacing the scheduler in a local copy. That lets you compare timing policies without also changing collision geometry.
Accumulate elapsed time
An accumulator stores time not yet simulated. Each animation callback adds an elapsed interval. While enough time remains for one fixed step, update consumes that step. Rendering happens after the update work for that callback.
const step = 1 / 60;
const maxSteps = 5;
let accumulator = 0;
function advance(elapsed) {
accumulator += Math.min(elapsed, step * maxSteps);
let updates = 0;
while (accumulator >= step && updates < maxSteps) {
update(step);
accumulator -= step;
updates++;
}
render();
}
This sketch omits the scheduling and lifecycle methods so the accumulator is visible. A complete replacement still needs the single-chain guard, cancellation, initial timestamp handling, and pause behavior found in the existing loop.
Bound catch-up work
Without a bound, a long stall can trigger a large number of updates in one callback. Those updates delay the next frame, which creates more backlog. A small game should not lock the page trying to catch up with several seconds of invisible simulation.
The sketch caps incoming time and limits work to five updates. It intentionally drops excess wall time. That is a documented policy, not a mathematically exact replay of everything that happened while the browser was busy. The displayed timer must still be described as simulation time if it advances only through those steps.
A different implementation could retain some backlog, pause automatically, or use a separate deadline clock. Pick a policy appropriate to the project rather than copying an accumulator and assuming its behavior is neutral.
Preserve the engine interface
The collector already accepts a delta, so a fixed scheduler can call the same update method with a constant value. Movement, spawning, and collision remain inside the engine. Avoid moving those rules into the scheduler simply because you are editing timing code.
The renderer can initially draw the latest completed state. That may appear slightly uneven when display and simulation rates differ. Interpolation is a later extension: store previous and current positions and render between them using the accumulator fraction. It requires extra model snapshots and should not be introduced without understanding what is being interpolated.
Discrete states such as lives or matched cards should not be visually averaged like positions. A half-matched card is not a meaningful simulation state. Interpolation belongs to selected rendering properties, not to every field in the model.
Handle pause and completion
On resume, reset the previous callback timestamp so paused wall time does not enter the accumulator. Decide whether to preserve a partial accumulated step or clear it; either choice should be consistent and tested. Clearing it is simple and loses at most a small fraction of simulation time.
If update finishes the game during a catch-up loop, stop further steps immediately. Checking only before the callback begins can allow several post-completion updates. Engine guards help, but the scheduler should also honor its stopped state before doing more work.
Compare behavior, not just frame rate
Run the same seeded object sequence through variable and fixed stepping. Compare collision outcomes, spawn counts, and active time under normal intervals and a long gap. A smoother graph alone does not prove the rules remained equivalent.
Also test whether the page remains responsive during a deliberately delayed callback. The purpose of the bound is to protect interaction, not merely to produce a particular count in a unit test.
Next steps
Keep the existing loop source as a baseline. Add the fixed-step version in a separate module, document its backlog policy, and switch schedulers without changing the engine factory. Only consider interpolation after the lifecycle and simulation results are clear.