the-infocom-files / zork2

Zork II: The Wizard of Frobozz
8 stars 4 forks source link

"Fantasize" spell doesn't work, and can crash Frotz #8

Open eriktorbjorn opened 5 years ago

eriktorbjorn commented 5 years ago

One of the Wizard's spells, "Fantasize", is supposed to make you hallucinate. This is handled in the PRINT-CONT routine, like so:

            <COND %<COND (<==? ,ZORK-NUMBER 2>
                      '(<NOT .Y>
                    <COND (<AND <0? .LEVEL>
                            <==? ,SPELL? ,S-FANTASIZE>
                            <PROB 20>>
                           <TELL "There is a "
                             <PICK-ONE ,FANTASIES>
                             " here." CR>
                           <SET 1ST? <>>)>
                    <RETURN>))
                     (ELSE
                      '(<NOT .Y>
                    <RETURN>))>

I'm not quite sure about the meaning of this, but Y is the object it would usually print a description for. So maybe that means that the hallucination is always added as the last object?

LEVEL may have something to do with how deeply nested the object list is. It's -1 when showing the contents of a room, and 0 when showing the contents of a container. Is the hallucination really only meant to affect containers?

Either way, if you manage to trigger a hallucination, Frotz will crash with the fatal error "Store out of dynamic memory". That's because of how PICK-ONE and FANTASIES work. The table has to be an LTABLE, which it is, but it also uses the first element for something that I can't quite figure out. So every other such LTABLE puts 0 as its first element. But FANTASIES doesn't.

So at the very least, to fix this we need to change FANTASIES to:

<GLOBAL FANTASIES
        <LTABLE 0 "pile of jewels" "gold ingot" "basilisk"
                "bulging chest" "yellow sphere" "grue"
                "convention of wizards" "copy of ZORK I">>

To test this, I also added the following debug code:

<SYNTAX DEBUG = V-DEBUG>
<ROUTINE V-DEBUG ()
         <MOVE ,IRON-BOX ,HERE>
         <FCLEAR ,IRON-BOX ,INVISIBLE>
         <SETG SPELL? ,S-FANTASIZE>>

Because waiting for the Wizard to cast that spell on you is very slow and tedious.

But even with that, something's wrong. Normally, it would print:

There is a dented steel box here.
The steel box contains:
  A fancy violin.

But now, when fantasizing, it prints something like:

There is a dented steel box here.
There is a bulging chest here.
There is a Stradivarius here.

(I did see hallucinating work as intended once in Mini-Zork II, but it strikes me that this may have been because I was in the balloon basket, i.e. in a vehicle.)

I'm not sure how it worked it older versions. If, indeed, it worked at all. In fact, in Release 48 there are no references at all to the FANTASIES table.

eriktorbjorn commented 5 years ago

Looking at the Mini-Zork II message I saw, that may have had the same bug after all:

>WAIT
Time passes...
The balloon ascends.

Volcano By Wide Ledge, in the basket
You are about 200 feet up, very near the rim of the volcano. To the west is
a wide ledge.
There is a yellow sphere here.
There is a blue label here.

I.e. the objects aren't listed as being in the basket.

eriktorbjorn commented 5 years ago

I did get another crash, but I think it may have been caused by this bug. I was in brief mode at the time when I saw this:

>NORTH
North End of Garden
There is a beautiful unicorn here cropping grass.
There is a Fatal error: Store out of dynamic memory
[Hit any key to exit.]

So does the "fantasize" spell work "better" in brief mode than in verbose mode? Worth investigating, perhaps?

Edit: The unicorn has OPENBIT, so it's probably related to there being a "container" in the room.

eriktorbjorn commented 4 years ago

This one is still confusing me. But it looks like it will only print fantasies if there is an open container in the room. It will then print the fantasy as if it's part of the room's inventory, but it will (or at least may?) also print the container's contents as if is part of the room's inventory. E.g.

>LOOK
Inside the Barrow
You are inside an ancient barrow hidden deep within a dark forest. The barrow
opens into a narrow tunnel at its southern end. You can see a faint glow at the
far end.
There is a dented steel box here.
The steel box contains:
  A fancy violin

>LOOK
Inside the Barrow
You are inside an ancient barrow hidden deep within a dark forest. The barrow
opens into a narrow tunnel at its southern end. You can see a faint glow at the
far end.
There is a dented steel box here.
There is a gold ingot here.
There is a Stradivarius here.

Of course, in this example I have fixed the crash but it still does not seem right.

eriktorbjorn commented 4 years ago

I just realized that Zarf's repository contains an earlier version (identified as Release 22 with Mac-specific tweaks). I guess I should take a closer look at that one, though I don't have the time right now.

Edit: At the very least, Release 22 doesn't depend on there being a container in the room, and it doesn't seem to mess up printing the contents of containers if there is. There are two loops in PRINT-CONT. In Release 22, the hallucination is printed in the second loop. In the most recent version, it's printed in the first loop. I need to look closer at this...

eriktorbjorn commented 4 years ago

If I understand the old PRINT-CONT correctly, though I'm sure this description leaves out some corner cases:

The routine keeps track of which LEVEL the objects are listed at. Objects in the room are listed at level -1. There is also a "verbose" flag (V?) but I'm not sure it fills any function.

It works in two passes, unless it's printing the player's inventory in which case the first pass is skipped.

The first pass prints the description of all untouched objects. If the object has NDESCBIT, no description is printed. If the object can be looked into (e.g. it's an open container), and its parent does not have a DESCFCN, its contents are listed with PRINT-CONT at level 0. (It's during this pass that the new code, incorrectly it seems, prints hallucinations.)

In the second pass, the adventurer any any vehicle he's in are skipped. Invisible objects and objects with NDESCBIT are also left out. Unless it's printing the player's inventory, untouched objects with FDESCare left out (since they were printed by the first pass). If it's the first object to be listed, a "firster" may be printed (e.g. "Sitting on the object is:"). If a firster is printed, the level is increased by one. The object is then printed by DESCRIBE-OBJECT, which uses the level to tell if and how much the text should be indented. If the object can be seen into, the level is increased by one and the contents is listed with PRINT-CONT.

Once the second pass runs out of objects, it may print a hallucination (if the current level is 0) and the contents of the player's vehicle (at the current level plus 1).

Phew!

So what does that mean? Well, for one thing it means that there will never be hallucinations in a room unless it contains other objects, and at least one of them has to be printed by the second pass (i.e. if the room only contains untouched objects with FDESC, there will be no hallucination because then the level will still be -1.)

Hallucinations only appear to be part of the room's inventory, not of any container in the room.

Indentation can sometimes be a bit odd. For instance:

>LOOK
Gazebo
This is a gazebo in the midst of a formal garden. It is cool and restful here. A
tea table adorns the center of the gazebo.
Sitting on the table is:
  A matchbook
  A china teapot
  A place mat
  A newspaper
  A letter opener

>DROP SWORD
Dropped.

>LOOK
Gazebo
This is a gazebo in the midst of a formal garden. It is cool and restful here. A
tea table adorns the center of the gazebo.
An Elvish sword of great antiquity is here.
  Sitting on the table is:
    A matchbook
    A china teapot
    A place mat
    A newspaper
    A letter opener

That's probably because the presence of the sword causes LEVEL to be increased from -1 to 0. So the contents of the table is listed at higher level than it was before. What if I had dropped a container instead?

Gazebo
This is a gazebo in the midst of a formal garden. It is cool and restful here. A
tea table adorns the center of the gazebo.
There is a dented steel box here.
The steel box contains:
  A fancy violin
  Sitting on the table is:
    A matchbook
    A china teapot
    A place mat
    A newspaper
    A letter opener

Putting the box on the table works fine, though:

Gazebo
This is a gazebo in the midst of a formal garden. It is cool and restful here. A
tea table adorns the center of the gazebo.
Sitting on the table is:
  A steel box
  The steel box contains:
    A fancy violin
  A matchbook
  A china teapot
  A place mat
  A newspaper
  A letter opener

If I can duplicate these cases with the new code, and make the hallucinations work, I'd say I'm in pretty good shape.

eriktorbjorn commented 4 years ago

The indentation seems to have been fixed in later games. E.g. this is what part of the routine looks like in Zork II:

                              (<AND <FIRST? .Y> <SEE-INSIDE? .Y>>
                               <SET LEVEL <+ .LEVEL 1>>
                               <PRINT-CONT .Y .V? .LEVEL>)>)>

The zork-substrate code looks like this:

                  (<AND <FIRST? .Y> <SEE-INSIDE? .Y>>
                   <SET LEVEL <+ .LEVEL 1>> ;"not in Zork III"
                   <PRINT-CONT .Y .V? .LEVEL>
                   <SET LEVEL <- .LEVEL 1>> ;"not in Zork III")>)>

But that seems to indent the object listing in the same odd way. In Planetfall, it looks like this:

                  (<AND <FIRST? .Y> <SEE-INSIDE? .Y>>
                   <PRINT-CONT .Y .V? .LEVEL>)>)>

And the indentation seems to work just fine there. I'll probably file a new bug report later, when I've investigated this a bit further.

eriktorbjorn commented 4 years ago

So if I understand things correctly, this is what PRINT-CONT should look like to both fix the Fantasize spell and the strange indentation:

<ROUTINE PRINT-CONT (OBJ "OPTIONAL" (V? <>) (LEVEL 0)
             "AUX" Y 1ST? SHIT AV STR (PV? <>) (INV? <>))
     <COND (<NOT <SET Y <FIRST? .OBJ>>> <RTRUE>)>
     <COND (<AND <SET AV <LOC ,WINNER>> <FSET? .AV ,VEHBIT>>
        T)
           (ELSE <SET AV <>>)>
     <SET 1ST? T>
     <SET SHIT T>
     <COND (<EQUAL? ,WINNER .OBJ <LOC .OBJ>>
        <SET INV? T>)
           (ELSE
        <REPEAT ()
            <COND (<NOT .Y> <RETURN>)
                  (<EQUAL? .Y .AV> <SET PV? T>)
                  (<EQUAL? .Y ,WINNER>)
                  (<AND <NOT <FSET? .Y ,INVISIBLE>>
                    <NOT <FSET? .Y ,TOUCHBIT>>
                    <SET STR <GETP .Y ,P?FDESC>>>
                   <COND (<NOT <FSET? .Y ,NDESCBIT>>
                      <TELL .STR CR>
                      <SET SHIT <>>
                      ;<SET 1ST? <>>)>
                   <COND (<AND <SEE-INSIDE? .Y>
                       <NOT <GETP <LOC .Y> ,P?DESCFCN>>
                       <FIRST? .Y>>
                      <COND (<PRINT-CONT .Y .V? 0>
                         <SET 1ST? <>>)>)>)>
            <SET Y <NEXT? .Y>>>)>
     <SET Y <FIRST? .OBJ>>
     <REPEAT ()
         <COND (<NOT .Y>
            %<COND (<==? ,ZORK-NUMBER 2>
                '<COND (<AND <0? .LEVEL>
                         <EQUAL? ,SPELL? ,S-FANTASIZE>
                         <PROB 20>>
                    <TELL "There is a "
                          <PICK-ONE ,FANTASIES>
                          " here." CR>
                    <SET 1ST? <>>)>)>
            <COND (<AND .PV? .AV <FIRST? .AV>>
                   <SET LEVEL <+ .LEVEL 1>> ;"not in Zork III"
                   <PRINT-CONT .AV .V? .LEVEL>)>
            <RETURN>)
               (<EQUAL? .Y .AV ,ADVENTURER>)
               (<AND <NOT <FSET? .Y ,INVISIBLE>>
                 <OR .INV?
                 <FSET? .Y ,TOUCHBIT>
                 <NOT <GETP .Y ,P?FDESC>>>>
            <COND (<NOT <FSET? .Y ,NDESCBIT>>
                   <COND (.1ST?
                      <COND (<FIRSTER .OBJ .LEVEL>
                         <COND (<L? .LEVEL 0>
                            <SET LEVEL 0>)>)>
                      <SET LEVEL <+ 1 .LEVEL>>
                      <SET 1ST? <>>)>
                   <COND (<L? .LEVEL 0> <SET LEVEL 0>)>
                   <DESCRIBE-OBJECT .Y .V? .LEVEL>)
                  (<AND <FIRST? .Y> <SEE-INSIDE? .Y>>
                   <PRINT-CONT .Y .V? .LEVEL>)>)>
         <SET Y <NEXT? .Y>>>
     <COND (<AND .1ST? .SHIT> <RFALSE>) (T <RTRUE>)>>

And, as stated earlier, FANTASIES has to be changed to:

<GLOBAL FANTASIES
        <LTABLE 0 "pile of jewels" "gold ingot" "basilisk"
                "bulging chest" "yellow sphere" "grue"
                "convention of wizards" "copy of ZORK I">>
eriktorbjorn commented 4 years ago

Or, if people prefer to read it as a diff, these are the two changes I made. The first one moves the printing of the hallucinations to the second loop, like in zork2-mac-r22. I think this matches the original behavior, and perhaps even the original intention:

@@ -1759,19 +1759,7 @@ long description (fdesc or ldesc), otherwise will print short."
        <SET INV? T>)
           (ELSE
        <REPEAT ()
-           <COND %<COND (<==? ,ZORK-NUMBER 2>
-                     '(<NOT .Y>
-                   <COND (<AND <0? .LEVEL>
-                           <==? ,SPELL? ,S-FANTASIZE>
-                           <PROB 20>>
-                          <TELL "There is a "
-                            <PICK-ONE ,FANTASIES>
-                            " here." CR>
-                          <SET 1ST? <>>)>
-                   <RETURN>))
-                    (ELSE
-                     '(<NOT .Y>
-                   <RETURN>))>
+           <COND (<NOT .Y> <RETURN>)
                  (<EQUAL? .Y .AV> <SET PV? T>)
                  (<EQUAL? .Y ,WINNER>)
                  (<AND <NOT <FSET? .Y ,INVISIBLE>>
@@ -1790,6 +1778,14 @@ long description (fdesc or ldesc), otherwise will print short."
     <SET Y <FIRST? .OBJ>>
     <REPEAT ()
         <COND (<NOT .Y>
+           %<COND (<==? ,ZORK-NUMBER 2>
+               '<COND (<AND <0? .LEVEL>
+                        <EQUAL? ,SPELL? ,S-FANTASIZE>
+                        <PROB 20>>
+                   <TELL "There is a "
+                         <PICK-ONE ,FANTASIES>
+                         " here." CR>
+                   <SET 1ST? <>>)>)>
            <COND (<AND .PV? .AV <FIRST? .AV>>
                   <SET LEVEL <+ .LEVEL 1>> ;"not in Zork III"
                   <PRINT-CONT .AV .V? .LEVEL>)>

The second change removes some fiddling with the LEVEL variable to match later games. This seems to fix the sometimes strange indentation when printing the contents of containers in the room.

@@ -1805,9 +1805,7 @@ long description (fdesc or ldesc), otherwise will print short."
                   <COND (<L? .LEVEL 0> <SET LEVEL 0>)>
                   <DESCRIBE-OBJECT .Y .V? .LEVEL>)
                  (<AND <FIRST? .Y> <SEE-INSIDE? .Y>>
-                  <SET LEVEL <+ .LEVEL 1>> ;"not in Zork III"
-                  <PRINT-CONT .Y .V? .LEVEL>
-                  <SET LEVEL <- .LEVEL 1>> ;"not in Zork III")>)>
+                  <PRINT-CONT .Y .V? .LEVEL>)>)>
         <SET Y <NEXT? .Y>>>
     <COND (<AND .1ST? .SHIT> <RFALSE>) (T <RTRUE>)>>

There are details of this code that I do not fully understand, but this is as good as I'm likely to get it by myself.