Recreating Zork 285 in ZIL - Part 4 - The main loop
This is part 4 in an ongoing series. The previous part: Part 3 - Extracting definitions for rooms and
objects is here.
In this part we will examine initializing and the main loop.
SAVE-IT and some other magic
The SAVE- and RESTORE-functions in the MDL environment basically works so that you call SAVE, the whole current state (stack, program counter and memory)
of the environment is saved, just like a snapshot. Then when RESTORE is called, this snapshot is restored and the program execution continues with the
function following the SAVE. The file MADMAN;MADADV JUN14
, for example, is exactly one of these snapshots.
ROOMS 154 [Jun 14 1977]
starts with the following functions:
<DEFINE SAVE-IT ("OPTIONAL" (FN "MADMAN;MADADV SAVE"))
<COND (<=? <SAVE .FN> "SAVED"> T)
(T
<COND (<=? <SUBSTRUC <UNAME> 0 3> "___">
<QUIT>)>
<PUT <1 <BACK ,INCHAN>> 6 #LOSE 13> ; "FIRST, TAKE OFF THE BACK...."
<START "WHOUS">)>>
<DEFINE HANDLE (FRM "TUPLE" ZORK)
<COND (<MEMBER <UNAME> ,WINNERS>
<AND <GASSIGNED? SAVEREP> <SETG REP ,SAVEREP>>
<INT-LEVEL 0>
<RESET ,INCHAN>
<PUT <1 <BACK ,INCHAN>> 6 #LOSE 27>)
(T
<COND (<AND <NOT <EMPTY? .ZORK>><==? <1 .ZORK> CONTROL-G?!-ERRORS>>
<INT-LEVEL 0>
<FINISH>
<ERRET T .FRM>)
(<TELL
"I'm sorry, you seem to have encountered an error in the program.
Send mail to BKD@DM or TAA@DM describing what it was you tried to do.">
<SCORE>
<QUIT>)>)>>
<SETG WINNERS '["BKD" "TAA" "MARC" "PDL"]>
<GDECL (WINNERS) <VECTOR [REST STRING]>>
<OR <LOOKUP "COMPILE" <ROOT>>
<LOOKUP "GLUE" <GET PACKAGE OBLIST>>
<SETG ERRH <HANDLER <GET ERROR!-INTERRUPTS INTERRUPT> ,HANDLE>>>
<GDECL (MOVES) FIX>
<DEFINE START (RM)
#DECL ((RM) STRING)
<SETG DEATHS 0>
<SETG MOVES 0>
<SETG WINNER <CHTYPE [<SETG HERE <FIND-ROOM .RM>> () 0] ADV>>
<SETG SAVEREP ,REP>
<SETG REP ,RDCOM>
<TELL "Welcome to adventure.">
,NULL>
In SAVE-IT
we see that when we RESTORE and a SAVE was successfull the execution continues with checking that you are a valid user. Next the
line <PUT <1 <BACK ,INCHAN>> 6 #LOSE 13>
does some magic and replaces the ESC
-key with the RETURN
-key. Finally it calls START with
the parameter “WHOUS” (“West of House”, i.e. the starting room).
START does some additional magic that I won’t go into in detail but it essentially sets up a call to RDCOM via SAVEREP. There is also defined an
interrupt-function (HANDLE) that intercepts errors and CTRL-G
. If you are among the WINNERS
you are returned to the MDL environment
with the ESC
-key restored, otherwise the game, and the environment quits.
This are things that we don’t relly need to bother with in ZIL.
Converting to ZIL
In zork_285.zil and rooms.zil
this translates to (remenber that the routine GO is the starting point for ZIL programs):
<ROUTINE GO ()
<FIXED-FONT-ON> ;"Fixed font for a more 'terminal' feeling."
<SAVE-IT>>
;"In MDL this function saved the workspace and the started the game. Due to how MDL
handle SAVE/RESTORE this had the effect that when the workspace was restored the
execution started with the code following the call to the SAVE function. In practice
this meant that a RESTORE loaded and started the game."
<ROUTINE SAVE-IT ()
<START-F ,WHOUS>>
;"START is a reserved word in ZIL and not valid as a name on a routine."
<ROUTINE START-F (RM) ;"Was START"
<SETG DEATHS 0>
<SETG MOVES 0>
<PUTP ,WINNER ,P?AROOM <SETG HERE .RM>>
<CRLF>
<TELL "Welcome to adventure." CR>
<RDCOM>>
The beauty of AND, OR and COND
Before we continue it may be worth to examine more on how AND, OR and COND works. This is applicable to both MDL and ZIL. Three fundamental
things to remember are these:
- All functions returns a value.
- A value are either false (0 = false for ZIL), all other values are considered true.
- Only statements needed to determine the final result are executed.
The last point means that AND work like this:
<AND <statement-1>
<statement-2>
<statement-3>
<statement-4>
...>
Statement-1 is executed, if the return value is true then statement-2 is executed. This is continued until one of the statements returns false, then AND is
considered false and no more statements are executed.
Example (ZIL):
<ROUTINE READABLE? (OBJ)
<AND <BTST <GETP .OBJ ,P?OFLAGS> 2>
<GETP .OBJ ,P?OREAD>>>
AND first tests if bit 2 of OFLAGS on the OBJ is set (BTST) if this bit is 0 then the result is false and READABLE? returns false (0), otherwise the OREAD
property on OBJ is returned (the text, presumably).
For OR it is the other way around:
<OR <statement-1>
<statement-2>
<statement-3>
<statement-4>
...>
All the statements are executed in order until one of them returns true, then OR is considered true and no more statements are executed.
Example:
<OR ,PATCHED <CRLF>>
This statement is used a lot in the game to print an extra CR when the game is in the unpatched mode. Basically OR tries the variable PATCHED and if it is true then
the CRLF is not executed but if it is false then it is.
COND is a little bit like AND except that it tests on a first clause to deterime if a statement should be executed. It continues to execute clauses until one of
them is true, then the following statement is executed and all the following clauses are skipped. Remember that clauses also are statement that are executed
and that they also returns true or false.
<COND (<clause-1> <statement>)
(<clause-2> <statement>)
(<clause-3> <statement>)
(<clause-4> <statement>)
...>
Example (ZIL):
<ROUTINE TREAS ()
<COND (<AND <W=? <1 ,PRSVEC> ,W?TREAS> <=? ,HERE ,TEMP1>>
<GOTO ,TREAS-R>
<ROOM-DESC>)
(<AND <=? <1 ,PRSVEC> ,W?TEMPL> <=? ,HERE ,TREAS-R>>
<GOTO ,TEMP1>
<ROOM-DESC>)
(T <TELL "Nothing happens." CR>)>>
Here we test if the verb is TREAS and the player is in the TEMP1 room then we move the player to the TREAS-R room. If this first clause returns false we test
the next clause and that is if the verb is TREAS and the player is in the TREAS-R then we move the player to the TEMP1 room. The third clause is T and it is
always true (by definition), this means that if the above clauses fails this statement is executed as a fallback and the text “Nothing happens.” is printed.
In practice the T works as an ELSE-statement in other languages (in ZIL ELSE actually is a statement that always returns true, so T could be replaced with ELSE
in the above example).
Remember that the beauty is in the eyes of the beholder…
The main loop - RDCOM
RDCOM is in the file ROOMS 154 [Jun 14 1977]
. This function is the heart of the program and I will go through it in some detail.
<DEFINE RDCOM ("AUX" RVEC RM INPLEN (INBUF ,INBUF))
#DECL ((RVEC) <OR FALSE VECTOR> (RM) ROOM (INPLEN) FIX (INBUF) STRING)
<PUT ,OUTCHAN 13 1000>
<ROOM-INFO T>
<REPEAT ()
<SET RM ,HERE>
<RESET ,INCHAN>
<PRINC ">">
<SETG TELL-FLAG <>>
<SET INPLEN
<READSTRING .INBUF
,INCHAN
<STRING <ASCII 27> <ASCII 13> <ASCII 15>>>>
<SETG MOVES <+ ,MOVES 1>>
<COND (<AND <EPARSE <LEX .INBUF <REST .INBUF .INPLEN> T> <>>
<TYPE? <1 <SET RVEC ,PRSVEC>> VERB>>
<COND (<APPLY <VFCN <1 .RVEC>>>
<COND (<AND <RACTION .RM>
<APPLY <RACTION .RM>>>)>)>)>
<OR ,TELL-FLAG
<TELL "Nothing happens.">>
<MAPF <>
<FUNCTION (X)
#DECL ((X) HACK)
<COND (<HACTION .X> <APPLY <HACTION .X> .X>)>>
,DEMONS>>>
After the keyword "AUX"
follow a declaration of local variables. These can be initialized as (INBUF ,INBUF)
that sets the local variable INBUF
to the global variable INBUF
.
The #DECL
is only a type declaration of the variables.
Then the current room description is printed before starting the loop with REPEAT
. Every turn prints a prompt, clears the TELL-FLAG
, increases the MOVES
counter and inputs a string, INBUF
, from the player.
The <EPARSE <LEX .INBUF <REST .INBUF .INPLEN> T> <>>
extracts a verb, a primary noun and a secondary noun and places them in a vector (“array”), PRSVEC
, that have three slots. If the result of this returns true and the word in the first slot is a valid verb, <TYPE? <1 <SET RVEC ,PRSVEC>> VERB>
, we do the verbs function. If the verbs function returns true and there is a room function we call the rooms function.
If nothing have been printed to the screen above (the TELL-FLAG
is still false), “Nothing happens.” is printed.
Finally all the interrupts (DEMONS
) are called.
In pseudo-code:
print room description
loop_start
print prompt
input text
advance move counter
parse text
if parse ok and verb ok do verb action
if parse ok and verb ok do eventual room action
if nothing have been printed yet, print "Nothing happens."
do DEMONS (interrupts)
loop_end
Converting to ZIL
One of the big differences between MDL and ZIL is that ZIL don’t have a string data type. This will make the parsing a bit different (more on this in later parts) but basically this means that instead of strings we have to work with pointers that point to the words in the vocabulary. PRSVEC
in ZIL is, for example, a TABLE, <GLOBAL PRSVEC <ITABLE 8 (BYTE)>>
, with 4 word slots (the first unused because ZIL is 0-base instead of 1-base in MDL). Aside from this the conversion to ZIL is pretty straightforward.
<ROUTINE RDCOM ("AUX" RM)
<PUTB ,INBUF 0 ,INBUF-SIZE> ;"Max length of INBUF"
<PUTB ,LEXV 0 10> ;"Max # words that will be parsed"
<ROOM-INFO T>
<REPEAT ()
<SET RM ,HERE>
<TELL ">">
<SETG TELL-FLAG <>>
<LEX>
<SETG MOVES <+ ,MOVES 1>>
;"EPARSE, extract PRSVEC (PRSA, PRSO & PRSI)"
<COND (<AND <EPARSE ,LEXV <>> <WT? <1 ,PRSVEC> ,PS?VERB>>
;"PARSE OK - DO VERB-ACTION"
<COND (<APPLY <VFCN <1 ,PRSVEC>>>
;"VERB-ACTION OK - DO ROOM-ACTION"
<AND <GETP .RM ,P?RACTION> <APPLY <GETP .RM ,P?RACTION>>>)>)>
<OR ,TELL-FLAG <TELL "Nothing happens." CR>>
;"Run DEMONS"
<REPEAT ((I 0))
<COND (<G? <SET I <+ .I 1>> <0 ,DEMONS>> <RETURN>)>
<APPLY <GET ,DEMONS .I>>>>