黑白棋程式简史
在1980年代,电脑并不普及,在黑白棋界里,最强的仍然是棋手(人类)。
到了1990年代初,电脑的速度以几何级数增长,写出来的黑白棋程式虽然仍然有点笨拙,但由于计算深度(电脑的速度快)和尾局的准确性,所以已经很强。1990年代初最有名的程式就是Thor,在那时候是最强的程式(还是DOS模式年代),棋力能比得上世界级棋手,在这个时期的程式都是人工地加入行动力、位置策略、偶数重要性等等,但又因为这些策略是直接编写在程式里,那程式的棋力很依赖程式员本人的棋力,程式下起来比较像人类的下法。而且程式里遗留了人类棋手的弱点、策略的不完整性等等的问题。因为早期Thor的通用性,影响到现在黑白棋界里的统一棋谱数据库都是用Thor database。
到了1994、1995年,黑白棋程式的编写方法有了突破性的发展,首先是有了一个称为IOS的网站(GGS的前身),让不同的程式同时连上去互相对局,突破了以往那种闭门造车的日子,在同期Michael Buro做出了能由程式自我学习的Logistello, 在差不多的时间,大家都学起来,用相似的方法来编写程式。程式员不再把人工的策略和下棋方法等死硬地写在程式里,而是由程式自我学习,程式会记录好、坏的 形状(patterns),根据实战的结果自动调整策略,又会把不同的开局棋谱根据实战的过程来评分、保存,有些程式能保存几十万步的开局棋谱。
就 是因为Logistello在根本方法的改进、先进的算法、编写方面的高效率和准确性等等,一直在黑白棋界保持为世界程式冠军。在1993 年~1997年的25场比赛中,Logistello有18场获得冠军、6场获得亚军。1997年8月,Logistello击败了1996年的世界冠军村上健(Takeshi Murakami),从此黑白棋程式把人类棋手远远甩在后面。直到Michael在1998年1月宣布Logistello退休为止。现在已经有数个程式在棋力上超越已退休的Logistello,包括edax, cyrano, saio,主要突破点在于它们把算法改善为多线程执行(multithreading)。
WZebra
黑白棋程序“斑马”(WZebra)是Zebra的Windows版本,除了可以下棋外,还提供了打谱、复盘、棋局分析、自我学习等功能,也可以加载Thor棋谱文件,进行针对性训练。在标准比赛时间(2x15分钟)内,其搜索深度可达中局18至27步、终局24至31步。WZebra是自由软件,提供了中文版本,并提供中文帮助。
S.A.I.O.
Saio是System Artificial Intelligence Othello缩写,是黑白棋程式之一。并不开放下载。
Problem——Source 来自中山大学ACM校赛中的一些问题,这里,我们要模拟黑白棋的流程,并实现一个简单的AI,可以判断——对于任意一个给定的局面(假设十步以内就是可以 结束的),黑白双方都以最NB的策略进行对弈(这里注明一下,对于十步以内的胜负几乎就要见分晓的局面中,这种黑白之间的“最优策略”是存在的,这有点类 似于围棋中的“局部手筋”),每次还是先给出棋盘的局面,分别用w和b表示白方和黑方,另外,在下一行给出是轮到白先下棋还是黑先下棋。
博弈树是神马??!
极大极小博弈树(Minimax Game Tree,简写为MGT,译者注)用于编写电脑之间的游戏程序,这类程序由两个游戏者轮流,每次执行一个步骤。当然,所有可能的步骤构成了一个树的结构。例如下面的图就是一个MGT,它表示了Tic-Tac-Toe游戏的前两步所有可能的步骤。
(Tic-Tac-Toe是一种简单的九宫格游戏,玩法是使用3*3的9个方格子,每人下一次看谁先连成一行3个,以下称ttt游戏,译者注)
我们注意到这棵树不同于其他的树结构,比如二叉树,2 3树以及堆树(heap tree,译者注),根据游戏规则,一个MGT节点上可能有很多个子节点。
图 中的树有三级,不过在编码中,极大极小树的级通常被称作层(级:level,层:ply,译者注)。在每一层中,“转换”开关指向另一个游戏者。两个不同 的游戏者通常被称作马克思(MAX,即最大,译者注)和米恩(Min,即最小,译者注)。(下面将简短的解释这些陌生的名称)
对 一棵完整的极大极小树来说,计算机能够向前遍历每一步,直到找到最佳步骤为止。当然,正如你在例图中看到的那样,仅仅几个步骤也会令这棵树变得异常庞大, 对于一台普通的计算机来说,预测五个步骤就足以令其迅速崩溃。因此,对于像国际象棋和围棋这样的大型博弈游戏来说,计算机程序不可能遍历所有结果,而是仅 仅通过最上层的几个步骤来判断胜负。此外,程序员们也提出了很多算法和技巧来减少节点数目,比如阿尔法 贝塔剪枝算法(Alpha-Beta pruning),Negascout搜索算法以及MTD(全称是:Memory enhanced Test Driver,即记忆增强测试驱动,译者注)方法。
MGT 当然不能预测所有计算机游戏的可能步骤。比如扑克游戏,计算机在判断对手动向的时候将会非常吃力,因为因为计算机不能看到对方手中的牌。因此,仅仅对于两 个游戏者都能看到全部博弈形式的游戏来说,MGT才是最好的选择。这些游戏包括国际跳棋、五子棋、国际象棋和围棋,这些游戏被称作完全信息博弈(原文为 games of perfect information,译者注)。
极 大极小博弈树是因描绘这种结构的一种简单算法而得名。我们来对ttt游戏的结果分配一下分值。如果叉(X)获胜,则分值为1。如果圈(O)获胜,则分值为 -1。现在,叉将试图获得最大化的分值,而圈将试图最小化分值。于是,第一位研究此问题的研究者决定把游戏者叉命名为马克思,并且把游戏者圈命名为米恩。 因此,这个完整的数据结构就被命名为极大(Max,马克思,译者注)极小(Min,米恩,译者注)博弈树。
极 大极小逻辑也被用于其它博弈,比如国际象棋中。然而,在这些更复杂的博弈中,程序仅仅能搜索极大极小树中的一部分;由于树太过庞大,程序往往不能搜索到博 弈最终的结局。计算机一般是搜索某几个节点之后就停止了。然后程序在某个节点上评估博弈的胜负,这些评估结果被换算成博弈形势的分值。如果计算机是马克思 一方,程序会试图使博弈形势的分值最大化,同时为获胜结局(将死)赋最大值(比如说这个值是一百万)。如果计算机是米恩一方,显然程序将试图最小化分值, 并为获胜结局赋最小分值(例如负一百万)。游戏双方将在两个最大值之间博弈,数值越接近哪一方则哪一方获利(象不象拔河?译者)。
极大极小算法背后的策略假定参与博弈的游戏者都尽自己最大的努力获得好结果。因此,无论对方选择有利或有害的步骤,计算机都将会根据对手的着法选择最于己有利的步骤。
这个简单浅显的概念就是极大极小树的最大奥妙。比如,对马克思的程序来说,无论米恩怎么做,最佳的步骤或步骤序列一定会得到最高分值的结果。而米恩显然将选择那些让它获得最低分值的结果。
从 某种意义上说,叶子节点(the bottom nodes,最下层节点,译者注)是唯一需要评估位置分值的节点,因为它们代表最终的结局。比如在马克思的博弈变化中,叶子节点始终处在同一位置。程序将 假定米恩将从可能的步骤中选择最低分值的步骤行动,那么任何马克思节点的最大最小值都会等同于米恩节点的最低分值子节点。
最 后,像人类的棋类游戏一样,程序的能力高低取决于计算机对所处形势的评估能力,以及程序搜索的深度。一位国际象棋大师对形势的估计误差要大大小于余位业余 选手,而且象棋大师对于棋局的预测也远比一般人更远。计算机同样也可以对棋局做出很长远的预测,并且它着棋不会失误,因为它会看到对手由于失误而做出的回 应。
有 很多算法可以帮助极大极小算法提高搜索效率。其中一种被称作阿尔法 贝塔剪枝算法。在使用这种算法进行的搜索中,计算机所要搜索的节点数大约只是不使用这种技术所需搜索节点数的平方根那么多。也就是说,如果程序原来需要搜 索四百个节点,使用新的算法后它只需要搜索二十个。
其 它的一些工具包括置换表(原文为Transposition table,译者注),记载搜索结果的纪录被放在一张可以快速存取的很小的表中。通常来说,不同的步骤序列可能达到相同的位置(结果)。这两个位置(结 果)就可以互换。该表可以帮助计算机认识目前棋局的形势,因为它已经付出了内存存取时间的代价对其进行了审查。
同时,这些技术也允许计算机搜索更多的节点,并模拟策略思考。尽管其它的技术也开始崭露头角(比如神经网络),但极大极小树仍然是该类程序的最佳心脏。
Solve: 很精彩的一段代码啊!看完有一种对称美的感觉,在很多地方都出现过了,我这里也不想对极大极小博弈做出什么评价,只是觉得这里的数组就用得非常巧妙,设置 empty[10][2]之余还设置enable[10],另外,吃子的描述也很精彩。运行黑棋搜索或者白棋搜索之后,就会陷入黑套白,白套黑这样的局 面,双方各自博弈的详细过程我表示看得一知半解,对博弈值的理解,我认为和每次吃的子的数目有关,但绝对不是单一的每次比较谁吃的子多!我认为这里面逻辑 还比较复杂,需要请教高手了。
2 #include< string.h>
3 #include<assert.h> // 断言,这个主要是为了调试的方便
4
5 ifstream filein( " stone.in "); // 文件读入
6 ofstream fileout( " stone.out "); // 文件输出
7
8 int grid[ 8][ 8]; // 定义整个棋盘,其中的grid就是网格的意思
9 int num,empty[ 10][ 2],enable[ 10]; // 由于问题中说明了10步以内下完全局,enable[]这里代表的是还可以装填的数目
10 const int path[ 8][ 2]={ 0, 1, 0,- 1, 1, 0,- 1, 0, 1, 1, 1,- 1,- 1, 1,- 1,- 1}; // 这里用path[8][2]标识了八个方向
11
12 // 计算黑白的数目并且判断输赢,其中,白赢则定义为1,黑赢则定义为-1,如果是平局则定义为0
13 int cal()
14 {
15 int i,j,whitecount= 0,blackcount= 0;
16 for(i= 0;i< 8;i++)
17 for(j= 0;j< 8;j++)
18 {
19 if(grid[i][j]== 1) whitecount++;
20 else if(grid[i][j]==- 1) blackcount++;
21 else assert(grid[i][j]== 0); // 这个可以不管,只是为了方便调试,向调试器发送的信号
22 }
23 if(whitecount>blackcount) return 1;
24 else if(whitecount==blackcount) return 0;
25 else return - 1;
26 }
27
28 int black(); // 这里不声明也是可以的,毕竟函数写在main函数的前面
29
30 int white()
31 {
32 int i,j,k,u,v,loop,temp1,temp2,res=- 1,tempres;
33 int rem[ 32][ 2],remnum;
34 int tag= 0;
35 // 考虑每一个可能的空位,虽然是指数级别的,但是由于在10步以内,空间还是比较小
36 for(i= 0;i<num;i++)
37 if(enable[i])
38 {
39 // 重新标记回来
40 enable[i]= 0;
41 // 获取空位点的x,y的坐标
42 u=empty[i][ 0]; v=empty[i][ 1];
43 grid[u][v]= 1;
44 remnum= 0; // 统计吃子的个数
45 // 朝着八个方向开始搜索
46 for(loop= 0;loop< 8;loop++)
47 {
48 // 棋子对该位置不超过棋盘,而且,朝向的前面一个格子不为空(否则无法吃掉)
49 for(j=u,k=v;j+path[loop][ 0]>= 0&&j+path[loop][ 0]< 8&&k+path[loop][ 1]>= 0&&k+path[loop][ 1]< 8&&grid[j+path[loop][ 0]][k+path[loop][ 1]];)
50 {
51 j+=path[loop][ 0];
52 k+=path[loop][ 1];
53 // 判断是否可以吃子,这里我还是很看了一下才看懂的!是这样的,注意到前面那个for循环,最后的执行是一个空的,就是说,如果遇到朝着那个方向不相同
54 // 的子的话,就直接加过去,直到遇到了一个与之颜色相同的棋子为止。这个时候,还没有完,我们还需要判断一下这个中间是否"夹着"一些棋子,这里可以将
55 // 只朝着方向移动一格的子和终点比较,如果坐标不同的话,就说明符合条件了,并统计可以吃的子的个数,并最后以remnum作为统计
56 if(grid[j][k]==grid[u][v])
57 {
58 for(temp1=u+path[loop][ 0],temp2=v+path[loop][ 1];(temp1!=j||temp2!=k);temp1+=path[loop][ 0],temp2+=path[loop][ 1])
59 {
60 rem[remnum][ 0]=temp1;
61 rem[remnum][ 1]=temp2;
62 remnum++;
63 }
64 break;
65 }
66 }
67 }
68 if(remnum> 0)
69 {
70 for(loop= 0;loop<remnum;loop++)
71 {
72 j=rem[loop][ 0];
73 k=rem[loop][ 1];
74 // 调整每个子的颜色,这也就是我们为什么要设置1表示白子而-1表示黑子了
75 grid[j][k]=-grid[j][k];
76 }
77 tempres=black();
78 tag= 1;
79 if(tempres>res) res=tempres; // 白棋希望博弈值越大越好,如果过小的话,白方不满意
80 // 这里返回到初始的状态
81 for(loop= 0;loop<remnum;loop++)
82 {
83 j=rem[loop][ 0];
84 k=rem[loop][ 1];
85 grid[j][k]=-grid[j][k];
86 }
87 }
88 grid[u][v]= 0;
89 enable[i]= 1;
90 }
91 if(tag== 0) reutrn cal();
92 else return res;
93 }
94
95 int black()
96 {
97 // 黑方下棋的规则和白方类似,这里略去一些注释
98 int i,j,k,u,v,loop,temp1,temp2,res= 1,tempres;
99 int rem[ 32][ 2],remnum;
100 int tag= 0;
101 for(i= 0;i<num;i++)
102 if(enable[i])
103 {
104 enable[i]= 0;
105 u=empty[i][ 0],v=empty[i][ 1];
106 grid[u][v]=- 1;
107 remnum= 0;
108 for(loop= 0;loop< 8;loop++)
109 {
110 for(j=u,k=v;j+=path[loop][ 0]>= 0&&j+path[loop][ 0]< 8&&k+path[loop][ 1]>= 0&&k+path[loop][ 1]< 8&&grid[j+path[loop][ 0]][k+path[loop][ 1]])
111 {
112 j+=path[loop][ 0];
113 k+=path[loop][ 1];
114 if(grid[j][k]==grid[u][v])
115 {
116 for(temp1=u+path[loop][ 0],temp2=v+path[loop][ 1];(tenmp1!=j||temp2!=k);temp1+=path[loop][ 0],temp2+=path[loop][ 1])
117 {
118 rem[remnum][ 0]=temp1;
119 rem[remnum][ 1]=temp2;
120 remnum++;
121 }
122 break;
123 }
124 }
125 }
126 if(remnum> 0)
127 {
128 for(loop= 0;loop<remnum;loop++)
129 {
130 j=rem[loop][ 0];
131 k=rem[loop][ 1];
132 grid[j][k]=-grid[j][k];
133 }
134 tempres=white();
135 tag= 1;
136 if(tempres<res) res=tempres; // 黑方则希望博弈值越小越好,如果过大的话,则会不满意
137 // 跳回到原来的状态
138 for(loop= 0;loop<remnum;loop++)
139 {
140 j=rem[loop][ 0];
141 k=rem[loop][ 1];
142 grid[j][k]=-grid[j][k];
143 }
144 }
145 grid[u][v]= 0;
146 enable[i]= 1;
147 }
148 if(tag== 0) return cal();
149 else return res;
150 }
151
152 int main()
153 {
154 char ch,str[ 100];
155 int i,j;
156 for(; ;) // 这个,用while(1)可能看起来更舒服一些吧!
157 {
158 // 由于是读str,这里是读一行
159 filein>>str;
160 if(strcmp(str, " EndOfInput ")== 0) break; // 读文件结束
161 for(i= 0;i< 8;i++)
162 {
163 if(str[i]== ' w ') grid[ 0][i]= 1; // 白方置1
164 else if(str[i]== ' b ') grid[ 0][i]=- 1; // 黑方置-1
165 else if(str[i]== ' e ') grid[ 0][i]= 0; // 空白置0
166 }
167 for(i= 1;i< 8;i++)
168 for(j= 0;j< 8;j++)
169 {
170 // 由于是读ch,这里是一个字符一个字符地读
171 filein>>ch;
172 if(ch== ' w ') grid[i][j]= 1;
173 else if(ch== ' b ') grid[i][j]=- 1;
174 else if(ch== ' e ') grid[i][j]= 0;
175 }
176 num= 0;
177 for(i= 0;i< 8;i++)
178 for(j= 0;j< 8;j++)
179 if(grid[i][j]== 0)
180 {
181 empty[num][ 0]=i;
182 empty[num][ 1]=j;
183 enable[num]= 1;
184 num++;
185 }
186 filestr>>str;
187 if(strcmp(str, " WHITE ")== 0)
188 {
189 i=white();
190 if(i== 1) fileout<< " White "<<endl;
191 else if(i== 0) fileout<< " Draw "<<endl;
192 else if(i==- 1) fileout<< " Black "<<endl;
193 else assert( 0); // 实在不行,就报错了,这里相当于异常处理
194 }
195 else if(strcmp(str, " Black ")== 0)
196 {
197 i=Black();
198 if(i== 1) fileout<< " White "<<endl;
199 else if(i== 0) fileout<< " Draw "<<endl;
200 else if(i==- 1) fileout<< " Black "<<endl;
201 else assert( 0);
202 }
203 else assert( 0);
204 }
205 return 0;
206 }