FSM default transitions--always allow?

When using the “defaultTransitions” interface to define allowed transitions for an FSM-derived class, is there a way to indicate that a given state may transition to any state?

Yes, there is a way to define an “any” state in the default transitions. Just pass ["*"] as the state list.
Guess I should update the manual on that.

An example can be found here:

Strange–I thought that I tried that! o_0

And indeed, trying it again, it doesn’t seem to work.

Here below is a small test-program that displays the issue on my machine. On startup it transitions into a state named “Mew”, which is indicated to transition into “*”. On a press of the space-bar, it attempts to then transition into a state called “Purr”–at which point, on my machine at least, it crashes with a “RequestDenied” error.

from direct.showbase.ShowBase import ShowBase
from direct.fsm.FSM import FSM

from panda3d import __version__ as pandaVersion
print (pandaVersion)

import sys
print (sys.version)

class Game(ShowBase, FSM):
    def __init__(self):
        ShowBase.__init__(self)
        FSM.__init__(self, "mew")

        self.accept("escape", base.userExit)
        self.accept("space", self.test)

        self.defaultTransitions = {"Mew" : "*", "Purr" : "Nyan"}

        self.request("Mew")

    def test(self):
        self.request("Purr")

    def enterMew(self):
        print ("Mew!")

    def enterNyan(self):
        print ("Nyan!")

    def enterPurr(self):
        print ("Purr!")

    def exitMew(self):
        print ("Stopped mewing")

    def exitNyan(self):
        print ("Stopped nyaning")

    def exitPurr(self):
        print ("Stopped purring")

app = Game()
app.run()

[edit]
For reference, I’m running Python 3.6.9, and Panda3D 1.10.10, I believe.

Oh sorry, it seems it’s not in the 1.10.x branch but only in the main branch. Haven’t noticed it got merged in 2020 :stuck_out_tongue: so just a little to late for 1.10.

Tough, you could override the defaultFilter function in your derived class with the one from the master branch or with your own, you do need to import the RequestDenied from FSM and also add an enum class (I totally forgot that I’ve changed it to that instead of the “*” string… guess I have to update the manual again):

from direct.showbase.ShowBase import ShowBase
from direct.fsm.FSM import FSM, RequestDenied

from panda3d import __version__ as pandaVersion
print (pandaVersion)

import sys
print (sys.version)

class Game(ShowBase, FSM):
    def __init__(self):
        ShowBase.__init__(self)
        FSM.__init__(self, "mew")

        self.accept("escape", base.userExit)
        self.accept("space", self.test)

        self.defaultTransitions = {"Mew" : [self.EnumStates.ANY], "Purr" : ["Nyan"]}

        self.request("Mew")

    def test(self):
        self.request("Purr")

    def enterMew(self):
        print ("Mew!")

    def enterNyan(self):
        print ("Nyan!")

    def enterPurr(self):
        print ("Purr!")

    def exitMew(self):
        print ("Stopped mewing")

    def exitNyan(self):
        print ("Stopped nyaning")

    def exitPurr(self):
        print ("Stopped purring")


    # An enum class for special states like the DEFAULT or ANY state,
    # that should be treatened by the FSM in a special way
    class EnumStates():
        ANY = 1
        DEFAULT = 2

    def defaultFilter(self, request, args):
        """This is the function that is called if there is no
        filterState() method for a particular state name.
        This default filter function behaves in one of two modes:
        (1) if self.defaultTransitions is None, allow any request
        whose name begins with a capital letter, which is assumed to
        be a direct request to a particular state.  This is similar to
        the old ClassicFSM onUndefTransition=ALLOW, with no explicit
        state transitions listed.
        (2) if self.defaultTransitions is not None, allow only those
        requests explicitly identified in this map.  This is similar
        to the old ClassicFSM onUndefTransition=DISALLOW, with an
        explicit list of allowed state transitions.
        Specialized FSM's may wish to redefine this default filter
        (for instance, to always return the request itself, thus
        allowing any transition.)."""

        if request == 'Off':
            # We can always go to the "Off" state.
            return (request,) + args

        if self.defaultTransitions is None:
            # If self.defaultTransitions is None, it means to accept
            # all requests whose name begins with a capital letter.
            # These are direct requests to a particular state.
            if request[0].isupper():
                return (request,) + args
        else:
            # If self.defaultTransitions is not None, it is a map of
            # allowed transitions from each state.  That is, each key
            # of the map is the current state name; for that key, the
            # value is a list of allowed transitions from the
            # indicated state.
            if request in self.defaultTransitions.get(self.state, []):
                # This transition is listed in the defaultTransitions map;
                # accept it.
                return (request,) + args

            elif self.EnumStates.ANY in self.defaultTransitions.get(self.state, []):
                # Whenever we have a '*' as our to transition, we allow
                # to transit to any other state
                return (request,) + args

            elif request in self.defaultTransitions.get(self.EnumStates.ANY, []):
                # If the requested state is in the default transitions
                # from any state list, we also alow to transit to the
                # new state
                return (request,) + args

            elif self.EnumStates.ANY in self.defaultTransitions.get(self.EnumStates.ANY, []):
                # This is like we had set the defaultTransitions to None.
                # Any state can transit to any other state
                return (request,) + args

            elif request in self.defaultTransitions.get(self.EnumStates.DEFAULT, []):
                # This is the fallback state that we use whenever no
                # other trnasition was possible
                return (request,) + args

            # If self.defaultTransitions is not None, it is an error
            # to request a direct state transition (capital letter
            # request) not listed in defaultTransitions and not
            # handled by an earlier filter.
            if request[0].isupper():
                raise RequestDenied("%s (from state: %s)" % (request, self.state))

        # In either case, we quietly ignore unhandled command
        # (lowercase) requests.
        assert self.notify.debug("%s ignoring request %s from state %s." % (self._name, request, self.state))
        return None


app = Game()
app.run()

Ah, I see! Well, fair enough!

Hum… For my current case, I think that I’m probably fine with using the “Off” state for the purpose that I have in mind. I’d prefer to not use a built-in starting-state so, but given that it is available, it seems like the easier approach.

Thank you for your help–and indeed, for implementing such “universal transitions”, whether they’re in 1.10 or come only in a later version! :slight_smile:

The base FSM class defines a defaultFilter() method that implements the default FSM transition rules (that is, allow all direct-to-state (uppercase) transition requests unless self.defaultTransitions is defined; in either case, quietly ignore input (lowercase) requests).

https://docs.panda3d.org/1.10/python/programming/finite-state-machines/fsm-with-input

Thank you! :slight_smile:

And indeed, I’ve seen that, I believe.

But for one thing I don’t want all transitions from all states to be allowed–only those from a single, specific state.

And for another, I feel that implementing filter-based logic might be overkill for most of my purposes–for the most part, transition-sets (as set via “defaultTransitions”) are fine. I’m not sure that I want to implement filter-rules for all of my FSM-based classes just for the sake of having a single state be capable of transitioning to all others.

No, I think that for now I’m content to just use the “Off” state for this purpose.