1
\$\begingroup\$

I've implemented tic-tac-toe in TypeScript before; now I'm porting it to Elixir. As before, this is pure game logic, no UI involved. I'm very new to Elixir, so any suggestions are appreciated.

The actual game logic:

defmodule MassivelyMultiplayerTtt.Game do
  defstruct current_player: :player_x,
            winning_player: :unfinished,
            board: Tuple.duplicate(:empty, 9)

  def make_move(game, cell_num) do
    case elem(game.board, cell_num) do
      :x ->
        {:square_filled, game}

      :o ->
        {:square_filled, game}

      :empty ->
        if game.winning_player != :unfinished do
          {:game_already_over, game}
        else
          game =
            if game.current_player == :player_x do
              %{game | board: put_elem(game.board, cell_num, :x), current_player: :player_o}
            else
              %{game | board: put_elem(game.board, cell_num, :o), current_player: :player_x}
            end

          game = check_for_end(game)

          case game.winning_player do
            :unfinished ->
              {:waiting_for_move, game}

            _ ->
              {:game_finished, game}
          end
        end
    end
  end

  defp check_for_end(game) do
    case game.board do
      {:x, :x, :x, _, _, _, _, _, _} ->
        %{game | winning_player: :player_x}

      {_, _, _, :x, :x, :x, _, _, _} ->
        %{game | winning_player: :player_x}

      {_, _, _, _, _, _, :x, :x, :x} ->
        %{game | winning_player: :player_x}

      {:x, _, _, :x, _, _, :x, _, _} ->
        %{game | winning_player: :player_x}

      {_, :x, _, _, :x, _, _, :x, _} ->
        %{game | winning_player: :player_x}

      {_, _, :x, _, _, :x, _, _, :x} ->
        %{game | winning_player: :player_x}

      {:x, _, _, _, :x, _, _, _, :x} ->
        %{game | winning_player: :player_x}

      {_, _, :x, _, :x, _, :x, _, _} ->
        %{game | winning_player: :player_x}

      {:o, :o, :o, _, _, _, _, _, _} ->
        %{game | winning_player: :player_o}

      {_, _, _, :o, :o, :o, _, _, _} ->
        %{game | winning_player: :player_o}

      {_, _, _, _, _, _, :o, :o, :o} ->
        %{game | winning_player: :player_o}

      {:o, _, _, :o, _, _, :o, _, _} ->
        %{game | winning_player: :player_o}

      {_, :o, _, _, :o, _, _, :o, _} ->
        %{game | winning_player: :player_o}

      {_, _, :o, _, _, :o, _, _, :o} ->
        %{game | winning_player: :player_o}

      {:o, _, _, _, :o, _, _, _, :o} ->
        %{game | winning_player: :player_o}

      {_, _, :o, _, :o, _, :o, _, _} ->
        %{game | winning_player: :player_o}

      board ->
        if Enum.any?(Tuple.to_list(board), &match?(:empty, &1)) do
          game
        else
          %{game | winning_player: :drawn}
        end
    end
  end
end

Unit tests:

defmodule MassivelyMultiplayerTtt.GameTest do
  use ExUnit.Case, async: true
  import MassivelyMultiplayerTtt.Game

  test "recognizes X's victory" do
    # Arrange
    game = %MassivelyMultiplayerTtt.Game{}

    # Act
    {_, game} = make_move(game, 0)
    {_, game} = make_move(game, 6)
    {_, game} = make_move(game, 1)
    {_, game} = make_move(game, 7)
    {result, game} = make_move(game, 2)

    # Assert
    assert(result == :game_finished)
    assert(game.winning_player == :player_x)
  end

  test "recognizes O's victory" do
    # Arrange
    game = %MassivelyMultiplayerTtt.Game{}

    # Act
    {_, game} = make_move(game, 0)
    {_, game} = make_move(game, 6)
    {_, game} = make_move(game, 1)
    {_, game} = make_move(game, 7)
    {_, game} = make_move(game, 5)
    {result, game} = make_move(game, 8)

    # Assert
    assert(result == :game_finished)
    assert(game.winning_player == :player_o)
  end

  test "recognizes a drawn game" do
    # Arrange
    game = %MassivelyMultiplayerTtt.Game{}

    # Act
    {_, game} = make_move(game, 0)
    {_, game} = make_move(game, 1)
    {_, game} = make_move(game, 3)
    {_, game} = make_move(game, 4)
    {_, game} = make_move(game, 7)
    {_, game} = make_move(game, 6)
    {_, game} = make_move(game, 2)
    {_, game} = make_move(game, 5)
    {result, game} = make_move(game, 8)

    # Assert
    assert(result == :game_finished)
    assert(game.winning_player == :drawn)
  end

  test "returns an error when attempting to move to an already-filled cell" do
    # Arrange
    game = %MassivelyMultiplayerTtt.Game{}

    # Act
    {_, game} = make_move(game, 0)
    {result, _} = make_move(game, 0)

    # Assert
    assert(result == :square_filled)
  end

  test "returns an error when attempting to move in an already completed game" do
    # Arrange
    game = %MassivelyMultiplayerTtt.Game{}

    # Act
    {_, game} = make_move(game, 0)
    {_, game} = make_move(game, 6)
    {_, game} = make_move(game, 1)
    {_, game} = make_move(game, 7)
    {_, game} = make_move(game, 2)
    {result, _} = make_move(game, 5)

    # Assert
    assert(result == :game_already_over)
  end

  test "recognizes X's turn" do
    # Arrange
    game = %MassivelyMultiplayerTtt.Game{}

    # Act
    {_, game} = make_move(game, 0)
    {result, game} = make_move(game, 1)

    # Assert
    assert(result == :waiting_for_move)
    assert(game.current_player == :player_x)
  end

  test "recognizes O's turn" do
    # Arrange
    game = %MassivelyMultiplayerTtt.Game{}

    # Act
    {result, game} = make_move(game, 0)

    # Assert
    assert(result == :waiting_for_move)
    assert(game.current_player == :player_o)
  end
end
\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Browse other questions tagged or ask your own question.