Imported from Metta-AI/cogame-pistonball (
AGENTS.md). Install upstream withnpx skills add Metta-AI/cogame-pistonball. Copyright stays with the author.
Agent operating guide — cogame-pistonball
Orientation for coding agents working in this repo. The rules live in docs/RULES.md, the wire protocol in docs/PROTOCOL.md, and the policy schema in docs/SCRIPTS.md. This file covers the things that are easy to get wrong.
The two lines you must not cross
1. The determinism boundary
src/pistonball/{sim,bank,trig,sim_types,sim_config,sim_state}.nim contain
no floating point at all, and tests/test_determinism.nim greps them and
fails on a single hit — including inside comments, so do not write sin( in
prose there either.
The reason is not style. The replay is re-simulated by the emscripten/wasm32
build of the same module the native amd64 server ran, and their per-tick
gameHash chain has to match bit-for-bit. Integers make that true by
construction rather than by an argument about two musl builds of libm agreeing.
Concretely:
- Every stored sim field is explicitly
int32(positions, velocities, heights, angles, counters),int64(the reward accumulators), orbool/enum. No bareintin a hashed field — Nim'sintis 64-bit natively and 32-bit under--cpu:wasm32. - Every product or quotient of two sim quantities is computed in
int64and narrowed with an explicit truncatingdiv. Nim'sdivtruncates toward zero, which is what makes leftward and rightward progress exactly opposite. - Trigonometry is a committed literal table (
src/pistonball/trig.nim), regenerated bytools/gen_trig_table.nimand re-derived entry by entry in the determinism test.isqrtis the only square root. - Randomness is ONE seeded
std/randomstream, integer draws only, used att = 0for exactly three things:perm, the twenty rest heights, and the ball's lateral drop offset. The sim draws no random numbers after tick 0.
The controller (control.nim), the renderer (global.nim) and the pixie bakes
may use floats freely: the replay records the BYTES the controller produced,
never the logic that produced them, and rendering never enters gameHash.
2. The two name spaces
In-game every cog is PST-01 … PST-20. Real policy names live ONLY in the
replay config JSON, the DOM roster, the endcard and results.names. A player
stream, a composed LLM message and a board label must never carry one;
tests/test_locality.nim and tests/test_server.nim are the enforcement.
GameVersion
GameVersion in src/pistonball/sim_types.nim gates replay compatibility. Its
comment is a prepend-only changelog: keep the
GVnn (short rule name): HEADLINE shape, because the number alone cannot
distinguish two independent claims on it — the RULE attached to it can.
Before you claim a number, scan the open branches, not just main:
tools/ci/check_gameversion.sh origin/main # checks your working HEAD
tools/ci/check_gameversion.sh origin/main <branch> # checks another branch
A GameVersion bump invalidates tests/data/golden_hashes.json. Delete it and
re-run tests/test_determinism.nim once to regenerate, then read the diff
before committing it — that file is the physics' anti-regression pin.
Layout
src/pistonball.nim— the game entrypoint. Seed randomization happens HERE, beforeconfig.update, so every seed-derived draw follows the final seed.src/pistonball_player.nim— the thin seat registrar (/bin/pistonball-player).src/pistonball/—sim_types(consts + types + the flatty wire shape: field order is sacred),trig,bank(geometry + the seeded draws + the contact broadphase),sim_config,sim_state(hash, events, lobby),sim(the step loop),roster(results),scripts(the reply schema),control(the deterministic controller),baselines,llm,decide(the per-turn parallel batch),replays,replay_runtime,broadcast,events,global(the board renderer),wire_constants,server.replay-viewer/— the wasm entry, the emscripten link flags, and the OffscreenCanvas worker.client/— the browser chrome. See below.tests/— run from the repo ROOT (assets resolve viadata/andclient/).
The chrome is the starter's, not a lookalike
client/chrome_common.js is copied byte-for-byte from
Metta-AI/coworld-ctf and must stay that way. It reads window.CTF_WIRE;
src/pistonball/wire_constants.nim ALIASES that name onto
window.PISTONBALL_WIRE rather than forking the shared chrome for one
identifier. That alias is the single documented exception to the
"no CTF_ identifier survives" rule, and tests/test_viewer.nim pins it.
client/replay_broadcast.html is the starter's page with a game block
appended under the PISTONBALL additions… banner. Everything above that
banner is the starter's, edited only where the game demands:
- the three removed elements (
#viewpanel,#fpv,#povBadge) — a fixed arena that always fits the frame has nothing to zoom to and no camera to open, and the starter's own code for them is left in place behind null guards rather than rewritten; - the
PB_MODElatch, which now keys on thepbblock in the state frame.
Do not rewrite it. A page written from scratch that reuses the starter's
ids is a rewrite and fails review. The game block is one IIFE with pb-prefixed
builders precisely so it can never shadow the chrome alias block's hoisted
declarations.
The emscripten link flags and the JS bootstrap are a matched pair:
replay-viewer/config.nims carries NO MODULARIZE and NO EXPORT_NAME,
because static_replay_worker.js waits on Module.onRuntimeInitialized. A
mixture throws nothing, logs nothing and hangs on "Loading replay…" for ever.
Baseline tuning
Three numbers are swept — wavebot's leadTicks, upUm and idleUm:
nim c -d:release -r --path:src tools/tune_baselines.nim
Commit the winning row to tools/ci/baseline_tuning.json;
tests/test_tuning.nim re-asserts that the shipped DefaultBaselineParams
still equal it. The physics constants in sim_types.nim are not swept. If
twenty wavebots cannot deliver, those three numbers are wrong, not the sim.
CI is the harness
nim r --hints:off --path:src tests/test_physics.nim # debug
nim r --hints:off -d:release --path:src tests/test_perf.nim # release
ci.yml runs every tests/*.nim twice, debug and release: debug catches
range and overflow bugs (the fixed-point arithmetic is full of them), release
catches codegen bugs a debug-only CI has shipped before. Two files are
release-only via the NIM_TESTS_RELEASE_ONLY repo variable
(tests/test_perf.nim tests/test_baselines.nim).
docker-smoke runs a real twenty-seat episode in raw Docker from the
certification fixture and uploads the replay it produced;
wasm-viewer builds the static bundle and opens that replay in headless
chromium. Everything else only proves files exist.
Forensics
tools/replay_summary.py (Python 3 stdlib only) prints one strict-UTF-8 JSON
object for any .replay file — the config, the roster, every script record
and the results document. It is what a spectator holding the bytes can run,
and it is the phase-60 substitute for the "replay is valid JSON" check a
JSON-replay game gets for free.