具有复杂棋盘表示的 AI:模拟单个可能的移动

AI with complex board representation: simulating a single possible move

我目前正在使用 C# 开发基于回合制的基于网格的策略游戏(类似于魔法门之英雄无敌和奇迹时代)。先关注战斗部分。
一个格子上有多个单位,轮流攻击、射击、施法、移动等。

AI 编程有点卡住了。我想要一个可以提前计划的人工智能,所以我认为寻找最佳行动的过程可能是这样的:

  1. 生成可能的着法列表
  2. 对于每一个可能的走法:
    • 模拟移动
    • 评估状态

然后我在 Minimax 算法中重复这个过程,有点类似于创建国际象棋 AI。

与制作国际象棋 AI 的最大区别在于 "board representation"(或状态);这个游戏有很多复杂性,简单地将状态存储为整数数组是不够的。多个单位,各种领域,存储生命值,攻击类型,移动方式和特殊能力等等......

所以我面临的问题是在 "simulating move" 部分。我需要做一个像 (previous state, move) => state 这样的函数。我通常复制以前的状态并在副本上执行移动。但是我找不到一种有效的方法来复制将执行移动的状态,因为它非常复杂,有很多不同的对象。而且移动不应改变之前的状态。

在不影响当前棋盘的情况下模拟走法的良好代码结构是什么?

它可能可以通过为每个移动设置一个 Undo() 方法来完成,但是随着逻辑变得复杂,将很难跟踪所有已更改的内容。

我可能正在寻找某种软件设计模式,或者如果有人做过类似的事情,我想知道他们是否遇到过这个问题。

Stack Overflow 上的很多问题都在谈论棋盘游戏 AI,但它似乎总是关于 chess/tic-tac-toe 简单的棋盘表示。

这看起来也是大多数 约束规划求解器 处理的问题。通常,这些实现了 CommandMemento 模式之间的混合。其工作方式如下:

你需要一个代表游戏状态的class(这里叫State)。包含例如棋盘状态、可以移动的当前玩家、每个玩家拥有的资源(如果这是游戏的一部分)等的东西。您最好将此类状态设为 ICloneable 的实例:这样就可以克隆状态并对其进行假设处理。

所以:

     ICloneable
          ^
          |
+-------------------+
|       State       |
+-------------------+
|+ Clone() : Object |
+-------------------+

接下来你有一个 class IMove 表示状态可能的移动。 IMove可以AlterState。在大多数情况下,这种方法 returns 类似于 IAlterResult 的东西:一个存储一些数据的对象,例如用于撤消移动。

+------------------------------+
|            IMove             |
+------------------------------+
|+ Alter(State) : IAlterResult |
+------------------------------+

+---------------------+
|    IAlterResult     |
+---------------------+
|+ Undo(State) : void |
+---------------------+

例如,如果您从棋盘上移除一个棋子,IAlterResult 会存储该棋子所在的位置,这样如果您撤销移动,IAlterResult 可以放置棋子(作为例如),回到板上。

每次你做一个IMove,你就把相应的IAlterResult推到一个Stack<IAlterResult>上。因此,该堆栈跟踪已完成的移动,并且可以通过迭代弹出 IAlterResults 并执行 Undo 命令使 State 回到原始状态。

大多数约束规划求解器将 State 的副本与 IAlterResults 交错放置。因此堆栈看起来像。

+---------------+
| AlterResult5  |
| AlterResult4  |
| Copy2         |
| AlterResult3  |
| AlterResult2  |
| AlterResult1  |
| Original copy |
+---------------+

如果你想回溯四个状态,你可以先看看是否有可用的副本,只需弹出前两个更改(没有 Undoing)然后使用 Copy2 并撤消AlterResult3AlterResult2。如果撤消操作需要大量计算结果,这可以提高一些效率。

在这种情况下,您还可以存储您在 IAlterMove 中所做的移动 - 如果移动无法撤消或难以撤消 - 您可以简单地回溯到最后保存的副本并执行所有操作在堆栈中移动。因此,例如,如果您想撤消最后一步 (AlterResult5) 但该结果无法撤消,您可以使用 Copy2 并重做存储在 AlterResult4.

中的移动