Skip to content

Driver

Using state machines, it’s common to have a big switch statement (or lots of ifs), altering behaviour depending on the the current state. These behaviours in turn might trigger a state change. Since this is such a common pattern, the StateMachine.driver is provided.

With it, you set up state handlers for different states and guiding the machine to subsequent states.

Each handler has an if field, a single string or array of strings corresponding to the state(s) that handler applies to. While one handler can handle multiple different states, there can’t be multiple handlers per state.

The other part of the handler is then field. At its simplest, it is an object that tells what state to transition to, for example:

const handlers = [{
if: `sleeping`, // If we're in the 'sleeping' state
then: { next: 'walking' } // Go to 'walking' state
}]

The then field can be an array of functions, all of which return the same kind of object. When the handler is run, it executes these functions to determine what to do. Functions defined under then don’t have to return a value - they could just be things you want to run when the state machine is in that state.

const handlers = [{
if: `walking`, // If we're in the 'walking' state
then: [
() => { // Randomly either go to 'resting' or 'running' state next
if (Math.random() > 0.5) return { next: 'resting' }
else return { next: 'running' }
}
]
}];

Once we have the state machine and the handlers, the driver can be initialised with StateMachine.driver . This would likely happen once, when your sketch is initialised.

// Set up driver (note the use of await)
const driver = await StateMachine.driver(states, handlers);

And then, perhaps in a timing-based loop, call run(), which will execute a state handler for the current state.

// Call .run every second
setInterval(async () => {
await driver.run(); // Note use of 'await' again
}, 1000);

Here’s a complete example:

// States
const states = {
sleeping: 'waking',
waking: ['resting','sleeping'],
resting: ['sleeping', 'walking'],
walking: ['running', 'resting'],
running: ['walking']
};
const handlers = [
{
// If we're in the 'sleeping' state, move to next state
if: 'sleeping',
then: { next: true }
},
{
// If we're in the 'waking' state, randomly either go to 'resting' or 'sleeping' state
if: 'waking',
then: [
() => {
if (Math.random() > 0.5) {
return { next: 'resting' }
} else {
return { next: 'sleeping' }
}
}
]
}
];
// Set up driver
const driver = await StateMachine.driver(states, handlers);

Once you have the state machine and driver set up, you need to call .run() whenever you want the driver to do its thing. This might be called for example in a loop based on a timer.

driver.run();

If you use asynchronous event handlers, call await driver.run() instead.

Some other things to do with the driver:

// Check current state
driver.getValue(); // eg. 'resting'
// Manually transition state
driver.to('walking');

So far, handlers have returned an object describing what state to transition. Instead of hardcoding the state, you can use { next: true } to transition to next available state. An alternative is { reset: true }. When that is returned, the machine goes back to its initial state.

Each result can also have a score field. This is only useful if you have several results under then. By default, the highest scoring result determines what happens.

With this in mind, we can re-write the earlier example, assigning random scores for each possible next state:

...
{
if: 'waking',
then: [
// Two functions, each returns a result with a random score each time they are executed
() => { score: Math.random(), next: 'resting' },
() => { score: Math.random(), next: 'sleeping' }
]
}
...

In practice you might want to weight the random values so one choice is more or less likely than another.

Each handler also has an optional resultChoice field, which can be ‘first’, ‘highest’, ‘lowest’ or ‘random’. By default, ‘highest’ is used, picking the highest scoring result. In our example, we might use resultChoice: 'random' to evenly pick between choices. With that enabled, we no longer need scores.

...
{
if: 'waking',
resultChoice: 'random',
then: [
// Because of resultChoice 'random', the driver
// will randomly pick one of these options when in the 'waking' state
{ next: 'resting' },
{ next: 'sleeping' }
]
}
...

When calling driver.run(), a result is returned with some status information, if that’s needed:

const result = await driver.run();
result.value; // state at the end of .run()
result.visited; // string array of unique states that have been visited
result.machine; // original machine description

Demo

In the demo below, the driver is used to autonomously change states based on an ‘energy’ level, also affected by current activity.