19 June – 26 July 2026
Three ways to ship one game
Character select, 25 riders, mobile gestures, the shop, locked levels, a
dedicated allocation-hunting performance pass, analytics, and then the same
build going out three doors: Cloudflare for the web, Electron for Steam, and an
831-byte iframe for itch.
The Steam wrapper cost a specific, unguessable discovery:
…appends in-process-gpu and
disable-direct-composition — the pair is load-bearing:
in-process-gpu alone presents a solid-gray window
while offscreen captures render fine.
The desktop build also needs a custom URL scheme rather than loading files
directly, because the menu CSS uses root-absolute paths — and that scheme has
to be registered with fetch and CORS privileges or every 3D asset loader fails.
There is a cheat, documented in the README: one localStorage key unlocks every
song and difficulty. It is gate-only — medals and gold are untouched — and it
is compiled out of the Steam build entirely, on the reasoning that paying
players should not be able to stumble into a progression bypass.
15 – 17 June 2026
Fourteen numbers that paint a world
What comes out of the classifier is a genre plus a fourteen-dimension trait
vector — hardness, aggression, neon, groove, organic, epic, intimate, urban,
uplifting, melancholy, dreamy, retro, tropical, digital. That vector becomes
the grass, the canopy, the sky dome, the time of day, the weather and the orb
accent.
The insight that makes it produce variety rather than 36 fixed presets is that
different genres vary along different axes:
A single shared driver collapses the gamut: this library's metal is
uniformly fast (bpm useless) but varies in density; electronic
varies in brightness. So the driver is per family.
docs/trait-palette.md
Three drivers do the work — heat, warmth and groove — each a blend of different
features, each assigned to the families it actually separates. And the arc is
continuous rather than bucketed, which is what stops two metal tracks landing
on an identical world.
That document also ships its own bug post-mortem, under a heading that reads
“Downstream gotcha (caused a real bug)”: two recolour passes existed, and
the second one built a single tint from the grass hue and assigned it to both
the ground and the canopy — so for a while every level had trees that exactly
matched the field they stood in.
14 June 2026
76%, and then it did not matter
The colour system needs to know what a song is. So: a hand-built genre
classifier, reading only the features that survive the compressed timeline
format, with an offline calibration harness that scores it against the genres
the songs were written in. The commit message ends with its own report card:
offline calibration/regression harness that scores the classifier
against ground-truth genres. Current
accuracy 76% (electronic/metal/acoustic/dnb all perfect; vocal
genres pop/rock/hip_hop fall into electronic — the ceiling of
these coarse features). Re-baked all 18 timeline bins +
library.json.
It is a good piece of engineering that knows its own limit. The next day, that
limit stops being a constraint:
2026-06-15 extracting genre and mood from yamnet model!
A self-hosted 521-tag audio classifier replaces the whole scorecard, and the
same day brings orbs emit light now! and we generate level colors from tracks
music mood genre and tempo. The hand-built classifier survives as the
fallback. Three commits later the log just says nuclear refactor.
1 – 10 June 2026
The game around the game
Scoring, penalties, medals, the end-of-run summary, adaptive quality tiers,
mobile, the options screen, a Cloudflare deploy, and major speed refactor.
Two details from this stretch are worth more than the feature list. Penalties
scale with song length — the cost of a bump or a stop is defined against a
two-minute reference and scaled inversely by how long the track is, clamped, so
one mistake stings proportionally the same in a two-minute sprint and a
six-minute epic. And a single contact costs a bump or a stop, never both: if
a touch turns into a sustained scrape it gets reclassified and only the
difference is charged.
The gold economy lands here too, with a decision that removes a whole class of
bug:
Gold is derived, never stored as a balance — there's no
counter to corrupt or double-spend.
src/game/progression.ts
Your wallet is recomputed by scanning your best medal on every track. Improve a
medal and the upgrade pays you the difference automatically; wipe your records
and the wallet cleanly zeroes.
26 May 2026
A debugger built for one frame
Something in music mode was producing a single black frame. One frame,
intermittently, in a game that runs at 60 of them a second.
The fix for that is 624 lines of watcher that reads a slab of the framebuffer
back off the GPU every frame, inside the animation callback, and pauses the
engine the instant it sees one — so the bad frame is still on screen to
inspect. The clever part is how it avoids crying wolf: the real bug produces a
pure black sample, while a camera whipping under foliage produces a muddy dark
colour in the 0.1–0.4 range, so the detector has an absolute-darkness ceiling
and rejects anything that is merely dim. It also carries a near-miss threshold
that exists purely so a human who saw the flash can find out what numbers
occurred when the detector stayed quiet.
The bug it caught:
fix: address dark-flash bug in music mode by clamping
ndv calculation to prevent NaN propagation
A single NaN, one frame wide. The watcher is lazy-imported and editor-only, so
none of it ships in the game bundle.
22 – 31 May 2026
The pivot
Refactor audio handling and integrate music-based road
generation features
That is the whole turn, in one commit message. Within nine days the
architecture is rebuilt around it: major refactor into ecs like!, then major
folder re-structure and split the audio analyze code, then worker-based
analysis, and then the piece that makes it a rhythm game rather than a game
with music in it:
feat: implement song time to world distance mapping for
accurate player positioning in music mode & added a minimap
spectogram!
Tests arrive the same week — Playwright and Vitest, in one commit, at the exact
moment the project stops being a toy.
And a revert with a post-mortem attached
The single most useful commit message in the repository is one that undoes a
week of work and says exactly why:
Revert “refactor road surface handling to prevent overlaps and
improve terrain integration” — The unified heightfield
fixed the original overlap and hard-mode terrain problems but
introduced FPS regressions and apron artifacts (huge cliffs,
road bridging gaps) that we couldn’t tune our way out of. Going
back to the per-section apron until we have a better approach.
It names what the change fixed, what it broke, and the condition for trying
again. Most reverts say “revert.”
25 April – 21 May 2026
Nothing
Two commits on 6 May. Then silence for sixteen days. Whatever happened in that
month is not in the log — but what comes out the other side is a different game
with a different reason to exist.
17 – 24 April 2026 · 46 commits
A sandbox, and a look
The first commit is Initial zen skater game (Three.js + TS + Bun) and the
fourth is already a live editor — Add LevelEditor for live editing of
LevelConfig with Tweakpane integration. Before there is a game there is a way
to tune the game while it runs, which turns out to be the only real safety net
this project ever builds.
Day two settles the art direction in one line: Ghibli-style rendering pass:
painterly grass, colored cel shading, bloom post-FX. It sticks — the config
still carries fields called ghibliGrass and ghibliFoliage.
The week ends deep in parallax-occlusion mapping for the road surface, which
leaves behind a rule that outlives it: anything that sits on the road — the
rider, the sparks, the dust trail, the shadow — has to be lifted by the same
extrusion constant, or it sinks into a surface that only looks like it has
depth.
Two tools that film and analyse the real game
The offline analyser does not decode audio in Node. It boots the dev server on a random port, launches a headless browser, and runs the analysis in the page — because“MP3 decoding in Node is a perpetual dependency mess”and, more importantly, because re-using the exact runtime analyser means a pre-baked timelinecannot drift from a fresh in-browser one. Same WASM, same FFT, same downstream features.
The capture harness works the same way: rather than a mock, it embeds the actual game in a 9:16 frame, deep-links to a track, clicks the game's own play button, pins a watchdog on auto-steer so it plays itself, and waits for the real score card to appear before filming the tail. It records MP4 rather than WebM specifically because WebM from a browser recorder carries no duration header, which makes an early-stopped clip unusable.
What the log admits
Test coverage is deliberately thin… Most ‘verification’ still means runningbun dev and observing behavior in the browser; TypeScript + the live editor are the primary safety net.
Which closes the loop on day one. The first real feature was a live editor, and a hundred days later it is still the thing standing between the project and its bugs. The backlog file, meanwhile, contains exactly one item: that scored badges at the end of a track should appear with fireworks.