Skip to content

feat(games): fourteen more for the arcade — eight to twenty-two - #367

Merged
ralyodio merged 6 commits into
mainfrom
games-arcade-rest
Aug 11, 2026
Merged

feat(games): fourteen more for the arcade — eight to twenty-two#367
ralyodio merged 6 commits into
mainfrom
games-arcade-rest

Conversation

@ralyodio

Copy link
Copy Markdown
Contributor

Follow-up to #366, which merged while the rest of this was still being built: main got asteroids and blackjack, and these are the other fourteen. Same frame, same key decoder, same driver.

game
invaders forty of them, and the last one moves fastest
centipede shoot it in the middle and now there are two of them
breakout dig a channel up the side and let the ball do the rest
pong first to seven, and the angle is all in where you hit it
tank two tanks, one yard, five hits — line it up and let go
digdug dig the tunnels, pump the monsters, drop rocks on the rest
frogger the road kills what it touches, the river kills what it doesn't
kong five girders, four ladders, and a barrel with your name on it
pitfall jump the logs, swing the pits, and get the gold before dark
choplifter fly out, land, fill the back, and get them home
spyhunter keep it on the tarmac, shoot the ones shooting back
outrun a road that bends, traffic that doesn't, and a clock that always wins
excitebike turbo until it cooks, and land the way you took off
stagedive run the barricade, hop the gear, duck the crowd, take the picks

Every game is tested for winnable and losable

A bot that plays properly has to get somewhere; a bot that doesn't has to die. That pair is what caught the real bugs:

  • Invaders ranked its aliens two rows apart, making the fleet nine rows tall on a board with eleven above the bunkers. It "landed" after two drops and no wave was survivable by anybody. An aiming player now always dies to the bombs instead — never to the fleet, and that is the assertion.
  • Kong flipped a barrel's direction when it landed, sending it back the way it came and off the board; nothing reached the lower girders and a player who never moved could not be hit at all. Then, with one ladder per girder, the only way up was the chute the barrels come down — a coin toss you cannot jump out of. Two ladders and a fixed direction per girder later, a climbing bot reaches level 21.
  • Tank faced you and drove, which grinds into the same block for ever: the first duel finished 0–0 after twenty thousand ticks with neither tank having found the other. It searches the yard now.
  • Excitebike's landing was inert — the ramp set your pitch and nothing changed it, so the second half of the game could be ignored. The nose drops on its own now: ride it properly and you spill nothing, hold turbo and ignore it and you spill 200 times over eight runs.
  • Pitfall's swing was one column short of a pit; its jump landed on the back half of a log.
  • Pong rallied for ever against a perfect player. OutRun's checkpoints handed back more clock than the road cost.
  • Shots that moved faster than their target was tall passed straight through it, in three separate games. They travel in sub-steps now.

Not built

Donkey Kong's sloped girders and Pole Position's perspective road are the two things a terminal row cannot draw honestly. Kong is here with flat girders — the slope's job is kept, since each girder has a direction and the chute sits at its far end, so you meet barrels head-on rather than following them. Pole Position's road is here as OutRun, which is the same renderer with a clock worth playing against; a second copy with a lap counter would have been the same game twice.

189 tests in test/games.test.mjs; full suite 1546 pass, 0 fail. Played in a real pty as well as in the harness.

🤖 Generated with Claude Code

ralyodio and others added 6 commits August 11, 2026 13:14
`/games stagedive` (also `dive`, `runner`, `stage`). You are running the
barricade: hop the monitor wedges, clear the amp stacks, duck the crowdsurfers,
take the picks. One mistake is the whole run.

A side-scroller in a terminal is really a scrolling list — the runner never moves
along the x axis at all, everything else slides left past a fixed column. That is
why speed is measured in columns per tick, and why the gap between hazards is
multiplied by it: a jump lasts a fixed number of ticks, so a fair gap has to get
longer as the stage gets faster. Without that the stage eventually outruns the
jump and the game stops being losable-by-mistake and starts being unfair.

Three things to read and two ways to answer. Wedges and stacks are jumped, and
the stack is two rows tall so it takes the height of the jump rather than the
start of it. A crowdsurfer sits at head height: crouch and it goes over you,
because a crouched runner is one row and a standing one is two. `↓` in mid-air is
a slam instead, which is how you save a jump you took too early.

Every hazard's hitbox is its own art, so a hazard cannot hit wider than it is
drawn — the oldest bug in the genre. The tests play it: a jump at six columns
clears a wedge and a jump one column late still wrecks on a stack, and a player
with reflexes runs 25 seeded stages to the end while standing still and holding
jump both die on every one of them.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Five more for the cabinet, on the same frame, decoder and driver as the rest.

Space Invaders is the one with the joke the original hardware told by accident:
cadence() divides by the number left alive, so forty of them crawl and the last
one sprints. Bunkers take two hits a cell from either side — including yours,
which is the whole reason stepping out from under your own cover is a skill.

Breakout's paddle is a steering wheel rather than a wall: where the ball lands
on it decides the angle it leaves at, which is what lets you dig a channel up
the side. Pong's machine waits until the ball crosses the halfway line and then
moves too slowly to reach a corner from the middle, so a flat return it always
gets and a sharp one it never does.

Tank searches the yard rather than facing you and driving: breadth-first over
546 cells, every decision. "Turn towards the enemy" grinds into the same block
for ever, and the first version of this duel finished 0–0 after twenty thousand
ticks with neither tank ever having found the other.

Spy Hunter's road is generated a row at a time from the row before it, with the
bend pulled back towards the middle in proportion to how far out it already is.
A plain random walk parks the road against one edge and leaves it there, which
reads as a bug rather than a road.

Three bugs worth naming, all found by playing them without a terminal:

- The invader ranks were two rows apart, which made the fleet nine rows tall on
  a board with eleven above the bunkers. It "landed" after two drops and no wave
  was survivable by anybody. One row per rank, and an aiming player now always
  dies to the bombs instead — which is the test.
- Shots that move faster than their target is tall skipped straight through it.
  Both the cannon and the car now travel in sub-steps and check what they pass.
- The tank yard's size was declared next to the map it described, and disagreed
  with it. It is derived from the map now.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Centipede's body is not a linked list, it is a bag of segments that each obey
the same rule: walk sideways, and when something is in the way drop a row and
turn round. Modelled that way a shot to the middle needs no surgery — you delete
one segment and the ones behind it carry on, which is exactly what splitting
looks like. Every piece you shoot leaves a mushroom where it fell, so the field
thickens as you play and the next wave is harder for free.

The spider is in there because without it the bottom of the board is safe: the
centipede only reaches your row on the sweeps it happens to end on, and a player
who never moves at all survived three thousand ticks. It comes in from the side,
bounces diagonally through your strip eating mushrooms, and is worth more than
anything else on the board. Standing still now loses on every seed, which is the
test.

Frogger is the inversion: on the road, being on something is death; on the
river, being on nothing is. The log carries you, including off the end of the
world, which is the lesson every player learns twice. Landing on the bank
between the homes is a loss and so is a home you have already filled.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Dig Dug has no pathfinding, and that is the design: a monster walks the tunnel
it is in and turns towards you at a junction, so the shape you dig is the shape
of the fight. Dig one straight line and they queue up behind you. Once in a
while one gives up on the tunnels and comes straight through the ground, because
without that a player who digs a single deep hole is untouchable. The harpoon
only travels down open tunnel, three pumps burst a monster, and walking away
drops it — so the pump is a commitment, not a button.

Kong's girders are flat. The originals slope, and a slope is the one thing a
terminal row cannot honestly draw; faking it with half-blocks makes a board that
looks like the arcade and plays like a bug report. What the slope *does* is
kept: each girder has a direction, they alternate, and the barrel chute sits at
the far end of each one, so barrels cross a whole girder before they drop.

Two rounds of that layout were unplayable before this one, both found by playing
it without a terminal:

- Barrels flipped direction when they landed, which sent them back the way they
  came and straight off the board. Nothing ever reached the lower girders and a
  player who never moved could not be hit at all.
- With one ladder per girder, the only way up was the chute the barrels come
  down — a coin toss you cannot jump out of. Each girder has a second ladder
  now, and a climbing player reaches level 21 before the pace catches them.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Pitfall's vine is what stops it being another jumping game. A jump is short and
you commit to it the instant you press it; a swing is long and can only be
started where a vine hangs. The spawner never puts down a pit without a vine
over it, because a pit is five columns wide and a jump covers about five — a pit
with no vine is a pit nobody gets past, and that is a test rather than a promise.

Tuning it was the usual business of playing it without a terminal: the first
swing was sixteen ticks, which carried you over all of a pit except its last
column, and the first jump was seven, which cleared a log and landed you on its
back half. Long enough now that a player who jumps and swings at the right
moments is only ever beaten by the clock — which is the game Pitfall actually is.

Choplifter keeps its world in world columns and only turns them into screen
columns in render(). The map is three screens wide with the pad at the far left
end, so "get them home" is a journey rather than a step, and the camera follows
you. Four seats in the back, and a hit costs everybody in them.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Excitebike is two mechanics and a side-scroller to hang them on. Turbo is free
until it isn't — heat climbs while it is held and the engine seizes at the top of
the gauge, so a fast race is one that cools off in the right places. And a jump
is only as good as its landing: the nose drops on its own all the way down, and
you pitch against it.

That last part was inert in the first version. The ramp set your pitch and
nothing changed it, so whatever you left the ramp with you landed on, and the
landing — the second half of the game — could be ignored entirely. A rider who
holds turbo and never touches the pitch now spills two hundred times over eight
runs, where one who rides it properly spills none. That difference is the test.

OutRun is the only board in the cabinet that fakes a third dimension, and it
does it the way the eighties did: the road is not an object, it is a rule for
drawing a row. roadHalf() and centreAt() are the entire renderer, and the same
two functions decide what you have hit — so what is drawn and what is solid
cannot drift apart. The bend is squared against distance, which is why it opens
up towards the horizon and closes to nothing under your bumper, and why a corner
throws you at the outside of itself.

The clock is the opponent. The first checkpoint handed back 420 ticks for 260 of
road, which meant a competent driver's clock only ever grew; it is 220 for 320
now, which is about fifty ticks of slack at full speed and none at all if you
spin.

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

Copy link
Copy Markdown

ThreatCrush Security Scan

95 finding(s)

HIGH/CRITICAL: 5 | MEDIUM: 39 | 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 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/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
LOW secret-generic-credential apps/pwa/test/approvals-credits.test.mjs:28
LOW secret-generic-credential apps/pwa/test/approvals-notify.test.mjs:26

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

Snippets are redacted; ThreatCrush never prints matched credential material.

@ralyodio
ralyodio merged commit f00fe0b into main Aug 11, 2026
4 checks passed
@ralyodio
ralyodio deleted the games-arcade-rest branch August 11, 2026 13:17
@ralyodio ralyodio mentioned this pull request Aug 11, 2026
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