Doe's Inform 6 Primer



Scissors. Contents this "Lesson:"
  1. Verbalizing without conjugating.
    Creating new verbs and verbsub routines.
  2. Multiply the power, multiply the fun.
    Arrays, property arrays, and found_in.
Third Homework Assignment:
  1. Verbalizing without Conjugating
    Replace BurnSub with your own BurnSub. Create a two room game with various burnable and non-burnable items scattered throughout the two rooms. Give the player a lighter or matches. There is a good example of a matchbook in toyshop.inf by Dr. Nelson, at the archive.
  2. Multipy the power, multipy the fun
    Create a two room game with a hidden door visible in only one of the two rooms. Altering the door's found_in property, add and remove the door from the other room when the player performs some action. Optional:   Have one action add the door and another action remove the door.

    Third Homework Assignment Solutions
    Note: This zip file only contains a solution to the second part of the assignment because of Toy Shop (see above).

  1. Verbalizing without Conjugating

    Creating new verbs is very easy to do in Inform, which is one of its many strengths.

    A Doe Crib Note:
    Become thoroughly conversant with grammar.h.

    Defining Verbs

    The default Inform verbs are defined in grammar.h.

    Verb 'climb' 'scale'
          * noun                     -> Climb
          * 'up'/'over' noun         -> Climb;
    
    
    Verb defines a verb. Climb is the verb and scale is its synonym. Alternate prepositions can be listed together when separated by a forward slash. Noun is the verb's object. Climb (after the ->) is the verb routine that will be executed when the climb command is entered by the player. That's pretty straight forward.

    A Doe Crib Note:
    Verb routines are always followed by "Sub" in code, but when part of a grammar declaration, such as in grammar.h, the Sub suffix is left off. Bit strange, but that is how it is.

    So creating another synonym for climb is as simple as:
    Verb 'clamber' = 'climb';
    
    Creating Verb Routines

    I don't think you will find the grammar used to create new verbs difficult. However, creating new verbsub routines (sub = subroutine) is a tad more complicated.

    All the default verbs have a corresponding verbsub routine. But only a few actually do something, most just print a default Inform library message. Graham calls these group 3 verbs, see page 92 of DM 3. These verbs have no after routine, only a before routine.

    But suppose, for instance, you want burn to do something? Normally it just prints, "This dangerous act would achieve little." (L__M prints the corresponding Inform library message.)

    [BurnSub; L__M(##Burn,1,noun); ];                 <- The original BurnSub in verblibm.h.
    
    Burn: "This dangerous act would achieve little."  <- The burn message in english.h.
    
    Well, first BurnSub needs to be replaced. It can be replaced by including in your code (before parser.h):
    Replace BurnSub;
    
    Now that it has been replaced, a new Burnsub routine needs to be provided.

    Let's create a paper attribute, is_paper (this could be a class, but we haven't gotten that far yet). So the routine would be:
    [ BurnSub;
      if ((match notin player) || (match in player && match hasnt light))
         "You have no source of flame.";
       if (noun has animate) 
          print_ret "I don't suppose ", (the) noun, " would care for that.";
       if (noun has light)
          print_ret (The) noun, " is already burning.";
       if (noun has is_paper)
       {  remove noun;
          print_ret (The) noun, " catches fire and burns down to nothing.";
       }
      "That dangerous act would achieve little.";
    ];
    
    A Doe Crib Note:
    Actually including burn in a game is not recommended, because it requires a lot of coding.

    Okay, the new Burnsub now covers these situations:  the player trying to light something without having a match, the player having a match but it is unlit, trying to burn a creature, trying to burn something that is already burning, and burning up something made of paper. If none of these conditions are met, the usual default message, "This dangerous act would achieve little." is finally printed.

    Doesn't look like this accomplishes much, does it? But let's say there is a bonfire in the game.

    Object bonfire "bonfire"
      with name "bonfire" "fire",
      description
      [; if (self has light) "The bonfire burns merrily away.";
         "Someone has stacked a bonfire here, ready to be burned.";
      ],
      before
      [; Burn : If (self hasnt light)
                { if ((match in player) && (match has light))
                  {  give self light;
                     "You touch the match to the bonfire and flames leap up.";
                  }
                }
      ];
    
    Of course, it would probably also use a timer to burn the bonfire down to ashes.

    THE MOST IMPORTANT THING TO REMEMBER IS:   Complicated actions should be done in an object's before routine, not in a verb's verbsub. In this case, lighting the bonfire should be done in its before routine. Because a verbsub is only executed if the object to be affected has no (a zero) response for that verb. A zero response occurs when:  1. that object has no before routine (value 0), 2. that object's before routine has no response for that verb (returns 0), or 3. that object's before routine returns false for that verb (0).

    1. No before routine - The man has no before routine, so Burnsub will be called and print "I don't suppose the man would care for that." (since man has animate).
      Object man "man"
           has animate male
           with name "man",
           life [; etc.; ];
      
    2. No response in the object's before routine for that verb - The chair has no response in its before routine for Burn, so BurnSub will be called and print "This dangerous act would achieve little."
      Object chair "chair"
           has supporter enterable
           with name "chair" "plastic" "ordinary",
           description "It's an ordinary, plastic chair.",
           before
           [; Enter : if (man in chair)
                         "It seems to be occupied.";
           ];
      
    3. Negative reponse in the object's before routine for that verb - The chair now has a message response for Burn, but it will still return false (since print just prints a message but does not return 1). So BurnSub will still be called to print its default message. Taken all together it will appear on the screen as -- "Plastic really smells when it burns. This dangerous act would achieve little." (Note that this is just an example, as this would not be a particularly good way to respond.)
      Object chair "chair"
           has supporter enterable
           with name "chair" "plastic" "ordinary",
           description "It's an ordinary, plastic chair.",
           before
           [; Enter : if (man in chair)
                         "It seems to be occupied.";
              Burn  : print "Plastic really smells when it burns. ";
           ];
      
      
    When an object has a response for a verb, that verb's routine will never be called. Maybe you can see why this means complicated actions should be done in an object's before routine. Remember, Inform's built-in routines handle defaults, so an author's coding is basically just exception handling ( True or False?, #1 ). When creating/changing verbs one should follow this same reasoning. Verbsubs are for default responses to a verb. When an object needs to respond differently to a verb (action), then that response should be in that object's before routine (and after routine, if present). This was one of the hardest things for me to learn.
    [ BurnSub;
      if ((match notin player) || (match in player && match hasnt light))
         "You have no source of flame.";
       if (noun has animate) 
          print_ret "I don't suppose ", (the) noun, " would care for that.";
       if (noun has light)
          print_ret (The) noun, " is already burning.";
       if (noun has is_paper)
       {  remove noun;
          print_ret (The) noun, " catches fire and burns down to nothing.";
       }
      "That dangerous act would achieve little.";
    ];
    
    Object bonfire "bonfire"
      with name "bonfire" "fire",
      description
      [; if (self has light) "The bonfire burns merrily away.";
         "Someone has stacked a bonfire here, ready to be burned.";
      ],
      before
      [; Burn : If (self hasnt light)
                { if ((match in player) && (match has light))
                  {  give self light;
                     "You touch the match to the bonfire and flames leap up.";
                  }
                }
      ];
    
    The bonfire is only lit, returning true (1) and skipping BurnSub, if it isn't already burning and the player has a lit match. If it is already burning, its before routine returns 0, which calls BurnSub to print "The bonfire is already burning." If the player doesn't have a match or has a match that isn't lit, its before routine also returns 0, which calls BurnSub to print "You have no source of flame."

    A Doe Crib Note:
    Conditions you want specific to an object (in this case, the bonfire already lit and a lit match in player's possession) have to be tested in that object's before routine itself, because the corresponding verbsub will only be executed afterward (if at all).


    To reemphasize - Don't put all the ramifications of an action in a verb's verbsub. Only put in a verbsub what you want to happen when there is no response for that verb in an object's before routine. Only put in defaults. Handle all exceptions in objects' before routines. Dr. Nelson has already gone to a lot of trouble to create default responses for most verbs. Rely on that. Also, a verbsub can quickly become spaghetti code trying to anticipate all the possible exceptions. That is the underlying reason why exceptions should be handled on a case by case basis in before routines (and after routines, if you include them). Creating new verb routines this way will also save you a lot of wasted time and additional coding. Believe me, I know from experience ;-).


    Command Example
    Verb
    (creates a new verb)
    Verb 'xyzzy'
         *        -> XYZZY;
    
    =
    (creates a synonym)
    Verb 'clamber' = 'climb';
    
    Extend
    (adds to the definition of an existing verb in grammar.h,
    new addition is placed last in the list)
    Extend 'climb'
      * 'up'/'down' noun -> Climb;
    
    Extend verb last
    (adds to the definition of an existing verb, new addition
    is placed at the end of the list, same as above)
    Extend 'climb' last
      * 'up'/'down' noun -> Climb;
    
    Extend verb first
    (adds to the definition of an existing verb, new addition
    is placed at the front of the list)
    Extend 'climb' first
      * 'up'/'down' noun -> Climb;
    
    Extend verb replace
    (completely replaces the default definition in grammar.h)
    Extend 'climb' replace
      * 'up'/'down' noun -> Climb;
    


    Contents


  2. Multiply the power, multiply the fun.

    Array Overview

    An array is a sequential collection of items. Each item in an array is usually called an element. I always have problems with arrays, whether they start with 1 or 0, because it depends on the computer language. (I also have problems with bytes and words, but that is my problem.)

    In Inform, arrays start with 0 and array elements are accessed with an arrow >. Byte arrays use a short arrow ->. Word arrays use a long arrow -->. Most Inform arrays are word arrays, unless one is accessing characters (char) in a string, which is automatically an array of characters (an array of bytes). There are also tables, which are arrays of strings. The 0 element in a string array or table array holds the length of the array, which can come in very handy.

    -> (byte) --> (word) string (array of characters) table (array of strings)

    Array - The word array declares an array. Because the first element is 0, an array declared with 10 elements will actually be 0-9, not 1-10. There are good examples of using arrays in the Designer's Manual and arrays.inf, also by Dr. Nelson, at the archive.

    A Doe Crib Note:
    Inform does not support multi-dimensional arrays, which is one of its biggest drawbacks and why creating a database in Inform is almost impossible.

    Property Arrays

    Where arrays really become interesting is when they are used as object properties. When I discovered how to create new properties, especially array properties, my Inform programming really "took off."

    Found_in is the most commonly used property array, although it can be a routine instead. (Most other property arrays you must create yourself.) Found_in is often used with doors:

    Object hidden_door "hidden door"
      has door openable static
      with name "hidden" "door",
      found_in secret_passage library;
    
    Hidden door's found_in property is a two element array:   secret_passage is element 0, and library is element 1. The word, array, is not included in a property array, so the only way you can tell that it is an array is by the multiple number of elements. (Yes, name is also a property array, but a special one.)

    Let's say, instead of a regular door, we want a hidden one that will only appear in the library when a special book on the shelves is pulled (trite, but a good example).

    THE MOST IMPORTANT THING TO REMEMBER IS:   Inform arrays cannot be dynamically allocated. This means an array must be declared with its maximum potential size before runtime.

    A Doe Crib Note:
    Trying to access non-existent array elements, going outside array bounds, is probably the second biggest cause of vile zero errors from hell (see Attributes, #4.)
    Array test_array ->10; (declaration)
    test_array->10 = 1;    (out of bounds, remember it would be 0-9)
    

    In property arrays, allocating space for future elements can be done with 0. (Well, it used to be able to, but in Inform 6.21, with strict error checking, that generates an error -- when it did not generate an error in previous Inform versions. So to get around that, allocate space for a future locations by starting with dummy locations. Note that hidden_door now has this and other code changes.)
    Object nowhere;
    
    found_in secret_passage nowhere;
    
    Now the door will only appear in the secret_passage and not in the library until the right conditions are met.

    Property Array Operators - Property arrays also involve two other important operators:  .& and .#. # works the same as the 0 element for strings and tables, giving the length of the property array. & is used to access the property array's individual elements.

    Putting it altogether, here is the hidden door in action:
    Object hidden_door "hidden door"
      has door openable static
      with name "hidden" "door",
      when_open
      [; if (location==secret_passage) "The hidden door opens west.";
            "The bookcase is open, revealing a secret passage east.";
      ],
     when_closed "The hidden door is closed.",
     door_dir
     [; if (location==secret_passage) return w_to;
        return e_to;
     ],
     door_to
     [; if (location==secret_passage) return library;
        return secret_passage;
     ],
     before
     [; Close : if (location == library)
                  "It doesn't move, you'll have to close it some
                   other way.";
                 
     ],
     found_in secret_passage nowhere;
    
    Object nowhere;
    
    Object secret_passage "Passage"
      has light
      with description "It is hard to see in here.",
      w_to hidden_door,
      cant_go "You can't see where the passage leads to.";
    
    Object library "Library"
      has light
      with description "You are in a big library with a bookcase crammed 
                        full of books.",
      e_to
      [; if (self hasnt general) "You can't go that way.";
         return hidden_door;
      ];
    
    Object -> shelves "bookcase"
     has scenery
     with name "shelves" "bookcase" "bookshelves" "case", article "some",
     description "There are a lot of books, but one big red book
                  is especially noticeable.";
    
    Object -> big_book "big book"
      has scenery
      with name "book" "big" "red",
      description "The red book is unusually big. It appears as if the title
                   was painted on.",
      before
      [; Take : "It moves a little, but otherwise seems attached to
                  the bookcase.";
         Push, Turn : "It doesn't budge.";
         Pull :  print "You pull the book. ";
                 if (library has general)
                 {  (hidden_door.&found_in)-->1 = nowhere;
                    give hidden_door ~open;
                    give library ~general;
                    MoveFloatingObjects();
                    "^^~Whoosh!~ The bookcase swings closed.";
                 }
                 (hidden_door.&found_in)-->1 = library;
                 give hidden_door open;
                 give library general;
                 MoveFloatingObjects();
                 "^^~Whoosh!~ The bookcase swings open revealing a
                 secret passage to the east.";
     ];
    
    
    MoveFloatingObjects is a built-in Inform library routine that is automatically called at the beginning of each turn to adjust objects that can be found_in more than one location. But if you change found_in directly, MoveFloatingObjects needs to be called immediately to make the necessary adjustments. It is the routine that will actually move a found_in to the right locations, in this case, the hidden door to/from the library.

    The second element of the hidden door's found_in property, hidden_door.&found_in-->1, starts with "nowhere" and nowhere is added to/removed from the hidden_door array as the door is found/disappears. Or to put it another way, one side of the hidden door is moved to the library from nowhere when it is found, and then removed from the library back to nowhere when it is closed. Pulling the book opens the door when it is hidden (adds it to the library), or closes it when it is open (removes it from the library). Otherwise, the player could try to touch/open/close, etc. the door when it is supposed to be hidden. Note that library is given the general attribute because the door can't be tested to see if it is open (and thus present) when it isn't in the library.

    One can create property arrays which have elements that are:  objects, numbers, or strings. However, array elements can't be routines. Found_in usually uses an object array.

    The next example comes from my Infotips page under the Invisiclues section.

    Option Hint1 "How do I get the kitten out of the tree?"
      class Hintobj
      with the_hints "Try chopping down the tree."
                     "Too destructive? Try a garden hose."
                     "No hose? Read raif for other ways to get the
                      kitten down.";
    
    the_hints is a string array. Notice, as with found_in, there is no comma between the elements.
    class Hintobj
     with hints_seen 0,
     hint_done 0,
     the_hints "Put list of hints here (in the object of this class).",
     description
     [; self.showhints(); rtrue;
     ],
     showhints
     [ max i k;
    
       max = (self.#the_hints / 2);
    
       for (i = 1: (i <= max): i++)
       {   if ((i==1) && ((max > 1) && (self.hints_seen < max)))
              print "There are ", (LanguageNumber) max, " increasingly clear \
                     hints. Press H for another hint, any other key to quit.^^^";
    
           print "(", i, "/", max, ") ";
           print (string) (self.&the_hints-->(i-1));
           new_line; new_line;
    
    ! How many times through this loop? That is the number of the current
    ! hint. Starting with 1 = 0 hints_seen.
    
           if (self.hints_seen == (i-1)) self.hints_seen = self.hints_seen + 1;
    
    ! Only call read_char if currently on the last hint seen and
    ! skip on the very last hint.
    
           if ((i < max) && (i == self.hints_seen))
           {  @read_char 1 0 0 k;
              if (k ~= 'H' or 'h') return;
           }
       }
    
    ];
    
    max uses the # operator (divided by two, that confusing byte/word thing again) to find the number of elements in each hint object's the_hints array. self.&the_hints--> uses (i-1) because the for loop starts at 1 instead of 0 (since I find it easier to think of 1-10 than 0-9, as many do). Each time a new hint is read, the hints_seen property is incremented by 1, so that the next time previously seen hints will automatically show on the screen. (Note that regular Inform uses two-byte words, and Glulx Inform uses four-byte words, so in regular Inform you divide by two and in Glulx Inform you divide by four.)

    For the rest, see my infotips page.

    As you can see, using property arrays can lead to some fancy Inform programming!

    Array Commands
    Command Examples
    Array
    (declares an array)
    Array test_array-->5;
    Array test_array-->1 2 3 4 5;
    Array test_array string 5;
    Array test_array->"Name";
    Array test_array->'N' 'a' 'm' 'e';
    Array test_array table 5;
    Array test_array table 'name1' 'name2' 'name3' 
       'name4' 'name5';
    Array test_array table [;
    "Name One";
    "Name Two";
    "Name Three";
    "Name Four";
    "Name Five";
    ];
    
    array types:
    -> (byte)
    --> (word)
    string (array of characters, bytes)
    table (array of strings, words)
    see above

    Property Array Commands
    Command Example
    #
    (returns the length of an array stored in an object's
    property)
    len = (obj.#property_array) / 2;
    
    &
    (accesses an array element stored in an object's
    property)
    if (obj.&property_array-->0 == 1);
    


    Contents



Previous Page - Sections 4 & 5 and Second Homework Assignment    Next Page - Sections 8 & 9 and Fourth Homework Assignment

Top of Page    Infotips    Interactive Fiction Main Page

If this primer has been of any use to you I wouldn't mind hearing about it.