Skip to content
Snippets Groups Projects
Binary.cs 18.11 KiB
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;
            }
        } 

    }
}