You were doing everything right. You'd chased the logic cell by cell — this one's a 4, so that one's a 7, so the box across the grid has to be a 2 — and then you hit it. A cell that could honestly be a 3 or a 6. No deduction settles it. Nothing on the board tells you which. The only way forward is to pick one and hope.

That's the moment a Sudoku betrays you. And here's the thing most people never realize: it usually isn't your logic that failed. It's the puzzle. A Sudoku with two valid answers was broken before you ever touched it — and you spent ten minutes feeling stupid about a defect someone shipped.

We built the Sudoku on PlayEye specifically so that moment can't happen. The entire generator bends around one promise: there is exactly one correct answer, every time. Here's how we keep it, and where the real cost of that promise hides.

A Sudoku puzzle on PlayEye mid-solve, showing the 9 by 9 grid with given clues in one color and player-entered numbers in another
Every PlayEye Sudoku is generated fresh with a single guaranteed solution — so there's always a logical path, never a guess.

Step one: build the answer before you build the puzzle

You can't carve a puzzle out of nothing, so we start at the end — with a complete, fully solved board. This is a backtracking fill: walk the empty cells, try a number, recurse, back out when you paint yourself into a corner. Standard stuff, with one twist that matters more than it looks. We try the candidate numbers in a shuffled order.

function fillBoard(board: number[]): boolean {
  const pos = board.indexOf(0)
  if (pos === -1) return true // solved

  const candidates = shuffle([1, 2, 3, 4, 5, 6, 7, 8, 9])
  for (const num of candidates) {
    if (isValid(board, pos, num)) {
      board[pos] = num
      if (fillBoard(board)) return true
      board[pos] = 0
    }
  }
  return false
}

Drop that Fisher–Yates shuffle and you'd generate the same handful of grids forever — players would start recognizing boards, which is its own kind of broken. With it, every solved grid is genuinely new. At the end of step one we have a legal, complete board: the answer key nobody's allowed to see yet.

Step two: take away clues without breaking the promise

Now we carve. We visit all 81 cells in random order and try to empty each one. But — and this is the whole game, the part that separates a real Sudoku from a number-shaped trap — after we remove a clue, we check that the puzzle still has only one solution. If emptying that cell would let a second answer sneak in, we put the number back and move on.

const positions = shuffle([...Array(81).keys()])
let givens = 81

for (const pos of positions) {
  if (givens <= target) break
  const backup = puzzle[pos]
  puzzle[pos] = 0
  if (!hasUniqueSolution(puzzle)) {
    puzzle[pos] = backup // restore — removing this breaks uniqueness
  } else {
    givens--
  }
}

This is the step everyone skips, because it's the expensive one and the puzzle looks fine without it. It isn't. A generator that just yanks clues until it hits a target count will hand you ambiguous boards — and you won't find out until you're three deductions deep and staring at a cell with two right answers. You have to check. Every single removal. No exceptions, no "probably fine."

The trick that keeps the check from grinding to a halt

Checking uniqueness sounds brutal. Solve the puzzle, then... keep solving to see if there's a second answer hiding somewhere? On a sparse board that could mean enumerating a forest of solutions.

Here's the insight that saves it: you never need to count all the solutions. You only need to know whether there are two. So our solution counter quits the instant it finds a second one.

function countSolutions(board: number[], limit: number): number {
  const pos = board.indexOf(0)
  if (pos === -1) return 1

  let count = 0
  for (let num = 1; num <= 9; num++) {
    if (isValid(board, pos, num)) {
      board[pos] = num
      count += countSolutions(board, limit - count)
      board[pos] = 0
      if (count >= limit) return count // stop the moment we hit the limit
    }
  }
  return count
}

We call it with limit = 2. The second a second solution surfaces, the recursion unwinds and bails — we don't burn a single cycle counting solutions three through nine hundred. "Has a unique solution" collapses into one tidy line: countSolutions(copy, 2) === 1.

That one short-circuit is the entire difference between a generator that feels instant and one that hangs forever on a hard board. Same logic, one early return, night and day.

Difficulty is just a clue count

Once uniqueness is guaranteed, difficulty gets refreshingly boring to control. We stop removing clues when we hit a target number of givens, and that target is the difficulty.

DifficultyGivens (clues shown)
Easy38
Medium32
Hard26
Expert22

Fewer clues, longer chains. With 38 givens the next number is usually sitting right there waiting for you. At 22, the deduction that places a single digit might thread through half the board first. But — and this is the part we refuse to compromise — every level, all the way down to Expert's 22 clues, is solvable by pure logic. No guessing required, ever. Stuck on our Sudoku? There's a deduction you haven't spotted. There is never a coin flip. We checked, at generation time, so you don't have to wonder.

(For the curious: 22 sits comfortably above 17, the known minimum number of clues a unique-solution Sudoku can have. We stay well clear of that cliff on purpose. Puzzles hugging the theoretical minimum are miserable to generate and brutal to solve, and "technically valid but joyless" isn't the experience we're after.)

Why we wrote our own

It would've been faster to drop in a puzzle pack or some third-party generator and move on. We didn't, for the same stubborn reason we wrote our own Solitaire solver: the guarantee is the product. A Sudoku you can trust to have one answer, dealt fresh every time so you never replay the same board, only exists if you own the generator end to end.

That's the through-line in everything we build at PlayEye. Fairness isn't a feature we bolt on after — it's baked into the machine that makes the levels. If you like the feeling of a game that's being honest with you, our Klondike Solitaire only ever deals hands that are provably winnable, on exactly this principle. Same promise. Different deck.