Avatar of paulfioravanti

paulfioravanti's solution

to Robot Simulator in the Elixir Track

Published at Jul 29 2019 · 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
  • advance

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
    • Advance twice
    • Turn left
    • Advance once
    • 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

If you're stuck on something, it may help to look at some of the available resources out there where answers might be found.

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
  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
  defstruct direction: :north, position: {0, 0}

  @bearings [:north, :east, :south, :west]
  @instructions %{
    "A" => :advance,
    "L" => :turn_left,
    "R" => :turn_right
  }

  alias __MODULE__, as: RobotSimulator

  defguardp invalid_direction?(direction) when not (direction in @bearings)

  defguardp invalid_position?(position)
            when not is_tuple(position) or
                   tuple_size(position) != 2 or
                   not is_integer(elem(position, 0)) or
                   not is_integer(elem(position, 1))

  @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 \\ nil, position \\ nil)
  def create(nil, nil), do: %RobotSimulator{}

  def create(direction, _position) when invalid_direction?(direction) do
    {:error, "invalid direction"}
  end

  def create(_direction, position) when invalid_position?(position) do
    {:error, "invalid position"}
  end

  def create(direction, position) do
    %RobotSimulator{direction: direction, position: position}
  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.graphemes()
    |> Enum.map(&parse_instruction/1)
    |> Enum.reduce(robot, &apply(RobotSimulator, &1, [&2]))
  catch
    :invalid_instruction ->
      {:error, "invalid instruction"}
  end

  @doc """
  Return the robot's direction.

  Valid directions are: `:north`, `:east`, `:south`, `:west`
  """
  @spec direction(robot :: any) :: atom
  def direction(%RobotSimulator{direction: direction}), do: direction

  @doc """
  Return the robot's position.
  """
  @spec position(robot :: any) :: {integer, integer}
  def position(%RobotSimulator{position: position}), do: position

  defp parse_instruction(instruction) do
    case Map.fetch(@instructions, instruction) do
      {:ok, instruction} ->
        instruction

      :error ->
        throw(:invalid_instruction)
    end
  end

  def turn_left(robot) do
    @bearings
    |> rotate(-1)
    |> turn(robot)
  end

  def turn_right(robot) do
    @bearings
    |> rotate(1)
    |> turn(robot)
  end

  def advance(robot) do
    {x, y} = robot.position

    new_position =
      case robot.direction do
        :north ->
          {x, y + 1}

        :east ->
          {x + 1, y}

        :south ->
          {x, y - 1}

        :west ->
          {x - 1, y}
      end

    %RobotSimulator{robot | position: new_position}
  end

  defp turn(new_bearings, robot) do
    index =
      @bearings
      |> Enum.find_index(&(&1 == robot.direction))

    new_direction =
      new_bearings
      |> Enum.fetch!(index)

    %RobotSimulator{robot | direction: new_direction}
  end

  defp rotate(list, 0), do: list

  defp rotate([head | tail], count) when count > 0 do
    rotate(tail ++ [head], count - 1)
  end

  defp rotate(list, count) when count < 0 do
    list
    |> Enum.reverse()
    |> rotate(abs(count))
    |> Enum.reverse()
  end
end

Community comments

Find this solution interesting? Ask the author a question to learn more.

What can you learn from this solution?

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.

  • What compromises have been made?
  • Are there new concepts here that you could read more about to improve your understanding?