wanessalabs games production

2x2 Cube Trainer

A 2x2 puzzle that knows the shortest solution from every position, and tells you how much longer yours was.

ReactTypeScriptthree.jsReact Three FiberWeb WorkersViteCloudflare Pages

Every cube app I have used will happily solve the puzzle for you. Almost none of them will tell you that you took 23 moves when 9 was possible.

What It Is

A 2x2 speedcube in the browser. Scramble it, turn it by dragging the faces, and when you finish it tells you two numbers: how many moves you used, and how many were actually necessary.

It also knows how far you are from solved at any moment mid-solve, which is what makes the hints work.

The Number That Made It Worth Building

A 2x2 has 3,674,160 possible positions.

That sounds like a lot. It is nothing. A browser tab can visit every single one of them in about a third of a second and remember exactly how far each is from solved. The whole table fits in 3.5 megabytes.

So I built that. On load, a background thread walks the entire puzzle, and from then on “how far am I from solved” is a single array lookup. Fifty nanoseconds.

Everything interesting in the app is downstream of that one decision:

A 3x3 cannot do any of this. Its position count is roughly ten million times larger. This app exists because a 2x2 is small, and I am not going to pretend otherwise by promising a 3x3 later.

God’s Number, Which I Did Not Look Up

The largest number of moves any 2x2 position needs is 11. I knew that going in, because it is a well-known result.

I made the app compute it anyway, and it came out at 11.

That is not a wasted afternoon. The computation walks every position through every move table, so the histogram it produces is a fingerprint of the entire engine. It now sits in the test suite as a list of expected numbers. If I ever break a move table, one of the twelve counts changes and the whole thing goes red.

I would rather a test told me my cube maths was wrong than a player.

The Part I Got Wrong Three Times

three.js is about 850 kilobytes. Twenty-six other games on the hub do not use it, and I did not want those players downloading it for nothing.

So I made it load only when someone opens the cube. Easy.

Then I looked at what the site actually did, and the offline cache was quietly downloading it anyway, for everyone, in the background.

Fixing that took three attempts. The first two both looked correct in the config and produced correct-looking builds. The one that finally exposed the truth was recording every network request on the home page and reading who asked for it. It was the main bundle. A one-line optimisation I had added to name the file had silently turned an on-demand load into an always load.

The lesson I keep relearning: you cannot tell what a bundler did by reading its settings. Count the requests.

The Bug That Passed Every Test

At one point every turn was applied twice. Click “turn the right face” once and the move log said R R.

The puzzle logic was correct. The solver was correct. The 3D maths was correct. Over a thousand tests passed. The bug was three lines of React state code that nobody had ever tested, because it was glue and glue feels too boring to test.

I only found it by opening the actual page and clicking the actual button.

The test I wrote afterwards has to run in React’s strict mode to catch it. Written any other way, it passes against the broken version too.

Colour, Because It Is The Only Information A Cube Has

You cannot solve a cube by shape. Colour is the entire signal, which makes it an accessibility problem with nowhere to hide.

So the six sticker colours are chosen so that the cube is still solvable with no colour vision at all. Convert the whole thing to greyscale and every face is still a different shade. That covers every form of colour blindness at once, rather than patching for one.

My first attempt at this was wrong, and I only know because I wrote the claim as a test instead of a comment. Two colours I had spaced carefully by lightness came out nearly identical in greyscale, in the wrong order, because perceptual lightness and screen brightness are not the same thing. A comment would have sat there being false for years.

It Also Turns Into A Head

The cube’s shape is a plug-in. The puzzle logic does not know or care what a piece looks like.

To prove that, there is a second option in the settings that turns the cube into a lumpy animal head with a snout and ears, generated from scratch in code. Scramble it and the head comes apart properly: the snout ends up on top, the ears point down.

Adding it changed nothing about how the puzzle works. No new logic, no special cases, no branching on which shape is active. That was the whole point, and it is the part I am most pleased with, because it is the part nobody will notice.

What It Does Not Do

No accounts. No leaderboards. No backend. No ads. No tracking. Nothing leaves your device, and everything works on a plane.

No 3x3, for the reason above.

No Bluetooth smart cube support, which I want and did not build.

Development timeline

17 logged updates over 2 days, 28 Jul 2026 to 29 Jul 2026.

  1. Phase 4a: renderer dependencies, measuredfeature

    Added three@0.185.1, @react-three/fiber@9.6.1, @types/three@0.185.1.

  2. Phase 4b: the geometry seamfeature

    Three source files and two test files under geometry/. 13 geometry tests, full hub suite 1089 across 113 files, twice.

  3. Phase 4c: design tokens and the colour pipelinefeature

    69 cube-trainer tests, full hub suite 1104 across 115 files, twice.

  4. Phase 4d: the visible scenefeature

    74 cube-trainer tests, full hub suite 1109 across 116 files, twice.

  5. Phase 4e: drag-to-turn, and a limitation worth namingfeature

    97 cube-trainer tests, full hub suite 1121 across 117 files, twice.

  6. Phase 4f: all six faces, without touching the enginefeature

    96 cube-trainer tests, full hub suite 1126 across 117 files, twice.

  7. Phase 5a: the solver reaches the playerfeature

    111 cube-trainer tests, full hub suite 1136 across 118 files, twice.

  8. Phase 5b: timer, inspection, persistence. v1 play surface donefeature

    124 cube-trainer tests, full hub suite 1149 across 119 files, twice.

Show the earlier 9 entries
  1. Phase 6a: hints, scoring, stage detectionfeature

    134 cube-trainer tests, full hub suite 1159 across 120 files, twice.

  2. Phase 6b: drill mode. v1 COMPLETEfeature

    146 cube-trainer tests, full hub suite 1170 across 121 files, twice.

  3. Phase 7: the geometry seam holdsfeature

    148 cube-trainer tests, full hub suite 1183 across 122 files, twice.

  4. Phase 8: hub integration. The build config fought backfeature

    Full hub suite 1183 across 122 files, twice.

  5. Post-v1: the gesture model was wrong, and the owner found itfix

    Owner feedback after playing the deployed version: "sometimes I try to drag the cube to inspect the sides, but it accidentally turns the side."

  6. Phase 0: auditdocs

    Read-only audit of wanessalabs-games/ before any file was written.

  7. Phase 1: POW docs and scaffolddocs

    Deviation from the brief, flagged for the owner. The brief asked for empty engine/, solver/, geometry/, and ui/ directories.

  8. Phase 2: headless engine and testsfeature

    33 tests. M1 verified mechanically: nothing in engine/ imports anything outside engine/.

  9. Phase 3: solver worker and scramble generatorfeature

    47 cube-trainer tests total, full hub suite 1076 across 111 files.

Written from this project's decision log as work happened, not afterwards. See every project →