Keep the praise word on screen, and take the cliff out of the groundstroke
Oliver on v371: the praise word is cut off on the left on the iPhone. It is centred under the player's feet, so out wide it ran past the edge; every pop is now clamped to the canvas by its measured width. Step 4, measured first. The rally sim gets a human bot (taps on the timed ring with a timing spread), a Climb mode and a lost-point breakdown. At level 26 the ladder falls over six rungs and then goes flat: nearly every lost point is the player's own early whiff, the CPU barely ever misses, and the auto-run reaches everything, so no opponent dial decides points. A quality ramp, a follow-through and CPU put-aways were each measured and reverted; the options go to Oliver in PROJECT.md. Shipped: in the rally modes q no longer drops from 1 to 0.43 one ms past PERFECT; it falls smoothly to the window's edge (TUNE.qSeamless). A tap 30 ms early is q 0.86 instead of 0.61. Same window, same whiff edge. v372. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
d561050af8
commit
5dd5d71c33
7 files changed
+170
-9
No files matched your search
+66
-5
@@ -3,6 +3,8 @@
|
||||
// a fixed tap window, a random lane/deep flick) and prints WHERE each side meets the ball, how often
|
||||
// it volleys, how long each shot gives the other side, and where rally balls land. It measures the
|
||||
// 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.
|
||||
// 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
|
||||
@@ -84,14 +86,49 @@ const SECS = Number(process.env.SECS || 240);
|
||||
TUNE.autoAssist = 1.15;
|
||||
const pct = (a, f) => a.length ? Math.round(100 * a.filter(f).length / a.length) : 0;
|
||||
const med = a => { const s = [...a].sort((x, y) => x - y); return s.length ? s[Math.floor(s.length / 2)] : NaN; };
|
||||
const rows = [], land = [], pts = [];
|
||||
// BOT=human (the default): taps on the timed gold ring like a player (JIT ms of timing spread, a
|
||||
// 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 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 probe = [];
|
||||
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;
|
||||
return { ax: Math.abs(fx) > 0.38 ? Math.sign(fx) : 0, az: Math.abs(fy) > 0.38 ? Math.sign(fy) : 0, fx, fy, mag }; };
|
||||
function humanAim() {
|
||||
const rz = process.env.CLASSIC ? state.O : zombies.find(z => z.variant === 'r');
|
||||
const oz = rz ? Math.abs(rz.z - COURT.netZ) : 12, r = Math.random();
|
||||
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 (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
|
||||
}
|
||||
const rows = [], land = [], pts = [], whys = [];
|
||||
for (const rung of RUNGS) {
|
||||
restartRun();
|
||||
restartRun(); Object.assign(bot, { armed: false, fire: -1, jit: null, swipe: null });
|
||||
prof.prof.lg = 5; prof.prof.xp = Number(process.env.XP || 0); prof.takePromo();
|
||||
G.mode = 'splatter'; G.everTransitioned = true; state.setupPoint();
|
||||
const cfgm = await import(R + 'config.js');
|
||||
if (process.env.CLASSIC) { G.mode = 'tennis'; G.everTransitioned = false; G.classic = true; G.diff = cfgm.DIFFS[rung]; state.setupPoint(); }
|
||||
// CLIMB=1: the arcade run (its own step boost at the bottom), pinned to this rung like the suite does
|
||||
else if (process.env.CLIMB) { beats.startMatch(false, null, rung); G.match.train = false; }
|
||||
else beats.startMatch(false, null, rung);
|
||||
const why = {};
|
||||
let prevE = null, lastHitE = null; // LOST is filed under how it came about: the player's last act before it
|
||||
G.missLog.push = function (e) {
|
||||
if (e.result === 'LOST') {
|
||||
const k = /never swung/.test(e.reason) ? 'never swung' : prevE && prevE.result === 'MISS' ? 'MISS ' + prevE.reason.split(' — ')[0].replace(/[\d.]+m?s?/g, '#').slice(0, 30)
|
||||
: lastHitE && /q=0\.[0-4]/.test(lastHitE.reason) ? 'after weak hit q<.5' : 'after good hit';
|
||||
why['L: ' + k] = (why['L: ' + k] || 0) + 1;
|
||||
} else if (e.result === 'WON') why.WON = (why.WON || 0) + 1;
|
||||
if (e.result === 'HIT') lastHitE = e;
|
||||
if (e.result === 'WON' || e.result === 'LOST') lastHitE = null;
|
||||
if (e.result !== 'THEM') prevE = e;
|
||||
return Array.prototype.push.call(this, e);
|
||||
};
|
||||
G.modal = null; G.modalQ.length = 0;
|
||||
for (let i = 0; i < 2.2 * 60; i++) { G.modal = null; G.modalQ.length = 0; update(DT); }
|
||||
const lands = [];
|
||||
@@ -101,11 +138,24 @@ for (const rung of RUNGS) {
|
||||
let swipe = null, pts0 = [...score.pts], net = { P: false, O: false };
|
||||
const won = { P: 0, O: 0, Pnet: 0, PnetWon: 0, Onet: 0, OnetWon: 0 };
|
||||
for (let i = 0; i < SECS * 60; i++) {
|
||||
G.modal = null; G.modalQ.length = 0;
|
||||
G.modal = null; G.modalQ.length = 0; if (process.env.CLIMB && G.match) { G.match.opp = rung; G.match.train = false; G.hp = G.hpMax; }
|
||||
if (G.mode === 'eaten' || G.done) { G.hp = G.hpMax; }
|
||||
t += DT;
|
||||
if (HUMAN && (ball.lastHit !== 'O' || !ball.active || G.phase !== 'rally')) { bot.jit = null; if (i !== bot.fire) { bot.armed = false; bot.fire = -1; } }
|
||||
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 (HUMAN) {
|
||||
// the finger lands when the ring says "now" give or take a human's timing (JIT ms, gaussian),
|
||||
// and a tap lifts LAG frames later carrying its age; a flick fires on the land (its crossing)
|
||||
if (G.phase === 'rally' && ball.active && ball.lastHit === 'O' && ball.vz > 0 && !bot.armed) {
|
||||
const c = gp.ringCue();
|
||||
if (c) {
|
||||
if (bot.jit === null) bot.jit = Math.max(-2, Math.min(2, gauss())) * JIT + Number(process.env.BIAS || 0) / 1000;
|
||||
if (c.eta - c.sweet + bot.jit <= 0 || c.eta <= 0) { bot.land = { i, eta: c.eta, sweet: c.sweet, b: ball.bounces }; bot.armed = true; bot.jit = null; bot.swipe = humanAim(); bot.fire = i + (bot.swipe ? 0 : LAG); }
|
||||
}
|
||||
}
|
||||
if (i === bot.fire) { G.swipe = bot.swipe; keyPressed('Space', bot.swipe ? 0 : LAG / 60, !!bot.swipe); }
|
||||
}
|
||||
else if (G.phase === 'rally' && ball.active && ball.lastHit === 'O') {
|
||||
const d = Math.hypot(ball.x - P.x, (ball.z - P.z) * 0.8);
|
||||
if (d < 2.9 && d > 2.2 && P.swingT < 0) {
|
||||
@@ -117,7 +167,10 @@ for (const rung of RUNGS) {
|
||||
keyPressed('Space');
|
||||
}
|
||||
}
|
||||
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 (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;
|
||||
const rz = process.env.CLASSIC ? state.O : zombies.find(z => z.variant === 'r');
|
||||
if (ball.active && ball.lastHit !== prevLast && (ball.lastHit === 'P' || ball.lastHit === 'O')) {
|
||||
@@ -161,7 +214,15 @@ for (const rung of RUNGS) {
|
||||
land.push({ rung, hitter: side, bounces: l.length, medLandFromBaseline: +med(l).toFixed(1),
|
||||
'deep<3m%': pct(l, x => x < 3), 'svcbox>5.5m%': pct(l, x => x > 5.5), medPace: +med(v).toFixed(1) });
|
||||
}
|
||||
pts.push({ rung, Pwon: won.P, Owon: won.O, 'P net pts': won.Pnet, 'P net won%': won.Pnet ? Math.round(100 * won.PnetWon / won.Pnet) : '', 'O net pts': won.Onet, 'O net won%': won.Onet ? Math.round(100 * won.OnetWon / won.Onet) : '' });
|
||||
const oh = hits.filter(x => x.side === 'O');
|
||||
pts.push({ rung, foe: (zombies.find(z => z.variant === 'r')?.foe?.short) || '', 'P win%': Math.round(100 * won.P / Math.max(1, won.P + won.O)),
|
||||
'shots/pt': +(rallies.reduce((a, b) => a + b, 0) / Math.max(1, rallies.length)).toFixed(1), 'O at net%': pct(oh, x => x.fromNet < 6.4), Pwon: won.P, Owon: won.O, 'P net pts': won.Pnet, 'P net won%': won.Pnet ? Math.round(100 * won.PnetWon / won.Pnet) : '', 'O net pts': won.Onet, 'O net won%': won.Onet ? Math.round(100 * won.OnetWon / won.Onet) : '' });
|
||||
whys.push({ rung, ...why });
|
||||
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) });
|
||||
}
|
||||
console.table(rows); console.table(land); console.table(pts);
|
||||
if (!process.env.SUMMARY) { console.table(rows); console.table(land); }
|
||||
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); }
|
||||
console.table(pts);
|
||||
if (process.env.WHY) console.table(whys);
|
||||
if (process.env.LOG) console.log(G.missLog.slice(process.env.LOG === "all" ? 0 : -25).map(e => `${e.at} ${e.result} ${e.reason}`).join("\n"));
|
||||
Reference in new issue
Block a user