Roger Firth's IF pages

Home

InFancy -- using Inform objects

Email
Back up

Actually, classes are remarkably straightforward.

"Class" directive

The Class compile-time directive is almost identical to the Object directive we studied earlier, except that the header_data consists only of a mandatory iname; you can't supply a set_of_arrows, xname or parent_iname. In the body, you can use the same four class, with, private and has segments (and, as for Objects, the class and private segments are relatively rare):

 
Class   iname
  with  prop_name value,
        prop_name value,
        ...
        prop_name value
  has   attr_name  attr_name ...  attr_name;

The iname is the name of your new class of objects, so by convention you should give it an initial capital letter. One of the mostly commonly-encountered user-defined classes is the Room:

 
Class   Room
  with  description "A bare room."
  has   light;

Here we have a prototype -- a default set of properties and attributes -- which is automatically applied to each object of this class, unless over-ridden. Let's see a few examples:

 
Object  cell "Monastery cell"
  class Room;

Object  kitchen "Kitchen"
  class Room
  with  description "Oddly, there are no exits.";

Object  cellar "Gloomy cellar"
  class Room
  with  description "Even with your torch, you see only dust and cobwebs."
  has   ~light;

Notice the class segment; way back, we said we'd defer discussion of this segment until later, and now's the time. class simply allows us to say that an object is a member of a named (and predefined) class, from which it inherits its behaviour. The cell inherits both the default description and the light attribute. The kitchen provides its own description but inherits the light. The cellar over-rides both, so that it has a description which you can't see unless you provide your own light. Another example:

 
Class   Furniture
  with  before [; Take,Pull,Push,PushDir:
            print_ret (The) self, " is too heavy for that.";
            ]
  has   static supporter;

Object  kitchen_table "table" kitchen
  class Furniture
  with  name 'battered' 'pine' 'table',
        initial
            "In the centre of the room stands a battered pine table.",
        description
            "The table is scuffed and stained with indeterminate substances.";

This time, our Furniture class supplies static supporter attributes, plus a before property which generates a more credible response than the standard "That's fixed in place.". These characteristics will be valid for most pieces of furniture, but can again be over-ridden as circumstances dictate. (For example, a marble statue might be defined with ~supporter.)

The basic principle is: if your game has more than one object with the same general characteristics, you should consider basing it on a Class. The advantages are:

Drinking vessels and their contents might also be fairly useful classes, so let's generalise our glass and wine as Vessel and Liquid classes:

 
Class   Vessel
  with  parse_name [ liq qty wd container_count contents_count;
            if (parser_action == ##TheSame) return -2;
            liq = ChildOfClass(self,Liquid);
            if (liq ~= 0) qty = liq.add_liquid(0);
            for (wd=NextWord() : wd~=0 : wd=NextWord()) switch (wd) {
                'of':
                    if (container_count > 0) container_count++;
                'empty','nothing':
                    if (qty == 0) contents_count++;
                default:
                    if (IsAWordIn(wd,self,name)) container_count++;
                    else
                        if (qty > 0 && IsAWordIn(wd,liq,name)) contents_count++;
                        else jump exitVessel;
                }
            .exitVessel;
            if (container_count > 0) return container_count + contents_count;
            return 0;
            ],
        before [; Receive:
            print_ret (The) self, " is meant for holding liquids."; ]
  has   transparent;

Class   Liquid
  with  name 'liquid',
        add_liquid [ qty;
            self.capacity = self.capacity + qty;
            if (self.capacity > 0)
                return self.capacity;
            else
                { remove self; return 0; }
            ],
        description [; print_ret "It looks rather like ", (name) self, "."; ],
        article "some",
        before [ container; container = parent(self);
            Take:
                if (container ofclass Vessel) <<Take container>>;
                print_ret "You can't take a puddle of ", (name) self, ".";
            Drink:
                if (container ofclass Vessel) <<Drink container>>;
                print_ret "You can't drink a puddle of ", (name) self, ".";
            ],
        capacity 0;                 ! Liquid currently in the vessel

Notice that we've changed the Vessel's parse_name slightly to re-use our IsAWordIn() routine, so that the dictionary words defining it as a container ('chipped', 'glass' and 'tumbler') are now taken from the object's name property rather than being built in, and that ChildWithProp() has become ChildOfClass(). Also, the Liquid has an additional add_liquid property to handle the additional and removal of quantities of liquid -- this isn't essential, but it makes for cleaner and more self-contained objects. Here's our new glass and wine:

 
Object  glass kitchen_table
  class Vessel
  with  name 'chipped' 'glass' 'tumbler',
        short_name [ liq qty;
            liq = ChildOfClass(self,Liquid);
            if (liq ~= 0) qty = liq.add_liquid(0);
            if (qty > 0)
                print "glass of ", (name) liq;
            else
                print "empty glass tumbler";
            rtrue;
            ],
        invent [ liq qty;
            liq = ChildOfClass(self,Liquid);
            if (liq ~= 0) qty = liq.add_liquid(0);
            if (inventory_stage == 2) switch (qty) {
                2: print " (full)";
                1: print " (partly full)";
                default: ;
                }
            !!rfalse;
            ],
        description [ liq qty;
            liq = ChildOfClass(self,Liquid);
            if (liq ~= 0) qty = liq.add_liquid(0);
            print "The glass tumbler is slightly chipped,
                but still usable with care. It's currently ";
            switch (qty) {
                2: print_ret "full of ", (name) liq, ".";
                1: print_ret "partly full of ", (name) liq, ".";
                default: "empty.";
                }
            ],
        before [ liq qty;
            liq = ChildOfClass(self,Liquid);
            if (liq ~= 0) qty = liq.add_liquid(0);
            Drink, Empty:
                if (self notin player)
                print_ret "You need to be holding ", (the) self, ".";
                switch (qty) {
                    2: liq.add_liquid(-1); print_ret "You take a mouthful of ", (name) liq, ".";
                    1: liq.add_liquid(-1); print_ret "You finish the rest of ", (the) liq, ".";
                    default: print_ret "There's nothing in ", (the) self, ".";
                    }
            ];

Object  wine "red wine" bottle
  class Liquid
  with  name 'red' 'wine' 'plonk',
        capacity 2;

You'll see that the glass is only slightly shorter: it still has to handle its own description, inventory and so on. The wine, on the other hand, is much simpler, since almost all of its behaviour is now encapsulated in its Liquid class. Also, now that we've added a DRINK action, we can actually test it:

 
Kitchen
Oddly, there are no exits.

In the centre of the room stands a battered pine table.

On the table are a corked bottle and a glass of red wine.

>EXAMINE BOTTLE
You see an ordinary wine bottle, of green glass, with a faded label
and a cork protruding from the slender neck.

>EXAMINE GLASS
The glass tumbler is slightly chipped, but still usable with care.
It's currently full of red wine.

>EXAMINE WINE
It looks rather like red wine.

>TAKE IT
Taken.

>INVENTORY
You are carrying:
  a glass of red wine (full)

>DRINK SOME WINE
You take a mouthful of red wine.

>AGAIN
You finish the rest of the red wine.

>INVENTORY
You are carrying:
  an empty glass tumbler

Multiple inheritance

Before we reached this page, new objects -- defined by using the Object directive -- were actually members (or instances) of the Object class; that was the common parent from which all objects inherited their default behaviour. (Not that you needed to know this; not that it mattered.) So what happens when we construct a new class -- say Room -- and then create the kitchen object? Easy: the kitchen is a member of two classes, Object and Room, and it inherits from both. This idea of multiple inheritance is both important and powerful, because it means that an object can be a member of many classes at once, acquiring some behavioural aspects from each. Those aspects don't need to be large and complex; sometimes just being a member of a class is sufficient to differentiate one collection of objects from the rest. For example, consider these three classes:

 
Class   Small;

Class   Food
  has   edible;

Class   Inflammable
  with  before [; Burn:
            remove self;
            print_ret "You set fire to ", (the) self,
                ", which quickly burns to nothing.";
            ];

From these classes, we might defines Small objects (which can perhaps be put in a pocket, or hidden in a mousehole), Food objects (which can be eaten), and Inflammable objects which can be consumed by fire. So here's a chocolate bar, which is all of these:

 
Object  choc_bar "chocolate" kitchen
  class Food Small Inflammable
  with  name 'chocolate' 'bar' 'hershey' 'block',
        article "some",
        description "The bar of chocolate looks yummy!";

That is, class allows us to say that an object is a member of several classes (as well as of the implicit Object class), and inherits its behaviour from all of them. Thus, the choc_bar acquires an edible attribute from Food, and a before property from Inflammable (but nothing specific, other than class membership, from Small). To these it adds its own name, article and description properties... and there's a complete object which can be examined, eaten or burned.

In this simple example, there is no overlap between the properties/attributes of the objects itself, and those of the three component classes. But suppose there had been some commonality? What if we'd written:

 
Class   Small
  with  name 'small' 'tiny' 'little',
        description "It's not very big.";

Class   Food
  with  name 'delicious',
        description "Looks good enough to eat."
  has   edible;

Class   Inflammable
  with  before [; Burn:
            remove self;
            print_ret "You set fire to ", (the) self,
                ", which quickly burns to nothing.";
            ];

Object  choc_bar "chocolate" kitchen
  class Food Small Inflammable
  with  name 'chocolate' 'bar' 'hershey' 'block',
        article "some",
        description "The bar of chocolate looks yummy!";

Here there's more than one name, and more than one description; which one is used? The answer's a bit confusing: all of the names, but only one of the descriptions -- the "yummy!" one from the object itself. It's exactly as though we'd written:

 
Class   Small;

Class   Food
  has   edible;

Class   Inflammable
  with  before [; Burn:
            remove self;
            print_ret "You set fire to ", (the) self,
                ", which quickly burns to nothing.";
            ];

Object  choc_bar "chocolate" kitchen
  class Food Small Inflammable
  with  name 'small' 'tiny' 'little' 'delicious'
             'chocolate' 'bar' 'hershey' 'block',
        article "some",
        description "The bar of chocolate looks yummy!";

What's happening here? There are two types of property: additive and non-additive. Additive properties, like name, are accumulated from the definitions in the object itself and in the class(es) on which it is based. So, all the words defined in the various name properties can be used to address the choc_bar. Non-additive properties, like description, don't accumulate. An object can have only one description, which is taken either from the object itself or, if the object doesn't provide one, from the first class in the list. So, the choc_bar is always "yummy!" rather than "not very big" or "good enough to eat".

Most properties are non-additive: the only ones that accumulate are: after, before, describe, each_turn, life and name. And attributes accumulate in a slightly different way: if an individual attribute is set or unset (for example edible or ~edible) in the object itself, that's what prevails. An attribute which isn't mentioned in the object itself will be set if it was set by any of its classes; unsetting an attribute in a class definition has no effect.

Replacing the "Object" directive

Inform supports an alternative syntax for defining objects of a user-specified class: you can supply the name of the class in place of the Object directive, and then you don't need the explicit class segment. For example, we could have said:

 
Room    cell "Monastery cell";

Room    kitchen "Kitchen"
  with  description "Oddly, there are no exits.";

Room    cellar "Gloomy cellar"
  with  description "Even with your torch, you see only dust and cobwebs."
  has   ~light;

Only one class name can be used in place of the Object directive, so for an object which inherits from more than one class, you still need a class segment. Our chocolate bar could be defined as:

 
Food    choc_bar "chocolate" kitchen
  class Small Inflammable
  with  name 'chocolate' 'bar' 'hershey' 'block',
        article "some",
        description "The bar of chocolate looks yummy!";

It's up to you whether to adopt this syntax: the advantage is that your program is easier to understand if the directive defining each object is a meaningful name rather than the ubiquitous Object. Be careful with multiple inheritance; remember that non-additive properties not defined by the object itself default to the setting from the first listed class -- that's now the one replacing the Object directive rather than the first one in the class list (in the example, Food).


Next, a bit more on classes, and particularly on how you can create instances of a class at run-time.