Hidden behind TUNE.aimCloud rather than deleted: the reading it gives — how
much width the swipe is buying, before the ball leaves — is the only teacher
the gesture has, and the panel can flip it back live for the next look at it.
And "a little harder to hit the out" is a number: flickReach 0.62 -> 0.52,
which halves the worst-case rate from about 15% to about 7%, with the test
tightened so it cannot drift back. The shape is untouched — a perfect flick
still never sails, a badly timed full swipe still can — but a mistimed one now
lands nearer the middle than the line, which is the only half of that trade a
player who cannot see his own timing can do anything about.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The rally flick was read as three lanes: sideways past 0.38 of its own length
took the whole corner, a hair under it took dead centre, and nothing lived in
between — "sometimes applied very strong, sometimes it barely does anything",
with no way to tell which one you had asked for. swayOf reads the swipe's LENGTH
as a continuous strength, exactly as the serve already did, and doHit spends
that instead of the lane vote. Keys keep the full lane; the opponent's aim has
no length and is untouched.
That is also what hands the OUT back: going out was never a choice, because
every aimed shot took the full width and then the scatter on top. Below about
0.7 of a full swipe — the line minus aimError, over the width timing can earn —
the worst mistiming in the game cannot sail it now, and above it the old risk
curve is exactly as tested.
And the aim is drawn while the thumb is still moving: the placement ring is a
receipt that arrives with the ball already gone, so the same ring is shown live
with a ray from the player's feet, cream while the ask is safe and hot once the
sway crosses that derived threshold. The receipt goes red when the shot really
is out, so the two rings together are the lesson.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Score-is-the-body was a picture: the opponent lost an arm and swung exactly as
far, lost its head and missed exactly as often. Two dials now read the same
racketStage the SKINS are picked with, so what you watch it lose is what it
loses — deliberately small (6% reach, 17.5% whiff per stage), texture under the
ladder rather than a second difficulty curve, with a test on that ceiling. An
unhurt body still returns exactly 1 and 1, and the whole thing is gated on
G.match: the gore duel's racket zombie is untouched.
And the placement flick, the mode's one act of skill, was taught nowhere on
touch — the placement ring is a receipt, not a lesson, and you only get one if
you already knew to swipe. The ghost hand teaches it now, in the same register
as PULL DOWN and TAP AS IT ARRIVES, riding the failure it answers: a run of
returns that left with no aim on them. The tap lesson still wins when both
qualify — placement is the wrong thing to teach someone who cannot reach the
ball.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The slid-serve test asked for 6 of 10 sliced serves returned. Measured over 200
trials a seed across eight seeds, the receiver's real rate is 0.85..0.92 — so
the assertion was sound and the SAMPLE was too small: at p ~= 0.88, 10 trials
land at or below 5 with probability ~0.004, one seed in 230, and SEED=80 was it.
n is now 40 at the same 60% bar, which puts the threshold five standard
deviations below the true rate while still failing instantly if the receiver
regresses toward the 0-in-24 the test was born from. It is free: a returned
serve breaks the frame loop early, and the suite still runs in ~0.11 s.
Green on seeds 1..400.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The trick: hard serve, slight slide, and the first ten opponents died where
they stood. Measured headless — that serve was returned 0 times out of 24 at
EVERY rung. Three things were wrong and all three were the same thing: he
waited on the tennis mark 1.3 m off the centre line (covering the T, conceding
the whole corner), he took no first step, and he chased the ball's LIVE
position, which during a serve is still up by the server's baseline.
Now: he waits on the bisector of the serves he has to cover, takes a
returner's first step (serve-scoped — the rally is untouched), and runs at the
bounce instead of at the ball. The slid serve comes back from rung one; a
flush serve into the very corner of the box still aces the bottom of the
ladder, which is the reward placement is supposed to earn.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Six sessions of asking an image model for a row of running figures, and it
never once drew a gait: three phases repeated with the same leg leading,
"ropeskipping, not running". A frame-by-frame LEFT/RIGHT skeleton spec, a
closing checklist, a loop clause, six figures instead of eight — all lost.
One 5-second image-to-video clip has no such problem, because it moves a
body instead of drawing poses. ~$0.42 a character through the key already in
.env, pose pinned at BOTH ends (pinned only at the front, she rotated into
profile halfway), stride period MEASURED off the frames rather than sampled
by eye — hers repeats in 6 clip frames, his in 23 — and the phases cut and
palette-snapped through _stride.cut. assets/_vidroll.py films and cuts it;
her first frame came from one still roll that took his approved pose as a
picture instead of a paragraph.
Wired: runN probes the sheet through SK() so the women's tour counts HER
frames, the player is no longer clamped to a two-frame pair, runHz 2.4 ->
1.9 ("this looks like roadrunner"), and the bench grew two PLAYER stops —
a cycle only ever seen mid-rally is a cycle nobody has watched.
Also in: the difficulty retune (medium is a real step above the shipped CPU
and is where `smart` first appears; hard stepped back off superhuman; easy
closed its gap) with MEDIUM's dial-for-dial pin to TUNE removed from the
test, and Oliver's own transparency fixes on the male frames.
SPRV 138 — the frames were right on disk and wrong on screen for an hour
because assets are cached for a year unless the ?v= moves. main.js v269.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Multiplayer setup moves out of a canvas modal and a prompt() dialog into one
menu screen: your name, the two ways to host, and the friend's code. The socket
opens from the menu and the game stays parked behind the overlay until the room
fills — netHooks.paired takes the court one line before netStart writes to it —
so the room code can carry a real SHARE button and a wrong code is a typo you
fix rather than a CONNECTION LOST card over a match that never began.
The name is the world board's name (stats.saveName), crosses on the existing
look message, and the scoreboard finally names the person instead of CPU.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Three things off the back of the picker.
THE LIVE PLAYER on the setup screen is the real sprite through the real tint, not
a mock-up of the pick — so the menu cannot promise a colour the court will not
keep. It redraws on every tap of a tour, a kit or a skin.
THE WOMEN'S TOUR IS NO LONGER SAT OUT. Her set is drawn head to toe in whites, so
there was no hue to rotate — the answer is not a dye but a WASH: the pick laid
over her whites at a low saturation with the lightness kept, which is how you dye
a white kit in life and gives rose, powder blue, mint, lilac. Her hair and skin
fall out of it on lightness alone, which is exactly what a dye could not manage.
THE FRIEND ACROSS THE NET now wears what they picked on their own phone. Their
look crosses on pairing, and since the opponent art is red rather than blue the
kit rides its own hue arc — safe there for the one reason it would not be on the
player's own sheets: the opponent sprites carry no drawn blood. An older build
simply never sends it and the opponent draws as before; the match still plays.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Testers asked for some control over how their player looks. From behind — the
only way you ever see him — there are two things worth controlling, and neither
needs a single new asset: the kit and the skin are one hue-bucket pixel pass over
the sheets already loaded, cached per frame and built on first draw.
The buckets are measured, not guessed, and three neighbours had to be fenced off
one at a time: the shirt's warm HIGHLIGHTS (which spattered the man with mud on
every dark pick), the drawn BLOOD on the death frames (which turned the pool
brown — the worst bug this file could ship), and the women's HAIR, which is the
same hue as his shorts and cannot be told apart from it by any rule. Her set is
played in whites and has no kit to dye, so it is flagged out and the row says so
rather than silently doing nothing.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Every run starts the ambience, and the ambience was un-ducking both court beds
at once — so once you had played the gore mode, its music followed you into
classic tennis for the rest of the session. Leaving the court is one event;
arriving is not. Each bed brings itself back and nothing else.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Splatter gore no longer makes you win a game of tennis first — the mode you
pick is the mode you get, with the man you came to play already turned. The
transition itself is untouched and still reachable; it just isn't a toll booth.
The menu theme could only ever be started, never brought back, so the first
run of a session silenced it for good while the court's ambience followed you
into the menu. Starting a bed now un-ducks it too.
Three world boards instead of one: kills, opponents felled, and games taken
off the CPU, each with its own name prompt and its own row per player.
...and the gore economy stops paying a barrel for every game won past game 16,
while the shamblers, the mix and the ball machines keep escalating past the
point they all used to stop.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Six new opponent families (head coach, stringer, strawberry seller, chair
umpire, spectator, roller man) through the famroll/zsheet route, placed by
hierarchy into an 18-rung ladder; the champion steps up behind them.
THE DRAW: picking the tournament now rolls its own bracket in front of you —
only the final was ever settled — and what lands is what walks on.
Netplay, off Oliver's cellular report: the mid-flight early-decide was missing
its !G.net gate, so the striker judged its own outbound flight and the two
scoreboards drifted a point apart into a court nobody could serve on. The
host's court now rides the room, its scoreboard rides the pretoss stream, and
a called ball rolls to its own halt instead of freezing where the call caught it.
Plus the night court, and somebody in the stand is taking pictures.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Two retest findings, one root: the death match borrowed the roster's
body. Gone — nothing spawns, no walk-on card, the living opponent is the
whole picture. His damage reads as blood (bleedO: an anchor per point
taken this game, love to game point, thin drops as ever), the game
point bursts him where he stands, and IT RISES AGAIN brings him back
clean. The heal beat became a card in netplay — a modal freezes this
world while the other phone's keeps moving.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Three chips under VS A FRIEND — the host's pick rides the room and the
start message rebuilds both courts on pairing. In the death match your
opponent IS the zombie: their lost points tear its wound stages, their
won points draw your blood, and bleeding out crosses the wire as the
other phone's win. The same receiver-is-judge verdicts drive the match
machinery through resolveInterlude, so the fall, the mop beat and the
burst come along unmodified. New: gushProne — the game-winning body
bleeds out on the ground, from prone-height anchors, until it bursts.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
One event on the toss, replayed as a real toss over O — update() already
poses the server off the phase and drops an uncaught ball back to pretoss
by plain physics, so the wind-up, the cancel and the re-toss all carry for
free. The strike's shot event overwrites whatever drifted.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
VS A FRIEND on the mode screen (host a code, join a code), a ~110-line
RFC6455 relay in server.py that holds no game state, and src/net.js: each
phone plays itself near-side and the opponent's court is this one rotated
half a turn about the net. The phone a ball flies toward makes every call
on it; the striker's copy is picture. Six tests pin the rule from both
seats. No reconnect yet — a dropped radio is a card and a reload.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Oliver benchmarked HARD against the game's own roster: "the runner in zombie
mode is way faster than the regular player in hard" — and the numbers agreed.
The RUNNER covers 6.15 m/s (racketSpeed x rung x foe.speed) with no reaction
gate; HARD's classic CPU did 5.59, only after its 0.22 s read, and ambled
back to centre at HALF speed between shots — that recovery jog was the free
points. HARD now moves 4.3 x 1.5 = 6.45 m/s, and DIFFS grows `recover`: the
between-shots factor, 0.85 on HARD where the shipped 0.5 stays pinned on
MEDIUM. Wrong-footing him still works; strolling him side to side no longer
does.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
"the hard mode is still too easy — make it really challenging." The dial that
was missing is aim: the CPU rolled a random lane on every shot, so a third of
everything came politely back to your racket regardless of the other six
numbers. DIFFS grows `smart` — the probability the CPU plays the OPEN court,
the corner away from P.x — and HARD sets it to 0.8: four shots in five now
make you run. EASY and MEDIUM keep random aim (MEDIUM pinned by test), so
the shipped classic is untouched below the top chip.
The rest of HARD tightened in the same pass: reaction 0.22 s, speed 1.30,
reach 1.85 m, quality floored at 0.70 under a cap of 1.0, serve 1.15, flub
one ball in a hundred. It reads your shot almost instantly, covers what it
reads, and places its reply where you are not.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
"using the difficulty in classic tennis I don't feel any difference" — and he
was right, verified live: G.diff arrives intact, the dials were just the
wrong ones. flub/cap/speed barely register: the quality cap trims a
distribution whose 0.45 floor never moved (EASY and HARD averaged 0.59 vs
0.70), and ±12% on aiSpeed never binds because the CPU pre-plans its
intercept. What a player reads as SKILL is how fast the CPU reacts to the
shot (aiReaction — the config's own comment says corners are winners because
of it), how long its racket is (aiReach), the FLOOR of its shot quality (a
raised qMin means every ball comes back with pace), and the serve — none of
which the first cut touched, and the serve was hardcoded 0.6-0.95 besides.
DIFFS carries seven dials now with real spread: EASY whiffs one ball in
three, reads late, swings short and serves soft; HARD reads in 0.28s, swings
1.75m, floors quality at 0.62 with a cap of 1.0, and serves 10% hotter.
MEDIUM is pinned dial-for-dial to the shipped TUNE values by test, so the
middle chip remains exactly yesterday's classic. All G.diff-guarded, so the
racket zombie and gore's opener keep their own numbers.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Oliver's restructure: mode first, then what the mode asks — classic deals
EASY / MEDIUM / HARD chips, ZOMBIE TENNIS deals two sub-mode posters (ARCADE,
the exhibition ladder as was, and THE TOURNAMENT), and SPLATTER GORE stands
alone as the third top card. config.MENU is the grouping, MODES stays the four
run modes, and pickedMode stays the run-mode id — start(), stats and the
summary never learn the menu got taller. The sub-row sits inset under the
posters; sub-cards are the poster at 2/3 height centred on their scene.
DIFFICULTY is the one real gameplay addition: config.DIFFS scales the three
dials the classic CPU already reads (flub, quality cap, aiSpeed), MEDIUM
pinned by test to the shipped classicFlub/classicQualityMax so the middle chip
IS yesterday's game. Rides on G.diff next to G.classic, so restarts keep it.
AND THE FEEL SPLIT OLIVER REPORTED IS REAL: "in classic tennis I tend to miss
balls way more often" — the match's three contact helpers (matchReach 1.35,
the matchAssist floor, the short-ball chase) were all G.match-gated, so
classic played with a 35% smaller racket and no rescue on the dying drop
shot. rallyFeel() = match || classic now carries all three; gore's opener
keeps the old prologue feel on purpose.
The ARCADE poster took two rolls: the first came back a flat sticker line-up
("very constructed, not artsy"), the second anchors on the zombie poster
itself as a MOOD reference — one challenger lit at the net, the queue receding
into purple haze. Rolls in _refcheck/modebtn. The cup's stats mapping also
joins MODE_MATCH (it banks through endRun as MATCH and read back as GORE).
SPRV 117, main.js v233, 66 tests.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Oliver's tournament freeze was the stale latch by a THIRD route:
matchFallWillPlay kept the deathmatch /4 while cup matchPoint divides by
4*need, so it promised a fall on a no-crossing point — whose synchronous
matchReady left the after-set latch scoping a dead rally. No more ordering
arguments: the latch is set BEFORE the resolve, the swallow exempts the
in-progress resolution via G.matchEarly, and every path that arms a rally
clears it in matchReady. willFall now mirrors the cup divisor exactly and
knows a mid-round game keeps its body. Regression test drives the full QF
sequence (no-crossing points, the fall, the mid-round game, game two).
And the two 'not really' reports: the trail's 0.26 alpha was tuned on the
gore palette and vanished on the bright lawn — 0.38 now, gate at 13 m/s;
oppBright 1.18 -> 1.3.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The fourth mode: four rounds up the dead hierarchy (CUP_ROUNDS), each round
drawing a rung from its band, the champion always the final. It rides the
match machinery whole — cupDraw feeds the drawn rung through d.opp so every
ladder dial scales untouched; the body's pool spans the round (need-scaled
pool and divisor, so wounds carry between games); cupGameWon holds the same
body mid-round, kills it on the round, and past the final cupWon banks a
NOT-dead match run under CHAMPION OF THE DEAD CLUB. Blood carries; round one
is a single game; deathmatch stays as the arcade run. Full-run state test.
Sidequests: the ghost trail is pace-qualified (TUNE.trailSpeed) so both
tennis modes streak — pow>0.8 was only ever reached by the gore slingshot;
opponent sprites draw ~18% brighter via a load-time baked copy
(render.brighten, TUNE.oppBright) with no PNG touched; the cup scoreboard
line names the round instead of lying with deathmatch arithmetic; the rack
and the apron demo no longer draw under a trophy screen.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Oliver, three refinements in one breath: the frame PROGRESSION quicker
(drop 0.06+0.25 -> 0.05+0.15 — against a live rolling ball the slower drop
read as mush), the prone hold KEEPS its bench-tuned 2 s ('the lying itself
can stay that long'), and the rise row is cut ('the opponent respawns
anyway') — done() re-marks the body in its wound skin regardless, so rising
first was a respawn with extra steps. The rise frames stay in the sheets
for the bench. Wound-beat test updated to the new contract.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Oliver's retest: 'the fall never happens, the game does not continue, the
ball is gone.' Root cause: the decided latch was set AFTER resolveInterlude,
and on a point with no stage change matchPoint re-arms the next rally
synchronously — so the latch outlived the rally it scoped and swallowed
every resolution after it.
Two changes: matchFallWillPlay() gates the early resolution to points that
actually put a collapse (or the game-winning kill) on court — an ordinary
point keeps its ordinary bounce-2 ending, so the ball never vanishes
mid-roll — and with only those paths admitted, the after-set latch is
provably safe: both defer matchReady until their beat ends. A regression
test drives the exact soft-lock sequence through the real update loop.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Oliver: 'the zombie falls after the ball is lost although we know this
earlier — it could play out as ball lost because of the falling.' The match
point now resolves at bounce ONE when the winner is provably beyond the
body's reach (closed-form flight vs its own chase dials, full swing reach
plus 0.6 m of doubt — borderline balls wait for the ordinary bounce-2 call).
startFall keeps the ball rolling for an early-decided point, and the
d.decided latch swallows the rally's later resolutions so it can never bank
twice. Verified in-pane: collapse starts ~0.3 s after the bounce, ball rolls
past the prone body, score banks exactly once.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Five of Oliver's six (the sixth is a play-mode discussion):
- THE DOUBLE FAULTS WERE FRAME QUANTIZATION: the bounce registered a full
integration step after true contact — ~0.75 m of travel at serve pace — so
serves aimed half a metre safe were judged long. Classic enforces the
service box, the match only the court, hence 'zombie tennis does not'.
The bounce now rewinds to y=0 before anything reads the position.
Measured in-pane: 30/40 faults on good taps before, 0/40 after (mistimed
and smudged taps also 0/40; aimed hard serves keep their scatter risk).
Also: the strike flick dead zone is 44 px so a smudged tap stays a tap,
and the aim base clamps 0.18 m inside the lines — scatter, not the aim,
is what faults.
- The mop beat holds the walk-on until the boy actually LEAVES the court
(ordered off at beat end, 6 s cap).
- A match game win that restores blood shows the duel's WOUNDS TREATED
popup; full-blood wins stay silent.
- The opponent walk-on card holds 4.0 s (was 2.4).
- Card heads are cut from the 4K sheets at 144 px (assets/_heads.py,
z_<fam>head.png, HEAD_CROP stays as fallback) — the old 65 px crops were
the 'very low res' heads.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Oliver's four picks, each rolled off its own settled BOSS frame through the
family pipeline (rows in _famroll.py) — the rung and the chapter boss are the
same person. matchFoe decoupled from bossArt's cycling on his call: the match
ladder is 12 with the champion capping rung 12; chapters keep their ten.
The champion refused to bulk through four rolls (words lost, then a new
pair-reference in char_ref only half-won), so his mass is stature: OPPONENTS
rows carry an optional world height h read by zombieH — champion 2.1 m, the
Bulk 2.0 m — and the serve aims at the body actually drawn. HEAD_CROP measured
for all four; ramp test extended to 12 rungs.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Three mechanical causes of 'no serve agency', measured then fixed:
- the 0.12 m perfect snap was ~0.3 s of hang time (now TUNE.apexSnap 0.05,
and the gold band reads the same knob);
- the fixed mid-box target depth capped every good serve at ~36 m/s in the
loft loop — player serve depth now rides quality, flush aims 0.3 m inside
the service line (ladder: 49 / 52 hard / 36 mistimed / 30 bad);
- the strike flick was splatter-only — in tennis and the duel it now sways
placement (horizontal) and buys pace (vertical, serveHard + serveHardSpin,
the extra dive IS the clearance), paid for in scatter against the real
fault machinery. Tap stays the safe second serve.
Phone verdict pending — see the PROJECT.md session log.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Oliver's batch, the code half:
- The tennis serve lost its ghost fingers (idle tap demo + apex rider deleted
with their counters; the rally-return hint survives).
- The toss rises over the tossing HAND, measured off the toss frames' raised
fingertips (TOSS.dx/oDx, women's tour override) — not over the head.
- An uncaught toss returns to the bounce stance instead of flashing the smash
frame the player never earned.
- Between match games the court belongs to the mop boy: 5 s (TUNE.matchMopT),
full job allowance, a hurry speed between stains, and the far-court reach
limit lifted for the match (his stains are exactly where opponents die).
The next opponent walks on only after — d.ending guards the empty court.
- The walk-on card wears the opponent's own head: config.HEAD_CROP, straight
crops measured off the family sheets, no new art.
- The match intro is DEATH MATCH, counting the real blood pool.
- Daniel is benched: every ump_* personality line routes through aside()'s
early return; officiating calls keep speaking. Personality pass later.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
CLASSIC TENNIS, third time asked and this time actually found. Both cues open
with a mode gate and I checked the wrong two features twice: drawApproachRing
(the closing circles that show when to tap) and drawApexBand (the serve bar
with the gold perfect window) each began `if (!G.match ...)`. So the mode whose
whole pitch is "if the core tennis is good everything else is good" was the one
mode with neither. They run on `G.match || G.mode === 'tennis'` now — the gore
mode stays out on purpose, there is no rally there. A test reads both function
bodies and fails if either is ever gated on the match alone again.
(The two I kept pointing at — the landing ring and the placement ring — really
are shared. They were just not the ones being asked about.)
THE BOUNCING WAS IN THE ART, and measurable: the six generated families came
back with the shuffle frame 30-44 px shorter than the stance it walks out of,
against 9 px on the hand-made racket family. The game draws every frame at its
own PNG height, so the body rose and dropped once per step. A walk cycle is one
drawing with the legs moved and must be one height; _zsheet levels the stance
and the stride pair now and leaves the forehand its own, exactly as the serve
and the fall keep theirs. All seven families re-cut: spreads are 1-8 px.
THE BENCH GETS BUTTONS. Arrow keys are not a control scheme on a phone, which
is the device the walk has to be judged on — so every family and every wound
stage was unreachable from the place the problem shows. Three on-screen pads:
BODY, WOUND, POSE, the two-way ones split left/right. Geometry lives beside the
drawing and input hit-tests the same function, the rule the rack established.
THE FALL is quicker still (0.05/0.07/2.0/0.30): drop, lie there, get up.
Three umpire lines pulled — ump_wound_you, ump_felled, ump_wound_opp2. Not
funny enough to keep. Files deleted, call sites collapsed to the two that stay.
60 tests green.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
THE MENU BUG, and it explains three reports at once. cStart's handler was
`addEventListener('click', start)` — and addEventListener hands a listener the
MouseEvent as its first argument, so `start(modeId = pickedMode)` never saw its
default: modeId was an EVENT OBJECT. `G.classic = modeId === 'classic'` was
therefore always false and the zombie branch never fired. Picking CLASSIC or
ZOMBIE TENNIS on the setup screen did nothing at all; SERVE UP always started
the gore mode. A default parameter only applies to `undefined`, and a click
handler always passes something. `() => start()`.
That is also why "in normal tennis i dont see the ball circles and the serve
hint bar": there was no normal tennis to be in. Both are drawn unconditionally
in render() and the tennis struggle hints are already mode-gated correctly.
Verified through the real menu after the fix: CLASSIC gives mode=tennis with
no bodies on court, ZOMBIE gives match=true against the SHAMBLER in its own
body. Every other bare handler in the codebase takes no parameters — checked.
THE MOP BOY had the same class of bug one layer down. sendBoy was called and
mopWorthIt() refused: it tests `d.z >= COURT.netZ - 2`, the NEAR half, because
in the gore mode everything dies on the near court. A match's blood is all on
the FAR side, where the body stands and bleeds through four wound stages, so
the boy could never qualify. The near-side bias stays; the match is the
exception.
DANIEL was being dropped, not missing. Two causes, both mine: the asides were
queued at 0.7-1.5 s and matchPoint fires the score call at 0.4 s with a clip
over a second long — voice() drops anything that would start while he is still
talking (one man, one microphone). And asideEvery is 55 s, a whole match game.
Delays moved past the score call; matchAsideEvery is 15 s, which fits a mode
whose events are a limb and your blood rather than a wave of bodies.
THE FALL: quicker down, longer on the floor (0.09/0.11/2.0/0.30). And it was
genuinely SHRUNKEN — the generator draws fall rows smaller than standing rows
(the brute's buckle came back 190 px against a 224 px stance) and _zsheet's one
global scale preserved that faithfully, so the body shrank the moment it
started to fall. A fall row is anchored on its own buckle now, which must draw
at standing height. All seven generated families re-cut: 224/153/98/181 against
the hand-made racket family's 224/154/85/179 — the convergence is the check.
THE FLICK: 0.42 made going out impossible, which fixed the 8-in-10 bug and
overshot it ("too easy. i must be able to play out"). 0.62 puts risk back where
it belongs — ~15% out at quality 0, 5% at 0.75, and never on a perfect flick.
The test now asserts that shape: bounded, monotonically falling, zero at 1.
59 tests green.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
THE FLICK, again. "i can barely reach the extremes of the court." It could
reach 3.3 m against a 4.115 m line — safe, and too safe. TUNE.flickWidth is
3.95 now (96% of the way to the corner, 17 cm to spare) with flickReach 0.42,
which keeps the guarantee intact: reach x timing + scatter stays inside the
line at EVERY quality, worst case 4.06. 20% more reach, still impossible to
send out by aiming.
A FORWARD FLICK IS A SMASH. Up-screen is the far court, so flicking into it
asked for depth and nothing else — a direction with no consequence behind it.
It buys pace now (smashBoost, scaled by timing). Deliberately NOT distance:
solveShot still lands it on the same spot, so the shot arrives flatter and
sooner rather than longer. A smash you can hit out on a whim is just the old
bug wearing a hat.
THE FALL HOLDS. "the opponent should fall and then lie there for a quick
moment, the player stands up too quick." fall3 goes 0.30 -> 0.62. A corpse
that touches the ground and is instantly upright reads as a stumble.
THE CARD. Ten opponents, six of them NEW bodies off the one-sheet pipeline
(shambler, ball boy, steward, runner, groundskeeper, line judge), with the
club pro's own body and the brute woven in as 7 and 8. It is a ladder of
STAFF: the further you get, the further up the club the thing across the net
used to be. Narrative and difficulty are the same list read twice, and a test
walks all ten — whiff must fall every rung, names must fit the board's 8-char
slot, speed may zigzag by character but never regress.
Steeper per rung (speedPer .08->.11, pacePer .035->.05, flubKeep .82->.76):
"in general this is too easy now". With aimed returns no longer sailing out,
the player stopped handing over free points and the ladder had to earn them
back. The opening got SOFTER at the same time — 0.72 where the old first
opponent was 0.85.
THE UMPIRE WAS MUTED IN THIS MODE and nobody noticed. aside() refused during
any interlude; the match runs on a permanent one. So the single mode that is
literally a tennis match he is sitting over was the one mode he never spoke
in. Same bug shape as sendBoy, same fix. Six dry lines via say -v Daniel: two
on taking a limb, two on your blood, one when there is nearly none left, one
on felling the thing. asideEvery still gates him.
...and sendBoy had the identical bug, so the mop boy now comes out between
games to the FAR court, where one body has been bleeding through four wound
stages. Between games only, never mid-point.
The line judge took two rolls: the first cut to 6 figures in its last row and
_zsheet.py refused it rather than mis-slicing. That refusal is the feature.
59 tests green. Verified in the browser: SHAMBLER on court in its own body.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
THE FLICK. "when returning a ball with a spin it seems too sensitive, 8/10
times the ball lands out." Arithmetic, not feel. The singles line is 4.115 m;
an aimed shot targeted a flat ax*3.3 and THEN added err() on top — up to
+-2.4 m at quality 0 — so a flicked return landed as wide as 5.7 m, and the
only clamp in the path sat at +-5, outside the court. An UNAIMED return
targets 0 + the same scatter and is therefore always in: aiming was the thing
losing the point, which is backwards for the mode's one act of skill.
TUNE.flickReach scales the aim width by the same quality the scatter shrinks
with, so the two can never sum past the line — worst case 1.49+2.4 = 3.89, and
a perfectly timed flick still gets the full 3.3 with nothing left to scatter.
The test asserts that sum across the whole quality range rather than sampling
rallies. Not named `aimReach`: that key already exists further down TUNE (the
slingshot's 22 m aim ray) and the duplicate silently won until a test caught it.
The placement ring carries `q` now and is the flick's receipt — tight and hot
when your timing earned the full width, wide and dim when you barely got a
racket on it. Without it the rule is invisible: the shot just goes less wide.
THE FALL (handoff item 4). A point that takes a limb knocks the body down and
it drags itself up short of it. The three collapse frames wear the stage it
STILL WAS and the rise wears the stage it has BECOME — that order is the whole
beat, and it is what turns a popping wound stage into something the point did.
Own clock beside the horde's warning, for the reason this file keeps relearning
(a beat that moves the phase under a latch strands it); the 'over' beat it sets
outlives the fall so it cannot expire into setupPoint() mid-collapse. Only
fires when the stage actually moved — a quarter off a full pool is 0.75, so
point one has no limb to explain and a second of collapse for nothing is a beat
the player waits out.
THE OPPONENT SERVES (handoff item 7), and it was mostly switched off rather
than missing. setupPoint already placed a non-player server behind its own
baseline and armed aiServeT; update() already tossed and struck for O. Both
were gated on `G.mode === 'tennis'` and the match runs in splatter under a
permanent interlude — one condition. Genuinely new: service alternates at each
game boundary (the match calls advancePoint directly and the flip lives in
tennis's pointTo, which it bypasses — the same shape as the G.serveNum bug),
the zombie takes O's mark so picture and ballistics agree on where it stands,
and the serve poses are named onto it because O is invisible here.
racketStage() moves to config: drawZombie and the fall must not answer the
same question twice, which is how the boss's skins and its blood drifted apart.
Docs: PROJECT.md gains THE ZOMBIE FAMILY PIPELINE — the measured API contract
(4K is a parameter and the default is 1K; n is 1 so "return both" is not a
thing; aspect is an enum so pad to it), why magenta stays despite green being
the documented key, and the two things _zsheet.py must do globally.
Verified in the browser: brute in the match, z_brfall2 -> z_brw1rise on a
wounding point, and bounce -> toss -> serve off the opponent's own baseline.
58 tests green.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The setup screen picks WHAT before WHO and WHERE. Three rows out of a table,
in the court row's own markup:
CLASSIC TENNIS is not the opener with the turn switched off and left there —
it is the mode the other two are built on, so it gets real match scoring: six
games with a two-game lead is a set, best of TUNE.classicSets is the match,
the board shows the sets column (hidden until now outside splatter) and the
bar under it says which set this is. The CPU stops rubber-banding and flubs
less: aiFlubLoss exists to get a new player PAST the opener, and a mode whose
whole promise is a real match cannot be won by an opponent that falls apart
the moment he goes ahead. No tiebreak — 6-6 plays on to 8-6. Marked ponytail.
That needed a second ENDING, the one with no corpse in it: G.done, the same
summary and gated tap as the death screen, its own words (GAME, SET, MATCH. /
BEATEN.) and the man still standing. The three endings now share one block
rather than three copies of it.
Caught by the first classic test: stats banked only the FIRST classic run of a
session. `banked` was cleared by the next KILL, and a classic match has no
kills in it at all — every later match was swallowed by the double-bank guard.
beginRun() is the honest reset and restartRun calls it.
ZOMBIE TENNIS from the menu is one line; the back room keeps its door.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The two modes share no currency: gore counts kills, multi-kills and weapons;
zombie tennis counts opponents felled and has exactly one kill per opponent,
always credited to 'bomb' (matchGameWon's explode). Every one of those was
landing in splatterRuns as a gore run, on the roster as THE CLUB PRO, and on
the KILLS world board — where a five-body match would post a row next to a
300-kill massacre and mean something else entirely by it.
One character on the record fixes all of it: `m`, absent meaning gore (which
is what every existing record is, so no migration). read() takes a mode and
so does endRun, which now compares you only against your own mode and refuses
a name prompt from a match.
The record's two headline slots carry each MODE's two headline numbers — gore
g=games s=sets, match g=felled s=games — so `g` stays the thing everything
ranks on and the match best is derived from the record instead of living in a
second key that drifts. That retires the matchBest key from the last commit.
Found on the way, pre-existing and visible: endRun returned `{...best, kills,
games}` and the later key won, so G.best.games was the run that had just died.
The gore death screen has been printing "BEST <this run> GAMES · <the record>
SETS". The return now carries the best and nothing else.
And stats.check() — the module's own fixture check — was exported and called
by nothing. Wired into the suite, with the mode split added to it.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Three things, all in service of the match reading as its own mode:
BLOOD, COUNTED. The vignette says "worse" and never says "how many". One pip
per hit left, top-right beside the board, in the panel language the game/sets
bar already uses; the last two breathe. Pips rather than a bar on purpose —
the pool is 4 or 8, and a smooth readout of a small integer is the exact
ambiguity being fixed.
OPPONENTS FELLED IS THE MEASURE (handoff slice 1). Blood-only scoring means a
lost game costs nothing but the blood already spent, so games survived is not
the achievement. The in-play bar prints OPPONENT n · m FELLED instead of the
game/sets line, the death screen says BLED OUT / FELLED n OPPONENTS · m GAMES,
and it keeps its own matchBest — drillBest's pattern. The kills-to-next-point
pips are hidden in a match: nothing there is killed for points, and they were
colliding with the new line anyway.
STAGING WEARS A DIFFERENT ICON. Both builds live on the same phone and two
identical bloody tennis balls is a coin toss. Driven by HOSTNAME, not by the
branch — the block is identical on staging and main, so it never conflicts on
a merge and never has to be remembered.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Third recorded session. The previous fixes did their job and exposed the last
gap: he ran onto every ball (2.9-3.9m per point, was 0.2-0.5), the ball
reached him (died at z 22.5-24, was 18-20), he swung every single time — and
the closest approach clustered at a sampled 1.3-1.6m, centimetres outside the
1.3m post-bounce reach, point after point. The assist delivers you NEAR the
ball but not inside a phone-tuned contact radius, and that also explains "not
really better or I played worse": every point suddenly looked makeable and
still died.
matchReach 1.35 widens the contact test in the match only — the gore modes'
"tight contact radius: the tap must be honest" rule stands untouched. A rally
mode lives on contact feeling fair.
Recorded for future sessions: a 20Hz sampler overstates closest approach by
up to ~0.5m on a 20 m/s ball. The recorder samples at 60Hz now.
49 tests, 12 seeds clean.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Second recorded session: 5 wins and a deuce war to 5-6 — the mode is a
contest — but all seven losses shared one signature: the ball died in court
1.4-2m short of him while he ran 0.2-0.5m. Three defects:
The pace cut created dying rollers. Slow returns bounce and die at z 18-20,
outside intercept's near zone, so nothing chased them, and the rolled-dead
rule hands the point to the zombie. They also arrive at his feet below legal
contact height — the "ball arrives right at the player" feel. intercept keeps
an anyHit candidate now: the earliest reachable point on his half where the
ball is still at hitting height, so the assist runs forward onto the short
ball. Verified: he sprints z 23 -> 16.4 and meets it at gap ~0.
intercept computed reachability with autoAssist — 0 on any fine pointer — so
a desktop match's planner answered "home" every time. He had legs, his
destination was always the baseline. assistK() is shared by mover and
planner now.
And the vignette was a radial gradient in a portrait canvas: the circle
reaches the corners but hits the side edges at fraction zero — "red pulsing
from below" was the bottom edge being the only place it got close. Four
linear edge fades now, hugging the rectangle on any aspect.
48 tests, 12 seeds clean.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
"can you watch me play in the browser pane and see why I loose". A passive
20Hz recorder sampled his real play: seven losses, and in every single one he
SWUNG (his timing was fine) while the ball died 2-3m to his side and his own
x never moved all game. He had no legs: autoAssist keys off pointer:coarse,
the pane reports a fine pointer, so auto-run was 0 — the exact trap the
verification recipe has warned about for weeks, bitten in live play.
matchAssist 1.15: the match brings its own legs on every device. It is a
tap-timing game; footwork is the game's job. Held keys still override. Pinned
by a test that plays with autoAssist 0 and asserts he runs.
The vignette curve is sqrt(damage): the match pool is 8, so hit one was a 10%
alpha nobody can see. sqrt front-loads it — first blood reads at ~0.3.
This also explains three rounds of disagreement between his reports and my
measurements: my autopilot always had legs (it set autoAssist by hand), his
pane play never did.
47 tests, 12 seeds clean.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
"there are still balls which are basically not reachable with auto run..
please recheck" — and, asked twice: "did you write tests for the last issues
we encountered?" The straight answer: no. The unreachable-balls issue only
ever had config assertions, which is why it came back twice.
Pace is the ladder's fourth dial. Width alone never fixed reachability
because the returns left at ~27 m/s, crossing to the baseline in ~0.6s — less
than auto-run needs from the far corner. matchOppPace 0.78 puts opponent one
at ~20 m/s (measured live: twelve returns, 20.0-20.7), a quarter more flight
time, and matchOppPacePer hands it back per rung.
The vignette deepens: dmg x 0.8, cap 0.85, and the clean centre shrinks with
damage instead of only the rim darkening.
And the reachability regression the issue always deserved: a headless 90s
autopilot soak asserting at least 10 returns sampled and at most 15% that
never came within volley reach. Sabotage-verified. Its honest limit is
recorded in PROJECT.md: the autopilot attends perfectly and taps
frame-precisely, so it out-covers a human — it is a floor against
catastrophic regressions, not a proxy for the thumb. Oliver's playtest stays
the real gate.
46 tests, 15 seeds clean.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Three asks. The placement ring: an aimed rally hit stamps a gold ring that
fades where the shot is going — flick, watch it bite the corner, learn the
cause. Not stamped for unaimed hits (marking scatter would teach noise), gone
in 0.8s (a permanent ring reads as a target rather than feedback).
The opponent's width is the ladder's third dial. Its returns scatter off
aimError x (1 - quality), which painted lines from game one. matchWideBase
2.1 keeps opponent one inside what an auto-run player can cover — measured,
25 returns all inside +-2.2m — and matchWidePer earns 0.4m per rung, so
line-painting is the late ladder. Speed, reliability, width: three dials.
The blood vignette replaces the temporary border. The old wash flashed 0.3s
and vanished, so between hits the screen said you were fine. A radial
vignette reads G.hp directly now — its depth is your damage fraction,
permanent until healed, hit pulse riding on top. Global: the hp channel is
global. Same no-HP-bar philosophy one layer out; at 1 hp the frame is
properly blood-rimmed and the court centre stays legible.
44 tests, 12 seeds clean.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
"every second ball is basically unreachable by an auto-run player.. are there
some hidden gestures I don't know about or is it just a very strong cpu?"
Neither. The match inherited gore-splatter's depth-only recovery — the
sidestep-protection rule — by being G.mode 'splatter'. One wide return pulled
you off centre and you stayed there, so the next ball to the opposite corner
was lost before it was struck. A player who never walks back experiences
every rally as unreachable, and it reads as CPU strength because the balls
keep landing where he isn't.
The match recovers to centre now, like tennis: real footwork, and in a rally
there is no artillery lane to protect. Gore splatter keeps depth-only — the
sidestep it protects has no role inside a rally. matchSpeedBase 1.4 -> 1.25:
it was calibrated against a player who couldn't move.
The full attacking vocabulary is recorded in PROJECT.md — nothing else is
hidden. The placement flick (aim during the swing) is taught nowhere on
touch; flagged as a ghost-hand lesson if the mode ships.
42 tests, 12 seeds clean.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
"still when I lost a single point and try to serve again every further tap
does hurt me more until I am dead". Reproduced tap-for-tap this time, and the
serveNum fix was a red herring — a real bug, but not this one.
tossBall never stamped ball.lastHit. After any rally the zombie touched last,
the freshly tossed ball still said lastHit 'O' — and the previous pass moved
the server behind the baseline, past the passing-shot line. So the moment the
toss went up, the game saw an opponent ball beyond your baseline: "its
passing shot ends the duel", point to them, blood. Every tap = a toss = an
instant lost point, faster the more he panicked. Two fixes composed into a
death loop; the stale field was harmless while serving happened inside the
court.
One stamp in tossBall — ball.lastHit = sv.side — fixes every caller. The duel
and the gore mode carried the same latent bug for anyone tossing deep behind
the line after a racket-zombie touch.
Verified with the exact field shape: lose a rally the zombie touched last,
then eight badly-timed panic taps — one honestly lost point, zero
toss-bleeds. Before the fix every single toss bled.
41 tests, 15 seeds clean.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
"still very weak and slow opponent. it also spawns at places where it is
impossible to react to my serve. so i win like always. in the cases i dont
win there is a bug. when trying to serve after i got hurt i get hurt again
and again until i die".
The death loop was G.serveNum. Tennis's pointTo is the only thing that ever
reset it, and the match bypasses pointTo entirely — so the first fault of the
match armed "second serve" permanently, and every missed serve after it was a
double fault, a point, and blood, repeating to death. Exactly the spiral a
rattled player serves into. matchReady(), the match's own point boundary,
resets it now; a regression pins it.
The receiver starts every point on its mark: matchReady drops it onto the
exact receiver position the tennis branch computes for O, deuce/ad mirrored,
instead of a random spawn x or wherever the last rally left it. An opponent
that cannot reach the serve is not weak, it is absent.
And the base opponent is a player now: matchSpeedBase 1.4 (the duel's 2.8m/s
lumber was tuned for one sudden-death point), matchFlub 0.08 (0.2 gives away
a fifth of all returns). Measured with the same autopilot that previously won
seven straight games: it now loses the match. Possibly too hot — the two
dials are named, Oliver's thumb decides.
40 tests, 15 seeds clean.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
"the player is too weak in this mode. also the killing by smashing occurs too
often. i cant really control it. maybe obsolete? also the serving after the
first serve does not happen from behind the groundline".
matchPlayerHp 8 — the match brings its own pool. The gore mode's 4 was an
inheritance, not a decision, and a real game has rallies you deserve to lose
without the run ending.
Regicide is out of the match (kept in the duel, where any-weapon-counts is
the point of sudden death). In a full game every hard serve that happened to
meet the body was a free kill nobody aimed. A match ball never damages the
opponent — it passes through like a soft ball and racketAI plays it — so the
wound stages are exactly the scoreboard. That also made hp-per-opponent
meaningless, so the ladder escalates in racketAI instead: +12% speed per body
felled, whiff chance x0.82 per rung.
And serving resets to behind the baseline with real deuce/ad mirroring:
setupPoint's "serve from wherever you're standing" branch now excludes the
match, which falls through to the tennis positioning. Verified live: second
serve from z 24.3, x mirrored to the ad side, opponent unwounded by anything
but the score.
38 tests, 15 seeds clean.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Triage failed the only test that matters — "i think the normal player does
not grasp this" — and the lesson across four drill designs is that every
invented rule needs teaching, while the rules the scene already implies need
none. So the new mode is the game's own oldest rule: tennis.
MATCH TEST in the back room (#match). One game per opponent, real 15/30/40
scoring. Built on top of the duel machinery: G.match runs a permanent
interlude, which is already what makes serving tennis-like, hides the rack,
quiets the spawn director and wakes racketAI. The one change is in
resolveInterlude — a settled point feeds matchPoint instead of ending
anything. About 90 lines for a whole mode, because the deepest system in the
repo was already built.
The score is the body: a quarter of its pool per point won, floored at 1, so
a love game walks the racket zombie through every wound stage and the game
point fells it. Its points draw your blood — hp--, eaten at zero, matchHeal 2
per game won is how you last. Regicide is inherited: kill the body mid-game
and the game banks by hand.
The timing is drawn, per Oliver's ask: an approach ring around the incoming
ball that kisses a fixed inner ring exactly at the sweet contact distance
(gold inside the band), and an apex band beside the toss spanning the same
0.12m keyPressed scores as perfect, with a tick riding the live ball. Both
cues use the mechanics' own numbers, so the picture cannot lie.
Verified by autopilot: 7 games in 90 simulated seconds, opponents 1..8, pools
4/6/8/10, wounds tracking points exactly.
36 tests, 15 seeds clean.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
"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>
The sequence drill tested nothing — the strip was always visible (no memory),
the batch was slow (no urgency), the aim was easy (no execution). A checklist
with gore, and Oliver said so: "not interesting enough to keep me playing".
Settled in discussion, he picked triage + streak. One rule: always kill the
body that would reach you first. Nothing is called, nothing shown — the court
is the puzzle. arrival = distance / speed down a held lane, and the roster's
speed spread means the answer is not the closest body: a runner at the far
baseline beats a roller at the service line. Court sense — and it trains the
exact target-priority read the gore mode rewards.
Wounds are free: only the kill is judged, so pre-softening a heavy while it
is third in line is legal and eventually necessary. The slack (0.9s) makes
near-ties legal both ways, or the rule is a coin flip. The streak is both
punishment and payout: x1..x5, a misread shows OUT OF TURN and resets it, and
nothing else happens — the queue keeps walking. Caught is the real setback.
Best score banks to localStorage; the world board can carry it if it ships.
drillMark draws a chevron over the queue head for playtest calibration. It
answers the exact question the mode asks, so it must be off in anything
shipped.
Deleted: the alphabet, the batch, the strip, the eaters. Shamblers are
first-class stream members with pts 20; a roller is 100.
30 tests, rewritten around arithmetic riggings (known speeds and distances)
instead of luck; 20 seeds clean.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
"one-shot is bad - way too easy - also we're losing all the nice gore
animation sprites" and "I noticed the last kill of a game can carry to the
spawn of a new zombie of the next round".
drillOneShot is off. The second half of that note is the part worth
remembering: a 1 hp body never passes through a wound stage, so every skin in
ZWOUND — arm off, ribs out, still walking — is skipped and the roster's best
art never renders at all. Kept as a switch, never a default.
The carry-over was a real unfairness, not a curiosity. A ball keeps pace
through a kill and travels UP-COURT, which is exactly where the next batch is
born — so the shot that finished your round flew on into bodies that did not
exist when you took it, and faulted you on a round you had not been shown
yet. The shot that ends a round is spent now, and drillCaught does the same
since it also refills the court up-court. Pierce within a batch is untouched:
that is honest occlusion you could have read, this was not.
Verified with real aimed serves through a whole round: round banks, ball goes
inactive, the next batch walks on with 0 faults and a clean sequence, and the
gang carries roster HP again (brute 5, ball boy 3).
33 tests, 15 seeds clean.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>