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': {