The simulation
Two players, two machines, real SOL on the result, and no server deciding who won. That constraint drives every decision below.
Both clients run the same simulation over the same inputs and must arrive at the same state. If they ever disagree, the match voids and both stakes come back — losing a match to a desync you did not cause would be worse than not playing.
No floating point, anywhere
Floating-point arithmetic is not guaranteed to give bit-identical results across processors, browsers and compiler versions. Two clients that both do everything right can still drift apart, and by the time they do, one of them thinks it won.
So the simulation has no floats. Every position, speed, hit point and range is a
fixed-point i32 at 1/1024 of a tile:
fp(1.0) === 1024
fp(0.5) === 512
speed, range, positions — all integers
Scaling is integer multiply-then-divide, always in the same order, always floored the same way. A one-point divergence in hit points is a divergent state hash, so "close enough" does not exist here.
Twenty ticks a second
The match advances in fixed steps of 50ms. Inputs are stamped with the tick they belong to and sorted deterministically before being applied:
sort by: player, then deck index, then x, then y
The sort matters. Two cards played on the same tick must be applied in the same order on both machines, and arrival order over a network is not something either machine controls.
Randomness that both sides agree on
The simulation needs randomness — deck shuffles, spawn jitter — and it cannot ask either client to supply it. It uses xorshift32, seeded from the match seed, advanced only by the simulation itself. Same seed and same inputs give the same stream on both sides.
The state hash
Every forty ticks — twice a second — each client folds the whole simulation state into an FNV-1a hash and compares it with the other side.
Matching hashes mean the two simulations are still identical. A mismatch means they are not, and there is no way to tell which one is right, so the match voids and the escrow returns both stakes. This is the check that makes a trustless real-time game possible: nobody has to trust the opponent's client, because a lying client cannot produce a matching hash.
What this buys
- No authoritative server, so nothing to compromise or bribe.
- Only inputs cross the wire, not state, so a match is a few hundred bytes.
- Any observer with the seed and the inputs can replay the match exactly.
What it costs
Every rule has to be integer-exact and order-independent, and a card whose stats are computed in a different order on one client is a desync. That is why traits are folded into an archetype's stats once, at spawn, in a fixed order — trait first, then level — and never recomputed per tick.
