Another Mars Rover Solution

6 minute read

I’ve noticed people like to share their solutions on the Github. No matter how good or bad was the design they still would like to show it to others (and others usually don’t care because they’re too lazy unless you’ve got to implement this problem yourself).

You can make any design for the problem, you can split and isolate as much as you want or you can make almost single line approach with Python.
No matter what your decision is going to be, as always, you have your own why’s – Why did you do it that way. Some people may disagree, someone may agree, but the fact is that the program works and now you can concentrate on the efficiency and its design.

Another obvious fact for me is that the articles at least for me are more interesting to read then just to go through the code line by line because it’s entertaining to hear the story behind when the person highlights and explains what he cares about while he does the design. I think it’s quite remarkable and it is what makes the difference between artist, engineer, and someone who never cares about a thing.

The problem

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 is represented by a combination of an 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. Test Input: 5 5 1 2 N LMLMLMLMM 3 3 E MMRMMRMRRM Expected Output: 1 3 N 5 1 E

Challenge comes from here.

Tests

First of all, as you see I didn’t want to have any dependency and thus I decided to rely only on the MiniTests because let’s be honest, this program won’t get anywhere close to NASA. The simpler the solution and the package is going to be – the higher chances that people will get what you mean.
It’s like the algorithm of encryption. Imagine you make it super hard and you add complexity to your solution, it looks somewhat cool but nobody is going to get the perfectness except you. It is quite possible that you will end up figuratively speaking cutting your ear off and we surely don’t want it.

You know myself I have a bachelor of graphic design – if you remember. What did it teach me first of all “All that glitters is not gold“. So some may see a bullshit instead of gold and not always it’s easy to convince them than to do another way around.

Code

Another interesting thing. I didn’t want to create many classes to keep it encapsulated everywhere and to hide all the data deep inside I mean some people like it, but I personally found it is a time waster in the concrete case. (Please leave a comment if you have a different opinion) I mean, as I said it won’t get close to NASA. Besides, after the solving “Wagon and small truck” of such problems as Russians would say, at the end, you might want to have a simple, short but still easy to read and maintain solution for someone who sees it for the first time.

We have 4 directions right? It means [N, E, S, W] – 0, 1, 2, 3 yeah? As I add +1 to the index it goes from N to E, and then from E to W. As I move left which is -1 it goes from W to S and backward to N. Whenever program receives command “L” I do -1 and when I get “R” I do +1. For “M” I move forward which means + or – for X or Y

Our class begins like this

class RoverProblem
  DIRECTIONS = %w(N E S W).freeze
  attr_reader :face, :x, :y
  def initialize(max_x, max_y)
    @x = 0
    @y = 0
    @max_x = max_x.to_i
    @max_y = max_y.to_i
  end
end

We basically (I have a tendency to say this word too frequently) have to add the right operator for the direction we need.

def face_to(operator, step)
  idx = DIRECTIONS.index(@face).method(operator).call(step) % 4
  @face = DIRECTIONS[idx]
end

Explanation: I know it looks cool though, in fact, it does N which is 0 + 1 % 4 or **0 – 1 % 4 **remainder (%) we need to simplify the code and to avoid writing if-else statements where we don’t need it. So in other words, if it looks the North and you turn your machine 5 times it’s going to see the East.

But what to do with positioning? If you have tried to implement any game you might remember that usually, you change the position of the actor in space. For that matter, you modify the position by changing current position = current position + 1 or vica versa -1.

All in all, depending on the @face variable we check where should we add or subtract the number.

case @face
when 'N'
  @y += 1
when 'S'
  @y -= 1
when 'E'
  @x += 1
when 'W'
  @x -= 1
end

Instead of using split(”) I go with each_char here. I assume it’s more efficient and more elegant way to iterate through the string, parsing each character. However, the choice what you want to use is up to you.

def process(commands)
  commands.each_char do |character|
    case character
      when 'L' then turn_left
      when 'R' then turn_right
      when 'M' then move_forward
    end
  end

  [@x, @y, @face].join(' ')
end

And besides other small things, we end up implementing the parse class method, where we parse the input, create rover instance and then process the commands.

def self.parse(string)
  commands = string.split("\n")
  rover = new(*commands.delete_at(0).split(' '))
  output = ''
  commands.each_slice(2) do |deploy_coords, instructions|
    rover.deploy(*deploy_coords.split(' '))
    output << rover.process(instructions) + "\n"
  end
  output
 end

And as always leave your comments, likes and let me know what I could improve. I would be happy to hear your recommendations and thoughts.

My code implementation can be found here.
Cheers, brothers, and sisters! High five and 🙌 may god bless you and your skills!
✝️ Amen.