 # davearonson's solution

## to Robot Simulator in the Elixir Track

Published at Jul 13 2018 · 0 comments
Instructions
Test suite
Solution

#### Note:

This solution was written on an old version of Exercism. The tests below might not correspond to the solution code, and the exercise may have changed since this code 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
@type t :: %RobotSimulator{ direction: atom, position: {integer, integer} }
defstruct direction: :north, position: {0,0}
@directions [:north, :east, :south, :west]
@directions_to_numbers %{north: 0, east: 1, south: 2, west: 3}
@numbers_to_directions %{0 => :north, 1 => :east, 2 => :south, 3 => :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 }) :: RobotSimulator.t()

def create(direction \\ :north, position \\ {0,0}) do
cond do
! Enum.member?(@directions, direction) ->
{ :error, "invalid direction" }
! (is_tuple(position) &&
tuple_size(position) == 2 &&
is_integer(elem(position, 0)) &&
is_integer(elem(position, 1))) ->
{ :error, "invalid position" }
true ->
%RobotSimulator{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
do_simulate(robot, instructions |> String.graphemes)
end

defp do_simulate(robot, [instruction|more]) do
case instruction do
"L" -> do_simulate(turn(robot, -1), more)
"R" -> do_simulate(turn(robot,  1), more)
_   -> { :error, "invalid instruction" }
end
end
defp do_simulate(robot, []), do: robot

# TODO MAYBE: we could probably do some funny math
# on the direction index, to replace case....
case robot.direction do
:north -> move(robot, 1,  1)
:east  -> move(robot, 0,  1)
:south -> move(robot, 1, -1)
:west  -> move(robot, 0, -1)
end
end

defp move(robot, axis, amount) do
create(robot.direction,
put_elem(robot.position,
axis,
elem(robot.position, axis) + amount))
end

defp turn(robot, way) do
# add 4 just in case it's a left turn from north
new_dir_num = rem(@directions_to_numbers[robot.direction] + way + 4, 4)
create(@numbers_to_directions[new_dir_num], robot.position)
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

@doc """
Return the robot's position, as a tuple of x,y coordinates.
"""
@spec position(robot :: any) :: { integer, integer }
def position(robot), do: robot.position
end``````