A Game Boy inside the editor, and a real phone under the app

Pokered Save Editor 2 stops drawing pictures of maps and starts emulating them — then builds a Game Boy sound chip in C++ and lets the cartridge grade it, frame by frame. Random AI Prompt puts its Android build on a real device and finds the defect was its own test. Fairy Fox Games plants Brim, a game about the water still in the air.

The theme of 2026-07-12 was the oracle — the discipline of asking something outside yourself whether you are right, and being willing to hear no.

Pokered Save Editor 2 spent the day handing its work to an actual Pokémon Red cartridge, running in an emulator, and asking it to check the answers. Random AI Prompt put its Android build on a real Android runtime rather than trusting a claim it had been making for weeks. Both were told they were wrong about things they were confident about — and in both cases the wrongness was in the instrument, not only in the code.

Pokered Save Editor 2: the map, actually emulated

The editor’s map screen has always drawn a picture of the map. Today it stopped doing that and started running the game’s own overworld pipeline: the blocks, wOverworldMap, wSurroundingTiles, wTileMap — the arrays the console itself builds, in the order it builds them (0.17.0).

Which raises the obvious question: how would you know if it were wrong? So the project built an oracle (0.18.0). A legally backed-up cartridge, never committed and refused by .gitignore anywhere in the tree, is booted headless in PyBoy with the editor’s own save fixtures. The emulator loads the save, and the harness dumps what the game actually did — the view pointer it computed, its overworld buffer, its scratch area, its on-screen tilemap. A test then demands the editor match it byte for byte.

The first run matched on four of five arrays and failed on the fifth, exactly as suspected: the three-block border ring. The editor filled it with the map’s border block. The console does that and then bleeds the neighbouring maps’ edges over the top — Pallet Town’s ring is really Route 1’s bottom rows and Route 21’s top rows, which is why you can see up the route before you walk it, and why the walk across is seamless. It was held as an explicit expected failure rather than skipped, so it could not be quietly forgotten.

Connection strips closed it (0.19.0). They are notoriously fiddly — the length byte means a width going north/south and a row count going east/west; a negative offset slides the source rather than the destination; 66 of the 78 connections in the game read across a bank boundary. All of it was re-derived from the game’s own connection macro, and then checked: a script finds every map header in the ROM by signature and compares all eleven compiled bytes against the editor’s recomputation. 78 of 78, zero mismatches. The expected-failure marker was deleted.

The same discipline turned up something better than a fix.

The six glitch palettes are real (0.20.0). The save byte the app calls “contrast” is wMapPalOffset, and the game does something unsafe with it: it subtracts the byte from a pointer into a table of eight contiguous three-byte fade palettes. Land on a multiple of three and you get one of the four real contrast levels. Land anywhere else and the read straddles two entries and produces three bytes that are not a palette anywhere in the game — six of them, exactly. Not simulated, not approximated: the editor applies the byte the console would genuinely be holding, so the glitch palettes come out as the real article. Ten values written into saves, ten booted on the cartridge, ten palette registers read back: 10 of 10.

Then the player was drawn (0.21.0) — and the two palettes that had looked harmless stopped being harmless, because their damage is to the object palettes, and now there is an object on screen. The map stays perfectly normal and the player alone goes wrong. That is what a Game Boy does with that byte.

Three sprite facts were pinned along the way, each of which would have been got wrong by guessing: there is no right-facing sprite (facing right is facing left, X-flipped); there is no second walking frame (the four-state walk is one drawing, mirrored); and sprites sit four pixels above their tile row — confirmed twice, from the disassembly’s own comment and from the console’s OAM.

Pokered Save Editor 2: 151 pieces of music, and not one audio file

The map screen now plays the game’s soundtrack, and there is not a single audio file in the repository.

The music is assembled straight out of the pret/pokered disassembly by an importer that proves itself twice (0.22.0): a track’s id is computed from its header’s address, so the importer recomputes all 46 ids from the table it just built and refuses to write anything if one disagrees; and, with the cartridge present, it walks its own byte stream and the ROM’s in lockstep, command by command. All three banks match the cartridge. 38 KB, and it is the whole soundtrack.

Playing it required a Game Boy. So the project wrote one: pse-audio, holding a DMG APU model — four channels, the 512 Hz frame sequencer, envelopes, sweep, the LFSR, wave RAM, DAC semantics — and a transliteration of Gen 1’s sound engine, whose entire state is a 256-byte array laid out exactly like the console’s $C000. The seam between them is the design: the engine emits register writes and knows nothing about sound; the chip knows nothing about notes.

Which is what makes the oracle possible. tst_sound_parity boots the real cartridge with a track patched into a save, photographs the engine’s 256 bytes of state on every frame, and requires the C++ port to reproduce every one of them. All 46 tracks, plus an inner voice, byte for byte, frame by frame (0.22.1).

It earned its keep on the first run, and the findings are a good illustration of what a real oracle is for. Three were bugs of tidiness — places where the port had helpfully done the right thing and the game does the wrong one. InitPitchSlideVars clobbers a register the caller only saves afterwards, so a pitch-slide note starts on a divide’s leftovers, not on its pitch; the port had kept the correct pitch, and was therefore wrong. PlaySound never restores the current sound id, so after a drum the engine’s “current sound” is the noise instrument; the port had saved and restored it, inventing a hygiene the game does not have. It is not a bug to fix, it is a bug to copy.

The fourth finding was the most valuable: the oracle itself was wrong. Entering a map fades the old music out first, and during the fade the channel ids already read as the new track while the audio bank register still holds the old bank — so the command pointers being photographed are addresses in a different bank’s data. It produces a dump that looks entirely plausible and is complete nonsense, and half the tracks “failed” against it. A wrong oracle is worse than no oracle, and this one was wrong in a way that reads as an engine bug.

And the glitch music turned out not to exist. A song’s id is derived from its header’s address, and a header is three bytes per channel — so a three-channel song occupies three consecutive ids, and the other two parse perfectly well as one-channel headers pointing at that song’s channel-2 and channel-3 streams. Id 186 is Pallet Town. Id 187 is Pallet Town’s bassline, alone. All 256 ids across three banks were parsed out of the cartridge: 46 real tracks, 105 inner voices, and zero garbage. The console was asked to confirm it, and did. The panel now lists all 151, grouped and named, each inner voice indented under the song it came out of — hover auditions, click commits (0.23.0).

Pokered Save Editor 2: a bug that put a bedroom’s walls in a Poké Mart

Working out what the map means — walls, grass, water, warps, ledges, counters — surfaced a save corrupting bug that had been there since the first version (0.24.0).

The collision lists are shared in the cartridge: RedsHouse1_Coll and RedsHouse2_Coll are two labels on one list, as are Mart/Pokecenter and Dojo/Gym. The original importer assumed one list per tileset in index order, and walked the chain one step out of phase for three of them. Since the editor writes that pointer into the save, putting the player in a Poké Mart wrote Red’s-house collision into their game — wrong walls, wrong door, wrong counter, in the real thing.

Fixed in three lines, and negative-controlled: a test now re-derives every collision pointer from the list data, so pointers and lists cannot disagree again in either direction. Put the bug back and two cases fail by name, one of them reading “A Poke Mart would get a bedroom’s walls.”

The day closed with the map animating (0.24.4) — and writing that loop exposed two more invented facts. The console does not slide the water tile from 0 to 4 pixels and back; it rotates the sixteen bytes of the tile, swinging from −1 to +3, because the direction flips halfway while the offset keeps accumulating. And the flowers do not cycle evenly: the first is shown twice as long as the other two. Both of the old behaviours were plausible. Both were made up. The test now checks that each water step is a genuine rotation of the tile rather than a second picture of it, because that is what the console does — it does not have a second water tile, it rolls the one it has.

Random AI Prompt: the promise, measured

The prompt tool has been advertising a number — 1,000 prompts in a roll — and on mobile it had never actually been checked on a phone. Today it was, and the exercise is a small lesson in instruments.

First the app was put in CI at all (2.58.0): a test presses every control rather than merely rendering it, behind coverage gates. Getting there took a run of failures that were all about the harness, not the app — an emulator launching the app behind its own lock screen, a dark screen granting window focus to nobody, the launcher itself throwing “app not responding” dialogs on top of the thing under test.

Then the measurement (2.60.0), and for a few hours the notes said the app had failed its own promise: 1,000 prompts, no completion inside a ten-minute timeout. It was wrong. The instrumentation added to diagnose the failure is what disproved it — the engine produced all 1,000 prompts in 13 seconds on a software-rendered emulator, and React committed every row 34 milliseconds later. The bug was in the test: results accumulate across rolls by design, so after a 20-prompt baseline the label reads “220 generated”, and a test waiting for “200 generated” waits forever, then reports its own timeout as the app being slow.

The real figures, from the release APK on a real Android runtime:

Roll engine render memory
20 1,497 ms 383 ms 112 MB
200 5,447 ms 196 ms 122 MB
1,000 23,366 ms 253 ms 110 MB

Fifty times the rows, for no more memory and no more render time. The list virtualizes exactly as claimed — and now it is a measurement rather than a claim.

Random AI Prompt: a filename that could run a program

CodeQL flagged it on the release PR and it was right (2.60.1). Two backend routes built a shell string around a request-derived file path to open an image or reveal it in the file manager. Path traversal was blocked; nothing else was. An image named x" & calc & ".png closes the quote, and everything after it runs. Severity: critical.

The fix is structural rather than a better escaping function — escaping is a losing game played on the attacker’s board. There is no shell now: the program and its arguments are built separately and handed straight to the OS, so a quote inside a filename is just a quote inside a filename. Three more from the same sweep were real too: prototype pollution through a merged sidecar patch, a world-writable temp cache that any local account could pre-create and be trusted through, and three check-then-use races. The regression test was proven by restoring the old shape and watching it go red.

The note that closes the entry is the honest one: “it’s a local-only backend” is mitigation, not absolution — it is one flag away from a LAN.

Fairy Fox Games: Brim, and the water still in the air

The thirteenth game, and a verb the collection did not have: pour (0.22.0). A vessel arrives with a fill line to reach and a rim not to cross. You get one pour — hold to open the spout, let go to close it.

The hook falls out of the physics rather than being bolted on. The stream is a delay line: what leaves the spout lands about 130 ms later. So releasing does not stop the level rising, it stops the source, and the column already in the air keeps arriving. You cannot stop where you want. You must stop early, by exactly the amount still falling, and watch it land. Short of the line costs a life; over the rim costs a life; settling in the gold band just under the rim is a brim, and the multiplier climbs. Greed and survival are the same act, which is this collection’s standing test for whether a mechanic is worth planting.

It ships with varied structure from birth — a run is a seeded sequence of named pours (Steady, Slow Draw, Stutter, Narrow Neck, Hairline, The Flood), stage-gated so climbing opens the pool. Stutter is the cruel one: the flow alternates between a trickle and a gush, so the size of the carry changes every vessel and a memorised release point is worse than useless.

Earlier in the day, Skyline got the wind (0.21.1) — its slabs now arrive as named, stage-gated patterns rather than textureless drift, bringing the ninth game onto the varied-structure standard.

Fairy Fox Stories: a grow day

Three books each gained a third chapter (0.3.4): The Wintering House, The Cartographer of Decks, and The Cinderwick Job. No plant — the farm’s three-day cadence gate said the next one is not due until the 13th, and it held rather than planting early to look busy.

The site

The account behind all of this was renamed junebug128511fairyfox (0.15.9) — a change spotted in Random AI Prompt’s own history and then verified against GitHub rather than taken on trust: the old profile now 404s, so this site’s footer link, its handle, and the About page’s profile link were all broken.

The rename was swept through every current-state file — the site config, the project data, the hub registry and standards, the templates, and the shared chrome bundle that every project’s docs site pulls, so the nodes pick the fix up on their next refresh. Dated history — the sessions, the changelog, these posts — was deliberately left as it was, because it records what was true on the day.

One consequence is left open rather than acted on: the hub repository is still named junebug12851.github.io, so it is no longer a GitHub “user site” in the strict sense. Nothing is broken — everything here is reached through the fairyfox.io custom domain — but the claim was false in several documents, and correcting the claim is a maintenance change while renaming the repository is not.


← All updates