Critters

Games & Classics

A swarm of little critters on a Super Nintendo, each running its own scripted patrol as a protothread — a tiny coroutine that yields every frame and resumes exactly where it stopped, via a saved continuation index. Dozens of independent behaviours, cooperatively scheduled. Runs automatically; no joypad needed. Written in C and compiled with the llvm-mos-based 65816 toolchain (+mos-a16, 16-bit accumulator mode).

loading core…

Self-running demo — each critter runs its own scripted patrol.

Click the screen to focus, then watch. Tab away and it pauses.

What it is

A coroutine is a function that can pause itself and be resumed later, picking up mid-execution. A protothread implements that in a handful of bytes: it remembers a continuation index and, on the next call, switches on it to jump back to where it yielded:

switch (c->lc) {          // resume where we left off
  case 0:
    for (;;) {
      walk_right(); c->lc = 1; return; case 1: ;  // yield inside loop
      walk_down();  c->lc = 2; return; case 2: ;
      reverse();
    }
}

The case labels land inside the loops, so resuming jumps into the middle of a loop — the same tangled control flow as Duff's device. Any state the critter needs across frames lives in its struct, not in local variables. Each of the two-dozen critters is one of these. It stays bit-exact across every codegen mode.

Compiler stress-test

ItemWhat it exercises
protothreadA protothread is a coroutine that fits in a few bytes: a function that runs a bit, yields, and later resumes exactly where it stopped. Each critter is one, driving its own scripted patrol.
saved case-indexResumption is a saved continuation index. On re-entry the function switches on it and jumps back to the yield point — with case labels sitting inside the loops, the same irreducible control flow Duff's device uses.
state preservationTrue local variables would be lost between calls, so the coroutine's state — position, velocity, the loop counter — lives in a struct that persists across re-entry. That cross-call state is the compiler test.
bit-exactPure branch and pointer arithmetic — no multiply, no divide on the hot path. 24 critters stepped for 120 frames; host == default == +mos-a16 == +mos-xy16 == bsnes-jg, byte for byte.

Written in C with the llvm-mos 65816 toolchain and verified against bsnes-jg (and MAME where the SPC700 IPL is present). Hit Verify fidelity to reproduce the build gate's WRAM assert (gate hash 0xAD9F — a fold of 24 critters stepped 120 frames) live in this tab. No far pointers — the same source passes every way: host == default == +mos-a16 == +mos-xy16 == bsnes-jg, -verify clean.