# ┌────────────────────────────────────────────────────────────────────┐
# │ 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