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:
- “Are Fortran control characters (carriage control) still implemented in compilers?” (StackOverflow, July 2010)
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!