A calculator that gives you 2+2=7 and your friend 2+2=5 is just a broken calculator. Nobody shares it. Nobody argues about it. Nobody screenshots it and sends it to their group chat. It's random noise, and random noise has no social life.
That one observation is the entire product decision behind Wrongulator — a calculator that returns a wrong answer on purpose. The key word in that sentence isn't "wrong." It's "on purpose." Specifically: the same wrong answer, every time, for every person, on every device. 2+2=5, not 2+2=some number I decided three seconds ago and nobody else will ever see again.
This is what I mean when I say the wrongness has to be deterministic. The answer isn't random — it's deterministic random in JavaScript: unpredictable to a human, but perfectly reproducible from the input alone. And that single rule cascades into every engineering decision in the project.
Why Random Kills Virality
Think about how sharing actually works. Someone computes something on your tool. They find it funny. They send it to someone else. That second person clicks the link, or types the same thing, or shows their phone to a third person.
At every step of that chain, the thing they're sharing has to be the same thing. "Wrongulator says 2+2 is 5" is a shared fact — it's something two people can both verify and argue about. "Wrongulator said something weird when I tried it" is a personal anecdote. Personal anecdotes don't travel.
A Math.random() implementation breaks the chain at step two. The link reproduces, but it doesn't reproduce the joke. Worse: it randomizes the answer every load, so even the person who found it funny can't re-verify it five minutes later. The permalink becomes a lottery ticket. Lottery tickets don't go viral — lottery winners do, and they're rare by definition.
The naïve implementation is tempting precisely because it's easy:
// what not to do
function wrongulate(expression) {
const real = evaluate(expression);
const offset = Math.floor(Math.random() * 10) - 5;
return real + offset;
}
That works fine in a demo. In a product built around sharing, it's fatal.
The Wrong Engine: A Pure Function
The engine signature I settled on is:
f(expression) → { answer, reason, mode }
No database reads. No per-user state. No session, no cookie, no server call. The output is a pure function of the input — same input, same output, everywhere, forever. This is what allows a permalink like /2+2 to reproduce the exact result on any device, instantly, without any server-side computation beyond serving a static page.
This has a pleasant secondary consequence: the entire engine runs 100% client-side. Infinitely scalable, offline-capable, and free in the infrastructure sense. But the reason I built it this way wasn't performance — it was shareability. The pure-function property is load-bearing for the product.
FNV-1a + mulberry32: The Mechanics
The engine never calls Math.random(). Instead, it converts the expression into a seed and runs a seeded PRNG from that seed. Same seed, same pseudorandom stream, same decisions.
The hash function is FNV-1a (Fowler–Noll–Vo):
function hashStr(s) {
let h = 2166136261 >>> 0; // FNV offset basis
for (let i = 0; i < s.length; i++) {
h ^= s.charCodeAt(i);
h = Math.imul(h, 16777619); // FNV prime
}
return h >>> 0;
}
FNV-1a is not cryptographically secure, and it doesn't need to be. What it needs to do is produce a consistent, well-distributed 32-bit integer from a string — cheap to compute in the browser, no dependencies, easy to audit. It does that.
The seed from the hash feeds into mulberry32, a fast 32-bit PRNG:
function mulberry32(a) {
return function () {
a |= 0;
a = (a + 0x6d2b79f5) | 0;
let t = Math.imul(a ^ (a >>> 15), 1 | a);
t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t;
return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
};
}
Each call to the returned function advances the PRNG state and returns a float in [0, 1). The key insight: I need multiple independent decisions from a single seed — which wrongness mode fires, which meme entry the answer snaps to, which justification line gets picked. A simple hash % N gives me one number. A seeded PRNG gives me a stream of numbers, each determined by the same original seed but usable independently for separate choices.
The call pattern looks roughly like this:
const seed = hashStr(normalize(expression));
const rand = mulberry32(seed);
const mode = pickMode(rand()); // first draw: which wrongness flavor
const meme = pickMeme(rand(), realResult); // second draw: which meme to anchor to
const reason = pickReason(rand(), mode); // third draw: which justification line
All three draws come from the same seeded stream. Change the expression and all three change. Same expression on a different device and all three are identical.
Normalization: The Silent Layer
There's a step before hashing that's easy to miss and breaks everything if you get it wrong.
"2+2" and "2 + 2" are the same calculation. If they produce different hashes, they produce different wrong answers — and you've broken the reproducibility guarantee for anyone who types a space differently from the person who shared the link.
The engine normalizes the expression before hashing: strip whitespace, collapse repeated spaces, apply any platform-specific canonicalization. Only the normalized form gets hashed.
function normalize(expr) {
return expr
.replace(/\s+/g, "") // "2 + 2" → "2+2"
.toLowerCase(); // "2+2" stays "2+2"; handles any operator casing
}
The edge cases here are real. Leading zeros (007+1 vs 7+1), Unicode operator characters (a mobile keyboard might insert × instead of *), implicit multiplication — each is a potential crack in the determinism. Any expression that should logically produce the same result must normalize to the same string. Any normalization gap means two people with the same calculation get different wrong answers, and the share chain breaks.
Legibly Wrong, Not Arbitrarily Wrong
There's a product distinction between "wrong" and "funny-wrong." Pure random offset produces answers that are technically wrong but not interesting. The engine adds a layer called MEME magnetism — it makes the wrong answer feel earned.
The catalog contains 18 meme entries — culturally loaded numbers that carry their own joke (64, 42, 69, 420, 9+10=21, and so on). Each entry has a magnet radius. When the real result of an expression lands within that radius of a meme number, the answer snaps to the meme instead:
// MAGNET — if the real answer is near a meme, snap to it (feels earned)
let best = null,
bestD = Infinity;
for (const m of MEMES) {
if (m.n == null || !m.magnet || m.n === realRounded || !inLocale(m)) continue;
const d = Math.abs(realRounded - m.n);
if (d > 0 && d <= m.magnet && d < bestD) {
best = m;
bestD = d;
}
}
So 64+5 doesn't return 69 by coincidence — it returns 67, "the only correct number," because 67 is close enough to 69 to trigger its magnet. The joke is that the calculator almost got there. The laugh is in the near-miss.
Note also realRounded rather than the raw float. Floating-point comparison against meme integers requires rounding first — 64 + 4.9999999999 should compare against 69 the same way 64 + 5 does. The rounding happens before the magnet check, not after.
slug="technical-consultation"
text="Designing a shareable generator, parameterized page, or anything where reproducibility is a product requirement? One session often saves weeks of rework. €125 flat."
/>
The Accidentally Right Easter Egg
There's one more wrinkle: the engine includes a "one-in-a-million" path where it returns the correct answer with a MALFUNCTION justification — as if the calculator had briefly broken down and accidentally done its job.
Critically, this "accidentally right" outcome is itself deterministic. It's drawn from the same seeded stream. So for a given expression, either it's always the MALFUNCTION card, or it never is. You can share "I got the MALFUNCTION card on 314*159" and anyone can verify it. The rarity is real — but it's a stable, findable rarity, not a one-off that disappears on refresh.
This matters for collectibility. Random rare events feel arbitrary. Deterministic rare events feel like secrets to discover. The distinction sounds philosophical but it's the difference between "huh, weird" and "wait, can I find another one?"
Where Determinism Gets Hard
None of this is free. Determinism is a constraint that gives up several things most applications take for granted.
You cannot A/B-test the output. The wrong answer for 2+2 is fixed at engine compile time. If you decide later that 5 is funnier than 7 for that expression, you have to change the engine — which means every expression's output may shift. There's no way to run a controlled experiment without changing the contract.
You cannot personalize. The engine doesn't know who's running it. It can't give a first-time visitor a gentler wrong answer or a power user a more obscure joke. Same input, same output, for everyone. This is the whole point — but it also means no personalization layer at all.
Normalization gaps break silently. If your normalization misses an edge case, two users with logically identical expressions get different answers, and they never know why. There's no error. The wrong answer is just... different. This is the class of bug that's hardest to notice and hardest to debug after the fact.
The engine is a commitment. Changing the output for an existing expression means that anyone who shared a link to that expression now shares a link to a different joke. This is a real versioning problem. In practice I treat the meme catalog as append-only — new memes can be added at the end without changing the output for existing expressions, because they only fire if the engine's draw reaches them. But changing a meme entry's values, adjusting its magnet radius, or reordering entries can silently invalidate every shared link that happened to hit that entry.
The pragmatic answer: if you need to change the engine in a backward-incompatible way, version the permalink format (/v2/2+2) and maintain the old engine in parallel until the share half-life of old links has decayed. It's not elegant, but it's honest.
The Payoff
The pure-function property produces a cascade of secondary benefits that I didn't fully appreciate until after the fact.
Because the output is fully determined by the URL path, every result page is cacheable indefinitely. A CDN can serve /2+2 as a static asset — there's no per-user state, no database query, nothing to recompute. The cache-control header on the OG image endpoint is immutable, max-age=31536000 for exactly this reason: the same expression will always produce the same image, so there's no reason to ever re-render it.
The Hall of Fame leaderboard, which I'll cover in a later article in this series, also relies on this property: the server recomputes the answer from the expression before incrementing the count, so clients can't inflate a fake wrong answer into the rankings. Determinism isn't just a feature — it's a security primitive.
And permalink sharing — the whole product mechanic — only works at all because the function is pure. Without that property, there's no product. Just a broken calculator.
Results
| Metric | Value |
|---|---|
| Wrong Engine | Pure function — f(expression) → { answer, reason, mode }
|
| Hash function | FNV-1a, 32-bit, no dependencies |
| PRNG | mulberry32, seeded from hash — deterministic stream |
| Meme catalog | 18 entries with magnet-snap radius |
| Easter egg | 1-in-a-million "accidentally right" — deterministic, findable |
| Client-side | 100% — no server call for the engine |
| CDN cacheability | Immutable — same expression, same answer, forever |
| Normalization | Expression canonicalized before hashing (whitespace, operators) |
Takeaways
Virality requires reproducibility. A shareable result has to be the same result for the person sharing and the person receiving.
Math.random()breaks that contract. If your product's value is in a specific output, that output needs to be stable.Seeded PRNG, not
hash % N. A single hash gives you one decision. A seeded PRNG gives you a stream of decisions, all deterministically derived from the same input. Use the latter any time you need multiple independent choices from one seed.Legibly wrong beats arbitrarily wrong. "Wrong" isn't inherently funny. "Wrong in a recognizable way" is. Meme magnetism — snapping results toward culturally loaded numbers — makes the wrongness feel like it means something. The design principle generalizes: outputs that connect to known reference points land better than outputs that are merely random.
Determinism is a commitment, not just a technique. Fixing the output for a given input means you can't A/B-test it, personalize it, or quietly change it later. If you need to evolve the engine, you need a versioning strategy for existing shared links. Treat your output mapping as a public API with backward-compatibility obligations.
The full project write-up — isomorphic Canvas rendering, per-link OG images, 17-language i18n, and the Hall of Fame — is in the Wrongulator project card. The next article in this series covers the isomorphic Canvas approach: one render path for the in-browser card and the server-side OG image.
If you're building something where the output has to be reproducible — a shareable result page, a generator, a parameterized experience — and you want to pressure-test the design before writing the first line of code, a technical consultation is the right starting point.
Top comments (0)