A programming kata is an exercise which helps a programmer hone his skills through practice and repetition.
This article is part of the series Java Tutorial Through Katas.
The article assumes that the reader already has experience with Java, that he is familiar with the basics of unit tests and that he knows how to run them from his favorite IDE (mine is IntelliJ IDEA).
Tests that prove that the solution is correct are displayed below. Recommended way to solve this kata is to use test-driven development approach (write the implementation for the first test, confirm that it passes and move to the next). Once all of the tests pass, the kata can be considered solved. For more information about best practices, please read the Test Driven Development (TDD): Best Practices Using Java Examples.
One possible solution is provided below the tests. Try to solve the kata by yourself first.
Mars Rover
Develop an api that moves a rover around on a grid.
Rules:
- You are given the initial starting point (x,y) of a rover and the direction (N,S,E,W) it is facing.
- The rover receives a character array of commands.
- Implement commands that move the rover forward/backward (f,b).
- Implement commands that turn the rover left/right (l,r).
- Implement wrapping from one edge of the grid to another. (planets are spheres after all)
- Implement obstacle detection before each move to a new square. If a given sequence of commands encounters an obstacle, the rover moves up to the last possible point and reports the obstacle.
Tests
Following is a set of unit tests that can be used to solve this kata in the TDD fashion.
package com.technologyconversations.kata.marsrover; | |
import org.junit.Before; | |
import org.junit.Test; | |
import java.util.ArrayList; | |
import java.util.Arrays; | |
import java.util.List; | |
import static org.assertj.core.api.Assertions.*; | |
/* | |
Source: http://dallashackclub.com/rover | |
Develop an api that moves a rover around on a grid. | |
* You are given the initial starting point (x,y) of a rover and the direction (N,S,E,W) it is facing. | |
* - The rover receives a character array of commands. | |
* - Implement commands that move the rover forward/backward (f,b). | |
* - Implement commands that turn the rover left/right (l,r). | |
* - Implement wrapping from one edge of the grid to another. (planets are spheres after all) | |
* - Implement obstacle detection before each move to a new square. | |
* If a given sequence of commands encounters an obstacle, the rover moves up to the last possible point and reports the obstacle. | |
*/ | |
public class RoverSpec { | |
private Rover rover; | |
private Coordinates roverCoordinates; | |
private final Direction direction = Direction.NORTH; | |
private Point x; | |
private Point y; | |
private List<Obstacle> obstacles; | |
@Before | |
public void beforeRoverTest() { | |
x = new Point(1, 9); | |
y = new Point(2, 9); | |
obstacles = new ArrayList<Obstacle>(); | |
roverCoordinates = new Coordinates(x, y, direction, obstacles); | |
rover = new Rover(roverCoordinates); | |
} | |
@Test | |
public void newInstanceShouldSetRoverCoordinatesAndDirection() { | |
assertThat(rover.getCoordinates()).isEqualToComparingFieldByField(roverCoordinates); | |
} | |
@Test | |
public void receiveSingleCommandShouldMoveForwardWhenCommandIsF() throws Exception { | |
int expected = y.getLocation() + 1; | |
rover.receiveSingleCommand('F'); | |
assertThat(rover.getCoordinates().getY().getLocation()).isEqualTo(expected); | |
} | |
@Test | |
public void receiveSingleCommandShouldMoveBackwardWhenCommandIsB() throws Exception { | |
int expected = y.getLocation() - 1; | |
rover.receiveSingleCommand('B'); | |
assertThat(rover.getCoordinates().getY().getLocation()).isEqualTo(expected); | |
} | |
@Test | |
public void receiveSingleCommandShouldTurnLeftWhenCommandIsL() throws Exception { | |
rover.receiveSingleCommand('L'); | |
assertThat(rover.getCoordinates().getDirection()).isEqualTo(Direction.WEST); | |
} | |
@Test | |
public void receiveSingleCommandShouldTurnRightWhenCommandIsR() throws Exception { | |
rover.receiveSingleCommand('R'); | |
assertThat(rover.getCoordinates().getDirection()).isEqualTo(Direction.EAST); | |
} | |
@Test | |
public void receiveSingleCommandShouldIgnoreCase() throws Exception { | |
rover.receiveSingleCommand('r'); | |
assertThat(rover.getCoordinates().getDirection()).isEqualTo(Direction.EAST); | |
} | |
@Test(expected = Exception.class) | |
public void receiveSingleCommandShouldThrowExceptionWhenCommandIsUnknown() throws Exception { | |
rover.receiveSingleCommand('X'); | |
} | |
@Test | |
public void receiveCommandsShouldBeAbleToReceiveMultipleCommands() throws Exception { | |
int expected = x.getLocation() + 1; | |
rover.receiveCommands("RFR"); | |
assertThat(rover.getCoordinates().getX().getLocation()).isEqualTo(expected); | |
assertThat(rover.getCoordinates().getDirection()).isEqualTo(Direction.SOUTH); | |
} | |
@Test | |
public void receiveCommandShouldWhatFromOneEdgeOfTheGridToAnother() throws Exception { | |
int expected = x.getMaxLocation() + x.getLocation() - 2; | |
rover.receiveCommands("LFFF"); | |
assertThat(rover.getCoordinates().getX().getLocation()).isEqualTo(expected); | |
} | |
@Test | |
public void receiveCommandsShouldStopWhenObstacleIsFound() throws Exception { | |
int expected = x.getLocation() + 1; | |
rover.getCoordinates().setObstacles(Arrays.asList(new Obstacle(expected + 1, y.getLocation()))); | |
rover.getCoordinates().setDirection(Direction.EAST); | |
rover.receiveCommands("FFFRF"); | |
assertThat(rover.getCoordinates().getX().getLocation()).isEqualTo(expected); | |
assertThat(rover.getCoordinates().getDirection()).isEqualTo(Direction.EAST); | |
} | |
@Test | |
public void positionShouldReturnXYAndDirection() throws Exception { | |
rover.receiveCommands("LFFFRFF"); | |
assertThat(rover.getPosition()).isEqualTo("8 X 4 N"); | |
} | |
@Test | |
public void positionShouldReturnNokWhenObstacleIsFound() throws Exception { | |
rover.getCoordinates().setObstacles(Arrays.asList(new Obstacle(x.getLocation() + 1, y.getLocation()))); | |
rover.getCoordinates().setDirection(Direction.EAST); | |
rover.receiveCommands("F"); | |
assertThat(rover.getPosition()).endsWith(" NOK"); | |
} | |
} |
One possible solution is following.
package com.technologyconversations.kata.marsrover; | |
/* | |
Method receiveCommands should be used to transmit commands to the rover. | |
*/ | |
public class Rover { | |
private Coordinates coordinates; | |
public void setCoordinates(Coordinates value) { | |
coordinates = value; | |
} | |
public Coordinates getCoordinates() { | |
return coordinates; | |
} | |
public Rover(Coordinates coordinatesValue) { | |
setCoordinates(coordinatesValue); | |
} | |
public void receiveCommands(String commands) throws Exception { | |
for (char command : commands.toCharArray()) { | |
if (!receiveSingleCommand(command)) { | |
break; | |
} | |
} | |
} | |
public boolean receiveSingleCommand(char command) throws Exception { | |
switch(Character.toUpperCase(command)) { | |
case 'F': | |
return getCoordinates().moveForward(); | |
case 'B': | |
return getCoordinates().moveBackward(); | |
case 'L': | |
getCoordinates().changeDirectionLeft(); | |
return true; | |
case 'R': | |
getCoordinates().changeDirectionRight(); | |
return true; | |
default: | |
throw new Exception("Command " + command + " is unknown."); | |
} | |
} | |
public String getPosition() { | |
return getCoordinates().toString(); | |
} | |
} |
Full source is located in the GitHub repo mars-rover-kata-java. Above code presents only the code of the main class. There are several other classes/objects with their corresponding specification. Besides tests and implementation, repository includes build.gradle that can be used, among other things, to download AssertJ dependencies and run tests. README.md contains short instructions how to set up the project.
What was your solution? Post it as a comment so that we can compare different ways to solve this kata.
Test-Driven Java Development
Test-Driven Java Development book wrote by Alex Garcia and me has been published by Packt Publishing. It was a long, demanding, but very rewarding journey that resulted in a very comprehensive hands-on material for all Java developers interested in learning or improving their TDD skills.
If you liked this article I am sure that you’ll find this book very useful. It contains extensive tutorials, guidelines and exercises for all Java developers eager to learn how to successfully apply TDD practices.
You can download a sample or purchase your own copy directly from Packt or Amazon.
Hi,
I was playing around this Kata to practice more TDD and DDD. It is a similar to yours solution but probably decoupling some responsabilities from Coordinates.
At the end, adding obstacles in my solution, I am not sure if make sense that Terrain is a VO instead an entity containing one or multiple Rovers.
Feedback is much appreciate to the solution on goo.gl/p61Aal
https://github.com/mustaine/katas/tree/master/mars-rover
I really liked how you decoupled responsibilities. The code is very clean and easy to understand (which is one of the things I value the most).
The only negative comment would be that you might have a bit more of code duplication than I’d like. For example, Direction.java could be refactored to something with less repeated code.
Overall, I think it’s a great solution. I’m currently writing a book on TDD, that will, among other things, have Mars Rover exercise. Would you mind if I put a link to your solution?
Feel free to do it, it would be an honor xD
Definetly I will look at your suggestion. I will try to do a refactoring with it and I was thinking also to decouple the command processor as should not be part of the Rover as I understand the problem.
If u are interested I did a post about it, also refactoring Direction.java
http://goo.gl/LsjW3o
Pingback: Data-driven tests in Junit5.0.0-SNAPSHOT | Markus Gärtner
Hi,
When I first read the specification I was confused and I am still a bit confused.
I may be wrong, so please correct me if I am.
My problem is with the teminology, CoordiantesSpec and PointSpec class names for me are misleading. Let me explain.
An N dimensional space can be spanned with N (ortogonal) basis vectors.
A point can be defined as the linear combination of these basis vectors.
So the usual notation is a list of the coefficients of these vectors called coordiantes.
To sum it up: In my understading a Point is made up from a set of N coordinates such as
Point point = new Point (1,2);
But am I right that in this specification Point is instead meant to be the implementation of a single coordiante with its maximum value?
My suggestion would be composing a Point from 2 Coordiantes and the Coordinate class should hold the maximum value.
Am I completely wrong or mistaken?
Balazs,
I completely agree with you.
No matter the cardinality of the space the rover is operating in, its coordinate is always a point.
If its world is 2-dimensional, you’ll be able to describe its coordinates with two numbers;
if it’s 3-dimensional, 3 numbers would be required; and so on.
Unfortunately, the authors decided to put the rover in two places at the same time on a two dimensional map (c.f. the @Before annotated method)
– once at (1,9)
– once at (2,9)
those positions seem to be mutated independently depending the direction and command
It is either the authors meant to say that the rover is a photon or something else that allows one instance to manifest itself in two different places at the same time, or the example is incorrect if designed to be applied in real life.
Dependencies for maven: