You boot a vanished professor’s computer in 1992. It has a BIOS, a filesystem that follows DOS rules, a shell, a hidden second operating system, an AES-encrypted vault, and an 808-line BASIC interpreter that runs a complete graphical game, with no third-party libraries in the shipped code.
Status
Working build: playable, not finished. Most of it was built in July 2026; a bug fix landed on 12 Oct 2026.
Version
v0.1.2, the last tag (3 Jul 2026), with 36 untagged commits since
Licence
Proprietary personal project; private repository
Runs on
The browser, plus a Tauri desktop shell. Only Windows installers were ever built, and none survive.
Stack
TypeScript (strict) · Vite 6 · Canvas 2D (80×25 text, 320×200 graphics) · Web Audio · Web Crypto
Links
None. The repository is private and there is no public build.
A whole 1992 PC, from the BIOS to two operating systems to a BASIC that runs programs written to loop forever, on the browser’s single UI thread, with zero third-party runtime imports.
At a glance
lines in the BASIC interpreter, with no runtime imports
808
native programs · bootable operating systems
13 · 2
third-party runtime imports; the game’s JavaScript is 52 KB gzipped
0
programs fuzz-clean after a same-day fix (two seeded runs of 1,000 keystrokes started in each)
13/13
How these were measured
808: wc -l src/programs/nbasic.ts at the current commit. Its only two imports are import type, which compile away, so the interpreter has no runtime imports.
13 · 2: the program registry (src/programs/index.ts) has 13 entries. Eight are in C:\GAMES (SNAKE, METEORS, BREAKER, ELIZA, TETRA, OZARK, TAIPAN, CROWN); the rest are two office programs, the BASIC interpreter, an encryption utility and a modem terminal. The machine boots either NovaDOS from C: or Novix, a fictional 386 Unix, from a hidden 40 MB second drive that DOS never gives a letter.
0: every import in src/ is relative except the stylesheet. (package.json still declares the Tauri API package, but nothing imports it.) Built from a clean export of the committed tree, the JavaScript is 160,602 bytes (52,033 with gzip -9) plus 3,573 bytes of CSS. Rebuilding the July commit gave the same content-hashed files as the project’s own July build, and a strict tsc --noEmit exits 0 on both commits. The only non-project code in the bundle is what the build tools inject: Vite’s 710-byte module-preload polyfill and a compiler helper.
13/13: headless Chromium loaded the built bundle with no server, started each of the 13 programs and sent it 1,000 seeded random keystrokes, on two seed sets, capturing every page error and console error. A random Escape or quit key ends some programs early, and the remaining keys then go to the DOS shell, so per-program coverage varies. The July build was clean in 12 of 13. All on 12 Oct 2026.
Fig. 1 DINOS.BAS, an artillery game written in the interpreter’s own BASIC dialect: sine-wave mountains, two dinosaurs built from filled boxes and a PAINT-filled circle, a PAINT-filled sun, a wind gauge and an INPUT prompt, in 320×200 graphics. A headless capture of the built game, cropped to the simulated monitor.
The short version
A cooperative scheduler, so a 1992 busy-wait can’t hang a browser tab. The interpreter runs at most 400 statements every 6 ms, then yields; a 103-line game written in its own dialect runs on it.
Puzzle secrets split so an AI assistant handed the listing can’t read off the answer. The vault behind them is real AES-256-GCM: a wrong passphrase simply fails to decrypt.
Re-measured instead of copied. A fresh fuzz of all 13 programs found a crash an earlier fuzz had missed. It was fixed the same day: a targeted repro went from 20 crashes in 20 runs to 0.
Case 1 of 5
A 1992 BASIC that won’t hang a 2026 browser tab
A cooperative scheduler runs at most 400 statements per 6 ms tick and then hands the thread back, so programs written to loop forever can’t freeze the page.
A line-numbered BASIC has to run programs that loop forever, busy-wait and stop for INPUT, all on the browser’s single UI thread. The artillery game paces its animation with an empty 2,400-step FOR loop and spins on INKEY$ until a key arrives. Run naively, the delay loop holds the thread for the whole flight, so not one frame of the shot is drawn, and the key loop never ends, because the key it is waiting for can’t arrive while the thread is busy.
So the interpreter flattens the program into a statement array, with a line-number index for jumps, and runs it under a cooperative scheduler: each 6 ms tick executes at most 400 statements, then yields. INPUT parks the runner until the line is entered, and Ctrl-C breaks a running program, as it did in 1992.
Graphics draw into a 320×200 offscreen raster in the 16-colour CGA/EGA palette, with hand-written Bresenham lines, midpoint circles and a stack-based PAINT fill, copied to the screen only when something changed. POINT reads a pixel back as the nearest palette colour, which is how the game detects a hit.
DINOS.BAS
5220IF POINT(CX,CY)<>SKY THEN 54005250FOR DL=1 TO 2400 : NEXT DL8010K$=INKEY$ : IF K$="" THEN 8010
Fig. 2 Three lines from the game: a hit test that reads the screen back, a delay loop a naive interpreter would race through without drawing a frame, and a key loop that would hang it.How we know
Read from the committed source on 12 Oct 2026: the statement budget, the 6 ms interval and the draw-only-when-changed step in src/programs/nbasic.ts, and lines 5220, 5250 and 8010 of the 103-line DINOS.BAS. Fig. 1 shows the game running in headless Chromium.
Case 2 of 5
Puzzles designed for a player with an AI assistant in the next tab
After a playtest showed an assistant solving a puzzle straight from its listing, the secret was split across two files. The vault behind it uses real AES-256-GCM, whose authentication tag is the only password check.
The design assumes players will have an AI assistant open in another tab. A playtest showed the cost: pasted into an assistant, one puzzle’s BASIC listing was solved at once, because the key, the data and the algorithm all sat in one copyable listing.
So the secret was split. The key moved into a hidden-attribute file that the program reads at run time, which meant giving the interpreter file I/O (OPEN, INPUT #, CLOSE). An assistant handed the listing can still explain it and help fix it, but it can’t read the answer off it.
The program’s output opens a vault built on real cryptography: AES-256-GCM, keyed by PBKDF2-SHA256 at 150,000 iterations through Web Crypto. It never compares a password. The GCM authentication tag is the check, so “Bad passphrase” is simply a failed decrypt, and the passphrase appears nowhere in the code or the shipped bundle. (The BIOS setup password, by contrast, is a plain comparison.) The vault file is dressed as PGP-style armour, so it reads as encryption and sends the player, or their assistant, looking for a key.
Fig. 3 The dashed box is what a player can paste into an assistant. The answer only exists once the program runs with the hidden key.How we know
Key derivation and decrypt read from src/programs/novacrypt.ts. On 12 Oct 2026 an independent Node Web Crypto script read the shipped vault file: the correct passphrase decrypted the 483-byte encrypted payload inside its armour to 439 bytes in 11–16 ms, and the same passphrase plus one character was rejected with an OperationError, the GCM tag failure. A search of src/, the July bundle and today’s rebuild found the passphrase 0 times.
Case 3 of 5
A bug only multi-line clears could trigger, fixed by making it impossible to write
TETRA’s line removal broke every double, triple and four-line clear. The fix rebuilt the board logic as a pure filter in which the mistake can’t be expressed, and ten tests that import the shipped source hold it there.
TETRA left completed lines on the board. The removal loop spliced out a row and then pushed an empty row in at the top. Each push re-indexed the remaining rows, so after the first removal every later index was off by one: it deleted an innocent row, and a full row survived.
Single-line clears worked, and the score and line counters were computed before removal, so the only symptom was visual and playtesting kept missing it.
Rather than patch the loop, I moved the board logic into a pure module and rebuilt it as a filter, so nothing is mutated while indices are live. Ten regression tests import the shipped source with no build step: Node strips the TypeScript types on import, which works because tested modules use only erasable syntax (no enums, no constructor parameter properties). The TAIPAN fix in Case 5 reused the pattern.
- rows.slice().sort((a, b) => b - a).forEach((r) => {- this.board.splice(r, 1);- this.board.unshift(new Array<Cell>(COLS).fill(0));- });+ const drop = new Set(rows);+ const width = board[0]?.length ?? COLS;+ const kept = board.filter((_, i) => !drop.has(i));+ while (kept.length < board.length) kept.unshift(new Array<Cell>(width).fill(0));+ return kept;
Fig. 4 The old loop, and the filter that replaced it. The new function returns a new board instead of changing the old one.How we know
The change is in the project’s git history: the old removal loop inside the TETRA program, and its replacement in src/programs/tetra-board.ts. On 12 Oct 2026 the 10 tests in test/tetra-lines.test.mjs, including a two-adjacent-lines regression and a no-mutation check, passed under node --test in a scratch copy of the repository.
Case 4 of 5
Save files that survive game updates
A save stores only what differs from a freshly built disk, so a content patch still reaches every file the player never touched.
A puzzle game that saves whole snapshots of its disk has a problem: once content is patched, every existing save pins its player to the old files. h@ck3r instead rebuilds a fresh copy of the authored disk and stores only the difference: changed, new and deleted files, plus the BIOS boot-order setting and the setup-password unlock.
Writes are coalesced into one, about 600 ms after a change, whether it came from the shell, the BIOS, a program’s keystrokes, or one of the two programs that change the disk on their own. The format is versioned, and a storage failure is never fatal.
Tested on 12 Oct: after one file was deleted and another set read-only, the save held exactly 2 of the disk’s 34 files (1,174 bytes) and survived a reload. UNDELETE then shrank the difference back to 1 file.
How we know
Headless runs against the July build and today’s build: DEL README.TXT and ATTRIB +R CONFIG.SYS, then read the saved state, reload, and UNDELETE README.TXT. Both builds gave the same result, with no page errors (12 Oct 2026). The logic is in src/fs/save.ts.
Case 5 of 5
Measured again rather than copied
Instead of repeating an old zero-crash figure, I fuzzed every program again. One crash surfaced, and it was fixed the same day with regression tests that fail against the old code.
Headless Chromium loaded the real bundle with no server, started each of the 13 programs and sent it 1,000 seeded random keystrokes (when a random key quit a program, the rest went to the DOS shell), and captured every uncaught exception and console error.
Twelve programs were clean. TAIPAN, the trading game, failed in every run, from one defect present since its first commit. At the “Set sail for which port?” prompt, the key went through parseInt and then a range check. Any key but a digit, Esc or X produced NaN, which slipped through because every comparison with NaN is false. The ship sailed to port NaN, and every redraw after it arrived threw, blanking the screen.
The same-day fix sends both numbered menus through a small pure function that tests the key against the digit set before converting it. Six regression tests import the real source, and three of them fail when the old guard is put back. The other key-to-number conversions were checked and don’t share the defect. Re-run on the fixed build, both seed sets came back clean in 13 of 13 programs.
- const d = parseInt(k, 10) - 1;- if (d < 0 || d >= PORTS.length) return;+ const d = menuChoice(k, PORTS.length);+ if (d < 0) return;+ export function menuChoice(key: string, count: number): number {+ if (!/^[1-9]$/.test(key)) return -1;+ const i = Number(key) - 1;+ return i < count ? i : -1;+ }
Fig. 5 The sail-destination guard before and after, and the function both menus now share. A non-digit key returns −1 before anything is converted.
Targeted repro (open TAIPAN, sail, press a letter, resolve any sea event): runs that crashed, out of 20. Lower is better.
July build
20
Fixed build, 12 Oct
0
How we know
Headless Chromium (Playwright 1.61.1) on each built bundle through request interception, with no server. 1,000 seeded keystrokes started in each program, on two seed sets (26,000 in all, though keys sent after a program quit went to the DOS shell), with page errors and console errors captured. Exception counts aren’t quoted, because TAIPAN’s own randomness is unseeded and they vary between runs. The targeted repro ran 20 times on each build. The 6 tests in test/taipan-keys.test.mjs pass; with the old guard swapped into a scratch copy, 3 fail. All 16 committed tests pass under Node 26. Run 12 Oct 2026.
More of the machine
Headless captures of the built game · no answers or passwords shown
Fig. 6 The power-on self-test counts memory to 4096K and detects two hard disks. The 40 MB primary slave is the hidden second drive: DOS never gives it a letter, and the second operating system boots from it.Fig. 7 CMOS Setup, reached the in-game way: Del during the self-test, then the supervisor password. The “(Shift)F2 : Change Color” legend is period set dressing; nothing handles that key.Fig. 8 ELIZA, after Weizenbaum’s 1966 DOCTOR script: 30 ordered rules where the first match wins, plus 9 fallbacks. It reflects pronouns and recalls an earlier “my …” line. It approximates his keyword ranking rather than reproducing it.Fig. 9 A deliberately wrong passphrase. No stored password sits behind this message: it is a failed AES-GCM decrypt (Case 2).Fig. 10 NovaTerm parses Hayes commands strictly. The busy signal is a synthesized 480 + 620 Hz tone, inaudible in a still; 555-1234 is a test number typed for this capture.Fig. 11 TETRA, whose multi-line clears were broken until the board logic was rebuilt (Case 3).