# 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. ## The score [`src/score.js`](src/score.js) — no level gets a chord somebody picked because it sounded nice. Every bed is derived from a real number belonging to its era: | hop | built from | |---|---| | coldboot / stack | **1000 Hz**, the reference tone every piece of telecoms test gear agrees on, folded down | | timewait | **50 Hz** mains and its harmonics — the room's own hum is the whole score | | war | the **1800 Hz** V.32 carrier, folded, with dial-up hiss under it | | smash | **0x90** — the NOP byte read as 144 Hz, so the sled has a pitch | | worm | seven partials, and the **seventh one is wrong** | | cuckoo | **45.45 baud**, the Model 33 teletype rate, as a clock | | board | the **1200 Hz** V.22bis carrier, folded four times — the doc's own instruction | | shell | the **750 Hz** terminal bell over Helsinki's 50 Hz mains | | handshake | *nothing.* The fight is the score and anything under it is noise | | copper | **20 Hz** ringing current, **50 Hz** mains, **10 pps** Strowger stepping — three real clocks beating against each other | | bluebox | the **six MF tones**. Literally what a blue box contains | | climb | all of it at once | | root | **2600 Hz**, folded six times until it is a low room tone, finally at rest | Some come out beautiful and some come out ugly. That's correct — a 1980 exchange wasn't tuned to A=440, and pretending otherwise would be the same lie as setting the ANSI art in a proportional font. ## 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. - 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.