2026-09-06 · KURO KISHI

One copy of every number

KURO KISHI now runs in Unity, with enemy paths, fire, formations, power meter, boss phase machines, HUD, sound and a run through all eight stages. The port never restates the game's balance, so a failure can never mean someone mistyped a constant.

KURO KISHI now runs in Unity 6000.0.45f1. Authored enemy paths, enemy fire, formations, the power meter, boss phase machines, a HUD, sound, and a run that plays all eight stages end to end.

The shape of the port

The Unity side does not restate the game's balance. A dumper in the web repo calls the shipping TypeScript and writes what it computes to JSON. The C# reads the same file and has to reproduce every value. So the port rewrites logic only, and a failure can never mean someone mistyped a constant, because there is only one copy of each.

Path parity alone is now 23,666 checks. It grew from nothing this session.

What runs

systemwhat it coversevidence
Enemy movementAuthored screen paths, drift profiles, set-piece motionpart of the 23,666 path checks
Enemy firingCadence, engage gates, aim, salvos, projectilesvolley intervals per kind per stage
FormationsBanded rows, jitter, staggered arrivaly, x and delay per member
Player progressionPower meter, capsules, damage, scoring, weapons, shieldincludes the all-maxed capsule case
BossesFour phase machines, authored volley tablescases land on every threshold
HUDStatus line, power meter, flashes, 5x7 bitmap fontstrings compared character for character
Audio26 of 39 cues, 45 clips, authored gainscoverage check, mapped or declared unported
Stage lifecycle8 stages, 7 changes, run completes81,157 steps of headless play

Stated plainly, the bosses' phase machines and volley tables are ported and verified, but nothing drives them yet, so a stage currently ends on its clock and not on a boss dying. This is also a verified port, not yet a player build.

Four real defects, three of them in things already passing

The value of porting is not the port. It is that a second implementation asks questions the first one stopped asking.

The spawner was wrong and every check passed

Waves were laid out as evenly spaced slots, all arriving on one frame. The game uses a fixed number of row bands with a deterministic jitter, an X spread, and staggered per-member delays. The stage-runner check asserts that every wave fires once, on time, with difficulty-scaled counts and HP. All of that stayed true while every formation on every level was the wrong shape. A check only catches what it looks at.

Destroy does nothing outside play mode

Every enemy the port destroyed during editor-driven verification stayed in the scene and kept drawing. A rendered frame showed ten enemies while the runner tracked three, and that screenshot had already been used as evidence the port was working. The screenshot tool now counts renderers against the live list and fails loudly when they disagree.

The port could never have shipped

Every loader read from the project's asset folder with a plain file read, and the sprite provider walked the editor's asset database. Neither exists in a build. Four passing parity checks, a rendered stage and real screenshots, all of it in the editor where those paths work. It would have surfaced as a file-not-found on the first launch of the first build.

A path pick that returned undefined

The function that picks a random authored path indexed with a raw modulo, so a negative id ran off the front of the array and returned undefined despite a type that promised otherwise. It is unreachable in play, because the id is a counter that only goes up, so this changes nothing in the web build. It removes a claim the code could not keep, and it was fixed at the source rather than worked around in the port.

Two verification tools that could not fail

This happened twice in one session, which is why it is worth naming as a pattern.

The stage-screenshot tool reported a kill count that was structurally always zero. It read a counter that only accumulates inside a method the tool bypassed. It read as "the player cannot hit anything" when the truth was the opposite.

The mission check passed in 25 seconds having exercised zero stage changes. The player sat still, died, and the check accepted a failed run as a legitimate outcome. It now runs with god mode and requires exactly seven stage changes and eight stages visited.

A check whose passing condition is reachable without the behaviour under test is decoration.

Decisions worth recording

  • The tuning is not typed twice. The path profile for each enemy kind is a content JSON merged with a thirteen-case switch. That merge now runs in the dumper and lands in a JSON the C# reads. Same decision as the combat constants.
  • The HUD uses a generated 5x7 bitmap font, not a vector text package. The web build picks Consolas over Courier because Courier antialiases at seven pixel rows and the grey fringe magnifies into mush. A vector renderer at that size reproduces the exact problem the original was tuned to avoid, and the bitmap font needs no new package.
  • Audio is mostly a mapping job. The synthesiser looked like the biggest remaining task and mostly is not. 26 of 39 cues play a CC0 sample, and all 50 OGGs were already in the project. The list of sound kinds became a runtime array so the port can check its own coverage instead of transcribing the list.
  • One parallax builder, shared by editor and runtime. That routine has been got wrong four separate ways, and a duplicate would inherit none of the comments recording them.
  • A sound that is not ported says so. Playing one of the 13 unported cues returns false, so a caller can tell "played" from "no such sound yet", and the coverage check fails if a cue is neither mapped nor declared.