defmodule Robot do
@moduledoc """
Given a robot which can only move in four directions, UP(U), DOWN(D), LEFT(L), RIGHT(R).
Given a string consisting of instructions to move. Output the coordinates of a robot
after executing the instructions. Initial position of robot is at origin(0, 0).
If the robot exceeds the established limits it will travel to the negative limit of
the maximum position.
The maximum position by default is set to 5, but it can be changed.
For more details, see `Robot.setBounds/2`.
"""
@robot_position_changed_payload :robot_position_changed
alias Robot.Models.Point
## API
@doc """
Initialize a Robot process.
Returns `{:ok, #PID<0.162.0>}`.
## Examples
iex> {:ok, _pid} = Robot.start()
"""
@spec start(pid()) :: {:ok, pid()}
def start(caller), do: Agent.start_link(fn -> %Point{subscribers: [caller]} end)
def start(), do: Agent.start_link(fn -> %Point{} end)
@doc """
Stop a Robot process.
Returns `:ok`.
## Examples
iex> {:ok, pid} = Robot.start()
iex> :ok = Robot.stop(pid)
:ok
"""
@spec stop(pid()) :: :ok
def stop(pid), do: Agent.stop(pid)
@doc """
Change the Robot bounds by a given tuple `{x, y}`.
Returns `:ok`.
## Examples
iex> {:ok, pid} = Robot.start()
iex> :ok = Robot.setBounds(pid, {10, 10})
:ok
"""
@spec setBounds(pid(), tuple()) :: :ok
def setBounds(pid, {x, y}), do: Agent.update(pid, &(Map.put(&1, :bounds, %{x: x, y: y})))
@doc """
Move the Robot to the desired location according to the given commands.
Returns `:ok`.
## Examples
iex> {:ok, pid} = Robot.start()
iex> Robot.move(pid, "UUUDR")
:ok
"""
@spec move(pid(), String.t()) :: :ok
def move(pid, command) when is_binary(command) do
:ok =
command
|> split_command()
|> update_movement(pid)
end
def move(_pid, _command), do: :ok
@doc """
Get the current position of the Robot.
Returns `%Robot.Models.Point{}`.
## Examples
iex> {:ok, pid} = Robot.start()
iex> Robot.get(pid)
%Robot.Models.Point{x: 0, y: 0}
"""
@spec get(pid()) :: %Point{}
def get(pid), do: Agent.get(pid, &(&1))
## private helpers
defp update_movement(["U" | tail], pid) do
Agent.update(pid, &(Map.put(&1, :y, (&1.y + 1) |> adjust_point(&1.bounds.y))))
pid |> get() |> send_message_robot_moved()
update_movement(tail, pid)
end
defp update_movement(["D" | tail], pid) do
Agent.update(pid, &(Map.put(&1, :y, (&1.y - 1) |> adjust_point(&1.bounds.y))))
pid |> get() |> send_message_robot_moved()
update_movement(tail, pid)
end
defp update_movement(["L" | tail], pid) do
Agent.update(pid, &(Map.put(&1, :x, (&1.x - 1) |> adjust_point(&1.bounds.x))))
pid |> get() |> send_message_robot_moved()
update_movement(tail, pid)
end
defp update_movement(["R" | tail], pid) do
Agent.update(pid, &(Map.put(&1, :x, (&1.x + 1) |> adjust_point(&1.bounds.x))))
pid |> get() |> send_message_robot_moved()
update_movement(tail, pid)
end
defp update_movement(_, _pid), do: :ok
defp adjust_point(value, bounds) when (value >= (bounds * -1) and value <= bounds), do: value
defp adjust_point(_value, bounds), do: (bounds * -1)
defp split_command(command), do: String.split(command, "", trim: true)
defp send_message_robot_moved(%Point{subscribers: []}), do: :ok
defp send_message_robot_moved(%Point{subscribers: subscribers, x: x, y: y}) do
for pid <- subscribers, do: send(pid, {@robot_position_changed_payload, %{x: x, y: y}})
:ok
end
end