Provably fair
How to check that the deck was decided before you played
In any online game where the server knows the answer, there is one question worth asking: could the server be choosing the answer after seeing my guess? For a game whose whole point is that a certain result is impossible, that question matters more than usual — a result nobody can verify is not really a result.
52! uses a commit–reveal scheme so you do not have to trust us on that. You can check any round yourself, with tools you already have.
The problem, stated plainly
The shuffle has to happen on the server. If it happened in your browser, the deck would be sitting in memory where anyone could read it, and every score on the leaderboard would be worthless. So the server shuffles — which means the server knows the order before you do, and you cannot see it.
A dishonest server could exploit that in either direction: it could quietly make sure you never do well, or make a favoured player do suspiciously well. Both are invisible from the outside unless the server is made to commit to something first.
The scheme
- Commit. When your round starts, the server shuffles two decks and generates a random nonce (16 random bytes). It then publishes a commitment: the SHA-256 hash of the deck plus the nonce. You receive the hash. You cannot read the deck out of it — that is what a cryptographic hash is for.
- Play. You turn cards. The server already knows the answer, but so does the hash you are holding, and the hash was fixed before you touched anything.
- Reveal. When the round ends, the server sends you the actual deck order and the nonce.
- Verify. Your browser recomputes SHA-256 over the revealed deck and nonce and checks it against the commitment from step 1. If they match, the deck you were shown at the end is exactly the deck that existed at the start.
The security rests on SHA-256 being resistant to second preimages: to cheat, the server would have to find a different deck order that hashes to the same commitment. No such method is known for SHA-256, and finding one by trial would take longer than the universe has existed.
What exactly gets hashed
The input string is the 52 card IDs in shuffled order, comma-separated, then a pipe, then the nonce:
AS,7H,QD,…,2C|3f9a1c7e2b8d4a6f0e5c1d9b7a3f2e8c
Card IDs are rank followed by suit letter: AS is the ace of spades,
10H the ten of hearts, 2C the two of clubs. The commitment is the
SHA-256 of that string, in lowercase hexadecimal.
Checking a round yourself
Open your browser’s developer tools on the game page, play a round, and look at the network
responses. POST /api/attempts returns the commitment before you
play; POST /api/guesses returns the deck and nonce
afterwards. Then run this in the console — it is the same computation the game does:
const input = deck.join(',') + '|' + nonce;
const buf = await crypto.subtle.digest('SHA-256', new TextEncoder().encode(input));
[...new Uint8Array(buf)].map(b => b.toString(16).padStart(2,'0')).join('');
The result must equal the commitment you saw before playing. The same check works from a terminal:
printf '%s' "AS,7H,…,2C|3f9a…" | sha256sum
What this does and does not prove
It proves:
- the deck was fixed before you played, and was not swapped afterwards;
- the result you were shown corresponds to that deck.
It does not, by itself, prove:
-
that the shuffle was uniformly random. A server could commit to a deliberately
poor shuffle and the commitment would still verify. For what it is worth, the shuffle is a
Fisher–Yates using the platform CSPRNG (
crypto.getRandomValues), and the project’s test suite includes a uniformity check on the resulting distribution — but a commitment cannot establish that on its own, and we would rather say so than imply otherwise.
A fully trustless version would require the player to contribute randomness to the shuffle as well — a client seed mixed into the server seed. That is a reasonable future addition and it is not implemented today.
The other half: making results count
Commit–reveal stops the server cheating. The opposite problem — a player replaying a round after the deck has been revealed, and submitting a perfect guess — is handled separately: each attempt has a unique ID that is locked in the database the first time a guess is submitted for it, inside the same atomic transaction that records the score. A second guess on the same attempt is rejected outright.
And because a round in which all 52 cards match is a 1-in-8×1067 event, any such result is flagged and reviewed by hand before it is ever counted. See the maths for why that number justifies the suspicion.