Game state machine (Tictac Part 2)

In a previous episode, we built a representation of a tic-tac-toe board and its squares. Now it's time to make a state machine for our tic-tac-toe game.

Tracking the state of a tic-tac-toe game

There are a few ways to look at the state of a game. At the highest level, we need to know if a game is being set up, is progress, or completed, etc. We'll call this the status of the game. Digging in a bit more deeply, to fully represent a game, we need to know the state of the board and whose turn it is. Finally, there are some properties that can be computed from the above information but are useful enough to deserve a top level representation. Here's the structure we'll use for Tictac.State:

  • status: can be :initial, :choose_p1, playing, game_over or winner_reported
  • over: true if the game is over, false otherwise
  • turn: whose turn it is to play next (nil, :x or :o)
  • winner: the winner of the current game (nil, :x, :o or :tie)
  • board: a map of all the squares on the board as keys and :empty, :x or :o as values
  • ui: a hook to the current UI being used by the game

All new games will start with a status of :initial, a board of all empty squares, whatever UI hook is passed into it at creation time and nil values for everything else.

Using events to transition between states

In order to transition from one state to another, our game will have to recognize various events, such as a first player being chosen, a player taking a turn or the game being won. The safest and easiest way to do this is by making a many-headed function that takes game state and event tuples, e.g. event(%State{status: :initial} = state, {:choose_p1, :x}) will be matched by a function that checks that :x is a valid player, updates the game state's :status to :playing and :turn to :x.

Other event tuples such as {:check_for_winner, :o} simply don't don't have a matching function head where state matches %State{status: :initial} because tic-tac-toe doesn't allow for players to win before it's decided who will play first. Those and similar calls will be caught by a default definition for &event/2 that catches any unmatched state and actions and returns an :invalid_state_transition error.

Allowed transitions (status, action) -> new status

  • (:initial, :choose_p1) -> :playing
  • (:playing, :play) -> :playing (with the state of :turn toggled to the other player and the board updated)
  • (:playing, :check_for_winner) -> :playing
  • (:playing, :check_for_winner) -> :game_over
  • (:playing, :game_over?) -> :playing
  • (:playing, :game_over?) -> :game_over
  • (:game_over, :game_over? -> :game_over
  • (:game_over, :report_outcome) -> :winner_reported (not implemented)

Any other transitions will result in an error. Note that the :game_over? action is a convenience and isn't needed if :check_for_winner is written in such a way that it will also handle full boards where neither player has won.

Implementing the game logic

With the above state transitions in place, the remaining non-UI logic to be implemented is the actions. Choosing a player 1 is trivial, but playing a turn and checking to see if there is a winner each warrant creating helper functions. The following episode in this series implements that logic and builds a CLI.

Challenge:

Model another game you know as a state machine. It could be a card game like Hearts or Blackjack, another board game or even a children's game like Capture the Flag.

(example solution here)

Back to index

No Comments