DS Laboratory

Lockstep

Why I built my own game engine.

Five hobby games became one. Here's what I learned owning the whole stack — and why you might want to.

David Silva · dslaboratory.uk · press / Space to advance
The itch

I'd built five games. They were secretly the same game.

A platformer. A rally racer. Tetris. A stealth game. An F1 racer. Different on screen — but underneath I kept writing the same things:

  • a fixed-timestep loop
  • a pure update function: new state from old state + input
  • keyboard-and-gamepad input, a pixel canvas, a little sound

Game six would start from zero again. Unless I stopped re-writing the spine and named it.

Let's be honest first

Unity, Godot, Bevy are brilliant. Usually you should use them.

If the goal is to ship fast — a jam, a deadline, a client — reach for an engine, gladly. I didn't. On purpose.

I wanted to own every line, understand every part, and be able to teach it. Borrowed power you can't see inside isn't power you understand. This is a talk about the other road.

What it is

A small, hand-built, zero-dependency engine. One idea at the heart.

Your game's rules are a pure function:

// no clock. no randomness. no DOM. same inputs → same result.
function tick(state, input) {
  return nextState; // a fresh state, every frame
}

Everything else — the screen, the speaker, the gamepad, the network — is a thin shell wrapped around that function. The rules never touch the outside world.

Why that one rule

Same inputs, same run. Every machine, every time.

Keeping the core pure looks like a constraint. It's the opposite — it buys you the two things that are hardest to get later:

  • Tests with no browser. The whole game logic runs headless, in milliseconds.
  • A recording that is a replay. Save the inputs and you can re-play any session frame-for-frame.
// a run-tape: the seed + every input. replay() reconstructs the run exactly.
{"frame":128, "type":"input", "data":{"held":"right,jump"}}
Where it came from

I didn't invent an engine. I harvested one.

Each game had already solved one hard thing well. The engine is those solutions, generalised and pulled into one place — consolidation, not invention.

smbreading real NES ROMs — the structural archaeology of an old game
rallypseudo-3D road projection + a track-builder language
tetrisfull gamepad remapping, and local 2-player on one screen
stealthanonymous playtest feedback + an A* solver that proves a level is beatable
redlineJSONL telemetry that doubles as a deterministic replay tape

Thirteen small, single-purpose packages. A game pulls in only what it needs.

Where it came from · stealth

The one I'm proudest of: a level you can prove.

Stealth gave the engine the most — anonymous playtest feedback, and the crown jewel: an A* search that plays the real game through the same tick(). A route it finds isn't a guess — it's a proof the level is beatable, so all twenty hand-curated levels are certified winnable and fair. That solver became @lockstep/validation. (Level 17 is named "Lockstep" — the engine took its name from a level in this very game.)

Stealth — a top-down stealth game: guards with vision indicators on a dark tile grid, a detect meter, a key to find
the game — read the cones, reach the exit
Stealth's level select: twenty curated levels, including 17 'Lockstep'
20 levels — each one proven beatable
The rules it lives by

A foundation you can't trust isn't a foundation.

So the engine has rules — and they're not polite suggestions:

  • Everything documented. Every file says why it exists; every folder has a README.
  • Safe games — no networking, ever. One sanctioned exception: anonymous playtest feedback.
  • Offline-first. Every game is fully playable with no internet.
  • Telemetry & a screenshot are not optional. Every game records a replayable tape and shows a picture of itself.
  • Produce intention. Before a line of code, put the idea into words — or it isn't a game yet.
Enforced, not hoped

Discipline that relies on remembering is discipline that rots.

So a machine checks it. One command, five gates — and a new game cannot be born missing any of them:

$ npm run verify check — types clean (strict) test — 638 deterministic tests docs:check — every file + folder documented telemetry — every game records a replayable tape screenshot — every game shows itself verify: OK — five green gates.
What's built on it

Game #6: Trackbound

A cozy gamepad train-track toy. You lay chunky track, then watch your train roll the loop.

Designed in words first, built on the engine, playtested, and mirrored to its own repo. Nothing in the engine knows Trackbound exists — that's the whole point.

Trackbound — a train rolling a loop of real track on a green board
Why this is worth talking about

You learn more building the engine than using one.

A pure function. Determinism. Documentation a machine enforces. These aren't game-dev tricks — they're how you reason about any system you want to trust.

And owned tooling compounds: the second game starts ahead of where the first ended; the third ahead of the second. The engine is where the lessons live — on purpose, documented for whoever comes next.

When you'd NOT do this

When shipping beats owning, use the engine. Gladly.

A jam. A deadline. A paying client. This road is the slow one — you trade speed now for understanding and tools that compound later. Know which one you're buying.

But if you want to understand your craft from the floor up: build the thing, own the thing.

Play them → davidslv.uk/games
Lockstep · DS Laboratory · dslaboratory.uk
← → / Space · click to advance