The deterministic engine
The physics kernel is integer-only C++17 compiled to both native and WebAssembly from one source. No floating point, no libm, no wall-clock - the same signed inputs produce byte-identical state on every machine. That bit-for-bit reproducibility is the foundation everything else is built on.

#Why determinism is the whole game
If a match is a pure function of its inputs, its result needs no trusted authority - anyone can recompute it. Determinism isn’t a nice-to-have; it is the property that makes the outcome auditable at all.
So the engine holds a hard line: the same signed inputs must produce byte-identical state on every machine - an old Android phone, a laptop, and a server in a data centre - now and years from now. Everything below exists to keep that promise. The kernel is integer-only C++17 compiled from one source to both native (server authority) and WebAssembly (client prediction).
#Integer-only, fixed-point math
There is no floating point in the simulation path. Floats round differently across compilers, CPUs and math libraries - that drift alone would break a bit-for-bit guarantee. Instead, board space is measured in fixed-point sub-units: exactly 65,536 of them per board unit. Positions and velocities are 32-bit integers.
// 65536 sub-units per board unit. A power of two, so unit<->sub
// conversions are exact bit shifts - never a rounding step.
inline constexpr std::int32_t kUnit = 1 << 16;
struct Vec2 { std::int32_t x, y; }; // +x right, +y down (screen-style)Where a distance is needed, the engine prefers a squared length (an exact integer) and, when it truly must take a root, uses a bitwise integer square root - never libm. The result is the same on every target.
#Frozen trig table, integer sqrt
Launch angles and orbiting pegs need sine and cosine. Calling the platform's math library would reintroduce drift, so the engine ships its own frozen trig table: a hardcoded set of sine values scaled by 4,096, with cosine derived by quadrant symmetry. Native and WASM read the same table and compute the same bytes.
// Fixed-point 1.0 == 4096. sin(deg) is a baked table; native and
// WASM compute byte-identical values - no libm involved, no drift.
inline constexpr std::int32_t kTrig = 4096;
constexpr std::int32_t cos_fp(int deg) { return sin_fp(deg + 90); }#A fixed 120 Hz tick
The world advances in whole ticks at a fixed 120 Hz. The real-time delta a caller passes in is advisory and never trusted - a real-time client accumulates elapsed nanoseconds and only steps the world in whole ticks. A slow frame or a fast one produces the exact same simulation; wall-clock never leaks into the result.
- Fixed tick rate
- 120 Hz
- kTickRate - the only clock the sim trusts.
- Sub-units / unit
- 65,536
- kUnit = 1<<16, exact bit-shift scale.
- Trig fixed-point 1.0
- 4,096
- kTrig - the baked sine table scale.
#A frozen resolution order
Determinism isn't only about the math - it's about order. Floating point aside, if two runs resolved collisions in a different sequence they could diverge. So every tick runs a fixed pipeline, and every ball and peg is visited in a fixed index order:
integrate → resolve pegs (peg index order) → resolve bounds (N, E, S, W)Kinematic pegs (orbiting, oscillating, path-following) are never integrated - their position is a pure closed-form function of the tick number, so they can't accumulate error and they survive a serialize-and-restore untouched.
#The state hash
To check that two engines agree, you don't compare pixels - you compare a hash of the state. Every dynamic field is serialized in a frozen order and folded with FNV-1a. Two engines stepping the same inputs match hashes on every single tick; a divergence of one sub-unit changes the hash.
// FNV-1a over the canonical serialize() bytes == the tick's state hash.
constexpr std::uint64_t kFnvOffset = 14695981039346656037ull;
constexpr std::uint64_t kFnvPrime = 1099511628211ull;#Deterministic RNG
Any randomness - bot decisions, board generation - uses seedable integer PRNGs, never rand() or mt19937. Two of them: a PCG32 generator seeded through SplitMix64 (with unbiased bounded draws and independent child streams via fork(tag)), and SplitMix64 directly inside the board generator. Same seed, same sequence, on every target.
#Who fired is part of the state
Each shot carries the firing wallet's roster index - its player_index - and that index is folded into the state hash. This is load-bearing for fairness: a replay that tried to relabel who took a shot (to steal a steal, or dodge a forfeit) would change the serialized bytes and fail to reproduce the committed checkpoints. Identity is welded to the physics.
With all of that in place, a match becomes a value you can recompute. The next page walks the exact formulas that run inside each tick.