2

本文作者: Lauri Hartikka
编译:胡子大哈

翻译原文:http://huziketang.com/blog/posts/
英文连接:A step-by-step guide to building a simple chess AI

转载请注明出处,保留原文链接以及作者信息

首先让我们先看几个对开发简单国际象棋 AI 很有帮助的概念:

  • 移动生成

  • 局面评估

  • 极大极小算法

  • α-β 剪枝

每一步中我们都会对经过时间检验的国际象棋程序进行改进,我会展示不同算法风格所产生的影响。你也可以在 GitHub 上看到最终的 AI 算法。

步骤 1:移动生成和棋盘可视化

使用 chess.js 库来生成移动规则,使用 chessboard.js 来可视化棋盘。移动生成库实现了所有国际象棋的规则,对于任意给定的棋盘状态我们都可以计算出下一步的合法的走棋方法。

(移动生成函数的可视化版本。起始位置作为输入,输出是所有可能的走法。)

使用这些库可以使我们专注于我们所感兴趣的任务:开发最佳下棋的算法。我们首先从创建以一个函数开始,在所有可能走法中返回一个随机的结果。

    var calculateBestMove =function(game) {
        //generate all the moves for a given position
        var newGameMoves = game.ugly_moves();
        return newGameMoves[Math.floor(Math.random() * newGameMoves.length)];
    };

用这种方法,尽管它不是一个合格的棋手,但是起码我们可以和它玩起来了。

步骤 2:位置评估

下面我们试着让它理解在一个确定的位置上怎么走比较好。实现这一功能最简单的方法是计算棋盘上棋子的相对强度大小,用下面的对照表。

通过评估函数,可以选择评估结果最佳的走法。

    var calculateBestMove = function (game) {
    
        var newGameMoves = game.ugly_moves();
        var bestMove = null;
        //use any negative large number
        var bestValue = -9999;
    
        for (var i = 0; i < newGameMoves.length; i++) {
            var newGameMove = newGameMoves[i];
            game.ugly_move(newGameMove);
    
            //take the negative as AI plays as black
            var boardValue = -evaluateBoard(game.board())
            game.undo();
            if (boardValue > bestValue) {
                bestValue = boardValue;
                bestMove = newGameMove
            }
        }
    
        return bestMove;
    
    };

这样一来,一个切实的改善是,算法会吃掉它可以吃掉的棋子。

(黑子使用了简单评估算法,在这里可以看到:https://jsfiddle.net/lhartikk...

步骤 3:用极大极小算法搜索树

接下来我们来创建一个搜索树,通过它算法可以选择最佳走法,这里需要用到极大极小算法

在这个算法中,会根据给定的树深度对递归树进行遍历,所要评估的状态就是树的叶子节点。

这一步完成以后我们把子节点中的最大或者最小值返回给父节点,这要依赖于白棋还是黑棋来走这一步(这就是说在树的每一层中都最大或者最小化输出)。

(给定状态的最大最小算法的可视化。白棋最好的走法是 b2-c3,因为可以保证获取一个状态评估值是 -50)

    var minimax = function (depth, game, isMaximisingPlayer) {
        if (depth === 0) {
            return -evaluateBoard(game.board());
        }
        var newGameMoves = game.ugly_moves();
        if (isMaximisingPlayer) {
            var bestMove = -9999;
            for (var i = 0; i < newGameMoves.length; i++) {
                game.ugly_move(newGameMoves[i]);
                bestMove = Math.max(bestMove, minimax(depth - 1, game, !isMaximisingPlayer));
                game.undo();
            }
            return bestMove;
        } else {
            var bestMove = 9999;
            for (var i = 0; i < newGameMoves.length; i++) {
                game.ugly_move(newGameMoves[i]);
                bestMove = Math.min(bestMove, minimax(depth - 1, game, !isMaximisingPlayer));
                game.undo();
            }
            return bestMove;
        }
    };

有了最大最小步骤以后,我们的算法可以下出一些国际象棋的基本策略了。

极大极小算法的效率取决于搜索树的深度,这就是我们后面步骤要优化的地方。

步骤 4:α-β 剪枝

Alpha-beta 剪枝是极大极小算法的一种优化方法,可以砍掉搜索树中的某些分支。这可以帮助我们用同样的资源的情况下,尽可能深地遍历极大极小搜索树。

α-β 剪枝的原理是在遍历搜索树的过程中发现可以终止遍历的状态,进而把整个分支剪掉的过程。这是因为发现下一步会导致比上一步更糟的结果,那么就不用再遍历下去了。

α-β 剪枝不影响极大极小算法的结果,仅仅是使极大极小算法运行的更快。假设遍历时恰巧第一个状态就是最佳走法,那么 α-β 剪枝会更加有效。

有了 α-β,极大极小算法如虎添翼,可以看下面的例子。

(本图是给定的起始棋盘状态,下面的数字是如果遍历深度是 4 的话,需要评估的状态总数。)

本链接是基于 α-β 算法优化的国际象棋 AI。

步骤 5:改善评估函数

初始的评估函数非常简单,只是数了盘面上的数值而已。下面我们来改善它,把棋子的位置因素也考虑到评估结果里面去。例如在棋盘中间的马会比在棋盘边缘的马位置更好(因为它的可选择性更多,也更加活跃)。

我们来稍微调整一下棋盘上棋子状态的权重,这一图表是在国际象棋程序维基百科中给出的。

(棋盘权值表的可视化。可以根据棋子的位置增加或者减少相应位置的权重)

通过上面的一系列改进,我们的算法可以下出像样的棋局了,起码开始像一个业余棋手这样了。

(改进后的评估函数加上搜索树深度设置成 3 的 α-β 算法,可以在这个地址看到:https://jsfiddle.net/q76uzxwe/1/

总结

这个简单的国际象棋算法不会犯一些很傻的错误,但是它依然是缺乏策略理解的。

通过我所介绍的这种方法,可以开发一个国际象棋程序来实现一些基本的玩法。“AI 部分”(不包括移动生成)只有 200 行代码,也就是说这里只实现了基本的概念。你可以在 GitHub 中获取最终版本的代码。

关于算法的一些更深层次的改善可以见下面链接:

如果你想要了解更多,查看国际象棋程序维基百科,这里介绍了很多有用的资源,本文只是演示了国际象棋 AI 算法实现的基本步骤。

Happy Coding!如果本文对你有帮助,欢迎关注我的专栏-前端大哈,定期发布高质量前端文章。


我最近正在写一本《React.js 小书》,对 React.js 感兴趣的童鞋,欢迎指点


胡子大哈
1.7k 声望1.6k 粉丝

前百度高级工程师,知乎“前端大哈”专栏作者,全栈架构师。