pytransitions / transitions

A lightweight, object-oriented finite state machine implementation in Python with many extensions
MIT License
5.49k stars 524 forks source link

Ordered Transitions with Conditions Doesn't Take Into Account Initial State #539

Closed allistera closed 2 years ago

allistera commented 2 years ago

Describe the bug

Conditions within Ordered Transitions do not seem to be taken into account the initial state.

Minimal working example

from transitions import Machine

class NarcolepticSuperhero(object):

    states = ['asleep', 'hanging out', 'hungry']

    def __init__(self, initial):

        # Initialize the state machine
        self.machine = Machine(model=self, states=NarcolepticSuperhero.states, initial=initial)
        self.machine.add_ordered_transitions(loop=False, unless=["is_grounded", "has_food"])

    def is_grounded(self):
      return False

    def has_food(self):
      return True

print("Starting From Start:")
i = NarcolepticSuperhero("asleep")
print(i.state)
i.next_state()
print(i.state)
i.next_state()
print(i.state)

print("\nStarting From Hanging Out:")
i = NarcolepticSuperhero("hanging out")
print(i.state)
i.next_state()
print(i.state)

As you can see in the first example the transition from hanging out>hungry is blocked because has_food is True.

But in the second example when we set the initial state to hanging out the transition to hungry is permitted because it is incorrectly using the is_grounded function.

Expected behavior

When a list is passed to conditions, it takes into account the initial state.

aleneum commented 2 years ago

Hello @allistera,

the current behavior of add_ordered_transitions will assume the initial state to be the first state in the state sequence. This has been mentioned here:

Conditions will be applied in the states' order BUT start from the initial state (in your case bbb).

The related source code can be found here:

        # reorder list so that the initial state is actually the first one
        try:
            idx = states.index(self._initial)
            states = states[idx:] + states[:idx]
            first_in_loop = states[0 if loop_includes_initial else 1]

This means that your first transition will be valid with is_grounded and your second will always fail (has_food). Instead of changing the initial state you might want to use i.set_state instead. This keeps the order of states (and condition checks) as you put them in the states array.

allistera commented 2 years ago

Using set_state in the initializer works perfectly. Thanks @aleneum