Make the double tap a stance that holds the net
Oliver on v376: "wenn man doppeltippt, geht er minimal vor". His own ball's flight covered 4-9 m of the 10 to the net. His next ball was met from mid-court, outside the net zone, and the recovery after that volley walked him straight back to the baseline. The double tap now sets a stance (P.post) that lasts until the next double tap, a lob he has to run down, or the end of the point. Every recovery heads for the net at full pace, and every incoming ball is planned as a net man's. The 'back' stance clears once he is behind the net zone, so short balls still pull him in. Measured: contacts while committed are ~35% at the net and ~40% around the service line, and he no longer falls back. The base ladder is unchanged. New test; v377. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
48d1069e40
commit
ba8d13a867
6 files changed
+72
-11
No files matched your search
+24
-1
@@ -5736,6 +5736,28 @@ only the deep hard ball, the long down-flick only the lob.
|
|||||||
- **Not built:** a lesson for it. The ghost hand teaches the tap and the side flick only; the double tap
|
- **Not built:** a lesson for it. The ghost hand teaches the tap and the side flick only; the double tap
|
||||||
is undiscoverable without being told. Add one if the phone test says so.
|
is undiscoverable without being told. Add one if the phone test says so.
|
||||||
|
|
||||||
|
### The double tap is a STANCE, not one ball's run (2026-10-14, v377)
|
||||||
|
Oliver on v376: *"funktioniert irgendwie nicht — wenn man doppeltippt, geht er minimal vor"*. **Measured**
|
||||||
|
(`NETX=1`, the bot double-tapping behind 100% of its deep flicks):
|
||||||
|
- His own ball's flight covered only 4-9 m of the ~10 m to `netStand`.
|
||||||
|
- His next ball was usually met from mid-court, 7-10 m out, which is OUTSIDE `netFrom` (7 m).
|
||||||
|
- The approach flag lived for one ball, so the recovery after that volley (`rallyHome`: outside the net
|
||||||
|
zone → baseline) walked him straight back. Forward a little, then back: exactly the report.
|
||||||
|
|
||||||
|
**Now `P.post`** ('net' / 'base') is set by the double tap and holds:
|
||||||
|
- 'net' lasts until the next double tap, a lob he cannot get under and runs down (`plan.over === null` →
|
||||||
|
'base'), or the point's end (`setupPoint` clears it).
|
||||||
|
- `atNetHome` reads it first for the player, so every recovery heads for `netStand` at full pace.
|
||||||
|
`P.rushing` follows it, so every incoming ball is planned as a net man's.
|
||||||
|
- 'base' clears itself once he is back beyond `netFrom` + 0.5, so a short ball still pulls him in as before.
|
||||||
|
|
||||||
|
**After:** of his contacts while committed, ~35% are at the net (< 3.5 m), ~40% around the service line (the
|
||||||
|
first volley), ~20% at 7-10 m, and ~5% further. The stance is dropped only by lobs and point ends. Ladder
|
||||||
|
(σ60, 8 seeds): the base is unchanged; following in is now about neutral, still good against the pushers
|
||||||
|
(Steward 52 → 61), and no longer punished by the Brute and the Champion.
|
||||||
|
Test: committed to the net, a volley struck 9 m out keeps him coming in (< 7 m a second later); without the
|
||||||
|
stance the old recovery still takes him back.
|
||||||
|
|
||||||
# OPEN — leftovers, in rough priority order
|
# OPEN — leftovers, in rough priority order
|
||||||
|
|
||||||
Nothing here is broken; these are known gaps and unverified risks.
|
Nothing here is broken; these are known gaps and unverified risks.
|
||||||
@@ -5751,7 +5773,8 @@ Nothing here is broken; these are known gaps and unverified risks.
|
|||||||
go back, and the SMASH. See "The player's way to the net and back" above.
|
go back, and the SMASH. See "The player's way to the net and back" above.
|
||||||
v375: the auto-run's shake is gone (a latched chase plan); he is a little stronger for it, so the bottom
|
v375: the auto-run's shake is gone (a latched chase plan); he is a little stronger for it, so the bottom
|
||||||
of the Climb and classic easy may now be too easy. See "The auto-run stops shaking".
|
of the Climb and classic easy may now be too easy. See "The auto-run stops shaking".
|
||||||
v376: the run to the net and back is a DOUBLE TAP while his own ball flies (replaces v374's flick length). Fallback tag `rally-baseline-v360`.
|
v376: the run to the net and back is a DOUBLE TAP while his own ball flies (replaces v374's flick length).
|
||||||
|
v377: the double tap is a stance (P.post) that holds until changed — v376 only moved him a few metres. Fallback tag `rally-baseline-v360`.
|
||||||
|
|
||||||
1. **`RES` 4 is unverified on a real phone under load.** It nearly doubled the fill rate (810 -> 1080
|
1. **`RES` 4 is unverified on a real phone under load.** It nearly doubled the fill rate (810 -> 1080
|
||||||
device px across). Oliver has not reported stutter, but he also has not played a late horde since.
|
device px across). Oliver has not reported stutter, but he also has not played a late horde since.
|
||||||
|
|||||||
+1
-1
@@ -1140,6 +1140,6 @@ if (location.hostname !== 'splatter-tennis.wuwi.one' && !window.Capacitor?.isNat
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<span id="recLamp" hidden>REC</span>
|
<span id="recLamp" hidden>REC</span>
|
||||||
<script type="module" src="src/main.js?v=376"></script>
|
<script type="module" src="src/main.js?v=377"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
+15
-5
@@ -1478,7 +1478,10 @@ const letBounce = () => ball.bounces === 0 && ball.y < COURT.netH && ball.rx &&
|
|||||||
// ...and where he recovers to is where he IS: at the net he holds the net, otherwise he goes back
|
// ...and where he recovers to is where he IS: at the net he holds the net, otherwise he goes back
|
||||||
// behind his baseline. Read live — recovery always moves him away from netFrom, so it cannot dither.
|
// behind his baseline. Read live — recovery always moves him away from netFrom, so it cannot dither.
|
||||||
// The one exception is the APPROACH: having taken a short ball, he follows it in.
|
// The one exception is the APPROACH: having taken a short ball, he follows it in.
|
||||||
const rallyHome = ch => ({ x: 0, z: (Math.abs(ch.z - COURT.netZ) < TUNE.netFrom && ball.retreat !== ch.side) || ball.approach === ch.side
|
// The PLAYER's double tap overrides all of that with a STANCE (P.post) that lasts until he changes it.
|
||||||
|
const atNetHome = ch => ch.side === 'P' && P.post ? P.post === 'net'
|
||||||
|
: (Math.abs(ch.z - COURT.netZ) < TUNE.netFrom && ball.retreat !== ch.side) || ball.approach === ch.side;
|
||||||
|
const rallyHome = ch => ({ x: 0, z: atNetHome(ch)
|
||||||
? COURT.netZ + (ch.side === 'P' ? TUNE.netStand : -TUNE.netStand)
|
? COURT.netZ + (ch.side === 'P' ? TUNE.netStand : -TUNE.netStand)
|
||||||
: ch.side === 'P' ? COURT.L + TUNE.rallyBack : -TUNE.rallyBack });
|
: ch.side === 'P' ? COURT.L + TUNE.rallyBack : -TUNE.rallyBack });
|
||||||
// Where a baseliner meets this ball: after the bounce, falling through rallyHit, or at the top of a
|
// Where a baseliner meets this ball: after the bounce, falling through rallyHit, or at the top of a
|
||||||
@@ -1574,7 +1577,7 @@ function intercept(side) {
|
|||||||
deepest = pt; // last resort: run at the deepest hittable point
|
deepest = pt; // last resort: run at the deepest hittable point
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (smashing) plan.over = null; // no overhead in reach: he runs it down
|
if (smashing) { plan.over = null; if (P.post === 'net') P.post = 'base'; } // no overhead in reach: he runs it down, and stays back
|
||||||
// ...and a frame that finds NOTHING mid-chase holds the plan it had: a dying drop's bounce sits on the
|
// ...and a frame that finds NOTHING mid-chase holds the plan it had: a dying drop's bounce sits on the
|
||||||
// 0.35 m floor, and every other frame without a candidate sent him home, 10 m away, and back again
|
// 0.35 m floor, and every other frame without a candidate sent him home, 10 m away, and back again
|
||||||
const hold = side === 'P' && P.chase && P.goal ? P.goal : null;
|
const hold = side === 'P' && P.chase && P.goal ? P.goal : null;
|
||||||
@@ -2162,8 +2165,13 @@ export function keyPressed(code, lead = 0, flick = false) {
|
|||||||
// In to the net, or back from it: the double tap's one meaning is "the other end of my half". At the net
|
// In to the net, or back from it: the double tap's one meaning is "the other end of my half". At the net
|
||||||
// (or already running in) it is the way back; anywhere else it is the way in. rallyHome reads the flags
|
// (or already running in) it is the way back; anywhere else it is the way in. rallyHome reads the flags
|
||||||
// while his ball flies, the run goes at full pace, and his reply is planned from wherever he has got to.
|
// while his ball flies, the run goes at full pace, and his reply is planned from wherever he has got to.
|
||||||
|
// It is a STANCE, not one ball's run (Oliver on v376: "wenn man doppeltippt, geht er minimal vor"): his own
|
||||||
|
// ball's flight covers 4-9 m of the 10 to the net, his next ball was met from mid-court, outside the net
|
||||||
|
// zone, and the recovery after it walked him straight back to the baseline. P.post holds until the next
|
||||||
|
// double tap, a lob he has to run down, or the point's end.
|
||||||
function moveCall() {
|
function moveCall() {
|
||||||
const goingIn = ball.approach === 'P' || (Math.abs(P.z - COURT.netZ) < TUNE.netFrom && ball.retreat !== 'P');
|
const goingIn = atNetHome(P);
|
||||||
|
P.post = goingIn ? 'base' : 'net';
|
||||||
ball.approach = goingIn ? null : 'P';
|
ball.approach = goingIn ? null : 'P';
|
||||||
ball.retreat = goingIn ? 'P' : null;
|
ball.retreat = goingIn ? 'P' : null;
|
||||||
popText(P.x, 0, P.z + TUNE.praiseBehind, goingIn ? 'BACK' : 'TO THE NET', UI.warm, true);
|
popText(P.x, 0, P.z + TUNE.praiseBehind, goingIn ? 'BACK' : 'TO THE NET', UI.warm, true);
|
||||||
@@ -2367,8 +2375,10 @@ export function update(dt) {
|
|||||||
// ...and lobbed at the net he backpedals, exactly as slowly as the CPU does (TUNE.lobTurn)
|
// ...and lobbed at the net he backpedals, exactly as slowly as the CPU does (TUNE.lobTurn)
|
||||||
// HIS OWN WAY IN AND OUT runs at full pace (the attack's follow-in, the lob's retreat), and while
|
// HIS OWN WAY IN AND OUT runs at full pace (the attack's follow-in, the lob's retreat), and while
|
||||||
// his attack flies he is RUSHING: the reply is planned as a net man's from wherever he has got to
|
// his attack flies he is RUSHING: the reply is planned as a net man's from wherever he has got to
|
||||||
if (ball.lastHit === 'P') P.rushing = ball.approach === 'P'; // a double tap behind his serve is serve and volley
|
// committed to the net he is a net man for every ball until he reaches it (a double tap behind his serve is serve and volley)
|
||||||
const ownRun = !chasing && rallyFeel() && ball.lastHit === 'P' && (ball.approach === 'P' || ball.retreat === 'P');
|
if (P.post === 'base' && Math.abs(P.z - COURT.netZ) > TUNE.netFrom + 0.5) P.post = null; // back: the old rules again (a short ball still pulls him in)
|
||||||
|
P.rushing = P.post ? P.post === 'net' : ball.lastHit === 'P' ? ball.approach === 'P' : P.rushing;
|
||||||
|
const ownRun = !chasing && rallyFeel() && (P.post || ball.lastHit === 'P' && (ball.approach === 'P' || ball.retreat === 'P'));
|
||||||
let rate = TUNE.playerSpeed * aK * dt * (chasing || ownRun ? 1 : 0.55) * (chasing && ball.rx && ball.rx.lob ? TUNE.lobTurn : 1);
|
let rate = TUNE.playerSpeed * aK * dt * (chasing || ownRun ? 1 : 0.55) * (chasing && ball.rx && ball.rx.lob ? TUNE.lobTurn : 1);
|
||||||
// ON HIS HEELS: a put-away of his own sitter is read late (step 4 (b)). Without this the auto-run
|
// ON HIS HEELS: a put-away of his own sitter is read late (step 4 (b)). Without this the auto-run
|
||||||
// started on the strike's frame with a perfect read and covered the whole court — no put-away ever
|
// started on the strike's frame with a perfect read and covered the whole court — no put-away ever
|
||||||
|
|||||||
+1
-1
@@ -207,7 +207,7 @@ export const other = s => s === 'P' ? 'O' : 'P';
|
|||||||
export const deuceSide = () => (score.pts[0] + score.pts[1]) % 2 === 0; // even points → deuce (right) side
|
export const deuceSide = () => (score.pts[0] + score.pts[1]) % 2 === 0; // even points → deuce (right) side
|
||||||
|
|
||||||
export function setupPoint() {
|
export function setupPoint() {
|
||||||
G.phase = 'pretoss'; ball.active = false; ball.bounces = 0; ball.scored = false; ball.rx = null; ball.heels = 0; P.chase = P.goal = P.goalKey = null; P.running = false; ball.approach = null; ball.retreat = null; ball.smash = false;
|
G.phase = 'pretoss'; ball.active = false; ball.bounces = 0; ball.scored = false; ball.rx = null; ball.heels = 0; P.chase = P.goal = P.goalKey = P.post = null; P.running = false; P.rushing = false; ball.approach = null; ball.retreat = null; ball.smash = false;
|
||||||
G.swingSnaps = null; G.rallyNear = null; G.swung = false;
|
G.swingSnaps = null; G.rallyNear = null; G.swung = false;
|
||||||
if (G.mode === 'splatter' && !G.match) { // always your serve, from wherever you're standing
|
if (G.mode === 'splatter' && !G.match) { // always your serve, from wherever you're standing
|
||||||
// (the MATCH falls through to the tennis branch below:
|
// (the MATCH falls through to the tennis branch below:
|
||||||
|
|||||||
@@ -1573,6 +1573,30 @@ test('THE DOUBLE TAP at the net is the way back; a long down-flick is only the l
|
|||||||
assert.ok(back.fromNet > TUNE.netFrom, `...and he is back behind the net zone: ${back.fromNet.toFixed(1)} m`);
|
assert.ok(back.fromNet > TUNE.netFrom, `...and he is back behind the net zone: ${back.fromNet.toFixed(1)} m`);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('THE DOUBLE TAP is a STANCE: a volley from mid-court keeps him coming in, not walking back', () => {
|
||||||
|
// Oliver on v376: "wenn man doppeltippt, geht er minimal vor". His own ball's flight covers 4-9 m of the 10,
|
||||||
|
// his next ball was met from mid-court — outside the net zone — and the recovery after it took him straight
|
||||||
|
// back to the baseline. P.post holds the net until the next double tap, a lob he runs down, or the point.
|
||||||
|
const assist = TUNE.autoAssist; TUNE.autoAssist = 1.15;
|
||||||
|
const after = post => {
|
||||||
|
freshSplatter(); startMatch(); G.interlude.introT = 0;
|
||||||
|
const rz = oppOf(); Object.assign(rz, { x: 3, z: -0.6, swingCool: 999, swingT: -1 });
|
||||||
|
G.phase = 'rally'; G.wasServe = false;
|
||||||
|
Object.assign(P, { x: 0, z: COURT.netZ + 9, swingT: -1 }); // his volley was struck 9 m out
|
||||||
|
P.post = post;
|
||||||
|
const v = solveShot(0, 1, P.z - 0.4, 1, 3, 18);
|
||||||
|
Object.assign(ball, { active: true, scored: false, lastHit: 'P', bounces: 0, rx: null, spin: 1, soft: false,
|
||||||
|
approach: null, retreat: null, x: 0, y: 1, z: P.z - 0.4, vx: v.vx, vy: v.vy, vz: v.vz, g: TUNE.gravity });
|
||||||
|
for (let i = 0; i < 60 && ball.active; i++) { rz.swingCool = 999; update(DT); }
|
||||||
|
return Math.abs(P.z - COURT.netZ);
|
||||||
|
};
|
||||||
|
try {
|
||||||
|
const committed = after('net'), loose = after(null);
|
||||||
|
assert.ok(committed < TUNE.netFrom, `committed to the net, he keeps coming in: ${committed.toFixed(1)} m`);
|
||||||
|
assert.ok(loose > 9, `without the stance the old recovery still walks him back: ${loose.toFixed(1)} m`);
|
||||||
|
} finally { TUNE.autoAssist = assist; }
|
||||||
|
});
|
||||||
|
|
||||||
test('an INCOMING ball keeps every tap a swing: the double tap never eats a shot', () => {
|
test('an INCOMING ball keeps every tap a swing: the double tap never eats a shot', () => {
|
||||||
freshSplatter(); startMatch(); G.interlude.introT = 0;
|
freshSplatter(); startMatch(); G.interlude.introT = 0;
|
||||||
oppOf().swingCool = 999;
|
oppOf().swingCool = 999;
|
||||||
|
|||||||
+7
-3
@@ -108,7 +108,7 @@ function humanAim() {
|
|||||||
const away = rz && rz.x > 0 ? -1 : 1; // the side he is not on
|
const away = rz && rz.x > 0 ? -1 : 1; // the side he is not on
|
||||||
if (oz < TUNE.netFrom) return r < 0.4 ? sw(0, 1, 150) : sw(away, -0.3, 120); // net man: lob him or pass him
|
if (oz < TUNE.netFrom) return r < 0.4 ? sw(0, 1, 150) : sw(away, -0.3, 120); // net man: lob him or pass him
|
||||||
if (oz > COURT.netZ + 0.5 && Math.abs(P.z - COURT.netZ) < COURT.netZ - 1.5 && r < 0.15) return sw(0, 1, 50); // drop
|
if (oz > COURT.netZ + 0.5 && Math.abs(P.z - COURT.netZ) < COURT.netZ - 1.5 && r < 0.15) return sw(0, 1, 50); // drop
|
||||||
if (ATTACK && Math.abs(P.z - COURT.netZ) >= TUNE.netFrom && Math.random() < ATTACK) { bot.dbl = true; return sw(away * 0.3, -1, 130); } // the attack
|
if (ATTACK && P.post !== 'net' && Math.abs(P.z - COURT.netZ) >= TUNE.netFrom && Math.random() < ATTACK) { bot.dbl = true; return sw(away * 0.3, -1, 130); } // the attack
|
||||||
if (r < 0.45) return sw(away, -0.2, 110); // to the open side
|
if (r < 0.45) return sw(away, -0.2, 110); // to the open side
|
||||||
if (r < 0.65) return sw(0, -1, 80); // deep through the middle
|
if (r < 0.65) return sw(0, -1, 80); // deep through the middle
|
||||||
return null; // a plain tap
|
return null; // a plain tap
|
||||||
@@ -181,8 +181,12 @@ for (const rung of RUNGS) {
|
|||||||
if (process.env.PROBE && lh === 'O' && bot.land && P.swingT >= 0) bot.land.sw = P.swingT, bot.land.d = Math.hypot(ball.x - P.x, (ball.z - P.z) * 0.8), bot.land.y = ball.y, bot.land.bb = ball.bounces;
|
if (process.env.PROBE && lh === 'O' && bot.land && P.swingT >= 0) bot.land.sw = P.swingT, bot.land.d = Math.hypot(ball.x - P.x, (ball.z - P.z) * 0.8), bot.land.y = ball.y, bot.land.bb = ball.bounces;
|
||||||
update(DT);
|
update(DT);
|
||||||
if (lh !== 'P' && ball.lastHit === 'P' && ball.active && ball.smash) { netx.smash++; netx.open = 'smash'; }
|
if (lh !== 'P' && ball.lastHit === 'P' && ball.active && ball.smash) { netx.smash++; netx.open = 'smash'; }
|
||||||
if (ball.lastHit === 'P' && ball.approach === 'P' && !netx.wasIn) netx.attacks++; // a run in (the double tap, or a short ball)
|
if (P.post === 'net' && !netx.wasIn) netx.attacks++; // a run in (the double tap, or a short ball)
|
||||||
netx.wasIn = ball.lastHit === 'P' && ball.approach === 'P';
|
netx.wasIn = P.post === 'net';
|
||||||
|
// every contact of his made while committed to the net: how far from the net (the "minimal vor" check)
|
||||||
|
if (lh === 'O' && ball.lastHit === 'P' && (P.post === 'net' || netx.postWas === 'net')) (netx.after ||= []).push(+Math.abs(P.z - COURT.netZ).toFixed(1));
|
||||||
|
if (netx.postWas === 'net' && P.post !== 'net') netx.dropped = (netx.dropped || 0) + 1, netx.why = [...(netx.why || []), (G.phase !== 'rally' ? 'pointEnd' : ball.rx && ball.rx.lob ? 'lob' : 'other')];
|
||||||
|
netx.postWas = P.post;
|
||||||
if (lh === 'P' && ball.lastHit === 'O' && ball.active && ball.soft && Math.abs(P.z - COURT.netZ) < TUNE.netFrom) netx.lobbed++;
|
if (lh === 'P' && ball.lastHit === 'O' && ball.active && ball.soft && Math.abs(P.z - COURT.netZ) < TUNE.netFrom) netx.lobbed++;
|
||||||
if (netx.open && (G.phase !== 'rally' || !ball.active)) { const d0 = score.pts[0] - netx.p0; if (d0 > 0) netx.smashWon++; netx.open = null; }
|
if (netx.open && (G.phase !== 'rally' || !ball.active)) { const d0 = score.pts[0] - netx.p0; if (d0 > 0) netx.smashWon++; netx.open = null; }
|
||||||
if (netx.open === null) netx.p0 = score.pts[0];
|
if (netx.open === null) netx.p0 = score.pts[0];
|
||||||
|
|||||||
Reference in new issue
Block a user