I am LOVING all things Blazor! You can probably tell, from my previous posts about it. The ability to write C# and have it run in the browser is amazing, particularly for those of us who don't like JavaScript very much.

With the recent release of Blazor WebAssembly to general availability in .NET Core 3.2, I've been spending all my free time on building some new models with it. I want to share what I've learned so far, and to do that in a fun way. So, let's build a game.

Specifically, let's build the venerable computer game Minesweeper using Blazor WebAssembly.

So close, so close! Image by Brandenads, found on Wikimedia.

In this post and the next, we will build a working game of Minesweeper and implement it using Blazor WebAssembly, ASP.NET Core, and C#. Let's get going!

The Sample Code

As always, the sample code for this series can be found over on GitHub.

exceptionnotfound/BlazorGames
Contribute to exceptionnotfound/BlazorGames development by creating an account on GitHub.

Also check out the other posts in my Modeling Practice series:

Modeling Practice - Exception Not Found
Stories about modeling real-world problems, such as games like Minesweeper and Battleship, with code.

Modeling the Game

NOTE: I previously modeled a Minesweeper solver as a C# project, and much of the C# code in these posts is modified from that one.

Solving Minesweeper with C# and LINQ
Anybody who’s spent any time at a Windows machine in the last 26 years has probably played a few games of Minesweeper [https://en.wikipedia.org/wiki/Minesweeper_(video_game)]: I mostly work in the ASP.NET space, and I’d been wondering for a few weeks how feasible it was to build a program that co…

Let's take a look at a sample board of Minesweeper.

There are a couple of components we need to consider when modeling this game, and it all starts with the small pieces in the board that the user clicks on. We're going to call those pieces "Panels", because that's what they look like.

Each panel can be either unrevealed (the starting state) or revealed, and once revealed, each panel has either a mine, a number that represents the count of adjacent mines, or a blank space. Further, an unrevealed panel can be "flagged" so that the user cannot accidentally click on it; this is done to mark where the mines likely are.

Let's create a C# class that has all of these attributes:

public class Panel
{
    public int ID { get; set; }
    
    //Horizontal position
    public int X { get; set; }
    
    //Vertical position
    public int Y { get; set; }
    
    public bool IsMine { get; set; }
    
    public int AdjacentMines { get; set; }
    
    public bool IsRevealed { get; set; }
    
    public bool IsFlagged { get; set; }

    public Panel(int id, int x, int y)
    {
        ID = id;
        X = x;
        Y = y;
    }
}
For easy access later, we also added an ID field.

Each panel has two possible actions that can be performed against it: they can be revealed (when clicked), or flagged (when right-clicked).

public class Panel
{
    //... Properties

    public void Flag()
    {
        if(!IsRevealed)
        {
            IsFlagged = !IsFlagged;
        }
    }

    public void Reveal()
    {
        IsRevealed = true;
        IsFlagged = false; //Revealed panels cannot be flagged
    }
}

As far as the panels are concerned, this is the extent of their functionality. The real work comes next, as we begin to design the game board itself.

The Board

A woman gazes through blinds and out a window, with a bored expression.
Wrong spelling Photo by Joshua Rawson-Harris / Unsplash

A game of minesweeper takes place on a board of X width and Y height, where those numbers are defined by the user. Each board has Z mines hidden in its panels. For every panel that is not a mine, a number is placed showing the count of mines adjacent to that panel.  We therefore need our GameBoard class to keep track of its own dimensions, mine count, and collection of panels.

public class GameBoard
{
    public int Width { get; set; } = 16;
    public int Height { get; set; } = 16;
    public int MineCount { get; set; } = 40;
    public List<Panel> Panels { get; set; }
}
Note that these settings are for an "intermediate" game of Minesweeper.

We will be greatly expanding this class as we develop our game.

Game Status

Because the appearance of the Minesweeper board is determined by the state of the game, we can use an enumeration to track said state.

public enum GameStatus
{
    AwaitingFirstMove,
    InProgress,
    Failed,
    Completed
}

We must also allow the board to track the game state:

public class GameBoard
{
    //...Other properties
    
    public GameStatus Status { get; set; }
}

Now let's build out the functionality of this board.

Initializing the Game

Every game of Minesweeper begins in the same state, with all panels hidden. Let's create a method to initialize a game, including creating the collection of panels. Given the height, width, and mine count as parameters, here's what our initialization method would look like:

public class GameBoard
{
    //...Properties
    
    public void Initialize(int width, int height, int mines)
    {
        Width = width;
        Height = height;
        MineCount = mines;
        Panels = new List<Panel>();

        int id = 1;
        for (int i = 1; i <= height; i++)
        {
            for (int j = 1; j <= width; j++)
            {
                Panels.Add(new Panel(id, j, i));
                id++;
            }
        }

        Status = GameStatus.AwaitingFirstMove;
    }
}

You might be wondering: why didn't we place the mines and calculate the adjacent numbers? That's because of a trick in most implementations of Minesweeper: they don't calculate where the mines are placed until after the user clicks on their first panel. This is so that the user doesn't click on a mine on the first move, because that's no fun.

For later usage, let's also implement a Reset method, which will reset the board to a new game using the same width, height, and mine count:

public class GameBoard
{
    //...Other Properties and Methods
    
    public void Reset()
    {
        Initialize(Width, Height, MineCount);
    }
}

Now we can work on the next major step of this implementation: the first move.

The First Move and Getting Neighbors

Two hands, one wearing a wedding ring, are clasped together.
No, not that kind of first move. Algorithms do not help with this. Photo by Paul García / Unsplash

We now need a method which represents the user's first move, and therefore determines where the mines go and calculates the adjacent mines numbers. That algorithm looks something like this:

  1. When the user makes the first move, take that panel and a certain number of neighbor panels, and mark them as unavailable for mines.
  2. For every other panel, place the mines randomly.
  3. For every panel which is not a mine (including the panels from Step 1), calculate the adjacent mines.
  4. Mark the game as started

The trickiest part of this algorithm is calculating the neighbors of a given panel. Remember that the neighbors of any given panel are the first panel above, below, to the left, to the right, and on each diagonal.

C for clicked, N for neighbor.

For our implementation, we have an entire method to do this:

public class GameBoard
{
    //...Other Properties and Methods

    public List<Panel> GetNeighbors(int x, int y)
    {
        var nearbyPanels = Panels.Where(panel => panel.X >= (x - 1)
                                                 && panel.X <= (x + 1)
                                                 && panel.Y >= (y - 1)
                                                 && panel.Y <= (y + 1));
                                                 
        var currentPanel = Panels.Where(panel => panel.X == x 
                                                 && panel.Y == y);
                                                 
        return nearbyPanels.Except(currentPanel).ToList();
    }
}

Using our "first move" algorithm, the new GetNeighbors() method, and some fancy LINQ, we end up with this method to implement the first move:

public class GameBoard
{
    //...Other Properties and Methods
    
    public void FirstMove(int x, int y)
    {
        Random rand = new Random();

        //For any board, take the user's first revealed panel 
        // and any neighbors of that panel, and mark them 
        // as unavailable for mine placement.
        var neighbors = GetNeighbors(x, y); //Get all neighbors
        
        //Add the clicked panel to the "unavailable for mines" group.
        neighbors.Add(Panels.First(z => z.X == x && z.Y == y));

        //Select all panels from set which are available for mine placement.
        //Order them randomly.
        var mineList = Panels.Except(neighbors)
                             .OrderBy(user => rand.Next());
                             
        //Select the first Z random panels.
        var mineSlots = mineList.Take(MineCount)
                                .ToList()
                                .Select(z => new { z.X, z.Y });

        //Place the mines in the randomly selected panels.
        foreach (var mineCoord in mineSlots)
        {
            Panels.Single(panel => panel.X == mineCoord.X 
                           && panel.Y == mineCoord.Y)
                  .IsMine = true;
        }

        //For every panel which is not a mine, 
        // including the unavailable ones from earlier,
        // determine and save the adjacent mines.
        foreach (var openPanel in Panels.Where(panel => !panel.IsMine))
        {
            var nearbyPanels = GetNeighbors(openPanel.X, openPanel.Y);
            openPanel.AdjacentMines = nearbyPanels.Count(z => z.IsMine);
        }

        //Mark the game as started.
        Status = GameStatus.InProgress;
    }
}

There's one last thing we need to finish the first move implementation: a method which activates when a user makes a move, and if it is the first move, calls our FirstMove() method. Here's that method:

public class GameBoard
{
    //...Other Properties and Methods
    
    public void MakeMove(int x, int y)
    {
        if (Status == GameStatus.AwaitingFirstMove)
        {
            FirstMove(x, y);
        }
        RevealPanel(x, y);
    }
}

All right! First move implementation is complete, and we can now move on to what happens on every other panel.

Revealing Panels

For every move except the first one, clicking on a panel reveals that panel. A more specific algorithm for moves after the first one might be this:

  1. When the user left-clicks on a panel, reveal that panel.
  2. If that panel is a mine, show all mines and end the game.
  3. If that panel is a zero, show that panel and all neighbors. If any neighbors are also zero, show all their neighbors, and continue until all adjacent zeroes and their neighbors are revealed. (I termed this a "cascade reveal").
  4. If that panel is NOT a mine, check to see if any remaining unrevealed panels are not mines. If there are any, the game continues.
  5. If all remaining unrevealed panels are mines, the game is complete. This is the "win" condition.

Using that algorithm, we end up with this method:

public class GameBoard
{
    //...Other Properties and Methods
    
    public void RevealPanel(int x, int y)
    {
        //Step 1: Find and reveal the clicked panel
        var selectedPanel = Panels.First(panel => panel.X == x 
                                                  && panel.Y == y);
        selectedPanel.Reveal();

        //Step 2: If the panel is a mine, show all mines. Game over!
        if (selectedPanel.IsMine)
        {
            Status = GameStatus.Failed;
            RevealAllMines();
            return;
        }

        //Step 3: If the panel is a zero, cascade reveal neighbors.
        if (selectedPanel.AdjacentMines == 0)
        {
            RevealZeros(x, y);
        }

        //Step 4: If this move caused the game to be complete, mark it as such
        CompletionCheck();
    }
}

We now have several methods to create to finish this part of our implementation.

Revealing All Mines

A deep pit mine, with yellow mining equipment scattered about.
Found one! Photo by Dominik Vanyi / Unsplash

The simplest of the methods we need is this one: showing all mines on the board.

public class GameBoard
{
    //...Other Methods and Properties

    private void RevealAllMines()
    {
        Panels.Where(x => x.IsMine)
        	  .ToList()
              .ForEach(x => x.IsRevealed = true);
    }
}

Cascade Reveal Neighbors

When a zero is clicked, the game needs to reveal all of that panel's neighbors, and if any of the neighbors are zero, reveal the neighbors of that panel as well. This calls for a recursive method:

public class GameBoard
{
    //...Other Methods and Properties
    
    public void RevealZeros(int x, int y)
    {
        //Get all neighbor panels
        var neighborPanels = GetNeighbors(x, y)
                               .Where(panel => !panel.IsRevealed);
                               
        foreach (var neighbor in neighborPanels)
        {
            //For each neighbor panel, reveal that panel.
            neighbor.IsRevealed = true;
            
            //If the neighbor is also a 0, reveal all of its neighbors too.
            if (neighbor.AdjacentMines == 0)
            {
                RevealZeros(neighbor.X, neighbor.Y);
            }
        }
    }
}
We could do this in LINQ as well, but I like the readability of this more.

Is The Game Complete?

A game of Minesweeper is complete when all remaining unrevealed panels are mines. We can use this method to check for that:

public class GameBoard
{
    //...Other Properties and Methods
    
    private void CompletionCheck()
    {
        var hiddenPanels = Panels.Where(x => !x.IsRevealed)
                                 .Select(x => x.ID);
                                 
        var minePanels = Panels.Where(x => x.IsMine)
                               .Select(x => x.ID);
                               
        if (!hiddenPanels.Except(minePanels).Any())
        {
            Status = GameStatus.Completed;
            Stopwatch.Stop();
        }
    }
}

With this functionality in place, there's only one thing left to do: flagging.

Flagging a Panel

"Flagging" a panel means marking it as the location of a mine. Doing this means that left-clicking that panel will do nothing.

I mean, that is one way to get all the mines...

In our game, we will use right-click to flag a panel.

Recall that our Panel class already has a Flag() method. Now we just need to call that method from the GameBoard:

public class GameBoard
{
    //...Other Properties and Methods
    
    public void FlagPanel(int x, int y)
    {
        var panel = Panels.Where(z => z.X == x && z.Y == y).First();

        panel.Flag();
    }
}

Just a little bit left to do.

Implementing a Timer

Take a look at the top part of the Minesweeper game we saw earlier:

Other than the smiley face's piercing stare, what do you notice? The counters!

The left counter is for mines; it goes down as the number of flagged panels increases. The right counter is for the time; it ticks up every second. We need to implement a timer for our Minesweeper game.

For this, we will use the C# Stopwatch class. We will use the following algorithm:

  1. Whenever the game is created or reset, we reset the timer,
  2. When the player makes their first move, start the timer.
  3. When the game is either complete or failed, stop the timer.

The changes we need to make to our GameBoard class look like this:

public class GameBoard
{
    //...Other Properties
    public Stopwatch Stopwatch { get; set; }
    
    //...Other Methods
    
    public void Reset()
    {
        Initialize(Width, Height, MineCount);
        Stopwatch = new Stopwatch();
    }
    
    public void FirstMove()
    {
        //...Implementation
        Stopwatch.Start();
    }
    
    private void CompletionCheck()
    {
        var hiddenPanels = Panels.Where(x => !x.IsRevealed)
                                 .Select(x => x.ID);
                                 
        var minePanels = Panels.Where(x => x.IsMine)
                               .Select(x => x.ID);
                               
        if (!hiddenPanels.Except(minePanels).Any())
        {
            Status = GameStatus.Completed;
            Stopwatch.Stop(); //New line
        }
    }
}

Ta-da! With the timer implemented, all of our implementation for the Minesweeper game is complete!

Summary

To model a game of Minesweeper, we needed a Panel class and a GameBoard class. The panels can be revealed or flagged, and the board tracks everything else, including the dimensions, mine count, and current game status.

Mines are not placed on the board until after the user clicks their first panel, at which point they are spread randomly around.

When a panel is clicked, if it's a mine, it's game over. If it's a zero, we reveal all neighbors, and if any neighbors are also zero, we reveal their neighbors. If it's a number, the game continues.

If, after revealing a panel, all remaining unrevealed panels are mines, the player has won the game!

Feeling good, like I should, went and took a walk around the neighborhood...

Thanks for Reading!

Thank you, dear readers, for reading this post. Watch this space on Thursday for Part 2 of this series, where we finish our implementation by creating a Blazor WebAssembly component using this code, as well as a special announcement.

Don't forget to check out the sample project over on GitHub! And if you see a way to improve this implementation, don't hesitate to submit a pull request.

Happy Coding!


Did you enjoy this post? Then you'll love my premium newsletter The Catch Block! Issues come out every Wednesday, and they have the best links to quality stories from all around the ASP.NET and web programming worlds. Plus, original stories, tips, tutorials and sample code you won't find anywhere else. Even better, it's only $5/month, or $55/year! Check out the previous issues, and then sign up to get The Catch Block today!