The following code is my solution to a code challenge I submitted a few days ago. I got rejected straight away with no feedback and I've wondered why.
Here is the full code repo.
Requirements
The application is a simulation of a toy robot moving on a square tabletop, of dimensions 5 x 5 units. There are no other obstructions on the table surface. The robot is free to roam around the surface of the table, but must be prevented from falling to destruction. Any movement that would result in the robot falling from the table must be prevented, however further valid movement commands must still be allowed. Create an application that can read in commands of the following form:
PLACE X,Y,F
MOVE
LEFT
RIGHT
REPORT
PLACE will put the toy robot on the table in position X,Y and facing NORTH, SOUTH, EAST or WEST.
The origin (0,0) can be considered to be the SOUTH WEST most corner.
MOVE will move the toy robot one unit forward in the direction it is currently facing.
LEFT and RIGHT will rotate the robot 90 degrees in the specified direction without changing the position of the robot.
REPORT will announce the X,Y and F of the robot.
Constraints:
- The application must be a Spring-Boot-Application
- Input must be realised over the REST-API, take care when designing the REST-API
- The robot that is not on the table can choose the ignore the MOVE, LEFT, RIGHT and REPORT commands.
- The robot must not fall off the table during movement. This also includes the initial placement of the toy robot.
- Any move that would cause the robot to fall must be ignored.
- It is not required to provide any graphical output showing the movement of the toy robot.
Plain input Examples:
PLACE 0,0,NORTH MOVE REPORT
Output: 0,1,NORTH
PLACE 0,0,NORTH LEFT REPORT
Output: 0,0,WEST
PLACE 1,2,EAST MOVE MOVE LEFT MOVE REPORT
Output: 3,3,NORTH
MOVE REPORT
Output: ROBOT MISSING
Solution
ToyRobotApplication.java
package com.puzzle.toyrobot;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class ToyRobotApplication {
public static void main(String[] args) {
SpringApplication.run(ToyRobotApplication.class, args);
}
}
RobotSimulationController.java
package com.puzzle.toyrobot.controller;
import com.puzzle.toyrobot.model.Report;
import com.puzzle.toyrobot.model.SimulationRound;
import com.puzzle.toyrobot.service.RobotSimulationService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class RobotSimulationController {
private RobotSimulationService simulationService;
@Autowired
public RobotSimulationController(RobotSimulationService simulationService) {
this.simulationService = simulationService;
}
@PostMapping
@RequestMapping("/robot/simulation")
public ResponseEntity<Report> newSimulationRound(@RequestBody SimulationRound simulationRound) {
Report report = simulationService.start(simulationRound);
return ResponseEntity.ok(report);
}
}
RobotSimulationService.java
package com.puzzle.toyrobot.service;
import com.puzzle.toyrobot.model.Report;
import com.puzzle.toyrobot.model.Robot;
import com.puzzle.toyrobot.model.SimulationRound;
import com.puzzle.toyrobot.model.command.Command;
import com.puzzle.toyrobot.model.command.CommandFactory;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;
@Service
public class RobotSimulationService {
private final Logger log = LoggerFactory.getLogger(RobotSimulationService.class);
public Report start(SimulationRound simulationRound) {
Robot robot = new Robot();
Report report = new Report();
for (String commandString : simulationRound.getCommands()) {
Command command = CommandFactory.getCommand(commandString);
if (command != null) {
command.execute(robot, report);
} else {
log.debug("Wrong command: " + commandString);
}
}
return report;
}
}
SimulationRound.java
package com.puzzle.toyrobot.model;
import java.util.ArrayList;
import java.util.List;
public class SimulationRound {
private List<String> commands = new ArrayList<>();
public List<String> getCommands() {
return commands;
}
public void setCommands(List<String> commands) {
this.commands = commands;
}
public void addCommand(String command) {
commands.add(command);
}
}
Robot.java
package com.puzzle.toyrobot.model;
public class Robot {
public static final Integer MAX_POSITION = 4;
public static final Integer MIN_POSITION = 0;
private Integer xPosition;
private Integer yPosition;
private CardinalDirection cardinalDirection;
public Robot() {
}
public Robot(Integer xPosition, Integer yPosition, CardinalDirection cardinalDirection) {
this.xPosition = xPosition;
this.yPosition = yPosition;
this.cardinalDirection = cardinalDirection;
}
public Integer getXPosition() {
return xPosition;
}
public void setXPosition(Integer xPosition) {
this.xPosition = xPosition;
}
public Integer getYPosition() {
return yPosition;
}
public void setYPosition(Integer yPosition) {
this.yPosition = yPosition;
}
public CardinalDirection getCardinalDirection() {
return cardinalDirection;
}
public void setCardinalDirection(CardinalDirection cardinalDirection) {
this.cardinalDirection = cardinalDirection;
}
public boolean isOnTable() {
return xPosition != null && yPosition != null && cardinalDirection != null;
}
public String getCurrentStatus() {
return String.join(",", xPosition.toString(), yPosition.toString(), cardinalDirection.toString());
}
public void increaseYPosition() {
yPosition++;
}
public void decreaseYPosition() {
yPosition--;
}
public void increaseXPosition() {
xPosition++;
}
public void decreaseXPosition() {
yPosition++;
}
}
Report.java package com.puzzle.toyrobot.model;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
public class Report {
private List<String> output = new ArrayList<>();
public List<String> getOutput() {
return output;
}
public void setOutput(List<String> output) {
this.output = output;
}
public void addOutput(String outupt) {
this.output.add(outupt);
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Report report = (Report) o;
return Objects.equals(output, report.output);
}
@Override
public int hashCode() {
return Objects.hash(output);
}
}
CardinalDirection.java
package com.puzzle.toyrobot.model;
public enum CardinalDirection {
EAST, WEST, SOUTH, NORTH
}
Command.java
package com.puzzle.toyrobot.model.command;
import com.puzzle.toyrobot.model.Report;
import com.puzzle.toyrobot.model.Robot;
public abstract class Command {
public abstract void execute(Robot robot, Report report);
}
CommandFactory.java
package com.puzzle.toyrobot.model.command;
public class CommandFactory {
private static final String PLACE_COMMAND_REGEX = "^(PLACE)\\s\\d+,\\d+,(NORTH|WEST|EAST|SOUTH)$";
public static Command getCommand(String commandString) {
if (commandString.matches(PLACE_COMMAND_REGEX)) {
return new PlaceCommand(commandString);
} else if (commandString.equals(CommandType.LEFT.name())) {
return new LeftCommand();
} else if (commandString.equals(CommandType.RIGHT.name())) {
return new RightCommand();
} else if (commandString.equals(CommandType.REPORT.name())) {
return new ReportCommand();
} else if (commandString.equals(CommandType.MOVE.name())) {
return new MoveCommand();
}
return null;
}
}
CommandType,java
package com.puzzle.toyrobot.model.command;
public enum CommandType {
PLACE, MOVE, LEFT, RIGHT, REPORT
}
LeftCommand.java
package com.puzzle.toyrobot.model.command;
import com.puzzle.toyrobot.model.CardinalDirection;
import com.puzzle.toyrobot.model.Report;
import com.puzzle.toyrobot.model.Robot;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class LeftCommand extends Command {
private final Logger log = LoggerFactory.getLogger(LeftCommand.class);
public void execute(Robot robot, Report report) {
if (!robot.isOnTable()) {
log.debug("Left command ignored");
} else {
switch (robot.getCardinalDirection()) {
case NORTH:
robot.setCardinalDirection(CardinalDirection.WEST);
break;
case SOUTH:
robot.setCardinalDirection(CardinalDirection.EAST);
break;
case EAST:
robot.setCardinalDirection(CardinalDirection.NORTH);
break;
case WEST:
robot.setCardinalDirection(CardinalDirection.SOUTH);
break;
}
log.debug("The robot is rotating 90 degree to " + robot.getCardinalDirection());
}
}
}
MoveCommand.java
package com.puzzle.toyrobot.model.command;
import com.puzzle.toyrobot.model.Report;
import com.puzzle.toyrobot.model.Robot;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class MoveCommand extends Command {
private final Logger log = LoggerFactory.getLogger(MoveCommand.class);
@Override
public void execute(Robot robot, Report report) {
if (!robot.isOnTable()) {
log.debug("Move command ignored");
} else {
switch (robot.getCardinalDirection()) {
case NORTH:
if (robot.getYPosition() < Robot.MAX_POSITION) {
robot.increaseYPosition();
log.debug("The robot is moving");
} else {
log.debug("Move command ignored");
}
break;
case SOUTH:
if (robot.getYPosition() > Robot.MIN_POSITION) {
robot.decreaseYPosition();
log.debug("The robot is moving");
} else {
log.debug("Move command ignored");
}
break;
case EAST:
if (robot.getXPosition() < Robot.MAX_POSITION) {
robot.increaseXPosition();
log.debug("The robot is moving");
} else {
log.debug("Move command ignored");
}
break;
case WEST:
if (robot.getXPosition() > Robot.MIN_POSITION) {
robot.decreaseXPosition();
log.debug("The robot is moving");
} else {
log.debug("Move command ignored");
}
break;
}
}
}
}
PlaceCommand.java
package com.puzzle.toyrobot.model.command;
import com.puzzle.toyrobot.model.CardinalDirection;
import com.puzzle.toyrobot.model.Report;
import com.puzzle.toyrobot.model.Robot;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class PlaceCommand extends Command {
private final Logger log = LoggerFactory.getLogger(PlaceCommand.class);
private String commandString;
PlaceCommand(String commandString) {
this.commandString = commandString;
}
@Override
public void execute(Robot robot, Report report) {
String placementArgs = StringUtils.substringAfter(commandString, CommandType.PLACE.name());
String[] args = StringUtils.split(placementArgs, ",");
Integer initialX = Integer.parseInt(args[0].trim());
Integer initialY = Integer.parseInt(args[1].trim());
if (initialX <= Robot.MAX_POSITION && initialX >= Robot.MIN_POSITION
&& initialY <= Robot.MAX_POSITION && initialY >= Robot.MIN_POSITION) {
robot.setXPosition(initialX);
robot.setYPosition(initialY);
robot.setCardinalDirection(CardinalDirection.valueOf(args[2].trim()));
log.debug("Robot is placed at " + robot.getCurrentStatus());
} else {
log.debug("Place command ignored");
}
}
}
ReportCommand.java
package com.puzzle.toyrobot.model.command;
import com.puzzle.toyrobot.model.Report;
import com.puzzle.toyrobot.model.Robot;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class ReportCommand extends Command {
private final Logger log = LoggerFactory.getLogger(ReportCommand.class);
@Override
public void execute(Robot robot, Report report) {
if (!robot.isOnTable()) {
log.debug("Missing Robot");
report.addOutput("ROBOT MISSING");
} else {
report.addOutput(robot.getCurrentStatus());
}
}
}
RightCommand.java
package com.puzzle.toyrobot.model.command;
import com.puzzle.toyrobot.model.CardinalDirection;
import com.puzzle.toyrobot.model.Report;
import com.puzzle.toyrobot.model.Robot;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class RightCommand extends Command {
private final Logger log = LoggerFactory.getLogger(RightCommand.class);
@Override
public void execute(Robot robot, Report report) {
if (!robot.isOnTable()) {
log.debug("Right command ignored");
} else {
switch (robot.getCardinalDirection()) {
case NORTH:
robot.setCardinalDirection(CardinalDirection.EAST);
break;
case SOUTH:
robot.setCardinalDirection(CardinalDirection.WEST);
break;
case EAST:
robot.setCardinalDirection(CardinalDirection.SOUTH);
break;
case WEST:
robot.setCardinalDirection(CardinalDirection.NORTH);
break;
}
log.debug("The robot is rotating 90 degree to " + robot.getCardinalDirection());
}
}
}
pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.puzzle</groupId>
<artifactId>toy-robot</artifactId>
<version>0.0.1-SNAPSHOT</version>
<packaging>jar</packaging>
<name>toy-robot</name>
<description>Toy Robot coding puzzle</description>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.0.3.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>3.7</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
and Finally a controller test
package com.puzzle.toyrobot.controller;
import com.puzzle.toyrobot.model.Report;
import com.puzzle.toyrobot.model.SimulationRound;
import org.junit.Assert;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.web.client.TestRestTemplate;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
@RunWith(SpringJUnit4ClassRunner.class)
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
public class RobotSimulationControllerTest {
@Autowired
private TestRestTemplate template;
@Test
public void simulationReportAsExpectedTest() {
Report report = new Report();
report.addOutput("0,1,NORTH");
SimulationRound round = new SimulationRound();
round.addCommand("PLACE 0,0,NORTH");
round.addCommand("LEFT");
round.addCommand("RIGHT");
round.addCommand("MOVE");
round.addCommand("REPORT");
HttpEntity<Object> simulationRound = getHttpEntity(round);
ResponseEntity<Report> resultAsset = template.postForEntity("/robot/simulation", simulationRound, Report.class);
Assert.assertEquals(resultAsset.getBody(), report);
}
@Test
public void missingRobotTest() {
Report report = new Report();
report.addOutput("ROBOT MISSING");
SimulationRound round = new SimulationRound();
round.addCommand("MOVE");
round.addCommand("REPORT");
HttpEntity<Object> simulationRound = getHttpEntity(round);
ResponseEntity<Report> resultAsset = template.postForEntity("/robot/simulation", simulationRound, Report.class);
Assert.assertEquals(resultAsset.getBody(), report);
}
@Test
public void ignoringWrongCommandTest() {
Report report = new Report();
report.addOutput("0,0,WEST");
SimulationRound round = new SimulationRound();
round.addCommand("PLACE 0,0,WEST");
round.addCommand("MOVEEEE");
round.addCommand("REPORT");
HttpEntity<Object> simulationRound = getHttpEntity(round);
ResponseEntity<Report> resultAsset = template.postForEntity("/robot/simulation", simulationRound, Report.class);
Assert.assertEquals(resultAsset.getBody(), report);
}
@Test
public void ignoringCommandThatCausesFailTest() {
Report report = new Report();
report.addOutput("0,0,SOUTH");
SimulationRound round = new SimulationRound();
round.addCommand("PLACE 0,0,SOUTH");
round.addCommand("MOVE");
round.addCommand("REPORT");
HttpEntity<Object> simulationRound = getHttpEntity(round);
ResponseEntity<Report> resultAsset = template.postForEntity("/robot/simulation", simulationRound, Report.class);
Assert.assertEquals(resultAsset.getBody(), report);
}
@Test
public void simulationRoundWithoutReportTest() {
Report report = new Report();
SimulationRound round = new SimulationRound();
round.addCommand("PLACE 1,2,EAST");
round.addCommand("MOVE");
round.addCommand("MOVE");
round.addCommand("LEFT");
HttpEntity<Object> simulationRound = getHttpEntity(round);
ResponseEntity<Report> resultAsset = template.postForEntity("/robot/simulation", simulationRound, Report.class);
Assert.assertEquals(resultAsset.getBody(), report);
}
private HttpEntity<Object> getHttpEntity(Object body) {
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
return new HttpEntity<>(body, headers);
}
}
Notes on solution:
- All commands will be ignored until a valid PLACE command.
- The robot can be re-PLACEd at any time.
- Any number of REPORT commands are allowed.
- The REST-API request body is a Simulation Round object that contains a list of commands.
- The REST-API response object is a Report object that contains a list of reports, which is the output of the REPORT command if any.
- Many Integration tests are added.