- Rust 97.1%
- Just 2.1%
- Nix 0.8%
|
All checks were successful
ci / test (push) Successful in 1m13s
Migrating the repo to git.bcnelson.dev (Forgejo 15.0.6), so replace .woodpecker/ci.yml with an equivalent .forgejo/workflows/ci.yml: same single linux/amd64 job, same rust:bookworm image, same fmt/clippy/test sequence, and the same compiler-level sccache cache under /cache/rust. Two Actions-specific deviations from a straight port: - No actions/checkout. It is a JS action and rust:bookworm has no node, so act_runner cannot run it inside the job container. The Checkout step shallow-fetches $GITHUB_REF instead (refs/heads/main on push, refs/pull/N/merge on PRs); the repo is public so no token is needed. - The /cache bind mount must be whitelisted in the runner's config (container.valid_volumes). Without it CI still passes, just cold every run — documented in the file's header. |
||
|---|---|---|
| .forgejo/workflows | ||
| configs | ||
| crates | ||
| examples | ||
| .envrc | ||
| .gitignore | ||
| Cargo.lock | ||
| Cargo.toml | ||
| CLAUDE.md | ||
| devenv.nix | ||
| flake.lock | ||
| flake.nix | ||
| justfile | ||
| README.md | ||
BattleTime
A programming game where competitors submit WebAssembly modules that pilot ships in a 3D Newtonian space-combat simulation. The simulation is bit-deterministic across platforms; matches are distributed as small replays and are independently verifiable by re-simulation.
This repository implements the design in spec/spec.md. Section
references below (§) point into that document.
Design pillars
- Bit-exact determinism across OS, libc, and CPU. The load-bearing property.
- Distributed agency — one WASM instance per entity, coordinating over a light-lagged, lossy channel.
- Partial, delayed information — sensor-limited, light-speed propagation.
- Compute is a resource — efficient bots think more often; efficiency never buys raw power.
- The viewer is downstream — rendering never feeds back into the sim.
Workspace layout (§2)
| Crate | Role | Spec |
|---|---|---|
sim-types |
Frozen repr(C), little-endian, versioned ABI structs |
§2, §3.3 |
sim-core |
no_std deterministic kernel: (State, [BotOutput]) -> State |
§3, §4, §5, §6, §7, §12 |
comms |
Unified light-lagged propagation queue (sensors + radio, one mechanism) | §8.1 |
replay |
Sparse, re-simulatable replay: manifest + per-decision command log; keyframes + seek | §9.1, §9.2 |
wasm-validate |
Feature-posture validation + mutable-global snapshot rewriter | §11 |
runtime |
wasmtime host: persistent instances, fuel=cycle metering, snapshots | §7, §9.3 |
harness |
Determinism harness: per-tick BLAKE3 hashing, scripted-bot driver, fixtures, forensics | §3.4, §9.4, §9.5, §13 M0 |
scenario |
TOML match config (ruleset overrides + fleet) and a unified driver mixing WASM and scripted pilots | §2, §7 |
cli (game) |
Developer flow: run, test, bench, replay, inspect, abi-header | §2 |
ladder |
Tournament runner, round-robin scheduling, Elo ratings | §13 M5 |
viewer |
Downstream match viewer (reads replays; never feeds back) | §13 M4 |
bot-sdk-rust |
Ergonomic Rust wrapper over the bot ABI | §2 |
bot-abi |
C header + byte-layout spec for other languages | §2 |
Milestone status (§13)
- M0 — determinism harness first (per-tick hashing, self-comparing CI checks, fixture matches), then physics + heat + reaction mass + cycles + command programs with scripted in-process bots. Done.
- M1 — WASM host: persistent instances, cycle (fuel) metering, validation +
instrumentation,
game inspect. Done. - M2 — replay format (sparse,
cycles_consumedmandatory), keyframe snapshots, forensic bundle, seek. Done. - M3 — comms/sensor propagation queue, resolution tiers, weapon families (kinetic/beam/missile), missiles-as-agents. Done (parameters provisional, §14).
- M4 — viewer. Downstream frame extraction + a text renderer are implemented; a Bevy/wgpu 3D front-end is the eventual GPU layer.
- M5 — tournament runner, scheduling, ratings. Done.
Determinism
sim-core is no_std, allocation-light, zero-I/O. It uses vendored libm (no
std transcendentals), BTreeMap never HashMap, fixed accumulation and
collision-resolution order, fixed-step (1000 Hz) semi-implicit Euler
integration, and no wall-clock or entropy (§3.2). Per-tick state is hashed with
BLAKE3 over a canonical little-endian serialization (§3.4).
crates/harness/tests/golden.rs pins per-tick
hashes at ticks 1/100/10000/end for every fixture. It is the one pinned check —
a deliberate physics change requires re-blessing it — so it is #[ignore]d and
not run in CI; run it on demand with just determinism. CI instead relies on
the self-comparing checks below, which never need a re-bless.
Determinism testing system
Alongside the pinned goldens, several self-comparing checks assert
determinism without any frozen constants — a legitimate physics/ruleset change
never forces a re-bless, because every run is only ever compared to another run
of the same code (just determinism-all). Every comparison uses the 32-byte
chain_digest — the running BLAKE3 over every per-tick state hash — so a
divergence at any single tick fails, even a transient one that later reconverges
(unlike comparing only final/checkpoint hashes):
- Parallel N-way (
tests/parallel.rs) — launches many(fixture, seed)simulations on their own threads concurrently; all replicas of a case must agree, distinct cases must differ. Running them at once is what would surface any shared-mutable-state regression a sequential loop hides. - Fuzzer (
tests/fuzz.rs) — a deterministic generator (src/scenarios.rs) turns a seed into a full randomized match (varied fleet size, positions, velocities, masses, orientations, pilots), andBT_FUZZ_N(default 200) of them are each run twice and required to match. Widening the input space is how edge cases surface that the five fixtures never reach; the generator uses only correctly-rounded IEEE ops so a seed yields a bit-identical start on every platform. Hunt harder withBT_FUZZ_N=5000 just determinism-all. - Cross-environment (
tests/digest.rs) — an emitter prints the digest report (every fixture +BT_FUZZ_Nscenarios) from whatever the tree computes.just determinism-crossruns it natively and again elsewhere in a QEMU-emulated Docker container, then diffs. One recipe, several axes — verified bit-identical onlinux/arm64(other ISA),linux/s390x(big-endian — stresses the little-endian marshalling), and musl/Alpine (other OS userland). If a target diverges,just determinism-locate <label>re-runs it emitting the full per-tick chain in both environments and prints the first divergent tick. (Thecrosstool assumes rustup and does not work with the nix toolchain, so the recipe drives Docker+QEMU directly; a one-timedocker run --privileged --rm tonistiigi/binfmt --install allregisters the emulators.)
Two more reach platforms Docker can't:
- WASM / browser (
examples/determinism-wasm/) — the samedigest_report(fixtures + fuzz scenarios) compiled towasm32.just determinism-wasmruns it under Node (V8) and diffs against native;just wasm-servehosts a self-contained page so opening it in any browser on macOS, Windows, or a phone prints that machine's digests with no toolchain (?fuzz=Nto widen). Verified bit-identical to native x86_64. - Native anywhere —
just determinism-nativeprints this machine's digests. The emitter is pure portable Rust (std+ BLAKE3, no platform code), so on a real macOS/Windows box it is one command; compare itsDIGESTlines to any other machine's. (macOS can't be virtualized on non-Apple hardware, so native macOS needs a real or cloud Mac.)
What runs in CI: only the fast self-comparing checks — determinism.rs
(run-to-run) and parallel.rs (concurrent N-way) — via cargo test --workspace.
The pinned golden.rs, the fuzzer, the Docker+QEMU cross-environment checks, and
the WASM harness are all #[ignore]d or just-only and run on demand. (CI runs
a single Linux job and never compares hashes across environments — that
cross-platform equality check lives in just determinism-cross.)
Quick start
# Run everything
cargo test --workspace
# Run a fixture match, write a replay, then verify it re-simulates
cargo run -p cli -- run --fixture duel --out /tmp/duel.btrp
cargo run -p cli -- replay /tmp/duel.btrp --verify
# Determinism checkpoints, throughput, and the C ABI header
cargo run -p cli -- test
cargo run -p cli -- bench --fixture tumble --ticks 200000
cargo run -p cli -- abi-header --out battletime_bot.h
# Inspect a module's validation + instrumentation diff (§11.4)
cargo run -p cli -- inspect path/to/bot.wasm
# Run a real WASM bot in a live duel (see examples/seeker-bot)
just build-example-bot
cargo run -p cli -- run --config configs/wasm-duel.toml
Match configs (§2)
run and bench accept a TOML match config that fully specifies the
ruleset and fleet — any mix of WASM modules and built-in scripted bots — instead
of a fixed fixture. See configs/ for examples.
# Bench a full match (WASM-vs-WASM); throughput excludes compile/setup time
cargo run --release -p cli -- bench --config configs/wasm-duel.toml --ticks 500000
# Play a mixed WASM-vs-scripted match and record a verifiable replay
cargo run -p cli -- run --config configs/mixed-duel.toml --out /tmp/mixed.btrp
# Override any ruleset field from the CLI, repeatably; --seed wins over the config
cargo run -p cli -- bench --config configs/wasm-duel.toml \
--set ruleset.max_thrust_accel=8000 --set ruleset.tick_cap=20000 --seed 12
A config is [ruleset] (any subset of the tunable Ruleset scalars, §14) plus
a list of [[player]] tables; each player is a ship piloted by exactly one of
wasm = "path" or builtin = "<name>" (idle, constant-thrust, pulse,
spinner, gunner, pinger) with a args = { ... } table. Relative wasm
paths resolve against the config file's directory.
Example bot
examples/seeker-bot/ is a complete WASM bot written against bot-sdk-rust: a
sensor-cued point-defense gunner with a bump allocator and persistent static mut state. It compiles to a ~7 KB wasm32-unknown-unknown cdylib, is validated
- instrumented on load like any submission (§11), and is run through the full
sim-coresimulation byruntime'sWasmDriver. The end-to-end tests live incrates/runtime/tests/end_to_end.rs(a full deterministic WASM-vs-WASM match). The committedcrates/runtime/tests/fixtures/seeker.wasmis rebuilt withjust build-example-bot.
Fixtures: drift, tumble, duel, salvo, recon — each pins a subsystem's
hash (§13).
License
MIT OR Apache-2.0.