JWZ's TADbits (#15: some Lite overlap)

I didn’t have the $ trick when I wrote PQ but that hasn’t stopped me from starting to use it when I’m going back over the PQ code looking for errors and omissions :slight_smile:
A person could probably also write a TADS function to iterate over the source files and look for duplicate tokens in vocabWords fields, eliminating one and pasting the $ before the other…

Bumping with a little observation that came out of some performance testing I’ve been doing. Figured it’s not quite worth its own thread but it’s probably worth knowing, so figured this is as close to a “general TADS3 discussion” thread as we’ve got.

Anyway: if you have a Vector instance, call it foo, and some element of it bar, then foo.removeElementAt(foo.indexOf(bar)) performs better than foo.removeElement(bar). Which is not what I would’ve expected.

It is much worse if bar is an object than, for example, an integer. But it’s still true for integers.

Here’s a little demonstration that creates a 65535-element Vector of sequential integers, creates two copies of it, shuffles the original into random order, and then traverses the shuffled list removing each element in it from one un-shuffled copy via removeElementAt() and indexOf, and then does the same only with removeElement() on the second unshuffled copy:

#charset "us-ascii"
#include <adv3.h>
#include <en_us.h>

#include <date.h>
#include <bignum.h>

class Foo: object
        active = nil
        isActive() { return(active == true); }
;

versionInfo: GameID;
gameMain: GameMainDef
        count = 65535
        newGame() {
                local i, l0, l1, ref, ts;

                // Create a vector of sequential integers.
                ref = new Vector(count);
                for(i = 0; i < count; i++) ref.append(i);
                //for(i = 0; i < count; i++) ref.append(new Foo());

                // Make two copies of it.
                l0 = new Vector(ref);
                l1 = new Vector(ref);

                // Shuffle the original vector.
                shuffle(ref);

                t3RunGC();
                ts = getTimestamp();
                ref.forEach(function(o) { l0.removeElementAt(l0.indexOf(o)); });
                aioSay('\nremoveElementAt()/indexOf() time:
                        <<toString(getInterval(ts))>>\n ');

                t3RunGC();
                ts = getTimestamp();
                ref.forEach(function(o) { l1.removeElement(o); });
                aioSay('\nremoveElement() time:
                        <<toString(getInterval(ts))>>\n ');
        }
        getTimestamp() { return(new Date()); }
        getInterval(d) { return(((new Date() - d) * 86400).roundToDecimal(3)); }

        // Fisher-Yates shuffle.
        shuffle(l) {
                local i, k, tmp;

                for(i = l.length; i >= 1; i--) {
                        k = rand(l.length) + 1;
                        tmp = l[i];
                        l[i] = l[k];
                        l[k] = tmp;
                }
        }
;

On my machine that produces:

removeElementAt()/indexOf() time: 6.198
removeElement() time: 8.713

If you comment out the first for() loop (the one that populates the list with sequential integers) and uncomment the second (which fills the list with instances of the Foo class) it becomes:

removeElementAt()/indexOf() time: 8.603
removeElement() time: 18.365

Anyway, just figured this was worth knowing.

This came out of a bunch of refactoring I’ve been doing. Specifically, I’ve been re-working all the rule engine and derived modules because I discovered an assumption I’d made about performance was wrong—I’d been doing a bunch juggling to keep the number of rules evaluated each turn low, by keeping track of which rulebooks were active at any given time and adding or removing them from the rule engine’s lists when they changed state. This turns out to be substantially less performant by just leaving everything in the list(s) and evalutating each rules’ active property/method every turn.

6 Likes

Here’s another minor scrap of TADS3 that came out of testing.

Often I find myself wanting to know what the upper bound on the size of some T3 data structure is. For example the maximum number of elements in a LookupTable. This is a slightly subtle question, as you can make LookupTable instances with numbers of elements that will not cause problems when created but will cause exceptions when accessed in various ways, for example if you do a table.keysToList().

Anyway, here’s a little code to find an upper bound by wrapping a callback function in a try/catch block and iterating until the maximum value (that doesn’t throw an exception) is found:

function findMaxValue(v0, v1, cb, maxTries?) {
        local i, j, n;

        maxTries = ((maxTries != nil) ? maxTries : 65535);      // max tries
        n = 0;                                                  // current tries

        while(1) {
                if(n >= maxTries)
                        return(nil);

                i = (v1 + v0) / 2;              // current guess at value
                if(i == j)                      // guess same twice, done
                        return(i);

                j = i;                          // remember last guess
                n += 1;                         // increment tries

                try {
                        cb(i);                  // invoke callback
                        if(i > v0)              // update lower bound
                                v0 = i;
                }
                catch(Exception e) {
                        v1 = i;                 // update upper bound
                }
        }
}

The args are:

  • v0 and v1 lower and upper integer values to try
  • cb callback function. it will be repeatedly called with integer arguments between v0 and v1
  • maxTries optional maximum number of iterations. default is 65535

Here’s an example of use. It finds the maximum number of keys in a LookupTable that won’t cause an exception when keysToList() is used:

        v = findMaxValue(1, 65535, function(n) {
                local i, l, table;

                // Create a lookup table
                table = new LookupTable();

                // Add n arbitrary elements to it.
                for(i = 0; i < n; i++) table[i] = i;

                // Get the keys as a list.  This has an upper
                // bound of 13106, for some reason. 
                l = table.keysToList();

                // Meaningless conditional so the compiler doesn't 
                // complain that we assigned a value to l and then
                // didn't do anything with it.
                if(l.length()) {}
        });

In this case, v should, after a few seconds, turn out to be 13106.

5 Likes

it’s very curious, because 13106 multiplied by 5 gives 65530 and by 10 gives 131060, that is, the overflow exception seems to happen when reaching the next power of two (2^16 in the first case…) so I think the overflow is caused by overflowing at the the end of a 64K two-byte array.
(ugly remnant of the accursed segmented architecture of the 8086 ?)

Best regards from Italy,
dott. Piergiorgio.

1 Like

Bumping this thread for another “worth posting but not worth its own thread” thing because this seems to be the closest thing to a general TADS3 discussion thread we have.

This is some code I put together for debugging backrefs/refcounts for instances of specific classes.

This is out of the procgen code I’m (still) working on. Every time a “dungeon” is generated there are a lot of temporary, just-for-the-generation-process objects created. The idea is that all of these objects get their refcount set to zero during cleanup after the generation process is complete, which means they’ll end up garbage collected. But occasionally there are situations where there’ll be a dangling reference left after cleanup, which if unfixed leads to a memory leak (as more and more stray instances stick around after each successive generation and cleanup).

Anyway, here’s the code. The bit designed to be directly interacted with is the searchObjects() global function, which takes a class as its argument. There’s also a “private” __searchObject() class that searchObjects() uses internally but isn’t of much use outside of that.

#ifdef __DEBUG

searchObjects(cls) {
        local n, r;

        n = 0;
        forEachInstance(cls, { x: n += 1 });
        if(n == 0)
                "\nNo instances.\n ";
        else
                "\nTotal of <<toString(n)>> instances.\n ";

        r = new Vector();
        forEachInstance(TadsObject, function(x) {
                local l;

                l = __searchObject(x, cls);
                if(l.length == 0)
                        return;
                r.append([ x, l ]);
        });
        if(r.length() == 0) {
                "\nNo matches.\n ";
        } else {
                local ar, i, j, txt0, txt1;

                "\n<<sprintf('%_.-30s %_.-30s', 'OBJECT', 'PROPERTY')>>";
                for(i = 1; i <= r.length; i++) {
                        ar = r[i];
                        for(j = 1; j <= ar[2].length; j++) {
                                if(j == 1) txt0 = ar[1];
                                else txt0 = '';
                                txt1 = ar[2][j];
                        }
                        "\n<<sprintf('%_.-30s %_.-30s', toString(txt0),
                                toString(txt1))>>";
                }
        }
}

__searchObject(obj, cls) {
        local r, v;

        r = new Vector();
        obj.getPropList().forEach(function(x) {
                if(!obj.propDefined(x, PropDefDirectly)) return;
                if((x == &symtab_) || (x == &reverseSymtab_))
                        return;
                switch(obj.propType(x)) {
                        case TypeList:
                                if(obj.(x).valWhich(
                                        { y: isType(y, cls) })
                                        != nil)
                                        r.append(x);
                                break;
                        case TypeObject:
                                v = (obj).(x);
                                if(isLookupTable(v)) {
                                        v.forEachAssoc(function(foo, bar) {
                                                if(isType(foo, cls)
                                                        || isType(bar, cls))
                                                        r.append(x);
                                        });
                                } else if(isCollection(v)) {
                                        if(v.valWhich({
                                                y: isType(y, cls)
                                        }) != nil)
                                                r.append(x);
                                } else {
                                        if(isType(v, cls))
                                                r.append(x);
                                }
                                break;
                        default:
                                return;
                }
        });
        return(r.toList());
}

#endif // __DEBUG

As an example of use, for debugging I’ve defined a >REFCOUNT system action, which looks like:

DefineSystemAction(Refcount)
        _search = DungeonBranch

        execSystemAction() {
                searchObjects(_search);
        }
;
VerbRule(Refcount) 'refcount' : RefcountAction
        verbPhrase = 'refcount/refcounting';

This is just a wrapper for the searchObjects() function, calling it with a defined class as its argument. This could be cleaner…parsing the class from the command or something…but I’m generally only doing this for one class at a time, doing it a bunch of times while I’m doing it, and then I’m not doing it again unless I’m more careless than usual. So this approach works fine. Anyway, here’s sample output:

>refcount
Total of 3 instances.
OBJECT........................  PROPERTY......................
{obj:BranchAndBottleneck}.....  _branches.....................

This tells me that I’ve got three (almost-)orphaned instances of DungeonBranch, and they’re referenced in instances of the BranchAndBottleneck class (a dungeon template), in the _branches property.

This works for instances that are referenced by properties directly, instances that occur in properties that are lists and vectors, and instances that are either a key or value in a lookup table.

Not sure how useful this is to a general audience (it’s something you only have to worry about if you’re generating stuff on the fly) but it seems worth putting out there.

5 Likes

Some more light thread necromancy because I think this is still the closest thing we have to a general TADS discussion thread.

Anyway, just pushed an update to the adv3Utils module I use as a junk drawer for general little TADS3/adv3 tweaks. In this case it was to add an OrdinalThing class.

OrdinalThing

Properties

  • ordinalNumber = nil

    The number of this instance. The value must be an integer.

  • ordinalVocab = nil

    The vocabulary to use for the ordinal vocabulary. This can be either a single single-quoted noun ('pebble') or a list of single-quoted nouns ([ 'pebble', 'rock' ]).

    If ordinalVocab is nil, the object’s noun list will be used instead.

  • ordinalDisambig = true

    If boolean true disambiguation will automagically use ordinal numbers and the disambiguation will be sorted by each object’s ordinalNumber.

    Default is true.

Methods

  • addOrdinalVocab(n, str)

    Automatically called by initializeThing(). Will be called with n equal to the value of ordinalNumber and str equal to each value defined for ordinalVocab.

    You generally won’t have to call this directly unless you’re fussing around with objects after they’re initialized.

Template

The OrdinalThing template is largely identical to the basic Thing template:

pebble01: OrdinalThing 'pebble' 'pebble number one' +1
        "There's a one painted on it. "
;

The +1 gives this instance’s ordinalNumber. A single-quoted noun (or a List of them) could be added after the +1 if a different list of nouns is to be used for the ordinal vocabulary. In this case pebble, the object’s only noun, will be used instead.

Objects can also be declared without the template like a standard Thing:

pebble02: OrdinalThing 'pebble' 'pebble number two'
        "There's a two painted on it. "
        ordinalNumber = 2
;

Having declared the above, the following will all work:

>X FIRST PEBBLE
>X PEBBLE NUMBER ONE
>X PEBBLE #1
>X #1 PEBBLE   
>X NUMBER ONE PEBBLE

Also, disambiguation will look like:

>X PEBBLE
Which pebble do you mean, the first pebble, or the second pebble?

Discussion

About half of this can be accomplished “directly” by just declaring adjectives on the objects. >X FIRST PEBBLE just requires declaring 'first' as an adjective in the object’s vocabWords. But this doesn’t work by default with a construction like >X PEBBLE NUMBER ONE, because “noun adjective adjective” isn’t a standard adv3 noun phrase format.

Under the hood this is works the same way declaring the vocabWords as ''(first) (number) (one) (#1) pebble one/pebble'. That’s declaring the noun as a non-weak adjective and the number as a weak noun. This means >X ONE will not resolve “one” as a noun phrase but, >X PEBBLE NUMBER ONE will.

The class also handles setting up the ordinal-based disambiguation, but that’s just straightforward setting of the disambigName and disambigPromptOrder. So it’s taking care of some bookkeeping but isn’t handling anything particularly confusing or fiddly. And I find the noun/adjective/weak token tweaking very fiddly, so don’t have to remember the whole ritual every time I declare an object.

Anyway, it’s another one of those “not a big deal but took me longer than I would’ve liked” kind of things, so figured I’d put it out there.

5 Likes

I see “about” is doing a lot of work when the system manual de#drives lists as being limited to “about 13100” elements.

Another thread bump because, again, I think it’s the closest thing to a “general TADS3 discussion” thread we have.

Added another little snippet to the adv3Utils library, this time a couple of lines to make it easier to modify an Action’s indirect object scope list. By default Action.objInScope() and Action.getScopeList() only affect the direct objects’ scope, not any indirect objects’ (which end up inheriting the default behavior on IobjResolver).

It’s a little patch that uses iobjInScope() and iobjScopeList() as objInScope() and getScopeList() (respectively) for indirect objects if they’re defined on the Action:

modify Action
        iobjInScope = nil
        iobjScopeList = nil
;

modify IobjResolver
        objInScope(obj) {
                if(action_.propType(&iobjInScope) != TypeNil)
                        return(action_.iobjInScope(obj));
                return(inherited(obj));
        }

        getScopeList() {
                if(action_.propType(&iobjScopeList) != TypeNil)
                        return(action_.iobjScopeList());
                return(inherited());
        }
;

This coming out of a bunch of very bewildering testing involving a lot of actions involving out-of-scope objects. Which, incidentally interact in a number of very counterintuitive ways with Unthing instances. For example an Unthing is “visible” in its location and not in any other location. Which makes sense in the context (since Unthing basically exists specifically to tweak the messages involving non-present objects in specific scopes) but can get very messy when noun resolution scope changes.

3 Likes

Another smattering of tiny adv3 tweaks.

This time it’s some code to handle something I’ve taken a swing at before, specifically how to handle objects with dynamic vocabulary.

The builtin ThingState almost does what I want, but a) it only allows one state/vocabulary to be active at once, and b) the only other behavior it changes is the naming of the object e.g. in list contexts.

What I need is a way to have an object reflect any of a number of possible player knowledge states, specifically where the changes aren’t necessarily triggered in a specific order and they aren’t necessarily cumulative.

So an NPC might be initially described as a guy in a Zork t-shirt. The player might at some point learn that his name is Bob, that he’s the player’s neighbor, or that Bob is a werewolf. But not necessarily in that order.

With stock ThingState the problem is if the “werewolf” state is active, then the vocabulary associated with the “neighbor” state is active unless it is also defined on the “werewolf” state. Which it can’t be if you don’t know that the player will have discovered the former fact before the latter.

Anyway, the tweak(s) I’ve added to the adv3Utils module are:

Add a generic matchStateToken() method to ThingState

This is borrowed from the adv3Patches module. It adds a couple of fixes for ThingState vocabulary matching, specifically it treats tokens as case-insensitive and it handles possessives:

modify ThingState
        matchStateToken(tok) {
                tok = tok.toLower();

                return(stateTokens.indexWhich(function(o) {
                        o = o.toLower();

                        if(o.endsWith('\'s'))
                                o = o.substr(1, o.length() - 2);

                        return(o == tok);
                }) != nil);
        }
;

Update ThingState.matchName() to allow multiple states to match vocabulary

First we define an active property and an order property.

The order property is used elsewhere but I won’t get into it here, right now all that matters is that it being a number greater than -1 identifies a ThingState as using the new multi-state vocabulary matching.

The active property is a flag to enable/disable the state. This starts out nil by default and is toggled by whatever game logic reveals the knowledge associated with the state.

Then we basically just reproduce the stock ThingState.matchName() logic, with the caveats:

  • If the state doesn’t have an order greater than -1 then the stock behavior is used (so existing ThingStates will work as before)
  • If the state does have an order and is not active matching immediately fails
  • If another state is also active and matches the token, processing continues (instead of immediately failing, as in the stock method)

The code:

modify ThingState
        active = nil
        order = -1

        isActive() { return(isMulti() ? (active == true) : true); }
        isMulti() { return(order > -1); }

        matchName(obj, origTokens, toks, states) {
                local i, l, len, tok;

                if(!isActive())
                        return(nil);

                len = toks.length();
                for(i = 1; i <= len; i+= 2) {
                        tok = toks[i];

                        if(matchStateToken(tok))
                                continue;

                        l = states.subset({
                                x: x.matchStateToken(tok) != nil
                        });

                        if(l.indexWhich({ x: x.isMulti() && x.isActive() })
                                != nil)
                                continue;

                        if(l.length > 0)
                                return(nil);
                }

                return(obj);
        }
;

Add a stateDesc property to ThingState

This just adds a state-specific description to the object that will be displayed if the given state is the current state.

Note that this has nothing to do with the multi-state logic above; this only applies to the state that’s returned by Thing.getState():

modify ThingState
        stateDesc = ""
;

modify Thing
        examineStatus() {
                local st;

                inherited();

                if((st = getState()) != nil)
                        st.stateDesc;
        }
;

Implement something similar for rooms

To get multi-state rooms that work as above, we now add the room-specific description properties to ThingState and tweak Thing.lookAroundWithinDesc() to use them:

modify ThingState
        roomDesc = ""
        roomFirstDesc { roomDesc; }
        roomDarkDesc = ""
        roomRemoteDesc(actor) {}
;

modify Thing
        lookAroundWithinDesc(actor, illum) {
                local pov, st;

                inherited(actor, illum);

                if((st = getState()) == nil)
                        return;
                if(illum > 1) {
                        pov = getPOVDefault(actor);
                        if(!actor.isIn(self) || (actor != pov)) {
                                st.roomRemoteDesc(actor);
                        } else if(actor.hasSeen(self)) {
                                st.roomDesc;
                        } else {
                                st.roomFirstDesc;
                        }
                } else {
                        st.roomDarkDesc;
                }
        }
;

Some code for declaring arbitrary message parameter substitutions

Finally we declare a MessageToken class and some init gymnastics to declare arbitrary message parameter strings. This is almost what you get out of Thing.globalParamName, but this allows greater flexibility (multiple substitutions on a single object, or multiple objects using the same substitution) :

class MessageToken: object
        id = nil
        prop = nil
        token = nil
        obj = nil
;

messageTokensPreinit: PreinitObject
        execAfterMe = [ MessageBuilder ]
        _messageTokens = perInstance(new Vector)

        execute() {
                initMessageTokens();
        }

        initMessageTokens() {
                forEachInstance(MessageToken, { x: addMessageToken(x) });

        }

        addMessageToken(obj) {
                if(!isMessageToken(obj)) return(nil);
                _messageTokens.appendUnique(obj);

#ifdef __DEBUG
                if(obj.id != obj.id.toLower()) {
                        aioSay('\n===WARNING===\n ');
                        aioSay('\nconverting MessageToken id <<obj.id>>
                                to lower case\n ');
                        aioSay('\n===WARNING===\n ');
                }
#endif // __DEBUG

                obj.id = obj.id.toLower();

                obj.token = 'token_' + obj.id;

                langMessageBuilder.paramList_
                        = langMessageBuilder.paramList_.append(
                                [ obj.id, obj.prop, obj.token, nil, nil ]);

                return(true);
        }
;

messageTokensInit: InitObject
        execute() {
                initMessageTokens();
        }

        initMessageTokens() {
                messageTokensPreinit._messageTokens.forEach({
                        x: addMessageToken(x)
                });
        }

        addMessageToken(obj) {
                if(!isMessageToken(obj)) return(nil);

                langMessageBuilder.nameTable_[obj.token] = obj.obj;

                return(true);
        }
;

Putting it all together

A simple “game” that illustrates what this gets us. We define a couple of states on the starting room, with the states controlled by a <.reveal> tag in the sign description:

#include <adv3.h>
#include <en_us.h>

#include "adv3Utils.h"

versionInfo: GameID;
gameMain: GameMainDef initialPlayerChar = me;

startRoom: Room '{void}'
        ""
        vocabWords = '(featureless) (some) (odd) void/room'
        allStates = [ nonVoid, void ]
        getState() {
                return(gRevealed('void') ? void : nonVoid);
        }
        voidName() { return(getState().voidName); }
;
+nonVoid: ThingState
        order = 1
        active = true
        stateTokens = [ 'odd' ]
        roomDesc = "This is an odd room with a sign on the wall. "
        voidName = 'Some Room'
;
+void: ThingState
        order = 2
        active = (gRevealed('void'))
        stateTokens = [ 'featureless', 'void' ]
        roomDesc = "This is a featureless void with a sign on the wall that
                totally doesn't count as a feature. "
        voidName = 'Void'
;
+sign: Decoration 'sign' 'sign'
        "<q>This is the Void.</q> <.reveal void> ";
+me: Person;

MessageToken 'void' ->(&voidName) @startRoom;

Thrilling transcript:

Some Room
This is an odd room with a sign on the wall.

>x odd room
This is an odd room with a sign on the wall.

>x void
You see no void here.

>x sign
"This is the Void."

>x void
This is a featureless void with a sign on the wall that totally doesn't count
as a feature.

>x odd room
This is a featureless void with a sign on the wall that totally doesn't count
as a feature.

2 Likes

This is pretty great. Three years in, I have kludged my way around this capability in the most ugly ways imaginable. sigh Do I REALLY refactor at this point???

I also appreciate your discipline in “Patch” definition. My own adv3_jjmcc is, at this point, a grotesque, inseparable melange of bugfixes and extensions.

3 Likes

That’s the third? Fourth? Approach to the problem I’ve “formally” done. As in code in a repo instead of code doodles in a scratch directory somewhere.

It’s one of those problems where you get stuck in loops going “Option A is bad because of [reasons], so I should do B, which fixes the problems of A”, then “Option B is bad because of [different reasons], so I should do C, which fixes the problems of B”, and then “Option B is bad because of [third set of reasons], so I should do A, which fixes the problems of C”.

2 Likes