Validating puzzle scores without storing a single puzzle
How a cozy dumpling game runs a global leaderboard on zero stored grids. By the person who drew the dumplings.
My game has a worldwide leaderboard, which means my game has cheaters. Not many. It's a gentle puzzle game where the pieces have blush cheeks, and the community is mostly lovely people comparing streaks. But a leaderboard is a promise, and you cannot keep a promise with an honor system, because the client is a liar. Every client. Yours, mine, everyone's. The first rule of leaderboards is that the phone in someone else's pocket is enemy territory.
The obvious fix is the expensive one: generate every puzzle on the server, store it, serve it, and check submitted solutions against the stored copy. Now you own a puzzle database, a cache layer, a sync problem, and a bill. For a solo dev whose entire backend is a handful of edge functions, that's a lot of infrastructure to babysit for grids of colored squares.
So Dumpling Galaxy stores zero puzzles. Not "few". Zero. Here's the trick, which is barely a trick at all.
A puzzle is just a number that grew up
Every board in the game is a pure function of an integer seed. Level 42 is not a row in a database; it's
levelSeed(level) = (level * 2654435761 + 88172645) >>> 0
fed into a deterministic generator with a tiny seeded PRNG. Same seed, same board, forever, on every device, in every runtime. The daily challenge works the same way, except the seed is hashed from the date string. Same idea for the three-minute blitz mode: the day key seeds a whole sequence of boards, identical for every player on Earth. That shared sequence is the competition.
The generator itself does the heavy lifting once: it places stars (sorry, dumplings), grows colored zones around them, then counts solutions and hunts for a board it can prove uniquely solvable, walking neighboring seeds until one passes. All of this is ordinary backtracking, fast enough that a phone does it in milliseconds.
And now the important part: the server imports the exact same generator module as the client. Not a port. Not a reimplementation kept in sync by discipline and prayer. The same JavaScript file, bundled into the edge function at deploy time.
What a score submission actually says
When you finish a level, your phone does not send "I scored 175, trust me." It sends something closer to a confession:
kind: level | daily
level: 42 (or the day key for dailies)
solution: [2, 0, 3, 1, ...] // one column index per row
secs, hints
The server regenerates board 42 from its seed, replays your solution against the real rules, recomputes the score itself from the verified inputs, and inserts the result with its service role. Row Level Security gives clients read access to the leaderboard and no write access to anything. There is no code path where a client-declared number reaches the standings. Blitz submissions are the fun extreme: the run's whole board sequence gets replayed server-side, grid by grid, and one wrong board voids the run.
A few numbers do have to be taken on faith, like your solve time. For those there are plausibility caps: a time longer than any human solve gets clamped, a device submitting two hundred scores a day gets ignored. You cannot make faith load-bearing, but you can make it cheap to withdraw.
The three places this bit me
Multi-solution boards. Above 9×9, uniquely solvable boards get rare, and scanning seeds for one would freeze the generator for seconds. So big boards ship with their full solution set, and validation accepts any member of it. Fairness means "a correct solution", not "my favorite correct solution".
Midnight. A daily solved at 23:58 arrives at the server at 00:01. The submission carries the day key of the board that was actually played, the server accepts a one-day window, and the daily seed is anchored to noon so daylight saving time can't split the world into two different puzzles. Time zones remain undefeated, but they can be contained.
My own unique key. This one's a proper confession. The submissions table had unique (user_id, day_key, kind, level), and the comment next to it proudly said "replays earn nothing". Read it again. With day_key in the key, it meant "replays earn nothing today". Come back tomorrow, replay level 1, get paid again. A patient player could ride the easiest board in the game up the all-time leaderboard. The fix was one partial unique index, (user_id, level) where kind = 'level', a dedup of the farmed history, and an announcement to the players that some ranks were about to tell the truth. They took it well. I'd like to think the blush cheeks helped.
The lesson I keep from that last one: your unique key is your spec. Whatever invariant it encodes is the rule your game actually has, no matter what the comment beside it claims.
Why bother, for dumplings
Because the alternative rots. A stored-puzzle backend drifts: the client updates its generator, the stored grids don't, and one Tuesday your players solve a puzzle the server has never heard of. Sharing one deterministic module means the client and the validator cannot disagree about what puzzle 42 is. The constants become sacred (change one multiplier and every historical submission stops validating), but sacred constants are a small price for a backend that cannot lie to itself.
Also, and I want to be honest about the engineering priorities here: it left more time for drawing the dumplings. If you'd like to check whether the validation holds up, the daily puzzle is free in your browser, and the full game is on iOS and Android. Bring a cross or two. The diagonals are where they get you.