/ elixir

Let's Write a Tic-Tac-Toe game in Elixir

I a past post I presented a version of tic tac toe game using a single file elixir script. I'd like to extend that demo and provide an example demo application with a main script, unit tests and text based UI.

Let's Build a Project

Elixir projects are created and maintained with a command line utility script called mix. To create a starter project we can use:

$ mix new tic_tac_toe

Running the above in a shell creates a new directory called tictactoe and with it the following subdirectories and files:

.
├── README.md
├── config
│   └── config.exs
├── lib
│   └── tic_tac_toe.ex
├── mix.exs
└── test
    ├── test_helper.exs
    └── tic_tac_toe_test.exs

Adding Main

We can already run mix test to run the tests, but the starter template has no main function. Elixir has two ways to create runnable applications: The first, for long running applications, is to create a supervision tree and register child workers. The second which is more suitable for a text based UI is an elixir script.

Head over to mix.exs and add the following line to your project functions:

      escript: [main_module: Game],

The entire file now looks like this:

defmodule TicTacToe.Mixfile do
  use Mix.Project

  def project do
    [
      app: :tic_tac_toe,
      version: "0.1.0",
      elixir: "~> 1.5",
      start_permanent: Mix.env == :prod,
      deps: deps(),
      escript: [main_module: Game],
    ]
  end

  # Run "mix help compile.app" to learn about applications.
  def application do
    [
      extra_applications: [:logger]
    ]
  end

  # Run "mix help deps" to learn about dependencies.
  defp deps do
    [
      # {:dep_from_hexpm, "~> 0.3.0"},
      # {:dep_from_git, git: "https://github.com/elixir-lang/my_dep.git", tag: "0.1.0"},
    ]
  end
end

Next we need to create the module that will match the script we just defined. Create a new file called lib/game.ex with the following content:

defmodule Game do
  def main(args \\ []) do
    IO.puts "X/O Game."
  end
end

Now we're ready to build our executable script. In the application directory run:

$ mix escript.build

This command built an executable elixir script file that we can now execute to play the game (well so far "the game" only means show a text message, but never mind that now):

$ ./tic_tac_toe
X/O Game.

The resulting file is huge in size, and it's also worth noting that the general recommendation is to use escripts mainly for development tools and not for live systems:

$ ls -lh tic_tac_toe 
-rwxr-xr-x  1 ynonperek  staff   960K Feb 25 14:58 tic_tac_toe

Nontheless escripts provide a quick way to build CLI developer tools and use existing elixir libraries as dependencies.

Implementing Tic-Tac-Toe Game

Now that we have the basic structure we can move to implement the game logic. In my implementation I used a map to hold the board data: Every time a user makes a move a new entry is added to the map, its key is the coordinates {row, col} and its value is the player glyph (X or O).

For example this map:

%{{0, 0} => 'X', {0, 2} => 'O'}

Represents this board:

X . O
. . .
. . .

The full source code of the module is:

defmodule GameState do
  defstruct player: 'X', board: %{}
end

defmodule TicTacToe do
  def init do
    %GameState{}
  end

  def is_winning_triplet(['X', 'X', 'X']), do: true
  def is_winning_triplet(['O', 'O', 'O']), do: true
  def is_winning_triplet(_), do: false

  def has_winner?(state) do
    Enum.any?([
      [ {0, 0}, {0, 1}, {0, 2} ],
      [ {1, 0}, {1, 1}, {1, 2} ],
      [ {2, 0}, {2, 1}, {2, 2} ],
      [ {0, 0}, {1, 0}, {2, 0} ],
      [ {0, 1}, {1, 1}, {2, 1} ],
      [ {0, 2}, {1, 2}, {2, 2} ],
      [ {0, 0}, {1, 1}, {2, 2} ],
      [ {0, 2}, {1, 1}, {2, 0} ],
    ],
    fn x ->
      Map.take(state.board, x)
      |> Map.values
      |> is_winning_triplet
    end)
  end

  def game_over?(state) do
    has_winner?(state) || Map.size(state.board) == 9
  end

  def next_player('X'), do: 'O'
  def next_player('O'), do: 'X'

  def play({row, col}, state) do
    cond do
      # tried to play a finished game
      game_over?(state) ->
        state

      # square taken
      {row, col} in Map.keys(state.board) ->
        state

      # square free - take it!
      true ->
        %GameState{
          player: next_player(state.player),
          board: Map.put_new(state.board, { row, col }, state.player),
        }
    end
  end
end

Read Lines While Input Loop

More interesting I think is the text based UI. The first pattern is a read while input loop, which in elixir looks like this:

  def main(args \\ []) do
    IO.puts "X/O Game."
    s = TicTacToe.init

    IO.stream(:stdio, :line)
    |> Enum.reduce(s, &play_line/2)
  end

The last two lines in main instruct elixir to read all available data from stdin and send each line as input to play_line. That function takes two input arguments: The first is the line itself and the second is an accumulator that is passed from line to line. The accumulator is our way to represent change.

Let's see what play_line is doing:

  def play_line(line, s) do
    line
    |> String.split
    |> Enum.map(&String.to_integer/1)
    |> List.to_tuple
    |> TicTacToe.play(s)
    |> print_board
    |> check_winner
  end

The accumulator (s in the above code) represents the game state after every line handled. Each play_line returns the new game state, to have Enum.reduce pass it to the next line. Each line is split, converted to integers and sent to TicTacToe.play to receive a new game state.

Running the script we get:

$ ./tic_tac_toe 
X/O Game.
0 0
 X  .  . 
 .  .  . 
 .  .  . 
0 1
 X  O  . 
 .  .  . 
 .  .  . 
1 0
 X  O  . 
 X  .  . 
 .  .  . 

So input format is just two numbers separated by space.

Exit Early

The next question is how to handle victory or tie. When any of the halting conditions happen we need to tell Enum.reduce to stop reading input lines. To halt reduce we can use exceptions or simply System.halt. The latter terminates the program which is good enough here:

  def check_winner(s) do
    if TicTacToe.game_over?(s) do
      IO.puts("Game Over")
      System.halt
    end

    s
  end

Note that in the normal flow the return value of check_winner becomes the return value of the reducer, which means we need to return the state.

Printing Without Loops

The final UI pattern I used is a nested loop to print the board. Unlike other languages, elixir doesn't provide an imperative for loop but instead offer list comprehension.

The workaround is to generate a list of strings, each represents a square in the game and then join them with newlines:

  def print_board(s) do
    for i <- 0..2 do
      for j <- 0..2 do
        " #{Map.get(s.board, {i, j}, ".")} "
      end
    end

    |> Enum.map(&Enum.join/1)
    |> Enum.join("\n")
    |> IO.puts
    s
  end

A sample input from the nested "for" loops is just a nested list:

[[" X ", " . ", " . "], [" . ", " . ", " . "], [" . ", " . ", " . "]]

That's why we can pass it on to Enum.map to join each inner list, and then use Enum.join to join the 3 rows into one large string that can be printed.

As all other functions in the read_line pipeline this one too needs to return the accumulator so it can be passed on to the next function in the pipeline.

The full source code for game.ex file:

defmodule Game do
  def print_board(s) do
    for i <- 0..2 do
      for j <- 0..2 do
        " #{Map.get(s.board, {i, j}, ".")} "
      end
    end
    |> IO.inspect
    |> Enum.map(&Enum.join/1)
    |> Enum.join("\n")
    |> IO.puts
    s
  end

  def check_winner(s) do
    if TicTacToe.game_over?(s) do
      IO.puts("Game Over")
      System.halt
    end

    s
  end

  def play_line(line, s) do
    line
    |> String.split
    |> Enum.map(&String.to_integer/1)
    |> List.to_tuple
    |> TicTacToe.play(s)
    |> print_board
    |> check_winner
  end

  def main(args \\ []) do
    IO.puts "X/O Game."
    s = TicTacToe.init

    IO.stream(:stdio, :line)
    |> Enum.reduce(s, &play_line/2)
  end
end

Regarding the code structure, in my opinion the ability to combine multiple functions into a larger pipeline as demonstrated in play_line function above is one of the main strong sides of elixir and functional programming in general.