Pace was the missing dial, and the reachability test finally exists
"there are still balls which are basically not reachable with auto run.. please recheck" — and, asked twice: "did you write tests for the last issues we encountered?" The straight answer: no. The unreachable-balls issue only ever had config assertions, which is why it came back twice. Pace is the ladder's fourth dial. Width alone never fixed reachability because the returns left at ~27 m/s, crossing to the baseline in ~0.6s — less than auto-run needs from the far corner. matchOppPace 0.78 puts opponent one at ~20 m/s (measured live: twelve returns, 20.0-20.7), a quarter more flight time, and matchOppPacePer hands it back per rung. The vignette deepens: dmg x 0.8, cap 0.85, and the clean centre shrinks with damage instead of only the rim darkening. And the reachability regression the issue always deserved: a headless 90s autopilot soak asserting at least 10 returns sampled and at most 15% that never came within volley reach. Sabotage-verified. Its honest limit is recorded in PROJECT.md: the autopilot attends perfectly and taps frame-precisely, so it out-covers a human — it is a floor against catastrophic regressions, not a proxy for the thumb. Oliver's playtest stays the real gate. 46 tests, 15 seeds clean. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
8ccf0a8fad
commit
fdbadce94d
6 files changed
+73
-4
No files matched your search
+19
@@ -948,6 +948,25 @@ strength because the balls kept landing where he wasn't.
|
|||||||
the state is worn, not reported. At 1 hp the frame is properly blood-rimmed and the court centre
|
the state is worn, not reported. At 1 hp the frame is properly blood-rimmed and the court centre
|
||||||
stays legible.
|
stays legible.
|
||||||
|
|
||||||
|
**NINETEENTH PASS — pace was the missing dial, and the reachability test finally exists.** Oliver:
|
||||||
|
*"the vignette can get more pronounced and there are still balls which are basically not reachable
|
||||||
|
with auto run.. please recheck"* — plus, asked twice and deserving a straight answer: *"did you
|
||||||
|
write tests for the last issues we encountered?"* **No — the unreachable-balls issue only ever had
|
||||||
|
config assertions, which is exactly why it came back twice.**
|
||||||
|
- **Pace, the ladder's FOURTH dial** (`matchOppPace` 0.78, `matchOppPacePer` 0.05/rung): width alone
|
||||||
|
never fixed reachability because the returns left at ~27 m/s — crossing to your baseline in
|
||||||
|
~0.6 s, less than auto-run needs from the far corner. Opponent one now returns at ~20 m/s
|
||||||
|
(measured live: twelve returns, 20.0-20.7), a quarter more flight time; the rungs take it back.
|
||||||
|
- **The vignette deepens**: `dmg * 0.8` (was 0.5), cap 0.85, and the clean centre SHRINKS with
|
||||||
|
damage rather than only the rim darkening.
|
||||||
|
- **The REACHABILITY REGRESSION** (the test this issue always deserved): a headless 90 s autopilot
|
||||||
|
soak — auto-run, fixed tap window, no placement — asserting ≥10 returns sampled and ≤15% that
|
||||||
|
never came within volley reach. Sabotage-verified: cranked knobs trip the suite.
|
||||||
|
**Its honest limit, recorded so nobody overtrusts it**: the autopilot attends perfectly and taps
|
||||||
|
frame-precisely, so it OUT-COVERS a human — knob-legal-but-hostile values still pass it. It is a
|
||||||
|
floor against catastrophic regressions, not a proxy for Oliver's thumb; his playtest stays the
|
||||||
|
real gate for "reachable".
|
||||||
|
|
||||||
**Open, and every one of them is Oliver's call after he plays it:**
|
**Open, and every one of them is Oliver's call after he plays it:**
|
||||||
1. ~~**A ball that carries through the lane can complete two steps at once.**~~ Largely CLOSED by
|
1. ~~**A ball that carries through the lane can complete two steps at once.**~~ Largely CLOSED by
|
||||||
the slice removal (seventh pass) — a cut ball picking its own next victim was the main way this
|
the slice removal (seventh pass) — a cut ball picking its own next victim was the main way this
|
||||||
|
|||||||
+1
-1
@@ -400,6 +400,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<script type="module" src="src/main.js?v=170"></script>
|
<script type="module" src="src/main.js?v=171"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
@@ -394,6 +394,9 @@ export const TUNE = {
|
|||||||
matchFlub: 0.08, // the match's own whiff base — the duel's 0.2 gives away a fifth of all
|
matchFlub: 0.08, // the match's own whiff base — the duel's 0.2 gives away a fifth of all
|
||||||
// returns, which across a game is an opponent that beats itself
|
// returns, which across a game is an opponent that beats itself
|
||||||
matchFlubKeep: 0.82, // ...and it keeps only this much per rung — the ladder now that hp is flat
|
matchFlubKeep: 0.82, // ...and it keeps only this much per rung — the ladder now that hp is flat
|
||||||
|
matchOppPace: 0.78, // scale on the opponent's return SPEED for opponent one (~21 m/s instead
|
||||||
|
// of ~27) — the flight time auto-run needs. THE reachability dial
|
||||||
|
matchOppPacePer: 0.05,// ...climbing back per rung, capped at full pace
|
||||||
matchWideBase: 2.1, // m either side of centre the FIRST opponent's returns may land — the width an
|
matchWideBase: 2.1, // m either side of centre the FIRST opponent's returns may land — the width an
|
||||||
// auto-run player can actually cover. Its scatter painted lines from game one
|
// auto-run player can actually cover. Its scatter painted lines from game one
|
||||||
// ("still a LOT of unreachable balls"); now line-painting is the late ladder
|
// ("still a LOT of unreachable balls"); now line-painting is the late ladder
|
||||||
|
|||||||
@@ -146,6 +146,13 @@ function doHit(ch, quality, aim) {
|
|||||||
tz = (toFar ? tzBase : COURT.L - tzBase) + err() * 0.8;
|
tz = (toFar ? tzBase : COURT.L - tzBase) + err() * 0.8;
|
||||||
}
|
}
|
||||||
tx = Math.max(-5, Math.min(5, tx));
|
tx = Math.max(-5, Math.min(5, tx));
|
||||||
|
if (G.match && ch.side !== 'P') {
|
||||||
|
// ...AND ITS PACE IS THE FOURTH. Width alone did not fix "unreachable" because the returns left
|
||||||
|
// at ~27 m/s (baseShotSpeed + 0.55 quality) — crossing to your baseline in ~0.6 s, which is less
|
||||||
|
// time than auto-run needs from the far corner. Opponent one gives you the flight time; the
|
||||||
|
// rungs take it back.
|
||||||
|
s *= Math.min(1, TUNE.matchOppPace + (G.match.opp - 1) * TUNE.matchOppPacePer);
|
||||||
|
}
|
||||||
if (G.match) {
|
if (G.match) {
|
||||||
// THE OPPONENT'S WIDTH IS THE LADDER'S THIRD DIAL. Its returns scatter off aimError x (1 - 0.55
|
// THE OPPONENT'S WIDTH IS THE LADDER'S THIRD DIAL. Its returns scatter off aimError x (1 - 0.55
|
||||||
// quality), which paints lines an auto-run player cannot reach — "still a LOT of unreachable
|
// quality), which paints lines an auto-run player cannot reach — "still a LOT of unreachable
|
||||||
|
|||||||
+6
-3
@@ -1971,9 +1971,12 @@ export function render() {
|
|||||||
const dmg = 1 - Math.max(0, G.hp) / (G.hpMax || 1);
|
const dmg = 1 - Math.max(0, G.hp) / (G.hpMax || 1);
|
||||||
const pulse = G.hurtFlash > 0 ? Math.min(1, G.hurtFlash / 0.3) * 0.3 : 0;
|
const pulse = G.hurtFlash > 0 ? Math.min(1, G.hurtFlash / 0.3) * 0.3 : 0;
|
||||||
if (dmg > 0 || pulse > 0) {
|
if (dmg > 0 || pulse > 0) {
|
||||||
const a = Math.min(0.72, dmg * 0.5 + pulse);
|
// dmg * 0.8: at half health the frame is unmistakably bloodied, at 1 hp it closes in hard
|
||||||
const g = ctx.createRadialGradient(W / 2, H / 2, Math.min(W, H) * (0.52 - dmg * 0.14),
|
// ("the vignette can get more pronounced") — the inner radius tightens with damage too, so
|
||||||
W / 2, H / 2, Math.max(W, H) * 0.74);
|
// deep wounds shrink the clean centre instead of only darkening the rim
|
||||||
|
const a = Math.min(0.85, dmg * 0.8 + pulse);
|
||||||
|
const g = ctx.createRadialGradient(W / 2, H / 2, Math.min(W, H) * (0.5 - dmg * 0.22),
|
||||||
|
W / 2, H / 2, Math.max(W, H) * 0.7);
|
||||||
g.addColorStop(0, 'rgba(120,8,12,0)');
|
g.addColorStop(0, 'rgba(120,8,12,0)');
|
||||||
g.addColorStop(1, `rgba(120,8,12,${a.toFixed(3)})`);
|
g.addColorStop(1, `rgba(120,8,12,${a.toFixed(3)})`);
|
||||||
ctx.fillStyle = g; ctx.fillRect(0, 0, W, H);
|
ctx.fillStyle = g; ctx.fillRect(0, 0, W, H);
|
||||||
|
|||||||
@@ -607,6 +607,43 @@ test('the placement mark fades on its own clock', () => {
|
|||||||
assert.equal(G.placeMark, null, 'gone within a second');
|
assert.equal(G.placeMark, null, 'gone within a second');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('REACHABILITY: an auto-run player can cover the first opponent\'s returns', () => {
|
||||||
|
// The test this issue always deserved and never had — "unreachable balls" came back TWICE because
|
||||||
|
// the suite only asserted the config dials, never the experience. This plays the match headless
|
||||||
|
// with the same mediocre autopilot the browser uses (auto-run + a fixed tap window, no placement)
|
||||||
|
// and asserts that the returns actually present themselves: a ball that never comes within volley
|
||||||
|
// reach of an auto-run player is unreachable BY CONSTRUCTION, whatever the knobs claim.
|
||||||
|
const assist = TUNE.autoAssist; TUNE.autoAssist = 1.15;
|
||||||
|
try {
|
||||||
|
startMatch();
|
||||||
|
for (let i = 0; i < 2.2 * 60; i++) update(DT);
|
||||||
|
const returns = [];
|
||||||
|
let cur = null;
|
||||||
|
for (let i = 0; i < 90 * 60 && G.mode !== 'eaten'; i++) {
|
||||||
|
G.modal = null; G.modalQ.length = 0;
|
||||||
|
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') {
|
||||||
|
const d = Math.hypot(ball.x - P.x, (ball.z - P.z) * 0.8);
|
||||||
|
if (!cur) cur = { minD: 99 };
|
||||||
|
cur.minD = Math.min(cur.minD, d);
|
||||||
|
if (d < 2.9 && d > 2.2) keyPressed('Space');
|
||||||
|
}
|
||||||
|
if (cur && (!ball.active || ball.lastHit !== 'O')) { returns.push(cur.minD); cur = null; }
|
||||||
|
update(DT);
|
||||||
|
}
|
||||||
|
assert.ok(returns.length >= 10, 'the soak saw a real rally sample: ' + returns.length + ' returns');
|
||||||
|
const unreachable = returns.filter(d => d > 2.6).length; // never inside volley reach
|
||||||
|
assert.ok(unreachable / returns.length <= 0.15,
|
||||||
|
unreachable + ' of ' + returns.length + ' returns never came within reach of an auto-run player');
|
||||||
|
} finally { TUNE.autoAssist = assist; }
|
||||||
|
});
|
||||||
|
|
||||||
|
test('the ladder pace dial exists and starts below full pace', () => {
|
||||||
|
assert.ok(TUNE.matchOppPace < 1, 'opponent one returns slower than the physics maximum');
|
||||||
|
assert.ok(TUNE.matchOppPacePer > 0, 'and earns pace back per rung');
|
||||||
|
});
|
||||||
|
|
||||||
test('THE REAL DEATH LOOP: a toss after their rally touch is not their passing shot', () => {
|
test('THE REAL DEATH LOOP: a toss after their rally touch is not their passing shot', () => {
|
||||||
// tossBall left ball.lastHit stale. After any rally the zombie touched last, the fresh toss still
|
// tossBall left ball.lastHit stale. After any rally the zombie touched last, the fresh toss still
|
||||||
// said lastHit 'O' — and the match serves from BEHIND the baseline (z 24.3), past the passing-shot
|
// said lastHit 'O' — and the match serves from BEHIND the baseline (z 24.3), past the passing-shot
|
||||||
|
|||||||
Reference in new issue
Block a user