|
1 | 1 | 上一章进行了一个五子棋游戏框架的搭建。应该来说,除开AI以外,其他的部分全部写完了。从这章开始,就详细介绍一下五子棋的AI算法。这里说一件非常令人振奋的消息:这章看完之后,你的五子棋AI已经可以下棋了,唯一的缺点就是奇慢无比,但是只要你愿意让他思考足够长的时间,他的棋力绝对是非常棒的了。优化算法将在后续的章节慢慢讲解。
|
2 | 2 |
|
3 |
| -回顾一下上一章的内容,我们需要一个RobotPlayer类,实现Player接口的三个方法: |
| 3 | +回顾一下上一章的内容,我们需要一个`RobotPlayer`类,实现`Player`接口的三个方法: |
4 | 4 |
|
5 | 5 | ```java
|
6 | 6 | public class RobotPlayer implements Player {
|
@@ -32,17 +32,17 @@ public class RobotPlayer implements Player {
|
32 | 32 | }
|
33 | 33 | ```
|
34 | 34 |
|
35 |
| -在讲这个play方法之前,我们首先需要掌握一些数学知识。 |
| 35 | +在讲这个`play`方法之前,我们首先需要掌握一些数学知识。 |
36 | 36 |
|
37 | 37 | # 完全信息动态零和博弈
|
38 | 38 |
|
39 | 39 | 五子棋按照分类属于完全信息动态零和博弈。这里就涉及了很多看上去貌似很复杂的词汇。
|
40 | 40 |
|
41 |
| -首先,博弈就是下棋,或者说就是game,这个很好理解。前面三个词语就需要一一解释了: |
| 41 | +首先,**博弈**就是下棋,或者说就是game,这个很好理解。前面三个词语就需要一一解释了: |
42 | 42 |
|
43 |
| -- 完全信息:游戏进行的任何时候,双方参与者对棋盘上的全部信息都完全了解。这一点就不像军旗、斗地主、麻将,我并不知道对方的一些信息。 |
44 |
| -- 动态:双方是轮流下子,每一步棋做决策之前,需要上一步棋的局面状态做出相应的选择。而非石头剪刀布那样,双方同时做决策。 |
45 |
| -- 零和(zero sum):或者说是“非合作性”,五子棋不存在任何“双方共同目标”。换句话说,某步棋对这个玩家产生了正价值,则一定对他的对手产生了等量的负价值。关于什么是“非零和博弈”,可以自行搜索一下“囚徒困境”。 |
| 43 | +- **完全信息**:游戏进行的任何时候,双方参与者对棋盘上的全部信息都完全了解。这一点就不像军旗、斗地主、麻将,我并不知道对方的一些信息。 |
| 44 | +- **动态**:双方是轮流下子,每一步棋做决策之前,需要上一步棋的局面状态做出相应的选择。而非石头剪刀布那样,双方同时做决策。 |
| 45 | +- **零和(zero sum)**:或者说是“非合作性”,五子棋不存在任何“双方共同目标”。换句话说,某步棋对这个玩家产生了正价值,则一定对他的对手产生了等量的负价值。关于什么是“非零和博弈”,可以自行搜索一下“囚徒困境”。 |
46 | 46 |
|
47 | 47 | 对于这类“完全信息动态零和博弈”问题,我们有一种通用的解法。因为是“完全信息”的,我们不需要考虑复杂的概率问题,只需要找到那个最好的决策即可。因为是“零和”的,我们只需要一个评估函数就可以对场上的局面进行评价(我的评分和对方的评分一定互为相反数)。因为是“动态”的,所以要考虑的问题很多,就需要用到后文的“博弈树”进行解决了。
|
48 | 48 |
|
@@ -99,27 +99,133 @@ public class RobotPlayer implements Player {
|
99 | 99 |
|
100 | 100 | 循序渐进,我们先考虑一步棋,如下图所示:
|
101 | 101 |
|
102 |
| - |
| 102 | +```mermaid |
| 103 | +graph TD |
| 104 | + 当前盘面 --> 10 |
| 105 | + 当前盘面 --> 15 |
| 106 | + 当前盘面 --> 7 |
| 107 | + 当前盘面 --> 9 |
| 108 | +``` |
103 | 109 |
|
104 |
| -假设在当前盘面下,我们有四种走法,对于每种走法我们调用上文的评估函数*evaluateBoard*,得到四个得分,显然,我们更倾向于选择最高分15对应的那个走法。换句话说,我们可以认为以当前局面发展,可以到达15分的局面。 |
| 110 | +假设在当前盘面下,我们有四种走法,对于每种走法我们调用上文的评估函数`evaluateBoard`,得到四个得分,显然,我们更倾向于选择最高分15对应的那个走法。换句话说,我们可以认为以当前局面发展,可以到达15分的局面。 |
105 | 111 |
|
106 | 112 | 现在我们开始考虑两步棋,如下图所示:
|
107 | 113 |
|
108 |
| - |
| 114 | +```mermaid |
| 115 | +flowchart TD |
| 116 | + subgraph MAX 1 |
| 117 | + 10 |
| 118 | + 15 |
| 119 | + 7 |
| 120 | + 9 |
| 121 | + end |
| 122 | + subgraph MIN 2 |
| 123 | + 6 |
| 124 | + 5 |
| 125 | + 2 |
| 126 | + 8 |
| 127 | + 0 |
| 128 | + 4 |
| 129 | + 3 |
| 130 | + 1 |
| 131 | + end |
| 132 | + 当前盘面 --> 10 |
| 133 | + 10 --> 6 |
| 134 | + 10 --> 5 |
| 135 | + 当前盘面 --> 15 |
| 136 | + 15 --> 2 |
| 137 | + 15 --> 8 |
| 138 | + 当前盘面 --> 7 |
| 139 | + 7 --> 0 |
| 140 | + 7 --> 4 |
| 141 | + 当前盘面 --> 9 |
| 142 | + 9 --> 3 |
| 143 | + 9 --> 1 |
| 144 | +``` |
109 | 145 |
|
110 | 146 | 假设对于我的这四种走法,对方分别有两种走法进行应对。现在情况开始变得复杂了。我们重新强调一下,这是一个“零和博弈”,也就是说,我的正分一定等于对方的负分。如果我选择了15分这种走法,对方肯定不傻,一定会选择2分这种走法,想让我的分更低。如果我选择了10分这种走法,对方一定会选择5分这种走法。想要将局面变成6分或者8分的结果,是不可能的(除非对面犯傻)。那么对于图上的那种情况,我们分析一下:如果我选第一种走法,则会得到5分;如果我选第二种走法,则会得到2分;如果我选第三种走法,则会得到0分;如果我选第四种走法,则会得到1分。那我到底应该选择哪种走法呢?显然,我更希望两步棋后,局面是5分,我选择了第一种走法。
|
111 | 147 |
|
112 | 148 | 重新审视一下这个问题,我们不难发现,如果我考虑两步棋,那么第一步棋的得分是没有用的。我的实际求解过程是:先通过每种第一步棋,求得对应的第二步棋的最小得分,再从这些最小得分中,找到那个最大得分。
|
113 | 149 |
|
114 |
| -好了,为了游戏更加精确,我们继续尝试考虑4步棋。自己画图太过麻烦,我就随便搜索了一张图片: |
115 |
| - |
116 |
| - |
| 150 | +好了,为了游戏更加精确,我们继续尝试考虑4步棋: |
| 151 | + |
| 152 | +```mermaid |
| 153 | +flowchart TD |
| 154 | + subgraph MAX 1 |
| 155 | + b0 |
| 156 | + b1 |
| 157 | + end |
| 158 | + subgraph MIN 2 |
| 159 | + c0 |
| 160 | + c1 |
| 161 | + c2 |
| 162 | + c3 |
| 163 | + end |
| 164 | + subgraph MAX 3 |
| 165 | + d0 |
| 166 | + d1 |
| 167 | + d2 |
| 168 | + d3 |
| 169 | + d4 |
| 170 | + d5 |
| 171 | + d6 |
| 172 | + d7 |
| 173 | + d8 |
| 174 | + end |
| 175 | + subgraph MIN 4 |
| 176 | + e0 |
| 177 | + e1 |
| 178 | + e2 |
| 179 | + e3 |
| 180 | + e4 |
| 181 | + e5 |
| 182 | + e6 |
| 183 | + e7 |
| 184 | + e8 |
| 185 | + e9 |
| 186 | + e10 |
| 187 | + e11 |
| 188 | + e12 |
| 189 | + e13 |
| 190 | + e14 |
| 191 | + end |
| 192 | + 当前盘面 --> b0 |
| 193 | + 当前盘面 --> b1 |
| 194 | + b0 --> c0 |
| 195 | + b0 --> c1 |
| 196 | + b1 --> c2 |
| 197 | + b1 --> c3 |
| 198 | + c0 --> d0 |
| 199 | + c0 --> d1 |
| 200 | + c0 --> d2 |
| 201 | + c1 --> d3 |
| 202 | + c1 --> d4 |
| 203 | + c2 --> d5 |
| 204 | + c2 --> d6 |
| 205 | + c3 --> d7 |
| 206 | + c3 --> d8 |
| 207 | + d0 --> e0 |
| 208 | + d1 --> e1 |
| 209 | + d1 --> e2 |
| 210 | + d1 --> e3 |
| 211 | + d2 --> e4 |
| 212 | + d3 --> e5 |
| 213 | + d3 --> e6 |
| 214 | + d4 --> e7 |
| 215 | + d4 --> e8 |
| 216 | + d5 --> e9 |
| 217 | + d5 --> e10 |
| 218 | + d6 --> e11 |
| 219 | + d7 --> e12 |
| 220 | + d8 --> e13 |
| 221 | + d8 --> e14 |
| 222 | +``` |
117 | 223 |
|
118 | 224 | 同样,按照上面的思路,我们需要反着考虑。首先考虑第四步棋,这是对方选择的一步棋。对于每一种第三步的局面,对方肯定选择分数最低的一步棋,我们把同一个第三步下的所有第四步的最小值求出来,作为第三步的分数即可。然后对于每个第二步的局面,我肯定选择分数最高的那个第三步,因此只需要求出同一个第二步下的所有第三步的最大值求出来,即可作为第二步的分数。同理,我们继续找第二步的最小值当做第一步的分数。最后再找到第一步的最大值,作为我决策的下一步棋。
|
119 | 225 |
|
120 | 226 | 以上,就是我们所说的“极小极大值搜索”算法。
|
121 | 227 |
|
122 |
| -值得一提的是,如果我优先下出了五连珠,游戏会立即结束,如果下一步棋对方也下出了五连珠,则我的五连珠调用evaluateBoard(1)减去对方的五连珠调用evaluateBoard(2)等于0,这个情况我们要排除掉,因为我已经先下出五连珠了,游戏已经结束了,对方再下出来的棋是无效的。 |
| 228 | +值得一提的是,如果我优先下出了五连珠,游戏会立即结束,如果下一步棋对方也下出了五连珠,则我的五连珠调用`evaluateBoard(1)`减去对方的五连珠调用`evaluateBoard(2)`等于0,这个情况我们要排除掉,因为我已经先下出五连珠了,游戏已经结束了,对方再下出来的棋是无效的。 |
123 | 229 |
|
124 | 230 | 推广到连续考虑N步棋,我们可以得到这样的代码:
|
125 | 231 |
|
@@ -181,7 +287,7 @@ public class RobotPlayer implements Player {
|
181 | 287 | }
|
182 | 288 | ```
|
183 | 289 |
|
184 |
| -到此为止,我们就可以用*getMaxEvaluate*方法求出一个“最好的策略”了。把这个方法再进一步组合,我们可以得到play方法: |
| 290 | +到此为止,我们就可以用`getMaxEvaluate`方法求出一个“最好的策略”了。把这个方法再进一步组合,我们可以得到`play`方法: |
185 | 291 |
|
186 | 292 | ```java
|
187 | 293 | import java.util.*;
|
@@ -223,4 +329,4 @@ public class RobotPlayer implements Player {
|
223 | 329 |
|
224 | 330 | 好了,至此为止,恭喜你,我们已经可以和AI进行五子棋对弈了。
|
225 | 331 |
|
226 |
| -写完了算法,我们大致看看这个算法的计算量。尽管我们可以排除距离局面太远的点,但是下到中局的时候,我们每一步棋起码也要考虑三四十个点。假设要考虑八步棋,就是30^8^=6561亿个分支,简直庞大的计算量。目前这个算法在搜索深度是4的情况下,可以保证在几秒钟最多一分钟之内计算出结果,如果想要考虑八步甚至十步棋,显然远远是不够的。下一章会介绍α-β剪枝、启发式搜索等一些算法,对目前的算法进行优化。 |
| 332 | +写完了算法,我们大致看看这个算法的计算量。尽管我们可以排除距离局面太远的点,但是下到中局的时候,我们每一步棋起码也要考虑三四十个点。假设要考虑八步棋,就是 $30^8=6561亿$ 个分支,简直庞大的计算量。目前这个算法在搜索深度是4的情况下,可以保证在几秒钟最多一分钟之内计算出结果,如果想要考虑八步甚至十步棋,显然远远是不够的。下一章会介绍α-β剪枝、启发式搜索等一些算法,对目前的算法进行优化。 |
0 commit comments