五子棋AI教程第二版八:算杀
lihongxun945 opened this issue · 6 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层的算杀,能达到一个比较理想的棋力。
你好,为什么我在具体的代码里没有找到哪里调用了算杀?
@LaoMai 我的代码中并没有使用算杀模块。
大佬你好。。。对于类似的这种棋类AI是不是不用机器学习技术就没有办法规避相同错误?就是相同的下棋顺序会导致完全相同的棋局
大佬你好。。。对于类似的这种棋类AI是不是不用机器学习技术就没有办法规避相同错误?就是相同的下棋顺序会导致完全相同的棋局
是的,传统棋类算法都是这样
@JoeXu1997 是的,传统基于搜索的方法是没有学习能力的,不过可以加一些随机机制增加趣味性
想要规避这种可以用神经网络嘛。这个算法明显是deterministic的。