Building Minesweeper - Showing Why Logic Should Not Be in the UI

Hello and welcome! This post is part of the 2022 .NET Advent Calendar series of posts, but you can enjoy the content without worrying about that!

For this event, I thought I’d build a program that can show why you don’t want to have any logic in your controllers, pages, views, forms, etc. in your .NET application. I also figured we’d make it a fun little game.

Picking an Application Front-End

Now you might be wondering which choice I made for the UI of the application, since I mentioned a few types of front-ends and there are quite a few to choose from in each of those I mentioned above.

I could have chosen: WPF, UWP, WinForms, or even a Console application. This isn’t even considering the 3rd party options and variations on the first party ones!

As the title may have given the game away already, I’m not going to mention which UI we’re using yet. Why? I don’t need to! We can build the game and decide the UI later!

You’re likely also wondering which game I chose to make? I decided to make a Minesweeper-style game as I don’t even have a copy of it on my Windows PC anymore! A shame!

Shameless self-promoting side note: I did a coding live stream with Guy Royse (after writing this game) on my DevChatter programming channel on Twitch where we created a simple, static HTML and JavaScript version of Minesweeper from scratch. You can watch the recording of us Coding Minesweeper in Static HTML with JavaScript on YouTube.

What is Minesweeper?

If you aren’t lucky enough to have played minesweeper when it was one of the few included games on Windows, I’ll explain the basic rules of the game.

When it loads, you have a grid of blank squares and an indicator of how many bombs are unmarked on the board.

When you left click a square, it will reveal that square (and possibly others).

  • If the square is a bomb, you lose.
  • If the square is adjacent to a bomb, it will display a number indicating how many of the 8 squares surrounding it contain bombs (1-8).
  • Otherwise, the square is blank, and the game will automatically reveal all contiguous blank spaces and the numbered spaces next to them.

When you right click a square, it will mark that space with a flag. This is mostly to remind you that you think a bomb is there. You can remove it by right clicking it again. These are also not required to use in order to win the game.

To win the game, you need only reveal the spaces that are not bombs, and the game will automatically mark the remaining spaces as bombs.

Initial Game Object

One of basic rules with dotnet is to create types when I want them and to get names that are “good enough for now”. Refactoring is the name of the game, because we have tools like Visual Studio, Rider, etc. that are quite good at handling renames.

Following that logic, I created a class to handle interactions with the game called… Game. I gave that game class a field containing a multidimensional array of integers positives are a bomb hint, 0 is no adjacent bombs, and negatives indicate a bomb is in the space.

Why did I start so simple? Easy, I may not have needed a Cell or a Grid class to implement a basic version of the game! I recommend people always stat simple like this, as it’s easy to add complexity when it’s needed, but harder to remove complexity.

My initial game looked a bit like this:

public class Game
{
    private readonly int _size;
    public readonly int[,] Grid { get; }

    public void Start(int size)
    {
        Grid = new int[size, size];
        FillWithBombs(0.1);
        SetHintValues(size);
    }

    private void FillWithBombs(double bombPercent)
    {
        // Get bombs as a percent of all spaces
        int bombCount = (int)(Grid.Length * bombPercent);
        // Get an array of indexes as if the grid were one array.
        var possibles = Enumerable.Range(0, Grid.Length).ToArray();
        // Pick a random selection of those indexes to receive bombs.
        var locations = possibles.OrderBy(x => _random.Next()).Take(bombCount);

        foreach (var index in locations)
        {
            Grid[index % _size, index / _size] = _random.Next(-5, 0);
        }
    }

    public bool IsBomb(int rowIndex, int colIndex)
    {
        return Grid[rowIndex, colIndex] < 0;
    }

    private void SetHintValues(int size)
    {
        for (int rowIndex = 0; rowIndex < size; rowIndex++)
        {
            for (int colIndex = 0; colIndex < size; colIndex++)
            {
                if (IsBomb(rowIndex, colIndex))
                {
                    SafeIncrement(rowIndex-1, colIndex);
                    SafeIncrement(rowIndex+1, colIndex);
                    SafeIncrement(rowIndex, colIndex-1);
                    SafeIncrement(rowIndex, colIndex+1);

                    SafeIncrement(rowIndex-1, colIndex-1);
                    SafeIncrement(rowIndex+1, colIndex+1);
                    SafeIncrement(rowIndex+1, colIndex-1);
                    SafeIncrement(rowIndex-1, colIndex+1);
                }
            }
        }
    }

    private void SafeIncrement(int rowIndex, int colIndex)
    {
        if (rowIndex < 0
            || colIndex < 0
            || rowIndex >= _size
            || colIndex >= _size)
        {
            return;
        }

        Grid[rowIndex, colIndex]++;
    }
}

Notice that I’m able to create the grid with some basic values without too much trouble. I randomly placed some bombs and mark the hint values. Efficient? Fancy? Nope. Nope. But it works!

With this little bit of code, I was able to get a basic test that the board could get created. What was my initial UI? Console Application. Why? I could easily print out the contents of that array to see if the board looked like I expected. Yeah, the test confirmed that it created positive and negative numbers in a 2 dimensional array, but that’s not much gameplay tested yet. Next we’ll need to be able to hide and reveal spaces on the board.

Hiding and Revealing Spaces

Before we can play this game, we’ll need to hide the spaces, so we can reveal them when the player picks them later. Displaying everything was great for confirming that the application was creating the grid as we expected it.

Since we need to keep track of whether a space has been revealed or not, we either need two grids of data, one with the status and one for the value, or we could upgrade our grid to have an object that knows its value and the revealed state.

To start with, I created a Cell class and gave it properties for the Count of neighboring bombs and a Revealed boolean value to know when it should be displayed.

public class Cell
{
    public int Count { get; set; } = 0;
    public bool Revealed { get; set; } = false;

    public static Cell operator ++(Cell x)
    {
        x.Count += 1;
        return x;
    }
}

Now in order to make it an easier refactoring, I can add some implicit operator methods to the type, so our number operations on it will modify the Count property. That looks like this:

public class Cell
{
    public int Count { get; set; } = 0;
    public bool Revealed { get; set; } = false;

    public static Cell operator ++(Cell x)
    {
        x.Count += 1;
        return x;
    }

    public static implicit operator int(Cell x) => x.Count;
    public static implicit operator Cell(int number) => new() { Count = number };
}

The code where I did this will work on the cells the same way it did when these were numbers:

private void SafeIncrement(int rowIndex, int colIndex)
{
    if (rowIndex < 0
        || colIndex < 0
        || rowIndex >= _size
        || colIndex >= _size)
    {
        return;
    }

    Grid[rowIndex, colIndex]++;  // Works for int or Cell now!
}

Handling Game Over

Minesweeper wouldn’t be much fun if we don’t lose by clicking a bomb, so let’s make sure that this causes an end game. To solve this, we have a few options. We could send a message then reset the game, or we could wait until the user starts a new game and change our “state” to be “Game Over”.

I like the idea of remaining in the “game over” state, so that the player can see the grid and their mistake that lost the game. That means we need to store that somewhere. We could use booleans for things like IsStarted, IsWon, IsLost, etc. to know the state. I think we’re only ever going to be in one state at a time, so an enum for this might be the simpler solution. Let’s create a GameState enum to handle this.

public enum GameState
{
    NotStarted,
    Started,
    Won,
    Lost
}

Now we can adjust the reveal method to trigger a Lost state if we reveal a bomb while we’re in the Started state. We can also restrict the player selecting to reveal a space, so that it only happens if we’re in the Started state.

public void Reveal(int rowIndex, int columnIndex)
{
    if (State != GameState.Started) return;

    Cell? cell = Grid.SafeGet(rowIndex, columnIndex);
    if (cell == null) return;

    cell.Reveal();
    if (cell.IsBomb)
    {
        State = GameState.Lost;
    }
}

Handling Winning the Game

We can lose the game, but I think we’d all rather win. It’s time to add in the condition to allow a player to win! As we mentioned, that happens when the player has revealed every non-bomb space and not revealing any bomb spaces.

As we already created the Won value on the GameState enum, we can use it now to indicate that the player has won the game.

Thankfully, we already locked the revealing of spaces to require that it be in the Started state, which means that we won’t have to worry about accidentally clicking a bomb space after we’ve revealed all of the other spaces.

public void Reveal(int rowIndex, int columnIndex)
{
    if (State != GameState.Started) return;

    Cell? cell = Grid.SafeGet(rowIndex, columnIndex);
    if (cell == null) return;

    cell.Reveal();
    if (cell.IsBomb)
    {
        State = GameState.Lost;
    }
    else if (Grid.OnlyBombsLeft())
    {
        State = GameState.Won;
    }
}

Playing the Game without a UI

Now I’ll reveal the secret that I was able to run these tests before writing most of the code. My tests just needed to call methods on the Game object, because I could automate playing the game without a UI at all. I can do this, because I kept all of the logic out of the UI.

public class FullGameTests
{
    private readonly Game _game = new();

    [Fact]
    public void ClearAllSpacesSafely()
    {
        _game.State.Should().Be(GameState.NotStarted);
        _game.Start(4, Difficulty.Reindeer);
        _game.State.Should().Be(GameState.Started);

        ClickAllNonBombs();

        _game.State.Should().Be(GameState.Won);
    }

    [Fact]
    public void ClickBombFirst()
    {
        _game.State.Should().Be(GameState.NotStarted);
        _game.Start(4, Difficulty.Reindeer);
        _game.State.Should().Be(GameState.Started);
        ClickBomb();

        _game.State.Should().Be(GameState.Lost);
    }
}

If I’d tied code into the UI, I’d have to spin up controllers, views or other context objects, which I’d rather not do in tests. I also haven’t tied myself to MVVM, MVC, MVP, etc. either. We can easily add those as wrappers, or directly put the concept on these classes.

Optional Homework

I built only the interface for the Console Application, but I left an empty WinForms and WPF application referencing the game library. The nice part is that you can build a front-end using any of the front-end technologies in the .NET space. I’ve put the Minesweeper in DotNet with C# code on GitHub with some empty projects that you could wire up a UI for and make buttons to reveal spaces and play the game.

Outro

One of my favorite things to do while programming in any language is to try to keep the application code away from the UI, because it gives us so much power of it when it’s just in a referenced class library.

Thanks for participating in this year’s .NET Advent Calendar! I hope you enjoy the next couple of weeks of these dotnet posts from members of the developer community.

If you don’t know me, my name is Brendan Enrick, and I’m a regular speaker at conferences and user groups. I host a live coding streams and create coding videos on my DevChatter Twitch and DevChatter YouTube channels. You can also follow me as @Brendoneus on Twitter or @Brendoneus@Our.DevChatter.com on Mastodon.

Lastly, and most importantly, I want to be sure to thank the organizers and other authors of the .NET Advent Calendar for making this an awesome bit of fun for everyone!

Comments