Skip to content

Instantly share code, notes, and snippets.

@ebababi
Created November 29, 2012 12:35
Show Gist options
  • Save ebababi/4168754 to your computer and use it in GitHub Desktop.
Save ebababi/4168754 to your computer and use it in GitHub Desktop.
MARS ROVER TEST: Simulation of a squad of robotic rovers that are to be landed by NASA on a plateau on Mars executing movement instructions
= MARS ROVER TEST
A squad of robotic rovers are to be landed by NASA on a plateau on Mars. This
plateau, which is curiously rectangular, must be navigated by the rovers so
that their on-board cameras can get a complete view of the surrounding
terrain to send back to Earth.
A rover's position and location is represented by a combination of x and y
co-ordinates and a letter representing one of the four cardinal compass
points. The plateau is divided up into a grid to simplify navigation. An
example position might be 0, 0, N, which means the rover is in the bottom
left corner and facing North.
In order to control a rover, NASA sends a simple string of letters. The
possible letters are 'L', 'R' and 'M'. 'L' and 'R' makes the rover spin 90
degrees left or right respectively, without moving from its current spot. 'M'
means move forward one grid point, and maintain the same heading.
Assume that the square directly North from (x, y) is (x, y+1).
== INPUT
The first line of input is the upper-right coordinates of the plateau, the
lower-left coordinates are assumed to be 0,0.
The rest of the input is information pertaining to the rovers that have been
deployed. Each rover has two lines of input. The first line gives the rover's
position, and the second line is a series of instructions telling the rover
how to explore the plateau.
The position is made up of two integers and a letter separated by spaces,
corresponding to the x and y co-ordinates and the rover's orientation.
Each rover will be finished sequentially, which means that the second rover
won't start to move until the first one has finished moving.
== OUTPUT
The output for each rover should be its final co-ordinates and heading.
== INPUT AND OUTPUT EXAMPLE
Test Input:
5 5
1 2 N
LMLMLMLMM
3 3 E
MMRMMRMRRM
Expected Output:
1 3 N
5 1 E
== OTHER ASSUMPTIONS
The assumption is made that if a rover has instructions to move outside the
plateau, it will perform the movement and warn the user. This decision was
based on the general principal that, since there were not clear business rules
on that, the system should act as a transparent process box, i.e. the users may
use it as they see fit.
== USAGE
To run the program execute on the command-line, being in program's path:
ruby mars_rovers.rb
or in Windows:
ruby.exe mars_rovers.rb
To run the automated unit testing execute:
ruby test/mars_rovers_test.rb
or in Windows:
ruby.exe test/mars_rovers_test.rb
Of course Ruby should be installed on the system and its installation directory
referenced in PATH.
require 'models'
# MarsRoversCLI acts as the controller of the application receiving user input,
# executing the business logic and outputing the results.
class MarsRoversCLI
# Receives data input in terms of the Map and the Rover entities and executes
# the instructions of each Rover instance, outputing the results.
def self.execute
# Initialize data store
rovers_with_instructions = Array.new
# Get data input
map = get_map
return unless map # Exit on erroneous input
begin
rover = get_rover
rovers_with_instructions << [rover, get_rover_instructions] if rover
end while rover # Allow arbritrary number of rover entities
# Execute instructions on each rover and output results
for rover_with_instructions in rovers_with_instructions
rover = rover_with_instructions.first
instructions = rover_with_instructions.last
for instruction in instructions.to_s.split('') # In Ruby 1.9 use each_char
case instruction
when 'M' then rover.move
when 'R', 'L' then rover.rotate(instruction == 'L')
end
end if instructions
puts 'ROVER OUT OF MAP BOUNDS' unless rover.in_map?(map.width, map.height)
puts '%d %d %s' % rover.position
end
end
protected
# Returns a Map instance based on width and height string input, or nil on
# empty line. Invalid input requires repetition.
def self.get_map
# TODO: Refactor input and its validation
until (input = gets) && (input =~ /(\d+)\s+(\d+)/)
return nil if input.to_s.chomp.empty?
puts "Invalid input"
end
Map.new($1.to_i, $2.to_i)
end
# Returns a Rover instance based on coordinates and heading string input, or
# nil on empty line. Invalid input requires repetition.
def self.get_rover
# TODO: Refactor input and its validation
until (input = gets) && (input =~ /(\d+)\s+(\d+)\s+([NESW])/i)
return nil if input.to_s.chomp.empty?
puts "Invalid input"
end
Rover.new($1.to_i, $2.to_i, $3.to_s.upcase)
end
# Returns a string of instructions based on input, or nil on empty line.
# Invalid input requires repetition.
def self.get_rover_instructions
# TODO: Refactor input and its validation
until (input = gets) && (input =~ /([RLM]+)/i)
return nil if input.to_s.chomp.empty?
puts "Invalid input"
end
return $1.to_s.upcase
end
end
#!/usr/bin/env ruby
$LOAD_PATH.unshift(File.join(File.dirname(__FILE__), 'lib'))
# This file acts as the entry-point executable to the project.
require 'controller'
MarsRoversCLI.execute
#!/usr/bin/env ruby
$LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '../lib'))
require 'test/unit'
require 'models'
# Test suite for Rover model class.
class TestRover < Test::Unit::TestCase #:nodoc:
# Test initialization
def test_initialization
rover = Rover.new(1, 2, 'W')
assert_equal(rover.x, 1)
assert_equal(rover.y, 2)
assert_equal(rover.heading, 3)
end
# Test movement
def test_movement
# Test movement to North
rover = Rover.new(1, 1, 'N')
rover.move
assert_equal(rover.position, [1, 2, 'N'])
# Test movement to East
rover = Rover.new(1, 1, 'E')
rover.move
assert_equal(rover.position, [2, 1, 'E'])
# Test movement to South
rover = Rover.new(1, 1, 'S')
rover.move
assert_equal(rover.position, [1, 0, 'S'])
# Test movement to West
rover = Rover.new(1, 1, 'W')
rover.move
assert_equal(rover.position, [0, 1, 'W'])
end
# Test rotation
def test_rotation
rover = Rover.new(0, 0, 'N')
rover.rotate(false)
assert_equal(rover.position, [0, 0, 'E'])
# Test rotation in HEADINGS bounds
rover = Rover.new(0, 0, 'N')
rover.rotate(true)
assert_equal(rover.position, [0, 0, 'W'])
rover = Rover.new(0, 0, 'W')
rover.rotate(false)
assert_equal(rover.position, [0, 0, 'N'])
end
# Test position reporting
def test_position_reporting
rover = Rover.new(1, 1, 'W')
assert_equal(rover.position, [1, 1, 'W'])
end
# Test map inclusion
def test_map_inclusion
# Test map inclusion in outside, on the border and inside the map.
rover = Rover.new(2, 2, 'W')
assert(!rover.in_map?(1, 1))
assert(rover.in_map?(2, 2))
assert(rover.in_map?(3, 3))
end
end
# Map struct class represents the rectangle mapped area of a plateau on Mars,
# defined by its width and height properties.
class Map < Struct.new(:width, :height); end;
# Rover class represents the vehicle entity of a robotic rover to be landed by
# NASA on a plateau on Mars. It implements the actions the vehicle may do and
# reports its status.
class Rover
HEADINGS = ['N', 'E', 'S', 'W'].freeze
# Initialize entity properties
attr_accessor :x, :y, :heading
# The Rover entity is initialized by setting up landing position and heading.
def initialize(x, y, heading)
@x, @y, @heading = x, y, HEADINGS.find_index(heading)
end
# Move the vehicle one map square towards current heading by updating the
# relevant position coordinate. The rover may find itself out of map bounds.
def move
case @heading
when 0 then @y += 1 # Move towards North
when 1 then @x += 1 # Move towards East
when 2 then @y -= 1 # Move towards South
when 3 then @x -= 1 # Move towards West
end
end
# Rotate the vehicle clockwise or counter-clockwise depending on the first
# argument by updating the heading property.
def rotate(counterclockwise = false)
if counterclockwise
@heading == 0 ? @heading = 3 : @heading -= 1
else
@heading == 3 ? @heading = 0 : @heading += 1
end
end
# Returns the vehicle current position and heading.
def position
[ @x, @y, Rover::HEADINGS[@heading] ]
end
# Returns true if the vehicle coordinates are within the rectangle defined by
# the arguments width and height.
def in_map?(width, height)
@x.between?(0, width) && @y.between?(0, height)
end
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment