Making Castlequest compilable

Earlier this week I posted that the source code for Castlequest (Holtzman and Kershenblatt, 1980) had been found — Castlequest exhumed!” (2021-03-09). But I named several obstacles that still needed to be overcome to make it playable. Well, those obstacles have now been overcome!

First of all, a big thanks to Torbjörn Andersson for proofreading my transcribed copy of the source. At last report he’d proofed about 12 pages, and found 11 errors, including one error with a visible in-game effect — skipping a potentially important message in the description of room 99. His bug bounty reward is en route! (Furthermore, knowing that people were actually planning to take my money motivated me to take an hour this morning and write a Python script to check all the line numbers, which found seven low-hanging typos and thus saved me $35. ;))

Okay, let’s talk about how Castlequest became playable.

(I also have some thoughts on the game’s programming and how it differs from Adventure, but this blog post has gone on too long already. That’ll have to be another post, later.)

Random number generation

The original source code invokes SUBROUTINE RSTART, which calls out to a couple of library routines that seemed to be specific to VS Fortran: TOD to get the time of day, and then RDMIN to initialize the random number generator. (In C we’d say srand(time(0)) for the same effect.)

During the game, random numbers are generated by calling RDM(SEED), which produces a random real number in the range [0.0, 1.0). This is the same functionality as the now-standard Fortran library routine RANDOM_NUMBER(R). The parameter SEED (or R) confused me at first, but some library documentation explained that in Fortran, the optimizer assumes by default that mathematical functions are “pure” — the same input always produces the same output. So if your random number generator was a zero-argument function — RDM() — then the optimizer would assume it always returned the same value, and eliminate the “redundant” calls. This would be very bad! So, Fortran random number generators always take a dummy parameter, thus tricking the optimizer into thinking that they might be stateful.

Strings as integers

I’m fuzzy on the details here, but basically, Fortran IV had character strings without having a character string data type. A string like 'ABCD' could be stored into a variable of type INTEGER, by invisibly undergoing what in C we’d call an “implicit conversion.” The result would be that the bytes of the string 'ABCD' wound up in memory as the bytes of the integer; you’d have an integer with value 0x41424344 (on a big-endian system) or 0x44434241 (on a little-endian system). C’s non-standard “multibyte character constants” work pretty much the same way.

Fortran seems even more loosey-goosey about this kind of “type punning” than C. Another example is the Fortran EQUIVALENCE statement, which is kind of like a C union except that it can be used to create aliases among basically any pair of variables anywhere in the program. Castlequest relies heavily on EQUIVALENCE statements to convert between integers and strings of characters.

And not just integers! The code also stores strings into COMPLEX variables, and in one place into a LOGICAL variable.

I discovered that in modern Fortran, quoted string literals can’t be used as integer initializers anymore. One solution is the TRANSFER function, which is like a C++ reinterpret_cast: it takes a “source” (the string) and a “mold” (a variable exemplifying the shape you want the source to take on). The downside is that it’s a runtime function that also (as far as I know) can’t be used as an initializer. So a declaration of the form

  COMPLEX*16 LOADED /'Bullet in gun'/

turns into a declaration plus an assignment statement:

  COMPLEX*16 LOADED

  LOADED = TRANSFER('Bullet in gun', LOADED)

The other trick that still works in modern Fortran (although I’m sure it’ll be on its way to the scrap heap in another couple of decades) is to use a Hollerith constant. This code from Castlequest’s SUBROUTINE INPUT doesn’t compile anymore:

 INTEGER VERBS(2,80)
 
 DATA VERBS  /'ATTA', 15, 'BACK', 40, 'BREA', 37, 'BRIE', 61,
2             'CHOP', 37, 'CLIM',  9, 'CLOS', 28, 'CROS', 43,

But this more obfuscated code compiles fine!

 INTEGER VERBS(2,80)
 
  DATA VERBS  /4hATTA, 15, 4hBACK, 40, 4hBREA, 37, 4hBRIE, 61,
 2             4hCHOP, 37, 4hCLIM,  9, 4hCLOS, 28, 4hCROS, 43,

The data table of VERBS above was the source of another tricky problem. It’s a dictionary that maps the spellings of verbs onto their “verb numbers” used internally; for example, when the player inputs the word ATTACK, it becomes “verb number 15” as far as the main game routines are concerned.

So, when the player inputs a word, SUBROUTINE INPUT looks it up in the VERBS table using a hand-coded binary search. Which expects the VERBS array to be sorted. Which it is…

…on a big-endian machine!

Remember that on a big-endian machine, the strings ATTA, BACK, BREA type-pun into the integers 0x41545441, 0x4241434b, 0x42524541 — sorted and suitable for binary-searching. But on a modern little-endian x86, they type-pun into 0x4154541, 0x4b434142, 0x41455242 — completely unsorted! So Holtzman’s clever binary search code goes completely haywire, and the result is that basically any word you enter (except I guess for MELT) goes unrecognized.

My pragmatic solution was to replace the binary search with a simple linear search through the unsorted array.

The mystery of the zeros in column 1

My own mother, a retired programmer, knew the answer to this mystery off the top of her head! Virginia Downes writes:

My suspicion would be that the zero at the beginning of the string is for carriage control — a blank would mean ‘start on the next line’, while a zero would mean ‘next line blank, start printing on the line after’.

I’d never heard of Fortran carriage control, but once I knew what to google, it was abundantly obvious that this was exactly the answer. Each line printed by Castlequest has either a blank space or a 0 in column 1. A 0 definitely means “put an extra blank line here.”

As I mentioned before, Castlequest was written in VS Fortran, which was maybe a bit of a living fossil in the carriage control department. Some documentation from 1994 says:

Under VS FORTRAN, carriage control is implemented by default; under UNIX FORTRAN it is not.

See also:

So the mystery is solved — and in fact this leads to an easy way to improve the authenticity of your Castlequest experience! On many POSIX systems, including my Macbook, there is a utility program named asa. It’s a Unix filter you can stick in a pipeline with any program to deal with carriage-control characters in its output. So, for people with access to asa, the best way to play Castlequest is to run

./cquest | asa

The downside is that asa does buffer a whole line (up to the newline character) before it outputs anything. So if there are any interactive prompts in the game that don’t end with a newline, asa will hide the prompt from you until after you’ve done your input. So far, the only place I saw this happening was in the logic for entering “debug mode,” and I solved it by just adding a newline to the offending prompt.

The mystery of the data file format

The data files in Mike Holtzman’s USCO deposit initially seemed to be in some weird symbolic format:

('0  You are in the bedroom.')
('0  You are in the dim corridor.')
('0  You''re in the parlor.')
('0  You are in the locked room.')

I assumed they’d have to be munged somehow before they’d be usable, and anticipated a lot of digging and experimenting. But guess what? There is no munging process! The data file format expected by Castlequest is literally a text file where each line is a Fortran format string. The game slurps up these lines and prints them out as needed using the statement WRITE(6,FMT), which is analogous to C’s printf(fmt). (Horribly unsafe if you don’t control the format string, but in this case we’re not worried about that.) Fortran format strings are expected to start with an open-parenthesis and a single-quote, just like a FORMAT statement in the source code. A room whose long description takes up multiple lines is simply encoded as multiple lines in the data file: the mapping that tells you how many lines to print for a given room number is in array FIRST, at the top of SUBROUTINE DES.

Give me the code, redux

I’ve added a section to the GitHub repository’s README explaining how to compile and play. If you know how to git clone and how to install gfortran (via brew or apt-get or whatever), that’s pretty much everything you need to start playing Castlequest today!

Posted 2021-03-11