我认为我可以为一个游戏滴答生成所有可能的状态,但是在有四个玩家和5个基本动作(4个动作和炸弹放置)的情况下,它在游戏树的第一级给出5 ^ 4个状态。
正确!您需要为每个游戏滴答搜索所有5 ^ 4(甚至6 ^ 4,因为您可以在4个方向上行走,停止并“放炸弹”?)动作。但是,当玩家已经决定移动时,要花一些时间才能执行移动(例如10个游戏滴答声)。在此期间,可能性减少。
该值将随着下一个级别的增长呈指数增长。我想念什么吗?有什么方法可以实现它,还是应该使用完全不同的算法?
您可以使用哈希表仅计算一次相同的游戏状态“子树”。想象玩家A上下走动,而所有其他玩家“等待”,则您最终处于相同的游戏状态。与“左右”或“左右”相同。同样,“先左上移”和“先左上移”也会导致相同的状态。使用哈希表,您可以针对已评估的游戏状态“重用”计算出的分数。这大大降低了生长速度。从数学上讲,它减小了指数增长函数的基础。为了弄清楚它在多大程度上降低了复杂性,让我们看一下如果一个玩家只是向上/向下/向左/向右/停止移动,则与地图上可到达的位置(=不同的游戏状态)相比,仅一个玩家可能的移动。
深度1:5次移动,5个不同状态,此递归的5个其他状态
深度2:25个移动,13个不同的状态,此递归的8个其他状态
深度3:6125次移动,25个不同的状态,此递归的12个其他状态
为了使之可视化,请自己回答:一招,二招,三招可以到达地图上的哪些字段。答案是:与起始位置的最大距离为1、2或3的所有字段。
使用HashTable时,您只需评估每个可到达的游戏状态(在我们的示例中为25,深度3)。而如果没有HashTable,则需要对其进行多次评估,这意味着在深度级别3上需要进行6125次评估,而不是25次评估。最好:一旦计算了HashTable条目,便可以在以后的时间步骤中重复使用它...
您还可以使用不值得进行更深入搜索的增量加深和alpha-beta修剪“剪切”子树。对于国际象棋,这将搜索到的节点数减少到大约1%。可以在以下视频中找到有关alpha-beta修剪的简短介绍:http : //www.teachingtree.co/cs/watch? concept_name=Alpha-beta+Pruning
http://chessprogramming.wikispaces.com/Search是一个进行进一步研究的好起点。该页面与国际象棋有关,但是搜索和优化算法完全相同。
另一个(但很复杂)的AI算法-更适合于游戏-是“时间差异学习”。
问候
斯特凡
PS:如果您减少了可能的游戏状态数量(例如,地图尺寸非常小,每位玩家只有一颗炸弹,没有别的),那么就有机会预先计算所有游戏状态的评估值。
- 编辑 -
您还可以使用minimax计算的离线计算结果来训练神经元网络。或者您可以使用它们来评估/比较手动实施的策略。例如,您可以实施一些建议的“个性”和一些启发式检测,以找出在哪种情况下哪种策略是好的。因此,您应该“分类”情况(例如游戏状态)。这也可以由神经网络来处理:训练神经网络以预测在当前情况下哪种手动编码策略发挥最佳效果并执行该策略。对于真实游戏,这将产生非常好的实时决策。这比通过其他方式可以实现的低深度限制搜索要好得多,因为脱机计算花费多长时间(在游戏开始之前)并不重要。
-编辑#2-
如果仅每1秒重新计算一次最佳移动,则还可以尝试执行更多更高级别的计划。那是什么意思 您知道在一秒钟内可以执行多少步。因此,您可以列出一个可到达的位置列表(例如,如果在1秒内进行3次移动,则您将有25个可到达的位置)。然后,您可以像这样计划:转到“位置x并放置炸弹”。正如其他人建议的那样,您可以创建一个“危险”地图,该地图用于路由算法(如何定位到x?应该选择哪个路径[在大多数情况下可能会有一些变化])。与庞大的HashTable相比,这减少了内存消耗,但产生的最佳结果却更少。但是由于它使用较少的内存,因此可能会由于缓存效果(更好地使用L1 / L2内存缓存)而更快。
另外:您可以进行预搜索,其中仅包含一个玩家的移动,以找出导致丢失的变化。因此,将所有其他玩家从游戏中移除。。。存储每个玩家可以选择的组合而不会丢失。如果只有松散的举动,请寻找玩家最长存活时间的举动组合。要存储/处理这种树形结构,您应该使用带有索引指针的数组,如下所示:
class Gamestate {
int value;
int bestmove;
int moves[5];
};
#define MAX 1000000
Gamestate[MAX] tree;
int rootindex = 0;
int nextfree = 1;
每个状态都有一个评估“值”,并在移动时(0 =停止,1 =向上,2 =右,3 =向下,4 =左)链接到下一个游戏状态,方法是将数组索引存储在move [0]中的“ tree”中]移至[4]。要递归构建树,可能看起来像这样:
const int dx[5] = { 0, 0, 1, 0, -1 };
const int dy[5] = { 0, -1, 0, 1, 0 };
int search(int x, int y, int current_state, int depth_left) {
// TODO: simulate bombs here...
if (died) return RESULT_DEAD;
if (depth_left == 0) {
return estimate_result();
}
int bestresult = RESULT_DEAD;
for(int m=0; m<5; ++m) {
int nx = x + dx[m];
int ny = y + dy[m];
if (m == 0 || is_map_free(nx,ny)) {
int newstateindex = nextfree;
tree[current_state].move[m] = newstateindex ;
++nextfree;
if (newstateindex >= MAX) {
// ERROR-MESSAGE!!!
}
do_move(m, &undodata);
int result = search(nx, ny, newstateindex, depth_left-1);
undo_move(undodata);
if (result == RESULT_DEAD) {
tree[current_state].move[m] = -1; // cut subtree...
}
if (result > bestresult) {
bestresult = result;
tree[current_state].bestmove = m;
}
}
}
return bestresult;
}
这种树结构要快得多,因为动态分配内存确实非常慢!但是,存储搜索树还是很慢的……因此,这更多的是启发。