C# MineSweeper 项目:显示相邻地雷的任何更简单的方法?

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

我做了一个扫雷WPF项目作为学习面向对象编程的练习。但是,我显示按钮 "see" 有多少地雷的方法有点长而且混乱。当前网格是 6x6,如您所见,它是一长串行。主要问题是如果我想扩展网格,例如扩展到 100x100,则需要更多行。

我想知道是否有任何 workarounds/ways 可以让它更干净 or/and 更短?

这是显示一个按钮看到多少地雷的方法:

 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;
        }

这就是循环的用途,这是所有编程语言、OOP 和其他语言中的一个特性。

每当您遇到可以按照 "I have an algorithm that works similarly over N cases" 的方式表达的问题时,您通常都在谈论循环。在此示例中,您有几个概括:

  1. 每个相邻的单元格都被相同地对待。 IE。如果在您的 mines collection 中找到了它的地址,您的总数就会增加一。
  2. 无论您从哪个特定索引开始,您都需要检查相同模式的相邻单元格。

唯一的问题是,当您不在边缘时,上面的方法效果很好,但是当您在边缘时,您需要忽略一些您通常会找到相邻单元格的地方。

使您的场景难以立即找到解决方案的部分原因是您选择将单元格作为 single-dimensional 索引进行寻址,即使实际游戏板是 two-dimensional.作为一般规则,最好使您的数据结构与数据结构旨在建模的事物尽可能匹配。代码会更容易编写,尤其是它会更容易深入了解如何解决特定问题,因为您可以从原始问题的角度来思考它们(例如这里的 two-dimensional搜索),而不必在您的数据结构和原始问题之间进行心理映射。

随着时间的推移和练习,您可以更好地进行这种心理映射,但即使对于非常有经验的程序员,也最好避免这种情况。对于没有经验的程序员来说,这是专注于始终确保您的数据结构尽可能接近原始问题的好时机。

以下是与您的原始代码保持一致的解决方案。但是,它仍然内部化了一个抽象,以在您的 single-dimensional 数据结构和原始 two-dimensional 问题 space 之间进行转换。这在代码中引入了一点效率低下,但与使算法更易于编写和理解相比,这是非常小的成本:

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;
}

以上将所有逻辑封装到一个方法中。但即使在有令人信服的理由将数据存储在与原始问题不匹配的数据结构中的情况下 space,在某些辅助方法甚至包装器中抽象出这种差异也是有用的 class . IE。 single-dimensional 索引和游戏板的 two-dimensional 坐标之间的转换可以在 MineInfo() 方法和代码的类似区域可以使用的方法中实现。

我把那个练习留给 reader。 :)

当然,还有其他方法可以解决这个问题。如果我能改变什么,我要做的第一件事就是停止使用 single-dimensional 数据结构来存储数据。不用 collection 索引,而是创建一个 two-dimensional 数组来跟踪单元格的状态,包括是否存在地雷、单元格是否已暴露、标记或其他任何内容。那么实现就简单多了。

更难的是,当然也可以在 single-dimensional space 中严格执行上述所有操作。但是处理边缘情况变得更加复杂,并且在代码中更难读写。恕我直言,这不值得。 :)