Daniel Daniel - 1 month ago 11
C# Question

C# MineSweeper project: Any simplier way to show adjacent mines?

I made a minesweeper WPF project as a practice to study object oriented programming. However, my method for showing how many mines the buttons "see" is kinda long and messy. The current grid is 6x6 and as you can see, it is a long list of lines. The main problem is if I want to extend the grid, for example to 100x100, It would require way more lines.

I was wondering if there would be any workarounds/ways to make it cleaner or/and shorter by any chance?

This is the method for showing how many mines does a button see:

private int MineInfo(int index)
//shows how many mines one button can "see".
{
// n = mines
int n = 0;

// Edges:
if (index == 0)
{
if (mines.Contains(index + 1))
{
n++;
}
if (mines.Contains(index + 6))
{
n++;
}
if (mines.Contains(index + 7))
{
n++;
}
}
if (index == 5)
{
if (mines.Contains(index - 1))
{
n++;
}
if (mines.Contains(index + 6))
{
n++;
}
if (mines.Contains(index + 5))
{
n++;
}
}
if (index == 30)
{
if (mines.Contains(index + 1))
{
n++;
}
if (mines.Contains(index - 6))
{
n++;
}
if (mines.Contains(index - 5))
{
n++;
}
}
if (index == 35)
{
if (mines.Contains(index - 1))
{
n++;
}
if (mines.Contains(index - 6))
{
n++;
}
if (mines.Contains(index - 7))
{
n++;
}
}

// Top Row
if (index > 0 && index < 5)
{
if (mines.Contains(index - 1))
{
n++;
}
if (mines.Contains(index + 1))
{
n++;
}
if (mines.Contains(index + 6))
{
n++;
}
if (mines.Contains(index + 5))
{
n++;
}
if (mines.Contains(index + 7))
{
n++;
}
}

// Bottom row
if (index > 30 && index < 35)
{
if (mines.Contains(index - 1))
{
n++;
}
if (mines.Contains(index + 1))
{
n++;
}
if (mines.Contains(index - 6))
{
n++;
}
if (mines.Contains(index - 5))
{
n++;
}
if (mines.Contains(index - 7))
{
n++;
}
}

// left side
if ((index == 6) || (index == 12) || (index == 18) || (index == 24))
{
if (mines.Contains(index - 6))
{
n++;
}
if (mines.Contains(index + 6))
{
n++;
}
if (mines.Contains(index + 1))
{
n++;
}
if (mines.Contains(index - 5))
{
n++;
}
if (mines.Contains(index + 7))
{
n++;
}
}

// Right side
if ((index == 11) || (index == 17) || (index == 23) || (index == 29))
{
if (mines.Contains(index - 6))
{
n++;
}
if (mines.Contains(index + 6))
{
n++;
}
if (mines.Contains(index - 1))
{
n++;
}
if (mines.Contains(index - 7))
{
n++;
}
if (mines.Contains(index + 5))
{
n++;
}
}


// Middle buttons

if ((index > 6 && index < 11) || (index > 12 && index < 17) || (index > 18 && index < 23) || (index > 24 && index < 29))
{
if (mines.Contains(index - 1))
{
n++;
}
if (mines.Contains(index + 1))
{
n++;
}
if (mines.Contains(index - 6))
{
n++;
}
if (mines.Contains(index + 6))
{
n++;
}
if (mines.Contains(index - 7))
{
n++;
}
if (mines.Contains(index + 7))
{
n++;
}
if (mines.Contains(index - 5)) // Right top
{
n++;
}
if (mines.Contains(index + 5)) // right bottom
{
n++;
}
}

return n;
}

Answer

This is what looping is for, a feature found in all programming languages, OOP and otherwise.

Whenever you have a problem that you can express along the lines of "I have an algorithm that works similarly over N cases", you're usually talking about a loop. In this example, you have a couple of generalizations:

  1. Each adjacent cell is treated identically. I.e. if its address is found in your mines collection, you increment your total by one.
  2. You have the same pattern of adjacent cells to examine, regardless of the specific index you start with.

The one wrinkle is that the above works great when you're not at the edge, but when you are you'll need to ignore some of the places you'd normally find an adjacent cell.

Part of what makes your scenario a bit harder to immediately see a solution for is that you've chosen to address your cells as a single-dimensional index, even though the actual game board is two-dimensional. As a general rule, it's best to keep your data structure matching as closely as possible the thing that data structure is intended to model. The code will be easier to write, and especially it will make it a lot easier to gain insights as to how to solve specific problems, because you can think of them in terms of the original problem (e.g. here, the two-dimensional search) instead of having to mentally map between your data structure and the original problem.

With time and practice, you can get better at doing that sort of mental mapping, but even for a very experienced programmer, it's better to avoid that. For the inexperienced programmer, this is a great time to focus on always making sure your data structures are as close to the original problem as you can make them.

Below is a solution that keeps with your original code. However, it still internalizes an abstraction to convert between your single-dimensional data structure and the original two-dimensional problem space. This introduces a slight inefficiency in the code, but that's a very small cost compared to making the algorithm easier to write and understand:

int MineInfo(int index, int rows, int columns)
{
    int centerRow = index / rows, centerColumn = index % columns,
        result = 0;

    for (int i = -1; i <= 1; i++)
        for (int j = -1; j <= 1; j++)
        {
            // Ignore the center cell
            if (i == 0 && j == 0)
            {
                continue;
            }

            int checkRow = centerRow + i, checkColumn = centerColumn + j;

            // Ignore cells not within the game board
            if (checkRow < 0 || checkRow >= rows ||
                checkColumn < 0 || checkColumn >= columns)
            {
                continue;
            }

            int checkIndex = checkRow * columns + checkColumn;

            if (mines.Contains(checkIndex))
            {
                result++;
            }
        }

    return result;
}

The above encapsulates all of the logic into a single method. But even in situations where there's a compelling reason to store your data in a data structure that doesn't match the original problem space, it's useful to abstract that difference in some helper methods or even a wrapper class. I.e. the conversion between the single-dimensional index and the two-dimensional coordinates of the game board can be implemented in methods that the MineInfo() method and similar areas of your code can use.

I leave that exercise for the reader. :)

Of course, there are other ways to solve this problem. If I could change anything, the first thing I'd do is stop using a single-dimensional data structure to store the data. Instead of having a collection of indexes, make a two-dimensional array that tracks the state of a cell, including whether a mine is present, whether the cell has been exposed, flagged, or whatever. Then the implementation becomes much simpler.

Going the harder way, it is of course also possible to do all of the above strictly in the single-dimensional space. But dealing with the edge cases becomes a lot more complicated, and a lot harder to read and write in code. IMHO, it's just not worth it. :)

Comments