Inspiration
Arrow puzzles are a crowded genre and I had played a lot of them. The mechanic is excellent — one tap, instant feedback, no timer — but the levels are almost always the same rectangle of arrows, over and over. After fifty of them you are not solving anything new; you are doing the same thing again.
So the question became: what if the board itself were a picture? Not decoration around the puzzle — the puzzle is the shape. Level 9509 is a heart, 60×59, 2,291 cells, 204 arrows, and every one of them has to come out.
What it does
You tap an arrow. If nothing stands between its head and the edge of the board, it slides off. If something does, you lose a heart. Clear every arrow to win.
That is the entire rule and it is learnable in four seconds. The depth comes from the board: with 200 arrows on screen, finding the one that can move is the real puzzle.
- 20,000 campaign levels, every one a distinct board — 1,178 different board sizes, shapes running from hearts and animals to CJK characters and abstract lattices
- A daily challenge — 153 dated levels with a monthly trophy
- A 3000-level tournament season, where every arrow you bank is a point
- Six languages — English, Hindi, Spanish, French, German, Portuguese
- Hearts, hints and a path viewer, with rewarded video to earn more of each
- Remove Ads — a one-time purchase that also grants free hints on every level
How I built it
Unity 6 with C#, and a small Android library the game calls into for everything native. The board is drawn by hand rather than built from prefabs: every arrow is a stroked ribbon whose width is computed from the cell size, so a 60×59 board draws as cleanly as a 10×10 one.
Levels are generated offline, never at runtime. A Kotlin generator produces candidate boards across parallel JVMs; a Python pipeline rasterises each target shape at its own native size, fills it with arrows, and then proves the result. Solvability is decided by a topological sort (Kahn's algorithm) over the blocking graph — if a legal ordering exists, the level is clearable. That runs in milliseconds, where the exponential DFS I started with did not finish.
Levels ship encrypted. All 20,000 are one 11.6 MB AZLP pack: each level is
zlib-compressed and ChaCha20-encrypted under a content key, and that content key is
stored once per certificate in a keyring — so a build can be re-keyed without
regenerating a single level. The master key lives in native C, not in managed code.
Monetization is RevenueCat end to end. RevenueCat powers the Remove Ads purchase
and the entitlement that gates it, reached through purchases-hybrid-common — the
same SDK layer the official plugins sit on. AdMob still serves the ads, but every
impression is reported to RevenueCat's ad tracker, so ad revenue and purchase revenue
sit in one dashboard instead of two. Rewarded ads use server-side verification pointed
at RevenueCat, so a reward cannot be granted by a modified client.
OneSignal runs the win-back push campaigns, keyed to the player's Firebase uid so one person can be targeted across devices, and tagged with their level and language so a message can be segmented and localised.
Firebase covers analytics, Crashlytics, Remote Config, auth and the leaderboard — the tournament season opens on a Remote Config date condition rather than an app update.
Challenges I ran into
The bug that broke 4,103 levels. The rule is "an arrow leaves if nothing blocks its path" — and the app counts the arrow's own body as a blocker, because a long arrow can curl around and come to rest on its own exit ray. My offline verifier did not count it. The two disagreed silently, and 4,103 of 20,000 generated levels were unplayable while every check reported them fine. The fix was one line; finding it took days. There is now a regression test named exactly that: no arrow sits on its own exit ray.
Difficulty is not arrow count. I spent a long time tuning difficulty tiers by how many arrows a board carries. It was the wrong number. Because a legal move can never lose the level, any board can be brute-forced by trying arrows one at a time — so difficulty is how long it takes your eye to find a legal move, not how many arrows exist. Re-deriving the tiers from scan cost instead of count fixed the ramp.
Shapes that are different puzzles but the same picture. Deduplication compares raw cell silhouettes at one fixed raster with a Jaccard threshold, and deliberately never rescales — rescaling makes a small circle and a large circle identical, which is exactly the collision you are trying to catch.
Rebuilding a live game in a different engine without losing anyone. The first
release was Flutter; the game is now Unity. The hard part was not the rendering, it
was the save file: thousands of people already had progress, streaks and a purchase.
So the Android library reads the old Flutter SharedPreferences once, on the first
launch of the new build — including both of Flutter's string-list encodings, the newer
JSON one and the older serialized-object one — and migrates it in place. An update
lands and a player's streak is simply still there. Nobody has to be told the engine
changed, which is the whole point.
Accomplishments that I'm proud of
Every one of the 20,000 shipped levels is verified: 0 arrows sitting on their own exit ray, 0 levels that cannot be cleared. That check runs by decrypting the shipped pack itself rather than trusting the generator's output — because the pack is the only thing a player ever receives.
And the shapes. Level 9509 is a heart you can recognise from across the room, and it is also a genuinely hard puzzle.
What I learned
The interesting constraint in a puzzle game is almost never the rule — it is proving that ten thousand instances of the rule are all fair. Most of the engineering here turned out to be verification, not gameplay.
Also: ad revenue and purchase revenue living in two separate dashboards quietly stops you reasoning about either one. Putting both behind RevenueCat changed what I could actually see about the game.
What's next for Arrows Go
Season 1 of the tournament opens on 1 October with a server-side leaderboard and seven collectibles — ranks 1/2/3, a flawless run, participation, half the season, and all 500 levels. After that, the second 20,000 levels: the pipeline is built and shape supply is the only bottleneck left.
Log in or sign up for Devpost to join the conversation.