我已经为我的国际象棋游戏实现了alpha-beta算法,但它需要很长时间(4步需要几分钟)才能做出一个相当愚蠢的移动。
我已经花了两天时间试图找出错误(我认为我犯了一个错误),我非常希望能得到一些关于我的代码的外部意见。
getMove函数:在根节点被调用,它会为所有子节点(可能的移动)调用alphaBeta函数,然后选择得分最高的移动。
Move AIPlayer::getMove(Board b, MoveGenerator& gen){ // 定义常量: ALPHA=-20000 和 BETA= 20000 int alpha = ALPHA; Board bTemp(false); // 测试棋盘 Move BestMov; int i = -1; int temp; int len = gen.moves.getLength(); // moves是一个保存所有合法移动的链表 BoardCounter++; // AIPlayer对象的私有属性,用于统计分析的棋盘数 Move mTemp; // mTemp用于将列表中的下一个移动应用到临时测试棋盘上 gen.mouvements.Begin(); // 将列表计数器设置到列表的第一个元素 while (++i < len && alpha < BETA){ mTemp = gen.moves.nextElement(); bTemp.cloneBoard(b); bTemp.applyMove(mTemp); temp = MAX(alpha, alphaBeta(bTemp, alpha, BETA, depth, MIN_NODE)); if (temp > alpha){ alpha = temp; BestMov = mTemp; } } return BestMov;}
alphaBeta函数:
int AIPlayer::alphaBeta(Board b, int alpha, int beta, char depth, bool nodeType){ Move m; b.changeSide(); compteurBoards++; MoveGenerator genMoves(b); // 当构造函数给定一个棋盘时,它会自动生成可能的移动 // 棋盘对象有一个玩家属性,保存当前玩家 if (genMoves.checkMate(b, b.getSide(), moves)){ // 如果当前玩家被将死 return 100000; } else if (genMoves.checkMate(b, ((b.getSide() == BLACK) ? BLACK : WHITE), moves)){ // 如果另一方被将死 return -100000; } else if (!depth){ return b.evaluateBoard(nodeType); } else{ int scoreMove = alpha; int best; genMoves.moves.Begin(); short i = -1, len = genMoves.moves.getLength(); Board bTemp(false); if (nodeType == MAX_NODE){ best = ALPHA; while (++i < len){ bTemp.cloneBoard(b); if (bTemp.applyMove(genMoves.moves.nextElement())){ scoreMove = alphaBeta(bTemp, alpha, beta, depth - 1, !nodeType); best = MAX(best, scoreMove); alpha = MAX(alpha, best); if (beta <= alpha){ std::cout << "max cutoff" << std::endl; break; } } } return scoreMove; //return alpha; } else{ best = BETA; while (++i < len){ bTemp.cloneBoard(b); if (bTemp.applyMove(genMoves.moves.nextElement())){ scoreMove = alphaBeta(bTemp, alpha, beta, depth - 1, !nodeType); best = MIN(best, scoreMove); beta = MIN(beta, best); if (beta <= alpha){ std::cout << "min cutoff" << std::endl; break; } } } return scoreMove; //return beta; } return meilleur; }}
编辑:我应该指出,evaluateBoard函数只评估棋子的移动性(可能的移动数量,捕获移动会根据被捕获的棋子得到更高的分数)
谢谢你。
回答:
我可以看出你正在尝试实现一个极小化-极大化算法。然而,代码中有一些让我怀疑的地方。我们将与开源的Stockfish国际象棋引擎进行比较。请参考https://github.com/mcostalba/Stockfish/blob/master/src/search.cpp中的搜索算法
1. 按值传递Board b
你的代码中有这样的内容:
alphaBeta(Board b, int alpha, int beta, char depth, bool nodeType)
我不知道“Board”具体是什么。但这在我看来不太对。我们来看Stockfish:
Value search(Position& pos, Stack* ss, Value alpha, Value beta, Depth depth, bool cutNode)
在Stockfish中,位置对象是按引用传递的。如果“Board”是一个类,每次调用alpha-beta函数时,程序都需要创建一个新的副本。在国际象棋中,当我们需要评估大量节点时,这显然是不可接受的。
2. 没有哈希
在Stockfish中,哈希是这样的:
ttValue = ttHit ? value_from_tt(tte->value(), ss->ply) : VALUE_NONE;
没有哈希,你将需要一次又一次地评估同一个位置。没有实现哈希,你将寸步难行。
3. 检查将死
这可能不是最显著的减速原因,但我们永远不应该在每个节点都检查将死。在Stockfish中:
// 所有合法移动都已被搜索。一个特殊情况:如果我们处于被将死的状态// 并且没有找到合法移动,这就是将死。如果(InCheck && bestValue == -VALUE_INFINITE) return mated_in(ss->ply); // 从根节点到将死的步数
这是在所有可能的移动都被搜索后完成的。我们这样做是因为通常非将死节点比将死节点多得多。
4. Board bTemp(false);
这看起来是一个主要的减速原因。我们来看Stockfish:
// 第14步。执行移动 pos.do_move(move, st, ci, givesCheck);
你不应该在每个节点都创建一个临时对象(创建bTemp的对象)。机器需要分配一些堆栈空间来保存bTemp。如果bTemp不是主要变量(即,不太可能被处理器缓存),这可能会是一个严重的性能惩罚。Stockfish只是修改内部数据结构,而不创建新的数据结构。
5. bTemp.cloneBoard(b);
与4类似,甚至更糟,这是为节点中的每个移动执行的。
6. std::cout << “max cutoff” << std::endl;
可能难以置信,向终端打印比处理要慢得多。这里你可能会造成一个潜在的减速,即字符串需要被保存到IO缓冲区。该函数可能会(我不确定100%)甚至会阻塞你的程序,直到文本显示在终端上。Stockfish只在统计摘要时这样做,绝对不是每次你有高失败或低失败时都这样做。
7. 没有排序PV移动
在解决其他问题之前,可能不是你想做的事情。在Stockfish中,他们有:
std::stable_sort(RootMoves.begin() + PVIdx, RootMoves.end());
这是在迭代加深框架中的每次迭代中完成的。