Making Minesweeper Clone with C#

In this post, We will see how we can make a minesweeper game clone. Let's start it. First of all if you've played in a minesweeper before, You know more or less how minesweeper works. Frankly, I will try to make the game based on this. 

So what are the basic rules of the minesweeper game? First of all, if the person playing the game clicks on the box containing the mines, it is game over. If a number comes out of the box that the player pressed, which can be 1, 2, 3, 4, 5, 6, 7, 8, then it means that there are as many mines as that number around the opened box. It will also be in empty boxes. Clicking on these empty boxes will open other empty boxes around it. I think I will solve this situation with the Breadth First Search algorithm. If the player is sure that a box contains a mine, the player will mark it with a flag.

Adapting all these rules to the game may sound a bit complicated, but it is actually quite simple. I think we can understand this better by making the game. 

I created a tileset that I will use in the game. I quickly made this texture with Aseprite. You can download it if you want to use it:

tileset minesweeper

In order, there is the image representing an unopened box, opened boxes indicating the number of mines 1,2,3,4,5,6,7 and 8. representative mine image, an unopened and checked box, and and finally an empty opened box image. Instead of using them all individually, I combined them into a tileset. I will take advantage of using the TextureRect feature that SFML provides.

I added this image into the Assets folder I created in the project directory. I created a class called Game. I loaded the tileset as texture.

using System;
using SFML.Graphics;
using SFML.Window;

namespace minesweeperclone
{
    class Game
    {
        Texture texture;

        public Game()
        {
            texture = new Texture("Assets/tileset.png");
        }
    }
}
Now it's time to create the box class. There will be 400 boxes in total, and forty of them will be mines.
using System;
using System.Collections.Generic;
using SFML.Graphics;
using SFML.Window;
using SFML.System;

namespace minesweeperclone
{
    class Box
    {
        Vector2f position;
        public bool isOpened;
        public bool isMine;
        public int mineCount;
        public bool isMarked;

        // ui
        public RectangleShape uiBox;
        Texture texture;
        public int type;
        

        public Box(Vector2f position, bool isMine, Texture texture)
        {
            this.position = position;
            this.isMine = isMine;

            this.uiBox = new RectangleShape(new Vector2f(16, 16));
            this.uiBox.Texture = texture;
            this.uiBox.Position = this.position;
            this.uiBox.TextureRect = new IntRect(type,0,10,10);
        }
    }
...
The size of each box will be 16 px. I will create a hashset inside Game class. The key type of this hashtable will be Vector2f and the value type will be Box.
using System;
using SFML.Graphics;
using SFML.Window;

namespace minesweeperclone
{

    class Game
    {
    	Sprite board;
        
        Texture texture;

        Dictionary<Vector2f, Box> boxes;
        

        public Game()
        {
            texture = new Texture("Assets/tileset.png");

            boxes = new Dictionary<Vector2f, Box>();

            for (int y = 0; y < 20; y++)
            {
                for (int x = 0; x < 20; x++)
                {
                    boxes[new Vector2f(x, y)] = new Box(new Vector2f(x * 16, y * 16), false, texture);
                }
            }
        
        }
    }
}
Now we need to draw these boxes on the screen as 20x20. I'm creating a canvas in the Game class. This canvas is actually referred to as rendertexture in SFML. The size of this Rendertexture must be 320x320, because of 16*20=320:
using System;
using SFML.Graphics;
using SFML.Window;

namespace minesweeperclone
{

    class Game
    {
    	Sprite board;
        
        Texture texture;

        Dictionary<Vector2f, Box> boxes; 

        RenderTexture renderTexture = new RenderTexture(320, 320);
        
    	public Game()
        {
        	board = new Sprite(renderTexture.texture);
            ...
Again, I am creating a draw method within the Game class:
        public void Draw(RenderTarget window)
        {
            renderTexture.Clear();

            for (int y = 0; y < 20; y++)
            {
                for (int x = 0; x < 20; x++)
                {
                    renderTexture.Draw(boxes[new Vector2f(x, y)].uiBox);
                }
            }

            renderTexture.Display();

            window.Draw(board);
        }
Now it's time to test. I've created an object of the Game class and rendered it in the game loop:
using System;
using SFML.Graphics;
using SFML.Window;

namespace minesweeperclone
{
    class Program
    {
        static void Main(string[] args)
        {
            const int WIDTH = 640;
            const int HEIGHT = 480;
            const string TITLE = "Minesweeper";
            
            VideoMode mode = new VideoMode(WIDTH, HEIGHT);
            RenderWindow window = new RenderWindow(mode, TITLE);
            Game game = new Game();
            
            window.SetVerticalSyncEnabled(true);

            window.Closed += (sender, args) => window.Close();

            while (window.IsOpen)
            {
                window.DispatchEvents();
                window.Clear(Color.Blue);

                game.Draw(window);
                
                window.Display();
            }
        }
    }
}
The result I got:

I think I need to center the board. There is a very simple formula for this (parent.width / 2 - child.width/2, parent.height/2 - child.height/2). We can adapt this to the position of the board:
        public Game()
        {
            board = new Sprite(renderTexture.Texture);
            board.Position = new Vector2f(640/2 - 320/2, 480/2 - 320/2);

            ...
If we run again we will see the board is centered:
result
It's time to bring the click feature to these squares. For this, I create a field named IntRect in Box class. If the mouse coordinates are within the area defined as IntRect, it means that the square can be clicked. 
namespace minesweeperclone
{
    class Box
    {
        ...
        public IntRect rect;
        
        public Box(Vector2f position, bool isMine, Texture texture)
        {
            ...

            this.rect = new IntRect(160 + (int)this.position.X, 80 + (int)this.position.Y, 16, 16);
        }
    }
Now, we will be able to easily check if that square is clicked by this feature. I will create update method in Game class. This method will continuously check all tiles:
        public void Update(Vector2i mousePos)
        {
            foreach (var box in boxes)
            {
                if(box.Value.rect.Contains(mousePos.X, mousePos.Y))
                {
                    // change view of the clicked box
                    box.Value.type = 9;
                    box.Value.uiBox.TextureRect = new IntRect(box.Value.type * 10, 0, 10, 10);
                }
            }
        }
As you can see, it takes mousePos as a parameter. We will send these mouse coordinates in the game loop.
            while (window.IsOpen)
            {
                window.DispatchEvents();

                Vector2i mousePos = Mouse.GetPosition((Window)window);
                game.Update(mousePos);
                ...
If we run it and hover the mouse cursor over the boxes:
minesweeper over button


But we have to click on the boxes. That's why I will create two properties called LeftClick and RightClick:
    class Game
    {
        ...

        RenderTexture renderTexture = new RenderTexture(320, 320);

        public bool LeftClick {get; set;}
        public bool RightClick {get; set;}
I returned the file containing the game loop and wrote an eventhandler for them:
            ...
            window.SetVerticalSyncEnabled(true);

            window.Closed += (sender, args) => window.Close();

            window.MouseButtonPressed += (sender, args) => 
            {
                if(args.Button == Mouse.Button.Left)
                {
                    game.LeftClick = true;
                }
                if(args.Button == Mouse.Button.Right)
                {
                    game.RightClick = true;
                } 
            };

            while (window.IsOpen)
            {
            	...
We will use these values in the update method of Game class. For example, when we click on the left click, the box will open, and when we click on the right click, the clicked box will be marked with a flag:
        public void Update(Vector2i mouseCoords)
        {
            
            foreach (var box in boxes)
            {
                if(LeftClick && box.Value.rect.Contains((int)mouseCoords.X, (int)mouseCoords.Y))
                {
                    LeftClick = false;

                    // change view of the clicked box
                    box.Value.type = 9;
                    box.Value.isOpened = true;
                    box.Value.uiBox.TextureRect = new IntRect(box.Value.type * 10,0,10,10);
                }

                // set flag
                if(RightClick && box.Value.rect.Contains((int)mouseCoords.X, (int)mouseCoords.Y))
                {
                    RightClick = false;

                    if(!box.Value.isOpened)
                    {
                        box.Value.type = 11;
                        box.Value.uiBox.TextureRect = new IntRect(box.Value.type * 10,0,10,10);
                    }
                }
            }
        }
So, let's test it:
test of game

I think we've taken care of the interface part in general. Now it's time to settle the background of the game. I mentioned that we will place 40 mines randomly. But of course, we need to add them periodically. I mentioned that we will place 40 mines randomly. But of course, we need to add them periodically.

If I remember correctly, the probability of the first clicked box being a mine is zero, because the placing of mines works at this point. That's why I will do it according to this condition. We will place the mines at the very moment when the first click is made, that is, when the box is clicked with a left click for the first time:
        public void GenerateMines()
        {   
            int limit = 0;
            int rowLimit = 0;
            Random random = new Random();

            for (int y = 0; y < 20; y++)
            {
                rowLimit = 0;
                for (int x = 0; x < 20; x++)
                {
                    if(!boxes[new Vector2f(x, y)].isOpened && rowLimit < 4 && !boxes[new Vector2f(x, y)].isMine && limit < 80)
                    {
                        if(random.Next(0,8) >= 6)
                        {
                            rowLimit++;
                            limit++;
                            boxes[new Vector2f(x, y)].isMine = true;
                            boxes[new Vector2f(x, y)].type = 10;
                        }
                    }
                }
            }
        }
It may not be a very good generator, but I think it is enough. As I said this generator will only run once. That's why I created a property called NotFirst in Game class.
public bool NotFirst {get; set;}
And inside the update method I used it like this:
        public void Update(Vector2i mouseCoords)
        {
            
            foreach (var box in boxes)
            {
                if(LeftClick && box.Value.rect.Contains((int)mouseCoords.X, (int)mouseCoords.Y))
                {
                    LeftClick = false;

                    // change view of the clicked box
                    box.Value.type = 9;
                    box.Value.isOpened = true;
                    box.Value.uiBox.TextureRect = new IntRect(box.Value.type * 10,0,10,10);

                    if(!NotFirst)
                    {
                        GenerateMines();
                        NotFirst = true;
                    }
                }

                // set flag
                if(RightClick && box.Value.rect.Contains((int)mouseCoords.X, (int)mouseCoords.Y))
                {
                    RightClick = false;

                    if(!box.Value.isOpened)
                    {
                        box.Value.type = 11;
                        box.Value.uiBox.TextureRect = new IntRect(box.Value.type * 10,0,10,10);
                    }
                }
            }
        }
Now it's time to place the numbers according to the mines placed. We will do this first too.
        public void CalculateNumbers()
        {
            Vector2f[] offset = 
            {
                new Vector2f(-1, -1),
                new Vector2f(0, -1),
                new Vector2f(1, -1),
                new Vector2f(-1, 0),
                new Vector2f(1, 0),
                new Vector2f(-1, 1),
                new Vector2f(0, 1),
                new Vector2f(1, 1)
            };
            int mineCounter = 0;

            for (int y = 0; y < 20; y++)
            {
                for (int x = 0; x < 20; x++)
                {
                    mineCounter = 0;

                    if(!boxes[new Vector2f(x, y)].isMine)
                    {
                        int i = 0;
                        while(i < 8)
                        {
                            if(boxes.ContainsKey(new Vector2f(x + offset[i].X, y + offset[i].Y)))
                            {
                                if(boxes[new Vector2f(x + offset[i].X, y + offset[i].Y)].isMine)
                                {
                                    mineCounter++;
                                }
                            }
                            i++;
                        }
                        if(mineCounter == 0)
                        {
                            boxes[new Vector2f(x, y)].type = 9;
                        }
                        else
                        {
                            boxes[new Vector2f(x, y)].type = mineCounter;
                        }
                    }
                }
            }
        }
Neighbors of each block will be checked. Neighbors of each block will be checked. If it is a mine, it will be added to mineCounter. If the above code is examined carefully, it can be easily understood what we are doing. This time, we are making the following change in our update method:
        public void Update(Vector2i mouseCoords)
        {
            
            foreach (var box in boxes)
            {
                if(LeftClick && box.Value.rect.Contains((int)mouseCoords.X, (int)mouseCoords.Y))
                {
                    LeftClick = false;

                    // change view of the clicked box
                    box.Value.type = 9;
                    box.Value.isOpened = true;
                    

                    if(!NotFirst)
                    {
                        GenerateMines();
                        CalculateNumbers();
                        NotFirst = true;
                    }
                    box.Value.uiBox.TextureRect = new IntRect(box.Value.type * 10,0,10,10);
                }

                // set flag
                if(RightClick && box.Value.rect.Contains((int)mouseCoords.X, (int)mouseCoords.Y))
                {
                    RightClick = false;

                    if(!box.Value.isOpened)
                    {
                        box.Value.type = 11;
                        box.Value.uiBox.TextureRect = new IntRect(box.Value.type * 10,0,10,10);
                    }
                }
            }
        }
Our game is very close to the end. There are two important things that come to my mind. One of them is to be game over when clicking on a mine. First, I created a function called ShowAllMines. If the player clicks on the mine, all mines will be visible. I also defined a property named EnabledClick. This property is set to true in the constructor function. If the player clicks on the mine, the game will be over and the player will not be able to press the boxes again:
        ...
        public void ShowAllMines()
        {
            foreach (var box in boxes)
            {
                if(box.Value.isMine)
                {
                    box.Value.uiBox.TextureRect = new IntRect(10 * 10, 0, 10, 10);
                }
            }
        }

        public void Update(Vector2i mouseCoords)
        {
            if(EnabledClick)
            {
                foreach (var box in boxes)
                {
                    if(LeftClick && box.Value.rect.Contains((int)mouseCoords.X, (int)mouseCoords.Y))
                    {
                        LeftClick = false;

                        // change view of the clicked box
                        box.Value.isOpened = true;

                        // if it is mine then show game over text on the screen.
                        if(box.Value.isMine)
                        {
                            // game over
                            ShowAllMines();
                            EnabledClick = false;
                        }
                        ...
Let's test it:
Mines Explode

Normally, when clicking on the empty parts, all the empty cells adjacent to it and the numbers adjacent to them should be visible. But I haven't added that feature here yet, it still works logically.
I will use the BFS algorithm so that I can do this:
        private void showOthers(Vector2i tilePos)
        {
            if(tilePos.X >= 0 && tilePos.X < 20 && tilePos.Y >= 0 && tilePos.Y < 20)
            {
                
                Queue<Vector2i> queue = new Queue<Vector2i>();
                
                Dictionary<Vector2i, bool> registry = new Dictionary<Vector2i, bool>();

                queue.Enqueue(tilePos);
                
                registry[new Vector2i(tilePos.X, tilePos.Y)] = true;

                List<int[]> dirs = new List<int[]>()
                {
                    new int[] {1, 0},
                    new int[] {-1, 0},
                    new int[] {0, 1},
                    new int[] {0, -1},
                    new int[] {1,1},
                    new int[] {1,-1},
                    new int[] {-1,1},
                    new int[] {-1,-1}
                };
                
                while(queue.Count != 0)
                {
                    Vector2i currentTile = queue.Dequeue();

                    foreach (var dir in dirs)
                    {
                        int xx = currentTile.X + dir[0];
                        int yy = currentTile.Y + dir[1];

                        if(registry.ContainsKey(new Vector2i(xx, yy)))
                        {
                            continue;
                        }
                        if(xx >= 0 && xx < 20 && yy >= 0 && yy < 20)
                        {
                            if(
                                !boxes[new Vector2f(xx, yy)].isOpened && boxes[new Vector2f(xx, yy)].type >= 1 && 
                                boxes[new Vector2f(xx, yy)].type < 10 && !boxes[new Vector2f(xx, yy)].isMine
                            )
                            {
                                
                                boxes[new Vector2f(xx, yy)].uiBox.TextureRect = new IntRect(boxes[new Vector2f(xx, yy)].type * 10, 0, 10, 10);
                                boxes[new Vector2f(xx, yy)].isOpened = true;
                                registry[new Vector2i(xx, yy)] = true;
                            }
                        }
                        if(xx < 0 || xx >= 20 || yy < 0 || yy >= 20)
                        {
                            continue;
                        }
                        
                        registry[new Vector2i(xx, yy)] = true;

                        if(boxes[new Vector2f(xx, yy)].type == 9)
                            queue.Enqueue(new Vector2i(xx, yy));
                    }
                }
            }
        }
It may seem a little scary, but this is how the BFS algorithm generally works. It controls all cells from the center outward. Here's how we used this function in the update function:
        public void Update(Vector2i mouseCoords)
        {
            if(EnabledClick)
            {
                foreach (var box in boxes)
                {
                    if(LeftClick && box.Value.rect.Contains((int)mouseCoords.X, (int)mouseCoords.Y))
                    {
                        ...

                        if(!NotFirst)
                        {
                            GenerateMines();
                            CalculateNumbers();
                            NotFirst = true;
                        }
                        box.Value.isOpened = true;
                        box.Value.uiBox.TextureRect = new IntRect(box.Value.type * 10,0,10,10);
                        
                        if(box.Value.type == 9)
                        {
                            showOthers(new Vector2i((int)box.Value.position.X/16, (int)box.Value.position.Y/16));
                        }
                    }

                    ...
                }
            }
        }
Let's run it our game:
Minesweeper Clone
That's great. If we flag all the mines and open the remaining boxes, we will win this game. We have the right to use a total of 80 flags. If all mines are flagged then we have won the game. Each time a flag is used, the game will check that all flags match all mines.

        public bool CheckMines()
        {
            foreach (var box in boxes)
            {
                if(box.Value.isMine && !box.Value.isMarked)
                {
                    return false;
                }
            }
            return true;
        }
Let's use it in update method:
                    // set flag
                    if(RightClick && box.Value.rect.Contains((int)mouseCoords.X, (int)mouseCoords.Y))
                    {
                        RightClick = false;

                        if(!box.Value.isOpened)
                        {
                            box.Value.type = 11;
                            box.Value.uiBox.TextureRect = new IntRect(box.Value.type * 10,0,10,10);
                        }

                        if(NotFirst && CheckMines())
                        {
                            Console.WriteLine("You won!");
                            EnabledClick = false;
                        }
                    }
                }
            }
        }
Well, it works well. But there are some problems:
  • The amount of flags must be limited, otherwise the game can be won by placing a flag in each box.
  • If we set the flag by mistake, we cannot remove it again. We need to fix this.
The flag must be equal to the number of mines in the game. There will be 80 flags in total.But there may not be 80 mines in the game, there will usually be less than 80 mines(max 80).

Since I was writing the program at the same time as I was writing the article, it was a little annoying to discover some problems.

        public void Update(Vector2i mouseCoords)
        {
            if(EnabledClick)
            {
                foreach (var box in boxes)
                {
                    if(LeftClick && box.Value.rect.Contains((int)mouseCoords.X, (int)mouseCoords.Y) && !box.Value.isOpened)
                    {
                        LeftClick = false;

                        // change view of the clicked box
                        box.Value.isOpened = true;

                        // if it is mine then show game over text on the screen.
                        if(box.Value.isMine)
                        {
                            ShowAllMines();
                            Console.WriteLine("Game over!");
                            box.Value.type = 10;
                            EnabledClick = false;
                        }

                        if(!NotFirst)
                        {
                            GenerateMines();
                            CalculateNumbers();
                            NotFirst = true;
                        }
                        box.Value.uiBox.TextureRect = new IntRect(box.Value.type * 10,0,10,10);
                        
                        if(box.Value.type == 9)
                        {
                            showOthers(new Vector2i((int)box.Value.position.X/16, (int)box.Value.position.Y/16));
                        }
                    }

                    // set flag
                    if(RightClick && box.Value.rect.Contains((int)mouseCoords.X, (int)mouseCoords.Y) && flagCounter < LIMIT_FLAGS)
                    {
                        RightClick = false;

                        if(!box.Value.isMarked && !box.Value.isOpened)
                        {
                            box.Value.isMarked = true;
                            box.Value.uiBox.TextureRect = new IntRect(11 * 10,0,10,10);
                            flagCounter++;
                        }
                        else if(box.Value.isMarked && !box.Value.isOpened)
                        {
                            box.Value.isMarked = false;
                            box.Value.uiBox.TextureRect = new IntRect(0 * 10,0,10,10);
                            flagCounter--;
                        }


                        if(NotFirst && CheckMines())
                        {
                            Console.WriteLine("You won!");
                            EnabledClick = false;
                        }
                    }
                }
            }
        }
I have defined two variables. These are the LIMIT_FLAGS and flagCounter variables. You can see how they are used in the code above. Now let's move on to the finish. So I will design two banners for "You Won!" and "Game Over!". I will use Aseprite again for this. You can download this images:
 
Images for game
Images for game

I added these images in Assets folder. Let's use them. I will display these images when we win or lose the game. I will create two sprite objects and send each image as texture to these objects. Then I will position it at the required point. If the game is won or lost, it will be displayed on the screen accordingly. 
    class Game
    {
        ...
        
        Sprite gameOver;
        Sprite youWon;

        bool isWon;
        bool isLose;
After that, I updated the update method like this:
        public void Update(Vector2i mouseCoords)
        {
            if(EnabledClick)
            {
                foreach (var box in boxes)
                {
                    if(LeftClick && box.Value.rect.Contains((int)mouseCoords.X, (int)mouseCoords.Y) && !box.Value.isOpened)
                    {
                        ...

                        // if it is mine then show game over text on the screen.
                        if(box.Value.isMine)
                        {
                            ShowAllMines();
                            isLose = true;
                            box.Value.type = 10;
                            EnabledClick = false;
                        }

                        ...
                    }

                    // set flag
                    if(RightClick && box.Value.rect.Contains((int)mouseCoords.X, (int)mouseCoords.Y) && flagCounter < LIMIT_FLAGS)
                    {
                        ...


                        if(NotFirst && CheckMines())
                        {
                            isWon = true;
                            EnabledClick = false;
                        }
                    }
                }
            }
        }
And finally, in the draw method:
        public void Draw(RenderTarget window)
        {
            renderTexture.Clear();

            ...

            if(isLose)
            {
                renderTexture.Draw(gameOver);
            }
            if(isWon)
            {
                renderTexture.Draw(youWon);
            }

            renderTexture.Display();
            
            window.Draw(board);
        }
    }
Let's run it again:
Final Version of minesweeper

Finally 💨.

No comments:

Post a Comment