Keep the auto-run's target where the player is allowed to stand
Oliver's v369 log ("vieeel besser") showed two misses with the auto-run
heading to x 6.5 and 8.7, far outside the frame he is held in. Since the
dip ends at the bounce, a hard cross-court topspin ball drifts sideways
long enough that its comfort point lies off the court; intercept had no
lateral bound, and its last-resort target was never checked at all
(headless: x 14.8). Candidates now respect xLimit — a kicking ball is
taken earlier, on the rise — and the final target is clamped. v370.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
e25047ed0c
commit
8e4b298d92
4 files changed
+53
-6
No files matched your search
+13
@@ -5502,6 +5502,19 @@ planner that rolls a bounce forward switches at the same moment (physics, `inter
|
||||
approach, quality by distance from the sweet spot. That is a separate decision. The timed ring still
|
||||
returns 92-98%. 162 tests, nine seeds.
|
||||
|
||||
### v369's phone verdict: "vieeel besser" — and one planning bug the topspin fix exposed (v370)
|
||||
His v369 log against THE BRUTE: long clean rallies. His flicks now scatter around the centre (q = 1 for
|
||||
most; the misses are ordinary ±50-90 ms timing, both directions). That confirms the v368 revert: no
|
||||
offset is left. One new pattern: two misses where the auto-run was heading to `runTo` x 6.51 and 8.73,
|
||||
far outside the frame the player is held in (`xLimit`, ~4.3 m on a phone). Since the dip ends at the
|
||||
bounce, a hard cross-court topspin ball landing just inside the line drifts on sideways long enough that
|
||||
its comfort point lies off the court. Two things were wrong. `intercept` had no lateral bound on any
|
||||
candidate. And its last resort (`deepest`, "run at the deepest hittable point") was never checked for
|
||||
reach at all; headless it planned to x 14.8. Every candidate now respects `xLimit`, so a kicking ball is
|
||||
taken earlier, on the rise, while it is still inside. The final target is clamped too, so an unreachable
|
||||
ball still gets a run to the nearest legal spot. Test: his ball (hard cross-court, lands at 3.3, kicks
|
||||
out). Without the fix the target is 14.8 m; with it the run stays in bounds and he returns it.
|
||||
|
||||
# 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=369"></script>
|
||||
<script type="module" src="src/main.js?v=370"></script>
|
||||
</body>
|
||||
</html>
|
||||
+11
-5
@@ -1491,6 +1491,10 @@ function intercept(side) {
|
||||
if (tc > 0 && y + vy * tc - 0.5 * g * tc * tc > CONTACT.maxY) { plan.net = false; plan.lob = true; }
|
||||
}
|
||||
const back = plan && !plan.net; // a baseliner lets it bounce
|
||||
// ...and only where his legs may go: the player is held on screen (xLimit), and since the dip ends at
|
||||
// the bounce a kicking ball drifts on sideways long enough to plan him out to x 6.5-8.7 — he ran to
|
||||
// the edge of the frame and watched it pass (his v369 log, 65.77 s and 84.26 s)
|
||||
const xl = side === 'P' ? xLimit() : 99;
|
||||
let fallback = null, deepest = null, anyHit = null, rise = null, nb = ball.bounces;
|
||||
for (let i = 0; i < 300; i++) {
|
||||
vy -= g * dt; x += vx * dt; y += vy * dt; z += vz * dt;
|
||||
@@ -1501,7 +1505,7 @@ function intercept(side) {
|
||||
// (up to rallyTop since the dip ends at the bounce: a kicking ball is taken at the top of its
|
||||
// bounce, chest high, rather than chased forward and volleyed — 40% volleys was the first cut)
|
||||
if (back && nb === 1 && i > 5 && y > 0.45 && y < (plan.short ? TUNE.rallyHit + 0.6 : TUNE.rallyTop)) {
|
||||
const inB = side === 'P' ? z <= COURT.L + TUNE.rallyBehind : z >= -TUNE.rallyBehind;
|
||||
const inB = (side === 'P' ? z <= COURT.L + TUNE.rallyBehind : z >= -TUNE.rallyBehind) && Math.abs(x) <= xl;
|
||||
if (inB && Math.hypot(x - ch.x, z - ch.z) / spd <= i * dt) {
|
||||
if (plan.short || vy < 1) return { x, z };
|
||||
rise ||= { x, z };
|
||||
@@ -1514,17 +1518,19 @@ function intercept(side) {
|
||||
// height: running forward onto the short ball is the tennis answer, and now it is the assist's.
|
||||
if (side === 'P' && rallyFeel() && !anyHit && i > 5 && z > COURT.netZ && y < 1.6 && y > 0.35) {
|
||||
const pt = { x, z: Math.min(24.5, z) };
|
||||
if (Math.hypot(pt.x - ch.x, pt.z - ch.z) / spd <= i * dt) anyHit = pt;
|
||||
if (Math.abs(x) <= xl && Math.hypot(pt.x - ch.x, pt.z - ch.z) / spd <= i * dt) anyHit = pt;
|
||||
}
|
||||
if (i > 5 && inZone(z) && y < 1.6 && y > 0.3) {
|
||||
const pt = { x, z: side === 'O' ? Math.max(0.8, z) : Math.min(24.5, z) };
|
||||
const reachable = Math.hypot(pt.x - ch.x, pt.z - ch.z) / spd <= i * dt;
|
||||
const reachable = Math.abs(x) <= xl && Math.hypot(pt.x - ch.x, pt.z - ch.z) / spd <= i * dt;
|
||||
if (y > 0.7 && reachable && !back) return pt; // waist-high AND arrivable in time
|
||||
if (reachable && !fallback) fallback = pt; // low but arrivable
|
||||
deepest = pt; // last resort: run at the deepest hittable point
|
||||
}
|
||||
}
|
||||
return rise || fallback || deepest || anyHit || home; // zone points first: meeting deep is still better
|
||||
const to = rise || fallback || deepest || anyHit || home; // zone points first: meeting deep is still better
|
||||
// a ball no point of which he can reach still gets a run — to the nearest spot he is allowed to stand
|
||||
return Math.abs(to.x) > xl ? { x: Math.sign(to.x) * xl, z: to.z } : to;
|
||||
}
|
||||
|
||||
// THE GOLD RING, TIMED (2026-10-14). The approach ring used to go gold at a DISTANCE (matchRingSweet
|
||||
@@ -1549,7 +1555,7 @@ export function ringCue() {
|
||||
const legal = y <= CONTACT.maxY && (y >= CONTACT.minY || vy > 0)
|
||||
&& !(volley && y < COURT.netH && ball.rx && !ball.rx.net);
|
||||
const k = togo > 0.01 ? Math.min(1, sp * t / togo) : 0;
|
||||
const px = P.x + (tg ? tg.x - P.x : 0) * k, pz = P.z + (tg ? tg.z - P.z : 0) * k;
|
||||
const xl = xLimit(), px = Math.max(-xl, Math.min(xl, P.x + (tg ? tg.x - P.x : 0) * k)), pz = P.z + (tg ? tg.z - P.z : 0) * k;
|
||||
if (legal && Math.hypot(x - px, (z - pz) * 0.8) <= reach) {
|
||||
const win = TUNE.swingWindow * (volley ? VOLLEY.window : 1) * winLevel();
|
||||
const sweet = TUNE.swingWindup + win / 2, band = win / 2 * TUNE.ringBand;
|
||||
|
||||
@@ -1437,6 +1437,34 @@ test('a deep ball the player waits for BEHIND his baseline is still his to hit
|
||||
} finally { TUNE.matchFlub = flub; TUNE.autoAssist = assist; TUNE.matchAssist = 1.15; }
|
||||
});
|
||||
|
||||
test('a topspin ball kicking out wide is planned where the player may go, and returned', () => {
|
||||
// Oliver's v369 log, 84.26 s: "ball line 2.72m wide — run, don't swing", runTo=8.73. Since the dip
|
||||
// ends at the bounce, a ball landing just inside the line drifts on sideways long enough that the
|
||||
// auto-run planned him far outside the frame he is held in (xLimit) — he waited at the edge.
|
||||
const flub = TUNE.matchFlub; TUNE.matchFlub = 0;
|
||||
const assist = TUNE.autoAssist; TUNE.autoAssist = 1.15;
|
||||
try {
|
||||
startMatch(); G.interlude.introT = 0;
|
||||
oppOf().swingCool = 999;
|
||||
G.phase = 'rally'; G.wasServe = false; G.missLog.length = 0;
|
||||
Object.assign(P, { x: -1.2, z: 24.4, swingT: -1, tgt: null });
|
||||
const g = TUNE.gravity * (1 + 0.68 * TUNE.spinFactor);
|
||||
const v = solveShot(-4, 1, -0.6, 3.3, COURT.L - 3.67, 30, g); // hard cross-court: lands 3.3, kicks on out
|
||||
Object.assign(ball, { active: true, scored: false, lastHit: 'O', bounces: 0, rx: null, spin: 1.15, soft: false,
|
||||
x: -4, y: 1, z: -0.6, vx: v.vx, vy: v.vy, vz: v.vz, g });
|
||||
let lift = -1, widest = 0;
|
||||
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 (P.tgt) widest = Math.max(widest, Math.abs(P.tgt.x));
|
||||
}
|
||||
// (headless the frame is wide enough that the bound is P_BOUNDS.x; on a phone it is the screen, ~4.3)
|
||||
assert.ok(widest <= cfg.P_BOUNDS.x + 1e-6, 'the run stays where he may stand: widest target x ' + widest.toFixed(2));
|
||||
assert.equal(ball.lastHit, 'P', 'and he gets it back: ' + JSON.stringify(G.missLog.map(e => e.result + ' ' + e.reason)));
|
||||
} finally { TUNE.matchFlub = flub; TUNE.autoAssist = assist; }
|
||||
});
|
||||
|
||||
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