lib/islands/board/response.ex

defmodule Islands.Board.Response do
  @moduledoc """
  A 4-element tuple reflecting the effect of a guess (hit or miss) on a board.
  """

  alias Islands.{Board, Coord, Island}

  @typedoc "Preliminary response to a guess"
  @type guess_check :: {:hit, Island.t()} | {:miss, Coord.t()}
  @typedoc "Full response to a guess"
  @type t :: {:hit | :miss, Island.type() | :none, :no_win | :win, Board.t()}

  @doc """
  Checks if `guess` hit any island on `board`.

  Returns `{:hit, hit_island}`, where hit_island is the island hit by `guess`
  once updated, or `{:miss, guess}` if `guess` was a miss.
  """
  @spec check_guess(Board.t(), Coord.t()) :: guess_check
  def check_guess(%Board{} = board, %Coord{} = guess) do
    Enum.find_value(board.islands, {:miss, guess}, fn {_type, island} ->
      case Island.guess(island, guess) do
        {:hit, island} -> {:hit, island}
        :miss -> false
      end
    end)
  end

  @doc """
  Converts a preliminary response to a guess into a full response.
  """
  @spec format_response(guess_check, Board.t()) :: t
  def format_response({:hit, island} = _guess_check, %Board{} = board) do
    board = put_in(board.islands[island.type], island)
    {:hit, forest_check(island), win_check(board), board}
  end

  def format_response({:miss, guess} = _guess_check, %Board{} = board) do
    board = update_in(board.misses, &MapSet.put(&1, guess))
    {:miss, :none, :no_win, board}
  end

  ## Private functions

  @spec forest_check(Island.t()) :: Island.type() | :none
  defp forest_check(island) do
    if Island.forested?(island), do: island.type, else: :none
  end

  @spec win_check(Board.t()) :: :win | :no_win
  defp win_check(board), do: if(all_forested?(board), do: :win, else: :no_win)

  @spec all_forested?(Board.t()) :: boolean
  defp all_forested?(%Board{islands: islands} = _board) do
    Enum.all?(islands, fn {_type, island} -> Island.forested?(island) end)
  end
end