The toilet minigame is finished, verified, and nobody has played it
The pub in this game has toilets. You can use them, and using them makes a mess: water on the floor, marks in the bowl, and the mess persists between visits. The obvious next thing was a job — the landlady offers you cleaning work, you clock on, you clean up the mess the minigame made, and she pays you based on how much of it you actually shifted.

The design line I kept coming back to is one sentence: the minigame makes the mess, the job cleans it up, and they share one state model. No second bookkeeping layer. If a pan is dirty it is dirty for both systems, and mopping a puddle is the exact arithmetical inverse of the miss that created it.
That is now built. All eight phases are done, it has been driven in a live in-editor play session, and the numbers came out exactly where the maths said they would. It has also never been played by a human being, which is the more interesting half of this post.
Why it mattered
This is a solo Blueprint-only project — no C++ — and jobs are the spine of it. A job is the first thing a system-heavy game can offer a player that is not a cutscene. It needs a supervisor, a score, a wage, a way to be sacked, and a mess generator that keeps producing work while you are on shift. Getting one job right means the shape is reusable.
It also stresses everything at once: persistent state on the game instance, per-actor state on four separate toilets, materials driven at runtime, particle effects, dialogue with a character who has to hire you and later inspect your work, a heads-up display strip, and a CCTV supervision model that decides which tasks are worth more because she can see them.
The tools and the method
Everything is authored by driving the live editor through two MCP servers — one for Blueprint graphs, assets and actors, one reaching particle systems, widgets, animation, audio and editor Python. Blueprint functions get written as a text DSL and compiled; where a graph already contains something the writer cannot express, the edit is done as node surgery instead — splice one exec wire, verify the node count moved by exactly one.
Meshes came out of Blender through generator scripts that build into whatever file is open and clean up after themselves, so the same run works inside somebody else's work-in-progress scene. Six new props: mop, bucket, cloth, wet-floor sign, a printed "cleaner wanted" card, and a CCTV camera housing.
The scoring turned into the single best decision in the build. Rather than crediting each cleaning verb as it happens — six verbs, six call sites to forget — the score is:
numerator = (work outstanding at clock-on) − (work outstanding right now)
A background sample re-runs the whole survey twice a second and subtracts. That scores all six task types at once with no per-verb plumbing, and it composes with the mess director: when new mess appears mid-shift it raises the total too, so a long shift is not automatically a bad one, but ignoring what appears is.
The trap: every proximity check was permanently open
Search-box version of the symptom: my Blueprint distance check always passes. The gate is always open. Compiles clean, validates clean, GetHorizontalDistanceTo returns 0.
I had written, in eight separate functions in one day, the equivalent of "is this actor near me?" as a call to the engine's horizontal-distance helper with the loop element passed in as the single argument. It reads perfectly. It is completely wrong.
That helper is a member function with a hidden self pin. Passing one positional argument binds it to self — not to the other actor. The OtherActor pin was left connected to nothing. So it measured that actor's distance to a null reference, returned 0.0, and 0.0 < any reach is always true.
The mechanism behind the mechanism is the part worth keeping. I had copied the form out of a readback of an existing working function. The readback renders keyword arguments as positional. Round-tripping it therefore silently rebinds the first argument to the hidden self pin. It is documented in my own notes as a prime rule, and I walked into it anyway, because the source I copied from was a graph that works.
Consequences: polishing glasses, picking up the cloth, collecting empties, pocketing dropped cash, wringing the mop and refilling the bucket all worked from anywhere in the level, and the mess director believed every customer and every dust patch in the entire game world was inside the pub.
It compiled with zero errors and the Blueprint validator was silent. There is no disconnected node — the graph is complete. It is just wrong.
The fix is to stop being clever about it and do the subtraction by hand: take both actors' locations explicitly, subtract, take the horizontal length, compare. Both calls in that form bind correctly and there is no second pin to forget.
How it was caught
Not by the compiler, and not by playing it. Before the first play-in-editor cycle I ran an adversarial audit: five reviewers each took a subsystem and read the live graphs rather than the design, then every top finding was handed to a separate reviewer whose only job was to refute it. Eight defects confirmed, two refuted, six of the eight critical. Seven were fixed the same day.
Every single one of them compiled clean and passed validation. Read that as the standing argument for auditing before a play cycle rather than after.
The refutations were as useful as the confirmations. One reviewer claimed a variable was never written anywhere, having searched the compiled asset files for its name as text. Blueprint writes do not appear as source text, and a variable's name appears in its defining asset whether or not anything ever writes it. That was a method failure, not a misreading — worth knowing before you trust a grep over binary assets.
Other confirmed defects from the same sweep, all in the same family of "looks right, measures nothing":
- An empty pub scored 100% for doing nothing. Clock-on stored the outstanding work with
a floor of 1.0 applied; the sampler subtracted the raw unclamped figure. On a nearly clean map that starts the numerator above zero. A first smoke test would have reported total success — full marks, a bonus, a "spotless, that" verdict — while measuring absolutely nothing.
- The score numerator was never written by anything. Every shift therefore scored zero,
every shift took a strike, and two strikes is a sacking. You were fired after exactly two shifts no matter how well you cleaned.
- The counters counted presence, not dirtiness. Mopping a puddle scales it to nothing but
the actor survives; sweeping dust lowers a value but the patch survives. So the outstanding work total could never fall. All six counters are now weighted by how dirty the thing actually is, which is only possible because the weighting helper is a pure function and can nest inside the accumulator.
- The mop's credit died mid-shift. The volume measurement was reading a pool that only
ever got one positive write, so after roughly seventy strokes it read zero and printed "nothing to mop just there" on strokes that visibly removed water.
- The decline option soft-locked the dialogue. Turning the job down never reached the
phase that closes the panel.
- Slipping on a wet floor fought the sprint key. The slip wrote a saved speed value while
the sprint input wrote the movement component directly, so tapping sprint mid-slip cancelled the freeze and releasing it afterwards left you sprinting permanently.
And separately, the one that nearly shipped as "the puddles just don't work": a stored placement file describing where the toilets are was stale — rotated ninety degrees and offset from the live level. The floor material built from it evaluated its wet field about twenty-five metres away from the actual toilets. A material cannot raise an error for being evaluated in the wrong place. It compiled, it rendered, and it passed every wiring assertion. The rule that came out of it: derive building-local constants from the live level, never from a stored coordinate file, and pair every coordinate-keyed asset with a verifier that checks it against the level.
What the verification actually showed
The play-in-editor run placed the kit, clocked on, and measured. Eleven of twelve automated checks passed, and the twelfth failure was a flaw in the check rather than the system — the editor's Blueprint-class loader returns nothing at all during play, so the obvious "find all actors of this class" call yields an empty list and confidently reports zero for actors that are demonstrably standing in the world. My own verifier had that bug and produced five false failures.
The genuinely convincing results:
- The persistence layer replayed saved state on start-up: three of three pub toilets came up
already soiled, at 0.57, 0.46 and 0.35. That is the cheapest possible proof that the whole save-and-restore path works.
- Clock-on produced an outstanding-work total of 26.93, and every component of it was verified
by hand against what was in the room — the toilets contributing by soil level rather than a flat rate, litter split into watched and unwatched at double weight, glassware weighted by dullness, carpet by dust.
- The numerator read 0.0 at clock-on, confirming the phantom-progress defect was properly fixed.
- Sweeping four dust patches moved the numerator from 0.000 to 1.400 — exactly the
predicted four times 0.35.
Play was stopped immediately afterwards and none of the test mutations persisted.
The honest part
Nobody has played it. Every number above is measured, not played. No human has picked up the mop, walked to the gents, mopped a real puddle, walked the landlady's hiring and clock-off dialogue, watched the on-screen shift strip, slipped over, pocketed a dropped note while on camera, or been sacked. The verbs, the dialogue, the display, the slip and the mess director are all wired and none of them has been exercised by hand. The carry pose for the mop has a follow-strength dial that has never been touched and might well be the wrong sign.
Three defects are deliberately still open. The most interesting is a design call rather than a bug: the save writes a per-toilet floor-water volume that the load never reads back, so the toilets come up soiled but the seeded puddles do not appear — the play test recorded water at zero on all three. Restoring it and deleting it are both defensible, and guessing at a design decision to close a ticket is worse than leaving the ticket open.
The next action is a human playtest. It is the only thing left that produces information nothing else can.
What to take from it
- A helper with both a
selfpin and an "other" pin is never safe called positionally.
Either name both arguments explicitly or do the vector subtraction yourself. Check the pins, never the readback.
- A readback of a working graph is not a template. It renders named arguments positionally
and re-binds them on the way back in. Copying from something that works is exactly how a silent rebind spreads to eight functions in a day.
- "Compiles clean" and "the validator is silent" are not evidence of correctness. Both
answer "is this graph complete", not "is this graph right". Node counts per graph are the real test for an orphaned chain — the validator reports only the head of one.
- Audit before the test cycle, not after. Adversarial review, with a separate reviewer paid
to refute each finding, caught six critical defects that nothing else in the project would have caught. Two findings were correctly refuted, which is what makes the other eight worth believing.
- Score by measuring the world, not by crediting the verbs. "Work at start minus work now"
scored six task types with zero call sites to forget, and composed for free with a system that adds new work mid-shift.
- Distinguish "it has run" from "it has been played", in writing. A handover document in
this project carried a capitalised "none of this has ever run" warning directly above a table saying it had run and been verified. Both halves cannot be true; the warning was the stale one and it sat there for a day. Measured and played are different claims and each deserves its own sentence.