Seven verdicts from the test group

- opening CPU beatable in a game or two (softer cap, more whiffs, rubber band)
- modals are for calm courts only; live-court news is a card now
- the transition howto is gone: a ghost hand teaches the pull, the toss,
  the apex and the swing, each on the exact struggle it answers
- a shot barrel keeps your weapon and glows its slot instead
- switching weapons clicks, the live tile is unmistakable
- barrels come faster the deeper the run goes
- the umpire triple-tap cheat is deleted

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
wuwiandClaude Fable 5 committed 2026-08-08 19:49:27 +02:00
1 parent f13cbef28a
commit a89e8ff419
10 files changed
+242 -79

No files matched your search

+62 -4
View File
@@ -5,7 +5,7 @@ everything they specified is shipped, and what still matters from them is record
long history, `~/.claude/projects/.../memory/splatter-tennis-history.md` is auto-loaded; for the
detail of any individual change, read the git log.
Last updated 2026-07-31 (serve wind-up pass). **Start with "How to work on this", "OPEN — leftovers" and "LATER —
Last updated 2026-08-08 (testgroup round: popup policy, ghost hand, barrel economy). **Start with "How to work on this", "OPEN — leftovers" and "LATER —
shipping it"** (all three just above the CLOSED sections) — the first tells you how Oliver's feedback
should be handled, the second is everything known to be outstanding in the game, the third is the
demo/store/campaign plan, which is decided but entirely unbuilt. All four playtest rounds are closed
@@ -52,7 +52,8 @@ P/O/ball/score + `setupPoint`/`sendBoy` · `gameplay.js` ballistics/swings/serve
transition + `splatPoint` + ball-boy routines · `zombies.js` zombie/gore/particle state +
`updateZombies` · `render.js` camera/palette-lerp/sprites/all drawing · `input.js` keys/touch/flick ·
`audio.js` sfx + recorded umpire (`assets/audio/*`, macOS `say -v Daniel`) · `panel.js` the T panel ·
`main.js` boot, loop, umpire-tap cheat, `window.T` test surface.
`main.js` boot, loop, scoreboard test-mode taps, `window.T` test surface (the umpire-tap cheat is
removed — the testteam found it; `T.forceZombies()` is the console replacement).
THREE runtime-only import cycles (documented in headers): input↔gameplay, zombies↔gameplay,
and state↔zombies (`sendBoy` reads `gore` to decide whether a mopping round is worth walking out for).
All three are safe for the same reason: the imported binding is only ever touched inside a function at
@@ -80,6 +81,10 @@ zombie (now an occasional opponent, `TUNE.racketEvery`, not the every-game fixtu
it, and the `pointerdown` dismisser is registered **only on `pointer: fine`** — on touch,
pointerdown fires *before* touchstart, so a shared handler cleared the modal and let the
touchstart underneath serve. `G.seen.<key>` gates first-time-only modals.
**POLICY (2026-08-08, testgroup round): a modal may only fire on a guaranteed-calm court** (duel
intro, boss walk-on, post-duel healing, test mode, the horde's first-ever arrival). Anything that
can land on a live court announces as a CARD or a flash — the freeze itself is safe (measured:
a body moves 0.000 m behind a modal), but the stolen tap and the broken rhythm cost runs.
**`m.in` (0..1 over `TUNE.modalIn`) is the arcade entrance** and the ONE thing that animates behind
the freeze — it is ticked inside that same early-return, so it advances while nothing else does.
Icon and title fly in from the right (with a two-ghost speed streak), body lines cascade from the
@@ -371,6 +376,55 @@ zombie (now an occasional opponent, `TUNE.racketEvery`, not the every-game fixtu
`RETURNED`, `DOUBLE KILL`) is a footnote to something you already watched happen. Sharing the loud
FACE with the milestone cards is right; sharing their SIZE made every footnote read as a milestone.
## Session log: the testgroup round — popups, ghost hand, barrel economy (2026-08-08)
First round of feedback from the small test group (via Oliver), seven items, all landed.
**The popup policy changed: a modal is for CALM courts only.** The report was "popups block
interaction while the game loop continues and the zombies arrive". Measured first, per the
meta-lesson: the world genuinely FREEZES behind a modal (a shambler moved 0.000 m over 3 s with one
up — the invariant holds). What kills is the CONTROL steal: a popup lands mid-crowd, eats the next
tap, and hands the court back with the player's rhythm gone. So every announcement that can land on
a live court is a CARD now (non-blocking): the weapon first-sight modals are deleted outright
(deliverTin's card and popBarrel's new card carry the one desc line that matters), and the
barrel/machine/racket introductions are first-sight cards. The horde popup shows as a modal ONCE
per run (it changes the control mode) and every later set starts on the flash alone — re-teaching
the same rules at the hottest moment of every set was the worst offender. Still modals: sudden
death, the chapter boss, WOUNDS TREATED, test mode — all fire on guaranteed-calm courts.
**The transition howto modal is gone; a GHOST HAND teaches instead.** "Nobody seems to read this."
`drawGhostHand` (render.js) is a drawn pointing hand — canvas paths, UI palette, deliberately not a
sprite — with `drawTapRing` for the contact ripple that makes a mimed tap read as a tap. The apron
demo's old thumb-dot now rides its strokes, and the transition primes `G.idleHintT` so the pull
demo starts miming the moment splatter begins. TENNIS got three lessons on the same hand, each
keyed to the exact failure it answers and reset by doing the thing: idle in the serve stance →
tap demo on the apron (`drawTapHint`); `G.tossDrops` ≥ 2 → the hand rides the toss and PRESSES AT
THE APEX (`drawApexHint` — the timing is the lesson); `G.passStreak` ≥ 2 untouched passes → the
hand presses as the ball closes (`drawHitHint`).
**The opening CPU is beatable in a game or two now.** Testers ground for rounds. `aiFlub` 0.09 →
0.18, `aiQualityMax` 0.85 → 0.72, plus a rubber band: every game the CPU takes adds `aiFlubLoss`
(0.08) to its whiff chance. Tennis mode only exists before the first transition, so these are
purely the opening-match dials (`racket*` covers the duels). **The old "CPU returns ~91.7%"
benchmark is deliberately retired by this** — the expected first-swing return rate is now ~82%
at 0–0 and falls as the CPU loses games.
**A shot barrel no longer yanks the new weapon into your hand.** `popBarrel` restores `G.weapon`
after `grantWeapon` and pulses the new slot instead (`G.rackGlow`, drawn in drawRack) — "the
current weapon should stay active, the new slot should glow briefly". The duel's `deliverTin`
still auto-equips; that one lands on a calm court and IS the prize.
**Switching weapons clicks** (`assets/audio/weapon-click2.mp3`, 0.45 gain — `sfx()` now accepts
names carrying their own extension) **and the live rack tile is unmistakable**: brighter well,
2 px gold frame.
**Barrels ramp with the run** (`barrelRamp`): the gap between barrels shrinks by one every 8
finished games, floored at every game (g0–7 every 3rd, g8–15 every 2nd, g16+ every game). The
late game was "fast tapping until your thumb gives out" once the tins capped; the rack is the
strategic answer, so the run keeps feeding it.
**The umpire triple-tap cheat is deleted** — the test group found it. `T.forceZombies()` remains.
## Session log: the sidestep, and two type registers (2026-08-03)
Oliver's post-ship playtest. One blocker and seven polish items; all eight landed.
@@ -630,7 +684,7 @@ something looks. `T.pause(false)` runs again.
## TEST MODE — triple-tap the scoreboard
A sandbox for trying new content without playing twenty games to reach it. Three taps on the
scoreboard (the same shape as the umpire's triple-tap cheat next to it in main.js) toggles
scoreboard (main.js; the umpire's neighbouring triple-tap cheat is deleted) toggles
`G.testMode`; three more turns it off, and a SINGLE tap is still the miss-log overlay.
- a fixed MEDIUM wave: `testZMax` 8 bodies, `testZSpeed` 1.15 m/s, `testSpawnEvery` 1.6 s — it
@@ -669,7 +723,11 @@ Clear the intro first: `document.getElementById('intro')?.remove()`.
`T.flickTarget`, `T.showModal`/`T.dismissModal`, `T.render`.
**Benchmarks that must keep holding** (re-run after any physics/aim change):
- CPU returns ~11/12 serves (91.7%). Recent 150-sample runs: 90.7%, 89.3%, 90.7%, 88.7% (±2.5%).
- **RETIRED 2026-08-08**: the ~91.7% CPU return rate. The opening CPU was deliberately softened
(aiFlub 0.18 + aiFlubLoss rubber band, see that session log) — expect ~82% at 0–0, less as it
drops games. The differential method below is still how to prove "no unintended regression".
The old record, for the archaeology:
CPU returns ~11/12 serves (91.7%). Recent 150-sample runs: 90.7%, 89.3%, 90.7%, 88.7% (±2.5%).
Watch this — the last few sit under the bar rather than scattered around it.
**The harness for this number was never written down and I could not reconstruct it.** A rig that
counts "did the CPU get a racket on the served ball" scores 100/100, because after a flub the CPU