My First Finite State Machine

Cover Image for My First Finite State Machine

First of all I hope that everyone is staying safe during this Pandemic!

Given the current situation, I was involved in a couple of group chats which involved quizzes which had Movie titles described in Emojis. I thought this was quite fun, but what I saw was depending on the size of the group and how available people were, that often people would reply with answers in the group chat which meant that others didn't get a chance to play the whole quiz without seeing the answers and it also meant that you often kept losing track of the questions.

I thought it would be fun to play around with creating a simple quiz game online for this purpose 😀.

I spun up a Nuxt application, created a Firebase project so I could use the hosting and Firestore services, and then I started to draw out my simple idea (which can be seen online here).

Finite State Machines (FSM)

I have been hearing a lot about FSM through colleagues and the Twitterverse and thought this would be a good opportunity for me to dip my toe in and try one out.

What are Finite State Machines

Let's start at the beginning to see what FSMs are and how they can be useful. An FSM is a way to describe a finite number of states in which the application can be in exactly one of at any given time. This means that I can describe all the states that my application could be in and I can be sure that it will only be in one of those states.

How did I create the FSM

I started with a pretty rough game component which looked similar to the following:

1<template>
2 <main>
3 <Quiz v-if="!completed" : questions="questions" />
4 <Results v-if="completed && answered" : results="results" />
5 </main>
6</template>

This might not look too crazy as this application is fairly simple but it does allow for the application to be in a state where if the game is completed but there are no answers the user will see nothing. Easily solvable, sure, but then that's no fun and I want to play with an FSM 😜.

I decided to create my first FSM using xstate and the state machine looks similar to this:

1import {
2 Machine,
3 assign
4} from 'xstate'
5const gameMachine = Machine({
6 id: 'game',
7 initial: 'play',
8 context: {
9 answered: 0,
10 guesses: {}
11 },
12 states: {
13 play: {
14 on: {
15 RESOLVE: {
16 target: 'results',
17 actions: assign({
18 answered: (context, event) => event.answered,
19 guesses: (context, event) => event.guesses
20 })
21 }
22 }
23 },
24 results: {
25 on: {
26 RESET: 'play'
27 },
28 initial: 'hide',
29 states: {
30 hide: {
31 on: {
32 TOGGLE: 'show'
33 }
34 },
35 show: {
36 on: {
37 TOGGLE: 'hide'
38 }
39 }
40 }
41 }
42 }
43})

What I found incredibly useful when developing this was the online visualizer which allowed me to toggle through all the states and see it in action.

Breaking this down I have two states play or results, within results there are two child states which allow the user to show/hide the answers. It's pretty simple. The actions within the play state allow me to send data to the gameMachine so it can hold additional context. My GameComponent now looks like:

1<main>
2 <component
3 :is="currentComponent"
4 :questions="questions"
5 :answers="answers"
6 :answered="context.answered"
7 :guesses="context.guesses"
8 :show-answers="current.matches('results.show')"
9 @answer="validate"
10 @reset="send('RESET')"
11 @toggleAnswers="send('TOGGLE')"
12 />
13</main>

I use Vue's dynamic component to render the component based on which state the machine is in, the components $emit to the parent component which then sends a signal to the gameMachine to instruct it to move to the next state. This means that my game can never be in a state where nothing is visible!

Using the FSM with Vue

xstate provides a simple way to use a State Machine with Vue and that is to use a interpreter. This handles things the state transitions, executing events and many more. I would recommend reading up on the docs for the full list of what it does.

To use this within Vue / Nuxt, within your component you will need to import or create your machine (recommend you keep your machines outside of the component so they can be shared/tested in isolation of the component), then start the machine and listen to any state transitions so you can update the state. That's a lot to take in, let's see some code:

1<template>
2 <div> My Current State {{ current.value }} <button @click="toggle()"> </div>
3</template>
4<script>
5import { Machine, interpret } from 'xstate'
6const demoMachine = Machine({
7 id: 'demo',
8 initial: 'hello',
9 context: {
10 answered: 0,
11 guesses: {}
12 },
13 states: {
14 hello: {
15 on: { TOGGLE: 'goodbye' }
16 },
17 goodbye: {
18 on: { TOGGLE: 'hello' }
19 }
20 }
21})
22
23export default {
24 data: () => ({
25 demoMachine: interpret(demoMachine),
26 current: demoMachine.initialState
27 }),
28 created () {
29 // Start service on component creation
30 this.demoMachine
31 .onTransition((state) => {
32 // Update the current state component data property with the next state
33 this.current = state
34 })
35 .start()
36 },
37 methods: {
38 toggle () {
39 this.demoMachine.send('TOGGLE')
40 }
41 }
42}
43</script>

Breaking this down we can see that we set the interpreted machine onto the component's data along with the machine's initialState. Within the created lifecycle hook we start the interpreted machine and listen to transitions, within the callback of the transition change we update the components state from the machine.

Thanks for reading!


Published by Matt Chaffe

Popular Stories