Rust · deterministic 2D game engine
Forge is a from-scratch, dependency-free 2D game engine in pure Rust std. It has an entity component system, a fixed-timestep loop, semi-implicit Euler physics, swept collision, and a headless core. A whole run collapses to one world-state hash, so a bug at step 4000 replays on demand and a regression shows up as a changed number, not a flaky pixel diff. Run the browser twin, then replay from the seed and watch the hash come back.
Every control explained, plus a short guided tour that shows what determinism means and why it matters.
42 into the Seed box and press Replay from seed. The scene resets to the exact same starting layout.A deterministic engine replays a run exactly from a seed and a step count, so a bug that appears at step 4000 can be reproduced on demand instead of chased by luck. It makes the simulation headless-testable, because a test can step the world and assert on a single hash rather than eyeballing pixels. It also keeps the simulation frame-rate independent, since the fixed-timestep accumulator steps the same slices whether your machine runs at 30 or 144 frames per second.
A from-scratch JavaScript mirror of Forge's ECS and fixed-timestep physics running in your browser.
Run the world to step K, record the hash, reset from the same seed, run to step K again. The two hashes must match to the last digit.
The playground above is a browser twin. The real Rust engine runs headless from the command line: it plays a scripted scenario from a seed, prints the world-state hash as it goes, then plays the identical scenario again and checks the two runs agree bit for bit.
$ forge --seed 42 --steps 600 --balls 40 Forge headless determinism check seed=42 steps=600 balls=40 Run 1 (traced): tick 0 entities 44 hash 0xf32bf346b3ebc51f tick 100 entities 44 hash 0x3dc9906b7cdb578a tick 200 entities 44 hash 0x22478117891999b2 tick 300 entities 44 hash 0xc646848c772d378c tick 400 entities 45 hash 0x476f56bfe971b5bd tick 500 entities 45 hash 0xd60b08a455a04bab tick 600 entities 45 hash 0x2ff4b15d6510b88d Run 1 final hash: 0x2ff4b15d6510b88d Run 2 final hash: 0x2ff4b15d6510b88d determinism (same seed same hash): PASS serialize/restore round-trip: PASS
Most engines are hard to test because rendering, timing, and randomness are tangled into the core, and floating point results wobble from run to run. Forge inverts each of those choices.
The renderer is a trait, so the simulation core never needs a screen. The same world that draws in a window runs headless in CI.
Real frame time feeds an accumulator drained in fixed 1/120 second slices, so the physics is identical at 30 or 144 frames per second.
Randomness comes from a seeded SplitMix64 generator held inside the world state, so a scene regenerates identically from its seed.
The whole world serializes to a canonical encoding and folds with FNV-1a into a single number, so a regression shows up as a changed hash, not a flaky pixel diff.
Pure Rust standard library, edition 2021. Nothing to audit, pin, or wait on but the compiler.
The same ideas the Rust engine is built on, reimplemented here for teaching. Rendering sits behind a trait so the core never needs a screen, time advances in fixed slices so the result never depends on frame rate, and randomness lives inside the world state, so five correctness gates (deterministic replay, serialize round trip, collision correctness, adversarial hardening, and rollback reproduction) run headless in CI on every push.
dt = 1/120 second slices, so the physics is frame-rate independent.v += gravity * dt then pos += v * dt, with wall bounds and restitution.Vector math and a transform with translation, rotation, and scale. The primitives everything else is built on.
A SplitMix64 generator. The standard library ships none, so scene generation stays reproducible from a seed.
Register component types, spawn, insert, remove, and query, iterating in deterministic entity order.
Integrate velocities under forces and gravity, then advance positions, in fixed slices.
Broadphase pairs plus narrowphase, with continuous resolution that stops fast bodies tunneling through thin walls.
A canonical binary encoding of the whole world, folded with FNV-1a into a single world-state fingerprint.
SimConfig, Command, and the fixed-timestep loop that ties the pieces into one deterministic world.
Record snapshots into a bounded ring and replay forward, the shape rollback-netcode rewinds are built on.
Drive it from the command line, embed it as a crate, or run the correctness gates yourself.
forge --seed N --steps N --balls N plays a scripted headless scenario and prints world-state hashes as it runs, then re-runs it to prove determinism.
Simulation::new(config, seed), seed_scene, run, and hash drive a world; serialize / deserialize snapshot it; and SnapshotRing with replay_to rewinds it.
Five properties run as tests: deterministic replay, serialize round trip, collision correctness, adversarial hardening, and rollback reproduction, with the fuzzing workload scaled by FORGE_FUZZ_OPS.
# run the headless engine and its self-check cargo run --release --bin forge -- --seed 42 --steps 600 --balls 40 # the five correctness gates, scale the fuzzing with an env knob cargo test FORGE_FUZZ_OPS=200 cargo test cargo clippy --all-targets -- -D warnings