From 0576f246145cfbb605db4c7c855e5248b0fc9d80 Mon Sep 17 00:00:00 2001 From: Oliver Rieger Date: Thu, 15 Oct 2026 22:02:30 +0200 Subject: [PATCH] Flatten the ladder: cap how wide an opponent paints Oliver: from the Runner on the zombie game is very hard. Measured with the ring bot: the win rate fell off a cliff wherever an opponent's width crossed ~3.3 m, the auto-run's reach from the centre (at the Steward, and again at the Seller), and from there every line could be painted. Legs and flub decay moved nothing. Every match opponent's spread is now capped at 3.0 m (matchWideMax), and the put-away chance runs 0.15 to 0.8 instead of 0.2 to 0.85. Level 10: the Runner goes from 44% to 68% of points, the Champion from 24% to 33%, with a steady slope between them. New test pins the cap. v387. Co-Authored-By: Claude Opus 5.5 --- PROJECT.md | 28 ++++++++++++++++++++++++++++ index.html | 2 +- src/config.js | 10 ++++++++-- src/gameplay.js | 2 +- test.js | 25 +++++++++++++++++++++++++ 5 files changed, 63 insertions(+), 4 deletions(-) diff --git a/PROJECT.md b/PROJECT.md index 119e36d..48f91d3 100644 --- a/PROJECT.md +++ b/PROJECT.md @@ -636,6 +636,34 @@ Each phase ships to staging and gets a phone verdict, and tests ride along with 5. **LATER.** The metal bed (when Oliver has a track: start at the transformation, gore only) and Members' Night (option C). +## Session log: the ladder is flatter — the width cliff (2026-10-15, v387) +Oliver: *"ab Runner ist das Zombie-Game schon sehr schwer — können wir die Schwierigkeitsleiter ein wenig +abflachen?"* **Measured first**: cup mode, the ring bot, 6-8 seeds (`tools/rally-sim.mjs`, `T_` sweeps). +- **The cliff.** Roller (rung 5) 90-98% of points, then Steward 54-79%, Mascot ~50, Runner 41-55. At level + 10 it fell again at the Seller (29%). +- **What it is NOT:** sweeps of the legs (`legKnee` 5.3, `matchSpeedPer` 0.07) and of the flub decay + (`matchFlubKeep` 0.84) all moved it within the noise. +- **What it IS: the width.** `matchWideBase + step·matchWidePer` × the foe's `wide` grows without bound. + Wherever it crosses ~3.3 m (the auto-run's reach from the centre) the win rate falls off. That happens + at the Steward, and with a slower slope it just moved to the Seller. From there every line can be + painted. +- **The put-away chance** sets the top: without it, rungs 9-18 sat flat at ~60%. +- **Shipped (variant G):** + - `TUNE.matchWideMax` 3.0 m caps every match opponent's spread (Climb, cup, story cups, training). + - `punishMin`/`punishMax` go from 0.2/0.85 to 0.15/0.8. +- **Bot, level 10, cup** (before → after): + + | rung | 6 | 7 | 8 | 9 | 10 | 11 | 13 | 14 | 16 | 18 | + |---|---|---|---|---|---|---|---|---|---|---| + | before | 57 | 51 | 44 | 29 | 31 | 27 | 26 | 34 | 22 | 24 | + | after | 72 | 73 | 68 | 53 | 56 | 57 | 42 | 42 | 36 | 33 | + + The rushers stay their usual bumps. Level 26: Runner 43 → 83, Champion 29 → 41. Climb, level 26: + 85 → 44, smooth. +- 3.1 m was tried as a middle way. It barely helped at level 10, because the cliff sits between 3.0 and + 3.3. +- Test: rungs 8, 12 and 18 never land wider than 3.35 m (it reads 4.05 at the Runner without the cap). + ## Session log: no story boss crosses the net (2026-10-15, v385, STAGING ONLY) Oliver: *"strawberry seller as first endboss is way too hard — or it should not cross the net"*. The seller is the arcade's chapter-9 design. His move is `charge`, the one style deliberately exempt from diff --git a/index.html b/index.html index 632d01c..a1cdbaa 100644 --- a/index.html +++ b/index.html @@ -1201,6 +1201,6 @@ if (location.hostname !== 'splatter-tennis.wuwi.one' && !window.Capacitor?.isNat - + diff --git a/src/config.js b/src/config.js index 19b2e92..2bb3237 100644 --- a/src/config.js +++ b/src/config.js @@ -169,8 +169,8 @@ export const TUNE = { // its window closes — the follow-through, q 0.05 (a weak short ball, not a whiff). // 0 = the old edge: a tap ~70 ms early whiffed (step 4, see gameplay.trySwing) punishBelow: 0.5, // a player ball under this q is a SITTER the CPU may put away (gameplay.punishes) - punishMin: 0.2, // ...the chance it does at the bottom of the ladder - punishMax: 0.85, // ...and at the CHAMPION (classic: DIFFS[].punish) + punishMin: 0.15, // ...the chance it does at the bottom of the ladder (0.2 until the flattening) + punishMax: 0.8, // ...and at the CHAMPION (0.85 until 2026-10-15; classic: DIFFS[].punish) punishQ: 0.95, // the put-away's quality, played to the open court... punishX: 3.5, // ...at this x, inside the singles line (4.1), bounded by the rung's width dial punishRead: 0.5, // s the player's auto-run stands flat-footed on a put-away of his own sitter @@ -656,6 +656,12 @@ export const TUNE = { // 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 matchWidePer: 0.25, // ...earning this much more width per opponent felled (softened with speedPer) + // ...and NEVER WIDER THAN THIS (Oliver, 2026-10-15: "ab Runner ist das Zombie-Game schon sehr schwer — + // die Schwierigkeitsleiter ein wenig abflachen"). Measured: the ladder fell off a cliff wherever its + // width crossed ~3.3 m — the auto-run's reach from the centre — at the Steward and again at the + // Seller, and from there every line could be painted. Capped at 3.0, the late ladder climbs through + // put-aways, flub, pace and open-court aim instead (bot, level 10: Runner 44 -> 68%, Champion 24 -> 33%) + matchWideMax: 3.0, matchHeal: 2, // hp back for a game won (the duel's healPerDuel rate) netWinBurst: 1.4, // s the friend's burst plays before the win screen covers it. update() freezes // the world on G.done, so this beat is the only thing that lets the gore fly. diff --git a/src/gameplay.js b/src/gameplay.js index 571b4b5..da29f4f 100644 --- a/src/gameplay.js +++ b/src/gameplay.js @@ -345,7 +345,7 @@ function doHit(ch, quality, aim) { // balls". Opponent one keeps its returns inside matchWideBase of the centre line; each rung // earns matchWidePer more width, so the line-painting is the LATE ladder, not the opening one. if (ch.side !== 'P') { - const wide = (TUNE.matchWideBase + matchStep() * TUNE.matchWidePer) * (ch.foe?.wide || 1); + const wide = Math.min(TUNE.matchWideMax, (TUNE.matchWideBase + matchStep() * TUNE.matchWidePer) * (ch.foe?.wide || 1)); tx = Math.max(-wide, Math.min(wide, tx)); } } diff --git a/test.js b/test.js index ff8112f..0346d76 100644 --- a/test.js +++ b/test.js @@ -3662,6 +3662,31 @@ function winStoryCup() { G.modal = null; G.modalQ.length = 0; } +test('THE LADDER IS FLATTER: no opponent paints wider than matchWideMax (the cliff at ~3.3 m)', () => { + // Oliver, 2026-10-15: "ab Runner ist das Zombie-Game schon sehr schwer". The win rate fell off a cliff + // wherever the rung's width crossed the auto-run's reach from the centre; the width is capped now. + startMatch(); G.interlude.introT = 0; + const zb = oppOf(); + for (const n of [8, 12, 18]) { + G.match.opp = n; zb.foe = cfg.matchFoe(n); + let widest = 0; + for (let i = 0; i < 600; i++) { + G.phase = 'rally'; G.wasServe = false; G.match.surprised = G.match.expected = false; + P.x = i % 2 ? 3 : -3; P.z = 23; P.swingT = -1; + zb.x = 0; zb.z = 2; zb.swingT = -1; zb.swingCool = 0; zb.serveHold = 0; + Object.assign(ball, { active: true, lastHit: 'P', pow: 1, bounces: 1, rx: null, spin: 1, soft: false, + x: 0, z: 2.4, y: 1.2, vx: 0, vy: 0, vz: -1 }); + update(DT); + if (ball.lastHit !== 'O' || ball.soft) { ball.active = false; continue; } + const t = (ball.vy + Math.sqrt(ball.vy * ball.vy + 2 * ball.g * ball.y)) / ball.g; + widest = Math.max(widest, Math.abs(ball.x + ball.vx * t)); + ball.active = false; + } + assert.ok(widest > 2.2 && widest <= 3.0 + 0.35, `rung ${n}: widest landing ${widest.toFixed(2)} m (the flattening set 3.0)`); + } + endMatch(); +}); + test('THE STORY: every act\'s cup final is the body that becomes its boss, and the stock grows act by act', () => { const famOf = n => cfg.matchFoe(n).fam; const bossFam = { 0: 'gk', 5: 'ma', 2: 'cu', 3: 'hc', 9: 'ch' }; // design -> the ladder body it grew from