如何构建一个程序以处理扫雷游戏的配置

13
编辑:这是一段时间之前的事情,我已经把它搞定了,如果你想看代码,请访问github.com/LewisGaul/minegaulerQt
我正在尝试编写一个计算扫雷游戏概率的程序,并且在确定最佳结构方面遇到了一些困难。虽然从下面的示例开始看起来很简单,但我想知道允许更复杂配置的最佳方法。请注意,我不是在寻求如何计算概率的帮助 - 我知道方法,我只需要实现它!
为了清楚地说明我要计算什么,我将通过手动完成一个简单的示例来解释。考虑一个扫雷配置
# # # # # 1 2 # # # # #
其中“#”表示未点击的单元格。“1”告诉我们,在最左边的7个未点击的单元格中恰好有1个地雷,“2”告诉我们,在最右边的7个未点击的单元格中恰好有2个地雷。为了计算每个单独单元格包含地雷的概率,我们需要确定所有不同的情况(在这种简单情况下仅有2种):
1. 最左边的3个单元格中有1个地雷,最右边的3个单元格中有2个地雷(总共3个地雷,3x3=9种组合)。 2. 中心4个单元格中有1个地雷,在最右边的3个单元格中有1个地雷(总共2个地雷,4x3=12种组合)。
鉴于在随机单元格中出现地雷的概率约为0.2,因此在随机选择的单元格中,出现2个地雷的总数要比3个地雷的总数多4倍,因此配置中的总雷数以及每个配置的组合数量都很重要。因此,在这种情况下,情况1的概率为9/(9+4x12)= 0.158,因此给定最左边的单元格中存在地雷的概率约为0.158/3 = 0.05,因为这些单元格是等效的(它们共享完全相同的已揭示邻居)。
我用Tkinter创建了一个GUI,可以轻松输入像示例中存储网格的numpy数组这样的配置。然后我创建了一个NumberGroup类,它隔离每个点击/编号的单元格,存储数字和其未点击邻居的坐标集合。这些可以相减以获得等价组...如果不是只有两个数字,而是三个或更多数字,这将不会那么简单。但是,我不确定如何从这里获得不同的配置。我尝试过创建一个Configuration类,但对不同类之间的工作方式并不十分熟悉。请参见下面的工作代码(需要numpy)。
注意:我知道我可以尝试使用蛮力方法,但如果可能的话,我想避免这样做,将等效组保持分开(在上面的示例中,有3个等效组,最左边的3个,中间的4个,最右边的3个)。我想听听你的想法。
import numpy as np

grid = np.array(
    [[0, 0, 0, 0],
     [0, 2, 1, 0],
     [0, 0, 0, 0]]
    )
dims = (3, 4) #Dimensions of the grid

class NumberGroup(object):
    def __init__(self, mines, coords, dims=None):
        """Takes a number of mines, and a set of coordinates."""
        if dims:
            self.dims = dims
        self.mines = mines
        self.coords = coords

    def __repr__(self):
        return "<Group of {} cells with {} mines>".format(
            len(self.coords), self.mines)

    def __str__(self):
        if hasattr(self, 'dims'):
            dims = self.dims
        else:
            dims = (max([c[0] for c in self.coords]) + 1,
                    max([c[1] for c in self.coords]) + 1)
        grid = np.zeros(dims, int)
        for coord in self.coords:
            grid[coord] = 1
        return str(grid).replace('0', '.').replace('1', '#')

    def __sub__(self, other):
        if type(other) is NumberGroup:
            return self.coords - other.coords
        elif type(other) is set:
            return self.coords - other.coords
        else:
            raise TypeError("Can only subtract a group or a set from another.")


def get_neighbours(coord, dims):
    x, y = coord
    row = [u for u in range(x-1, x+2) if u in range(dims[0])]
    col = [v for v in range(y-1, y+2) if v in range(dims[1])]
    return {(u, v) for u in row for v in col}

groups = []
all_coords = [(i, j) for i in range(dims[0])
    for j in range(dims[1])]
for coord, nr in [(c, grid[c]) for c in all_coords if grid[c] > 0]:
    empty_neighbours = {c for c in get_neighbours(coord, dims)
        if grid[c] == 0}
    if nr > len(empty_neighbours):
        print "Error: number {} in cell {} is too high.".format(nr, coord)
        break
    groups.append(NumberGroup(nr, empty_neighbours, dims))
print groups
for g in groups:
    print g
print groups[0] - groups[1]
更新:
我添加了一些其他类并进行了重构(请参见下面的工作代码),现在它可以创建和显示等价组,并朝着正确方向迈出了一步。但是,我仍然需要解决如何遍历所有可能的地雷配置的问题,方法是按一种方式分配给每个组一定数量的地雷,从而创建出一个有效的配置。感谢任何帮助。

例如:
# # # #
# 2 1 #
# # # #
这里有三个等价组G1:左3个,G2:中间4个,G3:右3个。我想让代码循环遍历,以以下方式分配具有地雷的组:

  • G1=2(最大第一组)=> G2=0 => G3=1(这是所有具有G1=2的配置)
  • G1=1(递减一)=> G2=1 => G3=0(这是所有具有G1=1的配置)
  • G1=0 => G2=2 不合法

因此我们得到了两个配置。这需要适用于更复杂的设置!

import numpy as np

def get_neighbours(coord, dims):
    x, y = coord
    row = [u for u in range(x-1, x+2) if u in range(dims[0])]
    col = [v for v in range(y-1, y+2) if v in range(dims[1])]
    return {(u, v) for u in row for v in col}

class NrConfig(object):
    def __init__(self, grid):
        self.grid = grid
        self.dims = grid.shape # Dimensions of grid
        self.all_coords = [(i, j) for i in range(self.dims[0])
            for j in range(self.dims[1])]
        self.numbers = dict()
        self.groups = []
        self.configs = []
        self.get_numbers()
        self.get_groups()
        self.get_configs()

    def __str__(self):
        return str(self.grid).replace('0', '.')

    def get_numbers(self):
        for coord, nr in [(c, self.grid[c]) for c in self.all_coords
            if self.grid[c] > 0]:
            empty_neighbours = {c for c in get_neighbours(
                coord, self.dims) if self.grid[c] == 0}
            if nr > len(empty_neighbours):
                print "Error: number {} in cell {} is too high.".format(
                    nr, coord)
                return
            self.numbers[coord] = Number(nr, coord, empty_neighbours,
                self.dims)

    def get_groups(self):
        coord_neighbours = dict()
        for coord in [c for c in self.all_coords if self.grid[c] == 0]:
            # Must be a set so that order doesn't matter!
            coord_neighbours[coord] = {self.numbers[c] for c in
                get_neighbours(coord, self.dims) if c in self.numbers}
        while coord_neighbours:
            coord, neighbours = coord_neighbours.popitem()
            equiv_coords = [coord] + [c for c, ns in coord_neighbours.items()
                if ns == neighbours]
            for c in equiv_coords:
                if c in coord_neighbours:
                    del(coord_neighbours[c])
            self.groups.append(EquivGroup(equiv_coords, neighbours, self.dims))

    def get_configs(self):
        pass # WHAT GOES HERE?!


class Number(object):
    """Contains information about the group of cells around a number."""
    def __init__(self, nr, coord, neighbours, dims):
        """Takes a number of mines, and a set of coordinates."""
        self.nr = nr
        self.coord = coord
        # A list of the available neighbouring cells' coords.
        self.neighbours = neighbours
        self.dims = dims

    def __repr__(self):
        return "<Number {} with {} empty neighbours>".format(
            int(self), len(self.neighbours))

    def __str__(self):
        grid = np.zeros(self.dims, int)
        grid[self.coord] = int(self)
        for coord in self.neighbours:
            grid[coord] = 9
        return str(grid).replace('0', '.').replace('9', '#')

    def __int__(self):
        return self.nr


class EquivGroup(object):
    """A group of cells which are effectively equivalent."""
    def __init__(self, coords, nrs, dims):
        self.coords = coords
        # A list of the neighbouring Number objects.
        self.nr_neighbours = nrs
        self.dims = dims
        if self.nr_neighbours:
            self.max_mines = min(len(self.coords),
                max(map(int, self.nr_neighbours)))
        else:
            self.max_mines = len(coords)

    def __repr__(self):
        return "<Equivalence group containing {} cells>".format(
            len(self.coords))

    def __str__(self):
        grid = np.zeros(self.dims, int)
        for coord in self.coords:
            grid[coord] = 9
        for number in self.nr_neighbours:
            grid[number.coord] = int(number)
        return str(grid).replace('0', '.').replace('9', '#')


grid = np.array(
    [[0, 0, 0, 0],
     [0, 2, 1, 0],
     [0, 0, 0, 0]]
    )
config = NrConfig(grid)
print config
print "Number groups:"
for n in config.numbers.values():
    print n
print "Equivalence groups:"
for g in config.groups:
    print g

稍微偏离问题,但在扫雷游戏中,您不是总是知道给定游戏中有多少地雷吗?假设您可能正在尝试实现一个“AI”求解器,那么您可能会使事情比必要的复杂。如果不是这种情况,那么为自己设置一个良好的挑战是很好的 :-) - Tom Dalton
主要目的不是制作一个求解器,虽然我明白你的意思。我想能够在没有可能的确定移动的情况下计算概率。 - Siwel
1
请参阅http://math.stackexchange.com/questions/389619/probability-in-minesweeper/1396494#1396494,了解如何计算概率的完整细节。 - Siwel
我不确定我正确理解了问题。您正在寻找一个良好的表示方式来表示您的地雷区,以便进行简单的查找? - Pietro Saccardi
1个回答

2

如果您不想使用暴力破解的方法,您可以将此过程建模为决策树。假设我们从您的示例开始:

####
#21#
####

如果我们想要开始放置有效配置的地雷,现在基本上有八种选择。由于在等价组内选择哪个方格并不重要,我们可以将其缩小为三个选择。树分支。让我们走下一条分支:

*###
#11#
####

我在G1放了一枚地雷,用星号表示。此外,我已更新与此等价组相关的数字(仅有一个数字),以指示这些编号的方格现在可以与更少的地雷相邻。

这并没有减少我们在下一步的选择自由,我们仍然可以在任何等价组中放置地雷。让我们在G1再放一颗地雷:

*XX#
*01#
XXX#

又一个星号标记了新的矿井,编号的方块再次降低了一个。现在已经降到零,意味着它不能再与任何矿井相邻。这意味着对于我们下一个放置矿井的选择,所有依赖于此编号方块的等价组都被排除了。X标记了我们现在无法放置任何矿井的方块。我们现在只能做出一种选择:

*XX*
*00X
XXXX

这里的分支已经结束,你找到了一个有效的配置。通过以这种方式沿着树中的所有分支运行,您应该能够找到所有分支。在这里,我们找到了您的第一个配置。当然,有不止一种方法可以到达那里。如果我们从G3放置地雷开始,我们将被迫在G1中放置另外两个地雷。该分支导致相同的配置,因此您应该检查是否存在重复项。我现在看不到避免这种冗余的方法。
第二个配置是通过从G2开始或先在G1中放置一个地雷然后再在G2中放置第二个地雷来找到的。无论哪种情况,您最终都会到达分支末端:
**XX
X00X
XXXX

无效的配置,如G1中零个矿井的示例不会出现。沿着树形图没有通向那里的有效选择。这是所有有效选择的完整树形图。

Choice 1:        1     |     2     |     3
Choice 2:    1   2   3 | 1         | 1   
Choice 3:     3     1  |           |1

有效的配置是分支结束时不再有其他选择的地方,即

113
12
131
21
311

如果我们不考虑数字的顺序,那么显然可以将其分成两个等价类。


谢谢您的回答,这肯定是一个选项。我唯一担心的是,在我们遇到更复杂的情况时,是否这是最有效的方法,因为通常相同的配置可能会出现多次。 - Siwel
1
有可能你可以微调一下,避免走下会导致冗余的分支。例如,知道1-2是死路,就没有必要尝试走1-3-2或2-1或3-1-2或3-2-1。它们都被排除了或者显然会导致冗余的结果。这样你就可以防止一些不必要的计算。 - Marco Tompitak
还要注意,由于容量限制,这实际上可能会导致无效的配置,例如如果将2替换为4,则唯一有效的配置将是G1=0,G2=1,G3=3,但是该方法不知道将矿分配给G1将是无效的。这不是一个很大的问题,可以解决,但我认为值得指出。 - Siwel
这是一个非常好的观点,我没有考虑过。这意味着您不仅需要验证您的分支结束不允许更多的移动,而且还要确保棋盘上的所有数字都为零。 - Marco Tompitak

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接