Same inputs, same match

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.

Determinism & math8 min read
A neon peg board with glowing ball trajectories arcing into a payout bucket
One board, one set of inputs - every machine computes the identical bounce, peg by peg, into the same bucket.

#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.

engine/cpp/include/rr/vec2.hcpp
// 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.

engine/cpp/include/rr/fixed.hcpp
// 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)
Every tick, every run, in exactly this order.

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.

engine/cpp - kiln/core/base/hash.hcpp
// 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.