No description
  • Rust 97.1%
  • Just 2.1%
  • Nix 0.8%
Find a file
Bradley Nelson ea9be485cf
All checks were successful
ci / test (push) Successful in 1m13s
Switch CI from Woodpecker back to Forgejo Actions
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.
2026-08-17 23:53:26 -06:00
.forgejo/workflows Switch CI from Woodpecker back to Forgejo Actions 2026-08-17 23:53:26 -06:00
configs Add TOML match config + unified WASM/scripted driver; remove wasm-duel 2026-07-22 15:20:13 -06:00
crates Add multi-modal raw sensors: radar, optical, subspace (§8.2) 2026-07-22 20:16:06 -06:00
examples Add multi-modal raw sensors: radar, optical, subspace (§8.2) 2026-07-22 20:16:06 -06:00
.envrc Initialize Rust project with devenv and just 2026-07-22 01:24:41 -06:00
.gitignore Add self-comparing determinism testing system 2026-07-22 16:16:41 -06:00
Cargo.lock Add TOML match config + unified WASM/scripted driver; remove wasm-duel 2026-07-22 15:20:13 -06:00
Cargo.toml Add TOML match config + unified WASM/scripted driver; remove wasm-duel 2026-07-22 15:20:13 -06:00
CLAUDE.md Switch CI from Forgejo Actions to Woodpecker 2026-07-22 16:56:25 -06:00
devenv.nix Implement deterministic WASM space-combat simulation 2026-07-22 10:02:54 -06:00
flake.lock Initialize Rust project with devenv and just 2026-07-22 01:24:41 -06:00
flake.nix Initialize Rust project with devenv and just 2026-07-22 01:24:41 -06:00
justfile Add self-comparing determinism testing system 2026-07-22 16:16:41 -06:00
README.md Switch CI from Forgejo Actions to Woodpecker 2026-07-22 16:56:25 -06:00

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

  1. Bit-exact determinism across OS, libc, and CPU. The load-bearing property.
  2. Distributed agency — one WASM instance per entity, coordinating over a light-lagged, lossy channel.
  3. Partial, delayed information — sensor-limited, light-speed propagation.
  4. Compute is a resource — efficient bots think more often; efficiency never buys raw power.
  5. 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_consumed mandatory), 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), and BT_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 with BT_FUZZ_N=5000 just determinism-all.
  • Cross-environment (tests/digest.rs) — an emitter prints the digest report (every fixture + BT_FUZZ_N scenarios) from whatever the tree computes. just determinism-cross runs it natively and again elsewhere in a QEMU-emulated Docker container, then diffs. One recipe, several axes — verified bit-identical on linux/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. (The cross tool assumes rustup and does not work with the nix toolchain, so the recipe drives Docker+QEMU directly; a one-time docker run --privileged --rm tonistiigi/binfmt --install all registers the emulators.)

Two more reach platforms Docker can't:

  • WASM / browser (examples/determinism-wasm/) — the same digest_report (fixtures + fuzz scenarios) compiled to wasm32. just determinism-wasm runs it under Node (V8) and diffs against native; just wasm-serve hosts 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=N to widen). Verified bit-identical to native x86_64.
  • Native anywherejust determinism-native prints 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 its DIGEST lines 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-core simulation by runtime's WasmDriver. The end-to-end tests live in crates/runtime/tests/end_to_end.rs (a full deterministic WASM-vs-WASM match). The committed crates/runtime/tests/fixtures/seeker.wasm is rebuilt with just build-example-bot.

Fixtures: drift, tumble, duel, salvo, recon — each pins a subsystem's hash (§13).

License

MIT OR Apache-2.0.