PrimalityGame: a snake puzzle built around prime numbers
I had a weird idea once: what if prime numbers were the core mechanic of a game? Not just "collect the prime number on screen" but something where understanding primality actually changes how you play. So I built it.
PrimalityGame is a Java/LibGDX puzzle game where you move a snake-like character across a grid (15 columns wide, 9 rows tall) collecting checkpoints. The interesting part is that your body length has to be prime at the exact moment you step on one. It sounds simple. It's not.
Rather than describe it and make you imagine it, here is the actual game. Play a round or two and then come back, the rest of the post will make a lot more sense.
Play it right here (WebAssembly)
This is the actual Java/LibGDX project compiled to WebAssembly (via TeaVM), running entirely in your browser. No plugins, no server round-trips, no downloads.
First load pulls a ~3.7Â MB .wasm module. If your browser lacks WebAssembly-GC it silently falls back to a JavaScript build.
How to play
The very first thing you see is a welcome dialog. Close it, and you are the single blue block sitting on the grid. The green (and yellow) blocks scattered around are the checkpoints you need to collect.
Controls:
- Click or tap a cell right next to your head to move there. You can only move one cell at a time, up/down/left/right, never diagonally. You can also press and drag to move several cells in a row.
- Click on your own head to toggle a hint. It highlights how far you can travel in each of the four directions and, more importantly, what your body length would be if you stopped at each wall. This is the single most useful thing in the game once you understand the scoring, so use it.
- Press R at any time to restart the current level from scratch.
- Press I to let the built in solver take a stab at the level (it is a debug tool more than a feature, but it is fun to watch).
The goal of a level: collect every checkpoint. The moment the last one is gone you win the level, a "you won" message flashes, and after a few seconds the next (harder) level loads automatically.
The catch: every move grows your body by one block, and a checkpoint only counts when your body length is a valid number as you step onto it. On a normal level "valid" means prime. If your length is not valid, you walk straight through the checkpoint as if it were empty floor. Nothing happens, and you have to loop back around and try again with a better length.
So the entire game is about arriving at the right place with the right length. Walking straight to the nearest checkpoint almost never works, because the number of steps to get there is rarely the prime you need.
The rules, precisely
Now that you have played, here is exactly what the code is doing on every move (this is straight out of Primality.java).
- Your length is tracked as
relativeCount. It starts small and goes up by one per block you place. After you successfully collect a checkpoint it resets (to 2) so the next checkpoint has its own fresh prime target, rather than one giant running count. - Stepping onto a checkpoint runs a primality test on
relativeCount. If it passes, the checkpoint is collected, your score goes up, and the game drops a little cluster of dark barricade blocks around where that checkpoint used to be. Those barricades are now walls. This is why the board gets more cramped the longer you play a level. - If the length is not prime, nothing is collected. You occupy the cell and keep going. No penalty beyond the wasted steps.
- Every single move triggers a reachability check. The game runs a breadth-first flood fill from your head across all non-blocked cells and asks: can I still reach every remaining checkpoint? If the answer is no (your own body or a barricade has sealed one off), the level ends immediately. This is deliberate: it means that as long as the game is still running, a winning path still exists. The pressure is all about the prime routing, never about accidentally trapping yourself without realising.
- Win condition: the checkpoint list is empty. Lose condition: a checkpoint became unreachable.
Your best score is saved between sessions (in the desktop build it went to an online database, the WebAssembly build keeps it in your browser's local storage instead).
Why prime numbers are surprisingly fun as a mechanic
Here is what makes this work: primes are irregular. If they followed a predictable pattern you could just route yourself mechanically. But because the gaps between primes grow and shrink unexpectedly (2, 3, 5, 7, 11, 13... then suddenly 17, 19, 23, 29), you end up backtracking or taking longer detours to land on a valid body length. The unpredictability of primes directly translates to unpredictability in the puzzle.
It also means every level feels different even though the rules are always the same. The randomized checkpoint placement combined with the prime constraint creates a different puzzle every run.
A quick refresher on prime numbers
A prime number is any integer greater than 1 that has no divisors other than 1 and itself. 2, 3, 5, 7, 11, 13 are all prime. 4 is not (it's 2×2). 9 is not (it's 3×3). 1 is not prime by definition (it would break a lot of nice theorems if it were).
One of the oldest and most beautiful algorithms in computer science is the Sieve of Eratosthenes, invented roughly 2200 years ago. It finds all primes up to some limit N by starting with every number and crossing out multiples:
- Start with all numbers from 2 to N
- 2 is prime. Cross out every multiple of 2 (4, 6, 8...)
- 3 is prime. Cross out every multiple of 3 (6, 9, 12...)
- 5 is prime (4 was already crossed out). Cross out multiples of 5...
- Repeat until you reach √N. Everything left standing is prime.
Here it is live:
The game precomputes the first 100 primes (up to 541) using this exact sieve, then uses trial division against that list to check primality at runtime. For the numbers involved in a single level it's more than fast enough.
Let's play it out mentally
Say you start a level with 3 checkpoints. You are at position (0, 0), the first checkpoint is at (2, 3). The shortest path there is 5 steps. 5 is prime, so you can collect it with body length 5.
After collecting, your relativeCount resets to 2 for the next checkpoint. The second checkpoint is at (7, 1). You need to walk 7 steps minimum. 2 + 7 = 9. Not prime. So you have to take a detour: one extra step to make it 10 or two extra steps for 11 (which is prime). You reroute, add those steps, arrive at 11, collect it.
By the third checkpoint you're tracking at the same time: body length, position, remaining empty cells, and whether any valid prime length still lands on a reachable cell. This is the kind of planning that makes it fun.
Difficulty scaling through primes
As levels increase, more checkpoints appear on the board. But the number of checkpoints is not just "level + 1". The game uses the sequence of prime numbers directly to pick the count:
n = PrimeFactory.primes[(int) Math.sqrt(gameInfo.getLevel())]
So at level 1 you get 2 checkpoints, at level 4 you get 3, at level 9 you get 5, at level 16 you get 7... The prime sequence controls the jumps, which means difficulty grows non-linearly in a way that feels natural rather than a fixed "add one per level" ramp.
The bonus levels: now it's modular arithmetic
After level 1, the game occasionally triggers bonus levels based on a congruence condition. Specifically, a bonus fires when:
gcd(level, absoluteCounter) == 1
(where absoluteCounter is your cumulative score across all levels)
On a bonus level, instead of checking if your body length is prime, the game checks if it satisfies a congruence relation:
relativeCount ≡ res (mod m)
The values m and res are computed deterministically from the level number, so each bonus level has a different "target residue" you need to hit. Modular arithmetic tells you that the valid body lengths form a regular pattern (every m steps you get another valid length), which changes the routing strategy completely.
If you want to play with modular arithmetic a bit:
Notice how the valid lengths (highlighted in blue) form a perfectly regular pattern. That predictability is both easier to plan for and harder in a different way: the gaps between valid lengths can be large, forcing long detours.
How the pathfinding works
Every single move you make, the game runs a BFS from your current position to every remaining checkpoint. If any checkpoint becomes unreachable (because your body blocks the only path to it), the game declares it over immediately.
This is genuinely important for the game to feel fair. Without it, you could get yourself into unwinnable states without realising it, and that's frustrating rather than challenging. With it, you know that if the game is still running, you still have a valid solution. The pressure is entirely about finding the prime-length path, not accidentally trapping yourself.
The BFS is a textbook implementation: expand all four neighbours, skip visited cells and body segments, return whether all checkpoints were reached. Nothing fancy, but it runs on every frame so it has to be fast enough. On a 15x10 grid with a few dozen cells to check, it's trivially fast even in Java.
Technical notes
The game is built with LibGDX, a Java framework that compiles to desktop (via LWJGL), Android, and the web from the same codebase. The playable version above is that same core compiled to WebAssembly with TeaVM: the Java bytecode is transpiled ahead-of-time to a WASM-GC module, libGDX's rendering runs on WebGL, and VisUI's Scene2D skin is bundled as classpath assets. There's a JavaScript build alongside it as an automatic fallback for browsers without WebAssembly-GC. The architecture follows the factory pattern pretty aggressively: PrimeFactory, GridFactory, TextureFactory, EvalFactory, DictionaryFactory — everything is a factory.
Interesting things about the implementation:
- Multilingual support (English, Spanish, Catalan) via a
dictionary.jsonfile (the web build is pinned to English) - An online leaderboard via a MySQL database on the desktop build; the WebAssembly build runs fully client-side, so it keeps your high score in browser local storage instead
- A hint system: click on the player head to see the reachable cells and steps to the next checkpoint
- A debug AI solver (press I) that tries to find the optimal path
- Body growth is animated with elastic interpolation over 2 seconds, which makes it feel satisfying rather than abrupt
The graphics are straightforward 2D sprites: a blue block for the player head, red blocks for the body, green/yellow alternating for checkpoints, dark blocks for obstacles. Nothing fancy, but it reads clearly at a glance.
What I learned from this
The interesting thing about using prime numbers as a mechanic is that it creates a constraint that's hard to game. You can't just count tiles and walk straight. You need to internalize the prime sequence and develop a feel for which lengths are "safe" to arrive at a checkpoint with. After playing for a while you stop thinking "is 17 prime?" and just know it.
It also made me appreciate how surprisingly deep the distribution of primes is. The fact that no one has found a formula for the n-th prime, that we still don't have a proof of the Goldbach conjecture, that the twin prime conjecture is still open... there's something fitting about building a game around them. The difficulty of the puzzle mirrors the difficulty of the math.
If I were to build a v2, I'd add a time bonus for arriving at a checkpoint with the smallest prime above your minimum possible path length, which would reward players who know their primes cold and penalize sloppy routing. Also audio. I completely forgot audio the first time around.
The full source is in Java/LibGDX if you want to dig into it. The core game logic lives in Primality.java and PrimeFactory.java, and the bonus level congruence system is in EvalFactory.java.
