When I was 17, Terrapin published my first commercial code on their C64 Logo utilities disk: a Logo Adventure program, a simple non-graphical game that showed off Logo’s list processing and functional programming capabilities.
I love Terrapin Logo! I got away with not having to write a parser, by simply using the Logo top-level read-eval-print loop as the parser, and defining Logo words like LOOK, N, S, E, W, TAKE, EXAMINE, etc. So it’s really easy to cheat by examining and modifying the state of the world, but that helps you learn Logo!
The ADVENTURE word starts the game by switching to text mode (NODRAW), printing an introduction, setting up all the data structures by calling INIT, and printing a description of the current room by calling LOOK. First it sets [ADVENTURE] to be the startup function, so it starts the game automatically when you load it.
PR [WELCOME TO LOGO ADVENTURE]
PR [WRITTEN BY DON HOPKINS]
PR [TYPE HELP FOR HELP]
MAKE "STARTUP [ADVENTURE]
A good place to start our walk through the code would be the HELP word, which just prints out a bunch of helpful tips and wishes you luck, which should give you an idea of what’s to come:
PR [TO MOVE, TYPE]
PR [N, S, E, W]
PR [FOR NORTH, SOUTH, EAST, WEST]
PR [TYPE LOOK TO SEE WHAT ROOM YOU]
PR [ARE IN. YOU CAN GET AND DROP ITEMS.]
PR [INVENT SHOWS YOUR INVENTORY.]
PR [THE WORD "IT" MEANS THE LAST THING YOU]
PR [REFERRED TO.]
PR [THERE ARE SOME SPECIAL THINGS YOU CAN]
PR [DO, LIKE SAYING EXAMINE SOMETHING.]
PR [TYPE SCORE TO SEE YOUR SCORE, AND]
PR [DONE TO QUIT.]
PR [GOOD LUCK!]
The INIT word sets up our model of the Adventure universe, which is very simple: some rooms, some items, and a player.
MAKE "ITEMS [[1 0 SWORD] [1 0 HATCHET] [1 0 SHIELD] [2 100 GOLD] [2 100 DIAMOND] [2 50 AMULET] [3 0 SCREWDRIVER] [4 0 MACHINE] [0 100 WAND] [5 200 CROWN]]
MAKE "RMOVES [[0 2 3 0] [0 0 4 1] [1 4 0 0] [2 0 0 3] [0 0 0 0]]
MAKE "RNAMES [[YOU ARE IN THE WEAPON SHOP.] [THIS IS THE VAULT.] [THIS ROOM IS THE TOOLSHED.] [THIS IS THE ALTAR ROOM.] [YOU ARE IN A SECRET INCANTING ROOM.]]
MAKE "RNUM 1
INITITEMS :ITEMS 1
Rooms are numbered from 1 (to match 1-based Logo list indexes). The information about each room is stored in lists, indexed by the room number.
The RNAMES list is simply a list of room names, which are lists of words describing the room. The RMOVES list contains a list of four numbers per room. Those numbers tell the room number you can move to in each direction (in the order [North, East, South, West]), or 0 if you can’t move in that direction.
It’s possible to define one-way or crooked doors this way, but if you want a normal two-way door, each room should point back and forth to one another in opposite directions. But if you really want to make a twisty little maze of passages, then knock yourself out! But please give your players a lot of items to drop behind so they can map our your mazes.
The ITEMS list contains a bunch of lists describing the room number, score (a number) and name of each item (a string). The first thing in the list is the number of the room containing the item, or -1 if the item is in the player’s inventory, or 0 for nowhere. The second thing in the list is the item score: how many points you get for having that item in your inventory. The third thing in the list is the item name.
The most important but simplest part of the model is the player’s room number, RNUM. The N, S, E and W words move from room to room by changing the player’s room number by calling MOVEDIR (and waving the wand transports you to the secret incanting room by magically changing RNUM).
Theoretically, if the room number were 0, the player would be nowhere at all, and if it were -1, the player would be carrying themselves in their own inventory! But don’t worry: that could never happen, unless you used magic or hacked into the system, which is pretty easy since the system is a top-level Logo interpreter.
Finally INIT calls INITITEMS passing it the list of ITEMS as :I and the number 1 as :F. This recursively defines a bunch of magical functions referring to each of the items by name, which also remember the last item you referred to as the pronoun IT.
Those magical functions accomplish two things: We can refer to items by their name without typing additional quotes (by defining a function named for each item), and it automatically remembers the last item we referred to in the global variable IT, by calling the word SETIT.
TO INITITEMS :I :F
IF :I =  STOP
TEST :F = 1
IFT DEFINE LAST FIRST :I LPUT LPUT WORD "" LAST FIRST :I [OP SETIT] []
IFF DEFINE LAST FIRST :I 
INITITEMS BF :I :F
Now we’re doing some real fun list processing and function definition! The :I parameter is the list of items to recurse over, and the :F parameter controls whether we should initialize the items (1) or clean them up (0). The expression FIRST :I is the current item, and LAST FIRST :I is the name of the current item.
Finally INITITEMS recurses on itself to process the rest of the list, BF :I, which means (don’t giggle) “but first”, that is: all elements of :I but the first.
To explain what the magical functions do, I will give an example: When the item’s name is “SWORD”, it will define a function named SWORD with no parameters, whose body is [OP SETIT “SWORD], which outputs the result of calling SETIT with the parameter “SWORD”. The SETIT word sets the global variable IT to “SWORD” and returns “SWORD”. So we can say GET SWORD, then say DROP IT.
It’s helpful to know the definition of LPUT, which takes two parameters “thing” and “list”, and returns a copy of “list” with “thing” appended to the end. And also WORD, which takes two things and concatenates them into a word, which is just used in this case to convert a string to a word (like a LISP symbol). And DEFINE takes two arguments: the name of a function, and a list. The first element of the list is the list of parameters (an empty list in our case). The subsequent elements are list expressions to evaluate ([OP SETIT “SWORD] in our case).
Let’s unpack those expressions in the IFT clause by inserting some indentation and parenthesis and comments to make it look like LISP (since Logo is essentially LISP without parens, whose parser necessarily knows the number of parameters each function takes), and linking to the Logo function documentation.
(LAST (FIRST :I)) ; function name to define is the item name
(LPUT ; function body is a list of lists
(LPUT ; expression is a list like [OP SETIT "SWORD]
(WORD ; concatenate the two parameters to get word "SWORD
"" ; this just converts the string to a word
(LAST (FIRST :I))) ; the word is the item name
[OP SETIT]) ; output result of SETIT with item name appended
[]))) ; empty function with zero parameters to append to
Ugh! But if you think that’s hard to understand, take a look at some of the FORTH code I was writing at the time!
Finally, if the :F parameter to INITITEMS is not 1, then it executes the IFF clause which removes the function definition, by going DEFINE LAST FIRST :I . This doesn’t actually ever get called, apparently, but there you go.
The final sneaky trick to integrate the game with the Logo top level interpreter is the CMD word, which prints a “COMMAND” prompt, and jumps to the Logo TOPLEVEL to read and interpret a command from the player. Each Adventure word can finally call CMD at the end to show a prompt to the player.
My coffee is wearing off now so I’m going to take a break explaining things for now, but you can read the rest of the program here:
TO MOVEDIR :DIR
MAKE "TRYMOVE ITEM :DIR ITEM :RNUM :RMOVES
TEST :TRYMOVE = 0
IFT PR [YOU CAN'T GO THAT WAY.]
MAKE "RNUM :TRYMOVE
PITEMS - 1
PR ITEM :RNUM :RNAMES
TO PITEMS :LOC
PITEMS2 :LOC :ITEMS
TO PITEMS2 :LOC :I
IF :I =  STOP
IF FIRST FIRST :I = :LOC PRINT LAST FIRST :I
PITEMS2 :LOC BF :I
TO GET :ITEM
TEST :ITEM = "EVERYTHING
IFT GETALLITEMS :ITEMS
IF IHAVE? :ITEM ( PR [YOU ALREADY HAVE] PERIOD :ITEM ) CMD
IF NOT HERE? :ITEM SEENO :ITEM
PUTITEM :ITEM ( - 1 )
PR SE :ITEM "TAKEN.
TO TAKE :THING
TO GETALLITEMS :I
IF :I =  CMD
TEST :RNUM = ITEMLOC LAST FIRST :I
IFT PUTITEM LAST FIRST :I ( - 1 )
IFT PR SE LAST FIRST :I "TAKEN.
GETALLITEMS BF :I
TO DROP :ITEM
TEST :ITEM = "EVERYTHING
IFT DROPALLITEMS :ITEMS
IF NOT IHAVE? :ITEM PR SE [YOU'RE NOT CARRYING THE] WORD :ITEM "! CMD
PUTITEM :ITEM :RNUM PR SE :ITEM "DROPPED.
TO DROPALLITEMS :I
IF :I =  CMD
TEST ITEMLOC LAST FIRST :I = ( - 1 )
IFT PUTITEM LAST FIRST :I :RNUM
IFT PR SE LAST FIRST :I "DROPPED.
DROPALLITEMS BF :I
TO HERE? :ITEM
MAKE "LOC ITEMLOC :ITEM
OP ANYOF - 1 = :LOC :RNUM = :LOC
TO ITEMLOC :ITEM
OP ITEMLOC2 :ITEM :ITEMS
TO ITEMLOC2 :ITEM :I
IF :I =  OP 0
IF LAST FIRST :I = :ITEM OP FIRST FIRST :I
OP ITEMLOC2 :ITEM BF :I
TO PUTITEM :ITEM :LOC
MAKE "ITEMS PUTITEM2 :ITEM :LOC :ITEMS
TO PUTITEM2 :ITEM :LOC :LIST
IF :LIST =  OP 
IF LAST FIRST :LIST = :ITEM OP FPUT FPUT :LOC BF FIRST :LIST BF :LIST
OP FPUT FIRST :LIST PUTITEM2 :ITEM :LOC BF :LIST
TO SEENO :I
PR SE [I SEE NO] SE :I "HERE!
TO IHAVE? :ITEM
OP - 1 = ITEMLOC :ITEM
TO PERIOD :WORD
OP WORD :WORD ".
TO WAVE :ITEM
IF NOT IHAVE? :ITEM PR SE [YOU ARE HOLDING NO] PERIOD :ITEM CMD
IF NOT :ITEM = "WAND NOTHING
IF ALLOF NOT :RNUM = 4 NOT :RNUM = 5 PR [NOTHING HAPPENS.] CMD
PR [POOF! THE SCENE CHANGES!]
IF :RNUM = 4 MAKE "RNUM 5 ELSE MAKE "RNUM 4
TO FIX :ITEM
IF IHAVE? :ITEM PR [YOU HAVE TO DROP IT TO FIX IT!] CMD
IF NOT HERE? :ITEM SEENO :ITEM
IF NOT :ITEM = "MACHINE PR [YOU CAN'T FIX THAT!] CMD
IF NOT ITEMLOC "WAND = 0 PR [THE MACHINE IS NOT BROKEN!] CMD
IF NOT IHAVE? "SCREWDRIVER PR [YOU DON'T HAVE THE PROPPER TOOLS TO] PR [FIX IT] CMD
PR [YOU FIX THE MACHINE WITH YOUR TRUSTY]
PR [SCREWDRIVER. UPON BEING FIXED, THE]
PR [MACHINE STARTS UP AND PRODUCES A WAND!]
PUTITEM "WAND 4
TO EXAMINE :ITEM
IF NOT HERE? :ITEM ( PR [I SEE NO] :ITEM [HERE!] ) CMD
IF :ITEM = "WAND PR [IT BEARS A FADED INSCRIPTION:] PR ["WAVE ME AND YOU'LL BE GLAD."] CMD
IF NOT :ITEM = "MACHINE PR SE [I SEE NOTHING SPECIAL ABOUT THE] PERIOD :ITEM CMD
IF NOT 0 = ITEMLOC "WAND PR [IT SEEMS TO BEAR THE MARKS OF A HASTY] PR [REPAIR JOB.] CMD
PR [IT IS BROKEN! YOU COULD FIX IT WITH]
PR [THE RIGHT TOOL.]
PR ( SE [YOUR SCORE IS] SCORE2 :ITEMS [POINTS.] )
TO SCORE2 :LIST
IF :LIST =  OP 0
IF NOT FIRST FIRST :LIST = - 1 OP SCORE2 BF :LIST
OP ( ITEM 2 FIRST :LIST ) + SCORE2 BF :LIST
PR [NOTHING HAPPENS.]
IF NOT :RNUM = 5 NOTHING
LOCAL "SCORE MAKE "SCORE SCORE2 :ITEMS
IF :SCORE = 0 NOTHING
PR SE [YOUR SCORE IS] :SCORE
IF :SCORE = 550 PR [PERFECT!] ELSE PRINT [THERE'S MORE TREASURE, THOUGH.] DONE