diff --git a/PuzzlePlayer/Binary.cs b/PuzzlePlayer/Binary.cs index d3ec27f873e4c69fac1931c59f7458f50eb800a8..f3c453a8e1fd9585b00329ba3dd09384f008986e 100644 --- a/PuzzlePlayer/Binary.cs +++ b/PuzzlePlayer/Binary.cs @@ -69,19 +69,7 @@ namespace PuzzlePlayer_Namespace } } - // static meathode for filling a int[,] with -1 - public static int[,] GetClearBoard(int boardSize) - { - int[,] result = new int[boardSize, boardSize]; - // fill the board with empty spaces (-1) - for (int i = 0; i < boardSize; i++) - { - for (int j = 0; j < boardSize; j++) - result[i, j] = emptySpace; - } - - return result; - } + // generates a random solvable board public override void Generate() @@ -227,21 +215,8 @@ namespace PuzzlePlayer_Namespace return null; } - ///* - // shuffle the choices in the list around to get different results every time - // First answer from: https://stackoverflow.com/questions/273313/randomize-a-listt - Random random = new Random(); - int n = choices.Count; - while (n > 1) - { - n--; - int rand = random.Next(n + 1); - Move copy = choices[rand]; - choices[rand] = choices[n]; - choices[n] = copy; - } - //*/ - + choices.Shuffle(); //shuffle the choices around for random results everytime + //(Shuffle methode from the extentions class in the Board.cs file // do the algorithm for every move foreach (Move m in choices) diff --git a/PuzzlePlayer/Board.cs b/PuzzlePlayer/Board.cs index 341c3d32c49de17a87885934456ed1bca93d50c9..766aae9d457971a2e6cf85c67ec86295298f9652 100644 --- a/PuzzlePlayer/Board.cs +++ b/PuzzlePlayer/Board.cs @@ -45,6 +45,19 @@ namespace PuzzlePlayer_Namespace public int[,] boardState; public int[,] lastGeneratedBoard; + // static meathode for filling a int[,] with -1 + public static int[,] GetClearBoard(int boardSize) + { + int[,] result = new int[boardSize, boardSize]; + // fill the board with empty spaces (-1) + for (int i = 0; i < boardSize; i++) + { + for (int j = 0; j < boardSize; j++) + result[i, j] = emptySpace; + } + + return result; + } // checks if the board is valid and solvable before setting the variable. @@ -60,10 +73,11 @@ namespace PuzzlePlayer_Namespace return false; } } + public abstract void Draw(Graphics gr, Rectangle r); // a methode for solving the whole board. It uses the private SolveStep methode untill the whole board is solved // it has one parameter. setting this to true will only give a return value without changing the current boardState - public SOLUTIONS Solve(bool CheckOnly) + public virtual SOLUTIONS Solve(bool CheckOnly) { // two variables for storing the result and the next solveStep int[,] result = (int[,])boardState.Clone(); @@ -124,4 +138,26 @@ namespace PuzzlePlayer_Namespace // performs a left/right (X) click on tile P, changing its value public virtual void TileClick(Point p, int x) { } } + + + // static class for extentions + public static class Extensions + { + private static readonly Random rng = new Random(); + + // shuffle the elements in a list around to get different results every time from the generator algoritms + // First answer from: https://stackoverflow.com/questions/273313/randomize-a-listt + public static void Shuffle<T>(this IList<T> list) + { + int n = list.Count; + while (n > 1) + { + n--; + int k = rng.Next(n + 1); + T value = list[k]; + list[k] = list[n]; + list[n] = value; + } + } + } } diff --git a/PuzzlePlayer/Maze.cs b/PuzzlePlayer/Maze.cs new file mode 100644 index 0000000000000000000000000000000000000000..0f44895bab1f926ccc5c84857a27507e469aa218 --- /dev/null +++ b/PuzzlePlayer/Maze.cs @@ -0,0 +1,406 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Drawing; +using System.Threading; +using System.Windows.Forms; + + +namespace PuzzlePlayer_Namespace +{ + /* + * all the info about where the walls in the maze are is stored in the mazeState int[,] + * the info of wich cells are already visited by the generator methode are stored in the visitedCells + * the boardState is used to keep track of the spaces that the player has visited while solving the maze + * + * if the player/generator has not yet visited a space then it is equal to the emptyspace constant + * if the player/generator did visit the space then it is equal to 1 + * + * The maze board consists of cells with a number between 1 and 15 + * the number represents where the walls are + * 1111 is 15 in binary the first bit is the top wall, second bit the right wall, etc. continuing clockwise + * _ + * | | fully surounded is 15, so 1111 in binary + * - + * _ + * | right side open is 11, so 1011 in binary + * - + * + * -1 means that the cell is empty so there are no walls anywhere + * + * the topleft corner is always the starting point (left wall), and the bottom right corner is always the end point (right wall) + * + */ + + class Maze : Board + { + int[,] mazeState; + int[,] visitedCells; + + Point playerPos; + List<Point> shortestPath; + + public Maze(int size = 20) + { + //drawFactor = 1; + // init all 2D array's + Reset(size); + } + + private void Reset(int size) + { + boardState = GetClearBoard(size); + mazeState = GetClearBoard(size); + visitedCells = GetClearBoard(size); + } + + // two funcions to go from number to walls and back + private int getNumberFromWalls((bool top, bool right, bool bottom, bool left) cell) + { + int result = 0; + + if (cell.top) + result++; + if (cell.right) + result += 2; + if (cell.bottom) + result += 4; + if (cell.left) + result += 8; + + //if (result == 0) + //return emptySpace; //if there are no walls then the space is empty + + return result; + } + + private (bool,bool,bool,bool) getWallsFromNumber(int number) + { + //if(number == emptySpace) + // return (true,true,true,true); //if the place is empty then it is completly walled in, + //so that the walls can be removed when generating the maze + + // bitwise and opperations to check each bit + bool top = (number & 1) != 0; + bool right = (number & 2) != 0; + bool bottom = (number & 4) != 0; + bool left = (number & 8) != 0; + + return (top, right, bottom, left); + } + + public override void Draw(Graphics gr, Rectangle r) + { + // clear screen + gr.FillRectangle(Brushes.Wheat, r); + + Size tilesize = new Size(r.Width / boardState.GetLength(0), r.Height / boardState.GetLength(1)); + Pen wall = new Pen(Color.Black, tilesize.Width / 5); + + //debug colors + //Pen wall1 = new Pen(Color.Black, tilesize.Width / 5); + //Pen wall2 = new Pen(Color.Blue, tilesize.Width / 5); + //Pen wall3 = new Pen(Color.Green, tilesize.Width / 5); + //Pen wall4 = new Pen(Color.Purple, tilesize.Width / 5); + + for (int i = 0; i < boardState.GetLength(0); i++) + for(int j = 0; j < boardState.GetLength(1); j++) + { + Rectangle currentRect = + new Rectangle(r.X + i * tilesize.Width, r.Y + j * tilesize.Height, tilesize.Width, tilesize.Height); + + // draw the space blue if the player has visited it + if (boardState[i, j] == 1) + gr.FillRectangle(Brushes.Blue, currentRect); + + // draw board outline + gr.DrawRectangle(Pens.LightGray,currentRect); + + // drawing walls + (bool top,bool right,bool bottom,bool left) = getWallsFromNumber(mazeState[i,j]); + + if (top) + gr.DrawLine(wall, currentRect.Left, currentRect.Top, currentRect.Right, currentRect.Top); + if (right) + gr.DrawLine(wall, currentRect.Right, currentRect.Top, currentRect.Right, currentRect.Bottom); + if (bottom) + gr.DrawLine(wall, currentRect.Left, currentRect.Bottom, currentRect.Right, currentRect.Bottom); + if (left) + gr.DrawLine(wall, currentRect.Left, currentRect.Bottom, currentRect.Left, currentRect.Top); + + } + + // draw an indication of where the start and end from the maze are + + gr.FillRectangle(Brushes.Blue, r.X, r.Y, tilesize.Width, tilesize.Height); + gr.FillRectangle(Brushes.Red, r.X + tilesize.Width*(boardState.GetLength(0)-1), r.Y + tilesize.Height * (boardState.GetLength(1)-1), tilesize.Width, tilesize.Height); + + //gr.DrawString("START", SettingForm.mainFont, Brushes.Red, r.Left, r.Top + tilesize.Height/2); + //gr.DrawString("END", SettingForm.mainFont, Brushes.Red, r.Right - tilesize.Width / 4, r.Bottom - tilesize.Height / 2); + } + + // this methode generates a Maze starting with an empty board + // it uses the randomized depth first search algoritm as described on this wiki page: + // https://en.wikipedia.org/wiki/Maze_generation_algorithm#Randomized_depth-first_search + public override void Generate() + { + //clear the board and all 2D array's + Reset(mazeState.GetLength(0)); + + Random rnd = new Random(); + int x = rnd.Next(mazeState.GetLength(0)); + int y = rnd.Next(mazeState.GetLength(0)); + + if (Recursive_DepthFirstSearchMazeGenerator(x, y)) + Debug.WriteLine("Maze succesfully generated"); + else + Debug.WriteLine("nope"); + + } + + private bool Recursive_DepthFirstSearchMazeGenerator(int x, int y) + { + visitedCells[x, y] = 1; // mark current cell as visited (this is needed for the first cell) + + // get a list of all the unvisited neigbour cells + List<(int, int)> validNeigbours = getNeigbours(x, y, emptySpace); + if (validNeigbours.Count == 0) + return false; // backtrack + + validNeigbours.Shuffle(); //shuffle the neigbours around for random results everytime + //(Shuffle methode from the extentions class in the Board.cs file + + foreach ((int nx,int ny) neighbour in validNeigbours) + { + if (visitedCells[neighbour.nx, neighbour.ny] == 1) + continue; + visitedCells[neighbour.nx,neighbour.ny] = 1; // mark as visited and update all the walls that are effected + UpdateWalls(x,y, neighbour.nx,neighbour.ny); + + if(BoardComplete()) + return true; + + if(Recursive_DepthFirstSearchMazeGenerator(neighbour.nx,neighbour.ny)) //recursion + return true; // if true then the maze is finished and this cell should return true too + } + + return false; // backtrack if all the neighbours are already visited + } + + private bool BoardComplete() + { + for(int i = 0;i<visitedCells.GetLength(0);i++) + for(int j = 0;j<visitedCells.GetLength(1);j++) + { + if (visitedCells[i, j] == emptySpace) + return false; + } + return true; + } + + // update the walls between two cells + private void UpdateWalls(int x, int y, int x2, int y2) + { + // get the walls from the two cells + (bool top, bool right, bool bottom, bool left) first = getWallsFromNumber(mazeState[x, y]); + (bool top, bool right, bool bottom, bool left) second = getWallsFromNumber(mazeState[x2, y2]); + + int dx = x - x2; + int dy = y - y2; + + if(dx == -1) + { + first.right = false; + second.left = false; + } + else if (dx == 1) + { + first.left = false; + second.right = false; + } + else if (dy == -1) + { + first.bottom = false; + second.top = false; + } + else if (dy == 1) + { + first.top = false; + second.bottom = false; + } + + // update the walls + mazeState[x, y] = getNumberFromWalls(first); + mazeState[x2, y2] = getNumberFromWalls(second); + } + + // checks all the neighbours for the checkFor param (this is used to get either the visited or the unvisited neighbours) + private List<(int,int)> getNeigbours(int x, int y, int checkFor) + { + List<(int,int)> result = new List<(int,int)> (); + + if(x-1 >= 0) + if(visitedCells[x-1,y] == checkFor) + result.Add((x-1,y)); + if (y - 1 >= 0) + if (visitedCells[x, y - 1] == checkFor) + result.Add((x, y - 1)); + + if (x + 1 < visitedCells.GetLength(0)) + if (visitedCells[x + 1, y] == checkFor) + result.Add((x + 1, y)); + if (y + 1 < visitedCells.GetLength(1)) + if (visitedCells[x, y + 1] == checkFor) + result.Add((x, y + 1)); + /* + for (int checkX = x-1;checkX <= x+1; checkX++) + for(int checkY = y-1;checkY <= y+1;checkY++) + { + // check if checkX/Y are within bounds + if(checkX >= 0 && checkY >=0 && checkX < visitedCells.GetLength(0) && checkY < visitedCells.GetLength(1)) + if(visitedCells[checkX,checkY] == checkFor) + result.Add((checkX,checkY)); + } + */ + return result; + } + + // overrite the Solve methode because a maze and an ordinary board puzzle like a sudoku or binary puzzle + // don't have much in common in regards to solving. So most of the code in the abstract Board class is to no use for us here + public override SOLUTIONS Solve(bool CheckOnly) + { + visitedCells = GetClearBoard(mazeState.GetLength(0)); + boardState = GetClearBoard(mazeState.GetLength(0)); + shortestPath = new List<Point>(); + + shortestPath.Add(new Point(0,0)); // starting point + bool foundSolution = Recursive_DepthFirstSearchMazeSolve(0, 0); + if(CheckOnly && foundSolution) + return SOLUTIONS.UNIQUE; + else if(!CheckOnly && foundSolution) + { + foreach(Point p in shortestPath) + { + boardState[p.X, p.Y] = 1; + } + } + + return SOLUTIONS.NONE; + } + + // we solve the maze in almost the same way as we generated the maze + private bool Recursive_DepthFirstSearchMazeSolve(int x, int y) + { + visitedCells[x, y] = 1; // mark current cell as visited (this is needed for the first cell) + + // get a list of all the unvisited neigbour cells + List<(int, int)> validNeigbours = getNeigbours(x, y, emptySpace); + if (validNeigbours.Count == 0) + { + shortestPath.RemoveAt(shortestPath.Count-1); // remove path if we backtrack + return false; // backtrack + } + + validNeigbours.Shuffle(); //shuffle the neigbours around for random results everytime + //(Shuffle methode from the extentions class in the Board.cs file + + foreach ((int nx, int ny) neighbour in validNeigbours) + { + // if the neighbour is already visited or it is a invalid neighbour then continue to the next + if (visitedCells[neighbour.nx, neighbour.ny] == 1 || !isValidNeighbour(x,y,neighbour.nx,neighbour.ny)) + continue; + + visitedCells[neighbour.nx, neighbour.ny] = 1; // mark as visited + shortestPath.Add(new Point(neighbour.nx, neighbour.ny)); // mark as correct path + + // if the bottom right space is reached then we found a solution + if (neighbour.nx == (mazeState.GetLength(0)-1) && neighbour.ny == (mazeState.GetLength(1)-1)) + return true; + + if (Recursive_DepthFirstSearchMazeSolve(neighbour.nx, neighbour.ny)) //recursion + return true; // if true then the maze is finished and this cell should return true too + } + + shortestPath.RemoveAt(shortestPath.Count - 1); // remove path if we backtrack + return false; // backtrack if all the neighbours are already visited + } + + private bool isValidNeighbour(int x, int y, int nx, int ny) + { + // get the walls from the two cells + (bool top, bool right, bool bottom, bool left) first = getWallsFromNumber(mazeState[x, y]); + (bool top, bool right, bool bottom, bool left) second = getWallsFromNumber(mazeState[nx, ny]); + + int dx = x - nx; + int dy = y - ny; + + if (dx == -1) + { + if (!(first.right && second.left)) + return true; + } + else if (dx == 1) + { + if (!(first.left && second.right)) + return true; + } + else if (dy == -1) + { + if (!(first.bottom && second.top)) + return true; + } + else if (dy == 1) + { + if (!(first.top && second.bottom)) + return true; + } + + + return false; + } + + // this methode is not needed because we write our own Solve methode + protected override List<Move> GetSolveList(int[,] boardToSolve) + { + return null; + } + + /* Add later + public override void TileInput(Point p, Keys key) + { + (bool top, bool right, bool bottom, bool left) = getWallsFromNumber(mazeState[p.X,p.Y]); + + switch (key) + { + case Keys.Up | Keys.W: + if (!top) + playerPos.Y++; + break; + case Keys.Down | Keys.S: + if (!bottom) + playerPos.Y--; + break; + case Keys.Right | Keys.D: + if (!right) + playerPos.X++; + break; + case Keys.Left | Keys.A: + if (!left) + playerPos.X--; + break; + } + + // upadte the spaces where the player has walked + boardState[playerPos.X, playerPos.Y] = 1; + } + */ + public override void TileInput(Point p, int x) + { + throw new NotImplementedException(); + } + public override void TileClick(Point p, int x) + { + //MessageBox.Show($"{mazeState[p.X, p.Y]}"); + } + } +} diff --git a/PuzzlePlayer/PuzzleForm.cs b/PuzzlePlayer/PuzzleForm.cs index 8bda5037b633a7ccdd64e6b2c277957d04d570b7..de33cd932d769657583bd58074a2d9801c1293bf 100644 --- a/PuzzlePlayer/PuzzleForm.cs +++ b/PuzzlePlayer/PuzzleForm.cs @@ -97,9 +97,6 @@ namespace PuzzlePlayer_Namespace Board = b; CreateUI(); - - Board.boardState[1, 1] = 1; - Board.boardState[2, 2] = 0; } private void CreateUI() //sets up ui elements { diff --git a/PuzzlePlayer/PuzzlePlayer.cs b/PuzzlePlayer/PuzzlePlayer.cs index f64b7945fc5cc78fe50b64fe3d9cef31b4f28572..de329eb6114bb1522374a58146332d2be4cc075e 100644 --- a/PuzzlePlayer/PuzzlePlayer.cs +++ b/PuzzlePlayer/PuzzlePlayer.cs @@ -10,7 +10,7 @@ namespace PuzzlePlayer_Namespace { internal static void Main(string[] args) { - Application.Run(new MainForm()); + Application.Run(new PuzzleForm(new Maze())); } }