Commit Graph
423 Commits
Author SHA1 Message Date
wuwiandClaude Opus 5 66fdad3d74 A match run is a different kind of run, and stops polluting the gore stats
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>
2026-08-12 08:58:04 +02:00
wuwiandClaude Opus 5 8011186cda The blood you have left, the bodies behind you, and a face for staging
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>
2026-08-12 08:53:02 +02:00
wuwiandClaude Opus 5 a54cfa5f4f HANDOFF-MATCH.md: scoring model A, and the slice to make the match a real mode
Temporary file, written because a session ran out of context mid-slice — fold
into PROJECT.md and delete when the work lands. Not in the Dockerfile COPY;
docs are not served.

Records the decision (A: blood-only, measure becomes opponents felled) and
the finding behind it: Oliver believed losing a match killed him, when he was
bleeding out during deuce wars. That misreading is itself the bug — model A
only works if the player can see they are bleeding, not losing.

Also carries the build order (summary+measure, named ladder with
personalities, wound-driven play, the fall animation, menu, flick teaching,
opponent serve games), the flight-recorder recipe that ended four rounds of
guesswork, the traps this mode already sprung, and the do-nots.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-12 08:28:19 +02:00
wuwiandClaude Opus 5 9b9f18d312 LATER item 7: 1v1 multiplayer — shot-relay architecture, ghost match first
Decided with Oliver 2026-08-12. Records why tennis is the friendly case (the
sync unit is the shot, alternation already enforced by ball.lastHit, each
client authoritative for its own contacts), what the codebase already brings
(deterministic sim, fixed dt, beats.js ownership contract), the zero-dep
WebSocket path (~120 lines of stdlib RFC6455 in server.py, friend-code
rooms), the honest cost (reconnect-and-resume is the real work), and that
step one is the GHOST MATCH over plain HTTP — half the netcode falls out of
it as a byproduct. Plus the hook: score-is-the-body on both players, losing
is zombification.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-12 00:13:53 +02:00
wuwiandClaude Opus 5 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>
2026-08-11 23:43:57 +02:00
wuwiandClaude Opus 5 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>
2026-08-11 23:40:08 +02:00
wuwiandClaude Opus 5 f37fb93875 The short ball gets chased, intercept gets legs, the vignette gets corners
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>
2026-08-11 23:36:15 +02:00
wuwiandClaude Opus 5 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>
2026-08-11 23:28:47 +02:00
wuwiandClaude Opus 5 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>
2026-08-11 23:18:33 +02:00
wuwiandClaude Opus 5 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>
2026-08-11 23:09:33 +02:00
wuwiandClaude Opus 5 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>
2026-08-11 23:02:14 +02:00
wuwiandClaude Opus 5 24be613c61 The toss is the server's ball — the real death loop
"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>
2026-08-11 22:54:13 +02:00
wuwiandClaude Opus 5 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>
2026-08-11 22:41:10 +02:00
wuwiandClaude Opus 5 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>
2026-08-11 22:31:57 +02:00
wuwiandClaude Opus 5 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>
2026-08-11 22:19:37 +02:00
wuwiandClaude Opus 5 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>
2026-08-11 21:44:58 +02:00
wuwiandClaude Opus 5 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>
2026-08-11 21:32:17 +02:00
wuwiandClaude Opus 5 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>
2026-08-11 20:43:26 +02:00
wuwiandClaude Opus 5 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>
2026-08-11 20:37:01 +02:00
wuwiandClaude Opus 5 d34683bc68 A flaky gate is worse than no gate: seed the suite
The pipeline caught two flaky tests of mine that sixty local runs had not.
Both were bad tests rather than bad code, and both were mine from this
session:

- the eater test counted SURVIVORS, not arrivals. An eater crosses the whole
  court in ~24s and despawns, so a snapshot at 40s can legitimately show an
  empty court — that read as "the eaters never came". It tracks arrivals over
  the window now, and asserts the cap as a running peak.
- the auto-aim test never actually aimed at the body it claimed to: fy was
  pinned at -0.95 and fx was a scaled x offset, so the ray leaned up-court and
  the called body could genuinely be the best-aligned thing in the cone. That
  is the magnet working. It now normalises a vector straight at the wrong
  body, picks the one furthest in x from the called one so the two are
  separable, and asserts the magnet takes what you pointed at.

Measured 8 failures in 40 runs before, 0 in 60 after.

And the durable fix: Math.random is seeded at the top of test.js. This suite
is a Dockerfile build stage, so a red run blocks the deploy, which makes a
1-in-5 flake worse than no gate — it trains you to re-run it. The game leans
on randomness everywhere, so the stream is made reproducible rather than the
randomness stripped out. SEED=1234 node test.js re-rolls it; 25 seeds pass.
The cost is noted in PROJECT.md: under one seed a guarded test can silently
take its early-out forever, so I checked all four guards still run their
bodies.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-11 20:29:34 +02:00
wuwiandClaude Opus 5 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>
2026-08-11 20:24:58 +02:00
wuwiandClaude Opus 5 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>
2026-08-11 20:08:58 +02:00
wuwiandClaude Opus 5 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>
2026-08-11 19:57:56 +02:00
wuwiandClaude Opus 5 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>
2026-08-11 19:29:04 +02:00
wuwiandClaude Opus 5 586e8d4e4b Drop the dots, and stop handing back the dodge
"what are the dice dots for?" -> "drop them", and "the dodge swipe should not
reset to the middle when firing".

The pips were colourblind redundancy so the mode did not rest on hue alone,
but they were noise to everyone who can separate the colours and they shrank
to nothing on a body at distance — exactly where the channel had to work.
Gone from the bib and the chip. Recorded in PROJECT.md as a known gap rather
than pretended away: if the mode ships, the answer is bibs that differ by
shape at sprite scale, not dots.

The dodge one was never a drill bug. The auto-assist recovered toward HOME.P
including its x, so firing walked you back to the centre line: measured, a
step out to 1.94 m was dragged to 0.77 m over one ball's flight — most of a
dodge, handed back for free, which is the welded-in-place complaint the
sidestep exists to answer arriving slowly instead of all at once. Splatter
recovers in DEPTH ONLY now and holds whatever lane you stepped to. Tennis
keeps the full recovery: recovering to centre is the real sport's footwork
and there is no sidestep there to protect.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-11 19:17:20 +02:00
wuwiandClaude Opus 5 57357c7865 PROJECT.md: the stream pass, and the two test traps it cost me
The doc edit threw on its second assert last commit, so the code shipped
without its notebook entry. Also records the two ways a drill test lies to
you: one that never shoots gets caught and the sweep looks like a stalled
stream (use eatenR = 0), and zombies[i] is not the same body across frames
because the array shifts at the baseline.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-11 19:07:28 +02:00
wuwiandClaude Opus 5 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>
2026-08-11 19:06:48 +02:00
wuwiandClaude Opus 5 435a470ec7 The umpire is one man with one microphone, and voiceSeq forgot it
"the drill mode immediately goes into some horrible audio loop" — on a phone,
never on desktop, and that is the tell. Three of the four defects can only
happen on a phone, and every one of them is a rule voice() had followed for
months that my new function quietly broke.

- A SUSPENDED AudioContext HAS A FROZEN CLOCK. Scheduling four clips at
  currentTime +0/+0.9/+1.7/+2.4 while the context is suspended puts all of
  them in the past by the time it starts, so they fire on one instant through
  a 1.3s reverb tail. Resume first, re-read the clock after the decode.
- A second call layered instead of replacing. voiceSeq SET voiceUntil and
  never checked it. It stops the outstanding list now.
- ...and cancelling a list has to give back the time it booked, or the
  replacement queues behind a list that will never play. Measured at 6.6s of
  silence before the call the player is waiting on.
- The amplifier was in drill.js: the empty-court net redealt on the spot,
  making the repair rate the frame rate — and every deal calls the umpire. It
  goes through the ordinary between-rounds beat now.

Verified by tagging each decoded buffer with its source file and counting
start() calls: one drill_call, three colours, spaced, zero overlapping voice
lines. The earlier "clean" reading counted fetches, which proves nothing —
VOICE[] caches, so a repeat play never fetches again.

27 tests; the new one empties the court every frame for six seconds and
asserts at most one umpire call per beat.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-11 18:29:11 +02:00
wuwiandClaude Opus 5 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>
2026-08-11 18:16:25 +02:00
wuwiandClaude Opus 5 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>
2026-08-11 18:01:33 +02:00
wuwiandClaude Opus 5 c659b6882f A burst fires for its whole window, and the countdown is the tile itself
The gun, beacon, flamer and rocket fell silent the moment they cleared the court:
firing and steering were both gated on phase 'rally', and every beat raised by the
kill that empties the court (a point, a won game, the mop-up) parks the phase in
'over'. A burst is its own control mode for its whole len, like a horde — its own
clock is the only thing that ends it. Not fixed by forcing the phase back to rally:
a parked 'over' beat that never expires is how pendingHorde gets stranded.

The countdown moved off the tile's bottom edge (the rounded frame, and the bottom of
the screen) into the tile: the armed slot's well drains as a gold wash, clipped to
its own rounded path, under the icon and labels.

16 tests.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-11 16:49:02 +02:00
wuwiandClaude Opus 5 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>
2026-08-11 16:38:29 +02:00
wuwiandClaude Fable 5 1f636530b9 The code moves into src/: index, server and the test harness keep the root
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-11 13:45:23 +02:00
wuwiandClaude Fable 5 c122745390 One file owns the court: beats.js, PROJECT.md (né HANDOFF), and tests gate the build
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-11 13:23:27 +02:00
wuwiandClaude Fable 5 05930f0877 One mode owns the court: the duel can no longer start inside a horde, and the game has tests
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-11 12:55:37 +02:00
wuwiandClaude Fable 5 4569981365 Dust settles on every court: tennis ticks particles too, EPIC! cheers, the machine explains itself
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-10 22:30:54 +02:00
wuwiandClaude Fable 5 4e80bd8cb2 Cards say one thing: trimmed copy, and a title that fits the band
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-10 22:14:32 +02:00
wuwiandClaude Fable 5 9fab26eec9 The court keeps no score: the landing ring is gone
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-10 22:05:32 +02:00
wuwiandClaude Fable 5 8307a05f79 The wind-up is a drawn bow, not a crouch: her pull re-rolled off his arch
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-10 21:55:15 +02:00
wuwiandClaude Fable 5 70642a0a3d Her serve is his serve: the women's pull and smash redrawn off the men's poses
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-10 21:51:44 +02:00
wuwiandClaude Fable 5 04fe4098a7 Pin the sprite generator to Nano Banana Pro on OpenRouter
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-10 21:38:02 +02:00
wuwiandClaude Fable 5 1c793f59a4 A miss at the rack is a miss, not a shot
Tapping a tile and landing a thumb-width off it returned -1 — free court — and
the tap became Space, which instantly armed whatever burst weapon was selected.
Reaching for the flamer and missing by 10px fired the gun: 22px tiles, a second
row the thumb hasn't learned, and a selection that moves under you (a won duel
re-equips the tin it tops up). rackHit now reports a dead margin around the
band (-2), and only the TAP dies there, decided at touchend — a drag or hold
starting in the same spot still steers and still serves, so burst steering
parked low keeps working.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-10 21:28:37 +02:00
wuwiandClaude Fable 5 15cad26105 The zombie finger iOS never buries: a drag now digs the stick out from under it
dropStale trusted e.touches — but a finger stolen for a system edge gesture can
stay listed there forever, so the stick survived and the player stayed welded to
the edge (field-proven: escape taps still swished, meaning dropStale ran and
found the id). The zombie is indistinguishable from a parked thumb by data, so
it's told apart by intent: in the absolute-steering modes, a new finger that
drags past the deadzone while the stick has been silent over a second inherits
the stick. Taps still swish and steal nothing.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-10 21:06:45 +02:00
wuwiandClaude Opus 5 482c7cdce4 The rush picks a lane and commits to it, and stops on a strike line
"They just overrun you without a chance" — two reasons, and the second one only
existed because of the first.

IT FOLLOWED YOU. The rush tracked your x every frame it was running, so the
sidestep was decoration: it simply came across and took you anyway. The lane is
LOCKED when the rush starts now. You get the whole 2.2 s run-up to leave it, and
what it runs down is that line, not you.

AND IT LANDED ON YOU. A rush that stopped at your feet left the boss standing
there afterwards, and nothing gets you out from under that — dodgeCool is 0.5 s
against a 0.28 s i-frame, so the second contact kills you while the first is
still fading. It stops on a STRIKE LINE 0.4 m short instead, reached on the same
frame the rush timer expires, so it turns and withdraws the instant it arrives.

The version of this that ran the rush THROUGH you and out the far side is why
bossStrike is a short stop and not an overrun: a body past the baseline is
deleted and scores a point, so the boss quietly despawned itself mid-fight and
left the fight running with nothing in it.

Measured, 45 s against the seller, the champion's third phase and the physio:
stand still and the first rush kills you (4.1 s); step out of the lane and you
survive all of it — seven or eight rushes, one sidestep each, the boss on court
the whole time and out at its line between them where you can hit it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-09 17:11:21 +02:00
wuwiandClaude Opus 5 d57433ed2b A charge that passes, two strides that actually stride, a door in the corner
THE LEGS. Measured as lower-third silhouette difference between the two stride
frames: the groundskeeper, whose walk has always been right, is 0.80. The seller
was 0.017 — the same legs twice, which is why he hopped along on one of them.
Both sheets regenerated with the stride as the entire subject (one foot planted
ahead, the other trailing, and they SWAP): seller 0.42, champion 0.67.

Three things the champion's re-rolls proved, all of them the same lesson —
THE REFERENCE IS WHAT IT COPIES. Anchored on his own one-legged frame it drew
that pose again. Given a working walk from another boss alongside him "for the
legs only" it blended the two and returned a slim side-on walker, then a
pot-bellied man in cream. One character in the reference, one character out —
and pick the keeper by EYE, because the metric happily crowned the side view.

THE CHARGE. It rushed, then withdrew a fraction of what it covered, so it ended
the fight parked on top of you: dodge or die every two seconds with no window to
hit it, and nothing but a hitscan laser quick enough to use the window there
was — Oliver, "they come too close, they seem unbeatable". A rush now RETREATS
to its line afterwards, and to keep it from being toothless the other way (from
the band it ran out of clock 8 m short) the rush is TIMED rather than paced: it
covers whatever ground the retreat left in bossChargeT, so every charge arrives
and every charge has the same readable run-up. Measured over 30 s: five rushes,
each one reaching you, 13% of the fight in your face instead of all of it.

And the back room moved off the whole splash onto an invisible box in the TOP-
LEFT CORNER. Five taps in the middle of the screen — which is where your thumb
goes on the way to PLAY MATCH — now do nothing.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-09 16:56:05 +02:00
wuwiandClaude Opus 5 1bb528f7e0 The back room: five taps on the logo, not three on the scoreboard
The weapon-test cheat sat on the SCOREBOARD, where a single tap is the miss-log
overlay — so the thing you tap to read a log was also the thing that opened a
sandbox mid-run, three taps in, by accident. That tap does one job again.

The cheats now live behind five taps on the splash logo, where nothing marks
them and nothing in play can reach them:

  WEAPON TEST  the old sandbox. It stocked only the BARREL weapons, on a screen
               called the weapon test — it stocks every weapon in the game now,
               eight of the nine slots.
  BOSS TEST    a portrait of every chapter boss, built by asking bossArt() what
               each chapter gets, so the list cannot drift from what a run
               actually spawns. Pick one and you land in a real fight at that
               chapter's game number with the full arsenal at ×99.

Neither is a special mode: both ride the transition out with laserScene's trick
(force the turn, then step the ten-second cinematic out in one frame) and hand
you the ordinary update loop, ordinary scoring, ordinary death. A test that
plays by different rules tells you nothing about the game.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-09 16:19:12 +02:00
wuwiandClaude Opus 5 04ee0174ce Three more chapters, and at game 100 the champion returns everything
The roster ran out at chapter 6 and then repeated itself; game 70 got the
groundskeeper again, bigger. bossArt() now walks nine designs, one per chapter,
and hands out THE CHAMPION on every tenth — the floor(ch/10) term takes his
chapters back out of the count so chapter 11 opens the second lap on the
groundskeeper rather than skipping him.

  70 THE STRINGER — leather apron, bandoliers of gut string, racket frames on
     his back. He takes your lane and puts two dead-straight ones on it.
  80 THE TOURNAMENT DIRECTOR — blazer, lanyard, radio. `adds` is new: he calls
     stewards onto the court while you fight him, which is the one thing a boss
     fight could not do before, since the stage is cleared for it.
  90 THE STRAWBERRY SELLER — tray of cream, apron soaked red, ladle. A tank
     that sprays four at once and then comes for you.
 100 THE CHAMPION — torn whites, a laurel of strapping tape, the trophy still
     in one fist. `rally`: anything under splatter quality comes straight back
     at your head, so he is the one body you have to out-HIT rather than
     out-shoot. `phases` lays three rows over his own as his health falls — he
     holds the baseline and rallies, then runs the line drilling volleys, then
     drops to a charge — and each turn is an event: cry, shake, the stand up.

Thirty-six frames through assets/_regen.py, the mascot's three-batch recipe
(2 stride, 3 wounds, 4 throws). The trap that cost the re-rolls: a throw batch
references its own wound frames, so a wounds batch that fails leaves the throw
sheet drawn against the SEED — two of the four came back half groundskeeper.
Wounds first, verify, then throws.

And the physio, who Oliver reported "does basically nothing but move towards
the net": he kept 4.2 of every 6.3 m he charged, so the fight was always over
before he arrived. `withdraw` is his own number now (0.2) and he charges half
again as fast — measured, he is on top of you in 11 s instead of never — and he
drills three balls a cycle on the way in.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-09 16:08:57 +02:00
wuwiandClaude Opus 5 f4c6f408e7 A stolen finger no longer welds you to the edge, and the stack is yours
The right edge is where iOS keeps Control Center, and it is exactly where the
horde's absolute finger-steering parks your thumb. When the system takes that
gesture it delivers neither touchend nor touchcancel: `stick` survives forever,
so every later touchstart falls through to the extra-finger branch (swish, and
only swish) while touchmove ignores the new finger's id. steerPX stays frozen at
0.97 and the player is pinned to the screen edge, melee firing, for the rest of
the run. Any new touch event proves the old finger is gone if it is not in
e.touches — drop the stick then. Verified against the bug: pinned at x=6.11
before, steers to -5.20 after, and a genuine second finger still only swishes.

And wCap 9 -> 99, which is no cap in practice: game 70 wants a bigger stack, not
a rationed one. The barrel weapons move from "paid out only once all four tins
are full" (a branch an uncapped rack would never reach again) to rungs of the
same ladder, so a late duel still hands out a rocket instead of a fifth bomb.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-09 15:35:53 +02:00
wuwiandClaude Opus 5 5de42648ec The slice stops steering: each kill strikes the ball once, then physics
Oliver, watching a sliced ball circle a shambler it had only wounded: "an
orbiting tennis ball midair, nobody knows what is going on." He is right,
and not only about that shot. Every ball in this game flies a parabola you
can read, so the one that thinks reads as broken even on the shots where it
works. The hook was the wrong primitive, not a hook with two bugs in it.

So there is no hook. At the instant of a kill the ball is struck again —
ONE velocity out of solveShot, the same solver the serve uses, aimed at the
next body's chest — and from the next frame it is an ordinary ball under
ordinary gravity. It cannot orbit, cannot hover and cannot outlive its shot,
because after the strike nothing about the slice is still running.

That also deletes the wound bug rather than fixing it. Six sites spend the
ball (lastHit = 'X') and not one of them cleared the hook, so a ball that
merely WOUNDED its target was left steering at 30% pace toward a point that
body had already walked off — the loop he saw. With no persistent state
there is nothing to clear: a ball spent on a body it failed to kill just
drops, like every other spent ball.

The hop draws its own band, white, so a chain leaves the same chalk streak
every other shot leaves. sliceTurn and sliceHookT are gone with the steering
that needed them; sliceArc stays, no longer as a turn-radius limit but to
keep a carom from doubling back down-court, which reads as a stray
deflection rather than as a cut.

Net 24 lines lighter than the thing it replaces.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-09 14:15:24 +02:00
wuwiandClaude Opus 5 e11ad30a63 The hook turns as hard as the geometry needs, or not at all
A slice hooking onto a body two metres to the side flew a closed loop around
it forever: 285 frames of the same circuit, never arriving, never falling.
A constant turn rate has a constant turn radius (v/w = 2 m), and anything
inside that circle cannot be reached however long the ball steers — while
the hook pins vy at 0, so nothing else ends it either. A crowd is nothing
but close bodies, which made the one place this weapon should shine the one
place it hung.

The rate is no longer a constant. An arc leaving along the current heading
and passing through a body at distance d, bearing a, has radius d/(2 sin a),
so the ball needs 2 v sin(a) / d — solved every frame, because the body is
walking while the ball travels. sliceTurn stays as the FLOOR, which is what
it was always really for: how wide the curl looks when there is room.
Turning faster than needed is safe, the aim being re-solved each frame.

Two things the maths cannot cover, so they are ruled out instead: a body
behind the ball, which no tangent arc reaches at any rate (sliceArc), and a
target that walks out of reach mid-hook (sliceHookT, a hard 0.7 s licence to
steer). An endless orbit is now impossible by construction rather than by
the geometry happening to work out.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-09 13:53:20 +02:00