Write a robot simulator.
A robot factory's test facility needs a program to verify robot movements.
The robots have three possible movements:
Robots are placed on a hypothetical infinite grid, facing a particular direction (north, east, south, or west) at a set of {x,y} coordinates, e.g., {3,8}, with coordinates increasing to the north and east.
The robot then receives a number of instructions, at which point the testing facility verifies the robot's new position, and in which direction it is pointing.
Execute the tests with:
$ elixir robot_simulator_test.exs
In the test suites, all but the first test have been skipped.
Once you get a test passing, you can unskip the next one by
commenting out the relevant @tag :pending
with a #
symbol.
For example:
# @tag :pending
test "shouting" do
assert Bob.hey("WATCH OUT!") == "Whoa, chill out!"
end
Or, you can enable all the tests by commenting out the
ExUnit.configure
line in the test suite.
# ExUnit.configure exclude: :pending, trace: true
For more detailed information about the Elixir track, please see the help page.
Inspired by an interview question at a famous company.
It's possible to submit an incomplete solution so you can see how others have completed the exercise.
if !System.get_env("EXERCISM_TEST_EXAMPLES") do
Code.load_file("robot_simulator.exs", __DIR__)
end
ExUnit.start()
ExUnit.configure(exclude: :pending, trace: true)
defmodule RobotSimulatorTest do
use ExUnit.Case
test "create has sensible defaults" do
robot = RobotSimulator.create()
assert RobotSimulator.position(robot) == {0, 0}
assert RobotSimulator.direction(robot) == :north
end
@tag :pending
test "create works with valid arguments" do
robot = RobotSimulator.create(:north, {0, 0})
assert RobotSimulator.position(robot) == {0, 0}
assert RobotSimulator.direction(robot) == :north
robot = RobotSimulator.create(:south, {-10, 0})
assert RobotSimulator.position(robot) == {-10, 0}
assert RobotSimulator.direction(robot) == :south
robot = RobotSimulator.create(:east, {0, 10})
assert RobotSimulator.position(robot) == {0, 10}
assert RobotSimulator.direction(robot) == :east
robot = RobotSimulator.create(:west, {100, -100})
assert RobotSimulator.position(robot) == {100, -100}
assert RobotSimulator.direction(robot) == :west
end
@tag :pending
test "create errors if invalid direction given" do
position = {0, 0}
invalid_direction = {:error, "invalid direction"}
assert RobotSimulator.create(:invalid, position) == invalid_direction
assert RobotSimulator.create(0, position) == invalid_direction
assert RobotSimulator.create("east", position) == invalid_direction
end
@tag :pending
test "create errors if invalid position given" do
direction = :north
invalid_position = {:error, "invalid position"}
assert RobotSimulator.create(direction, {0, 0, 0}) == invalid_position
assert RobotSimulator.create(direction, {0, :invalid}) == invalid_position
assert RobotSimulator.create(direction, {"0", 0}) == invalid_position
assert RobotSimulator.create(direction, "invalid") == invalid_position
assert RobotSimulator.create(direction, 0) == invalid_position
assert RobotSimulator.create(direction, [0, 0]) == invalid_position
assert RobotSimulator.create(direction, nil) == invalid_position
end
@tag :pending
test "simulate robots" do
robot1 = RobotSimulator.create(:north, {0, 0}) |> RobotSimulator.simulate("LAAARALA")
assert RobotSimulator.direction(robot1) == :west
assert RobotSimulator.position(robot1) == {-4, 1}
robot2 = RobotSimulator.create(:east, {2, -7}) |> RobotSimulator.simulate("RRAAAAALA")
assert RobotSimulator.direction(robot2) == :south
assert RobotSimulator.position(robot2) == {-3, -8}
robot3 = RobotSimulator.create(:south, {8, 4}) |> RobotSimulator.simulate("LAAARRRALLLL")
assert RobotSimulator.direction(robot3) == :north
assert RobotSimulator.position(robot3) == {11, 5}
end
@tag :pending
test "simulate errors on invalid instructions" do
assert RobotSimulator.create() |> RobotSimulator.simulate("UUDDLRLRBASTART") ==
{:error, "invalid instruction"}
end
end
defmodule RobotSimulator do
@enforce_keys [:direction, :position]
defstruct direction: nil, position: nil
@right "R"
@left "L"
@advance "A"
@instructions [@right, @left, @advance]
@directions [:north, :east, :south, :west]
@doc """
Create a Robot Simulator given an initial direction and position.
Valid directions are: `:north`, `:east`, `:south`, `:west`
"""
@spec create(direction :: atom, position :: {integer, integer}) :: any
def create(direction \\ :north, position \\ {0, 0}) do
cond do
!direction_valid?(direction) ->
{:error, "invalid direction"}
!position_valid?(position) ->
{:error, "invalid position"}
true ->
%__MODULE__{
direction: direction,
position: position
}
end
end
@doc """
Simulate the robot's movement given a string of instructions.
Valid instructions are: "R" (turn right), "L", (turn left), and "A" (advance)
"""
@spec simulate(robot :: any, instructions :: String.t()) :: any
def simulate(robot, instructions) do
instructions = String.split(instructions, "", trim: true)
if Enum.all?(instructions, &instruction_valid?/1) do
Enum.reduce(instructions, robot, &execute/2)
else
{:error, "invalid instruction"}
end
end
@doc """
Return the robot's direction.
Valid directions are: `:north`, `:east`, `:south`, `:west`
"""
@spec direction(robot :: any) :: atom
def direction(robot) do
robot.direction
end
@doc """
Return the robot's position.
"""
@spec position(robot :: any) :: {integer, integer}
def position(robot) do
robot.position
end
defp execute(@right, robot), do: turn(robot, 1)
defp execute(@left, robot), do: turn(robot, -1)
defp execute(@advance, robot), do: advance(robot)
defp turn(robot, step) do
new_direction = Loop.move(@directions, robot.direction, step)
%__MODULE__{robot | direction: new_direction}
end
defp advance(robot) do
new_position = move(robot.position, robot.direction)
%__MODULE__{robot | position: new_position}
end
defp move({x, y}, :north), do: {x, y + 1}
defp move({x, y}, :east), do: {x + 1, y}
defp move({x, y}, :south), do: {x, y - 1}
defp move({x, y}, :west), do: {x - 1, y}
defp direction_valid?(direction) do
Enum.member?(@directions, direction)
end
defp position_valid?({x, y}) when is_integer(x) and is_integer(y), do: true
defp position_valid?(_), do: false
defp instruction_valid?(instruction) do
Enum.member?(@instructions, instruction)
end
end
defmodule Loop do
@moduledoc """
Operations on lists that treat them like a loop.
"""
def move(list, from, step) when is_list(list) do
new_index =
list
|> Enum.find_index(&(&1 == from))
|> Kernel.+(step)
|> Kernel.rem(Enum.count(list))
Enum.at(list, new_index)
end
end
A huge amount can be learned from reading other people’s code. This is why we wanted to give exercism users the option of making their solutions public.
Here are some questions to help you reflect on this solution and learn the most from it.
Level up your programming skills with 3,450 exercises across 52 languages, and insightful discussion with our volunteer team of welcoming mentors. Exercism is 100% free forever.
Sign up Learn More
Community comments