lib/islands/state.ex

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

  The state struct contains the fields `game_state`, `player1_state` and
  `player2_state` for implementing a state machine 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

  alias __MODULE__
  alias Islands.PlayerID

  @player_ids [:player1, :player2]
  @player_turns [:player1_turn, :player2_turn]
  @position_actions [:position_island, :position_all_islands]

  @derive Jason.Encoder
  defstruct game_state: :initialized,
            player1_state: :islands_not_set,
            player2_state: :islands_not_set

  @typedoc "State machine event"
  @type event ::
          :add_player
          | {:position_island, PlayerID.t()}
          | {:position_all_islands, PlayerID.t()}
          | {:set_islands, PlayerID.t()}
          | {:guess_coord, PlayerID.t()}
          | {:stop, PlayerID.t()}
          | {:win_check, :no_win | :win}
  @typedoc "Game state"
  @type game_state ::
          :initialized
          | :players_set
          | :player1_turn
          | :player2_turn
          | :game_over
  @typedoc "Player state"
  @type player_state :: :islands_not_set | :islands_set
  @typedoc "A state struct for the Game of Islands"
  @type t :: %State{
          game_state: game_state,
          player1_state: player_state,
          player2_state: player_state
        }

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

  @doc """
  Creates a new state struct.

  ## Examples

      iex> Islands.State.new()
      %Islands.State{
        game_state: :initialized,
        player1_state: :islands_not_set,
        player2_state: :islands_not_set
      }
  """
  @spec new :: t
  def new, do: %State{}

  @doc """
  Decides whether to permit the `state`/`event` combination. Also decides
  whether to transition to a new state. Returns `{:ok, new_state}` if the
  combination is permissible. Returns `:error` if it is not.
  """
  @spec check(t, event) :: {:ok, t} | :error
  def check(state, event)

  def check(%State{game_state: :initialized} = state, :add_player),
    do: {:ok, put_in(state.game_state, :players_set)}

  def check(%State{game_state: :players_set} = state, {action, player_id})
      when action in @position_actions and player_id in @player_ids do
    case state[player_state_key(player_id)] do
      :islands_not_set -> {:ok, state}
      :islands_set -> :error
    end
  end

  def check(%State{game_state: :players_set} = state, {:set_islands, player_id})
      when player_id in @player_ids do
    state = put_in(state[player_state_key(player_id)], :islands_set)

    if both_players_islands_set?(state),
      do: {:ok, put_in(state.game_state, :player1_turn)},
      else: {:ok, state}
  end

  def check(
        %State{game_state: :player1_turn} = state,
        {:guess_coord, :player1}
      ),
      do: {:ok, put_in(state.game_state, :player2_turn)}

  def check(
        %State{game_state: :player2_turn} = state,
        {:guess_coord, :player2}
      ),
      do: {:ok, put_in(state.game_state, :player1_turn)}

  def check(%State{game_state: :player1_turn} = state, {:stop, :player1}),
    do: {:ok, put_in(state.game_state, :game_over)}

  def check(%State{game_state: :player2_turn} = state, {:stop, :player2}),
    do: {:ok, put_in(state.game_state, :game_over)}

  def check(%State{game_state: player_turn} = state, {:win_check, :no_win})
      when player_turn in @player_turns,
      do: {:ok, state}

  def check(%State{game_state: player_turn} = state, {:win_check, :win})
      when player_turn in @player_turns,
      do: {:ok, put_in(state.game_state, :game_over)}

  # Catchall clause
  def check(_state, _event), do: :error

  ## Private functions

  @spec both_players_islands_set?(t) :: boolean
  defp both_players_islands_set?(state) do
    state.player1_state == :islands_set and state.player2_state == :islands_set
  end

  @spec player_state_key(PlayerID.t()) :: atom
  defp player_state_key(:player1), do: :player1_state
  defp player_state_key(:player2), do: :player2_state
end