象棋和五子棋的AI开发(四)

四、五子棋AI算法

相较于象棋,五子棋的走法相当简单,也就是空白的地方都可以走。但五子棋的棋局判断,也就是估值函数比较复杂。另外,在一开始的时候,五子棋的可走步数是254、252、250,而象棋基本都是40多,所以五子棋每一层的运算量都会更大。后面我们会说到一些五子棋的优化方法。

4.1 估值核心

这是五子棋AI算法里面最复杂的一部分。五子棋的得分是基于线的,这些线可以是横的、竖的,也可以是斜的。为了计算方便,我们一开始就初始化了所有的这些线:

  1. internal class Position
  2. {
  3. public Position(int r, int c)
  4. {
  5. row = r;
  6. col = c;
  7. }
  8. public int row;
  9. public int col;
  10. }
  11. internal class Line
  12. {
  13. public Line()
  14. {
  15. line = new List<Position>();
  16. score白 = 0;
  17. score黑 = 0;
  18. }
  19. public List<Position> line;
  20. public int score白;
  21. public int score黑;
  22. }
  23. LineList = new List<Line>();
  24. //横
  25. for (int i = 0; i < Row; i++)
  26. {
  27. Line l = new Line();
  28. LineList.Add(l);
  29. for (int j = 0; j < Col; j++)
  30. {
  31. l.line.Add(new Position(i, j));
  32. }
  33. }
  34. //竖
  35. for (int i = 0; i < Col; i++)
  36. {
  37. Line l = new Line();
  38. LineList.Add(l);
  39. for (int j = 0; j < Row; j++)
  40. {
  41. l.line.Add(new Position(j, i));
  42. }
  43. }
  44. //右斜
  45. for (int i = Row - 1; i >= 1 - Col; i--)
  46. {
  47. Line l = new Line();
  48. LineList.Add(l);
  49. int r = i >= 0 ? i : 0;
  50. int c = i < 0 ? -i : 0;
  51. while (r < Row && c < Col)
  52. {
  53. l.line.Add(new Position(r, c));
  54. r++;
  55. c++;
  56. }
  57. }
  58. //左斜
  59. for (int i = 0; i <= Row + Col - 2; i++)
  60. {
  61. Line l = new Line();
  62. LineList.Add(l);
  63. int r = i < Col ? 0 : i - Col + 1;
  64. int c = i >= Col ? Col - 1 : i;
  65. while (r < Row && c >= 0)
  66. {
  67. l.line.Add(new Position(r, c));
  68. r++;
  69. c--;
  70. }
  71. }

这样处理的话,我们后面寻找一行有多少个子不需要再分几种情况去处理了。

然后我们需要做的,就是分析一条线上,各种子排成的序列,应该值多少分。我们用_代表空,用O代表白子,用X代表黑子,如果一行是这样______XXOO____OXO___OOOX___XOOX__,那白子应该是多少分,黑子又是多少分呢?

我考虑了几种方案,有必要在这里说一下思路。一开始,我想统计出有多少种XOOO、_OOO、XOOOO、_OOOO这样的模式,累加作为分数。但我马上发现,如果是OOO__和OOO_O,或是OOO__O,或是OOO_X,它们的分值应该都是不一样的吧。

由于把不同颜色的棋子混在一起考虑比较复杂,我想先扫描一次整个串,然后分成同一颜色的段(空白属于所有颜色)。例如一开始的例子可以这样分:______XX、OO____O、X、O___OOO、X___X、OO、X__。这样对于每一段的分析就会简单一些。我考虑一个这样的问题:如果整个串是O_OOO_OOO,那应该怎么切分呢?是O_OOO + _OOO?还是O_O + OOO_OOO?我认为这里涉及到一个动态规划的问题。就是一个串,按不同的切分方法,能得到的最高分的组合。但是这样算下来的话,每一行需要的计算时间都会很长。我只能考虑使用贪婪算法。也就是说,先挑出最高分的序列,然后在剩下的序列里找出最高分的序列,直到剩下序列分数为0。

对于每一种序列,我对他们的分数安排是这样的:

象棋和五子棋的AI开发(四)

计算一条线分数的核心代码如下:

  1. private void CalcLineScore(Line l)
  2. {
  3. Chess pre_chess = new Chess(ChessColor.无, ChessType.有);
  4. List<bool> list = new List<bool>();
  5. List<bool> last_space = new List<bool>();
  6. int list_type = 0;
  7. int score白 = 0;
  8. int score黑 = 0;
  9. for (int j = 0; j < l.line.Count; j++)
  10. {
  11. Position p = l.line[j];
  12. Chess c = board[p.row, p.col];
  13. if (c.type == ChessType.空)
  14. {
  15. list.Add(false);
  16. last_space.Add(false);
  17. }
  18. else
  19. {
  20. if (pre_chess.color == ChessColor.无)
  21. {
  22. list.Add(true);
  23. list_type = (c.color == ChessColor.白 ? 1 : 2);
  24. pre_chess = c;
  25. }
  26. else if (c.color == pre_chess.color)
  27. {
  28. list.Add(true);
  29. }
  30. else
  31. {
  32. if (list_type != 0)
  33. {
  34. int value = GetSequenceValue(list);
  35. if (list_type == 1)
  36. {
  37. score白 += value;
  38. }
  39. else
  40. {
  41. score黑 += value;
  42. }
  43. }
  44. list.Clear();
  45. list.AddRange(last_space);
  46. list.Add(true);
  47. list_type = (c.color == ChessColor.白 ? 1 : 2);
  48. pre_chess = c;
  49. }
  50. last_space.Clear();
  51. }
  52. }
  53. if (list_type != 0)
  54. {
  55. int value = GetSequenceValue(list);
  56. if (list_type == 1)
  57. {
  58. score白 += value;
  59. }
  60. else
  61. {
  62. score黑 += value;
  63. }
  64. }
  65. l.score白 = score白;
  66. l.score黑 = score黑;
  67. }

4.2 终局判断

五子棋的终局判断并不简单,需要把所有线都扫描一遍。因为终局判断调用频率极高,它的性能对于整个算法的性能影响是很大的。其实,终局只会由最终下棋所在的线决定,我们只需要计算四条线就可以了!

  1. public override bool GameOver(Put[] pre_steps)
  2. {
  3. if (pre_steps == null)
  4. {
  5. return false;
  6. }
  7. Put step = pre_steps[pre_steps.Length - 1];
  8. ChessColor c = board[step.ToRow, step.ToCol].color;
  9. int num = 1;
  10. int row = step.ToRow;
  11. int col = step.ToCol;
  12. while (row > 0)
  13. {
  14. row--;
  15. if (board[row, col].type != ChessType.空 && board[row, col].color == c)
  16. {
  17. num++;
  18. if (num >= 5)
  19. {
  20. wincolor = c;
  21. return true;
  22. }
  23. }
  24. else
  25. {
  26. break;
  27. }
  28. }
  29. row = step.ToRow;
  30. while (row < Row - 1)
  31. {
  32. row++;
  33. if (board[row, col].type != ChessType.空 && board[row, col].color == c)
  34. {
  35. num++;
  36. if (num >= 5)
  37. {
  38. wincolor = c;
  39. return true;
  40. }
  41. }
  42. else
  43. {
  44. break;
  45. }
  46. }
  47. num = 1;
  48. row = step.ToRow;
  49. col = step.ToCol;
  50. while (col > 0)
  51. {
  52. col--;
  53. if (board[row, col].type != ChessType.空 && board[row, col].color == c)
  54. {
  55. num++;
  56. if (num >= 5)
  57. {
  58. wincolor = c;
  59. return true;
  60. }
  61. }
  62. else
  63. {
  64. break;
  65. }
  66. }
  67. col = step.ToCol;
  68. while (col < Col - 1)
  69. {
  70. col++;
  71. if (board[row, col].type != ChessType.空 && board[row, col].color == c)
  72. {
  73. num++;
  74. if (num >= 5)
  75. {
  76. wincolor = c;
  77. return true;
  78. }
  79. }
  80. else
  81. {
  82. break;
  83. }
  84. }
  85. num = 1;
  86. row = step.ToRow;
  87. col = step.ToCol;
  88. while (row > 0 && col > 0)
  89. {
  90. row--;
  91. col--;
  92. if (board[row, col].type != ChessType.空 && board[row, col].color == c)
  93. {
  94. num++;
  95. if (num >= 5)
  96. {
  97. wincolor = c;
  98. return true;
  99. }
  100. }
  101. else
  102. {
  103. break;
  104. }
  105. }
  106. row = step.ToRow;
  107. col = step.ToCol;
  108. while (row < Row - 1 && col < Col - 1)
  109. {
  110. row++;
  111. col++;
  112. if (board[row, col].type != ChessType.空 && board[row, col].color == c)
  113. {
  114. num++;
  115. if (num >= 5)
  116. {
  117. wincolor = c;
  118. return true;
  119. }
  120. }
  121. else
  122. {
  123. break;
  124. }
  125. }
  126. num = 1;
  127. row = step.ToRow;
  128. col = step.ToCol;
  129. while (row > 0 && col < Col - 1)
  130. {
  131. row--;
  132. col++;
  133. if (board[row, col].type != ChessType.空 && board[row, col].color == c)
  134. {
  135. num++;
  136. if (num >= 5)
  137. {
  138. wincolor = c;
  139. return true;
  140. }
  141. }
  142. else
  143. {
  144. break;
  145. }
  146. }
  147. row = step.ToRow;
  148. col = step.ToCol;
  149. while (row < Row - 1 && col > 0)
  150. {
  151. row++;
  152. col--;
  153. if (board[row, col].type != ChessType.空 && board[row, col].color == c)
  154. {
  155. num++;
  156. if (num >= 5)
  157. {
  158. wincolor = c;
  159. return true;
  160. }
  161. }
  162. else
  163. {
  164. break;
  165. }
  166. }
  167. return false;
  168. }

4.3 每层搜索数量的优化

五子棋一般使用15*15的棋盘,也就是说有225个格子。一开始,每一层的搜索数量是224、222、220,即使使用了历史启发,搜索的数量还是相当巨大。

其实我们可以用一个最小矩形来优化。假如敌方第一步把棋子下在了中心(7,7),那最优的下法会出现在哪里呢?一般就是在这棋子的四周。假如我们把所有棋子都圈在一个矩形里面,然后适当考虑一个阈值(这里我们定的是3),在这个阈值之外的棋子根本就不需要考虑。这样我们的计算速度能得到极大的提高。例如在只有一个棋(7,7)的情况下,我们的最小矩形是(L7,R7,T7,B7),加上阈值,就是(L4,R10,T4,B10),这样需要考虑的棋子只有48颗!

经过测试,使用最小矩形,算法速度能提高10倍以上。