# ┌────────────────────────────────────────────────────────────────────┐ # │ Based on the book "Functional Web Development" by Lance Halvorsen. │ # └────────────────────────────────────────────────────────────────────┘ defmodule Islands.Engine do use PersistConfig @book_ref Application.get_env(@app, :book_ref) @moduledoc """ Models an Islands game. ##### #{@book_ref} """ alias __MODULE__.{Coord, Game, Island, Server, Sup, Tally} @board_range Application.get_env(@app, :board_range) @island_types Application.get_env(@app, :island_types) @player_ids Application.get_env(@app, :player_ids) @doc """ Starts a new game. ## Examples iex> alias Islands.Engine iex> me = self() iex> {:ok, game_id} = Engine.new_game("Meg", me) iex> {:error, {:already_started, ^game_id}} = Engine.new_game("Meg", me) iex> is_pid(game_id) true """ @spec new_game(String.t(), pid) :: Supervisor.on_start_child() def new_game(player1_name, player1_pid) when is_binary(player1_name) and is_pid(player1_pid) do DynamicSupervisor.start_child(Sup, {Server, {player1_name, player1_pid}}) end @doc """ Ends a game. ## Examples iex> alias Islands.Engine iex> me = self() iex> Engine.new_game("Ben", me) iex> Engine.end_game("Ben") :ok """ @spec end_game(String.t()) :: :ok def end_game(player1_name) when is_binary(player1_name) do player1_name |> Server.via() |> GenServer.stop(:shutdown) end @doc """ Stops a game. ## Examples iex> alias Islands.Engine iex> alias Islands.Engine.{Grid, Tally} iex> him = self() iex> her = self() iex> {:ok, game_id} = Engine.new_game("Tarzan", him) iex> Engine.add_player("Tarzan", "Jane", her) iex> Engine.position_island("Tarzan", :player2, :atoll, 1, 1) iex> Engine.position_island("Tarzan", :player2, :l_shape, 3, 7) iex> Engine.position_island("Tarzan", :player2, :s_shape, 6, 2) iex> Engine.position_island("Tarzan", :player2, :square, 9, 5) iex> Engine.position_island("Tarzan", :player2, :dot, 9, 9) iex> Engine.set_islands("Tarzan", :player2) iex> tally = Engine.stop_game("Tarzan", :player2) iex> %Tally{ ...> game_state: :players_set, ...> player1_state: :islands_not_set, ...> player2_state: :islands_set, ...> request: {:stop, :player2}, ...> response: {:error, :not_both_players_islands_set}, ...> board: board, ...> guesses: guesses ...> } = tally iex> is_map(board) and guesses == Grid.new() and is_pid(game_id) true iex> alias Islands.Engine iex> alias Islands.Engine.{Grid, Tally} iex> him = self() iex> her = self() iex> {:ok, game_id} = Engine.new_game("Napoleon", him) iex> Engine.add_player("Napoleon", "Josephine", her) iex> Engine.position_island("Napoleon", :player2, :atoll, 1, 1) iex> Engine.position_island("Napoleon", :player2, :l_shape, 3, 7) iex> Engine.position_island("Napoleon", :player2, :s_shape, 6, 2) iex> Engine.position_island("Napoleon", :player2, :square, 9, 5) iex> Engine.position_island("Napoleon", :player2, :dot, 9, 9) iex> Engine.set_islands("Napoleon", :player2) iex> Engine.position_island("Napoleon", :player1, :atoll, 1, 1) iex> Engine.position_island("Napoleon", :player1, :l_shape, 3, 7) iex> Engine.position_island("Napoleon", :player1, :s_shape, 6, 2) iex> Engine.position_island("Napoleon", :player1, :square, 9, 5) iex> Engine.position_island("Napoleon", :player1, :dot, 9, 9) iex> Engine.set_islands("Napoleon", :player1) iex> tally = Engine.stop_game("Napoleon", :player1) iex> %Tally{ ...> game_state: :game_over, ...> player1_state: :islands_set, ...> player2_state: :islands_set, ...> request: {:stop, :player1}, ...> response: {:ok, :stopping}, ...> board: board, ...> guesses: guesses ...> } = tally iex> is_map(board) and guesses == Grid.new() and is_pid(game_id) true """ @spec stop_game(String.t(), Game.player_id()) :: Tally.t() def stop_game(player1_name, player_id) when is_binary(player1_name) and player_id in @player_ids do player1_name |> Server.via() |> GenServer.call({:stop, player_id}) end @doc """ Adds the second player of a game. ## Examples iex> alias Islands.Engine iex> alias Islands.Engine.{Grid, Tally} iex> him = self() iex> her = self() iex> {:ok, game_id} = Engine.new_game("Romeo", him) iex> tally = Engine.add_player("Romeo", "Juliet", her) iex> %Tally{ ...> game_state: :players_set, ...> player1_state: :islands_not_set, ...> player2_state: :islands_not_set, ...> request: {:add_player, "Juliet", player2_pid}, ...> response: {:ok, :player2_added}, ...> board: board, ...> guesses: guesses ...> } = tally iex> board == Grid.new() and guesses == Grid.new() and ...> player2_pid == her and is_pid(game_id) true """ @spec add_player(String.t(), String.t(), pid) :: Tally.t() def add_player(player1_name, player2_name, player2_pid) when is_binary(player1_name) and is_binary(player2_name) and is_pid(player2_pid) do player1_name |> Server.via() |> GenServer.call({:add_player, player2_name, player2_pid}) end @doc """ Positions an island on a player's board. # Examples iex> alias Islands.Engine iex> alias Islands.Engine.{Grid, Tally} iex> her = self() iex> him = self() iex> {:ok, game_id} = Engine.new_game("Bonnie", her) iex> Engine.add_player("Bonnie", "Clyde", him) iex> tally = Engine.position_island("Bonnie", :player2, :atoll, 1, 1) iex> %Tally{ ...> game_state: :players_set, ...> player1_state: :islands_not_set, ...> player2_state: :islands_not_set, ...> request: {:position_island, :player2, :atoll, 1, 1}, ...> response: {:ok, :island_positioned}, ...> board: board, ...> guesses: guesses ...> } = tally iex> is_map(board) and guesses == Grid.new() and is_pid(game_id) true """ @spec position_island( String.t(), Game.player_id(), Island.type(), Coord.row(), Coord.col() ) :: Tally.t() def position_island(player1_name, player_id, island_type, row, col) when is_binary(player1_name) and player_id in @player_ids and island_type in @island_types and row in @board_range and col in @board_range do player1_name |> Server.via() |> GenServer.call({:position_island, player_id, island_type, row, col}) end def position_all_islands(player1_name, player_id) when is_binary(player1_name) and player_id in @player_ids do player1_name |> Server.via() |> GenServer.call({:position_all_islands, player_id}) end @doc """ Declares all islands set for a player. ## Examples iex> alias Islands.Engine iex> alias Islands.Engine.{Grid, Tally} iex> him = self() iex> her = self() iex> {:ok, game_id} = Engine.new_game("Adam", him) iex> Engine.add_player("Adam", "Eve", her) iex> Engine.position_island("Adam", :player2, :atoll, 1, 1) iex> tally = Engine.set_islands("Adam", :player2) iex> %Tally{ ...> game_state: :players_set, ...> player1_state: :islands_not_set, ...> player2_state: :islands_not_set, ...> request: {:set_islands, :player2}, ...> response: {:error, :not_all_islands_positioned}, ...> board: board, ...> guesses: guesses ...> } = tally iex> is_map(board) and guesses == Grid.new() and is_pid(game_id) true iex> alias Islands.Engine iex> alias Islands.Engine.{Grid, Tally} iex> her = self() iex> him = self() iex> {:ok, game_id} = Engine.new_game("Mary", her) iex> Engine.add_player("Mary", "Joseph", him) iex> Engine.position_island("Mary", :player2, :atoll, 1, 1) iex> Engine.position_island("Mary", :player2, :l_shape, 3, 7) iex> Engine.position_island("Mary", :player2, :s_shape, 6, 2) iex> Engine.position_island("Mary", :player2, :square, 9, 5) iex> Engine.position_island("Mary", :player2, :dot, 9, 9) iex> tally = Engine.set_islands("Mary", :player2) iex> %Tally{ ...> game_state: :players_set, ...> player1_state: :islands_not_set, ...> player2_state: :islands_set, ...> request: {:set_islands, :player2}, ...> response: {:ok, :islands_set}, ...> board: board, ...> guesses: guesses ...> } = tally iex> is_map(board) and guesses == Grid.new() and is_pid(game_id) true """ @spec set_islands(String.t(), Game.player_id()) :: Tally.t() def set_islands(player1_name, player_id) when is_binary(player1_name) and player_id in @player_ids do player1_name |> Server.via() |> GenServer.call({:set_islands, player_id}) end @doc """ Allows a player to guess a coordinate. # ## Examples iex> alias Islands.Engine iex> alias Islands.Engine.{Grid, Tally} iex> him = self() iex> her = self() iex> {:ok, game_id} = Engine.new_game("Caesar", him) iex> Engine.add_player("Caesar", "Cleopatra", her) iex> Engine.position_island("Caesar", :player2, :atoll, 1, 1) iex> Engine.position_island("Caesar", :player2, :l_shape, 3, 7) iex> Engine.position_island("Caesar", :player2, :s_shape, 6, 2) iex> Engine.position_island("Caesar", :player2, :square, 9, 5) iex> Engine.position_island("Caesar", :player2, :dot, 9, 9) iex> Engine.set_islands("Caesar", :player2) iex> tally = Engine.guess_coord("Caesar", :player1, 9, 9) iex> %Tally{ ...> game_state: :players_set, ...> player1_state: :islands_not_set, ...> player2_state: :islands_set, ...> request: {:guess_coord, :player1, 9, 9}, ...> response: {:error, :islands_not_set}, ...> board: board, ...> guesses: guesses ...> } = tally iex> board == Grid.new() and guesses == Grid.new() and is_pid(game_id) true iex> alias Islands.Engine iex> alias Islands.Engine.{Grid, Tally} iex> him = self() iex> her = self() iex> {:ok, game_id} = Engine.new_game("Tristan", him) iex> Engine.add_player("Tristan", "Isolde", her) iex> Engine.position_island("Tristan", :player2, :atoll, 1, 1) iex> Engine.position_island("Tristan", :player2, :l_shape, 3, 7) iex> Engine.position_island("Tristan", :player2, :s_shape, 6, 2) iex> Engine.position_island("Tristan", :player2, :square, 9, 5) iex> Engine.position_island("Tristan", :player2, :dot, 9, 9) iex> Engine.set_islands("Tristan", :player2) iex> Engine.position_island("Tristan", :player1, :atoll, 1, 1) iex> Engine.position_island("Tristan", :player1, :l_shape, 3, 7) iex> Engine.position_island("Tristan", :player1, :s_shape, 6, 2) iex> Engine.position_island("Tristan", :player1, :square, 9, 5) iex> Engine.position_island("Tristan", :player1, :dot, 9, 9) iex> Engine.set_islands("Tristan", :player1) iex> tally = Engine.guess_coord("Tristan", :player1, 9, 9) iex> %Tally{ ...> game_state: :player2_turn, ...> player1_state: :islands_set, ...> player2_state: :islands_set, ...> request: {:guess_coord, :player1, 9, 9}, ...> response: {:hit, :dot, :no_win}, ...> board: board, ...> guesses: guesses ...> } = tally iex> is_map(board) and is_map(guesses) and is_pid(game_id) true """ @spec guess_coord(String.t(), Game.player_id(), Coord.row(), Coord.col()) :: Tally.t() def guess_coord(player1_name, player_id, row, col) when is_binary(player1_name) and player_id in @player_ids and row in @board_range and col in @board_range do player1_name |> Server.via() |> GenServer.call({:guess_coord, player_id, row, col}) end @doc """ Returns the tally of a game for a given player. # Examples iex> alias Islands.Engine iex> alias Islands.Engine.{Grid, Tally} iex> him = self() iex> {:ok, game_id} = Engine.new_game("Jim", him) iex> tally = Engine.tally("Jim", :player1) iex> %Tally{ ...> game_state: :initialized, ...> player1_state: :islands_not_set, ...> player2_state: :islands_not_set, ...> request: {}, ...> response: {}, ...> board: board, ...> guesses: guesses ...> } = tally iex> board == Grid.new() and guesses == Grid.new() and is_pid(game_id) true """ @spec tally(String.t(), Game.player_id()) :: Tally.t() def tally(player1_name, player_id) when is_binary(player1_name) and player_id in @player_ids do player1_name |> Server.via() |> GenServer.call({:tally, player_id}) end end