Strengthen final-league placement and returns

This commit is contained in:
wuwi committed 2026-09-04 09:11:12 +02:00
1 parent bfc70d7388
commit e3faeb0a3a
5 files changed
+101 -8

No files matched your search

+62
View File
@@ -904,6 +904,68 @@ test('the opponent card is a RAMP: it starts easy and only gets harder', () => {
assert.equal(expectLo, LADDER + 1, 'the five bands cover the ladder exactly');
});
test('league 5 has no speed dip below league 4, including serve and wrong-footed chases', () => {
startMatch(); G.interlude.introT = 0;
const zb = oppOf();
const measure = (n, serve, surprised) => {
G.match.opp = n; G.match.surprised = surprised; G.match.expected = false;
zb.foe = cfg.matchFoe(n); zb.x = -3; zb.z = 2; zb.swingCool = 999;
G.phase = 'rally'; G.wasServe = serve;
Object.assign(ball, { active: true, lastHit: 'P', bounces: 0,
x: 3, z: 5, y: 2, vx: 0, vy: 4, vz: -8 });
update(DT);
return Math.hypot(zb.vx, zb.vz);
};
for (const serve of [false, true]) for (const surprised of [false, true]) {
const fourth = cfg.leagueOf(4).band, fifth = cfg.leagueOf(5).band;
let floor = 0;
for (let n = fourth[0]; n <= fourth[1]; n++) floor = Math.max(floor, measure(n, serve, surprised));
assert.ok(floor > 7, 'the rig measures a real full-speed chase');
for (let n = fifth[0]; n <= fifth[1]; n++) {
const speed = measure(n, serve, surprised);
assert.ok(speed >= floor, `${cfg.matchFoe(n).short}: ${speed} must clear ${floor} (serve ${serve}, surprised ${surprised})`);
}
}
endMatch();
});
test('league 5 returns hunt the open court and use their own quality caps; league 4 stays unchanged', () => {
startMatch(); G.interlude.introT = 0;
const zb = oppOf();
for (let n = 1; n <= cfg.leagueOf(4).band[1]; n++) {
assert.equal(cfg.matchFoe(n).smart, undefined, 'no new aim bonus in earlier leagues');
assert.equal(cfg.matchFoe(n).qMax, undefined, 'no new quality bonus in earlier leagues');
}
for (const [n, smart, cap] of [[15, 0, 0.55], [16, 0.2, 0.60], [17, 0.4, 0.65], [18, 0.6, 0.70]]) {
G.match.opp = n; zb.foe = cfg.matchFoe(n);
let hits = 0, open = 0, maxQ = 0;
for (let i = 0; i < 1200; 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', bounces: 1,
x: 0, z: 2.4, y: 1.2, vx: 0, vy: 0, vz: -1 });
update(DT);
if (ball.lastHit !== 'O') continue; // actual flubs remain part of the opponent
hits++;
assert.ok(ball.pow >= TUNE.aiQualityMin && ball.pow <= cap, `${n}: quality ${ball.pow} within its band`);
maxQ = Math.max(maxQ, ball.pow);
const t = (ball.vy + Math.sqrt(ball.vy * ball.vy + 2 * ball.g * ball.y)) / ball.g;
const landingX = ball.x + ball.vx * t;
// At q >= .45 the corner's inward scatter stays beyond 1.35; centre scatter stays inside.
if (landingX * Math.sign(P.x) < -1.35) open++;
ball.active = false;
}
assert.ok(hits > 1100, `${n}: enough actual returns (${hits})`);
assert.ok(maxQ > cap - 0.002, `${n}: the live return reaches its cap (${maxQ})`);
const expected = 1 / 3 + 2 / 3 * smart;
assert.ok(Math.abs(open / hits - expected) < 0.05,
`${n}: open-court share ${open / hits} should be near ${expected}`);
}
endMatch();
});
test('the first opponent cannot paint lines: its returns are width-clamped', () => {
// aimError x (1 - 0.55 quality) scattered returns to +-3.5 m, which an auto-run player cannot
// cover — "still a LOT of unreachable balls". Width is the ladder's third dial now.