part 10
Puzzles without chance, where the solver proves that a task has a solution
In a game full of dice a mode appeared in which nothing is rolled at all. How to prove that a puzzle has a solution, and why almost the whole first set of puzzles was too easy.
Dice Wars is a game of risk. An attack with a 45% chance may succeed and may not. That is exactly what one person likes about it and another does not. A third group of players wants something else: a task they solve by thinking, without the roll of a die deciding the outcome.
The brief for this mode went roughly: Puzzle Lab as in Hearthstone, fixed rolls, an elite task is not a lottery but a solvable puzzle. From that one line came the labs (four today), a solver, help and the measurement of difficulty. And one useful mistake.
What "fixed rolls" means
The brief could be understood in three ways.
- Fixed random rolls. The same situation always gives the same roll. The game stays the same and the roll is merely "frozen". The player, however, does not see it in advance, so they would solve the puzzle by trial and repetition. It would be a lottery with a memory, not thinking. Rejected.
- Visible values of the dice. Every die on the map has its face, and an attack compares the totals. The richest option, but it changes the state of the game, the saving and the drawing, and it is farthest from the real game. Rejected as a first step, it remains as a possibility for the future.
- Lab dice. Chosen. Nothing is rolled in a puzzle. An attack is won by whoever has more dice, on a tie the defender. Bonuses in points are converted to dice (3.5 points make one die), so a fortress defends like a stack one die stronger.
What decided it: with lab dice the result of every attack can be read from the numbers on the map, a puzzle is solved by thinking, and above all it can be proved that it has a solution.
The solver
The proof is done by the module puzzle.js. It searches all moves in depth and remembers positions it has already seen. In doing so it uses the same rules as the game (not a copy of them), so it cannot diverge from what the player sees. For every task it can say:
- whether it has a solution and which is the shortest (that is the
par, which is not estimated but calculated), - how many solutions there are and how many variants (sets of moves, regardless of the order of the independent ones),
- which first moves are traps, that is, winning attacks after which the goal can no longer be reached,
- how many positions it examined.
One tool has four functions: checking of tasks (an invalid puzzle does not get into the game), Help (from the current position it says whether it can be won, and shows a move), Undo (nothing is random, so the game is built again and the moves except the last one are replayed) and inventing new puzzles with the command node tools/puzzles.js --search.
Chess players know a related notion. The author of a chess problem has to check that the task has a solution and that it has no cook, an unintended second solution. A problem with a different solution from the one the author wanted is faulty. The solver does the same work automatically.
An example: Snowball
The map of a puzzle is a small text. The first digit is the owner (0 is you, 1 the opponent), the second is the number of dice, n is a neutral tile. This is what a real puzzle from the game looks like, Snowball from the Mastery class:
14 n2 15 12
04 03 n2 n2
12 15 14 11
The goal: to own at least 8 tiles within one turn. You have two stacks, with 4 and 3 dice. Two bonuses are part of the task: Vanguard (the first attack of the turn counts with two extra dice) and Plunder (the first tile captured in the turn gets two extra dice). Both belong only to the first attack, so the question is which stack should go first.
The solver finds that the shortest solution has six attacks. There are five solutions, but they all make up a single variant. There are seven first moves and six of them are a dead end: you win a tile, but you will never get to eight. Only the first attack from the stack with 4 dice onto the opponent's stack with 5 dice in the bottom row is correct. Thanks to Vanguard it wins (4 + 2 against 5) and the captured tile grows to 5 dice, from which one can go on. A stack that grows with its first win gave the puzzle its name.
A player who chose moves at random would solve the puzzle in 0.7% of attempts.
The first set was too easy
The most instructive thing is what happened to the first set. The tool tools/puzzles.js has seven simple rules ("the biggest stack onto the weakest" and the like), with which one can try to solve any puzzle. The first set turned out like this: 11 of 15 puzzles were solved by a single rule, the biggest stack onto the strongest.
Such a puzzle is not a puzzle. It is an exercise, and whoever finds the rule solves ten more. That is why from then on the difficulty is not estimated but measured. For each puzzle three quantities are found:
| Quantity | What it says |
|---|---|
| simple rules | by how many of the seven the puzzle is solved (from level 2 it must be 0) |
| decisions | the number of points where one has to choose (from level 3 at least 3) |
| random choice | the exact chance that a random choice of moves solves the puzzle |
The level is determined from the last quantity: 20% and more is level 1, 4 to 20% level 2, 1 to 4% level 3, 0.1 to 1% level 4 and below 0.1% level 5. A test in the project requires the recorded level to match the measured one.
This is what the measurement looks like today (a selection from node tools/puzzles.js --check, 20 October 2026):
| Puzzle | shortest solution | positions examined | decisions | random choice |
|---|---|---|---|---|
| Two stacks (Basics) | 3 attacks | 12 | 2 | 23% |
| Back door (Terrain) | 4 attacks | 29 | 3 | 5.6% |
| Snowball (Mastery class) | 6 attacks | 104 | 5 | 0.7% |
| The long road (Mastery class) | 8 attacks and a move | 4,990 | 8 | 0.0% |
| The great trial (Mastery class) | 10 attacks and a move | 15,692 | 9 | 0.0% |
The solver goes through even the biggest puzzle, with 15,692 positions, in a fraction of a second. The limit is readability (a map of up to 15 tiles), not computation.
After the first play: all of them were easy
The measurement said that the puzzles were in order, and yet at the first play all of them felt easy. They were good as a teaching aid, but for puzzles that is too little. The scale of difficulty then had three grades, and the hardest puzzle had a random chance of around one to four percent.
The answer is the Mastery class, the fourth lab. These are tasks that combine two or three mechanics at once (Fortress and plunder, Tower above the portal), with solutions 6 to 10 attacks long and with difficulty levels 4 and 5. The tool also got a check for decorative mechanics: if a puzzle can be solved even without one of its mechanics, that mechanic is just an ornament and the puzzle is rejected.
Of the original 31 puzzles the three simplest (First step, Strongest first, Sharpshooter) later became lessons of the introduction, where they belong, and 28 remained in the labs.
A lab that is built differently, because the measurement showed it
The cheapest fix of a puzzle is the one that comes before the first line of code. The plan has a fifth lab, the Absorber: after a win the hero adds the dice of the defeated opponent, as in the ads of mobile games of the Hero Wars type. Before any code existed, this was simulated on 6,000 random boards (a simplified model):
| Mode | A simple rule solves it |
|---|---|
| The tile grows, a win only adds | 100% |
| + bonuses +k | 100% |
| + multipliers ×2 | 99.9 to 100% |
| A single road without return | roughly 80% |
Pure absorbing has no decision, because adding cannot hurt, so the order does not matter. Decisions arise only with a single road without return. In Dice Wars, according to the plan, that road creates itself, because only a stack that has at least two dice can attack and move (in the lab this still has to be verified). The design of the lab therefore changed before building began.
The puzzle of the day
The puzzle of the day (the same task for everyone, without a server) is not chosen from a list. It is generated on the fly from the date, in a separate thread, and the solver verifies it before it is shown. The difficulty rises from Monday to Sunday. If a puzzle matching the specification cannot be found, the search softens the condition, and if it finds nothing, it says so. The tests have the clock of the page pinned to an "easy day", so that the result of the suite does not depend on the day of the week on which they run.
What I take from it
- A puzzle without a proof is a hope. If nobody has solved it, you do not know whether it can be.
- The author is the worst judge of difficulty. The author knows the trick that others do not. A measured blind chance is more objective than a feeling.
- Simulate first, then build. Without the simulation the Absorber would have come out as a game without decisions.
- Play your own puzzles. The measurement says that the task is in order. Only playing says that it is boring.
Sources and notes
- Decisions:
docs/DESIGN.md§17 (§17.1 fixed rolls, §17.3 the solver, §17.6 and §17.9 the measurement and the Mastery class), the proceduredocs/TUNING.md§16. The table:node tools/puzzles.js --checkof 20 October 2026. - Absorber:
docs/ROADMAP.md, chapter 2 (a simulation of 6,000 boards, a simplified model; the lab is not built yet). - The picture of Snowball: the drawing of the game (
Scenario.svg) for the puzzlesnowball, 5 November 2026; the colours of the standard palette, the player red. - The term cook in chess problems is common in composition; in the text it is used as an analogy.
- Puzzle Lab in the card game Hearthstone came out on 21 August 2018, two weeks after the expansion The Boomsday Project (Blizzard press releases, Hearthstone wiki). In the text it is only an inspiration by tasks without chance.