The same board for everyone, with no server

Every player is supposed to get the same puzzle on the same day, and that is the entire point of calling it a daily challenge. Doing it on a static site with no backend turned out to be almost free. It also came with one flaw. We decided to live with it.

What a daily challenge is actually for

The appeal is not the puzzle. What people want is the shared reference, the knowledge that everyone they know opened the same board this morning, because without it a score is just a number with nothing to measure itself against. You are not competing against a global average; you are competing against your colleague, who played the same board at lunch and will bring it up tomorrow whether you want them to or not, and again on Friday if it went their way. That is the feature, not a side effect of it.

Delivering it usually means a server. Generate a puzzle, store it, serve the same one to everybody for twenty-four hours, repeat tomorrow, and all of that machinery exists purely so two browsers receive identical bytes on one day. We do not run a server, we did not want to start running one for this particular feature, and luckily the whole problem gets a lot softer once you notice that you never needed to store today’s puzzle in the first place, because deriving it on demand works just as well.

Deriving it instead of storing it

Enter the seeded generator. Give a pseudo-random generator the same starting number and it produces the same sequence of values every time, on every device, forever, with no memory of earlier runs and no coordination between them. So that is what we used. Instead of deciding what today’s stones are and then remembering the answer somewhere, we let the date be the seed and let the generator produce the stones on demand.

Our seed is simply the date written as a number: the year times ten thousand, plus the month times a hundred, plus the day. On the 13th of October 2026 that is 20261013. Every player who opens the game that day computes the same seed, therefore gets the same sequence of spawned stones, and no coordination, storage or network request happens at any point in the process.

The generator itself is a well-known thirty-two-bit function: you hand it a seed, it gives you back a repeatable stream of values between zero and one. It is about six lines. We use the same function in our test simulations, for the same underlying reason, since a simulation you cannot rerun is not evidence of much.

Because the sequence is derived rather than remembered, the whole feature adds no infrastructure. There is a consequence, though, and depending on your game it is either dull or alarming: the puzzle for any date at all, including one five years from now, is knowable in advance by anybody who cares to run the same code, which is mildly unsettling the first time you notice it and completely uninteresting about ten minutes later. For a game with no prizes, we do not care. I find it faintly pleasing, honestly.

Remembering the result, per day

Scores go into the browser’s local storage under a key that includes the seed. A player’s result for one day is therefore a separate entry from the next day’s, and neither can collide with the all-time best sitting next to them, because all three live under different keys.

Putting the seed in the key matters more than it sounds. Under a single “daily best” key, today’s result quietly overwrites yesterday’s, and a player wanting to look back at a good run finds it already gone, which is exactly the kind of quiet data loss that takes a week of confused emails before anyone notices the pattern. Keying by seed gives each day its own record, and costs nothing.

It also degrades without drama. Private browsing, a full quota, a browser configured against it: if storage is unavailable the game stops remembering. Nothing throws, nothing breaks, the player just plays without a record, and probably never realises anything was missing. We wrap every storage access so a failure is a no-op rather than an exception.

The flaw we decided to live with

Here is the flaw. The seed comes from the player’s local date, so a player in Tokyo and a player in London are not on the same day at the same moment, and for a window of a few hours they are quietly playing different puzzles, with no signal anywhere in the interface that this is happening to them.

The fix is obvious. Use UTC, and everybody’s date flips at the same instant. We have not done it. The reason is worth examining, because it is not simply laziness, though I am not going to pretend laziness plays no part in it either. The change is roughly one line of code, and we have written that line in a scratch branch twice, read it back, imagined a player in Perth watching their board flip in the middle of dinner, reverted it, and gone back to whatever else we were doing that week.

For most players, the day they care about is their own. A puzzle that resets at midnight local time matches how a day actually feels; one that resets at six in the evening lands in the middle of dinner and keeps surprising people, which is not the kind of surprise a daily game benefits from. Switching to UTC makes the “everyone has the same puzzle” property true at every instant, and pays for it with a reset time that suits very few of the people playing.

The shared experience we are protecting mostly happens between people who share a timezone. Colleagues, friends, people in the same house. Local dates serve that group better than UTC does, so we are trading a thin slice of global correctness for a better local one, and for a game nobody is betting money on I think that is the right call. I would not defend it to the death, and it remains a compromise. Add anything that compares scores between players and it immediately becomes the wrong compromise.

Why there is no global leaderboard

This is the honest consequence of having no server, and I would rather put it on the screen than bury it. A global leaderboard needs an authority that accepts and verifies scores. We do not have one. We could bolt on a third-party service, but that means sending player data somewhere else, taking on a dependency that can go down, and worst of all implying a verification step we cannot perform. Anyone can write to their own local storage, including you, right now.

So leaderboards in our games are explicitly local, the label on screen says exactly that, and there is no hiding behind a word that means something grander than what we actually built here. It is a narrower feature than the one people picture when they read “leaderboard”, and we would rather ship the narrow honest version than a broad one that quietly means nothing, even though the honest version looks less impressive in a screenshot.

There is a habit behind this that has served us. When the architecture cannot support something, design the feature around that limit instead of papering over it with somebody else’s service, because the paper tears at the worst moment and, unlike the limit itself, nobody will have planned for it. “No server” is not an obstacle to engineer around. It is a property of the game, and the features should be honest about it.

What we would reuse

← Back to the blog