Diagramming Finite State Machines with Mermaid.js

Diagramming Finite State Machines with Mermaid.js

Creating UML State Diagrams with markdown to illustrate FSMs and HFSMs

In this article we’ll take a look at how Mermaid.js can help you transform simple markdown into state diagrams suitable for illustrating a finite state machine, hierarchical state machine, or the standard complexities of software systems.

What are Finite State Machines (FSMs)?

A year or two ago I built a small game prototype that featured a boss fight with a crab monster that was powered by a finite state machine. This monster waited for the player to enter its arena, then descended from the ceiling, roared a challenge, and began fighting the player.

The monster was only damageable after it finished descending. Taking enough damage would make the monster react in pain before it could attack again. Hurting the monster enough caused it to die.

So what is a finite state machine (FSM)?

A finite state machine is a set of inter-related states that reacts to events by moving between different states in a controlled manner.

In this example, the states the boss could be in included descending, attacking, reacting to pain, and dying.

This boss fight could be represented by the following Mermaid state machine:

A finite state machine

In this state machine we start at the leftmost dark circle, move to the Descending state, and then move between states until we reach the Dead state and the double circle at the right edge of the diagram. Once we reach the final circle, the state machine terminates and is not evaluated further.

Building Simple Finite State Machines with Mermaid.js

You can build a state machine like the one above fairly easily with Mermaid.js and markdown.

Using Mermaid.js, you use a mermaid-compatible environment such as GitHub markdown, Polylgot Notebooks, the online live editor, or Obsidian. Once in that environment, you can begin a code block and specify the programming language as mermaid and then enter in markdown like the following:

stateDiagram-v2
    [*] --> Descending
    Descending --> Attack
    Attack --> Pain
    Pain --> Attack
    Attack --> Dead
    Pain --> Dead
    Dead --> [*]

This markdown generates the following Mermaid.js Finite State Machine diagram:

A finite state machine

Here we declare that we want a state diagram by specifying stateDiagram-v2.

Next we declare the various transitions between states by writing the name of the state and the state it can transition to. States may transition to multiple other states. For example ,the attack state may transition to pain or to dead.

The first state is represented by using [*] to the left of the arrow and the last state is represented by [*] to the right of the arrow.

Note that this diagram is identical to the one we saw earlier, except that it is arranged from top to bottom instead of from left to right. If you want to generate a left to right Mermaid.js finite state machine diagram, you can add the line direction LR after the stateDiagram-v2 line.

Highlighting Relationships in Mermaid.js FSMs

If you want to be explicit about the reasons for transferring from one state to another, you can add optional descriptions to each transition by adding a : and then the additional comments to the right of the relationship as shown with the following markdown:

stateDiagram-v2
    [*] --> Descending      : Player entered arena
    Descending --> Attack   : After roar animation
    Attack --> Pain         : Hurt a lot
    Pain --> Attack         : Finished animation
    Attack --> Dying        : Ran out of health
    Pain --> Dying          : Ran out of health
    Dying --> [*]           : After death animation

A finite state machine

These labels tend to produce busier diagrams, but the extra text can add valuable information as well.

Building Hierarchical Finite State Machines (HFSM) with Mermaid.js

One problem with traditional finite state machines is that you can get an almost combinatorial explosion of relationships between states the more you add new states to your finite state machine.

To combat this, you can nest state machines inside of other state machines to create a hierarchy of sorts.

Due to the hierarchical nature of these state machines, we call these nested state machines hierarchical finite state machines or HFSMs for short.

Nesting finite state machines can make the different states much more easy to manage while also making larger transitions more apparent as shown in the diagram below:

A finite state machine

Declaring hierarchical finite state machines in Mermaid.js is somewhat straightforward, though the syntax involved is a bit different:

stateDiagram-v2
    direction LR
    
    state intro {
        [*] --> Descending
        Descending --> Roar
        Roar --> [*]
    }
    state combat {
        [*] --> Attacking
        Attacking --> Pain
        Pain --> Attacking
    }
    state defeated {
        [*] --> Dying
        Dying --> Dead
        Dead --> [*]
    }

    [*] --> intro
    intro --> combat
    combat --> defeated
    defeated --> [*]

A finite state machine

Here we declare 3 large root-level states named intro, combat, and defeated.

Inside of each state we list the various states inside of that larger state and how they transition between each other.

We also list how the three states relate to one another at the bottom of the markdown. In this case the three states form a sequence, but states will often cycle between each other more frequently than this.


In the diagram above, each state linked to the state next in sequence, but you can also link to states inside of a parent state by mentioning them explicitly as shown in the following markdown:

stateDiagram-v2
    direction LR

    state intro {
        Descending --> Roar
        Roar --> Attacking
    }
    state combat {
        Attacking --> Pain
        Pain --> Attacking
    }
    state defeated {
        Dying --> Dead
    }

    [*] --> Descending
    combat --> Dying
    Dead --> [*]

    note left of combat: The boss is damageable in this state

A finite state machine

Here the various states appear significantly more simple because we’re relying less on [*] nodes to communicate state entry and exit and more on direct transitions between states.

This diagram does still have a transition from the entire combat state to the dying state within defeated. This is to indicate that any state inside of combat can transition directly to dying if it needs to.

Also note that you can declare a note to the left or right of any state to annotate things that need special attention.

Finally, it is possible to use hierarchical finite state machines and messages for transitions between states in the same diagram, though the result gets a bit messy:

stateDiagram-v2
    direction LR

    state intro {
        Descending --> Roar : Movement Finished
        Roar --> Attacking  : Animation Finished
    }
    state combat {
        Attacking --> Pain  : Took Enough Damage
        Pain --> Attacking  : Animation Finished
    }
    state defeated {
        Dying --> Dead      : Animation Finished
    }

    [*] --> Descending      : Spotted player
    combat --> Dying        : Took enough damage
    Dead --> [*]            : AI Stopped

    note left of combat: The boss is damageable in this state

A finite state machine

Final Thoughts

I think that Mermaid.js finite state machine diagrams are pretty interesting and help convey the possible states a system or agent might be in.

State machine diagrams in Mermaid.js can do more than just emulate finite state machines and hierarchical finite state machines and I’d encourage you to read the Mermaid.js documentation for features such as decisions, forking, and even concurrency.

If you like some of the features of these diagrams but want additional flexibility, you may want to check out Mermaid.js flowcharts instead.

As for me, I plan on using Mermaid.js for a state diagram the next time I design an AI agent or system with enough complexity in its states and state transitions.