我正在编写一个使用对抗搜索技术的 Connect4 游戏,并且遇到了瓶颈。我觉得我离解决方案不远了,但可能存在一个问题,我可能在切换视角(即:我基于哪个参与者的视角来评估分数),或者在某个地方缺少一个负号。
问题在于,在我尝试过的变体中,AI要么不阻止玩家的三连,但在其他情况下,AI会完美地进行游戏,要么他倾向于阻止玩家,即使他有机会赢得比赛。搜索深度是偶数还是奇数似乎也很重要,因为 AI 在 6 层的搜索中表现得非常糟糕,这很能说明问题。
搜索
使用的算法是带有 alpha-beta 剪枝的 negamax,实现如下:
private int Negamax(int depth, int alpha, int beta, Player player){ Player winner; if (Evaluator.IsLeafNode(game, out winner)) { return winner == player ? (10000 / depth) : (-10000 / depth); } if (depth == Constants.RecursionDepth) { return Evaluator.Evaluate(game, depth, player); } foreach (var move in moves) { int row; if (board.DoMove(move, player, out row)) { var value = -Negamax(depth + 1, -beta, -alpha, (Player)1 - (int)player); board.UndoMove(move, row, player); if (value > alpha) { alpha = value; if (player == Player.AI) { bestColumn = move; } } if (alpha >= beta) { return alpha; } } } return alpha;}
我不认为问题出在这个函数中,但也有可能。
评估
我基于这样一个事实来设计评估函数:在 7×6 的棋盘上只有 69 种可能的四连方式。我有一个包含大约 350 个项目的查找表,其中包含每个列和行所属的硬编码信息,这些信息是哪个获胜组合的一部分。例如,对于第 0 行和第 0 列,该表如下所示:
//c1r1table[0][0] = new int[3];table[0][0][0] = 21;table[0][0][1] = 27;table[0][0][2] = 61;
这意味着第 0 列,第 0 行是获胜组合 21、27 和 61 的一部分。
我有一个第二个表,其中包含两个玩家在每个获胜组合中有多少个棋子。当我进行移动时,我会执行以下操作:
public bool DoMove(int column, Player p, out int row){ row = moves[column]; if (row >= 0) { Cells[column + row * Constants.Columns] = p; moves[column]--; var combinations = this.Game.PlayerCombinations[p]; foreach (int i in TerminalPositionsTable.Get(column,row)) { combinations[i]++; } return true; } else { return false; }}
对于 UndoMove
,当然会执行相反的操作。
因此,在 Player.Human
在第 0 列,第 0 行上进行移动后,该表将在索引 21、27 和 61 处填充值为 1。如果我在也属于获胜组合 27 的单元格中进行另一次移动,则玩家组合表将在索引 27 处递增到 2。
我希望我已经说清楚了,因为它在评估函数中用于非常快速地确定玩家离获得四连有多近。
我认为问题所在的评估函数如下:
public static int Evaluate(Game game, int depth, Player player){ var combinations = game.PlayerCombinations[player]; int score = 0; for (int i = 0; i < combinations.Length; i++) { switch (combinations[i]) { case 1: score += 1; break; case 2: score += 5; break; case 3: score += 15; break; } } return score;}
所以我只是简单地循环遍历 69 种可能的获胜组合,并根据它是单个棋子、两连还是三连,将一个量添加到分数中。
在这个对抗搜索中,我仍然感到困惑的是,我是否应该关心哪个玩家在移动?我的意思是,我应该像这里一样传入玩家,还是应该始终从 AI 玩家的角度评估棋盘?我已经尝试过很多 aiScore - humanScore
的组合,或者只是总是从 Player.AI
的角度来看,等等。但是我遇到了死胡同,我尝试过的每种组合都存在很大的缺陷。
所以:
- 我的评估逻辑基础是否扎实?
- 我应该什么时候“切换视角”?
非常感谢任何帮助。
更新
我已经实现了 Brennan 在下面的建议,虽然它已经有了很大的改进,但由于某种原因,它不会阻止任何列上的三连,除了最左边和最右边的两列,并且只有当搜索深度不均匀时。AI 在偶数搜索深度下是无敌的,但只有在深度 8 及以上时。然后它再次拒绝阻止。这很能说明我可能非常接近了,但仍然存在一些关键缺陷。
也许这与我设置 AI 应该放置棋子的列有关,正如 Brennan 评论的那样,但我不知道什么时候该设置它。仅在深度 0 处设置它不起作用。
更新 2
编辑了代码,使其看起来像现在这样,并使用了 Brennan 的更改。
更新 3
创建了一个包含完整代码的 Github 仓库。如果您不知道如何使用 Git,只需从 这里 下载一个 zip 文件即可。
这是一个 .NET 4.0 项目,运行它将在您的 documents/logs 目录中创建 negamax 算法的日志文件。该解决方案还包含一个测试项目,其中包含每个棋盘列的测试,用于测试当玩家在该处有三连时,AI 是否选择阻止玩家。
回答:
这些东西让我的大脑很受伤,所以我不能确定这个答案是正确的,但还是说一下吧。
在 negamax 中,分数总是相对于当前正在移动的玩家进行评估。如果是白方的回合,那么高分对白方有利。如果是黑方的回合,那么高分对黑方有利。因此,如果你有一个叶子节点,那么分数是 +inf 还是 -inf 取决于该节点是白方还是黑方获胜,而是取决于它是否对你当前正在评估的玩家有利。将此:
return winner == Player.AI ? (10000 / depth) : (-10000 / depth);
替换为:
return winner == player ? (10000 / depth) : (-10000 / depth);
你的评估函数中也存在类似的问题。将此:
return player == Player.AI ? score : -score;
替换为:
return score;
同样,我不确定这是对的。但我希望你尝试这两个更改,并告诉我它是否有效。我很好奇!