THE WAR — the trojan arms race. WinNuke, Sub7, Smurf, DCC send. Each one works, and then the level patches it seventy seconds after you first use it, with the countdown on screen and a line about how it died: a hotfix went out; the scanners caught up; the amplifiers stopped answering broadcast; the client asks before it accepts now. You cannot finish the level on one tactic. An exploit is not a tool you own, it is a window that is already closing while you are climbing through it. Plus a rival crew who turn up after the first rejoin, who have read the same text files you have, and who will take the ghost crown out from under you during a split. THE WORM — you can try to stop it. An antidote, three seeds, and it travels only over live links while you are killing links. It cures fast and then stops dead with its component severed, while the death count climbs past it. Measured: 4, 12, 18, 19, stall — dead 20 through 30. It works perfectly on everything it can reach. It cannot reach the rest, because you already took the road. The phones ring all night and nobody can send mail to warn anybody, because the broken thing is the thing you would have used. Pacing fix: spread rate now scales with the infection, since every infected host is running a copy. Correct, and the only way the level ever reached its own milestones — before this, nothing died inside ten minutes. THE BOARD — three days. Day one as before, and you post to FidoNet on the way out. Then the 04:00 run dials five nodes across the world while nobody is awake. Day two the board knows you: last on yesterday, calls 2, user #44 of 44 — and the first message is FIDONET quoting what you actually typed, with an answer. Four days, three boards, a long distance call somebody else paid for. Received. Hello. If you over-took on the files, your ratio has been adjusted to 1:1, because he said it would be. Day three is LAST CALL. Not raided, not busted: the second line was never coming, the long distance is forty dollars a month out of a job he does not much like, and a third of them have shell accounts now and are not calling any more, and he does not blame them, because it is better, it is genuinely better. Nine years. Forty-four users. Six real names. Never met one of them. Ratio off, elite board open, the NUP given away because it does not matter now. He asks nobody to say anything sad in the message base. They all ignore him. 65 philes. All 13 hops swept clean. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|---|---|---|
| .claude | ||
| src | ||
| vendor | ||
| .gitignore | ||
| DESIGN.md | ||
| index.html | ||
| README.md | ||
| serve.py | ||
| STAGES.md | ||
HANDSHAKE
a love story in 64 hops
You are SYN. You want ACK. Root is the only one who can give it to you.
Design doc: DESIGN.md.
Run it
python3 /Users/johnking/Documents/handshake/serve.py 8123
Then open http://localhost:8123/. Headphones — every frequency in this is the real
frequency and half the mechanics are inaudible without them.
serve.py is http.server plus Cache-Control: no-store, because ES modules cache
hard and it makes every edit look like it did nothing.
Controls
WASD |
move |
SPACE |
jump / hold 2600 Hz |
| mouse | look (click to capture; dragging works too) |
E |
use |
M |
traceroute — the level select, and where you drop cargo |
P |
philes — read what you've picked up |
` |
hop select (dev) |
ESC |
release the mouse |
It saves. Every hop completes a handshake with the world and writes to
localStorage; the boot screen offers R — RESUME when there's something to resume.
What's here
Thirteen playable hops, each a different genre, one renderer.
| id | hop | what it is |
|---|---|---|
coldboot |
1 | FP tutorial in a 512-slot NIC ring buffer, then the DMA rips you out |
stack |
2–8 | 7-floor third-person descent. NX floors, rate gates, canaries, ASLR reshuffles on death, stateful FIREWALL boss |
timewait |
9 | walking sim. beige room, 50 Hz hum, half-open connections in plastic chairs, FIN by the door |
war |
10–16 | EFnet twin-stick. netsplit riding, ops crowns, Sub7 body-snatch |
smash |
17–20 | 2D side-scroll char buf[64]. canary, return address, NOP sled, shellcode |
worm |
21–28 | Morris worm RTS with the 1-in-7 reinfection locked on. you lose by winning |
cuckoo |
29–34 | LBL 1986 terminal. connection time is visibility. you get caught |
board |
35–44 | the BBS. busy signal, ANSI painting at live baud, ratios, NUP gate, ANSI bomb, the sysop watching you type, FidoNet, playable LORD |
handshake |
45–50 | five-boss rhythm arena. dial tone → DTMF → ANSam → V.8bis → carrier |
copper |
51–58 | FP through duct, load coils, a Strowger cathedral, a lineman's handset |
bluebox |
59–63 | playable 13-button blue box. 2600, KP/MF/ST, tandem stacking, then SS7 |
climb |
63→1 | on-rails reverse ascent. Spectre split-timeline, Rowhammer, Log4Shell |
root |
64 | the room is empty. then the ACK comes back |
The three load-bearing ideas
TTL is the health bar and it only goes down. 64 hops, no healing, ever. Levels aren't hard, they're expensive.
Bandwidth is resolution — src/link.js. One render target and one
shader. Your baud rate sets the target height; at 1200 and below the scene renders as
ASCII glyphs. HS.link.setBaud(0..6) in the console to see it. The V.8bis fight sets
this for real.
Every header you drop is a piece of clothing — makeSyn() in
src/kit.js. Seven wireframe shells, one per OSI layer, shed as you
descend.
MTU
Files taken off the BBS are lore, and lore is paid for in distance. Two is free; after that you don't fit down the next hop in one piece and every hop costs a TTL per extra fragment. You can put things down again from the traceroute map. That's the whole economy: everything worth reading costs you some of the distance you have left.
Audio
src/audio.js — every tone is oscillators, nothing is sampled, and
every frequency is the real one: 350+440 dial tone, the DTMF matrix, R1 MF pairs with
real 100 ms KP / 60 ms digit timing, 2600 Hz, ANSam at 2100 Hz reversing phase every
450 ms, Bell 103 FSK. About 300 lines and zero megabytes.
Layout
index.html shell, all CSS, importmap
serve.py dev server, no-store
src/main.js router, TTL, traceroute map, the loop
src/link.js bandwidth → resolution. the post shader.
src/audio.js every tone in the game
src/term.js 80x25 cell terminal, pipe colours, CP437 blocks as geometry
src/kit.js body/collision, camera rigs, SYN, labels, seeded rng
src/input.js keys, pointer lock, the ANSI bomb's remap table
src/hud.js status line, subtitles, title cards, NO CARRIER
src/levels/*.js one file per hop
vendor/ three.js r17x, vendored
Each level exports { id, hop, title, via, baud, mount(ctx), update(dt,t,ctx), unmount(ctx) }.
ctx hands it a fresh scene, camera and collision world, plus say/card/hint/phile/ spendTTL/setBaud/next. Genre switches are just different camera + controller modules
over one renderer — that's the whole trick.
Dev handles
HS on window: HS.goto(id), HS.link.setBaud(n), HS.game, HS.philes,
HS.run(n) — drives n frames by hand, because requestAnimationFrame is throttled
to zero in a hidden tab and that makes the game look broken when it's only asleep.
Skipped
- Multiplayer (
CHECKSUM) — the Journey-style anonymous pass-by. Needs a server; add when there's something to connect to. - Playing both fragments — fragmenting costs TTL, it doesn't split you into two packets you alternate between. Add when a level has a corridor narrow enough to need it.
- The 200 philes — 40 written in
src/philes.js, all readable withP. Adding the next 160 is a writing job, not a code job: put the text in that file, callctx.phile('id')wherever it should be found. - Trade Wars — the second door game. LORD is real and playable; TW2002 is "closed for maintenance since 1994", which is both a joke and true.
- Levels are one hop's worth of content each, not the full 5–20 minutes the design doc describes. The genres, mechanics and set pieces are all in — the duration isn't.