Flexible word ordering in verbs - dobj and iobj

I’m writing a Czech translation of TADS 3 and while I’m mostly done, two or three problems still remains in my TODO list and I’m not sure what’s the best approach to solve them.

Thing is, that my language have seven grammatical cases and have little more flexible word ordering than English. It’s not that bad actually, as most verbs have well defined dobj vs. iobj object roles by prepositions. However few actions (ShowTo, GiveTo, ThrowTo, MoveWith, TurnWith, BurnWith, CutWith, CleanWith, (Un)LockWith, (Un)ScrewWith) don’t and player can enter iobj and dobj in either order after a verb. So I’m searching for some possibility to have something like this:

[code]
VerbRule(ShowToType2)
‘show’ (singleIobj dobjList | dobjList singleIobj)
: ShowToAction
verbPhrase = ‘show/showing (what) (to whom)’
askIobjResponseProd = toSingleNoun

/* this is a non-prepositional phrasing */
isPrepositionalPhrasing = nil

;[/code]
This example of course didn’t work, because parser will choose always the first alternative, because it is structuraly OK. Parser do this before it will resolve vocabulary words to objects and before it can take logicalness into account. So when you enter (in Heidi sample game) “show ring burner”, it will reply “You cannot show anything to a diamond ring.” I think that there are two very different kind of solutions:

  1. Modify parser to understand grammatical cases.

In Czech language even if there si no preposition “to” before iobj, object roles can be distinguished by grammatical case. The words have slightly altered endings to indicate case. So when I say both “ukaz prsten uhlirovi” and “ukaz uhlirovi prsten”, I mean “show ring to burner”. If I would like to errornously say “show burner to ring”, it would be both “ukaz prestenu uhlire” and “ukaz uhlire prstenu”. (Burner is an actor - man burning charcoal.)

That would mean extending dictionary to not only distinguish between adjectives, nouns and so on, but also every grammatical case of each. Then modify parser grammars and do something like:

‘show’ (singleIobj_ToWhomCase dobjList_WhatCase | dobjList_WhatCase singleIobj_ToWhomCase)

This looks like a clean solution to learn parser better understand language, but it has in fact several practical problems:

  • First of all it would be an additional load to an IF author. Now he must specify object name in all grammatical cases and vocabWords in all grammatical cases. This would require adding information which vocabWord is in which case, which will complicate vocabWords format.

  • Grammatical case is distinguished by word ending and when player would use abbreviations it can efectively remove information about case which could lead to hard to explain misunderstanding.

  • Players sometimes seems to underestimate language processing and enter commands which are not much correct from a grammatical standpoint. It is quite common to not decline word into correct case especialy when shortening some verbs to short abbreviations. And as most verbs don’t suffer from this unambiguity, players don’t expect that it sometimes depends on case.

  • There are very few words which are even undistinguishable by grammatical case.

Because of all these problems I was thinking about:

  1. Let the game try both orderings and pickup the correct one depending on logicalness.

When we talk about ShowTo and GiveTo, iobj must be an actor. So it should be fairly easy to resolve ambiguity. Remaining are little less certain, but in reality I think it shouldn’t be a problem. Or could be? Do you know any situation in which this simplification would do a harm?

And second problem is that I’m really not certain how to implement that behavior. Thank you in advance for any opinions or sugestions.

Yep, that’s also true for German.

We have some non-prepositional phrases where distinguishing between dobj and iobj can be difficult and we have verbs which have another meaning when used with a certain kind of thing.

e.g.:

show man hat
zeig dem Mann den Hut

or

show hat man
zeig den Hut dem Mann

We have the dative case here, but it’s too vague to be reliable and we have too much irregular declinations in German.

In former versions of the lib I had a simple remapping which changes the dobj and iobj when necessary. But this is not ideal, because there is only one remapping per action allowed, so that the author cannot use this any more.

Now I came up with that code – thanks to Eric Eve and his DupDobj.t extension which brought me to right point, I guess.


modify TIAction
    preferredIobj = nil
    replace doActionMain()
    {
        local lst;
        local preAnnouncedDobj;
        local preAnnouncedIobj;
        
        /* 
         *   Get the list of resolved objects for the multiple object.  If
         *   neither has multiple objects, it doesn't matter which is
         *   iterated, since we'll just do the command once anyway.  
         */
        lst = (iobjList_.length() > 1 ? iobjList_ : dobjList_);

        /* 
         *   Set the pronoun antecedents, using the game-specific pronoun
         *   setter.  Don't set an antecedent for a nested command.
         */
        if (parentAction == nil)
        {
           /* 
            *   Set both direct and indirect objects as potential
            *   antecedents.  Rather than trying to figure out right now
            *   which one we might want to refer to in the future, remember
            *   both - we'll decide which one is the logical antecedent
            *   when we find a pronoun to resolve in a future command.  
            */ 
           gActor.setPronounMulti(dobjList_, iobjList_); 

            /*
             *   If one or the other object phrase was specified in the
             *   input as a pronoun, keep the meaning of that pronoun the
             *   same, overriding whatever we just did.  Note that the
             *   order we use here doesn't matter: if a given pronoun
             *   appears in only one of the two lists, then the list where
             *   it's not set has no effect on the pronoun, hence it
             *   doesn't matter which comes first; if a pronoun appears in
             *   both lists, it will have the same value in both lists, so
             *   we'll just do the same thing twice, so, again, order
             *   doesn't matter.  
             */
            setPronounByInput(dobjList_);
            setPronounByInput(iobjList_);
        }

        /* 
         *   pre-announce the non-list object if appropriate - this will
         *   provide a common pre-announcement if we iterate through
         *   several announcements of the main list objects 
         */
        if (lst == dobjList_)
        {
            /* pre-announce the single indirect object if needed */
            preAnnouncedIobj = preAnnounceActionObject(
                iobjList_[1], dobjList_, IndirectObject);

            /* we haven't announced the direct object yet */
            preAnnouncedDobj = nil;

            /* pre-calculate the multi-object announcements */
            cacheMultiObjectAnnouncements(dobjList_, DirectObject);
        }
        else
        {
            /* pre-announce the single direct object if needed */
            preAnnouncedDobj = preAnnounceActionObject(
                dobjList_[1], iobjList_, DirectObject);

            /* we haven't announced the indirect object yet */
            preAnnouncedIobj = nil;

            /* pre-calculate the multi-object announcements */
            cacheMultiObjectAnnouncements(iobjList_, IndirectObject);
        }

        /* we haven't yet canceled the iteration */
        iterationCanceled = nil;

        /* iterate over the resolved list for the multiple object */
        for (local i = 1, local len = lst.length() ;
             i <= len && !iterationCanceled ; ++i)
        {
            local dobjInfo;
            local iobjInfo;

            /* 
             *   make the current list item the direct or indirect object,
             *   as appropriate 
             */
            if (lst == dobjList_)
            {
                /* the direct object is the multiple object */
                dobjInfo = dobjInfoCur_ = lst[i];
                iobjInfo = iobjInfoCur_ = iobjList_[1];
            }
            else
            {
                /* the indirect object is the multiple object */
                dobjInfo = dobjInfoCur_ = dobjList_[1];
                iobjInfo = iobjInfoCur_ = lst[i];
            }

            // -- here goes the magic code
            // -- we have now resolved objects for indirect and direct slots
            
            if (preferredIobj != nil) {
                if (dobjInfo.obj_.ofKind(preferredIobj)) {
                    /* change both objects */
                    dobjCur_ = iobjInfo.obj_;
                    iobjCur_ = dobjInfo.obj_;
                }
                else {
                    /* get the current dobj and iobj from the resolve info */
                    dobjCur_ = dobjInfo.obj_;
                    iobjCur_ = iobjInfo.obj_;
                }    
            }
            
            /* 
             *   if the action was remapped, and we need to announce
             *   anything, announce the entire action 
             */
            if (isRemapped())
            {
                /*
                 *   We were remapped.  The entire phrasing of the new
                 *   action might have changed from what the player typed,
                 *   so it might be nonsensical to show the objects as we
                 *   usually would, as sentence fragments that are meant
                 *   to combine with what the player actually typed.  So,
                 *   instead of showing the usual sentence fragments, show
                 *   the entire phrasing of the command.
                 *   
                 *   Only show the announcement if we have a reason to: we
                 *   have unclear disambiguation in one of the objects, or
                 *   one of the objects is defaulted.
                 *   
                 *   If we don't want to announce the remapped action,
                 *   still consider showing a multi-object announcement,
                 *   if we would normally need to do so.  
                 */
                if (needRemappedAnnouncement(dobjInfo)
                    || needRemappedAnnouncement(iobjInfo))
                {
                    /* show the remapped announcement */
                    gTranscript.announceRemappedAction();
                }
                else
                {
                    /* announce the multiple dobj if necessary */
                    if (!preAnnouncedDobj)
                        maybeAnnounceMultiObject(
                            dobjInfo, dobjList_.length(), DirectObject);

                    /* announce the multiple iobj if necessary */
                    if (!preAnnouncedIobj)
                        maybeAnnounceMultiObject(
                            iobjInfo, iobjList_.length(), IndirectObject);
                }
            }
            else
            {
                /* announce the direct object if appropriate */
                if (!preAnnouncedDobj)
                    announceActionObject(dobjInfo, dobjList_.length(),
                                         DirectObject);

                /* announce the indirect object if appropriate */
                if (!preAnnouncedIobj)
                    announceActionObject(iobjInfo, iobjList_.length(),
                                         IndirectObject);
            }

            /* run the execution sequence for the current direct object */
            doActionOnce();

            /* if we're top-level, count the iteration in the transcript */
            if (parentAction == nil)
                gTranscript.newIter();
        }
    }
;

VerbRule(ShowTo)
    'zeig' dobjList singleIobj
    : ShowToAction
    verbPhrase = 'zu zeigen/zeigen (was) (dativ wem)'
    askIobjResponseProd = singleNoun
    
    /* this is a non-prepositional phrasing */
    isPrepositionalPhrasing = nil
    preferredIobj = Actor
;

Pretty much of the code is just simply copied from the doActionMain() method. Here is what this does:
It sets a new property for TIAction preferredIobj. When set to a certain kind of thing, it checks in doActionMain() from TIAction if there is a dobj which seems better in the iobj slot, and then changes both. This seems to work fine so far.

Greetings,
– MI

Perfect! Thank you Mikawa, this is a nice solution. It never stop amazing me how elegantly things can be solved in TADS.

And when I use: 'zeig' (dobjList singleIobj | singleIobj dobjList) it seems to handle correctly the situation when dobj is actually a list of multiple objects, i.e. “zeig den Hut und den Hut dem Mann”. I have just a little correction to the if statement to not allow a condition in which no assignment will be made: if (preferredIobj != nil && dobjInfo.obj_.ofKind(preferredIobj)) { /* change both objects */ dobjCur_ = iobjInfo.obj_; iobjCur_ = dobjInfo.obj_; } else { /* get the current dobj and iobj from the resolve info */ dobjCur_ = dobjInfo.obj_; iobjCur_ = iobjInfo.obj_; }

Yes, correct, thank you!

I would go one step further and disable the replacement if the iobj is already of kind “preferredIobj” … Actually I don’t know if that makes a difference, but It would be possible to let the player give a cat (Actor) to a woman (also Actor). We can’t distinguish that, of course, so that a replacement would’t make sense.


            if (preferredIobj != nil && dobjInfo.obj_.ofKind(preferredIobj) &&
                !iobjInfo.obj_.ofKind(preferredIobj)) {
                /* change both objects */
                dobjCur_ = iobjInfo.obj_;
                iobjCur_ = dobjInfo.obj_;
            }
            else {
                /* get the current dobj and iobj from the resolve info */
                dobjCur_ = dobjInfo.obj_;
                iobjCur_ = iobjInfo.obj_;
             }  

Edit: We can distinguish that, when we make an own class for the cat, which is not inherited from Actor, of course …