Avatar of paulfioravanti

paulfioravanti's solution

to Robot Simulator in the Ruby Track

Published at May 24 2019 · 0 comments
Instructions
Test suite
Solution

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.

For installation and learning resources, refer to the Ruby resources page.

For running the tests provided, you will need the Minitest gem. Open a terminal window and run the following command to install minitest:

gem install minitest

If you would like color output, you can require 'minitest/pride' in the test file, or note the alternative instruction, below, for running the test file.

Run the tests from the exercise directory using the following command:

ruby robot_simulator_test.rb

To include color from the command line:

ruby -r minitest/pride robot_simulator_test.rb

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.rb

require 'minitest/autorun'
require_relative 'robot_simulator'

class RobotTurningTest < Minitest::Test
  attr_reader :robot

  def setup
    @robot = Robot.new
  end

  def test_robot_bearing
    [:east, :west, :north, :south].each do |direction|
      robot.orient(direction)
      assert_equal direction, robot.bearing
    end
  end

  def test_invalid_robot_bearing
    skip
    assert_raises ArgumentError do
      robot.orient(:crood)
    end
  end

  def test_turn_right_from_north
    skip
    robot.orient(:north)
    robot.turn_right
    assert_equal :east, robot.bearing
  end

  def test_turn_right_from_east
    skip
    robot.orient(:east)
    robot.turn_right
    assert_equal :south, robot.bearing
  end

  def test_turn_right_from_south
    skip
    robot.orient(:south)
    robot.turn_right
    assert_equal :west, robot.bearing
  end

  def test_turn_right_from_west
    skip
    robot.orient(:west)
    robot.turn_right
    assert_equal :north, robot.bearing
  end

  def test_turn_left_from_north
    skip
    robot.orient(:north)
    robot.turn_left
    assert_equal :west, robot.bearing
  end

  def test_turn_left_from_east
    skip
    robot.orient(:east)
    robot.turn_left
    assert_equal :north, robot.bearing
  end

  def test_turn_left_from_south
    skip
    robot.orient(:south)
    robot.turn_left
    assert_equal :east, robot.bearing
  end

  def test_turn_left_from_west
    skip
    robot.orient(:west)
    robot.turn_left
    assert_equal :south, robot.bearing
  end

  def test_robot_coordinates
    skip
    robot.at(3, 0)
    assert_equal [3, 0], robot.coordinates
  end

  def test_other_robot_coordinates
    skip
    robot.at(-2, 5)
    assert_equal [-2, 5], robot.coordinates
  end

  def test_advance_when_facing_north
    skip
    robot.at(0, 0)
    robot.orient(:north)
    robot.advance
    assert_equal [0, 1], robot.coordinates
  end

  def test_advance_when_facing_east
    skip
    robot.at(0, 0)
    robot.orient(:east)
    robot.advance
    assert_equal [1, 0], robot.coordinates
  end

  def test_advance_when_facing_south
    skip
    robot.at(0, 0)
    robot.orient(:south)
    robot.advance
    assert_equal [0, -1], robot.coordinates
  end

  def test_advance_when_facing_west
    skip
    robot.at(0, 0)
    robot.orient(:west)
    robot.advance
    assert_equal [-1, 0], robot.coordinates
  end
end

class RobotSimulatorTest < Minitest::Test
  def simulator
    @simulator ||= Simulator.new
  end

  def test_instructions_for_turning_left
    skip
    assert_equal [:turn_left], simulator.instructions('L')
  end

  def test_instructions_for_turning_right
    skip
    assert_equal [:turn_right], simulator.instructions('R')
  end

  def test_instructions_for_advancing
    skip
    assert_equal [:advance], simulator.instructions('A')
  end

  def test_series_of_instructions
    skip
    commands = [:turn_right, :advance, :advance, :turn_left]
    assert_equal commands, simulator.instructions('RAAL')
  end

  def test_instruct_robot
    skip
    robot = Robot.new
    simulator.place(robot, x: -2, y: 1, direction: :east)
    simulator.evaluate(robot, 'RLAALAL')
    assert_equal [0, 2], robot.coordinates
    assert_equal :west, robot.bearing
  end

  def test_instruct_many_robots
    skip
    robot1 = Robot.new
    robot2 = Robot.new
    robot3 = Robot.new
    simulator.place(robot1, x: 0, y: 0, direction: :north)
    simulator.place(robot2, x: 2, y: -7, direction: :east)
    simulator.place(robot3, x: 8, y: 4, direction: :south)
    simulator.evaluate(robot1, 'LAAARALA')
    simulator.evaluate(robot2, 'RRAAAAALA')
    simulator.evaluate(robot3, 'LAAARRRALLLL')

    assert_equal [-4, 1], robot1.coordinates
    assert_equal :west, robot1.bearing

    assert_equal [-3, -8], robot2.coordinates
    assert_equal :south, robot2.bearing

    assert_equal [11, 5], robot3.coordinates
    assert_equal :north, robot3.bearing
  end
end
# frozen_string_literal: true

class Robot
  BEARINGS = %i[north east south west].freeze
  private_constant :BEARINGS
  LEFT = -1
  private_constant :LEFT
  RIGHT = 1
  private_constant :RIGHT

  attr_reader :bearing, :coordinates

  def orient(direction)
    raise ArgumentError unless BEARINGS.include?(direction)

    self.bearing = direction
  end

  def at(x_coord, y_coord)
    self.coordinates = [x_coord, y_coord]
    self
  end

  def advance
    x, y = coordinates
    case bearing
    when :north then at(x, y + 1)
    when :east then at(x + 1, y)
    when :south then at(x, y - 1)
    else at(x - 1, y)
    end
  end

  def turn_left
    turn(LEFT)
  end

  def turn_right
    turn(RIGHT)
  end

  private

  attr_writer :bearing, :coordinates

  def turn(direction)
    bearing
      .then(&BEARINGS.method(:index))
      .to_enum(:then)
      .with_object(direction)
      .next
      .then(&method(:determine_new_direction))
      .then(&method(:orient))
  end

  def determine_new_direction((index, direction))
    direction
      .then(&BEARINGS.method(:rotate))
      .fetch(index)
  end
end

class Simulator
  INSTRUCTIONS = {
    "A" => :advance,
    "L" => :turn_left,
    "R" => :turn_right
  }.freeze
  private_constant :INSTRUCTIONS

  def instructions(input)
    input
      .chars
      .map(&INSTRUCTIONS)
  end

  # rubocop:disable Naming/UncommunicativeMethodParamName
  def place(robot, x:, y:, direction:)
    robot
      .at(x, y)
      .orient(direction)
  end
  # rubocop:enable Naming/UncommunicativeMethodParamName

  def evaluate(robot, input)
    input
      .then(&method(:instructions))
      .each
      .with_object(robot, &method(:instruct_robot))
  end

  private

  def instruct_robot(instruction, robot)
    robot.public_send(instruction)
  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?