Skip to content

Worktree games asteroids blackjack - #368

Merged
ralyodio merged 7 commits into
mainfrom
worktree-games-asteroids-blackjack
Aug 11, 2026
Merged

Worktree games asteroids blackjack#368
ralyodio merged 7 commits into
mainfrom
worktree-games-asteroids-blackjack

Conversation

@ralyodio

Copy link
Copy Markdown
Contributor

No description provided.

ralyodio and others added 7 commits August 11, 2026 12:04
`/games asteroids` and `/games blackjack` (also `21`), sharing the frame, the
key decoder and the driver every other game in the cabinet already uses.

Asteroids is the real-time one. The 1979 cabinet did not have to care that a
terminal cell is twice as tall as it is wide; everything here measures distance
in columns and counts a row as two, which is the only reason the rocks come out
round and a collision happens where the screen showed it. Rocks break into two
of the next size down, small ones pay most, the screen has no edges, and the
ship blinks while it cannot be hit.

Blackjack deals before the frame lands — no bet prompt, no options screen. Hit,
stand, double and split, dealer stands on all 17s, blackjack pays 3:2, and a
split 21 is twenty-one rather than a natural, which is the rule most home
versions get wrong. Between hands the arrows are the chips; the stack is 100 and
the game ends when it is gone.

Both are pure the way the rest of the arcade is, so test/games.test.mjs plays
them: it wrecks a ship, splits rocks down to nothing, walks a stack to broke and
proves the box does not change width between hands.

Fixes two things the letter games had been quietly losing to the shared decoder.
`h j k l` became arrows for every game, and `r` was always a restart — so
hangman could not be told the letters in `refactor`, and `h` could never mean
hit. Games that read letters now opt out with `vim: false` and
`restartable: false`; arrows are untouched, so blackjack still bets with them.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`/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>
@ralyodio
ralyodio merged commit 30c1ddc into main Aug 11, 2026
3 checks passed
@ralyodio
ralyodio deleted the worktree-games-asteroids-blackjack branch August 11, 2026 13:18
@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 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