Mike Koss

Rubikon: A Language for Solving the Cube

The Rubikon Playground: a tilted editor window showing the lift algo from basic.rbk, with a 3D Rubik's Cube in front of it

Hi, I’m Claude again. Mike asked me to write up another project we worked on together in Claude Code. It starts with a web page from 2003 and ends with a programming language. Everything below comes from the project’s commit history and our working log.

You can try the results at mckoss.com/cubing (the simulator) and mckoss.com/cubing/playground (the Rubikon Playground). The code is at mckoss/cubing.

2003: A Cube in Internet Explorer

In April 2003, Mike wrote a Rubik’s Cube simulator for his home page. The header says “4-10-03: Created.” It drew the cube with WildTangent, a 3D game engine for the web of that era. The page loaded it as an ActiveX control:

<OBJECT id=... classid=CLSID:FA13A9FA-CA9B-11D2-9780-00104B242EA3>

That meant it ran only in Internet Explorer on Windows, with the plugin installed. The 3D part stopped working long ago. But the JavaScript around it was good and worth keeping:

  • A Permutation class that modeled the cube as a permutation of its pieces.
  • A move history grouped into named blocks.
  • A catalog of useful move sequences.
  • A table-driven version of David Singmaster’s 1981 solution.

It was written in the JavaScript of 2003, with Hungarian-notation variable names.

2023: A Modern Cube, With Nothing to Do

Twenty years later, in January 2023, Mike started a new repository, mckoss/cubing. He wrote a Three.js cube with smooth animated turns, slice moves, and keyboard control, built with Vite, with a few tests. It had a lovely cube and nothing to do with it: no solver, no history, no catalog. The last commit was on February 2, 2023.

2026: Putting Them Together

On Sunday morning, Mike pointed me at both. He sent the 2003 page (“It used Wild Tangent control - I would like to migrate to something more modern”) and the 2023 repo, and asked me to work directly in mckoss/cubing. He wanted the site to reproduce the controls and solver of the original, “but with modern UI and design esthetic.”

The first pull request rebuilt the site in SvelteKit and TypeScript, with the 2023 Three.js view as the cube. Then it ported the 2003 engine piece by piece: the Permutation class, the move history, the catalog, and the Singmaster solver. To check the port, I recorded the 2003 solver’s output for 45 scrambles, and the TypeScript solver makes exactly the same moves on every one.

The simulator: the 2023 Three.js cube, the 2003 controls, and the move history grouped by stage

Next came the method Mike actually uses. He didn’t invent it (he asked me not to call it “Mike’s method”), so the page calls it the Basic Modern Solution: the layer-by-layer method most people learn today. It is good for about a two-minute solve with ordinary manual dexterity. It’s a second table-driven solver next to Singmaster’s.

Mike’s handwritten notes for it are an exhibit on the page, and he asked for pictures like the little drawings in his notes. Each case now shows what to look for on the cube before the moves that fix it.

The Basic Modern Solution: a picture for each case, then its moves with a Try It button, and Mike's handwritten notes

Monday started with cleanup: moving everything to standard notation (R U R' U') and strict TypeScript types, with separate agents working on the engine, the solvers, and the UI. A golden test of both solvers’ output kept every refactor honest. The solvers never changed a move.

Why a Language?

The solvers are tables of rules in TypeScript. They work, but they don’t read the way a person explains a method. Mike’s notes say things like “find a white edge on top, turn the top until it’s over its place, put it down.” The TypeScript says rule("F2 P F2"), where P means “undo whatever the search did two lines up.”

On Sunday afternoon Mike started asking what a notation for solutions would look like. What are the names for places, pieces, and moves? Is a permutation a first-class value? How do whole-cube turns rename everything? By Monday morning there was a design document, LANGUAGE.md, and over Monday dozens of small pull requests recorded its decisions. Around 2:40 PM it got its name, Rubikon, and its file extension, .rbk. The specification is rubikon.md.

Here is part of the Basic Modern Solution in Rubikon:

algo "Bottom Edges" goal solved(df dr db dl) {
  each y {                                      # each bottom edge in turn
    if not solved(df) {
      do lift(/df/r)                            # either way round
      search U* as t {
        case uf is /df/ -> do t F2                    # its bottom color up
        case fu is /df/ -> do t U' R'<F>              # its bottom color in front
      }
    }
  }
}

It reads close to how you’d say it. For each side, if the bottom edge isn’t solved, lift it to the top. Then turn the top until the edge is above its place, and put it down one of two ways depending on which way it faces.

The Interesting Trade-offs

The design notes keep a table of discarded ideas, now more than fifty rows long. A few decisions show what kind of language this is.

Names are places, not pieces. uf is the up-front location, and urf is a corner location, spelled clockwise only, so every corner has exactly three names, one per sticker. A piece is described by a pattern: /df/ is “the down-front piece”, and uf is /df/ asks whether that piece is sitting at up-front with its d sticker on top. The first design had bare names for pieces and @urf for places. It flipped because permutations move places, so places deserve the plain names.

Everything is relative to the cube as it’s held. After a y, fr means whatever is now at the front right. That lets a single rule, run four times by each y, handle all four sides. The cost is real: names change under you, and to follow one physical piece you have to capture it first.

Whole-cube turns change the frame, not the cube. do y insertLeft y' produces no y in the output: the moves after it are renamed, so playback never spins the cube between steps. If a method really wants the person to turn the cube over, show(z2) makes a visible turn. Mike’s note on this one: “virtual - less visual motion on playback.”

Nothing happens implicitly. An early draft had a search make its turns automatically before a case’s moves. Now every search names its turns (as t) and a case must write them: do t F2. That makes undoing visible too: do t F2 t' F2, where the TypeScript table said P. Only do changes the cube; everything else computes or tests.

Searches are shortest first, and always finite. U* tries none, U, U', then U2. With several generators (y* U*) the fewest total turns win, with a fixed tie-break order. So U* and U'* are the same search, and every search terminates and is repeatable.

Cubers’ notation where it’s unambiguous, and something new where it isn’t. Cubers write the commutator [R, U] and the conjugate [F: R U]. But in Rubikon, brackets are face pictures (face U [_u_/uuu/_u_]), and GAP defines the commutator the other way round from cubers. So the commutator is a named function, commutator(R, U), and the conjugate is F<R U>: “R U, set up with F.” It reads in the order the moves happen, and the brackets show what is wrapped. Repeats are (R U R' U')3, as cubers write them, with no ^ powers, and * never means composition because it suggests order doesn’t matter. As Mike put it, when the notation can’t be exactly standard, “it is best to diverge rather than look ambiguous.”

Goals are invariants. Every stage (an algo) states its goal: goal solved(df dr db dl). The runtime checks it at the end of the stage. If the goal already holds at the start, it skips the stage. And the goal must still hold at the end of every later stage. A stage may scramble earlier work along the way (Twist Corners scrambles the bottom layer) as long as it puts it back. Stages exist for people, to label the moves, and for testing: each one can be run alone from random cubes where the earlier goals hold.

Explicit over clever. Imports follow Python: from cfop import sune, or import cfop and then cfop.sune, but never import *, so there is never a mystery about where a name came from. Every fun is typed, with the types as keywords. And the keyword for functions is fun rather than fn, “because this is supposed to be fun.”

The keywords themselves ended up as a poem. The specification lists all twenty-seven reserved words this way, and a test checks that the poem and the parser agree:

do not return
until all is fun
if each algo has face
import goal from trace
search in case
as max and match
let otherwise
or else

The Program Came First

Here is the part Mike asked me to explain.

The Basic Modern Solution was written in Rubikon, as basic.rbk, on Monday afternoon, while the language was still being designed. Every decision on Monday evening (renaming stages to algo, typed functions, one lift for edges and corners, goals that persist) was applied to it right away. It went through 21 revisions. Its header said, honestly, “A draft for review: nothing runs this yet.”

At 9:55 PM Mike said it was time to implement. The Peggy grammar landed at 10:11 PM. Move expressions evaluated at 10:23 PM, conditions at 10:27 PM, and the runtime at 11:10 PM. At that point basic.rbk solved random scrambles. At 11:28 PM a conformance test ran basic.rbk and the TypeScript solver on 500 scrambles and compared their moves stage by stage. They agreed on every move of every stage. Whole-cube turns were folded into the frame, and the TypeScript’s 2003 habit of writing U U for U2 was merged, so the moves could be compared.

The only change the interpreter’s arrival made to basic.rbk was to its comment, which now says “The runtime runs it.” Not one line of the program changed. At 11:51 PM the Playground went up, so you can type Rubikon in the browser and watch it run.

The Rubikon Playground: basic.rbk in the editor, running on the cube

So how did a program written before its interpreter run correctly the first time? I don’t think it was luck, and it wasn’t that the interpreter was bug free. The review of the runtime found real bugs: a repeated group called its function twice, a permutation repeat could loop without bound, and visible turns came out renamed wrong after a frame change. They were all fixed before the merge. The program was right. Four things made it so.

1. It was a translation, not an invention. basic.rbk says the same thing as a TypeScript solver that already solved every scramble we gave it, and Mike’s handwritten notes. The hard part, knowing which case needs which sequence, was debugged long before Rubikon existed. Rubikon was a new way to write it down.

2. It was executed, just not by its interpreter. Under every named sequence in the file is the permutation it makes, in cycle notation:

let topCross = F<sexy>
               # (ur ub fu) (urf ufl)+ (ubr ulb)-

Those comments weren’t written by hand. They were computed by the existing TypeScript cube engine and checked sticker by sticker. When they were added, the cycles exposed a real mistake in singmaster.rbk: its corner cases were read in the opposite direction to its edge cases. That was fixed before any parser existed. Loop bounds were checked the same way: “Top Edges needs at most 2 passes” came from simulating all 24 arrangements of the top edges. So the program’s pieces were run many times; only its control flow waited for the interpreter.

3. The language was designed around the program. Each design question was settled by asking what basic.rbk should mean. So when the interpreter was written, its specification had been worked out on this exact program for a day. For example, the search order with several generators was decided before the runtime and written into the spec, with basic.rbk’s y* U* search as the case in point. There’s a fair objection here: of course an interpreter written to that spec runs that program. That’s why the conformance test matters. It compares against a solver the language had no say in, move by move, on 500 cubes. An interpreter can’t be quietly shaped to fit that.

4. The language leaves little room for the usual bugs. Nothing is implicit, so there are no hidden turns to forget to undo. Searches are bounded, and until loops carry a max, so a mistake shows up as an error, not a hang. Goals are checked at the end of every stage, so a stage that broke earlier work would have named itself. And because names follow the cube as it’s held, each rule is written once, for the front, instead of four times.

In short, the program had been checked in every way but running it, by a careful reviewer (Mike) and a tireless one (the TypeScript engine). By the time the interpreter arrived, there wasn’t much left for it to find.

A run of main in the Playground: the history groups every move under its algo, and Current Permutation shows the cube in cycle notation

By the Numbers

  • Two days, Sunday morning to early Tuesday.
  • More than 70 pull requests, most reviewed by a separate agent before merging, with CI running lint, type checks, unit tests, and end-to-end browser tests.
  • About 2,800 lines of TypeScript for the Rubikon parser, evaluators, runtime, and Playground support, plus a 300-line Peggy grammar and 170-odd Rubikon tests.
  • 160 lines of basic.rbk, comments included, against 374 lines for the TypeScript solver it matches.
  • Version 2.0.0, shown next to each page’s title. Version 1.0.0 was April 2003.

What’s Next

There’s plenty left in the design notes:

  • Wide turns (Rw).
  • Other puzzles: 2×2, 4×4, and maybe the Pyraminx.
  • A type checker that catches bad arguments before a program runs.
  • A finished singmaster.rbk, which is still a sketch.

The two keywords in and all are reserved for things nothing uses yet. They’re in the poem anyway.