🎉 Exercism Research is now launched. Help Exercism, help science and have some fun at research.exercism.io 🎉 # angelikatyborska's solution

## to Robot Simulator in the Elixir Track

Published at Nov 15 2018 · 0 comments
Instructions
Test suite
Solution

#### Note:

This exercise has changed since this solution was written.

Write a robot simulator.

A robot factory's test facility needs a program to verify robot movements.

The robots have three possible movements:

• turn right
• turn left

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.

• The letter-string "RAALAL" means:
• Turn right
• Turn left
• Turn left yet again
• Say a robot starts at {7, 3} facing north. Then running this stream of instructions should leave it at {9, 4} facing west.

## Running tests

Execute the tests with:

``````\$ elixir robot_simulator_test.exs
``````

### Pending tests

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.

## Source

Inspired by an interview question at a famous company.

## Submitting Incomplete Solutions

It's possible to submit an incomplete solution so you can see how others have completed the exercise.

### robot_simulator_test.exs

``````if !System.get_env("EXERCISM_TEST_EXAMPLES") do
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"
@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 turn(robot, step) do
new_direction = Loop.move(@directions, robot.direction, step)

%__MODULE__{robot | direction: new_direction}
end

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``````