#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