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.rpyinit 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:
-
Increments the
_ticksM
counter. -
If the countdown timer
_timerM
is non-zero it decrements it. If it reaches zero it sets the_timerTrigM
boolean and calls the methodtimerTriggered()
.
# --------------------------------------------------------------------- # 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.
# --------------------------------------------------------------------- # 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.
@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.
@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.
@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.
@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