Powered by AppSignal & Oban Pro
Would you like to see your link here? Contact us

Hangman - Responsabilidades

03-single_responsability.livemd

Hangman - Responsabilidades

Implementación previa

defmodule Hangman do
  @moduledoc """
  The famous Hangman game
  """

  @doc """
  Starts the game
  """
  def start_game do
    state = %{word: "hangman", misses: [], matches: [], limit: 5, mask: "_", completed?: false}
    {mask_word(state), state}
  end

  @doc """
  Lets the user to take a guess
  """
  def take_a_guess(letter, %{limit: limit, completed?: false} = state) when limit > 0 do
    letter
    |> String.downcase()
    |> guess(state)
    |> format_response()
  end

  def take_a_guess(_letter, state), do: format_response(state)

  ## Helpers
  defp format_response(%{limit: limit, completed?: false} = state) when limit > 0 do
    {mask_word(state), state}
  end

  defp format_response(%{limit: limit, word: word} = state) when limit > 0 do
    {"You won, word was: #{word}", state}
  end

  defp format_response(%{word: word} = state) do
    {"Game Over, word was: #{word}", state}
  end

  defp mask_word(%{matches: [], mask: mask, word: word} = _state) do
    String.replace(word, ~r/./, mask)
  end

  defp mask_word(%{matches: matches, mask: mask, word: word}) do
    matches = Enum.join(matches)
    String.replace(word, ~r/[^#{matches}]/, mask)
  end

  defp guess(letter, state) do
    %{word: word, matches: matches, misses: misses, limit: limit} = state

    if String.contains?(word, letter) do
      matches = [letter | matches]
      completed? = word |> String.codepoints() |> Enum.all?(&(&1 in matches))
      %{state | matches: matches, completed?: completed?}
    else
      %{state | misses: [letter | misses], limit: limit - 1}
    end
  end
end

Asignando responsabilidades

En la sección anterior, mencionamos que el módulo Hangman, tal como está, mezcla algunas responsabilidades. Por ejemplo, algunas funciones privadas solo se ocupan de la presentación de los datos, otras funciones privadas se ocupan de la lógica del juego en sí, vamos a separar dichas responsabilidades antes de continuar.

En principio definamos lo que de ahora en adelante vamos a llamar capa de presentación, también conocida como vista en algunos entornos.

defmodule Hangman.View do
  @moduledoc """
  Presentation layer for the Hangman game
  """

  @doc """
  Returns a human-friendly response
  """
  def format_response(%{limit: limit, completed?: false} = state) when limit > 0 do
    {mask_word(state), state}
  end

  def format_response(%{limit: limit, word: word} = state) when limit > 0 do
    {"You won, word was: #{word}", state}
  end

  def format_response(%{word: word} = state) do
    {"Game Over, word was: #{word}", state}
  end

  ## Helpers
  defp mask_word(%{matches: [], mask: mask, word: word} = _state) do
    String.replace(word, ~r/./, mask)
  end

  defp mask_word(%{matches: matches, mask: mask, word: word}) do
    matches = Enum.join(matches)
    String.replace(word, ~r/[^#{matches}]/, mask)
  end
end

Definamos la lógica principal del juego:

defmodule Hangman.GameLogic do
  @moduledoc """
  Main logic for the game
  """

  @doc """
  Creates the initial game state
  """
  def init(word) do
    %{word: word, misses: [], matches: [], limit: 5, mask: "_", completed?: false}
  end

  @doc """
  Returns the game state after the user takes a guess
  """
  def guess(letter, state) do
    %{word: word, matches: matches, misses: misses, limit: limit} = state

    if String.contains?(word, letter) do
      matches = [letter | matches]
      completed? = word |> String.codepoints() |> Enum.all?(&(&1 in matches))
      %{state | matches: matches, completed?: completed?}
    else
      %{state | misses: [letter | misses], limit: limit - 1}
    end
  end
end

Modifiquemos el módulo principal que tiene la función de director de orquesta

defmodule Hangman do
  @moduledoc """
  The famous Hangman game
  """

  alias Hangman.GameLogic
  alias Hangman.View

  @doc """
  Starts the game
  """
  def start_game do
    word = "hangman"

    word
    |> GameLogic.init()
    |> View.format_response()
  end

  @doc """
  Lets the user to take a guess
  """
  def take_a_guess(letter, %{limit: limit, completed?: false} = state) when limit > 0 do
    letter
    |> String.downcase()
    |> GameLogic.guess(state)
    |> View.format_response()
  end

  def take_a_guess(_letter, state), do: View.format_response(state)
end

Nuestra implementación del juego ha sido organizado de la siguiente manera:

graph TD;
Hangman-->Hangman.View
Hangman-->Hangman.GameLogic

Probemos que hemos mantenido la funcionalidad previa después de aplicar la separación de responsabilidades.

{word, state} = Hangman.start_game()

Enum.reduce(["h", "a", "n", "g", "m"], state, fn letter, state ->
  {word, state} = Hangman.take_a_guess(letter, state)
  IO.inspect(word)
  state
end)

IO.puts("\nLets start a new game...\n")

{word, state} = Hangman.start_game()

Enum.reduce(["z", "q", "r", "i", "w", "p"], state, fn letter, state ->
  {word, state} = Hangman.take_a_guess(letter, state)
  IO.inspect(word)
  state
end)

Después que hemos separado nuestra implementación, podemos introducir algunas pequeñas mejoras.

Retos

  • Si bien nuestro objetivo en este punto no es obtener el mayor rendimiento, ¿qué pequeño cambio podrías introducir para mejorar la lógica del juego?
  • En la capa de presentación, en los pasos intermedios, parece que falta indicarle al jugador cuantos intentos le quedan disponibles, ¿puedes actualizar la salida que se le presenta al usuario?