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:
1 parent
f33d46366a
commit
4e7c816a25
8 files changed
+292
-29
No files matched your search
@@ -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();
|
||||
|
||||
Reference in new issue
Block a user