# ┌────────────────────────────────────────────────────────────────────┐ # │ Based on the book "Functional Web Development" by Lance Halvorsen. │ # └────────────────────────────────────────────────────────────────────┘ defmodule Islands.Board do @moduledoc """ A `board` struct and functions for the _Game of Islands_. The `board` struct contains the fields islands and misses representing the characteristics of a board in the _Game of Islands_. ##### Based on the book [Functional Web Development](https://pragprog.com/book/lhelph/functional-web-development-with-elixir-otp-and-phoenix) by Lance Halvorsen. """ alias __MODULE__ alias __MODULE__.Response alias Islands.{Coord, Island} @island_types [:atoll, :dot, :l_shape, :s_shape, :square] @derive [Poison.Encoder] @derive Jason.Encoder @enforce_keys [:islands, :misses] defstruct [:islands, :misses] @typedoc "A map assigning islands to their types" @type islands :: %{Island.type() => Island.t()} @type t :: %Board{islands: islands, misses: Island.coords()} @doc """ Returns an empty `board` struct. """ @spec new :: t def new, do: %Board{islands: %{}, misses: MapSet.new()} @doc """ Positions `island` on `board` and returns an updated board or `{:error, reason}` if `island` overlaps another `board`'s island. """ @spec position_island(t, Island.t()) :: t | {:error, atom} def position_island(%Board{} = board, %Island{} = island) do if overlaps_board_islands?(island, board.islands), do: {:error, :overlapping_island}, else: put_in(board.islands[island.type], island) end @doc """ Checks if all islands have been positioned on `board`. """ @spec all_islands_positioned?(t) :: boolean def all_islands_positioned?(%Board{} = board) do Enum.all?(@island_types, &Map.has_key?(board.islands, &1)) end @doc """ Checks if `guess` hit any island on `board` and returns a response. """ @spec guess(t, Coord.t()) :: Response.t() def guess(%Board{} = board, %Coord{} = guess) do Response.check_guess(board, guess) |> Response.format_response(board) end @doc """ Returns a list of island types for forested islands. """ @spec forested_types(t) :: [Island.type()] def forested_types(%Board{islands: islands} = _board) do islands |> Map.values() |> Enum.filter(&Island.forested?/1) |> Enum.map(& &1.type) end @doc """ Returns the `board`'s total number of hits. """ @spec hits(t) :: non_neg_integer def hits(%Board{islands: islands} = _board) do islands |> Map.values() |> Enum.map(&MapSet.size(&1.hits)) |> Enum.sum() end @doc """ Returns the `board`'s total number of misses. """ @spec misses(t) :: non_neg_integer def misses(%Board{misses: misses} = _board), do: MapSet.size(misses) @doc """ Returns a map assigning the CSS grid position of each island on `board` to its island type. """ @spec grid_positions(t) :: %{Island.type() => Island.grid_position()} def grid_positions(%Board{islands: islands} = _board) do for {type, island} <- islands, into: %{} do {type, Island.grid_position(island)} end end @doc """ Returns a map assigning the list of hits "cells" of each island on `board` to its island type. """ @spec hit_cells(t) :: %{Island.type() => [Island.grid_cell()]} def hit_cells(%Board{islands: islands} = _board) do for {type, island} <- islands, into: %{} do {type, Island.hit_cells(island)} end end @doc """ Returns a map assigning to :squares the list of square numbers for the `board`'s misses. """ @spec miss_squares(t) :: %{:squares => [Coord.square()]} def miss_squares(%Board{misses: misses} = _board) do %{squares: Enum.map(misses, &Coord.to_square/1)} end ## Private functions @spec overlaps_board_islands?(Island.t(), Board.islands()) :: boolean defp overlaps_board_islands?(new_island, islands) do Enum.any?(islands, fn {type, island} -> type != new_island.type and Island.overlaps?(new_island, island) end) end end