State design pattern

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.

Description

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.

Objectives

The main objectives of the State pattern is to develop FSM are as follows:

  • To have a type that alters its own behavior when some internal things have changed
  • Model complex graphs and pipelines can be upgraded easily by adding more states and rerouting their output states

A small guess the number game

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.

Acceptance criteria

For this simple game, we have five acceptance criteria that basically describe the mechanics of the game:

  1. The game will ask the player how many tries they will have before losing the game.
  2. The number to guess must be between 0 and 10.
  3. Every time a player enters a number to guess, the number of retries drops by one.
  4. If the number of retries reaches zero and the number is still incorrect, the game finishes and the player has lost.
  5. If the player guesses the number, the player wins.

Implementation of State pattern

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.

A state to win and a state to lose

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.

The game built using the State pattern

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.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.143.203.43