From bb72a01a3e9eeedf7eea8542a2475ac4a0d60b63 Mon Sep 17 00:00:00 2001 From: Oliver Rieger Date: Wed, 14 Oct 2026 20:15:25 +0200 Subject: [PATCH] Stop the auto-run shaking while it chases a ball Oliver: "wenn der Autoplay den Ball versucht zu bekommen, zittert der Player schnell hin und her". Measured: the run's target flipped by metres every other frame. intercept samples the flight every 10 ms against a 16.7 ms frame, so any candidate on a margin (a low bounce peaking on the 0.45 m floor, a point on the back boundary, a dying drop) passed one frame and failed the next. Frames with no candidate sent him home and back. 9-62 reversals inside 200 ms per five minutes. The chase now latches its plan: the flight is fixed at the strike, so the first point stays the answer while his legs still make it in time, net of any time he stood (split-step, heels). Reach is judged from where the chase began, and the target eases with a start/stop hysteresis. After: no reversals at any rung, timing unchanged. He wastes no steps, so he is a little stronger, mostly at the bottom of the ladder. New test; v375. Co-Authored-By: Claude Opus 5.5 --- PROJECT.md | 37 +++++++++++++++++++++++- index.html | 2 +- src/config.js | 4 +++ src/gameplay.js | 68 +++++++++++++++++++++++++++++++++++---------- src/state.js | 2 +- test.js | 37 +++++++++++++++++++++++- tools/rally-sim.mjs | 14 ++++++++++ 7 files changed, 145 insertions(+), 19 deletions(-) diff --git a/PROJECT.md b/PROJECT.md index bf90189..75a92d8 100644 --- a/PROJECT.md +++ b/PROJECT.md @@ -5676,6 +5676,39 @@ Lob vom Gegner als Volley annimmt, soll man derb schmettern"*. - `tools/rally-sim.mjs`: `ATTACK=0.3`, `NETX=1` (attacks, lobs faced at the net, smashes, smashes won), and `T_=` sets any TUNE dial for a sweep. +### The auto-run stops shaking (2026-10-14, v375) +Oliver: *"wenn der Autoplay den Ball versucht zu bekommen, zittert der Player schnell hin und her. bitte glätten"*. +**Measured first** (`tools/rally-sim.mjs JITTER=1`, `=2` prints target jumps, `=3` the reversals): +- the target jumped ~10 cm a frame (p90 0.2-0.4 m), and 15-25% of chase frames moved it more than 20 cm; +- there were 9-62 direction reversals inside 200 ms per five minutes of play. + +Smoothing the target alone (`runSmooth`) cut the small jumps 7x and fixed nothing. The shake was the PLAN +flipping by metres, every other frame, between two candidates: +- **The cause: margins against a sliding grid.** `intercept` samples the flight every 10 ms, the game steps + 16.7 ms, so the sample phase slides every frame. A candidate sitting on any margin passed one frame and + failed the next: + - a low slice whose bounce peaks at ~0.46 m against the 0.45 m back-court floor; + - a point on the back boundary (`rallyBehind`); + - a dying drop at the 0.35 m floor (frames with no candidate at all sent him home, 10 m, and back). + Judging reach from where he stood made it worse, since his own step moved the answer. Each margin patched + with hysteresis just moved the flip to the next margin. +- **The fix: the chase LATCHES its plan** (`P.chase.plan`). The flight is fixed when the ball is struck, so + the point chosen first stays the answer while his legs still make it in time, net of any time he stood + (split-step, a put-away's heels: `P.chase.lost`). It re-plans only when that fails. Reach is judged from + where the chase began plus the time he has run (`P.chase`). The margin hysteresis (`runKeep`, 6 cm of + height, 10 cm of back boundary, holding the plan on an empty frame) stays as a second line, and so does the + eased target and a start/stop hysteresis (`runStop` 0.12 m against the 0.35 m start). `setupPoint` + clears it all. +- **After:** 0 reversals at rungs 4/8/12/15/18. The timing sweep is unchanged (−30 ms q 0.91, kiss q 1.00). + The ring leads contact by 5-14 ms, which is frame quantisation. +- **Side effect: he no longer wastes steps, so he is a little stronger.** + - Climb (σ60, level 26): bottom rungs 94 → 99%, mid ladder about +5, top unchanged (Champion 33 → 30). + - Classic (σ60, level 1): easy ~80 → ~93, hard ~30 → ~37. + - Not retuned yet: put it to his phone first. +- Test: the ring-tapping player chasing at rungs 4/12/18 for a minute each makes at most 2 reversals inside + 200 ms. The old code makes 26. The early-tap test's floor moved 0.45 → 0.42: he now stops 0.12 m from his + mark instead of 0.35, so contact comes a hair later. The old curve still reads 0.33. + # OPEN — leftovers, in rough priority order Nothing here is broken; these are known gaps and unverified risks. @@ -5688,7 +5721,9 @@ Nothing here is broken; these are known gaps and unverified risks. through, put-aways, on his heels). Awaiting the phone verdict on v373, especially the half-second flat-footed read and the net rushers. See "Step 4 (b)" above. v374 adds the player's own net game: long up-flick = attack and follow in, long down-flick = lob and - go back, and the SMASH. See "The player's way to the net and back" above. Fallback tag `rally-baseline-v360`. + 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 + of the Climb and classic easy may now be too easy. See "The auto-run stops shaking". Fallback tag `rally-baseline-v360`. 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. diff --git a/index.html b/index.html index 5efbaf7..03cb406 100644 --- a/index.html +++ b/index.html @@ -1140,6 +1140,6 @@ if (location.hostname !== 'splatter-tennis.wuwi.one' && !window.Capacitor?.isNat - + diff --git a/src/config.js b/src/config.js index b17f4ae..a7ec1ce 100644 --- a/src/config.js +++ b/src/config.js @@ -115,6 +115,10 @@ export const TUNE = { // but under the 2.8 m the opponent's lob clears a net man by, or he smashes it standing) smashPace: 0.8, // ...extra pace on top of the forward flick's, scaled by timing smashRead: 0.3, + runSmooth: 10, // 1/s: the auto-run's target eases toward each fresh plan instead of jumping to it + runSnap: 1.5, // m: a re-plan this far away (a new shot, the lob read) is taken at once + runStop: 0.12, + runKeep: 0.06, // s of reach slack intercept grants the point he is already running to (hysteresis) // m: a running auto-run stops this close to its target (it starts at 0.35) smashRun: 0.25, // share of his legs a lobbed net man may need to get UNDER the lob for the smash — // a short lob is smashed, a deep one is not (he turns and runs it down, lobTurn) // s the opponent stands reading a smash before he moves softRead: 0.35, // s any opponent stands reading a soft ball (drop, lob) — they are disguised diff --git a/src/gameplay.js b/src/gameplay.js index bd4f48b..8b47a4b 100644 --- a/src/gameplay.js +++ b/src/gameplay.js @@ -1528,6 +1528,16 @@ function intercept(side) { // 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; + // ...and the PLAYER's reach is judged from where his chase BEGAN plus the time he has run since (P.chase), + // not from where he stands this frame. Judged live, his own step moved the answer: a deep point became + // reachable one frame and not the next, and the plan flipped 2.2 m every other frame (the shake). + const o = side === 'P' && P.chase ? P.chase : { x: ch.x, z: ch.z, t: 0 }; + // ...with HYSTERESIS: the flight is sampled every 10 ms against a 16.7 ms frame, so a candidate on a + // margin passed one frame and failed the next as the grid slid — a low slice whose bounce peaks at ~0.46 m + // against the 0.45 m floor below, measured flipping the plan 2.2 m every other frame. The point he is + // already running to keeps runKeep of time and 6 cm of height slack. + const near = (px, pz) => side === 'P' && P.goal && Math.hypot(px - P.goal.x, pz - P.goal.z) < 0.6; + const can = (px, pz, i, k = 1) => Math.hypot(px - o.x, pz - o.z) / (spd * k) <= o.t + i * dt + (near(px, pz) ? TUNE.runKeep : 0); let fallback = null, deepest = null, anyHit = null, rise = null, nb = ball.bounces; // ...but the PLAYER lobbed at the net first looks for the overhead: the first point the lob comes down // into his smash band (smashMaxY) that his backpedal reaches in time. A short lob is smashed; one he @@ -1540,17 +1550,17 @@ function intercept(side) { vy -= g * dt; x += vx * dt; y += vy * dt; z += vz * dt; if (y <= 0 && vy < 0) { y = 0; vy = -vy * bY(); vx *= bXZ(); vz *= bXZ(); nb++; g = gAfter(g); } if (smashing && nb === 0 && vy < 0 && y <= TUNE.smashMaxY - 0.2 && y >= TUNE.smashMinY && Math.abs(x) <= xl - && Math.hypot(x - ch.x, z - ch.z) / (spd * TUNE.smashRun) <= i * dt) return (plan.over = { x, z }); + && can(x, z, i, TUNE.smashRun)) return (plan.over = { x, z, ti: o.t + i * dt }); // FROM THE BACK: meet it between its bounce and the next one — at the top or falling through // rallyHit+0.3, from as far as rallyBehind behind the line; a short ball the first moment it is // up off the grass. Only if nothing like that is reachable do the old candidates below apply. // (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) && 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 }; + if (back && nb === 1 && i > 5 && y > (near(x, z) ? 0.39 : 0.45) && y < (plan.short ? TUNE.rallyHit + 0.6 : TUNE.rallyTop)) { + const inB = (side === 'P' ? z <= COURT.L + TUNE.rallyBehind + (near(x, z) ? 0.1 : 0) : z >= -TUNE.rallyBehind) && Math.abs(x) <= xl; + if (inB && can(x, z, i)) { + if (plan.short || vy < 1) return { x, z, ti: o.t + i * dt }; + rise ||= { x, z, ti: o.t + i * dt }; } } // THE SHORT BALL (match): a slow return can bounce and DIE at z 18-20, which never enters the @@ -1559,21 +1569,24 @@ function intercept(side) { // losses. Remember the earliest reachable point ON HIS HALF where the ball is still at hitting // 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.abs(x) <= xl && Math.hypot(pt.x - ch.x, pt.z - ch.z) / spd <= i * dt) anyHit = pt; + const pt = { x, z: Math.min(24.5, z), ti: o.t + i * dt }; + if (Math.abs(x) <= xl && can(pt.x, pt.z, i)) 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.abs(x) <= xl && Math.hypot(pt.x - ch.x, pt.z - ch.z) / spd <= i * dt; + const pt = { x, z: side === 'O' ? Math.max(0.8, z) : Math.min(24.5, z), ti: o.t + i * dt }; + const reachable = Math.abs(x) <= xl && can(pt.x, pt.z, i); 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 } } if (smashing) plan.over = null; // no overhead in reach: he runs it down - const to = rise || fallback || deepest || anyHit || home; // zone points first: meeting deep is still better + // ...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 + const hold = side === 'P' && P.chase && P.goal ? P.goal : null; + const to = rise || fallback || deepest || anyHit || hold || 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; + return Math.abs(to.x) > xl ? { x: Math.sign(to.x) * xl, z: to.z, ti: to.ti } : to; } // THE GOLD RING, TIMED (2026-10-14). The approach ring used to go gold at a DISTANCE (matchRingSweet @@ -2346,7 +2359,11 @@ export function update(dt) { // 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 // won a point, and the CPU still had no way to take one - if (chasing && ball.heels > 0) { ball.heels -= dt; rate = 0; } + const key = ball.lastHit + (chasing ? 'c' : 'r'); + if (!chasing) P.chase = null; + else if (!P.chase || P.goalKey !== key) P.chase = { x: P.x, z: P.z, t: 0, lost: 0, plan: null }; // where this chase began + if (chasing && ball.heels > 0) { ball.heels -= dt; rate = 0; if (P.chase) P.chase.lost += dt; } + else if (P.chase) P.chase.t += dt; // the time he has had to run, for intercept's reach // Run to the ball — then recover. WHERE you recover to is mode-specific and each rule exists // for a reason: // GORE SPLATTER recovers in DEPTH ONLY, holding your lane — recovering to HOME.P's x undid @@ -2358,12 +2375,33 @@ export function update(dt) { // unreachable": one wide return pulled you off centre and you STAYED there, so the next // ball to the opposite corner was lost before it was struck. Not a strong CPU — a player // who never walked back. - const tgt = chasing ? intercept('P') + // ...and the plan is LATCHED for the chase: the flight is fixed the moment the ball is struck, so the + // point chosen first stays the answer while his legs still make it in time (net of any time he stood: + // a split-step, a put-away's heels). Re-asked every frame, every margin in intercept — a bounce that + // peaks on a height floor, a point on the back boundary — flipped the plan by metres as the 10 ms + // sampling grid slid against the frame: the shake Oliver saw. Hysteresis there only moved the margin. + const c = P.chase, spdP = TUNE.playerSpeed * aK; + const latched = c && c.plan && c.plan.ti !== undefined + && Math.hypot(c.plan.x - c.x, c.plan.z - c.z) / spdP <= c.plan.ti - (c.lost - c.plan.lost0) + 0.02; + const raw = chasing ? (latched ? c.plan : (c ? (c.plan = { ...intercept('P'), lost0: c.lost }) : intercept('P'))) : G.mode === 'splatter' && !G.match ? { x: P.x, z: HOME.P.z } : rallyFeel() ? rallyHome(P) : HOME.P; + // THE RUN, SMOOTHED (2026-10-14, Oliver: "wenn der Autoplay den Ball versucht zu bekommen, zittert der + // Player schnell hin und her"). intercept asks every frame for the EARLIEST point he reaches in time, + // and his own step moves that point: the target jumped ~10 cm a frame (p90 0.3 m) and flipped across + // him, start/stop at the deadzone's edge, dozens of reversals a minute. It now EASES toward each plan + // (runSmooth); a new shot or a re-plan further than runSnap (the lob read) is still taken at once. + const jump = P.goal ? Math.hypot(raw.x - P.goal.x, raw.z - P.goal.z) : Infinity; + if (!P.goal || P.goalKey !== key || jump > TUNE.runSnap) P.goal = { x: raw.x, z: raw.z }; + else { const k = Math.min(1, dt * TUNE.runSmooth); P.goal = { x: P.goal.x + (raw.x - P.goal.x) * k, z: P.goal.z + (raw.z - P.goal.z) * k }; } + P.goalKey = key; + const tgt = P.goal; P.tgt = tgt; // the timed gold ring (ringCue) predicts the contact from where this run ends const dx = tgt.x - P.x, dz = tgt.z - P.z, d = Math.hypot(dx, dz); - if (d > 0.35) { // deadzone: stand ready near the spot instead of dithering over it + // deadzone WITH HYSTERESIS: start at 0.35, and once running, carry on to runStop — a target hovering + // at the edge no longer toggles him between a step and a stand every other frame + P.running = d > (P.running ? TUNE.runStop : 0.35); + if (P.running) { // deadzone: stand ready near the spot instead of dithering over it P.x += dx / d * Math.min(rate, d); P.z += dz / d * Math.min(rate, d); assist = dx; diff --git a/src/state.js b/src/state.js index 7d8e2dc..3152cd2 100644 --- a/src/state.js +++ b/src/state.js @@ -206,7 +206,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 function setupPoint() { - G.phase = 'pretoss'; ball.active = false; ball.bounces = 0; ball.scored = false; ball.rx = null; ball.heels = 0; 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 = null; P.running = false; ball.approach = null; ball.retreat = null; ball.smash = false; G.swingSnaps = null; G.rallyNear = null; G.swung = false; if (G.mode === 'splatter' && !G.match) { // always your serve, from wherever you're standing // (the MATCH falls through to the tennis branch below: diff --git a/test.js b/test.js index eef3c90..b5b169d 100644 --- a/test.js +++ b/test.js @@ -1146,6 +1146,41 @@ test('a SHORT ball is an approach, a deep one keeps the baseliner back, and nobo } finally { TUNE.matchFlub = flub; } }); +test('the auto-run does not SHAKE: no back-and-forth while it chases a ball', () => { + // Oliver, 2026-10-14: "wenn der Autoplay den Ball versucht zu bekommen, zittert der Player schnell hin und + // her". intercept re-planned every frame and its margins (a bounce peaking on a height floor, a point on + // the back boundary) flipped the target by metres as the 10 ms flight grid slid against the frame: + // 9-62 reversals inside 200 ms per five minutes. The chase now latches its plan. Ring-tapping play. + const assist = TUNE.autoAssist; TUNE.autoAssist = 1.15; + try { + let fast = 0, chases = 0; + for (const opp of [4, 12, 18]) { + startMatch(); G.interlude.introT = 0; G.match.opp = opp; + for (let i = 0; i < 2.2 * 60; i++) update(DT); + let last = 0, lastRev = -99, armed = false; + for (let i = 0; i < 60 * 60; i++) { + G.modal = null; G.modalQ.length = 0; G.hp = G.hpMax; G.match.opp = opp; + 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 && ringCue()?.gold) { armed = true; keyPressed('Space'); } + if (ball.lastHit !== 'O') armed = false; + const x0 = P.x, chasing = G.phase === 'rally' && ball.active && ball.lastHit === 'O'; + update(DT); + if (!chasing) { last = 0; continue; } + chases++; + const dx = P.x - x0; + if (Math.abs(dx) > 1e-4) { + if (last && Math.sign(dx) !== Math.sign(last)) { if (i - lastRev < 12) fast++; lastRev = i; } + last = dx; + } + } + endMatch(); + } + assert.ok(chases > 3000, 'a real sample of chasing: ' + chases + ' frames'); + assert.ok(fast <= 2, `no shake: ${fast} direction reversals inside 200 ms`); + } finally { TUNE.autoAssist = assist; } +}); + 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 @@ -1270,7 +1305,7 @@ test('a groundstroke a little off PERFECT is a little worse, not a weak ball (no } assert.ok(incoming >= 20, 'a real sample: ' + incoming); assert.ok(returned / incoming >= 0.9, `an early-ish tap still returns: ${returned}/${incoming}`); - assert.ok(sumQ / returned >= 0.45, 'and still returns a real ball (0.32 on the old curve): mean q ' + (sumQ / returned).toFixed(2)); + assert.ok(sumQ / returned >= 0.42, 'and still returns a real ball (0.33 on the old curve): mean q ' + (sumQ / returned).toFixed(2)); } finally { TUNE.autoAssist = assist; } }); diff --git a/tools/rally-sim.mjs b/tools/rally-sim.mjs index 6ef7875..f61f1fd 100644 --- a/tools/rally-sim.mjs +++ b/tools/rally-sim.mjs @@ -6,6 +6,7 @@ // Since step 4: BOT=human (default; JIT, BIAS ms), CLIMB=1, XP, WHY=1, PROBE=1, LOG=1|all, SUMMARY=1, // JSON=1 — see PROJECT.md "Step 4, measured first". Pool 8 seeds before reading a win rate. // ATTACK=0.3 (the long up-flick share), NETX=1 (net/smash tally), KILL=1 (put-aways), T_= sets TUNE. +// JITTER=1 (auto-run shake: reversals, target jumps), =2 prints big jumps, =3 prints fast reversals. // ponytail: the platform stubs are copied from test.js; fold this into a test once the rework lands. let _seed = (Number(process.env.SEED) || 20260811) >>> 0; Math.random = () => { // xorshift32 — small, fast, good enough for jitter @@ -96,6 +97,7 @@ const ATTACK = Number(process.env.ATTACK || 0); // share of baseline shots playe const JIT = Number(process.env.JIT ?? 60) / 1000, LAG = 7; const gauss = () => Math.sqrt(-2 * Math.log(1 - Math.random())) * Math.cos(2 * Math.PI * Math.random()); const netx = { attacks: 0, smash: 0, smashWon: 0, lobbed: 0, open: null, p0: 0 }; +const jit = { shown: 0, rev: 0, fastRev: 0, lastRev: -99, last: 0, jumps: [], frames: 0 }; const probe = [], kills = []; let kOpen = null, kpow = 1; const bot = { armed: false, fire: -1, jit: null, swipe: null }; const sw = (dx, dy, mag) => { const n = Math.hypot(dx, dy) || 1, fx = dx / n, fy = dy / n; @@ -173,6 +175,7 @@ for (const rung of RUNGS) { } } const lh = ball.lastHit; + const px0 = P.x, pz0 = P.z, tg0 = P.tgt; 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); if (lh !== 'P' && ball.lastHit === 'P' && ball.active) { if (ball.approach === 'P' && !G.wasServe) netx.attacks++; if (ball.smash) { netx.smash++; netx.open = 'smash'; } } @@ -183,6 +186,15 @@ for (const rung of RUNGS) { if (ball.lastHit !== 'O') G.swipe = null; // KILL=1: every put-away (a q >= .95 CPU ball straight after a player ball under .5) — where it went, // how far the player had to go, how long he had, and whether he got it back + // JITTER=1: the auto-run's shake — a direction reversal of the x step inside a chase, and how far the + // target jumps between frames (the two things a "zittert hin und her" can be made of) + if (process.env.JITTER && G.phase === 'rally' && ball.active && ball.lastHit === 'O' && lh === 'O') { + const dx = P.x - px0; + if (Math.abs(dx) > 1e-4) { if (jit.last && Math.sign(dx) !== Math.sign(jit.last)) { jit.rev++; if (i - jit.lastRev < 12) { jit.fastRev++; if (process.env.JITTER === '3' && jit.shown++ < 12) console.log('rev', i, 'dx', dx.toFixed(3), 'prev', jit.last.toFixed(3), 'P', P.x.toFixed(2), P.z.toFixed(2), 'tgt', P.tgt && P.tgt.x.toFixed(2), P.tgt && P.tgt.z.toFixed(2), 'swingT', P.swingT.toFixed(2), 'heels', ball.heels.toFixed(2)); } jit.lastRev = i; } jit.last = dx; } + if (tg0 && P.tgt) { const j = Math.hypot(P.tgt.x - tg0.x, P.tgt.z - tg0.z); jit.jumps.push(j); } + if (process.env.JITTER === '2' && tg0 && P.tgt && Math.hypot(P.tgt.x - tg0.x, P.tgt.z - tg0.z) > 0.5 && jit.shown++ < 25) console.log('jump', i, 'from', tg0.x.toFixed(2), tg0.z.toFixed(2), 'to', P.tgt.x.toFixed(2), P.tgt.z.toFixed(2), 'P', P.x.toFixed(2), P.z.toFixed(2), 'ball', ball.y.toFixed(2), ball.z.toFixed(2), 'b', ball.bounces, JSON.stringify(ball.rx)); + jit.frames++; + } else jit.last = 0; if (process.env.KILL) { if (lh === 'P' && ball.lastHit === 'O' && kpow < 0.5 && ball.pow >= 0.95) { const L = gp.predictLanding(); kills.push({ px: +P.x.toFixed(1), pz: +P.z.toFixed(1), lx: L && +L.x.toFixed(1), lz: L && +L.z.toFixed(1), tl: L && +L.t.toFixed(2), v: +Math.hypot(ball.vx, ball.vz).toFixed(1), got: null }); kOpen = kills[kills.length - 1]; @@ -241,6 +253,8 @@ for (const rung of RUNGS) { } if (!process.env.SUMMARY) { console.table(rows); console.table(land); } if (process.env.NETX) console.log(JSON.stringify(netx)); +if (process.env.JITTER) { const j = jit.jumps.sort((a, b) => a - b), q = f => +(j[Math.floor(j.length * f)] || 0).toFixed(3); + console.log(JSON.stringify({ chaseFrames: jit.frames, reversals: jit.rev, fastReversals_lt200ms: jit.fastRev, tgtJump_p50: q(0.5), p90: q(0.9), p99: q(0.99), max: q(0.9999), over20cm: j.filter(x => x > 0.2).length })); } if (process.env.KILL) { console.table(kills.slice(0, 30)); const l = kills.filter(k => k.got === 'LOST').length; console.log('put-aways', kills.length, 'won by CPU', l); } if (process.env.PROBE) { const l = probe.map(p => p.late).sort((a, b) => a - b); console.log("contact after predicted entry, ms: n", l.length, "mean", (l.reduce((a, b) => a + b, 0) / l.length).toFixed(1), "p10", l[l.length * 0.1 | 0], "p50", l[l.length / 2 | 0], "p90", l[l.length * 0.9 | 0]); } if (process.env.JSON) { console.log(JSON.stringify(pts)); process.exit(0); }