#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 involviing 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 03/07/2024 from adv3Lite version 2.1