A C# coding challenge to solve a range of mazes with differing dimensions and styles. The total run time was considerably less than a target maximum run time.
Learn the Fundamentals of XCUITest Framework_ A Beginner's Guide.pdf
Maze solving app listing
1. Maze Solving App
Original Challenge:
Solving a maze
==============
The idea here is to write a program to solve simple mazes. The mazes are given in
a file and the program must read in the file, solve the maze and output the solution.
If no solution is possible the output should indicate this somehow. The program
should be written to the following specification:
- Arbitrary sized mazes should be handled
- Valid moves are N, S, E, W (not diagonally)
- All input will be clean, no validation is necessary
- Any suitable language can be used although one of Java, C#, Python is preferred
- The maze file format is described below with an example
- The program should be tested on the sample mazes provided
- Output should be written to the Standard Output/Console
================================================
== ALL the sample mazes DO have a solution! ==
================================================
The emphasis should be on code readability and simplicity. Runtime for all of the sample mazes
should be <30 seconds.
Please email the solution in source code form, with short instructions on how to run.
Good luck!
2. Maze file format
================
The input is a maze description file in plain text.
1 - denotes walls
0 - traversable passage way
INPUT:
<WIDTH> <HEIGHT><CR>
<START_X> <START_Y><CR> (x,y) location of the start. (0,0) is upper left and (width-
1,height-1) is lower right
<END_X> <END_Y><CR> (x,y) location of the end
<HEIGHT> rows where each row has <WIDTH> {0,1} integers space delimited
OUTPUT:
the maze with a path from start to end
walls marked by '#', passages marked by ' ', path marked by 'X', start/end marked by 'S'/'E'
Example file:
10 10
1 1
8 8
1 1 1 1 1 1 1 1 1 1
1 0 0 0 0 0 0 0 0 1
1 0 1 0 1 1 1 1 1 1
1 0 1 0 0 0 0 0 0 1
1 0 1 1 0 1 0 1 1 1
1 0 1 0 0 1 0 1 0 1
1 0 1 0 0 0 0 0 0 1
1 0 1 1 1 0 1 1 1 1
5. #X# # #XXXXXXX# # # #
#X# # #X#####X# ##### #
#X# # #XXXXX#X# # # #
#X### ### #X#X# # # # #
#XXX # #X#X# # # # #
###X#######X#X# ### # #
# XXX#XXXXX#X# # # #
#####X#X#####X### #XXX#
# #XXX # XXX# #X#X#
# # ##### ### #X# #X#X#
# # # # #X #X#X#
# ##### # # ###X###X#X#
# # # # # #X#XXX#X#
# # # # ##### #X#X# #X#
# # # #XXX# #E#
#######################
The Code:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace MazeSolvingApp
{
class Program
{
static void Main(string[] args)
{
SolveMaze solve = new SolveMaze();
solve.Run();
}
}
}
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace MazeSolvingApp
{
6. class Node
{
public neighbour location { get; set; }//the location of the node itself
public neighbour top { get; set; }//the location of the neighbouring cell
public neighbour bottom { get; set; }
public neighbour left { get; set; }
public neighbour right { get; set; }
public int Neighbours()
{
int count = 0;
if (top != null)
{
count += 1;
}
if (bottom != null)
{
count += 1;
}
if (left != null)
{
count += 1;
}
if (right != null)
{
count += 1;
}
return count;
}
}
class neighbour
{
public int row { get; set; }
public int column { get; set; }
}
}
using System;
using System.IO;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Reflection;
namespace MazeSolvingApp
{
class SolveMaze
{
private int Width { get; set; }
private int Height { get; set; }
private int StartX { get; set; }
private int StartY { get; set; }
private int EndX { get; set; }
private int EndY { get; set; }
private int position { get; set; }
private char[,] MazeArray { get; set; }
private char[,] SolvedMaze { get; set; }
7. private string outFile { get; set; }
private List<Node> nodeList { get; set; }
public void Run()
{
string FolderPath =
Path.Combine(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location), @"Data");
Directory.CreateDirectory(Path.Combine(Path.GetDirectoryName(Assembly.GetExecutingAssembly().
Location), @"Solutions"));//somewhere to put the solutions
List<String> mazes = new List<string>(Directory.EnumerateFiles(FolderPath,
"*.txt", SearchOption.AllDirectories));
foreach (string maze in mazes)
{
string[] contents = File.ReadAllLines(maze);
position = maze.LastIndexOf("") + 1;
outFile = maze.Substring(0, position - 5) + "SolutionsSolved" +
maze.Substring(position, maze.Length - position);
int[] mazeFirst = getIntValues(contents[3]);
if (getDims(contents[0]))
if (getStart(contents[1]))
if (getEnd(contents[2]))
if (contents.Count() == Height + 3 && mazeFirst.Count() ==
Width)//check the maze matches the supplied dimensions
{
Console.WriteLine("Width = " + Width.ToString() + " Height =
" + Height.ToString() + " ");
Console.WriteLine("Start X = " + StartX.ToString() + " Start
Y = " + StartY.ToString() + " ");
Console.WriteLine("End X = " + EndX.ToString() + " End Y = "
+ EndY.ToString() + " ");
string[] mazeContents = new string[Height];
Array.Copy(contents, 3, mazeContents, 0, Height);//copies the
maze strings only
makeMazeArray(mazeContents);
CopyMaze();
nodeList = makeNodeList(mazeContents);
nodeList = RelateNodeList(nodeList);
nodeList = TrimNodeList(nodeList);
generateSolution();
}
else
{
Console.WriteLine("The file " + maze + " is not a valid maze
file ");
}
}
Console.ReadLine();
}
private List<Node> makeNodeList(string[] maze)
{
List<Node> NodeList = new List<Node>();
for (int i = 0; i < Height; i++)
{
8. char[] row = getCharValues(maze[i]);
for (int j = 0; j < Width; j++)
{
if (row[j] == '0')
{
Node node = new Node() { location = new neighbour() { row = i, column
= j } };
/*just a basic node with its location*/
NodeList.Add(node);
}
}
}
return NodeList;
}
private List<Node> RelateNodeList(List<Node> NodeList)
{
foreach (Node node in NodeList)
{
int X = node.location.column;
int Y = node.location.row;
neighbour top = new neighbour { column = X - 1, row = Y };
neighbour left = new neighbour { column = X, row = Y - 1 };
neighbour right = new neighbour { column = X, row = Y + 1 };
neighbour bottom = new neighbour { column = X + 1, row = Y };
/*this connects the nodes up to each other, establishing their
relationships*/
int indexTop = NodeList.FindIndex(f => f.location.row == top.row &&
f.location.column == top.column);
int indexLeft = NodeList.FindIndex(f => f.location.row == left.row &&
f.location.column == left.column);
bool indexRight = NodeList.Any(n => n.location.row == right.row &&
n.location.column == right.column);// NodeList.FindIndex(f => f.location == right);
int indexBottom = NodeList.FindIndex(f => f.location.row == bottom.row &&
f.location.column == bottom.column);
if (indexTop >= 0)
{
node.top = top;
}
if (indexLeft >= 0)
{
node.left = left;
}
if (indexRight)
{
node.right = right;
}
if (indexBottom >= 0)
{
node.bottom = bottom;
}
}
return NodeList;
}
private List<Node> TrimNodeList(List<Node> NodeList)
{
//This finds all the cells with one or no connected nodes. These will be the ends
of dead ends
List<Node> orphanedNodes = (from n in NodeList where n.Neighbours() <= 1 &&
!(n.location.row == StartY && n.location.column == StartX) && !(n.location.row == EndY &&
n.location.column == EndX) select n).ToList();
9. foreach (Node node in orphanedNodes)
{
if (node.Neighbours() > 0)
{
neighbour neybour = null;
Node attachedNode = null;
if (node.top != null)//we remove the connection from the adjacent node.
If this becomes the end of the dead end, it will go next loop around.
{
neybour = node.top;
attachedNode = NodeList.FirstOrDefault(n => n.location.row ==
neybour.row && n.location.column == neybour.column);
attachedNode.bottom = null;
}
if (node.left != null)
{
neybour = node.left;
attachedNode = NodeList.FirstOrDefault(n => n.location.row ==
neybour.row && n.location.column == neybour.column);
attachedNode.right = null;
}
if (node.right != null)
{
neybour = node.right;
attachedNode = NodeList.FirstOrDefault(n => n.location.row ==
neybour.row && n.location.column == neybour.column);
attachedNode.left = null;
}
if (node.bottom != null)
{
neybour = node.bottom;
attachedNode = NodeList.FirstOrDefault(n => n.location.row ==
neybour.row && n.location.column == neybour.column);
attachedNode.top = null;
}
}
NodeList.Remove(node);
}
if (NodeList.Any(n => n.Neighbours() <= 1 && !(n.location.row == StartY &&
n.location.column == StartX) && !(n.location.row == EndY && n.location.column == EndX))) {
TrimNodeList(NodeList); }
return NodeList;// This should now only have nodes that connect start and end
}
private bool makeMazeArray(string[] maze)
{
MazeArray = new char[Width, Height];
for (int i = 0; i < Height; i++)
{
char[] row = getCharValues(maze[i]);
for (int j = 0; j < Width; j++)
{
MazeArray[j, i] = row[j];
}
}
return true;
}
private void generateSolution()
{
List<Node> path = shortestPath();
foreach (Node node in path)
{
10. SolvedMaze[node.location.column, node.location.row] = 'X';
//plot the shortest path on the solved maze array
}
makeOutputFile();// make the solution text file
}
private List<Node> shortestPath()
{
List<List<Node>> paths = new List<List<Node>>();
List<Node> path1 = plotForward();
paths.Add(path1);
/*This is just in case we get a shorter route by taking a different branch of
multiple paths
There are opportunities for providing multiple navigation methods and selecting
randomly,
running these multiple times and finding the shortest, but it wasn't necessary
here.
*/
List<Node> path2 = plotBackward();
paths.Add(path2);
List<Node> shortestPath = findShortest(paths);
return shortestPath;
}
private List<Node> plotForward()
{
List<Node> forwardListCopy = CopyList(nodeList);
List<Node> forwardList = new List<Node>();
Node currentNode = forwardListCopy.First(n => n.location.row == StartY &&
n.location.column == StartX);
Node previousNode = currentNode;
while (!(currentNode.location.row == EndY && currentNode.location.column ==
EndX))
{
forwardList.Add(currentNode);
neighbour next = findNeighbour(currentNode, previousNode);
previousNode = currentNode;
currentNode = forwardListCopy.First(n => n.location.row == next.row &&
n.location.column == next.column);
}
return forwardList;
}
private List<Node> plotBackward()
{
List<Node> backwardListCopy = CopyList(nodeList);
List<Node> backwardList = new List<Node>();
Node currentNode = backwardListCopy.First(n => n.location.row == EndY &&
n.location.column == EndX);
Node previousNode = currentNode;//this duplicates the first node position
while (!(currentNode.location.row == StartY && currentNode.location.column ==
StartX))
{
backwardList.Add(currentNode);//save the node to the path
neighbour next = findNeighbour(currentNode, previousNode);//find the next
node on the path
previousNode = currentNode;//this saves the node as previous, as we are about
to get the next one
currentNode = backwardListCopy.First(n => n.location.row == next.row &&
n.location.column == next.column);
}
return backwardList;
11. }
private neighbour findNeighbour(Node node, Node prev)
{
neighbour first = new neighbour() { row = -1, column = -1 };//see if the node has
a neighbour other than the previous node
if (node.location.row == prev.location.row && node.location.column ==
prev.location.column)//first instance
{
if (node.bottom != null)
{
first = node.bottom;
}
else if (node.left != null)
{
first = node.left;
}
else if (node.right != null)
{
first = node.right;
}
else if (node.top != null)
{
first = node.top;
}
}
else if (node.bottom != null && (node.bottom.row == prev.location.row &&
node.bottom.column == prev.location.column))
{
/* each of these options indicates the direction of travel
if the movement was from the bottom, then target top first, and then left
and right.
Don't go backwards*/
if (node.top != null)
{
first = node.top;
}
else if (node.left != null)
{
first = node.left;
}
else if (node.right != null)
{
first = node.right;
}
}
else if (node.left != null && (node.left.row == prev.location.row &&
node.left.column == prev.location.column))
{
if (node.right != null)
{
first = node.right;
}
else if (node.bottom != null)
{
first = node.bottom;
}
else if (node.top != null)
{
first = node.top;
}
12. }
else if (node.right != null && (node.right.row == prev.location.row &&
node.right.column == prev.location.column))
{
if (node.left != null)
{
first = node.left;
}
else if (node.bottom != null)
{
first = node.bottom;
}
else if (node.top != null)
{
first = node.top;
}
}
else if (node.top != null && (node.top.row == prev.location.row &&
node.top.column == prev.location.column))
{
if (node.bottom != null)
{
first = node.bottom;
}
else if (node.left != null)
{
first = node.left;
}
else if (node.right != null)
{
first = node.right;
}
}
return first;
}
private List<Node> findShortest(List<List<Node>> lists)
{
int minLength = lists.Min(x => x.Count);
List<Node> shortest = lists.First(x => x.Count == minLength);
//We take the first example with the shortest count
return shortest;
}
private List<Node> CopyList(List<Node> list)
{
List<Node> copy = new List<Node>();
/*Copies each node in the list into a new list of nodes*/
foreach (Node node in list)
{
Node copyNode = new Node() { location = new neighbour() { row =
node.location.row, column = node.location.column } };
if (node.bottom != null)
{
copyNode.bottom = new neighbour() { row = node.bottom.row, column =
node.bottom.column };
}
if (node.left != null)
{
13. copyNode.left = new neighbour() { row = node.left.row, column =
node.left.column };
}
if (node.right != null)
{
copyNode.right = new neighbour() { row = node.right.row, column =
node.right.column };
}
if (node.top != null)
{
copyNode.top = new neighbour() { row = node.top.row, column =
node.top.column };
}
copy.Add(copyNode);
}
return copy;
}
private void CopyMaze()
{
//create a copy of the mazed to plot the solution on
SolvedMaze = new char[Width, Height];
Array.Copy(MazeArray, SolvedMaze, MazeArray.Length);//copies the original maze
}
private bool getDims(String dims)
{
//gets the width and height of the maze
int[] Dims = getIntValues(dims);
if (Dims.Count() == 2)
{
Width = Dims[0];
Height = Dims[1];
return true;
}
else return false;
}
private bool getStart(String start)
{
//gets the start location
int[] Start = getIntValues(start);
if (Start.Count() == 2)
{
StartX = Start[0];
StartY = Start[1];
return true;
}
else return false;
}
private bool getEnd(String end)
{
//gets the end location
int[] End = getIntValues(end);
if (End.Count() == 2)
{
EndX = End[0];
EndY = End[1];
return true;
14. }
else return false;
}
private int[] getIntValues(String param)
{
//splits a string of space separated ints into individual ints
return param.Split(new[] { ' ' }).Select(int.Parse).ToArray();
}
private char[] getCharValues(String param)
{
//splits space separated chars
return param.Split(new[] { ' ' }).Select(char.Parse).ToArray();
}
private void makeOutputFile()
{
char[,] output = new char[Width, Height];
for (int i = 0; i < Height; i++)
{
for (int j = 0; j < Width; j++)
{
char temp = SolvedMaze[j, i];
if (temp == '1')
{
output[j, i] = '#';
}
else if (temp == 'X')
{
output[j, i] = 'X';
}
}
}
output[StartX, StartY] = 'S';
output[EndX, EndY] = 'E';
StringBuilder outList = new StringBuilder();
for (int i = 0; i < Height; i++)
{
StringBuilder sb = new StringBuilder();
for (int j = 0; j < Width; j++)
{
sb.Append(output[j, i]);
}
outList.AppendLine(sb.ToString());
}
System.IO.File.WriteAllText(outFile, outList.ToString());
int fileStart = position + 5;
string filename = outFile.Substring(fileStart);
string time = DateTime.Now.ToLongTimeString();
Console.WriteLine("Output file " + filename + " at " + time);
return;
}
}
}
To see the app working, first open the solution in Visual Studio 2017.
Build the solution, then navigate to the bin/release folder :
15. Double click the Application file (without a file extension) and the console display will report on
progress. The solutions appear in a folder called Solutions.