dobjFor(UnlockWith) { verify() } misbehaving (adv3Lite)

I have three doors—A, B, and C—that all have lockability = lockablewWithKey.

I have one key, with its actualLockList = [doorA].

So far so good. I can unlock door A with the key, and the key does not work on either door B or C.

But I want to customize the failure message when I try to unlock B or C with the key, so I have implemented on both doors B and C a dobjFor(UnlockWith) with a new illogical message when the gIobj is the key to door A…

dobjFor(UnlockWith) { verify() { if(gIobj == aptKey) illogical('The key does not fit, the door will not unlock.<.p>'); inherited; } }

And it does not work. Instead, when I try to unlock door B with the key by saying unlock door then responding with B when asked which door and then key when asked with what, the game proceeds to unlock Door A with the key…

A breakpoint in the verify() code shows that gIobj is in fact aptKey, and the line of code that sets the illogical message to my custom message is being hit.

Here’s a test bed implementation…

[code]#charset “us-ascii”

#include <tads.h>
#include “advlite.h”

versionInfo: GameID
IFID = ‘445C38A3-AD1B-4729-957A-F584600DE5C1’
name = ‘test’
byline = ‘by Jerry Ford’
htmlByline = ‘by
Jerry Ford

version = ‘1’
authorEmail = ‘Jerry Ford jerry.o.ford@gmail.com
desc = ‘Testing unlock.’
htmlDesc = ‘Testing unlock.’

;

gameMain: GameMainDef
initialPlayerChar = me
paraBrksBtwnSubcontents = nil

;

me: Actor ‘me’ @room
“The main man.<.p>”
isHim = true

person = 2

;

  • aptKey: Key ‘key to door a’
    “The key to door A. <.p>”

    actualLockList = [doorA]
    ;

room: Room ‘room’
“The room.<.p>”

north = doorA
south = doorB
east = doorC 

;

  • doorA: Door → nextRoom ‘() door A’
    “Door A. <.p>”

    lockability = lockableWithKey
    isLocked = true
    isOpen = nil
    ;

  • doorB: Door → nextRoom ‘() door B’
    “Door B. <.p>”

    lockability = lockableWithKey
    isLocked = true
    isOpen = nil

    dobjFor(UnlockWith)
    {
    verify()
    {
    if(gIobj == aptKey)
    illogical(‘The key does not fit, the door will not unlock.<.p>’);
    inherited;
    }
    }
    ;

  • doorC: Door → nextRoom ‘() door C’
    “Door C. <.p>”

    lockability = lockableWithKey
    isLocked = true
    isOpen = nil

    dobjFor(UnlockWith)
    {
    verify()
    {
    if(gIobj == aptKey)
    illogical(‘The key does not fit, the door will not unlock.<.p>’);
    inherited;
    }
    }
    ;

nextRoom: Room ‘next room’
“The next room.<.p>”
west = room
;

[/code]

Jerry

I don’t know if this is the source of the problem you’re encountering, but it is a general case in TADS 3 that verify() will be called several times while the parser is disambiguating. During some of these calls, gIobj will not be known yet, so it will be nil. The fact that you’re seeing it set to the appropriate value in a breakpoint may be an artifact of the way the debugger works.

Also, I’m pretty sure that the illogical macro does not immediately cause a message to print. Later in the cycle, that message might be suppressed.

By the time the check() code is reached, gIobj will have a value. Have you tried putting your “The key does not fit” message in the check() block?

Yes. But because it is the wrong key for the lock, the action fails in verify(), and the check() code is never reached.

And some of the times it will not be nil, so the illogical message does still get changed. I’ve used verify() in this way elsewhere, on other commands, and it has worked.

Additionally, my verify() code looks nearly identical (other than the actual words) to the example given in Learning T3Lite (page. 95)…

banana: Food 'banana' hasBeenPeeled = nil dobjFor(Eat) { verify() { if(!hasBeenPeeled) illogicalNow('You\'ll have to peel it first. '); } } ;

Not how I understand it, and as noted previously, not how it’s been working elsewhere.

What I understand to be happening is, the illogical message starts out each command cycle as some library-defined text. Then verify() runs and, if there’s code to support it, the message is changed to some non-standard text. Doesn’t matter how many times verify() runs, there is no code that sets it back to the library default until the next command cycle—after the message has been displayed in the game window.

What’s different about how I’m using it this time, as opposed to previous usage where it worked, is that there are three potential candidates for the door that the key will open—door A, B, and C. The library seems to be cycling through each of them and at some point, the obj being evaluated changes from lock B to lock A—which the key does fit—and the library declares it a success.

I just haven’t been able to follow the bouncing ball through the debugger closely enough to spot the transition point.

Also, note that I’m not so much complaining about what the message says (well, yes, I am, but that’s not the most serious error). When all is said and done, I put the key in door B and viola! door A is opened.

That’s not a messaging error, it’s a functional error.

Jerry

Actually, your verify() routine is quite different from the one you quote from Learning. The one you quote doesn’t refer to gIobj. And indeed, on testing I find that my standard solution (apology – I should have given you code the first time) works perfectly. Try this:

verify() { if(gIobj && gIobj == aptKey) illogical('The key does not fit, the door will not unlock.<.p>'); inherited; }
The difference is that the && condition only evaluates to true after gIobj has been established. When you do that, the output is:

This is a standard “Sneaky TADS Trick.”

Still doesn’t work. You’re disambiguating the door selection in your command syntax, not forcing the library to disambiguate. I get the same results that you do, without your code change, if I enter the same command as you have—unlock b.

When you specify a door in the command, you remove doors a and c from the equation and the library doesn’t have to figure out which door you want to unlock.

When I say unlock door and let the game ask which door, the library evaluates all three doors—and gets it wrong.

Here’s the output, with your code—if(gIobj && gIobj == aptKey)—in place…

Jerry

Part of the answer is that it would be simpler to override the message property the library provides for this purpose. Instead of overriding dobjFor(UnlockWith) on two door, simply do this on the key:

+ aptKey: Key 'key to door a'
    "The key to door A. <.p>"
    
    actualLockList = [doorA]
    
    notAPlausibleKeyMsg = 'The key does not fit, the door will not unlock.<.p>'
;

That should solve your immediate problem. However, there still is a bug in the library, as you say. I’ve tracked down where it’s happening; I just haven’t figured out to solve it yet.

The problem is basically that when you reply to the prompt “What do you want to unlock it with?” the parser reparses the whole command, as if you had entered the command UNLOCK DOOR WITH KEY. Since it now knows you want to unlock a door with the key to Door A, your verify routines made Door A the most logical choice of door to be unlocked with the key, so the parser decided that UNLOCK DOOR WITH KEY must mean UNLOCK DOOR A WITH KEY.

What I now have to work out is how to amend this behaviour without breaking a whole lot of other things in the process. I fear it may not be a five-minute job!

Okay, got it. Sorry about that. And yes, that looks like a difficult bug to fix. It amounts to asking the parser to store some information about the what’s gDobj and use that information while re-parsing – across two turns (and two disambiguation requests). Does this issue ever arise when there is only one disambiguation request? That may be the core of the difficulty.

Hm, I thought I’d edited my reply, but it seems my edit got lost.

Well, I think I’ve fixed it now, and I’ve uploaded the fix to GitHub.

Basically what the fix does is have the function that’s called to request the missing indirect object store the role it’s looking for (in this case IndirectObject) in a new property of the current Command object. When the parser re-parses the player’s input as a response to the request to supplty a missing object, it then only resolves the object for that role (the IndirectObject, say) leaving any previously resolved objects (the direct object, say) in place.

What happened in Jerry’s example wasn’t two disambiguation requests, it was a disambiguation request followed by a request to supply a missing noun. This is the scenario needed to trigger the bug, I think, since it’s the request to supply the missing noun that triggers the re-parse, and was doing so with an ambiguously specified direct object (‘door’), which then ended up as doorA on the reparse. Even so, for reasons I haven’t fully fathomed, this seems to occur only when Jerry’s verify routines are in place; if they’re taken out the direct object gets changed to doorA but then gets changed back to the correct door again (weird!). So it takes a pretty specific set of circumstances for this bug to manifest itself, which is probably why it hasn’t come to light until now.