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, convert the
             *   original topic text to lower case 
             */
            if (!matchExactCase)
                txt = txt.toLower();

            /* if the regular expression matches, we match */
            if (rexMatch(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); }
;


/*  
 *   A TopicDatabase is a container for TopicEntries that provides a method for
 *   determining the TopicEntry that best matches a list of topics
 */
class TopicDatabase: object
    
    /* 
     *   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;
        
        /* 
         *   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});
        
        /* 
         *   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 valToList(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()
        {
            /* 
             *   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;
        }
    }
    
    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 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 01/03/2024 from adv3Lite version 1.6.2