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, and the sky generator's prompt still asks
for background art in the manner of Kazuo Oga.
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.
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.
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.”
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.
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.
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 style prompts the songs were written from.
The commit message ends with its own report card:
offline calibration/regression harness that scores the classifier
against ground-truth genres from docs/songs Style prompts. 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.
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.
The soundtrack
Thirty-three songs that did not exist
A rhythm game needs a setlist, and a setlist is the one thing an
independent project cannot license. The answer here was to write one.
Every track in the game is an original, generated to sit in the same
lane as a well-known reference — fresh title, fresh lyrics, a style
prompt aimed at the same corner of the room. Fifty-five were written;
thirty-three shipped.
The house guide for writing them contains the one rule that
everything else depends on, and it is phrased as a warning:
Anything in the Lyrics box that is not inside square brackets gets
SUNG. That includes bare description lines and parentheses.
And the honest note about the process:
Generate many, then curate — it is a slot machine. Roll the same
prompt 4–10 times and keep the take with the best vocal/melody.
One-shot output is generic by default; the great version is
selected, not summoned.
Raw output was too soft to ship, so there is a three-stage repair
pipeline: split the mixdown into stems, re-sum them with per-stem
gain and a de-mud EQ — a bell at 300 Hz for the boxiness, a shelf at
9 kHz for the dull top end — then match the loudness, EQ curve and
stereo width to a real reference master. The whole chain, including
transcode, silence trim, analysis and difficulty baking, is one
command.
Two tracks in the list are the same prompt rolled twice and both kept.
One song exists in two languages from the same reference. And the
thumbnails folder still holds fossils from before any of this — files
named after real bands, left over from the prototype that ran on
commercial MP3s.
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.
CLAUDE.md
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.
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 timeline cannot 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 running bun dev and observing behavior in the
browser; TypeScript + the live editor are the primary safety net.
CLAUDE.md
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.