The Problem
I built Mathemphetamine as a multiple-choice quiz back in March: six topics, 150 AI-generated questions, instant feedback, an explanation on every answer. I wrote about the design decisions at the time and stood behind every one of them.
Then I never really opened it again.
Months later I found myself going back through other tools I'd built in the past, after massively changing lolevel and creating reflex, a typing-drill trainer for syntax fluency across C, Python, JavaScript, SQL, and assembly, touching them up, fixing rough edges, adding things I'd been meaning to add. Mathemphetamine came up in that same pass. Coming back to it after all that time away made it obvious why I'd never touched it since shipping it: the format itself wasn't something I wanted to sit down and use. It wasn't that I'd gotten busy; the design just never gave me a reason to come back.
Diagnosing the Gap
Multiple choice was just boring. Not boring in some subtler design sense; picking one of four options gave me nothing to actually enjoy while doing it, and if there's nothing enjoyable in the moment there's no reason to open the tool again tomorrow. What pulls me back into a drill tool is the physical act of typing itself: fingers on keys, an answer taking shape under my hands instead of getting picked with a click. That sensation is the engagement. The learning happens alongside it, as a side effect of enjoying the process, not as the reason I sit down in the first place.
reflex works for exactly this reason. Every drill is something typed, never something picked, so there's always something to actually produce. Multiple choice can't offer that no matter how well the questions are written, because the moment the interaction is choosing instead of typing, there's nothing physical left to hook into.
The fix wasn't better questions. It was a different verb. Not "pick the right one," but "type it, and let something exact tell you if you're right."
The Rearchitecture
I gutted the quiz engine and rebuilt around procedural generation instead of static question banks. Every topic module now exposes a shared interface:
{
id, name,
drillType: "formula" | "single" | "sequence",
generate: () => instance, // fresh procedural problem
checkStep: (instance, stepIndex, typed) => ({ valid, correctAnswer, explanation }),
isComplete: (instance, stepIndex) => bool,
}
generate builds a new instance on the spot: new variable names, new graphs, new numbers. checkStep verifies your typed answer against a real checker, not a stored string:
| Topic | You type… | How it's checked |
|---|---|---|
| logic | an equivalent formula | exhaustive truth table (boolexpr.js) |
| sets | an equivalent set expression | membership truth table over A, B, C |
| graphs | a BFS/DFS order or degree sequence | reference traversal on the instance |
| combinatorics | a counting expression (C(10,3)*2!) | evaluated vs. an independent recount |
| number theory | each Euclidean-algorithm line | real modular arithmetic, line by line |
Finite discrete problems are exact to enumerate, so there's no need for a curated bank of right/wrong pairs; the checker is the ground truth, and the deck of possible instances is effectively endless. questions-.js (nine files, one per topic, all fixed arrays) got deleted and replaced by topic-.js generators plus boolexpr.js, a from-scratch boolean/set parser that does the truth-table equivalence checking. That one commit, rearchitected mathemphetamine v2, deleted about 2,500 lines of AI-generated questions and added about 1,600 lines of generator/checker code. Net smaller, and it doesn't run out.
Typing math notation without LaTeX is its own problem, so symbols.js does ASCII-to-glyph substitution live as you type: p AND q becomes p ∧ q, (A ∪ B)comp becomes (A ∪ B)ᶜ. There's also a click-to-insert symbol palette for anyone who doesn't want to memorize triggers, plus (added a day later) prefix autocomplete: type three letters of a trigger and hit space to complete the shortest match.
The Overcorrection
My first pass at replacing the quiz went further than it needed to. I built a three-stage "learning ladder" per sub-skill: walkthrough, then fading completion, then independent recall, gated behind mastery thresholds. Three walkthroughs to unlock stage two, a clean fade level to unlock stage three, a correct streak to unlock deeper hiding levels. It was a reasonable-sounding idea, borrowed loosely from spaced-repetition apps, and I shipped it as "better easy mode."
It lasted about an hour of actual use before I ripped it back out (no more auto advancing and tool tips). The gating was friction with a gamification costume on. I wanted to jump straight to recall on a topic I already knew, and the ladder wouldn't let me; exactly the kind of obstacle that had made the original quiz something I never opened. Locking content behind auto-promotion doesn't make a tool feel more rewarding to open; it makes it feel like homework with extra steps.
What replaced it is a plain mode selector (trace, fill-in, recall, and later code) that persists your last choice and never locks anything:
Pick what to drill with the mode selector; your choice persists and applies to every drill until you change it. The selector is a menu, not a progression system: all four modes are always available, nothing is locked, and nothing ever auto-promotes you.
Same underlying content, same generator/checker pipeline, no gate. That one reversal is the clearest "easier to get started" change I made: removing a system I'd just built, because using it revealed that the thing it was optimizing for, perceived structure, wasn't what the tool actually needed.
Code-It: Writing the Actual Function
The other big addition is a fourth mode that has nothing to do with retyping notation. Code-It gives you a spec in plain English (gcd(a, b), is_transitive(relation), bfs(graph, start)) and you write a real Python function for it. It runs client-side via Pyodide in a Web Worker against 20 to 50 freshly generated test instances, judged by the same JS generator/checker logic that verifies the notation drills, so the reference implementation is never duplicated in Python.
After a pass, a "type it again" drill has you reproduce your own solution from memory, timed, with a lifetime rep counter; the same trace/fade/recall philosophy applied to code instead of notation. It carries reflex's progressive-masking idea directly: ◯ show → ◐ syntax → ◑ library → ● blank, hiding keywords, then built-ins, then the whole function. The one implementation detail that took a second pass to get right: when a reference solution is showing and you're typing through it, Enter now follows the model's own indentation for the next line, including dedents, instead of making you fight the editor to get out of a nested block by hand.
Writing the general algorithm is a strictly deeper test than reproducing one instance's steps, which is part of why this mode is the most engaging one to actually sit down and use. A handful of sub-skills that don't map cleanly to a function shape (Big-O classification, double-complement identities) are deliberately left out of Code-It rather than force-fit; logic and boolean algebra get truth-table challenges (eval_expr, is_tautology) instead, since a single rewrite identity isn't an algorithm.
Why This Approach Works
Typing an answer and getting it checked exactly is a different act from picking the least-wrong-looking option out of four. It's also the one I actually enjoy doing, which is what makes it possible to sit down and do reps in the first place instead of avoiding the tool. The quiz version's explanations were real value; I said as much the first time. But an explanation attached to a guess among four options is a smaller learning event than an explanation attached to something you typed and got checked character-by-character or line-by-line against real logic.
The generator-instead-of-bank design has a bonus effect too: there's no finite bank to run out of, so there's no "I've seen this one before" moment either. A fresh instance is one generate() call away, every time.
And the reversal on gating taught me something about my own tools specifically: a drill trainer's job is to lower the activation energy to open it and do five minutes of reps. Anything that adds a decision, a lock, or a "not yet" between opening the app and typing an answer is working against that job, no matter how principled it sounds on paper.
Current State
All of this (the full rearchitecture, the ladder, the reversal, autocomplete, formula mode, Code-It) landed over two days. Eight topics are live: logic, boolean algebra, sets, relations, graphs, combinatorics, number theory, and algorithms. ladder.js persists your mode, scaffold level, and per-sub-skill × mode rep counts in localStorage; nothing in that state is used to gate anything anymore, only to show progress.
Code-It's Python specs (codeit-specs.js) are the newest and least battle-tested part of the codebase; I'm still finding sub-skills whose test-instance generation needs tightening, the same way question quality was the ongoing work in the quiz version.
Lessons
A tool you never open again is telling you something. I didn't drift away from Mathemphetamine gradually. I shipped it and never came back to it, and that in itself was the signal. It just took a maintenance pass through my older tools, months later, to actually sit with why.
The mechanic has to be something I enjoy doing, not just something that teaches well. Multiple choice can be pedagogically fine and still be a tool I never open, because picking an option doesn't give me anything to enjoy in the moment. Typing does. That enjoyment is what gets me to open the tool; the learning rides along as a side effect.
Ripping out your own system is sometimes the fix, not a failure. The gated learning ladder was a coherent idea, implemented cleanly, and wrong for this tool. Building it wasn't wasted work; I couldn't have known it added friction instead of structure until I used it. Reversing it an hour later was the right call, not a rollback to be embarrassed about.
Reuse the checker, never the answer key. Every mode (trace, fill-in, recall, and now Code-It's Python grading) routes through the same generate/checkStep pair per topic. There is exactly one place that knows what's correct, and every surface that asks "is this right?" asks that place.
Why I'm Documenting This
The first Mathemphetamine post ended with a hope that it would compound the same way typesec had. It didn't; not because the topic was wrong, but because the mechanic was wrong, and a tool with the wrong mechanic doesn't get opened again once it's shipped. This rewrite didn't come from noticing that gap right away. It came months later, while I was going back through other old tools to touch them up, and found this one needed the same treatment.