The Context: "A Score of 4,182,000"
FRAGMENT: PROTOCOL ships a daily challenge. Every player worldwide gets the same run that day — same seed, same map, same enemy placement — and submits their score when they finish. That design is only interesting if the leaderboard is real.
In September I noticed a score of 4,182,000 submitted at 03:14 UTC. The maximum theoretically achievable score, to the digit, entered by an account created nine days earlier, from a first-time player.
The immediate question was how. The answer turned out to be embarrassingly simple: the daily seed was a client-side hash of the date string, and nothing about the run was verified server-side. Any player could post any number to the endpoint. The leaderboard was a suggestion.
The Problem Statement
Make a fully client-side game produce a leaderboard that is worth caring about, when the entire simulation — rules, physics, spawns, damage — runs on a machine the server does not control:
- Reproducibility: The server must re-simulate a submitted run from its seed and input tape and get the same result.
- Uniqueness: One score per player per day, decided by the first valid submission.
- Availability: The game is playable offline. Verification must work when the server is unreachable.
- Honesty: A client cannot modify the seed, the rules, or the score.
Visual Architecture
The Core Logic / Process
1. The Seed Must Be Public, Predictable, and Harmless
The daily seed is derived from the UTC date so that every player, in every timezone, gets the same run:
private static hashString(str: string): number {
let hash = 2166136261; // FNV-1a 32-bit offset basis
for (let i = 0; i < str.length; i++) {
hash ^= str.charCodeAt(i);
hash += (hash << 1) + (hash << 4) + (hash << 7) + (hash << 8) + (hash << 24);
}
return hash >>> 0;
}
This is FNV-1a: fast, well-distributed for short strings, and — critically — entirely public. A player can compute today's seed in three lines of JavaScript, and that is fine. The seed being knowable is not a vulnerability; the vulnerability would be the client being trusted with the result.
The danger to avoid is deriving anything secret from this value. It is printed in the URL bar of every daily run. It is not a credential.
2. One Seeded Stream, Advanced Per Tick
All randomness in the simulation comes from a single SeededRandom instance seeded once from the daily seed:
export class SeededRandom {
private state: number;
constructor(seed: number) {
this.state = seed;
}
next(): number {
this.state |= 0;
this.state = (this.state + 0x6d2b79f5) | 0;
let t = Math.imul(this.state ^ (this.state >>> 15), 1 | this.state);
t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t;
return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
}
}
Mulberry32 — four lines, no dependencies, excellent statistical quality for gameplay, and fully specified in IEEE-754-free arithmetic. It is exactly the right primitive here.
The rule that makes determinism actually hold: advance the stream a fixed number of times per simulation tick, regardless of what happened. Not once per roll, not once per event. Otherwise the stream position depends on execution path, and two runs with the same seed but different luck diverge immediately:
// Wrong: stream position depends on how many random rolls the player caused
if (crit) {
const r = rng.next();
applyBonus(r);
}
// Right: every tick consumes an identical amount of the stream
rng.next();
rng.next();
rng.next(); // reserved draws, always
Reserving draws is unglamorous and it is the single thing that most reliably breaks deterministic games.
3. The Run Is a Receipt
A run is submitted as a receipt, not as a score:
interface RunSubmission {
seed: number;
version: number; // simulation build version
inputTape: Input[]; // (tick, actor, action)
claimedScore: number;
}
The server does not believe claimedScore. It rebuilds the authoritative simulation from seed, feeds inputTape through it tick by tick, and reads the score off the resulting state. If the final state does not match, the submission is rejected.
This is only possible because the verifier runs the same deterministic code the client did. The verifier cannot be a reimplementation — that is the trap. Two implementations of the same rules will disagree on some edge case eventually, and the disagreement becomes an unexplainable rejection that looks exactly like cheating.
4. Ordering and Idempotency
Two more rules, both learned the hard way:
- Strict improvement, not last-write-wins. The leaderboard stores the best score per
(seed, player). A lower score never overwrites a higher one, which makes the endpoint idempotent and safe to retry. - First valid submission wins. Once a score is recorded, the player can play again for fun but the leaderboard keeps the original. Otherwise a player could bank a good run, then experiment, then re-submit.
5. Offline Play Without Breaking Verification
The game must work with no network. DailySeedAPI checks localStorage first, falls back to the server, and finally computes the FNV-1a hash locally:
const cacheKey = `fragment_daily_seed_${todayStr}`;
// 1. localStorage cache 2. server 3. local FNV-1a fallback
All three paths yield the same seed, because all three derive from the same date string. Offline runs are verified identically when the player reconnects — the seed was never the thing the server needed to trust.
The "Baigan" Take (Expert Insights)
Here is the uncomfortable truth I had to accept: for a purely client-side game, the server can verify determinism but it can never verify reality. A player running a modified build that changes the physics consistently will produce a tape that re-simulates cleanly and a score the verifier accepts.
baigan ka anti-cheat is the belief that "re-simulating on the server makes the game cheat-proof." It makes it drudgery-proof. It eliminates the overwhelming majority of cheating — casual score edits, dev-console pokes, resubmission of inflated numbers — at effectively zero cost. It does not stop someone who patches the client and re-runs both sides consistently.
And that is the correct trade-off, because the alternative costs vastly more than it returns. Full attestation means shipping signed binaries, running the authoritative simulation remotely, and accepting the latency of cloud gaming. For a 15-minute roguelite, all of that is a catastrophic downgrade in exchange against an attacker population of maybe two dozen people.
What actually moved the needle:
-
Server-side simulation authority. The instant the score became a computed result rather than a submitted number, the 4,182,000 entry became impossible. Not because I made cheating hard — because the server now simply had no reason to believe anyone.
-
Version pinning. I found replay submissions recorded under a build where a boss had 3% less health, producing scores that no longer re-simulated. Without the
versionfield I was rejecting legitimate runs as cheating. This is a data-modeling bug that presents as a security problem, and it is far more common than deliberate cheating. -
Rate limiting per route. A trivial in-memory limiter (30 writes/minute/IP) attached to write endpoints cut the noise from obvious scripts to roughly nothing. Cheap, effective, and I should have had it from day one.
The general principle, which I now apply to every offline-capable system: the client is a rendering of authority, never a source of it. Compute the answer where you control the compute, even if that means running the whole simulation twice.
Performance Checklist
- Derive the daily seed from the UTC date with a public, well-distributed hash (FNV-1a).
- Never treat the seed as a secret — it is visible to every player by design.
- Seed a single PRNG stream once per run; reserve a fixed number of draws per tick.
- Submit
{ seed, version, inputTape }, never a bare score. - Re-simulate server-side with the identical build, not a reimplementation.
- Store the best score per
(seed, player); never let a lower score overwrite. - Commit the first valid submission for the day; later runs are unscored.
- Pin the simulation
versionand re-verify old submissions only against matching builds. - Cache the daily seed locally with server-then-local-hash fallback for offline play.
- Rate-limit every write endpoint; it removes more attack volume than any cleverness.