返回

Prolog扫雷open_adjacent函数问题详解及解决方案

Ai

Prolog 扫雷:解决 open_adjacent 函数问题

玩扫雷,点开一个空格,周围一片区域都跟着开了,这感觉很爽。但如果程序写得不对,该开的没开,或者开了不该开的,那就糟心了。 你遇到的问题就是这样,open_adjacent 函数没有按照预期工作,输出了奇怪的结果,还重复打开格子。 咱们一起来看看问题出在哪儿,怎么解决。

一、 问题在哪儿?

你的代码里,open_adjacent(X, Y) 函数负责递归打开相邻的空格。问题在于,这个函数没有一个机制来“记住”哪些格子已经被访问过了。结果就是,它会在相邻的空格之间来回“蹦跶”,不断地重复检查和打开,导致了无限循环(或者说是大量的重复操作)和错误的输出。 想象一下,两个人互相指着对方说:“你检查过了吗?你检查过了吗?”... 没完没了了。

此外, game_over谓词在递归中多处被调用, 但其并没有产生真正有效的作用。

二、 怎么修?

解决这个问题,核心在于要让 open_adjacent 函数“记住”它已经访问过的格子。 有几种方法可以做到:

2.1 方案一: 使用一个“已访问”列表 (Recommended)

这个方法最直观。 我们可以创建一个列表,把所有已经访问过的格子坐标放进去。每次 open_adjacent 打算处理一个格子之前,先看看这个格子是不是在这个列表里。如果在,就跳过;如果不在,就处理,并把这个格子加到列表里。

原理:

通过维护一个已访问列表,避免重复处理相同的单元格, 防止无限递归。

代码:

open_adjacent(X, Y) :-
    open_adjacent_helper(X, Y, []). % 从一个空的已访问列表开始

open_adjacent_helper(X, Y, Visited) :-
    member((X, Y), Visited), !. % 如果已经在 Visited 列表里, 直接返回

open_adjacent_helper(X, Y, Visited) :-
    adjacent(X, Y, AdjX, AdjY),
    \+ member((AdjX, AdjY), Visited), % 确保相邻格子没被访问过
    (   cell(AdjX, AdjY, closed, _) ->
        (   count_mines(AdjX, AdjY, Count),
            (   Count = 0 ->
                retract(cell(AdjX, AdjY, closed, _)),
                assert(cell(AdjX, AdjY, open, 0)),
                write('open cell: ('), write(AdjX), write(', '), write(AdjY), write('), adjacent mines: '), writeln(Count),
                open_adjacent_helper(AdjX, AdjY, [(X, Y) | Visited]) % 递归, 把当前格子加到 Visited
            ;
                retract(cell(AdjX, AdjY, closed, _)),
                assert(cell(AdjX, AdjY, open, Count)),
                write('open cell:  ('), write(AdjX), write(', '), write(AdjY), write('), adjacent mines: '), writeln(Count)
            )
        )
    ;   write('Cell was open:('), write(AdjX), write(', '), write(AdjY), write(')')
    ).

解释:

  1. open_adjacent(X, Y) 调用 open_adjacent_helper(X, Y, []),并传入一个空的 Visited 列表。
  2. open_adjacent_helper(X, Y, Visited) 首先检查 (X, Y) 是否在 Visited 中。如果是,直接 ! (cut) 停止。
  3. 如果 (X, Y) 不在 Visited 中, 它会找到一个相邻的格子 (AdjX, AdjY)
  4. 使用 \+ member((AdjX, AdjY), Visited) 来确保这个相邻的格子没被访问过。
  5. 如果相邻的格子是关闭的, 且周围没有地雷 (Count = 0), 打开它,并递归调用 open_adjacent_helper(AdjX, AdjY, [(X, Y) | Visited]),注意这里,我们把当前格子 (X, Y) 加到了 Visited 列表的 头部
  6. 如果相邻格子周围有雷,打开。
  7. 如果相邻格子已经打开,则输出提示信息。

修改 open_cell: 去除多余的 game_over.

open_cell(X, Y) :-
   cell(X, Y, closed, _),
   mine(X, Y),
   retract(cell(X, Y, closed, _)),
   assert(cell(X, Y, open, _)),
   write('Вы попали на мину!'), nl,
   !,
   game_over.


open_cell(X, Y) :-
   cell(X, Y, closed, _),
   count_mines(X, Y, Count),
   (Count > 0 ->
       retract(cell(X, Y, closed, _)),
       assert(cell(X, Y, open, Count)),
       write('open cell > 0\n')
       ;
       retract(cell(X, Y, closed, _)),
       assert(cell(X, Y, open, 0)),
       write('open cell = 0\n'),
       open_adjacent(X, Y)
   ).

2.2 方案二: 直接修改 cell 的状态

另一个办法是,直接修改 cell 事实的状态。 比如,在访问一个格子之后,我们可以把它的状态从 closed 改成 visited,然后再改成 open。这样,open_adjacent 只需要检查格子的状态是不是 closed 就行了。

原理:

修改单元格状态,用visited状态表示已经访问过的单元格。

代码:

open_adjacent(X, Y) :-
    adjacent(X, Y, AdjX, AdjY),
    (   cell(AdjX, AdjY, closed, _) ->  
        (   count_mines(AdjX, AdjY, Count),
            (   Count = 0 ->  
                retract(cell(AdjX, AdjY, closed, _)),
                assert(cell(AdjX, AdjY, visited, 0)),  % 先标记为 visited
                write('open cell: ('), write(AdjX), write(', '), write(AdjY), write('), adjacent mines: '), writeln(Count),
                open_adjacent(AdjX, AdjY),
                retract(cell(AdjX, AdjY, visited, 0)),                
                assert(cell(AdjX, AdjY, open, 0)) %递归返回之后再变成 open

            ;   
                retract(cell(AdjX, AdjY, closed, _)),
                assert(cell(AdjX, AdjY, open, Count)),
                write('open cell:  ('), write(AdjX), write(', '), write(AdjY), write('), adjacent mines: '), writeln(Count)
            )
        )
    ;
       cell(AdjX, AdjY, visited, _) ->  
        (retract(cell(AdjX, AdjY, visited, _)),                
         assert(cell(AdjX, AdjY, open, 0)))
       ;write('Cell was open:('), write(AdjX), write(', '), write(AdjY), write(')'))
     ).

解释:

  1. 如果相邻的格子是 closed, 并且周围没有地雷 (Count = 0), 先把它标记成 visited
  2. 递归调用 open_adjacent
  3. 当递归返回后,将visited状态改为open状态
  4. 如果相邻的格子状态为visited, 将其状态改为open.

注意: 这种方法需要你在游戏逻辑的其他部分做相应的修改。例如,在判断游戏是否结束,或者判断格子是否可以点击时, 要同时考虑 visitedopen 两种状态。另外这种方案可能会使状态复杂化, 我更推荐方案一。

2.3 安全建议 (方案一、二通用)

  • 边界检查: 你的 adjacent 函数已经做了很好的边界检查, 确保不会访问超出棋盘范围的格子。
  • 输入验证: 在实际应用中,你可能需要验证用户输入的坐标是否合法。

2.4 进阶技巧(可选)

  • 剪枝 (Cut): 你已经在代码里用了一些 ! (cut)。 ! 可以用来控制回溯, 提高效率。 但是要小心使用, 确保你清楚它的作用。
  • Difference Lists(差异列表): 对于大型棋盘,使用差异列表可以更高效地管理“已访问”列表。

三、 总结

通过上述方法修改代码后, 再次运行你的扫雷游戏,open_adjacent 函数应该就能正常工作了,不会再有重复打开和奇怪的输出。 使用方案一 (已访问列表) 来修改你的代码,既简单又可靠, 也方便理解。