Prolog扫雷open_adjacent函数问题详解及解决方案
2025-03-08 18:20:05
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(')')
).
解释:
open_adjacent(X, Y)
调用open_adjacent_helper(X, Y, [])
,并传入一个空的Visited
列表。open_adjacent_helper(X, Y, Visited)
首先检查(X, Y)
是否在Visited
中。如果是,直接!
(cut) 停止。- 如果
(X, Y)
不在Visited
中, 它会找到一个相邻的格子(AdjX, AdjY)
。 - 使用
\+ member((AdjX, AdjY), Visited)
来确保这个相邻的格子没被访问过。 - 如果相邻的格子是关闭的, 且周围没有地雷 (Count = 0), 打开它,并递归调用
open_adjacent_helper(AdjX, AdjY, [(X, Y) | Visited])
,注意这里,我们把当前格子(X, Y)
加到了Visited
列表的 头部。 - 如果相邻格子周围有雷,打开。
- 如果相邻格子已经打开,则输出提示信息。
修改 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(')'))
).
解释:
- 如果相邻的格子是
closed
, 并且周围没有地雷 (Count = 0), 先把它标记成visited
。 - 递归调用
open_adjacent
。 - 当递归返回后,将
visited
状态改为open
状态 - 如果相邻的格子状态为
visited
, 将其状态改为open
.
注意: 这种方法需要你在游戏逻辑的其他部分做相应的修改。例如,在判断游戏是否结束,或者判断格子是否可以点击时, 要同时考虑 visited
和open
两种状态。另外这种方案可能会使状态复杂化, 我更推荐方案一。
2.3 安全建议 (方案一、二通用)
- 边界检查: 你的
adjacent
函数已经做了很好的边界检查, 确保不会访问超出棋盘范围的格子。 - 输入验证: 在实际应用中,你可能需要验证用户输入的坐标是否合法。
2.4 进阶技巧(可选)
- 剪枝 (Cut): 你已经在代码里用了一些
!
(cut)。!
可以用来控制回溯, 提高效率。 但是要小心使用, 确保你清楚它的作用。 - Difference Lists(差异列表): 对于大型棋盘,使用差异列表可以更高效地管理“已访问”列表。
三、 总结
通过上述方法修改代码后, 再次运行你的扫雷游戏,open_adjacent
函数应该就能正常工作了,不会再有重复打开和奇怪的输出。 使用方案一 (已访问列表) 来修改你的代码,既简单又可靠, 也方便理解。