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 <noreply@anthropic.com>
This commit is contained in:
1 parent
c8d43fbe15
commit
0576f24614
5 files changed
+63
-4
No files matched your search
+28
@@ -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'
|
5. **LATER.** The metal bed (when Oliver has a track: start at the transformation, gore only) and Members'
|
||||||
Night (option C).
|
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_<knob>` 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)
|
## 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
|
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
|
seller is the arcade's chapter-9 design. His move is `charge`, the one style deliberately exempt from
|
||||||
|
|||||||
+1
-1
@@ -1201,6 +1201,6 @@ if (location.hostname !== 'splatter-tennis.wuwi.one' && !window.Capacitor?.isNat
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<span id="recLamp" hidden>REC</span>
|
<span id="recLamp" hidden>REC</span>
|
||||||
<script type="module" src="src/main.js?v=386"></script>
|
<script type="module" src="src/main.js?v=387"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
+8
-2
@@ -169,8 +169,8 @@ export const TUNE = {
|
|||||||
// its window closes — the follow-through, q 0.05 (a weak short ball, not a whiff).
|
// 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)
|
// 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)
|
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
|
punishMin: 0.15, // ...the chance it does at the bottom of the ladder (0.2 until the flattening)
|
||||||
punishMax: 0.85, // ...and at the CHAMPION (classic: DIFFS[].punish)
|
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...
|
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
|
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
|
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
|
// 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
|
||||||
matchWidePer: 0.25, // ...earning this much more width per opponent felled (softened with speedPer)
|
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)
|
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
|
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.
|
// the world on G.done, so this beat is the only thing that lets the gore fly.
|
||||||
|
|||||||
+1
-1
@@ -345,7 +345,7 @@ function doHit(ch, quality, aim) {
|
|||||||
// balls". Opponent one keeps its returns inside matchWideBase of the centre line; each rung
|
// 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.
|
// earns matchWidePer more width, so the line-painting is the LATE ladder, not the opening one.
|
||||||
if (ch.side !== 'P') {
|
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));
|
tx = Math.max(-wide, Math.min(wide, tx));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3662,6 +3662,31 @@ function winStoryCup() {
|
|||||||
G.modal = null; G.modalQ.length = 0;
|
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', () => {
|
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 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
|
const bossFam = { 0: 'gk', 5: 'ma', 2: 'cu', 3: 'hc', 9: 'ch' }; // design -> the ladder body it grew from
|
||||||
|
|||||||
Reference in new issue
Block a user