Boosting a topic's rank in the InScopeList (TADS3)

My game has spells that you can cast (either alone as >cast foospell, or onto something as >cast foospell on object). If you try to cast an object that isn’t a spell, the game converts the input to ThrowAt (for inputs like >cast bread upon the waters).

The code works very well for spells that have unique names (i.e., there are no other objects in the game that use their vocabWords). I can:

foospell
cast foospell
cast foospell on object
cast foospell spell
cast foospell spell on object.

However, I have run into trouble in the following situation.

I have an alpaca, an alpaca spell, and an alpaca pen.
“cast alpaca” results in “What do you want to throw it at?”
“cast alpaca on object” results in "The alpaca won’t let you do that.

“alpaca” by itself, “cast alpaca spell” and “cast alpaca spell on object” work OK.

What is happening with “cast alpaca” is that gTopic.getBestMatch is finding all three alpaca objects in the following order

  1. th_alpaca (the actual animal)
  2. tp_alpaca_spell (the spell)
  3. rm_alpaca_pen (the room)

Because the actual animal appears first on the inScopeList, the game skips over the spell stuff and executes ThrowAt.

I can avoid this altogether by changing the vocabWords of the spell in the code below from
vocabWords = ‘(magic) alpaca spell’
to
vocabWords = ‘(magic) alpaca/spell’

Which converts alpaca from an adjective to a noun, and makes all the inputs work correctly. However, this kills the input “cast alpaca spell” which is an input that I want to be valid.

So it seems to me that the answer is to boost tp_alpaca_spell to the top of the inScopeList, but all my ham-fisted efforts to do so have failed.

Help along any lines would be appreciated.

Here is the code…

class SpellIAction: IAction
;

class SpellTAction: TAction
;
    
VerbRule(Cast)
    [badness 500] // This prevents the parser from converting >cast frob on futon (while on the futon)  to Cast,  instead of CastOn.
    'cast' singleTopic
    : CastAction
    verbPhrase = 'cast/casting (what)'
;

VerbRule(CastOn)
    ('cast') singleTopic ('on' | 'at') singleDobj // Note singleTopic (for scope reasons) and singleDobj instead of singleIobj
                                                  // See "Look Up" example as a parallel in Tech Manual under "Verbs with one object plus a topic"
    : CastOnAction
    verbPhrase = 'cast/casting (what) (on what)'
;

class Spell: Topic
    /* Default if player types >cast spell. */
    castAction () { "You must specify which spell you want to cast. "; } 
    /* Default if player types >cast spell on foo. */
    castOnAction () { "You must specify which spell you want to cast. "; }
;

DefineTopicAction(Cast)
    execAction()
    {
        local spell = gTopic.getBestMatch();
        local obj = gTopic.getBestMatch(); // the same as the 'spell' local, but easier to read in code
        local wrd = gTopic.getTopicText(); // Get the literal text that the player typed after the word >cast
        if (wrd == 'spell' // Player typed >cast spell  instead of cast {specific} spell
            || wrd == 'a spell'
            || wrd == 'the spell'
            || wrd == 'magic'
            || wrd == 'magic spell'
            || wrd == 'a magic spell'
            || wrd == 'the magic spell'
            || wrd == 'any spell') {
            "You must specify which spell you want to cast. ";
            exit;
        }
        else if (spell != nil && spell.ofKind(Spell)) { 
            spell.castAction();  //If it's a legitimate spell, cast it
        }
        //        else if (obj == obj.ofKind(SomeTopicWeHaven'tDefinedYet, like Love))  TODO
        //           reportFailure('That\'s not a spell. ');
        else if (obj != nil) { 
            replaceAction (Throw, obj);  //If it's an object, throw it
        }
        else { 
            reportFailure('That\'s not a spell. ');  //If we don't recognize it, say so.
        }
    }
;

DefineTopicTAction(CastOn, DirectObject) // Note use of DirectObject here because of TopicTAction
    execAction()
    {
        local spell = gTopic.getBestMatch();
        local obj = gTopic.getBestMatch(); // the same as the 'spell' local, but easier to read in code
        local wrd = gTopic.getTopicText(); // Get the literal text that the player typed after the word >cast
        if (wrd == 'spell' // Player typed >cast spell on foo, instead of cast {specific} spell on foo
            || wrd == 'a spell'
            || wrd == 'the spell'
            || wrd == 'magic'
            || wrd == 'magic spell'
            || wrd == 'a magic spell'
            || wrd == 'the magic spell'
            || wrd == 'any spell') {
            "You must specify which spell you want to cast. ";
            exit;
        }
        if (spell != nil && spell.ofKind(Spell)) {
            spell.castOnAction();
        }
//      else if (obj == obj.ofKind(SomeTopicWeHaven'tDefinedYet, like Love))  TODO
//         reportFailure('That\'s not a spell. ');
        else if (obj != nil) { // If it's an object, throw it at the original iobj
            replaceAction (ThrowAt, obj, gDobj);
        }
        else {
            reportFailure('That\'s not a spell. ');
        }
    }
;

modify Thing 
    dobjFor(CastOn) 
    { 
        preCond = [objVisible] // Assumes we need to see a thing to cast a spell on it. If not, this preCond isn't needed
        verify() { } 
        check() 
        { 
            /* if the topic in this command doesn't match any game object at all, we can't proceed with this action. */ 
            if(gTopic.getBestMatch() == nil) 
                failCheck(unknownSpellMsg); 
        } 
        action() 
        { 
            local obj = gTopic.getBestMatch(); 
                /*Note that if we've got this far, we know that obj is not nil (otherwise the action would have been stopped in 
                check()). So we can next test whether it's a Thing; if so we want to convert the action to THROW obj AT self */ 
            if(obj.ofKind(Thing))
                replaceAction(ThrowAt, obj, self);
                /* Next test if obj is a Spell; if so, cast it */
            if(obj.ofKind(Spell))
                obj.castOnAction(self); // might be worth passing self to the castOnAction method.  TODO.  RAB, That's an Eric Comment. What does it mean?
                /* Otherwise obj is presumably a Topic that's not a spell */
            else //TODO.  Do we need a check for a topic here? or is that not necessary because we are in thing.
                reportFailure(unknownSpellMsg);
        }
    }
    unknownSpellMsg = 'That\'s not a spell. ' 
; 

/************************
ALPACA SPELL
************************/

tp_alpaca_spell: Spell
{
    name = 'alpaca spell'
    vocabWords = '(magic) alpaca spell'
    spellLearned = nil
    castAction () {
        replaceAction (Alpaca);
    }
    castOnAction () {
        if (!tp_alpaca_spell.spellLearned) {
            "That spell won't work unless it is in your spellbook. ";
            exit; // Stops here
        }
        else {
            replaceAction (AlpacaOn, gDobj);
        }
    }
}
    
DefineSpellIAction(Alpaca)
    execAction()
    {
        if (!tp_alpaca_spell.spellLearned) {
                "That spell won't work unless it is in your spellbook. ";
            exit; // Stops here
        }
        else {
            "A ball of yarn suddenly pops into existence at your feet (Or, at least it will one day. At which time
            we will also make sure that it only happens once). ";
        }
    }
;

VerbRule(Alpaca)
    'alpaca'
    : AlpacaAction
    verbPhrase = 'summon/summoning'
;

/************************
ALPACA-ON SPELL
************************/

DefineSpellTAction(AlpacaOn);

VerbRule(AlpacaOn)
     ('alpaca' ) singleDobj
     : AlpacaOnAction
     verbPhrase = 'summon/summoning (what)'
;

modify Thing
     dobjFor(AlpacaOn) {
        action () {
            if (!tp_alpaca_spell.spellLearned) {
                "That spell won't work unless it is in your spellbook. ";
            }            
            else {
                "You can't cast alpaca on things. ";
            }
        }
    }
;

/***************************************
    THINGS
****************************************/

th_alpaca: UntakeableActor
{
    name = 'alpaca'
    vocabWords = 'alpaca'
    location = rm_alpaca_pen
    dobjFor(Examine) {
        action() {
            say('It\'s woolly. ');
        }
    }
}

Untested, but look up vocabLikelihood in the LRM. If (gActionIn(Cast, Throw, ThrowAt)), you can return a higher value for vocabLikelihood (assuming that gAction has been established – I’m not sure of the processing order).

Hi Jim. I tried scattering this around in a few different places and I couldn’t find one where it made a difference. Do you have a specific place where you think it might work? Thanks. --Bob

Try putting it on the spell object:

tp_alpaca_spell: Spell { name = 'alpaca spell' vocabWords = '(magic) alpaca spell' spellLearned = nil vocabLikelihood { if (gActionIn(Cast, Throw, ThrowAt)) return 5; else return 0; } // etc....
I’m wingin’ it here. I don’t know that that will work; it’s just the first thing I’d think of trying.

Thanks for the quick replies, Jim, but unfortunately that didn’t work. I even tried just setting the vocabLikelihood unconditionally to a high number (10), and it had no effect.

But your suggestion did put me on the trail of another possible solution which is simply to set the local “spell” back to tp_alpaca_spell in Cast and CastOn if the player used the word ‘alpaca’ in his input. (See code below). This seems like a pretty kludgy way of doing things, and it somehow seems dangerous, but it seems to be working (for now - I haven’t had time to test it thoroughly). If it holds up, I’ll go with it unless someone can step in with the “right” (and no doubt more elegant) way to do it.

–Bob


DefineTopicAction(Cast)
    execAction()
    {
        local spell = gTopic.getBestMatch();
        local obj = gTopic.getBestMatch(); // the same as the 'spell' local, but easier to read in code
        local wrd = gTopic.getTopicText(); // Get the literal text that the player typed after the word >cast
        if (wrd == 'spell' // Player typed >cast spell  instead of cast {specific} spell
            || wrd == 'a spell'
            || wrd == 'the spell'
            || wrd == 'magic'
            || wrd == 'magic spell'
            || wrd == 'a magic spell'
            || wrd == 'the magic spell'
            || wrd == 'any spell') {
            "You must specify which spell you want to cast. ";
            exit;
        }
        if (wrd == 'alpaca') { // A ham-fisted way of getting around ">Cast alpaca on foo" getting "What do you want to throw it at?"
            spell = tp_alpaca_spell;
        }
        if (spell != nil && spell.ofKind(Spell)) { 
            spell.castAction();  //If it's a legitimate spell, cast it
        }
        //        else if (obj == obj.ofKind(SomeTopicWeHaven'tDefinedYet, like Love))  TODO
        //           reportFailure('That\'s not a spell. ');
        else if (obj != nil) { 
            replaceAction (Throw, obj);  //If it's an object, throw it
        }
        else { 
            reportFailure('That\'s not a spell. ');  //If we don't recognize it, say so.
        }
    }
;

DefineTopicTAction(CastOn, DirectObject) // Note use of DirectObject here because of TopicTAction
    execAction()
    {
        local spell = gTopic.getBestMatch();
        local obj = gTopic.getBestMatch(); // the same as the 'spell' local, but easier to read in code
        local wrd = gTopic.getTopicText(); // Get the literal text that the player typed after the word >cast
        if (wrd == 'spell' // Player typed >cast spell on foo, instead of cast {specific} spell on foo
            || wrd == 'a spell'
            || wrd == 'the spell'
            || wrd == 'magic'
            || wrd == 'magic spell'
            || wrd == 'a magic spell'
            || wrd == 'the magic spell'
            || wrd == 'any spell') {
            "You must specify which spell you want to cast. ";
            exit;
        }
        if (wrd == 'alpaca') { // A ham-fisted way of getting around ">Cast alpaca on foo< getting "the alpaca won't let you do that"
            spell = tp_alpaca_spell;
        }
        if (spell != nil && spell.ofKind(Spell)) {
            spell.castOnAction();
        }
//      else if (obj == obj.ofKind(SomeTopicWeHaven'tDefinedYet, like Love))  TODO
//         reportFailure('That\'s not a spell. ');
        else if (obj != nil) { // If it's an object, throw it at the original iobj
            replaceAction (ThrowAt, obj, gDobj);
        }
        else {
            reportFailure('That\'s not a spell. ');
        }
    }
;

In general I feel it’s a bad idea to write your own code to do the parser’s job. There are too many edge cases that you may miss.

I may have a look at how I would do this, if I have time. In the meantime, you should note that this code is almost certainly wrong:

modify Thing dobjFor(CastOn) { preCond = [objVisible]
If I understand your intentions correctly, you’re expecting the user to enter ‘cast frobozz on troll’ or something of the sort. In that situation, the troll would be the iobj, not the dobj. The topic object would never be visible, but of course the troll would have to be visible.

Hmmm. According to a comment Eric made long ago, “since CAST ON is a TopicTAction, the direct object of CAST FOO ON STONE is the stone.”

But a quick test shows that commenting out the line “preCond = [objVisible]” doesn’t affect things one way or the other (i.e., even if the line is not there, if I try to cast a spell on something that isn’t there, I still get “You see no foo here.”)

With regard to trying to do the parser’s job, I totally agree with you and feel that my solution is a dangerous patch that could come back to bite me. I’d much rather “play by the rules” if I could, but I haven’t been able to figure out how.

–Bob

You’re undoubtedly right about Eric’s comment … but I find myself wondering: Does the fact that you’ve defined a topic object mean that this shouldn’t be a TopicTAction at all? My vague guess is that TopicTAction may be useful for strings of text (such as ‘write beware of ghouls! on mirror’) where there is no topic object called beware of ghouls! Once a spell is defined as a Topic object, maybe a different type of action is needed. That could be the source of the difficulty.

Right now I’m trying to wrap my brains around Inform 7 again (after an absence of 5 years or so), so I’m reluctant to try to whip up any T3 code to diagnose this…

… Inform 7…

No need to break out trial code, Jim. You’ve been more than helpful!

I tremble at the thought of trying to be current in more than one language at a time, so best of luck with your return to Inform.

Cheers,

–Bob