Click here to Skip to main content
15,880,796 members
Articles / Programming Languages / C#

Minesweeper Game in C#

Rate me:
Please Sign up or sign in to vote.
2.33/5 (12 votes)
12 Jun 2021CPOL3 min read 23.2K   9   12
Simple object-oriented and clean-coded Minesweeper Game in C# Console Application
In this article, I'm going to make a simple object-oriented and clean-coded Minesweeper Game in C# console application, that creates and initializes a simple game sheet for a minesweeper board. The main job of this application will be to uncover the location of board cells, under which mines can lie. Uncovered fields will be marked with numbers that indicate how many mines are adjacent to the field.

Introduction

The simple object-oriented and clean-coded Minesweeper Game console application presented in this article will accept a text file. The sample text file for this app will look like this:

Image 1

In text file, we consider that asterisks are mines, and dots are empty cells on board.

Background

It's good if you know the basics of clean coding and object oriented programming. For a brief overview of Clean Coding, you can just type few words in Google like: "10 basic rules of clean coding".

Using the Code

We start our program by adding a new console project. You can create a new project by going to File -> New -> Project.

Image 2

For application solution structure, I'm going to separate files in folders like: Models, Constants, Services and Utilities. In order to separate my concerns and have a better app structure, I'm also going to add Unit Tests Project, in order to have tests for better refactoring and bug fixing in my app.

Image 3

Location.cs

The smallest unit in the Minesweeper game is the Locations that can be clicked, and have only two properties. I want to put it in the Models folder. Row and Column properties represent the panel's horizontal and vertical coordinates.

C#
namespace AmansMineSweeper.Model
{
    public struct Location
    {
        public int Row { get; set; }
        public int Column { get; set; }
    }
}

GridPanelConstantValues.cs

I want to define the constants to store the probability of a bomb in an adjacent cell number from 0 to 9. The number 0 (Zero) will indicate, that there is no Mine in that location. The number 9 (Nine) will indicate, that there is Mine in that location. The other numbers between Zero and Nine will show the probability of a bomb in that location.

C#
namespace AmansMineSweeper.Constants
{
    public static class GridPanelConstantValues
    {
        public const int Mine = 9;
        public const int Zero = 0;
        public const int MinimumRow = 0;
        public const int MinimumColumn = 0;
    }
}

GridPanel.cs

In our project, we will have a rich entity model called GridPanel with properties and behaviors, which will contain a collection of locations on the board. The important property of this class will be the private field named gridCells, which is two dimensional array of integers to store the estimation of mine existences, and the only way for accessing this property will be by class indexer. By adding an indexer property, we would have implemented the iterator pattern, and we could iterate over this class.

C#
using AmansMineSweeper.Constants;
namespace AmansMineSweeper.Model
{
    public class GridPanel
    {
        private readonly int[,] _gridCells;
        private readonly int _maxRows;
        private readonly int _maxColumns;

        public int MaxRows {
            get { return _maxRows-1; }
        }

        public int MaxColumns {
            get { return _maxColumns-1; }
        }

        public GridPanel(int maxRows, int maxColumns)
        {
            _maxRows = maxRows;
            _maxColumns = maxColumns;
            _gridCells = new int[maxRows, maxColumns];
        }

        public int this[Location cellLocation]
        {
            get { return _gridCells[cellLocation.Row, cellLocation.Column]; }
            set { _gridCells[cellLocation.Row, cellLocation.Column] = value; }
        }

        public void IncrementCellValue(Location location)
        {
            var currentValue = this[location];
            if (currentValue == GridPanelConstantValues.Mine) 
                return;
            this[location] = currentValue + 1;
        }
    }
}

IAdjacentCalculator.cs

Our next important class will be AdjacentCalculator. The only job of this class will be calculating the probability of a bomb in an adjacent cells of the game board. As you see in below interface definition, the class will have eight methods to calculate the count of mines around the cell.

C#
using AmansMineSweeper.Model;
namespace AmansMineSweeper.Service
{
    public interface IAdjacentCalculator
    {
        void CalculateBelowAdjacent(Location mineLocation);
        void CalculateBelowLeftAdjacent(Location mineLocation);
        void CalculateBelowRightAdjacent(Location mineLocation);
        void CalculateLeftAdjacent(Location mineLocation);
        void CalculateRightAdjacent(Location mineLocation);
        void CalculateUpperAdjacent(Location mineLocation);
        void CalculateUpperLeftAdjacent(Location mineLocation);
        void CalculateUpperRightAdjacent(Location mineLocation);
    }
}

AdjacentCalculator.cs

The AdjacentCalculator class implements IAdjacentCalculator interface, and accepts GridPanel in its constructor.

C#
using AmansMineSweeper.Constants;
using AmansMineSweeper.Model;
namespace AmansMineSweeper.Service
{
    public class AdjacentCalculator : IAdjacentCalculator
    {
        private readonly GridPanel _gridPanel;
        public AdjacentCalculator(GridPanel gridPanel)
        {
            _gridPanel = gridPanel;
        }

        public void CalculateBelowAdjacent(Location location)
        {
            if (location.Row >= _gridPanel.MaxRows)
                return;

            var adjacentLocation = new Location
            {
                Row = GetRowDownIndex(location.Row),
                Column = location.Column
            };

            _gridPanel.IncrementCellValue(adjacentLocation);
        }

        public void CalculateBelowLeftAdjacent(Location location)
        {
            if (location.Row < _gridPanel.MaxRows && 
                location.Column > GridPanelConstantValues.MinimumColumn)
            {
                _gridPanel.IncrementCellValue(new Location
                {
                    Row = GetRowDownIndex(location.Row),
                    Column = GetColumnLeftIndex(location.Column)
                });
            }
        }

        public void CalculateBelowRightAdjacent(Location location)
        {
            if (location.Row < _gridPanel.MaxRows && location.Column < _gridPanel.MaxColumns)
            {
                _gridPanel.IncrementCellValue(new Location
                {
                    Row = GetRowDownIndex(location.Row),
                    Column = GetColumnRightIndex(location.Column)
                });
            }
        }

        public void CalculateLeftAdjacent(Location location)
        {
            if (location.Column <= GridPanelConstantValues.MinimumColumn)
                return;

            _gridPanel.IncrementCellValue(new Location
            {
                Row = location.Row,
                Column = GetColumnLeftIndex(location.Column)
            });
        }
 
        public void CalculateRightAdjacent(Location location)
        {
            if (location.Column >= _gridPanel.MaxColumns)
                return;

            var adjacentLocation = new Location
            {
                Row = location.Row,
                Column = GetColumnRightIndex(location.Column)
            };

            _gridPanel.IncrementCellValue(adjacentLocation);
        }

        public void CalculateUpperAdjacent(Location location)
        {
            if (location.Row <= GridPanelConstantValues.MinimumRow)
                return;

            var adjacentLocation = new Location
            {
                Row = GetRowUpIndex(location.Row),
                Column = location.Column
            };

            _gridPanel.IncrementCellValue(adjacentLocation);
        }

        public void CalculateUpperLeftAdjacent(Location location)
        {
            if (location.Row > GridPanelConstantValues.MinimumRow && 
                location.Column > GridPanelConstantValues.MinimumColumn)
            {
                _gridPanel.IncrementCellValue(new Location
                {
                    Row = GetRowUpIndex(location.Row),
                    Column = GetColumnLeftIndex(location.Column)
                });
            }
        }

        public void CalculateUpperRightAdjacent(Location location)
        {
            if (location.Row > GridPanelConstantValues.MinimumRow && 
                location.Column < _gridPanel.MaxColumns)
            {
                _gridPanel.IncrementCellValue(new Location
                {
                    Row = GetRowUpIndex(location.Row),
                    Column = GetColumnRightIndex(location.Column)
                });
            }
        }

        private static int GetRowUpIndex(int row)
        {
            return --row;
        }

        private static int GetRowDownIndex(int row)
        {
            return ++row;
        }

        private static int GetColumnRightIndex(int column)
        {
            return ++column;
        }

        private static int GetColumnLeftIndex(int column)
        {
            return --column;
        }
    }
}

Now I think that's enough, and it's time to write a few unit tests, to test and debug AdjacentCalculator methods, in isolation.

  1. Right click on solution Explorer, and choose Add --> New project.

    Image 4

  2. Select the template project that you want to use, for this app, you could select MSTest Test or Unit Test Project, and then choose Next.
  3. Enter a name for your project, and choose Create.

    Image 5

  4. In unit test project, add a reference to the AmansMineSweeper (code under test).

Now let's write some unit tests for AdjacentCalculator methods.

TestTool.cs

At first, we write some static class tools for the Grid Panel assertion.

C#
using AmansMineSweeper.Constants;
using AmansMineSweeper.Model;
using Microsoft.VisualStudio.TestTools.UnitTesting;
 
namespace AmansMineSweeper.UnitTest
{
    public static class TestTool
    {
        public static void AssertOutputs(GridPanel actualGrid, int[,] expectedGrid)
        {
            for (var row = GridPanelConstantValues.MinimumRow; 
                 row <= actualGrid.MaxRows; row++)
            {
                for (var column = GridPanelConstantValues.MinimumColumn; 
                     column <= actualGrid.MaxColumns; column++)
                {
                    Assert.AreEqual(expectedGrid[row, column], 
                    actualGrid[new Location() { Row = row, Column = column }]);
                }
            }
        }
 
        public static void ArrangeInputs(GridPanel actualGridPanel, int[,] array)
        {
            for (var row = 0; row <= actualGridPanel.MaxRows; row++)
            {
                for (var column = 0; column <= actualGridPanel.MaxColumns; column++)
                {
                    var location = new Location() { Row = row, Column = column};
 
                    actualGridPanel[location] = array[row, column];
                }
            }
        }
    }
}

AdjacentCalculatorTests.cs

Here, we have written some tests for Below Adjacent, Right Adjacent and Left Adjacent methods. You can also write more unit tests for every eight directions around a cell in a panel. You can also find more unit tests in the source code.

C#
using AmansMineSweeper.Constants;
using AmansMineSweeper.Model;
using AmansMineSweeper.Service;
using Microsoft.VisualStudio.TestTools.UnitTesting;

namespace AmansMineSweeper.UnitTest
{
    [TestClass]
    public class AdjacentCalculatorTests
    {
        [TestMethod]
        public void CalculateBelowAdjacent_
                    FindsTheBelowLocationAndIncrementsTheLocationValue
                    _SetsTheGridPanelToNewCorrectStatus()
        {
            // arrange
            var actualGrid = new int[,]
            {
                { GridPanelConstantValues.Mine, 0, 0 }, 
                { 0, 0, GridPanelConstantValues.Mine }
            };

            var actualGridPanel = new GridPanel(2,3);

            TestTool.ArrangeInputs(actualGridPanel, actualGrid);

            // act
            IAdjacentCalculator adjacentCalculator = new AdjacentCalculator(actualGridPanel);
            adjacentCalculator.CalculateBelowAdjacent(new Location{ Row = 0, Column = 0});

            // assert
            var expectedGrid = new int[,]
            {
                { GridPanelConstantValues.Mine, 0, 0 }, 
                { 1, 0, GridPanelConstantValues.Mine }
            };

            TestTool.AssertOutputs(actualGridPanel, expectedGrid);
        }

        [TestMethod]
        public void CalculateRightAdjacent_
        FindsTheRightLocationAndIncrementsTheLocationValue_SetsTheGridPanelToNewCorrectStatus()
        {
            // arrange
            var actualGrid = new int[,]
            {
                { GridPanelConstantValues.Mine, 0, 0 }, 
                { 0, 0, GridPanelConstantValues.Mine }
            };

            var actualGridPanel = new GridPanel(2, 3);
            TestTool.ArrangeInputs(actualGridPanel, actualGrid);

            // act
            IAdjacentCalculator adjacentCalculator = new AdjacentCalculator(actualGridPanel);
            adjacentCalculator.CalculateRightAdjacent(new Location { Row = 0, Column = 0 });

            // assert
            var expectedGrid = new int[,]
            {
                { GridPanelConstantValues.Mine, 1, 0 }, 
                { 0, 0, GridPanelConstantValues.Mine }
            };
            TestTool.AssertOutputs(actualGridPanel, expectedGrid);
        }

        [TestMethod]
        public void CalculateLeftAdjacent_
        FindsTheLeftLocationAndIncrementsTheLocationValue_SetsTheGridPanelToNewCorrectStatus()
        {
            // arrange
            var actualGrid = new int[,]
            {
                { GridPanelConstantValues.Mine, 0, 0 }, 
                { 0, 0, GridPanelConstantValues.Mine }
            };
            var actualGridPanel = new GridPanel(2, 3);
            TestTool.ArrangeInputs(actualGridPanel, actualGrid);

            // act
            IAdjacentCalculator adjacentCalculator = new AdjacentCalculator(actualGridPanel);
            adjacentCalculator.CalculateLeftAdjacent(new Location { Row = 1, Column = 2 });

            // assert
            var expectedGrid = new int[,]
            {
                { GridPanelConstantValues.Mine, 0, 0 }, 
                { 0, 1, GridPanelConstantValues.Mine }
            };

            TestTool.AssertOutputs(actualGridPanel, expectedGrid);
        }
    }
}

IMineSweeperService.cs

The MineSweeperService is the main coordinator, that calls the AdjacentCalculator class methods for calculating all cells of Grid Panel.

C#
using AmansMineSweeper.Model;
namespace AmansMineSweeper.Service
{
    public interface IMineSweeperService
    {
        void CalculateAdjacentValues(GridPanel gridPanel);
    }
}

MineSweeperService.cs

The MineSweeperService is the main coordinator, that calls the AdjacentCalculator class methods for calculating all cells of Grid Panel.

C#
using AmansMineSweeper.Constants;
using AmansMineSweeper.Model;
namespace AmansMineSweeper.Service
{
    public class MineSweeperService : IMineSweeperService
    {
        private readonly IAdjacentCalculator _adjacentCalculator;
        public MineSweeperService(IAdjacentCalculator adjacentCalculator)
        {
            _adjacentCalculator = adjacentCalculator;
        }
        
        public void CalculateAdjacentValues(GridPanel gridPanel)
        {
            for (var row = GridPanelConstantValues.MinimumRow; 
               row <= gridPanel.MaxRows; row++)
            {
                for (var column = GridPanelConstantValues.MinimumColumn; 
                    column <= gridPanel.MaxColumns; column++)
                {
                    var location = new Location { Row = row, Column = column };
                    if (gridPanel[location] == GridPanelConstantValues.Mine)
                    {
                        CalculateAdjacents(location);
                    }
                }
            }
        }
        
        private void CalculateAdjacents(Location location)
        {
            _adjacentCalculator.CalculateRightAdjacent(location);
            _adjacentCalculator.CalculateLeftAdjacent(location);
            _adjacentCalculator.CalculateUpperAdjacent(location);
            _adjacentCalculator.CalculateBelowAdjacent(location);

            //--------------------------------

            _adjacentCalculator.CalculateUpperLeftAdjacent(location);
            _adjacentCalculator.CalculateUpperRightAdjacent(location);
            _adjacentCalculator.CalculateBelowLeftAdjacent(location);
            _adjacentCalculator.CalculateBelowRightAdjacent(location);
        }
    }
}

At the end, we will have some utility classes for fetching data from external resources like text-file, and utility classes for logging output info to console.

IGridPanelMapper.cs

C#
using System.Collections.Generic;
using AmansMineSweeper.Model;
namespace AmansMineSweeper.Utilities
{
    public interface IGridPanelMapper
    {
        GridPanel MapArrayOfStringToGridPanel(string[] lines);
        List<string> MapGridPanelToArrayOfString(GridPanel gridPanel);
    }
}

GridPanelMapper.cs

C#
using System;
using System.Text;
using System.Collections.Generic;
using AmansMineSweeper.Constants;
using AmansMineSweeper.Model;

namespace AmansMineSweeper.Utilities
{
    public class GridPanelMapper : IGridPanelMapper
    {
        public GridPanel MapArrayOfStringToGridPanel(string[] lines)
        {
            var grid = new GridPanel(lines.GetLength(0), lines[0].Length);
            var row = 0;
            foreach (var line in lines)
            {
                var column = 0;
                foreach (var charachter in line)
                {
                    grid[new Location() 
                    { Row = row, Column = column }] = SymbolParser(charachter);
                    column = column + 1;
                }

                row = row + 1;
            }

            return grid;
        }
        
        public List<string> MapGridPanelToArrayOfString(GridPanel gridPanel)
        {
            var strings = new List<string>();
            for (var row = GridPanelConstantValues.MinimumRow; 
                 row <= gridPanel.MaxRows; row++)
            {
                var stringLine = new StringBuilder();
                for (var column = GridPanelConstantValues.MinimumColumn; 
                    column <= gridPanel.MaxColumns; column++)
                {
                    var symbol = SymbolParser(gridPanel[new Location() 
                                 {Row = row, Column = column}]);
                    stringLine.Append(symbol);
                }
                
                strings.Add(stringLine.ToString());

                stringLine.Clear();
            }
            return strings;
        }

        private static int SymbolParser(char symbol)
        {
            switch (symbol)
            {
                case SymbolConstants.Mine:
                    return GridPanelConstantValues.Mine;

                case SymbolConstants.Dot:
                    return GridPanelConstantValues.Zero;
            }

            return Convert.ToInt32(symbol);
        }

        private static char SymbolParser(int number)
        {
            return GridPanelConstantValues.Mine == number ? 
                   SymbolConstants.Mine : Char.Parse(number.ToString());
        }
    }
}

IGridPanelLoader.cs

C#
using AmansMineSweeper.Model;
namespace AmansMineSweeper.Utilities
{
    public interface IGridPanelLoader
    {
        GridPanel LoadGridPanel();
    }
}

GridPanelTextFileLoader.cs

C#
using System.IO;
using AmansMineSweeper.Model;
 
namespace AmansMineSweeper.Utilities
{
    public class GridPanelTextFileLoader : IGridPanelLoader
    {
        private readonly string _path;
 
        private readonly IGridPanelMapper _gridPanelMapper;
 
        public GridPanelTextFileLoader(string path, IGridPanelMapper gridPanelMapper)
        {
            _path = path;
 
            _gridPanelMapper = gridPanelMapper;
        } 
 
        public GridPanel LoadGridPanel()
        {
            var lines = File.ReadAllLines(_path);
 
            return _gridPanelMapper.MapArrayOfStringToGridPanel(lines);
        } 
    }
}

ILogger.cs

C#
using AmansMineSweeper.Model;
namespace AmansMineSweeper.Utilities
{
    public interface ILogger
    {
        void Log(GridPanel gridPanel);
    }
}

LoggerConsole.cs

C#
using System;
using AmansMineSweeper.Model;

namespace AmansMineSweeper.Utilities
{
    public class LoggerConsole : ILogger
    {
        private readonly IGridPanelMapper _gridPanelMapper;
        public LoggerConsole(IGridPanelMapper gridPanelMapper)
        {
            _gridPanelMapper = gridPanelMapper;
        }

        public void Log(GridPanel gridPanel)
        {
            var strings = _gridPanelMapper.MapGridPanelToArrayOfString(gridPanel);
            foreach (var line in strings)
            {
                Console.WriteLine(line);
            }
        }
    }
}

Program.cs

C#
using System;
using AmansMineSweeper.Model;
using AmansMineSweeper.Service;
using AmansMineSweeper.Utilities;

namespace AmansMineSweeper
{
    class Program
    {
        static void Main(string[] args)
        {
            Console.Write("Please enter the full filePath and fileName :");
            var filePath = Console.ReadLine();
            var gridPanel = GetGridPanel(filePath);
            IMineSweeperService meinSweeperService = 
                  new MineSweeperService(new AdjacentCalculator(gridPanel));
            meinSweeperService.CalculateAdjacentValues(gridPanel);
            LogGridPanel(gridPanel);
            Console.ReadKey();
        }
        
        private static void LogGridPanel(GridPanel gridPanel)
        {
            ILogger logger = new LoggerConsole(new GridPanelMapper());
            logger.Log(gridPanel);
        }

        private static GridPanel GetGridPanel(string filePath)
        {
            IGridPanelLoader gridLoder = 
             new GridPanelTextFileLoader(filePath, new GridPanelMapper());
            return gridLoder.LoadGridPanel();
        }
    }
}

Output:

Image 6

History

  • 27th January, 2021: Initial version

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Software Developer
Germany Germany
Microsoft C# .Net Developer, with more than 10 years of application design, development, and support experience.

I always try to improve my software engineering knowledge. I try to keep myself updated and learn and implement new software design approaches and technologies.

Comments and Discussions

 
GeneralMy vote of 5 Pin
Behrad Norouzieh3-Feb-21 1:11
Behrad Norouzieh3-Feb-21 1:11 
PraiseGreat Pin
Ahmad66m30-Jan-21 21:17
Ahmad66m30-Jan-21 21:17 
GeneralRe: Great Pin
Amanmohammad Toumaj3-Feb-21 0:42
Amanmohammad Toumaj3-Feb-21 0:42 
Praisereally good job Pin
mansoor_azizi29-Jan-21 22:01
mansoor_azizi29-Jan-21 22:01 
PraiseGreat job Pin
Mohamad Ansari29-Jan-21 18:35
Mohamad Ansari29-Jan-21 18:35 
GeneralMy vote of 5 Pin
Member 1505989629-Jan-21 10:28
Member 1505989629-Jan-21 10:28 
GeneralMy vote of 5 Pin
Mehdi Dev29-Jan-21 3:42
Mehdi Dev29-Jan-21 3:42 
QuestionHi amanmohammad Pin
Member 1505937428-Jan-21 23:39
Member 1505937428-Jan-21 23:39 
AnswerRe: Hi amanmohammad Pin
Amanmohammad Toumaj3-Feb-21 0:42
Amanmohammad Toumaj3-Feb-21 0:42 
GeneralMy vote of 5 Pin
Member 1505890528-Jan-21 8:58
Member 1505890528-Jan-21 8:58 
GeneralRe: My vote of 5 Pin
Amanmohammad Toumaj3-Feb-21 0:40
Amanmohammad Toumaj3-Feb-21 0:40 
GeneralRe: My vote of 5 Pin
Amanmohammad Toumaj3-Feb-21 0:42
Amanmohammad Toumaj3-Feb-21 0:42 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.