Block puzzles — the 8×8 "place three pieces, clear lines" genre — share a dirty secret: sometimes the game simply deals you pieces that cannot fit anywhere. You didn't blunder. The board had room. The generator just rolled three shapes that don't go together, and your run is over.
Players feel this even when they can't name it. It's why runs in this genre so often end with "that was unfair" instead of "I messed up". When I built Block Peak, I made one rule non-negotiable: every dealt hand must be provably playable. If your run ends, it was your choices — which is also what makes beating your best score mean something.
Here's how that guarantee actually works, and the two search problems behind it.
1. The solvability check: prove it before you deal it
The core primitive is a function traySolvable(board, pieces): can these three pieces be placed in some order, at some positions, given that full rows and columns clear between placements? That last clause is what makes the problem interesting — a piece that doesn't fit now may fit after an earlier placement clears a line. Order matters.
The check is an exhaustive recursive search:
bool traySolvable(board, pieces):
if pieces is empty: return true
for each distinct piece p in pieces: // identical shapes: one branch
for each legal position (r, c) of p:
b' = place(board, p, r, c)
b' = clearFullLines(b') // clears open up space
if traySolvable(b', pieces - p): return true
return false
Worst case that's 3! orders × every legal placement at each step, but two things keep it fast in practice: identical shapes are pruned to a single branch, and the search short-circuits on the first witness. On an 8×8 board with 3 pieces it almost always resolves in well under a millisecond; a positive answer comes with an actual placement order that works, so it's a constructive proof, not a heuristic.
Every tray Block Peak deals has passed this check against the live board. No exceptions: if the smart generator somehow produces no solvable candidate, it falls back to a simpler generator that constructs a solvable tray piece-by-piece (place a fitting piece on a simulated board, then pick the next one against the updated board). There is no code path that hands you an unplayable set.
2. The generator: rating candidate hands, not rolling dice
Fairness alone would be easy — deal 1×1 squares forever. The real job is dealing hands that are fair and interesting. Block Peak's generator works like a tiny tournament:
Generate ~16 candidate trays. Piece sizes lean bigger as difficulty rises, and difficulty itself follows a slow sine wave over the move count — the game breathes: easy in, ramps up, eases off. A hot combo streak biases candidates toward sets that can keep the chain alive.
Rate every candidate: is it solvable (the check above), how many lines can it clear at best (a budgeted DFS), how many legal placements does it have, how snugly do its pieces "fit" existing gaps (an edge-contact score), does it repeat shapes you just saw (a bag-style anti-repeat penalty)?
Classify candidates into buckets — easy / medium / hard / rescue — and pick from a bucket distribution that shifts with the difficulty wave. On a dangerously full board, "rescue" hands (solvable and able to clear) get heavily favored: the game throws you a rope instead of a shovel.
The player never sees any of this. What they feel is: pieces mostly make sense, crowded boards get workable hands, and the game never flips the table.
3. The finisher: the "it handed me the perfect pieces" moment
Top games in this genre have a signature moment where a low board suddenly gets exactly the pieces that wipe it clean. I first tried to get this with random search — generate trays, check if any sequence fully empties the board. Over 2,000 simulated moves it fired approximately never. The space of board-emptying sequences is far too sparse to hit by luck.
The fix was to invert the problem: search for the sequence first, then deal exactly those pieces. When the board is low (≤28 filled cells), a best-first DFS looks for a sequence of up to three catalog shapes whose placements fully empty the board — pieces are allowed to grow the board mid-sequence as long as a later placement clears it, which is exactly how satisfying multi-line wipes work. Branching is capped and the whole search is node-budgeted, so it's a few milliseconds, not a solver stall.
findEmptyingSequence(board):
if board is empty: found it
score every (shape, position): clears first, low residue second
recurse into the top 10, depth ≤ 3, within a node budget
When a sequence exists, the tray you're dealt is that sequence. In bot diagnostics this took full board clears from roughly zero to one every couple hundred moves — and they cluster exactly where they should: when you've played well and kept the board low. The reward follows skill, which is the whole point.
4. Doesn't this make the game easy?
No — and this distinction is the design core. The guarantee is that a solution exists, not that you'll find it. Place pieces greedily and you'll wall yourself in; the solvable order was there, and you didn't take it. An optimal-order bot playing thousands of moves hits zero deadlocks — every game-over in Block Peak is attributable to a placement the player chose. "Fair" and "forgiving" are different words.
5. The boring-but-real part: doing this on a phone's UI thread
All of this runs synchronously between your placement and the next tray appearing — an async "cheap tray now, smart tray later" swap felt like the game was lying, so it had to go. That means budgets everywhere: candidate counts, DFS node caps, branch caps. On flagship phones the whole pipeline is a few milliseconds. On entry-level devices (think Samsung A10) the same search measured 50–150 ms — a visible hitch — so generation is now also wall-clock-boxed: it self-calibrates on the first slow refill and hard-caps the search afterwards, always keeping a minimum candidate floor so quality degrades gracefully instead of fairness ever being compromised.
Takeaways
- "Random with vibes" is how most block puzzles deal — and why they feel unfair. An exhaustive solvability check over (order × placement × mid-sequence clears) is cheap enough to run on every deal.
- Rare delight moments (the full-board wipe) can't be found by filtering random output. Construct them: search for the outcome, then deal its inputs.
- A fairness guarantee is a floor, not a difficulty setting. Existence of a solution ≠ finding it; skill stays load-bearing.
Block Peak is free on iOS and Android — fully offline, no account, no forced ads. It's built with Flutter; the whole generation pipeline is pure Dart. If you try it, I'd genuinely love feedback on difficulty pacing.
— Beycan, solo dev at NextFly
Top comments (0)