The uname escalation. rms does not get angrier, he gets tireder — each interjection shorter and flatter and typed faster than the last, down to "GNU/linux.", then "...", then "no. i am not doing this again." And then he stops trying to change your mind and goes and rebuilds uname so that it says GNU/Linux, and installs it, and does not ask anybody. Which is, in fairness, the entire strategy. He never won the argument about naming and was never going to. What he did instead was write a compiler, a linker, a shell, a libc and a debugger, put all of them under a licence that forces everyone downstream to keep them free, and wait. That part worked completely. THE RETRAIN — the last cross-cutting system in the design doc that was not in. Real modems renegotiated mid-call: the copper got wet, somebody picked up an extension, the exchange rerouted you through a worse pair, and the two ends stopped and did the whole handshake again in the middle of your download. So now, once per run, somewhere after hop 51, wherever you happen to be standing, the line goes and takes a baud rate off you. The way back is the beat you were taught at hop 47: SPACE on the ANSam phase reversal, 450 ms, four clean ones. Spamming it fails, correctly. Terminal levels opt out with noRetrain, because taking SPACE off somebody mid-sentence is not a mechanic, it is a bug with a story attached. Fixed while testing: the reversal flash was decaying on a setTimeout while the beat it was cueing ran on dt. Two clocks for one event. Frame-based now, which is both more correct and the reason the win path is reachable. 75 philes. All 14 hops swept clean. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
129 lines
6.8 KiB
Markdown
129 lines
6.8 KiB
Markdown
# 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](DESIGN.md).
|
||
|
||
## Run it
|
||
|
||
```bash
|
||
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
|
||
|
||
Fourteen 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 descent, **one verb per layer**: transcode, keep the session, reassemble in sequence, route within a hop budget, listen-before-transmit with real exponential backoff, ride the waveform. 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, and a **trojan arms race** — four weapons the level patches 70 s after you first use one |
|
||
| `smash` | 17–20 | 2D side-scroll `char buf[64]`. canary, return address, NOP sled, shellcode — then **NX lands** and you build a **ret2libc** chain out of other people's platforms |
|
||
| `worm` | 21–28 | Morris worm RTS, 1-in-7 reinfection locked on. An **antidote you can release** that travels only over live links, while you are killing links |
|
||
| `cuckoo` | 29–34 | LBL 1986 terminal. connection time is visibility. you get caught |
|
||
| `board` | 35–44 | the BBS, over **three days**. busy signal, ANSI painting at live baud, ratios, NUP gate, ANSI bomb, the sysop watching you type, FidoNet, playable LORD *and* Trade Wars — then LAST CALL, and it closes |
|
||
| `shell` | 43–44 | kruuna.helsinki.fi, 1991. always on, 38 users at once, talk(1) character by character, the comp.os.minix post, and a kernel compiling in the background while you use the shell |
|
||
| `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, **one gate per era** testing the verb it taught you. Spectre is a real **split screen**, plus Rowhammer and Log4Shell |
|
||
| `root` | 64 | the room is empty. then the ACK comes back — and on Tuesday, so does your FidoNet message |
|
||
|
||
## 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`](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`](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.
|
||
|
||
## Retrain
|
||
|
||
The line doesn't stay the way you negotiated it. Somewhere after hop 51, wherever you
|
||
happen to be, it degrades and takes a baud rate off you — and the way to claw it back
|
||
is the ANSam beat from hop 47: `SPACE` on the phase reversal, 450 ms, four clean ones.
|
||
Terminal levels opt out with `noRetrain: true`, because stealing `SPACE` from someone
|
||
mid-sentence isn't a mechanic, it's a bug with a story attached.
|
||
|
||
## Audio
|
||
|
||
[`src/audio.js`](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** — 74 written in [`src/philes.js`](src/philes.js), all readable with `P`. Adding the next 126 is a writing job, not a code job: put the text in that file, call `ctx.phile('id')` wherever it should be found.
|
||
- **Era-generated music** — the design doc says each level's pad is built from that era's protocol. Right now they're chords.
|
||
- Levels run 3–8 minutes against a design doc that asks for 5–20. The genres, mechanics and set pieces are all in — the *duration* is still short. See [STAGES.md](STAGES.md) for a per-hop breakdown of what's in and what's next.
|