HARD finally outruns the runner

Oliver benchmarked HARD against the game's own roster: "the runner in zombie
mode is way faster than the regular player in hard" — and the numbers agreed.
The RUNNER covers 6.15 m/s (racketSpeed x rung x foe.speed) with no reaction
gate; HARD's classic CPU did 5.59, only after its 0.22 s read, and ambled
back to centre at HALF speed between shots — that recovery jog was the free
points. HARD now moves 4.3 x 1.5 = 6.45 m/s, and DIFFS grows `recover`: the
between-shots factor, 0.85 on HARD where the shipped 0.5 stays pinned on
MEDIUM. Wrong-footing him still works; strolling him side to side no longer
does.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
wuwiandClaude Fable 5 committed 2026-08-14 12:07:59 +02:00
1 parent 11ca935ad0
commit 8044857457
4 files changed
+13 -5

No files matched your search

+1 -1
View File
@@ -492,6 +492,6 @@ if (location.hostname !== 'splatter-tennis.wuwi.one') {
</div>
</div>
</div>
<script type="module" src="src/main.js?v=238"></script>
<script type="module" src="src/main.js?v=239"></script>
</body>
</html>
+7 -3
View File
@@ -661,10 +661,14 @@ export const MENU = [
// the dial that turns "returns everything" into "plays you". HARD got a second pass after
// Oliver's "still too easy": it now reads your shot in 0.22 s, covers court at 1.3x, floors its
// quality at 0.70, and hunts the corner you are not standing in four times out of five.
// `recover` scales the between-shots jog (the rally block's 0.5 "slow recovery: wrong-footing wins
// points") — a HARD CPU that ambles back to centre is free points, a recovering one is a wall.
// Speed calibration: the zombie RUNNER covers 2.8 x 1.8 x 1.22 = 6.15 m/s with no reaction gate,
// and Oliver clocked HARD as slower than him — HARD now moves 4.3 x 1.5 = 6.45.
export const DIFFS = [
{ id: 'easy', name: 'EASY', flub: 0.30, quality: 0.62, qMin: 0.45, speed: 0.80, reaction: 0.62, reach: 1.45, serve: 0.75, smart: 0 },
{ id: 'medium', name: 'MEDIUM', flub: 0.10, quality: 0.86, qMin: 0.45, speed: 1.0, reaction: 0.45, reach: 1.6, serve: 1.0, smart: 0 },
{ id: 'hard', name: 'HARD', flub: 0.01, quality: 1.0, qMin: 0.70, speed: 1.30, reaction: 0.22, reach: 1.85, serve: 1.15, smart: 0.8 },
{ id: 'easy', name: 'EASY', flub: 0.30, quality: 0.62, qMin: 0.45, speed: 0.80, reaction: 0.62, reach: 1.45, serve: 0.75, smart: 0, recover: 0.4 },
{ id: 'medium', name: 'MEDIUM', flub: 0.10, quality: 0.86, qMin: 0.45, speed: 1.0, reaction: 0.45, reach: 1.6, serve: 1.0, smart: 0, recover: 0.5 },
{ id: 'hard', name: 'HARD', flub: 0.01, quality: 1.0, qMin: 0.70, speed: 1.50, reaction: 0.22, reach: 1.85, serve: 1.15, smart: 0.8, recover: 0.85 },
];
// THE TOURNAMENT (Oliver's pick over the endless ladder, 2026-08-13): four rounds up the club's
+3 -1
View File
@@ -1733,7 +1733,9 @@ export function update(dt) {
const incoming = ball.active && ball.lastHit === 'P'
&& G.aiReactT >= (G.diff ? G.diff.reaction : TUNE.aiReaction);
const tgt = incoming ? intercept('O') : HOME.O;
const spd = TUNE.aiSpeed * (G.diff ? G.diff.speed : 1) * (incoming ? 1 : 0.5); // slow recovery: wrong-footing wins points
const spd = TUNE.aiSpeed * (G.diff ? G.diff.speed : 1)
* (incoming ? 1 : (G.diff ? G.diff.recover : 0.5)); // slow recovery: wrong-footing wins points
const dx = tgt.x - O.x, dz = tgt.z - O.z, d = Math.hypot(dx, dz);
if (d > 0.1) {
O.x += dx / d * Math.min(spd * dt, d);
+2
View File
@@ -1127,7 +1127,9 @@ test('classic wears the match feel, and the difficulty table is a ladder', () =>
assert.ok(e.qMin <= m.qMin && m.qMin < h.qMin, 'harder raises the quality floor');
assert.ok(e.serve < m.serve && m.serve < h.serve, 'harder serves better');
assert.ok(e.smart <= m.smart && m.smart < h.smart, 'harder plays the open court');
assert.ok(e.recover <= m.recover && m.recover < h.recover, 'harder recovers to centre faster');
assert.equal(m.smart, 0, 'MEDIUM aims at random, as the shipped classic did');
assert.equal(m.recover, 0.5, 'MEDIUM keeps the shipped recovery jog');
assert.equal(m.flub, TUNE.classicFlub, 'MEDIUM flub IS classicFlub');
assert.equal(m.quality, TUNE.classicQualityMax, 'MEDIUM quality IS classicQualityMax');
assert.equal(m.speed, 1.0, 'MEDIUM leaves aiSpeed alone');