diff --git a/README.md b/README.md index 2e55d2b..c5a322d 100644 --- a/README.md +++ b/README.md @@ -36,23 +36,24 @@ localStorage; the boot screen offers `R — RESUME` when there's something to re ## What's here -Thirteen playable hops, each a different genre, one renderer. +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 third-person descent. NX floors, rate gates, canaries, ASLR reshuffles on death, stateful FIREWALL boss | +| `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, Sub7 body-snatch | -| `smash` | 17–20 | 2D side-scroll `char buf[64]`. canary, return address, NOP sled, shellcode | -| `worm` | 21–28 | Morris worm RTS with the 1-in-7 reinfection locked on. you lose by winning | +| `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. busy signal, ANSI painting at live baud, ratios, NUP gate, ANSI bomb, the sysop watching you type, FidoNet, playable LORD | +| `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. Spectre split-timeline, Rowhammer, Log4Shell | -| `root` | 64 | the room is empty. then the ACK comes back | +| `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 @@ -114,6 +115,7 @@ to zero in a hidden tab and that makes the game look broken when it's only aslee - **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** — 40 written in [`src/philes.js`](src/philes.js), all readable with `P`. Adding the next 160 is a writing job, not a code job: put the text in that file, call `ctx.phile('id')` wherever it should be found. -- **Trade Wars** — the second door game. LORD is real and playable; TW2002 is "closed for maintenance since 1994", which is both a joke and true. -- Levels are one hop's worth of content each, not the full 5–20 minutes the design doc describes. The genres, mechanics and set pieces are all in — the *duration* isn't. +- **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. +- **The retrain** — a mid-game renegotiation that drops your baud without warning. One system, touches every level after it. +- 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. diff --git a/STAGES.md b/STAGES.md index ae1f86a..d2f2bf7 100644 --- a/STAGES.md +++ b/STAGES.md @@ -3,7 +3,7 @@ > One page per hop. What's built, what's thin, what I'd add, ranked. > Write your direction under **DIRECTION** and I'll build to it. -Current state: 13 playable hops, ~8,000 lines, all clean. +Current state: **14 playable hops**, ~8,300 lines, all clean. **Since the first pass** (you gave me the wheel; these were my calls): the philes are readable, the game saves, MTU costs you distance, ROOT reads your whole save and @@ -209,6 +209,39 @@ is read-only. --- +## 8b · THE SHELL — `src/levels/shell.js` +*hops 43–44 · terminal · ~6 min · whatever you earned* + +**Why it exists:** day three of THE BOARD already names the thing that kills it — +*"about a third of you have got shell accounts now and are not calling any more, and +i do not blame you, because it is better, it is genuinely better."* The player never +got to see it. Now they do, immediately afterwards, and the board's ghost is still +warm. + +**In:** kruuna.helsinki.fi, 1991. It connects instantly — no dial tone, no ring, no +busy — and that silence after eight minutes of the board is the whole opening beat. +`who` shows eight of the thirty-eight people logged in *at this second*, against a +board that had forty-four users in nine years and could hold one at a time. `news` +is the 25 August 1991 comp.os.minix post, the Freax/linux directory story, and the +January 1992 GPL relicence. `talk` puts every character on screen as it is typed, +because that is what talk did. And `make` compiles a kernel **in the top four lines +of the screen while you carry on using the shell underneath it** — two things at +once, which the board could never do, demonstrated rather than stated. + +**Thin:** it is mostly reading. One real interaction (the compile) and one scripted +one (the talk). + +**What I'd add, ranked** +1. **Let the player be talked *to* unprompted.** Somebody opens a talk request while you are mid-command. That is the actual texture of a shared machine. +2. **`finger` and `.plan` files.** The whole social layer of a Unix box was people leaving notes in a file anyone could read. +3. **Quota.** 4MB, enforced. Try to keep something and have to delete something else. +4. **The Tanenbaum debate.** "LINUX is obsolete", comp.os.minix, January 1992 — a second news thread, and it is funnier and sadder than the first. + +**DIRECTION:** +> _ + +--- + ## 9 · THE HANDSHAKE — `src/levels/handshake.js` *hops 45–50 · rhythm boss rush · ~6 min · sets your baud* @@ -325,7 +358,7 @@ TIME_WAIT with FIN saying *told you*, then it loops to hop 1. | | | |---|---| -| **The philes** | 66 of 200 written. All in `src/philes.js`, all readable with `P`. Pure writing. | +| **The philes** | 73 of 200 written. All in `src/philes.js`, all readable with `P`. Pure writing. | | **Audio** | Every tone is real but the *music* isn't built — the design says each level's pad is generated from that era's protocol and right now they're just chords. | | **The retrain** | A mid-game renegotiation that drops your baud without warning. One system, touches every level after it. | | **Difficulty** | Nothing scales. TTL is the only pressure and it's linear. | diff --git a/src/levels/shell.js b/src/levels/shell.js new file mode 100644 index 0000000..1f19302 --- /dev/null +++ b/src/levels/shell.js @@ -0,0 +1,372 @@ +// HOPS 43–44 — THE SHELL +// +// The thing that killed the board. It is on all the time. There is no busy signal. +// Thirty-eight people are logged in at this exact second, which is fewer than the +// board had in nine years and more than the board could ever have at once. +// +// And it can do two things at the same time. That is the whole level: a kernel +// compiles in the top of the screen while you talk to somebody in the bottom, and +// on the board you could not have done either of those while doing the other. + +import * as THREE from '../../vendor/three.module.js'; +import * as A from '../audio.js'; +import * as In from '../input.js'; +import { Term } from '../term.js'; + +let S = {}; + +const MOTD = `|10 + SunOS 4.1.3 (kruuna) ttyp4 + +|07 welcome to kruuna.helsinki.fi + + * quota is 4MB. it is not negotiable and it is not enough. + * do not run anything big before 18:00. + * be decent to each other. we can all see who you are. + +|08 you have mail. +`; + +// Real people, on a real box, at the same time as you. The board had one line. +const WHO = [ + ['torvalds', 'ttyp0', '11:02', '(kruuna)'], + ['jkh', 'ttyp1', '09:14', '(freefall)'], + ['ari', 'ttyp2', '10:47', '(nic.funet.fi)'], + ['lars', 'ttyp3', '08:31', '(sirppi)'], + ['you', 'ttyp4', 'now', '(-)'], + ['minna', 'ttyp5', '11:20', '(otax)'], + ['pekka', 'ttyq0', '07:55', '(kruuna)'], + ['hannu', 'ttyq1', '11:31', '(cs.hut.fi)'], + ['rms', 'ttyp7', '06:02', '(gnu.ai.mit.edu)'], +]; + +// The post. 25 August 1991, comp.os.minix. Quoted short, because it is somebody's +// actual words and the famous part is one sentence long anyway. +const POST = `|08 Newsgroups: comp.os.minix + From: torvalds@kruuna.helsinki.fi (Linus Benedict Torvalds) + Date: 25 Aug 91 20:57:08 GMT + Subject: What would you like to see most in minix? + +|07 he says he is doing a free operating system. he says it is + |15"just a hobby, won't be big and professional like gnu"|07. + + he says it will only ever run on 386 AT clones, because that is the + only hardware he has. he asks what people would like to see in it. + he says he will get to it |15"if anybody is interested"|07. + +|08 ───────────────────────────────────────────────────────────────── +|07 he wanted to call it Freax. + + the man who ran the ftp server did not like Freax, so when he made + the directory to put it in he called it |15linux|07 instead, and never + mentioned it, and that was that. + + five months later, version 0.12, he changes the licence to the GPL. + that is the part that matters. not the code — the code was ten + thousand lines and most of it was a terminal driver. +`; + +// He is right, and the level had it coming: every line of that build log is gcc, +// which is GNU, and the level calls the result "a whole operating system". +const TALK_RMS = [ + "i'd just like to interject for a moment.", + 'what you are referring to as linux is in fact GNU/linux. or as i have', + 'recently taken to calling it, GNU plus linux.', + '', + 'linux is not an operating system unto itself. linux is the kernel: the', + 'program that allocates the machine to the other programs. it is essential', + 'and it is useless on its own.', + '', + 'look at what you just ran. scroll up. every line of it says gcc.', + '', + 'gcc is ours. so is the linker you called. so is make. so is the shell you', + 'typed make into, and the coreutils, and the libc it linked against. we', + 'started in 1983 and by 1991 we had all of it finished except a kernel.', + '', + 'he wrote the kernel. it was the last piece and it was a very good piece.', + 'the whole system is basically GNU with linux added.', + '', + '...', + 'you may go back to what you were doing. i am aware of how i come across.', +]; + +const TALK = [ + ['minna', 'you are new'], + ['you', ''], + ['minna', 'i can see you typing. that is not a trick, that is just what talk is.'], + ['minna', 'it sends every character as you hit it. no send button. no enter.'], + ['minna', 'where were you before'], + ['you', ''], + ['minna', 'oh — a board. one line?'], + ['minna', 'my brother had one. i used to have to wait for him to finish.'], + ['minna', 'there are thirty-eight of us on here right now and nobody is waiting.'], + ['minna', '...'], + ['minna', 'sorry. that was a horrible thing to say to somebody who just got here.'], +]; + +// A kernel build, in the background, while you do something else. +const BUILD = [ + 'gcc -Wall -O -fomit-frame-pointer -c boot/head.S', + 'gcc -Wall -O -fomit-frame-pointer -c init/main.c', + 'gcc -Wall -O -fomit-frame-pointer -c kernel/sched.c', + 'gcc -Wall -O -fomit-frame-pointer -c kernel/fork.c', + 'gcc -Wall -O -fomit-frame-pointer -c kernel/traps.c', + 'gcc -Wall -O -fomit-frame-pointer -c kernel/signal.c', + 'gcc -Wall -O -fomit-frame-pointer -c mm/memory.c', + 'gcc -Wall -O -fomit-frame-pointer -c mm/page.s', + 'gcc -Wall -O -fomit-frame-pointer -c fs/buffer.c', + 'gcc -Wall -O -fomit-frame-pointer -c fs/inode.c', + 'gcc -Wall -O -fomit-frame-pointer -c fs/namei.c', + 'gcc -Wall -O -fomit-frame-pointer -c fs/read_write.c', + 'kernel/chr_drv/tty_io.c:104: warning: unused variable `c\'', + 'gcc -Wall -O -fomit-frame-pointer -c kernel/chr_drv/console.c', + 'gcc -Wall -O -fomit-frame-pointer -c kernel/blk_drv/hd.c', + 'gcc -Wall -O -fomit-frame-pointer -c kernel/blk_drv/floppy.c', + 'gcc -Wall -O -fomit-frame-pointer -c lib/string.c', + 'ld -M boot/head.o init/main.o kernel/kernel.o mm/mm.o fs/fs.o -o vmlinux', + 'nm vmlinux | grep -v \'\\(compiled\\)\\|\\(\\.o$\\)\' | sort > System.map', +]; + +export default { + id: 'shell', hop: 43, title: 'THE SHELL', via: 'kruuna.helsinki.fi', ms: '31 ms', + baud: null, + + async mount(ctx) { + S = { t: 0, done: false, built: false, building: false, buildAt: 0, talked: false, read: false, + rmsWaiting: false, metRms: false }; + window.__S = S; // debug handle + + ctx.scene.background = new THREE.Color(0x000000); + ctx.camera.position.set(0, 0, 5); + ctx.termCanvas.style.display = 'block'; + const term = new Term(ctx.termCanvas); + ctx.term = term; S.term = term; + In.captureText(e => { if (term.busy) { term.skip = true; return true; } return term.key(e); }); + ctx.hint(''); + + S.script = this.run(ctx, term); + }, + + async run(ctx, term) { + const baud = () => Math.max(1200, ctx.link.rate); + term.clear(); + + // ── it just connects. no dial tone. no ring. no busy. that is the whole shock. + await term.paint('|07$ ', 1200); + await ctx.sleep(500); + await term.paint('telnet kruuna.helsinki.fi\n', 900); + await ctx.sleep(260); + await term.paint('|08Trying 128.214.6.100...\nConnected to kruuna.helsinki.fi.\n', 2400); + await ctx.sleep(200); + ctx.say('no dial tone. no ring. no busy signal. it was just already there.', 5600); + await term.paint('\n|07login: |15syn\n|07Password:\n', 1600); + await ctx.sleep(700); + await term.paint(MOTD, baud()); + await ctx.sleep(900); + ctx.phile('always-on'); + + ctx.hint('who news talk make logout'); + ctx.say('type help. it is a real shell and it is on all the time.', 5000); + + for (;;) { + if (S.done) return; + this.tickBuild(ctx, term); + const raw = (await term.ask('|02syn@kruuna|07:|11~|07$ ')).trim(); + if (S.done) return; + const [cmd, arg] = raw.split(/\s+/); + term.writeln(''); + if (!cmd) continue; + + if (cmd === 'help') { + term.writeln('|08 who news talk minna make uptime logout'); + term.writeln('|08 (and it will keep compiling while you do all of those.)'); + } else if (cmd === 'who') { + WHO.forEach(([u, tty, at, from]) => + term.writeln(`|${u === 'you' ? '15' : '11'} ${u.padEnd(10)}${tty.padEnd(8)}${at.padEnd(8)}|08${from}`)); + term.writeln(''); + term.writeln('|03 thirty-eight logged in. the board had forty-four users in nine years'); + term.writeln('|03 and could hold exactly one of them at a time.'); + ctx.phile('multiuser'); + } else if (cmd === 'uptime') { + term.writeln('|11 11:34am up 91 days, 3:12, 38 users, load average: 2.14, 1.88, 1.51'); + term.writeln('|08 ninety-one days. it has not been switched off since february.'); + } else if (cmd === 'news' || cmd === 'rn') { + await term.paint(POST, baud()); + S.read = true; + ctx.phile('the-post'); + } else if (cmd === 'talk') { + if ((arg || '').toLowerCase() === 'rms') await this.rms(ctx, term); + else await this.talk(ctx, term); + } else if (cmd === 'make') { + if (S.built) { term.writeln('|08 make: `vmlinux\' is up to date.'); continue; } + if (S.building) { term.writeln('|12 it is already building. look up.'); continue; } + S.building = true; S.buildAt = 0; S.buildLine = 0; + term.writeln('|10 make: building the kernel. it will take a while.'); + term.writeln('|03 do something else while it runs. you could not do that before.'); + ctx.phile('multitask'); + } else if (cmd === 'logout' || cmd === 'exit') { + return this.logout(ctx, term); + } else { + term.writeln(`|12 ${cmd}: Command not found.`); + } + } + }, + + // ── the compile, in the top four lines, while you get on with your life + tickBuild(ctx, term) { + if (!S.building) return; + const save = { x: term.cx, y: term.cy, f: term.fg }; + const n = Math.min(4, BUILD.length - S.buildLine); + for (let i = 0; i < 4; i++) { + term.goto(0, i); + const li = S.buildLine - 3 + i; + const txt = li >= 0 && li < BUILD.length ? BUILD[li] : ''; + term.fg = txt.includes('warning') ? 14 : 8; + term.write((txt ? ' ' + txt : '').padEnd(79).slice(0, 79)); + } + term.goto(save.x, save.y); term.fg = save.f; + void n; + }, + + async talk(ctx, term) { + term.writeln('|10 [Connection established]'); + await ctx.sleep(600); + for (const [who, line] of TALK) { + this.tickBuild(ctx, term); + if (who === 'you') { + const said = await term.ask('|15 '); + void said; + continue; + } + // talk sent every character as it was typed. no send button. no enter. + term.write('|11 '); + for (const ch of line) { + term.write(ch); + if (ch !== ' ') A.blip(1500 + Math.random() * 300, 0.006, 0.014); + await ctx.sleep(28 + Math.random() * 34); + } + term.writeln(''); + await ctx.sleep(500); + } + term.writeln('|08 [Connection closed]'); + S.talked = true; + ctx.phile('talk'); + ctx.say('she saw every character as you typed it. the board made you press enter and wait a day.', 6000); + }, + + async rms(ctx, term) { + S.rmsWaiting = false; + term.writeln(''); + term.writeln('|10 [Connection established]'); + await ctx.sleep(700); + for (const line of TALK_RMS) { + this.tickBuild(ctx, term); + if (!line) { term.writeln(''); await ctx.sleep(340); continue; } + term.write('|14 '); + for (const ch of line) { + term.write(ch); + if (ch !== ' ') A.blip(1400 + Math.random() * 200, 0.005, 0.012); + await ctx.sleep(17 + Math.random() * 20); + } + term.writeln(''); + await ctx.sleep(260); + } + term.writeln('|08 [Connection closed]'); + S.metRms = true; + ctx.game.flags.gnuPlusLinux = true; + ctx.phile('gnu'); + ctx.say('he is right, and it took him nine years of saying it to be heard once.', 6000); + }, + + async logout(ctx, term) { + S.done = true; + In.releaseText(); + term.clear(); + + if (S.building && !S.built) { + // it finishes whether you are watching or not, because it is not your computer + await term.paint('|08 [1]+ Done make\n', 1600); + S.built = true; + } + await ctx.sleep(600); + await term.paint(`|07 + it is better. + + it is on all the time. it holds thirty-eight people. it does two things + at once. it costs eighteen dollars a month and there is no ratio and + nobody is waiting for the line. + +|03 and in january he put it under a licence that says anybody can have it, + and change it, and give it away, as long as they let the next person do + the same. + +|07 he could have sold it. he was twenty-one and he could have sold it and + he did not, and that is the entire reason any of the rest of this exists. + +|08 (and the compiler, and the linker, and the shell, and make, and libc were + already finished and already free, and had been for years, and somebody on + ttyp7 would very much like that mentioned.) +`, Math.max(1500, ctx.link.rate)); + if (!S.metRms) { + await ctx.sleep(900); + await term.paint('|08\n You have new mail.\n |14 From: rms@gnu.ai.mit.edu Subject: GNU/Linux\n' + + '|08 (you never answered the talk request. he wrote it down instead.)\n', 1500); + await ctx.sleep(2200); + } + await ctx.sleep(1200); + await term.paint('|08\n somewhere a phone line is ringing out and nobody is answering it.\n', 1400); + await ctx.sleep(2600); + ctx.game.flags.sawTheShell = true; + term.writeln(''); + term.writeln('|08Connection closed by foreign host.'); + await ctx.sleep(1800); + ctx.spendTTL(1); + await ctx.card('THE SHELL', 'hop 43 · it is better, and that is what is sad about it', 4200); + ctx.next(); + }, + + update(dt, t, ctx) { + S.t += dt; + if (!S.building || S.built) return; + // it compiles in real time whether you are looking at it or not + S.buildAt += dt; + if (S.buildAt > 1.15) { + S.buildAt = 0; + S.buildLine++; + A.blip(900 + Math.random() * 200, 0.008, 0.02); + if (S.term) this.tickBuild(ctx, S.term); + if (S.buildLine >= BUILD.length) { + S.building = false; S.built = true; + const save = { x: S.term.cx, y: S.term.cy, f: S.term.fg }; + S.term.goto(0, 0); S.term.fg = 10; + S.term.write(' vmlinux built. a whole operating system, on a machine you do not own.'.padEnd(79)); + for (let i = 1; i < 4; i++) { S.term.goto(0, i); S.term.write(''.padEnd(79)); } + S.term.goto(save.x, save.y); S.term.fg = save.f; + A.burst(880, 1320, 0.3, 0.16); + ctx.say('vmlinux — and it compiled while you were talking to somebody.', 5200); + ctx.phile('vmlinux'); + // and somebody on ttyp7 has been waiting a very long time to say something + S.rmsWaiting = true; + setTimeout(() => { + if (!S.term || S.done) return; + const sv = { x: S.term.cx, y: S.term.cy, f: S.term.fg }; + S.term.writeln(''); + S.term.writeln('|14Message from rms@gnu.ai.mit.edu on ttyp7 at 11:41 ...'); + S.term.writeln('|14talk: connection requested by rms@gnu.ai.mit.edu.'); + S.term.writeln('|14talk: respond with: talk rms'); + S.term.write('|02syn@kruuna|07:|11~|07$ '); + void sv; + A.blip(1800, 0.05, 0.06); + ctx.say('somebody on ttyp7 has been waiting since six in the morning to say something.', 5200); + }, 2600); + } + } + }, + + async unmount(ctx) { + In.releaseText(); + ctx.termCanvas.style.display = 'none'; + ctx.term = null; + ctx.hint(''); + }, +}; diff --git a/src/main.js b/src/main.js index 7f0c7a3..9077328 100644 --- a/src/main.js +++ b/src/main.js @@ -16,6 +16,7 @@ import smash from './levels/smash.js'; import worm from './levels/worm.js'; import cuckoo from './levels/cuckoo.js'; import board from './levels/board.js'; +import shell from './levels/shell.js'; import handshake from './levels/handshake.js'; import copper from './levels/copper.js'; import bluebox from './levels/bluebox.js'; @@ -24,7 +25,7 @@ import root from './levels/root.js'; export const ORDER = [ coldboot, stack, timewait, war, smash, worm, - cuckoo, board, handshake, copper, bluebox, climb, root, + cuckoo, board, shell, handshake, copper, bluebox, climb, root, ]; const BY_ID = Object.fromEntries(ORDER.map(l => [l.id, l])); diff --git a/src/philes.js b/src/philes.js index e4d76dc..952adc9 100644 --- a/src/philes.js +++ b/src/philes.js @@ -614,6 +614,117 @@ keep calling."`, ignore me about and i find that i do not mind."`, }, + // ── hop 43–44, the shell ─────────────────────────────────────────────── + + 'always-on': { + title: 'no busy signal', + from: 'the first thing you notice', + body: `it is on. + +not "it is on in the evenings when the sysop is not using the phone". not +"it is on if nobody else got there first". on, at four in the afternoon on +a tuesday, and at three in the morning, and in february when nobody has +touched it for ninety-one days. + +everything the board was — the ratio, the time limit, the wanting, the +whole feeling of somewhere you had to get into — was a consequence of one +phone line in one man's spare room. take the scarcity away and all of it +goes, the bad and the good together.`, + }, + + 'multiuser': { + title: 'thirty-eight at once', + from: 'who(1)', + body: `the board had forty-four users in nine years and could hold exactly one +of them at a time. + +this holds thirty-eight right now, and none of them know each other, and +none of them are waiting.`, + }, + + 'multitask': { + title: 'two things at once', + from: 'make(1), in the background', + body: `it is compiling a kernel in the top of the screen while you are talking +to somebody in the bottom of it. + +on the board you could read a message or play a door game or download a +file, and while you did any one of those the other forty-three people got +a busy signal. + +this is not a better version of that. it is a different kind of object.`, + }, + + 'the-post': { + title: 'what would you like to see most in minix?', + from: 'comp.os.minix, 25 August 1991', + body: `he says he is doing a free operating system. he says it is "just a hobby, +won't be big and professional like gnu". he says it will only ever run on +386 AT clones because that is the only hardware he has. + +he wanted to call it Freax. the man who ran the FTP server did not like +Freax, so when he made the directory he called it linux, and never +mentioned it, and that was that. + +version 0.01 was about ten thousand lines and a good deal of it was a +terminal driver, because what he had actually set out to write was a +program for dialling into the university with. + +that is worth sitting with for a second. the thing that ended the era of +the board began life as a terminal emulator for calling one.`, + }, + + 'talk': { + title: 'talk(1)', + from: 'every character, as you hit it', + body: `no send button. no enter. it puts each character on their screen at the +moment you type it, and theirs on yours, both at once, on a split screen. + +you watch people think. you watch them start a sentence, delete it, and +start again, and there is nothing you can do about having seen it. + +we replaced it with something where you compose in private and then +publish, and we called that an improvement, and it might have been.`, + }, + + 'vmlinux': { + title: 'vmlinux', + from: 'a whole operating system, on a machine you do not own', + body: `version 0.12, January 1992: he relicenses it under the GPL. Anybody can +have it, change it, and give it away, provided they let the next person do +the same. + +he was twenty-one. he could have sold it. + +that one decision is why the machine this is running on, and the machine +in your pocket, and most of the machines between here and root, are all +running some descendant of a hobby project by a student in Helsinki who +mostly wanted a better way to dial into the university.`, + }, + + 'gnu': { + title: 'GNU/Linux', + from: 'ttyp7, since six in the morning', + body: `he is right, and it is worth saying plainly because the joke has almost +completely buried the fact. + +the GNU project started in 1983 with the goal of a complete free operating +system. by 1991 they had written nearly all of it — the compiler, the +linker, make, the shell, the core utilities, the debugger, the libc. what +they did not have finished was a kernel; theirs was late and stayed late. + +then a student in Helsinki wrote a kernel, and put it under their licence, +and the last piece fitted. + +look at the build log in this level. every single line of it is gcc, and +gcc is GNU. the level then prints "a whole operating system" over the top +of it, which is exactly the error he has spent his life correcting, and +being extremely annoying about, and being correct about. + +he has been on ttyp7 since six in the morning. he saw who(1) go past. he +waited until you called it an operating system.`, + }, + // ── hop 45–50, the handshake ─────────────────────────────────────────── 'dtmf': {