Skip to content

games: smooth the ball in pong and breakout - #373

Merged
ralyodio merged 1 commit into
mainfrom
worktree-games-smooth-ball
Aug 12, 2026
Merged

games: smooth the ball in pong and breakout#373
ralyodio merged 1 commit into
mainfrom
worktree-games-smooth-ball

Conversation

@ralyodio

Copy link
Copy Markdown
Contributor

/games pong and /games breakout had a jiggly ball and a choppy redraw. Three causes, only one of them about the ball.

The clock was re-armed off every keypress

A held arrow key arrives as a burst of repeats, not one key. Each repeat cancelled the pending tick and restarted the period, so at any repeat rate faster than the tick the ball stopped dead for as long as you steered, then lurched. Measured on origin/main, holding the paddle key at 30/s:

ticks in 4s worst stall
before, key held at 30/s 0 ball never moved
before, key held at 14/s 57 (18/s idle → 14/s) 142ms
after, either rate 249 (62/s) 66ms

The clock now runs on its own deadline. A keypress may only pull the next tick nearer, never push it away — chess, whose tick rate depends on whose move it is, still gets its fast reply.

The tick was slower than the eye

The ball can only be drawn on whole cells. At 55ms and fifteen columns a second it advanced a cell on six ticks out of seven and stood still on the seventh; that stalled frame, three times a second, is the jiggle. Both games now tick at 16ms with speeds scaled to match — same ball, same place, same moment, but the stall is 16ms instead of 55.

mean gap jitter (sd) worst stall
before 64.4ms 20.6ms 112ms
after 62.5ms 5.6ms 66ms

The tuned constants are kept as written, per 55ms and scaled to the tick, because they are what makes the machine beatable off the end of the paddle and not from the middle. Gameplay speed and difficulty are unchanged.

The redraw erased the whole board

Twenty-one rows rewritten for a ball that moved one cell, and the erase-first is a visible blink. Only changed rows are written now, skipped over with a cursor-down rather than a newline so a frame at the bottom of the screen cannot scroll it. 15.8 KiB/s → 2.1 KiB/s, which is most of the "rendering delay" in the pit.

Higher tick rates are free because of this: a tick that leaves the ball in the same cell renders an identical frame, and identical frames were already never written.

Verification

  • Full suite green: 1621 pass, 0 fail (same 228 skips as origin/main).
  • Four new driver tests, each checked to fail against the old code: keypresses don't postpone the tick, a key can still pull it forward, a late clock catches up (capped), and a redraw writes only changed rows and lands the cursor back where it started.
  • The escape sequences were replayed through a small terminal emulator and the screen compared against the intended frame after every one of ~2,000 writes across pong idle, pong steering and breakout — proving the row-diff cursor arithmetic adds up and the board can't walk up the screen.

The clock and the painter are shared, so all 22 real-time games get both fixes.

🤖 Generated with Claude Code

Three separate things were making the ball jiggle, and only one of them was
about the ball.

The clock was re-armed off every keypress. A held arrow key does not arrive as
one key, it arrives as a burst of repeats, and each one cancelled the pending
tick and started the period again — so at any repeat rate faster than the tick,
the ball stopped dead for as long as you steered and then lurched. Measured on
pong at a 30/s repeat: 0 ticks in four seconds. The clock now runs on its own
deadline, and a keypress may only pull the next tick nearer, never push it
away. Chess still gets its fast reply; the ball no longer waits for you.

Pong and breakout ticked at 55 and 50ms. The ball can only be drawn on whole
cells, so at fifteen columns a second it advanced a cell on six ticks out of
seven and stood still on the seventh — and that stalled frame, three times a
second, is the jiggle. Both now tick at 16ms with the speeds scaled to match, so
the ball is in the same place at the same moment and the stall is 16ms instead
of 55. Jitter between column steps drops from 21ms to 6ms, worst stall from
112ms to 66ms. The tuned constants are kept as they were, written per 55ms and
scaled, because they are what makes the machine beatable off the end of the
paddle and not from the middle.

Redrawing erased the whole board and wrote it again, twenty-one rows for a ball
that moved one cell — the erase is a visible blink, and over the pit's socket it
is the delay. Only changed rows are written now, skipped over with a cursor-down
rather than a newline so a frame at the bottom of the screen cannot scroll it.
15.8 KiB/s down to 2.1 KiB/s.

The clock and the painter are shared, so every real-time game in the cabinet
gets the same two fixes.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown

ThreatCrush Security Scan

97 finding(s)

HIGH/CRITICAL: 5 | MEDIUM: 41 | LOW: 51

Severity Rule Location
HIGH js-ssrf-outbound-request apps/pwa/public/sw.js:45
HIGH tls-verification-disabled apps/pwa/src/lib/moshpit-gateway.mjs:299
HIGH sh-remote-script-execution install.sh:79
HIGH sh-remote-script-execution install.sh:83
HIGH tls-verification-disabled src/dns.mjs:741
MEDIUM sql-template-interpolation apps/pwa/src/moshpit.mjs:61
MEDIUM sql-template-interpolation apps/pwa/src/moshpit.mjs:75
MEDIUM sql-template-interpolation apps/pwa/src/moshpit.mjs:101
MEDIUM sql-template-interpolation apps/pwa/src/moshpit.mjs:265
MEDIUM sql-template-interpolation apps/pwa/src/moshpit.mjs:269
MEDIUM sql-template-interpolation apps/pwa/src/moshpit.mjs:314
MEDIUM sql-template-interpolation apps/pwa/src/moshpit.mjs:499
MEDIUM sql-template-interpolation apps/pwa/src/moshpit.mjs:675
MEDIUM sql-template-interpolation apps/pwa/src/moshpit.mjs:677
MEDIUM sql-template-interpolation apps/pwa/src/moshpit.mjs:736
MEDIUM sql-template-interpolation apps/pwa/src/moshpit.mjs:782
MEDIUM sql-template-interpolation apps/pwa/src/moshpit.mjs:852
MEDIUM sql-template-interpolation apps/pwa/src/moshpit.mjs:955
MEDIUM sql-template-interpolation apps/pwa/src/moshpit.mjs:1063
MEDIUM sql-template-interpolation apps/pwa/src/moshpit.mjs:1199
MEDIUM js-unescaped-html-sink apps/pwa/src/routes/moshpit.mjs:1419
MEDIUM js-timing-unsafe-mac-compare apps/pwa/src/routes/settings-sync.mjs:179
MEDIUM js-dynamic-code-execution apps/pwa/test/apikey-mask.test.mjs:129
MEDIUM sql-template-interpolation apps/pwa/test/credits-webhook-event-match.test.mjs:111
MEDIUM sql-template-interpolation apps/pwa/test/credits-webhook-event-match.test.mjs:131
MEDIUM sql-template-interpolation apps/pwa/test/moshpit-terms.test.mjs:192
MEDIUM sql-string-concatenation src/cli-schema.mjs:128
MEDIUM sql-string-concatenation src/cli-schema.mjs:417
MEDIUM sql-string-concatenation src/cli-schema.mjs:543
MEDIUM js-timing-unsafe-mac-compare src/settings-sync.mjs:309
MEDIUM insecure-temp-file test/dns-disable-restore.test.mjs:93
MEDIUM insecure-temp-file test/dns-disable-restore.test.mjs:310
MEDIUM insecure-temp-file test/news.test.mjs:479
MEDIUM insecure-temp-file test/plugins.test.mjs:152
MEDIUM insecure-temp-file test/pty.test.mjs:28
MEDIUM insecure-temp-file test/pty.test.mjs:31
MEDIUM insecure-temp-file test/pty.test.mjs:40
MEDIUM insecure-temp-file test/pty.test.mjs:42
MEDIUM insecure-temp-file test/pty.test.mjs:47
MEDIUM insecure-temp-file test/pty.test.mjs:48
MEDIUM insecure-temp-file test/pty.test.mjs:49
MEDIUM insecure-temp-file test/tabs.test.mjs:8
MEDIUM insecure-temp-file test/tabs.test.mjs:13
MEDIUM insecure-temp-file test/tabs.test.mjs:14
MEDIUM insecure-temp-file test/tabs.test.mjs:22
MEDIUM insecure-temp-file test/trust.test.mjs:240
LOW secret-generic-credential apps/pwa/test/apikey-bearer-scheme.test.mjs:30
LOW secret-generic-credential apps/pwa/test/apikey-mask.test.mjs:38
LOW secret-generic-credential apps/pwa/test/apikey-reveal.test.mjs:35
LOW secret-generic-credential apps/pwa/test/approvals-context.test.mjs:28

…and 47 more. Full results in the Security tab.

Snippets are redacted; ThreatCrush never prints matched credential material.

@ralyodio
ralyodio merged commit 4f746c1 into main Aug 12, 2026
4 checks passed
@ralyodio
ralyodio deleted the worktree-games-smooth-ball branch August 12, 2026 03:49
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant