Fact Relations
Overview
The purpose of the Fact Relations extension is to define a number of potentially useful relations involving Facts and to employ them for various tasks, such as noting when facts are contradictory, defining a new FactAgendaClass that can be used to help NPCs steer a conversation towarda a desired goal. To understand how Fact Relations extension works you may find it helpful to refer to the documenation on the Relations extension, on which it builds.
New Relations, Classes and Modifications
This extension defines the following new relations, classes and features:
- concerning: relation between Facts and the topic they concern.
- abutting: reciprocal relation between Facts that share at least one topic in common.
- contradicting: reciprocal relation between Facts that contradict each other.
- Believing Relations: a set of relations between Actors (and or Consultables) and the facts they know about, according to their belief or otherwise in those facts.
- FactAgendaItem: is a kind of ConvAgendaItem that can be used to get an NPC to steer the conversation towards a desired goal.
Usage
Include the factrel.t file after the library files but before your game source files. To use the Fact Relations extension the Facts, Actor and TopicEntry module must also be included in your game along with the Relations extension.
The Concerning Relation
The concerning relation is a many-to-many DerivedRelation that relates Facts to the topics (i.e. Topics or Things) they concern (i.e., which are listed in their topics property). Being a DerivedRelation means that the extension works out what it is related to what by itself, without you having to do any further work. The definition of the concerning relation begins:
concerning: DerivedRelation 'concerns' 'referenced by' @manyToMany
/* A Fact concerns all the topics listed in its topics property. */
relatedTo(a)
{
return gFact(a).topics;
}
...
Using the standard Relation functions then allows you to test for the relatedness of facts to topics in the following ways:
related('rain-in-spain', concerning, tSpain) // tests whether the rain-in-spain fact concerns tSpain. related('rain-in-spain', 'concerns', tSpain) // tests whether the rain-in-spain fact concerns tSpain. related(tSpain, 'referenced by', 'rain-in-spain') // tests whether the tSpain topic is referenced by 'rain-in-spain'. related(tSpain, 'referenced by') // returns a list of all the facts that concern tSpain. related('rain-in-spain', concerning) //returns a list of all the topics that 'rain-in-spain' concerns. related('rain-in-spain', 'concerns') //returns a list of all the topics that 'rain-in-spain' concerns.
Note that since concerning is a DerivedRelation it makes no sense to use the relate() function to set or unset the concerning relation.
The Abutting Relation
Abutting is a reciprocal DerivedRelation between Facts that share at least one topic in common. Its definition begins:
abutting: DerivedRelation 'abuts' @manyToMany
reciprocal = true
/* Fact a abuts Fact b if their topics lists have any topics in common. */
isRelated(a, b)
{
return gFact(a).topics.overlapsWith(gFact(b).topics);
}
...
There is no obvious concise word meaning "to concern one or more topics in common"; the word "abut" means (among other things) "to share a common boundary with", which seems close enough for the purpose, concisely conveying the idea that Facts that abut each other are in some sense neighbouring, related by virtue of having something in common.
As we shall see below the FactAgendaItem employs the abutting relation to calculate a path from the current state of the conversation in progress to a goal (fact or topic) that the associated NPC wants to reach.
As with the concerning relation, it makes no sense to attempt to use the relate() function to set or unset the abutting relation between facts, although you can, of course us the related() function to test for the existence of the abutting relation between facts, e.g.:
related('rain-in-spain', abutting, 'madrid-capital') // tests whether the 'rain-in-spain' fact abuts the 'madrid-capital' one. related('rain-in-spain', 'abuts', 'madrid-capital') // tests whether the 'rain-in-spain' fact abuts the 'madrid-capital' one. related('rain-in-spain', abutting) //returns a list of all the facts that abut 'rain-in-spain'. related('rain-in-spain', 'abuts') //returns a list of all the facts that abut 'rain-in-spain'.
The Contradicting Relation
The Contradicting Relation is a reciprocal relation between Facts that contradict each other. It is defined as:
contradicting: Relation 'contradicts' @manyToMany +true ;
The library has no way of knowing which facts contradict each other, so this is up to you, the game author, to define. Suppose our game has defined:
Fact 'jumping-silly' [tJumping] 'jumping is silly' [bob] ; Fact 'jumping-healthy' [tJumping] 'jumping is good for your health' [bob] ; Fact 'madrid-capital' [tSpain, tMadrid] 'the capital city of Spain is Madrid' [book] qualifiedDesc(source, topic) { if(topic == tMadrid) return 'Madrid is the capital city of Spain'; else return inherited(source, topic); } listOrder = 10 ; Fact 'lisbon-capital' [tSpain, tLisbon] 'the capital city of Spain is Lisbon' ;
We might reasonably reckon that 'jumoing-silly' and 'jumping-healthy' contradict each other, and it's certainly the case that 'madrid-capital' and 'lisbon-capital' are mutually contradictory. We could establish this in our game by using the relate() function at game start up or preninit:
relate('jumping-silly', contradicting, 'jumping-healthy'); relate('madrid-capital', 'contradicts', 'lisbon-capital');
And since the relationahip is a reciprocal one, we don't need to set the relationships the other way round.
Alternatively, we may find it more convenient (especially if we have a lot of contradicting relations to set up) to modify the contradicting relation in our game to list the contradictory facts on its relTable property:
modify contradicting relTab = [ 'lisbon-capital' -> ['madrid-capital'], 'jumping-silly' -> ['jumping-healthy'] ] ;
Either way, we can then test for the existence of a contradiction between two Facts by ussing the related() function in this usual way, for example:
related('rain-in-spain', contradicting, 'madrid-capital') // tests whether'rain-in-spain' contradicts 'madrid-capital'. related('lisbon-capital', 'contradicts', 'madrid-capital') // tests whether'lisbon-capital' contradicts 'madrid-capital'. related('jumping-silly', contradicting) //returns a list of all the facts that contradict 'jumping-silly'. related('madrid-capital, 'contradicts') //returns a list of all the facts that contradict 'madrid-capital'.
The Fact Relations extensions can use this information to tell players when they have been presented with contradictory information in response to a THINK ABOUT or LOOK UP command. For example, suppose we had so arranged things that Bob expresses the view that jumping is healthy when asked about jumping but says that it's silly if he sees the player character doing it. We might then get something like:
Lounge The lounge is large and luxurious. The only way out is to the west. Bob is here. >ask bob about jumping “Hello, Bob,” you say. “Hello, you,” he replies. “Jumping is good for your health,” Bob tells you. >think about jumping You recall that Bob told you that jumping is good for your health. >jump You jump on the spot, fruitlessly. “Jumping is silly,” says Bob. >think about jumping You recall that: Bob told you that jumping is silly. Bob told you that jumping is good for your health. There would seem to be some contradiction here.
We deliberately refrain from pointing out where the contradiction occurs, even in a much longer listing of facts, since to do so would risk insulting the player's intelligence, quite apart from it being arguably up to players to spot where contradictions occur. The reasons for pointing out the existence of the contradiction are rather (1) to reassure players that it is deliberate and not simply a blunder on the part of the game author and (2) to avoid the sense of oddness that might result from a THINK ABOUT command resulting in a list of facts containing contradictions without reflecting the likelihood that the process of thinking would likely result in the contradiction being noticed.
Game code can also use the contradicting relation to allow NPCs to react in some way to being fed contradictory information. With the Fact Relations extension included in the game, the setInformed(fact) method (called when an actor is informed of something) checks
bob: Actor 'Bob;;;him' @lounge notifyContradiction(fact, factList) { new Fuse(self, &rubbish, 0); rubbishMsg = gFactDesc(factList[1]); } rubbish() { "<q>Rubbish!</q> cries Bob. <q>Everyone knows that <<rubbishMsg>>.</q><.p>"; } rubbishMsg = nil ; + TellTopic @tLisbon "<q>As everyone knows, <<infTag>>,</q> you say. " tTag = 'lisbon-capital' name = 'Lisbon' ;
This could then lead to the following exchange:
Lounge The lounge is large and luxurious. The only way out is to the west. Bob is here. >tell bob about lisbon “As everyone knows, the capital city of Spain is Lisbon,” you say. “Rubbish!” cries Bob. “Everyone knows that the capital city of Spain is Madrid.”
Note how in this example we use a Fuse to delay displaying Bob's reaction to the end of the turn. If we had simply displayed Bob's rubbish method directly from our notifyContradiction() method it would have appeared in the middle of what the Player Character was saying.
We could do something rather more sophisticated that this if we wished, including, but not limited, having the notifyContradiction() method call an equivalent method on bob's current ActorState, or some other object of our own devising to avoid the need to define too much complex code on bob.
The extension also defines a markContradiction() method that can rate the incoming contradictory fact as untrue, unlikely, or dubious according to the following rules:
- If the incoming fact contradicts a fact that the actor currently takes to be true, then the incoming fact is rated untrue.
- Otherwise, if the incoming fact contradicts a fact that the actor currently takes to be likely, then the incoming fact is rated unlikely.
- Otherwise, the incoming fact is rated dubious.
In an earlier version of the libary this was called from the checkForContradictions() method so that the belief value of incoming facts would automatically be updated. This is no longer the case, since experimentation revealed it could produce perverse results. There's more than one way of resolving a contradiction and which applies in any given case probably needs to be dealt with in game code rather than library code. Although the library no longer calls it, the markContradiction() is still in the library, however, in case game code might wish to make use of it.
Your notifyContradiction() method is, of course, free to override these ratings as you see fit. Note, however, that the incoming fact contradicting an already known fact already considered to be untrue, unlikely or dubious does not of itself tell us anything about the likely truth value of the incoming fact, which is why the extension defaults to it being dubious in such cases. (Formally, A contradicts B means !(A && B) which is equivalent to !A || !B; so if A the incoming fact contradicts a fact B that's known to be untrue, !B will be true, so !A || !B will be true regardless of the truth value of A, for example if B is 'The moon is a cube made entirely of blue stilton' it would be contradicted by 'The moon is a torus made entirely of ice cream' without either statement being true).
Finally, note that notifyContradiction() should also work on the Player Character.
Believing Relations
The Fact Relations extension defines a set of six relations that can test (and in some cases set) whether an actor (or Consultable) regards a fact they know about as true, likely, dubious, unlikely, or untrue. The first five are quite straightforward:
believing:BeliefRelation 'believes' 'believed by' @manyToMany status = true ; consideringLikely: BeliefRelation 'considers likely' 'considered likely by' @manyToMany status = likely ; doubting: BeliefRelation 'doubts' 'doubted by' @manyToMany status = dubious ; consideringUnlikely: BeliefRelation 'considers unlikely' 'considered unlikely by' @manyToMany status = unlikely ; disbelieving:BeliefRelation 'disbelieves' 'disbelieved by' @manyToMany status = untrue ;
These enable you to query what various actor (including the player character) believe, doubt, disbelieve, etc. with function calls such as related(bob, disbelieving, 'lisbon-capital'), related('lisbon-capital', 'believed by', me) and related(bob, 'considers likely'). You can also set any of these relations using, for example, relate(bob, 'doubts', 'rain-in-spain) or relate(me, 'considers likely', 'rain-tomorrow'). You cannot, however, use unrelate() with any of these relations, since it would make no sense; you instead need to use relate() with a different relation; if Bob no longer doubts 'rain-in-spain' does he believe it, think it likely, think it unlikely, or consider it untrue? If you really want Bob to forget all about hearing that the rain in Spain stays mainly in the plain, you'd need to use the statement bob.forget('rain-in-Spain'};.
Finally, there is relation to cover all three degrees of uncertainty, likely, dubious, unlikely:
wondering: BeliefRelation 'wonders if' 'wondered about' @manyToMany isRelated(a, b) { return a.informedNameTab && a.informedNameTab[b] is in (likely, dubious, unlikely); } ... ;
This can be used in just the same way as the other five, except that you cannot use the relate() function with it (for obvious reasons: what value would we be trying to set?).
Note that none of these six relations requires the corresponding Fact objects to be defined, since all that is being tested (or set) is the presence and corresponding values of fact-tag keys in informedNameTab LookupTables.
FactAgendaItem
A FactAgendaItem is a kind of is a kind of ConvAgendaItem that can be used to get an NPC to steer the conversation towards a desired goal, which can either be a topic of conversation or a particular fact. If we're content to use the default behaviour of FactAgendaItem, all we need to specify is its target property, to contain the goal (fact or topic) we're trying to reach, for example:
bob: Actor 'Bob;;;him' @lounge actorAfterAction() { if(gActionIs(Jump)) initiateTopic('jumping-silly'); } ; + bobAgenda: FactAgendaItem target = 'mavis-proposal' endCondition = gRevealed(target) ;
We may also want to add a DefaultAgendaItem so that Bob can nudge the conversation towards his desired goal whenever he's asked or told about a topic he hasn't got a relevant TopicEntry for:
+ DefaultAgendaTopic "Hm,says Bob. " ;
To ensure bobAgenda starts out active at the beginning of the game, we want to ensure that it's added both to Bob's agenda list and that of our DefaultAgendaTopic at game startup, perhaps by using gameMain's showIntro() method:
gameMain: GameMainDef initialPlayerChar = me showIntro() { bob.addToAllAgendas(bobAgenda); "You're in your own house. Your friend Bob is visiting and waiting for you in the lounge, so you could go and chat to him, or you could explore a bit.<.p>"; } ;
Then on avery turn on which has the chance to seize the conversational initiative and there is a path available from the last mentioned fact or topic to the target fact or topic, the FactAgendaItem will call Bob's initiateTopic(nextStep) method, where nextStep is the next fact in the FactAgendaItem's curPath list, curPath being the list of facts leading from the starting position to the target via the abutting relation.
This is best illustrated by continuing our example. Suppose Bob wants to ask the Player Character to be his best man at his wedding to Mavis, to whom he intends to propose on her return from a trip to Madrid. The first thing we need to do is define the relevant Facts, which may include:
Fact 'rain-in-spain' [tWeather, tSpain] 'the rain in Spain stays mainly in the plain' [me, bob] pcComment = '--- or so the song goes' priority = 110 ; Fact 'rain-tomorrow' [tWeather] 'it will rain tomorrow' [bob] qualifiedDesc(source, topic) { if(source == bob) return 'it\'ll rain tomorrow'; else return inherited(source, topic); } ; Fact 'madrid-capital' [tSpain, tMadrid] 'the capital city of Spain is Madrid' [book] qualifiedDesc(source, topic) { if(topic == tMadrid) return 'Madrid is the capital city of Spain'; else return inherited(source, topic); } listOrder = 10 ; Fact 'mavis-in-madrid' [tMadrid, mavis] 'his girlfriend Mavis is in Madrid right now' [bob] qualifiedDesc(source, topic) { if(source== bob) return 'my girlfriend Mavis is in Madrid right now'; else return inherited(source, topic); } listOrder = 20 ; Fact 'mavis-proposal' [mavis, bob] 'he intends to propose to Mavis on her return' [bob] qualifiedDesc(source, topic) { if(source== bob) return 'I\'m planning to propose to Mavis when she gets back'; else return inherited(source, topic); } listOrder = 30 ;
Then we need to define the associated InitiateTopics:
+InitiateTopic 'rain-in-spain' "<q><<if defaultInvocation>>But since<<else>>While<<end>> we're talking about the weather, I can't help thinking of that song that goes, <q><<revTag()>></q>,</q> Bob remarks. " ; +InitiateTopic 'madrid-capital' "<q><<if defaultInvocation>>Hang on a mo! While we're on the subject of Spain<<else>>As you know<<end>>, <<revTag()>>,</q> says Bob. " ; + InitiateTopic 'mavis-in-madrid' "<q>As it so happens, <<revTag>>,</q> Bob remarks. " ; + InitiateTopic 'mavis-proposal' "<q>The thing is, <<revTag>>,</q> he tells you. <.convnodet wedding>" ;
Together with some further TopicEntries to round out the conversation:
+ HelloTopic
"<q>Hello, Bob,</q> you say.\b
<q>Hello, you,</q> he replies. "
;
+ ByeTopic
"<q>Cheerio,</q> you say.\b
<q>Bye,</q> he replies. "
;
+ AskTellTalkTopic, StopEventList
[
/* We wrap this first response in an anoynmous function to avoid revTag() being triggered on subsequent invocations of this topic. */
{: "<q>\^<<revTag()>></q> Bob warns you. "},
'Bob has already told you <<fText()>>. '
]
rTag = 'rain-tomorrow'
;
+ DefaultAnyTopic
"Bob frowns, as if he's eager to move the conversation on to something else. "
;
+ ConvNode 'wedding'
;
++ SayTopic 'congratulations'
"<q>Congratulations!</q> you declare. <q>That's great!</q>\b
<q>Thank you!</q> Bob beams. <q>So will you be my best man?</q> <.convnodet best-man>"
;
++ QueryTopic 'when the wedding will be; (is)'
"<q>That'a great!</q> you declare. <q>When will the wedding be?</q>\b
<q>I'm hpoing for August</q> he tells you. <q>I hope you'll be free then,
because I'd like you to be my best man. Will you?</q> <.convnodet best-man>"
;
++ SayTopic 'wow'
"<q>Wow! I wasn't especting that!</q> you declare.\b
<q>Really?</q> Bob replies. <q>I suppose we have been quite discreet. So thing thing is,
I was hoping, well, would you be my best man?</q> <.convnodet best-man>"
;
++ DefaultAnyTopic
"That hardly seems appropriate right now. <.convstayt> "
isConveraational = nil
;
++ NodeContinuationTopic
"Bob starea at you, eagerly awaiting your response. "
;
+ ConvNode 'best-man';
++ YesTopic
"<q>Yes, of course!</q>\b
Bob beams. <q>That's splendid! Thank you!</q>"
;
++ NoTopic
"<q>No, certainly not!</q>\b
Bob looks crestfallen. <q>Why ever not?</q>"
;
We'll take it as read that suitable Topic objects (such as tWeather and tMadrid) have been defined, along with a suitable mavis object (preferably defined to be familiar). We might then get the following exchange:
Lounge The lounge is large and luxurious. The only way out is to the west. Bob is here. >ask bob about weather “Hello, Bob,” you say. “Hello, you,” he replies. “It’ll rain tomorrow” Bob warns you. >a tomorrow “But since we’re talking about the weather, I can’t help thinking of that song that goes, ‘the rain in Spain stays mainly in the plain’,” Bob remarks. >a plain “Hang on a mo! While we’re on the subject of Spain, the capital city of Spain is Madrid,” says Bob. >a madrid “As it so happens, my girlfriend Mavis is in Madrid right now,” Bob remarks. >a life “The thing is, I’m planning to propose to Mavis when she gets back,” he tells you. (You could say wow or say congratulations; or ask him when the wedding will be) >wow “Wow! I wasn’t especting that!” you declare. “Really?” Bob replies. “I suppose we have been quite discreet. So thing thing is, I was hoping, well, would you be my best man?” (You could say yes; or say no) >yes “Yes, of course!” Bob beams. “That’s splendid! Thank you!”
But without any further work on our part, the conversation could also have gone:
>talk to bob “Hello, Bob,” you say. “Hello, you,” he replies. >a madrid “As it so happens, my girlfriend Mavis is in Madrid right now,” Bob remarks. >a mavis “The thing is, I’m planning to propose to Mavis when she gets back,” he tells you. (You could say wow or say congratulations; or ask him when the wedding will be) ...
There are various properties and methods of FactAgendaItem we can use to customize this behaviour; in roughly descending of usefulness (from the game authors' perspective) these are:
- invokeItem() By default, this calls getActor.initiateTopic(nextStep), but we could override it to handle the next step in any other way we liked.
- endCondition: The condition that becomes true when we've reached our goal. By default this is when we've reached the last step in our path to our target, but we could override it to some other condition, such as gRevealed(target) or gInformed(target).
- reset(target_?):Reset this FactAgendaItem so that it can be used again. If the optional target_ parameter is supplied, we'll set our target to the new target_.
- relations: This specifies the relation, or a list of relations, between Facts to be used when calculating the shortest path from our starting position to our target. By default this is simply abutting, but if our game defined another relation, or other relations, we thought might do a better job for this purpose, we could override relations to use it/them instead of, or as well as, abutting.
- getStart(): This is the method that works out the Fact to start from when calculating the path to our target. By default we use gLastFact or, failing that, a fact referred to by gLastTopic, but it is conceivable that your game might benefit from reckoning the starting fact in some other way.
Note that the reset() method could allow us to use a single FactAgendaItem per actor to cater for all that actor's fact-based conversational agendas. It could also be used to change the actor's conversational target in mid-conversation, before the original goal had been reached.
Note also the comment in the example above about wrapping the first response to asking about the weather in an anonymous function. If we had simply used a single-quoted string here ('<q>\^<<revTag()>></q> Bob warns you. ') it would have displayed what we wanted but there would have been a subtle unwanted side-effect. Because of the way TADS 3 handles lists, every single-quoted string in an EventList is evaluated every time the EventList is referenced, regardless of which element of the EventList we actually want. So had we used a single-quoted string, revTag() would have been called again even on the second or subsequent invocation of this TopicEntry. Because revTag() not only displays the desc of the associated fact but reveals the associated fact tag, this would cause libGlobal.lastFactMentioned to revert to 'rain-tomorrow', which in turn would set bobAgenda's starting fact to 'rain-tomorrow', with the result that Bob would repeat his remark about the rain in Spain, which is precisely what we were trying to avoid by calling fText() rather than revTag() second time around.
Using Topic Entries with FactAgendaItems
Finally, the Fact Relation extension adds a couple of features to ActorTopicEntry to work with FactAgendaItem in case the player enters a conversational command concerning a topic that is or maybe related to a Fact on the conversational path a FactAgendaItem is trying to follow. In that case we might want the FactAgendaItem to provide the response, rather than have our TopicEntry provide one that may not be so appropriate in context.
Suppose, for example, we had defined the following AskTopic:
++ AskTopic @mavis "How is Mavis?you ask. \b <She's fine,Bob tells you." ;
This might be a perfectly sensible response to give once Bob has revealed his plan to propose to Mavis, and we may feel that some such response ought to be provided, but since Bob is meant to be bursting to tell our player character about his planned proposal, one might expect him to cut straight to the chase. We could, of course, try to rectify the situation by adding a suitable isActive property to our AskTopic:
++ AskTopic @mavis "<q>How is Mavis?</q> you ask. \b <<q>She's fine,</q> Bob tells you." isActive = !gRevealed('mavis-proposal') ;
But this would then rob the player character of his ability to ask how Mavis is until Bob has revealed his proposal plan. What we might ideally like is for our AskTopic to advance along our FactAgendaItem's path towards the Mavis proposal if it has not yet reached its goal, but provide the "She's fine" response otherwise. We can do this with:
++ AskTopic @mavis "<q>How is Mavis?</q> you ask. \b <<unless tryAgenda()>><q>She's fine,</q> Bob tells you. <<end>>" autoName = true autoUseAgenda = true ;
This then gives us the best of both worlds: our AskTopic will see if a suitable FactAgendaItem can provide a suitable reply about Mavis and display it if so; otherwise it uses the "She's fine" response.
The relevant new properties/methods of ActorTopicEntry that enable us to do this are:
- agenda: The FactAgendaItem we want this ActorTopicEntry to work with. If autoUseAgenda is true there's no need to specify this.
- autoUseAgenda: flag, to we want this ActorTopicEntry to find an appropriate FactAgendaItem for us? An 'appropriate' one will be one that's active in our actor's agenda list and that has a path from the topic our TopicEntry matches to the FactAgendaItem's target.
- tryAgenda(): Try executing our actor's agenda, and return true or nil according to whether or not it executed succesfully; this should normally execute the FactAgendaItem we're looking for, but it's just possible another agenda item might take priority. The risk of that is small, and the upside of using this method is that all the proper agenda housekeeping is carried out and that we use the overrideen version of out FactAgendaItem's invokeItem() method if game code has overriden it, so this is generally the better option.
- tryNextStep(): In the event that tryAgenda() doesn't do what we want, we can use this method instead, which simply calls initiateTopic(nextStep) on our actor and returns true or nil according to whether this was succesful (i.e., whether or not there was a matching InitiateTopic to call).
- nextStep: The next step along our FactAgendaItem's path of facts to follow.
- agendaPath: Our associated FactAgendaItem's current path (a list of Fact name tags) to its goal.
This covers most of what you need to know to use this extension. For additional information see the source code and comments in the factrel.t file.