So I got this idea of making a Rubik’s Cube solver in Ruby and I would call it RubyCube. Hilarious, right? But kind of stupid, because nobody really cares what the solver is written in as long as it actually works. Looking back, I think similarly few users would care whether I vibe-coded it with AI or handcrafted each line, especially given that I’m not open-sourcing it. Here, for what matters, I vibe-coded the front entirely (I just told Copilot to use three.js, don’t ask me more) and manually coded the backend solver in Ruby.

It’s live at rubycube.jmarhic.com!

A 3D Rubik's Cube

My 2x2x2 Rubik's cube solver

So, while I can’t talk much about the frontend, the backend is quite elegant. I followed part of Can a Rubik’s Cube be brute-forced—in short, I use a “brute-force + meet-in-the-middle” approach. To solve a given “state,” we compute the follow-up states by applying every possible rotation. We get a tree that we traverse in “breadth-first order” (not depth-first) to ensure we get the shortest solution. And to speed things up, I precomputed the first 6 levels (every possible state up to 6 rotations) and stored them in a db, so when solving a cube we only have to solve it “up to one of the stored states,” not “up to the initial state.” Anyway, I think reading the “solve” method below is easier to understand: notice the commented “if state == solved_state”, replaced by a db lookup “find_moves_for_state”.

The cube state is represented as an array of numbers (each sticker from 0 to 23—it’s a 2x2x2 cube, by the way), and a move is just a permutation of this array. It was easy to write the permutation by hand, since I had a cube at hand and just wrote the number on each sticker. The most painful part of the code was converting from a “color state” (what the frontend sends, the color of each cubie for each face) to this “permutation state” (list of numbers).

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
  def self.solve(state)
    queue = [[state, []]]

    while !queue.empty?
      state, moves = queue.shift
      #if state == solved_state
      pregenerated_moves = find_moves_for_state(state)
      if !pregenerated_moves.nil?
          # Return move names instead of permutations
          return moves + reverse_moves(pregenerated_moves.split(',').reverse)
      end

      next if moves.length > 6

      MOVES.each do |move_name, move|
        prev_move = moves.last
        # Avoid immediate inverse moves
        next if prev_move && inverse_move?(prev_move,move_name)

        queue << [rotate(state, move), moves + [move_name]]
      end
    end

    return [] # Couldn't find a solution, shouldn't happen
  end

Notice I can stop the search after 6 moves—that’s because any state can be solved in less than 11 moves (it’s called God’s number), and the first 6 moves are already stored in the db. I really appreciate this “meet-in-the-middle” search where we trade some storage cost (precomputing the states and storing them in a db) for a speed boost at search time. It’s kind of like the famous “Leetcode 4 sum” problem where you have to find 4 numbers that sum up to some target; you “meet in the middle” by generating two lists instead of brute-forcing all combinations of 4 numbers.

Anyway, that marks my first project of 2026 and I’m happy with it!