topicEntry.t

documentation
#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