Jonathan Mellette Contact

Short builds · Unpublished

The lab

Short builds where I try the verification habits from my main projects on new ground: a puzzle book whose test suite checks every shipped puzzle’s content, a card engine whose solver is cross-checked against exhaustive search, a wireframe 3D prototype, and a platformer drawn entirely in code.

Four short builds, measured on a fresh copy of each build’s committed tree. None is published, so nothing here links out.
BuildStatusVersionStackMeasured
Pocket Puzzles In development 0.7.0 Godot 4.7, GDScript 469 assertions in 8 suites
CartoFancy
working title
FreeCell complete 0.1.0 TypeScript, PixiJS 8, Vitest 4, Web Workers 1,005 tests
3dPuzzler Prototype 0.1.1 Godot 4.7, Jolt 65 assertions
Platformer
working title
Prototype — unversioned Godot 4.7 38 bodies × 8 rig states

Measured · how

Why a lab

A short build is the cheapest place to find out whether a verification habit carries over to a new problem. I set the architecture and the checks, and AI coding agents build inside them. Each build’s gates are made to fail on purpose before they are trusted.

Build 1 of 4 · Puzzle book In development · v0.7.0 Proprietary · Ephphatha Games

Pocket Puzzles

A variety puzzle book as software: six puzzle types on one shared page, mobile-first in both orientations. Its test suite checks the shipped content on every run, and every generated word-search grid is re-checked before it can be played.

assertions across 8 headless suites, all passing, plus 3 runner self-tests
469
puzzle types: cryptogram, word ladder, Cryptogories, word search, codeword, alphametic
6
visual themes, each one contrast-gated
108
device shapes every screen is laid out and tested on, from a 320 px phone to a desktop
6
How these were measured
  • 469: the project’s own runner, node scripts/run_tests.mjs, on a fresh git-archive copy of the v0.7.0 tree, 12 Oct 2026: 3 self-tests, import and type-check passed, then 8 suites with 0 failed. Passing assertion lines counted per suite: alphametic 47, codeword 24, Cryptogories 26, cryptogram 71, layout 174, themes 18, word ladder 75, word search 34.
  • 6 types: the playable scenes in scenes/, matching the README’s status section.
  • 108 themes: the length of the theme list in content/themes.json; the contrast gate is tests/test_themes.gd.
  • 6 shapes: the device list at the top of tests/test_layout.gd.
Four phone-size Pocket Puzzles pages side by side: an alphametic reading CROSS plus ROADS equals DANGER with a digit keypad, a cryptogram in a blue monospace theme, a word search of birds, and a cryptogram in a dark candlelit theme
Fig. 1 Four phone pages rendered by the project’s own screenshot tool from v0.7.0: an alphametic, two cryptogram themes and a word search. The cryptograms encipher public-domain quotations.

Case 1 of 4 · Pocket Puzzles

A test runner that refuses to trust an exit code

Green has to be earned three separate ways, and the runner proves its own gates can go red before it trusts any of them.

Godot’s tooling reports success in three ways that aren’t. Its type-check exits 0 even when it finds errors. A script it is told to run exits 0 when that script fails to parse. And a runtime error inside a test unwinds that one test function while the suite carries on and prints PASS. The last one surfaced the expensive way: a refactor renamed two methods, and the word-ladder suite skipped six assertions and still reported green.

So I designed a gate with three conditions. The type-check must pass before any suite runs. Each suite must print its own closing verdict line, because a crash halfway through also exits 0, with a plausible transcript. And no SCRIPT ERROR may appear anywhere in the output.

Before the runner trusts those gates, it tests them. A suite that doesn’t exist must come back red. The verdict check must reject a transcript with no verdict line, and the error check must recognise a real runtime-error line while letting a clean transcript that ends in PASS through. The 3D prototype and the platformer further down this page use the same runner.

node scripts/run_tests.mjs
  ok    self-test: a suite that cannot run is caught
  ok    self-test: the verdict line is what decides, not the exit code
  ok    self-test: a runtime error fails the suite even with a PASS line
  ok    project import
  ok    type-check
  ok    tests/test_cryptogram.gd
  ok    tests/test_word_ladder.gd
  ok    tests/test_cryptogories.gd
  ok    tests/test_word_search.gd
  ok    tests/test_codeword.gd
  ok    tests/test_alphametic.gd
  ok    tests/test_themes.gd
  ok    tests/test_layout.gd

8 suites, 0 failed
Fig. 2 The runner on a fresh copy of v0.7.0, 12 Oct 2026, re-rendered as text. Its three self-tests run before the import, the type-check and any suite.
How we know

The three gates and the self-tests are in scripts/run_tests.mjs; the type-check wrapper, scripts/check_scripts.mjs, exists because Godot’s check-only mode exits 0 and reports errors only on stderr. Re-run 12 Oct 2026 on a fresh git-archive copy of the committed tree, with Godot’s home directory redirected into a scratch folder so nothing was written to the project. The 3D prototype and the platformer carry the same suite gate, line for line (compared with diff), and both are green.

Case 2 of 4 · Pocket Puzzles

Puzzle content that is proved, not trusted

Every shipped sum, ladder and theme is checked on every test run, each gate has a case it must reject, and a screenshot tool catches what measurement can’t.

A puzzle with two answers, or none, is a defect the player finds before you do. So the suite tests the shipped content, not only the code that plays it:

  • Every alphametic (SEND + MORE = MONEY) is solved from scratch and must have exactly one answer. That answer is checked against the arithmetic, not against the solver that found it, and no answer is stored anywhere.
  • The word ladder stores no solution at all. A breadth-first solver over the word list decides how many rungs a ladder has, any equally short route the player finds is accepted, and the solver is checked against route lengths from a separate implementation.
  • Every word-search grid must contain each word exactly once, counting accidental spellings by the random filler, and every generated grid is checked before it can be played.
  • Every Cryptogories theme must score as solvable rather than guessable, and every visual theme must pass a contrast gate.

Each gate carries a control case it must reject. The alphametic gate’s is TWO + TWO = FOUR, a real sum with more than one answer.

Then come the things measurement can’t see. A screenshot tool renders every page on a phone in portrait, three of them with the phone on its side, and one page across fourteen themes. Its first runs found layout defects that had passed every measurement (the 0.6.1 changelog lists four), among them a digit keypad of eleven keys laid out as a row of ten and a row of one.

How we know

The gates are tests/test_alphametic.gd (uniqueness, with its control, and the arithmetic check), tests/test_word_ladder.gd (route lengths from a separate breadth-first search), tests/test_word_search.gd with the generator’s own check in scripts/word_search.gd, tests/test_cryptogories.gd with the solvability score in scripts/substitution_cipher.gd, and tests/test_themes.gd. The screenshot tool is scripts/shoot_pages.gd, and the defects are listed in the 0.6.1 changelog entry. It was run again 12 Oct 2026 on a clean copy for this page, and rendered 25 pages: 8 in portrait, 3 on the side and 14 themes.

Build 2 of 4 · Card engine FreeCell complete · v0.1.0 Proprietary · Ephphatha Games

CartoFancy working title

A playing-card engine that knows the rules and nothing about drawing, paired with a renderer that draws and knows no rules. FreeCell is complete, and its hints and its warning that a game is lost come from a solver cross-checked against exhaustive search.

tests in 48 files, all passing, in about 10 seconds
1,005
of 4,000 seeded deals solved within the solver’s 200,000-state budget
4,000
small boards, each run with and without moves back, where every verdict agrees with exhaustive search
240
lines of tests, against 15,049 lines of application code
16,908
How these were measured
  • 1,005: npx vitest run on a fresh git-archive copy of the committed tree at HEAD, 12 Oct 2026 (package version 0.1.0, nine commits past the v0.1.0 tag), with Vite’s cache directed to a scratch folder: 48 files, 1,005 passed, 9.64 s.
  • 4,000 of 4,000: a scratch-only benchmark, not part of the project’s suite, that ran the solver on seeds 1 to 4,000 with the shipped budget: no deal ran out of budget, mean 2,482 states, worst 75,354, about 10 ms a deal.
  • 240: tests/freecell/solverTruth.test.ts, 4 board shapes × 60 boards, run once without and once with moves back.
  • 16,908 and 15,049: git ls-files over the TypeScript in tests/, and over the TypeScript and CSS in src/, piped to wc -l at HEAD.
CartoFancy’s FreeCell table on green felt: four empty free cells, the ace of clubs on the first foundation, eight cascades of cream card faces, a game bar with New deal, Restart, Undo, Redo, Hint, Statistics and Settings, and a status strip with moves, time and the deal’s seed
Fig. 3 A FreeCell opening deal from a production build of the current committed tree, which reports v0.1.0. Restart, Undo and Redo are greyed at move 0, the ace of clubs has already been played home, and the status strip carries the deal’s seed.

Case 3 of 4 · CartoFancy

A solver that has to be right before it can tell a player they’ve lost

The game says “unwinnable from here” only after a proof, so the solver’s verdict has to match a search with no shortcuts on every board in a 240-board sample.

CartoFancy checks the next new FreeCell deal in the background, and throws it away only if a full search proves it unwinnable. During play, a second background search runs after every move. When it proves that no line can win from the current position, the game says so and asks whether to play on. That is only honest if the solver’s “exhausted” really means lost, and never that one of its shortcuts pruned the win away.

The shortcuts are real. The search is best-first and capped by a node budget. It considers only canonical moves, and it folds boards that differ only in which free cell or column holds what, so each is searched once. It is also deterministic, with no clock and no randomness, so any time limit has to be expressed as a node budget.

So I made its verdict answer to a search with no shortcuts at all. On 240 small random boards, with and without moving cards back off the foundations, every verdict must match a plain exhaustive search, and every winning line must replay to a win. The test also fails unless both wins and losses actually occur in the sample. Hints, the lost-position check and the next-deal check each run the solver in a Web Worker of its own, so a long search never blocks a move.

States the solver searched, against its 200,000-state budget (shorter is cheaper). Re-run 12 Oct 2026 on a fresh copy of the committed tree.
Mean, 4,000 seeded deals2,482
Worst of the 4,00075,354
Fixed unwinnable deal, proven lost61,643
The same deal, moves back allowed88,669
Budget200,000
How we know

The search, its canonical moves, the board folding and the 200,000-state budget are in src/games/freecell/solver.ts; the cross-check is tests/freecell/solverTruth.test.ts; the proof-only rule is src/app/nextDeal.ts; the three workers are wired in src/main.ts around src/app/solverWorker.ts. The deal figures come from a scratch-only benchmark run 12 Oct 2026 on a fresh copy of the committed tree. The fixed unwinnable deal is a known-unwinnable board the game deals for one typed seed (4294967295), pinned by its own test; the solver proved it lost in about half a second.

Case 4 of 4 · CartoFancy

Architecture rules that fail a test, not a code review

The split between rules and rendering is a test: it scans every source file, enforces each layer’s imports and bans, and proves every rule against a planted violation.

The card engine is worth having because of one split: rules that know nothing about drawing, and a renderer that knows no rules. That split breaks the first time a rules file reaches for the DOM or a clock, so I made it a test rather than a convention.

The architecture suite reads every source file through the TypeScript scanner. Comments are skipped, and unicode escapes are resolved so a banned name can’t be respelled. It enforces import rules for each layer and bans the DOM, timers, clocks, Intl and Math.random from pure files. It keeps PixiJS inside the impure render files, allows colour literals in no pure file except the one colour registry, and compiles every pure file under a configuration with no DOM types. Every rule has a planted violation it must reject.

Beside it, a fuzz suite plays 100 seeded games of up to 300 random legal moves through the real session, with every state deep-frozen, so an in-place mutation throws instead of quietly corrupting the undo history. It then undoes each game to the exact deal and redoes it to the exact end.

Two review passes over the code turned up defects: nineteen in the headless core, then fourteen in the first version, by the commits’ own counts. Every fix landed with a regression test, and in the first pass each test was shown to fail against the unfixed code.

How we know

The rules and their planted breaches are in tests/architecture.test.ts; the pure compile is tsconfig.pure.json (the ES2022 library, no ambient types); the fuzz suite is tests/freecell/fuzz.test.ts, whose own control is an in-place move that must throw under the freeze. The defect counts are the two commits’ own, both dated 10 Oct 2026, and were not recounted. All 1,005 tests pass, re-run 12 Oct 2026.

Build 3 of 4 · 3D prototype Prototype · v0.1.1 Private · no licence stated

3dPuzzler

A first-person prototype drawn as a living wireframe: drafting edges computed from each mesh’s geometry over translucent fills, and any object you click materializes into a lit surface.

headless assertions in one suite, all passing
65
edges the extractor must find on a box; an eight-sided prism must give 24, and a smooth 64-sided cylinder only its two rims
12
commits, over one evening and the next morning
15
lines of first-party source: scripts, shaders and tests
1,910
How these were measured
  • 65: the shared runner on a fresh git-archive copy of the committed tree, 12 Oct 2026 (1 suite, 0 failed), with passing assertion lines counted from tests/test_main.gd’s output.
  • 12, 24 and two rims: the three edge checks in tests/test_main.gd, part of the 65; the smooth cylinder must give 128 edges, its two 64-segment rims.
  • 15 commits: git rev-list --count HEAD; git log dates them 12 Sep 2026 from 19:52 (13 commits) and the next morning (2).
  • 1,910 lines: the site’s line-count script over the committed scripts, shaders and tests at HEAD. The edge extractor is scripts/feature_edges.gd and the two-state fill is shaders/ghost.gdshader.

The lines aren’t drawn by hand. An edge extractor reads each mesh’s triangles and keeps only the creases, where two faces meet at more than a set angle, plus any open borders. It matches vertices by position rather than by index, because Godot’s primitive meshes duplicate a vertex wherever the normal changes, which would make every crease look like a border. Each object’s fill is one shader with two states, a translucent ghost and an opaque lit surface, so materializing is a tween on a single number rather than a material swap.

The extractor’s own tests pin that geometry. Walking, climbing, the grapple, slow-time focus and materializing are tested headlessly too, through the same fail-closed runner as Pocket Puzzles.

A first-person view down a long wireframe hall in white lines on deep blue: rows of translucent blocks on either side, ribbed arches overhead and a ring hanging at the far end
Fig. 4 The prototype’s one room, captured 12 Sep 2026 in the first build session, before climbing, focus and the grapple landed.

Build 4 of 4 · 2D prototype Prototype · unversioned Proprietary · Ephphatha Games

Platformer working title

A side-scrolling platformer without a single sprite file. Every character is a skeleton rig whose animation is computed from a body definition, and bodies are chosen on a bench that runs the game’s own code.

candidate bodies on the workshop bench
38
computed rig states, posed by every candidate
8
assertions in 2 headless suites, all passing
36
sprite files: the only image in the repository is the app icon
0
How these were measured
  • 38 and every pose finite: tests/test_workshop.gd on a fresh copy of the committed tree, 12 Oct 2026.
  • 8 states: the state list in scripts/rig.gd.
  • 36: the shared runner, 2 suites and 0 failed, with 26 and 10 passing assertion lines.
  • 0 sprites: git ls-files at HEAD lists no image file except icon.svg.

A body is data: a head and torso with sizes and shapes, arms and legs with lengths and widths, colours, and a few extras such as a hood, a scarf or a cape. The rig turns any body into every pose a platformer needs (idle, walk, run, skid, jump, fall, land and crouch), so a new body gets all eight animations at once. Feet are planted: each ground pose sets the hip so the lowest foot touches the floor, which is what makes a walk bounce and a crouch sink without a number for either. Because a pose is just numbers, a test can walk every body through every state without a renderer.

The character was chosen on a workshop bench, not in an image editor. The bench lines up the candidate bodies, animated by the game’s own rig on the game’s own ground and sky, pages through them, and isolates any one, enlarged, beneath a row of all its states. Its tests check that every candidate poses in every state with finite numbers, that each has its own name, and that the bench shows what it claims to.

The workshop bench: one silhouette character with glowing eyes, a white shirt, red tie and suspenders, shown eight times in a row posed idle, walk, run, skid, jump, fall, land and crouch, above an enlarged figure against layered dusk hills and trees
Fig. 5 The bench with the chosen body isolated: all eight rig states, idle to crouch, above an enlarged figure on the game’s own ground and dusk backdrop. Captured 5 Sep 2026, the day of the latest commit.

ClaudeBar, the fifth short build, lives with the framework →