lib/hangman/engine.ex

# ┌──────────────────────────────────────────────────────────────┐
# │ Based on the course "Elixir for Programmers" by Dave Thomas. │
# └──────────────────────────────────────────────────────────────┘
defmodule Hangman.Engine do
  @moduledoc """
  Models the _Hangman Game_. Times out after 30 minutes of inactivity.

  ##### Based on the course [Elixir for Programmers](https://codestool.coding-gnome.com/courses/elixir-for-programmers) by Dave Thomas.
  """

  alias __MODULE__.{DynGameSup, GameServer}
  alias Hangman.Game

  @doc """
  Starts a new game server process and supervises it.

  ## Examples

      iex> alias Hangman.Engine
      iex> {:ok, game_id} = Engine.new_game("Meg")
      iex> {:error, {:already_started, ^game_id}} = Engine.new_game("Meg")
      iex> is_pid(game_id)
      true
  """
  @spec new_game(Game.name()) :: Supervisor.on_start_child()
  def new_game(game_name) when is_binary(game_name) do
    DynamicSupervisor.start_child(DynGameSup, {GameServer, game_name})
  end

  @doc """
  Stops a game server process normally. It won't be restarted.

  ## Examples

      iex> alias Hangman.Engine
      iex> Engine.new_game("Ben")
      iex> Engine.end_game("Ben")
      :ok
  """
  @spec end_game(Game.name()) :: :ok
  def end_game(game_name) when is_binary(game_name) do
    GameServer.via(game_name) |> GenServer.stop(:shutdown)
  end

  @doc """
  Returns the tally of a game.

  ## Examples

      iex> alias Hangman.Engine
      iex> Engine.new_game("Jim")
      iex> tally = Engine.tally("Jim")
      iex> %{
      ...>   game_state: :initializing,
      ...>   turns_left: 7,
      ...>   letters: letters,
      ...>   guesses: []
      ...> } = tally
      iex> all_underscores? = Enum.all?(letters, & &1 == "_")
      iex> is_list(letters) and length(letters) > 0 and all_underscores?
      true
  """
  @spec tally(Game.name()) :: Game.tally()
  def tally(game_name) when is_binary(game_name) do
    GameServer.via(game_name) |> GenServer.call(:tally)
  end

  @doc """
  Makes a move by guessing a letter.

  ## Examples

      iex> alias Hangman.Engine
      iex> Engine.new_game("Ed")
      iex> Engine.make_move("Ed", "a").game_state in [:good_guess, :bad_guess]
      true
  """
  @spec make_move(Game.name(), Game.letter()) :: Game.tally()
  def make_move(game_name, guess)
      when is_binary(game_name) and is_binary(guess) do
    GameServer.via(game_name) |> GenServer.call({:make_move, guess})
  end
end