T3: Tracking Knowledge

Starting to think about filling in NPC knowledge, so as to write appropriate conversations. I’m contemplating these sentences, from p. 119 of “Learning T3”:

I understand the part about using me.hasSeen(obj) – but the idea that I must remember to use gSetSeen(obj) worries me deeply, because, according to the LRM notes on the seen property, “we set this to true whenever we show a room description from the player character’s perspective, and the object is visible.”

The implication of these two bits is that if I’m using gSetSeen(obj) manually to set what the player character has seen, the library’s default seen property won’t function as expected. That is, the library will set seen to true for various objects when the PC encounters them, but I won’t be testing that property anymore – I’ll be testing meHasSeen. In which case, I will have to modify the library in some manner so as to cause me.hasSeen(obj) to work properly, reporting true for rooms and objects that are visible in the rooms when the PC first visits them.

Or does the library use gSetSeen(obj) on the me object by default, causing me.hasSeen(obj) to work in the way that it’s supposed to? If that’s the case, then the sentence in Learning T3 about “we must remember to use gSetSeen(obj)” isn’t quite true, because we don’t need to remember to do that…

I guess I could do something like this:

modify me hasSeen(obj) { return obj.seen; } ;
That should cause me.hasSeen to pick up the library’s use of the seen property … but again, if I do that, why do I need to remember to use gSetSeen(obj)?

I’m also wondering where in the code I would use something like bob.setKnowsAbout(treehouse) or carol.setKnowsAbout(oldBook). I mean, the NPC might encounter the treehouse or the old book while wandering around on some agenda or other, even when the PC is not in the same location. So where exactly would the call to setKnowsAbout go? In the takeTurn of bob’s current ActorState? That might work … but it’s still messy, because Bob might wander into a room where there are several things, so the code in takeTurn would have to iterate somehow through everything in bob’s current location.

Urk. I think I must be making this more complicated than it actually is. Suggestions welcome!

What this part of Learning TADS 3 presupposes is that in the event that you start tracking NPC knowledge separately from PC knowledge, then you’ll probably need to define new properties for tracking PC knowledge as well (unless you’ve defined new properties for every NPC in your game), or there’ll be a danger that PC and NPC knowledge getting muddled up. In that case, if you ever need to set or check what the PC has seen manually, you should use:

gSetSeen(foo);
if(me.hasSeen(foo))

Rather than:

foo.seen = true;
if(foo.seen)

The comment in the LRM that’s worrying you is slightly inaccurate. It applies if and only if you haven’t changed me.seenProp from &seen. If you have changed it, e.g. to &meHasSeen, then the library will use your new meHasSeen property instead, and everything will work as expected (including the automatic registration of objects seen in a room).

You don’t need to remember to do that for cases where the library automatically updates what the player character has seen, but you do in the rare cases where the library doesn’t, e.g. because you move an object into view in the course of a turn, in this sort of case:

wand: Thing 'magic wand' 'wand'
   dobjFor(Wave)
   {
         action()
         {
              if(ring.moved)
                  "Nothing happens! ";
              else
              {
                   ring.moveInto(me);
                   ring.wornBy = me;
                   gSetSeen(ring);
                   "A ring suddenly appears on your finger! ";
               }
          }
    }
;

Yes, this is likely to be tricky, and the right way of doing it probably depends on how much NPC information you want to track. Before reinventing the wheel, however, you might want to take a look at Steve Breslin’s t3knowledge extension on the TADS 3 extension page to see if it does what you want. As I recall it redefines the mechanism used in the library, and it’s quite old now, and probably hasn’t been tested with recent versions of TADS 3, but it may still work.

– Eric

I’ll look at Steve’s extension, but this morning I thought I’d tackle this situation myself. I seem to have it working … except that I don’t know how to iterate through everything that is visible in the current room. I know how to iterate through everything of Thing class in the game, and I known how to iterate through the contents list for the current room. But if something is lying in plain sight on a table, it’s not in the contents list for the current room. I’ve poked around in the documentation without seeing a convenient way to access (or construct) a list of everything that’s visible in the current room. (I’m going to assume that the NPC is not wearing a blindfold…)

If I iterate through every Thing in the game and test whether cur.getOutermostRoom() is the current room, that’s bound to be slow – maybe even slow enough to be noticeable to the player. An embedded while loop that looks for things inside of items in the room’s contents list would work, if I could figure out the correct syntax.

What I’m doing at present, in the ActorState for each NPC, is this:

takeTurn { local rm = getActor.getOutermostRoom(); for (local cur in rm.contents) { if (getActor.canSee(cur)) getActor.setHasSeen(cur); } }

The following code seems to work, even for the me object (which I gather has a default ActorState) and even with objects that are visible in ComplexContainers:

[code]modify Thing
meHasSeen = nil
bobHasSeen = nil
carolHasSeen = nil
// etc.
;

modify ActorState
takeTurn {
local rm = getActor.getOutermostRoom();
if (getActor.canSee(rm))
getActor.setHasSeen(rm);
local cur, cur2, cur3;
for (cur in rm.contents) {
if (getActor.canSee(cur))
getActor.setHasSeen(cur);
if (cur.contents != []) {
for (cur2 in cur.contents) {
if (getActor.canSee(cur2))
getActor.setHasSeen(cur2);
if (cur2.contents != []) {
for (cur3 in cur2.contents) {
if (getActor.canSee(cur3))
getActor.setHasSeen(cur3);
}
}
}
}
}
inherited;
}
;[/code]

It only nests three levels deep – I’m a little nervous about writing a recursive method to dig down to an arbitrary depth within the containment hierarchy, but that would probably be better.

The one “gotcha” I’m aware of is that if you use takeTurn in any of your own ActorStates, you have to remember to call inherited.

Even if you don’t use all of Steve Breslin’s extension, you can borrow the silentLook mechanism from the end.

modify Actor
    afterAction() {
        if (self != gPlayerChar)
            location.silentLook(self, self);
        inherited();
    }
;

modify Thing
    silentLook(actor, pov) {
        if (location != nil && actor.canSee(location))
            location.silentLook(actor, pov);
        else {
            local infoTab = actor.visibleInfoTableFromPov(pov);
            local info = infoTab[pov];
            if (info != nil && info.ambient > 1)
                actor.setHasSeen(self);
            setContentsSeenBy(infoTab, actor);
        }
    }
;

That setContentsSeenBy call in silentLook will recursively mark all visible objects as seen.

Thanks, Ben. That seems to work. (More testing remains to be done.)

My original code was slowing down the game … only slightly, but noticeably … because I was updating the knowledge of every actor in every turn. Steve’s method specifically doesn’t update the knowledge of an NPC unless the player is in the room, so if you have a lot of NPCs, some of whom are offstage, it’s more efficient. It would cause problems only if you wanted an NPC to specifically learn about something when the PC is not in the room. And if that becomes necessary, a little one-off code would be a better way to handle it.