Why SETPOINT games work with the server switched off

Most web games treat the network as load-bearing. The session opens with a handshake, the score submits synchronously, and the results screen waits on a response to tell you what you already know — that you lost. On a good connection nobody notices. On a train, it falls apart.

SETPOINT is built the other way round, on a rule short enough to hold in your head:

Every game must be fully playable with the backend completely unavailable. Any gameplay path that awaits a network call is a bug.

What the rule costs

Stated as a principle this sounds uncontroversial. Stated as consequences it gets much sharper, because it eliminates designs that would otherwise be reasonable.

The server cannot own game state. It can validate a result after the fact, but it cannot be the thing that decides what happens next. The daily puzzle cannot be fetched — it has to be derivable in the browser. Score submission cannot block the results screen. And there is no real-time multiplayer, because there is no honest way to build it under this rule.

That last one has a real price. A constraint that never costs you anything is not a constraint, it is a preference.

Deriving the daily puzzle instead of fetching it

Everyone should get the same daily puzzle, and nobody should get two by flying east. Both properties fall out of computing the day index in UTC and seeding a small deterministic PRNG with it:

export const EPOCH_UTC_MS = Date.UTC(2026, 0, 1);

export function getDayIndex(now = Date.now()) {
  return Math.floor((now - EPOCH_UTC_MS) / 86400000);
}

The subtlety is that this function exists twice — once in the browser, once on the server — and the two copies must agree exactly. The moment one reaches for a local-time getter, a player in Auckland and a player in Los Angeles are playing different puzzles while sharing a leaderboard.

So both carry the same warning comment, and the API’s tests assert the output is identical under several simulated timezones. A small thing that is very annoying to discover in production.

One file that can touch the network

A rule that lives only in a document gets broken during the first busy week. This one is enforceable instead, because the whole frontend has exactly one file that knows the backend exists: src/lib/api.js. Every function in it wraps a three-second timeout, catches every class of failure, and returns null rather than throwing.

The useful consequence is that checking compliance is a single search. If fetch( appears anywhere else, someone has bypassed the guarantee — and you see it in one command rather than in a code review.

Null and empty are different answers

One detail worth being pedantic about: a leaderboard that returns null and one that returns [] are not the same event. The first means we could not reach the service. The second means we reached it and nobody has scored yet.

Collapsing them produces the worst empty state in software — “No scores yet”, stated with total confidence, while the server is on fire. Keeping them distinct costs one branch and buys an interface that never lies about what it knows.