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:
wuwiandClaude Opus 5.5 committed 2026-10-14 20:40:56 +02:00
1 parent 48d1069e40
commit ba8d13a867
6 files changed
+72 -11

No files matched your search

+24 -1
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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:
+24
View File
@@ -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
View File
@@ -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];