43784c5b3301b82178f7e7a0a2f0e21e846a262c
84
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
e1ce2486c0 |
SERVE UP was dropping you into gore whatever you picked
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>
|
||
|
|
52b38e4a39 |
Eight bodies on the card, and the umpire gets his voice back
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> |
||
|
|
c1f7268a04 |
The flick stops losing the point, and the corpse falls and serves
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> |
||
|
|
cba2fb8a8b |
One roll, one character: the whole zombie family from a single sheet
Nine sheets meant nine chances for the character to drift, and all day it took
every one of them — a brute that was three different brutes, a corpse that came
back a living man. Asking for the WHOLE family in one image removes that by
construction: one roll, one light, one palette, 31 figures.
What was missing was the way back down. _fine.py cuts a single row; a family
sheet is nine. _zsheet.py is that step and nothing else — it reuses _fine's
keyer, block detection and column cutter, and adds the two things that have to
be GLOBAL or the family breaks:
SCALE taken once from the play row's standing frame. Per-band scaling would
make the serve strike the same height as the idle stance, and the game
sizes a sprite off its own PNG height. Measured after: serve 319,
prone fall 83 — the racket family's are 307 and 85.
PALETTE taken once from the largest figure anywhere, so a wound row cannot
quantise onto slightly different colours than the row above it.
The enclosed magenta rule is INVERTED here, deliberately. _fine.py repaints
magenta trapped inside a figure because the generator used to fill string beds
with the backdrop colour by accident. This sheet asks for exactly that and gets
it, so the bed cuts to transparency and the racket is an open hoop.
_regen.py keeps three fixes found on the way: margins as a PROPORTION (an
absolute pixel margin means nothing to a model that picks its own resolution —
it clipped 3 rolls in 4), reference sheets that fill keyed-out string beds
instead of showing the model a magenta racket, and --verify, which asks the
question no gate here ever asked: is the art already on disk what it claims to
be. It found 27 of the brute's 31 frames dead — z_brw1fall was one drawing
pasted four times, 0.011 apart where a real sequence is 0.53.
The brute is live: ZFAMS gains 'br', the KEEPER wears it. Verified in #bench,
healthy and arm-off, 54 tests green.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
||
|
|
525f017636 |
Park the brute — and name what its next attempt has to change
The agreed single pass did not land: br_w1 installed, br_serve and br_fall were refused by the palette gate at 12 to 22 away from z_br1, and everything drawn from them was correctly skipped rather than built on sand. 'br' comes out of ZFAMS. The art stays on disk and the nine batches stay in the JSON, so re-registering it later is one line — this is parked, not deleted, and nothing in the game referenced it (no opponent row names it). THE FINDING WORTH KEEPING, written at the top of _master.py: the brute's PLAY sheet is a good brute; its serve and fall sheets are each a slightly DIFFERENT brute. All three restyle from the master independently, so each invents the character afresh from the words, and "pale grey-green work shirt" lands somewhere new every time. The gate is not being fussy — it is measuring three different zombies. The fix is to hand the later sheets BOTH references: the master frame for the POSE and the variant's own first sheet for the CHARACTER, instead of the words for the character. Which is the lesson the wound stages already taught — a reference beats a description — applied one level up. Until that lands, a variant is reliable for its play sheet and a gamble for the other two. The racket zombie is untouched and complete: 33 frames, every gate green, and it is the opponent the match actually plays. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
df454c5d92 |
Stop calling three different things "the ladder"
Oliver: "the talk about ladder and rung is extremely confusing." It was, and it
was my vocabulary, not his reading. "Ladder" had come to mean three unrelated
things in this codebase — the sequence of opponents in zombie tennis, the
sequence of WOUND STAGES on one body, and the name of a stats screen — and
"rung" meant a position in whichever of those you happened to be reading about.
One word per thing now:
the OPPONENTS who you play, in order, one game each (config.OPPONENTS,
matchFoe(n), zb.foe). Never "rungs".
WOUND STAGES healthy -> arm off -> headless, on one body. Unchanged, and
now the only thing allowed to be called a progression.
a FAMILY the sprite set a body wears (config.ZFAMS).
Renames: LADDER_TRAITS -> OPPONENTS, matchRung -> matchFoe, zb.rung -> zb.foe,
and the stats screen THE LADDER -> OPPONENTS FELLED, which is what it lists.
The 'L0'..'L9' record keys are deliberately NOT renamed: they are written into
saved run records, and renaming them would orphan every match anyone has
played. A comment says so where they are built.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
||
|
|
e2487b784a |
A match opponent is a FAMILY now, so a generated variant is one row and not dead art
--variant produces 31 frames; nothing could draw them. drawZombie hardcoded the `z_r` prefix, the preloader spelled all 31 filenames out by hand, and the sprite bench built its names from the same literal. A new zombie would have sat on disk. config.ZFAMS = ['r'] the families that exist config.famFrames(f) the 31 names derived from a key Everything that needs those names now derives them: the preloader spreads ZFAMS.flatMap(famFrames), drawZombie reads `zb.fam` (unset = 'r', so the club pro is untouched) and builds its wound lookups off it, and the bench walks families on up/down. Adding an opponent is ONE ROW — which also retires 31 hand-typed filenames from the preload list, each of which was a chance to typo one and have a body silently vanish for a frame. That is the oldest bug in render.js and it is now impossible in this corner of it. And the link that makes it worth anything: a LADDER RUNG can name a family. `fam: 'br'` on a row in LADDER_TRAITS and that rung walks on as a different body, all the way down its own wound ladder. So the whole path is now: _master.py --variant br "…" -> generate 31 frames ZFAMS += 'br' -> the game loads them fam: 'br' on a ladder row -> that opponent is in the match Verified: the bench still walks all 33 cells pulling the right derived name at every wound stage, and a real match still opens on THE GROUNDSKEEPER wearing family 'r' and drawing z_r1. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
5c0db0a22e |
The opponent is complete: 3 wound stages x 11 states, and a gate for wounds that never happened
The serve and the fall now exist at every wound stage, so a one-armed opponent still serves and a headless one still goes down, instead of reverting to a whole body to do it. 33 frames, no gaps. THE BALL IS OUT OF THE SERVE SPRITES. Oliver: it was painted into three frames and the game draws the ball itself, so every one of them would have ridden along at the wrong place, the wrong time and in the wrong number. Hands open and empty now, poses unchanged as if a ball were there. THE FOURTH GATE, and the one nothing else could see: "copy this exactly, change ONE thing" loses when the change is a REMOVAL. Taking a head off worked every time — it is dramatic. Taking an ARM off mostly did not: five of seven wound frames came back as the reference faithfully redrawn, 0.02-0.05 of their own silhouette different, and the ladder quietly HEALED the body the score had just maimed. Frame count, background, clipping, detail, kit and stride all pass that sheet. A batch now declares how far it must move from the sheet it was drawn from — a copy scores 0.02-0.05, a real amputation 0.12-0.38, a decapitation 0.27-0.75 — and the whole set now measures 0.18-0.65. AND THE BACKGROUND RULE MOVED TO THE FRONT OF EVERY PROMPT. Six rejections in eleven rolls were "background is not magenta", and the generator was not glitching: it was painting the zombies ON A TENNIS COURT. The chained restyle subjects are long, and the magenta instruction sat behind three hundred words of "keep everything exactly as the reference", where it stopped being honoured. Same sentence, said before anything can bury it. One design consequence kept rather than papered over: the missing arm IS the tossing arm, so a wounded opponent tosses off the racket face the way one-armed players actually do. The wound changes how he plays, which is what slice 3 has been asking for. Known and NOT fixed: the shorts hue drifts from -9 to -19 down the ladder. Each stage is measured against the one before it, so a legal 4-point hop three times over is a 14-point total nobody authorised. The fix is to anchor every stage to the healthy frame with a total budget instead of a per-hop one. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
4bbdf6357a |
The opponent can serve and fall, and there is a bench to judge it on
THE MISSING ART, generated by the whole-sheet restyle method: bouncing the ball at the baseline, the toss with the ball leaving the fingertips, the strike at full stretch — and the fall, which slice 4 has been waiting for: buckling, down on the knees, flat on the court face down, then dragging itself back up. One zombie is now complete as a tennis player rather than as a body that shuffles. (Healthy only so far; the wound variants are the next sheet.) THE SPRITE BENCH, in the back room. No ball, no scoring, no AI — one opponent on an empty court, walking, at the size and speed it is actually played at, because that is the only place a walk cycle can be judged. A sheet that looks fine at 224 px on a contact sheet is a different animal at 30 px crossing a lawn, and every sprite argument this project has had was really about that gap. It steps through every state including the ones the game cannot reach yet, so the serve and the fall could be looked at the day they were drawn instead of the day they get wired up. It prints the FILENAME it is drawing and the hp the wound stage was read from — the two numbers that separate "the art is wrong" from "the wiring picked the wrong art" — and it says HEALTHY ONLY where a wound variant does not exist rather than implying one does. The stride steps off ground covered, like the real thing: a bench animating on a clock would flatter exactly the sprites that foot-slide in play. It owns the court like every other mode and every start* refuses while it runs. FOUND BY IT, ten seconds in: `start()` applies the mode the setup screen picked, so a remembered ZOMBIE TENNIS was taking the court before the back room could — every back-room entry (weapons, bosses, drill, match, bench) had been silently refusing since the mode select landed this morning. The back room passes null. And a third calibration lesson: POSE MOVES A GLOBAL COLOUR STATISTIC. A body face down shows a fraction of the shirt it shows standing, so `redness` fell 14 points across the fall sheet without a colour changing. The measure that survives a pose change is the shorts HUE — mid-tone neutrals, present in every frame in the same proportion — which is why it is the one that caught the drift Oliver actually saw. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
d5942e8aad |
The match gets its own board, and it names what is across the net
A second plate, drawn rather than tinted (assets/_board.py — _regen.py cannot do this job: it keys magenta, crops to ink and rescales figures to a common height, and a scoreboard is a full-bleed rectangle with no figure in it). The club's bottle-green board is the club's; the match is played on one nobody has cleaned in a long time, and the ink changes with it — a green 'dim' over dried blood reads as a printing fault. Two column changes go with it: the name slot says KEEPER / JUDGE / CHAMPION instead of THEM (eight characters, `short` on the ladder rows), and the SETS column is gone in the match, where it was a permanent 0 — the bar under the board already carries that mode's progress. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
c629ae1108 |
The ladder has names, the bodies play tennis, and the stats screen splits
THE SPRITES. The match opponent borrowed the turned CPU's art — a body drawn to SHAMBLE at the camera with its racket hanging at its side, which is what was playing tennis points against you. It has its own family now, 'r', drawn off the LIVING opponent's poses: a ready stance, a shuffle pair and a real forehand, at every wound stage, men's and women's — 20 frames, six batches. 'c' keeps its shamble untouched, because in the gore mode shambling is exactly what the turned CPU should be doing. The pose comes from racketAI's own `pose`, so the picture cannot drift from the behaviour, and a one-armed opponent still plays tennis instead of reverting to a walk. Two pipeline bugs found doing it, both fixed at the root in assets/: - _fine.py keyed EVERY magenta pixel, not just the background, and the generator paints racket strings in the backdrop colour more often than not — so frame after frame installed a man holding an empty hoop. Background is now the region connected to the border; magenta left INSIDE a body is repainted pale grey rather than punched out. - the flood fill silently did nothing, because Image.fromarray images are read-only and PIL's floodfill swallows the write error. .copy() plus a real error if it ever fills zero pixels. THE LADDER (handoff slice 2). Rung N takes design N's name off the chapter roster — the club's dead staff play you at tennis here and come for you in gore, same joke twice — plus a personality that is DIALS, not new mechanics: pace, width, flub and speed scalings multiplying numbers racketAI already reads. A rung is a data row. Name plate on the walk-on, and the kill files under the rung rather than under the generic body. THE STATS SCREEN follows the mode split: BODY COUNT / THE LADDER / THE RECORD behind a mode strip, each with its own three figures, its own roster heading and its own footer. The weapon shelf and the roster hide in the modes that have neither, rather than printing a line about something the mode cannot do. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
c89d4a3ff9 |
Three modes, picked up front — and CLASSIC TENNIS is one of them
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> |
||
|
|
78292a2d53 |
First recorded game win, and the rungs flatten
Fourth recorded session, verdict "better": he beat opponent one — the first game won in any recorded session — and reached opponent two, which took him apart with gaps of 1.66-2.13m (60Hz, accurate) while he swung at closest approach every time. The gradient is real; the step was a wall, because a rung moves three dials at once. Per-rung increments softened: matchSpeedPer .12->.08, matchWidePer .4->.25, matchOppPacePer .05->.035. Opponent one untouched — the bases stay, only the slope flattens. The contact chain is now verified end to end by recorded play: legs (matchAssist) -> destination (assistK in intercept + the anyHit short-ball chase) -> reach (matchReach). Each link was a separate defect found by a separate recorded session. 49 tests, seeds clean. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
ac588ec6a7 |
The longer racket
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> |
||
|
|
3a789eb2e9 |
The flight recorder finds the statue with a racket
"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> |
||
|
|
fdbadce94d |
Pace was the missing dial, and the reachability test finally exists
"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> |
||
|
|
8ccf0a8fad |
The flick shows itself, the width is a dial, the blood stays on
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> |
||
|
|
d6df582cd4 |
The match walks back to centre — "unreachable" was the player, not the CPU
"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> |
||
|
|
f67411e330 |
The opponent shows up, and the death loop dies
"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> |
||
|
|
ea6e50f8f8 |
The match plays fair: deeper pool, no regicide, serves from the line
"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> |
||
|
|
0620e97d6c |
Zombie tennis: one game per opponent, blood only
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> |
||
|
|
be191dba82 |
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> |
||
|
|
6a16e77480 |
Triage: always kill the body that would reach you first
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> |
||
|
|
57e96d629e |
The finishing shot is spent, and the wound skins come back
"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> |
||
|
|
0f524a08c9 |
Two testing switches: one-shot bodies, and the eaters off
"please make the zombie killable with a single hit for testing" and "remove the fake zombies for now". drillOneShot drops every body on a drill court to 1 hp, applied at spawn to the batch and the eaters alike. Worth knowing what it removes: the roster's HP is what makes the called ORDER matter — a roller is normally seven serves, so being told to take it third means starting on it first. At one hit that planning layer is gone and the sequence is pure recall. Flagged in PROJECT.md so it gets decided on feel rather than left on by drift. drillEaterMax 3 -> 0 switches the plain shamblers off. The mechanism is untouched — spawnEater, the wide entry away from the gang, the never-a-fault rule — so one number brings them back. The eater test now asserts whichever contract the knob describes (at 0: nothing but the puzzle ever appears; above 0: they arrive, capped, always plain shamblers) instead of being deleted and rotting. And the never-a-fault rule gets its own test with a hand-placed body, so the rule the eaters depend on stays covered while they are switched off. 31 tests, 15 seeds clean. Verified live: brute/steward/roller all at 1 hp where the roster says 5/5/7, zero strays over 40s, one aimed serve puts down the roller. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
5b2f1d55ce |
The roster is the alphabet: the sequence is the zombies themselves
Oliver's insight, and it retires a whole subsystem: "we don't need colors at
all! we already have different zombie types.. so the sequence is the zombies
themselves".
The bib and then the ground disc were both inventions bolted on to give a
body an identity it already had. A brute does not look like a runner, the
game is built on reading those silhouettes at speed, and the strip can show
the body's own walk frame instead of a swatch — no legend, no mapping, no new
art ever. It also deletes the accessibility problem outright rather than
deferring it, because a silhouette is not a hue. DRILL_COLS, drawColourRing,
drawBib, PIP_FACES, blip(), drillRingR and drillTones are all gone.
Two rules keep a type unambiguous: the plain shambler is never in the
alphabet (it is what the eaters are, so "not the puzzle" needs no marking),
and one of each type is on court at a time.
The new failure mode this created is instructive. Types move at different
speeds and drill bodies hold their lane, so one in a lane you are not in
walks the whole court and off the back untouched — stranding the sequence
asking for a body that no longer existed, i.e. the un-winnable state the mode
exists to prevent, reintroduced by a change that looked cosmetic. A batch
body crossing your baseline counts as caught now ("GOT PAST YOU"), which is
also just better design, and the speed spread is narrowed to 1.35..0.70 so a
batch stays coherent. Their real difference is HP and that is untouched: a
runner is one shot, a roller is seven.
Verified: 14400 frames of simulation, zero frames where the sequence named a
body that was not on court.
29 tests.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
||
|
|
3379e83efc |
Clear the court of everything that is neither the puzzle nor the threat
"remove the mob boy - make the normal zombies rarer and not so close to the color gang". sendBoy refuses during a drill, guarded at the function rather than at the call sites — same shape as its existing G.interlude guard, so every present and future caller inherits it. He is one more body crossing the lanes you are reading colours down, and unlike everything else out there he is neither the puzzle nor the threat. Measured: 0 boy-frames in 90 seconds. Gore no longer gets mopped in this mode; the existing 40/30 caps are what bound it. The eaters are rarer — every 12s falling to 5s, capped at 3 on court. At the old rate they were a second crowd rather than an interruption, and a threat you break off to deal with only works if breaking off is occasional. 8 arrivals in 90s where the old numbers gave ~13. And they enter wide of the colour gang: best-of-9 lane sampling against the bibbed bodies, the same trick the batch uses. Narrowest clearance measured across 8 spawns was 0.91 m. They still converge on you afterwards — that is what they are for — this only decides where they walk in, so they stop screening the bodies you are trying to read. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
d775ac40ec |
The colour goes on the court, and the dead go off like everyone else
"the colored zombies should also explode like they did before and make the normal splash sound on destruction.. no bleeps please.. can we make the color as a color circle the walk on? the sign they're holding is not nice. also remove the ball slicing in this mode.. this leads to unwanted hits". The silent deaths were my bug: the hook in killZombie did drillKill(zb) and then RETURNED, and everything below that return is the death — decal, pile, 36-particle burst, splat, headshot crack. It resolves after the splice and without returning now. Both halves are load-bearing and each was a bug in turn: splice first or a freshly drawn combination names the corpse; don't return, or the body simply vanishes. The colour is a disc on the court the body walks on, drawn before the sprite. The bib was a flat quad pinned to a walking sprite — UI stuck onto a character, covering the art it sat on. On the ground it takes the court's own perspective, measures 12.4px across at the spawn line against a chest patch's few, and hides none of the sprite. A hard dark rim plus a bright inner line is what keeps it from reading as another gore decal. No slicing in the drill: a cut ball leaves the corpse and strikes whatever it finds next, which in a mode where the wrong body is the failure is the game choosing your target for you, badly. And the mode has no sound of its own now, deliberately. The umpire reading the list was vetoed, the arpeggio that replaced him was vetoed. blip() is deleted. A kill bursts and splats like every other kill in this game; the strip carries the list. 28 tests. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
abd6d4f789 |
Every colour you are asked for is already on the far side
"make the game that it is actually winable meaning that all required colors are visible on the far side before when a color combination is shown.. for now no colors that are not in the current combination but occasional zombies which don't have a color and are converging to the player trying to eat him". Two rules define the file now. Every colour on court is IN the current combination — no coloured decoys, so the strip can never name a body that is not there. And a combination is only shown once its bodies are all on the far side, which is free because the batch is BORN there: it is a property of the construction rather than a check that could be wrong. The pressure moved to the eaters: plain unbibbed shamblers on their own clock, deliberately not flagged `drill` so they keep the gore mode's homing and converge on you while the bibbed batch walks its lanes. Killing one is never a fault; letting one reach you is the ordinary caught. The puzzle plays fair and the danger arrives separately, which is a better trade than a puzzle that cheats. The batch is the round: kill the last of it and the next walks in on the same frame with its combination already called. One real bug, caught by the new test: killZombie resolved the drill BEFORE splicing the body, so a combination drawn on the spot could name the corpse — a round asking for a colour that is not on court, i.e. exactly the unwinnable state this pass exists to prevent. Splice first, resolve second. 28 tests. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
1d49c4343b |
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> |
||
|
|
ef8a413031 |
You aim it yourself now, and no two rounds are dealt alike
Oliver played it: "the auto aim just mean no skills involved at all. this should just go away.. there is no penalty at the moment.. make some variations of the position on the court and the color". The first two notes are one finding. With the shot locked to the called body it was unmissable, so the fault could never fire — there was no penalty because there was no way to earn one. Both snaps are deleted. What replaced them is one line: aimTie is 0 during a drill. That is not auto-aim, it is removing the guess — the ordinary magnet lets a nearer body inside the same cone steal a shot aimed past it, which is right for a crowd you are mowing down and exactly wrong where hitting the wrong body IS the failure. An unaimed tap now lands on grass. And the variation: - six colours (purple, white) with the court dealt three to five of them, so which colours turn up and how many decoys there are are live questions. At four out of four every round dealt every colour and only the order moved. - PLACEMENTS: fan, cluster, column, stagger, wing — weighted, mirrored per round, lanes shuffled against the bib order. column stacks bodies in one lane, and the ball hits the first thing it touches, so feet matter. - pips are dice faces now, shared by bib and HUD chip. A row of five dots has to be counted; a die is recognised. Measured over 30 deals: all five shapes, counts even, all six colours, and the first target screened by another body in ~10% of rounds. Verified end to end in the browser that FAULT and DOUBLE FAULT both fire. 26 tests, including a named regression for the auto-aim staying gone. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
22df96cc1d |
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> |
||
|
|
5ea84f8511 |
Nothing arrives unannounced: the horde gets its breather, the boss gets a name
THEY KEEP COMING is a 2.4s 'over' beat again (warnHorde in beats.js) instead of a flash raised on the same frame the wave lands, and the chapter boss walks on behind an overlay — his name in red over ENTERED THE COURT — held for 3s by the new self-dismissing modal clock (m.hold) rather than announced on a routine-set card. startHorde returns a bool: if the court has been taken when the warning expires the pendingHorde latch is re-armed rather than the set losing the horde it paid for. The warning ticks on its own clock, not on the 'over' beat, so a beat that moves the phase underneath it can't strand it (the mgT lock, again). Three tests, 15 total. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
1f636530b9 |
The code moves into src/: index, server and the test harness keep the root
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |