Skip to content

Utilizing Automata4j

pavl_g edited this page Aug 27, 2023 · 19 revisions

This page describes the use of Automata4j to implement both forms of Finite-State-Automata, the Non-deterministic, and Deterministic patterns.

This tutorial discusses the following:

  • Create a Gradle project and add the dependencies.
  • The non-deterministic pattern (NDFSA).
  • The deterministic pattern (DFSA).
  • Recovering from a TransitionPathNotUniqueException.
  • Injecting an NDFSA path into the DFSA pattern.
  • Cascading transitions in a state machine.
  • More examples.

1) Create a Gradle project and add the dependencies:

Find the latest version at:

# initialize a gradle project and build
$ gradle init # select Java and JUnit
$ ./gradlew build
// add the Gradle dependency in build.gradle file
plugins {
    id 'application'
}

application {
    mainClass.set("com.example.Main")
}

repositories {
    // Use Maven Central for resolving dependencies.
    mavenCentral()
}

dependencies {
    implementation "io.github.software-hardware-codesign:automata4j:1.0.0-alpha"
}
# download the binaries and re-build the project
$ ./gradlew build

2) The non-deterministic pattern (NDFSA):

  • Recall, a SpaceShip traveling from point A to point B with speed [x].
  • Recall, state E is an Engine Start state, and state AB is a travel state traveling from point A to point B.
  • Recall, the path marked from state E to state AB is the transition path with an input value [x] representing the speed of the SpaceShip.
  • An NDFSA pattern can have multiple start states and doesn't assert the uniqueness of a transition path, which means a transition path from state E to state AB with an input value [x] may repeat without any runtime consequences.
  • In this case, if a transition path is defined from state E back to state AB with speed [x], the SpaceShip can repeat the previous transition path without any runtime consequences.

To implement this in a Game Loop using an NDFSA pattern, the following implementation routines are available:

  • Use a Transition object with a Next State assigned as the engine start state (state E), and a TransitionalListener to assign the concurrent travel state (state AB) when the transition to the start state has been completed.
/* define an NDFSA manager object */
final TransitionalManager transitionalManager = new TransitionalManager();
/* the input value is of type Speed and the tracer object is of type Vector3f 
   representing the location of the space ship after traveling the required distance */
/* the engine start state */
final AutoState<Speed, Vector3f> stateE = new EngineStart<>();
/* travels from point A back to point B*/
final AutoState<Speed, Vector3f> stateAB = new TravelPath<>();
/* Assigns the engine start state */
transitionalManager.assignNextState(stateE);
/* Use the NDFSA transitional manager to traverse through the states with pre-defined input values */
transitionalManager.transit(new TransitionalListener<Speed, Vector3f>() {
    @Override
    public void onTransition(AutoState<Speed, Vector3f> presentState) {
        /* assigns the next state and starts the transition */
        transitionalManager.assignNextState(stateAB);
        /* transits to stateAB without dispatching an event on finishing transition */
        transitionalManager.transit(null);
    }
}); 
  • Use a TransitionPath object with 2 AutoStates, a present state, and a next state, the TransitionalManager assigns and transits to the present state and then assigns the next state, but it will never transit to the next state unless an explicit dispatch to the transit function is invoked in a registered TransitionalListener object.
/* define an NDFSA manager object */
final TransitionalManager transitionalManager = new TransitionalManager();
/* the input value is of type Speed and the tracer object is of type Vector3f 
   representing the location of the space ship after traveling the required distance */
/* the engine start state */
final AutoState<Speed, Vector3f> stateE = new EngineStart<>();
/* travels from point A back to point B*/
final AutoState<Speed, Vector3f> stateAB = new TravelPath<>();
/* Creates a transition path from stateE to stateAB */
final TransitionPath<Speed, Vector3f> transitionPath = 
                                               new TransitionPath<>("EngineStart-->StateAB", 
                                                                           stateE, stateAB);
/* Use the NDFSA transitional manager to traverse through the states with pre-defined input values */
transitionalManager.transit(transitionPath, new TransitionalListener<Speed, Vector3f>() {
    @Override
    public void onTransition(AutoState<Speed, Vector3f> presentState) {
        /* transits to stateAB without dispatching an event on finishing transition */
        transitionalManager.transit(null);
        /* the previous transition path can repeat again since this pattern is an NDFSA */
    }
}); 

3) The deterministic pattern (DFSA):

  • Recall, that the SpaceShip from the previous example is not capable of repeating the transition path that starts by starting the engine and then commands the SpaceShip to travel from point A to point B (in the context of if point B is for example a new planet or a new Galaxy).
  • A DeterministicManager comes into play, in which the path "EngineStart-->StateAB" is asserted to be unique and not repeat.
/* define a DFSA manager object */
final TransitionalManager transitionalManager = new DeterministicManager();
/* the input value is of type Speed and the tracer object is of type Vector3f 
   representing the location of the space ship after traveling the required distance */
/* the engine start state */
final AutoState<Speed, Vector3f> stateE = new EngineStart<>();
/* travels from point A back to point B*/
final AutoState<Speed, Vector3f> stateAB = new TravelPath<>();
/* Creates a transition path from stateE to stateAB */
final TransitionPath<Speed, Vector3f> transitionPath = 
                                               new TransitionPath<>("EngineStart-->StateAB", 
                                                                           stateE, stateAB);
/* Use the NDFSA transitional manager to traverse through the states with pre-defined input values */
transitionalManager.transit(transitionPath, new TransitionalListener<Speed, Vector3f>() {
    @Override
    public void onTransition(AutoState<Speed, Vector3f> presentState) {
        /* transits to stateAB without dispatching an event on finishing transition */
        transitionalManager.transit(null);
        /* subsequent dispatch using this transition path will result in a TransitionPathNotUniqueException */
    }
}); 

4) Catching and recovering from TransitionPathNotUniqueException:

  • Catching and recovering from a TransitionPathNotUniqueException is one of the keys to best practices of using a DFSA pattern.
  • It's your choice of the type of exception recovery you choose, simple implementations can recover that by displaying a GUI dialog or a user message telling the user that this action (of utilizing this transition path again) is prohibited.
  • Other advanced implementations can recover this exception by suggesting other transition paths for the SpaceShip.
  • Other implementations will internally increment a counter for the repeated transition paths and recover from this exception by changing the name of the transition path and re-using it (See DFSA Hacking below).
  • This snippet shows an example of recovering by using other paths:
transitionalManager.transit(transitionPath, new TransitionalListener<Speed, Vector3f>() {
    @Override
    public void onTransition(AutoState<Speed, Vector3f> stateE) {
        /* transits to stateAB without dispatching an event on finishing transition */
        try {
            transitionalManager.transit((stateAB) -> 
                 transitionalManager.transit(transitionPath, null));
        } catch (TransitionPathNotUniqueException e) {
            Logger.getLogger("Suggesting other paths").fine("Using other paths");
            final AutoState<Speed, Vector3f> stateBA = new TravelPath<>();
            /* reallocates a new chunk for the new transition path */
            /* This new transition path achieves the same previous transition path goals 
               which is to travel from point A to point B*/
            /* This new transition path commands the SpaceShip to travel from 
               point B back to point A, then from point A again to point B*/
            transitionPath = new TransitionPath(transitionPath.getName(), stateBA, stateAB);
            transitionalManager.transit(transitionPath, 
                                          (localStateBA) -> transitionalManager.transit(null));
        }      
    }
}); 

5) Injecting an NDFSA path into the DFSA pattern:

  • There are 2 known ways (or at least hacks!) that could break the TransitionPath uniqueness assertion in a DFSA pattern:
  • Using a Transition object instead and simulating a transition path, repeating the same path again.
  • Changing the transition path object name using TransitionPath#setName(String) before transiting using this path will bypass the assertion of uniqueness.
  • By bypassing the assertion of transition paths' uniqueness, the DFSA pattern can now have some NDFSA transition patterns, hence a hybrid Finite-State-Automaton pattern is created.
  • By catching the TransitionPathNotUniqueException, and recovering from it by changing the transition path name (see the previous) or extracting the transition path and assigning its states directly to the transitional manager (machine) object.
// hacking the DFSA pattern by changing the transition path name!
transitionalManager.transit(transitionPath, new TransitionalListener<Speed, Vector3f>() {
    @Override
    public void onTransition(AutoState<Speed, Vector3f> stateE) {
        /* transits to stateAB without dispatching an event on finishing transition */
        transitionalManager.transit((stateAB) -> {
             transitionPath.setName(transitionPath.getName() + "*");
             /* runtime consequences have been hypnotized */
             transitionalManager.transit(transitionPath, null);
        });        
    }
}); 
// hacking the DFSA pattern by extracting the machine states from a transition path!
transitionalManager.transit(transitionPath, new TransitionalListener<Speed, Vector3f>() {
    @Override
    public void onTransition(AutoState<Speed, Vector3f> stateE) {
        /* transits to stateAB without dispatching an event on finishing transition */
        try {
            transitionalManager.transit((stateAB) -> 
                 transitionalManager.transit(transitionPath, null));
        } catch (TransitionPathNotUniqueException e) {
            Logger.getLogger("Extracting states from the transition path")
                  .fine("Using the same transitions");
            transitionalManager.assignNextState(transitionPath.getPresentState());
            transitionalManager.transit((presentState) -> {
                 transitionalManager.assignNextState(transitionPath.getNextState());
                 transitionalManager.transit(null);
            });
        }
    }
});

6) Cascading transitions in a state machine:

  • To cascade transitions in a state machine, a data structure is required to store the AutoStates for the TransitionalManager, the abstraction on the TransitionalManager can then assign and transit the states accordingly.
  • The best abstract data structure that is compatible with the finite-state-automaton is the Queue.
  • To avoid code duplicates, the adaptation pattern CascadedTransition adapts the base TransitionPath to accommodate more than 2 AutoStates via a Queue object.
  • The Queue introduced in the CascadedTransition is an abstract data type (ADT) that is left to the user implementation, however, there are some popular implementations provided through CascadedTransition.QueueImplementation in addition to the default implementation.
  • A Queue generally operates in a first-in-first-out (FIFO) principle, though its operation in Java is implementation-wise, for example, a Deque (pronounced D.K) (aka. Double-ended Queue) is a base implementation for the Queue that provides the ability to operate in a last-in-first-out (LIFO) order, and there are other implementations that can remove specific items from the middle of the collection ignoring the original order (FIFO order).
  • An ArrayDeque provides a circular array data structure for a Queue, very useful for inserting states at the beginning of the array (using the CascadedTransition#assignPresentState(AutoState)) or at the end of the array (using the CascadedTransition#assignNextState(AutoState)).
  • A LinkedList is another implementation of the Deque that can operate in a LIFO fashion, LinkedList provides a nodal-system to control adding/removing nodes, both the head and the tail can be involved in both the operations.

7) For advanced usages, see the examples: