Welcome back to this two part series where we focus on development of a simple console application game. In part 1 we covered the rules, we established the basic game logic of movement and we also coded some validation. In this blog post we’ll be looking at execution of turtle moves, returning the right response to the end user and an all round polishing of the program to have a product we’re happy with.
Continuing where we left off, I created a new class and called it Minefield.cs. This will represent the instance of the game. It will keep track of different features of the game but most predomanantly it will handle the turtle’s current position and direction. Equally before we start executing the moves we want to establish some parameters such as the grid size, and starting, exit and mines’ positions.
I then created a new public method with the intention of using this as an entry point from my main class. All it does is go through the lines of moves one by one and for each character determine which private methods to call. If M then move, else if R or L then rotate.
| public class Minefield | |
| { | |
| private readonly (int, int) _gridSize; | |
| private readonly List<(int, int)> _minePositions; | |
| private readonly (int, int) _exitPosition; | |
| private readonly (int, int) _startPosition; | |
| private readonly string _startDirection; | |
| private readonly string[] _movesList; | |
| private (int, int) _currentPosition; | |
| private string _currentDirection; | |
| public Minefield(string[] setupLines) | |
| { | |
| _gridSize = Util.ParseSetupLineInPosition(setupLines[0]); | |
| _minePositions = Util.GetMines(setupLines[1]); | |
| _exitPosition = Util.ParseSetupLineInPosition(setupLines[2]); | |
| _startPosition = Util.ParseSetupLineInPosition(setupLines[3]); | |
| _startDirection = setupLines[3].Trim().Split(' ')[2]; | |
| _movesList = setupLines.Skip(4).Take(setupLines.Length – 2).ToArray(); | |
| } | |
| public Result ExecuteMoves() | |
| { | |
| var result = Result.TurtleLost; | |
| _currentPosition = _startPosition; | |
| _currentDirection = _startDirection; | |
| foreach (var line in _movesList) | |
| { | |
| foreach (var move in line) | |
| { | |
| switch (move) | |
| { | |
| case 'M': | |
| if (result == Result.TurtleLost) | |
| result = Move(); | |
| else | |
| return result; | |
| break; | |
| case 'R': | |
| RotateRight(); break; | |
| case 'L': | |
| RotateLeft(); break; | |
| } | |
| } | |
| } | |
| return result; | |
| } | |
| } |
The rotation methods are quite simple to understand. I made sure to keep track of the turtle’s current position and the direction it’s facing. Depending on the current’s turtle direction, and the whether the turtle is rotating clockwise or anti-clockwise, the turtle’s direction is simply updated to the new direction which is denoted by just a string character.
Similarly, the Move method checks the current turtle’s direction and based on that it will add or substract the x or y coordinate. The logic behind the x and y coordinate is explained in part 1. Once the turtle moved one position in the minefield grid, we should then check if the new position is still within the grid, hits a mine or lands on the exit position.
The Minefield instance keeps track of item positions and turtle position, thus evaluating the turtle’s outcome after moving was easy enough. Remember, all of this is in an iterative loop and therefore happening for each character being parsed. This will ensure that if the turtle moved, say 4 positions, and hit a mine along the way it would return the right response based on it’s “journey” to get there.
| private Result Move() | |
| { | |
| // update current position values and compare with mines list & exit in here | |
| switch (_currentDirection) | |
| { | |
| case "N": | |
| _currentPosition.Item2–; break; | |
| case "S": | |
| _currentPosition.Item2++; break; | |
| case "E": | |
| _currentPosition.Item1++; break; | |
| case "W": | |
| _currentPosition.Item1–; break; | |
| } | |
| var result = CheckIfStillInGrid(); | |
| if (result == Result.TurtleLost) result = CheckForMine(); | |
| if (result == Result.TurtleLost) result = CheckForExit(); | |
| return result; | |
| } | |
| private void RotateRight() | |
| { | |
| _currentDirection = _currentDirection switch | |
| { | |
| "N" => "E", | |
| "S" => "W", | |
| "E" => "S", | |
| "W" => "N", | |
| _ => throw new System.NotImplementedException(), | |
| }; | |
| } | |
| private void RotateLeft() | |
| { | |
| _currentDirection = _currentDirection switch | |
| { | |
| "N" => "W", | |
| "S" => "E", | |
| "E" => "N", | |
| "W" => "S", | |
| _ => throw new System.NotImplementedException(), | |
| }; | |
| } | |
| private Result CheckIfStillInGrid() | |
| { | |
| if (_currentPosition.Item1 < 0 || | |
| _currentPosition.Item2 < 0 || | |
| _currentPosition.Item1 >= _gridSize.Item1 || | |
| _currentPosition.Item2 >= _gridSize.Item2) | |
| return Result.TurtleOutside; | |
| return Result.TurtleLost; | |
| } | |
| private Result CheckForMine() | |
| { | |
| foreach (var mine in _minePositions) | |
| { | |
| if (_currentPosition.Item1 == mine.Item1 && _currentPosition.Item2 == mine.Item2) | |
| return Result.TurtleMine; | |
| } | |
| return Result.TurtleLost; | |
| } | |
| private Result CheckForExit() | |
| { | |
| if (_currentPosition.Item1 == _exitPosition.Item1 && _currentPosition.Item2 == _exitPosition.Item2) | |
| return Result.TurtleExit; | |
| return Result.TurtleLost; | |
| } |
At this point I think it’s only fair that I explain how I decided to handle the outcome message. The requirements said that there can be three outcomes; Success, Hit or Lost. I thought that just these three outcomes exclude two other possibilities. There’s a possibility that the data in the setup file isn’t correct and validation fails. If that happens it wouldn’t be right to use any of the three original outcomes for a validation error and therefore I decided to add a new one. Equally I thought of another possibility which in my opinion wasn’t perhaps thought of, and therefore not included. This happens when the turtle walks/falls out of the minefield grid. Yes one could say that the Lost outcome could be used but then again I thought it would be better to make a distinction between stepping out of the minefield and staying inside the minefield but not hitting a mine or finding the exit.
Before we look into that I wanted to share how I handle states in the game, and this can be either validation errors, or simply the turtle’s state. I created an enum and called it Result. As you’ve already seen in my previous code snippets I used this enum to manage the flow of the game and to determine whether to continue execution of the program. Equally I added a description attribute to each enum to be able to return a more detailed message.
| public enum Result | |
| { | |
| [Description("")] | |
| ValidationOk, | |
| [Description("The grid settings are required to setup.")] | |
| MissingGridInput, | |
| [Description("The grid settings in the setup file are not valid.")] | |
| InvalidGridInput, | |
| [Description("The mines positions in the setup file are not valid.")] | |
| InvalidMinesInput, | |
| [Description("The exit position in the setup file is not valid.")] | |
| InvalidExitInput, | |
| [Description("The starting position is required to setup.")] | |
| MissingStartInput, | |
| [Description("The starting position in the setup file is not valid.")] | |
| InvalidStartInput, | |
| [Description("The list of moves in the setup file is not valid.")] | |
| InvalidMovesInput, | |
| [Description("One or more mines are outside the grid.")] | |
| OutOfBoundsMines, | |
| [Description("The exit is outside the grid.")] | |
| OutOfBoundsExit, | |
| [Description("The start is outside the grid.")] | |
| OutOfBoundsStart, | |
| [Description("A mine is in the same position of start or exit.")] | |
| MineSamePositionStartExit, | |
| [Description("The start and exit are in the same position.")] | |
| StartExitSamePosition, | |
| [Description("The turtle has stepped outside of the grid.")] | |
| TurtleOutside, | |
| [Description("The turtle has stepped on a mine.")] | |
| TurtleMine, | |
| [Description("The turtle has found the exit.")] | |
| TurtleExit, | |
| [Description("The turtle did not hit a mine but didn't find the exit either.")] | |
| TurtleLost, | |
| } |
With that in place I created another class, Response. This will handle the three different outcomes, or in my case two since I added another two.
| public class Response | |
| { | |
| public string Description { get; set; } | |
| public Response(Result result) | |
| { | |
| Description = GetResponseFromResult(result); | |
| } | |
| private string GetResponseFromResult(Result result) | |
| { | |
| switch (result) | |
| { | |
| case Result.MissingGridInput: | |
| case Result.InvalidGridInput: | |
| case Result.InvalidMinesInput: | |
| case Result.InvalidExitInput: | |
| case Result.MissingStartInput: | |
| case Result.InvalidStartInput: | |
| case Result.InvalidMovesInput: | |
| case Result.OutOfBoundsMines: | |
| case Result.OutOfBoundsExit: | |
| case Result.OutOfBoundsStart: | |
| case Result.MineSamePositionStartExit: | |
| case Result.StartExitSamePosition: | |
| return $"Error – {Util.GetEnumDescription(result)}"; | |
| case Result.TurtleOutside: | |
| return $"Outside Grid – {Util.GetEnumDescription(result)}"; | |
| case Result.TurtleMine: | |
| return $"Mine Hit – {Util.GetEnumDescription(result)}"; | |
| case Result.TurtleExit: | |
| return $"Success – {Util.GetEnumDescription(result)}"; | |
| case Result.TurtleLost: | |
| return $"Still in Danger – {Util.GetEnumDescription(result)}"; | |
| default: | |
| throw new ArgumentOutOfRangeException(); | |
| } | |
| } | |
| } |
Finally all I had to do was update my Main method and make reference to the new Response class.
| static void Main(string[] args) | |
| { | |
| var cfg = InitOptions<AppConfig>(); | |
| var setupDirectory = Path.Combine(cfg.SetupFilePath, cfg.SetupFileName); | |
| var setupLines = File.ReadAllLines(setupDirectory); | |
| var validation = new Validation(); | |
| var result = validation.ValidateSetupFile(setupLines); | |
| if (result == Result.ValidationOk) | |
| { | |
| var minefield = new Minefield(setupLines); | |
| result = minefield.ExecuteMoves(); | |
| } | |
| Console.WriteLine(new Response(result).Description); | |
| Console.ReadKey(); | |
| } |
And done! At this point you should have a functioning console application game. I also added the entire solution to a GitHub repository for better visibility.
In conclusion, I really enjoyed this coding exercise and I hope you enjoyed reading my two blog posts. Can this be done differently? Yes, 100%. I can think of many ways how this can be improved and perhaps certain priciniples like SOLID can be applied. Feel free to update and change the program how you like, and comment below your thoughts or any feedback you might have.
Thanks again for reading and stay tuned for more posts.
Bjorn
Pingback: integration testing a .net core console application | It's not a bug, it's a feature