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 <noreply@anthropic.com>
This commit is contained in:
1 parent
599d5b0506
commit
bb72a01a3e
7 files changed
+145
-19
No files matched your search
+36
-1
@@ -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_<knob>=<value>` 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.
|
||||
|
||||
+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=374"></script>
|
||||
<script type="module" src="src/main.js?v=375"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -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
|
||||
|
||||
+53
-15
@@ -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;
|
||||
|
||||
+1
-1
@@ -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:
|
||||
|
||||
@@ -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; }
|
||||
});
|
||||
|
||||
|
||||
@@ -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_<knob>=<v> 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); }
|
||||
|
||||
Reference in new issue
Block a user