Let the player take the net and come back, and smash a lob he gets under
Oliver: "Steuerung ans Netz gehen", option (1), and "einen Lob vom Gegner als Volley annehmen soll derb schmettern". - A long up-flick (90 px or more) is the attack: the deep ball, and he follows it in. A long down-flick is the lob, and he goes back behind it. Short flicks keep the deep ball / the drop and leave him where he is. Length only, never context. - The net costs him what it costs the CPU: he split-steps (netReact) before reading the pass, and a CPU facing a net man passes at the line. Without both, the attacker won 78-95% of his net points. - The smash: a ball taken out of the air at 1.9-2.5 m is struck harder and to the open court, read late by the opponent, and pops "SMASH!". It wins ~80% of its points. - Lobbed at the net, he decides once whether he can get under it. If he can, he smashes. If not, he runs it down and only blocks it back, even if he meets it in the air. Re-deciding every frame smashed every lob. Measured at level 26: the base ladder is unchanged. Attacking pays against pushers and touch players and costs against big hitters, slicers and rushers. Three new tests; v374. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
4406aeafdb
commit
599d5b0506
8 files changed
+201
-17
No files matched your search
@@ -5,6 +5,7 @@
|
||||
// shape of a rally, not a win rate — the bot is not Oliver. See PROJECT.md "THE RALLY GOES FLAT".
|
||||
// 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.
|
||||
// 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
|
||||
@@ -91,8 +92,10 @@ const med = a => { const s = [...a].sort((x, y) => x - y); return s.length ? s[M
|
||||
// 120 ms tap), and plays the game a little — a lob or a pass against a net man, the odd drop when
|
||||
// the opponent hangs back. BOT=dist is the original fixed-distance tapper with random flicks.
|
||||
const HUMAN = (process.env.BOT || 'human') === 'human';
|
||||
const ATTACK = Number(process.env.ATTACK || 0); // share of baseline shots played as the attack (long up-flick)
|
||||
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 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;
|
||||
@@ -103,6 +106,7 @@ function humanAim() {
|
||||
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 > 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 && r < ATTACK) return sw(away * 0.3, -1, 130); // the attack: follow it in
|
||||
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
|
||||
return null; // a plain tap
|
||||
@@ -171,6 +175,10 @@ for (const rung of RUNGS) {
|
||||
const lh = ball.lastHit;
|
||||
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'; } }
|
||||
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 === null) netx.p0 = score.pts[0];
|
||||
if (process.env.PROBE && lh === 'O' && ball.lastHit === 'P' && bot.land) { const L = bot.land; probe.push({ late: Math.round(((i - L.i) / 60 - L.eta) * 1000), sw: Math.round(L.sw * 1000), q: +ball.pow.toFixed(2), bAtLand: L.b, bAtHit: L.bb, d: +L.d.toFixed(2), y: +L.y.toFixed(2) }); bot.land = null; }
|
||||
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,
|
||||
@@ -232,6 +240,7 @@ for (const rung of RUNGS) {
|
||||
rows.push({ rung, side: 'rally', shots: rallies.length, medDistNet: med(rallies), '<6.4m(svcline)%': '', '<3.5m(net)%': '', 'volley%': '', medTimeToHit_s: 'shots/pt mean ' + (rallies.reduce((a, b) => a + b, 0) / rallies.length).toFixed(1) });
|
||||
}
|
||||
if (!process.env.SUMMARY) { console.table(rows); console.table(land); }
|
||||
if (process.env.NETX) console.log(JSON.stringify(netx));
|
||||
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