#charset "us-ascii"
#include "advlite.h"
/*
* ****************************************************************************
* topicEntry.t
* This module forms part of the adv3Lite library
* (c) 2012-13 Eric Eve
*/
/*
* TopicEntry is the base class for ConsultTopics and various kinds of
* Conversation Topics. It can be used to match a particular topic and output
* an appropriate response.
*/
class TopicEntry: object
/*
* Determine how well this TopicEntry matches top (a Topic or Thing). If
* it doesn't match at all we return nil, otherwise we return a numerical
* score indicating the strength of the match so that a routine that's
* looking for the best match can choose the one with the highest score.
*/
matchTopic(top)
{
/*
* Note the topic we're trying to match so that topicResponse() can
* make use if it, if it wants to.
*/
topicMatched = top;
/*
* If top is nil we're programmatically passing a topic that will
* match anything. Otherwise test if top matches the matchObj, where
* match means that top is one of items in the matchObj list or else
* belongs to a class in the list. If we have a match, return the sum
* of our matchScore and our scoreBoost.
*/
if(top == nil ||
valToList(matchObj).indexWhich({x: top.ofKind(x)}) != nil)
return matchScore + scoreBooster();
/*
* Next test to see if we should match a regular expression. This will
* be the case if we have a matchPattern to match and our top object
* is a Topic (which the parser will have created to encapsulate the
* text our matchPattern needs to match).
*/
if(matchPattern != nil && top.ofKind(Topic))
{
local txt;
/*
* There's no match object; try matching our regular
* expression to the actual topic text. Get the actual text.
*/
txt = top.getTopicText();
/*
* If they don't want an exact case match, make the regex search non case sensitive,
* otherwise make it case sensitive.
*/
local caseHandling = matchExactCase ? '<Case>' : '<NoCase>';
/* if the regular expression matches, we match */
if (rexMatch('<<caseHandling>><<matchPattern>>', txt) != nil)
return matchScore + scoreBoost;
}
/* If we haven't found a match, return nil */
return nil;
}
/* Initialize this Topic Entry (actually carried out at pre-init */
initializeTopicEntry()
{
/* if we have a location, add ourselves to its topic database */
if (location != nil)
location.addTopic(self);
}
/*
* Output our response to the topic. This can be typically be overridden
* to a double-quoted string or method to output the required response.
*/
topicResponse()
{
/*
* If we're not overridden, then if this TopicEntry is also some kind
* of Script (normally because it also includes an EventList class in
* its superclass list), then call its doScript() method to display
* the next item in the list.
*/
if(ofKind(Script))
doScript();
}
/*
* Our matchScore is the base score we return if we match the topic
* requested; this is used to determine whether we're the best match under
* the circumstances. By default we use a value of 100.
*/
matchScore = 100
/*
* The object, topic or list of objects/topics that this TopicEntry
* matches.
*/
matchObj = nil
/*
* The topic that this TopicEntry actually matched (set by matchTopic()).
*/
topicMatched = nil
/*
* A regular expression that this TopicEntry might match, if it doesn't
* match a matchObj. We don't need to define this if we've defined a
* matchObj.
*/
matchPattern = nil
/*
* Do we want to restrict this TopicEntry to an exact case match with its
* matchPattern? By default we don't.
*/
matchExactCase = nil
/*
* The set of database lists we're part of. This is a list of one or more
* property pointers, giving the TopicDatabase properties of the
* lists we participate in.
*/
includeInList = []
/*
* A method or property that can be used to dynamically alter our score
* according to circumstances if needed.
*/
scoreBoost = 0
scoreBooster()
{
local sb;
/* Add any boost from our location */
sb = location.propDefined(&scoreBooster) ? location.scoreBooster() : 0;
/* Add our own scoreBoost. */
return sb + scoreBoost;
}
/*
* Is this TopicEntry currently active? Game code can set a condition here
* so that a TopicEntry only becomes active (i.e. available) under
* particular circumstances.
*/
isActive = true
/*
* The active property is used internally by the library to determine
* whether a TopicEntry is currently available for use. On the base
* TopicEntry class a topic entry is active if its isActive property is
* true, but this is not necessarily the case on the ActorTopicEntry
* subclass defined in actor.t, which needs to distinguish between these
* properties.
*
* Game code should not normally need to override the active property.
*/
active = isActive
/*
* If something located in us wants us to add it to our topic database,
* pass the request up to our location (this is used by AltTopic).
*/
addTopic(top) { location.addTopic(top); }
/* Our notional actor is our location's actor. */
getActor = location.getActor
;
/*
* A TopicDatabase is a container for TopicEntries that provides a method for
* determining the TopicEntry that best matches a list of topics
*/
modify TopicDatabase
/*
* Find the topic entry among those supplied in myList that best matches
* at least one of the topics passed in requestedList.
*/
getBestMatch(myList, requestedList)
{
local bestMatch = nil;
local bestScore = 0;
/* Ensure that our requestedList is actually a list. */
requestedList = valToList(requestedList);
/*
* The implementation of the Actor Conversation system requires a
* property pointer to be passed as the first parameter in the
* corresponding method. To prevent accidents, we check whether we
* have a property pointer here and if so convert it to the
* corresponding list.
*/
if(dataType(myList) == TypeProp)
myList = self.(myList);
/* Remove any inactive topic entries from the list to search */
myList = myList.subset({c: c.active});
/*
* if requestedList contains any topics that have not been newlyCreated, eliminate the
* new;y created ones.
*/
local revList = requestedList.subset({x: x.newlyCreated == nil});
/*
* If we've anything left after removing newly created topics, set our requested list to
* the new list
*/
if(revList.length > 0)
requestedList = revList;
/*
* If we have more than one entry, try to eliminate any that the player probably didn't
* mean. We do this by excluding any entries with names longer than the shortest. The
* rationale is that if the player types a topic name that's a subset of another topic
* name, the player probably means to refer to the topic with the shorter name. For
* example, THINK ABOUT WEDDING is more likely to be intended to match a topic with name
* 'wedding' than one with the name 'when the wedding will be'. We don't do this with
* Query or Say however, since here the player may be abbreviating a much longer command,
* which might then get masked by a shorter topic; e.g. we don't want ASK WHEN THE WEDDING
* WILL BE to be masked by a 'wedding' topic if the player types ASK WHEN WEDDING.
*/
if(requestedList.length > 1 && gAction not in (Query, SayTo, QueryAbout, SayAction))
{
/* Sort the list in descending order of name length. */
requestedList = requestedList.sort(nil, {a, b: a.name.length - b.name.length});
/* Note the length of the shortest name. */
local minLength = requestedList[1].name.length;
/* Reduce our list to items whose name length is that of the shortest name. */
requestedList = requestedList.subset({x: x.name.length == minLength });
}
/*
* For each topic in our requested list of topics, see if we can find
* a topic entry that's a better match than any we've found so far.
*/
foreach(local req in requestedList)
{
/* Go through every topic entry in our list */
foreach(local top in myList)
{
/*
* Compute the score that indicates how well the topic entry
* matches the topic (top) we're currently testing for.
*/
local score = top.matchTopic(req);
/*
* If we found a match (the score is non-nil) and the score is
* greater than the best score we've found so far, note our
* new best score and best matching topic entry.
*/
if(score != nil && score > bestScore)
{
bestScore = score;
bestMatch = top;
}
}
}
/* Return the best match. */
return bestMatch;
}
/* Add a topic entry to the appropriate list or list on this TopicDatabase. */
addTopic(top)
{
/*
* Go through each property pointer in the topic entry's includeInList
* and add the topic entry to the corresponding list.
*/
foreach(local prop in valToList(top.includeInList))
self.(prop) += top;
}
;
/*
* A Consultable is an object like a book, timetable or computer that can be
* used to look things up in through commands such as LOOK UP SELVAGEE IN
* DICTIONARY or CONSULT BLUE BOOK ABOUT RABBITS
*/
class Consultable: TopicDatabase, Thing
/* The list of ConsultTopics associated with this Consultable */
consultTopics = []
/* A Consultable is indeed consultable */
isConsultable = true
/* Our handling of the ConsultAbout action when we're the direct object */
dobjFor(ConsultAbout)
{
action()
{
/*
* We don't want this action to be construed as conversational from the point of view
* of revealing information to bystsanders, so we first store the identity of the
* current interlocutor and then set the current interlocutor to ni.
*/
local interlocutor = gPlayerChar.currentInterlocutor;
gPlayerChar.currentInterlocutor = nil;
try
{
/*
* Find the topic we're meant to be matching by getting the best match to the list
* of topics contained in the indirect object
*/
local matchedTopic = getBestMatch(consultTopics, gIobj.topicList);
/* If we don't find a match, display a message explaining that */
if(matchedTopic == nil)
say(noMatchedTopicMsg);
/*
* Otherwise display the topic response of the ConsultTopic we matched.
*/
else
matchedTopic.topicResponse();
/*
* Boost our currentConsultableScore in recognition that we were the last item to
* be consulted.
*/
currentConsultableScore = 20;
}
finally
{
/* Restore the current interlocutor */
gPlayerChar.currentInterlocutor = interlocutor;
}
}
}
noMatchedTopicMsg = BMsg(no matched topic, '{The subj dobj} {has} nothing to
say on that. ')
/*
* Modify our score (from the point of view of the parser matching this
* Consultable) if we've been recently consulted (on the assumption that
* other things being equal, if we've been consulted recently, we're quite
* likely to be the object the player wants to consult again)
*/
scoreObject(cmd, role, lst, m)
{
/* Carry out the inherited handlind */
inherited(cmd, role, lst, m);
/*
* If the parser is looking to match a ConsultAbout action, boost our
* score if we've been consulted recently.
*/
if(cmd.action == ConsultAbout && role == DirectObject)
m.score += currentConsultableScore;
}
/*
* The additional score we add in our scoreObject() method if we've been
* recently consulted.
*/
currentConsultableScore = 0
afterAction()
{
/*
* Decrement out currentConsultableScore if we weren't one of the
* objects for the current action, but don't decrement it below zero.
*/
if(gIobj != self && gDobj != self && currentConsultableScore > 0)
currentConsultableScore-- ;
}
/*
* A list of the ConsultTopics we want to create, each item in the list should be a
* two-element list in the form of [match, topic-response], where match is what we want the
* ConsultTopic to match and topic-responsse is what we want the ConsultTopic's topicResponse
* to be. match can be an object (Topic or Thing), a list of objects, or a match patter.
* topic-response will normally be a single-quoted string but could be a function pointer or
* floating method. A third entry can be supplied, which will be used as the matchScore, but
* this is probably seldom useful.
*/
topicEntryList = nil
/* Modifications to allow the automatic creation of ConsultTopics from our topicList. */
preinitThing()
{
/* Carry out the inherited handling. */
inherited();
/*
* Loop through our topicList to create a corresponding ConsultTopic for every item
* therein.
*/
foreach(local item in valToList(topicEntryList))
preinitTopic(item);
}
/* Create a ConsultTopic corrersponding to item */
preinitTopic(item)
{
/* Make sure that item is expressed as a list. */
item = valToList(item);
/* Set up a local variable to contain our new ConsultTopic */
local top;
/*
* Set up a local variable to hold the object, list of objects, or matchPattern our new
* ConscultTopic is to match.
*/
local topkey;
/* If the first entry in out item list in 'default', create a new DefaultTopicEntry. */
if(item[1] == 'default')
{
top = new DefaultConsultTopic;
topkey = nil;
}
else
{
/* Otherwise create a new ConsultTopic */
top = new ConsultTopic;
/* And note what it is to match on */
topkey = item[1];
}
/* Set the new ConsultTopic's entry to ourself. */
top.location = self;
/* Carry out the initializing of our new TopicEntry */
top.initializeTopicEntry();
/*
* Assign our matchObj or match pattern to the appropriate property of our new
* ConsultTopic.
*/
switch(dataType(topkey))
{
/*If it's an object or list, assign it to the matcchObj property. */
case TypeObject:
case TypeList:
top.matchObj = topkey;
break;
/* If it's a single-quoted string, assign it to the matchPattern property. */
case TypeSString:
top.matchPattern = topkey;
break;
/* If it's nil (as it will be for a DefaultConsultTopic) do nothing */
case TypeNil:
break;
};
/*
* Provided we have a second entry in our item list, assign in to the new ConsultTopic's
* topicResponse property.
*/
if(item.length > 1)
{
local txt = item[2];
setTopicResponse(top, topkey, txt);
}
/* Should we have a third item, assign it to the new ConsultTopic's matchScore */
if(item.length > 2 && dataType(item[3]) == TypeInt)
top.matchScore = item[3];
}
setTopicResponse(top, topkey, txt)
{
top.setMethod(&topicResponse, txt);
}
/* We're our own 'actor' in the sense of being the source of any information we supply. */
getActor = self
;
/*
* A ConsultTopic is a kind of TopicEntry used in conjunction with a
* Consultable, and represents something the Consultable can be successfully
* consulted about.
*/
class ConsultTopic: TopicEntry
/*
* ConsultTopics are listed in the consultTopics property of the
* Consultable that contains them.
*/
includeInList = [&consultTopics]
;
/*
* A DefaultConsultTopic is used to provide a response when a Consultable is
* consulted about something not otherwise provided for.
*/
class DefaultConsultTopic: ConsultTopic
/* A DefaultConsultTopic matches anything, so just return our matchScore */
matchTopic(top)
{
/* Note the Topic we matched. */
topicMatched = top;
/*
* Since we can match anything, simply return the sum of our
* matchScore and our scoreBoost.
*/
return matchScore + scoreBooster();
}
/*
* A DefaultConsultTopic has the lowest possible matchScore so that any
* matching ConsultTopic will always take precedence.
*/
matchScore = 1
/* A DefaultConsultTopic is normally active */
isActive = true
;
/* Preinitializer for ConsultTopics */
consultablePreinit: PreinitObject
execute()
{
/* Initialize every ConsultTopic */
forEachInstance(ConsultTopic, {c: c.initializeTopicEntry()} );
}
;
Adv3Lite Library Reference Manual
Generated on 03/07/2024 from adv3Lite version 2.1