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>
This commit is contained in:
wuwiandClaude Opus 5 committed 2026-08-12 19:45:31 +02:00
1 parent c1f7268a04
commit 52b38e4a39
200 files changed
+153 -27

No files matched your search

+45 -15
View File
@@ -22,10 +22,16 @@ export const TUNE = {
// net-clearance loop ate the whole smash: a flat fast ball can't get over the
// tape AND come down on a target, so it got throttled back to walking pace.
// Heavy topspin lets it leave flat and DIVE, which is what a smash actually is.
smashBoost: 0.42, // extra pace on a FORWARD flick (aim.az < 0), scaled by timing. It does not
// send the ball longer — solveShot still lands it on the same spot — it
// gets there flatter and sooner, which is what a smash is.
aimError: 2.4, // m of scatter at quality 0
// NOT `aimReach` — that name is taken further down by the slingshot's aim-ray LENGTH (22 m), and
// the duplicate key silently won: every aimed shot reached 22x its width until a test caught it.
flickReach: 0.45, // how much of an aimed shot's width a MISTIMED flick gets (1 = all of it).
flickWidth: 3.95, // m off centre a PERFECTLY timed flick reaches. The singles line is 4.115,
// so this is the corner with 17 cm to spare. Raising it past the line does
// not make the flick punchier, it makes a perfect one go out.
flickReach: 0.42, // how much of an aimed shot's width a MISTIMED flick gets (1 = all of it).
// Reach x quality must stay under the sideline once aimError is added on
// top, or the flick goes out — which is exactly the bug this fixed:
// 3.3 * 0.45 + 2.4 = 3.89 against a 4.11 m line. Raise it and the flick
@@ -403,12 +409,16 @@ export const TUNE = {
// the broken recovery (the player never walked back to centre, so every
// wide return looked like opponent strength) — with real footwork restored
// it starts gentler and the ladder still climbs
matchSpeedPer: 0.08, // ...growing this much per opponent felled. Softened once: each rung moves
// STEEPER PER RUNG (2026-08-12): "in general this is too easy now". The flick fix means an aimed
// return can no longer sail out, so the player stopped handing over free points and the ladder had
// to earn them back. The OPENING is deliberately softer than before (the shambler starts at 0.72
// where the old first opponent was 0.85) — the ramp got steeper at both ends, not just harder.
matchSpeedPer: 0.11, // ...growing this much per opponent felled. Softened once: each rung moves
// THREE dials at once (speed, flub, width, pace), and the recorder showed
// opponent two as a wall — he beat one and lost every gap to two by ~0.3 m
matchFlub: 0.08, // the match's own whiff base — the duel's 0.2 gives away a fifth of all
// returns, which across a game is an opponent that beats itself
matchFlubKeep: 0.82, // ...and it keeps only this much per rung — the ladder now that hp is flat
matchFlubKeep: 0.76, // ...and it keeps only this much per rung — the ladder now that hp is flat
matchReach: 1.35, // contact reach multiplier in the MATCH. The recorder showed swings missing by
// centimetres point after point — the assist parks you near the ball, not inside
// a 1.3 m reach. A rally mode lives on contact feeling fair. Match-only
@@ -417,7 +427,7 @@ export const TUNE = {
// is the game's job. Held keys still override
matchOppPace: 0.78, // scale on the opponent's return SPEED for opponent one (~21 m/s instead
// of ~27) — the flight time auto-run needs. THE reachability dial
matchOppPacePer: 0.035,// ...climbing back per rung, capped at full pace (softened with speedPer)
matchOppPacePer: 0.05,// ...climbing back per rung, capped at full pace (softened with speedPer)
matchWideBase: 2.1, // m either side of centre the FIRST opponent's returns may land — the width an
// auto-run player can actually cover. Its scatter painted lines from game one
// ("still a LOT of unreachable balls"); now line-painting is the late ladder
@@ -690,18 +700,38 @@ export const bossArt = ch => ch % 10 === 0 ? BOSS_ART[BOSS_ART.length - 1]
// name it here, and that opponent walks on as it — through all three wound stages.
// short 8 characters, because that is the board's name slot — the match board names
// the thing you are playing instead of calling it THEM
// THE CARD, in order. It is a ladder of STAFF: the further you get, the further up the club's
// hierarchy the thing across the net used to be. That is the narrative, and the difficulty ramp is
// the same list read as numbers — every dial climbs monotonically, so "start easy and get harder"
// is a property of the table rather than something the code has to arrange.
// 1-2 the ones who fetch and sweep. Slow, wild, forgiving.
// 3-4 the ones who keep order. They start covering ground.
// 5-6 the ones who judge. Width arrives: they begin painting lines.
// 7-8 the ones who actually played. The club pro's own body, and the brute behind him.
// 9-10 management, and then the man whose name is on the honours board.
const OPPONENTS = [
{ short: 'KEEPER', line: 'HE MOWED THIS LAWN FOR THIRTY YEARS.', speed: 0.85, pace: 0.9, wide: 0.85, fam: 'br' },
{ short: 'JUDGE', line: 'EVERY BALL OF YOURS IS OUT.', wide: 1.25, flub: 0.8 },
{ short: 'UMPIRE', line: 'HE WILL NOT BE OVERRULED.', speed: 1.1, flub: 0.7 },
{ short: 'COACH', line: 'HE HAS SEEN YOUR BACKHAND.', pace: 1.15, wide: 1.1 },
{ short: 'PHYSIO', line: 'HE IS HERE TO TREAT YOU.', speed: 1.2, pace: 1.1 },
{ short: 'MASCOT', line: 'THE CROWD LOVES HIM.', flub: 1.2, wide: 1.3 },
{ short: 'STRINGER', line: 'HE RESTRUNG THIS RACKET HIMSELF.', pace: 1.2, flub: 0.75 },
{ short: 'DIRECTOR', line: 'HE CAN HAVE YOU REMOVED.', speed: 1.15, wide: 1.2 },
{ short: 'SELLER', line: 'WITH CREAM.', pace: 1.1, speed: 1.1, wide: 1.15 },
{ short: 'CHAMPION', line: 'HE RETURNS EVERYTHING.', pace: 1.25, speed: 1.3, wide: 1.25, flub: 0.5 },
{ short: 'SHAMBLER', name: 'THE SHAMBLER', fam: 'sh',
line: 'HE WAS HERE BEFORE ANY OF THIS.', speed: 0.72, pace: 0.72, wide: 0.75, flub: 1.35 },
{ short: 'BALL BOY', name: 'THE BALL BOY', fam: 'bb',
line: 'HE STILL FETCHES. HE JUST BITES NOW.', speed: 0.82, pace: 0.8, wide: 0.82, flub: 1.2 },
{ short: 'KEEPER', name: 'THE GROUNDSKEEPER', fam: 'gk',
line: 'HE MOWED THIS LAWN FOR THIRTY YEARS.', speed: 0.9, pace: 0.88, wide: 0.9, flub: 1.05 },
{ short: 'STEWARD', name: 'THE STEWARD', fam: 'st',
line: 'HE IS ASKING YOU TO LEAVE.', speed: 0.98, pace: 0.95, wide: 1.0 },
{ short: 'RUNNER', name: 'THE RUNNER', fam: 'ru',
line: 'HE COVERS THE COURT. ALL OF IT.', speed: 1.22, pace: 1.0, wide: 1.05, flub: 0.95 },
{ short: 'JUDGE', name: 'THE LINE JUDGE', fam: 'lj',
line: 'EVERY BALL OF YOURS IS OUT.', speed: 1.05, pace: 1.05, wide: 1.28, flub: 0.85 },
{ short: 'CLUB PRO', name: 'THE CLUB PRO',
line: 'HE GAVE LESSONS ON THIS COURT.', speed: 1.12, pace: 1.15, wide: 1.15, flub: 0.8 },
{ short: 'THE BULK', name: 'THE BULK', fam: 'br',
line: 'NOTHING GETS PAST HIM TWICE.', speed: 1.0, pace: 1.3, wide: 1.2, flub: 0.75 },
{ short: 'DIRECTOR', name: 'THE DIRECTOR',
line: 'HE CAN HAVE YOU REMOVED.', speed: 1.22, pace: 1.22, wide: 1.25, flub: 0.65 },
{ short: 'CHAMPION', name: 'THE CHAMPION',
line: 'HE RETURNS EVERYTHING.', speed: 1.35, pace: 1.3, wide: 1.3, flub: 0.5 },
];
// Who opponent N is. The chapter roster's own order is reused wholesale (bossArt), so the champion
// lands on opponent 10 exactly as he lands on chapter 10 and the two rosters can never drift apart.
export const matchFoe = n => {
@@ -741,7 +771,7 @@ export const BENCH_STAGES = ['HEALTHY', 'ARM OFF', 'HEADLESS'];
// that (15 to 48 away from z_br1, where a good frame scores under 8.5) and refuses them, which is
// the gate working, not the gate being wrong. Re-registering it is one entry here; see the note at
// the top of _master.py for the fix its next attempt needs.
export const ZFAMS = ['r', 'br'];
export const ZFAMS = ['r', 'br', 'sh', 'bb', 'st', 'ru', 'gk', 'lj'];
// THE RACKET FAMILY'S THREE STAGES, as a FRACTION of the pool — never against literal hp, the rule
// this whole file already holds (see woundStage). It lived inline in drawZombie; the FALL BEAT needs