我目前正在使用C#开发一款基于回合制的网格策略游戏(类似于《魔法门之英雄无敌》和《奇迹时代》),首先专注于战斗部分。
游戏中网格上有多个单位,轮流进行攻击、射击、施放魔法、移动等操作。
在编程AI方面遇到了点困难。我想要一个能够提前规划的AI,我认为寻找最佳行动的过程可以是这样的:
- 生成可能移动的列表
- 对于每个可能的移动:
- 模拟移动
- 评估状态
然后我会在这个Minimax算法中重复这个过程,有点类似于创建一个国际象棋AI。
与制作国际象棋AI的最大区别在于“棋盘表示”(或状态);这个游戏有很多复杂性,仅仅用整数数组来存储状态是不够的。有多个单位,包含各种字段,用于存储生命值、攻击类型、移动方式和特殊能力等…
所以我面临的问题是在“模拟移动”部分。我需要创建一个类似于(previous state, move) => state
的函数。我通常会复制之前的状态,并在复制上执行移动。但是我找不到一种高效的方式来复制将要执行移动的状态,因为它非常复杂,包含许多不同的对象。而且移动不应该改变之前的状态。
有什么好的代码结构可以模拟移动而不影响当前棋盘吗?
可能可以通过为每个移动添加一个Undo()方法来实现,但随着逻辑变得复杂,追踪所有被更改的内容会变得困难。
我希望能找到某种软件设计模式,或者如果有人做过类似的事情,我想知道他们是否也遇到了这个问题。
Stack Overflow上的很多问题都讨论了棋盘游戏AI,但似乎总是关于国际象棋/井字游戏,具有简单的棋盘表示。
回答:
这看起来像是大多数约束编程求解器也会处理的问题。一般来说,这些求解器实现了Command
和Memento
模式的混合。具体操作如下:
你需要一个表示游戏状态的类(这里称为State
)。例如,包含棋盘的状态、当前可以移动的玩家、每个玩家的资源(如果这是游戏的一部分)等。最好使这种状态成为ICloneable
的实例:这样可以克隆状态并在其上进行假设性的处理。
因此:
ICloneable ^ |+-------------------+| State |+-------------------+|+ Clone() : Object |+-------------------+
接下来,你有一个表示状态上可能移动的类IMove
。IMove
可以Alter
状态。在大多数情况下,这种方法会返回一个IAlterResult
:一个存储一些数据的对象,例如用于撤销移动的数据。
+------------------------------+| IMove |+------------------------------+|+ Alter(State) : IAlterResult |+------------------------------+
和
+---------------------+| IAlterResult |+---------------------+|+ Undo(State) : void |+---------------------+
例如,如果你从棋盘上移走一个棋子,IAlterResult
会存储该棋子之前的位置,这样在撤销移动时,IAlterResult
可以将棋子(例如)放回棋盘上。
每次你进行一个IMove
,你会将对应的IAlterResult
推入一个Stack<IAlterResult>
中。这个堆栈因此会跟踪已完成的移动,并且可以通过迭代地弹出IAlterResult
并执行Undo
命令,将State
恢复到原始状态。
大多数约束编程求解器将State
的副本与IAlterResults
交错。一个堆栈看起来像这样。
+---------------+| AlterResult5 || AlterResult4 || Copy2 || AlterResult3 || AlterResult2 || AlterResult1 || Original copy |+---------------+
如果你希望回溯四个状态,你可以首先查看是否有可用的副本,简单地弹出前两个改变(不执行Undo
),然后使用Copy2
并撤销AlterResult3
和AlterResult2
。如果撤销操作需要大量计算结果,这可以提高一些效率。
你也可以在IAlterMove
中存储你所做的移动 – 如果移动无法撤销,或难以撤销 – 你可以简单地回溯到最后保存的副本并执行堆栈中的所有移动。所以如果你想要例如撤销最后一个移动(AlterResult5
),但那个结果无法撤销,你可以使用Copy2
并重新执行存储在AlterResult4
中的移动。