Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Open
lihongxun945 opened this issue Jul 16, 2018 · 6 comments
Open

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

lihongxun945 opened this issue Jul 16, 2018 · 6 comments

Comments

@lihongxun945
Copy link
Owner

lihongxun945 commented Jul 16, 2018

需要算杀吗?

算杀其实是很多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设计教程第二版九:性能优化

@lihongxun945 lihongxun945 changed the title 五子棋AI教程第二版八:性能优化 五子棋AI教程第二版八:算杀 Jul 16, 2018
@LaoMai
Copy link

LaoMai commented Aug 1, 2018

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

@lihongxun945
Copy link
Owner Author

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

@JoeXu1997
Copy link

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

@unlsycn
Copy link

unlsycn commented Jan 8, 2020

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

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

@lihongxun945
Copy link
Owner Author

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

@Rui-Wang-813
Copy link

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

5 participants