Roger Firth's IF pages

Home

InFancy -- using Inform objects

Email
Back up

An object's external name (xname) -- the double-quoted text in the header line -- is used whenever Inform needs to tell you of the object's presence. You have considerable control over the way in which the xname is used.

"pluralname" and "proper" attributes

When a object is mentioned in a room's description, or in the player's inventory, Inform generally prefixes it with the indefinite article A or AN. Two attributes modify this behavior: pluralname changes the prefix to SOME (giving, for example "On the table are some scissors."), and proper removes the prefix altogether (as in "You can also see Aunt Bessie here.").

"article" property

A related technique is to create an article property -- a string or routine -- which defines an appropriate indefinite article. For example, you could use this to cause messages like "On the table is a handful of grain." or "You can also see your Aunt Bessie here.".

Note that the pluralname also affects pronouns, causing its possessor to be addressable as THEM rather than IT. That's fine for some things, for example the scissors, but not for others. You might use article "some" to achieve "On the table is some chocolate." which is still addressable as IT.

There's also an articles property, but it's useful only in non-English games.

"short_name" property

The short_name provides a way of extending or replacing an object's xname at run-time. We can use it to bring the cork into play, by making the bottle announce itself as a "corked bottle" when appropriate. All it takes is this simple routine:

 
Object  bottle "bottle" kitchen_table
  with  parse_name [ wd adj_count noun_count;
        if (parser_action == ##TheSame) return -2;
        wd = NextWord();
        while (wd == 'green' or 'glass' or 'wine' or 'corked')
            { wd = NextWord(); adj_count++; }
        while (wd == 'bottle' or 'flask' or 'flagon')
            { wd = NextWord(); noun_count++; }
        if (noun_count > 0) return noun_count + adj_count;
        return 0;
        ],
        short_name [;
            if (cork in self) print "corked ";
            rfalse;
            ]
  has   transparent;

The routine returns false, so Inform carries on and prints the original xname after the word "corked ". Alternatively, the routine could print a completely new xname and then, by returning true, prevent the original xname from being output.

As things stand, we can examine the cork but not take it: Inform treats something inside a transparent object as being part of that object. Let's fix that, by giving the cork a before routine:

 
Object  cork "cork" bottle
  with  name 'cork' 'stopper',
        before [;
            Remove, Take, Pull:
                if ((self notin bottle) || (second ~= bottle or nothing))
                    rfalse;
                move self to player;
                "You pull the cork from the bottle.";
            Insert:
                if ((self notin player) || (second ~= bottle)) rfalse;
                move self to bottle;
                "You put the cork in the bottle.";
            ];

Finally, while we're dealing with the cork, we can extend the vocabulary by adding CORK and UNCORK verbs, so that UNCORK THE BOTTLE is a synonym for REMOVE THE CORK FROM THE BOTTLE:

 
Object  bottle "bottle" kitchen_table
  with  parse_name [ wd adj_count noun_count;
        if (parser_action == ##TheSame) return -2;
        wd = NextWord();
        while (wd == 'green' or 'glass' or 'wine' or 'corked')
            { wd = NextWord(); adj_count++; }
        while (wd == 'bottle' or 'flask' or 'flagon')
            { wd = NextWord(); noun_count++; }
        if (noun_count > 0) return noun_count + adj_count;
        return 0;
        ],
        short_name [;
            if (cork in self) print "corked ";
            rfalse;
            ],
        before [;
            Uncork: <<Remove cork self>>;
            Cork:   <<Insert cork self>>;
            ]
  has   transparent;

[ UncorkSub;    "You can't uncork that."; ];
[ CorkSub;      "You can't cork that."; ];

Verb    'uncork'    * noun  -> Uncork;
Verb    'cork'      * noun  -> Cork;

Let's see those verbs in action:

 
Kitchen
Oddly, there are no exits.

You can see a table (on which are a bottle and a glass tumbler) here.

>TAKE THE BOTTLE AND GLASS
corked bottle: Taken.
glass tumbler: Taken.

>UNCORK THE BOTTLE
You pull the cork from the bottle.

>INV
You are carrying:
  a cork
  a glass tumbler
  a bottle

There's also a short_name_indef property, but it's useful only in non-English games.

"invent" property

The invent property gives you some control over the way an object is described by the INVENTORY command. An object's invent routine is called twice, once before the object's xname is printed, and then again afterwards. Returning true from the routine prevents further processing, so either of these examples would have the same effect:

 
Object  cork "cork" bottle
  with  name 'cork' 'stopper',
        dots
        invent [;
            if (inventory_stage == 1) { print "a cork from a wine bottle"; rtrue; }
            ];

Object  cork "wine cork" bottle
  with  name 'wine' 'cork',
        dots
        invent [;
            if (inventory_stage == 2) { print " from a wine bottle"; rtrue; }
            ];

Actually, the cork isn't a very interesting example. Instead, we'll try something more ambitious -- putting some wine into the glass. First of all, here's our wine object:

 
Object  wine "red wine" glass
  with  name 'red' 'wine' 'plonk' 'liquid',
        article "some",
        capacity 2;

You can see that we're using the standard article property, as explained above, and that we've also added a capacity property (a standard property normally applied only to containers and supporters) whose use will become apparent in a moment. The wine is a child of the glass, whose definition now looks like this:

 
Object  glass "glass tumbler" kitchen_table
  with  name 'chipped' 'glass' 'tumbler',
        invent [ liq qty;
            liq = ChildWithProp(self,capacity);
            if (liq ~= 0) qty = liq.capacity;
            if (inventory_stage == 2) switch (qty) {
                2: print " (full)";
                1: print " (partly full)";
                default: ;
                }
            ]
  has   transparent;

[ ChildWithProp obj prop
        k;
        objectloop (k in obj) if (k provides prop) return k;
        return 0;
        ];

Lots going on here! This invent property is a routine with two local variables. liq is set by calling our little ChildWithProp() routine which tests whether any of the glass's child objects have a capacity property, if so returning that object. Then, qty is set to the value of the child object's capacity property, or remains at zero. Having obtained that property value from the child, we then use it at the second inventory_stage to print "(full)" or "(partly full)" after the glass's xname.

You might well ask: why not make the glass an open container? Two reasons: you'd then have to write code to prevent commands like PUT THE BOTTLE IN THE GLASS from being obeyed, and because the room description would then look like this:

 
Kitchen
Oddly, there are no exits.

You can see a table (on which are a corked bottle and a glass tumbler
(in which is some red wine)) here.

Those nested parentheses look a bit clumsy here, IMHO. You might also ask: why go to the trouble of providing a ChildWithProp() routine? why not simplify the glass thus:

 
Object  glass "glass tumbler" kitchen_table
  with  name 'chipped' 'glass' 'tumbler',
        invent [;
            if (inventory_stage == 2 && wine in self) switch (wine.capacity) {
                2: print " (full)";
                1: print " (partly full)";
                default: ;
                }
            ]
  has   transparent;

This would work perfectly well; the only disadvantage is that it hard-wires the wine object into the definition of the glass. By using the ChildWithProp() routine, we create a universal glass which can also be used for other liquids. (Actually, once we reach classes, we can change the routine to become ChildOfClass(), but this'll do to be going on with.)

"short_name" and "parse_name" properties revisited

Now that we've made the inventory listing sensitive to the contents of the glass, let's do the same with its xname, by providing a short_name property:

 
Object  glass kitchen_table
  with  name 'chipped' 'glass' 'tumbler',
        short_name [ liq qty;
            liq = ChildWithProp(self,capacity);
            if (liq ~= 0) qty = liq.capacity;
            if (qty > 0)
                print "glass of ", (name) liq;
            else
                print "empty glass tumbler";
            rtrue;
            ],
        dots
  has   transparent;

And now, instead of always announcing itself as "a glass tumbler", the glass is either "a glass of red wine" or "an empty glass tumbler".

 
Kitchen
Oddly, there are no exits.

You can see a table (on which are a corked bottle and a glass of red wine) here.

>TAKE THE GLASS OF RED WINE
I only understood you as far as wanting to take the glass of red wine.

Whoops! If we change the way in which the glass announces itself, we've got to expect that the player will want to use those same words. Time for another parse_name routine:

 
Object  glass kitchen_table
  with  parse_name [ liq qty wd container_count contents_count;
            if (parser_action == ##TheSame) return -2;
            liq = ChildWithProp(self,capacity);
            if (liq ~= 0) qty = liq.capacity;
            for (wd=NextWord() : wd~=0 : wd=NextWord()) switch (wd) {
                'chipped','glass','tumbler':
                    container_count++;
                 'of':
                    if (container_count > 0) container_count++;
                'empty','nothing':
                    if (qty == 0) contents_count++;
                 default:
                    if (qty > 0 && IsAWordIn(wd,liq,name)) contents_count++;
                    else jump exitGlass;
                }
            .exitGlass;
            if (container_count > 0) return container_count + contents_count;
            return 0;
            ],
        dots
  has   transparent;

[ IsAWordIn wd obj prop
        k l m;
        k = obj.&prop; l = (obj.#prop)/2;
        for (m=0 : m<l : m++) if (wd == k-->m) rtrue;
        rfalse;
        ];

Another mass of stuff to take on board. Work thought it slowly, and you'll see that we allow things like THE GLASS and CHIPPED GLASS, also THE EMPTY GLASS and GLASS OF NOTHING (if appropriate), and finally THE GLASS OF RED WINE. This last possibility is thanks to another little routine -- IsAWordIn() -- which looks in the child object's name property when parsing. Note that our glass in still independent of its contents: in another context, THE GLASS OF FULL CREAM MILK would be just as acceptable. (By the way, sorry about that jump; it seems like the cleanest way to exit both the switch and the for.)

Compatibility with Glulx

There's one last detail before we move on. In IsAWordIn() you'll see the statement l = (obj.#prop)/2; to find the number of bytes occupied by the property array and thence calculate its number of word-length entries. This works fine on Inform, but fails on Glulx, where there are four bytes to a word. Even if you're not using Glulx at the moment you might do so soon, so code defensively:

 
#ifndef WORDSIZE;
    Constant TARGET_ZCODE 0;
    Constant WORDSIZE 2+TARGET_ZCODE;
#endif;

[ IsAWordIn wd obj prop
        k l m;
        k = obj.&prop; l = (obj.#prop)/WORDSIZE;
        for (m=0 : m<l : m++) if (wd == k-->m) rtrue;
        rfalse;
        ];

Using Glulx, WORDSIZE is pre-defined as 4. It's not defined by Inform, so we set it to 2, and everything works properly. Note that by convention we also define TARGET_ZCODE, even though it's never actually used, and make that little artificial reference to it (setting WORDSIZE to 2+TARGET_ZCODE rather than simply to 2) to avoid a compiler warning.


That was a pretty heavy session! Next, we'll take things a bit easier, talking about object descriptions.