# factrel.t | documentation

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

    /*
     *   FACT RELATIONS EXTENSION by Eric Eve
     *
     *   Version 1.1 22nd April 2024
     *
     *   The factrel.t extension defines and uses relations involving Facts. It must be included after
     *   tha main library and the relations.t extension.
     */

    /*
     *   The concerning relation tracks relations between Facta and the topics they reference (via their
     *   topics property).
     */
    concerning: DerivedRelation 'concerns' 'referenced by' @manyToMany
         /* A Fact concerns all the topics listed in its topics property. */
        relatedTo(a)
        {
            if(gFact(a) == nil)
                return [];

            return gFact(a).topics;
        }

        /*
         *   List all the possible values of everything that can enter into the left-hand side of the
         *   relation "a concerns b", which is the list of all the Facts define in the game. Since we
         *   don't expect this to change during the course of the game we can calculate this once and
         *   then replace this method with the resulting list of
         Facts.*/
        listKeys()
        {
            /* Set up a new Vector for working storage. */
            local vec = new Vector();

            /* Iterate through every fact in the game. */
            for(local fact = firstObj(Fact); fact != nil; fact = nextObj(fact, Fact))
            {
                /* Add each fact to our vector. */
                vec.append(fact.name);
            }

            /* Convert the vector to a list. */
            local lst = vec.toList();

            /* Store the resulting list in this listKeys() property. */
            listKeys = lst;

            /* Then return the list. */
            return lst;
        }

        /* A topic a is inversely related to a fact b if it occurs in b's topics property. */
        isInverselyRelated(a, b)
        {
            if(gFact(b) == nil)
                return nil;

            return gFact(b).topics.find(a);
        }
    ;

    /* A Fact abuts another Fact if the two Facts share (concern) at least one topic in common. */
    abutting: DerivedRelation 'abuts' @manyToMany
        reciprocal = true

        /* Fact a abuts Fact b if their topics lists have any topics in common. */
        isRelated(a, b)
        {
            if(!gFact(a) || !gFact(b) )
                return nil;
            return gFact(a).topics.overlapsWith(gFact(b).topics);
        }

        /*
         *   The possible values that can appear on either side of this reciprocal relation a abuts b
         *   are all the Facts defined in the game.
         */
        listKeys()
        {
             /* Set up a new Vector for working storage. */
            local vec = new Vector();

            /* Iterate through every fact in the game. */
            for(local fact = firstObj(Fact); fact != nil; fact = nextObj(fact, Fact))
            {
                /* Add each fact to our vector. */
                vec.append(fact.name);
            }

            /* Convert the vector to a list. */
            local lst = vec.toList();

            /* Store the resulting list in this listKeys() property, since it shouldn't change. */
            listKeys = lst;

            /* Then return the list. */
            return lst;
        }
    ;

    /*
     *   contradicting is a reciprocal relationship between Facts that are defined to contratdict each
     *   other. The library cannot determine which these are, so only provides the bare framework here.
     *   Game code then needs to define which facts oontradict by listing them on the contradicting
     *   relation's relTab property, e.g.
     *.
     *.  relTab = [
     *.       'lisbon-capital' -> ['madrid-capital'],
     *.       'jumping-silly' -> ['jumping-healthy']
     *.   ]
     */
    contradicting: Relation 'contradicts'  @manyToMany +true
    ;

    /*
     *   BeliefRelation is the base class for a group of six relations that test whether Actors (and/or
     *   Consultables) assess the facts they know about in particular ways.
     */
    BeliefRelation: DerivedRelation
        /*
         *   Actor a is related to fact b if b is present as a key in a's informedNameTab with a value
         *   of status (true, likely, dubious, unlikely, or untruy). The a parameter is the Thing (Actor
         *   or Consultable) that potentially knows of the fact in question and b is the fact string-tag
         *   representing the fact.
         */
        isRelated(a, b)
        {
            return a.informedNameTab && a.informedNameTab[b] == status;
        }

        /* a is inversely  related to b if b is related to a. */
        isInverselyRelated(a, b)
        {
            return isRelated(b, a);
        }

        /*
         *   We can make this relation hold between obj{1] and obj{2] if we set obj{2} to status on
         *   obj{1].
         */
        addRelation(objs)
        {
            objs[1].setInformed(objs[2], status);
        }

        /*
         *   Obtain the list of fact tags related  to a, which is the subset of keys in a's
         *   informedNameTab for which a is related to be.
         */
        relatedTo(a)
        {
            return a.informedNameTab.keysToList().subset({b: isRelated(a, b)});
        }

        /* Obtain the list of Things that are inversely related to fact tag a */
        inverselyRelatedTo(a)
        {
            /* Set up a new vector for working storage of the list we're building. */
            local vec = new Vector();

            /* Interate through every Thing in the game. */
            for(obj = firstObj(Thing); obj != nil; obj = nextObj(obj, Thing))
            {
                /* If obj (the current Thing) is related to a, add it to our Vector. */
                if(isRelated(obj, a))
                    vec.append(obj);
            }

            /* Convert our vector to a list and return the result. */
            return vec.toList();
        }

        /*
         *   The belief status (true, likely, dubious, unlikely, or untrue) that we want this relation
         *   to test for.
         */
        status = nil
    ;

    /* Tests relation between Actors/Consultables and the facts they believe to be true. */
    believing:BeliefRelation 'believes' 'believed by' @manyToMany
       status = true
    ;

    /* Tests relation between Actors/Consultables and the facts they believe to be untrue. */
    disbelieving:BeliefRelation 'disbelieves' 'disbelieved by' @manyToMany
        status = untrue
    ;

    /* Tests relation between Actors/Consultables and the facts they believe to be likely. */
    consideringLikely: BeliefRelation 'considers likely' 'considered likely by' @manyToMany
        status = likely
    ;

    /* Tests relation between Actors/Consultables and the facts they believe to be dubious. */
    doubting: BeliefRelation  'doubts' 'doubted by' @manyToMany
        status = dubious
    ;

    /* Tests relation between Actors/Consultables and the facts they believe to be unlikely. */
    consideringUnlikely: BeliefRelation 'considers unlikely' 'considered unlikely by' @manyToMany
        status =  unlikely
    ;

    /*
     *   Tests relation between Actors/Consultables and the facts they believe to be uncertain, i.e.,
     *   likely, dubious or unlikely.
     */
    wondering: BeliefRelation 'wonders if' 'wondered about' @manyToMany
        /*
         *   Actor or Consultable is related to fact b through this relation if the value corresponding
         *   to key b on a's informedNameTab is either likely, dubious, or unlikely.
         */
        isRelated(a, b)
        {
            return a.informedNameTab && a.informedNameTab[b] is in (likely, dubious, unlikely);
        }

        addRelation(objs)
        {
            inherited DerivedRelation(objs);
        }
    ;

    /*
     *   A FactAgendaItem is a specialization of a ConvAgendaItem that seeks a path from the current
     *   state of an ongoing conversation towards a target Fact or topic the associated actor wishes to
     *   reach and then attempts to follow that conversational path whenever the NPC gets the chance to
     *   take the conversational initiative.
     */
    class FactAgendaItem: ConvAgendaItem
        /* The topic or fact our actor wishes to reach */
        target = nil

        /* Get the fact we want out path calculation to start from. */
        getStart()
        {
            local startFact = gLastFact;

            /* If we can't find a last mentioned fact, try starting from the last mentioned topic .*/
            if(startFact == nil)
            {
                /*
                 *   If there's no last mentioned topic, there's nothing more we can try, so return nil.
                 */
                if(gLastTopic == nil)
                    return nil;

                /* Obtained a list of facts that reference this topic. */
                local fList = related(gLastTopic, 'referenced by');

                /* If we succeed in obtaining such a list, return its first element. */
                if(fList && fList.length() > 0)
                    startFact = fList[1];
            }

            /* Return whatever start fact we found */
            return startFact;
        }

        /* Get a path from the last fact mentioned to our target, if we can. Return nil if we can't. */
        getPath()
        {
            /* Set up a local variable to cache our path. */
            local path = nil;

            /* Make our starting fact (the beginning of our path) the last fact mentioned. */
            local startFact = getStart();

            /*
             *   We can only go ahead with calculating a path if we have a starting fact to calculate it
             *   from.
             */
            if(startFact)
            {
                /*
                 *   If our target is an object (Topic or Thing used as a topic) we need to find the
                 *   appropriate fact to start from.
                 */
                if(dataType(target) == TypeObject)
                    path = getPathToTopic(startFact);

                /*
                 *   Otherwise, our target is a fact, and we simply set path to the optimum path from
                 *   our starting fact to our target fact.
                 */
                else
                    path = relationPath(startFact, relations, target);
            }

            /*
             *   Save a copy of our path, including any relations it ran through if we defined the
             *   relation property as a list.
             */
            fullPath = path;
            /* If our path contains a list of lists, transform it into a simple list of facts. */
            if(path && dataType(path[1]) == TypeList)
                path = path.mapAll({x: x[2]});

            /* Either way, return the path we've just found. */
            return path;
        }

        /* Get the path from a topic to our target */
        getPathToTopic(startFact)
        {
            /* First obtain a list of all the facts that reference this topic. */
            local facts = related(target, 'referenced by');

            /*
             *   We want to select the fact that gives us the shortest path, so we start with a very
             *   long maximum path length to be sure that any path we find will be shorter than this.
             */
            local maxLen = 100000;

            /* Set uo a local variable for the path we're looking for. */
            local path = nil;

            /*
             *   Iterate through all the related facts we found to identify the one that gives us the
             *   shortest path to our target
             .*/
            foreach(local fact in facts)
            {
                /*
                 *   Find the optimum path from the last fact mentioned to this potential target fact
                 *.
                 */
                local newPath = relationPath(startFact, relations, fact);

                /*
                 *   If we've found a path and it's shorter than any previous path, make it our
                 *   provisionally chosen path and reduce the maximum path length to its path length.
                 */
                if(newPath && newPath.length < maxLen)
                {
                    maxLen = newPath.length;

                    path = newPath;
                }
            }

            /* Return whatever path we found. */
            return path;
        }

        /* The current conversational path we're pursuing. */
        curPath = nil

        /* The full path, including the relationships used. */
        fullPath = nil

        /*
         *   The relation or a list of relations we want to use for proximity of facts when calculating
         *   our path.
         */
        relations = abutting

        /*
         *   The next step along our current path. If we have a path of at least two elemeents, the next
         *   step is the second element, otherwise we don't have a next step.
         */
    //    nextStep = ((curPath && curPath.length > 1) ? curPath[2] : nil)

        /*
         *   If we have been called by a DefaultAgendaTopic, it will be neater if what we display is
         *   related to the topic the DefaultAgendaTopic just matched, so we attempt to find a nextStep
         *   that meets this condition.
         */
        nextStep()
        {
            /* We need only try to do this if we've just been called by a DefaultAgendaTopic */
            if(calledBy && calledBy.ofKind(DefaultAgendaTopic) && curPath)
            {
                /*
                 *   Try to find the latest step (fact name) in our current path that relates to the
                 *   topic just matched by our caller.
                 */
                local step = curPath.lastValWhich({x: gFact(x).topics.indexOf(calledBy.topicMatched)});

                /*  If we found one, return it. */
                if(step)
                    return step;
            }

            /* Otherwise, return the next step along our path. */
            return ((curPath && curPath.length > 1) ? curPath[2] : nil);
        }

        /*
         *   Are we ready to execute? By default we are if our inherited conditions are matched and
         *   there's a next step we can take, unless we don't have a target defined, in which case just
         *   use the inherited handling.
         */
        isReady()
        {
            /*
             *   If we have a target defined, store it in our curPath property (so we won't need to
             *   calculate it again this turn.
             */
            if(target)
                curPath = getPath();

            /* otherwise return our inherited value. */
            else
                return inherited();

            /* We're ready is we meet the inherited conditions and we have an available next step. */
            return inherited() && nextStep != nil;

        }

        /* The object that called us */
        calledBy = nil

        invokeItemBase(caller)
        {
            /* Store a reference to our called */
            calledBy = caller;

            /* Carry out the inherited handling. */
            inherited(caller);
        }

        /*
         *   We provide a default topicResponse() that triggers an appropriate InitiateTopic for the
         *   next step along our fact path, and marks this FactAgendaItem as done once we've reached the
         *   final step. Game code is free to override if you want to handle this actor's goal-seeking
         *   conversational agenda in some other way.
         */

        /*
         *   By defaut we trigger the InitiateTopic corresponding to our next step, but game code can
         *   ovverride to do somethng different here is required.
         */
        invokeItem()
        {
            getActor.initiateTopic(nextStep);
        }

        /*
         *   Our endCondition is the state we must reach for us to have reached our goal, so that we can
         *   set our isDone to true. By default this is when we've reached the end of our path, which is
         *   when our next step would be the one at the end of our path. Game could override this, say,
         *   to either gRevaled(target) or gInformed(target) or some similar condition.
         */
        endCondition = (curPath && curPath[curPath.length] == nextStep)

        /*
         *   Reset this FactAgendaItem so that it can be used again. If the optional target_ parameter
         *   is supplied, we'll set the our target to the new target_.
         */
        reset(target_?)
        {
            /*
             *   Provided that isDone is simply true (rather than a method or expression that might
             *   evaluate to true) reset it to nil.
             */
            if(propType(&isDone) == TypeTrue)
                isDone = nil;

            /* If the target_ parament has been supplied, set our target property to target_ .*/
            if(target_)
                target = target_;
        }
    ;

    /*
     *   Modification in the FACT RELATIONS Extension to allow contradictions between listed Facts to be
     *   noted. We may no attempt to say precisely where any contradictions occur, since that seems best
     *   left to the player to spot.
     */
    modify FactHelper
        /*
         *   Add a message noting contradictions (as defined by the contradicting Relation) between any
         *   two facts just listed in our response.
         */
        topicResponse()
        {
            /* Carry out the inherited handling. */
            inherited();

            /*
             *   If the contradicting relation has no conflicting facts defined or we don't want to note
             *   (report on) contradictions there's nothing for us to do.
             */
            if(contradicting.relTab == nil || noteContradictions == nil)
                return;

            /* Set up a local variable to keep track of the number of contraditions. */
            local contradictionCount = 0;

            /*
             *   Iterate through every fact in our tagList, which will be every Fact we've just
             *   displayed.
             */
            foreach(local fact in tagList)
            {
                /*
                 *   If any facts contradicting the current fact occur in our tagList, incremenent our
                 *   contradiction count.
                 */
                if(valToList(related(fact, contradicting)).overlapsWith(tagList))
                    contradictionCount ++;

                /*
                 *   If we've found more than one contradiction we can stop looking (it's possible that
                 *   we might want to know whether there was one or several).
                 */
                if(contradictionCount > 1)
                    break;
            }

            /* If we found any contradications, display a message to that effect. */
            if(contradictionCount > 0)
                DMsg(contradiction, '<.p>There would seem to be some contradiction {here}.<.p>');

        }

        /*
         *   Do we actually want to note (i.e. report on) the presence of contradictions here? By
         *   default we do but game code can override.
         */
        noteContradictions = true
    ;

    modify ActorTopicEntry

        /*
         *   If we like, we can specify a particuler FactAgendaItem to use in conjunction with this
         *   ActorTopicEntry. But note that if autoUseAgenda is true the AgendaItem we specify here will
         *   be ignored unless useAgenda() can't find a suitable AgendaItem.
         */
        agenda = nil

        /*
         *   Do we want the ActorTopicEntry to find an appropriate FactAgendaItem for us? By default we
         *   don't. This should only be set to true on ActorTopicEntries that are going to make use of
         *   FactAgendaItem path information.
         */
        autoUseAgenda = nil

        /* Look for an appropriate FactAgendaItem to use with this TopicEntry. */
        useAgenda()
        {
            /* Don't do anything here if we don't want to reference an associated FactAgendaItem */
            if(agenda == nil && autoUseAgenda == nil)
                return nil;

            /* Setup the starting position for our agenda's path calculation. */
            if(objOfKind(topicMatched, Topic) || objOfKind(topicMatched, Thing))
                libGlobal.lastTopicMentioned = topicMatched;

            local formerFact = libGlobal.lastFactMentioned;

            try
            {
                /*
                 *   Ensure it uses the latest topic rather than any legacy fact matched, unless the
                 *   last fact matched is referenced by the topic we just matched, in which case we may
                 *   as well retain it.
                 */
                if(related(gLastFact, 'referenced by').indexOf(topicMatched) == nil)
                    libGlobal.lastFactMentioned = nil;

                /*
                 *   If we don't want to find a suitable AgendaItem automatically, use the one defined
                 *   on our agenda property.
                 */
                if(!autoUseAgenda)
                    return agenda;

                /*
                 *   Obtain a ready FactAgendaItem from our actor's agendaList (in all likelihood there
                 *   won't be more than one at any one time).
                 */
                local ag = getActor().agendaList.valWhich({x: x.ofKind(FactAgendaItem) && x.isReady});

                /*
                 *   If we found a suitable FactAgendaItem, return it, otherwise return the AgendaItems
                 *   specified on our agenda property.
                 */
                return ag ?? agenda;
            }
            finally
            {
                /* Restore the previous last fact mentioned. */
                libGlobal.lastFactMentioned = formerFact;
            }

        }

        baseHandleTopic()
        {
            /*
             *   Set up a variable to hold the AgendaItem we want to work with and initialize it to the
             *   value of our agenda property, so we cab restore it to its original value when we're
             *   done.
             */
            local agenda_ = agenda;

            try
            {
                if(agenda || autoUseAgenda)
                {
                    /* Obtain the AgendaItem we wish to use with this ActorTopicEntry */
                    local ag = useAgenda();

                    /* Check that ag is a FactAgendaItem before trying to work with it. */
                    if(objOfKind(ag, FactAgendaItem))
                    {
                        /*
                         *   If ag can provide a path from our topic to its target, set our own next
                         *   step and agendaPath to those of ag.
                         */
                        agendaPath = ag.getPath();

                        /* Cache our agenda item's nextSteo in our own nextStept property. */
                        nextStep = ag.nextStep;

                        /* Store the agenda we've found in our agenda property. */
                        agenda = ag;
                    }
                    else
                    {
                        /* Otherwise reset our nextStep and agendaPath to nil */
                        nextStep = nil;

                        /* Reset our agendaPath property to nil. */
                        agendaPath = nil;
                    }
                }

                /* Carry out the inherited handling. */
                inherited();
            }
            finally
            {
                /* Restore the original value of our agenda property. */
                agenda = agenda_;
            }
        }

        /*
         *   The next step (next fact) our associated FactAgendaItem would like us to use. We can use
         *   this to tailor our topicResponse as we see fit, which could include responding with our
         *   nextStep's description, or could be something more subtle.
         */
        nextStep = nil

        /*
         *   The path out associated FactAgendaItem wants to take. This will be a list of fact name
         *   tags.
         */
        agendaPath = nil

        /*
         *   A method our topicResponse property can call to try executing the next step of our
         *   associated agenda item to provide a response. This returns true if there is a nextStep to
         *   execute or nil otherwise so our topicResponse can test whether tryNextStep() has provided a
         *   response.
         */
        tryNextStep()
        {
            /*
             *   If we have a next step, call our getActor's initiateTopic with nextStep as its argument
             *   abd see if it provides a response.
             */
            if(nextStep)
            {
                /*
                 *   If calling initiateTopic(nextStep) on our actor provides a response, return true to
                 *   tell our caller that we've done so.
                 */
                if(getActor.initiateTopic(nextStep))
                    return true;
            }
            /* Otherwise we haven't provided a response, so return nil to tell our caller we haven't. */
            return nil;
        }

        /*
         *   tryAgenda is an alternative method our topicResponse can call; it simply executes our
         *   associated AgendaItem and returns true or nil to our caller so it can tell whether this
         *   provided a response.
         */
        tryAgenda()
        {
            return getActor.executeAgenda();
        }
    ;

    /* Modifications to AltTopic to work with FACT RELATIONS modifications to ActorTopicEntry */
    modify AltTopic
        agenda = location.agenda
        autoUseAgenda = location.autoUseAgenda
    ;

    /* Modification to Actor to work better with Fact Relations extension */
    modify Actor
        /*
         *   We need to update gLastTopic so that any FactAgendaItem can reference it when checking its
         *   isReady property before any TopicEntry has handled the topic.
         */
        handleTopic(prop, topic, defaultProp = &noResponseMsg)
        {
            /* Store a referece to topic, which will be a list at this stage */
            local top = topic;

            /* topic has probably been passed as a list. */
            if(dataType(topic) == TypeList)
            {
                /* If so prefer an element of the list that hasn't just been created by the parser. */
                top = topic.valWhich({t:!t.newlyCreated});

                /* If we found one, choose it, otherwwise select the first element in the list. */
                top = top ?? topic[1];
            }

            /* Updte gLastTopic */
            gLastTopic = top;

            /* Then carry out the inherited handling and return the result. */
            return inherited(prop, topic, defaultProp);
        }
    ;

    modify Thing
        setInformed(tag, val?)
        {
            /* Carry out the inherited handling */
            inherited(tag, val);

            /*
             *   Then check for contradictions betwen the new piece of information (tag) and information
             *   we already know about.
             */
            checkForContradictions(tag, val);
        }

        /*
         *   Check for contradictions betwen the new piece of information (tag) and information we
         *   already know about.
         */
        checkForContradictions(tag, val)
        {
            /* Get the fact corresponding to tag. */
            local fact = gFact(tag);

            /* If we don't find a fact or our informedTab is empty, there's nothing left to do. */
            if(fact == nil || informedNameTab == nil)
                return;

            /* Obtain a list of all the facts we've been informed of. */
            local factList = informedNameTab.keysToList().subset({x: informedNameTab[x] != nil});

            /*
             *   Reduce this to the list of facts that contradict tag (the new fact name we've just been
             *   informed of.
             */
            factList = factList.subset({x: related(tag, contradicting, x) });

            /* If we found any, mark tag as dubious and call our notifyContrediction method. */
            if(factList.length > 0 && val == nil)
            {
                /* Mark this item of information as dubious */
    //            informedNameTab[tag] = dubious;

                /*
                 *   For new we don't call this as it doesn't produce reliable results --- there's more
                 *   than one way of resolving a contradiction and it may be this needs to be handled on
                 *   a case-by-case basis in game code rather than in a generalized library routine.
                 */
    //            markContradiction(tag, factList);

                /* Call our notifyContradiction method. */
                notifyContradiction(tag, factList);
            }
        }
        /*
         *   Mark the incoming 'fact' denoted by tag as either untrue, unlikely, or dubious, depending
         *   on what it contradicts. For now this isn't called as it seems capable of producing perverse
         *   results --- in particular it doesn't allow for the fact that the revealing of new
         *   information that contradicts existing information could result in changes to what is
         *   believed about either.
         */
        markContradiction(tag, factList)
        {
            /*
             *   If tag contradicts a fact we believe to be true, we presumably believe tag to be untrue
             */
            if(factList.indexWhich({x: informedNameTab[x] == true}))
                informedNameTab[tag] = untrue;
            else
            {
                /*
                 *   Otherwise if tag contradicts a fact we believe to be likely, we presumably believe
                 *   tag to be unlikely.
                 */
                if(factList.indexWhich({x: informedNameTab[x] == likely}))
                    informedNameTab[tag] = unlikely;
                else
                    /*
                     *   Otherwise we consider tag to be dubious (that tag contradcts a fact we regard
                     *   as either dubious, unlikely or untrue says little about how we regard tag - two
                     *   muutally contradictory facts could easily both be untrue, unlikely, or dubious.
                     */
                    informedNameTab[tag] = dubious;
            }
        }

        /*
         *   Receive a notification that we've just been informed of a Fact that contradicts another
         *   fact we already know or have been informed of. We don't do anything here by default; it's
         *   up to game code to impelement any response required.
         */
        notifyContradiction(fact, factList)
        {
        }
    ;

Adv3Lite Library Reference Manual
Generated on 08/12/2025 from adv3Lite version 2.2.2