Game guide · source of truth
Ledger

M1-03

zoen-zone: 20 Hz loop, spatial hash, AOI interest sets

← Ledger

partialstatus · M1
10✓ tests passed / failed
9.01Mtokens · 16 min
1evidence files · 2 kB

Evidence

From logs/evidence/M1-03/. Click a thumbnail for the full image.

Logs and data (1)

tick-bench.json · 2 kB · open
{
  "task": "M1-03",
  "generatedBy": "pnpm zone:bench (cargo run --release -p zoen-zone --bin bench-tick)",
  "machine": {"os": "macos", "arch": "aarch64", "availableParallelism": 8, "threadsUsed": 1},
  "config": {"tickHz": 20, "budgetMs": 50.000, "targetP99Ms": 15.000, "cellM": 16, "grid": [256, 256], "radiusM": 90, "townRadiusM": 70, "leaveMarginM": 5, "maxInterestSet": 512},
  "covers": "zoen-sim step (one input per entity per tick + movement), spatial hash update, interest sets with enter/leave diffs, state hash. Not yet: AI, combat, snapshot encoding, network (M1-03B, M1-04, M2).",
  "criterion": "tick p99 <= targetP99Ms and 0 allocations after warmup on one thread",
  "pass": true,
  "scenarios": [
    {"id": "movers-1000", "what": "1,000 moving players with interest sets in a 1,024 m field", "players": 1000, "monsters": 0, "viewers": 1000, "radiusM": 90, "warmupTicks": 100, "ticks": 1200, "tickMs": {"p50": 0.457, "p99": 0.796, "max": 1.348, "mean": 0.471}, "allocationsAfterWarmup": 0, "interestSetAvg": 23.3, "interestSetMax": 37, "fullSets": 0, "entersPerTick": 49.7, "leavesPerTick": 49.8, "cellMovesPerTick": 25.2, "droppedChanges": 0, "pass": true},
    {"id": "channel-1000p-4000m", "what": "full channel: 1,000 players with interest sets + 4,000 moving monsters, all in a 1,024 m field", "players": 1000, "monsters": 4000, "viewers": 1000, "radiusM": 90, "warmupTicks": 100, "ticks": 1200, "tickMs": {"p50": 1.661, "p99": 2.508, "max": 3.728, "mean": 1.690}, "allocationsAfterWarmup": 0, "interestSetAvg": 117.7, "interestSetMax": 158, "fullSets": 0, "entersPerTick": 249.1, "leavesPerTick": 248.7, "cellMovesPerTick": 126.3, "droppedChanges": 0, "pass": true},
    {"id": "town-crowd-1000p-4000m", "what": "worst case: 1,000 players crowding a 256 m town (70 m radius) + 4,000 monsters in the 1,024 m field", "players": 1000, "monsters": 4000, "viewers": 1000, "radiusM": 70, "warmupTicks": 100, "ticks": 1200, "tickMs": {"p50": 2.523, "p99": 4.051, "max": 27.672, "mean": 2.614}, "allocationsAfterWarmup": 0, "interestSetAvg": 268.0, "interestSetMax": 353, "fullSets": 0, "entersPerTick": 663.7, "leavesPerTick": 658.5, "cellMovesPerTick": 127.6, "droppedChanges": 0, "pass": true}
  ]
}

Tests

pass zone tick bench (release, 3 loads) · 7.7 s · raw log$ cargo run --quiet --release -p zoen-zone --bin bench-tick -- --out logs/evidence/M1-03/tick-bench.json
pass pnpm sim:test (workspace tests + clippy -D warnings) · 14.8 s · raw log$ pnpm sim:test
pass pnpm data:test (incl. netcode.test.mjs, roadmap M1-03B) · 7.5 s · raw log$ pnpm data:test
pass data tests · 8.2 s · raw log$ pnpm data:test
pass zoen-sim cargo tests + clippy · 9.0 s · raw log$ pnpm sim:test
pass Zoen.Core dotnet build + xunit · 2.8 s · raw log$ pnpm core:test
pass docs lint + index · 0.3 s · raw log$ pnpm docs:lint
pass wiki typecheck · 6.2 s · raw log$ pnpm --filter @zoen/wiki typecheck
pass wiki production build · 12.2 s · raw log$ pnpm wiki:build
pass wiki e2e (changed specs) · 0.8 s · raw log$ pnpm --filter @zoen/wiki exec playwright test --only-changed

Notes

2026-10-10 09:09 UTC · Part A built. Layout: services/zone = crate zoen-zone (workspace members crates/* + services/*): zone.rs pure tick(state, inputs, out) = zoen_sim::step (reused, parity test vs plain step) -> grid.rs SpatialHash (16 m uniform grid 256x256, intrusive per-cell lists, O(1) insert/move/remove, overflow-safe radius query) -> interest.rs InterestSets (bitset + member list per viewer, enter/leave spans, nearest-kept cap) -> sim state hash; scheduler.rs FixedRateScheduler separate from the tick (Clock trait, absolute deadlines start + n*1s/hz in u128 = no drift; catch-up policy: run all due ticks back to back up to maxTicksPerFrame=4, older backlog dropped + schedule re-anchored to now + 1 period, stall counted; metrics: over-budget, over-p99-target, lateness, catch-up, dropped, 10 us histogram percentiles); rng.rs SplitMix64 for tests/bench/bots. New crates/zoen-data (JSON reader + load_data moved out of zoen-sim-tools, which re-exports them; servers must not link sim-tools). Data moved to packages/game-data/data/netcode.json (documented: tick budget 50 ms, p50 11, p99 15, per-system budgets, cell 16 m, grid 256x256, radius 90 m, town 70 m, maxSentPerSnapshot 150, channel soft 900 / hard 1000 / 85 percent / 4000 monsters; proposed with note: scheduler.maxTicksPerFrame 4, interestSet.leaveMarginM 5, interestSet.maxSize 512). Tests: grid 20k random ops vs brute force; interest sets vs brute-force reference with hysteresis, despawns, attach/detach, radius switches and the cap; scheduler on a test clock (90k ticks at 30 Hz no drift, catch-up, stall drop, overruns, percentiles, real-clock 10 ticks); zone determinism + sim parity; no_alloc at full channel size (0 allocations in 100 ticks). Bench (release, 1 thread, this Mac): movers-1000 p99 0.796 ms, channel-1000p-4000m p99 2.508 ms, town-crowd-1000p-4000m p99 4.051 ms (max 27.7 ms, machine noise), 0 allocations after warmup -> logs/evidence/M1-03/tick-bench.json; pnpm zone:bench. Docs: NETCODE_1000.md points to netcode.json, ADR-003 build status, ARCHITECTURE layout; data test netcode.test.mjs. Remaining = M1-03B (roadmap card added): delta snapshots (baselines/acks, bit-packing, priority accumulators + near/mid/far rates), packages/protocol schema entries + Rust generator, WebSocket endpoint for the M1-04 bots. Multi-core (rayon/4 cores) not needed yet: single thread already under target.