The opponent shows up, and the death loop dies
"still very weak and slow opponent. it also spawns at places where it is impossible to react to my serve. so i win like always. in the cases i dont win there is a bug. when trying to serve after i got hurt i get hurt again and again until i die". The death loop was G.serveNum. Tennis's pointTo is the only thing that ever reset it, and the match bypasses pointTo entirely — so the first fault of the match armed "second serve" permanently, and every missed serve after it was a double fault, a point, and blood, repeating to death. Exactly the spiral a rattled player serves into. matchReady(), the match's own point boundary, resets it now; a regression pins it. The receiver starts every point on its mark: matchReady drops it onto the exact receiver position the tennis branch computes for O, deuce/ad mirrored, instead of a random spawn x or wherever the last rally left it. An opponent that cannot reach the serve is not weak, it is absent. And the base opponent is a player now: matchSpeedBase 1.4 (the duel's 2.8m/s lumber was tuned for one sudden-death point), matchFlub 0.08 (0.2 gives away a fifth of all returns). Measured with the same autopilot that previously won seven straight games: it now loses the match. Possibly too hot — the two dials are named, Oliver's thumb decides. 40 tests, 15 seeds clean. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
ea6e50f8f8
commit
f67411e330
6 files changed
+83
-14
No files matched your search
+19
@@ -870,6 +870,25 @@ after the first serve does not happen from behind the groundline"*.
|
||||
tennis branch. Serving from mid-court where the last rally left you read as a bug because it was
|
||||
one. Verified: second serve from z 24.3, x mirrored to the ad side.
|
||||
|
||||
**FIFTEENTH PASS — the opponent shows up, and the death loop dies.** Oliver: *"still very weak and
|
||||
slow opponent. it also spawns at places where it is impossible to react to my serve. so i win like
|
||||
always. in the cases i dont win there is a bug. when trying to serve after i got hurt i get hurt
|
||||
again and again until i die"*.
|
||||
- **THE DEATH LOOP was `G.serveNum`.** Tennis's `pointTo` is the only thing that ever reset it — and
|
||||
the match bypasses pointTo entirely. So the first fault of the match armed "second serve"
|
||||
PERMANENTLY: every missed serve after it was a double fault -> a point -> blood -> repeat to
|
||||
death, which is exactly the spiral a just-hurt, rattled player serves into. `matchReady()` (the
|
||||
match's own point boundary) resets it now, and a regression pins it.
|
||||
- **The receiver starts every point ON ITS MARK.** It was spawning at a random x and standing
|
||||
wherever the last rally abandoned it — an opponent that cannot reach the serve is not weak, it is
|
||||
absent, and every ace against it was hollow. `matchReady` drops it onto the exact receiver
|
||||
position the tennis branch computes for O (deuce/ad mirrored with the server).
|
||||
- **The base opponent is a player now**: `matchSpeedBase` 1.4 (the duel's 2.8 m/s lumber was tuned
|
||||
for ONE sudden-death point, not a game) and `matchFlub` 0.08 (the duel's 0.2 gives away a fifth of
|
||||
all returns — an opponent that beats itself). Measured with the same autopilot that previously won
|
||||
seven straight games: **it now loses the match** (2-3, then blooded out in game two). That may
|
||||
even be too hot — `matchSpeedBase`/`matchFlub` are the two dials, and Oliver's thumb is the judge.
|
||||
|
||||
**Open, and every one of them is Oliver's call after he plays it:**
|
||||
1. ~~**A ball that carries through the lane can complete two steps at once.**~~ Largely CLOSED by
|
||||
the slice removal (seventh pass) — a cut ball picking its own next victim was the main way this
|
||||
|
||||
+1
-1
@@ -400,6 +400,6 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<script type="module" src="src/main.js?v=166"></script>
|
||||
<script type="module" src="src/main.js?v=167"></script>
|
||||
</body>
|
||||
</html>
|
||||
+22
-6
@@ -8,7 +8,7 @@
|
||||
// input <-> gameplay: the imported bindings are only touched inside functions, never at load.)
|
||||
|
||||
import { TUNE, UI, bossArt, COURT, HOME } from './config.js';
|
||||
import { G, P, ball, score, setupPoint, sendBoy } from './state.js';
|
||||
import { G, P, O, ball, score, setupPoint, sendBoy } from './state.js';
|
||||
import { voice, zombieDuck, aww } from './audio.js';
|
||||
import { zombies, pellets, extras, shots, spawnRacketZombie, spawnChapterBoss, spawnZombie, explode, killPlayer } from './zombies.js';
|
||||
import { cheer, aside, endBurst, deliverTin, showModal, logSwing, advancePoint, scoreKey } from './gameplay.js';
|
||||
@@ -227,7 +227,7 @@ export function startMatch() {
|
||||
G.boy = null;
|
||||
score.pts = [0, 0];
|
||||
matchOpponent();
|
||||
ball.active = false; setupPoint();
|
||||
matchReady();
|
||||
zombieDuck(true);
|
||||
voice('play_resume', 0.2);
|
||||
if (!G.seen.match) {
|
||||
@@ -238,6 +238,22 @@ export function startMatch() {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Every point starts like a real one: the server behind his line (setupPoint's tennis branch) and
|
||||
// THE RECEIVER ON ITS MARK — the zombie is dropped onto the receiver position the tennis branch
|
||||
// just computed for O, instead of standing wherever the last rally abandoned it. Oliver: "it spawns
|
||||
// at places where it is impossible to react to my serve. so i win like always." An opponent that
|
||||
// cannot reach the serve is not weak, it is absent.
|
||||
// ...and G.serveNum resets HERE, because the match bypasses tennis's pointTo (the only other place
|
||||
// that resets it). Without this, the first fault of the match armed 'second serve' PERMANENTLY:
|
||||
// every missed serve after it was a double fault, a point, and blood — the death loop Oliver hit.
|
||||
function matchReady() {
|
||||
ball.active = false;
|
||||
G.serveNum = 1;
|
||||
setupPoint();
|
||||
const rz = zombies.find(z => z.variant === 'r');
|
||||
if (rz) { rz.x = O.x; rz.z = 1.0; rz.pose = 'idle'; rz.swingT = -1; rz.swingCool = 0.3; }
|
||||
}
|
||||
|
||||
function matchOpponent() {
|
||||
spawnRacketZombie();
|
||||
const zb = zombies.find(z => z.variant === 'r');
|
||||
@@ -279,7 +295,7 @@ function matchPoint(side) {
|
||||
if (over) return side === 'P' ? matchGameWon(zb, true) : matchGameLost();
|
||||
voice(scoreKey(), 0.4);
|
||||
if (side === 'P') cheer(0.8, 0.3);
|
||||
ball.active = false; setupPoint(); // next point, same opponent
|
||||
matchReady(); // next point, same opponent, both on their marks
|
||||
}
|
||||
|
||||
function matchGameWon(zb, banked = false) {
|
||||
@@ -294,9 +310,9 @@ function matchGameWon(zb, banked = false) {
|
||||
voice('game_player', 0.3); cheer(2.4, 0.6);
|
||||
G.card = { title: 'GAME.', sub1: 'NEXT OPPONENT', color: UI.pick, t: 2.2 };
|
||||
d.opp++;
|
||||
matchOpponent(); // fresh body, more hp, its own intro beat
|
||||
matchOpponent(); // fresh body, its own intro beat
|
||||
d.ending = false;
|
||||
ball.active = false; setupPoint();
|
||||
matchReady();
|
||||
G.phase = 'over'; G.overT = 2.2;
|
||||
}
|
||||
|
||||
@@ -304,7 +320,7 @@ function matchGameLost() {
|
||||
// Reachable only through deuce (four straight points is exactly your hp) — the body keeps its
|
||||
// wounds, the scoreboard banks their game, and the match simply plays on.
|
||||
voice('game_opponent', 0.3); aww(1);
|
||||
ball.active = false; setupPoint();
|
||||
matchReady();
|
||||
G.phase = 'over'; G.overT = 1.8;
|
||||
}
|
||||
|
||||
|
||||
+7
-2
@@ -386,8 +386,13 @@ export const TUNE = {
|
||||
matchPlayerHp: 8, // your pool for the MATCH — richer than the gore mode's 4, because a real game
|
||||
// has rallies you deserve to lose without the run ending ("the player is too
|
||||
// weak in this mode"). Their point still costs 1; matchHeal is how you last
|
||||
matchSpeedPer: 0.12, // racketAI speed grows this much per opponent felled...
|
||||
matchFlubKeep: 0.82, // ...and its whiff chance keeps only this much — the ladder now that hp is flat
|
||||
matchSpeedBase: 1.4, // racketAI speed multiplier for opponent ONE — the duel's 2.8 m/s lumber was
|
||||
// tuned for a single sudden-death point and read as "very weak and slow"
|
||||
// across a whole game (~3.9 m/s now, rising per rung)
|
||||
matchSpeedPer: 0.12, // ...growing this much per opponent felled
|
||||
matchFlub: 0.08, // the match's own whiff base — the duel's 0.2 gives away a fifth of all
|
||||
// returns, which across a game is an opponent that beats itself
|
||||
matchFlubKeep: 0.82, // ...and it keeps only this much per rung — the ladder now that hp is flat
|
||||
matchHeal: 2, // hp back for a game won (the duel's healPerDuel rate)
|
||||
// The timing windows, DRAWN (the match's whole ask: "what is the possible tap window, what is the
|
||||
// sweet spot"). Numbers shared with the mechanics they visualise, so the picture cannot lie:
|
||||
|
||||
+7
-3
@@ -950,8 +950,10 @@ function racketAI(rz, dt) {
|
||||
const dx = ball.x - rz.x, dz = tz - rz.z, d = Math.hypot(dx, dz);
|
||||
if (d > 0.15) {
|
||||
// THE MATCH LADDER ESCALATES HERE, not in hp: with regicide gone the pool is pure scoreboard,
|
||||
// so a harder opponent is a FASTER one that flubs less — the same knobs, scaled per body felled.
|
||||
const spd = TUNE.racketSpeed * (G.match ? 1 + (G.match.opp - 1) * TUNE.matchSpeedPer : 1);
|
||||
// so a harder opponent is a FASTER one that flubs less. The BASE is boosted too — the duel's
|
||||
// 2.8 m/s lumber was tuned for one sudden-death point against a fresh player, and across a
|
||||
// whole game it read as "very weak and slow": opponent one already moves at matchSpeedBase.
|
||||
const spd = TUNE.racketSpeed * (G.match ? TUNE.matchSpeedBase + (G.match.opp - 1) * TUNE.matchSpeedPer : 1);
|
||||
const step = Math.min(spd * dt, d);
|
||||
rz.dist = (rz.dist || 0) + step; // ground covered — his walk cycle steps off this, not the clock
|
||||
rz.x += dx / d * step; rz.z += dz / d * step; rz.runT += dt;
|
||||
@@ -961,7 +963,9 @@ function racketAI(rz, dt) {
|
||||
if (rz.swingCool <= 0 && rz.swingT < 0
|
||||
&& Math.hypot(ball.x - rz.x, (ball.z - rz.z) * 0.8) < TUNE.aiReach
|
||||
&& ball.y < CONTACT.maxY && ball.y > CONTACT.minY) {
|
||||
const flub = TUNE.racketFlub * (G.match ? Math.pow(TUNE.matchFlubKeep, G.match.opp - 1) : 1);
|
||||
// ...and the match has its OWN flub base: the duel's 0.2 hands a fifth of all returns away,
|
||||
// which across a game is an opponent that beats itself.
|
||||
const flub = G.match ? TUNE.matchFlub * Math.pow(TUNE.matchFlubKeep, G.match.opp - 1) : TUNE.racketFlub;
|
||||
if (Math.random() < flub) rz.swingCool = 0.5; // whiff — it hesitates (rarer per rung of the ladder)
|
||||
else { rz.swingT = TUNE.swingWindup + TUNE.swingWindow / 2; trySwing(rz, true, TUNE.racketQMax); rz.swingT = -1; rz.swingCool = 0.3; }
|
||||
}
|
||||
|
||||
@@ -81,7 +81,7 @@ globalThis.ResizeObserver = class { observe() {} disconnect() {} };
|
||||
globalThis.getComputedStyle = () => ({ paddingTop: '0' });
|
||||
|
||||
// ---- game modules (real code) ----------------------------------------------
|
||||
const { G, P, score, ball, setupPoint } = await import('./src/state.js');
|
||||
const { G, P, O, score, ball, setupPoint } = await import('./src/state.js');
|
||||
const { update, forceZombies, splatPoint, restartRun, flickTarget } = await import('./src/gameplay.js');
|
||||
const { startDuel, resolveInterlude, startBossFight, endBossFight, startDrill: _startDrill, endDrill, startMatch: _startMatch, endMatch } = await import('./src/beats.js');
|
||||
// the drill's first-sight modal freezes the world (that is what modals do) — dismiss it the way a
|
||||
@@ -557,11 +557,36 @@ test('a match ball never wounds the body: the score is the only way through', ()
|
||||
assert.equal(zb.hp, zb.hpMax, 'and unwounded — "killing by smashing" is out of the match');
|
||||
});
|
||||
|
||||
test('THE DEATH LOOP: a fault before a lost point must not arm second-serve forever', () => {
|
||||
// The match bypasses tennis's pointTo — the only thing that ever reset G.serveNum — so the first
|
||||
// fault of the match made EVERY later missed serve a double fault: a point, blood, and repeat
|
||||
// until dead. Oliver hit it: "when trying to serve after i got hurt i get hurt again and again
|
||||
// until i die".
|
||||
startMatch();
|
||||
G.serveNum = 2; // a fault has burned the first serve
|
||||
resolveInterlude('O'); // the point is lost (double fault, passing shot, whatever)
|
||||
assert.equal(G.serveNum, 1, 'the NEXT point starts on a first serve');
|
||||
resolveInterlude('P');
|
||||
assert.equal(G.serveNum, 1, '...whoever won it');
|
||||
});
|
||||
|
||||
test('every point starts with the receiver on its mark', () => {
|
||||
startMatch();
|
||||
const rz = oppOf();
|
||||
rz.x = 6; rz.z = 12; // stranded mid-court by the last rally
|
||||
resolveInterlude('P');
|
||||
assert.ok(Math.abs(rz.x) < 3 && rz.z <= 1.5,
|
||||
'reset to the receiver spot, not wherever the rally left it: x=' + rz.x.toFixed(1) + ' z=' + rz.z.toFixed(1));
|
||||
assert.equal(rz.x, O.x, 'on the exact mark the tennis branch computed for the receiver');
|
||||
});
|
||||
|
||||
test('the ladder escalates in speed and reliability, not in hp', () => {
|
||||
// pure arithmetic on the knobs racketAI reads — pinned so a future tuning pass cannot silently
|
||||
// flatten the ladder again
|
||||
assert.ok(TUNE.matchSpeedBase > 1, 'opponent one is already quicker than the duel lumber');
|
||||
assert.ok(TUNE.matchSpeedPer > 0, 'each opponent is faster');
|
||||
assert.ok(TUNE.matchFlubKeep < 1, 'and whiffs less');
|
||||
assert.ok(TUNE.matchFlub < TUNE.racketFlub, 'the match whiffs less than the duel from the start');
|
||||
assert.ok(TUNE.matchFlubKeep < 1, 'and less again per rung');
|
||||
});
|
||||
|
||||
test('a game won heals, so a clean player can go the distance', () => {
|
||||
|
||||
Reference in new issue
Block a user