Skip to content
Rishad Rahman
All work

Graphics · Interactive web app

CubeSolver 3D

Enter a scrambled Rubik's Cube, get a verified ~20-move solution, and follow it turn by turn on an animated 3D cube.

Stack

  • JavaScript
  • Three.js
  • Web Workers
  • Node.js test runner
  • Playwright
  • Vercel
moves per solution, found in milliseconds
~20
moves per solution, found in milliseconds
automated tests
27
automated tests
random move sequences cross-checked
2,000
random move sequences cross-checked
main-thread blocking: the solver lives in a worker
0
main-thread blocking: the solver lives in a worker
CubeSolver 3D showing a scrambled 3D cube beside a step-by-step solution panel: 22 moves, found in 31 ms, verified, currently on move 7, R2.

What it does

You hold a scrambled cube with green facing you and white on top, and enter its six faces into an unfolded-cube grid: click a sticker and pick a color, press keys 1–6, or choose a color from the palette and paint with it. Press Validate, then Solve. The app returns a move sequence and animates it on a Three.js cube, one layer turn at a time, so you can make the same turn on the cube in your hands.

  • Next, Previous, Restart and Play controls, plus keyboard shortcuts (→, ←, Space, Home, V to reset the camera).
  • A plain-language explanation of every move, for example "Right face, half turn (180°)".
  • A clickable move list to jump anywhere in the solution, and a copy button for the whole sequence.
  • Explanations when input is impossible, with the offending stickers highlighted.
The solution panel: 22 moves found in 31 ms and verified. Move 7 of 22 is R2, explained as a right-face half turn, with Restart, Previous, Next move and Play controls, a speed slider and the full numbered move sequence.
The solution panel. Each move is spelled out in plain language, and any move in the sequence can be jumped to.
The input view: a 3D preview of an empty cube on the left, and on the right six sticker grids laid out as an unfolded cube with a color palette and a 6 of 54 counter.
Input view. Only the six centers are filled in, so the counter reads 6/54. The colored bars around each grid show which neighboring face touches that edge.

The problem

Solving a physical cube from an arbitrary state is really four problems: turning 54 colored stickers into a model a program can reason about, rejecting states that cannot exist, finding a short solution fast, and presenting it so a person can follow along with a cube in their hands. Each one fails in its own way: a misread sticker, a frozen tab, a sequence that drifts out of sync with the animation. The design aims to make each of those failures either impossible or loudly visible.

Architecture

There is one cube format everywhere: a 54-character facelet string in Kociemba's U R F D L B order, nine stickers per face. It is exactly what the solver library's Cube.fromString() expects, so the input grids, validator, solver and player all share it, and grid button n on the page is facelet index n.

Main thread

color-input.js
Six sticker grids → 54-char facelet string (U R F D L B)
cube-validation.js
Counts, centers, pieces, twist, flip, parity
solver.js · CubeSolver.solve()
Promise per request id, 30 s timeout
verifiedSolution()
Re-apply with the app's own move model; reject if not solved
solution-player.js → cube3d.js
Animate each layer turn in Three.js
readColors() === logical state
Only then: “Cube solved!”

Web Worker

solver-worker.js
Classic worker; importScripts(cube.js)
Cube.initSolver()
Build move + pruning tables (a few seconds, off the main thread)
Cube.fromString(f).solve()
Kociemba two-phase search: milliseconds
{ solution, ms } | { error }
The main thread owns input, validation, verification and rendering. The worker only builds tables and searches.
ModuleResponsibility
cube-state.jsLogical cube: facelet format, 3D sticker geometry, moves as rotations
cube-validation.jsIs this a physically possible cube?
solver.jsPromise wrapper for the worker, plus the solution safety check
solver-worker.jsRuns cube.js (Kociemba two-phase) off the main thread
cube3d.jsThree.js cube: 26 cubies, real layer rotations, camera
solution-player.jsNext / Previous / Restart / Play; keeps 3D and logic in sync
color-input.jsSticker grids, color popover and paint brushes

There is no build step and nothing to install at runtime. Three.js r160 and cube.js 1.3.2 are vendored, and the app deploys to Vercel as static files with immutable cache headers on vendor/.

The solving algorithm

Herbert Kociemba's two-phase algorithm splits an intractable search into two tractable ones. Phase 1 searches for moves that bring the cube into the subgroup G1 = ⟨U, D, R2, L2, F2, B2⟩, where every corner and edge is correctly oriented and the four middle-slice edges sit in the middle slice. Phase 2 solves the cube using only moves from G1, which can never undo that progress. Precomputed pruning tables give lower bounds that let an iterative-deepening search skip most branches.

In practice this gives solutions of usually 19–22 moves in milliseconds. They are short but not guaranteed optimal, and the app says so in the solution header.

Why a Web Worker

Before it can solve anything, cube.js builds its move and pruning tables, which takes a few seconds. On the main thread that would freeze the page. In a worker, the tables are built once in the background while you enter colors, and a status pill flips to Solver ready.

js/solver-worker.jsSource (opens GitHub in a new tab)
importScripts('../vendor/cubejs/cube.js', '../vendor/cubejs/solve.js'); let tablesReady = false; function ensureTables() {  if (!tablesReady) {    Cube.initSolver();    tablesReady = true;  }} self.onmessage = (event) => {  const { id, type, facelets } = event.data;  try {    ensureTables();    if (type === 'init') {      self.postMessage({ id, type: 'ready' });      return;    }    if (type === 'solve') {      const started = performance.now();      const solution = Cube.fromString(facelets).solve();      self.postMessage({ id, type: 'solution', solution, ms: performance.now() - started });      return;    }    // …  } catch (error) {    self.postMessage({ id, type: 'error', message: String((error && error.message) || error) });  }};

On the main thread, CubeSolver wraps the worker in promises. Each request carries an id so replies can't be mixed up, solves have a 30-second timeout, and if the worker errors or times out every pending request is rejected and the worker is torn down, so the next solve starts with a fresh one.

Validation: is this a real cube?

cube.js does not validate its input, so the app does it first. A state is only reachable by turning faces if it passes all six checks below. Failing the last three means the cube was taken apart and reassembled, or (much more likely) a sticker was misread.

  1. Every sticker has a color, and each color appears exactly 9 times.
  2. The six centers are six different colors.
  3. Every corner and edge shows a color combination that exists on a real cube, and each of the 8 corners and 12 edges appears exactly once.
  4. Corner twist: the sum of corner orientations is divisible by 3.
  5. Edge flip: the sum of edge orientations is divisible by 2.
  6. Parity: the corner and edge permutations are both even or both odd. Otherwise exactly two pieces are swapped.

Corner twist

Σ corner orientations ≡ 0 (mod 3)

Each corner has 3 orientations, set by which sticker carries the U/D color. A single twisted corner is impossible.

Edge flip

Σ edge orientations ≡ 0 (mod 2)

Each edge is either flipped or not. A single flipped edge is impossible.

Permutation parity

parity(corners) = parity(edges)

Every quarter turn swaps an odd number of pairs of both. If they disagree, exactly two pieces were swapped.

js/cube-validation.jsSource (opens GitHub in a new tab)
/** Number of transpositions parity of a permutation: 0 = even, 1 = odd. */export function permutationParity(permutation) {  const seen = new Array(permutation.length).fill(false);  let parity = 0;  for (let i = 0; i < permutation.length; i++) {    if (seen[i]) continue;    let length = 0;    for (let j = i; !seen[j]; j = permutation[j]) { seen[j] = true; length++; }    parity ^= (length - 1) & 1; // a cycle of length L = L−1 swaps  }  return parity;}
Parity in O(n) by cycle decomposition: a cycle of length L is L − 1 swaps.

Errors are written for people, not programmers. Instead of "invalid input" you get messages like Red and Orange are opposite colors and never touch, and the stickers involved flash red.

Trust, but verify

Even with valid input, the app does not take the solver's word for it. Every returned sequence is re-applied with the app's own, independent move model, and any sequence that does not produce a solved cube is rejected instead of shown. It also handles one quirk explicitly: cube.js returns a non-empty sequence for an already-solved cube, which the app reports as Already solved.

export function verifiedSolution(facelets, rawSolution) {  if (isSolvedFacelets(facelets)) return [];  if (typeof rawSolution !== 'string' || !rawSolution.trim()) {    throw new Error('The solver did not return a solution for this cube.');  }  const moves = parseMoveSequence(rawSolution);  if (!isSolvedFacelets(applyMoves(facelets, moves))) {    throw new Error('Safety check failed: the computed moves do not solve this cube. Please re-check your input.');  }  return moves;}

Keeping the 3D cube honest

Moves are real rotations, not lookup tables. cube-state.js gives every sticker a 3D position and normal, and builds each move by rotating those integer vectors 90°, with exact arithmetic and no floating point. The Three.js scene rotates the actual cubie meshes using the same axis/direction table, so the model and the animation cannot disagree about what R means.

export const FACE_TURNS = {  U: { axis: 'y', layer: 1, clockwiseSign: -1 },  D: { axis: 'y', layer: -1, clockwiseSign: 1 },  R: { axis: 'x', layer: 1, clockwiseSign: -1 },  L: { axis: 'x', layer: -1, clockwiseSign: 1 },  F: { axis: 'z', layer: 1, clockwiseSign: -1 },  B: { axis: 'z', layer: -1, clockwiseSign: 1 },}; /** * Rotate an integer vector by one quarter turn (+90°, right-hand rule) * around the given axis. Exact integer arithmetic — no floating point. */function rotateQuarterPositive([x, y, z], axis) {  if (axis === 'x') return [x, -z, y];  if (axis === 'y') return [z, y, -x];  return [-y, x, z]; // axis 'z'}

The player updates the logical state immediately, queues the matching animation, and afterwards reads the colors back from the 3D scene to confirm they match. The "Cube solved!" banner only appears once the rendered cube itself is confirmed solved.

Drag to rotate

Try the notation

R turns the right face clockwise as seen looking at it, R' counter-clockwise. Keys U D R L F B work too (Shift for ').

Sequence · 0 moves

Solved.

Play inverse reverses your moves and inverts each one, the same scramble + inverse = solved identity CubeSolver's tests check. This playground only turns layers. The real solver handles any state you type in.

Testing

npm test runs 27 tests on Node's built-in test runner, with no dependencies:

  • Move definitions match standard notation for all six faces, and X⁴ = identity.
  • Agreement with cube.js on all 18 moves and on 2,000 random move sequences, so two independent models must produce identical states.
  • Scramble + inverse = solved.
  • UI color ↔ solver format mapping.
  • Rejection of incomplete input, wrong color counts, flipped edges, twisted corners, swapped pieces (parity), and impossible, mirrored or duplicate pieces.
  • The full pipeline (validation → cube.js → verification) on a known scramble plus 25 uniformly random cube states.

A separate Playwright end-to-end test drives the real page in headless Microsoft Edge: sticker entry by popover and paint brush, each validation error, solving, then stepping through a full solution while comparing the 3D scene with the logical state after every move. It also covers Previous/Restart/Play, keyboard shortcuts, three random scrambles, the absence of horizontal scroll at 390 px, and zero console errors or warnings.

Try the real thing

The deployed app is embedded below. It loads only when you ask, so this page stays fast. It works best on a laptop or desktop, or open it in its own tab.

rubiks-cube-solver-eta-henna.vercel.appNew tab
CubeSolver 3D showing a scrambled 3D cube beside a step-by-step solution panel: 22 moves, found in 31 ms, verified, currently on move 7, R2.
↑ ↓ to move · Enter to open