吴昊品游戏核心算法 Round 5 ——(转载)关于无禁手下先手必胜的证明

关于五子棋先手必胜的证明,用人工的方式过于复杂,其难度相当于证明四色定理的正确性或者是若儿当定理的正确性。但是,如果采用计算机来解决,则复杂程度 会降低许多。由于很难地毯式地枚举到所有可能的情形,这一款五子棋终结者的算法(最新的应该是1.22版本,网络上流传的2.0和4.0版都是子虚乌有) 几乎可以达到先手必胜了。作者甚至开出了悬赏92万美元的奖励,就是说如果你用他的程序战胜了执黑的他,他就会给你这么多钱作为补偿,但是,到目前为止该 算法还是几乎可以实现先手必胜的!

 

   五子棋终结者算法        
  

   任何一种棋类游戏其关键是对当前棋局是否有正确的评分,评分越准确则电脑的AI越高。五子棋游戏也是


 

如此,但在打分之前,我们先扫描整个棋盘,把每个空位从八个方向上的棋型填入数组gStyle(2, 15, 15, 


 

8, 2),其中第一个下标为1时表示黑棋,为2时表示白棋,第二和第三个下标表示(x,y),第四个下标表示8个


 

方向,最后一个下标为1时表示棋子数,为2时表示空格数,如:


 

gStyle(1,2,2,1,1)=3表示与坐标(2,2)在第1个方向上相邻的黑棋棋子数为3 

gstyle(1,2,2,1,2)=4表示与坐标(2,2)在第1个方向上的最近的空格数为4 

在定义方向时,也应该注意一定的技巧,表示两个相反的方向的数应该差4,在程序中我是这样定义的: 

Const DIR_UP = 1 

Const DIR_UPRIGHT = 2 

Const DIR_RIGHT = 3 

Const DIR_RIGHTDOWN = 4 

Const DIR_DOWN = 5 

Const DIR_DOWNLEFT = 6 

Const DIR_LEFT = 7 

Const DIR_LEFTUP = 8 

这样我们前四个方向可以通过加四得到另一个方向的值。请看下面的图: 

--------- 

--------- 

---oo---- 

-ox*xx--- 

--------- 

--------- 

图中的*点从标为(4,4),(打*的位置是空位),则: 

gStyle(2,4,4,1,1)=1在(4,4)点相邻的上方白棋数为1 

gStyle(2,4,4,1,2)=2在(4,4)点的上方距上方白棋最近的空格数为2 

gStyle(1,4,4,3,1)=2在(4,4)点相邻的右方黑棋数为2 

gStyle(1,4,4,3,2)=1在(4,4)点的右方距右方黑棋最近的空格数为3 

...


 

  一旦把所有空点的棋型值填完,我们很容易地得出黑棋水平方向上点(4,4)的价值,由一个冲1(我把有


 

界的棋称为冲)和活2(两边无界的 

棋称为活)组成的。对于而白棋在垂直方向上点(4,4)的价值是一个活1,而在/方向也是活1所以,只要我们


 

把该点的对于黑棋和白棋的价值算出 

来,然后我们就取棋盘上各个空点的这两个值的和的最大一点作为下棋的点。然而,对各种棋型应该取什么


 

值呢?我们可以先作如下假设: 

 Fn 表示先手n个棋子的活棋型,如:F4表示先手活四 

 Fn'表示先手n个棋子的冲棋型,如:F4'表示先手冲四 

 Ln 表示后手n个棋子的活棋型,如:L3表示后手活三 

 Ln'表示后手n个棋子的冲棋型,如:L3'表示后手冲三 

 . 

 . . 

  根据在一行中的棋型分析,得到如下关系: 

L1'<=F1'<L2'<=F2'<=L1<F1<L2<F2<L3'<=F3'<L4'<F4'=F4从这个关系包含了进攻和防守的关系(当然,这个


 

关系是由我定的,你可以自己定义这些关系)。对这些关系再进一步细化,如在一个可下棋的点,其四个方


 

向上都有活三,也比不上一个冲四,所以我们可以又得到4*F3<L4'这个关系,同样,我们还可以得到其它的


 

关系,如:4*F2<L3、4*L3<F3...,这些的关系由于你的定法和我的定法制可能不一样,这样计算机的AI也就


 

不一样,最后我们把分值最小的L1'值定为1,则我们就得到了下面各种棋型的分值,由C语言表示为: 

F[2][5]={{0,2,5,50,16000},{0,10,30,750,16000}}; 

L[2][5]={{0,1,5,50,3750},{0,10,30,150,4000}}; 

  F数组表示先手,第一个下标为0时表示冲型,第二个下标表示棋子数,则F2'对应F[0][2]L数组表示后手


 

,第一个下标为0时表示冲型,第二个下标表示棋子数,则L2对应F[1][2]Ok,棋型的分值关系确定好了以后


 

,我们把每一个可下点的四个方向的棋型值相加(包括先手和后手的分值),最后选择一个最大值,并把这


 

一点作为计算机要下的点就OK了:)。


 

后话: 

1、得到最大值也许不止一个点,但在我的程序中只选择第一个最大点,当然你可以用于个随机数来决定 

选择那一个最大值点,也可以对这些最大值点再作进一步的分析。 

2、在这个算法中我只考虑了周围有棋子的点,而其它点我没有考虑。 

3、可以再更进一步,用这个算法来预测以后的几步棋,再选择预测值最好的一步,这样电脑的AI就更高了 

4、这个算法没有考虑黑棋的禁手(双3、双四和多于五子的连棋)。因为在平时我下的五子棋是没有这些 

禁手的

 

五子棋终结者的算法求解过程2008-11-14 15:23
终 结五子棋不是一个很难的问题,和普通的遍历求解问题没多大区别。只是计算量稍微大点,设计的时候需要考虑系统性层次性逐步发展的观点 ,不可能三两天之内完成很精妙的算法。因此终结五子棋算不上卓有成效的工作,只是解了一个状态空间稍微大点的遍历题目而已,所需要的全部知识只是C语言、 二叉树和对五子棋规则的了解,并不需要多么好的棋力才可以写程序,只是将想法赋予机器。

五子棋终结者终结五子棋的计算引擎分为三层,上层调用下层,从上至下依次为

>>目标过程层。

受限于计算机能力,五子棋终结者是不可能一次搜索就被全面终结的,而是不断地从主要干支到次要枝叶到全面终结的一个目标渐进过程。此层引擎只是一个for循环,逐步放大终结目标的宽度,从5到10到30到50到225,当到225时,五子棋就被完全地终结了。

>>策略引擎层。

最佳优先与或求解树引擎。不要看到名字这么复杂,其实就是一个不断扩展发展M叉树。让最优的棋子获得CPU,棋子的优先度根据下层的结果动态计算调整,也称作反馈,直到分支在当前的目标宽度下被终结。

>>VC攻击引擎层。

VCF和VCT,简称VC,也就是连续攻击取胜引擎。求解速度和求解严格精确至关重要。
如何在0.01S内进行深度为几十步的攻击?计算攻防时黑与白之间的无关性以及各自的相关性。
无关性的考虑可以化解对方的随意反攻,相关性的考虑可以使自己的进攻关联,保持节奏和组织性,不至盲目,二者结合使自己的进攻如行云流水,长驱直入,势如破竹。
1.00 版本的vc设计是近似的,因为在VC中考虑了宽度估计,而不是全面地推理。因此终结也是近似的,很容易打败。1.20版本vc设计是严格的,但是程序中 “相邻三子形成的连活三只需防守与两边的两个空白位置”是一条有漏洞的攻防逻辑,因此1.20版本也在出来一个月后被打败。1.22版本改正了1.20版 本的上述漏洞,只改动了一个字符,也就是紧邻活三的防守掩码。以前我所认为的很严格的推理逻辑在后来仍然被发现了意外漏洞的存在,因此对于1.22的VC 引擎,我也不知道是否还有逻辑漏洞存在,暂时没人打败1.22并不能说明漏洞不存在。

以上算法实现后,执行终结命令,经过半个月的连续计算后,会生成一个完整的地毯必胜树,包含大约百万个棋子节点,树的所有叶子都是可以VC求解的,如果VC求解引擎不存在漏洞,那么五子棋是必胜的。

终结者程序很小,算法部分的代码只有一万多行,编译结果只有一百多K,而且运行只需要几M的内存,必胜树只有百来万个节点。

下面是读取必胜树的代码,你可以用下面的函数操作终结者资源里面的必胜树:

 1 //将资源文件读取到数组book[]中,调用此函数就可以生成一颗必胜树
 2 //int key=1;//!!!!!!!
 3 //传入当前key和指向子孙的指针root,将root指向的树生长完整
 4 //上层调用此函数之时key已经指向[的下一个字符(,我的处理:
 5 //(--添加root的孩子
 6 //)--添加孩子完毕,可有可无
 7 //[--为孩子添加子孙
 8 //]--root树生长完成
 9 //参数为长子的指针的指针和父亲的指针
10 void make(NODEOFTREE ** root,NODEOFTREE *parent)
11 {
12 /* *root指向节点,我们要给*root赋值 */
13 int rootOK=0;
14 NODEOFTREE *rightC=NULL; /* rightC总是指向刚刚加入的兄弟 */
15 NODEOFTREE *rightT=NULL; /* rightT指向新开辟兄弟 */
16 
17 if(book[key]!='(')
18 {
19    F5_PRINT(1," make book[key]!='(' ");
20    return;
21 }
22 
23 do
24 {   
25     switch(book[key])
26     {
27 case '[' : //fprintf(outcourse," 开始生孩子... \n");
28                key++;
29                make(&(rightC->down),rightC);
30                break;
31 case ']' : //fprintf(outcourse," 孩子完成... \n");
32                key++;
33                return ;
34 
35          case '(' : /* 加一个兄弟 */
36                 
37                 rightT=(NODEOFTREE *)malloc(sizeof(NODEDATA));
38                 if(rightT==NULL)
39                 F5_PRINT(1," make rightT==NULL");
40     node_mum++;
41                 memset(rightT,0,sizeof(NODEDATA));
42                 rightT->data[0]=cover(book[key+1],1);
43                 rightT->data[1]=cover(book[key+2],2);
44                 if(key==1)
45                 rightT->data[2]=1;/*第一个子是黑子*/
46                 else
47                 {/*和父亲的颜色相反*/
48                 if(parent->data[2]==1)rightT->data[2]=2;
49                 if(parent->data[2]==2)rightT->data[2]=1;
50                 }
51                 rightT->right=NULL;
52                 rightT->down=NULL;
53                 
54                 if(rightC!=NULL)
55                 {
56                 rightC->right=rightT;
57                 }                
58                 rightC=rightT;
59                 
60                 
61                 if(rootOK!=1)/*兄弟的一个,即老大*/
62                 {*root=rightT; rootOK=1;
63                 }
64                 
65                 key=key+3;
66                 // fprintf(outcourse,"加一个兄弟(%c-%c-%c)\n",rightC->data[0],rightC->data[1],rightC->data[2]);
67                 
68                 break;
69      case ')' :
70                 key++;
71                 break;
72                 
73      case '\0'//fprintf(outcourse,"不可能有此一字,在最后一个]的时候返回!\n");
74                 break;      
75 
76      default : key++;
77                 //fprintf(outcourse,"字符串中有错误的字符!\n");
78                 break;
79 }
80 } while(book[key]!='\0');
81 return;
82 }


 

posted on 2013-02-27 20:56  吴昊系列  阅读(1316)  评论(0编辑  收藏  举报

导航