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\$

0

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

Browse other questions tagged or ask your own question.