lib/islands/game.ex

# ┌────────────────────────────────────────────────────────────────────┐
# │ Based on the book "Functional Web Development" by Lance Halvorsen. │
# └────────────────────────────────────────────────────────────────────┘
defmodule Islands.Game do
  @moduledoc """
  A game struct and functions for the _Game of Islands_.

  The game struct contains the fields `name`, `player1`, `player2`, `request`,
  `response` and `state` representing the characteristics of a game in the
  _Game of Islands_.

  ##### Based on the book [Functional Web Development](https://pragprog.com/titles/lhelph/functional-web-development-with-elixir-otp-and-phoenix/) by Lance Halvorsen.
  """

  @behaviour Access

  use PersistConfig

  alias __MODULE__

  alias Islands.{
    Board,
    Coord,
    Guesses,
    Player,
    PlayerID,
    Request,
    Response,
    State
  }

  @adjectives get_env(:haiku_adjectives)
  @genders [:f, :m]
  @hit_or_miss [:hit, :miss]
  @nouns get_env(:haiku_nouns)
  @player_ids [:player1, :player2]

  @derive Jason.Encoder
  @enforce_keys [:name, :player1, :player2]
  defstruct name: nil,
            player1: nil,
            player2: nil,
            request: {},
            response: {},
            state: State.new()

  @typedoc "Game name"
  @type name :: String.t()
  @typedoc "A game overview map"
  @type overview :: %{
          game_name: name,
          player1: overview_player,
          player2: overview_player
        }
  @typedoc "A game overview player map"
  @type overview_player :: %{name: Player.name(), gender: Player.gender()}
  @typedoc "A game struct for the Game of Islands"
  @type t :: %Game{
          name: name,
          player1: Player.t(),
          player2: Player.t(),
          request: Request.t(),
          response: Response.t(),
          state: State.t()
        }

  # Access behaviour
  defdelegate fetch(game, key), to: Map
  defdelegate get_and_update(game, key, fun), to: Map
  defdelegate pop(game, key), to: Map

  @doc """
  Creates a game struct from `name`, `player1_name`, `gender` and `pid`.

  ## Examples

      iex> alias Islands.{Game, Player}
      iex> {player_name, gender, pid} = {"James", :m, self()}
      iex> game = Game.new("Sky Fall", player_name, gender, pid)
      iex> %Game{name: name, player1: player1} = game
      iex> %Player{name: ^player_name, gender: ^gender, pid: ^pid} = player1
      iex> {name, is_struct(player1, Player), is_struct(game.player2, Player)}
      {"Sky Fall", true, true}

      iex> alias Islands.Game
      iex> {player_name, gender, pid} = {"James", :m, self()}
      iex> Game.new('Sky Fall', player_name, gender, pid)
      {:error, :invalid_game_args}
  """
  @spec new(name, Player.name(), Player.gender(), pid) :: t | {:error, atom}
  def new(name, player1_name, gender, pid)
      when is_binary(name) and is_binary(player1_name) and is_pid(pid) and
             gender in @genders do
    %Game{
      name: name,
      player1: Player.new(player1_name, gender, pid),
      player2: Player.new("?", :f, nil)
    }
  end

  def new(_name, _player1_name, _gender, _pid), do: {:error, :invalid_game_args}

  @doc """
  Updates a player's board struct with `board`.
  """
  @spec update_board(t, PlayerID.t(), Board.t()) :: t
  def update_board(%Game{} = game, player_id, %Board{} = board)
      when player_id in @player_ids,
      do: put_in(game[player_id].board, board)

  @doc """
  Updates a player's guesses struct using `hit_or_miss` and `guess`.
  """
  @spec update_guesses(t, PlayerID.t(), Guesses.type(), Coord.t()) :: t
  def update_guesses(%Game{} = game, player_id, hit_or_miss, %Coord{} = guess)
      when player_id in @player_ids and hit_or_miss in @hit_or_miss do
    update_in(game[player_id].guesses, &Guesses.add(&1, hit_or_miss, guess))
  end

  @doc """
  Updates a player struct using `name`, `gender` and `pid`.
  """
  @spec update_player(t, PlayerID.t(), Player.name(), Player.gender(), pid) :: t
  def update_player(%Game{} = game, player_id, name, gender, pid)
      when player_id in @player_ids and is_binary(name) and is_pid(pid) and
             gender in @genders do
    player = %Player{game[player_id] | name: name, gender: gender, pid: pid}
    put_in(game[player_id], player)
  end

  @doc """
  Sends the game state to a player's process.
  """
  @spec notify_player(t, PlayerID.t()) :: t
  def notify_player(%Game{} = game, player_id) when player_id in @player_ids do
    send(game[player_id].pid, game.state.game_state)
    game
  end

  @doc """
  Returns a player's board struct.
  """
  @spec player_board(t, PlayerID.t()) :: Board.t()
  def player_board(%Game{} = game, player_id) when player_id in @player_ids,
    do: game[player_id].board

  @doc """
  Returns a player's opponent ID.
  """
  @spec opponent_id(PlayerID.t()) :: PlayerID.t()
  def opponent_id(:player1), do: :player2
  def opponent_id(:player2), do: :player1

  @doc """
  Updates the state struct.
  """
  @spec update_state(t, State.t()) :: t
  def update_state(%Game{} = game, %State{} = state),
    do: put_in(game.state, state)

  @doc """
  Updates the request tuple.
  """
  @spec update_request(t, Request.t()) :: t
  def update_request(%Game{} = game, request) when is_tuple(request),
    do: put_in(game.request, request)

  @doc """
  Updates the response tuple.
  """
  @spec update_response(t, Response.t()) :: t
  def update_response(%Game{} = game, response) when is_tuple(response),
    do: put_in(game.response, response)

  @doc """
  Returns a random name of 4 to 10 characters.
  """
  @spec random_name :: name
  def random_name do
    length = Enum.random(4..10)

    :crypto.strong_rand_bytes(length)
    |> Base.url_encode64()
    # Starting at 0 with length "length"...
    |> binary_part(0, length)
  end

  @doc """
  Returns a unique, URL-friendly name such as "bold-frog-8249".
  """
  @spec haiku_name :: name
  def haiku_name do
    [Enum.random(@adjectives), Enum.random(@nouns), :rand.uniform(9999)]
    |> Enum.join("-")
  end

  @doc """
  Returns the game overview map of `game`.
  """
  @spec overview(t) :: overview
  def overview(%Game{} = game) do
    %{
      game_name: game.name,
      player1: %{name: game.player1.name, gender: game.player1.gender},
      player2: %{name: game.player2.name, gender: game.player2.gender}
    }
  end

  ## Helpers

  defimpl Jason.Encoder, for: Tuple do
    @spec encode(tuple, Jason.Encode.opts()) :: iodata
    def encode(data, opts) when is_tuple(data) do
      Tuple.to_list(data) |> Jason.Encode.list(opts)
    end
  end
end