五子棋代码解析

 近来随着计算机的快速发展,各种棋类游戏被纷纷请进了电脑,使得那些喜爱下棋,又常常苦于没有对手的棋迷们能随时过足棋瘾。而且这类软件个个水平颇高,大有与人脑分庭抗礼之势。其中战胜过国际象棋世界冠军-卡斯帕罗夫的“深蓝”便是最具说服力的代表;其它像围棋的“手淡”、象棋的“将族”等也以其优秀的人工智能深受棋迷喜爱;而我们今天将向大家介绍的是五子棋的算法。
当我们与电脑对战时,您知道这些软件是怎样象人脑一样进行思考的吗?在这里就以此为例和大家一起探讨探讨。

为了使读者对五子棋搜索复杂度有个形象的认识,举一个中国象棋跟五子棋搜索次数的比较(如图)。可以看出同中国象棋相比,五子棋的分支系数大的多,而且胜负条件判断也复杂一些。在极大的分支系数下,搜索程序的最大搜索深度增加1层,耗费的运算时间都将大量增加。因此设计出一个有效的搜索算法是非常重要的。



中国象棋 五子棋
棋子种类 14 2
棋盘大小 9×10 15×15
分支系数 约40 约200
棋子数量 递减 递增
胜负条件 某方将帅丧失 某方五个棋子连成一线
文章的****如下:首先简单介绍用C语言作图的基本方法(Turbo C 2.0环境)以及主循环控制下棋模块,其次介绍设计这个五子棋程序的数据结构,然后介绍了评分算法以及胜负判断,最后重点讨论实现搜索算法。 1. 基本的C作图方法及主循环控制模块 Turbo C提供了非常丰富的图形函数,所有的图形函数的原型均建立在graphics.h中,在使用图形函数时要确保有显示器图形驱动程序*.BGI,同时将集成开发环境Options/Linker中的Graphics lib选为on,只有这样才能保证正确使用图形函数。 这个程序调用一个EGA、VGA显示器下能独立图形运行的函数。所谓独立图形运行程序,就是在编译和连接时将相应的驱动程序(*.BGI)直接装入到执行程序,从而能在独立的计算机上运行,避免需要重新编译连接才能运行(请查阅参考书1以及源码)。Turbo C进行画点、画线、封闭图形填充以及图形下文本输出只需要调用graphics.h中相关的函数。 主循环控制模块:控制下棋顺序,当轮到某方下子时,负责将程序转到相应的模块中去,主要担当一个调度者的角色。这个五子棋程序是用键盘控制下棋,所以要用到Turbo C中的bios.h。在一个循环块中等待键盘信息,判断键盘所输入的信息是否需要响应,调用相关的代码进行下棋(参考源码中的main函数部分)。 2. 五子棋基本数据结构 为整个棋盘建立一张表格用以记录棋子信息,使用一个15*15的二维数组 chessman[15][15] (15*15是五子棋棋盘的大小),数组的每一个元素对应棋盘上的一个交叉点,用“0”表示空位、“1”代表己方棋子、“2”代表对方棋子。这张表也是今后分析的基础。其次要建立一个结构,主要用于搜索过程中,定义如下: typedef struct five_chess* point; struct five_chess{ int x; int y; int layer; int value; int score; int chess[LENGTH][LENGTH]; int record[LENGTH][LENGTH]; }; x,y表示在某个位置上扩展出来的新节点,layer是表示第几层扩展,用于控制扩展深度。value表示该点上极大极小值,score表示叶子节点的得分,用于推算父辈节点的value,chess这个二维数组表示扩展出来的棋盘信息,record记录在x、y点上扩展过的节点,如果没有扩展record中对应某个值为0。如果record中没有可以扩展的节点,那么该层扩展结束,返回一个特定值。 数组和一个结构构成了程序的基本数据骨架,今后只要再加入一些辅助变量便可以应付自如了。 3. 评分规则以及胜负判断 评估一个棋盘的分数,主要通过扫描整个棋盘,对每个点评分。对某个点上评分从四个方向(角度分别为0、45、90、135的四个方向)分别统计,进而累积该点总分,最后得到整个棋盘的分数。实际上对当前的局面按照下面的规则的顺序进行比较,如果满足某一条规则的话,就给该局面打分并保存,然后退出规则的匹配。注意这里的规则是根据一般的下棋规律的一个总结,在实际运行的时候,用户可以添加规则和对评分机制加以修正(源码中选用了其中部分规则)。评分规则如下: l 判断是否能成5, 如果是机器方的话给予30000分,如果是人方的话给予-30000 分; l 判断是否能成活4或者是双死4或者是死4活3,如果是机器方的话给予10000分,如果是人方的话给予-10000分 l 判断是否已成双活3,如果是机器方的话给予5000分,如果是人方的话给予-5000 分 l 判断是否成死3活3,如果是机器方的话给予1000分,如果是人方的话给予-1000 分 l 判断是否能成死4,如果是机器方的话给予500分,如果是人方的话给予-500分 l 判断是否能成单活3,如果是机器方的话给予200分,如果是人方的话给予-200分 l 判断是否已成双活2,如果是机器方的话给予100分,如果是人方的话给予-100分 l 判断是否能成死3,如果是机器方的话给予50分,如果是人方的话给予-50分 l 判断是否能成双活2,如果是机器方的话给予10分,如果是人方的话给予-10分 l 判断是否能成活2,如果是机器方的话给予5分,如果是人方的话给予-5分 l 判断是否能成死2,如果是机器方的话给予3分,如果是人方的话给予-3分 胜负判断实际上是据当前最后一个落子的情况来判断胜负的。实际上需要从四个位置判断,以该子为出发点的水平,竖直和两条分别为 45度角和135度角的线,目的是看在这四个方向是否最后落子的一方构成连续五个的棋子,如果是的话,就表示该盘棋局已经分出胜负。 4. 搜索算法的实现 α-β剪枝是在极大极小搜索算法基础上发展起来的,因此先来分析下经典的极大极小搜索过程,如下:
①T:=(s,MAX),OPEN:=(s),CLOSED:=( );开始时树由初始节点构成,OPEN表只含有s。
②LOOP1:IF OPEN=( )THEN GO LOOP2;
③n:=FIRST(OPEN),REMOVE(n,OPEN),ADD(n,CLOSED);
④IF n可直接判定为赢、输或平局
THEN f(n):=∞∨-∞∨0,GO LOOP1
ELSE EXPAND(n)→{ni},ADD({ },T)
IF d(ni)<k THEN ADD({ },OPEN),GO LOOP1
ELSE计算f( ),GO LOOP1;nI达到深度k,计算各端节点f值。
⑤LOOP2:IF CLOSED=NIL THEN GO LOOP3
ELSE :=FIRST(CLOSED);
⑥IF( ∈MAX)∧(f( ∈MIN)有值)
THEN f( ):=max{f( )},REMOVE( ,CLOSED);若MAX所有子节点均有值,则该MAX取其极大值。
IF ( ∈MIN)∧(f( ∈MAX)有值)
THEN f( ):=min{f( )},REMOVE( ,CLOSED);若MIN所有子节点均有值,则该MIN取其极小值。
⑦GO LOOP2;
⑧LOOP3:IF f(s)≠NIL THEN EXIT(END∨M(Move,T));s有值,则结束或标记走步。 MINIMAX过程是把搜索树的生成和格局估值这两个过程分开来进行,即先生成全部搜索树,然后再进行端节点静态估值和倒推值计算,这显然会导致低效率。为了使生成和估值过程紧密结合,采用有界深度优先策略进行搜索,这样当生成达到规定深度的节点时,就立即计算其静态估值函数,而一旦某个非端节点有条件确定其倒推值时就立即计算赋值。这就是所谓的α-β过程。α-β算法归纳如下: α剪枝:若任一极小值层节点的β值小于或等于它任一先辈极大值居节点的α值,即α(先辈层)≥β(后继层),则可中止该极小值层中这个MIN节点以下的搜索过程。这个MIN节点最终的倒推值就确定为这个β值 β剪枝:若任一极大值层节点的α值大于或等于它任一先辈极小值层节点的β值,即α(后继层)≥β(先辈层),则可以中止该极大值层中这个MAX节点以下的搜索过程。这个MAX节点的最终倒推值就确定为这个α值。 5. 算法的改进 这里的算法改进主要是集中于五子棋程序运用上的极大极小、α-β剪枝的改进。这个五子棋的算法流程图如下(以扩展两层为例):
根据棋盘信息chessman[15][15]建立根结点s0(数据结构:five_chesman),并把s0压入栈中
扩展s1=top();判断s1->layer是否等于-1
s1->layer不等于-1,push(s1),扩展s2=top(),查看s2->layer是否-1
s2->layer!=-1,计算此时棋盘得分score,并判断是否要更改上一层的极小值
s2->layer==-1,pop():判断是否更改极大值,max_chess指向得分最高的棋盘
如果s1->layer=-1,表示搜索结束,返回最大棋盘信息max_chess


采用C语言写的代码段如下: s0=malloc(sizeof(struct five_chess)); for(i=0;i<3000;i++) close=NULL; for(i=0;i<LENGTH;i++) for(j=0;j<LENGTH;j++){ s0->chess[j]=chessman[j]; s0->record[j]=chessman[j]; } s0->layer=0; s0->value=-30000; s0->score=-30000; push(s0); while(is_empty()!=0){ s0=top(); s1=expand(s0); if(s1->layer==-1){ pop(); continue; } close[num++]=s1; s1->value=30000; push(s1); while(1){ s2=expand(s1); if(s2->layer==-1){ s1=top(); pop(); if(s1->value>top()->value){ top()->value=s1->value; max_chess=s1; } break; } s2->score=score(s2->chess); temps=top(); if(s2->score<temps->value) temps->value=s2->score; 这个程序主要特点有: l 没有使用closed表,而改用一个指针指向得分最大的棋盘信息,并且使用一个记录表登记已经扩展过的结点。这样就不需要对closed表进行大量的访问,很大程度上提高了搜索性能。 l 在扩展结点的时候,把棋盘分成三个部分:中间层(坐标5<=x<10,5<=y<10)、第二层(2<=x<12,2<=y<12并且除去中间层的那些点),第三层(0<=x<14,0<=y<14除去中间层和第二层的结点)。把棋盘分成这样三个部分扩展的依据是:越靠近中间位置的结点得分越高,这样先从得分高的结点开始计算,那么剪枝的次数就更多,从而很大程度上提高运算效率。实际上,扩展的最佳算法是以中间结点为中心,采用螺旋式搜索,这样最大程度上提高效率。 l 使用一个记录得分最高的棋盘信息的指针typedef struct five_chess *point,这点改进能节省大量空间。因为扩展过程的结点非常多,如果采用这个改进,那么就可以扩展后删除那些占用空间。此外在返回棋盘信息时,查找某个位置下棋也非常方便

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值