---
title: "An on-device interactive fiction engine: 38 decisions, 1,010 tests, and why it&#8217;s parked"
url: "https://toddpaulbrownjr.com/writing/inside-the-interactive-fiction-engine/"
author: "Todd Paul Brown Jr."
description: "** A parked on-device interactive fiction engine: 1,010 tests, 38 decisions, zero network calls, and what I learned building the harness that could refuse…"
kind: "article"
updated: "2026-09-26T02:50:34+00:00"
---

# An on-device interactive fiction engine: 38 decisions, 1,010 tests, and why it&#8217;s parked

I built an on-device interactive fiction engine: a small language model running entirely on a phone, wrapped in a TypeScript harness that owns canon state, pacing, and branching logic. The model writes prose and dialogue. The code decides what happens.

58 commits landed across 6 active calendar days in late August and early September 2026. The project runs on a phase-gated PRD (3,020 lines, nine phases), a 38-entry numbered decision log, 19 distinct scored evals with numeric thresholds, and a replay mode that reruns guardrail checks against recorded model responses without burning GPU. The test count reached **1,010 tests green** — the highest figure I've recorded for any project.

Status: parked. Phases 1 through 8 of the nine-phase roadmap had all passed their exit gates on 2026-08-30, and Phase 9 ("Hardening") was nearly complete except for checks that needed a real phone. The last commit was 2026-09-11.
## Why I built it

I wanted to prove the harness mattered more than the model. A small on-device model can't write beautiful prose on its own — so if the engine worked, it would be because the architecture around it made up the difference. The harness defines what the model is allowed to say and when; the model fills in the words.

Zero network calls. Everything runs locally. The app stores state, the engine validates every passage against its own rules, and the model never sees anything it shouldn't.

## How it works

The engine is deterministic TypeScript. It holds the game's state (where you are, what you've done, what you know), presents the model with a tightly scoped prompt for each passage, validates the response against code-defined boundaries, and only accepts it if it covers the beats the design requires and stays within the knowledge fence for that moment.

Every passage has a checklist. The model writes prose that must hit those beats. If it leaks information the player shouldn't have yet, or skips a required story point, the response is rejected before it reaches the screen.

The model's sampler settings — temperature, top-p, everything — are set explicitly on every call. That explicitness came the hard way.

## What broke and how I fixed it

**Evals validated a configuration nobody was running.** Up to six shipped Android builds ran different sampler settings than every eval battery that had supposedly validated them. The phone runtime silently filled in its own defaults wherever the engine didn't set a field explicitly. I found it because the app on my phone never matched the behavior the eval batteries recorded.

The fix was decision D-038: the engine now sets every sampler field explicitly on every call. Build 7 was the first build whose sampling actually matched the eval batteries.

**A self-reported metric that measured nothing.** One eval compared the model's own claim about which story beats it had covered against a separate, code-computed definition of the same thing. Agreement came back at chance level — kappa approximately zero. The model's answer correlated with its position in the response, not with what it actually covered. I deleted the self-reported field and replaced it outright with the code-computed definition.

**73 accessibility violations on first run.** I ran an accessibility audit across 28 screens on 2026-08-30. It found 73 violations. All 73 were fixed the same day.

## The overnight run

An 11.43-hour gap separates one evening commit from a cluster of four morning commits, self-labeled "morning session, unattended." In that window, three engine systems shipped — presence, sensing/attention, and recall — and the project's two hardest zero-violation gates were re-run before any of it could merge.

Those gates: zero knowledge-isolation leaks across 18 scenarios, checked in both directions, and zero locked-development violations (the model must never write story content the design has deliberately left unwritten yet). Both passed.

What made the run trustworthy wasn't the length of time I was asleep. It was the phase-gated PRD, the decision log, the scored probes, and the replay mode that already existed and could refuse a bad result.

## The numbers

- **1,010 deterministic tests green** (measured; the highest recorded figure across all my projects).
- **58 commits, 6 active days** (measured, 2026-08-30 to 2026-09-11).
- **38 formal numbered decisions** (D-001 through D-038), each with the options I didn't take written down.
- **19 distinct evals and probes**, each with a numeric threshold it had to pass.
- **73 accessibility violations found, 73 fixed**, same day (2026-08-30).
- **Zero network calls**, proven by the eval suite and verified on a real phone.

Every decision in the log names what I chose and what I ruled out. Decision D-015, for example: three candidate architectures for passage validation, scored on six criteria, with the winner and the two losers both documented. The log exists so a future session — or a future person — can see not just what I built, but what I decided not to build and why.

## What's next

It's parked, not abandoned. The harness works. The gates work. The architecture proved itself. If a better model ships — one that writes prose at the quality the design needs without burning the phone's battery — the engine is ready.

In the meantime, the techniques transferred. The phase-gated PRD structure, the numbered decision log, the replay mode that validates without re-running inference — all of those patterns now live in other projects. This one taught me how to build them.
