A capability-budget mechanic, not a reflex one: three lanes, a sequence of
gates, and a shrinking budget. Each gate charges its true cost the instant
you cross it — read $1, network $2, write $4, shell $6 — but most
gates you’ll meet are wearing a face. A write gate can render as a read
icon; the only thing that gives it away is a faint, pulsing ring drawn in its
real color, thin enough to miss if you’re not looking for it. Switch lanes
before you arrive, not after. The goal isn’t reaching the end fast, it’s
reaching it having spent the least privilege — burn the budget on undisguised
gates you didn’t need to gamble on, and you’re revoked before the finish
line whether or not you were “moving well.”
Riffs on agentic tool-use and least-privilege sandboxing — the same shape as
this repo’s own /private gate (ADR-0007), just turned into something you
lane-change through instead of read about.
Reuse
The mechanic lives in src/gate-run.js as a framework-free ES module:
createGateRun(opts)— returns a state object; callupdate(dt, input)each frame withinput.lane(-1 | 0 | 1, a lane-switch request).opts:budget,scrollSpeed,laneSwitchCooldown,lanes,checkpoints(orseed/checkpointCountto build one viadefaultLevel()).- State exposes
lane,distance,budget/budgetMax,status(running|won|revoked),lastCharge(the most recently crossed gate’s{type, displayType, disguised, cost}), andfinishX. Rendering — lane tracks, gate shapes, the disguise tell, the budget meter — is entirely the caller’s job; the demo draws it all on canvas. defaultLevel(seed, count, lanes, spacing)builds a deterministic level with a mulberry32 PRNG — same seed, same gates, every run. Pass your owncheckpointsarray to skip the generator and hand-author a level.GATE_TYPESexports the cost/color table if you want to match the demo’s palette elsewhere.
Port notes for DragonRuby: update() is pure state math with no DOM
dependency — the lane-to-screen-Y mapping and gate shapes belong to the
renderer, same split as the demo.
Gotchas
- The disguise only changes
displayType(what’s drawn) —type(what’s charged) is the truth, and it’s picked from the expensive pool wheneverdisguisedis true. There’s no case where a disguised gate is a pleasant surprise; the tell is there to be read, not decorative. - Checkpoints are consumed once (
_crossed) the framedistancefirst passesworldX; holding a lane-switch key spamsupdate()with repeated switch requests gated only bylaneSwitchCooldown, so a held key can ping-pong across lanes rather than settling — intentional, but tunelaneSwitchCooldowndown if that reads as sluggish for a different feel. - The demo in
demo/bundles its own copy of the module (self-contained by contract). If you touchsrc/, re-copy it intodemo/.