K53 Drill Master
A learner licence app that works with the plane in the air
Offline-first drills for the South African learner licence test, built to run on a budget Android phone with no signal and no patience.
- 129 commits
- 2026-02-27 → 2026-07-29
- JavaScript 92.7%
The audience is specific and the constraints follow from it: a 360-pixel screen, an entry-level Android, data that costs real money, and a test that is failed far more often than it is passed. Every kilobyte is a decision, and every dependency has to earn the download.
It is also the build where the correctness of the content is a safety question. A road sign that is nearly right teaches the wrong pattern to somebody who is about to drive.
5 decisions
What the constraint forced.
Each of these is a choice with a reason attached. The reason is the part worth reading — a stack list tells you what was used, and nothing at all about the judgment that put it there.
- 01
No router. No state manager. No CSS framework.
Routing is a state string that swaps which game component is mounted. State is props plus localStorage. Styling is a theme object passed down. Each of those is a library that was considered and declined with a reason: fifty kilobytes for navigation on a single-page app, a global store for a codebase five people will ever touch, a build step for control over every pixel at 360 wide. The discipline is not minimalism for its own sake — it is that on this device the download is the user experience.
// the entire routing layer const [activeGame, setActiveGame] = useState(null); if (activeGame === 'gauntlet') return <Gauntlet onBack={() => setActiveGame(null)} />; - 02
Synthesise the sound instead of shipping it.
Every tone is generated with the Web Audio API — no audio files, so no requests, no cache entries, and it works offline from the first launch. The correct-answer sound is a rising two-note sine wave with a tuned attack and decay, because a reward sound is heard fifty times in a session and the difference between rewarding and irritating is about thirty minutes of tuning. Note the detail that matters more than the waveform: the audio context is resumed on every call, because iOS and Chrome suspend it and a silently dead sound on first tap reads as a broken app.
osc.frequency.setValueAtTime(660, now); osc.frequency.setValueAtTime(880, now + 0.08); gain.gain.linearRampToValueAtTime(0.18, now + 0.01); // attack gain.gain.exponentialRampToValueAtTime(0.001, now + 0.28); // decay - 03
Get the signs out of the official manual, not out of a drawing tool.
The first version used hand-drawn inline SVGs — zero requests, crisp at any density, and wrong. South African road signs are legally specified, and a Give Way that is slightly off shape teaches the wrong recognition. The pivot was to extract the real artwork from the official learner manual PDF, using the renderer's paint-operation count to tell actual signs from decorative page furniture, then serving them as small on-demand JPEGs cached by the service worker after first view.
Page 3 → images 10– 16 : Stop (R1.1), Yield (R2) Page 4 → images 17– 25 : No Entry, speed limits Page 22 → images 272–285 : warning — crossroads, junctions Page 26 → images 329–343 : pedestrian & traffic warnings - 04
Let the browser find the corrupt files.
Some extracted images came out damaged. Rather than audit several hundred files by hand, each image carries an `onError` that hides it and falls back to a drawn shape. Runtime detection of a known-possible failure is cheaper than an exhaustive up-front audit, and it stays correct when the asset set changes.
- 05
Count to three, then five, then five again.
Streak encouragement fires at three, five and seven, then every five after that. Three is achievable enough to start the loop, seven feels like momentum, and past that the interval widens so the reward does not become wallpaper. Reward schedules are engineering; picking the numbers by feel and then never revisiting them is how an app becomes annoying.
The part that survives the build.
A dependency is a download before it is an abstraction.
The usual calculus — a library costs a little size and saves a lot of time — inverts when the user is on a metered connection and a mid-range handset. Both directions are engineering. Only one of them is a default.
Animate the number the way a person counts it.
A linear count-up reads as mechanical. Ease-out-cubic reaches most of the final value quickly and then settles, which is how counting actually feels, and tabular figures stop the layout jumping as digit widths change. Two small choices that are the entire difference between a score that feels designed and one that feels rendered.
When the content has a legal source, the content is a dependency.
Treating the official manual as the upstream — with a documented extraction procedure — turns "are these signs right?" from a matter of memory into a matter of re-running a script. Several later commits correct individual sign codes against the official chart. That is only possible because there is an official chart in the pipeline.
Built with
- React + Vite
- JavaScript
- Web Audio API
- Service worker / PWA
- Framer Motion
- Supabase
- PayFast
Measured
- commits
- 129
- last commit
- 2026-07-29
- journal sections
- 56
Written from K53 Drill Master build journal — dossier, 60 sections. The dossier is composed from the journal rather than quoted out of it — the raw sections stay behind the review gate described in docs/THE_FORGE.md §4, and every figure above is read from the GitHub API at build time.
