Go to file
type-two cb22239363 The soundtrack is built out of the signal
The last structural promise in the design doc. No level gets a chord any
more. Every bed in the game is derived from a real number belonging to the
era it plays under, and src/score.js says which one and why in a comment
above each line.

  coldboot/stack  1000 Hz, the reference tone every piece of telecoms test
                  gear on earth agrees on. The fortress is tuned to a
                  calibration signal.
  timewait        50 Hz mains and its harmonics. The room's own hum was
                  always the entire score; now the bed agrees with it.
  war             the 1800 Hz V.32 carrier folded down, with dial-up hiss
                  under it, because everybody on EFnet was on a modem.
  smash           0x90 is the NOP. Read as a number it is 144, so the sled
                  has a pitch and the level is tuned to the byte you slide
                  down.
  worm            seven partials, and the seventh one is detuned. One in
                  seven, as a chord.
  cuckoo          45.45 baud — the Model 33 rate, 22 ms a bit. Fifty of
                  them in a basement is what he actually heard all night.
  board           the 1200 Hz V.22bis carrier folded four times, which is
                  the doc's exact phrase: a modem carrier detuned into a
                  pad.
  shell           the 750 Hz terminal bell over Helsinki's 50 Hz mains.
  handshake       silence. The fight is the score; anything underneath is
                  noise competing with the only thing in this game that
                  has to be listened to.
  copper          20 Hz ringing current, 50 Hz mains, and Strowger stepping
                  at ten pulses a second. Three real clocks, and the
                  cathedral is the beat they make against each other.
  bluebox         the six MF tones at once. That is literally what is
                  inside a blue box.
  climb           all of it, layered.
  root            2600 Hz folded six times until it is a low room tone.
                  The number the whole game turns on, finally at rest.

ESTABLISHED gets its own cue rather than a bed: 350 and 440, which is the
dial tone the handshake arena opened on and called the sound a line makes
when nothing is happening — with something finally on it.

Fixed while building: the pulse gate was driven by a bare square
oscillator swinging -1..+1, so its off half was not silence, it was the
same tone phase-inverted. Tremolo where a clock was wanted. DC offset now.

All 14 hops swept clean, and no voices leak between them.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-02 20:17:18 +10:00
.claude HANDSHAKE — 13 playable hops, one renderer 2026-08-02 15:34:58 +10:00
src The soundtrack is built out of the signal 2026-08-02 20:17:18 +10:00
vendor HANDSHAKE — 13 playable hops, one renderer 2026-08-02 15:34:58 +10:00
.gitignore HANDSHAKE — 13 playable hops, one renderer 2026-08-02 15:34:58 +10:00
DESIGN.md HANDSHAKE — 13 playable hops, one renderer 2026-08-02 15:34:58 +10:00
index.html Philes you can read, a save, and MTU that costs you something 2026-08-02 15:46:18 +10:00
README.md The soundtrack is built out of the signal 2026-08-02 20:17:18 +10:00
serve.py HANDSHAKE — 13 playable hops, one renderer 2026-08-02 15:34:58 +10:00
STAGES.md The soundtrack is built out of the signal 2026-08-02 20:17:18 +10:00

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

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 resolutionsrc/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 clothingmakeSyn() 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.

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 — 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 — 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, 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 38 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 for a per-hop breakdown of what's in and what's next.