預算有限
聯絡我們 →

a unique answer isn't a solvable puzzle.English version

I got into Queens on LinkedIn, then hit the one-puzzle-a-day limit and decided to make my own on a hexagon. The hard part was not the game — it was generating puzzles that were unique and still unsolvable.

A while back I made an iOS puzzle game called Hexhive.

The reason I made it is simple: I got into Queens on LinkedIn, thought it was genuinely good — and then hit the fact that you only get one puzzle a day.

There are similar games on the App Store, but I thought, why not just make my own? And while I am at it, why not put it on a hexagonal board and see whether that makes it better?

So I talked the idea over with Claude Code: design a hexagonal version of Queens.

It worked through the maths and concluded this would be straightforward — just make sure each puzzle has a unique solution.

The board
A side-5 board: one crown per line on each of three axes, one per colour region.

So, going by that, this should have been easy.

In theory.

This is a post about how long "in theory" took.

I thought the hard part was the game. The hard part was generating puzzles.

Once the rules were written, cells were tappable, and the win condition was right, I assumed the rest was interface work.

Then I started playing, and found I could not solve a lot of my own puzzles.

Not hard-in-a-good-way. I would reach a point where there was nothing left to deduce — every cell still possible, no move forced anywhere. The only way on was to guess a cell and back out if it was wrong.

At first I assumed I had just got worse at the game. Then I had Claude Code run the numbers:

Every puzzle the first generator produced had a unique solution, and almost none of them were playable.

Why that happens

The generation flow goes roughly: find a random solution, grow colour regions around the crowns, then keep adjusting regions until the board has only one solution left.

That last step is genuinely necessary. Purely random regions land on a unique solution often enough at side 4 or 5, but at side 6 it tried three thousand times without a single hit.

The problem is in the phrase "keep adjusting until only one solution is left".

That walks the board to the boundary where uniqueness is barely true. And barely-unique is exactly the shape with no path of reasoning through it — the answer is pinned down, but pinned down by exhaustion, not forced by logic. A human sits there and cannot find a single move that has to be true.

I was stuck here for a while, wondering whether the rules themselves were wrong, whether a hexagon just does not suit this kind of puzzle. Then it clicked that I had been asking it to fix the wrong thing:

Uniqueness is a property of the answer. Playability is a property of the path to the answer. I had been guaranteeing the first, and that guarantees nothing about the second.

Repairing toward "deducible" instead

Once that was clear the fix followed: if what I want is deducible, then repair toward that, rather than stopping at unique.

After the board is unique, run the solver again from an empty board — and let it use only rules a person can follow:

If it solves the board, keep the puzzle. If it stalls, look at where it stalled: take the region closest to being forced — fewest candidates left — hand one of its non-answer candidate cells to a neighbour, and run again.

Every pass nudges the board a little further toward deducible.

That is not what I did first. First I did generate-then-filter: make a puzzle, test whether the simple rules crack it, throw it away if not. At side 6 the pass rate is about 0.04% — roughly 30 seconds per puzzle.

Repairing toward the target instead: 56 milliseconds per puzzle.

About 500x. But to be honest, I did not make that change for speed. I made it because I could not stand it producing bad puzzles. The speed came from no longer throwing almost everything away.

Then I realised I had the whole thing backwards

This was the part that surprised me.

If the simple rules can solve the board from empty, then every crown was forced — at that moment it was the only cell it could go in. And a forced chain cannot have branched.

So the solution is necessarily unique. No check required.

The code still verifies it, but that is insurance now, not a requirement. And the uniqueness-repair phase I had put the most work into mostly stopped mattering: by the time a board is deducible, it is nearly always unique already.

I had the dependency inverted for weeks. I thought uniqueness was the hard constraint and playability was a bonus on top.

It is the other way round.

The hint engine is the same thing

The hint
The hint does not fill in the answer: it marks the next cell you can rule out, and says why.

The solver that decides whether a puzzle is deducible is the same one that runs when you press hint.

That is not reuse for convenience — it is what makes the guarantee real. The generator only ships boards the hint engine can talk its way through, so the hint engine always has something to say.

The hint does not fill in the answer. It finds the next cell you can cross out, marks it, and tells you which rule it came from.

Two decisions in there were harder than they look.

Rules are ordered by how hard they are to understand, not by power

Seven rules, ordered from most legible to least:

  1. Two placed crowns clash — same line or same region
  2. A crown is somewhere the answer cannot have it
  3. A region is confined to one line
  4. A line is confined to one region
  5. A crown here would starve some region or line of every cell
  6. A crown here leads to a contradiction further down
  7. Only one cell left in a region or line — that is a crown

"This region has only one cell left" is easier to follow than any lookahead, so it has to get its turn before the lookahead rules do.

I tried ordering by result type. What you get is a hint that explains a forced move through trial-and-error reasoning while a one-line explanation was sitting right there. Correct, and useless.

Rule 6 is trial and error wearing a suit. The generator now guarantees puzzles that never need it — across the whole test set it never fires — but I keep it as a backstop.

The engine does not trust the player's own marks

Players cross out cells as notes. Those crosses can be wrong.

If the hint engine treated them as facts, it would happily derive a hint from your mistake and then be confidently, uselessly wrong.

So it maintains its own exclusion set, derived only from the rules. Your crosses are folded in as extra knowledge and it carries on from there — which is what makes a hint the next step you have not worked out yet, rather than one you already took.

One smaller decision: when it finds nothing, it says so, and it does not charge you a hint. No forced move usually means you have a crown in the wrong place, and saying that is more useful than inventing something.

Checking that it actually holds

45 puzzles — 15 each at sides 4, 5 and 6 — from an empty board, doing nothing but pressing hint:

Average hints to solve: 12.5 / 15.4 / 17.7.

I like that last set of numbers. A puzzle that takes 15 forced steps has 15 things in it a person can find. That is a measure of how much game is in the game — and it falls out of the same solver. I did not write anything separate to compute it.

A smaller trap, about the hint panel

Worth noting because I got it wrong twice the same way.

On a phone the hint panel has to appear somewhere, and the board already fills the screen.

First I added 300px of bottom padding to the game area and scrolled the target cell into view. The result: every button jumps away from your thumb the moment the hint opens, and the board stays scrolled after it closes.

Then I measured the space left and shrank the board to fit. The page stops moving, but the board changes size mid-game, which is just as distracting.

Both attempts were trying to make room. Both moved the thing the player was looking at.

The version that works moves nothing: the panel floats over the board, translucent, with a very light blur behind the text — deliberately light, because blurring hard enough to make text comfortable defeats the point of being able to see through it.

It covers the button row for a few seconds. That is cheaper than the other two: it closes after five seconds, on Esc, or the moment you touch the board.

On how this was made

The part worth talking about is not "AI wrote my code" — it is that this process was possible at all. Moving the generator from generate-and-filter to repair-toward-target, the statistics, the 45-puzzle verification, the versions I got wrong and cut — if I had hand-rolled every round, I would probably have quit at "the puzzles are not playable" and just lowered the difficulty instead.

But which direction to repair in, what counts as a rule a person can follow, how to order those rules — nobody could decide those for me. "Uniqueness is a property of the answer, playability is a property of the path" is not something I was told. It is what I worked out after being stuck for weeks.

The tooling let me try enough times that the idea had a chance to surface. That is where I have landed on this.

Finally

The generator and the hint engine are one solver used twice — once to decide whether a board deserves to exist, once to explain it while you play.

Every puzzle Hexhive ships can be reasoned out with rules that fit in a person's head, because that is the condition it had to pass to be generated at all.

Hexhive is on the App Store: https://apps.apple.com/app/id6799698455

← all entries · 全部文章

let's index something together.

我們聊聊 — 一份你的清單,一份我們的提案。

30 minutes, no slides. Send a brief or a paragraph — we'll come back with three directions and a rough budget within a week.

info@myfundslimited.com · @預算有限公司