# rules.t | documentation

    #charset "us-ascii"
    #include "advlite.h"

    /*
     *   RULES EXTENSION by Eric Eve July 2014
     *
     *   The rules.t extension allows Inform7-style rules and rulebooks to be
     *   defined in a TADS 3 game.
     */

    /*
     *   A RuleBook is a container for one or more rules. Calling the follow()
     *   method of a RuleBook causes each of its contained rules to be executed in
     *   turn until one returns a non-null value. That value is then returned to the
     *   caller of the RuleBook. [DEFINED IN RULES EXTENSION]
     */
    class RuleBook: PreinitObject

        /* A list of rules contained in this rulebook */
        contents = []

        /*
         *   The actor to use for comparison with the who property for rules in this
         *   RuleBook. The default is gPlayerChar, but for some RuleBooks gActor may
         *   be more appropriate.
         */
        actor = gPlayerChar

        /* Add a rule to the contents of this rulebook [RULES EXTENSION] */
        addToContents(ru)
        {
            contents += ru;
        }

        /* Remove a rule from the contents of this rulebook [RULES EXTENSION] */
        removeFromContents(ru)
        {
            contents -= ru;
        }

        /*
         *   Follow this Rule. This is the method game code will normally call to
         *   make use of this RuleBook. Each of our rules will be tested to see if
         *   it matches its conditions; we then run through those of our rules that
         *   match their rules in order of precedence until one returns a non-null
         *   value, which we then in turn return to our caller. If no rule returns a
         *   non-null value we return our own default value, which is normally nil.
         *
         *   This method can be called with as many arguments as the game code finds
         *   useful, or with none at all. Our arguments will then be passed on to
         *   each Rule that is called. The first argument will also be stored in our
         *   matchObj property, which our Rules can compare with their own matchObj
         *   condition to see if they match. This allows game code to, for example,
         *   run a RuleBook related to some object that isn't one of the objects
         *   directly involved in the current action. [RULES EXTENSION]
         */
        follow([args])
        {
            /*
             *   If we have any arguments at all, store the first one in our
             *   matchObj property for comparison with our Rules' matchObj
             *   conditions.
             */
            if(args.length > 0)
                matchObj = args[1];

            /* Carry out any custom initialization required. */
            initBook(args...);

            /*
             *   Make sure that all the Rules in our contents list know that we're
             *   the RuleBook that's currently calling them. This is necessary in
             *   case the same Rule is associated with more than one RuleBook.
             */
            contents.forEach({ r: r.setRulebook(self) });

            /* Extract the subset of Rules that match their conditions. */
            local validRules = contents.subset({r: r.matchConditions});

            /* Sort the matching rules in descending order of precedence. */
            validRules = validRules.sort(SortDesc, {a, b: a.compareTo(b)} );

            /*
             *   Go through each of the matching rules in turn, calling its follow
             *   method. If any rule returns a non-null value, stop running through
             *   the rules and return that value to our caller.
             */
            foreach(local ru in validRules)
            {
                IfDebug(rules,
                        "[Executing rule: <<valToSym(ru)>> in rulebook
                        <<valToSym(self)>>]\n" );
                local res = ru.follow(args...);

                if(res != contValue)
                    return res;
            }

            /*
             *   If no rule returned a non-null value, return our own defaultVal to
             *   our caller.
             */
            return defaultVal;
        }

        /*
         *   contValue (continue value) is the value that a Rule in this RuleBook
         *   needs to return to avoid the RuleBook stopping at that Rule (rather
         *   than going on to consider more Rules). By default this is null, which
         *   means by default a Rule that does not explicitly return a value (and so
         *   effectivelt returns nil) will stop the RuleBook. If you want the
         *   default behaviour for this RuleBook to be not for Rules to stop the
         *   book, then override this to nil. [RULES EXTENSION]
         */
        contValue = null

        /*
         *   The default value to return to our caller. By default this is the same
         *   as our contValue , to make it easy to test whether we any rule returned
         *   a non-null value. By default a rule that does something will return
         *   nil, so if no rule does anything we want to return a different value.
         *   By making the defaultValue the same as the contValue, we ensure that we
         *   can tell our caller that no rule was executed (if that is indeed the
         *   case). [RULES EXTENSION]
         */
        defaultVal = contValue

        /*
         *   The value our associated rules use by default to stop this RuleBook
         *   considering any further rules (when a Rule uses the stop macro). By
         *   default we use a value of true. [RULES EXTENSION]
         */
        stopValue = true

        /*
         *   Game code can use this method to initialize the values of custom
         *   RuleBook properties at the start of the processing of following a
         *   RuleBook. [RULES EXTENSION]
         */
        initBook([args]) { }

        /*
         *   The object (or any other value) to be matched by our Rule's matchObj
         *   conditions if they have any. This property is set by our follow()
         *   method (from its first argument) and so should not normally be directly
         *   changed from game code. [RULES EXTENSION]
         */
        matchObj = nil

    ;

    /* Preinitializer for Rules. [DEFINED IN RULES EXTENSION]*/
    rulePreinit:PreinitObject
        execute()
        {
            /* Initialize all the Rules in the game. */
            for(local ru = firstObj(Rule); ru != nil; ru = nextObj(ru, Rule))
            {
                ru.initializeRule();
            }
        }
    ;

    /*
     *   A Rule is an object that defines a set of conditions that need to be met
     *   for it to be executed when its RuleBook is run and a method that's executed
     *   when its conditions are met. A Rule can be associated with one or more
     *   RuleBooks; it starts out in the RuleBook with which it is associated via
     *   its + property (i.e. its location). [DEFINED IN RULES EXTENSION]
     */
    class Rule: object
        /*
         *   Our location is the RuleBook with which we start out being associated.
         *   Normally this will be defined by locating a Rule inside its RukeBook
         *   using the + notation. [RULES EXTENSION]
         */
        location = nil

        /*
         *   The rulebook that's currently considering us. Normally this will be our
         *   location, but it could be a different RuleBook if we belong to one.
         *   Note that this property is automatically set by the library and so it
         *   should never need to be altered by game code. [RULES EXTENSION]
         */
        rulebook = location

        /*
         *   Set our current rulebook to r. Note that this method is normally called
         *   by the Rulebook that's running us, and shouldn't normally be used by
         *   game code. [RULES EXTENSION]
         */
        setRulebook(r) { rulebook = r; }

        /*   A list of all the rulebooks this rule is currently associated with. [RULES EXTENSION] */
        rulebooks = []

        /*
         *   Initialize this Rule by adding it to the contents list of its location
         *   and calculating its specificity (i.e. how specific its conditions are)
    	 *   [RULES EXTENSION]
         */
        initializeRule()
        {
            if(location)
            {
                location.addToContents(self);
                rulebooks += location;
            }

            specificity = calcSpecficity();
        }

        /*
         *   Do whatever this Rule needs to do when its conditions are met. This
         *   method will need to be defined on each individual Rule in game code.
    	 *   [RULES EXTENSION]
         */
        follow([args])
        {
        }

        /*
         *   The priority of this Rule. This can be used to alter the order in which
         *   this Rule is considered in its RuleBook. If two Rules have different
         *   priorities they will be run in priority order, highest priority first.
         *   The default value is 100. [RULES EXTENSION]
         */
        priority = 100

        /*
         *   Where two Rules have the same priority, the one with the more specific
         *   conditions is taken first. The specificity property holds a measure of
         *   the Rule's specificity which is calculated by the calcSpecificity()
         *   method at PreInit. [RULES EXTENSION]
         */
        specificity = nil

        /*
         *   Return true if this Rule should always execute after other (despite all
         *   other ranking criteria). By default we return true if and only if other
         *   is in our execAfter list. [RULES EXTENSION]
         */
        runAfter(other)
        {
            return valToList(execAfter).indexOf(other) != nil;
        }

        /*
         *   A list of Rules this Rule should specifically run after; this overrides
         *   all other ranking. [RULES EXTENSION]
         */
        execAfter = []

        /*
         *   Return true if this Rule should always execute before other (despite all
         *   other ranking criteria). By default we return true if and only if other
         *   is in our execBefore list. [RULES EXTENSION]
         */
        runBefore(other)
        {
            return valToList(execBefore).indexOf(other) != nil;
        }

        /*
         *   A list of Rules this Rule should specifically run before; this
         *   overrides all other ranking except for runAfter/execAfter.
         *   [RULES EXTENSION]
         */
        execBefore = []

        /*
         *   A Rule is normally active (that is it will normally be considered when
         *   a RuleBook is being followed) but it can be temporarily disabled by
         *   setting its isActive property to nil. [RULES EXTENSION]
         */
        isActive = true

        /* Make this Rule active [RULES EXTENSION] */
        activate() { isActive = true; }

        /* Make this Rule inactive [RULES EXTENSION] */
        deactivate() { isActive = nil; }

        /*
         *   Calculate the specificity of this Rule. The principles are (a) Rules
         *   that specify more conditions are more specific than Rule that specify
         *   fewer condition; (b) conditions involving specific objects are more
         *   specific that those relating to classes and (c) Rooms are more specific
         *   than Regions in a where condition. [RULES EXTENSION]
         */
        calcSpecficity()
        {
            local p = 0;

            /* a 'when' has priority over no 'when' */
            if(propDefined(&when))
                p += 10;

            /*
             *   a 'where' has priority over no 'where', and a where property that
             *   specifies a Room is more specific than one that specifies a Region.
             */
            if(propDefined(&where))
                p += valToList(where).indexWhich({r: r.ofKind(Room)}) ? 10 : 5;

            /* a 'during' has priority over no 'during' */
            if(propDefined(&during))
                p += 10;

            /* a 'who' has priority over no 'who' */
            if(propDefined(&who))
                p += 10;

            /* an 'action' has prioity over no 'action' */
            if(propDefined(&action))
            {
                p += 5;
            }

            /*
             *   A Rule that refers to command objects (direct, indirect and
             *   accessory) is more specific than one that does not, and particular
             *   objects are more specific than classes.
             */
            for(local prop in [&dobj, &iobj, &aobj])
            {
                if(propDefined(prop))
                {
                    p += valToList(self.(prop)).indexWhich( {x: !x.isClass()} ) !=
                    nil ? 2 : 1;
                }
            }

            /*
             *   If matchObj is defined, then what we do with it depends on the type
             *   of value(s) it contains.
             */
            if(propDefined(&matchObj))
            {
                switch(propType(&matchObj))
                {
                    /* If matchObj is nil, then ignore it. */
                case TypeNil:
                    break;

                    /*
                     *   If it's an object, increase our specificity by 10, unless
                     *   it's a class, in which case only increase it by 5 (a class
                     *   is less specific than an object).
                     */
                case TypeObject:
                    p += (matchObj.isClass ? 5 : 10);
                    break;
                    /*
                     *   If it's a list, check what kind of values the list
                     *   contains.
                     */
                case TypeList:
                    /*
                     *   If the first item in the list is an object, assume the
                     *   whole list contains objects, then increase our specificity
                     *   by 10 if any of those objects is not a class, and by 5
                     *   otherwise (classes are less specific than objects).
                     */
                    if(matchObj.length > 0 && dataType(matchObj[1]) == TypeObject)
                    {
                        if(matchObj.indexWhich({o: !o.isClass}))
                            p += 10;
                        else
                            p += 5;
                    }
                    /*
                     *   Otherwise, if it's some other kind of value, simply
                     *   increase our specificity by 10.
                     */
                    else
                        p += 10;

                    break;
                    /*
                     *   For any other kind of value, simply increase our
                     *   specificity by 10.
                     */
                default:
                    p += 10;
                    break;

                }
            }

            /* Check if our present property is defined. */
            if(propDefined(&present))
            {
                if(dataType(&present) == TypeObject && present.isClass)
                    p += 5;
                else
                    p += 10;
            }

            /* Return the result of the calculation. */
            return p;
        }

        /*
         *   Get the processing priority sorting order relative to another
         *   Rule. [RULES EXTENSION]
         */
        compareTo(other)
        {
            /*
             *   If we specifically want this Rule to run after other, rank us after
             *   other
             */
            if(runAfter(other))
                return -1;

            /*
             *   If we specifically want this Rule to run before other, rank us
             *   before other.
             */
            if(runBefore(other))
                return 1;

            /*
             *   If the two Rules have different priorities, rank them in order of
             *   priority.
             */
            if(priority != other.priority)
                return priority > other.priority ? 1 : -1;

            /*
             *   Otherwise, if they have different speficifities, rank them in order
             *   of specificity.
             */
            if(specificity >= other.specificity)
                return specificity > other.specificity ? 1 : -1;

            /*
             *   Failing all else, go by the relative location of the source
             *   code definitions: the definition that appears later in the
             *   source code takes precedence.  If the two are defined in
             *   different modules, the one in the later module takes
             *   precedence.
             */
            if (sourceTextGroup != other.sourceTextGroup)
                return sourceTextGroup.sourceTextGroupOrder
                - other.sourceTextGroup.sourceTextGroupOrder;

            /* they're in the same module, so the later one takes precedence */
            return sourceTextOrder - other.sourceTextOrder;

        }

        /* Check whether a Rule matches its where, when, who and during conditions. [RULES EXTENSION] */
        matchConditions()
        {
            /* If this Rule is currently inactive it can't match any conditions. */
            if(!isActive)
                return nil;

            /* first check the where condition, if there is one. */
            if(propDefined(&where))
            {
                local whereLst = valToList(where);

                /*
                 *   if we can't match any item in the where list to the player
                 *   char's current location, we don't meet the where condition, so
                 *   return nil
                 */
                if(whereLst.indexWhich( {loc: gActor.isIn(loc)}) == nil)
                    return nil;
            }

            /*
             *   Interpret 'when' as simply a routine that returns true or nil
             *   aocording to some condition defined by the author; so we simply
             *   test whether when returns nil if the property is defined.
             */
            if(propDefined(&when) && when() == nil)
                return nil;

             /* check the who condition, if there is one. */
            if(propDefined(&who))
            {
                local whoLst = valToList(who);

                /*
                 *   If we can't match any item in the who list to the current
                 *   actor, we don't meet the who condition, so return nil
                 */
                if(whoLst.indexOf(gActor) == nil)
                    return nil;
            }

            /*
             *   if we're using the scene manager and a during condition is
             *   specified, test whether the scene is currently happening.
             */
            if(defined(sceneManager) && propDefined(&during))
            {
                local duringList = valToList(during);

                if(duringList.indexWhich({s: s.isHappening}) == nil)
                    return nil;
            }

            /*
             *   If we've specified an action to match, test whether gAction (the
             *   current action) matches it.
             */
            if(propDefined(&action)
               && valToList(action).indexWhich({a: gAction.ofKind(a)}) == nil)
                return nil;

            /*
             *   If we've specified a dobj, iobj and/or aobj to match, test whether
             *   they match the direct object, indirect object and/or accessory
             *   object of the current action (provided there is one, which there
             *   may not be at startup).
             */
            if(gAction)
            {
                for(local objs in [[&dobj, gDobj], [&iobj, gIobj], [&aobj, gAobj]])
                {
                    local prop = objs[1];
                    local obj = objs[2];

                    if(propDefined(prop)
                       && valToList(self.(prop)).indexWhich({o: obj.ofKind(o)}) == nil)
                        return nil;

                }
            }

            /*
             *   If we have a matchObj defined, test whether it matches our current
             *   rulebook's matchjObj.
             */
            if(propDefined(&matchObj))
            {
                local mList = valToList(matchObj);
                local mo = rulebook.matchObj;

                if(mList.length > 0)
                {
                    /*
                     *   If we want to match an object (or class), test whether the
                     *   rulebook's match obj is of the appropriate kind. Since an
                     *   object is always of its own kind, this tests either whether
                     *   the rulebook's matchObj appears in our matchObj list or
                     *   whether it belongs to one of the classes in our matchObh
                     *   list.
                     */
                    if(dataType(mList[1]) == TypeObject)
                    {
                        if(mList.indexWhich({o: mo.ofKind(o) } ) == nil)
                            return nil;
                    }
                    /*
                     *   Otherwise, if we're not testing for an object, simply test
                     *   whether the value of our rulebook's matchObj is equal to
                     *   anyhting in our matchObj property.
                     */
                    else if(mList.indexOf(mo) == nil)
                        return nil;

                }
            }

            /*
             *   If present is defined, check whether at least one of the items in
             *   the list is in the same room, or can be sensed.
             */
            if(propDefined(&present))
            {
                local pList = valToList(present);

                /*
                 *   First test for the special case that the present property
                 *   specifies a single class. In which case whether anything in the
                 *   location of actor matches that class.
                 */
                if(pList.length == 1 && dataType(pList[1]) == TypeObject &&
                   pList[1].isClass())
                {
                    if(actor.getOutermostRoom.allContents.indexWhich(
                        {o: o.ofKind(present) }) == nil)
                        return nil;
                }

                /*
                 *   Check whether the first item in list is a property pointer. If
                 *   it is we want to use it as a property of the Q object to test
                 *   for a sense connection.
                 */
                else if(pList.length > 0 && dataType(pList[1]) == TypeProp)
                {
                    /* The first item in the list is a property pointer. */
                    local prop = pList[1];

                    /*
                     *   Reduce the list to its remaining elements, which should all
                     *   be objects.
                     */
                    pList = pList.sublist(2);

                    /*
                     *   If no item in pList has a sense path from the actor via the
                     *   prop property, we don't match.
                     */
                    if(pList.indexWhich({o: Q.(prop)(actor, o) }) == nil)
                       return nil;
                }
                /*
                 *   Otherwise, simply test for the presence of one of the objects
                 *   in the actor's room.
                 */
                else
                {
                    local loc = actor.getOutermostRoom();

                    if(pList.indexWhich({o: o.isIn(loc)}) == nil)
                        return nil;
                }
            }

            /*
             *   If we haven't failed any of the conditions, we're okay to match, so
             *   return true.
             */
            return true;
        }

        /* Add this rule to another rulebook [RULES EXTENSION] */
        addTo(rb)
        {
            if(rb && rb.ofKind(RuleBook))
            {
                rb.addToContents(self);
                rulebooks += rb;
            }
        }

        /* Remove this rule from a rulebook [RULES EXTENSION] */
        removeFrom(rb)
        {
            if(rb && rb.ofKind(RuleBook))
            {
                rb.removeFromContents(self);
                rulebooks -= rb;

                if(location == rb)
                    location = nil;
            }
        }

        /*
         *   Move this rule to another rulebook, removing it from all its current
         *   rulebooks. If rb is nil, simply remove this Rule from its current
         *   rulebooks. [RULES EXTENSION]
         */
        moveInto(rb)
        {

            /*
             *   If rb is actually a RuleBook, add us to its contents and make rb
             *   our current location.
             */
            if(rb && rb.ofKind(RuleBook))
            {
                rb.addToContents(self);

                location = rb;
            }

            /*
             *   Remove us from the contents of all our previous rulebook
             */
            rulebooks.forEach({r: r.removeFromContents(self)});

            /* Add our new RuleBooks to our list of rulebooks */
            if(rb)
                rulebooks += rb;

            /* Make rb our currently active rulebook */
            rulebook = rb;

        }

        /*
         *   The value this rule should return when the stop macro is used at the
         *   end of its follow method. By default we use our rulebook's stopValue.
    	 *   [RULES EXTENSION]
         */
        stopValue = (rulebook.stopValue)

        /*
         *   The actor to use to compare with the who property of this Rule. This
         *   will normally be gPlayerChar, but the value of this property is taken
         *   from our RuleBook's actor property. [RULES EXTENSION]
         */
        actor = (rulebook == nil ? gPlayerChar : rulebook.actor)

        /* ------------------------------------------------------------------- */
         /*
          *   One or more of the following properties, if defined, determine what
          *   conditions this Rule needs to match in order to be executed.
          */

        /*
         *   A Room or Region, or a list of Rooms and/or Regions in which our actor
         *   (usually either gActor or gPlayerChar - the latter by default - must be
         *   for this Rule to match. [RULES EXTENSION]
         */
        // where = []

        /*
         *   A condition that must hold (or a method returning a Boolean value to
         *   determine whether or not appropriate conditions hold) for this Rule to
         *   match. This is only needed if none of the other properties in this
         *   section provide a way of speficifying the required conditions.
         */
        // when() {}

        /*
         *   An actor, or a list of actors, one of whom must be performing the
         *   current action for this Rule to match. [RULES EXTENSION]
         */
        // who = []

        /*
         *   A Scene, or a list of Scenes, one of which much be currently happening
         *   for this Rule to match. [RULES EXTENSION]
         */
        // during = []

        /*
         *   An action, or a list of Actions, one of which (e.g. Take or Jump) must
         *   be the current action in order for this Rule to match. [RULES EXTENSION]
         */
        // action = []

        /*
         *   An object (or class), or a list of objects (and or classes) one of
         *   which of each the direct, indirect and accessory objects of the current
         *   action must match in order for this Rule to match. (The accessory
         *   object is only relevant if the TIAAction extension is in use).
    	 *   [RULES EXTENSION]
         */
        // dobj = []
        // iobj = []
        // aobj = []

        /*
         *   An object, class, or other value, or a list of objects and/or classes
         *   or of other values, one of which must match the matchObj property of
         *   our rulebook (which is set by the first parameter of a call to that
         *   RuleBooks's follow() method) for this Rule to match. [RULES EXTENSION]
         */
        // matchObj = []

        /*
         *   An object in the presence of which the actor must be for this rule to
         *   match. Presence normally means in the same room, but if this property
         *   is defined as a list and the first item in the list is a property
         *   pointer (&canSee, &canHear, &canReach, &canSmell), this property will
         *   be used to test for tne appropriate sense connection between the actor
         *   and at least one of the other items in the list instead. [RULES EXTENSION]
         */
        // present = []
    ;

    #ifdef __DEBUG
    /* [RULES EXTENSION] include 'rules' in the list of debugging options. */
    modify DebugCtl
       /* list of all debugging options */
        all = inherited + 'rules'
    ;
    #endif

Adv3Lite Library Reference Manual
Generated on 15/03/2023 from adv3Lite version 1.6.1