I am doing MarsRoverKata exersise just to train my coding skills and I came up with the following solution.
I was trying to follow SOLID principles in my implementation and I used TDD approach to write code.
Please criticize.
I have two main classes: MarsRover (manages main parameteres of the Rover like initial position and final position) and MarsRoverNavigator (responsible for movements and spinning).
MarsRover.cs:
public class MarsRover
{
private readonly string input;
private MarsRoverNavigator marsRoverNavigator;
public MarsRover(string input)
{
this.input = input;
}
public NavigationParameters NavigationParameters { get; private set; }
public string FinalPosition { get; private set; }
public void Initialize()
{
NavigationParameters = InputValidator.GetNaviagtionParametersFromInput(input);
}
public void Navigate()
{
marsRoverNavigator = new MarsRoverNavigator(NavigationParameters);
FinalPosition = marsRoverNavigator.Navigate();
}
}
MarsRoverNavigator.cs:
public class MarsRoverNavigator
{
private readonly NavigationParameters navigationParameters;
private SpinningControl spinningControl;
private MovingControl movingControl;
public MarsRoverNavigator(NavigationParameters navigationParameters)
{
this.navigationParameters = navigationParameters;
spinningControl = new SpinningControl();
movingControl = new MovingControl();
}
public string Navigate()
{
var command = navigationParameters.Command;
foreach (var step in command)
{
DoAStep(step);
}
var result = $"{navigationParameters.CurrentCoordinates.X} {navigationParameters.CurrentCoordinates.Y} {navigationParameters.CurrentDirection}";
return result;
}
private void DoAStep(char stepCommand)
{
var newDirection = spinningControl.GetNextDirection(navigationParameters.CurrentDirection, stepCommand);
navigationParameters.UpdateCurrentDirection(newDirection);
var newCoordinates = movingControl.Move(stepCommand, navigationParameters.CurrentDirection, navigationParameters.CurrentCoordinates);
if (newCoordinates.X > navigationParameters.PlateauDimenstions.X || newCoordinates.Y > navigationParameters.PlateauDimenstions.Y)
{
throw new InvalidCommandException();
}
navigationParameters.UpdateCurrentCoordinates(newCoordinates);
}
}
NavigationParameters.cs:
public class NavigationParameters
{
public string CurrentDirection { get; private set; }
public string Command { get; }
public Coordinates PlateauDimenstions { get; }
public Coordinates CurrentCoordinates { get; private set; }
public NavigationParameters(string currentDirection, Coordinates plateauDimenstions, Coordinates currentCoordinates, string command)
{
CurrentDirection = currentDirection;
PlateauDimenstions = plateauDimenstions;
CurrentCoordinates = currentCoordinates;
Command = command;
}
public void UpdateCurrentDirection(string newDirection)
{
CurrentDirection = newDirection;
}
internal void UpdateCurrentCoordinates(Coordinates newCoordinates)
{
CurrentCoordinates = newCoordinates;
}
MovingControl.cs is implemented as a dictionary:
public class MovingControl
{
public Dictionary<string, Func<Coordinates, Coordinates>> MoveFunctions =
new Dictionary<string, Func<Coordinates, Coordinates>>
{
{"N", MoveNorth},
{"W", MoveWest},
{"S", MoveSouth},
{"E", MoveEast}
};
public Coordinates Move(char command, string currentDirection, Coordinates currentCoordinates)
{
if (command == 'M')
{
return MoveFunctions[currentDirection](currentCoordinates);
}
return currentCoordinates;
}
private static Coordinates MoveEast(Coordinates coordinates)
{
return new Coordinates()
{
X = coordinates.X + 1,
Y = coordinates.Y
};
}
private static Coordinates MoveSouth(Coordinates coordinates)
{
return new Coordinates()
{
X = coordinates.X,
Y = coordinates.Y - 1
};
}
private static Coordinates MoveWest(Coordinates coordinates)
{
return new Coordinates()
{
X = coordinates.X - 1,
Y = coordinates.Y
};
}
private static Coordinates MoveNorth(Coordinates coordinates)
{
return new Coordinates()
{
X = coordinates.X,
Y = coordinates.Y + 1
};
}
}
SpinningControl.cs is implemented as a Circular LinkedList:
public class SpinningControl
{
static readonly LinkedList<string> directions =
new LinkedList<string>(new[] { "N", "W", "S", "E" });
public readonly Dictionary<char, Func<string, string>> SpinningFunctions =
new Dictionary<char, Func<string, string>>
{
{'L', TurnLeft},
{'R', TurnRight},
{'M', Stay }
};
public string GetNextDirection(string currentDirection, char stepCommand)
{
return SpinningFunctions[stepCommand](currentDirection);
}
private static string TurnRight(string currentDirection)
{
LinkedListNode<string> currentIndex = directions.Find(currentDirection);
return currentIndex.PreviousOrLast().Value;
}
private static string TurnLeft(string currentDirection)
{
LinkedListNode<string> currentIndex = directions.Find(currentDirection);
return currentIndex.NextOrFirst().Value;
}
private static string Stay(string currentDirection)
{
return currentDirection;
}
}
Circular LinkedList extension:
public static class CircularLinkedList
{
public static LinkedListNode<T> NextOrFirst<T>(this LinkedListNode<T> current)
{
return current.Next ?? current.List.First;
}
public static LinkedListNode<T> PreviousOrLast<T>(this LinkedListNode<T> current)
{
return current.Previous ?? current.List.Last;
}
}
InputValidator.cs:
public static class InputValidator
{
private static Coordinates plateauDimenstions;
private static Coordinates currentCoordinates;
private static string currentDirection;
private static string command;
private static string[] inputByLines;
private const int expectedNumberOfInputLines = 3;
private const int expectedLineWithPlateauDimension = 0;
private const int expectedLineWithStartPosition = 1;
private const int expectedLineWithCommand = 2;
private const char linesDelimeter = '\n';
private const char parametersDelimeter = ' ';
private static readonly List<string> allowedDirections = new List<string> { "N", "W", "E", "S" };
public static NavigationParameters GetNaviagtionParametersFromInput(string input)
{
SplitInputByLines(input);
SetPlateauDimensions(inputByLines);
SetStartPositionAndDirection(inputByLines);
SetCommand();
return new NavigationParameters(currentDirection, plateauDimenstions, currentCoordinates, command);
}
private static void SplitInputByLines(string input)
{
var splitString = input.Split(linesDelimeter);
if (splitString.Length != expectedNumberOfInputLines)
{
throw new IncorrectInputFormatException();
}
inputByLines = splitString;
}
private static void SetPlateauDimensions(string[] inputLines)
{
var stringPlateauDimenstions = inputLines[expectedLineWithPlateauDimension].Split(parametersDelimeter);
if (PlateauDimensionsAreInvalid(stringPlateauDimenstions))
{
throw new IncorrectPlateauDimensionsException();
}
plateauDimenstions = new Coordinates
{
X = Int32.Parse(stringPlateauDimenstions[0]),
Y = Int32.Parse(stringPlateauDimenstions[1])
};
}
private static void SetStartPositionAndDirection(string[] inputByLines)
{
var stringCurrentPositionAndDirection = inputByLines[expectedLineWithStartPosition].Split(parametersDelimeter);
if (StartPositionIsInvalid(stringCurrentPositionAndDirection))
{
throw new IncorrectStartPositionException();
}
currentCoordinates = new Coordinates
{
X = Int32.Parse(stringCurrentPositionAndDirection[0]),
Y = Int32.Parse(stringCurrentPositionAndDirection[1])
};
currentDirection = stringCurrentPositionAndDirection[2];
}
private static void SetCommand()
{
command = inputByLines[expectedLineWithCommand];
}
private static bool StartPositionIsInvalid(string[] stringCurrentPositionAndDirection)
{
if (stringCurrentPositionAndDirection.Length != 3 || !stringCurrentPositionAndDirection[0].All(char.IsDigit)
|| !stringCurrentPositionAndDirection[1].All(char.IsDigit) || !allowedDirections.Any(stringCurrentPositionAndDirection[2].Contains))
{
return true;
}
if (Int32.Parse(stringCurrentPositionAndDirection[0]) > plateauDimenstions.X ||
Int32.Parse(stringCurrentPositionAndDirection[1]) > plateauDimenstions.Y)
{
return true;
}
return false;
}
private static bool PlateauDimensionsAreInvalid(string[] stringPlateauDimenstions)
{
if (stringPlateauDimenstions.Length != 2 || !stringPlateauDimenstions[0].All(char.IsDigit)
|| !stringPlateauDimenstions[1].All(char.IsDigit))
{
return true;
}
return false;
}
}
Tests around MarsRoverNavigator:
[TestFixture]
public class MarsRoverNavigatorShould
{
[TestCase("5 5\n0 0 N\nL", "0 0 W")]
[TestCase("5 5\n0 0 N\nR", "0 0 E")]
[TestCase("5 5\n0 0 W\nL", "0 0 S")]
[TestCase("5 5\n0 0 W\nR", "0 0 N")]
[TestCase("5 5\n0 0 S\nL", "0 0 E")]
[TestCase("5 5\n0 0 S\nR", "0 0 W")]
[TestCase("5 5\n0 0 E\nL", "0 0 N")]
[TestCase("5 5\n0 0 E\nR", "0 0 S")]
[TestCase("5 5\n1 1 N\nM", "1 2 N")]
[TestCase("5 5\n1 1 W\nM", "0 1 W")]
[TestCase("5 5\n1 1 S\nM", "1 0 S")]
[TestCase("5 5\n1 1 E\nM", "2 1 E")]
public void UpdateDirectionWhenPassSpinDirections(string input, string expectedDirection)
{
var marsRover = new MarsRover(input);
marsRover.Initialize();
marsRover.Navigate();
var actualResult = marsRover.FinalPosition;
actualResult.Should().BeEquivalentTo(expectedDirection);
}
[TestCase("5 5\n0 0 N\nM", "0 1 N")]
[TestCase("5 5\n1 1 N\nMLMR", "0 2 N")]
[TestCase("5 5\n1 1 W\nMLMLMLM", "1 1 N")]
[TestCase("5 5\n0 0 N\nMMMMM", "0 5 N")]
[TestCase("5 5\n0 0 E\nMMMMM", "5 0 E")]
[TestCase("5 5\n0 0 N\nRMLMRMLMRMLMRMLM", "4 4 N")]
public void UpdatePositionWhenPassCorrectInput(string input, string expectedPosition)
{
var marsRover = new MarsRover(input);
marsRover.Initialize();
marsRover.Navigate();
var actualResult = marsRover.FinalPosition;
actualResult.Should().BeEquivalentTo(expectedPosition);
}
[TestCase("1 1\n0 0 N\nMM")]
[TestCase("1 1\n0 0 E\nMM")]
public void ReturnExceptionWhenCommandSendsRoverOutOfPlateau(string input)
{
var marsRover = new MarsRover(input);
marsRover.Initialize();
marsRover.Invoking(y => y.Navigate())
.Should().Throw<InvalidCommandException>()
.WithMessage("Command is invalid: Rover is sent outside the Plateau");
}
}
Tests around input:
[TestFixture]
public class MarsRoverShould
{
[TestCase("5 5\n0 0 N\nM", 5, 5, 0, 0, "N", "M")]
[TestCase("10 10\n5 9 E\nLMLMLM", 10, 10, 5, 9, "E", "LMLMLM")]
public void ParseAnInputCorrectly(string input, int expectedXPlateauDimension, int expectedYPlateauDimension,
int expectedXStartPosition, int expectedYStartPosition, string expectedDirection, string expectedCommand)
{
var expectedPlateausDimensions = new Coordinates() { X = expectedXPlateauDimension, Y = expectedYPlateauDimension };
var expectedStartingPosition = new Coordinates() { X = expectedXStartPosition, Y = expectedYStartPosition };
var expectedNavigationParameters = new NavigationParameters(expectedDirection, expectedPlateausDimensions,
expectedStartingPosition, expectedCommand);
var marsRover = new MarsRover(input);
marsRover.Initialize();
var actualResult = marsRover.NavigationParameters;
actualResult.Should().BeEquivalentTo(expectedNavigationParameters);
}
[TestCase("10 10 5\n1 9 E\nLMLMLM")]
[TestCase("10\n5 9 E\nLMLMLM")]
[TestCase("10 A\n5 9 E\nLMLMLM")]
public void ReturnExceptionWhenWrongPlateauDimensionsInput(string input)
{
var marsRover = new MarsRover(input);
marsRover.Invoking(y => y.Initialize())
.Should().Throw<IncorrectPlateauDimensionsException>()
.WithMessage("Plateau dimensions should contain two int parameters: x and y");
}
[TestCase("1 1\n1 1\nLMLMLM")]
[TestCase("1 1\n1 N\nLMLMLM")]
[TestCase("1 1\n1\nLMLMLM")]
[TestCase("5 5\n5 A N\nLMLMLM")]
[TestCase("5 5\n5 1 A\nLMLMLM")]
[TestCase("1 1\n5 1 N\nLMLMLM")]
public void ReturnExceptionWhenWrongStartPositionInput(string input)
{
var marsRover = new MarsRover(input);
marsRover.Invoking(y => y.Initialize())
.Should().Throw<IncorrectStartPositionException>()
.WithMessage("Start position and direction should contain three parameters: int x, int y and direction (N, S, W or E)");
}
[TestCase("10 10; 5 9; LMLMLM")]
[TestCase("10 10\nLMLMLM")]
public void ReturnExceptionWhenWrongInputFormat(string input)
{
var marsRover = new MarsRover(input);
marsRover.Invoking(y => y.Initialize())
.Should().Throw<IncorrectInputFormatException>()
.WithMessage("Error occured while splitting the input: format is incorrect");
}
}