(编辑:2013 年 8 月 8 日)- 我编写了一些示例代码:Ultimate Tic-Tac-Toe,其中我有一个最小/最大游戏搜索的通用实现。代码是用 C++ 的愚蠢变体编写的,称为 prepp。
最好的资源是大学水平的人工智能课程。经典教材是"Artificial Intelligence: A Modern Approach" by Russel & Norvig
我将尝试对关键概念进行分解。
游戏状态 - 这是一组可用于确定的变量:
Value - 特定状态的“值”是当前玩家评估的该状态的函数,我们称之为 f(x)。此函数的另一个名称是“启发式”函数。在给定棋盘状态 x 的情况下,它确定当前玩家可以达到的最佳值。如何建模 x 取决于您,但 x 应该包含游戏自然流程中使用的所有内容。因此,在您的情况下,x 可能是一个 8x8 矩阵的值映射到棋盘上的棋子。而且,f 将是一个函数,它为红色玩家(或类似的)提供该棋盘配置的“价值”。
在 2 人回合制游戏中考虑的一种可能的简化是将“当前玩家”编码为状态的一部分。这将状态添加到启发式函数的输入。所以,我们用 f(x, player) 代替 f(x)。但是,这可以通过将 player 滚动到 x 来简化。无论哪种方式,f 将始终为同一玩家返回“值”,但根据下一个轮到谁。
通过(简化的)示例进行说明,如果白棋可以杀死黑棋的皇后,则国际象棋中的 f 几乎总是会返回非常高的分数。但是如果黑方有可能在下一步中杀死白方的后,那么 f 应该返回一个非常低的数字。
请注意,到目前为止还没有提到 minmax 的树搜索部分。 f 总是基于当前状态 定义的。所以,当我说“黑色有可能......”时,我的意思是你的启发式函数可以确定,例如,给定 x,有一条视线(对角线,水平或垂直)在黑主教或白车与白皇后之间。国际象棋的启发式算法应该足够复杂,以知道仅此一项就会造成威胁,因此在计算 f 的总价值时应该对它进行相应的评分。
在我讨论 minmax 之前,请注意 A* 是一种用于搜索可能的 x 值空间的优化。但是,在您完全理解并实现 minmax 之前,您不必担心优化问题。
因此,minmax 算法是一种组织 AI 决策过程的方法。为了实现 minmax,您需要做的事情:
- 从现有游戏状态x生成有效游戏状态。
- 在达到特定深度后停止循环或递归。
- 通过将每个级别递归的“值”传递回调用方来记住它。
基本流程从理解开始,轮到谁了?正如我之前提到的,f 总是返回高值表示玩家 1 成功,低值表示玩家 2 成功。在每个更深层次的递归中,玩家将使您了解是选择通过递归发现的潜在值的最小值还是最大值。
让我换个方式说。实际评估 f(x) 有两个原因。
- 您想知道游戏是否在状态 x 结束。
- 您已达到最深的递归级别,并且您想评估如果游戏进行到此为止的分数。
所以,你的代码会做这样的事情:
函数minmax(x, player) 返回值
- 我的当前状态是 x,下一个要移动的 玩家 是已知的。我知道这是因为
- function choose-move 告诉我的。 (见下文)
- 我被自己递归调用。 (见下文)
- 游戏结束了吗?问 f(x)。如果它返回正无穷大,那么白色就赢了。如果它返回负无穷大,那么黑色赢了。如果您的游戏有僵局选项,或者有一个结束得分(可能是更大游戏的一部分),那么您只需想出一种方法来表示获胜 + 得分作为返回值 f。或者,如果您想将其分开,只需创建一个新函数 g(x) 即可。如果游戏结束,则将该值返回给调用者。如果游戏还没有结束,请继续。
- 从 x 我将枚举所有可能的 x'。在 8x8 游戏中,任何单个游戏状态都可能有几个、几十个或几百个可能的有效移动。这取决于您的游戏规则。
- 如果已达到所需递归的最深级别,
- 对于每个 x',调用 f(x', player)。对于每个调用,我们将其返回值与我们传入的特定 x' 相关联。
- 其他
- 对于每个 x',递归调用 minmax(x', other-player)。 (请注意,此时 other-player 与 player 相反。)对于每个调用,我们将其返回值与特定的 x' 我们已经过去了。
此时,所有可能的下一步行动都应该有一个与之关联的“价值”。
- 如果 player 是玩家 1,则从与所有 x' 关联的所有值中选择 maximum 值,然后返回该值。这是玩家 1 在此游戏状态下能够做到的最好的事情。
- 如果 player 是玩家 2,请从与所有 x' 关联的所有值中选择 minimum 值,然后返回该值。这是玩家 2 在此游戏状态下能够做到的最好的事情。
结束函数minmax
function choose-move(x, player) return *next_state*
- 我目前的状态是x,我们正试图弄清楚玩家的下一步应该是什么。
- 如上所述,检查游戏是否结束。
- 从 x 我将枚举所有可能的 x'。 (您应该能够在 minmax 中重用您必须执行此操作的任何代码。
- 对于每个 x',调用 minmax(x', player)。对于每个调用,我们将其返回值与我们传入的特定 x' 相关联。
- 如果 player 是玩家 1,则从与所有 x' 关联的所有值中选择 maximum 值,然后返回该值。这是玩家 1 在此游戏状态下能够做到的最好的事情。
- 如果 player 是玩家 2,请从与所有 x' 关联的所有值中选择 minimum 值,然后返回该值。这是玩家 2 在此游戏状态下能够做到的最好的事情。
结束函数选择移动
您的驱动程序代码只需要调用 choose-move,它应该返回下一个游戏板。显然,您还可以将返回值编码为“移动”而不是状态,以获得不同的表示。
希望这会有所帮助。很抱歉解释冗长,我刚喝了点咖啡。