Stop calling "passed you" while the ball is still on its way to him
Oliver's v366 log: a LOST "passed you" with no swing. The match gave the CPU the point the moment its ball crossed 0.3 m behind the baseline — a line from when the player stood inside it. Since step 1 he waits up to 2.2 m behind the line for deep balls, and a ball going by ~1.5 m beside him only enters his reach just in front of him, so the point was called before it was ever playable. In the match "past you" now means past his reach. The swing log's hits also say whether the timing was early or late. v367. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
5bbde720ad
commit
dfdb699f9d
4 files changed
+73
-4
No files matched your search
+21
@@ -5445,6 +5445,27 @@ Oliver: *"bitte bau nochmal die Funktion ein die protokolliert warum ich einen B
|
||||
the bounce will land inside the swing window. Otherwise it digs the ball out as a half-volley, capped
|
||||
below the tape. His 48.14 s MISS was exactly this.
|
||||
|
||||
### "passed you" called before the ball reached him (2026-10-14, v367) — a step-1 regression
|
||||
Oliver's v366 log vs THE BRUTE, first loss:
|
||||
1. A 29 m/s topspin drive landed 3.7 m to his left.
|
||||
2. He reached it at the edge of his reach (q 0.09).
|
||||
3. The Brute came in behind the weak reply and hit 3 m to his left.
|
||||
4. LOST "passed you", with no swing at all.
|
||||
|
||||
The match awards the CPU the point the moment its ball crosses `COURT.L + 0.3`. That line was
|
||||
written when the player stood INSIDE his baseline. Since step 1 he waits up to `rallyBehind` (2.2 m)
|
||||
BEHIND it, deepest against heavy topspin. A ball passing ~1.5 m beside him only enters his reach about
|
||||
0.5 m in front of him (the reach ellipse is shallow off-centre), so the point was called before the
|
||||
ball was ever playable. A late-but-legal tap was cut off the same way. **In the match, "past you" now
|
||||
means past your reach:** `max(L + 0.3, P.z + reach·matchReach + 0.3)`. The duel keeps the old line;
|
||||
classic was never affected (it calls the second bounce). Test: a deep, slightly wide topspin ball
|
||||
against a player held 1.8 m behind his line. Without the fix it reproduces his exact log line
|
||||
(`LOST passed you`); with it he returns it. The "never swung, it came within X m" annotation is pinned
|
||||
in the same test.
|
||||
|
||||
The swing log's HIT line now also says which way the timing missed: `late 40ms` (the ball was already
|
||||
there when the window opened) or `early 40ms`. That was the question his first loss could not answer.
|
||||
|
||||
# OPEN — leftovers, in rough priority order
|
||||
|
||||
Nothing here is broken; these are known gaps and unverified risks.
|
||||
|
||||
+1
-1
@@ -1140,6 +1140,6 @@ if (location.hostname !== 'splatter-tennis.wuwi.one' && !window.Capacitor?.isNat
|
||||
</div>
|
||||
</div>
|
||||
<span id="recLamp" hidden>REC</span>
|
||||
<script type="module" src="src/main.js?v=366"></script>
|
||||
<script type="module" src="src/main.js?v=367"></script>
|
||||
</body>
|
||||
</html>
|
||||
+11
-3
@@ -1705,7 +1705,10 @@ function trySwing(ch, isAI, qCap = TUNE.aiQualityMax) {
|
||||
const sway = swayOf(aim), lane = Math.abs(sway) > 0.15 ? (sway < 0 ? ' left' : ' right') : '';
|
||||
const cap = q >= q0 ? '' : ball.rx && ball.rx.lob ? ' · capped: lobbed, on your heels'
|
||||
: ball.bounces === 0 ? ' · capped: volley below the tape' : ' · capped: dug out a drop';
|
||||
logSwing({ result: 'HIT', reason: `q=${r2(q)}${kind}${lane}${volley ? ' volley' : ''} · ${P.flick ? 'flick' : 'tap'} ${P.tap || 0}ms${cap}`,
|
||||
// ...and WHICH WAY the timing missed: contact before the window's centre means the ball was
|
||||
// already there — the swing came LATE; after it, the swing came EARLY
|
||||
const off = Math.round((t - center) * 1000), timing = q0 >= 0.95 ? '' : off < 0 ? ` · late ${-off}ms` : ` · early ${off}ms`;
|
||||
logSwing({ result: 'HIT', reason: `q=${r2(q)}${kind}${lane}${volley ? ' volley' : ''}${timing} · ${P.flick ? 'flick' : 'tap'} ${P.tap || 0}ms${cap}`,
|
||||
d: r2(Math.hypot(dx, dz * 0.8)), y: r2(ball.y) });
|
||||
}
|
||||
doHit(ch, q, aim);
|
||||
@@ -2496,8 +2499,13 @@ export function update(dt) {
|
||||
} else if (ball.bounces === 1) pointTo(ball.lastHit, null);
|
||||
}
|
||||
}
|
||||
// a racket zombie's return that gets past you (crosses your baseline) is a point for THEM
|
||||
if (G.mode === 'splatter' && ball.active && !ball.scored && ball.lastHit === 'O' && ball.z > COURT.L + 0.3) {
|
||||
// a racket zombie's return that gets past you (crosses your baseline) is a point for THEM.
|
||||
// IN THE MATCH "past you" means past your REACH (2026-10-14, Oliver's v366 log): since step 1 a
|
||||
// baseliner waits up to rallyBehind BEHIND his line, so the old line — 0.3 m behind the baseline —
|
||||
// called the point while the ball was still on its way to his racket. Every deep topspin ball the
|
||||
// auto-run met deep was a "passed you" with no swing. The duel keeps the old line.
|
||||
const passZ = G.match ? Math.max(COURT.L + 0.3, P.z + TUNE.reach * TUNE.matchReach + 0.3) : COURT.L + 0.3;
|
||||
if (G.mode === 'splatter' && ball.active && !ball.scored && ball.lastHit === 'O' && ball.z > passZ) {
|
||||
ball.active = false;
|
||||
if (G.interlude) resolveInterlude('O', 'passed_player'); // sudden death: its passing shot ends the duel
|
||||
else { if (!G.horde && !G.clearing) splatPoint('O'); spentBall(); } // clearing = mop-up, no scoring either way
|
||||
|
||||
@@ -1397,6 +1397,46 @@ test('THE SWING LOG writes in the match too: a whiff, a hit with its tap, and wh
|
||||
} finally { TUNE.autoAssist = assist; }
|
||||
});
|
||||
|
||||
test('a deep ball the player waits for BEHIND his baseline is still his to hit — not "passed you"', () => {
|
||||
// Oliver's v366 log vs THE BRUTE: "LOST passed you" with no swing. The match called the point the
|
||||
// moment its ball crossed 0.3 m behind the baseline — but since step 1 the player waits up to
|
||||
// rallyBehind behind it for a deep, kicking ball, so the ball was taken away on its way to him.
|
||||
const flub = TUNE.matchFlub; TUNE.matchFlub = 0;
|
||||
const assist = TUNE.autoAssist; TUNE.autoAssist = 1.15;
|
||||
try {
|
||||
startMatch(); G.interlude.introT = 0;
|
||||
const zb = oppOf(); zb.swingCool = 999;
|
||||
G.phase = 'rally'; G.wasServe = false; G.missLog.length = 0;
|
||||
Object.assign(P, { x: 0, z: COURT.L + 1.8, swingT: -1, tgt: null, vx: 0, vz: 0 });
|
||||
TUNE.autoAssist = 0; TUNE.matchAssist = 0; // he holds his deep mark: the ball comes to him
|
||||
const v = solveShot(0, 1, 1, 1.25, COURT.L - 2.2, 27, TUNE.gravity * 1.6); // deep, a little wide, heavy topspin
|
||||
Object.assign(ball, { active: true, scored: false, lastHit: 'O', bounces: 0, rx: null, spin: 1.15, soft: false,
|
||||
x: 0, y: 1, z: 1, vx: v.vx, vy: v.vy, vz: v.vz, g: TUNE.gravity * 1.6 });
|
||||
let crossed = false, lift = -1;
|
||||
for (let i = 0; i < 3 * 60 && ball.active && ball.lastHit === 'O'; i++) {
|
||||
if (lift < 0 && ringCue()?.gold) lift = i + 6;
|
||||
if (i === lift) keyPressed('Space', 0.1);
|
||||
update(DT);
|
||||
if (ball.z > COURT.L + 0.3) crossed = true;
|
||||
}
|
||||
assert.ok(crossed, 'the ball really did cross the old line');
|
||||
assert.equal(ball.lastHit, 'P', 'and he returned it: ' + JSON.stringify(G.missLog.map(e => e.result + ' ' + e.reason)));
|
||||
// ...while a ball that truly gets past him is still called, and says whether he swung
|
||||
TUNE.autoAssist = assist; TUNE.matchAssist = 1.15;
|
||||
freshSplatter(); startMatch(); G.interlude.introT = 0;
|
||||
oppOf().swingCool = 999;
|
||||
G.phase = 'rally'; G.wasServe = false; G.missLog.length = 0;
|
||||
Object.assign(P, { x: 0, z: 23, swingT: -1 });
|
||||
const w = solveShot(0, 1, 1, 3.9, COURT.L - 3, 28); // wide, past him
|
||||
Object.assign(ball, { active: true, scored: false, lastHit: 'O', bounces: 0, rx: null, spin: 1, soft: false,
|
||||
x: 0, y: 1, z: 1, vx: w.vx, vy: w.vy, vz: w.vz, g: TUNE.gravity });
|
||||
TUNE.autoAssist = 0; TUNE.matchAssist = 0; // a statue: it must go by
|
||||
for (let i = 0; i < 3 * 60 && G.phase === 'rally'; i++) update(DT);
|
||||
const lost = G.missLog.find(e => e.result === 'LOST');
|
||||
assert.ok(lost && /never swung, it came within/.test(lost.reason), 'the verdict says how close it came: ' + (lost && lost.reason));
|
||||
} finally { TUNE.matchFlub = flub; TUNE.autoAssist = assist; TUNE.matchAssist = 1.15; }
|
||||
});
|
||||
|
||||
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;
|
||||
|
||||
Reference in new issue
Block a user