Yesterday I got an e-mail from Douwe Homans in Antwerp. He played my text adventure Dracula
as a child, and he had just reverse-engineered DRACULA.EXE and rewritten the game in
JavaScript, as a web version. A game I wrote in 1980, whose source code I lost decades ago, was being played
again. I didn't get much sleep that night.
Dracula, 1980
As a student at TU Delft I played Colossal Cave Adventure and Zork (Dungeon, as the PDP-11 version was called) on a PDP-11. I wanted one in Dutch, so I wrote it: Dracula, in BASIC, on a Commodore PET 2001, in 8 KB. To make it fit, the texts were very terse.
Later, as a developer at Data Union, I ported it to CP/M on the 8080/Z80 and wrote out all the texts in full. A Dutch linguist who worked there turned my fake old Dutch into real old Dutch. You see that best in the "Vampier Handboeck" you find in the game:
Vampieren sijn al sedert menschenheugenisse ende tiden die parasieten der menschen.
The MS-DOS version is that CP/M source recompiled with BASCOM, Microsoft's BASIC compiler,
under PC-DOS 1.0, on the first IBM PC in the Netherlands, imported from the US: 4.77 MHz,
32 KB of memory and a 160 KB floppy drive. It was mostly a test of whether the compiler worked on that strange new
machine. It did. The EXE needs BASRUN.EXE, the BASCOM runtime, next to it, and reads all
its texts from a file called DRACULA.TXT.
In 1985 CYCO Automatisering sent it out as a New Year's card on floppy, about 200 copies. Everybody copied it, and that is how it got popular in the Netherlands. My brother drew the sleeve.
The BASIC source is on an 8-inch floppy that nothing can read anymore. In 2001, for the game's twentieth birthday, I put the EXE online as freeware at cosninix.com/dracula. That was all there was: a compiled program, a data file and a scan of the sleeve.
An e-mail from Antwerp
Douwe's version runs at dracula.apphost.be. You type your name, and the browser remembers your game, so you can continue later. Same rooms, same texts, same Dracula, no DOSBox needed. I played it through to the end, and it all works.
Getting the BASIC back
Douwe's mail made me want to see my own code again. That evening I asked Claude, on
claude.ai, whether it could reverse-engineer a program compiled with BASCOM. That works
better than you'd think: BASCOM turns every BASIC statement into a fixed pattern of machine
code that calls routines in the runtime for PRINT, INPUT, MID$ and so on. Recognise
the calls and you get the statements back. The strings are in the EXE as plain text.
Just after midnight I uploaded DRACULA.EXE and BASRUN.EXE. Claude recognised my own
game from the copyright string, and a quarter of an hour later there was a DRACULA.BAS
of about 1,200 lines: the Dutch parser, the rooms, Dracula walking through his castle, save
and load, the bug reports and the title screen.
What didn't come back is what the compiler had thrown away: variable names, comments and
line numbers. R% for the room and OL%() for where the objects are, are Claude's names,
not mine. Also missing: five little assembler routines that were linked into the EXE. The
game checked whether it ran on an IBM PC or a Zenith Z-100. On the IBM it cleared the screen
and positioned the cursor through those routines, on the Zenith with escape codes, and on
anything else it just printed line after line:
11940 IF IBM% <> 0 THEN CALL CLRSCR
11950 IF ZEN% <> 0 THEN PRINT CHR$(27);"E";
11960 RETURN
11970 IF IBM% <> 0 THEN CALL CURSOR(X%,Y%)
11980 IF ZEN% <> 0 THEN PRINT CHR$(27);"Y";CHR$(Y% + 32);CHR$(X% + 32);
The first DRACULA.TXT I uploaded arrived mangled. Despite the name it is a binary file,
and the upload had treated it as text and converted the encoding. Zipped, it came through
intact: 1,289 records of 80 bytes with the exits per room, the room descriptions, the
messages and the objects, padded with zeros to a multiple of 128 bytes. That padding is a
CP/M leftover; CP/M stored files in 128-byte records.
Ten minutes to one, I was standing in my own house again, in PC-BASIC, a GW-BASIC emulator written in Python:
Je bent nu in je eigen huis. Er is een slaapkamer op het zuiden
en een kleine hal oostelijk. Er is een deur die naar het westen leidt. De
deur staat op een kier en een gure wind blaast zand en stof naar binnen.
Door een raam kan je een bos zien.
Er zit een gat in het plafond, maar je kunt het niet bereiken.
Het gat is groot genoeg om er doorheen te klimmen.
Er is een klein briefje hier.
->z
Je bent nu in de slaapkamer. De enige uitgang is naar het
noorden, waar een rommelige kamer te zien is. Het is vrij donker
hier, maar door een klein raampje valt net voldoende licht binnen
om te kunnen zien.
Er is een kleine brandende lantaren hier.
Er is een zwaar eikenhouten bed hier.
->pak lamp
Ok
1980 from the inside
This is what I love about software from that time: everything had to fit, so every byte
has a job, and 44 years later you can still see every decision. The verb parser is one long
chain of IFs:
740 IF V$ = "GA" OR V$ = "BETRE" OR V$ = "KRUIP" OR V$ = "LOOP" OR V$ = "KLIM" OR V$ = "VOLG" THEN 1260
750 IF V$ = "$RVW" THEN PRINT "Room";R% : INPUT "Room"; R% : GOTO 340
760 IF (V$ = "KIJK" OR V$ = "K") AND N$ = "" THEN 340
Only the first five letters of a verb and the first four of a noun count, so BETREED
becomes BETRE. Colossal Cave Adventure also looked at only five letters. Line 750 is my
cheat: type $RVW and the game asks which room you want to go to.
The six magic words are in DATA lines, every letter shifted by one, so they don't show up
as text in the EXE:
10 DATA "WBNQJSBOJB","GMBVXFLVMB","ESBLFMTUFJOJB"
11 DATA "NJDSPTPGUFOTUFJO","JODPSPOJVN","USBOTTZMWBOJVN"
WBNQJSBOJB is VAMPIRANIA. The fourth one is MICROSOFTENSTEIN. Every game picks one at
random.
There is a tester mode that writes the testers' remarks to a file, a BUG command for bug
reports, a list of Dutch swear words with an answer of its own, and bugs of mine that
survived all those years: saving the game forgets the ladder in the house, "Illigal item
number" is spelled like that, and drinking the poison doesn't kill you, because the routine
that runs every turn immediately resets the death flag. Claude kept them all.
Two bugs that weren't mine
Then I moved to Claude Code, in the project folder. Under PC-BASIC the game sometimes crashed when you opened the coffin in the tomb:
KeyError: Dereferencing detached string
That turned out to be a bug in PC-BASIC 2.0.8. INSTR, LEFT$, RIGHT$ and MID$
register their string arguments as temporary values, but skip the clean-up when they return
early, for instance when INSTR finds nothing. My parser calls INSTR a few times for every
command, and most of those calls find nothing. After one playthrough some 900 temporary
strings had leaked, the string space was full, and the garbage collector ran every few
statements, most of all in the busy-wait loop at the coffin. Sooner or later a leaked value
pointed to a string that had already been freed. The current master of PC-BASIC on GitHub
still has this bug.
The fix is pcbasic_fix.py, a launcher that wraps those four functions, cleans up after
them and then starts PC-BASIC. It also fixes a second bug: in terminal mode PC-BASIC didn't
scroll during the autoplay, so every line overwrote the one before. The crash was rare to
begin with, so I can't prove it's gone, but the leak went from 900 values to zero.
And then in C
At two in the morning I asked for a C version. dracula.c is about 2,200 lines of C99,
without the GOTO spaghetti: every verb is a function that returns where the BASIC would have
jumped to, and one main loop does the jumping. Each function names the BASIC line it came
from.
The fun part was making it play exactly the same game. RND in C uses the same generator as
GW-BASIC:
rnd_zaad = (rnd_zaad * 214013 + 2531011) % RND_PERIODE; /* RND_PERIODE = 2^24 */
And BASIC evaluates every term of an AND or OR, even when the outcome is already clear.
So when RND is part of such a condition, a number is drawn every time. C stops evaluating
early, so the C version draws those numbers up front; otherwise the random sequence drifts
apart after a few turns. With that in place, the autoplay (the game plays its own solution
from the file PLAYER) gives the same output line for line in C and in PC-BASIC, all the way
to the treasure, and a saved game is byte-for-byte the same. In C you can even reincarnate
after dying, which under PC-BASIC stops with "File already open".
Before three o'clock the reconstructed BASIC, the C version and a README in English and Dutch were in git.
Play it
- In your browser: Douwe's web version.
- The source: github.com/rvwoens/dracula, with
DRACULA.BASfor PC-BASIC,dracula.cand the original EXE from 2001. - The original for DOSBox: the download on /dracula. That's the page from 2001, back at its old address.
The C version builds on macOS and Linux:
git clone https://github.com/rvwoens/dracula
cd dracula
cc -O2 -o dracula dracula.c && ./dracula
The game is in Dutch. Don't swear at it:
->shit
Zeker christelijke opvoeding gehad ? Dacht ik al.
That's "Christian upbringing, I suppose? Thought so."