Minimax 和 Alpha-beta 剪枝算法简介,以及以此完成的井字棋游戏(Tic-tac-toe)

前段时间用 React 写了个2048 游戏来练练手,预备用来回忆下 React 相干的种种手艺,以及实验一下新手艺。在写这个2048的历程当中,我斟酌是不是能够在个中到场一个 AI 算法来自动举行游戏,因此我找到了这篇文章:2048-AI递次算法剖析,文中引见了 minimax 算法和 alpha-beta 剪枝算法。因此我决议先进修下这两种算法,并以此写了这个 tic-tac-toe 游戏:tic-tac-toe-js代码见此处)。本文将申明怎样用 JavaScript 来简朴地完成算法,并将其运用到 tic-tac-toe 游戏中。

Minimax 算法简介

我觉得要诠释 minimax 算法的道理,须要用示意图来诠释更清晰,以下的几篇文章都对道理说的充足清晰。

  1. 2048-AI递次算法剖析
  2. Tic Tac Toe: Understanding the Minimax Algorithm
  3. An Exhaustive Explanation of Minimax, a Staple AI Algorithm

个中背面的两篇文章都是以 tic-tac-toe 游戏为例,并用 Ruby 完成。

以棋类游戏为例来申明 minimax 算法,每一个棋盘的状况都邑对应一个分数。两边将会轮番下棋。轮到我方下子时,我会挑选分数最高的状况;而对方会挑选对我最不利的状况。能够这么以为,每次我都须要从敌手给我挑选的最差(min)局势中选出最好(max)的一个,这就是这个算法称号 minimax 的意义。

《Minimax 和 Alpha-beta 剪枝算法简介,以及以此完成的井字棋游戏(Tic-tac-toe)》

(图片来自于 http://web.cs.ucla.edu/~rosen…

我们接下来会处置惩罚如许一个题目,如上图所示,正方形的节点对应于我的决议计划,圆形的节点是敌手的决议计划。两边轮番挑选一个分支,我的目的是让末了选出的数字只管大,对方的目的是让这个数字只管小。

Minimax 算法的完成

为了简朴起见,关于这个特定的题目,我用了一个嵌套的数组来示意状况树。

const dataTree = [
    [
        [
            [3, 17], [2, 12]
        ],
        [
            [15], [25, 0]
        ]
    ],
    [
        [
            [2, 5], [3]
        ],
        [
            [2, 14]
        ]
    ]
];

图中的节点分为两种范例:

  1. Max 节点:图中的正方形节点,对应于我的回合,它会拔取一切子节点中的最大值作为自身的值
  2. Min 节点:图中的圆形节点,对应于敌手的回合,它会拔取一切子节点中的最小值作为自身的值

先定义一个 Node 类,constructor 以下:

constructor(data, type, depth) {
    this.data = data;
    this.type = type; // 辨别此节点的品种是 max 或 min
    this.depth = depth;
}

根节点的 depth 为0,以下的每一层 depth 顺次加一。最底层的节点 depth 为4,其 data 是写在图中的数字,别的层节点的 data 均是一个数组。

接下来斟酌怎样给每一个节点打分,能够会涌现如许的几种状况:

  1. 最底层的节点,直接返回自身的数字
  2. 中间层的 max 节点,返回子节点中的最大分数
  3. 中间层的 min 节点,返回子节点中的最小分数

为轻易形貌,我们根据由上到下、由左到右的递次给图中节点举行标号。节点1是 max 节点,从节点2和节点3中挑选较大值;而关于节点2来讲,须要从节点4,5中拔取较小值。很显然,我们这里要用递归的要领来完成,当搜刮到最底层的节点时,递归历程最先返回。

《Minimax 和 Alpha-beta 剪枝算法简介,以及以此完成的井字棋游戏(Tic-tac-toe)》

以下是打分函数 score 的详细代码:

score() {
    // 抵达了最大深度后,此时的 data 是数组最内层的数字
    if (this.depth >= 4) {
        return this.data;
    }

    // 关于 max 节点,返回的是子节点中的最大值
    if (this.type === 'max') {
        let maxScore = -1000;

        for (let i = 0; i < this.data.length; i++) {
            const d = this.data[i];
            // 天生新的节点,子节点的 type 会和父节点差别
            const childNode = new Node(d, changeType(this.type), this.depth + 1);
            // 递归获取其分数
            const childScore = childNode.score();

            if (childScore > maxScore) {
                maxScore = childScore;
            }
        }

        return maxScore;
    }

    // 关于 min 节点,返回的是子节点中的最小值
    else if (this.type === 'min') {
        // 与上方代码相似,省略部份代码
    }
}

完全的 minimax 算法代码

Alpha-beta 剪枝算法简介

Alpha-beta 剪枝算法能够以为是 minimax 算法的一种革新,在现实的题目中,须要搜刮的状况数目将会异常巨大,运用 alpha-beta 剪枝算法能够去除一些没必要要的搜刮。

关于 alpha-beta 算法的详细诠释能够看这篇文章 Minimax with Alpha Beta Pruning。我们在前文中斟酌的那张图就来自这篇文章,以后我们会用 alpha-beta 剪枝算法来革新之前的处置惩罚方案。

剪枝算法中主要有这么些观点:

每一个节点都邑由 alpha 和 beta 两个值来肯定一个局限 [alpha, beta],alpha 值代表的是下界,beta 代表的是上界。每搜刮一个子节点,都邑按划定规矩对局限举行修正。

Max 节点能够修正 alpha 值,min 节点修正 beta 值。

假如涌现了 beta <= alpha 的状况,则没必要搜刮更多的子树了,未搜刮的这部份子树将被疏忽,这个操纵就被称作剪枝(pruning)

接下来我会只管申明为何剪枝这个操纵是合理的,省略了一部份节点为何不会对效果产生影响。用原图中以4号节点(第三层的第一个节点)为根节点的子树来举例,轻易形貌这里将他们用 A – G 的字母来从新标记。

《Minimax 和 Alpha-beta 剪枝算法简介,以及以此完成的井字棋游戏(Tic-tac-toe)》

从 B 节点看起,B 是 min 节点,须要在 D 和 E 中寻觅较小值,因此 B 取值为3,同时 B 的 beta 值也设置为 3。假定 B 另有更多值大于3的子节点,但由于已涌现了 D 这个最小值,所以不会对 B 产生影响,即这里的 beta = 3 肯定了一个上界。

.A 是 max 节点,须要在 B 和 C 中找到较大值,由于子树 B 已搜刮终了,B 的值肯定为 3,所以 A 的值最少为 3,如许肯定了 A 的下界 alpha = 3。在搜刮 C 子树之前,我们愿望 C 的值大于3,如许才会对 A 的下界 alpha 产生影响。因此 C 从 A 这里取得了下界 alpha = 3 这个限定前提。

.C 是 min 节点,要从 F 和 G 里找出较小值。F 的值为2,所以 C 的值肯定小于即是 2,更新 C 的上界 beta = 2。此时 C 的 alpha = 3, beta = 2,这是一个空区间,也就是说纵然继承斟酌 C 的别的子节点, 也不能够让 C 的值大于 3,所以我们没必要再斟酌 G 节点。G 节点就是被剪枝的节点。

反复如许的历程,会有更多的节点由于剪枝操纵被疏忽,从而对 minimax 算法举行了优化。

Alpha-beta 剪枝算法的完成

接下来议论怎样修正前面完成的 minimax 算法,使其变成 alpha-beta 剪枝算法。

第一步在 constructor 中到场两个新属性,alpha、beta。

constructor(data, type, depth, alpha, beta) {
    this.data = data;
    this.type = type; // 辨别此节点的品种是 max 或 min
    this.depth = depth;

    this.alpha = alpha || -Infinity;
    this.beta = beta || Infinity;
}

然后每次都搜刮会视状况更新 alpha, beta 的值,以下的代码片断来自于搜刮 max 节点的历程:

// alphabeta.js 中的 score() 函数

for (let i = 0; i < this.data.length; i++) {
    // ...

    if (childScore > maxScore) {
        maxScore = childScore;
        // 相关于 minimax 算法,alpha-beta 剪枝算法在这里增加了一个更新 alpha 值的操纵
        this.alpha = maxScore;
    }

    // 假如满足了退出的前提,我们不须要继承搜刮更多的节点了,退出轮回
    if (this.alpha >= this.beta) {
        break;
    }

相对应的是在 min 节点中,我们更新的将是 beta 值。好了,只须要做这么些简朴的转变,就将 minimax 算法转变成了 alpha-beta 剪枝算法了。

末了看看怎样将算法运用到 tic-tac-toe 游戏中。

完全的 alpha-beta 剪枝算法代码

Tic-tac-toe 游戏中的运用

Tic-tac-toe,即井字棋游戏,划定规矩是在两边轮番在 3×3 的棋盘上的恣意位置下子,率先将三子连成一线的一方得胜。

这就是一个异常合适用 minimax 来处置惩罚的题目,纵然在不斟酌对称的状况,一切的游戏状况也只需 9! = 362880 种,比拟于别的棋类游戏天文数字般的状况数目已很少了,因此很合适作为算法的示例。

我在代码中将棋盘的状况用一个长度为9的数组来示意,然后运用 canvas 绘制出一个浅易的棋盘,下子的历程就是修正数组的对应位置然后重绘画面。

如今我们已有了现成的 minimax 和 alpha-beta 剪枝算法,只需加上一点儿细节就可以完成这个游戏了?。

先来定义一个 GameState 类,个中保留了游戏的状况,对应于之前剖析历程当中的节点,其 constructor 以下:

constructor(board, player, depth, alpha, beta) {
    this.board = board;
    // player 是用字符 X 和 O 来标记当前由谁下子,以此来推断当前是 max 照样 min 节点
    this.playerTurn = player;
    this.depth = depth;

    // 保留分数最高或最低的状况,用于肯定下一步的棋盘状况
    this.choosenState = null;
    this.alpha = alpha || -Infinity;
    this.beta = beta || Infinity;
}

为举行游戏,起首须要一个 checkFinish 函数,搜检游戏是不是完毕,完毕时返回成功者信息。搜刮的历程是在 getScore 函数中完成的,每次搜刮先搜检游戏是不是完毕,平手返回零分,我们的算法是站在 AI 的角度来斟酌的,因此 AI 成功时返回10分,AI 败北时返回-10分。

// alphabeta.js 中的 getScore() 要领

const winner = this.checkFinish();
if (winner) {
    if (winner === 'draw') return 0;
    if (winner === aiToken) return 10;
    return -10;
}

接着是对 max 和 min 节点的分类处置惩罚:

// alphabeta.js 中的 getScore() 要领

// 取得一切能够的位置,运用 shuffle 到场随机性
const availablePos = _.shuffle(this.getAvailablePos());

// 关于 max 节点,返回的是子节点中的最大值
if (this.playerTurn === aiToken) {
    let maxScore = -1000;
    let maxIndex = 0;

    for (let i = 0; i < availablePos.length; i++) {
        const pos = availablePos[i];
        // 在给定的位置下子,天生一个新的棋盘
        const newBoard = this.generateNewBoard(pos, this.playerTurn);

        // 天生一个新的节点
        const childState = new GameState(newBoard, changeTurn(this.playerTurn), this.depth + 1, this.alpha, this.beta);
        // 这里最先递归挪用 getScore() 函数
        const childScore = childState.getScore();

        if (childScore > maxScore) {
            maxScore = childScore;
            maxIndex = i;
            // 这里保留产生了最大的分数的节点,以后会被用于举行下一步
            this.choosenState = childState;
            this.alpha = maxScore;
        }

        if (this.alpha >= this.beta) {
            break;
        }
    }

    return maxScore;
}

// min 节点的处置惩罚与上面相似
// ...

完全代码见
alphabeta.js

总结

如许就简朴地引见了 minimax 算法和 alpha-beta 算法,并离别给出了一个简朴的完成,然后在 tic-tac-toe 游戏中运用了算法。

文章中所提到的一切代码可见此项目:Tic-tac-toe-js。个中的 algorithms 文件夹中是两种算法的简朴完成,src 文件中是游戏的代码。

文章开首说到了这篇文章起源于写2048游戏项目的历程当中,以后我将 minimax 算法运用到了2048游戏的 AI 中,不过关于局势的评价函数尚不完美,如今 AI 只能委曲合成1024?, 另有很大的革新空间。

本文原链接

    原文作者:noiron
    原文地址: https://segmentfault.com/a/1190000013527949
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞