Co-op Determinism Coverage — Design
Co-op Determinism Coverage — Design
Section titled “Co-op Determinism Coverage — Design”Date: 2026-07-14
Status: Approved (Chris, 2026-07-14)
Origin: overnight-review triage item — “No 2-pilot determinism test; state_checksum()/snapshot_string() omit drone state (c1).”
Problem
Section titled “Problem”Two gaps in the determinism keystone that protect Phase-3 co-op netcode:
- No 2-pilot determinism test.
tests/test_determinism_survival.gdruns a single pilot only; it never callsadd_player2(), so the 2-pilottick()path is never proven byte-identical across two same-seed runs. The co-op sim (per-pilot arsenals, per-pilot upgrades, per-pilot drones — all landed 2026-07-09..11) has no determinism guard. state_checksum()/snapshot_string()omit drone state. Both already loop over all pilots for pos/hp/xp/level (multi-pilot player state IS covered), but neither includes thedronespool.sim/drone_director.gd:508documents this as deliberate — “NOT in snapshot_string()/state_checksum() (drones are opt-in, so this never affects determinism)” — a rationale that held only because no determinism test ever deployed a drone. A co-op drone desync (drones are per-pilot now) would go undetected.
Key facts that shape the design
Section titled “Key facts that shape the design”- The existing single-pilot baseline run drives
InputState.new(dir)with no decoy press, so no drone is ever deployed → thedronespool is empty throughout that run. - Appending a
for d in drones:loop tostate_checksum()/snapshot_string()therefore leaves the existing single-pilot output byte-identical (empty loop contributes nothing) → the existing baseline4217109746/3312602286is unchanged, no re-pin. - BUT the per-pilot
drone_chargescalar accrues every tick unconditionally (drone_director.gd:102, not gated on a loadout), so it is nonzero after 600 ticks. Including it in the checksum WOULD change the existing baseline. It is therefore deliberately excluded (see below).
Decision (Chris, 2026-07-14): drone-inclusive
Section titled “Decision (Chris, 2026-07-14): drone-inclusive”Fold the drones pool into the checksum and add a 2-pilot determinism test that deploys a drone. Overturns the “drones never affect determinism” exclusion for the pool, keeps it for the accruing scalar.
Components
Section titled “Components”1. state_checksum() — append the drones pool (sim/sim.gd)
Section titled “1. state_checksum() — append the drones pool (sim/sim.gd)”After the existing gems loop, before return hash(parts):
for d in drones: parts.append(d.pos) parts.append(d.vel) parts.append(d.hp) parts.append(d.life) parts.append(d.charge) parts.append(d.pulse_timer) parts.append(d.phase) parts.append(pilots.find(d.owner)) # owner as a stable pilot index (co-op)Deliberately excludes the per-pilot drone_charge scalar. Rationale: it accrues every tick, so including it would re-pin the existing keystone value for near-zero gain — a drone_charge desync can only matter by changing WHEN a drone deploys, which changes the pool and is already caught here. The pool is the desync-prone state; the scalar’s effects surface through it. d.charge (the per-DroneState charge field, distinct from pilot.drone_charge) IS included — it only exists once a drone is in the pool, so it is baseline-safe.
pilots.find(d.owner) returns the owner’s index in pilots (or -1) — a stable, deterministic integer that verifies drone ownership is assigned consistently in co-op.
2. snapshot_string() — append a per-drone segment (sim/sim.gd)
Section titled “2. snapshot_string() — append a per-drone segment (sim/sim.gd)”Inside a for d in drones: loop (placed after the aggregate e=… a=… pr=… g=… k=… line, before the per-pilot xp/lv loop, or at the end — any fixed position), append a compact per-drone segment, e.g.:
for d in drones: s += " d=(%.3f,%.3f)" % [d.pos.x, d.pos.y]No drone count is added to the aggregate line (that would change the single-pilot format and break the byte-identical output). With an empty pool the loop appends nothing, so test_pilots_query.gd’s old-vs-new-tick byte-match and test_determinism.gd’s trace comparisons are unaffected.
3. Update the stale comment (sim/drone_director.gd:508)
Section titled “3. Update the stale comment (sim/drone_director.gd:508)”Change the comment to reflect that deployed-drone pool state now IS in state_checksum()/snapshot_string(); only the monotonic per-pilot recharge scalar (pilot.drone_charge) stays excluded, and its effects surface via the pool (deploy timing).
4. New test tests/test_determinism_coop.gd
Section titled “4. New test tests/test_determinism_coop.gd”- Two Sims, seed 1234,
enable_survival()on both,add_player2()on both. - Deploy a drone for P2 identically in each sim up front:
deploy_drones(sim, sim.pilots[1], [SPEC])whereSPEC = {"klass": "sentinel", "life": 1.0, "dmg": 1.0, "radius": 1.0, "heal": 0.0, "extras": 0}(the shape used intests/test_drones.gd). Deploying directly (not via the decoy-charge/button path) keeps the setup deterministic and focused on the drone’s per-tick behavior. - Run 600 ticks. Each tick builds a 2-pilot input array: P1 moving on one Lissajous path, P2 on a different one (both non-trivial so the pilots diverge and exercise
_nearest_pilot), and tick both sims with the identical input sequence. Use the canonicaltick(inputs: Array[InputState])(ortick_single(input, input2)), whichever the 2-pilot path expects — confirm againstSim.tick. - Assert
a.state_checksum() == b.state_checksum()anda.snapshot_string() == b.snapshot_string()— the primary guarantee: the co-op sim (2 pilots + a deployed per-pilot drone) is byte-identical across two same-seed runs. - Pin a new, separate 2-pilot absolute baseline:
assert_eq(a.state_checksum(), <value>)andassert_eq(a.snapshot_string().hash(), <value>). The values are captured from the first green run (standard determinism-baseline procedure — read the printed value, pin it, document the run parameters in a comment). These are NEW baselines, not a change to the existing single-pilot ones.
Data flow / determinism
Section titled “Data flow / determinism”/sim-internal. The existing single-pilot baseline is unchanged (verified: empty pool + scalar excluded). The a==b assertion in the new test is the real guarantee; the pinned absolute values catch future drift. This is additive determinism coverage, not a change to the existing keystone value.
Error handling
Section titled “Error handling”Pure additions. The only risk is the new pinned baseline being wrong — mitigated by the a==b assertion running first (if the sim weren’t deterministic, a==b fails before the absolute pin is even reached) and by capturing the pinned value from a confirmed-green run.
Testing
Section titled “Testing”- The new
test_determinism_coop.gdis itself the test. - Regression:
test_determinism_survival.gd(single-pilot absolute baseline) must still pass unchanged — the explicit proof that Components 1–2 didn’t perturb the existing keystone. - Regression:
test_pilots_query.gdandtest_determinism.gd(snapshot_string byte/relative comparisons) must still pass unchanged. - Full suite: 0 failing.
Out of scope
Section titled “Out of scope”- Including the per-pilot
drone_chargescalar in the checksum (excluded by design — would force an existing-baseline re-pin for near-zero gain). - Forcing per-pilot level-ups / upgrade rolls in the co-op test (the upgrade RNG stream is already determinism-tested in
test_determinism.gd; YAGNI here — the co-op test’s job is the 2-pilot + drone tick path). - Networked (M-B/M-C) determinism — that unmerged worktree is separate.