Twist: teaching the cube without a solver
There is a well-known way to build a Rubik's Cube app, and I did not build it. The usual approach imports a solver — Kociemba, two-phase, something that takes a scramble and returns a twenty-move solution — and wraps it in an interface. It works, it is impressive, and it teaches nothing. Nobody solves a cube in twenty moves. People solve a cube by learning a method.
Twist is the other thing: a tutorial for the methods people actually learn. It starts with the beginner method and then branches into CFOP, Roux, or ZZ — 276 algorithms across 29 lessons, each demonstrated on a live 3D cube you can rotate mid-explanation, plus a trainer that drills recognition and execution and weights practice toward whatever you keep missing.
Taking the solver out changes where correctness has to come from, and that turned out to be the interesting part of the build.
The state is the cube, not a string
Most cube engines store the puzzle as a permutation array or a facelet string, then apply moves as index shuffles. That is fast and compact, and it is also a second model of the cube that has to be kept in sync with the one on screen.
Twist is cubie-based instead. There are twenty-six little cubes; each one owns a position and a quaternion; and that is the state. A move is a small record — {axis, layers, quarterTurns} — applied to whichever cubies currently sit in the affected layers. Rendering does not translate from a model, it reads the model.
The payoff shows up in a distinction the app needs constantly. isSolved asks whether every cubie is back in its home seat. isColorSolved asks the weaker question — does it look solved — which is what you actually need after a last-layer algorithm that ends with the cube visually complete but rotated in space. With cubie state, both are cheap and obviously correct. With a facelet string, the second one is easy and the first one is a re-derivation.
Every algorithm has to prove itself
Without a solver, the algorithm data is the product. 276 hand-entered move sequences, transcribed from published sources, any one of which could carry a typo that would quietly teach someone the wrong thing.
So every case carries its own setup, and the test suite asserts a round trip: apply the setup to a solved cube, apply the algorithm the lesson teaches, and the cube must be solved again. That single property catches almost everything that can go wrong with transcription — a swapped letter, a missing prime, an inverted trigger — because a broken sequence lands somewhere other than solved.
It also caught something I did not expect it to catch.
While adding Roux's EOLR set — 47 cases for orienting and placing the last-layer edges — I pulled a second, alternate algorithm for case 41 from a published list. The test failed it. My first assumption was that I had mistyped it, so I re-verified the sequence character for character against the source. It was transcribed correctly. The algorithm was simply not valid for the case as modeled here: it is not corner-neutral, and it permanently displaces four U-layer corners rather than returning them home. As a piece of a longer solve, in a context where those corners get fixed later, it is fine. As the independent solution to a pure-edge case, it cannot be right.
Case 41 ships with only its verified primary algorithm. The lesson I took is narrower and more useful than "check your typing": a site listing an algorithm is not evidence that the algorithm is independently valid for the case you are teaching. The round-trip test is what actually knows, and it is the only reason I found out.
Teach the method people actually teach
The strongest pull in a project like this is toward symmetry. CFOP has a fixed 41-case F2L table, so ZZ should have a fixed ZZF2L table, and building it is just more data entry. I nearly did it.
What stopped it was checking how ZZ is really taught. ZZF2L is not memorized as a table — not by beginners, and by community consensus not by advanced solvers either, a position AlgDb.net takes about its own listing. It is solved intuitively, with the edge orientation from EOLine making it easier than CFOP's version rather than harder. A 200-case table would have been a large, confident, useless artifact.
The architecture already had room for the honest answer. A lesson step with no demo cases just renders its prose, so intuitive steps — CFOP's cross, all of Roux's block building, ZZ's EOLine and F2L — are taught as decision-making with a handful of illustrative worked examples, not as tables to grind. The rule I ended up writing down for myself: check how the method is actually taught before assuming it needs a case table. Sometimes the correct output is less content.
The bug that never fired
Trainer cases can highlight the stickers you are supposed to look at. The engine names a corner by a canonical letter order — up/down face first, then right/left, then front/back — so the corner at the up-right-front intersection is URF.
Every three-letter corner entry in CFOP's highlight data had been authored as UFR. Same corner, wrong order, and the lookup matched nothing. Corner recognition highlights had never once lit up, for any case, since the feature shipped. Nothing crashed, no test failed, and the feature degraded into silence — the interface just quietly stopped pointing at anything. Roux and ZZ, authored later against the engine's convention, were correct from the start.
I only found it while reading the data for an unrelated reason. Silent no-ops are the failure mode I now go looking for deliberately: the ones where the code runs, returns nothing, and nobody is told.
The refactor I didn't do
The main bundle was too big, and there was a plausible story for why: a tangled data layer, where importing anything about a method dragged in every algorithm for every method. The ticket assumed a data-layer refactor.
That story was wrong. The lightweight catalog was already separate, and every method's definition already depended only on its own id list. Exactly one page — the trainer's home page — was eagerly loaded and imported the heavy case-data barrel, and it did so to look up three case names for three "needs work" cards. Making that page lazy, matching every other secondary route, dropped the main bundle from 357KB to 305KB raw (112KB to 97KB gzipped). No data-layer changes at all.
The refactor I would have spent a day on would have "fixed" a problem that was one import statement. Worth confirming the root cause before accepting the one a ticket hands you.
Open it
Twist runs entirely in your browser. No accounts, no backend, no telemetry; progress lives in local storage. Start with the beginner method if you have never solved one — it is the whole path from a scrambled cube to a solved one, and the 3D cube demonstrates every move as you read.
