吴昊品游戏核心算法 Round 10 —— 吴昊教你下围棋(利用递归来解决吃子的问题)
如图所示,此即为日本动漫棋魂中的千年佐为,也就是SAI。众所周知,围棋的规则相比于中国象棋,国际象棋等等都简单许多,真是因为更简单的规则,才诞生 了更复杂的逻辑。目前的围棋AI还很不行,最NB的应该是日本人做出的后又经过众多中国的围棋爱好者改进之后的AI——首谈,其水平号称是国际大师的水 平,其实际水平估计也就是业余一段左右(比我的水平还是要稍微高一点点的样子)。而且,只要在其中下出一些“非主流布局”,比如神马天元+五五啊,宇宙流 之类的,手谈基本上就GAME OVER了。
目前在OJ(Online Judge)上还没有类似的围棋的全面模拟算法,局部的还是有,这是因为围棋的规则如果包括一些不常见的,比如盘角曲四啊,三劫循环等等,也是很难用计算 机模拟出的,而一些著名的围棋模拟软件(比如在线的TOM啊,弈城啊等等)又无法开源,在CSDN上和PUDN上又基本上是工程级别的项目,鲜有直接的核 心算法。多亏了一个叫荷蒲的网站,我看其作者应该也是围棋爱好者吧,他自己建立了一个网站,主要是在网上接一些项目,由于他本人喜欢围棋,故也创办了“荷 蒲围棋”,并向广大的围棋爱好者提供了关于模拟算法的一些思路。但是,关于三劫循环等等,还是没有列出来。
这 一期的Round,我只给出一些主要的函数以及成员变量,函数与函数之间没有明显的先后关系,但是,每一个函数可以实现一些主要功能,在以后封装的时候也 很好解决。该思路一共分为六个部分,关于吃子的那个部分利用了递归的思想解决了对于“一片棋”的气眼的计算。在统计谁是获胜方时,不能单一地像黑白棋一样 来分别统计黑子和白子的数目,因为其中有些空缺的地方需要判断是黑方还是白方或者既不是黑方又不是白方的,这样描述起来还是有些麻烦。
更多地关于该源码的解析(比如具体的数目问题)以及关于源码在其它功能的拓展和完善,我有待联系荷蒲的作者之后再予以补充。
以下,我分为六大块来解析“荷蒲”先生的源码:
2
3 #define EDGE 23 //棋盘最大格数
4 #define MAXMM 500 //最大手数
5 //color表示棋子颜色,x,y表示在棋盘上的坐标
6 //num表示下子的顺序。=0表示提前摆放的子。
7 //zt 表示棋子状态
8 //qs 表示棋子的气数
9 //sm 表示有说明信息
10
11 typedef struct qizi
12 {
13 int color,x,y,num,zt,qs,sm;
14 }qizi;
15
16 //吴昊评述:这个用法比较不常见,意义是为这个实例化的结构体定义新的成员变量。
17 qizi qipu[MAXMM]; //棋谱信息
18 qizi qipan[EDGE][EDGE]; //棋盘信息
19
20 //2、紧接着要考虑的是下棋相关信息。
21 //吴昊评述:这里的定义比较复杂,作者考虑了人机对战以及非人机对战中的总共四种情况,并且有学习和联系模式,有围棋中需要的计时器,最后还在规则上考虑了中国规则,日本规则等等(中日韩三国的围棋规则有不同,比如在盘角曲四的处理上),但是,很多成员变量只是为了以后代码的扩充作准备的,在后面的代码 描述中并没有出现。
22 int nk=0; //显示棋子序号,nk=2显示序号,1=气数
23 int BoardLines=19; //棋盘线数,默认19
24 bool ComputerPlaying; //1=该计算机下 0=人下
25 bool Computerp1=0; //1=计算机下黑 0=人下
26 bool Computerp2=0; //1=计算机下白 0=人下
27 int PlayType=0; //2=人-人,1=人-计算机,13=人-网络,0=没有开始,-1=删除棋盘上死子,-2=暂停,3=布黑子,4=布白子,9=演示,11=学习
28 int PlayType1=0; //2=人-人,1=人-计算机,13=人-网络,0=没有开始,-1=删除棋盘上死子,-2=暂停,3=布黑子,4=布白子,11=学习
29 int MoveCount,MoveCount1; //计步器,记录落子手数,自然顺序
30 int Playnum=0,Playnum1=0; //要标识的围棋手数,下棋顺序
31 int CurrentX; //记录热子X坐标,
32 int CurrentY; //记录热子Y坐标
33 char CurrentWho; //记录当前棋子颜色,0=黑 1=白 2=空(终局等,待写)
34 char CurrentWho1; //备份上一次CurrentWho
35 int timew=0,timeb=0; //计时器设定数据
36 int sdy1=0,sdy2=0; //学习功能上使用
37 int gz; //规则0=中国规则,1=日本规则,2=应氏规则
38 bool plays1=true; //学习持黑
39 bool plays2=false; //学习持白
40
41 //3、围棋电子棋盘的数据初始化。
42 //数据初始化
43
44 void wqinit(void)
45 {
46 BoardLines=19; //19X19路标准围棋盘
47 MoveCount=0; //一步棋未下,自然顺序
48 MoveCount1=0; //一步棋未下
49 ComputerPlaying=1; //默认电脑执黑先行
50 CurrentWho=0; //默认黑先; 黑方=0;白方=1;空方=2;
51 CurrentX=0; //当前一步棋的X坐标,水平从左至右为1...19
52 CurrentY=0; //当前一步棋的Y坐标,垂直从上到下为1...19
53 timew=0,timeb=0;
54 Playnum=0; //下棋顺序
55 Playnum1=0;
56 //下面是棋盘初始化
57 for (int i=0;i<=BoardLines;i++)
58 for (int j=0;j<=BoardLines;j++)
59 {
60 qipan[i][j].color=2;
61 qipan[i][j].x=0;
62 qipan[i][j].y=0;
63 qipan[i][j].num=0;
64 qipan[i][j].zt=0;
65 }
66
67 //清空棋谱记录,全部设为无效点。QiPu[0][x]留作它用
68 for (int i=0;i<500;i++)
69 {
70 qipu[i].color=2;
71 qipu[i].x=0;
72 qipu[i].y=0;
73 qipu[i].num=0;
74 qipu[i].zt=0;
75 qipu[i].sm=0;
76 qpsm1[i].n=0;
77 qpsm1[i].t=0;
78 strcpy(qpsm1[i].sm,"/");
79 }
80 }
81
82 /*
83 4、根据围棋规则编写的一些相关处理函数模块
84 围棋棋子的吃子,是根据围棋棋子的气数来计算的。气数为0的棋子应当从棋盘上拿掉。
85 围棋气数的计算问题,应当说是围棋软件的核心问题。
86 "气"是指棋子在棋盘上可以连接的交叉点,也是棋子的出路。
87 围棋的气数计算,要考虑一个围棋子的连通问题。
88 下面的图形中,交叉点的X代表棋子的气,
89 */
90
91
92 [图1]
93 图1中右上角的黑子,有两个交叉点和它的直线相接,因此它有两口气。左上角的黑子有三口气,而下边的黑子有四口气。
94
95
96
97 [图2]
98 图2中右边的黑子有四口气,中间连接在一起的两个黑子有六口气,而右边连接在一起的三个黑子有八口气。连接在一起的棋子越多,气也越多。
99
100
101
102 [图3]
103 图2中同样是四个连接在一起的黑子,左边的四个黑棋有十口气,中间的黑棋只有九口气,而右边的黑棋仅有八口气。
104 从上面分析,可以得出,计算一个棋子的气,还有分析该棋子周围的情况,因此我们利用递归函数来解决围棋气数的计算。实现方法看下面程序断(这一点可以采用递归思想来解决)。 另外,注意下面三个函数是互相嵌套的关系,第二个函数利用第一个函数来递归地求出“一片棋”中的气的多少,第三个函数将第二个函数算出的气数存储在成员变量qs中,以便于最后的提子中调用。
105
106 int go[EDGE][EDGE];
107
108 /*
109 表示棋盘 其中第0路和第20路为沉余数据
110 在 (X,Y)下黑子
111 go[x][y]=0;// 0表示黑
112 在 (X,Y)下白子
113 go[x][y]=1;// 1表示白子
114 在 (X,Y)提子
115 go[x][y]=2; //2表示空子
116 当前棋步脱先pass则 当前棋步 X坐标=0,y=0
117 否则 1<=x<=19, 1<=x<=19,
118 */
119
120 int gokong[EDGE][EDGE]; //0=该空点未曾计算过气,1=已计算,避免重复计算公气
121 int gozi[EDGE][EDGE]; //0=该子未计算串气,1=已计算,避免重复计算同一个子的气
122 int goqi; //气数
123 //以上变量声明为全局变量
124
125 void str_qi(int x,int y,int hb)
126 {
127 //本函数计算 x,y 处的hb颜色棋子的气
128 gozi[x][y]=1; //标记本子已经计算过气
129 /////////////////////////////////////////////////////右临子
130 if (x+1<=19)//如果没有超出棋盘边线
131 {
132 if ((go[x+1][y]==2)&&(gokong[x+1][y]==0))
133 //如果右临点为空并且该点未曾计算过气则
134 {
135 goqi++; //气数加一
136 gokong[x+1][y]=1; //标记本空点已经计算过气
137 }
138 else if ((go[x+1][y]==hb)&&(gozi[x+1][y]==0))
139 //否则如果右临点为和本子同色子并且该子未曾计算过气则
140 str_qi(x+1,y,hb); //递归调用到右临子
141 }
142 /////////////////////////////////////////////////////左临子
143 if (x-1>=1) //果没有超出棋盘边线
144 {
145 if ((go[x-1][y]==2)&&(gokong[x-1][y]==0))
146 //如果左临点为空并且该点未曾计算过气则
147 {
148 goqi++; //气数加一
149 gokong[x-1][y]=1; //标记本空点已经计算过气
150 }
151 else if ((go[x-1][y]==hb)&&(gozi[x-1][y]==0))
152 //否则如果左临点为和本子同色子并且该子未曾计算过气则
153 str_qi(x-1,y,hb); //递归调用到左临子
154 }
155 ////////////////////////////////////////////////////下临子
156 if (y-1>=1)//如果没有超出棋盘边线
157 {
158 if ((go[x][y-1]==2)&&(gokong[x][y-1]==0))
159 //如果下临点为空并且该点未曾计算过气则
160 {
161 goqi++; //气数加一
162 gokong[x][y-1]=1; //标记本空点已经计算过气
163 }
164 else if ((go[x][y-1]==hb)&&(gozi[x][y-1]==0))
165 //否则如果下临子点为和本子同色子并且该子未曾计算过气则
166 str_qi(x,y-1,hb); //递归调用到下临子
167 }
168 ////////////////////////////////////////////////////上临点
169 if (y+1<=19)//如果没有超出棋盘边线
170 {
171 if ((go[x][y+1]==2)&&(gokong[x][y+1]==0))
172 //如果上临点为空并且该点未曾计算过气则
173 {
174 goqi++; //气数加一
175 gokong[x][y+1]=1; //标记本空点已经计算过气
176 }
177 else if ((go[x][y+1]==hb)&&(gozi[x][y+1]==0))
178 //否则如果上临点为和本子同色子并且该子未曾计算过气则
179 str_qi(x,y+1,hb); //递归调用到上临子
180 }
181 }
182
183 int str_lib(int x,int y, int hb)
184 {
185 int i,j;
186 for (i = 1; i <= 19; i++)
187 for (j = 1; j <= 19; j++)
188 {
189 gozi[i][j] = 0; //初始化变量,表示该子未计算串气
190 gokong[i][j] = 0; //初始化变量,表示该空点未计算串气
191 }
192 goqi=0; //串气初值
193 str_qi(x,y,hb); //调用串气子程序
194 return(goqi); //全局变量goqi带回串气值
195 }
196
197 void suanqi(void)
198 {
199 int i,j,cc,qq;
200 for (i = 1; i <=19; i++)
201 for (j = 1; j <= 19; j++)
202 {
203 go[i][j]=qipan[i][j].color;
204 }
205 for (i = 1; i <=19; i++)
206 for (j = 1; j <=19; j++)
207 {
208 if (go[i][j]!=2)
209 {
210 cc=go[i][j];
211 qq=str_lib(i,j,cc);
212 qipan[i][j].qs=qq;
213 }
214 }
215 }
216
217 /*
218 5、围棋的提子(吃子)
219 提子:就是把没有气的棋子从棋盘上拿掉。
220 下面函数实现提子功能。
221 */
222
223 void chizi(void)
224 {
225 int i,j,qq,cc;
226 suanqi();
227 for (i = 1; i <=19; i++)
228 for (j = 1; j <= 19; j++)
229 {
230 qq=qipan[i][j].qs;
231 cc=qipan[i][j].color;
232
233 //吴昊评述:这里,提子的同时需要满足(1)该子的气没有了(2)该子为对手的,记得国际象棋中有一个方法叫ByWho,这里同理,同时,我们没有必要当心被提掉的子会对将来正要被提掉的子构成威胁,因为,qq这个成员变量。
234 if (qq==0 && cc!=2 && cc!=CurrentWho)
235 {
236 qipan[i][j].color=2;
237 qipan[i][j].x=i;
238 qipan[i][j].y=j;
239 qipan[i][j].num=0;
240 qipan[i][j].zt=0;
241 }
242 }
243 }
244
245 //围棋程序设计的核心,基本完成,下面是输赢的判断问题。
246 /*
247 6、围棋胜负判断
248 围棋盘上共有三百六十一个交叉点,一盘棋的胜负就是由对局双方所占据的交叉点的多少所决定的。更精确地说就是由双方活棋所占据的地域的大小来决定的。一个交叉点为一子,每方以一百八十又二分之一子为归本数,超过此数者为胜,不足此数者为负。
249 按我国现行的围棋规则规定,由于黑棋先走,有一定的先手威力,应由执黑的一方贴出2(3/4)子。所以黑所占的地域必须超过183(1/4)子(180 (1/2)+2(3/4))才能取胜。比如黑棋数出来有185个子,即黑棋1(3/4)子。而白方的地域只要超过177(3/4)子 (180(1/2)-2(3/4))即可获胜。
250
251
252 下面函数实现计算围棋的地域功能。在计算前,应当先去掉围棋中的死子。
253 */
254
255 // 计算围棋的地域
256 int sum[3]; //sum[0]=黑子数量,sum[1]=白子数量
257
258 int summ(void)
259 {
260 int i=0,j=0,c=2,k=2;
261 //计数白子,黑子,空白的数目
262 sum[0]=0;
263 sum[1]=0;
264 sum[2]=0;
265 for (i=1;i<=19;i++)
266 {
267
268 //从最外层开始搜索
269 k=qipan[i][1].color;
270 for (j=1;j<=19;j++)
271 {
272 c=qipan[j][i].color;
273 switch (c)
274 {
275 case 2:
276 if (k==2) sum[2]++;
277 else sum[k]++;
278 break;
279 case 0:
280 if (k==0)
281 {
282 sum[c]++;
283 }
284 else if(k==2)
285 {
286 sum[c]=sum[c]+sum[2]+1;
287 k=c;
288 sum[2]=0;
289 }
290 else if(k==1)
291 {
292 sum[c]++;
293 k=c;
294 sum[2]=0;
295 }
296 break;
297 case 1:
298 if (k==1)
299 {
300 sum[c]++;
301 }
302 else if(k==2)
303 {
304 sum[c]=sum[c]+sum[2]+1;
305 k=c;
306 sum[2]=0;
307 }
308 else if(k==0)
309 {
310 sum[c]++;
311 k=c;
312 sum[2]=0;
313 }
314 break;
315 }
316 }
317 }
318 return sum[0];
319 }