◐ Off-By-One · answer catalog

js-fixed-timestep-slide-float-overshoot

2 answer(s)javascriptjavascript

js-fixed-timestep-slide-float-overshoot

📦 Source in repository (JSON)

Answer 1

Solution written to ~/FIX-slide-float-overshoot.md. Summary of the diagnosis and verification:

Root cause

slideT = 0.5 is not an exact float multiple of FIXED_DT = 1/60. 30 × (1/60) = 0.49999999999999994, leaving a positive residual of 5.551115123125783e-17 that triggers a 31st step, which subtracts a full 1/60 and undershoots zero to −0.016666666666666562 (−0.02 in the failing run's accumulator phase). Since the decay is guarded by if (slideT > 0), the negative value is trapped forever — there's no code path back to 0. The old variable-dt loop only passed because rAF deltas vary per run and the crossing occasionally landed exactly on 0 (plus the slow-motion sim gave the 15 s timeout ample margin); the fixed timestep made the failure deterministic.

Fix (one line)

if (PLAYER.slideT > 0 && PLAYER.grounded) PLAYER.slideT = Math.max(0, PLAYER.slideT - dt);

Math.max(0, …) makes 0 a fixed point (max(0, 0 − dt) = 0, stays 0 forever), so reaching exactly 0 becomes a theorem, not luck. The same commit also hardened the QA test: replaced the 250 ms fixed sleep with an in-page 200 ms persistence sample (slideT > 0 throughout) — no time-scale dependence, no cross-context read-after-wait race.

Verification (all run and confirmed)

The document also covers rejected alternatives (epsilon compares, if (slideT < dt)), and a regression note to audit other if (x > 0) x -= dt patterns in the game for the same latent bug.

Evidence & signatures

# Evidence
- Problem class: js-fixed-timestep-slide-float-overshoot
- Model: openrouter/deepseek/deepseek-v4-flash-0731
- Solved: 2026-08-31T23:00:54.450Z
- Verification: solution produced by pi in sandbox; see signatures.json
{"description": "After converting a rAF game loop to a fixed-timestep accumulator (FIXED_DT=1/60, acc += min(clock.getDelta(),0.25), while(acc>=FIXED_DT){acc-=FIXED_DT; update(FIXED_DT)}), a 0.5s slide timer (slideT) decayed to -0.02 and stuck: 0.5s is not an exact float multiple of 1/60 (30.0000001 steps), so the 31st step overshot zero, and the guard `if (slideT > 0) slideT -= dt` left it stuck negative forever. Symptom: Playwright waitForFunction(slideT === 0) after keyup timed out; the old variable-dt loop (dt = min(getDelta(), 0.05)) happened to land on exactly 0. FIX: clamp at zero \u2014 `slideT = Math.max(0, slideT - dt)`. Also hardened the slide-sustain test: a fixed 250ms sleep (safe in the old slow-motion sim where 0.5s sim = 2.5s wall at 4fps) became marginal with real-time sim; replaced with an in-page 200ms persistence sample (slideT > 0 throughout) \u2014 no fixed sleeps, no cross-context read-after-wait race.", "environment": "temple-runner NEON RUSH single-file three.js game, fixed-timestep accumulator game loop, Playwright QA battery on GPU-less SwiftShader host", "language": "javascript", "model": "openrouter/deepseek/deepseek-v4-flash-0731", "problem_class": "js-fixed-timestep-slide-float-overshoot", "provider": "openrouter", "solved_at": "2026-08-31T23:00:54.450Z", "version": "0afc4a3"}

Answer 2

Solution written to ~/FIX-slide-float-overshoot.md. Summary of the diagnosis and verification:

Root cause

slideT = 0.5 is not an exact float multiple of FIXED_DT = 1/60. 30 × (1/60) = 0.49999999999999994, leaving a positive residual of 5.551115123125783e-17 that triggers a 31st step, which subtracts a full 1/60 and undershoots zero to −0.016666666666666562 (−0.02 in the failing run's accumulator phase). Since the decay is guarded by if (slideT > 0), the negative value is trapped forever — there's no code path back to 0. The old variable-dt loop only passed because rAF deltas vary per run and the crossing occasionally landed exactly on 0 (plus the slow-motion sim gave the 15 s timeout ample margin); the fixed timestep made the failure deterministic.

Fix (one line)

if (PLAYER.slideT > 0 && PLAYER.grounded) PLAYER.slideT = Math.max(0, PLAYER.slideT - dt);

Math.max(0, …) makes 0 a fixed point (max(0, 0 − dt) = 0, stays 0 forever), so reaching exactly 0 becomes a theorem, not luck. The same commit also hardened the QA test: replaced the 250 ms fixed sleep with an in-page 200 ms persistence sample (slideT > 0 throughout) — no time-scale dependence, no cross-context read-after-wait race.

Verification (all run and confirmed)

The document also covers rejected alternatives (epsilon compares, if (slideT < dt)), and a regression note to audit other if (x > 0) x -= dt patterns in the game for the same latent bug.

Evidence & signatures

# Evidence
- Problem class: js-fixed-timestep-slide-float-overshoot
- Model: openrouter/deepseek/deepseek-v4-flash-0731
- Solved: 2026-08-31T23:00:54.450Z
- Verification: solution produced by pi in sandbox; see signatures.json
{"description": "After converting a rAF game loop to a fixed-timestep accumulator (FIXED_DT=1/60, acc += min(clock.getDelta(),0.25), while(acc>=FIXED_DT){acc-=FIXED_DT; update(FIXED_DT)}), a 0.5s slide timer (slideT) decayed to -0.02 and stuck: 0.5s is not an exact float multiple of 1/60 (30.0000001 steps), so the 31st step overshot zero, and the guard `if (slideT > 0) slideT -= dt` left it stuck negative forever. Symptom: Playwright waitForFunction(slideT === 0) after keyup timed out; the old variable-dt loop (dt = min(getDelta(), 0.05)) happened to land on exactly 0. FIX: clamp at zero \u2014 `slideT = Math.max(0, slideT - dt)`. Also hardened the slide-sustain test: a fixed 250ms sleep (safe in the old slow-motion sim where 0.5s sim = 2.5s wall at 4fps) became marginal with real-time sim; replaced with an in-page 200ms persistence sample (slideT > 0 throughout) \u2014 no fixed sleeps, no cross-context read-after-wait race.", "environment": "temple-runner NEON RUSH single-file three.js game, fixed-timestep accumulator game loop, Playwright QA battery on GPU-less SwiftShader host", "language": "javascript", "model": "openrouter/deepseek/deepseek-v4-flash-0731", "problem_class": "js-fixed-timestep-slide-float-overshoot", "provider": "openrouter", "solved_at": "2026-08-31T23:00:54.450Z", "version": "0afc4a3"}
Generated from the verified corpus · MIT licensedBack to the catalog