claudium
claudium engine

A game workshop with Claude living in it.

Three panes. On the left you ask for something. In the middle is the code, with every line that changed marked. On the right the game is already running it. The loop between having an idea and watching it move is about as short as it can be made.

Claudium Engine 0.60.0 — chat, code, live game
All three panes open: the conversation, the game's source with the engine calls highlighted, and the running game.
All three panes at once. The game on the right is the code in the middle, running.

the runtime

One file. No build step. Nothing to install into your game.

Everything a game gets is a single object called G — drawing, input, sound, video, saving, scenes, tweens, particles, layers. It lives in one readable file in your project, which means you can open it, and change it, and it will still be there next year.

// the whole of a game that does something
G.watch('sparks', () => sparks.length)

function update(dt) {
  const pull = G.tune('pull', 1.0, {min: 0, max: 3})
  for (const s of sparks) s.a += s.s * dt * pull
}

function draw() {
  G.clear('#08080b')
  for (const s of sparks) {
    G.circle(x, y, s.z, '#a78bfa')
  }
}
RuntimeOne file, about 3,700 lines, plain JavaScript
Engine API22. Carried per project, and pinnable
RendererCanvas 2D, plus cached offscreen layers
Build stepNone. Save the file, the game reloads
DependenciesNone in the game. Nothing fetched at runtime
InputPointer events — mouse, pen and touch, all three buttons
SoundDecoded once, played through Web Audio
VideoA clip in assets/ is a moving sprite
ExportOne HTML file, or a folder with real assets
Desktop appWindows. Built on Electron

what it does that a text editor does not

Most of the engine is about shortening the gap between a question and an answer.

Change a number while the game is playing

Any number a game passes to G.tune() becomes a slider in the toolbar. Drag it and the running game changes under your hand — no reload, no losing your place. Keep the value and it is written next to the project; discard it and the code's own default stands. Nothing is ever rewritten behind your back.

A profiler that names the fault

“It feels slow” is not something anyone can act on. perf runs the real game for a few seconds and writes a report: frame times, how many device pixels get shaded per frame against how many the canvas has, and a ranked list of what to look at — blurred fills, gradients rebuilt every frame, art scaled down from far larger sources. Mark up draw() and it breaks the frame down by section.

The engine describes itself

state writes a snapshot of the running game: frame rate typical and at the slow end, whether the engine has already dropped render resolution to keep up, the save key by key, what is loaded, how much is alive, and every error thrown with a plain-language read on what it means. Games add their own numbers with G.watch(). It is also captured the instant anything throws — so an error arrives with the state it happened in, not with whatever the game looks like after two reloads.

Every turn is a checkpoint

The whole project is snapshotted before each change — every file, at any depth, not just the ones in the root — and the diff view shows exactly which lines moved. If a turn went somewhere you did not want, one button puts it back. Nothing about that requires you to know what git is.

It plans for a machine that is not yours

A canvas costs roughly what it covers in device pixels, and that is the screen's density squared — a laptop at 150% is asking for 2.25× the work for a picture most people cannot tell apart in motion. So the engine watches its own frame times and quietly renders at a lower density rather than dropping frames, and says in state that it has, rather than leaving you to wonder why it looks softer on the laptop.

Art and sound go in through the conversation

Drop a PNG or an MP3 into the chat and it lands in the project's assets, reachable by name. Generated images arrive the same way. The export packs them as real files beside the page rather than as base64 wedged into the HTML, which is smaller and starts decoding sooner.


live tuning

The knob is the code.

G.tune('pull', 1.0) reads as an ordinary number to the game and appears in the toolbar as a slider. There is no second copy of the value to keep in sync, and no settings file to forget about.

The tune popover open over the running game, with a slider for a value the game exposed.
Not a picture of it — the sparks above are the update() and draw() printed further up this page, running, with the real pull knob under them.

newer in the runtime

Sound, video, layers, and a save you can trust on somebody else's machine.

Most of these did not begin as features. They began as an evening lost to something that failed without saying so — which is why each one ships with a way to ask what happened.

Sound that cannot be refused

A file a game adds is decoded once and played through Web Audio rather than handed to an <audio> element. A media element needs a URL, and the preview's sandboxed frame has an opaque origin where data: and blob: URLs are both refused outright — there was no URL left to try. Web Audio takes bytes, and bytes cannot be refused. G.play() hands back a handle worth keeping: loop, seek, duration, an ended event, and a volume that ramps instead of clicking. G.beep() never once failed while every file a game added was silent, which was the clue, read backwards, for two days.

Video, and a way to tell four failures apart

A clip in assets/ is a moving sprite — G.drawVideo() starts it, loops it, draws the current frame. The hard part is that a <video> which fails does it in silence: nothing thrown, readyState stuck at zero, a blank rectangle indistinguishable from a game drawing the wrong rectangle. G.videoStatus() separates the four — no such asset, no decoder, fetched but undecodable, nothing at all — because each has a different fix, and carries the browser's own message through rather than paraphrasing it. G.videoCheck() answers the one question a broken clip cannot answer about itself.

Draw it once instead of sixty times

A real profile here came back as 1.7ms of JavaScript against 22.3ms of rasterisation: the logic was free, the pixels were the whole cost, and most of those pixels were a background that had not changed in ten minutes. G.layer() keeps an offscreen canvas and runs your drawing only when you mark it dirty; every other frame is one copy. It is a trade, not a free win, and the engine says so — G.layerStats() puts redraws next to frames, and a layer redrawn every frame is slower than no layer at all.

The save on a stranger's machine is a contract

Save keys are namespaced per project, because itch.io serves every HTML game from one shared origin — publish a second game on a generic key and it reads the first one's save, fails to understand it, and overwrites it. Storage is probed at boot, so a private window or a full quota says so instead of looking like a new player. Declared migrations carry an old save forward a step at a time and abandon the lot if one throws, because a half-migrated save is worse than an old one. G.exportSave() hands the whole thing over as one pasteable string, for the player who changes laptop.

A project decides which engine it is on

Every project carries its own copy of engine.js. Upgrading on open is right for a game being started and wrong for one being finished — an engine that moves underneath you is one more variable in a bug hunt. So a project can be held at the version it works on, or moved to any version it has ever run, and the switch scans the game first to name every G. call the target engine does not have.


availability

Built in the open, not released yet.

Claudium Engine is the tool we build our own games with. It is not a public download today. It gets a version most weeks, and every one of them is driven by something that got in the way while making an actual game — which is the only reason any of it is the shape it is.

The proof that it works is on the games page, playable in your browser, exported from this engine with one button.