Fix the early-fall soft-lock: the decided latch must die with its rally
Oliver's retest: 'the fall never happens, the game does not continue, the ball is gone.' Root cause: the decided latch was set AFTER resolveInterlude, and on a point with no stage change matchPoint re-arms the next rally synchronously — so the latch outlived the rally it scoped and swallowed every resolution after it. Two changes: matchFallWillPlay() gates the early resolution to points that actually put a collapse (or the game-winning kill) on court — an ordinary point keeps its ordinary bounce-2 ending, so the ball never vanishes mid-roll — and with only those paths admitted, the after-set latch is provably safe: both defer matchReady until their beat ends. A regression test drives the exact soft-lock sequence through the real update loop. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
53df079db6
commit
f991f1ab10
3 files changed
+54
-6
No files matched your search
@@ -785,6 +785,32 @@ test('the fall beats the ball: a passing winner fells the body while the ball st
|
||||
assert.ok(!G.match.decided, 'the latch cleared for the next rally');
|
||||
});
|
||||
|
||||
test('an ordinary far winner keeps its ordinary ending, and the match plays on (the soft-lock)', () => {
|
||||
// The first shipped version of the early fall latched `decided` AFTER resolveInterlude — and on
|
||||
// a point with no stage change matchPoint re-arms the next rally synchronously, so the latch
|
||||
// outlived its rally and swallowed every resolution after it: no falls, no points, no serve,
|
||||
// ever again. Oliver: "the fall never happens, the game does not continue, the ball is gone."
|
||||
startMatch();
|
||||
G.mode = 'splatter';
|
||||
G.interlude.introT = 0;
|
||||
const zb = oppOf(); // full pool: this point crosses NO stage
|
||||
zb.x = -4; zb.z = 2;
|
||||
const launch = () => {
|
||||
ball.active = true; ball.lastHit = 'P'; ball.bounces = 0; ball.wid = 'ball'; ball.collected = false;
|
||||
ball.x = 3.4; ball.z = 7; ball.y = 0.5; ball.vx = 0.5; ball.vy = -2; ball.vz = -6; ball.g = TUNE.gravity;
|
||||
G.phase = 'rally'; G.wasServe = false;
|
||||
};
|
||||
launch();
|
||||
const pts = score.pts[0];
|
||||
for (let i = 0; i < 5 * 60 && score.pts[0] === pts; i++) update(DT);
|
||||
assert.equal(G.matchFall, null, 'no stage to show: no early fall, the ordinary ending played');
|
||||
assert.equal(score.pts[0], pts + 1, 'the point banked once');
|
||||
assert.ok(!G.match.decided, 'and the latch is not left armed');
|
||||
launch(); // THE NEXT RALLY MUST STILL RESOLVE — the lock test
|
||||
for (let i = 0; i < 5 * 60 && score.pts[0] === pts + 1; i++) update(DT);
|
||||
assert.equal(score.pts[0], pts + 2, 'the match plays on');
|
||||
});
|
||||
|
||||
test('service alternates by game, and the opponent actually serves', () => {
|
||||
// Until now the corpse only ever returned: score.server was pinned to 'P' because the match calls
|
||||
// advancePoint directly and the flip lives in tennis's pointTo, which the match bypasses. Return
|
||||
|
||||
Reference in new issue
Block a user