ZIL: Zork Implementation Language. Used by Infocom for their wide range of well-known games, including classics such as its namesake, Zork, as well as Trinity, Plundered Hearts, and The Hitchhiker’s Guide to the Galaxy. In the previous article in this series, I studied how the Z-machine – a virtual machine compiled from ZIL code – evolved from its first version, z1, through to z6, with new capabilities added all the time amidst market competition. I examined the improvements made from z-machine version 3 to 5, including colors, status lines, file size, and finally – due to increases in RAM and file capacity, as well as more complex capabilities – the release of the new-parser of version 6. This included the ability to reference objects from other rooms, and style more complex input structures – such as three objects in one command, a feat I accomplished for my debut game, Milliways: the Restaurant at the End of the Universe.
In the following articles, I will be writing about my personal experiences in coding the 2023 IFComp game, Milliways: the Restaurant at the End of the Universe. But for you to understand anything I say, I’m going to have to teach you how ZIL works first.
CODE SYNTAX & FLOW CONTROL
To understand the complexities of ZIL, you’re going to need to know a bit about the very basics. As a start, ZIL syntax relies heavily on angle brackets (“<”, “>”), as it is based off of the programming language MDL, itself based off of LISP. Most of the time, you’ll find yourself using multiple layers of nested brackets in complex lists, in order to write sufficient code.
For people like me who spend a lot of time listing things, and enjoy neat order of lists and nesting, you will love ZIL. Flow control (i.e. if-else statements and iteration) relies on lots of nesting, sometimes reaching up to 5-8 layers just for a simple routine, and we will look at some examples of this in a few moments.
To begin with, we need to know some simple ZIL+MDL code functions. The very first thing to note is that in general, functions (known as ROUTINEs) are written inside matched angle brackets to the left and right. Any parameters that are passed go after the function, separated by nothing more than a space.
Before we begin, I would like to introduce the equivalent of true and false in ZIL. T by itself is the ZIL version of true, and a pair of empty angle brackets ( <> ) is the equivalent of false. This will come in very handy as you continue to use ZIL.
ROUTINE
This is the thing that you run throughout the game, and is essentially the flesh, muscles and blood of any game. Without them, nothing would be able to happen in the game.
All parameters go in a pair of parentheses after the routine name. Then everything after the parentheses are the instructions that are run. E.g.:
<ROUTINE KILL-ALIENS (X "AUX" MSG)
<COND (,ALIENS
<SET MSG "T-minus 2">)
(<=? .X 5 7 9>
<TELL "Run!!!" CR>
<SETG HARDCORE T>)
(T
<SETG ALIENS .X>)>>
Anything in the parameter list after "AUX" (with the quotation marks) are auxiliary variables, which are a form of local variable which are created to be used later, but aren’t passed as arguments when the routine is called. For example, when you run the routine later on in the game, you could write <KILL-ALIENS 5>, which sets X to 5 and runs the routine, but you never need to define the variable MSG there. Auxiliary variables are the equivalent of using “let MSG be nothing” to define a variable in Inform 7, except for the fact that you can’t just make variables on the fly. ZIL requires that you create local variables in the first parentheses.
SET & SETG
These set local (only in the current routine) and global (everywhere in the code) variables, respectively, to the value of the second argument. Then the second argument is returned. So:
<SET FAKE 7>
will make the local variable FAKE equal to 7 and then return 7 to the rest of the code, so you can use this command in a conditional statement, whereas:
<SETG ALIENS <>>
sets the global variable ALIENS to <> and returns <> (false). Another thing to note is that when retrieving the value of a local or global variable, you do so by putting “,” immediately before a global variable (e.g. ,ALIENS) and “.” immediately before a local variable (.FAKE).
COND
This is the general if statement. All if-else statements are each surrounded by parentheses (“()”). The first command in a pair of parentheses is the condition. The rest of the commands are the code being run if that evaluates to true. An example of a working condition statement is:
<COND (,ALIENS
<SET MSG "T-minus 2">)
(<=? .X 5 7 9>
<TELL "Run!!!" CR>)
(T
<SETG ALIENS .X>)>
Here, it sets the local variable MSG to the string "T-minus 2" if the global variable ALIENS is not false. Otherwise, if X equals 5, 7 or 9, prints "Run!!!"1 followed by a carriage return (“CR”), which ZIL does not automatically print. Otherwise, set (global) ALIENS to (local) X.
Notice the T in the last condition? Yes, although T means “true”, if you use it in a condition, it just returns true. In this way, it’s essentially a way of saying “else”.2
You will pick up other important bits in routines along the way, but those are the most important things you need to understand. I’ll come onto loops much, much later.
VERBS
Creating verbs come in two parts: definition (the syntax) and action (carrying it out).
The syntax part contains information on which words to use to call a verb. These are compile-time macros, meaning they can’t be inside routines – they are stored in static memory and can’t be created or changed during play. Creating one is fairly straightforward, actually:
<SYNTAX TAKE OBJECT = V-TAKE>
In the SYNTAX, there is the PRSA (TAKE), and the PRSO (OBJECT), then after the = sign is the name of the routine that will be run. In case you’re curious, PRSA stands for PaRSer Action, otherwise known as the verb. The others are PRSO (PaRSer Object) and PRSI (Indirect object, i.e. second noun – we’ll get onto this in a second), and all of these are also global variables that hold the current turn’s objects and routines. You can refer to in the code to identify which verbs and objects were used.
You can have a preposition in the syntax definition as well, which always has to be followed by an object definition. The second OBJECT here will be what the PRSI holds:
<SYNTAX PUT OBJECT ON OBJECT = V-PUT-ON>
You can also create synonyms:
<SYNONYM PUT PLACE THROW>
When writing SYNTAX definitions, the first argument is the word that you will write in all the verb definitions (so never write PLACE or THROW in verb definitions, only PUT! It can cause unnecessary problems otherwise3).
Now, on to the verbs:
<ROUTINE V-TAKE ()
<COND (<IN? ,PRSO ,PLAYER>
<TELL "You're holding it!" CR>)
(<NOT <FSET? ,PRSO ,TAKEBIT>>
<TELL "You can't take that!" CR>)
(T
<MOVE ,PRSO ,PLAYER>
<TELL "Taken." CR>)>>
A very simple routine for V-TAKE (the real taking action is much more complex) checking if the player is holding the object – you can see this in the definition of <IN? ,PRSO ,PLAYER>, which literally means “is the PRSO inside the PLAYER?”. Failing that, if the thing is untakeable, comment on this – <NOT <FSET? ,PRSO ,TAKEBIT>> – which checks if the PRSO has not had the takeable flag (FSET means Flag-SET) set to it. Note, for later, that FSET sets a flag, whereas FSET? checks whether a flag is present. In this case, the TAKEBIT flag defines whether an object is takeable or not. If all else fails, move the PRSO to the PLAYER. One thing to know is that the verb routine must always start with “V-”. Just because.4
To make “Get What I Mean” verbs (it assumes what you are talking about if you don’t specify the object), you need to use parentheses after an OBJECT in the syntax definition. For example:
<SYNTAX TAKE OBJECT (ON-GROUND IN-ROOM) = V-TAKE>
All the tags you can use are: ON-GROUND, IN-ROOM, HELD, CARRIED, HAVE, TAKE and EVERYWHERE. My understanding of them is:
ON-GROUNDandIN-ROOMmean similar things: anywhere in the game that is currently visible to the player, howeverON-GROUNDtends to prefer things not already held. Even so, you generally always use them together.HELDandCARRIEDare the same as the first point, but when you use this if there is a choice between an object on the floor and being held, it will choose being held.HAVEandTAKEare used together to allow the game to use an implicit taking action if the object is not held – “(first taking …)”.EVERYWHEREis a new-parser addition which means the object can be from anywhere in the game. I mentioned it briefly in the first article, as it only appears in the new-parser.
Later, we’ll come back to handling verbs, when we study objects. Speak of the devil, here we have…
OBJECTS & ROOMS
If routines are the flesh, muscles, and blood of a ZIL game, then the objects are the bones. You could, in theory, make a ZIL game without objects at all. In theory it would definitely work. But it would be a completely different sort of game. A different creature.
Again, objects are defined at compile-time, so you can’t create new ones on the fly during play, like you can in TADS. A well known example of an object is here:
<OBJECT BRASS-LANTERN
(LOC KITCHEN)
(DESC "brass lantern")
(LDESC "There is a brass lantern here.")
(SYNONYM LANTERN LAMP TORCH)
(ADJECTIVE BRASS METAL SMALL)
(FLAGS LIGHTBIT TAKEBIT)>
Every pair of parentheses contains first the name of the property (such as LDESC) and then the according value.
LOCis always followed by the room or object that contains it.DESCis the string that the game uses to refer to the object. Although it looks like a property, it’s slightly weird in thatDESCspecifically can’t be changed during play. It’s also not technically a property, but it’s close enough that it can be considered one for all purposes.LDESCdoesn’t have to be used, but it allows an object to have its own line in a room description.SYNONYMhere is not the routine that you use in verb definitions. It lists all the nouns that are used to refer to the object.ADJECTIVEis the same, but with adjectives – in ZIL, synonyms, or nouns, can only be used as the final word in the object part of a command and everything else is anADJECTIVE– for example, if you try toEXAMINE BRASS LANTERN, the word “BRASS” is an adjective, and so therefore is any word before the final in that object section, which would be “LANTERN”. Only one synonym is allowed per object part of a command. Neither of the two require you to list the words in any particular order: you can call itSMALL METAL LANTERNorMETAL SMALL BRASS TORCH– but notTORCH BRASSnorBRASS, sinceBRASSisn’t under theSYNONYMproperty.FLAGSis very important. This lists what capabilities the object has.LIGHTBITin an object means it can be turned on and off.ONBITmeans it actually is producing light/turned on.TAKEBITmeans you can pick up the object.OPENBITmeans the object is open.CONTBITmeans it is a container.TRANSBITmeans the container is transparent.INVISIBLEis the only flag bit that doesn’t end in BIT – it means it is invisible.
There are many more, which you can find by studying ZIL code. Many are specific to certain games. Earlier I mentioned FSET? under the verbs section. Well, here’s it’s whole family. You can add a flag to an object during runtime using <FSET ,[obj] ,[flag]>;5 you can remove using the same syntax with FCLEAR; and check whether objects contain a certain flag using FSET? – the question mark is important. This routine only allows one object and one flag parameter each.
Rooms are similar, though some other properties can be added. They are fairly straightforward, actually. See:
<ROOM KITCHEN
(LOC ROOMS)
(DESC "Kitchen")
(LDESC "This is your kitchen, where you cook and eat.")
(NORTH TO BEDROOM)
(SE PER OPEN-ALCOVE)
(FLAGS LIGHTBIT ONBIT)>
LOCshould always beROOMSin a room.LDESCis for room description here.- All the directions (which will be defined somewhere at the start of the game) can be used here. The parameters that can be passed are:
TO [rm]– move player to room[rm]. Don’t use the comma syntax on objects here.PER [rtn]– run the routine[rtn], and if anything is returned (using<RETURN ,KITCHEN>for example), move them to that room – otherwise (if false is returned using<RFALSE>) nothing happens. You should never return true in a movement routine (<RTRUE>).SORRY "[str]"– do nothing but print the string[str].TO [rm] IF [var]– if global variable [var] is true (or at least non-zero), move the player to room[rm]. You can addELSE "[str]"to the end of that to make it function likeSORRYif it fails.TO [rm] IF [door] IS OPEN– fairly self-explanatory. You can also addELSE "[str]"here.
FLAGSis important, asLIGHTBITandONBITshould always be set in a non-dark room.
Here’s a basic example of the (PER [RTN]) counterpart routine in action, using the (SE PER OPEN-ALCOVE) from the example room above:
<ROUTINE OPEN-ALCOVE ()
<COND (<FSET? ,ALCOVE ,ONBIT>
<RETURN ,ALCOVE>)
(T
<TELL "It’s too dark over there." CR>
<RFALSE>)>>
Objects, rooms and flags can be referred to in routines using the “,” before the name, just like in global variables and constants, but not if you’re in a ROOM definition (anything physically inside the <ROOM> code).
One more thing regarding objects and rooms. Now you’ve got the objects, and rooms, and the like, but we need to know how you interact with objects: how to implement them. And it doesn’t require editing the verb routine.
Unlike in Inform where all rules (such as “Instead of” or “Carry out”) exist separately from the objects and verbs, in ZIL you must go through the objects or verbs. Every object and room can have its own routine attached, so when a verb is going to be carried out, first it’ll run the PRSI’s routine, and if nothing happens there, it runs the PRSO’s routine. If that fails, it runs the PRSA routine (such as the V-TAKE routine I showed earlier).
You can set up an object’s routine through its object definition. At the end of the brass lantern object definition, we’re going to edit it:
...
(FLAGS LIGHTBIT TAKEBIT)
(ACTION BRASS-LANTERN-F)>
Then we define a mini routine:
<ROUTINE BRASS-LANTERN-F ()
<COND (<VERB? EXAMINE>
<TELL "The brass lantern is small and is turned ">
<COND (<FSET? ,BRASS-LANTERN ,ONBIT>
<TELL "on">)
(T
<TELL "off">)>
<TELL "." CR>)>>
Note that the VERB? condition must take the exact name of the verb routine (but take off the “V-”). If you have a verb that has both a PRSO and PRSI, you can check whether this is the right object by using <PRSO? ,[obj] (restofobjs…)> and <PRSI? ,[obj] (restofobjs…)>. It will return true only if one of the objects matches either the PRSI or PRSO (respectively).
A lot of times, you’ll have many objects that are similar in functionality, so you can use a generic object routine called something like “FROBS-F” to give a similar response for your routine, and then you can get more specific using the PRSO? and PRSI? routines, which check the PRSO or PRSI for one of the arguments passed, respectively. It saves time, storage, and if you want to change something, you can change all in one go.
Room routines are used if you want an adaptive room description or something specific to the room you’re in, such as entering or leaving the room in general (instead of specific to one exit, which you can do with PER). I’m not going to go too deep into it, but it’s easier for me to show you than it is to explain:
<ROUTINE KITCHEN-F ("OPT" (RARG <>))
<COND (<=? .RARG ,M-LOOK>
<TELL "This kitchen is very small, save
for a large cupboard in the other corner which is ">
<COND (<FSET? ,CUPBOARD ,OPENBIT>
<TELL "open">)
(T
<TELL "closed">)>
<TELL "." CR>
<RTRUE>)>>
There’s a lot that’s new here, but in summary, ("OPT" (RARG <>)) creates an optional parameter that either is set when the routine is run (as would a normal argument), or is set to <>. In the case that the player tries to look around, this routine will be called with the global variable ,M-LOOK as a parameter. The only use for this parameter is to differentiate between looking and others, such as M-ENTER and M-EXIT. The actual value of M-LOOK is not important, as long as none of the others have the same value.
NEW-PARSER SPECIFICS
In Part 1, I studied how version 6 introduced the new-parser for ZIL. After Infocom closed and the z6 games stopped being created, the new parser went unused as it was even less documented than normal ZIL, which was already barely documented. As well as that, the new parser required some work to get fixed for more public use, since there were some significant bugs in the code that went unnoticed. The few ZIL games that have been released since the end of Infocom have mostly been choice games (by an author named “SD Separa” on IFDB), as well as a few smaller parser games – but again, none using the new-parser. All of that is until Milliways, but that’s for later.
The new parser games have the EVERYWHERE syntax flag, which is an addition to the grammar area. The grammar area defines the different command structures (eg. “JEFF, OPEN THE BOX” vs “OPEN THE BOX” vs “OPEN THE BOX WITH THE GLOVES”). The non-new-parser (such as the – ironically – newer ZILF parser, which is a cleanup of the pre-z6 Infocom old parser) doesn’t have the exact same design on identifying these structures, but it is similar. It’s a very confusing area that even I haven’t fully sorted through, but I’ve gained enough knowledge to understand what each part was doing and where to look for whatever I need.
So far, we have studied objects, routines and basic syntax, flow control to a very limited extent, and verbs. We’re off to a pretty good start – you have enough knowledge now for the next part, in which I will explore some of Milliways’s code and its alternatives in other coding languages. See you next time.
- When typing strings like “Run!!!”, curly quotes are not recognised by ZILF, so best to use something like Visual Studio Code so that problem doesn’t arise (which also includes a ZILF extension with bracket highlighting, so it’s highly recommended). ↩︎
- ZIL also understands ELSE in place of T specifically in conditional statements, but I just don’t use it because T is faster to type. ↩︎
- There are really easy workarounds, such as checking in the routine whether you used the right verb, and if not, rejecting the command. But there are always workarounds. ↩︎
- Because when the parser is correlating input to verbs, verbs are defined by their routine name, and the requirement (in simplified terms) is that it begins with “V-”. ↩︎
- Just to clarify: you place a comma before a global variable, an object, or a flag – and a full stop before a local variable. I placed a command before each in the example as it felt the most common use in that case. ↩︎


I’m curious if you know how the code for the labeled cubes in Spellbreaker worked? Player-provided object names is clearly doing something wacky to the parser.