DEV Community

Cover image for Refactoring the Game
Diego Novais
Diego Novais

Posted on

Refactoring the Game

Our game is almost finished (at least part of it). But we can improve the module game and do it together.

Let's start...

I found modular arithmetic, looking for something with a mathematical approach to solving the logic of our game (if you are interested in the subject, take a look here).

Now we'll use modular arithmetic to add a few math to our code. And make the code brighter and cleaner.

The mathematical approach

The mod function provides the remainder when one integer is divided by another. And it will help us to a cyclical relationship between the three choices: Rock, Paper, and Scissors.

r = a mod b
r is the remainder when a is divided by b
Enter fullscreen mode Exit fullscreen mode

So, looking into our code, more specifically on module attributes:

  @stone 1
  @paper 2
  @scissor 3
Enter fullscreen mode Exit fullscreen mode

I saw a tip that can help us to make calculus more efficiently:

(first_player_choice - second_player_choice) % 3
Enter fullscreen mode Exit fullscreen mode

Refactoring the Game

Adding the function to calculate the result of the game game_calc:

defmodule Game do
  @moduledoc """
  Documentation for `Game`.
  """

  @stone 1
  @paper 2
  @scissor 3

  def play(first_player_choice, second_player_choice) do
    result(first_player_choice, second_player_choice)
  end

  defp result(first_player_choice, second_player_choice) do
    cond do
      first_player_choice == second_player_choice ->
        {:ok, "Draw!"}

      first_player_choice == @scissor && second_player_choice == @paper ->
        {:ok, "First player win!!!"}

      first_player_choice == @paper && second_player_choice == @stone ->
        {:ok, "First player win!!!"}

      first_player_choice == @stone && second_player_choice == @scissor ->
        {:ok, "First player win!!!"}

      first_player_choice == @paper && second_player_choice == @scissor ->
        {:ok, "Second player win!!!"}

      first_player_choice == @stone && second_player_choice == @paper ->
        {:ok, "Second player win!!!"}

      first_player_choice == @scissor && second_player_choice == @stone ->
        {:ok, "Second player win!!!"}
    end
  end

  defp game_calc(first_player_item, second_player_item) do
    rem(first_player_item - second_player_item, 3)
  end
end
Enter fullscreen mode Exit fullscreen mode

And then now, we can simplify the function result:

defmodule Game do
  @moduledoc """
  Documentation for `Game`.
  """

  @stone 1
  @paper 2
  @scissor 3

  def play(first_player_choice, second_player_choice) do
    result(first_player_choice, second_player_choice)
  end

  defp result(first_player_choice, second_player_choice) do
    game_calc_result = game_calc(first_player_choice, second_player_choice)

    case game_calc_result do
      0 -> {:ok, "Draw!"}
      1 -> {:ok, "First player win!!!"}
      _ -> {:ok, "Second player win!!!"}
    end
  end

  defp game_calc(first_player_item, second_player_item) do
    rem(first_player_item - second_player_item, 3)
  end
end
Enter fullscreen mode Exit fullscreen mode

Running the tests:

mix test
Enter fullscreen mode Exit fullscreen mode

Something is wrong. We got three warnings and one failure message when we ran the tests.

Compiling 1 file (.ex)
warning: module attribute @scissor was set but never used
  lib/game.ex:8

warning: module attribute @paper was set but never used
  lib/game.ex:7

warning: module attribute @stone was set but never used
  lib/game.ex:6

..

  1) test Game.play/2 when first player wins when first player chooses stone and second player chooses scissors (GameTest)
     test/game_test.exs:55
     Assertion with == failed
     code:  assert match == "First player win!!!"
     left:  "Second player win!!!"
     right: "First player win!!!"
     stacktrace:
       test/game_test.exs:61: (test)

......

Finished in 0.05 seconds (0.00s async, 0.05s sync)
9 tests, 1 failure

Randomized with seed 811857
Enter fullscreen mode Exit fullscreen mode

To solve the warning messages, we need to remove the module attributes:

defmodule Game do
  @moduledoc """
  Documentation for `Game`.
  """

  def play(first_player_choice, second_player_choice) do
    result(first_player_choice, second_player_choice)
  end

  defp result(first_player_choice, second_player_choice) do
    game_calc_result = game_calc(first_player_choice, second_player_choice)

    case game_calc_result do
      0 -> {:ok, "Draw!"}
      1 -> {:ok, "First player win!!!"}
      _ -> {:ok, "Second player win!!!"}
    end
  end

  defp game_calc(first_player_item, second_player_item) do
    rem(first_player_item - second_player_item, 3)
  end
end
Enter fullscreen mode Exit fullscreen mode

And now, if we rerun the tests:

mix test
Enter fullscreen mode Exit fullscreen mode

We'll see only the tests failure:

Compiling 1 file (.ex)
....

  1) test Game.play/2 when first player wins when first player chooses stone and second player chooses scissors (GameTest)
     test/game_test.exs:55
     Assertion with == failed
     code:  assert match == "First player win!!!"
     left:  "Second player win!!!"
     right: "First player win!!!"
     stacktrace:
       test/game_test.exs:61: (test)

....

Finished in 0.04 seconds (0.00s async, 0.04s sync)
9 tests, 1 failure

Randomized with seed 730068
Enter fullscreen mode Exit fullscreen mode

Understanding the failure message

The failure is because we pass to kernel function rem/2 a dividend negative in our formula. And according to the documentation, this kernel function uses truncated division, which means that the result will always have the sign of the dividend.

When first player wins when first player chooses stone and second player chooses scissors, the result is -2:

# stone = 1
# paper = 2
# scissor = 3

# R = (first_player_choice - second_player_choice) % 3
# R = (stone - scissors) % 3
# R = (1 - 3) % 3

# In elixir using rem/2

rem(1-3, 3)
> -2
Enter fullscreen mode Exit fullscreen mode

Solving the failure message

According to the documentation, the function Integer.mod/2 Computes the modulo remainder of an integer division.

It's important to know: Integer.mod/2 uses floored division, which means that the result will always have the sign of the divisor.

So, when first player wins when first player chooses stone and second player chooses scissors, the result is 1:

# stone = 1
# paper = 2
# scissor = 3

# R = (first_player_choice - second_player_choice) % 3
# R = (stone - scissors) % 3
# R = (1 - 3) % 3

# In elixir using rem/2

Integer.mod(1-3, 3)
> 1
Enter fullscreen mode Exit fullscreen mode

So, To solve the failure message, we need to remove the rem/2 function and add the Integer.mod/2:

defmodule Game do
  @moduledoc """
  Documentation for `Game`.
  """

  def play(first_player_choice, second_player_choice) do
    result(first_player_choice, second_player_choice)
  end

  defp result(first_player_choice, second_player_choice) do
    game_calc_result = game_calc(first_player_choice, second_player_choice)

    case game_calc_result do
      0 -> {:ok, "Draw!"}
      1 -> {:ok, "First player win!!!"}
      _ -> {:ok, "Second player win!!!"}
    end
  end

  defp game_calc(first_player_item, second_player_item) do
    Integer.mod(first_player_item - second_player_item, 3)
  end
end
Enter fullscreen mode Exit fullscreen mode

And now, finally, if we rerun the tests:

mix test
Enter fullscreen mode Exit fullscreen mode

All the tests pass with success \o/:

Compiling 1 file (.ex)
.........

Finished in 0.04 seconds (0.00s async, 0.04s sync)
9 tests, 0 failures

Randomized with seed 992719
Enter fullscreen mode Exit fullscreen mode

It's time to celebrate, the game Rock, Paper, and Scissors is "done"!

Repository of the project: https://github.com/dnovais/rock_paper_scissor_elixir

See you soon!

Contacts

Email: contato@diegonovais.com.br
Linkedin: https://www.linkedin.com/in/diegonovais/
Twitter: https://twitter.com/diegonovaistech


Sources and references

Top comments (0)