Let an early tap be a weak ball, and let the CPU put a sitter away
Step 4 (b), Oliver's pick. v372 measured why the ladder could not climb: nearly every lost point was the player's own early whiff, the CPU barely missed, and the auto-run reached everything, so no opponent dial decided a point. - The follow-through: a groundstroke swing meets a bounced ball up to 60 ms after its window at q 0.05. The early whiff edge goes from ~70 ms back to ~130 ms, where the volley window had it before step 1. - A sitter (a player ball under q 0.5) is put away with a chance that is the ladder (0.2 at the bottom of the Climb, 0.85 at the Champion, by difficulty in classic): clean, at the line, bounded by the rung's width. - The player is on his heels for 0.5 s after a put-away of his own sitter. Without it the auto-run covered the whole court and no put-away won. - Fixed a step-2 regression this surfaced: the drop's dig-out cap fired on every slice drive, so every low contact against the slicer was a sitter. Measured at level 26: the Climb now slopes from 95% of points at the Shambler to 28-39% at the Champion. The net rushers are still easier than their neighbours. The log names put-aways. Three new tests; v373. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
5dd5d71c33
commit
4406aeafdb
8 files changed
+231
-31
No files matched your search
@@ -911,7 +911,7 @@ test('league 5 has no speed dip below league 4, including serve and wrong-footed
|
||||
G.match.opp = n; G.match.surprised = surprised; G.match.expected = false;
|
||||
zb.foe = cfg.matchFoe(n); zb.x = -3; zb.z = 2; zb.swingCool = 999;
|
||||
G.phase = 'rally'; G.wasServe = serve;
|
||||
Object.assign(ball, { active: true, lastHit: 'P', bounces: 0,
|
||||
Object.assign(ball, { active: true, lastHit: 'P', pow: 1, bounces: 0,
|
||||
x: 3, z: 5, y: 2, vx: 0, vy: 4, vz: -8 });
|
||||
update(DT);
|
||||
return Math.hypot(zb.vx, zb.vz);
|
||||
@@ -946,7 +946,7 @@ test('league 5 sustains strong returns and hunts the open court; league 4 stays
|
||||
G.match.surprised = G.match.expected = false;
|
||||
P.x = i % 2 ? 3 : -3; P.z = 23; 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,
|
||||
Object.assign(ball, { active: true, lastHit: 'P', bounces: 1, pow: 1, // a good ball: no put-away
|
||||
x: 0, z: 2.4, y: 1.2, vx: 0, vy: 0, vz: -1 });
|
||||
update(DT);
|
||||
if (ball.lastHit !== 'O') continue; // actual flubs remain part of the opponent
|
||||
@@ -1012,7 +1012,7 @@ test('match low contact: scoop rising balls, reject falling balls and contact ac
|
||||
const rz = oppOf();
|
||||
Object.assign(rz, { x: 0, z: Math.min(z, COURT.netZ - 1.2), swingT: -1, swingCool: 0 });
|
||||
G.phase = 'rally'; G.wasServe = false;
|
||||
Object.assign(ball, { active: true, lastHit: 'P', bounces: 1,
|
||||
Object.assign(ball, { active: true, lastHit: 'P', pow: 1, bounces: 1,
|
||||
x: 0, y, z, vx: 0, vy, vz: -1, g: TUNE.gravity });
|
||||
update(DT);
|
||||
assert.equal(ball.lastHit === 'O', returns, `contact at z=${z}, y=${y}, vy=${vy}`);
|
||||
@@ -1076,7 +1076,7 @@ test('THE COURT HAS ITS LENGTH: a match rally is played from the baselines, not
|
||||
oppOf().foe = cfg.matchFoe(11); // the BODY plays the style, not the rung number
|
||||
for (let i = 0; i < 2.2 * 60; i++) update(DT);
|
||||
const hits = [];
|
||||
let prev = ball.lastHit;
|
||||
let prev = ball.lastHit, putAway = false;
|
||||
for (let i = 0; i < 120 * 60; i++) {
|
||||
G.modal = null; G.modalQ.length = 0; G.hp = G.hpMax; G.match.opp = 11; // keep playing: the shape is the subject
|
||||
if (G.phase === 'pretoss' && !(G.interlude && G.interlude.introT > 0)) { if (i % 40 === 0) keyPressed('Space'); }
|
||||
@@ -1085,11 +1085,14 @@ test('THE COURT HAS ITS LENGTH: a match rally is played from the baselines, not
|
||||
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;
|
||||
const b0 = ball.bounces, kill = putAway;
|
||||
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 });
|
||||
// the answer to a PUT-AWAY (step 4) is a defence, not the rally's shape: a man caught on his heels
|
||||
// takes it where he can, in the air included
|
||||
if (ch) hits.push({ back: Math.abs(ch.z - COURT.netZ) >= TUNE.netFrom, volley: b0 === 0, t: i * DT, kill });
|
||||
putAway = ball.heels > 0;
|
||||
}
|
||||
prev = ball.active ? ball.lastHit : null;
|
||||
}
|
||||
@@ -1098,7 +1101,7 @@ test('THE COURT HAS ITS LENGTH: a match rally is played from the baselines, not
|
||||
// the autopilot mistimes into short balls and a body is MEANT to come in on those, so the share of
|
||||
// net contacts follows its timing; the back-court volley share below is the guard against the old T
|
||||
assert.ok(back.length / hits.length >= 0.5, `most of the rally is played from the back: ${back.length}/${hits.length}`);
|
||||
const volleys = back.filter(h => h.volley).length;
|
||||
const volleys = back.filter(h => h.volley && !h.kill).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)
|
||||
@@ -1143,6 +1146,105 @@ test('a SHORT ball is an approach, a deep one keeps the baseliner back, and nobo
|
||||
} finally { TUNE.matchFlub = flub; }
|
||||
});
|
||||
|
||||
test('THE FOLLOW-THROUGH: a tap ~80 ms early on a baseline ball is met weakly, not whiffed', () => {
|
||||
// Step 4 (b), 2026-10-14. An early tap starts the swing while the ball is in the air, the swing waits
|
||||
// for the bounce, and the groundstroke window had run out by then: a whiff at ~70 ms early, while a
|
||||
// tap 120 ms LATE still met it. swingTail lets the swing meet it at q 0.05 — a sitter, not a miss.
|
||||
const assist = TUNE.autoAssist; TUNE.autoAssist = 1.15;
|
||||
const run = () => {
|
||||
startMatch(); G.interlude.introT = 0; G.match.opp = 6;
|
||||
for (let i = 0; i < 2.2 * 60; i++) update(DT);
|
||||
let armed = false, incoming = 0, met = 0;
|
||||
for (let i = 0; i < 90 * 60; i++) {
|
||||
G.modal = null; G.modalQ.length = 0; G.hp = G.hpMax; G.match.opp = 6;
|
||||
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' && ball.vz > 0 && !armed) {
|
||||
const c = ringCue();
|
||||
if (c && c.eta - c.sweet <= 5 / 60 + 1e-6 && c.eta - c.sweet > 4 / 60) { armed = true; incoming++; keyPressed('Space'); }
|
||||
}
|
||||
const before = ball.lastHit;
|
||||
update(DT);
|
||||
if (armed && before === 'O' && ball.lastHit === 'P') { met++; armed = false; }
|
||||
else if (armed && (!ball.active || G.phase !== 'rally')) armed = false;
|
||||
}
|
||||
endMatch();
|
||||
return { incoming, met };
|
||||
};
|
||||
const tail = TUNE.swingTail;
|
||||
try {
|
||||
const on = run();
|
||||
assert.ok(on.incoming >= 20, 'a real sample: ' + on.incoming);
|
||||
assert.ok(on.met / on.incoming >= 0.8, `an ~80 ms early tap is met: ${on.met}/${on.incoming}`);
|
||||
TUNE.swingTail = 0;
|
||||
const off = run();
|
||||
assert.ok(off.met / off.incoming < 0.6, `...where the old edge whiffed it: ${off.met}/${off.incoming}`);
|
||||
} finally { TUNE.autoAssist = assist; TUNE.swingTail = tail; }
|
||||
});
|
||||
|
||||
test('THE SITTER IS PUNISHED: the put-away chance climbs the ladder, goes for the line, and finds him on his heels', () => {
|
||||
// Step 4 (b): before it, the CPU had no way to win a point — a q 0.05 floater came back like a drive
|
||||
// and every point it took was the player's own whiff, which no opponent dial touches.
|
||||
startMatch(); G.interlude.introT = 0;
|
||||
const zb = oppOf();
|
||||
const share = (n, pow) => {
|
||||
G.match.opp = n; zb.foe = cfg.matchFoe(n);
|
||||
let hits = 0, kills = 0, wide = 0, heels = 0;
|
||||
for (let i = 0; i < 1200; i++) {
|
||||
G.phase = 'rally'; G.wasServe = false;
|
||||
G.match.surprised = G.match.expected = false;
|
||||
P.x = 0; P.z = 23; 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, pow, rx: null, spin: 1, soft: false,
|
||||
x: 0, z: 2.4, y: 1.2, vx: 0, vy: 0, vz: -1 });
|
||||
update(DT);
|
||||
if (ball.lastHit !== 'O') continue;
|
||||
hits++;
|
||||
if (ball.heels > 0) {
|
||||
kills++; heels++;
|
||||
const t = (ball.vy + Math.sqrt(ball.vy * ball.vy + 2 * ball.g * ball.y)) / ball.g;
|
||||
if (Math.abs(ball.x + ball.vx * t) > 2.9) wide++;
|
||||
assert.ok(ball.pow >= TUNE.punishQ, 'a put-away is struck clean');
|
||||
}
|
||||
ball.active = false;
|
||||
}
|
||||
return { hits, kills: kills / hits, wide: kills ? wide / kills : 0 };
|
||||
};
|
||||
const champ = share(cfg.LADDER, 0.1), first = share(1, 0.1), good = share(cfg.LADDER, 0.9);
|
||||
assert.ok(Math.abs(champ.kills - TUNE.punishMax) < 0.06, `the champion puts a sitter away ${champ.kills.toFixed(2)}`);
|
||||
assert.ok(first.kills < champ.kills - 0.3 && first.kills > 0.1, `the first rung does it rarely ${first.kills.toFixed(2)}`);
|
||||
assert.equal(good.kills, 0, 'a good ball is never put away');
|
||||
assert.ok(champ.wide > 0.9, `the champion's put-away goes for the line ${champ.wide.toFixed(2)}`);
|
||||
endMatch();
|
||||
});
|
||||
|
||||
test('a low SLICE drive is not a dug-out drop: the scoop cap is for soft backspin balls only', () => {
|
||||
// Step 2's slicer hits every rally ball at spin 0.8, and the drop's dig-out cap read `spin < 1`, so every
|
||||
// low contact against him came back at lowVolleyQ — found when step 4 started punishing sitters.
|
||||
startMatch(); G.interlude.introT = 0;
|
||||
const zb = oppOf(); G.match.opp = 6; zb.foe = cfg.matchFoe(6);
|
||||
const flub = TUNE.matchFlub; TUNE.matchFlub = 0;
|
||||
try {
|
||||
for (const soft of [false, true]) {
|
||||
let n = 0, capped = 0;
|
||||
for (let i = 0; i < 300; i++) {
|
||||
G.phase = 'rally'; G.wasServe = false; G.match.surprised = G.match.expected = false;
|
||||
P.x = 0; P.z = 23; 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, pow: 1, rx: null, spin: 0.8, soft,
|
||||
x: 0, z: 2.4, y: 0.3, vx: 0, vy: 0.5, vz: -1 });
|
||||
update(DT);
|
||||
if (ball.lastHit !== 'O') continue;
|
||||
n++; if (ball.pow <= TUNE.lowVolleyQ + 1e-9) capped++;
|
||||
ball.active = false;
|
||||
}
|
||||
assert.ok(n > 200, 'returns: ' + n);
|
||||
if (soft) assert.equal(capped, n, 'a dug-out drop is still a weak reply');
|
||||
else assert.equal(capped, 0, 'a low slice drive is an ordinary ball');
|
||||
}
|
||||
} finally { TUNE.matchFlub = flub; endMatch(); }
|
||||
});
|
||||
|
||||
test('a groundstroke a little off PERFECT is a little worse, not a weak ball (no cliff past perfectMs)', () => {
|
||||
// Step 4's measurement (2026-10-14): q was 1 inside perfectMs and 1 - off/half outside it, so a
|
||||
// groundstroke (half-window ~80 ms) fell from 1 to 0.43 a millisecond past PERFECT, and a tap 30 ms
|
||||
@@ -1302,7 +1404,7 @@ function styleReturns(rung, pz, aiZ = 2) {
|
||||
G.phase = 'rally'; G.wasServe = false; G.match.surprised = G.match.expected = false;
|
||||
P.x = 0; P.z = pz; P.swingT = -1;
|
||||
zb.x = 0; zb.z = aiZ; zb.swingT = -1; zb.swingCool = 0; zb.serveHold = 0;
|
||||
Object.assign(ball, { active: true, lastHit: 'P', bounces: 1, rx: null, spin: 1, soft: false,
|
||||
Object.assign(ball, { active: true, lastHit: 'P', bounces: 1, rx: null, spin: 1, soft: false, pow: 1, // no sitter: no put-away
|
||||
x: 0, z: aiZ + 0.4, y: 1.2, vx: 0, vy: 0, vz: -1 });
|
||||
update(DT);
|
||||
if (ball.lastHit !== 'O') continue;
|
||||
@@ -1505,7 +1607,7 @@ test('rally depth: deep by default, mistiming lands it SHORTER, never longer', (
|
||||
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,
|
||||
Object.assign(ball, { active: true, lastHit: 'P', pow: 1, bounces: 1, rx: null,
|
||||
x: 0, z: 2.4, y: 1.2, vx: 0, vy: 0, vz: -1 });
|
||||
update(DT);
|
||||
if (ball.lastHit !== 'O' || ball.soft) continue; // a style's moonball has its own depth (and tests)
|
||||
@@ -1532,7 +1634,7 @@ test('The Climb opens briskly, converges at opponent eight, and leaves cup and t
|
||||
G.match.surprised = G.match.expected = false;
|
||||
zb.foe = cfg.matchFoe(n); zb.x = -3; zb.z = 2; zb.swingCool = 999;
|
||||
G.phase = 'rally'; G.wasServe = false;
|
||||
Object.assign(ball, { active: true, lastHit: 'P', bounces: 0,
|
||||
Object.assign(ball, { active: true, lastHit: 'P', pow: 1, bounces: 0,
|
||||
x: 3, z: 5, y: 2, vx: 0, vy: 4, vz: -8 });
|
||||
update(DT);
|
||||
const chase = Math.hypot(zb.vx, zb.vz);
|
||||
@@ -1543,7 +1645,7 @@ test('The Climb opens briskly, converges at opponent eight, and leaves cup and t
|
||||
G.phase = 'rally'; G.wasServe = false;
|
||||
P.x = 0; P.z = 23; 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,
|
||||
Object.assign(ball, { active: true, lastHit: 'P', pow: 1, bounces: 1,
|
||||
x: 0, z: 2.4, y: 1.2, vx: 0, vy: 0, vz: -1 });
|
||||
update(DT);
|
||||
if (ball.lastHit !== 'O') continue;
|
||||
|
||||
Reference in new issue
Block a user