DEV Community

James Robb
James Robb

Posted on

3 2

Mars Rover

In this post, we will be taking on the Mars Rover Kata. This challenge entails implementing a Rover which can drive from grid cell to grid cell on a grid based on commands. Commands are passed as a string of individual instructions, these instructions can be to move (M), turn left (L) or turn right (R).

Task solution

Tests

const {
  Rover
} = require("./rover");
let rover;

beforeEach(() => {
  rover = new Rover();
});

describe("rover", () => {
  it("Has an initial position of 0", () => {
    expect(rover.direction).toEqual(0);
  });

  it("Can move given instructions", () => {
    expect(rover.currentPosition).toEqual([0, 0]);
    rover.move("LMRMM");
    expect(rover.currentPosition).toEqual([0, 2]);
  });

  it("Translates direction correctly", () => {
    rover.translateInstructions(["L", "L", "R"]);
    expect(rover.direction).toEqual(-1);
  });

  it("Can move postion correctly", () => {
    rover.move("M");
    expect(rover.currentPosition).toEqual([0, 1]);
    rover.move("RM");
    expect(rover.currentPosition).toEqual([1, 1]);
    rover.move("RM");
    expect(rover.currentPosition).toEqual([1, 0]);
    rover.move("RM");
    expect(rover.currentPosition).toEqual([0, 0]);
    rover.move("M");
    expect(rover.currentPosition).toEqual([9, 0]);
    rover.move("LM");
    expect(rover.currentPosition).toEqual([9, 9]);
    rover.move("RRM");
    expect(rover.currentPosition).toEqual([9, 0]);
    rover.move("RM");
    expect(rover.currentPosition).toEqual([0, 0]);
  });

  it("throws when an invalid move is provided", () => {
    expect(() => rover.move("X")).toThrowErrorMatchingSnapshot();
  });
});
Enter fullscreen mode Exit fullscreen mode

Each test uses a new Rover instance and cover the following cases:

  1. Initial state
  2. Instruction execution
  3. Movement of the rover
  4. Error handling

We can also see that we are working with an x and y coordinate system for the rovers current position. You may also have noticed the integer based direction of the rover. It will make more sense as to why I chose to do directionality in this way once the implementation is seen but in short, we will have an array of potential directions, each of these will represent the points of a compass (North, South, East, West).

When we wish to see which direction we should move, we can use the % (modulo) operator which I explained in an earlier article to access the relevant direction. Since we are using 4 compass points we can only ever receive values between -4 and 4 when using any number modulo the count of compass points. I chose to only allow moves on positive values but we could use Math.abs to convert the negatives to positives and use them but the programme behaviour would change from how it is currently setup in the tests. Just as a side note, here are some examples of potential actions based on a direction modulod by the 4 compass points:

Direction Compass point Action
-1 -1 % 4 = -1 = None Don't move
2 2 % 4 = 2 = South Move down
5 5 % 4 = 1 = East Move right

Implementation

class Rover {
  constructor(gridDimension = 10) {
    this.currentPosition = [0, 0];
    this.direction = 0;
    this.compassPoints = ["N", "E", "S", "W"];
    this.gridDimension = gridDimension;
  }

  move(instructions) {
    const individualInstructions = instructions.split("");
    this.translateInstructions(individualInstructions);
  }

  shiftUp() {
    let [x, y] = this.currentPosition;
    if (y === this.gridDimension - 1) y = 0;
    else y = ++y;
    this.currentPosition = [x, y];
  }

  shiftDown() {
    let [x, y] = this.currentPosition;
    if (y === 0) y = this.gridDimension - 1;
    else y = --y;
    this.currentPosition = [x, y];
  }

  shiftLeft() {
    let [x, y] = this.currentPosition;
    if (x === 0) x = this.gridDimension - 1;
    else x = --x;
    this.currentPosition = [x, y];
  }

  shiftRight() {
    let [x, y] = this.currentPosition;
    if (x === this.gridDimension - 1) x = 0;
    else x = ++x;
    this.currentPosition = [x, y];
  }

  getCompassHeading() {
    return this.compassPoints[this.direction % this.compassPoints.length];
  }

  shiftRoverPosition() {
    const moveDirection = this.getCompassHeading();
    if (moveDirection === "N") this.shiftUp();
    else if (moveDirection === "S") this.shiftDown();
    else if (moveDirection === "E") this.shiftRight();
    else if (moveDirection === "W") this.shiftLeft();
  }

  translateInstructions(instructions) {
    instructions.forEach(instruction => {
      if (instruction === "L") this.direction--;
      else if (instruction === "R") this.direction++;
      else if (instruction === "M") this.shiftRoverPosition();
      else throw new Error("Invalid instruction provided");
    });
  }
}

module.exports = {
  Rover
};
Enter fullscreen mode Exit fullscreen mode

We interact with the Rover instance by calling the move method, this method takes 1 parameter, a string of instructions. This string is split into the individual characters and passed as an array into the translateInstructions function. Each instruction is checked and if the command is to move left (L), we add 1 from the current direction. If the command is to move right (R), we add one to the current direction. If the command is to move, we call the shiftRoverPosition method and finally, if the instruction is not recognised, we throw and error. The shiftRoverPosition method calls the getCompassHeading method which is where we try to get our value from the compass headings:

getCompassHeading() {
  return this.compassPoints[this.direction % this.compassPoints.length];
}
Enter fullscreen mode Exit fullscreen mode

If we get back a N, E, S or W, we move up, right, down or left respectively, in practice this merely means altering the x and y coordinates of the rover.

Conclusions

I actually did this Kata as part of an interview a while back and this was my solution. I will say though that this isn't the whole Kata, it is a stripped down version that the company I interviewed at used for their tech interview pair-programming session. I recommend trying it out yourself to see what you can come up with or extending the functionality for your rover to make it do even more than just move around a grid, why not give it a try and see what you come up with?

Image of Checkly

Replace beforeEach/afterEach with Automatic Fixtures in Playwright

  • Avoid repetitive setup/teardown in spec file
  • Use Playwright automatic fixtures for true global hooks
  • Monitor JS exceptions with a custom exceptionLogger fixture
  • Keep your test code clean, DRY, and production-grade

Watch video

Top comments (0)

Image of DataStax

Langflow: Simplify AI Agent Building

Connect models, vector stores, memory and other AI building blocks with the click of a button to build and deploy AI-powered agents.

Get started for free

👋 Kindness is contagious

DEV is better (more customized, reading settings like dark mode etc) when you're signed in!

Okay