A lane is free when its body is gone, not when it is far away

The drill spawner measured its best-of-9 lane spacing only against bodies still
in the back half (drillNearZ 9 m). On a court where lanes are HELD for life and
speed is stamped at spawn, a body 12 m down still owns its lane — handing it to
a faster arrival books an overtake for ten seconds later, mid-court, as two
sprites walking through each other. That is what the seed-sensitive test failure
(SEED=1234) was reporting: the assertion was right, the stream was not.

Spacing now measures against every live drill body; drillNearZ is deleted (no
other reader). Still best-of-9, no retry loop, so the stream cannot stall.

60 seeds x 150 s of drill: before, 7 seeds overlapped with narrowest clearance
0.00 m; after, zero overlap frames, narrowest 0.46 m (the test asks > 0.3).
Suite green on 20260811/1234/777/99/424242/8 and on 299 of seeds 1..300; the
straggler (SEED=80, the slid-serve return rate) is unrelated and logged as open.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
wuwiandClaude Opus 5 committed 2026-08-16 08:45:31 +02:00
1 parent 86b8275f5d
commit bdc3f23f92
4 files changed
+35 -9

No files matched your search

+26 -3
View File
@@ -414,6 +414,29 @@ zombie (now an occasional opponent, `TUNE.racketEvery`, not the every-game fixtu
`RETURNED`, `DOUBLE KILL`) is a footnote to something you already watched happen. Sharing the loud `RETURNED`, `DOUBLE KILL`) is a footnote to something you already watched happen. Sharing the loud
FACE with the milestone cards is right; sharing their SIZE made every footnote read as a milestone. FACE with the milestone cards is right; sharing their SIZE made every footnote read as a milestone.
## Session log: a lane is free when its body is gone, not when it is far away (2026-08-16)
A seed-sensitive red suite (`SEED=1234 node test.js` failed on the drill's "spaced *after the walk
in*" assertion) turned out to be the assertion telling the truth: **two drill bodies genuinely walked
through each other.** The spawner measured its best-of-9 lane spacing only against bodies with
`z < drillNearZ` (9 m) — sound for a court where bodies wander, wrong for this one, where a lane is
HELD for life and speed is stamped at spawn. A body 12 m down still owns its lane; handing that lane
to a faster arrival does not create a spacing problem at spawn, it books an overtake for ten seconds
later, mid-court, as two sprites occupying the same metre. The fix is the filter: spacing is measured
against every live drill body, `drillNearZ` is deleted (it had no other reader). No retry loop, so
the stream still cannot stall.
Measured, 60 seeds × 150 s of drill each: before, **7 seeds produced overlaps, narrowest clearance
0.00 m**; after, **zero overlap frames, narrowest 0.46 m** (the test asks for > 0.3). Peak on court
is still the cap of 7, so the fan is wider, not thinner. Suite green on 20260811 / 1234 / 777 / 99 /
424242 / 8, and on 299 of seeds 1..300 — the one straggler is unrelated and still open (see below).
**OPEN, found by the same fuzz:** `SEED=80` fails *"opponent one returns a slid serve: 5/10"*. That
assertion wants 6 of 10 sliced serves returned and the AI's real rate sits right on the threshold, so
it is a coin-flip gate of exactly the kind this file warns about. Untouched here because the honest
fix is a decision about the opponent, not the test: either widen `n` so the mean settles, or accept
5/10 as the real return rate and say so.
## Session log: THE NETPLAY LOBBY — a link to send, a code to type, a name to be (2026-08-15) ## Session log: THE NETPLAY LOBBY — a link to send, a code to type, a name to be (2026-08-15)
Oliver: *"multiplayer should be easier to setup — a share with link next to the code; also entering Oliver: *"multiplayer should be easier to setup — a share with link next to the code; also entering
@@ -1000,9 +1023,9 @@ are the ones being called now, which is a better decoy than the old fixed-batch
- **`PLACEMENTS` is deleted along with the marks.** Nothing rushes anywhere and nothing stops, so - **`PLACEMENTS` is deleted along with the marks.** Nothing rushes anywhere and nothing stops, so
there are no shapes to place — the geometry now comes from lane choice at spawn plus the depth the there are no shapes to place — the geometry now comes from lane choice at spawn plus the depth the
stream naturally strings bodies out into. That is a net DELETION of the fanciest code in the file. stream naturally strings bodies out into. That is a net DELETION of the fanciest code in the file.
- **Spacing is best-of-9 lane sampling** against everyone still in the back half, not a retry loop: - **Spacing is best-of-9 lane sampling** against every live drill body (it measured only the back
a hard minimum would silently stall the stream at exactly the moment it is meant to be getting half until 2026-08-16, see that session log), not a retry loop: a hard minimum would silently stall
harder, which reads as the mode dying. the stream at exactly the moment it is meant to be getting harder, which reads as the mode dying.
- **A DRILL BODY HOLDS ITS LANE**, and this was the fix the screenshot forced. `updateZombies` homes - **A DRILL BODY HOLDS ITS LANE**, and this was the fix the screenshot forced. `updateZombies` homes
bodies onto the player's x once they cross the net — a gore-mode behaviour — so they arrived nicely bodies onto the player's x once they cross the net — a gore-mode behaviour — so they arrived nicely
spread and then bunched into an unreadable knot right in front of you. Spawn spacing cannot survive spread and then bunched into an unreadable knot right in front of you. Spawn spacing cannot survive
+1 -1
View File
@@ -659,6 +659,6 @@ if (location.hostname !== 'splatter-tennis.wuwi.one') {
</div> </div>
</div> </div>
</div> </div>
<script type="module" src="src/main.js?v=274"></script> <script type="module" src="src/main.js?v=275"></script>
</body> </body>
</html> </html>
-1
View File
@@ -461,7 +461,6 @@ export const TUNE = {
drillSeed: 4, // bodies placed at the start so there is a queue to read from the first second drillSeed: 4, // bodies placed at the start so there is a queue to read from the first second
drillSeedGap: 2.6,// m of depth between those seeded bodies drillSeedGap: 2.6,// m of depth between those seeded bodies
drillSpawnZ: 0.6, // z they enter at (the far baseline is 0) drillSpawnZ: 0.6, // z they enter at (the far baseline is 0)
drillNearZ: 9.0, // spawn spacing is measured against bodies nearer than this
drillSpread: 3.4, // m either side of the centre line the lanes fan across drillSpread: 3.4, // m either side of the centre line the lanes fan across
drillSpeed: 0.85, // m/s at the bottom of the ramp (x the type's vMul)... drillSpeed: 0.85, // m/s at the bottom of the ramp (x the type's vMul)...
drillSpeedMax: 2.0,// ...and at the top. Stamped per body AT SPAWN — constant v is also what lets drillSpeedMax: 2.0,// ...and at the top. Stamped per body AT SPAWN — constant v is also what lets
+8 -4
View File
@@ -73,15 +73,19 @@ function pickType() {
return pool[Math.floor(Math.random() * pool.length)]; return pool[Math.floor(Math.random() * pool.length)];
} }
// ---- THE STREAM. One body at a time, spaced (best-of-9 lanes against the back half — a retry loop // ---- THE STREAM. One body at a time, spaced (best-of-9 lanes against EVERY live body — a retry loop
// would stall the stream exactly when it should be getting harder), holding its lane all the way in. // would stall the stream exactly when it should be getting harder), holding its lane all the way in.
// The measure used to ignore anything past the back half, which was wrong for a court where lanes are
// held for life and speeds are stamped per body: a lane 12 m down is still OWNED, and handing it to a
// faster body just books the overtake for ten seconds later, where it lands as two sprites walking
// through each other mid-court. A lane is free when its body is gone, not when it is far away.
function spawnDrillBody(d) { function spawnDrillBody(d) {
if (zombies.filter(z => z.drill).length >= TUNE.drillMaxOnCourt) return null; const live = zombies.filter(z => z.drill);
const near = zombies.filter(z => z.drill && z.z < TUNE.drillNearZ); if (live.length >= TUNE.drillMaxOnCourt) return null;
let bx = 0, bs = -1; let bx = 0, bs = -1;
for (let k = 0; k < 9; k++) { for (let k = 0; k < 9; k++) {
const x = -TUNE.drillSpread + (2 * TUNE.drillSpread) * (k / 8) + (Math.random() - 0.5) * 0.5; const x = -TUNE.drillSpread + (2 * TUNE.drillSpread) * (k / 8) + (Math.random() - 0.5) * 0.5;
const s = near.length ? Math.min(...near.map(z => Math.abs(z.x - x))) : 1e9; const s = live.length ? Math.min(...live.map(z => Math.abs(z.x - x))) : 1e9;
if (s > bs) { bs = s; bx = x; } if (s > bs) { bs = s; bx = x; }
} }
const t = pickType(); const t = pickType();