Skip to content

Arcade: give the touch pad the cartridge's own control map - #703

Merged
JSv4 merged 3 commits into
mainfrom
claude/mobile-demo-room-controls-32z6pk
Sep 6, 2026
Merged

Arcade: give the touch pad the cartridge's own control map#703
JSv4 merged 3 commits into
mainfrom
claude/mobile-demo-room-controls-32z6pk

Conversation

@JSv4

@JSv4 JSv4 commented Sep 6, 2026

Copy link
Copy Markdown
Owner

Why

The Arcade plays well on a desktop and only partly on a phone. The thumb pad shipped with four arrows and a round FIRE button — which is every control the platformer has, and about half of the ones a Doom-format level needs.

Missing on touch: strafe, sprint, weapon switching, the automap, Doom's own menu, and — decisively — USE. Freedoom's E1M1 puts its first door about thirty seconds in, and a door is a key press (E). A phone had no key to press, so the real Doom cartridge could be walked that far and no further.

What changed

The pad's buttons now come from the cartridge instead of being fixed in the dock. Each cartridge declares a touch profile beside the keyboard map it mirrors (cart.touch), and arcade-dock.js fills its fixed slots from that profile through a new setPad, which startArcade calls on every cartridge change:

cartridge pad
¶ Pilcrow's Quest walk ←/→, , round JUMP
▓ Dungeon · ☩ Freedoom E1M1 forward/back, turn (↺ ↻), strafe (◀ ▶), FIRE, RUN
☩ DOOM all of the above plus USE, and a KEYS tray: weapons 1–7, MAP, MENU,
attract screen one round START — nothing on a title card is steerable

Geometry stays in the dock; the control map stays with the cartridge, next to the keyboard controls line it has to agree with. A slot the cartridge does not use is hidden and loses its data-code, so a button can never send a key the cartridge ignores. Turning rotates (↺ ↻) and strafing translates (◀ ▶), so the two pairs of side buttons — one row apart on the same D-pad — never wear the same arrow. Doom's rarer keys ride in a tray above the action cluster rather than crowding the game screen, because they are what you reach for between fights rather than during one.

Mechanism

Three touch behaviours the keyboard never needed:

  • RUN latches. Shift is a held modifier; on a phone the hand that would hold it is the one steering. Tap it on, tap it off, with aria-pressed showing which.
  • Presses are tracked per pointer id. Firing with one thumb while turning with the other is two live pointers, and releasing one must lift only its own key. The pressed button also captures the pointer, so a thumb sliding off it still releases the key rather than leaving it stuck down.
  • Pausing releases whatever the pad holds (so does switching cartridge). A latched sprint must not survive into the paused document, where the same key is an ordinary Shift again.

The pad is wired by delegation and reads each button's code at press time. That is what lets one set of DOM nodes follow the cartridge and lets the tray mint its keys on the spot — a listener that captured a code at boot would go on sending the previous cartridge's key.

Validation

Headless checks (docs/demo/tools/ascii-arcade.test.mjs, run by npm run test:demo-logic) hold every profile to the keyboard map beside it:

  • every touch button sends a key the arcade actually claims while playing (a code outside that set would reach the document, not the game);
  • a thumb can reach every function Doom gives a keyboard — compared as Doom's own key bytes, so keyboard aliases don't paper over a missing function. Deleting USE from the profile fails this check with 0xa2 missing, which is the bug this PR fixes;
  • turn and strafe never share a glyph, and no two buttons in a profile share a name;
  • the code the pad labels "strafe" moves the player perpendicular to their heading without turning them — driven through the real raycaster simulation, so a profile that quietly re-pointed strafe at the turn keys fails.

Browser specs (npm/tests/demo-arcade-mobile.spec.ts, phone-shaped project) cover the dock in isolation — each cartridge's slots, the tray opening and being withdrawn, a hidden slot losing its code — and the live arcade: strafe and the RUN latch on the raycaster, USE and a weapon chip on Doom, the latch not surviving a pause, and the pad following a cartridge switch. The attract-screen assertion rides the existing intro spec.

Locally I ran the headless suites (green) and exercised the dock and its event path in a real Chromium on a Pixel 5 viewport — profile switching, tray, latch, release-on-pause, tray chips. The Playwright specs themselves need the WASM bundle, which this container cannot build (no .NET SDK), so they run for the first time in CI.

🤖 Generated with Claude Code

https://claude.ai/code/session_01WJWJUpxAvfMfopNVxtwbvW


Generated by Claude Code

claude and others added 3 commits September 6, 2026 00:13
The Arcade's thumb pad shipped with four arrows and a FIRE button. That is
every control the platformer has, and roughly half of the ones a Doom-format
level needs: no strafe, no sprint, no weapon switch, no automap, no way into
Doom's own menu — and no USE. E1M1's first door is thirty seconds in, and a
door is a key press, so on a phone the real Doom cartridge could be walked
that far and no further.

The pad now takes its buttons from the cartridge. Each cartridge declares a
touch profile beside the keyboard map it mirrors (`cart.touch`), and
`arcade-dock.js` fills its fixed slots from that profile through a new
`setPad`, which `startArcade` calls whenever the selected cartridge changes:

  quest            walk, jump
  dungeon / e1m1   + back, strafe (KeyA/KeyD), RUN
  doom             + USE, and a tray of weapons 1-7, MAP, MENU and Enter
  attract screen   one round START — nothing on a title card is steerable

Geometry stays in the dock, the control map stays with the cartridge. A slot
the cartridge does not use is hidden AND loses its data-code, so a button can
never send a key the cartridge ignores. Turning rotates and strafing
translates, so the two pairs of side buttons never wear the same arrow.

Three touch behaviours the keyboard never needed:

  · RUN latches instead of asking to be held — on a phone the hand that would
    hold Shift is the one steering;
  · presses are tracked per pointer id, so firing with one thumb while turning
    with the other lifts only the key that came up, and the pressed button
    captures the pointer so a thumb sliding off it still releases its key;
  · pausing (and switching cartridge) releases everything the pad holds, so a
    latched sprint cannot survive into the paused document.

The buttons are wired by delegation and read their code at press time, which
is what lets one set of DOM nodes follow the cartridge and lets the tray mint
its keys on the spot.

Tests: headless checks hold every profile to the keyboard map beside it —
every touch button sends a key the arcade claims while playing, a thumb can
reach every function Doom's key table gives a keyboard, turn and strafe never
share a glyph, and the code the pad labels "strafe" moves the player
perpendicular to their heading without turning them (driven through the real
raycaster simulation). Browser specs cover the dock in isolation (per
cartridge slots, the tray, a hidden slot losing its code) and the live arcade
(strafe and the RUN latch on the raycaster, USE and a weapon chip on Doom,
and the pad following a cartridge switch).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WJWJUpxAvfMfopNVxtwbvW
`npm run typecheck` runs two passes, and the second (`tsconfig.tests.json`,
which type-checks the Playwright specs) rejected the new dock spec: the
`import('./arcade-dock.js')` calls inside `page.evaluate` are browser code
loading files the harness serves beside the page, but TypeScript resolves a
literal specifier from the spec's own directory and finds nothing there.

Build the specifier from a variable instead, the same way the CDN specs load
`embed.bundle.js` — the runtime string is identical and resolves against the
page, and TypeScript no longer tries to resolve it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WJWJUpxAvfMfopNVxtwbvW
@JSv4
JSv4 merged commit 58a0c73 into main Sep 6, 2026
13 checks passed
@JSv4
JSv4 deleted the claude/mobile-demo-room-controls-32z6pk branch September 6, 2026 01:03
JSv4 pushed a commit that referenced this pull request Sep 6, 2026
A clean auto-merge, so the changelog placement check ran on its own merits
rather than to settle a conflict — the same reason it ran for v12.0.1. It
confirms one copy of each [Unreleased] entry with this branch's REDLINE THEATER
entry inside the section (line 47, ahead of the next released heading at 88).
The missing blank line before "### Fixed" is main's own shape, not something
this merge introduced, so it stays as main wrote it.

Verified but not rebuilt and not re-measured. #703 touches the arcade demo's
own JavaScript, its node checks and a new mobile spec — no npm/src, so the
embed bundle the redline demo loads is byte-identical, and nothing near the
diff, save or conversion paths the published figures describe. The shared
surfaces are the changelog, docs/demo/README.md and the demo-logic test list,
which is why the checks still ran: 61 redline node checks (45 theater, 16
stress), the arcade and engine-pin checks main just changed, and 14 browser
assertions.
JSv4 added a commit that referenced this pull request Sep 6, 2026
Moves the accumulated `### Changed` (the Arcade's per-cartridge touch pad,
#703) and `### Fixed` (the editor block drag handle clipped to the editor's
own viewport, #702) out of `[Unreleased]` under a dated 12.1.0 heading. A
`### Changed` section is present, so this is a minor bump rather than a patch.

Also restores the blank line between the `### Changed` block and the `### Fixed`
heading, which #703's entry had closed up — the release body copies those
sections verbatim, and a heading butted against the previous paragraph does not
render as a heading.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SxehJkp2547AyP8LSd6uFS
JSv4 pushed a commit that referenced this pull request Sep 6, 2026
Main cut v12.1.0, moving the accumulated "### Changed" (#703) and "### Fixed"
(#702) out of [Unreleased] under a dated heading. Textually that move is just a
heading inserted above entries this branch also carries, so git auto-merged it
with three insertions and no conflict — and left this branch's "### Added" sitting
underneath the new "## [12.1.0]" heading, filed into a version that has already
shipped, with [Unreleased] empty above it.

That is exactly the failure the placement check exists to catch, and it caught it:
the check prints the REDLINE THEATER line only while it is still inside
[Unreleased], and after the auto-merge it printed nothing. The entry is moved back
under [Unreleased]; the released section keeps main's two entries, in main's order,
with the blank line main restored before "### Fixed". The diff against main is now
purely additive — this branch adds its own entry and touches nothing of main's.

Worth stating plainly: a clean auto-merge is not evidence the changelog is right.
This is the second release cut in a row where the sweep earned its keep, and the
first where the damage was invisible in the merge output.

The demo pin needs no move yet. All eleven references across the seven pages still
read docxodus@12.0.1, so they agree and the pin check passes; jsDelivr answers 404
for 12.1.0 because Publish is still in flight. Re-pinning waits for main's own
re-pin and a 200.

Verified: 61 redline node checks (45 theater, 16 stress), 3 pin checks, 14 browser
assertions. Nothing rebuilt or re-measured — the merge carries changelog prose only.
JSv4 pushed a commit that referenced this pull request Sep 7, 2026
…asuring

Main's range 6f140f0..2a1e6f9 is mostly two large-but-inert changes — #728
corrects misattributed Microsoft copyright headers across ~200 post-fork files,
and #721 adds the ASCII DOOM showcase — plus the tail of the history archive
work. npm/package.json conflicted in the usual shape (both sides extending
pretest and test:demo-logic) and was resolved as a verified union: main's
doom-ascii copy and check alongside this branch's four redline entries, split on
" && ", diffed list against list, and revalidated with a JSON parse. The demo
logic suite is eight checks now.

Two things in that range could move the published diff figures, so this merge
re-measured rather than assuming: the WASM project now passes -Oz to the SDK's
Binaryen post-link pass, and #721 re-recorded docxodus.aotprofile (1.34 MB to
1.65 MB). Profile-guided AOT is exactly what halved these numbers in #653, so
both deserved a reading rather than a guess.

The figures do not move, and the table stays put for the third time. A pooled
reading — two controlled fixed-input sessions plus a three-depth stress run —
came out at 46 / 56 / 103 ms with ratios 32x / 41x / 89x. That is the band the
README already records for its second session (41 / 55 / 101 at 34x / 45x / 80x),
not a new one, and two of three ratios held. Only full + HTML crossed the noise
threshold, and it moved UP while its absolute moved down — the recording path got
quicker too, from about 2 ms to 1.3 ms, so the deepest pipeline merely failed to
keep pace. That is the opposite of a faster-engine signature. The README gains a
paragraph recording the reading, because "a size-optimizing link pass and a wider
AOT profile did not move the ratio" is worth knowing precisely because both sound
like they should.

One correction to this branch's own recent history: the last few merge messages
said they rebuilt "through the full pretest". They did not. pretest runs the demo
checks, typechecks and copies already-built artifacts into dist/wasm; npm run
build is what compiles the WASM engine and the bundles. dist/wasm/Docxodus.wasm
was dated 2026-09-06T01:05 — the #702 merge — so #703, v12.1.0 and all three
history batches were verified against that engine rather than a fresh one. The
specs passed, but they did not test what those messages claimed. This merge runs
the real build: Docxodus.wasm 3.47 MB to 3.80 MB, framework total up 708 KB,
which is the history subsystem and the wider AOT profile arriving at last. The
measurement above is therefore of everything since #702, not of this range alone,
and it is reported that way.

Verified on the genuinely rebuilt engine: 61 redline node checks, 3 pin checks,
14 browser assertions.
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.

2 participants