Forge logo

Rust · deterministic 2D game engine

Same seed,
same world, every run.

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.

Open the playground View on GitHub
How to use this playground

Every control explained, plus a short guided tour that shows what determinism means and why it matters.

Guided tour
  1. Press Start to run the simulation. Balls fall under gravity, bounce off the walls, and collide with each other.
  2. Watch the world-state hash readout. It is a fingerprint of every ball position and velocity, and it changes on every step.
  3. Type a seed like 42 into the Seed box and press Replay from seed. The scene resets to the exact same starting layout.
  4. Let it run to some step number, note the hash, then press Replay from seed again. At the same step you will see the same hash reappear. That is determinism: same seed plus same number of steps gives the identical world, every single time.
  5. For hard proof press Run proof. It runs the world to step K, resets from the seed, runs to K again, and shows both hashes with a green MATCH badge.
What every control does
Start / Pause
Toggles the fixed-timestep loop. While paused the world holds its exact state.
Step once
Advances the simulation by a single fixed step. Handy for watching the hash change one step at a time.
Seed
The number that feeds the seeded random generator. The same seed produces the same starting positions, sizes, colors, and velocities.
Replay from seed
Rebuilds the world from the current seed and entity count, resetting the step counter to zero. This is the core determinism demo.
Spawn 10
Adds ten more balls, placed with the seeded generator so the additions stay reproducible.
Entities
How many balls a replay or reset creates. Change it, then press Replay from seed to apply.
Gravity
Downward acceleration in pixels per second squared. It applies live and also becomes part of the reproducible run.
Reset
Clears the world back to an empty, stopped state at step zero.
Run proof
The automated determinism check described in the tour. MATCH means the engine is deterministic for this seed and step count.
Why determinism matters

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.

Live playground

A from-scratch JavaScript mirror of Forge's ECS and fixed-timestep physics running in your browser.

900
FPS
0
Entities
0
Step
0
World-state hash
00000000
Determinism proof

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.

Run A hash
not run yet
Run B hash
not run yet
Awaiting run
The engine, headless

A whole run, in one number.

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

What makes it different

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.

Rendering behind a trait

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.

Fixed timestep

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.

The seed lives in the world

Randomness comes from a seeded SplitMix64 generator held inside the world state, so a scene regenerates identically from its seed.

One run, one hash

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.

Zero dependencies

Pure Rust standard library, edition 2021. Nothing to audit, pin, or wait on but the compiler.

What runs under the hood

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.

Seeded PRNG
A SplitMix64 generator using 64-bit BigInt math. The same seed yields the identical sequence, so scene generation is reproducible.
Tiny ECS
Entities are integer ids. Positions, velocities, radii, and colors are parallel component arrays indexed by id. Systems iterate strictly in id order so results never depend on iteration luck.
Fixed-timestep loop
Real frame time feeds an accumulator that is drained in fixed dt = 1/120 second slices, so the physics is frame-rate independent.
Semi-implicit Euler physics
Each step applies v += gravity * dt then pos += v * dt, with wall bounds and restitution.
Circle collision
Pairwise circle-circle detection with positional correction and an impulse response, resolved in deterministic id order.
World-state hash
After every step, all positions and velocities are quantized to fixed integers and folded with FNV-1a into a stable 32-bit fingerprint.
The parts that matter

A game engine core in modules you can read.

math Vec2, Transform

Vector math and a transform with translation, rotation, and scale. The primitives everything else is built on.

prng Seeded Rng

A SplitMix64 generator. The standard library ships none, so scene generation stays reproducible from a seed.

ecs World, Entity

Register component types, spawn, insert, remove, and query, iterating in deterministic entity order.

physics Semi-implicit Euler

Integrate velocities under forces and gravity, then advance positions, in fixed slices.

collision Swept AABB

Broadphase pairs plus narrowphase, with continuous resolution that stops fast bodies tunneling through thin walls.

serialize Encode and hash

A canonical binary encoding of the whole world, folded with FNV-1a into a single world-state fingerprint.

sim Simulation

SimConfig, Command, and the fixed-timestep loop that ties the pieces into one deterministic world.

rollback SnapshotRing

Record snapshots into a bounded ring and replay forward, the shape rollback-netcode rewinds are built on.

Use it

Drive it from the command line, embed it as a crate, or run the correctness gates yourself.

CLI

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.

Library API

Simulation::new(config, seed), seed_scene, run, and hash drive a world; serialize / deserialize snapshot it; and SnapshotRing with replay_to rewinds it.

Correctness gates

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