Skip to content

feat(nightbloom): a vertical danmaku garden on the deterministic runtime#88

Draft
SummonLav wants to merge 6 commits into
pocket-stack:mainfrom
SummonLav:main
Draft

feat(nightbloom): a vertical danmaku garden on the deterministic runtime#88
SummonLav wants to merge 6 commits into
pocket-stack:mainfrom
SummonLav:main

Conversation

@SummonLav

Copy link
Copy Markdown

A new demo: NIGHTBLOOM, a vertical danmaku shooter in the Imperishable Night grammar with a Plants-vs-Zombies-flavored cast — built as a stress test of the framework's determinism story on a genre that usually resists it: a real-time bullet hell.

nightbloom

What it is

A 480×272 portrait-field STG (demos/nightbloom/): you pilot one plant at the bottom of the field and swap between a roster of three — a graze-master cat, a healer sakura whose every hit mends the most wounded form, and a gap-moe gorilla throwing banana boomerangs that must be caught (max 3 aloft). Foes descend in phased waves toward a midboss and a three-card diva whose every metamorphosis brings new art and her own voice. Locked roster cards wake through play (first stage-II ascension, midboss fall), each with a slanted rainbow sweep and particle tail. If your piloted form wilts and you don't swap within 1.5 s, the night is eternal — LAST BREATH. A run ends in a two-act dawn ceremony: the score counts up center stage, then a gold medal slams on like a sticker — always a roast (MERCY MEDAL: "2 foes strolled off unharmed").

Full gameplay + controls live in the in-game codex (SELECT) and demos/nightbloom/README.md.

Why it belongs in this repo

The whole game runs on the deterministic contract:

  • Rate-portable simulation — the same input tape produces byte-identical final frames and strictly subsampled hash streams at 60, 4, and 2 Hz, across a full 190 s run: bullets, homing, grazes, unlock sweeps, boss metamorphoses, marquee scrolling, and the dawn medal stamp included. The sim test asserts hashes4[m] === hashes60[15(m+1)−1] and cross-rate finalFrame equality.
  • Chaos-proof — a 4 Hz run under {maxSleepMs: 6, gcEvery: 32} hashes identically to the clean run.
  • Effect shell — the per-phase "augury" forecast is a pure respond() behind runEffect, delivered on the virtual clock exactly one virtual second later at every hz.
  • Sound as pure output — the engine pokes a globalThis sfx sink; the browser host installs a WebAudio synth, sim and PSP hosts install nothing, so audio never touches the state stream.
  • Determinism disciplines the engine had to learn (batch-edge input timing, tick-age fx instead of mount-anchored tweens, rate-aligned end-screen clock) are documented in the engine.ts header.

Content pipeline

All 35 sprites/scenes/portraits are generated by the PixelLab API from a seeded manifest in data.ts (bun demos/nightbloom/gen-assets.ts), with evolution stages chained via init_image so each creature keeps its identity as it grows, then re-encoded into the pak-safe canonical PNG subset. Generated art is committed; the script is a no-op unless assets are deleted or --forced. Same contract as tidelight's pipeline.

Tests

bun run test now ends with the nightbloom chain: content-table validation, repeat identity, chaos, 60/4/2 Hz subsampling on two tapes (a 190 s winning run and a no-input sleeper), cross-rate final-frame equality, augury delivery timing, and end-ledger probes (score, graze, medal text). 10 tests, 1320 assertions, green alongside the existing suites.

🤖 Generated with Claude Code

SummonLav and others added 4 commits July 11, 2026 11:58
…grammar on the deterministic runtime (#3)

* feat(demos): nightbloom — a plants-vs-yokai lane defense on the deterministic runtime

One shrine, five lanes, an eternal night: a garden-defense action game in
the Plants vs. Zombies grammar, flavored after the eternal-night yokai
stories of the Touhou fangame tradition, and simulated in fixed 1/60 s
micro-ticks so every simulationHz plays the SAME battle:

- plants and foes both evolve I -> II -> III on two laws: plants by their
  own work (damage dealt / lumen gathered / blows endured, feedable), foes
  by the night phase — and on the spot when they finish eating a plant
- attack/defense matchups as data: armor vs true petal damage, ranged
  mochi lobbers vs walls, a haste-aura songstress worth focusing first
- possession: CIRCLE possesses any plant (the required cute cat included),
  held CIRCLE channels its fire, L/R switches, TRIANGLE casts its spell
  card — MOONRISE / PIERCING GALE / NINE LIVES / STONEHEART / PETALFALL
- batches align to (t - tStart) * 60 ticks, edges land on the first tick
  of a frame, held input gates on a half-second plus a frame boundary, fx
  drift by battle-tick age: the 2 Hz world is a strict subsample of the
  60 Hz world, gameplay included

Content pipeline: demos/nightbloom/gen-assets.ts generates all 35 sprites
and backdrops through the PixelLab pixel-art API from the seeded manifest
in data.ts (stats, prompts and filenames in one table, checked by
validateContent); evolution stages are init_image chains so a creature
keeps its identity as it ascends. Assets are committed; the script is a
no-op unless one is deleted or --force is passed. .env (the API key) is
now gitignored.

test/nightbloom.sim.test.ts drives THE GARDENER (a full 170 s winning
night: 31 kills, one ward spent, a stage III bloom) and THE SLEEPER (an
untouched loss) through the sim host and asserts repeat identity, chaos
immunity, strict 4/2 Hz subsampling of both tapes, cross-rate byte-equal
outcome screens, augury effect timing, and the exact end-screen ledger;
wired into bun run test.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* feat(nightbloom): rework as a vertical danmaku shooter — the Imperishable Night grammar

The garden was a lane defense; the night wanted a bullet-hell. NIGHTBLOOM
is now a vertical STG: the player PILOTS one plant at the bottom of a
portrait playfield (centered on the 480x272 screen, HUD panels both
sides), the yokai horde descends from the treeline, and CIRCLE / L / R
switch the piloted form mid-fight — the Imperishable Night verb, with the
same PixelLab bestiary:

- five pilotable forms with distinct attack/defense trades: homing orbs
  (the cat), piercing bolts, a true-damage petal fan, an armored tank
  with a full-heal spell, and a moonlight gatherer; spell cards double
  as bullet clears
- two evolution laws: forms grow by their own work (damage dealt, motes
  gathered — auto-collected above the PoC line — and bullets grazed),
  the horde grows with the hour (dusk I, midnight II, witching III)
- nine waves, a midboss, and a three-card final boss with Touhou-style
  timeouts; enemy danmaku is native dots off a QUANTIZED sine table
  (1/8192 steps) because raw Math.sin is not bit-specified across JS
  engines and a spiral must replay byte-exactly on every host
- held verbs (movement, fire, focus) read the raw held mask — a hold's
  level track goes true at the same battle tick at every rate — while
  press edges land on a batch's first tick, so the 2 Hz world dodges
  the same spiral the 60 Hz world does, frame for frame

test/nightbloom.sim.test.ts drives THE MARKSMAN (a full clear: every
form flies, three reach stage III, the diva's last card breaks with 4
of 5 forms alive — score 8495, graze 61, 35 felled) and THE SLEEPER
(an untouched loss) and asserts repeat identity, chaos immunity,
strict 4/2 Hz subsampling of both tapes, cross-rate byte-equal outcome
screens, augury effect timing, and the exact end-screen ledger.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* feat(nightbloom): hit and impact sfx through a host sound sink

Sound is an OUTPUT, never an input. The engine emits SfxKind events from
deterministic tick state — the hit thock, the kill pop, hurt/wilt, graze
pings, mote chimes, form-switch clicks, spell-card declarations, boss
card breaks, and the dawn/eternal stingers — and reads nothing back, so
the simulation is byte-identical with or without audio.

demos/nightbloom/sfx.ts installs the sink where WebAudio exists (the web
host): every voice is synthesized from oscillators plus one xorshift
noise buffer (no assets, no pipeline), per-kind throttled, voice-capped,
and resumed on the first user gesture per the browser autoplay policy.
The headless sim and QuickJS-on-PSP never install a sink and skip the
poke entirely. A __nightbloomSfxState seam lets harnesses confirm the
device state without ears.

Verified live: the AudioContext resumes on a trusted key press, a
26-second browser battle emits shoot/hit/kill/mote/graze/hurt/switch/
spell in sensible proportions, and voices drain to zero after their
envelopes. bun run test stays green — the sim never hears a thing.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* feat(nightbloom): cuter bestiary regeneration + uniform-scale scene fix

All 35 PixelLab assets regenerated from softened prompts: kawaii chibi
style strings (huge sparkling eyes, blushing cheeks, plump rounded
bodies, pastel glow) and per-entry rewrites that trade fierce for fluffy
— marshmallow pyre flames, a chubby umbrella warlord with toy blades, an
extra-fluffy moon rabbit, storybook-spooky plum for the eternal night.
Seeds and init_image evolution chains are unchanged, so every creature
keeps its identity across stages; f-kasa-3 / f-uta-3 still dress the
bosses. The sim never sees a pixel: the ledger (score 8495, graze 61,
35 felled) is byte-for-byte the same night.

Scene stretch fix: the 256x128 backdrops were drawn inset-0 w-full
h-full — 1.875x wide but 2.125x tall, a 13% vertical squash the moon
wore visibly. Title and outcome screens now draw the scene at 480x240
(UNIFORM 1.875x, the tidelight rule) over a 32px letterbox band that
the footer legend sits in.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* feat(nightbloom): waking roster, the last breath, the moon cat and the mountain

Three player-facing changes, one night:

- The roster shrinks to three pilotable forms and WAKES as you play: only
  the CATNIP KIT answers at dusk (the other cards show ?); the first
  stage-II ascension wakes the sakura SAPLING, and the midboss's fall
  rouses MOON PRIMROSE. Bamboo and the stone lantern stay in the tables
  as reserve content.
- No pilot switches itself anymore: when the piloted form dies, the LAST
  BREATH opens — a 1.5 s window (invulnerable, guns silent, banner
  flashing) to switch to a waking form. Miss it, or fall with nobody else
  awake, and the night takes the run. The sleeper tape now ends at ~34 s
  for exactly that reason.
- New portraits: the catnip is a black-and-gold cat with a white moon on
  its brow that waxes as it ascends (crescent, crescent, full); the moon
  primrose is a gap-moe gorilla — a mountain of carved abs under the
  sweetest little face — regenerated through the same seeded init_image
  chains.

The ledger moves (score 8145, graze 42, 34 felled, 3 of 3 awakened
alive) and every determinism claim re-verifies: repeat identity, chaos
immunity, strict 4/2 Hz subsampling of both tapes, byte-equal outcome
screens. bun run test green.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* feat(nightbloom): living avatars, pokemon growth, rainbow reveals, bananas, three true abilities

Five player-facing touches, all on the tick:

- The avatar is alive: a 1.2 s triangle-wave idle bob, a lean into the
  strafe, and an invuln blink — pure functions of the battle tick, so the
  animation subsamples as exactly as the sim.
- Pokemon growth: each stage is its own portrait AND its own size — the
  avatar draws at 22 / 27 / 32 px as it ascends (the hitbox never grows).
  All three chains regenerated with baby -> grown -> majestic language;
  the moon cat and the ab-carved gorilla keep their approved looks.
- When a ? card wakes, a rainbow comet (pink-cyan head, amber-pink tail)
  sweeps it left to right — 48 ticks of reveal, driven by unlockedAt.
- MOON PRIMROSE throws BANANAS: heavy slow volleys of spinning fruit
  (rotation is a pure function of tick and shot id), new shot-banana art.
- Three true abilities beyond shot shapes: the catnip dances with death
  (wider graze ring, double graze glow); the sakura heals — every
  damaging petal mends the most wounded waking form by 1, at the price
  of soft hits; the gorilla hits hard and gathers — motes are worth
  double glow to him.

The ledger moves to score 8235 / graze 53 / 34 felled; every determinism
claim re-verifies at 60/4/2 Hz. bun run test green.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* feat(nightbloom): facing mirror, slanted particle reveals, world drift + prune unused art

- The avatar mirrors into its strafe (scaleX on the sprite, lean on the
  container so the two transforms never argue): press LEFT and the moon
  cat looks left, and keeps looking there until it flies the other way.
- The unlock reveal is now a SLANTED shine (18 degrees, two gradient
  bars) trailing twelve specks of pink/cyan/amber dust whose spacing
  widens quadratically — densest right behind the head, thinning out.
  Pure translateX off the reveal progress, deterministic as ever.
- The world never stops scrolling: hovering foes (wisps, sparrows) sink
  10 px/s with the starfield while on station, hold for 8 s instead of
  12, then accelerate off — an unkilled monster always leaves the field
  with the view instead of parking forever.
- Pruned everything the roster cut left behind: bamboo and stone-lantern
  defs, their six sprites, shot-bolt.png, bg-field.png (the STG field is
  native-rendered), the GALE beam, the STONEHEART shield, and the "bolt"
  shot kind. PlantId narrows to the three real forms; the pak drops to
  37 entries with zero placeholders.

Ledger: score 8280 / graze 56 / 34 felled, dawn at ~162 s; the sleeper
falls at ~40 s. All determinism claims re-verified at 60/4/2 Hz.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* feat(nightbloom): healer-soft sakura, banana boomerangs with a catch, per-art facing

- The sakura is a healer first: petal damage drops to 3/4/5 (still true
  damage, still a wide fan) — her worth is the mending, one hp to the
  most wounded waking form per damaging petal.
- The gorilla's bananas are BOOMERANGS now: they fly up, stall against
  260 px/s^2, turn, and home back to his hand at 240 px/s. A banana
  never despawns on a hit — it cuts through on the way out and again on
  the way home (one touch per 0.3 s) — and only a CAUGHT banana can be
  thrown again: at most three aloft, with a hand-count of banana pips on
  the HUD while he pilots. Throw depth grows with his stage.
- Facing correction per portrait: the sakura and the gorilla came out of
  the generator facing left, so their strafe mirror ran backwards. Each
  form now declares artFacing and the renderer mirrors facing * artFacing
  — every pilot looks where it flies.

Ledger: score 8435 / graze 78 / 33 felled, dawn at ~182.8 s, all three
awakened forms alive. Determinism re-verified at 60/4/2 Hz; bun run
test green.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* feat(nightbloom): boss metamorphoses — per-phase portraits, cries, and a harder diva

Every spell-card change is now a visible, audible transformation:

- Four new 64x64 boss portraits: IRON KASA GROWN WRONG for the midboss,
  and three diva forms — the stage dress, the flared-wing chanter, and a
  phoenix-like final form. The sprite AND its draw size change per card
  (52 -> 58 -> 66 px), the hit radius scales with the form, and the
  switch lands with a scale-in pulse and an expanding flash ring.
- Each boss has its own cry, played on entry and on every metamorphosis:
  the diva gets a three-chirp bird call, the umbrella a beating metal
  clang over a low whoomp — synthesized like the rest of the sink.
- The diva sings harder: card hp 720/830/950, denser rings (14), faster
  spirals and mochi pairs, +4 bullet speed. The gardener's tape still
  breaks the finale — at 184.1 s now, with the catnip down to 70 hp and
  92 grazes on the ledger. Score 8575, all three forms alive.

Cross-size init_image turned out to be a pixflux 500, so the two boss
bases anchor identity by description; the diva's later forms chain from
her first at 64x64. Determinism re-verified at 60/4/2 Hz; bun run test
green.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* docs(nightbloom): the whole game in the codex — a two-page in-game book

SELECT now cycles a two-page book instead of one screen:

- Page one, THE PILOT'S MANUAL: every control (fly, fire, focus, switch,
  spell, pause), the shape of the night (nine waves, the midboss, the
  diva's three transforming cards), the growth laws (glow from wounds,
  motes and grazes; the PoC line), and the LAST BREATH rule.
- Page two, FORMS AND FOES: the three forms with their ability laws, the
  waking rules (catnip at dusk, ascend for the sapling, fell the umbrella
  for the ape), the foes with theirs, and the phase table.

The native host does not soft-wrap text, so the book is typeset by hand
into short lines and the data laws are split at a word boundary; foe laws
are tightened to single lines so both columns sit inside 272 px.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
* fix(nightbloom): left-panel long lines scroll like a ticker instead of clipping

The AUGURY omen, the boss's name and the spell-card title overflowed the
134 px left panel and clipped at the box edge. They now ride a marquee:
two copies loop leftward, driven by the battle tick — so the scroll
subsamples exactly like everything else — and lines short enough to fit
hold still. Width is estimated from the glyph count.

The hero screenshot is retaken on the FINALE with the moon cat piloting
(NINE LIVES charging) and the panels mid-scroll.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* feat(nightbloom): dawn medals — decorations for the wrong things

Clear the night and the dawn screen pins HONORS, OF A SORT on you: up to
three roast medals derived from the run's ledger — MERCY MEDAL (foes that
strolled off unharmed), PINCUSHION (hits taken, and proud), COMPOST AWARD
(gardeners wilted on your watch), OUTSTAYED WELCOME (cards that died of
old age), LITTERBUG (motes left in the grass), PERSONAL SPACE (not one
graze all night). A spotless run earns SUSPICIOUSLY PERFECT — the night
demands a rematch. Win screen only; the eternal night roasts nobody.

The engine grows a roast ledger (escaped / hitsTaken / motesMissed /
cardTimeouts counters plus seam accessors), the end screen is compacted
to fit the medal case, and the sim test pins the marksman's exact
decorations: 2 strolled off, struck 33 times, 24 motes in the grass.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* feat(nightbloom): the dawn medal ceremony — score first, then the stamp

The dawn screen is a two-act sequence now, on the outcome screens' own
clock:

- Act one: the score takes center stage and ROLLS UP arcade-style over
  the first 1.2 s; the rest of the ledger waits in small print below.
- Act two, at the 2 s mark: ONE medal — the roast pick of the run — is
  slapped onto the glass: a PixelLab gold-and-ribbon medal base slams in
  from 3x scale, tilted, lands at -12 degrees with a screen shake and a
  thud from the sound sink, and the paperwork dims beneath it. The title
  is stamped in layered art lettering (amber over pink over shadow) with
  the detail line on a plaque below.

The end clock is (wall ticks - the tick the outcome settled on): both
terms are rate-aligned, and the settling frame updates it in-batch, so
the count-up and the slam land on the same virtual moments at every
simulationHz — the full 190 s runs, stamp animation included, remain
strict 4/2 Hz subsamples of 60 Hz. The sim test pins the final frame:
score 8575 on stage, MERCY MEDAL stamped over it.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Batch PSP swarm rendering, fix the 59-second particle corruption, reduce second-phase combat load, and rebalance healing around Moon Primrose.
@SummonLav

Copy link
Copy Markdown
Author

PSP performance follow-up is now included in b858ea5.

  • Standard overlap: 39.796 ms -> 13.592 ms average work
  • Midnight stress window: 27.518 ms -> 14.613 ms average, 180.774 ms -> 69.695 ms max
  • Fixed the reproducible 59-second cyan field by bytewise particle-buffer decoding and invalid-size rejection
  • Reduced ordinary waves to two multi-pattern foes and capped enemy shots at 48
  • Replaced square native bullets with cached antialiased disc textures
  • cargo test: 59 passed; bun run test: green; Nightbloom determinism/full-clear suite: 10 passed

Measurements are PPSSPP PSP-side timers, not real-hardware proof.

Reduce Nightbloom PSP frame cost, move Moon Primrose to a 28-mote unlock, and render PixelLab cat-paw projectiles through one shared particle texture.
@SummonLav

Copy link
Copy Markdown
Author

Nightbloom PSP performance follow-up is now included in this PR at 50a3586:

  • Moon Primrose now unlocks after 28 collected enemy drops (deterministic unlock tick 3738), decoupled from the midboss entrance/defeat work.
  • Catnip fire now uses a PixelLab-generated mint/lime cat-paw sprite, rendered through one shared texture on the existing retained particle batch.
  • The PSP hot paths remain paint-only and preserve one packed host call per shot batch.

Validation:

  • bun run test — pass
  • cargo test --manifest-path core/Cargo.toml — 60/60
  • PPSSPP 46b986e, 3 samples: unlock avg/max 9.212/15.515 ms; final boss avg/max 14.640/29.296 ms (boss avg was 15.977 ms before this follow-up).

Rename the playable cat, remove botanical cues from its PixelLab prompts, and replace its projectile with a pearly white prismatic paw while preserving the shared particle batch.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant