Arrows is a calm puzzle game I’ve just built for the iPhone and the web. The board is full of arrow-shaped paths. Tap an arrow whose way out is clear and it slides off the board; tap one that is blocked and you lose a chance. Clear the board to win.
There are endless levels, a daily challenge, a league and seasonal events, and no server behind any of it. Every board is generated on the device from a seed, in under a second, so level 37 is the same puzzle on every phone. This is how it works. You can play it in your browser.
One seed, one puzzle, for everyone
Every board has a name that is also its seed: level:37, daily:2026-10-04, board:event:autumn-2026:181. The seed is hashed into a small random number generator, and every random draw in the game goes through a seeded one. Math.random is banned by a lint rule, because a single stray call would give two players different versions of “the same” level.
That determinism is what makes the rest possible without a backend: the daily challenge is the same for everyone, and a league can be simulated on each phone. It also makes content a contract. A test pins the fingerprints of a few levels, so a change to the generator that would silently change every player’s level N fails the build until it is made on purpose.
Making a board: partition, peel, measure

Generating a puzzle that is always solvable turns out to be easiest when the solution is built together with the puzzle:
- Partition. Random walks split the board’s cells into paths. Each walk prefers the neighbour with the fewest free neighbours (Warnsdorff’s rule), which leaves fewer stranded single cells.
- Peel. The solution is simulated forward on the full board. At each step, any path with an end whose ray to the edge is empty can leave; one is chosen, that end becomes its head, and the path is removed. Removing a path only ever frees cells, so this greedy process never needs to backtrack, and the order it produces is a solution. If nothing can leave, a path is cut so that one of its pieces can.
- Measure. A solver plays the finished puzzle and reports what a player would feel: how many arrows, how long they are, and above all how many arrows are free at each step compared with how many are left.
Difficulty is measured, not assumed. Each seed yields several candidates (seed#0, seed#1, …) and the one that lands in its tier’s band wins: Hard keeps the hardest of 10, Super Hard the hardest of 12. A calibration suite generates levels 1 to 400 in CI and fails if any tier drifts out of its band.
The numbers mattered. An early Hard event board had about 25 arrows, and I cleared it in 36 seconds: with that few, the next free arrow is found at a glance. So every board grew: a Super Hard level now has about 85 arrows of about 8 cells, with about 3 free at a time on average.
Advertisement
Events drawn as pixel art

Event boards are shaped like pictures. Each one starts as a tiny PNG in a fixed palette, which the board enlarges one and a half times: the 17 by 18 pixel maple leaf becomes a board of 26 by 27 cells. A small script turns the art into masks for the engine, and the generator partitions each colour on its own, so the maple leaf keeps its stem. CI fails if the art and the generated code ever disagree.
A league without a server
The league has 30 players: you and 29 characters, each derived from a seed of the league and the day, with a name, a daily score drawn around the league’s median and a few play sessions spread over the day. Their scores climb during each of those sessions, so the table moves through the day, with nothing to download. Another calibration suite simulates 120 days per league to keep promotion rates fair.
Small things that make it feel right
- Forgiving taps. On a big board a cell can be 12 pixels wide, half a fingertip. A tap on an arrow reaches 22 pixels around its centre, and if a free arrow is in reach, it plays, even if the one under the centre is blocked. A tap on an empty cell does nothing, so sweeping the gaps finds no free arrows.
- Honest hints. A hint costs a rewarded ad. If no ad can be shown, or you are offline, the game says so instead of failing quietly, and everything else works without a connection.
One code base, three builds
The engine is plain TypeScript with zero dependencies and no DOM, tested with Vitest. The app is Vite, vanilla TypeScript and SVG, with no UI framework, and the same code is built three ways:
- iOS: wrapped with Capacitor and built on a macOS runner in GitHub Actions, then uploaded to TestFlight with fastlane. I work on Windows; there is no Mac in this story.
- Web: arrows.jonathas.net, published to GitHub Pages, installable, and playable offline thanks to a service worker.
- Development: the same build with a puzzle picker that opens any board by its seed.
Arrows is playable now on the web and on its way to the App Store. If you try it, I’d love to hear which tier you reached.


