All computer source code presented on this page, unless it includes attribution to another author, is provided by Ed Halley under the Artistic License. Use such code freely and without any expectation of support. I would like to know if you make anything cool with the code, or need questions answered.
python/
    bindings.py
    boards.py
    buzz.py
    caches.py
    cards.py
    constraints.py
    csql.py
    english.py
    getch.py
    getopts.py
    gizmos.py
    goals.py
    improv.py
    interpolations.py
    namespaces.py
    nihongo.py
    nodes.py
    octalplus.py
    patterns.py
    physics.py
    pids.py
    pieces.py
    quizzes.py
    recipes.py
    relays.py
    romaji.py
    ropen.py
    sheets.py
    stores.py
    strokes.py
    subscriptions.py
    svgbuild.py
    testing.py
    things.py
    timing.py
    ucsv.py
    useful.py
    uuid.py
    vectors.py
    weighted.py
java/
    CSVReader.java
    CSVWriter.java
    GlobFilenameFilter.java
    RegexFilenameFilter.java
    StringBufferOutputStream.java
    ThreadSet.java
    Throttle.java
    TracingThread.java
    Utf8ConsoleTest.java
    droid/
        ArrangeViewsTouchListener.java
        DownloadFileTask.java
perl/
    CVQM.pm
    Kana.pm
    Typo.pm
cxx/
    CCache.h
    equalish.cpp
Download goals.py
# goals - generic monitors of task progress

'''

A goal is a monitor of some task to be completed.  It offers a callback
hook to automatically perform when the goal has been achieved.

ABSTRACT

This module includes simple goals that watch variables, driving goals
that actually modify variables for you, and compound goals that can be
performed and monitored in series or parallel.

All goals have a progress() method which returns a value from 0.0 to 1.0.
The caller should check the goal progress occasionally to see if the goal
has been achieved.  (Goals by themselves do not represent or employ thread
execution.)

Once the goal has been achieved, the progress() method will return 1.0
and trigger a callback function exactly once.  Most goal types return a
numerical estimation of the overall progress, suitable for progress bars.

An alternative to calling progress() is to call done(), which just
returns True if the progress() has reached 1.0.

Example def-style callback:

    def task(g):
        print 'done!'
    ...
    goal = Goal(todo=task)

AUTHOR

    Ed Halley (ed@halley.cc) 10 November 2007

'''

#----------------------------------------------------------------------------

import time
import interpolations

#----------------------------------------------------------------------------

class Goal (object):

    '''A generic goal class, serving as the basis for several goal types.
    This goal is done immediately and triggers its callback as soon as the
    goal is first checked with progress() or done().
    '''

    def __init__(self, todo=None):
        self.todo = todo

    def __str__(self):
        return self.__class__.__name__ + "()"

    def progress(self):
        '''May be called anytime to evaluate or effect progress.
        Should return 0.0 for no progress made, to 1.0 if goal achieved.
        Should call self.trigger() if goal achieved.
        '''
        self.trigger()
        return 1.0

    def trigger(self):
        '''Called once the goal has been met.
        Calls the supplied callback todo function once if specified.
        '''
        if self.todo:
            fun = self.todo
            self.todo = None
            fun(self)

    def done(self):
        '''May be called anytime to evaluate or effect goal achievement.
        Returns False if not completely achieved, or True if achieved.
        '''
        return self.progress() >= 1.0

#----------------------------------------------------------------------------

def __time__():
    return time.time()

class RangeGoal (Goal):

    '''A RangeGoal fires when a given variable reaches a range limit.
    The variable must be a mutable object which supports certain operators.
    The limit values are exclusive by default, meaning that a variable
    value that equals the limit value will complete the goal.

    A goal cannot monitor a scalar variable directly, but it can monitor
    a scalar value wrapped in an object that supports the __cmp__()
    method.  A simple way to achieve this for scalar variables is to put
    it in a list reference.  Since lists can be compared by their
    elements, the minimum and maximum values are lists too.

    Example:
       var = [ 5.0 ]
       goal = RangeGoal(v, [0.0], [10.0])
       while not goal.done():
           print var[0]
           var[0] += random.random()-0.5
       print var[0]

    '''

    def __init__(self, variable=None,
                 minimum=None, maximum=None,
                 exclusive=True, todo=None):
        '''Construct a new TimeGoal.
        Specify a variable object reference to be monitored.
        Optionally specify a "minimum" limit value to compare against.
        Optionally specify a "maximum" limit value to compare against.
        If "exclusive" is set True, value must exceed the given range
        before the goal is considered achieved or finished.
        If "exclusive" is set False, value must meet or exceed the range.
        Optionally specify a callable hook function with a "todo" argument.
        '''
        super(RangeGoal, self).__init__(todo=todo)
        if isinstance(variable, (type(None), type(int), type(float))):
            raise ValueError, 'cannot monitor a scalar variable type'
        self.variable = variable
        self.minimum = minimum
        self.maximum = maximum
        self.exclusive = exclusive
        self.progress()

    def progress(self):
        '''Fractional progress is hard to measure in a general way since
        the RangeGoal cannot guess when the variable will go out of range.
        This class returns 1.0 for complete (a limit value hit), else 0.0.
        '''
        var = self.variable
        min = self.minimum
        max = self.maximum
        if self.exclusive:
            if min is not None and cmp(var, min) <= 0:
                self.trigger()
                return 1.0
            if max is not None and cmp(var, max) >= 0:
                self.trigger()
                return 1.0
        else:
            if min is not None and cmp(var, min) < 0:
                self.trigger()
                return 1.0
            if max is not None and cmp(var, max) > 0:
                self.trigger()
                return 1.0
        return 0.0

#----------------------------------------------------------------------------

def __time__(*args):
    return time.time()

class TimeGoal (Goal):

    '''A TimeGoal will trigger its callback when a given time passes,
    or when a given timespan into the future has elapsed.
    If an absolute time is not given, time is measured from the first
    call to check progress, not from the construction of the goal.
    '''

    def __init__(self, when=None, dt=0., todo=None):
        '''Construct a new TimeGoal.
        Optionally specify an absolute time to finish with a "when" value.
        Optionally specify a relative time to finish with a "dt" value.
        (If both are given, when+dt is the finish target.)
        Optionally specify a callable hook function with a "todo" argument.
        '''
        super(TimeGoal, self).__init__(todo=todo)
        self.start = None
        self.when = when
        self.dt = dt
        self.now = __time__
        if when is not None:
            self.begin()

    def begin(self):
        '''This is called to realize state on the first progress check.
        If the absolute completion time is not specified at construction,
        then the completion time is measured relative to the current time.
        '''
        self.start = self.now()
        if self.when is None:
            self.when = self.start
        self.when += self.dt
        self.progress()

    def progress(self):
        '''Monitor the progress of the goal. Returns a value between 0.0
        on the first call, and 1.0 when the completion time has arrived.
        '''
        if self.start is None:
            self.begin()
        now = self.now()
        if now < self.start:
            return 0.0
        if now >= self.when:
            self.trigger()
            return 1.0
        return interpolations.linear(self.start, self.when, now, 0.0, 1.0)

#----------------------------------------------------------------------------

class ChangeGoal (TimeGoal):

    '''Changes a specified object variable, interpolated over time.
    Each time the goal is checked for progress or completion, the value of
    the given variable is updated to an interpolated value towards a curve.
    The variable must be a mutable object which supports certain operators.
    The value of the variable is interpolated along a linear or bezier curve.
    If just a delta timespan is given, the curve starts at the variable
    value measured from the first call to check progress, not from the
    construction of the goal.
    '''

    def __init__(self,
                 variable=None, controls=None, relative=False,
                 when=None, dt=0., todo=None):
        '''Construct a new ChangeGoal.
        Specify a single value or list of bezier points for "controls".
        Optionally specify an absolute time to finish with a "when" value.
        Optionally specify a relative time to finish with a "dt" value.
        (If both are given, when+dt is the finish target.)
        Optionally specify a callable hook function with a "todo" argument.
        '''
        if isinstance(variable, (type(None), type(int), type(float))):
            raise ValueError, 'cannot monitor a scalar variable type'
        self.variable = variable
        self.relative = relative
        if controls is None:
            controls = []
        if not isinstance(controls, (list, tuple)):
            controls = [ None, controls ]
        self.controls = list(controls)
        super(ChangeGoal, self).__init__(when=when, dt=dt, todo=todo)

    def __str__(self):
        string = self.__class__.__name__ + "("
        string += str(self.variable)
        string += "->"
        string += str(self.controls[-1])
        string += ")"
        return string

    def begin(self):
        '''This is called to realize state on the first progress check.
        If the curve is specified as being relative, control points are
        calculated from the current value of our monitored variable.
        If the first curve control is None, we use the current value of
        the monitored variable.  This allows goals to be sequenced neatly.
        '''
        if self.relative:
            for i in range(len(self.controls)):
                if self.controls[i] is not None:
                    self.controls[i] = self.variable + self.controls[i]
        if self.controls[0] is None:
            self.controls[0] = 1*self.variable
        super(ChangeGoal, self).begin()

    def update(self, value):
        '''Internal hook to actually set the monitored variable value
        as the goal time target approaches.  This class assumes that the
        variable type offers support for __imul__() and __iadd__() methods,
        so as to allow for in-place adjustment of the variable value.
        If this is not the case, override this method with appropriate logic.
        '''
        self.variable *= 0.0
        self.variable += value

    def trigger(self):
        '''The monitored variable will be set to the final control value
        upon completion, to ensure smooth transitions if goals are stacked.
        '''
        self.update(self.controls[-1])
        super(ChangeGoal, self).trigger()

    def progress(self):
        '''As time passes toward the desired completion target time, the
        monitored variable value will be interpolated along a bezier curve.
        At progress 0.0, the variable is at the value of controls[0].
        At progress 1.0, the variable is at the value of controls[-1].
        '''
        i = super(ChangeGoal, self).progress()
        if i >= 1.0:
            return 1.0
        if i < 0.0:
            value = self.controls[0]
        else:
            value = interpolations.bezier(i, *self.controls)
        self.update(value)
        return i

#----------------------------------------------------------------------------

class CompoundGoal (Goal):

    '''Internal type to make compounds like SerialGoal and ParallelGoal.'''

    def __init__(self, goals=None, todo=None):
        if goals is None:
            goals = []
        if isinstance(goals, Goal):
            goals = [ goals ]
        self.goals = goals
        super(CompoundGoal, self).__init__(todo)

    def __str__(self):
        string = self.__class__.__name__
        return string + "(" + ', '.join([str(x) for x in self.goals]) + ")"

class SerialGoal (CompoundGoal):

    '''A SerialGoal is an ordered series of subgoals.
    Later goals are not monitored for progress until earlier goals report
    their completion.  Those subgoals that finish will be removed from
    the series.
    '''

    def __init__(self, goals=None, todo=None):
        '''Creates a goal to accomplish a series of goals in order.
        Optionally give a list of sub-"goals" to be active immediately.
        Goals may be added at any time, even after construction.
        '''
        super(SerialGoal, self).__init__(goals, todo)

    def __str__(self):
        string = self.__class__.__name__
        return string + "(" + ', '.join([str(x) for x in self.goals]) + ")"

    def progress(self):
        '''May be called anytime to evaluate or effect progress.
        Calls the first subgoal for progress().  If it is now done, it
        calls the next one, and so on.  Our progress is the ratio of
        done versus total goals added.  The ratio counts all goals added,
        including the ones which have been completed and removed.
        '''
        count = len(self.goals)
        if count > 0:
            done = 0
            for i in range(count):
                goal = self.goals[i]
                if not isinstance(goal, Goal) or goal.done():
                    self.goals[i] = None
                    done += 1
                else:
                    break
            if done < count:
                return float(done) / count
        self.trigger()
        return 1.0

    def append(self, goal):
        '''Adds another subgoal to the end of the chain at any time.
        Adding more goals will cause progress ratio to drop numerically,
        so callers should not assume the progress() value always increases.
        '''
        self.goals.append(goal)

    def prepend(self, goal):
        '''Adds another subgoal to the beginning of the chain at any time.
        The current partially-done subgoal will not be checked for
        further progress until this new subgoal has been done.
        Will cause our progress() ratio to drop, of course.
        '''
        self.goals.insert(0, goal)

#----------------------------------------------------------------------------

class ParallelGoal (CompoundGoal):

    '''A ParallelGoal is an unordered set of subgoals.
    Goals are not monitored for progress in any particular order;
    each one is checked on every request for the progress of the set.
    Those subgoals that finish will be removed from the set.
    '''

    def __init__(self, goals=None, todo=None):
        '''Creates a goal to accomplish a set of goals in any order.
        Optionally give a list of sub-"goals" to be active immediately.
        Goals may be added at any time, even after construction.
        '''
        super(ParallelGoal, self).__init__(goals, todo)

    def progress(self):
        '''May be called anytime to evaluate or effect progress.
        Calls each undone subgoal progress().  Do not assume any given
        ordering in how each subgoal is checked.  Our progress is the
        ratio of total subgoal progress versus total goals added.  The
        ratio counts all goals added, including the ones which have
        been completed and removed.
        '''
        count = len(self.goals)
        if count > 0:
            done = 0.0
            for i in range(count):
                goal = self.goals[i]
                amount = 1.0
                if goal is not None:
                    amount = max(0.0, min(goal.progress(), 1.0))
                if amount == 1.0:
                    self.goals[i] = None
                done += amount
            if done < count:
                return done / count
        self.goals = [ ]
        self.trigger()
        return 1.0

    def add(self, goal):
        '''Adds another subgoal at any time.
        All existing subgoals will continue to be called for further
        progress in parallel.  Will cause our progress() ratio to drop, of
        course.
        '''
        self.goals.append(goal)

#----------------------------------------------------------------------------

__test_t = 0
def __test_now():
    global __test_t
    now = __test_t
    __test_t += 1
    return now

def __test__():
    from testing import __ok__, __report__

    print 'Testing goals...'

if __name__ == '__main__':
    raise Exception, \
        'This module is not a stand-alone script.  Import it in a program.'


Contact Ed Halley by email at ed@halley.cc.
Text, code, layout and artwork are Copyright © 1996-2013 Ed Halley.
Copying in whole or in part, with author attribution, is expressly allowed.
Any references to trademarks are illustrative and are controlled by their respective owners.
Make donations with PayPal - it's fast, free and secure!