DOM ENGINEV0.9.0FIZLET

MICRO GAME ENGINE / FOR THE DOM

The page
is the board.

Fizlet is a tiny game engine that plays on the page you already have: one shared loop, bounded everything, and the page always comes back exactly as it was. The kernel is 3.3 KB gzipped. No runtime dependencies, ever.

SCRIPT TAG · 12.7 KB, EVERY BRICK
<script src="https://coded-dot-design.github.io/fizlet/fizlet.js"></script>
your.pageBOOTING

Hello, page.

Every letter here is a real element on this page. Knock it around, then press Recall.

SIGN UPNEWv0.2
FRAMES
0
STEPS
0
DROPPED
0
FRAME MS
0.00
WAVES
0/8
PARTICLES
0/420
LEDGER
0
LAYOUT IN STEP
0
LIVE · fizlet + input + fx + sound + debug, running this board
3.3 KBKERNEL GZIP 7BRICKS 0DEPENDENCIES 1rAF PER PAGE 1MSTEPS · NO GC V0.9.0MIT · OPEN SOURCE

01 WHY · THE CONTRACT

Nine promises.
Each one tested.

Most engines assume they own the page. Fizlet is a guest: it runs inside someone else's site, on a phone, next to their code, and it leaves no trace. Every brick and every game built on it keeps these.

  1. 01

    One clock per page

    Every engine on the page shares one requestAnimationFrame. A fixed 60 Hz step with a catch-up cap: a long frame drops time, it never spirals.

  2. 02

    Bounded everything

    Pools and the event ring have a fixed capacity and refuse work when full. Nothing grows during a session.

  3. 03

    Zero steady-state allocation

    One million kernel steps of a busy system cause no young-generation GC. Each brick alone allocates nothing.

  4. 04

    No layout reads in the step

    Read in read, simulate in step, write in render. fizlet/debug counts any layout read made during a step.

  5. 05

    Change-only writes

    The ledger skips writes that change nothing. Writing the same value twice makes zero DOM mutations.

  6. 06

    Pause by default

    Hidden tab, page leaving, root off screen, typing in the host's own fields. Zero CPU while paused.

  7. 07

    Own nothing you can't give back

    Every page change goes through the ledger. destroy() restores it exactly, and a newer host edit always wins.

  8. 08

    Embed-safe

    No globals, no global styles, many engines per page. Every root Fizlet owns is CSS-contained. One AbortSignal removes every listener and observer.

  9. 09

    Budgets in CI

    Kernel at most 4 KB gzip, each brick its own budget of 1.5 to 5 KB. Over budget, the build fails.

02 KERNEL + BRICKS

Take only
what you need.

One small kernel and a box of bricks, each its own import with its own budget. Sizes are measured on this build: minified, gzipped, with everything each one pulls in.

  • KERNEL3.3 KB / 4.0 KB gzip

    fizlet

    createEngine, the shared scheduler, Pool, EventRing, Ledger, seeded random and stats(). Everything else is optional.

    const e = createEngine({ root: board, seed: 7 });
    e.add({ step(e, dt) {/* sim */}, render(e) {/* DOM */} });
    e.start(); // e.destroy() gives the page back
  • INPUT1.3 KB / 1.5 KB gzip

    fizlet/input

    Keys to named intents with one-frame edges, pointer, a touch stick and on-screen press(). Keys typed in the host's fields never count.

    const pad = controls(e, { keys: { fire: ["Space"] } });
    if (pad.hit("fire")) fire(); // true for one frame
    if (pad.pointer.hit) aim(pad.pointer.x, pad.pointer.y);
  • COLLIDE1.3 KB / 1.5 KB gzip

    fizlet/collide

    A fixed-capacity spatial hash plus circle, rect and swept segment tests. Pass typed arrays and indices, so nothing boxes.

    const grid = new Grid(32, 64, w, h); // cell, cap, world
    grid.clear(); grid.add(xs, ys, rs, i); // typed arrays
    const n = grid.near(xs, ys, rs, i, out); // ids, once
  • FX2.7 KB / 3.0 KB gzip

    fizlet/fx

    Pooled particles (struct of arrays), the hero blast(), pop words, scorch marks and screen shake, drawn on a canvas.

    const fx = effects({ cap: 420 });
    fx.blast(x, y, 1.2);       // flash, rings, debris, scorch
    fx.step(dt); fx.draw(ctx); // pooled: never allocates
  • SOUND1.0 KB / 1.5 KB gzip

    fizlet/sound

    Reads the event ring after each frame and plays synth cues, or your own player. Silent until a gesture, and while paused.

    const sfx = sound(e, { cues: { boom: "boom" } });
    e.events.emit("boom"); // in a step; plays after render
    sfx.on = false;        // mute
  • EMBED2.0 KB / 2.5 KB gzip

    fizlet/embed

    Drop a game onto any page: a hex-only palette, easter-egg triggers, lazy load, a toggle key and Escape to give the page back.

    embed(() => import("./game.js").then((m) => m.mount), {
      name: "blitz", palette: { cmd: "#8573ee" },
      easterEgg: { sequence: "konami" } });
  • BOARD UI2.7 KB + 4.6 KB css / 3.0 KB gzip

    fizlet/ui

    The Battlefield design system as a brick: 3D keys, cards, stickers, overlays and the launch burst. This site is built with it.

    applyPalette(root, { sys: "#06c4ff" }); // hex only
    press(key, () => fire(), signal); // touch-safe
    launch(startKey, start); // press, invert, shatter
  • DEBUG0.5 KB / 1.5 KB gzip

    fizlet/debug

    Development only: guardLayout() counts every layout read made during a step, the contract's hardest rule, as a number.

    import { guardLayout } from "fizlet/debug";
    guardLayout(e);  // dev builds only
    e.violations;    // layout reads made in a step: 0

03 THE PAGE BECOMES THE BOARD

Read. Step.
Render.

A web page is a surprisingly good game map. The trick is reading it cheaply, changing only what you own and leaving cleanly. Every frame runs three phases, in order, for every system.

Geometry is read once in read and cached. The simulation runs in step on plain numbers, at a fixed 60 Hz, with no DOM at all. render writes only what changed, through the ledger. The demo above counts layout reads made during a step with fizlet/debug: the number stays at zero.

  1. READonce a frame

    Cached geometry, input edges latched. The only phase that may measure the page.

  2. STEP × 0–4fixed dt = 1/60 s

    The simulation. No DOM reads, no DOM writes, no allocation. Events go into the ring.

  3. RENDERonce a frame · alpha

    Change-only writes through engine.ledger. Sound and effects read the event ring.

04 A FRAME, NOT A FREE-FOR-ALL

One loop.
Bounded work.

One requestAnimationFrame drives every engine on the page. Pools are filled up front and never grow; when one is full it says no, and the frame stays inside its budget.

A slow frame runs at most four catch-up steps and drops the rest of the time (it shows in stats().dropped). Hidden tab, page leaving, root scrolled away, a visitor typing in a field: the engine pauses and costs nothing. Scroll the demo away and back: it sleeps.

RAF · ONE PER PAGE
ENGINES · SHARE IT
herogamewidget
STEPS · CAP 4, THEN DROP
POOL · 24 SLOTS, NEVER GROWS

A long frame (the wide tick) runs four steps and drops the rest. Engines 2 and 3 cost no extra frames.

05 THE PART WE CARE ABOUT MOST

Recall gives
the page back.

Every change Fizlet makes to the page goes through one ledger. It records the original and what the engine wrote. destroy() puts back everything the engine still owns, newest first.

If the host page changed something after the engine did, the host wins: the ledger stops writing it and never restores over it. A style attribute comes back byte for byte. Try it in the demo: blast the heading, then press Recall. That key runs engine.ledger.restore(), the same call destroy() makes.

engine.ledger · WHAT IT OWNSTESTED IN CHROMIUM
CallOwnsFor example
ledger.text(node, v)a text node's valuethe 0s and 1s a shot leaves in a word
ledger.style(el, prop, v)one inline style propertytransforms, custom properties
ledger.attr(el, name, v)an attribute (null removes)aria-busy, data-hp
ledger.cls(el, name, on)one classsmashed, burning
ledger.add(node)a node you put on the pagea canvas, an overlay, debris
  • Change-only. Writing a value that is already there makes no mutation.
  • Host wins. A newer edit by the page is never overwritten or undone.
  • Exact. After destroy() the DOM matches the snapshot taken before.

06 SPEC SHEET

Hard limits,
in actual numbers.

Limits constrain work; they are not frame-rate promises. Real frame time still depends on the page, the fonts and the device.

  • 1rAF per pageHowever many engines run, one requestAnimationFrame drives them.
  • 60 Hzfixed stepSame results on a 30 Hz phone and a 144 Hz monitor.
  • 4catch-up stepsPast that a frame drops time instead of spiralling.
  • 64events a frameThe default ring. Full means dropped and counted, never grown.
  • 1Msteps, 0 young GCsThe kernel test runs a busy system for a million steps.
  • 0mutations on rewriteWriting the same value twice touches nothing (browser test).
  • 420fx particlesThe default cap, struct of arrays. New ones are refused when full.
  • 3.3 KBkernel gzipOf a 4.0 KB budget, checked on every push.
  • fizlet (kernel)3.27 KB
  • fizlet/debug0.48 KB
  • fizlet/perf1.90 KB
  • fizlet/input1.25 KB
  • fizlet/pad1.95 KB
  • fizlet/collide1.26 KB
  • fizlet/grid1.50 KB
  • fizlet/fx2.69 KB
  • fizlet/camera1.30 KB
  • fizlet/sprite1.27 KB
  • fizlet/sound0.98 KB
  • fizlet/embed1.97 KB
  • fizlet/slot1.73 KB
  • fizlet/presence1.75 KB
  • fizlet/presence/check4.68 KB
  • fizlet/hud1.29 KB
  • fizlet/match2.54 KB
  • fizlet/rules1.23 KB
  • fizlet/board-page3.72 KB
  • page build (kernel+slot+input+fx)6.86 KB
  • fizlet/ui (js)2.68 KB
  • fizlet/ui (css)4.63 KB

Sizes measured on this build of v0.9.0, minified and gzipped (1 KB = 1024 bytes), the same way npm run size checks them in CI. The bar is the share of each module's budget. The one-file script-tag bundle (kernel and every brick but debug) is 12.7 KB.

07 INDEPENDENT PACKAGES · SHARED FOUNDATIONS

Build once.
Ship each.

The engine lives in one repo. Each game takes a fingerprinted snapshot, tests itself and tags its own release. Pages serves static files: no runtime iframe, no remote content service, no client framework.

  1. 01src/the engine
  2. 02npm run checktypes · tests · size
  3. 03CI tags vX.Y.Zfrom package.json
  4. 04vendor/fizletsnapshot + ORIGIN.json
  5. 05Pagesstatic, no server

08 BUILT ON FIZLET

Games that
play on your page.

FIRST GAME ON FIZLET · FREE FOR ALL

Battlefield Blitz

Every tank for itself. Parachute in, grab power tiles, call airstrikes, dodge mines. Most kills in three minutes wins. About 17 KB gzipped with the engine and the art, and it drops onto any website with one line: shells knock your headings around, and Escape gives the page back.

DROP-IN · ONE LINE ON ANY PAGE
<script src="https://coded-dot-design.github.io/dom-battlefield/blitz/blitz.js" defer></script>
  • ~17 KBgzip, engine and art
  • 3 minfree-for-all match
  • Escgives the page back

Built on fizlet · input · collide · fx · sound · embed · ui

THE ARCADE · PORTING IN E3

DOM VIRUS

Tiny games that turn your page into the game board. Fizlet is the engine they proved out: Null Invaders, Byte Runner, Sherman Bytes and Swarm Protocol move onto it one at a time.

SQUAD TACTICS · PORTING IN E3

DOM Battlefield

Twelve tanks, one radio. The game whose board UI became this design system, and the next big port: then multiplayer, in E4.

09 ROADMAP · E0 TO E6

What's done.
What's left.

The plan of record is ENGINE.md in the DOM VIRUS repo. The kernel is done, the first bricks shipped with Blitz, and the rest of E2 is next.

  1. E0PARTLY DONE

    Baseline and archives

    Freeze today's games as the before picture, so every port has numbers to beat.

    • Perf harness: every app under a 4× CPU throttle (Playwright)(done)
    • Baseline numbers and commits recorded for all six apps(done)
    • Tag engine-baseline in every repo(to do)
    • Battlefield archive selector (versioned paths)(to do)
    • Hub TEST A RELEASE selector for every game version(to do)
  2. E1DONE · 0.1.0

    Kernel

    The scheduler, fixed step, auto-pause, pools, event ring, seeded random, the Recall ledger and stats.

    • One rAF for N engines, fixed step with a catch-up cap(done)
    • Zero allocation over 1M steps, exact DOM restore(done)
    • Kernel under the 4 KB budget(done)
  3. E2IN PROGRESS · 0.2

    Bricks

    Small, separately importable bricks lifted from the games, each with tests and a size budget.

    • input · collide · fx · sound · embed · ui(done)
    • actor + steer(to do)
    • aim · rules(to do)
    • board-page · board-grid(to do)
    • view-dom · camera(to do)
    • A playground of runnable examples(to do)
  4. E3NOT STARTED

    Port the games

    One game at a time, smallest first, each as a 1.0 engine cut with the old build still playable.

    • Null Invaders → Byte Runner → Sherman Bytes(to do)
    • Swarm Protocol → DOM Battlefield(to do)
    • Perf equal or better than the baseline(to do)
  5. E4NOT STARTED

    Presence and multiplayer

    A few-KB presence client where humans and AI agents are the same kind of player.

    • presence brick + reference room server(to do)
    • Battlefield 1v1 and co-op(to do)
    • An AI agent joins a room(to do)
  6. E5NOT STARTED

    Docs for humans and AI

    Build a game or micro-interaction in an afternoon, from docs a person or an agent can follow.

    • Authoring guide, one page per brick, recipes(to do)
    • AGENTS.md: the strict, AI-readable spec(to do)
    • Live kernel stats on Under the Hood(to do)
  7. E6NOT STARTED

    Weekly performance wins

    A standing weekly pass: run the perf harness, fix the top cost, log the win.

    • Weekly harness run against the baseline(to do)
    • perf-log.md of every win(to do)

10 QUICK START

Ten lines
to a blast.

One script tag puts the kernel and every brick on window.fizlet. Click the canvas: that's the hero explosion, pooled, paused when you scroll away.

  1. Script tag. fizlet.js (12.7 KB gzip), one file, no build step.
  2. Modules. Import only the bricks you use from https://coded-dot-design.github.io/fizlet/dist/.
  3. From source. Not on npm yet: git clone, npm ci, npm run build gives you dist/ with the subpath exports.
SCRIPT TAG · index.html
<canvas id="board" width="640" height="320"></canvas>
<script src="https://coded-dot-design.github.io/fizlet/fizlet.js"></script>
<script>
  const { createEngine, effects } = fizlet;
  const canvas = document.getElementById("board"), ctx = canvas.getContext("2d");
  const engine = createEngine({ root: canvas }), fx = effects();
  canvas.onclick = (ev) => fx.blast(ev.offsetX * 640 / canvas.clientWidth, ev.offsetY * 320 / canvas.clientHeight);
  engine.add({
    step: (e, dt) => fx.step(dt),
    render: () => (ctx.clearRect(0, 0, 640, 320), fx.drawGround(ctx), fx.draw(ctx)),
  });
  engine.start();
</script>
MODULES · only what you use
<script type="module">
  import { createEngine } from "https://coded-dot-design.github.io/fizlet/dist/index.js";
  import { effects } from "https://coded-dot-design.github.io/fizlet/dist/fx.js";
  import { controls } from "https://coded-dot-design.github.io/fizlet/dist/input.js";
</script>

MIT · NO DEPENDENCIES · V0.9.0

Make the page play.