# eventList.t | documentation

    #charset "us-ascii"
    #include "advlite.h"

    /*
     *   ************************************************************************
     *   eventList.t This module forms an optional part of the adv3Lite library.
     *
     *
     *   (c) 2012-13 Eric Eve (but based largely on code borrowed from the adv3
     *   library Copyright (c) 2000, 2006 Michael J. Roberts.  All Rights Reserved.
     *   Lightly adapted by Eric Eve for use in the advLite library
     *
     *   adv3Lite Library - EventLists
     *
     *   This module contains definitions of various types of EventLists. These are
     *   defined in a separate module so that games that don't require EventLists
     *   can exclude this module from the build. The Script class, from which all
     *   EventLists inherits is defined in misc.t to allow other modules to test for
     *   an object being ofKind(Script) even when this EventList module is not
     *   present.
     */

    /*
     *   Random-Firing script add-in.  This is a mix-in class that you can add
     *   to the superclass list of any Script subclass to make the script
     *   execute only a given percentage of the time it's invoked.  Each time
     *   doScript() is invoked on the script, we'll look at the probability
     *   settings (see the properties below) to determine whether we really
     *   want to execute the script this time; if so, we'll proceed with the
     *   scripted event, otherwise we'll just return immediately, doing
     *   nothing.
     *
     *   Note that this must be used in the superclass list *before* the Script
     *   subclass:
     *
     *   myScript: RandomFiringScript, EventList
     *.    // ...my definitions...
     *.  ;
     *
     *   This class is especially useful for random atmospheric events, because
     *   it allows you to make the timing of scripted events random.  Rather
     *   than making a scripted event happen on every single turn, you can use
     *   this to make events happen only sporadically.  It can often feel too
     *   predictable and repetitious when a random background event happens on
     *   every single turn; firing events less frequently often makes them feel
     *   more realistic.
     */
    class RandomFiringScript: object
        /*
         *   Percentage of the time an event occurs.  By default, we execute an
         *   event 100% of the time - meaning every time that doScript() is
         *   invoked.  If you set this to a lower percentage, then each time
         *   doScript() is invoked, we'll randomly decide whether or not to
         *   execute an event based on this percentage.  For example, if you
         *   want an event to execute on average about a third of the time, set
         *   this to 33.
         *
         *   Note that this is a probabilistic frequency.  Setting this to 33
         *   does *not* mean that we'll execute exactly every third time.
         *   Rather, it means that we'll randomly execute or not on each
         *   invocation, and averaged over a large number of invocations, we'll
         *   execute about a third of the time.
         */
        eventPercent = 100

        /*
         *   Random atmospheric events can get repetitive after a while, so we
         *   provide an easy way to reduce the frequency of our events after a
         *   while.  This way, we'll generate the events more frequently at
         *   first, but once the player has seen them enough to get the idea,
         *   we'll cut back.  Sometimes, the player will spend a lot of time in
         *   one place trying to solve a puzzle, so the same set of random
         *   events can get stale.  Set eventReduceAfter to the number of times
         *   you want the events to be generated at full frequency; after we've
         *   fired events that many times, we'll change eventPercent to
         *   eventReduceTo.  If eventReduceAfter is nil, we won't ever change
         *   eventPercent.
         */
        eventReduceAfter = nil
        eventReduceTo = nil

        /*
         *   When doScript() is invoked, check the event probabilities before
         *   proceeding.
         */
        doScript()
        {
            /* process the script step only if the event odds allow it */
            if (checkEventOdds())
                inherited();
        }

        /*
         *   Check the event odds to see if we want to fire an event at all on
         *   this invocation.
         */
        checkEventOdds()
        {
            /*
             *   check the event odds to see if we fire an event this time; if
             *   not, we're done with the script invocation
             */
            if (rand(100) >= eventPercent)
                return nil;

            /*
             *   we're firing an event this time, so count this against the
             *   reduction limit, if there is one
             */
            if (eventReduceAfter != nil)
            {
                /* decrement the limit counter */
                --eventReduceAfter;

                /* if it has reached zero, apply the reduced frequency */
                if (eventReduceAfter == 0)
                {
                    /* apply the reduced frequency */
                    eventPercent = eventReduceTo;

                    /* we no longer have a limit to look for */
                    eventReduceAfter = nil;
                }
            }

            /* indicate that we do want to fire an event */
            return true;
        }
    ;

    /* ------------------------------------------------------------------------ */
    /*
     *   An "event list."  This is a general-purpose type of script that lets
     *   you define the scripted events separately from the Script object.
     *
     *   The script is driven by a list of values; each value represents one
     *   step of the script.  Each value can be a single-quoted string, in
     *   which case the string is simply displayed; a function pointer, in
     *   which case the function is invoked without arguments; another Script
     *   object, in which case the object's doScript() method is invoked; a
     *   property pointer, in which case the property of 'self' (the EventList
     *   object) is invoked with no arguments; or nil, in which case nothing
     *   happens.
     *
     *   This base type of event list runs through the list once, in order, and
     *   then simply stops doing anything once we pass the last event.
     */
    class EventList: Script
        construct(lst) { eventList = lst; }

        /* the list of events */
        eventList = []

        /* cached length of the event list */
        eventListLen = (eventList.length())

        /* advance to the next state */
        advanceState()
        {
            /* increment our state index */
            ++curScriptState;
        }

        /* by default, start at the first list element */
        curScriptState = 1

        /* process the next step of the script */
        doScript()
        {
            /* get our current event state */
            local idx = getScriptState();

            /* get the list (evaluate it once to avoid repeated side effects) */
            local lst = eventList;

            /* cache the length */
            eventListLen = lst.length();

            /* if it's a valid index in our list, fire the event */
            if (idx >= 1 && idx <= eventListLen)
            {
                /* carry out the event */
                doScriptEvent(lst[idx]);
            }

            /* perform any end-of-script processing */
            scriptDone();
        }

        /* carry out one script event */
        doScriptEvent(evt)
        {
            /* check what kind of event we have */
            switch (dataTypeXlat(evt))
            {
            case TypeSString:
                /* it's a string - display it */
                say(evt);
                break;

            case TypeObject:
                /* it must be a Script object - invoke its doScript() method */
                evt.doScript();
                break;

            case TypeFuncPtr:
                /* it's a function pointer - invoke it */
                (evt)();
                break;

            case TypeProp:
                /* it's a property of self - invoke it */
                self.(evt)();
                break;

            default:
                /* do nothing in other cases */
                break;
            }
        }

        /*
         *   Perform any end-of-script processing.  By default, we advance the
         *   script to the next state.
         *
         *   Some scripts might want to override this.  For example, a script
         *   could be driven entirely by some external timing; the state of a
         *   script could vary once per turn, for example, or could change each
         *   time an actor pushes a button.  In these cases, invoking the
         *   script wouldn't affect the state of the event list, so the
         *   subclass would override scriptDone() so that it does nothing at
         *   all.
         */
        scriptDone()
        {
            /* advance to the next state */
            advanceState();
        }
    ;

    /*
     *   An "external" event list is one whose state is driven externally to
     *   the script.  Specifically, the state is *not* advanced by invoking the
     *   script; the state is advanced exclusively by some external process
     *   (for example, by a daemon that invokes the event list's advanceState()
     *   method).
     */
    class ExternalEventList: EventList
        scriptDone() { }
    ;

    /*
     *   A cyclical event list - this runs through the event list in order,
     *   returning to the first element when we pass the last element.
     */
    class CyclicEventList: EventList
        advanceState()
        {
            /* go to the next state */
            ++curScriptState;

            /* if we've passed the end of the list, loop back to the start */
            if (curScriptState > eventListLen)
                curScriptState = 1;
        }
    ;

    /*
     *   A stopping event list - this runs through the event list in order,
     *   then stops at the last item and repeats it each time the script is
     *   subsequently invoked.
     *
     *   This is often useful for things like ASK ABOUT topics, where we reveal
     *   more information when asked repeatedly about a topic, but eventually
     *   reach a point where we've said everything:
     *
     *.  >ask bob about black book
     *.  "What makes you think I know anything about it?" he says, his
     *   voice shaking.
     *
     *   >again
     *.  "No! You can't make me tell you!"
     *
     *   >again
     *.  "All right, I'll tell you what you want to know!  But I warn you,
     *   these are things mortal men were never meant to know.  Your life, your
     *   very soul will be in danger from the moment you hear these dark secrets!"
     *
     *   >again
     *.  [scene missing]
     *
     *   >again
     *.  "I've already told you all I know."
     *
     *   >again
     *.  "I've already told you all I know."
     */
    class StopEventList: EventList
        advanceState()
        {
            /* if we haven't yet reached the last state, go to the next one */
            if (curScriptState < eventListLen)
                ++curScriptState;
        }
    ;

    /*
     *   A synchronized event list.  This is an event list that takes its
     *   actions from a separate event list object.  We get our current state
     *   from the other list, and advancing our state advances the other list's
     *   state in lock step.  Set 'masterObject' to refer to the master list
     *   whose state we synchronize with.
     *
     *   This can be useful, for example, when we have messages that reflect
     *   two different points of view on the same events: the messages for each
     *   point of view can be kept in a separate list, but the one list can be
     *   a slave of the other to ensure that the two lists are based on a
     *   common state.
     */
    class SyncEventList: EventList
        /* my master event list object */
        masterObject = nil

        /* my state is simply the master list's state */
        getScriptState() { return masterObject.getScriptState(); }

        /* to advance my state, advance the master list's state */
        advanceState() { masterObject.advanceState(); }

        /* let the master list take care of finishing a script step */
        scriptDone() { masterObject.scriptDone(); }
    ;

    /*
     *   Randomized event list.  This is similar to a regular event list, but
     *   chooses an event at random each time it's invoked.
     */
    class RandomEventList: RandomFiringScript, EventList
        /* process the next step of the script */
        doScript()
        {
            /* check the odds to see if we want to fire an event at all */
            if (!checkEventOdds())
                return;

            /* get our next random number */
            local idx = getNextRandom();

            /* cache the list and its length, to avoid repeated side effects */
            local lst = eventList;
            eventListLen = lst.length();

            /* run the event, if the index is valid */
            if (idx >= 1 && idx <= eventListLen)
                doScriptEvent(lst[idx]);
        }

        /*
         *   Get the next random state.  By default, we simply return a number
         *   from 1 to the number of entries in our event list.  This is a
         *   separate method to allow subclasses to customize the way the
         *   random number is selected.
         */
        getNextRandom()
        {
            /*
             *   Note that rand(n) returns a number from 0 to n-1 inclusive;
             *   since list indices run from 1 to list.length, add one to the
             *   result of rand(list.length) to get a value in the proper range
             *   for a list index.
             */
            return rand(eventListLen) + 1;
        }
    ;

    /*
     *   Shuffled event list.  This is similar to a random event list, except
     *   that we fire our events in a "shuffled" order rather than an
     *   independently random order.  "Shuffled order" means that we fire the
     *   events in random order, but we don't re-fire an event until we've run
     *   through all of the other events.  The effect is as though we were
     *   dealing from a deck of cards.
     *
     *   For the first time through the main list, we normally shuffle the
     *   strings immediately at startup, but this is optional.  If shuffleFirst
     *   is set to nil, we will NOT shuffle the list the first time through -
     *   we'll run through it once in the given order, then shuffle for the
     *   next time through, then shuffle again for the next, and so on.  So, if
     *   you want a specific order for the first time through, just define the
     *   list in the desired order and set shuffleFirst to nil.
     *
     *   You can optionally specify a separate list of one-time-only sequential
     *   strings in the property firstEvents.  We'll run through these strings
     *   once.  When we've exhausted them, we'll switch to the main eventList
     *   list, showing it one time through in its given order, then shuffling
     *   it and running through it again, and so on.  The firstEvents list is
     *   never shuffled - it's always shown in exactly the order given.
     */
    class ShuffledEventList: RandomFiringScript, EventList
        /*
         *   a list of events to go through sequentially, in the exact order
         *   specified, before firing any events from the main list
         */
        firstEvents = []

        /*
         *   Flag: shuffle the eventList list before we show it for the first
         *   time.  By default, this is set to true, so that the behavior is
         *   random on each independent run of the game.  However, it might be
         *   desirable in some cases to always use the original ordering of the
         *   eventList list the first time through the list.  If this is set to
         *   nil, we won't shuffle the list the first time through.
         */
        shuffleFirst = true

        /*
         *   Flag: suppress repeats in the shuffle.  If this is true, it
         *   prevents a given event from showing up twice in a row, which could
         *   otherwise happen right after a shuffle.  This is ignored for lists
         *   with one or two events: it's impossible to prevent repeats in a
         *   one-element list, and doing so in a two-element list would produce
         *   a predictable A-B-A-B... pattern.
         *
         *   You might want to set this to nil for lists of three or four
         *   elements, since such short lists can result in fairly
         *   un-random-looking sequences when repeats are suppressed, because
         *   the available number of permutations drops significantly.
         */
        suppressRepeats = true

        /* process the next step of the script */
        doScript()
        {
            /* cache the lists to avoid repeated side effects */
            local firstLst = firstEvents;
            local firstLen = firstLst.length();
            local lst = eventList;
            eventListLen = lst.length();

            /* process the script step only if the event odds allow it */
            if (!checkEventOdds())
                return;

            /*
             *   States 1..N, where N is the number of elements in the
             *   firstEvents list, simply show the firstEvents elements in
             *   order.
             *
             *   If we're set to shuffle the main eventList list initially, all
             *   states above N simply show elements from the eventList list in
             *   shuffled order.
             *
             *   If we're NOT set to shuffle the main eventList list initially,
             *   the following apply:
             *
             *   States N+1..N+M, where M is the number of elements in the
             *   eventList list, show the eventList elements in order.
             *
             *   States above N+M show elements from the eventList list in
             *   shuffled order.
             */
            local evt;
            if (curScriptState <= firstLen)
            {
                /* simply fetch the next string from firstEvents */
                evt = firstEvents[curScriptState++];
            }
            else if (!shuffleFirst && curScriptState <= firstLen + eventListLen)
            {
                /* fetch the next string from eventList */
                evt = lst[curScriptState++ - firstLen];
            }
            else
            {
                /* we're showing shuffled strings from the eventList list */
                evt = lst[getNextRandom()];
            }

            /* execute the event */
            doScriptEvent(evt);
        }

        /*
         *   Get the next random event.  We'll pick an event from our list of
         *   events using a ShuffledIntegerList to ensure we pick each value
         *   once before re-using any values.
         */
        getNextRandom()
        {
            /* if we haven't created our shuffled list yet, do so now */
            if (shuffledList_ == nil)
            {
                /*
                 *   create a shuffled integer list - we'll use these shuffled
                 *   integers as indices into our event list
                 */
                shuffledList_ = new ShuffledIntegerList(1, eventListLen);

                /* apply our suppressRepeats option to the shuffled list */
                shuffledList_.suppressRepeats = suppressRepeats;
            }

            /* ask the shuffled list to pick an element */
            return shuffledList_.getNextValue();
        }

        /* our ShuffledList - we'll initialize this on demand */
        shuffledList_ = nil
    ;

    /* ------------------------------------------------------------------------ */
    /*
     *   Shuffled List - this class keeps a list of values that can be returned
     *   in random order, but with the constraint that we never repeat a value
     *   until we've handed out every value.  Think of a shuffled deck of
     *   cards: the order of the cards handed out is random, but once a card is
     *   dealt, it can't be dealt again until we put everything back into the
     *   deck and reshuffle.
     */
    class ShuffledList: object
        /*
         *   the list of values we want to shuffle - initialize this in each
         *   instance to the set of values we want to return in random order
         */
        valueList = []

        /*
         *   Flag: suppress repeated values.  We mostly suppress repeats by our
         *   very design, since we run through the entire list before repeating
         *   anything in the list.  However, there's one situation (in a list
         *   with more than one element) where a repeat can occur: immediately
         *   after a shuffle, we could select the last element from the
         *   previous shuffle as the first element of the new shuffle.  If this
         *   flag is set, we'll suppress this type of repeat by choosing again
         *   any time we're about to choose a repeat.
         *
         *   Note that we ignore this for a list of one element, since it's
         *   obviously impossible to avoid repeats in this case.  We also
         *   ignore it for a two-element list, since this would produce the
         *   predictable pattern A-B-A-B..., defeating the purpose of the
         *   shuffle.
         */
        suppressRepeats = nil

        /* create from a given list */
        construct(lst)
        {
            /* remember our list of values */
            valueList = lst;
        }

        /*
         *   Get a random value.  This will return a randomly-selected element
         *   from 'valueList', but we'll return every element of 'valueList'
         *   once before repeating any element.
         *
         *   If we've returned every value on the current round, we'll
         *   automatically shuffle the values and start a new round.
         */
        getNextValue()
        {
            local i;
            local ret;
            local justReshuffled = nil;

            /* if we haven't initialized our vector, do so now */
            if (valuesVec == nil)
            {
                /* create the vector */
                valuesVec = new Vector(valueList.length(), valueList);

                /* all values are initially available */
                valuesAvail = valuesVec.length();
            }

            /* if we've exhausted our values on this round, start over */
            if (valuesAvail == 0)
            {
                /* shuffle the elements */
                reshuffle();

                /* note that we just did a shuffle */
                justReshuffled = true;
            }

            /* pick a random element from the 'available' partition */
            i = rand(valuesAvail) + 1;

            /*
             *   If we just reshuffled, and we're configured to suppress a
             *   repeat immediately after a reshuffle, and we chose the first
             *   element of the vector, and we have at least three elements,
             *   choose a different element.  The first element in the vector is
             *   always the last element we return from each run-through, since
             *   the 'available' partition is at the start of the list and thus
             *   shrinks down until it contains only the first element.
             *
             *   If we have one element, there's obviously no point in trying to
             *   suppress repeats.  If we have two elements, we *still* don't
             *   want to suppress repeats, because in this case we'd generate a
             *   predicatable A-B-A-B pattern (because we could never have two
             *   A's or two B's in a row).
             */
            if (justReshuffled && suppressRepeats && valuesAvail > 2)
            {
                /*
                 *   we don't want repeats, so choose anything besides the
                 *   first element; keep choosing until we get another element
                 */
                while (i == 1)
                    i = rand(valuesAvail) + 1;
            }

            /* remember the element we're returning */
            ret = valuesVec[i];

            /*
             *   Move the value at the top of the 'available' partition down
             *   into the hole we're creating at 'i', since we're about to
             *   reduce the size of the 'available' partition to reflect the
             *   use of one more value; that would leave the element at the top
             *   of the partition homeless, so we need somewhere to put it.
             *   Luckily, we also need to delete element 'i', since we're using
             *   this element.  Solve both problems at once by moving element
             *   we're rendering homeless into the hole we're creating.
             */
            valuesVec[i] = valuesVec[valuesAvail];

            /* move the value we're returning into the top slot */
            valuesVec[valuesAvail] = ret;

            /* reduce the 'available' partition by one */
            --valuesAvail;

            /* return the result */
            return ret;
        }

        /*
         *   Shuffle the values.  This puts all of the values back into the
         *   deck (as it were) for a new round.  It's never required to call
         *   this, because getNextValue() automatically shuffles the deck and
         *   starts over each time it runs through the entire deck.  This is
         *   provided in case the caller has a reason to want to put all the
         *   values back into play immediately, before every value has been
         *   dealt on the current round.
         */
        reshuffle()
        {
            /*
             *   Simply reset the counter of available values.  Go with the
             *   original source list's length, in case we haven't initialized
             *   our internal vector yet.
             */
            valuesAvail = valueList.length();
        }

        /*
         *   Internal vector of available/used values.  Elements from 1 to
         *   'valuesAvail', inclusive, are still available for use on this
         *   round.  Elements above 'valuesAvail' have already been used.
         */
        valuesVec = nil

        /* number of values still available on this round */
        valuesAvail = 0
    ;

    /*
     *   A Shuffled Integer List is a special kind of Shuffled List that
     *   returns integers in a given range.  Like an ordinary Shuffled List,
     *   we'll return integers in the given range in random order, but we'll
     *   only return each integer once during a given round; when we exhaust
     *   the supply, we'll reshuffle the set of integers and start over.
     */
    class ShuffledIntegerList: ShuffledList
        /*
         *   The minimum and maximum values for our range.  Instances should
         *   define these to the range desired.
         */
        rangeMin = 1
        rangeMax = 10

        /* initialize the value list on demand */
        valueList = nil

        /* construct with the given range */
        construct(rmin, rmax)
        {
            rangeMin = rmin;
            rangeMax = rmax;
        }

        /* get the next value */
        getNextValue()
        {
            /*
             *   If we haven't set up our value list yet, do so now.  This is
             *   simply a list of integers from rangeMin to rangeMax.
             */
            if (valueList == nil)
            {
                local ele = rangeMin;
                valueList = List.generate({i: ele++}, rangeMax - rangeMin + 1);
            }

            /* use the inherited handling to select from our value list */
            return inherited();
        }
    ;

Adv3Lite Library Reference Manual
Generated on 03/07/2024 from adv3Lite version 2.1