Give the rally its length back: deep balls, baseliners, approaches off short balls

Every match had collapsed into serve, both at the net, reflex volleys at
~0.7 s per shot. The rally ball landed on the service line by definition,
the match CPU stood on the bounce and volleyed 90-100% of everything, and
the auto-run chased the earliest contact.

Rally modes only (classic + every match; gore and the duel untouched):
rally balls land deep and mistiming only shortens them (no new long outs);
each shot latches the receiver's plan as it leaves the racket — from the
back he lets it bounce and meets it falling, up to 2.2 m behind the line,
a short ball pulls him in and he follows it to the net, nobody approaches
a man already there, and a volley below the tape is a weak block. Measured:
contacts 10.5-13.7 m from the net, back-court volleys 0-8%, 0.8-1.0 s per
baseline shot. Three new tests pin the shape; v361.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
wuwiandClaude Opus 5.5 committed 2026-10-13 12:55:05 +02:00
1 parent f33d46366a
commit 4e7c816a25
8 files changed
+292 -29

No files matched your search

+110 -2
View File
@@ -89,7 +89,7 @@ globalThis.getComputedStyle = () => ({ paddingTop: '0' });
// ---- game modules (real code) ----------------------------------------------
const state = await import('./src/state.js');
const { G, P, O, score, ball, setupPoint } = state;
const { update, forceZombies, startGore, splatPoint, restartRun, leaguePace, duelSkill, tinLadder, deliverTin, peerK: _peerK, legLevel: _legLevel, winLevel: _winLevel, assistK: _assistK, flickTarget, flatCount, swayOf, keyPressed, classicGame, serveApexY, assistK, rallyFeel, barrelGap, machCap, hitServe, cheer, inCourt } = await import('./src/gameplay.js');
const { solveShot, update, forceZombies, startGore, splatPoint, restartRun, leaguePace, duelSkill, tinLadder, deliverTin, peerK: _peerK, legLevel: _legLevel, winLevel: _winLevel, assistK: _assistK, flickTarget, flatCount, swayOf, keyPressed, classicGame, serveApexY, assistK, rallyFeel, barrelGap, machCap, hitServe, cheer, inCourt } = await import('./src/gameplay.js');
const { startDuel, resolveInterlude, startBossFight, endBossFight, startDrill: _startDrill, endDrill, startMatch: _startMatch, endMatch, FALL_LEN, startNetVs, endNetVs } = await import('./src/beats.js');
const { mirror, netMsg, netTest, netTick, netAgain } = await import('./src/net.js');
// the drill's first-sight modal freezes the world (that is what modals do) — dismiss it the way a
@@ -1030,7 +1030,8 @@ test('match recovery: finish the serve, recover during the outbound rally, hold
assert.deepEqual([rz.x, rz.z], [3, -0.5], 'serve follow-through holds its mark');
rz.serveHold = 0;
update(DT);
assert.ok(rz.x < 3 && rz.z > -0.5, 'recovers towards centre and into court before the return');
// THE COURT HAS ITS LENGTH: the baseliner's spot is BEHIND the line now (rallyBack), not 2.5 m in
assert.ok(rz.x < 3 && rz.z < -0.5 && rz.z >= -TUNE.rallyBack, 'recovers towards centre, to its spot behind the baseline');
const recoverySpeed = Math.hypot(rz.vx, rz.vz);
ball.lastHit = 'P'; ball.vz = -1;
update(DT);
@@ -1057,6 +1058,113 @@ test('the approaching Champion can still be passed into the open back corner', (
assert.deepEqual(score.pts, [1, 0], 'a legal passing shot beats a Champion stranded at the net');
});
// ---- THE COURT HAS ITS LENGTH (2026-10-13, PROJECT.md "THE RALLY GOES FLAT") ----------------------
// Oliver: "Aufschlag, dann schnell vor ans Netz ... ein sehr schneller Ballwechsel" — every match had
// become a volley exchange at the T. Measured: the rally ball landed ON the service line, the
// opponent stood on the bounce and volleyed 90-100% of everything from opponent 6 up, and the
// auto-run chased the earliest contact, ~0.7 s per shot. These pin the shape that replaced it.
test('THE COURT HAS ITS LENGTH: a match rally is played from the baselines, not volleyed at the T', () => {
// the reachability soak's autopilot, asserting the SHAPE of the rally rather than a win rate
const assist = TUNE.autoAssist; TUNE.autoAssist = 1.15;
try {
startMatch(); G.interlude.introT = 0; G.match.opp = 12;
for (let i = 0; i < 2.2 * 60; i++) update(DT);
const hits = [];
let prev = ball.lastHit;
for (let i = 0; i < 120 * 60; i++) {
G.modal = null; G.modalQ.length = 0; G.hp = G.hpMax; // keep playing: the shape is the subject
if (G.phase === 'pretoss' && !(G.interlude && G.interlude.introT > 0)) { if (i % 40 === 0) keyPressed('Space'); }
else if (G.phase === 'toss') { if (ball.vy < 0.4 && ball.y > 2.4) keyPressed('Space'); }
else if (G.phase === 'rally' && ball.active && ball.lastHit === 'O') {
const d = Math.hypot(ball.x - P.x, (ball.z - P.z) * 0.8);
if (d < 2.9 && d > 2.2) keyPressed('Space');
}
const b0 = ball.bounces;
update(DT);
if (ball.active && ball.lastHit !== prev && !G.wasServe) {
const ch = ball.lastHit === 'P' ? P : oppOf();
if (ch) hits.push({ back: Math.abs(ch.z - COURT.netZ) >= TUNE.netFrom, volley: b0 === 0, t: i * DT });
}
prev = ball.active ? ball.lastHit : null;
}
const back = hits.filter(h => h.back);
assert.ok(hits.length >= 40, 'a real rally sample: ' + hits.length + ' contacts');
assert.ok(back.length / hits.length >= 0.6, `most of the rally is played from the back: ${back.length}/${hits.length}`);
const volleys = back.filter(h => h.volley).length;
assert.ok(volleys / back.length <= 0.1, `a baseliner lets it bounce: ${volleys}/${back.length} back-court volleys`);
const gaps = [];
for (let k = 1; k < hits.length; k++) if (hits[k].back && hits[k - 1].back && hits[k].t - hits[k - 1].t < 3)
gaps.push(hits[k].t - hits[k - 1].t);
gaps.sort((a, b) => a - b);
assert.ok(gaps[gaps.length >> 1] >= 0.75, 'a baseline exchange leaves time to play: median '
+ gaps[gaps.length >> 1].toFixed(2) + ' s per shot');
} finally { TUNE.autoAssist = assist; }
});
test('a SHORT ball is an approach, a deep one keeps the baseliner back, and nobody approaches a man at the net', () => {
const flub = TUNE.matchFlub; TUNE.matchFlub = 0;
// the opponent's answer to one ball from the player's baseline, landing `depth` m inside its line
const play = (depth, pace, from) => {
freshSplatter(); startMatch(); G.interlude.introT = 0;
const rz = oppOf();
Object.assign(rz, { x: 0, z: -TUNE.rallyBack, swingT: -1, swingCool: 0, serveHold: 0 });
Object.assign(P, { x: 0, z: 24, swingT: -1 });
G.phase = 'rally'; G.wasServe = false;
const v = solveShot(0, 1, 22.5, 0.5, depth, pace);
Object.assign(ball, { active: true, scored: false, lastHit: 'P', bounces: 0, pow: 0.5, rx: null, from,
x: 0, y: 1, z: 22.5, vx: v.vx, vy: v.vy, vz: v.vz, g: TUNE.gravity });
let bounced = null, at = null;
for (let i = 0; i < 4 * 60 && ball.lastHit === 'P' && ball.active; i++) { bounced = ball.bounces; update(DT); }
assert.equal(ball.lastHit, 'O', `depth ${depth}: the opponent returns it`);
at = rz.z;
for (let i = 0; i < 30; i++) update(DT); // its own ball is in the air: where does it go now?
return { bounced, at, then: rz.z, approach: ball.approach };
};
try {
const deep = play(2.4, 24, 12);
assert.equal(deep.bounced, 1, 'a deep ball is played after the bounce');
assert.ok(deep.at < 1, 'from the baseline, not the T: met at z ' + deep.at.toFixed(2));
assert.equal(deep.approach, null, 'and it is no approach');
assert.ok(deep.then < 1, 'he stays back: ' + deep.then.toFixed(2));
const short = play(6.4, 15, 12);
assert.equal(short.approach, 'O', 'a short ball taken is an approach');
assert.ok(short.at > 2, 'he ran in for it: met at z ' + short.at.toFixed(2));
assert.ok(short.then > short.at + 0.5, 'and follows it in: ' + short.at.toFixed(2) + ' -> ' + short.then.toFixed(2));
const pass = play(6.4, 15, 3);
assert.equal(pass.approach, null, 'against a man at the net the same ball is a pass, never an approach');
} finally { TUNE.matchFlub = flub; }
});
test('rally depth: deep by default, mistiming lands it SHORTER, never longer', () => {
// the only out a rally ball may find is wide — depth must never put one past the baseline
startMatch(); G.interlude.introT = 0;
const zb = oppOf();
const sample = n => {
G.match.opp = n; zb.foe = cfg.matchFoe(n);
const depths = [];
for (let i = 0; i < 400; i++) {
G.phase = 'rally'; G.wasServe = false; G.match.surprised = G.match.expected = false;
P.x = 0; P.z = 24; P.swingT = -1;
zb.x = 0; zb.z = 2; zb.swingT = -1; zb.swingCool = 0; zb.serveHold = 0;
Object.assign(ball, { active: true, lastHit: 'P', bounces: 1, rx: null,
x: 0, z: 2.4, y: 1.2, vx: 0, vy: 0, vz: -1 });
update(DT);
if (ball.lastHit !== 'O') continue;
const t = (ball.vy + Math.sqrt(ball.vy * ball.vy + 2 * ball.g * ball.y)) / ball.g;
depths.push(COURT.L - (ball.z + ball.vz * t));
ball.active = false;
}
return depths;
};
const weak = sample(1), strong = sample(18);
// solveShot aims at ty 0.08 (the ball on the grass), so the true bounce is a few dm past the mark
for (const d of [...weak, ...strong]) assert.ok(d >= TUNE.rallyDeep - 0.5, 'never long, never near the line: ' + d.toFixed(2));
const mean = a => a.reduce((s, x) => s + x, 0) / a.length;
assert.ok(mean(weak) > mean(strong) + 0.4, `weaker timing sits up shorter: ${mean(weak).toFixed(2)} vs ${mean(strong).toFixed(2)} m inside`);
endMatch();
});
test('The Climb opens briskly, converges at opponent eight, and leaves cup and training intact', () => {
startMatch(); G.interlude.introT = 0;
const zb = oppOf();