Exercises

In each section, one or more objects are described. Consider how they might be implemented, then click on the question to check the answer.

1. A window that is painted shut. The player shouldn't find out until s/he tries opening it.

window: Fixture 'window' 'window'
    dobjFor(Open)
    {
        /*
         *  It's perfectly logical to try to open the window.
         *  So verify() allows the action.
         */
        verify() { }

        /* But as the player tries to open it, we fail. */
        action()
        {
            "As you try to open the window,
             you find that it's painted shut. ";
        }
    }
;

An alternative would be to use check() instead of action(), and then finish with exit;. You can read more about the difference in this technical article.

2. A cooking pot that's too hot to be picked up without gloves.

gloves: Wearable 'glove*gloves' 'gloves'
    isPlural = true
;

cookingPot: Container 'hot cooking pot' 'cooking pot'
    "It looks hot."
    dobjFor(Take)
    {
        action()
        {
            /*
             *  Check if gActor (= the actor doing the take) is
             *  wearing the gloves.
             */
            if (!gloves.isWornBy(gActor))
            {
                /*
                 *  Instead of just displaying the message (using
                 *  double-quotes), we use the special macro
                 *  reportFailure.
                 *  The only difference is the result of an implied
                 *  take:
                 *  > PUT POT IN DISHWASHER
                 *  (first trying to take the cooking pot)
                 *  Ow! It's burning hot.
                 */
                reportFailure('Ow! It\'s burning hot! ');
            }
            else inherited();
        }
    }
;

3. Now, have the pot contain some edible soup, but require the player to be holding a spoon to eat it. If the player isn't holding the spoon, but it's reachable, he should take it automatically.

spoon: Thing 'spoon' 'spoon';

/*
 *  We don't use the Food class, since it provides
 *  no functionality that we want.
 */
soup: Thing 'hot soup' 'soup' @cookingPot
    "Mmm. Looks good. "

    /* Prevent "pot (which contains a soup)"-type messages. */
    isMassNoun = true

    /*
     *  We use preconditions to make sure the player is holding
     *  the spoon. We want the objHeld precondition, but it's
     *  not the soup that should be held. Instead, we create
     *  an ObjectPreCondition which force the spoon to be held.
     *
     *  We only want to create it once, when the code is compiled,
     *  which is why we add the "static" keyword.
     */

    spoonHeldPreCond = static new ObjectPreCondition(spoon, objHeld)

    /* Taste is the same as Eat. */
    dobjFor(Taste) asDobjFor(Eat)

    dobjFor(Eat)
    {
        preCond = [spoonHeldPreCond]
        verify() { }
        action()
        {
            "Mmm. Tasty. ";
        }
    }
    dobjFor(Take)
    {
        verify() { illogical('You can\'t take that. '); }
    }
;

4. Implement some dynamite with a fuse as a separate component. LIGHT DYNAMITE should be the same as LIGHT FUSE, and must require a fire source to light the fuse. When the fuse burns out, the dynamite should explode, killing off the player if s/he is in the same room.

/* For a light-source, the library has a Matchstick class. */
Matchstick 'matchstick/match/stick*matchsticks sticks' 'matchstick';

dynamite: Thing 'dynamite' 'dynamite'
    isMassNoun = true

    /* Remap Burn and Extinguish to the fuse. */
    dobjFor(Burn) remapTo(Burn, dynamiteFuse)
    dobjFor(Extinguish) remapTo(Extinguish, dynamiteFuse)
;

/*
 *  For the fuse, we use the library's Candle class. Then we
 *  simply need to change the messages.
 *
 *  Notice that 'dynamite' is in a parenthesis.
 *  This means that 'dynamite' alone won't refer to the fuse.
 */

+ dynamiteFuse: Candle, Component '(dynamite) fuse' 'dynamite fuse'
    desc()
    {
        if (isLit) "It's lit! Run! ";
        else "It's not lit. ";
    }

    fuelLevel = 4

    sayBurnedOut()
    {
        /*
         *  We display this without checking the player's location,
         *  since Candle makes sayBurnedOut run in a SenseDaemon
         *  that hides all printed messages, if the player can't see
         *  the dynamite (well, the fuse, to be exact.)
         */

        "\bThe dynamite fuse burns out.\b";

        if (gPlayerChar.roomLocation == dynamite.roomLocation)
        {
            /*
             *  Check if our topmost location is the same as the
             *  player's. If it is, we print a message and end the
             *  game, though we allow the player to undo.
             */
            "\b<B>*** <BIG>BOOM</BIG> ***</B>\b";
            finishGame([finishOptionUndo]);

            /* (finishGame never returns.) */
        }

        /* Else, make the dynamite disappear from the game world. */
        dynamite.moveInto(nil);
    }
;

5. Code a DefaultAskTellTopic response that takes into account what object the player asked about, if any, and uses the literal topic text otherwise.

DefaultAskTellTopic
    topicResponse()
    {
        /* Find the best match for the specified topic. */
        local topic = gTopic.getBestMatch();

        if (topic && topic.ofKind(Thing))
        {
            /* There's a match, and it's a Thing. */
            "<q>What can you tell me about
             << topic.theName >>?</q> you ask.

             <q>I'm afraid I don't know much about
             << topic.itObj >>,</q> he replies. ";
        }
        else
        {
            /*
             *  No match, or the match is a Topic.
             *  Since topics don't have names, we must treat
             *  them just like we treat unrecognizable sentences.
             */
            "You ask him about << gTopic.getTopicText() >>,
             but he just shrugs. ";
        }
   }
;

6. A white horse named Black Beauty, and a black horse named White Beauty. The word "black" used alone should refer to Black Beauty, but "black horse" should refer to White Beauty.

whiteHorse: Actor 'white black beauty/horse' 'Black Beauty'
    /* Avoid "You see the Black Beauty"-type messages. */
    isProperName = true

    matchNameCommon(origTokens, adjustedTokens)
    {
        /*
         *  If both the words 'black' and 'horse' were used,
         *  we don't match. Nor if 'white' but not 'horse'
         *  were used.
         */

        if (adjustedTokens.indexOf('black')
            && adjustedTokens.indexOf('horse')) return nil;
        if (adjustedTokens.indexOf('white')
            && !adjustedTokens.indexOf('horse')) return nil;

        /* Otherwise, run the default checks. */
        return inherited(origTokens, adjustedTokens);
    }
;

blackHorse: Actor 'white black beauty/horse' 'White Beauty'
    isProperName = true

    matchNameCommon(origTokens, adjustedTokens)
    {
        if (adjustedTokens.indexOf('white')
            && adjustedTokens.indexOf('horse')) return nil;
        if (adjustedTokens.indexOf('black')
            && !adjustedTokens.indexOf('horse')) return nil;

        return inherited(origTokens, adjustedTokens);
    }
;

7. A player-character with hair, and a comb. The verb COMB should also be implemented, allowing both COMB, COMB HAIR and COMB HAIR WITH COMB.

[ Technical article on creating verbs ]
/* Define an action that takes two objects. */

DefineTIAction(CombWith);

VerbRule(CombWith)
    ('comb' | 'groom') singleDobj 'with' singleIobj : CombWithAction
    verbPhrase = 'comb/combing (what) (with what)'
;

/*
 *  Assuming that leaving out the indirect object never makes sense,
 *  we can use the following code to ask the player for one.
 *  Note that it's the same action, just another verb rule.
 */
VerbRule(CombWithWhat) ('comb' | 'groom') singleDobj : CombWithAction
    verbPhrase = 'comb/combing (what) (with what)'
    construct()
    {
        /* These two lines makes the parser ask for an object. */
        iobjMatch = new EmptyNounPhraseProd();
        iobjMatch.responseProd = withSingleNoun;
    }
;

/*
 *  Provide a default response to CombWith.
 *  This is optional; without it, TADS will just reply
 *  "You can't do that.".
 */
modify Thing
    dobjFor(CombWith) /* We're being combed */
    {
        preCond = [touchObj]
        verify() { illogical('{You/he} can\'t comb {the dobj/him}. '); }
    }
    iobjFor(CombWith) /* We're being used to comb something else */
    {
        preCond = [objHeld]
        verify()
        {
            illogical('{You/he} can\'t comb anything
                       with {the iobj/him}. ');
        }
    }
;

me: Person;

+ comb: Thing 'comb' 'comb'
    iobjFor(CombWith)
    {
        verify() { }
    }
;

+ myHair: Fixture 'hair' 'hair'
    theName = 'your hair'
    isMassNoun = true

    dobjFor(CombWith)
    {
        verify() { }
        action() { "Now it's nice. "; }
    }
;

8. Now, have the player wear a hat initially. Combing the hair should be impossible while the hat is worn.

/* Change me to this: */
me: Person
    checkTouchViaPath(obj, dest, op)
    {
        if (dest == myHair && hat.isWornBy(self))
             return new CheckStatusFailure(
                'Your hat is in the way. ', dest);
        return checkStatusSuccess;
    }
;
+ hat: Wearable 'hat' 'hat'
    wornBy = me
;

This example could easily be extended to make the player automatically remove the hat, using preconditions.
Have a look at the OutOfReach class in objects.t to see how.

9. Handle SHOOT GUN, SHOOT MAN, SHOOT AT MAN and SHOOT AT MAN WITH GUN.

DefineTAction(Shoot);        /* Ambiguous - shooting at or with? */
DefineTAction(ShootAt);      /* Missing weapon. */
DefineTAction(ShootWith);    /* Missing target. */
DefineTIAction(ShootAtWith); /* Complete command. */

/* SHOOT GUN or SHOOT MAN. */
VerbRule(Shoot) 'shoot' singleDobj : ShootAction
    verbPhrase = 'shoot/shooting (what)'
;

/* SHOOT AT MAN. */
VerbRule(ShootAt) ('shoot' | 'fire') 'at' singleDobj : ShootAtAction
    verbPhrase = 'shoot/shooting (at what)'
;

/* SHOOT WITH GUN. */
VerbRule(ShootWith)
    (('shoot' | 'fire') 'with' | 'fire') singleDobj : ShootWithAction
    verbPhrase = 'fire/firing (what)'
;

/* SHOOT MAN (dobj) WITH GUN (iobj). */
VerbRule(ShootAtWith)
    ('shoot' | 'fire') ('at' | ) singleDobj 'with' singleIobj : ShootAtWithAction
    verbPhrase = 'shoot/shooting (at what) (with what)'
;

/* SHOOT GUN (iobj) AT MAN (dobj). */
VerbRule(ShootWithAt)
    ('shoot' | 'fire') ('with' | ) singleIobj 'at' singleDobj : ShootAtWithAction
    verbPhrase = 'shoot/shooting (at what) (with what)'
;

modify Thing
    /* For most Things, Shoot means Shoot At. */
    dobjFor(Shoot) asDobjFor(ShootAt)
    dobjFor(ShootAt)
    {
        verify() { illogical('{You/he} can\'t shoot at {the dobj/him}. '); }
    }
    dobjFor(ShootWith)
    {
        preCond = [objHeld]
        verify() { illogical('{You/he} can\'t shoot with {the dobj/him}. '); }
    }
    dobjFor(ShootAtWith)
    {
        verify() { illogical('{You/he} can\'t shoot at {the dobj/him}. '); }
    }
    iobjFor(ShootAtWith)
    {
        preCond = [objHeld]
        verify() { illogical('{You/he} can\'t shoot with {the iobj/him}. '); }
    }
;

gun: Thing 'gun' 'gun' @me
    /* For a gun, Shoot means Shoot With. */
    dobjFor(Shoot) asDobjFor(ShootWith)
    dobjFor(ShootWith)
    {
        verify() { }
        /* Find a suitable dobj (target), making me iobj. */
        action() { askForDobj(ShootAtWith); }
    }
    iobjFor(ShootAtWith)
    {
        verify() { }
        /* We let the dobj (target) handle the shooting. */
    }
;

me: Person
    dobjFor(ShootAt)
    {
        verify() { }
        /* Find a suitable iobj (weapon). */
        action() { askForIobj(ShootAtWith); }
    }
    dobjFor(ShootAtWith)
    {
        verify() { }
        action()
        {
            "\b<B>*** You have died! ***</B>\b";
            finishGame([finishOptionUndo]);
        }
    }
;

10. An all-time personal favourite from the Inform Designer's Manual IV: Arrange for a bearded psychiatrist to place the player under observation, occasionally mumbling insights such as "Subject puts green cone on table. Interesting."

psychiatrist: Person
    'bearded doctor/psychiatrist/psychologist/shrink'
    'bearded psychiatrist'
;

+ HermitActorState
    /* This is the psychiatrist's initial state. */
    isInitState = true

    /* Shown in room descriptions when we're present. */
    specialDesc = "A bearded psychiatrist has you under observation. "

    /*
     *  So, what's with the extraReports, you wonder?
     *  Well, if the text was simply displayed using
     *  regular double-quotes, it'd cause default reports
     *  (like "Taken." and "Dropped.") to be suppressed:
     *
     *  >take thing
     *  "Subject feels lack of the thing. (...)"
     *
     *  >
     *
     *  By explicitly making the texts extraReports, they don't
     *  suppress the default report.
     */
    beforeAction()
    {
        if (gActionIs(Take))
            extraReport('<q>Subject feels lack of {the dobj/him}.
             Suppressed Oedipal complex? Hmm.</q>\b');
    }
    afterAction()
    {
        if (gActionIs(PutIn))
           extraReport('\b<q>Subject associates {the dobj/him}
            with {the iobj/him}. Interesting.</q>\b');

        else if (gActionIs(PutOn))
            extraReport('\b<q>Subject puts {the dobj/him} on
             {the iobj/him}. Interesting.</q>\b');

        else if (gActionIs(Look))
            extraReport('\b<q>Pretend I'm not here.</q>\b');
    }

    /*
     *  While beforeAction, afterAction, specialDesc and isInitState
     *  are available for any ActorState, noResponse is special to
     *  HermitActorState.
     */
    noResponse()
    {
        "He is fascinated by your behaviour, but makes no attempt
         to interfere with it. ";
    }
;

Inform authors might want to compare this with the Inform-version.