Created
November 29, 2012 12:35
-
-
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
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
= 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. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# 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