0n0FF: Teaching a Puzzle to Solve Itself

Published on

In previous episode we built 0n0FF, a minimalist tile flipping puzzle game in React and TypeScript. This time we’re giving our game a brain.

We are adding a solver that can compute the optimal solution to any board state and then show the next best move directly to the player using a subtle, gray toned guide beneath the main board.

What You’ll Learn

Project Code

You’ll find the complete source code here: 0n0ff-puzzle

How It Works

The core idea is simple: treat each board state as a node in a graph. A move transforms one state into another and we search for the shortest path to the win condition (all green tiles).

To avoid visiting the same state twice we encode the board as a single integer (a bitmask of 16 bits for a 4x4 grid). This turns a potentially slow puzzle solver into a blazing fast pathfinder.

const hash = (grid: Grid): number => {
  let result = 0;
  for (let r = 0; r < GRID_SIZE; r++) {
    for (let c = 0; c < GRID_SIZE; c++) {
      if (grid[r][c]) {
        result |= 1 << (r * GRID_SIZE + c);
      }
    }
  }
  return result;
};

Breadth First Search (BFS)

Using a BFS loop we explore all possible move sequences from the current board state, stopping the moment we find a winning path.

while (queue.length) {
  const { grid, moves } = queue.shift();
  if (isWon(grid)) return moves;
  // Try all 16 next moves
}

Each move toggles an entire row and column which we simulate using XOR on the bitmask.

Solver Integration

Once the solver is in place, we run it:

This allows us to continuously maintain an up to date optimal path to the win state. The next move is then shown to the player (but only subtly).

Visualizing the Hint

Beneath the main board we render a semi transparent, grayscale copy of the grid with a darker highlight on the next suggested move:

<div
  className={`solution-cell ${
    nextMove.row === rowIdx && nextMove.column === cellIdx ? "next-move" : ""
  }`}
/>

And in CSS:

.solution-cell.next-move {
  background: rgba(20, 20, 20, 0.6);
  border: 1px solid rgba(30, 30, 30, 0.8);
  box-shadow: 0 0 4px rgba(100, 100, 100, 0.8);
  filter: blur(0.8px);
}

Pure State, Clean React

To keep things clean and reactive:

This ensures the solver always computes from the true current board and allows for easy restarts or comparisons.

Performance

Thanks to bit manipulation and BFS:

Final Touches

We updated the header to show:

This adds a layer of motivation for players trying to solve it on their own.

Future Ideas

External Resources