Software 2026
Pokémon Typing
A Pokémon-themed 3D typing game I designed and shipped to production for my daughter after the commercial apps lost her attention - taken from concept to a self-hosted web app with AI as the primary build tool, and instrumented so a dashboard turns her practice into learning insights.
- TypeScript
- Three.js
- Vite
- Node.js
- SQLite
- Grafana
- nginx
- Docker
- Kokoro TTS
- Claude Code
My daughter was learning to type, and the commercial options weren’t landing. The drills were competent and joyless; she’d disengage within minutes. So I did what the toolset now makes possible for a parent with an evening and a clear picture of the reader: I built the thing that would actually hold her - a game where you walk down Route 1, a Pokémon steps out of the grass, and typing its name catches it.
The point of this write-up is the method: taking a concept from a written design all the way to a deployed, instrumented product, with AI as the primary engineering tool rather than an autocomplete on the side. Every line below was planned, ticketed, built, and validated through an autonomous issue-processor workflow - I set the direction and made the judgment calls; the agent did the building.
A private, non-commercial family project - so there’s no public repo or live link here. This is a case study of the process, illustrated with cropped screenshots that reveal no personal data.
The concept, written down first
Before any code, the whole thing was a plan: a junior researcher at Professor’s Pokémon Lab is sent on field assignments, and assignment one is Route 1. Four design pillars anchored every decision that followed, in priority order:
- Juice over difficulty. Every keystroke gives feedback; every catch is a celebration. Level one has no lose condition - a wrong key never punishes, it just doesn’t advance. Frustration is the enemy of a six-year-old’s practice streak, so the design engineers it out.
- The keyboard is the only controller. Once a session starts, nothing needs a mouse. Even the menus are arrow-and-enter navigable, so the game itself reinforces keyboard confidence.
- A hub that pretends to be bigger than it is. The Lab presents several research stations gated behind a trainer level, so one finished game reads as “the first unlock” rather than “the only content.”
- Everything static except the child’s profile. Sprites, cries, and spoken pronunciations are generated once by pipeline scripts and committed, so the deployed app has zero runtime dependency on any external service.
Naming the pillars up front is what let an AI agent build coherently across dozens of tickets: each one could be checked against a fixed rubric instead of my shifting mood.

From design to a production product
The architecture is deliberately small so every increment stays buildable and
verifiable. Route 1 is a real 3D scene - an auto-walking camera, instanced
grass with a wind shader, and billboarded Pokémon that pop out of the undergrowth
on a schedule - rendered with vanilla Three.js. Everything else is a DOM-and-CSS
overlay on top of the canvas, because per-letter text styling and menus are far
easier (and juicier) in HTML than in WebGL. A pure, unit-tested TypeScript engine
turns keystrokes into correct / wrong / wordComplete events and knows
nothing about rendering.

The assets that make it feel alive come from one-time pipeline scripts, run once and committed, never at runtime:
- Roster & pronunciation - each name carries a hand-tuned IPA transcription so it’s spoken correctly.
- Sprites & cries - official artwork and the real cry for every Pokémon in the roster.
- Text-to-speech - a self-hosted Kokoro model synthesises every name and every narrator line, with a per-child “buddy” voice brightened by a small pitch lift for encouragement.
- Music - theme-appropriate background tracks, loudness-normalised and loop-trimmed, with a CC0 fallback so a fresh checkout still has sound.
Production is boring on purpose: nginx serves a static dist/ as a read-only
bind mount and proxies /api to a dependency-free Node profile server (~150
lines, one JSON document per child). Profiles sync last-write-wins behind a
localStorage cache, so a Wi-Fi blip never stands between a six-year-old and Route
1 - she can play on the iPad or the desktop and pick up exactly where she left
off.
AI as the primary build tool
What makes this a portfolio piece rather than a weekend hack is how it was built. The entire project ran through a self-hosted issue-processor loop:
- Each iteration began as a written plan in my notes vault - vision, pillars, architecture trade-offs, an explicit list of alternatives considered and rejected (React Three Fiber, Prometheus, a single combined database).
- An approved plan was decomposed into dependency-linked tickets with native ordering edges, so work could only proceed in a buildable sequence.
- An autonomous agent picked up each ticket, implemented it, and had to pass the same gate every time: type-check, lint, unit tests on the pure modules, and a Playwright smoke test that plays the game end to end - load, create a researcher, enter Route 1, wait for a Pokémon, type its name key by key, catch it, and read the changed profile back out of the API over HTTP.
That smoke gate is the whole trick to letting an agent ship unsupervised: the only test that exercises the seams between the built bundle, nginx, the API proxy, and the profile store is also the one a human would run by hand, so “green” genuinely means “a child can play it.” My role was design authority and reviewer - deciding the pillars, resolving the trade-offs, approving each plan - while the agent carried the plan to working code. The product went from a blank repo to a deployed, multi-environment 3D game across seven planned iterations.
Metrics: turning practice into insight
A learning tool is only as good as your ability to see the learning. But a stats screen in the game itself would just be a scoreboard for a six-year-old to fixate on - so by design the game has no stats screen at all. Instead, every typed prompt is shipped out of the browser and answered by a real analytics pipeline:
Browser → POST /api/events → profile-api validates & writes
→ a second, separate SQLite database (WAL, idempotent ingest)
→ Grafana reads it directly
The engine already computes everything worth knowing - correct vs. total keystrokes, accuracy, active milliseconds, per-prompt speed - and used to throw it away after folding it into four rounded numbers. The insights iteration simply stopped discarding that snapshot, buffering each session client-side so a dropped connection never loses a child’s practice, and landing it in a stats database that a Grafana dashboard answers three questions from:
- Words typed per day - the durable “did we practice” signal.
- Average WPM per day, by mode - the trend line that shows real improvement.
- Fastest & slowest words - which names are easy and which need another day.
Two decisions in that pipeline are the kind of judgment an AI won’t make for you, and are worth calling out:
- Pooled WPM, not averaged. Averaging per-word speed weights a four-letter “Abra” the same as “Charmander” and drifts from what the player actually felt. The dashboard pools the same way the game does - sum the correct keystrokes, sum the active clock, divide once - so the number on the dashboard always matches the number on the results screen.
- “Each day” means the child’s day. A 9pm session is already tomorrow in UTC, which would smear an evening’s practice across two calendar days. The browser - the only party that actually knows the timezone - stamps each row with a local calendar date, so daily panels never lie.
The result is a small, honest feedback loop: she plays because catching Pokémon is fun, and I get a quiet, private view of exactly which letters are getting faster - the whole reason the commercial apps couldn’t hold her turned into something they never offered.