haxtory/README.md
type-two bb9decb484 The philes: 200 of 200, written and placed
The writing job, done. 89 new philes takes the store to 200, and every one
of them is now reachable in play — which turned out to be the harder half.

Where they live follows from the voice rule. Each phile is written by
somebody, in the year it happened, who did not know they were in a game —
so the board is 1991 and the board cannot carry a file about WannaCry.

  the board   seven read-online areas: g-philes, the law, the scene,
              v i r u s, the iron, telephony, unix. Reading online is free
              and downloading costs MTU, which is how a board actually
              worked, and is the only way ~60 files fit inside an economy
              built to make you choose.
  the climb   everything after 1992, 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.
  the shell   man(1) and cat(1) on kruuna, for the unix and network ones.

ctx.phile(id, ..., quiet) collects without a toast — thirty-six
notifications in fifty seconds is a wall, not a level. The count lands once
at the top of the climb.

Two real bugs found on the way:

  handshake   exit() guarded .tone instead of .b, and there is a window
              between S.i++ and enter() where a boss is current but its
              state does not exist. Leave via the traceroute map during a
              title card and unmount throws. Async continuations now bail
              on a dead level too.
  the climb   the camera does not travel, the shaft does. The new signs
              were nailed to the origin and never came into view. They also
              needed dark ink for the one zone whose walls are white.

tools/philecheck.mjs: duplicate ids in an object literal overwrite
silently, and that cost an hour. It also reports anything written but not
placed. 200/200 written, 200/200 findable.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-02 23:00:36 +10:00

174 lines
9.1 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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` | 28 | 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` | 1016 | 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` | 1720 | 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` | 2128 | 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` | 2934 | LBL 1986 terminal. connection time is visibility. you get caught |
| `board` | 3544 | 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` | 4344 | 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` | 4550 | five-boss rhythm arena. dial tone → DTMF → ANSam → V.8bis → carrier |
| `copper` | 5158 | FP through duct, load coils, a Strowger cathedral, a lineman's handset |
| `bluebox` | 5963 | 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 512 minutes against a design doc that asks for 520. 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.