using System; using System.Collections.Generic; using System.Diagnostics; using System.Drawing; using System.Linq; using System.Runtime.CompilerServices; using System.Windows.Forms; namespace PuzzlePlayer_Namespace { /* The binair board consist of 1 and 0 * This means that the possible states a cell can be are: empty, one or zero * To specify this in an int[,] array we will use -1 for an empty space, * 0 for a space with a zero and 1 for a space with a one * The empty space is a constant defined in the abstract Board class */ internal class Binary : Board { // constructor with boardSize parameter (default is set to 8 but can be changed) public Binary(int boardSize = 6) // should be even { boardState = GetClearBoard(boardSize,boardSize); lastGeneratedBoard = GetClearBoard(boardSize, boardSize); description = "Binary puzzle is played on any even-numbered square grid, with some cells initially containing black or white circles. The goal of the puzzle is to fill all cells such that:\r\n● More than two circles of the same color cannot be adjacent\r\n● Each row and column must contain an equal number of black and white circles"; drawFactor = 8; // clear the board (fill it in with -1) //Clear(false); } public override void Draw (Graphics gr, Rectangle r) //draws board in rectangle R. warning: will stretch image unless FitBoard() is used for rectangle size { Size tilesize = new Size(r.Width / boardState.GetLength(0), r.Height / boardState.GetLength(1)); Pen border = new Pen(Color.Black, 2); gr.FillRectangle(Brushes.Beige, r.X, r.Y, tilesize.Width*boardState.GetLength(0), tilesize.Height*boardState.GetLength(1)); for (int i = 0; i < boardState.GetLength(0); i++) { for(int j = 0; j < boardState.GetLength(1); j++) { gr.DrawRectangle(Pens.LightGray, r.X+i* tilesize.Width, r.Y+j* tilesize.Height, tilesize.Width, tilesize.Height); if (boardState[i,j] == 0) { gr.FillEllipse(Brushes.White, (int)(r.X + ((double)i + 0.125) * tilesize.Width), (int)(r.Y + ((double)j + 0.125) * tilesize.Height), tilesize.Width * 3 / 4, tilesize.Height * 3 / 4); gr.DrawEllipse(border, (int)(r.X + ((double)i + 0.125) * tilesize.Width + border.Width/2), (int)(r.Y + ((double)j + 0.125) * tilesize.Height + border.Width/2), tilesize.Width * 3 / 4 - border.Width, tilesize.Height * 3 / 4 - border.Width); } if (boardState[i, j] == 1) { gr.FillEllipse(Brushes.Black, (int)(r.X + ((double)i + 0.125) * tilesize.Width), (int)(r.Y + ((double)j + 0.125) * tilesize.Height), tilesize.Width * 3 / 4, tilesize.Height * 3 / 4); } if (lastGeneratedBoard[i,j] != Board.emptySpace) { gr.FillRectangle(Brushes.LightGray, (int)(r.X + ((double)i + 0.4375) * tilesize.Width), (int)(r.Y + ((double)j + 0.4375) * tilesize.Height), tilesize.Width / 8, tilesize.Height / 8); } } } } // generates a random solvable board public override void Generate() { // generate a board with a brute force methode // this methode works fine for boards with a size of 6x6 or less. But takes exponential more time if the board size is bigger int counter = 1; int[,] generatedBoard; //generates a board starting with an empty board and a empty HashSet while ((generatedBoard = BackTrackAlgorithm(GetClearBoard(boardState.GetLength(0), boardState.GetLength(1)), new HashSet<string>())) == null) { counter++; Debug.WriteLine($"board is null....trying for the {counter} time"); } MessageBox.Show($"Found board in {counter} tries"); boardState = (int[,])generatedBoard.Clone(); // generate a list with all positions List<(int,int)> allPositions = new List<(int,int)> (); for(int i = 0; i < boardState.GetLength(0); i++) for(int j = 0; j < boardState.GetLength(1); j++) allPositions.Add(new (i, j)); Random rnd = new Random(); //remove spaces until the board is unsolvable, then go one step back while (true) { if(allPositions.Count == 0) break; int i = rnd.Next(allPositions.Count); (int x, int y) = allPositions[i]; int copy = boardState[x,y]; // create a copy in case it is not possible to solve the puzzle anymore boardState[x,y] = emptySpace; allPositions.RemoveAt(i); if(Solve(true) == SOLUTIONS.NONE) { boardState[x, y] = copy; // restore the copy if there are no solutions found } } // save the generated board for testing the users solution and the use of a reset button lastGeneratedBoard = (int[,])boardState.Clone(); } private static void PrintBoard(int[,] board) { for (int i = 0; i < board.GetLength(0); i++) { for (int j = 0; j < board.GetLength(1); j++) Debug.Write(board[i, j] == emptySpace ? "." : board[i, j].ToString()); Debug.WriteLine(""); } Debug.WriteLine(new string('-', 20)); } private static string BoardToString(int[,] board) { string result = ""; for (int i = 0; i < board.GetLength(0); i++) { for (int j = 0; j < board.GetLength(1); j++) { result += (board[i, j] == emptySpace ? "." : board[i, j].ToString()); } result += "|"; } return result; } private static int[,] BoardFromString(string board) { string[] parts = board.Split('|',StringSplitOptions.RemoveEmptyEntries); int[,] result = new int[parts.Length, parts.Length]; for (int i = 0; i < result.GetLength(0); i++) { for (int j = 0; j < result.GetLength(1); j++) { string s = parts[i][j].ToString(); // convert char to string if (s == ".") result[i, j] = emptySpace; else if (s == "1" || s == "0") result[i, j] = int.Parse(s); // convert string to int else return null; //invalid input string } } return result; } private static int recursionDepth = 0; private static int maxDepth = 100000; // max recurtion depth that is allowed private static int maxBoardsInHashSet = 40000; // generates a random board with a backtracking algorithm // After searching online about what the best way is to make a random puzzle generator a lot of people pointed towards a backtracking algorithm // I found the information about what a backtracking algorithm is here: https://www.geeksforgeeks.org/introduction-to-backtracking-2/ // But i wrote all the code myself // It uses a HashSet to blablalblalbllablalblablablal private static int[,] BackTrackAlgorithm(int[,] board, HashSet<string> alreadyCheckedBoards) { //recursionDepth++; if (recursionDepth > maxDepth || alreadyCheckedBoards.Count > maxBoardsInHashSet) { return null; } // print board for debugging //PrintBoard(board); // check if the board is complete. if so then we can return the result if (IsBoardCompletlyFilledIn(board)) { recursionDepth--; return board; } // get all the possible choices and per choice do the backtrackAlgorithm List<Move> choices = GetChoices(board); // if there aren't any solutions then that could mean that the whole board is filled in, or that we are stuck and need to backtrack // and because we already did a check if the board is completly filled-in, we know that we need to backtrack if (choices.Count == 0) { recursionDepth--; return null; // backtrack } // check if the current board is already imposible to complete even tho there are still choices left if (!BoardIsPosible(board, choices)) { recursionDepth--; return null; } 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) { int[,] newBoard = (int[,])board.Clone(); //create a shallow clone to avoid multiple paths refrencing the same object newBoard[m.x, m.y] = m.changeTo; string newBoardString = BoardToString(newBoard); if (alreadyCheckedBoards.Contains(newBoardString)) { recursionDepth--; return null; } alreadyCheckedBoards.Add(newBoardString); int[,] result = BackTrackAlgorithm(newBoard, alreadyCheckedBoards); // recursion for every move if (result != null) { recursionDepth--; return result; } } recursionDepth--; // if all choices fail then we should also return null return null; } // checks if the board has a possible outcome to optimise the generating process private static bool BoardIsPosible(int[,] board, List<Move> choices) { if (choices.Count == 0) //if there are no choices then the board is imposible return false; List<(int, int)> emptyLocations = new List<(int, int)>(); for (int i = 0; i < board.GetLength(0); i++) { for (int j = 0; j < board.GetLength(1); j++) { if (board[i, j] == emptySpace) emptyLocations.Add((i, j)); //add all the empty locations to the list } } // Verify each empty location has a valid move foreach (Move move in choices) { if (emptyLocations.Contains((move.x, move.y))) { emptyLocations.Remove((move.x, move.y)); if (emptyLocations.Count == 0) // if there are none left we can return true return true; } } return emptyLocations.Count == 0; // if there are none left we can return true } // get all the possible choices private static List<Move> GetChoices(int[,] board) { List<Move> choices = new List<Move>(); // loop for both 1 and 0 for (int checkFor = 0; checkFor <= 1; checkFor++) { // check foreach cell if it doesn't violate the rules for (int i = 0; i < board.GetLength(0); i++) for (int j = 0; j < board.GetLength(1); j++) { if(board[i, j] == emptySpace && !DoAllChecks(i,j,board,checkFor)) choices.Add(new Move(i, j, checkFor)); } } return choices; } private static bool IsBoardCompletlyFilledIn(int[,] board) { for (int i = 0; i < board.GetLength(0); i++) for (int j = 0; j < board.GetLength(1); j++) if (board[i, j] == emptySpace) // if the space is equal to an empty space then the board is not filled in return false; return true; } // gets a list with all the possible moves protected override List<Move> GetSolveList(int[,] boardToSolve) { List<Move> result = new List<Move>(); for (int i = 0; i < boardToSolve.GetLength(0); i++) for (int j = 0; j < boardToSolve.GetLength(1); j++) { Move ?m = CheckMove(i, j, boardToSolve); if (m != null) result.Add((Move)m); } return result; } private Move? CheckMove(int x, int y, int[,] boardToSolve) { // empty check if (boardToSolve[x, y] != emptySpace) return null; //Debug.WriteLine("wa"); // loop two times for checking 0 and 1 for (int i = 0; i <= 1; i++) { int opposite; if (i == 0) opposite = 1; else opposite = 0; // check if it is a valid move if (DoAllChecks(x, y, boardToSolve, opposite)) { //Debug.WriteLine($"true {x} {y} {i}"); return new Move(x, y, i); } } // if both 0 and 1 fail, then the move is invalid and null is returned return null; } // shorthand for all the checks in one function private static bool DoAllChecks(int x, int y, int[,] board, int checkFor) { return MiddleCheck(x, y, board, checkFor) || SideCheck(x, y, board, checkFor) || EvenCheck(x, y, board, checkFor); } // check if the space is surrounded on both sides by the same number. If it is, the checked space should be the opposite number private static bool MiddleCheck(int x, int y, int[,] b, int checkFor) { // first check if x-1 and x+1 aren't out of bounds // after that do the check if the move is valid if(!(x-1 < 0 || x+1 > b.GetLength(0) - 1)) if (b[x - 1, y] == checkFor && b[x + 1, y] == checkFor) return true; // same for y if (!(y - 1 < 0 || y + 1 > b.GetLength(1) - 1)) if (b[x, y - 1] == checkFor && b[x, y + 1] == checkFor) return true; // return false if nothing was found return false; } // check if the two spaces left, right, up or down of the space are the opposite number. if so return true private static bool SideCheck(int x, int y, int[,] b, int checkFor) { //check the two spaces left if (x - 2 >= 0) if (b[x - 2, y] == checkFor && b[x - 1, y] == checkFor) return true; //check the two spaces right if (x + 2 < b.GetLength(0)) if (b[x + 2, y] == checkFor && b[x + 1, y] == checkFor) return true; //check the two spaces down if (y - 2 >= 0) if (b[x , y- 2] == checkFor && b[x , y- 1] == checkFor) return true; //check the two spaces up if (y + 2 < b.GetLength(1)) if (b[x , y+ 2] == checkFor && b[x , y+ 1] == checkFor) return true; return false; } // every row and colom should have an even number of 1'boardSize and 0'boardSize. So if the total number of 1'boardSize in a row is equal to half the with of the row a 0 should be filled in. private static bool EvenCheck(int x, int y, int[,] b, int checkFor) { // check for a row and colum (provided that the width and height of the board is the same) int countRow = 0, countCol = 0; for (int i = 0; i < b.GetLength(0); i++) { if (b[i, y] == checkFor) countRow++; if (b[x, i] == checkFor) countCol++; } // if there are more counted then possible return false if ((countRow) > b.GetLength(0) / 2 || (countCol) > b.GetLength(0) / 2) return false; // check if the total number of oppisite numbers is equal to half te widht/heigt if (countRow == b.GetLength(0) / 2 || countCol == b.GetLength(1) / 2) return true; return false; } public override void TileInput(Point? p, Keys k) { if (p == null) return; int num = (int)k - 48; if (num==0 || num==1 || num==2) boardState[((Point)p).X, ((Point)p).Y] = num%2; } public override void TileClick(Point p, int x) { if (boardState[p.X, p.Y] == emptySpace) { boardState[p.X, p.Y] = x; return; } if (boardState[p.X, p.Y] == x) { boardState[p.X, p.Y] = 1 - boardState[p.X, p.Y]; return; } if (boardState[p.X, p.Y] == 1 - x) { boardState[p.X, p.Y] = emptySpace; } } } }