Finite State Machine with Timer base class

Many finite state machines need some way of tracking the passage of time, such as days, or some other measure of progress. This extension to the FSM class adds that functionality. The time aspect is measured in abstract ticks, so you can choose what kind of time period is being used.

Constructor

This class inherits from the FSM class and adds some new members:

_ticksM
A count of how many ticks have been spent in the current state.
_timerM
A countdown of ticks until the timer elapses.
_timerTrigM
A boolean which indicates if the timer reached zero.

Note: There's a syntactical difference between the way the base class is initialised between Ren'Py7/Python2 and Ren'Py8/Python3. Use the tabs below to select the appropriate code for your version.

classes/fsmTimer.rpy

init python:
    class FsmTimer(Fsm):
        """
        Finite State Machine with ticks and count down timer.

        Ticks counts up and resets when the state changes.
        Timer counts down from a supplied value. When it reaches zero timerTrig
        is set and timerTrigger() is called.
        """

        # ---------------------------------------------------------------------
        # Constructor
        # - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

        def __init__(self, stateNames=None):
            """
            Construct a finite state machine with timers and initial state 0.

            Optionally provide a renpy.store name of sequence of state names.
            This can be a list or a tuple of strings.
            
            :param stateNames: Optional store name of a sequence of state names
            """
            super(FsmTimer, self).__init__(stateNames)   # Python 2
            self._ticksM = 0
            self._timerM = 0
            self._timerTrigM = False


init python:
    class FsmTimer(Fsm):
        """
        Finite State Machine with ticks and count down timer.

        Ticks counts up and resets when the state changes.
        Timer counts down from a supplied value. When it reaches zero timerTrig
        is set and timerTrigger() is called.
        """

        # ---------------------------------------------------------------------
        # Constructor
        # - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

        def __init__(self, stateNames=None):
            """
            Construct a finite state machine with timers and initial state 0.

            Optionally provide a renpy.store name of sequence of state names.
            This can be a list or a tuple of strings.
            
            :param stateNames: Optional store name of a sequence of state names
            """
            super().__init__(stateNames)                 # Python 3
            self._ticksM = 0
            self._timerM = 0
            self._timerTrigM = False

Timer methods

Tick

This method is used to record clock ticks from wherever they come from. A common source might be any end-of-day processing routine. It does the following:

classes/fsmTimer.rpy (cont.)

        # ---------------------------------------------------------------------
        # Timer methods
        # - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

        def tick(self):
            """
            Register a clock tick.

            The ticks value is incremented.
            The timer value is decremented is it is positive.
            If it reaches zero then timerTrig is set and timerTriggered is called.
            """
            self._ticksM += 1
            if self._timerM > 0:
                self._timerM -= 1
                if self._timerM == 0:
                    self._timerTrigM = True
                    self.timerTriggered()

Timer Triggered

This is an empty method as it is intended to be overridden if needed in a derived class.

classes/fsmTimer.rpy (cont.)

        def timerTriggered(self):
            """
            Called when timer reaches zero.

            Intended to be overridden.
            """
            pass

Accessors

As well as adding accessors for the new members, some of the accessors from the base class need to be overridden to support the new functionality.

Reset

The reset method also needs to clear down the timer components as well as the state in the base class.

Note: There's a syntactical difference between the way the base class reset() is invoked between Ren'Py7/Python2 and Ren'Py8/Python3. Use the tabs below to select the appropriate code for your version.

classes/fsmTimer.rpy (cont.)

        # ---------------------------------------------------------------------
        # Accessors
        # - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

        def reset(self):
            """
            Reset to state zero and cleat ticks and timer.
            """
            super(FsmTimer, self).reset()   # Python 2
            self._ticksM = 0
            self._timerM = 0


        # ---------------------------------------------------------------------
        # Accessors
        # - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

        def reset(self):
            """
            Reset to state zero and cleat ticks and timer.
            """
            super().reset()                 # Python 3
            self._ticksM = 0
            self._timerM = 0

State

For the _ticksM counter to be useful it needs to be reset when the state changes. To do this the state property setter needs to be replaced. The error checking is identical to before, but before assigning the new value a check is made to see if it is different. If it is _ticksM is set to zero. If it is the same then _ticksM is not reset.

classes/fsmTimer.rpy (cont.)

        @Fsm.state.setter
        def state(self, value):
            """
            Set the state number.

            The state number cannot be None or negative.
            If a sequence of state names was provided the value must be a
            valid index.
            If the state changes as a result then reset the ticks counter.

            :param value: The new state number
            """
            if value is None:
                raise ValueError("State cannot be set to None.")
            if not isinstance(value, int):
                raise ValueError("State cannot be set to a non-integer value ({})".format(value))
            if value < 0:
                raise ValueError("State cannot be set to a negative value ({}).".format(value))
            if self._stateNamesM is not None:
                names = self._lookupNames()
                if value >= len(names):
                    raise ValueError("State cannot be set outside known states 0..{} ({}).".format(len(names)-1, value))
            # If state changes, reset ticks.
            if self._stateNumM != value:
                self._ticksM = 0
            self._stateNumM = value

Ticks

The ticks is defined as a property so it can be read, but not (easily) assigned to.

classes/fsmTimer.rpy (cont.)

        @property
        def ticks(self):
            """
            The number of ticks since the last state change.
            """
            return self._ticksM

Timer

The timer is defined as a property so it can be accessed as though it were a member, but sanity checking can be performed when it is assigned to.

classes/fsmTimer.rpy (cont.)

        @property
        def timer(self):
            """
            The count down tick timer.
            """
            return self._timerM

        @timer.setter
        def timer(self, value):
            """
            Set the countdown timer and resets the triggered flag.

            :param value: The new value.
            """
            if value is None:
                raise ValueError("Timer cannot be set to None.")
            if not isinstance(value, int):
                raise ValueError("Timer cannot be set to a non-integer value ({})".format(value))
            if value < 0:
                raise ValueError("Timer cannot be set to a negative value ({}).".format(value))
            self._timerM = value
            self._timerTrigM = False

Timer Trigger

The timerTrig is defined as a property so it can be read as though it was a member. As this flag should only be cleared (never set, except through the tick() method) it is provided with a del operation that clears the flag.

classes/fsmTimer.rpy (cont.)

        @property
        def timerTrig(self):
            """
            Set when the count down tick timer reaches zero.
            """
            return self._timerTrigM

        @timerTrig.deleter
        def timerTrig(self):
            """
            Clear any timer trigger.
            """
            self._timerTrigM = False

Special methods

Since more members have been added it makes sense to add these to the string representation of these objects.

classes/fsmTimer.rpy (cont.)

        # ---------------------------------------------------------------------
        # Special methods
        # - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

        def __str__(self):
            """
            Generate a printable string describing the state.
            """
            names = self._lookupNames()
            if names is None:
                return "{} ticks={} timer={} trig={}".format(
                    self._stateNumM,
                    self._ticksM,
                    self._timerM,
                    self._timerTrigM\
                )
            return "{}:{} ticks={} timer={} trig={}".format(
                    self._stateNumM,
                    names[self._stateNumM],
                    self._ticksM,
                    self._timerM,
                    self._timerTrigM\
                )

Example use

This is really intended to be a base for a derived class that adds logic for transitions, but it can be used as it stands. First, create a state machine with three named states and print out its initial state number and name:


define  fsmTimerStateNames = ('init', 'wait', 'done')
default fsmTimer = FsmTimer('fsmTimerStateNames')

label exFsmTimer:
    "Initial state: [fsmTimer]"

Initial state: 0:init ticks=0 timer=0 trig=False

Each call to tick() will increment ticks if the state doesn't change.


    $ fsmTimer.tick()
    "After tick: [fsmTimer]"

After tick: 0:init ticks=1 timer=0 trig=False

Set the state to wait and the timer to two ticks:


    $ fsmTimer.stateName = 'wait'
    $ fsmTimer.timer = 2
    "After stateName='wait' and timer=2: [fsmTimer]"

After stateName='wait' and timer=2: 1:wait ticks=0 timer=2 trig=False

Call tick() until the timer triggers:


    while not fsmTimer.timerTrig:
        $ fsmTimer.tick()
        "After tick: [fsmTimer]"

After tick: 1:wait ticks=1 timer=1 trig=False
After tick: 1:wait ticks=2 timer=0 trig=True

Using del to clear the triggered flag:


    $ del fsmTimer.timerTrig
    "After del timerTrig: [fsmTimer]"

After del timerTrig: 1:wait ticks=2 timer=0 trig=False

Entering the done state clears ticks.


    $ fsmTimer.stateName = 'done'
    "After stateName='done': [fsmTimer]"

After stateName='done': 2:done ticks=0 timer=0 trig=False