What it does
A run is a single number, the distance along it, and the rules that hang off that number. You call run.step(dt)
once a frame and move your runner by run.state.speed. The core keeps the timers, ramps the speed, counts the score,
and lays chunks ahead of the runner while dropping the ones far behind. When your collision code decides the runner
touched something, you call run.hit('stumble') or run.hit('crash'), and the core answers 'ignored',
'shielded', 'stumbled' or 'over'.
The world is made of chunks you author: a length, your own content in items, and rules for when the chunk may
appear. A weight sets how often it is picked, from and until set the distances it may start between so harder
chunks arrive later, and follows and never decide what it may come straight after (a breather after a train
yard). If nothing fits every rule, the choice relaxes rather than stopping. The core never reads a chunk's items, so
lanes of trains, a turning temple path and a corridor of zappers are all just different data on the same run.
Power-ups are named definitions with a duration and any of a shield, invulnerability, a score multiplier and a speed
factor; a duration of 0 makes a shield that waits for its hit. Events since the last frame come from run.drain():
new chunks, coins, stumbles, the end of the run, power-ups starting and ending, and milestones. The whole run saves to
plain JSON with snapshot and comes back with restore, and validateRunner lists every fault in a set of options
before a run starts.
Features
- Seeded chunks — authored pieces of world with weights, distance bands, no-repeat spacing and follow rules, laid ahead and dropped behind.
- An opening — named chunks played first, in order, before the seeded choice takes over.
- A speed ramp — from a start speed to a cap, gaining by time or by distance.
- Stumbles or one-hit deaths — a stumble slows the runner and brings the chaser, two inside a window catch them; or set every hit to end the run.
- Power-ups — timed or waiting shields, invulnerability, score multipliers and speed factors, with refresh, extend or ignore on a second pickup.
- Score and coins — points per unit run and per coin under a multiplier, counted as the run goes so a doubler counts only while it lasts.
- Revives and protection — carry on after the end with the chaser gone, or grant a few seconds of safety after a landing.
- Events and milestones — a drained list of what happened since the last frame, with a milestone every so many units.
- Saves and validation — the run as plain JSON, and every fault in its options named before it starts.
Good for
- Lane runners that swipe between tracks past trains and barriers, in the style of Subway Surfers.
- Turning-path runners with corners, jumps and slides, in the style of Temple Run.
- Side-on jetpack runners through corridors of hazards, in the style of Jetpack Joyride.
- Your own endless runner, in 2D or 3D, where you write the motor and keep the run rules.
- Score-chasing mobile games that need distance, coins, multipliers and revives without building them again.
Make it yours
Every run takes its own options; the pack itself has none. You set the chunks and the opening, the seed, how far
chunks are laid ahead (default 150) and kept behind (default 30), a gap between chunks and noRepeat spacing.
speed takes a start, a maximum and an acceleration by time or distance (default 10 to 30, gaining 0.15 a second).
hits chooses between 'stumble' and 'kill'; stumble sets the chaser's window, the stumbles that catch the
runner, the slowdown and its recovery; grace sets the safe time after a shield breaks or a revive. powerUps are
yours by name, score sets points per unit and per coin, and milestone sets the event spacing. The defaults are in
runnerDefaults.
Two different runs: a forgiving lane runner with a hoverboard shield, a magnet and a doubler, where two scrapes inside
four seconds let the chaser catch you; and a hard one-hit runner with hits: 'kill', a speed ramp by distance, no
power-ups and score from coins alone.
A quick look
A run with an opening chunk, a breather that follows trains and three power-ups:
const run = createRunner({
seed: 4, opening: ['runway'],
chunks: [
{ id: 'runway', length: 60, weight: 0, items: [] },
{ id: 'trains', length: 80, items: trainsPattern, tags: ['trains'] },
{ id: 'coins', length: 40, items: coinArc, follows: ['trains'], weight: 2 },
],
speed: { start: 12, max: 28, accel: 0.12 }, stumble: { window: 4, limit: 2 },
powerUps: { hoverboard: { duration: 30, shield: true }, double: { duration: 15, multiplier: 2 } },
});
run.step(dt);
if (touching(trainFront)) run.hit('crash', 'train');
Works with
lane-runner-3d— a lane runner with trains, barriers, jumps and rolls, built on this core.turn-runner-3d— a runner on a path that turns and forks, built on this core.jetpack-runner-2d— a jetpack corridor of zappers, lasers and missiles, built on this core.rooftop-runner-2d— rooftops with gaps and crashing obstacles.ninja-rope-2d— for a finite course to a goal rather than an endless run.
Good to know
- It moves nothing and tests no collisions: your motor moves the runner, decides what touched what, and calls
hit,coinandpower. - Distance is one number. A world that branches maps the same distance to whichever branch was taken, and the chunks ahead do not depend on the choice.
- The chunk choice is the only randomness. A motor that randomises inside a chunk should use its own seeded stream so saves still replay.
- At most 512 events are kept between drains, and a chunk's items are kept by reference, so do not change them during a run.






