Logo Adventure for C64 Terrapin Logo

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.

Terrapin published Logo Adventure on the C64 Logo Utilities Disk

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.

TO ADVENTURE
NODRAW
PR [WELCOME TO LOGO ADVENTURE]
PR [WRITTEN BY DON HOPKINS]
PR []
PR [TYPE HELP FOR HELP]
PR []
INIT
LOOK
END

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:

TO HELP
PR [TO MOVE, TYPE]
PR [N, S, E, W]
PR [FOR NORTH, SOUTH, EAST, WEST]
PR []
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 []
PR [THERE ARE SOME SPECIAL THINGS YOU CAN]
PR [DO, LIKE SAYING EXAMINE SOMETHING.]
PR []
PR [TYPE SCORE TO SEE YOUR SCORE, AND]
PR [DONE TO QUIT.]
PR [GOOD LUCK!]
CMD
END

The INIT word sets up our model of the Adventure universe, which is very simple: some rooms, some items, and a player.

TO INIT
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
END

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
END
TO SETIT :THING
MAKE "IT :THING
OP :THING
END

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.

The first thing a recursive function should usually do is check to see if it’s done. So IF :I = [] STOP means stop if we are at the end of the list of items.

The next thing it does is to check if the :F parameter is equal to 1 or not. If that is true (IFT), it defines a magical function, and if not (IFF), it cleans up that function definition.

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.

(IFT
(DEFINE
(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.

TO CMD
PR []
PRINT1 "COMMAND
TOPLEVEL
END

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 N
MOVEDIR 1
END

TO E
MOVEDIR 2
END
TO S
MOVEDIR 3
END
TO W
MOVEDIR 4
END
TO MOVEDIR :DIR
MAKE "TRYMOVE ITEM :DIR ITEM :RNUM :RMOVES
TEST :TRYMOVE = 0
IFT PR [YOU CAN'T GO THAT WAY.]
IFT CMD
PR "OK.
MAKE "RNUM :TRYMOVE
LOOK
END
TO INVENT
PITEMS - 1
CMD
END

TO LOOK
PR ITEM :RNUM :RNAMES
PITEMS :RNUM
CMD
END
TO PITEMS :LOC
PITEMS2 :LOC :ITEMS
END
TO PITEMS2 :LOC :I
IF :I = [] STOP
IF FIRST FIRST :I = :LOC PRINT LAST FIRST :I
PITEMS2 :LOC BF :I
END
TO IT
OP :IT
END

TO EVERYTHING
OP "EVERYTHING
END
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.
CMD
END
TO TAKE :THING
GET :THING
END
TO GETALL
GETALLITEMS :ITEMS
END
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
END
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.
CMD
END
TO DROPALL
DROPALLITEMS :ITEMS
END

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
END
TO HERE? :ITEM
LOCAL "LOC
MAKE "LOC ITEMLOC :ITEM
OP ANYOF - 1 = :LOC :RNUM = :LOC
END
TO ITEMLOC :ITEM
OP ITEMLOC2 :ITEM :ITEMS
END
TO ITEMLOC2 :ITEM :I
IF :I = [] OP 0
IF LAST FIRST :I = :ITEM OP FIRST FIRST :I
OP ITEMLOC2 :ITEM BF :I
END

TO PUTITEM :ITEM :LOC
MAKE "ITEMS PUTITEM2 :ITEM :LOC :ITEMS
END
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
END
TO SEENO :I
PR SE [I SEE NO] SE :I "HERE!
CMD
END

TO IHAVE? :ITEM
OP - 1 = ITEMLOC :ITEM
END

TO PERIOD :WORD
OP WORD :WORD ".
END
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
LOOK
END

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
CMD
END

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.]
CMD
END

TO SCORE
PR ( SE [YOUR SCORE IS] SCORE2 :ITEMS [POINTS.] )
CMD
END
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
END
TO NOTHING
PR [NOTHING HAPPENS.]
CMD
END

TO GAME
OP "ADVSAVE
END
TO DONE
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
END