Same game, different orientation: finishing Number Match — Development · Blupoli

A screenshot could not answer the important question

Number Match reached Blupoli Puzzles on September 27, 2026 with a property we cared about more than a polished grid: every generated starting board carried a constructive route that could clear it. Issue #239 and PR #241 had defined the native game, persistence, Undo and Redo, hints, Add numbers, onboarding, statistics, achievements, localization and a long-form guide. The first delivery closed that scope and passed its verification. The next day a better question arrived than any checklist could provide: can the game actually be completed?

The doubt made sense. A screenshot can show a 9 without an obvious 1 nearby, and anyone remembering only the “make ten” half of the rules can read that 9 as stranded. Number Match has a second relationship, however: equal numbers are a valid pair too. A 9 may disappear with a 1, but it may also disappear with another 9. Explaining the rule was necessary, yet it was not enough. We needed to revisit the generator, identify what the tests genuinely proved, and separate the mathematical question from two product problems the review exposed at the same time: the four “levels” were acting as both size and difficulty, and the mobile layout was still forcing nine visible columns.

Issue #280 and PR #281 came out of that second pass. The goal was not to rewrite Number Match or replace a broken engine. It was more useful than that: determine which parts of the original implementation were already sound, find the concepts we had coupled unnecessarily, and make the public contract match what the code could actually demonstrate.

First, define what “solvable” means

The Number Match generator does not scatter digits randomly and hope useful combinations appear. It builds nested blocks. An outer pair wraps one or more inner pairs; the stored clearing sequence starts at the inside and works outward. When an inner pair disappears it leaves empty cells between the next pair of endpoints. Those endpoints were created either as equal values or as complements that add to ten. By construction, the route eventually becomes open under exactly the same rules the player uses.

Several blocks are concatenated to form the complete board. This gives us a way to change structural depth without changing the game rules and it also gives us a direct proof procedure: walk the constructive pairs in stored order, ask whether each pair is legal in the current state, clear it, and require zero active cells at the end. The revision now wraps that check in followsConstructiveSolution. It does not use privileged generator knowledge to remove cells. It calls canMatch, the same pure rule function used to decide whether a player selection is legal.

There is an important boundary around that guarantee. Proving one complete route from the starting position is not the same as proving that every arbitrary sequence of legal player choices must eventually win. Number Match can offer alternative moves, and one choice may alter later connections. Add numbers is part of the rules for positions with no available pair, while Undo exists because exploring another branch can be useful. What we can demonstrate is precise: the generator does not publish a starting board without a known complete route. A stronger claim about every reachable state would require a different proof, and we do not have that proof.

That distinction belongs in editorial copy as much as in code. “The generated board has a constructed, verified clearing route” is useful and testable. “You can never make a bad strategic choice” would be a different promise. This revision makes the first statement stronger rather than quietly drifting into the second.

A reassuring test name is not the same as broad evidence

The first release already had tests. One unit test generated a deterministic board for each difficulty and followed its constructive sequence, and one browser test did the same through the real interface. That coverage caught genuine integration problems during the original PR and showed that the mechanism worked. Once the review asked “is it actually completable?” while the variant space was about to expand, four representative cases no longer felt proportional to the question.

The new unit matrix walks all four sizes, all four difficulties and twenty seeds for every combination: 320 unique starting boards. Each case must be deterministic, contain the expected number of cells, expose at least one starting move and, most importantly, complete its entire constructive sequence through canMatch until no active number remains. Generating the same case twice must also yield the same structure. The internal selfTest samples the whole matrix again and checks that average nesting depth increases monotonically with difficulty.

We are not pretending that “many seeds” is a mathematical proof over every possible integer seed. The constructive method provides the underlying guarantee; the matrix attacks implementation mistakes in parameterization, block offsets and combinations that a single example might never exercise. Those are two different kinds of evidence and they reinforce each other: reasoning about the construction and repeatable execution over a broader surface.

A Playwright solve test remains important as well. It opens the real page, clicks the constructive pairs through the DOM and waits for the shared completion state. That covers a bridge the pure core cannot: generator indices must arrive at the right buttons, the UI must clear the same cells, and solved() must fire when the active count reaches zero.

The old “four difficulties” were hiding two dimensions

The audit exposed a more conceptual problem. The original core had four configurations named Easy, Medium, Hard and Expert. Each increased two things at once: pair count and average nesting depth. The visible result was 9×4, 9×6, 9×8 and 9×10. It worked technically, but it made the product model unnecessarily rigid. Choosing Expert also meant accepting the longest board, and asking for a short board meant lowering the difficulty even though those choices do not have to mean the same thing.

Blupoli already had a shared vocabulary for games that expose both dimensions. Numberlink, for example, declares sizes and difficulties independently. The achievement engine also understands size and difficulty as separate domain dimensions. Keeping Number Match outside that pattern forced the metagame to treat geometry as if it were a difficulty measure.

The revised core makes the distinction explicit. SIZES contains 9×4, 9×6, 9×8 and 9×10, with each size deciding the number of starting pairs. DIFFICULTIES keeps Easy, Medium, Hard and Expert, but now its job is to define a target nesting depth. The generator derives the number of constructive blocks from that target. More blocks for the same pair count make shorter, more local structures; fewer blocks create deeper chains.

That creates sixteen meaningful combinations without multiplying rule engines. A 9×4 Expert board can be compact but structurally deep. A 9×10 Easy board can ask for more scanning while keeping its nested chains shallower. The player can choose how much board to manage and how much look-ahead to invite. More importantly, each selector now says what it actually controls.

Achievement integration became a real domain instead of a checkbox

Number Match already had three special achievements in content/achievement-specials.json. “No expansion” is signalled when a game finishes without Add numbers, “At a glance” when it finishes without hints, and “No turning back” when Undo and Redo were never used. The engine already emitted those three signals at completion, so that part of the first integration was genuine and did not need replacing.

The generic mastery domain did change. The shared achievement engine inspects the variants declared by a game. When it sees several sizes and several difficulties, it builds the product of those dimensions to know which combinations exist. Before this revision Number Match declared only difficulty, so its domain contained four cells. It now contains sixteen. The Number Match test asserts domainDefinition(game).cells.length === 16 so a future metadata edit cannot silently collapse that surface again.

This feeds generic Explorer, Challenge and Domain-style progression without adding Number Match-specific branches to the achievement engine. That is the kind of integration we want from a platform: the game describes its surface correctly and shared systems derive behaviour from the contract. The less conditional code we need with a puzzle name embedded in it, the more confidence we have that the abstraction is doing useful work.

Statistics needed both dimensions to travel with the session

The original engine already called updateSession and solved(). It published moves and hints, inherited duration from the host, and provided a size string derived from nine columns and the starting rows. That is why basic statistics existed from day one. The limitation was subtler: while size and difficulty were the same choice, the size value could not represent an independent player decision.

The revision carries size: 9xN and difficulty separately from initialization through persistence and completion. Puzzle identity now includes size, difficulty and seed, avoiding an overly broad identifier where the same seed string could refer to distinct variants. Completion metadata includes size alongside duration, moves, hints and difficulty.

The shared record system already groups metrics by dimensions including size and difficulty. We did not need a custom “Number Match statistics” feature. We needed to feed accurate dimensions into the existing contract. This is less visible than adding a chart, but it prevents two runs that only look comparable from ending up in the same personal record bucket.

The first responsive solution preserved logic but not the best shape

The original PR made a conservative decision: keep nine columns visible at every width and use internal scrolling when the board grew. That was safe because the DOM matched the logical grid exactly, and the mobile E2E encoded the decision literally: at a 390×844 viewport it expected nine CSS columns.

The product review changed the criterion. A 9×4 board is strongly landscape-shaped. Squeezing nine columns into a phone wastes the abundant vertical dimension and makes touch targets smaller than they need to be. The request was easy to describe: keep the same board, but turn it ninety degrees. The hard part was taking “same” seriously.

We could not regenerate with four logical columns on mobile. That would change rows, diagonals, reading-order boundaries and every index. Copying the numbers into a second phone-oriented model would be worse: two states would have to stay synchronized and history would need a translation layer. The right solution lived above the data. The logical grid remains nine columns. Only the CSS grid swaps axes when board shape and viewport shape make that presentation more useful.

Rotate the view, not the game state

On a logical nine-column, four-row board, index zero remains index zero. canMatch still calculates its row with Math.floor(index / 9) and its column with index % 9. Persistence stores the same cells array. History clones the same array. None of those layers needs to know whether the screen is narrow.

Rendering does know the current logical row count. It exposes that number as a CSS property and classifies the board as logically wide, tall or square. On mobile, a logically wide shape presents its logical rows as visible columns and its nine logical columns as visible rows. Grid direction completes a real ninety-degree turn while each cell restores normal text direction so digits remain upright. On desktop, a logically tall board can use the inverse presentation and put its long axis across the wider surface.

Straight relationships stay straight under the rotation: a logical horizontal line becomes a visible vertical one, while a logical vertical relationship becomes horizontal. Diagonals remain diagonals. We also remap keyboard arrows. Once the view is rotated, Left and Right must follow the axis the player sees as horizontal even though that motion corresponds to moving between logical rows internally.

When Add numbers changes the board's aspect ratio, rendering recalculates the shape. A board that is no longer logically wide can return to its natural orientation on mobile. That avoids a position with many appended rows growing sideways without bound on a narrow screen. The orientation may change when the geometry changes, but the underlying indices and history never do.

Saved-game compatibility is part of “the same board” too

Adding a new dimension to state could have created a quiet regression: games saved between the September 27 release and this September 28 revision might have become invalid. The shared state store checks each engine's state version, so simply bumping the Number Match version would have discarded snapshots that no longer matched.

For such a recent format, that might have been tolerable, but it was unnecessary. The Number Match snapshot keeps its existing version and adds sizeRows as an optional field. When an older snapshot has no explicit size, the engine can infer it from a relationship that was guaranteed by the old implementation: Easy meant four rows, Medium six, Hard eight and Expert ten. The next save writes the explicit size. This is not an attempt at indefinite compatibility with unknown historical formats; it is a small migration we can derive without guessing.

URL parameters follow the same principle. If a requested seed, size or difficulty conflicts with the stored snapshot, the engine does not merge incompatible pieces. It clears that resume candidate and generates the requested variant. Throwing away an incompatible resume is safer than showing “9×4” in the selector while the saved cells belong to another size.

Responsive behaviour needed an identity test, not only geometry assertions

A responsive test that measures only width and height can pass while the application quietly changes the game. The new E2E therefore opens a 9×4 board at a desktop viewport and captures the text content of every cell. It then shrinks the viewport to 390×844 and reads the DOM again. The arrays must be identical. Only after proving content identity does it check that the visible grid has four columns, nine rows, and greater height than width.

A second geometry case opens 9×10 on desktop and expects the inverse presentation: ten visible columns, nine rows, and a longer horizontal axis. Together those cases encode the design intent without creating a second engine. If a future CSS cleanup restores nine visible columns on a narrow 9×4 board, Playwright will notice. If a future implementation regenerates on viewport changes, the cell-content comparison will notice too.

The resume test completes the triangle. It removes one pair, reloads the page, requires exactly two cleared cells and uses Undo to recover both endpoints. The new size dimension therefore does not merely exist in metadata. It survives storage and coexists with the same shared history semantics as before.

Blog and Devlog are different editorial integrations

The audit found that Number Match already had a substantial Blog guide in Spanish and English, with cover art, an infographic, SEO metadata and internal links. It also found no dedicated Number Match Devlog entry. We did not solve that by copying the guide and changing its heading. The Blog teaches the player how to read equal pairs, complements, gaps and future openings. This article exists because the technical story is different: a solvability guarantee with precise limits, a size/difficulty coupling, a responsive requirement that cannot alter topology, and platform integrations that should be demonstrated rather than assumed.

The Blog guide is updated so it no longer preserves false documentation. It no longer says Easy means four rows or that a phone must display nine columns across the screen. It explains the four independent sizes, four difficulties, and the idea that nine logical columns remain true even when the visual representation turns. The guide links here for readers interested in engineering; this Devlog links back to the Number Match guide so strategy and implementation do not compete for the same paragraph.

This story also connects naturally to Generating is not solving, because we are again separating a property we can prove from an output that merely looks plausible, and to Progress starts when you play, because size and difficulty only matter if they reach the shared session contract correctly.

What is actually integrated after this pass

An inventory is more useful than saying “everything is wired up.” Number Match runs inside the native game host and its shared session lifecycle. It saves snapshots and restores history. It publishes moves, hints, size, difficulty, puzzle identity and board metadata. Completion goes through solved(), feeding the common game result. The catalogue declares four sizes and four difficulties so progress and achievements understand the domain. Three measurable special signals remain connected. Onboarding and game copy exist across the six supported locales. The player-facing Blog guide exists in Spanish and English and now describes current behaviour. The missing technical Devlog is this entry.

The inventory also makes limitations visible. The constructive route proves the generated starting board has at least one complete clearing path. We have not added an exhaustive solver that certifies every continuation after every possible human decision. Add numbers remains a game rule, not a universal proof that an arbitrary state must recover after a finite number of presses. If we ever want to make that stronger claim, it should become a separate feature with a separate proof.

Being explicit about boundaries creates more confidence than attaching “guaranteed” to an undefined concept. We now know what the generator guarantees, where that property is checked, how a variant is represented, which shared systems consume those dimensions, and which transformation belongs only to presentation.

The broader lesson: responsive geometry and solvability belong to different layers

This revision ended up touching generation, UI, persistence, statistics, achievements, tests and editorial content. The architecture stayed understandable because those layers kept distinct responsibilities. The core decides which pairs exist and whether a route is legal. The engine stores the game and publishes dimensions. CSS decides how to orient the same geometry for the device. Progress consumes dimensions without knowing Number Match internals. Blog and Devlog tell different stories to different readers.

The tempting response to a visual complaint would have been changing the mobile column count. The tempting response to a solvability question would have been adding reassuring copy. Neither addresses the right layer. A board can be mathematically solvable while its size model is incomplete and its mobile presentation is awkward. Separating those questions let us fix each one without weakening the others.

That leaves Number Match in a more useful position for future work. Size density can be tuned without redefining difficulty. Difficulty can be calibrated without changing physical dimensions. Layout can evolve without migrating logical boards. Achievements can measure 4×4 coverage through the common engine. And when someone asks again whether a generated board starts with a solution, the answer no longer depends on reassurance: the generator retains the route and the tests walk it.

Return to the board with a better question

After all this engineering, the player's gesture is unchanged: look at two numbers and decide whether they can disappear. That is a good result. The additional complexity lives behind selectors, storage and presentation instead of sitting on top of the core rule.

If you see a 9 without a 1, look for another 9. If no pair is obvious, remember that equality and making ten are only the first layer; the path matters too. And when the board looks tall on a phone and wide on a desktop, you are not looking at two generated puzzles. You are looking at the same sequence, seed and history presented in the orientation that uses each screen more effectively.

You can try the result in Number Match on Blupoli Puzzles. For player strategy, the full guide goes deeper into pairs, gaps, row-order scanning and planning. This Devlog remains the record of the other half: what we had to prove and separate before “the same board” really meant the same board.

Number Match diagram separating logical topology, responsive presentation, sizes, difficulties and automated checks
The revision separates four responsibilities: rules, orientation, variants and repeatable evidence.