Powered by AppSignal & Oban Pro

Tic-tac-toe

exercises/tic-tac-toe.livemd

Tic-tac-toe

Mix.install([
  {:jason, "~> 1.4"},
  {:kino, "~> 0.8.0", override: true},
  {:youtube, github: "brooklinjazz/youtube"},
  {:hidden_cell, github: "brooklinjazz/hidden_cell"}
])

Navigation

Return Home Report An Issue

Overview

You’re going to create a game of Tic-tac-toe.

In Tic-tac-toe, players take turns placing either an X or an O onto a 3 by 3 grid.

We can represent a grid like this as a list of lists.

grid = [
  ["X", "O", nil],
  [nil, "X", "O"],
  [nil, nil, "X"]
]

Usually we say grids have an x and a y axis. Each location in the grid is called a coordinate.

We’ll use an {x, y} tuple to represent a coordinate on the board.

For example, the "X" in the top left corner of the board above would be at coordinate {0, 2}.

TicTacToe

You’re going to create a TicTacToe module which can read coordinates from a board, and fill them in.

For example, reading the coordinate {0, 2} on the following board returns "X".

board = [
  ["X", "O", nil],
  [nil, "X", "O"],
  [nil, nil, "X"]
]

TicTacToe.at(board, {0, 2})
"X"

Filling the coordinate will return a newly updated board.

board = [
  ["X", "O", nil],
  [nil, "X", "O"],
  [nil, nil, "X"]
]

TicTacToe.fill(board, {0, 1}, "O")
[
  ["X", "O", nil],
  ["O", "X", "O"],
  [nil, nil, "X"]
]

Implement the TicTacToe module as documented.

Hint

Remember that Enum.at can access a list at an index, however our coordinates are flipped on the board, so the first list is at y = 2.

Therefore, if y is 0, then we want to access the row list at index 2.

Example Solution

defmodule TicTacToe do
  def at(board, coordinate) do
    {x, y} = coordinate

    Enum.at(board, 2 - y) |> Enum.at(x)
  end

  def fill(board, coordinate, symbol) do
    {x, y} = coordinate
    List.update_at(board, 2 - y, fn row ->
      List.replace_at(row, x, symbol)
    end)
  end
end
defmodule TicTacToe do
  @moduledoc """
    Documentation for `TicTacToe`
  """

  @doc """
  Read a coordinate on a board.

  ## Examples

    We've used a board with letter symbols for sake of example.

    iex> board = [["A", "B", "C"], ["D", "E", "F"], ["G", "H", "I"]]
    iex> TicTacToe.at(board, {0, 0})
    "G"
    iex> TicTacToe.at(board, {1, 0})
    "H"
    iex> TicTacToe.at(board, {2, 0})
    "I"
    iex> TicTacToe.at(board, {0, 1})
    "D"
    iex> TicTacToe.at(board, {1, 1})
    "E"
    iex> TicTacToe.at(board, {2, 1})
    "F"
    iex> TicTacToe.at(board, {0, 2})
    "A"
    iex> TicTacToe.at(board, {1, 2})
    "B"
    iex> TicTacToe.at(board, {2, 2})
    "C"
  """
  def at(board, coordinate) do
  end

  @doc """
  Fill in a coordinate on a board with the provided symbol and return a new updated board.

  ## Examples

  Filling an empty board.

  iex> board = [[nil, nil, nil], [nil, nil, nil], [nil, nil, nil]]
  iex> TicTacToe.fill(board, {0, 0}, "X")
  [[nil, nil, nil], [nil, nil, nil], ["X", nil, nil]]
  iex> TicTacToe.fill(board, {1, 1}, "O")
  [[nil, nil, nil], [nil, "O", nil], [nil, nil, nil]]
  iex> TicTacToe.fill(board, {2, 2}, "X")
  [[nil, nil, "X"], [nil, nil, nil], [nil, nil, nil]]

  Filling an existing board

  iex> TicTacToe.fill([[nil, "X", nil], [nil, "X", "O"], [nil, nil, "X"]], {2, 2}, "O")
  [[nil, "X", "O"], [nil, "X", "O"],[nil, nil, "X"]]
  """
  def fill(board, coordinate, symbol) do
  end
end

Bonus: Winner

Create a TicTacToe.winner/1 function which accepts a board and determines if there is a winner. A player wins if they have three symbols in a row, three symbols in a column, or three symbols diagonally.

For example:

board = [
  ["X", "O", "O"]
  ["X", "O", nil]
  ["X", nil, nil]
]

TicTacToe.winner(board) # returns "X".
"X"

Example Solution

defmodule TicTacToe do
  def winner(board) do
    cond do
      winner?(board, "X") -> "X"
      winner?(board, "O") -> "O"
      true -> nil
    end
  end

  def winner?(board, symbol) do
    row_winner =
      Enum.any?(board, fn row ->
        Enum.all?(row, fn element -> element == symbol end)
      end)

    column_winner =
      Enum.any?(0..2, fn index ->
        Enum.all?(board, fn row ->
          Enum.at(row, index) == symbol
        end)
      end)

    diagonal_winner =
      Enum.all?(0..2, fn index ->
        board
        |> Enum.at(index)
        |> Enum.at(index) == symbol
      end) or
        Enum.all?(0..2, fn index ->
          board
          |> Enum.at(index)
          |> Enum.at(2 - index) == symbol
        end)

    row_winner or column_winner or diagonal_winner
  end
end

Note it’s also possible to use pattern matching to solve this problem.

defmodule TicTacToe do
  def winner(board) do
    cond do
      winner?(board, "X") -> "X"
      winner?(board, "O") -> "O"
      true -> nil
    end
  end

  def winner?(board, x) do
    match?([[^x, ^x, ^x], [_, _, _], [_, _, _]], board) or
      match?([[_, _, _], [^x, ^x, ^x], [_, _, _]], board) or
      match?([[_, _, _], [_, _, _], [_, _, _]], board) or
      match?([[^x, _, _], [^x, _, _], [^x, _, _]], board) or
      match?([[_, ^x, _], [_, ^x, _], [_, ^x, _]], board) or
      match?([[_, _, ^x], [_, _, ^x], [_, _, ^x]], board) or
      match?([[^x, _, _], [_, ^x, _], [_, _, ^x]], board) or
      match?([[_, _, ^x], [_, ^x, _], [^x, _, _]], board)
  end
end

To get started, copy the code below into the TicTacToe module above.

@doc """
Determine if a player has won the game.

## Examples

  Row Wins:

  iex> TicTacToe.winner([["X", "X", "X"], [nil, nil, nil], [nil, nil, nil]])
  "X"
  iex> TicTacToe.winner([[nil, nil, nil], ["X", "X", "X"], [nil, nil, nil]])
  "X"
  iex> TicTacToe.winner([[nil, nil, nil], [nil, nil, nil], ["X", "X", "X"]])
  "X"

  Column Wins:

  iex> TicTacToe.winner([["X", nil, nil], ["X", nil, nil], ["X", nil, nil]])
  "X"
  iex> TicTacToe.winner([[nil, "X", nil], [nil, "X", nil], [nil, "X", nil]])
  "X"
  iex> TicTacToe.winner([[nil, nil, "X"], [nil, nil, "X"], [nil, nil, "X"]])
  "X"

  Diagonal Wins

  iex> TicTacToe.winner([["X", nil, nil], [nil, "X", nil], [nil, nil, "X"]])
  "X"
  iex> TicTacToe.winner([[nil, nil, "X"], [nil, "X", nil], ["X", nil, nil]])
  "X"

  No Winner

  iex> TicTacToe.winner([[nil, nil, nil], [nil, nil, nil], [nil, nil, nil]])
  nil
  iex> TicTacToe.winner([[nil, "X", nil], [nil, "X", nil], [nil, nil, nil]])
  nil

"""
def winner(board) do
end

Mark As Completed

file_name = Path.basename(Regex.replace(~r/#.+/, __ENV__.file, ""), ".livemd")

save_name =
  case Path.basename(__DIR__) do
    "reading" -> "tic-tac-toe_reading"
    "exercises" -> "tic-tac-toe_exercise"
  end

progress_path = __DIR__ <> "/../progress.json"
existing_progress = File.read!(progress_path) |> Jason.decode!()

default = Map.get(existing_progress, save_name, false)

form =
  Kino.Control.form(
    [
      completed: input = Kino.Input.checkbox("Mark As Completed", default: default)
    ],
    report_changes: true
  )

Task.async(fn ->
  for %{data: %{completed: completed}} <- Kino.Control.stream(form) do
    File.write!(
      progress_path,
      Jason.encode!(Map.put(existing_progress, save_name, completed), pretty: true)
    )
  end
end)

form

Commit Your Progress

Run the following in your command line from the curriculum folder to track and save your progress in a Git commit. Ensure that you do not already have undesired or unrelated changes by running git status or by checking the source control tab in Visual Studio Code.

$ git checkout -b tic-tac-toe-exercise
$ git add .
$ git commit -m "finish tic-tac-toe exercise"
$ git push origin tic-tac-toe-exercise

Create a pull request from your tic-tac-toe-exercise branch to your solutions branch. Please do not create a pull request to the DockYard Academy repository as this will spam our PR tracker.

DockYard Academy Students Only:

Notify your teacher by including @BrooklinJazz in your PR description to get feedback. You (or your teacher) may merge your PR into your solutions branch after review.

If you are interested in joining the next academy cohort, sign up here to receive more news when it is available.