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:
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, Polyglot 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:
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
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:
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 --> [*]
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
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
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.
Top comments (0)