需要算杀吗?

算杀其实是很多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

  1. //找到所有比目标分数大的位置
  2. //注意,不止要找自己的,还要找对面的,
  3. var findMax = function(role, score) {
  4. var result = [],
  5. fives = [];
  6. for(var i=0;i<board.board.length;i++) {
  7. for(var j=0;j<board.board[i].length;j++) {
  8. if(board.board[i][j] == R.empty) {
  9. var p = [i, j];
  10. // 省略部分代码
  11. var s = (role == R.com ? board.comScore[p[0]][p[1]] : board.humScore[p[0]][p[1]]);
  12. p.score = s;
  13. if(s >= score) {
  14. result.push(p);
  15. }
  16. }
  17. }
  18. }
  19. // 能连五,则直接返回
  20. // 但是注意不要碰到连五就返回,而是把所有连五的点都考虑一遍,不然可能出现自己能连却防守别人的问题
  21. if (fives.length) return fives;
  22. //注意对结果进行排序
  23. result.sort(function(a, b) {
  24. return b.score - a.score;
  25. });
  26. return result;
  27. }
  28. // MIN层
  29. //找到所有比目标分数大的位置
  30. //这是MIN层,所以己方分数要变成负数
  31. var findMin = function(role, score) {
  32. var result = [];
  33. var fives = [];
  34. var fours = [];
  35. var blockedfours = [];
  36. for(var i=0;i<board.board.length;i++) {
  37. for(var j=0;j<board.board[i].length;j++) {
  38. if(board.board[i][j] == R.empty) {
  39. var p = [i, j];
  40. var s1 = (role == R.com ? board.comScore[p[0]][p[1]] : board.humScore[p[0]][p[1]]);
  41. var s2 = (role == R.com ? board.humScore[p[0]][p[1]] : board.comScore[p[0]][p[1]]);
  42. if(s1 >= S.FIVE) {
  43. p.score = - s1;
  44. return [p];
  45. }
  46. if(s2 >= S.FIVE) {
  47. p.score = s2;
  48. fives.push(p);
  49. continue;
  50. }
  51. // 省略,其他分数的判断
  52. }
  53. }
  54. }
  55. if(fives.length) return fives;
  56. // 注意冲四,因为虽然冲四的分比活四低,但是他的防守优先级是和活四一样高的,否则会忽略冲四导致获胜的走法
  57. if(fours.length) return fours.concat(blockedfours);
  58. //注意对结果进行排序
  59. //因为fours可能不存在,这时候不要忽略了 blockedfours
  60. result = blockedfours.concat(result);
  61. result.sort(function(a, b) {
  62. return Math.abs(b.score) - Math.abs(a.score);
  63. });
  64. return result;
  65. }
  66. var max = function(role, deep, totalDeep) {
  67. debugNodeCount ++;
  68. //board.logSteps();
  69. if(deep <= 1) return false;
  70. var points = findMax(role, MAX_SCORE);
  71. if(points.length && points[0].score >= S.FOUR) return [points[0]]; //为了减少一层搜索,活四就行了。
  72. if(points.length == 0) return false;
  73. for(var i=0;i<points.length;i++) {
  74. var p = points[i];
  75. board.put(p, role);
  76. // 如果是防守对面的冲四,那么不用记下来
  77. if (!p.score <= -S.FIVE) lastMaxPoint = p;
  78. var m = min(R.reverse(role), deep-1);
  79. board.remove(p);
  80. if(m) {
  81. if(m.length) {
  82. m.unshift(p); //注意 unshift 方法返回的是新数组长度,而不是新数组本身
  83. return m;
  84. } else {
  85. return [p];
  86. }
  87. }
  88. }
  89. return false;
  90. }
  91. //只要有一种方式能防守住,就可以了
  92. var min = function(role, deep) {
  93. debugNodeCount ++;
  94. var w = board.win();
  95. //board.logSteps();
  96. if(w == role) return false;
  97. if(w == R.reverse(role)) return true;
  98. if(deep <= 1) return false;
  99. var points = findMin(role, MIN_SCORE);
  100. if(points.length == 0) return false;
  101. if(points.length && -1 * points[0].score >= S.FOUR) return false; //为了减少一层搜索,活四就行了。
  102. var cands = [];
  103. for(var i=0;i<points.length;i++) {
  104. var p = points[i];
  105. board.put(p, role);
  106. lastMinPoint = p;
  107. var m = max(R.reverse(role), deep-1);
  108. board.remove(p);
  109. if(m) {
  110. m.unshift(p);
  111. cands.push(m);
  112. continue;
  113. } else {
  114. return false; //只要有一种能防守住
  115. }
  116. }
  117. var result = cands[Math.floor(cands.length*Math.random())]; //无法防守住
  118. return result;
  119. }
  120. //迭代加深
  121. var deeping = function(role, deep, totalDeep) {
  122. // 省略迭代加深代码
  123. }
  124. var vcx = function(role, deep, onlyFour) {
  125. deep = deep === undefined ? config.vcxDeep : deep;
  126. if(deep <= 0) return false;
  127. if (onlyFour) {
  128. //计算冲四赢的
  129. MAX_SCORE = S.BLOCKED_FOUR;
  130. MIN_SCORE = S.FIVE;
  131. var result = deeping(role, deep, deep);
  132. if(result) {
  133. result.score = S.FOUR;
  134. return result;
  135. }
  136. return false
  137. } else {
  138. //计算通过 活三 赢的;
  139. MAX_SCORE = S.THREE;
  140. MIN_SCORE = S.BLOCKED_FOUR;
  141. result = deeping(role, deep, deep);
  142. if(result) {
  143. result.score = S.THREE*2; //连续冲三赢,就等于是双三
  144. }
  145. return result;
  146. }
  147. return false;
  148. }
  149. // 连续冲四
  150. var vcf = function (role, deep) {
  151. var c = getCache(true);
  152. if (c) return c;
  153. var result = vcx(role, deep, true);
  154. cache(result, true);
  155. return result;
  156. }
  157. // 连续活三
  158. var vct = function (role, deep) {
  159. var c = getCache();
  160. if (c) return c;
  161. var result = vcx(role, deep, false);
  162. cache(result);
  163. return result;
  164. }
  165. export default {
  166. vct: vct,
  167. vcf: vcf
  168. }

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

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