The umpire reads a list, and the bibs come out to meet it

The late game is repetitive because the optimal move never changes: kill the
nearest thing, now. This is the first swing at a mode where it does — a
coaching drill where the umpire CALLS a colour sequence aloud while bibbed
bodies take their marks, and you must put them down in that order while they
walk at you. Uncalled colours are decoys you have to ignore. Two faults is a
double fault and the round is theirs.

Back room only (DRILL TEST, or #drill): this is a feel-test for one question —
tense, or just noisy? — so it keeps its own round count and touches neither
tennis scoring nor the run.

- voiceSeq: voice() drops a line that would start while the umpire is still
  talking, which is fatal for a list. Decode first, queue at accumulated
  offsets, and the anti-overlap rule is satisfied instead of fought.
- Four colours with PIPS as well as hue — red/green alone makes the mode
  unplayable for ~8% of men.
- drawBib is a ponytail-marked overlay that knowingly breaks the sprite rule;
  it is the thing to delete if the mode survives.
- BOTH target pickers snap (flickTarget and zombieTarget). Snapping only the
  slingshot left fifteen tap-serves in a row scoring nothing.
- Contact costs the point, not the run — permadeath stays in the gore mode.
- startDrill disarms the ordinary schedule; updateDrill redeals and logs an
  INVARIANT if anything empties its court.

Eight new tests (24 total) pin the ownership contract both ways, the call
beat, the win, the double fault, the un-winnable-round guard and the death.
PROJECT.md carries the four open questions for the playtest.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
wuwiandClaude Opus 5 committed 2026-08-11 18:01:33 +02:00
1 parent c659b6882f
commit 22df96cc1d
17 files changed
+556 -19

No files matched your search

+8 -1
View File
@@ -6,6 +6,7 @@
import { TUNE, UI, COURT, CONTACT, BOSS_ART, bossArt, WEAPONS, BARREL_WEAPONS, woundStage, zombieH, EATEN_BURST, EATEN_SUMMARY, EATEN_FEED_R } from './config.js';
import { G, P, ball, sendBoy, setupPoint } from './state.js';
import { splat, moan, pock, thud, zap, voice, boom, swish, shriek, aww, sfx , whoosh, flame } from './audio.js';
import { drillKill, drillCaught } from './drill.js';
import { splatPoint, cheer, spentBall, aside, grantWeapon, endBurst, sliceCarom } from './gameplay.js';
import { resolveInterlude, endBossFight } from './beats.js'; // runtime-only cycle, like the gameplay one
import { tally, multiKill, endRun } from './stats.js';
@@ -280,6 +281,9 @@ export function hurtPlayer(n) {
// here exactly once.
export function killPlayer() {
if (G.mode === 'eaten') return;
// In the drill, being caught costs the POINT and not the run — a deliberate contract change,
// see drillCaught. Permadeath stays where it was settled: the gore mode.
if (G.drill) return drillCaught();
gore.push({ x: P.x, z: P.z, r: 0.9, a: 0.9 }); if (gore.length > 40) gore.shift();
for (let k = 0; k < 30; k++) parts.push({
x: P.x, y: 1.1, z: P.z,
@@ -471,6 +475,9 @@ function bossDeath(zb) {
function killZombie(i, quiet) {
const zb = zombies[i];
// The drill's ONE rule, read at the single point every weapon routes through — hooking the ball
// path instead would have the swish, the pellets and the blast silently exempt from the order.
if (G.drill && zb.bib !== undefined) { drillKill(zb); zombies.splice(i, 1); return; }
const mul = GORE_MUL[zb.variant] || 1;
// BEFORE the early returns below: the ball machine is furniture but it is still a thing you put
// down, so it earns a row. The barrel is loot and scores nothing anywhere else in the game —
@@ -935,7 +942,7 @@ export function updateZombies(dt) {
if (gw.rate && (G.mgCool -= dt) <= 0) { firePellet(gw); G.mgCool = 1 / gw.rate; }
if (G.mgT <= 0) { endBurst(); setupPoint(); } // window over: spend the tin, resume the serve loop
}
if (!G.horde && !G.changeover && !G.interlude && !G.clearing && !G.bossFight) { // breathers, duels, the mop-up after a won game AND a boss fight all get a quiet court (no new spawns)
if (!G.horde && !G.changeover && !G.interlude && !G.clearing && !G.bossFight && !G.drill) { // breathers, duels, the mop-up after a won game, a boss fight AND a drill round all get a quiet court (no new spawns)
spawnT -= dt;
// The spawn director: games finished sets both crowd size and tempo. Both curves used to hit a
// wall — the cap pinned at 14 by game 11 and the gap floored at 1.0 s by game 10, after which