State patterns are directly related to FSMs. An FSM, in very simple terms, is something that has one or more states and travels between them to execute some behaviors. Let's see how the State pattern helps us to define FSM.
A light switch is a common example of an FSM. It has two states--on and off. One state can transit to the other and vice versa. The way that the State pattern works is similar. We have a State
interface and an implementation of each state we want to achieve. There is also usually a context that holds cross-information between the states.
With FSM, we can achieve very complex behaviors by splitting their scope between states. This way we can model pipelines of execution based on any kind of inputs or create event-driven software that responds to particular events in specified ways.
The main objectives of the State pattern is to develop FSM are as follows:
We are going to develop a very simple game that uses FSM. This game is a number guessing game. The idea is simple--we will have to guess some number between 0 and 10 and we have just a few attempts or we'll lose.
We will leave the player to choose the level of difficulty by asking how many tries the user has before losing. Then, we will ask the player for the correct number and keep asking if they don't guess it or if the number of tries reaches zero.
For this simple game, we have five acceptance criteria that basically describe the mechanics of the game:
The idea of unit tests is quite straightforward in a State pattern so we will spend more time explaining in detail the mechanism to use it, which is a bit more complex than usual.
First of all, we need the interface to represent the different states and a game context to store the information between states. For this game, the context needs to store the number of retries, if the user has won or not, the secret number to guess, and the current state. The state will have an executeState
method that accepts one of these contexts and returns true
if the game has finished, or false
if not:
type GameState interface { executeState(*GameContext) bool } type GameContext struct { SecretNumber int Retries int Won bool Next GameState }
As described in acceptance criteria 1, the player must be able to introduce the number of retries they want. This will be achieved by a state called StartState
. Also, the StartState
struct must prepare the game, setting the context to its initial value before the player:
type StartState struct{} func(s *StartState) executeState(c *GameContext) bool { c.Next = &AskState{} rand.Seed(time.Now().UnixNano()) c.SecretNumber = rand.Intn(10) fmt.Println("Introduce a number a number of retries to set the difficulty:") fmt.Fscanf(os.Stdin, "%d ", &c.Retries) return true }
First of all, the StartState
struct implements the GameState
structure because it has the executeState(*Context)
method of Boolean type on its structure. At the beginning of this state, it sets the only state possible after executing this one--the AskState
state. The AskState
struct is not declared yet, but it will be the state where we ask the player for a number to guess.
In the next two lines, we use the Rand
package of Go to generate a random number. In the first line, we feed the random generator with the int64
type number returned by the current moment, so we ensure a random feed in each execution (if you put a constant number here, the randomizer will also generate the same number too). The rand.Intn(int)
method returns an integer number between zero and the specified number, so here we cover acceptance criteria 2.
Next, a message asking for a number of retries to set precedes the fmt.Fscanf
method, a powerful function where you can pass it an io.Reader
(the standard input of the console), a format (number), and an interface to store the contents of the reader, in this case, the Retries
field of the context.
Finally, we return true
to tell the engine that the game must continue. Let's see the AskState
struct, which we have used at the beginning of the function:
type AskState struct {} func (a *AskState) executeState(c *GameContext) bool{ fmt.Printf("Introduce a number between 0 and 10, you have %d tries left ", c.Retries) var n int fmt.Fscanf(os.Stdin, "%d", &n) c.Retries = c.Retries - 1 if n == c.SecretNumber { c.Won = true c.Next = &FinishState{} } if c.Retries == 0 { c.Next = &FinishState{} } return true }
The AskState
structure also implements the GameState
state, as you have probably guessed already. This states starts with a message for the player, asking them to insert a new number. In the next three lines, we create a local variable to store the contents of the number that the player will introduce. We used the fmt.Fscanf
method again, as we did in StartState
struct to capture the player's input and store it in the variable n
. Then, we have one retry less in our counter, so we have to subtract one to the number of retries represented in the Retries
field.
Then, there are two checks: one that checks if the user has entered the correct number, in which case the context field Won
is set to true
and the next state is set to the FinishState
struct (not declared yet).
The second check is controlling that the number of retries has not reached zero, in which case it won't let the player ask again for a number and it will send the player to the FinishState
struct directly. After all, we have to tell the game engine again that the game must continue by returning true
in the executeState
method.
Finally, we define the FinishState
struct. It controls the exit status of the game, checking the contents of the Won
field in the context object:
type FinishState struct{} func(f *FinishState) executeState(c *GameContext) bool { if c.Won { println("Congrats, you won") } else { println("You lose") } return false }
The TheFinishState
struct also implements the GameState
state by having executeState
method in its structure. The idea here is very simple--if the player has won (this field is set previously in the AskState
struct), the FinishState
structure will print the message Congrats, you won
. If the player has not won (remember that the zero value of the Boolean variable is false
), the FinishState
prints the message You lose.
In this case, the game can be considered finished, so we return false
to say that the game must not continue.
We just need the main
method to play our game:
func main() { start := StartState{} game := GameContext{ Next:&start, } for game.Next.executeState(&game) {} }
Well, yes, it can't be simpler. The game must begin with the start
method, although it could be abstracted more outside in case that the game needs more initialization in the future, but in our case it is fine. Then, we create a context where we set the Next
state as a pointer to the start
variable. So the first state that will be executed in the game will be the StartState
state.
The last line of the main
function has a lot of things just there. We create a loop, without any statement inside it. As with any loop, it keeps looping after the condition is not satisfied. The condition we are using is the returned value of the GameStates
structure, true
as soon as the game is not finished.
So, the idea is simple: we execute the state in the context, passing a pointer to the context to it. Each state returns true
until the game has finished and the FinishState
struct will return false
. So our for loop will keep looping, waiting for a false
condition sent by the FinishState
structure to end the application.
Let's play once:
go run state.go Introduce a number a number of retries to set the difficulty: 5 Introduce a number between 0 and 10, you have 5 tries left 8 Introduce a number between 0 and 10, you have 4 tries left 2 Introduce a number between 0 and 10, you have 3 tries left 1 Introduce a number between 0 and 10, you have 2 tries left 3 Introduce a number between 0 and 10, you have 1 tries left 4 You lose
We lost! We set the number of retries to 5. Then we kept inserting numbers, trying to guess the secret number. We entered 8, 2, 1, 3, and 4, but it wasn't any of them. I don't even know what the correct number was; let's fix this!
Go to the definition of the FinishState
struct and change the line where it says You lose
, and replace it with the following:
fmt.Printf("You lose. The correct number was: %d ", c.SecretNumber)
Now it will show the correct number. Let's play again:
go run state.go Introduce a number a number of retries to set the difficulty: 3 Introduce a number between 0 and 10, you have 3 tries left 6 Introduce a number between 0 and 10, you have 2 tries left 2 Introduce a number between 0 and 10, you have 1 tries left 1 You lose. The correct number was: 9
This time we make it a little harder by setting only three tries... and we lost again. I entered 6, 2, and 1, but the correct number was 9. Last try:
go run state.go Introduce a number a number of retries to set the difficulty: 5 Introduce a number between 0 and 10, you have 5 tries left 3 Introduce a number between 0 and 10, you have 4 tries left 4 Introduce a number between 0 and 10, you have 3 tries left 5 Introduce a number between 0 and 10, you have 2 tries left 6 Congrats, you won
Great! This time we lowered the difficulty, allowing up to five tries and we won! we even had one more try left, but we guessed the number in the fourth try after entering 3, 4, 5. The correct number was 6, which was my fourth try.
Have you realized that we could have a winning and a lose state instead of printing the messages directly in the FinishState
struct? This way we could, for example, check some hypothetical scoreboard in the win section to see if we have set a record. Let's refactor our game. First we need a WinState
and a LoseState
struct:
type WinState struct{} func (w *WinState) executeState(c *GameContext) bool { println("Congrats, you won") return false } type LoseState struct{} func (l *LoseState) executeState(c *GameContext) bool { fmt.Printf("You lose. The correct number was: %d ", c.SecretNumber) return false }
These two new states have nothing new. They contain the same messages that were previously in the FinishState
state that, by the way, must be modified to use these new states:
func (f *FinishState) executeState(c *GameContext) bool { if c.Won { c.Next = &WinState{} } else { c.Next = &LoseState{} } return true }
Now, the finish state doesn't print anything and, instead, delegates this to the next state in the chain--the WinState
structure, if the user has won and the LoseState
struct, if not. Remember that the game doesn't finish on the FinishState
struct now, and we must return true
instead of false
to notify to the engine that it must keep executing states in the chain.
You must be thinking now that you can extend this game forever with new states, and it's true. The power of the State pattern is not only the capacity to create a complex FSM, but also the flexibility to improve it as much as you want by adding new states and modifying some old states to point to the new ones without affecting the rest of the FSM.
18.188.218.226