# 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.

## The philes

All 200 are written, in [`src/philes.js`](src/philes.js), and all 200 are findable.
Read them with `P`.

The voice rule is the whole thing: **each one is written by somebody, in the year it
happened, who did not know they were in a game.** Nobody explains anything for the
player's benefit and nothing is written in hindsight.

Where they live follows from that. The board is 1991, so the board cannot carry a
file about WannaCry — it carries the seven read-online areas (g-philes, the law, the
scene, virus, the iron, telephony, unix), and reading is free while *downloading*
costs MTU, which is how a real board worked. Everything after 1992 is on **the
climb**, because the climb is the only level that travels forwards in time: thirty-six
years bolted to the shaft wall, going past at speed. You get the file for having been
there whether or not you turned your head, and they arrive quietly — thirty-six
notifications in fifty seconds is a wall, not a level, so `ctx.phile(id, …, quiet)`
collects them and the count lands once at the top.

`node tools/philecheck.mjs` — duplicate ids in an object literal overwrite silently,
which cost an hour once. It also reports anything written but not placed in a level.

## 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.
- Levels run 5–12 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.
