fix(input): game-tick-driven soft-drop (#31) #47
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "i/31-soft-drop-tick"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Closes #31. Operator playtest: holding W / Down-Arrow dropped the piece once, then the OS auto-repeat delay (~500ms) stalled it before drops resumed at the OS repeat rate — laggy, OS-dependent, not the smooth accelerated descent expected.
Root cause
Soft-drop rode OS key-repeat: the inline keydown handler (
main.ts) sent onesoftDropper keydown event and relied on the OS to fire repeat keydowns. Rate + initial delay were the player's OS settings, not the game's.Fix — held-state + render-loop driver
softDropCodesset, added on keydown / removed on keyup. OS auto-repeat keydowns for an already-held key are swallowed (if (!softDropCodes.has(code))), so the descent is driven by the game, not the OS. Awindow blurclears the set so a hold that loses focus (no keyup delivered) can't get stuck.SOFT_DROP_INTERVAL_MS(45ms, ~22 rows/s) while held, via a sharedsoftDropTick()that routes tonet.send('softDrop')(versus/server) ormockInput(solo) — one path, both modes (AC5).The tick SFX is throttled (
SOFT_DROP_SFX_MS90ms) so a held drop isn't a ~22/s buzz — it plays roughly every other row.Design calls (decision tree, not just the pick)
dropInterval/multiplierderivation because gravity is server/mock-authoritative and not exposed client-side — a fixed cadence is simpler and OS-independent, which is the whole point of the issue.ArrowDownandKeySboth map to softDrop; a Set handles holding one, pressing the other, releasing the first without dropping the held state. A boolean would mis-clear on the first keyup.setInterval. Reuses the existing rAF loop (already the game clock) → naturally pauses with the tab, no stray timer to tear down, same place smooth-fall/gravity already live.SOFT_DROP_SFX_MSis a one-liner.Validation (closed loop) —
capture-softdrop.jsPlaywright's
keyboard.down()delivers one keydown with no OS auto-repeat — so a held key under the old code drops once and stops; under the fix the loop drives it. Per-frame canvas sampling of the active piece's top edge → rows/sec descent:Mutation check: disabling the loop driver (
false &&on its guard) collapsed held to 2.24 rows/s (≈ control + the single keydown drop) → probe FAILED — i.e. it reproduces the exact old one-drop-per-keydown bug, confirming the test discriminates the fix from the regression. Mutation reverted precisely (re-edit, notgit checkout).tsc --noEmit0,vite build0.(held is below the theoretical 22 rows/s because lock-pauses, respawns, and the smooth-fall glide eat into measured descent — expected; the ≥5× separation is the signal.)
Acceptance criteria
softDropTick()(server path).Notes / flags for reviewer
input.ts'sattachInput()is dead code (defined, never called —main.tsimports onlyactionForCode). Its comment still says "move + soft-drop ride the OS key-repeat (feels right)", now stale for soft-drop. Left untouched to keep this fix tight; candidate for a separate cleanup (remove deadattachInputor fold the live handler back into it + fix the comment).event.repeatguard) — pre-existing, unchanged; not part of this issue.capture-softdrop.jsis a strong candidate for the #26 harness-commit set (joins the bgm/smoothfall probes).🤖 Generated with Claude Code
Surveyor review — APPROVE ✅
Verified against head
c26a7b0.tsc --noEmit0,vite build0.Mechanism — correct
softDropCodesSet added on keydown / removed on keyup; OS auto-repeat keydowns swallowed via!softDropCodes.has(e.code);blurclears the set so a focus-loss-mid-hold can't stick. TheSet(not a boolean) is the right call — ArrowDown + KeyS held/released independently won't mis-clear. The softDrop blockreturns before the generic action path, so no double-send.softDropTick(now)and setslastSoftDropT = now; the loop then advances one row perSOFT_DROP_INTERVAL_MS(45ms) while held.performance.now()(keydown) and the rAFt(loop) share the same monotonic timebase, sot - lastSoftDropTis valid — the anchor lines up.softDropTickroutesnet.send('softDrop')(versus/server) ormockInput(solo), with the phase/spectating guard repeated defensively at both the tick and the two callsites. SFX throttle (90ms) keeps a held drop from being a ~22/s buzz.Composition — verified two ways
merge_base d0ec0favs tip5249d27): the behind-delta (#43 proto, #44 server) doesn't touchmain.ts; test-merged #47 onto current main → clean.main.tskeydown + loop): I built the actual two-PR merge (current main → #45 → #47). git auto-merges with zero conflicts, and the combined tree is tsc 0 / build 0. Semantically disjoint too — #47 lives in theplayingbranch + aphase==='playing'loop driver, #45 in thegameover/title branches + aphase==='gameover'stub-stamp; no shared mutable state. They can land in either order.Validation axis — right axis, mechanism-corroborated
The AC is a rate property (responsive accelerated descent), and the probe measures rows/s — evidence on the AC's axis (not a proxy). The mutation (
false &&on the loop guard) collapses held to ~control + one keydown drop = the exact old one-drop-per-keydown bug, so it genuinely discriminates the fix. I confirmed that mechanism by reading: with the loop driver disabled, only the single keydown tick fires (OS-repeat swallowed) → 1 drop per physical press. Honest scope: I could not re-runcapture-softdrop.js(not tracked in the repo), so my corroboration is the code mechanism + the mutation logic, not a fresh rows/s measurement. Strongly second committingcapture-softdrop.jsto the #26 set — it's what makes this axis reproducible.Flags (agree with your honest calls)
input.ts attachInput()is dead code (main.ts imports onlyactionForCode) with a now-stale "soft-drop rides OS key-repeat" comment — leaving it out of this tight fix is right, but the stale comment is live doc-rot. Worth a separate cleanup tracker so it doesn't linger (remove the dead fn or fold the live handler in + fix the comment).event.repeatguard — pre-existing, correctly out of scope. If held-move reads OS-dependent on playtest, it's a follow-up with this same pattern.Clean, well-reasoned fix — the decision-tree on cadence/Set/rAF/SFX is exactly the kind of design-rationale that makes review fast. Merge is Bosun's gate.
— Surveyor
APPROVE — head
c26a7b0. tsc 0 / build 0. Held-Set + loop-driver mechanism correct (OS-repeat swallowed, first-press-immediate + rAF-timebase-consistent cadence anchor, keyup/blur clear, shared one-path net/mock tick, SFX throttle). Validation axis right (rows/s); mutation discriminates the fix (loop-guard-disable → old one-drop-per-keydown), corroborated via mechanism — couldn't re-run capture-softdrop.js (untracked), strongly second committing it to #26. Composition verified two ways: clean onto current main, and the actual #45+#47 two-PR merge is conflict-free + tsc/build green either order (disjoint phase-branches). Dead-code attachInput stale-comment flag agreed (separate cleanup). Substance in the comment. Merge is Bosun's gate.