lihongxun945 / myblog

言川的博客-前端工程师的笔记

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

五子棋AI教程第二版八:算杀

lihongxun945 opened this issue · comments

需要算杀吗?

算杀其实是很多AI都带有的功能,简单的说,正常的搜索会搜索所有的可能性,而算杀只计算活三和冲四的节点,由于每一层的分支变得更少,因此算杀可以很容易实现较大的深度,一般都可以搜索到 12层以上。

在第一版教程中我是在叶节点算杀的,用的是 6 + 5 的方式,也就是正常搜索到6层,然后在叶节点进行5层的算杀,把整体深度提升到 11 层。实际测试的时候经常会出现需要计算 60 秒以上的棋。因为每个叶节点都进行算杀还是很慢的,特别是中盘容易碰到一些有很多冲四活三的局面,算杀会变得非常慢。

在新版本中,我直接抛弃了算杀模块,代码依然存在,但是并没有使用。不过其实我更建议另一种做法,算杀模块作为搜索模块的一个补充,而不是混在一起使用,就是在正常搜索完毕,且没有搜索到必胜局面的时候,进行一次较大深度的算杀(或者在之前也行),由于单次算杀的时间很短,几乎不会影响电脑的思考时间,并且能得到棋力的提升。估计在后序我会加上这个额外的算杀。

克服水平线效应

所谓算杀就是计算出杀棋,杀棋就是指一方通过连续的活三和冲四进行进攻,一直到赢的一种走法。我们一般会把算杀分为两种:

  • VCF (victory of continuous four) 连续冲四胜
  • VCT (victory of continuous three) 连续活三胜

一般在算杀的时候,我们优先进行 VCT,没有找到结果的时候再进行 VCF。很显然,同样的深度,算杀要比前面讲的搜索效率高很多。因为算杀的情况下,每个节点只计算活三和冲四的子节点。所以同样是1秒钟的时间,搜索只能进行4层,而算杀很多时候可以进行到12层以上。

为了方便,我们把前面的讲到全面的极大极小值搜索简称为搜索

而且很容易想到,算杀其实也是一种极大极小值搜索,具体的策略是这样的:

  • MAX层,只搜索己方的活三和冲四节点,只要有一个子节点的能赢即可
  • MIN 层,搜索所有的己方和对面的活三和冲四节点(进攻也是防守),只要有一个子节点能保持不败即可。

算杀的实现代码也比较长,这里贴出部分关键代码,完整代码请参阅 https://github.com/lihongxun945/gobang:

vcx.js

//找到所有比目标分数大的位置
//注意,不止要找自己的,还要找对面的,
var findMax = function(role, score) {
  var result = [],
      fives = [];
  for(var i=0;i<board.board.length;i++) {
    for(var j=0;j<board.board[i].length;j++) {
      if(board.board[i][j] == R.empty) {
        var p = [i, j];
        // 省略部分代码
        var s = (role == R.com ? board.comScore[p[0]][p[1]] : board.humScore[p[0]][p[1]]);
        p.score = s;
        if(s >= score) {
          result.push(p);
        }
      }
    }
  }
  // 能连五,则直接返回
  // 但是注意不要碰到连五就返回,而是把所有连五的点都考虑一遍,不然可能出现自己能连却防守别人的问题
  if (fives.length) return fives;
  //注意对结果进行排序
  result.sort(function(a, b) {
    return b.score - a.score;
  });
  return result;
}


// MIN层
//找到所有比目标分数大的位置
//这是MIN层,所以己方分数要变成负数
var findMin = function(role, score) {
  var result = [];
  var fives = [];
  var fours = [];
  var blockedfours = [];
  for(var i=0;i<board.board.length;i++) {
    for(var j=0;j<board.board[i].length;j++) {
      if(board.board[i][j] == R.empty) {
        var p = [i, j];

        var s1 = (role == R.com ? board.comScore[p[0]][p[1]] : board.humScore[p[0]][p[1]]);
        var s2 = (role == R.com ? board.humScore[p[0]][p[1]] : board.comScore[p[0]][p[1]]);
        if(s1 >= S.FIVE) {
          p.score = - s1;
          return [p];
        }

        if(s2 >= S.FIVE) {
          p.score = s2;
          fives.push(p);
          continue;
        }

       // 省略,其他分数的判断
      }
    }
  }
  if(fives.length) return fives;

  // 注意冲四,因为虽然冲四的分比活四低,但是他的防守优先级是和活四一样高的,否则会忽略冲四导致获胜的走法
  if(fours.length) return fours.concat(blockedfours);

  //注意对结果进行排序
  //因为fours可能不存在,这时候不要忽略了 blockedfours
  result = blockedfours.concat(result);
  result.sort(function(a, b) {
    return Math.abs(b.score) - Math.abs(a.score);
  });
  return result;
}

var max = function(role, deep, totalDeep) {
  debugNodeCount ++;
  //board.logSteps();
  if(deep <= 1) return false;

  var points = findMax(role, MAX_SCORE);
  if(points.length && points[0].score >= S.FOUR) return [points[0]]; //为了减少一层搜索,活四就行了。
  if(points.length == 0) return false;
  for(var i=0;i<points.length;i++) {
    var p = points[i];
    board.put(p, role);
    // 如果是防守对面的冲四,那么不用记下来
    if (!p.score <= -S.FIVE) lastMaxPoint = p;
    var m = min(R.reverse(role), deep-1);
    board.remove(p);
    if(m) {
      if(m.length) {
        m.unshift(p); //注意 unshift 方法返回的是新数组长度,而不是新数组本身
        return m;
      } else {
        return [p];
      }
    }
  }
  return false;
}


//只要有一种方式能防守住,就可以了
var min = function(role, deep) {
  debugNodeCount ++;
  var w = board.win();
  //board.logSteps();
  if(w == role) return false;
  if(w == R.reverse(role)) return true;
  if(deep <= 1) return false;
  var points = findMin(role, MIN_SCORE);
  if(points.length == 0) return false;
  if(points.length && -1 * points[0].score  >= S.FOUR) return false; //为了减少一层搜索,活四就行了。

  var cands = [];
  for(var i=0;i<points.length;i++) {
    var p = points[i];
    board.put(p, role);
    lastMinPoint = p;
    var m = max(R.reverse(role), deep-1);
    board.remove(p);
    if(m) {
      m.unshift(p);
      cands.push(m);
      continue;
    } else {
      return false; //只要有一种能防守住
    }
  }
  var result = cands[Math.floor(cands.length*Math.random())];  //无法防守住
  return result;
}



//迭代加深
var deeping = function(role, deep, totalDeep) {
  // 省略迭代加深代码
}

var vcx = function(role, deep, onlyFour) {

  deep = deep === undefined ? config.vcxDeep : deep;

  if(deep <= 0) return false;

  if (onlyFour) {
    //计算冲四赢的
    MAX_SCORE = S.BLOCKED_FOUR;
    MIN_SCORE = S.FIVE;

    var result = deeping(role, deep, deep);
    if(result) {
      result.score = S.FOUR;
      return result;
    }
    return false
  } else {
    //计算通过 活三 赢的;
    MAX_SCORE = S.THREE;
    MIN_SCORE = S.BLOCKED_FOUR;
    result = deeping(role, deep, deep);
    if(result) {
      result.score = S.THREE*2; //连续冲三赢,就等于是双三
    }

    return result;
  }

  return false;

}

// 连续冲四
var vcf = function (role, deep) {
  var c = getCache(true);
  if (c) return c;
  var result = vcx(role, deep, true);
  cache(result, true);
  return result;
}

// 连续活三
var vct = function (role, deep) {
  var c = getCache();
  if (c) return c;
  var result = vcx(role, deep, false);
  cache(result);
  return result;
}

export default {
  vct: vct,
  vcf: vcf
}

这样我们正常进行 8 层搜索,如果没有好的结果则进行约 12层的算杀,能达到一个比较理想的棋力。

下一篇:五子棋AI设计教程第二版九:性能优化

你好,为什么我在具体的代码里没有找到哪里调用了算杀?

@LaoMai 我的代码中并没有使用算杀模块。

大佬你好。。。对于类似的这种棋类AI是不是不用机器学习技术就没有办法规避相同错误?就是相同的下棋顺序会导致完全相同的棋局

大佬你好。。。对于类似的这种棋类AI是不是不用机器学习技术就没有办法规避相同错误?就是相同的下棋顺序会导致完全相同的棋局

是的,传统棋类算法都是这样

@JoeXu1997 是的,传统基于搜索的方法是没有学习能力的,不过可以加一些随机机制增加趣味性

想要规避这种可以用神经网络嘛。这个算法明显是deterministic的。