The marker stops lying, and the rule announces itself

"the marker flips all the time even during a ball being midair.. i am not
sure that a player would get the mechanic intuitively let alone without the
marker".

The flip had a specific cause and it was a design bug, not jitter. With lanes
held and speeds constant, arrival differences never change while bodies walk
— the queue only reorders on a kill or an arrival. The marker flipped because
it showed only q[0] while the rule accepts any near-tie within the slack:
every kill made it jump between tied bodies, so the marker was stricter than
the rule it was explaining. drillLegal() marks the whole slack set now,
bright head and dim ties, which is stable by construction. Measured: zero
head-changes across 12s of walking with no kills.

Learnability, two answers. The rule gets one modal, once per run, on the
mode's own calm court — CLEARANCE DRILL / DROP WHOEVER WOULD REACH YOU FIRST
/ FAST BEATS CLOSE / STREAKS PAY — because "most urgent first" is not
guessable from a chevron alone. And the markers are training wheels that take
themselves off: hidden at streak >= 8 (the streak is the evidence you can
read the court), back on any fault. The teacher steps in exactly when you
demonstrably need it.

31 tests, 15 seeds clean.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
wuwiandClaude Opus 5 committed 2026-08-11 21:44:58 +02:00
1 parent 6a16e77480
commit be191dba82
6 files changed
+100 -24

No files matched your search

+25 -2
View File
@@ -83,10 +83,13 @@ globalThis.getComputedStyle = () => ({ paddingTop: '0' });
// ---- game modules (real code) ----------------------------------------------
const { G, P, score, ball, setupPoint } = await import('./src/state.js');
const { update, forceZombies, splatPoint, restartRun, flickTarget } = await import('./src/gameplay.js');
const { startDuel, resolveInterlude, startBossFight, endBossFight, startDrill, endDrill } = await import('./src/beats.js');
const { startDuel, resolveInterlude, startBossFight, endBossFight, startDrill: _startDrill, endDrill } = await import('./src/beats.js');
// the drill's first-sight modal freezes the world (that is what modals do) — dismiss it the way a
// player would, so the tests step a live court
const startDrill = (...a) => { const r = _startDrill(...a); G.modal = null; G.modalQ.length = 0; return r; };
const { zombies, spawnZombie, pellets } = await import('./src/zombies.js');
const { killPlayer } = await import('./src/zombies.js');
const { drillQueue, arrival, drillMult, DRILL_TYPES } = await import('./src/drill.js');
const { drillQueue, drillLegal, arrival, drillMult, DRILL_TYPES } = await import('./src/drill.js');
const { TUNE, bossArt, COURT } = await import('./src/config.js');
const DT = 1 / 60;
@@ -467,6 +470,26 @@ test('drillOneShot is a switch, and OFF means the roster HP is intact', () => {
else assert.ok(heavy.hp > 1, 'one-shot off: a ' + heavy.variant + ' keeps its ' + heavy.hp + ' hp');
});
test('the rule announces itself once, and the markers are the legal SET', () => {
_startDrill(); // the raw one: we WANT the modal this time
assert.ok(G.modal, 'first drill of a run: the rule gets its one sentence');
assert.equal(G.modal.title, 'CLEARANCE DRILL');
G.modal = null;
endDrill(); _startDrill();
assert.equal(G.modal, null, 'second drill of the same run: no re-teaching');
G.modal = null;
// the legal set: everything within the slack of the head, and never anything beyond it
zombies.length = 0; G.drill.spawnT = 999;
for (const [dz, v] of [[6, 1.0], [6.5, 1.0], [16, 1.0]]) { // 6 s, 6.5 s, 16 s out
spawnZombie(0, COURT.L - dz, 'a');
const zb = zombies[zombies.length - 1];
zb.drill = true; zb.pts = 20; zb.v = v; zb.hp = zb.hpMax = 1;
}
const legal = drillLegal();
assert.equal(legal.length, 2, 'the 0.5 s tie is IN the marker set (slack ' + TUNE.drillSlack + ')');
assert.ok(!legal.some(z => Math.abs(arrival(z) - 16) < 0.5), 'the 16 s body is not');
});
test('endDrill hands the court back', () => {
drillPlaying();
endDrill();