-
Notifications
You must be signed in to change notification settings - Fork 1
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.
- 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.
# 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
- 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 aTransitionalListener
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, theTransitionalManager
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 registeredTransitionalListener
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 */
}
});
- 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 */
}
});
- 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));
}
}
});
- 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);
});
}
}
});
- To cascade transitions in a state machine, a data structure is required to store the
AutoStates
for theTransitionalManager
, the abstraction on theTransitionalManager
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 baseTransitionPath
to accommodate more than 2AutoStates
via a Queue object. - The
Queue
introduced in theCascadedTransition
is an abstract data type (ADT) that is left to the user implementation, however, there are some popular implementations provided throughCascadedTransition.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, aDeque
(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 theCascadedTransition#assignPresentState(AutoState)
) or at the end of the array (using theCascadedTransition#assignNextState(AutoState)
). - A
LinkedList
is another implementation of theDeque
that can operate in a LIFO fashion, LinkedList provides a nodal-system to control adding/removing nodes, both thehead
and thetail
can be involved in both the operations.