From 1facc954e20ed36d8c6aae8f4a733b588e1f8719 Mon Sep 17 00:00:00 2001 From: Oliver Rieger Date: Fri, 14 Aug 2026 12:02:49 +0200 Subject: [PATCH] HARD hunts the corner you are not standing in MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit "the hard mode is still too easy — make it really challenging." The dial that was missing is aim: the CPU rolled a random lane on every shot, so a third of everything came politely back to your racket regardless of the other six numbers. DIFFS grows `smart` — the probability the CPU plays the OPEN court, the corner away from P.x — and HARD sets it to 0.8: four shots in five now make you run. EASY and MEDIUM keep random aim (MEDIUM pinned by test), so the shipped classic is untouched below the top chip. The rest of HARD tightened in the same pass: reaction 0.22 s, speed 1.30, reach 1.85 m, quality floored at 0.70 under a cap of 1.0, serve 1.15, flub one ball in a hundred. It reads your shot almost instantly, covers what it reads, and places its reply where you are not. Co-Authored-By: Claude Fable 5 --- index.html | 2 +- src/config.js | 10 +++++++--- src/gameplay.js | 9 ++++++++- test.js | 2 ++ 4 files changed, 18 insertions(+), 5 deletions(-) diff --git a/index.html b/index.html index 0bf9c32..d67378b 100644 --- a/index.html +++ b/index.html @@ -471,6 +471,6 @@ if (location.hostname !== 'splatter-tennis.wuwi.one') { - + diff --git a/src/config.js b/src/config.js index 68c212b..af10f70 100644 --- a/src/config.js +++ b/src/config.js @@ -657,10 +657,14 @@ export const MENU = [ // how long its racket is (reach), the FLOOR of its shot quality (qMin — a raised floor means every // ball comes back with pace), how often it just misses (flub), and its serve. MEDIUM repeats the // shipped TUNE values dial for dial (pinned by test), so the middle chip IS the old game. +// `smart` is the probability the CPU aims at the OPEN court instead of a random lane (trySwing) — +// 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. 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 }, - { 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 }, - { id: 'hard', name: 'HARD', flub: 0.02, quality: 1.0, qMin: 0.62, speed: 1.18, reaction: 0.28, reach: 1.75, serve: 1.1 }, + { 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 }, ]; // THE TOURNAMENT (Oliver's pick over the endless ladder, 2026-08-13): four rounds up the club's diff --git a/src/gameplay.js b/src/gameplay.js index 591e835..d62a2e6 100644 --- a/src/gameplay.js +++ b/src/gameplay.js @@ -1174,8 +1174,15 @@ function trySwing(ch, isAI, qCap = TUNE.aiQualityMax) { if (Math.abs(t - center) * 1000 < TUNE.perfectMs) q = 1; ch.facing = dx >= 0 ? 1 : -1; // forehand right (righty), backhand left → mirrored sprite const flick = isAI ? null : takeFlickAim(); + // SMART AIM (G.diff.smart, classic difficulty): with this probability the CPU plays the OPEN + // COURT — the corner away from where you stand — instead of rolling a random lane. Random aim + // sends a third of everything straight back to your racket; hunting the open corner is what + // makes a hard CPU feel like it is PLAYING you. ax is an absolute court lane (see doHit). + const smart = isAI && G.diff && Math.random() < (G.diff.smart || 0); const aim = isAI - ? { ax: [ -1, 0, 1 ][Math.floor(Math.random() * 3)], az: Math.random() < 0.25 ? -1 : 0 } + ? { ax: smart ? (P.x > 0.6 ? -1 : P.x < -0.6 ? 1 : (Math.random() < 0.5 ? -1 : 1)) + : [ -1, 0, 1 ][Math.floor(Math.random() * 3)], + az: Math.random() < 0.25 ? -1 : 0 } : flick || { ax: axisX(), az: axisZ() * (ch.side === 'P' ? 1 : -1) }; // G.diff (classic difficulty) moves the FLOOR too: a raised qMin is what makes HARD's every // ball arrive with pace, where a cap alone just trims the best ones. Null outside classic, so diff --git a/test.js b/test.js index 2bd951a..c010594 100644 --- a/test.js +++ b/test.js @@ -1126,6 +1126,8 @@ test('classic wears the match feel, and the difficulty table is a ladder', () => assert.ok(e.reach < m.reach && m.reach < h.reach, 'harder swings a longer racket'); 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.equal(m.smart, 0, 'MEDIUM aims at random, as the shipped classic did'); 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');