五子棋AI算法第二篇-极大极小值搜索算法

AI实现的基本思路-极大极小值搜索算法

五子棋AI教程第二版发布啦,地址:https://github.com/lihongxun945/myblog/labels/%E4%BA%94%E5%AD%90%E6%A3%8BAI%E6%95%99%E7%A8%8B%E7%AC%AC%E4%BA%8C%E7%89%88

当前这个是旧版教程,强烈建议阅读新版教程。

五子棋看起来有各种各样的走法,而实际上把每一步的走法展开,就是一颗巨大的博弈树。在这个树中,从根节点为0开始,奇数层表示电脑可能的走法,偶数层表示玩家可能的走法。

假设电脑先手,那么第一层就是电脑的所有可能的走法,第二层就是玩家的所有可能走法,以此类推。

我们假设平均每一步有50种可能的走法,那么从根节点开始,往下面每一层的节点数量是上一层的 50被,假设我们进行4层思考,也就是电脑和玩家各走两步,那么这颗博弈树的最后一层的节点数为 50^4 = 625W 个。

先不考虑这么多个节点需要多久能算出来。有了对博弈树的基本认识,我们就可以用递归来遍历这一棵树。

那么我们如何才能知道哪一个分支的走法是最优的,我们就需要一个评估函数能对当前整个局势作出评估,返回一个分数。我们规定对电脑越有利,分数越大,对玩家越有利,分数越小,分数的起点是0。

我们遍历这颗博弈树的时候就很明显知道该如何选择分支了:

  • 电脑走棋的层我们称为 MAX层,这一层电脑要保证自己利益最大化,那么就需要选分最高的节点。
  • 玩家走棋的层我们称为MIN层,这一层玩家要保证自己的利益最大化,那么就会选分最低的节点。

这也就是极大极小值搜索算法的名称由来。直接盗一个图说明这个问题:

极大极小值搜索树

此图中甲是电脑,乙是玩家,那么在甲层的时候,总是选其中值最大的节点,乙层的时候,总是选其中最小的节点。

而每一个节点的分数,都是由子节点决定的,因此我们对博弈树只能进行深度优先搜索而无法进行广度优先搜索。深度优先搜索用递归非常容易实现,然后主要工作其实是完成一个评估函数,这个函数需要对当前局势给出一个比较准确的评分。

这篇博客讲的五子棋算法已经全部用JS实现并且是开源的,源码地址: https://github.com/lihongxun945/gobang ,预览地址: http://gobang.light7.cn/

可以clone这个仓库,参考其中js目录下的源码。因为代码量不小,后面要讲的只是关键部分的代码实现,不会贴完整的代码。而且关键代码可能会省略部分实现,UI因为跟AI算法无关,也不会讲,请自行参考源码。

我们下面来分部实现其中几个关键的部分。

极大极小值搜索

五子棋是一个15*15的棋盘,因为棋盘大小不会变动,所以目前来看用 15*15 的二维数组来存储,效果是最好的。

极大极小值的搜索比较简单,就是一个DFS,直接上代码,看不懂的可以

var maxmin = function(board, deep) {
  var best = MIN;
  var points = gen(board, deep);     //这个函数的作用是生成待选的列表,就是可以下子的空位。后面会讲这个方法。

  for(var i=0;i<points.length;i++) {
    var p = points[i];
    board[p[0]][p[1]] = R.com;     //尝试下一个子
    var v = min(board, deep-1);     //找最大值
    //如果跟之前的一个好,则把当前位子加入待选位子
    if(v == best) {
      bestPoints.push(p);
    }
    //找到一个更好的分,就把以前存的位子全部清除
    if(v > best) {
      best = v;
      bestPoints = [];
      bestPoints.push(p);
    }
    board[p[0]][p[1]] = R.empty;     //记得把尝试下的子移除
  }
  var result = bestPoints[Math.floor(bestPoints.length * Math.random())]; //在分数最高的几个位置中随机选择一个
  return result;
}

var min = function(board, deep) {
  var v = evaluate(board);     //重点来了,评价函数,这个函数返回的是对当前局势的估分。
  if(deep <= 0 || win(board)) {
    return v;
  }

  var best = MAX;
  var points = gen(board, deep);

  for(var i=0;i<points.length;i++) {
    var p = points[i];
    board[p[0]][p[1]] = R.hum;
    var v = max(board, deep-1);
    board[p[0]][p[1]] = R.empty;
    if(v < best ) {
      best = v;
    }
  }
  return best ;
}

var max = function(board, deep) {
  var v = evaluate(board);
  if(deep <= 0 || win(board)) {
    return v;
  }

  var best = MIN;
  var points = gen(board, deep);

  for(var i=0;i<points.length;i++) {
    var p = points[i];
    board[p[0]][p[1]] = R.com;
    var v = min(board, deep-1);
    board[p[0]][p[1]] = R.empty;
    if(v > best) {
      best = v;
    }
  }
  return best;
}

源码里面是有 Alpha Beta剪枝的,不过剪枝代码其实就几行,可以直接看源码,在 js/max-min.js 里面。

评估函数

在源码中 js/evaluate.js 就是评估函数,它接受一个二维数组,返回一个整型数。

我们对五子棋的评分是简单的把棋盘上的各种连子的分值加起来得到的,对各种连子的基本评分规则如下:

  • 成五,100000
  • 活四, 10000
  • 活三 1000
  • 活二 100
  • 活一 10

如果一侧被封死但是另一侧没有,则评分降一个档次,也就是死四和活三是相同的分

  • 死四, 1000
  • 死三 100
  • 死二 10

score.js 中是对各种情况估值的定义。

按照这个规则把棋盘上电脑的所有棋子打分,之和即为电脑的单方面得分 scoreComputer,然后对玩家的棋子同样打分 得到 scoreHuman

scoreComputer - scoreHuman 即为当前局势的总分数。

那么如何找出所有的连子呢,目前我的方式是先把二维的棋盘按照横竖撇捺四个方向变成N个一维数组,这个过程称为 flat。flat.js 是一个专门实现这个功能的模块。
然后我们就可以对所有的一维数组进行得分计算。
评分函数代码领比较大,请自行参考 evalute.js

请注意,这里我们说的评估函数是对整个棋局的评估,后面讲启发式搜索的时候还会有一个启发式评估函数是对某一个位置的评估。这两个评估函数是不同的

generator函数

generator顾名思义,就是在每一步生成所有可以落子的点。并不是所有的空位我们又要搜索,很多位置明显不合适的我们可以直接排除。
这个函数非常重要,是优化性能的关键。不过目前我们只做一个最简单的实现。就是把所有周围有邻居的空位认为是合适的位子。

有邻居的定义是:想个两步以内至少有一个不为空的点即可。比如 b[7,7] 有一个子,那么 b[6,7]是他的邻居,b[5,7] 也是,但是 b[4,7] 就不是,因为相隔了三步。

var gen = function(board, deep) {

  var neighbors = [];
  var nextNeighbors = [];

  for(var i=0;i<board.length;i++) {
    for(var j=0;j<board[i].length;j++) {
      if(board[i][j] == R.empty) {
        if(hasNeighbor(board, [i, j], 1, 1)) { //必须是有邻居的才行
            neighbors.push([i, j]);
        } else if(deep >= 2 && hasNeighbor(board, [i, j], 2, 2)) {
          nextNeighbors.push([i, j]);
        }
      }
    }
  }
  return  neighbors.concat(nextNeighbors)
}

这里做了一个简单的优化,就是按照邻居的远近做了一个排序,为什么要排序。后面讲AB剪枝的时候会讲到。排序对AB剪枝的效率起了决定性作用。

另外一些简单的函数,没有列出,直接去看源码就行了

下面我们将讲一个重点: Alpha Beta 剪枝算法。

  • 51
    点赞
  • 253
    收藏
    觉得还不错? 一键收藏
  • 16
    评论
极大极小值算法(Minimax Algorithm)是一种常用于博弈游戏的人工智能算法,可以用于实现五子棋游戏的AI。下面是一个基于C语言的简单实现示例: ```c // 定义棋盘大小 #define SIZE 15 // 定义玩家和AI的标记 #define PLAYER 'X' #define AI 'O' // 定义最大搜索深度 #define MAX_DEPTH 6 // 评估函数,用于评估当前局面得分 int evaluate(char board[SIZE][SIZE]) { // TODO: 根据当前棋盘状态评估得分 return 0; } // 极大极小值搜索算法 int minimax(char board[SIZE][SIZE], int depth, int isMaxPlayer) { // 终止条件:达到设定的搜索深度或者游戏结束 if (depth == MAX_DEPTH || isGameOver(board)) { return evaluate(board); } int bestScore; if (isMaxPlayer) { bestScore = INT_MIN; for (int i = 0; i < SIZE; i++) { for (int j = 0; j < SIZE; j++) { if (board[i][j] == ' ') { board[i][j] = AI; int score = minimax(board, depth + 1, 0); board[i][j] = ' '; bestScore = max(bestScore, score); } } } } else { bestScore = INT_MAX; for (int i = 0; i < SIZE; i++) { for (int j = 0; j < SIZE; j++) { if (board[i][j] == ' ') { board[i][j] = PLAYER; int score = minimax(board, depth + 1, 1); board[i][j] = ' '; bestScore = min(bestScore, score); } } } } return bestScore; } // AI下棋的函数 void makeMove(char board[SIZE][SIZE]) { int bestScore = INT_MIN; int bestRow, bestCol; for (int i = 0; i < SIZE; i++) { for (int j = 0; j < SIZE; j++) { if (board[i][j] == ' ') { board[i][j] = AI; int score = minimax(board, 0, 0); board[i][j] = ' '; if (score > bestScore) { bestScore = score; bestRow = i; bestCol = j; } } } } board[bestRow][bestCol] = AI; } // 判断游戏是否结束 int isGameOver(char board[SIZE][SIZE]) { // TODO: 判断游戏是否结束的逻辑 return 0; } ``` 以上是一个简单的极大极小值算法的实现示例,其中evaluate函数用于评估当前局面得分,isGameOver函数用于判断游戏是否结束。在makeMove函数中,AI会根据当前棋盘状态使用极大极小值算法来选择最佳下棋位置。注意,该示例只是一个基本框架,具体的评估函数和游戏结束判断逻辑需要根据实际需求进行实现。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 16
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值