The rounds stay, the seams go: one continuous stream

Oliver: "daniel announcing is maximum annoying" / "don't let the rush in
position just let them coming from the back in steady speed.. try to space
them not too close" / "when one combination is done just let the new one in..
in a continuous stream that increases in speed and difficulty" / "the rounds
can stay, they should be continuous".

The round survives as the unit. Every seam around it is deleted: no walk-on
to a mark, no announcement beat, no between-rounds pause, no deal. Bodies
walk in from the far baseline forever and a round is drawn from whoever is on
court — finish one and the next is drawn in the same frame, off the same
rolling population. The bodies you were ignoring a second ago are the ones
being called now.

- Daniel is gone from this mode. Each colour has a pitch; a round announces
  itself as a four-note figure in half a second (blip()). drillTones 0 mutes
  it. The recordings stay in assets.
- PLACEMENTS is deleted with the marks — nothing rushes anywhere and nothing
  stops, so there are no shapes to place.
- A DRILL BODY HOLDS ITS LANE. updateZombies homes bodies onto the player's x
  past the net, so they arrived spread and bunched into a knot right in front
  of you; spawn spacing cannot survive that. Their lane is the puzzle here.
- One ramp on TIME SURVIVED drives speed, spawn gap and round length. Speed
  is stamped per body at spawn, so the ramp reads as newcomers outpacing the
  ones already walking, not the court accelerating under you.
- The empty-court watchdog is gone and nothing replaced it: with a stream
  there is no state to be stuck in. One line covers every case.

27 tests, rewritten around the stream.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
wuwiandClaude Opus 5 committed 2026-08-11 19:06:48 +02:00
1 parent 435a470ec7
commit 1d49c4343b
9 files changed
+286 -287

No files matched your search

+93 -88
View File
@@ -273,22 +273,15 @@ test('a burst fires through the beat that follows the kill that emptied the cour
// ---- the coaching drill: a new mode, so it owes the ownership contract a test ------------------
// Drive a drill round past its announcement: no serving and no targets while the umpire reads.
function drillPlaying({ bodies } = {}) {
// Pin the court size where a test needs a guaranteed decoy (bodies > the called sequence). The
// count is a RANGE in play, so an unpinned round can legitimately deal no decoy at all.
const min = TUNE.drillBodiesMin, max = TUNE.drillBodies;
if (bodies) { TUNE.drillBodiesMin = TUNE.drillBodies = bodies; }
// Run the stream for a while. There is no announcement beat and no between-rounds pause any more —
// a round is always live once two bodies are on court.
function drillPlaying(seconds = 1) {
startDrill();
TUNE.drillBodiesMin = min; TUNE.drillBodies = max;
for (let i = 0; i < TUNE.drillCallT * 60 + 4; i++) update(DT);
for (let i = 0; i < seconds * 60; i++) update(DT);
}
// kill a body the way every weapon does — through the shared funnel, via the ball's contact test
// A shambler soaks TUNE.shamblerHp splat-quality hits, so this re-arms the ball until the body is
// actually down — the drill keeps the roster's toughness on purpose (Oliver: "some are easier, some
// more difficult to kill"), which means a round is paced by hp as well as by order.
// Returns false when the body is already down — a ball carries through the lane, so a shot at one
// body can take the one standing behind it (see PROJECT.md's open question 1).
// kill a body the way every weapon does — through the shared funnel, via the ball's contact test.
// Returns false when it is already down: a ball carries through the lane, so a shot at one body can
// take the one behind it (see PROJECT.md's open question).
const drillKillBib = bib => {
const zb = zombies.find(z => z.bib === bib);
if (!zb) return false;
@@ -319,44 +312,88 @@ test('a drill refuses to start while another mode owns the court', () => {
assert.equal(G.drill, null, 'and leave no half-built round behind');
});
test('the call is not play: no target and no serving until the umpire finishes', () => {
test('the stream never stops, is spaced, and a round is always live', () => {
startDrill();
assert.ok(G.drill.callT > 0, 'the walk-on is running');
assert.equal(drillTarget(), null, 'nothing is a legal target during the call');
assert.ok(zombies.length >= TUNE.drillBodiesMin && zombies.length <= TUNE.drillBodies,
'the whole court is placed up front, ' + TUNE.drillBodiesMin + '..' + TUNE.drillBodies + ' of them');
assert.equal(new Set(zombies.map(z => z.bib)).size, zombies.length, 'every bib on court is distinct');
for (let i = 0; i < TUNE.drillCallT * 60 + 4; i++) update(DT);
assert.ok(drillTarget(), 'and the first target arms when the call ends');
assert.equal(drillTarget().bib, G.drill.seq[0], 'which is the first colour called');
assert.ok(zombies.length >= 2, 'seeded, so the first round is callable at once');
assert.ok(G.drill.seq.length >= 2, 'and a round is already called');
const seeded = zombies.length;
// 18 s, deliberately short of the ~28 s it takes a body to walk the court: a test that never
// shoots WILL eventually be caught, and drillCaught sweeps the court — which is correct behaviour
// and would make this look like the stream had stalled.
for (let i = 0; i < 18 * 60; i++) update(DT);
assert.ok(zombies.length > seeded, 'bodies keep arriving: ' + seeded + ' -> ' + zombies.length);
assert.ok(zombies.length <= TUNE.drillMaxOnCourt, 'never past the court cap');
assert.equal(new Set(zombies.map(z => z.bib)).size, zombies.length, 'a colour is on court at most once');
// nothing rushes to a mark and stops: every body carries a real speed and keeps it
assert.ok(zombies.every(z => z.v > 0), 'every body is still walking');
// SPACING HAS TO SURVIVE THE WALK IN, not just the spawn. Drill bodies hold their lane (the
// gore mode's homing onto the player's x bunched them into a knot right in front of you), so
// this checks every body on court, not only the ones that just arrived.
for (let i = 0; i < zombies.length; i++) for (let j = i + 1; j < zombies.length; j++)
assert.ok(Math.abs(zombies[i].x - zombies[j].x) > 0.3 || Math.abs(zombies[i].z - zombies[j].z) > 0.8,
'two bodies ended up on top of each other');
// and the lane a body walks in on is the lane it keeps. Tracked by IDENTITY, not by index —
// the array shifts as bodies cross the baseline and despawn, so zombies[i] is not the same body
// six seconds later (this compared two different zombies and read as a 1.5 m drift).
const lanes = new Map(zombies.map(z => [z, z.x]));
for (let i = 0; i < 6 * 60; i++) update(DT);
for (const z of zombies) if (lanes.has(z))
assert.ok(Math.abs(z.x - lanes.get(z)) < 0.5,
'a drill body drifted out of its lane: ' + lanes.get(z).toFixed(2) + ' -> ' + z.x.toFixed(2));
});
test('killing in the called order wins the round; the tennis ladder stays out of it', () => {
test('the stream ramps: later arrivals are faster, come sooner, and the round gets longer', () => {
// eatenR 0 makes the player untouchable — the established soak trick (PROJECT.md). Without it a
// test that never shoots gets caught, the court is swept, and the measurement is of the reset.
const eaten = TUNE.eatenR; TUNE.eatenR = 0;
try {
startDrill();
const early = [];
for (let i = 0; i < 20 * 60; i++) { update(DT); for (const z of zombies) if (!z.__seen) { z.__seen = 1; early.push(z.v); } }
const earlyRound = G.drill.seq.length;
G.drill.t = TUNE.drillRampT; // jump to the top of the ramp
const late = [];
for (let i = 0; i < 20 * 60; i++) { update(DT); for (const z of zombies) if (!z.__seen) { z.__seen = 1; late.push(z.v); } }
const avg = a => a.reduce((x, y) => x + y, 0) / Math.max(1, a.length);
assert.ok(late.length > early.length, 'they arrive more often: ' + early.length + ' -> ' + late.length);
assert.ok(avg(late) > avg(early) * 1.5, 'and they walk faster: ' + avg(early).toFixed(2) + ' -> ' + avg(late).toFixed(2));
G.drill.seq = []; update(DT); // draw a round at the top of the ramp
assert.ok(G.drill.seq.length > earlyRound,
'and the round is longer: ' + earlyRound + ' -> ' + G.drill.seq.length);
} finally { TUNE.eatenR = eaten; }
});
test('finishing a round rolls straight into the next one — no pause, no deal', () => {
drillPlaying();
const seq = G.drill.seq.slice(), games = score.games.slice(), pts = score.pts.slice();
const seq = G.drill.seq.slice();
for (const bib of seq) drillKillBib(bib);
assert.equal(G.drill.won, 1, 'the round is won');
assert.equal(G.drill.faults, 0, 'cleanly');
assert.deepEqual(score.games, games, 'the drill does not bank tennis games');
assert.deepEqual(score.pts, pts, '...nor tennis points — it keeps its own count');
for (let i = 0; i < TUNE.drillBeat * 60 + 4; i++) update(DT);
assert.ok(zombies.length > 0, 'and the next round places a fresh court');
assert.ok(G.drill.rounds >= 1, 'the round is banked');
assert.ok(G.drill.seq.length >= 2, 'and the next one is ALREADY called, in the same frame');
assert.equal(G.drill.step, 0, 'from the top');
assert.ok(zombies.length > 0, 'off the bodies still walking at you — the court is never cleared');
});
test('two wrong bodies is a double fault, and the round goes to them', () => {
drillPlaying({ bodies: 5 }); // a full court, so a decoy is guaranteed
// a colour the sequence never called is a DECOY — the body you must let walk at you
test('the drill keeps its own count; the tennis ladder stays out of it', () => {
drillPlaying();
const games = score.games.slice(), pts = score.pts.slice();
for (const bib of G.drill.seq.slice()) drillKillBib(bib);
assert.deepEqual(score.games, games, 'the drill does not bank tennis games');
assert.deepEqual(score.pts, pts, '...nor tennis points');
});
test('two wrong bodies is a double fault, and the round is scrubbed', () => {
drillPlaying();
const decoy = zombies.find(z => !G.drill.seq.includes(z.bib));
assert.ok(decoy, 'setup: the court carries at least one decoy');
assert.ok(decoy, 'setup: the court carries a body that was not called');
drillKillBib(decoy.bib);
assert.equal(G.drill.faults, 1, 'a decoy is a fault');
assert.equal(G.drill.faults, 1, 'an uncalled body is a fault');
assert.equal(G.drill.lost, 0, 'but not the round');
assert.ok(G.drill.rush > 0, 'the rest lose patience — the punishment is tempo');
const wrong = zombies.find(z => z.bib !== G.drill.seq[G.drill.step]);
assert.ok(wrong, 'setup: something is still out of turn');
drillKillBib(wrong.bib);
assert.equal(G.drill.lost, 1, 'the second one takes the round off you');
assert.equal(G.drill.won, 0);
if (wrong) {
drillKillBib(wrong.bib);
assert.equal(G.drill.lost, 1, 'the second one takes the round off you');
assert.equal(G.drill.faults, 0, 'and a fresh round is called with a clean slate');
}
});
test('THE AUTO-AIM IS GONE: the shot goes where you point it', () => {
@@ -370,59 +407,27 @@ test('THE AUTO-AIM IS GONE: the shot goes where you point it', () => {
assert.notEqual(ft.zt && ft.zt.bib, called, 'pointing away from the called body must not hit it');
});
test('rounds vary: which colours turn up, how many, and where they stand', () => {
startDrill();
const seen = { cols: new Set(), counts: new Set(), shapes: new Set() };
for (let r = 0; r < 40; r++) {
for (const z of zombies) seen.cols.add(z.bib);
seen.counts.add(zombies.length);
seen.shapes.add(G.drill.shape);
G.drill.beat = 0.001; update(DT); update(DT); // deal the next round
}
assert.ok(seen.cols.size > 4, 'the palette is bigger than any one court: ' + seen.cols.size + ' colours seen');
assert.ok(seen.counts.size > 1, 'the body count moves: ' + [...seen.counts]);
assert.ok(seen.shapes.size > 2, 'the placement shape moves: ' + [...seen.shapes]);
});
test('a wrong kill never leaves an un-winnable round', () => {
drillPlaying({ bodies: 5 });
const later = G.drill.seq[G.drill.seq.length - 1]; // kill the LAST call first
drillKillBib(later);
assert.equal(G.drill.faults, 1, 'out of turn is a fault');
assert.ok(!G.drill.seq.includes(later), 'and the dead colour leaves the list rather than blocking it');
assert.ok(G.drill.seq.every(b => zombies.some(z => z.bib === b)),
'every colour still on the list is still on the court');
drillPlaying();
const later = G.drill.seq[G.drill.seq.length - 1]; // kill the LAST call first
if (G.drill.seq.length > 1 && drillKillBib(later)) {
assert.ok(!G.drill.seq.includes(later), 'the dead colour leaves the list rather than blocking it');
assert.ok(G.drill.seq.every(b => zombies.some(z => z.bib === b)),
'every colour still on the list is still on the court');
}
});
test('being caught costs the point, not the run', () => {
test('being caught costs the point and the court, not the run', () => {
drillPlaying();
const t0 = G.drill.t;
killPlayer();
assert.notEqual(G.mode, 'eaten', 'permadeath stays in the gore mode');
assert.equal(G.drill.lost, 1, 'they take the round instead');
assert.equal(zombies.length, 0, 'and the court is cleared for the next one');
});
test('an emptied court cannot machine-gun the umpire', () => {
// Oliver, on a phone: "the drill mode immediately goes into some horrible audio loop". The trigger
// was never reproduced on desktop (a suspended AudioContext has a frozen clock, so a scheduled
// sequence all fires at once on resume — see voiceSeq), but the AMPLIFIER was here: the
// empty-court net used to redeal on the spot, which makes the repair rate the frame rate, and
// every deal calls the umpire. Whatever empties the court, it must never announce more than once
// per beat.
drillPlaying();
let deals = 0;
const seq0 = G.drill.seq;
for (let i = 0; i < 60 * 6; i++) { // six seconds with the court emptied every single frame
zombies.length = 0;
update(DT);
if (G.drill.seq !== seq0 && G.drill.callT > 0 && zombies.length) { deals++; }
if (zombies.length) zombies.length = 0; // ...and emptied again, so it can never settle
}
const repairs = G.missLog.filter(m => m.result === 'INVARIANT').length;
assert.ok(repairs > 0, 'the net notices and leaves evidence');
assert.ok(repairs <= 6 / TUNE.drillBeat + 2,
'at most one repair per drillBeat, not one per frame — got ' + repairs);
assert.ok(deals <= 6 / TUNE.drillBeat + 2, 'and at most one deal (one umpire call) per beat — got ' + deals);
assert.equal(G.drill.lost, 1, 'they take the point instead');
assert.equal(zombies.length, 0, 'the court is swept — the one thing that interrupts the stream');
assert.ok(G.drill.t < t0, 'and the ramp backs off');
for (let i = 0; i < 10 * 60; i++) update(DT);
assert.ok(zombies.length > 0, 'then the stream simply resumes');
assert.ok(G.drill.seq.length >= 2, 'and a round is live again');
});
test('endDrill hands the court back', () => {