/* ---------------------------------------------------------------------- */
/*
 *   TADS 3 Object Matching
 *   Version 3
 *   by Krister Fundin (fundin@yahoo.com)
 */

/* ---------------------------------------------------------------------- */
/*
 *   Introduction.
 */

   This extension provides TADS 3 programs with a couple of new ways of
finding and looping over objects without having to write convoluted object
loops. To use it, add the library file "match.tl" to your project, and
include the file "match.h" from any project files that make use of the
extension.

/* ---------------------------------------------------------------------- */
/*
 *   Overview.
 */

   The basis of this extension is a method of finding objects using certain
rules, which we shall refer to as "criteria" throughout this manual. There
are functions that determine if an objects meets a set of criteria, that
returns a list of such objects, that count such objects, etc. The functions
themselves will be explained in the next section, while the critera which
they all rely on are explained later on.

/* ---------------------------------------------------------------------- */
/*
 *   Functions.
 */

         objMeets(obj, [criteria])

   Returns true if the given object meets the set of criteria, or nil if
not. This is the most important function in the extension, though not
necessarily the one which will be most useful to the developer.

         exists([criteria])

   Returns true if there exists at least one object that meets the given
criteria.

         all([criteria])

   Returns a list of all objects that meet the given criteria, or an empty
list if there are none.

         count([criteria])

   Returns the number of objects that meet the given criteria.

         any([criteria])

   Returns a randomly chosen object among those that meet the given
criteria, or nil if there were none to choose from.

         most(property, [criteria])

   Among the objects that meet the given criteria, returns the one which has
the highest value in the given property. This only works if the values are
of comparable types, I.E. they must all be integers, strings och BigNumbers.
Also note that the property must be given as a property pointer: "&bulk"
instead of "bulk".

         least(property, [criteria])

   Works the same way as most(), except that it returns the object with the
lowest value in the given property.

/* ---------------------------------------------------------------------- */
/*
 *   Criteria.
 */

   In order to use the functions from the previous section, we need to
provide them with a set of criteria. This is done in the form of a chain of
arguments. Each argument may be of one of the following types:

         A class

   In most cases, this criterion will be met if the object in question is an
instance of the given class.

   To count the number of rooms in the story, type:

   count(Room)

         A property pointer

   If the property of the object points to a single value, or to a method
which has no parameters, then the property is evaluated and the criterion is
met if the result is anything other than zero or nil.

   To acquire a list of all open containers in the story, type:

   all(Container, &isOpen)

   If the property points to a method which has one or more parameters, it
is assumed that the following one or more (however many it was) items are to
be passed as arguments to the method.

   To choose a random object which is owned by the shopkeeper, type:

   any(Thing, &ownedBy, shopkeeper)

         A function pointer

   The function - it can be either a named or an anonymous one - is called
with the object as the only argument. The criterion is met if the return
value is anything other than zero or nil.

   To find the brightest light source that the player can see, type:

   most(&brightness, LightSource, { cur: gPlayerChar.canSee(cur) })

         A list or a vector

   This criterion is met only by objects that belong to the given list or
vector. This allows us to perform local searches through a restricted set of
objects, rather than searching through all the objects in the story.

         The special keyword 'not'

   This keyword is not a criterion in itself, but merely signals that the
result of the following criterion should be negated. Therefore, the object
in question will only pass the test if the following criterion is NOT met.

   To acquire a list of all closed containers in the game, type:

   all(Container, not, &isOpen)

/* ---------------------------------------------------------------------- */
/*
 *   Nested 'any' and 'all' phrases.
 */

   The examples we have seen so far have all been relatively simple. The
benefit of using this extension in place of more efficient, hand-crafted
code does not present itself until we get to the situations that require
several nested object loops.

   Let's go back to the property pointer criterion. We have learned that we
can call a method this way, and that we can supply any number of arguments.
What we're asking is simply whether the method returns true for those
particular arguments. But it's also possible to supply a nested set of
criteria and have all their matching objects automatically tried as
arguments.

   When using a method call criterion, one or more of the arguments supplied
to the method can be the 'any' keyword followed by a list. The list is a set
of criteria of its own. All objects matching these criteria will be tried in
that argument slot. The method only needs to return true for one of the
objects in order for the outer criterion to be met.

   To loop through all thieves who can see a treasure, type:

   forall(cur, Thief, &canSee, any, [Treasure])

   If we replace the 'any' keyword with 'all', then the outer criterion is
only met if the method returns true for ALL the objects matching the nested
criteria.

   To see if the treasure chest contains all the treasures (using a made-up
contains() method which doesn't exist), type:

   objMeets(treasureChest, &contains, all, [Treasure])

   The results of one particular case may not be immediately obvious here,
namely the one where no objects match the nested 'all' phrase. If there had
been no treasures at all in the above example, then the function would still
have returned true. In a way, the chest would have contained all treasures
there were to contain, that is none.

   Now, we may also continue and invent even more complicated criteria.
Since a nested 'any' or 'all' phrase can contain any criterion, this also
means that we can keep on nesting for as long as we want.

   To loop through all thieves who can see a treasure which isn't carried by
anyone, type:

   forall(cur, Thief, &canSee, any, [Treasure, not, &isIn, any, [Person]])

   Finally, note that even though all these examples have featured methods
that take a single argument, the same principles apply when two or more
arguments are supplied. It is even possible to use nested phrases in several
argument slots at once, although the results might be a bit unpredictable if
both 'any' and 'all' phrases are used in the same method call criterion.

/* ---------------------------------------------------------------------- */
/*
 *   Macros.
 */

   Note: in order for a source file to use the macros from this section, the
file "match.h" must be included by that source file. This can be done by
adding the following line to the top of the source file:

#include <match.h>

         forall(variable, [criteria])

   We have already hinted at the 'forall' macro. This a simple short-cut to
setting up a loop through the objects returned by the all() function. The
first argument of the macro is the name of the variable which will hold the
current object in the loop body. This variable must not have been previously
declared. The rest of the arguments are the criteria for the objects to loop
through.

   To mark all seen objects as being also known, type:

   forall(cur, &seen)
   {
      cur.isKnown = true;
   }

   (Of course, when the code body is just a single statement, as it is here,
the braces can be omitted.)

         definition(name, [criteria])

   This macro is intended to make it easier to write criteria that can be
reused in different parts of the code. The macro creates a new kind of
criterion, which is only met if all the other criteria from the definition
are met.

   To decide that an object is nearby if it is in the same room as the
player, type the following as a top-level statement:

   definition(nearby, &isIn, gPlayerChar.getOutermostRoom());

   This new criterion can then be used anywhere. For instance, to count all
portable objects that are nearby, type:

   count(not, NonPortable, nearby)

/* ---------------------------------------------------------------------- */
/*
 *   Efficiency.
 */

   Even though this extension does a lot to ensure that it runs at an
acceptable speed (see the next section for some details), one must remember
that it is a general-purpose tool, and as such can never match the speed of
a more tailored solution. Therefore, it is always a good idea to consider
what the alternatives are, especially in code that is going to be executed
often.

   To acquire a list of the player's complete possessions, including objects
contained by other objects, one could perhaps type:

   all(&isIn, gPlayerChar)

   However, the adv3 library already provides a much better way of getting
the same information, namely:

   gPlayerChar.allContents()

   If more criteria were required, for instance if we wanted to find all the
edible items carried by the player, we could type:

   all(Food, &isIn, gPlayerChar)

   But we could do the same thing more efficiently using the 'subset'
method:

   gPlayerChar.allContents().subset({ x: x.ofKind(Food) })

   As the complexity of the criteria increases, this extension tends to be
more straight-forward to use than the alternatives, but this ease of use
does not come without a price.

/* ---------------------------------------------------------------------- */
/*
 *   Technical details.
 */

   For the sake of efficiency, the all() function doesn't test every game
object to see if it meets the criteria. If a class is used as a criterion,
then only instances of that class are tested. If a list or a vector is used,
then only its members are considered. If neither class nor list is supplied,
then an implicit Thing class criterion is added automatically.

   Internally, all criteria are converted to a more convenient format before
being used to match objects. This saves a lot of time whenever several
objects need to be tested against the same criteria, as in all(), but also
when using nested 'any' and 'all' phrases. However, this means that the
compiler needs to guess how many parameters are required when it finds a
property pointer. It will try to do this by looking at previous class
criteria, or the Thing class if nothing else. Therefore, when using a
property pointer criterion, we have to make sure that either the property is
defined (or inherited) by Thing, or some other class is specified which does
define (or, again, inherit) the property.

