本文分为2部分,第1部分继续深入分析子力的概率问题,第2部分记录下刚刚碰到的一个非常棘手的bug,解决这个bug后,目前这个版本基本上没有什么明显的bug,可以作为版本为2.0。如果全部着法都搜索的话,1秒最多搜4层,军棋每步可行的走法太多,搜索已经很难优化了,接下来的主要优化在局面评估和局部搜索,目前2.0版本的测试结果如下:

引擎A vs 引擎B 战绩(胜:负:和)
1.1 vs 1.0 8:2:0
1.2 vs 1.1 8:2:0
1.2 vs 1.0 10:0:0
2.0 vs 1.2 8:1:1
2.0 vs 1.1 8:0:2

1.概率分析

子力的概率判断在四国军棋中起着非常重要的作用,之前已经对这方面做过分析,现在调试后发现之前的分析还是太粗糙了,这次将会做的更加精细。这次概率优化主要针对地雷和炸弹的摆放,按照军棋的规则,炸弹不能放第一排,地雷只能放最后2排,为了说清这个问题,现在构造一个简化的场景,如下图

现在有2个炸弹和2个地雷,规定炸弹不能放第一排,地雷不能放最后一排,假设a4没被碰过,a9被碰过,那么a4不是炸弹的概率是多少?

因为a4没被碰过,所以a4可能是炸弹,这9个子里有5个子可能是炸弹,所以概率p=(5-2)/5=0.6,这是错误的做法,因为没有考虑炸弹不能放第一排这个条件。

我们发现a4在第2排,a4不可能是地雷,所以正确的概率应该是p(a4不是炸)=1-p(a4是炸弹),那么a4是炸弹的概率是多少呢,现在用nBomb表示炸弹的概率,nLand表示地雷的个数,nMayBomb表示可能的炸弹个数,nMayLand表示可能的地雷个数,现在nBomb=2,由于a9被撞过所以不是炸弹,所以nMayBomb=5,所以a4是炸弹的概率为p=nBomb/nMayBomb = 2/5=0.4

上面的算法仍然存在问题,虽然a4~a8这5个子都可能是炸弹,但是这里面可能混杂着地雷,比如a7、a8是地雷,a9是大子,这时概率是2/3,所以正确的做法是分母为所有可能是炸弹和地雷的棋减去地雷的个数即6-2,现在设nLand=2,nMayLand=3,nMayBombLand为既可能是炸弹也可能是地雷的数量,这个值可由软件检测出来,现在设为2(即a7和a8),所以有

p(a4是炸)=nBomb/(nMayLand+nMayBomb-nMayBombLand-nLand)=2/(5+3-2-2)=2/4=0.5,当然a9可能是地雷也可能不是地雷,这里是一个平均估算的概率。

现在考虑a7不是炸弹和地雷的概率,由于a7在最后一排且没有撞过所以地雷和炸弹都有可能,a7不是地雷的概率是1-p(地雷)=1-2/3=1/3,在此基础上再计算不是炸弹的概率就得到结果

p = (1-p(炸弹))*(1-p(地雷))=(1-0.5)(1-2/3)=1/6

这是2种比较困难的情况,当然还有许多其他情况,可以按照类似的方式算出。在实际代码中要比上述场景繁琐的多,有非常多的细节需要考虑,这些都需要不断的调试来解决,这里就不详细介绍了。

2.Bug调试记录

接下来分析一个bug的解决过程,因为这个bug是随机出现的,不能复现,而且里面的过程有点复杂,所以在这里记录一下。bug是这样的,当2个引擎对弈时,其中一个会出现内存崩溃现象,奔溃的引擎使用AlphaBeta1函数搜索,打印信息如下

......
search1 num 6302
gen num 6655
key num 0 0
time 0
best 10 11 12 10
gen time 0
gen0 time 47070
depth 3 value -118
best cnt 3 val -120 per 16
06 04 04 06 03 02 07 00
00 00
NULL
alpha1: -8 depth 3
best cnt 2 val -28 per 241
06 05 06 0B 02 00 00 00
00 00
best cnt 2 val -200 per 14
06 05 06 0B 04 00 00 00
00 00
alpha1: -38 depth 2
move
best cnt 1 val -10000 per 256
0A 0B 0C 0A 02 00 00 00
00 00
depth 1 val -10000 per 256
0A 0B 0C 0A 02 00 00 00
00 00
depth 2 val -10000 per 241
06 05 06 0B 02 00 00 00
00 00
depth 2 val -10000 per 14
06 05 06 0B 04 00 00 00
00 00
depth 3 val -10000 per 16
06 04 04 06 03 02 07 00
00 00
depth 4 val -10000 per 128
0D 0A 0C 0A 02 00 00 00
00 00
depth 4 val -10000 per 64
0D 0A 0C 0A 03 04 00 00
09 10
end0 [test3] test3 2184 cygwin_exception::open_stackdumpfile: Dumping stack t
race to test3.exe.stackdump

在搜索第4层的时候出现崩溃,当我把这个复盘保存下来再调用引擎去分析这个局面时不再出现崩溃,观察打印信息,在move后value的值就变为了异常的-10000,这个值是作为α 的负无穷大,现在情况是轮到对方下棋,引擎正在分析,此时对方行棋后,引擎收到行棋指令,会打印move,并把pJunqi->move置1来结束分析。

接下去要做的事就是分析val为什么会是-10000,pJunqi->move置1会导致TimeOut(pJunqi)函数返回1,

     if( TimeOut(pJunqi) ){pData->cut = 1;break;}

搜索过程中会遇到很多循环,pData->cut是结束循环的标志,"alpha1: -38 depth 2"这行信息是在SearchBestMove()函数中打印,所以下面代码中

search_data.mxVal = SearchBestMove(pJunqi,aBestMove,cnt,alpha,beta,&search_data.pBest,depth,1);

search_data.mxVal的值一开始在第2层的时候是-38,这是正常的,最后为什么会被改为10000,从而导致第一层的分数为-10000,所以只能是通过调用SearchMoveList继续递归第3、4层的时候得到分数10000

        SearchMoveList(pJunqi,pSrc,0,&search_data);val = search_data.mxVal;

search_data.mxVal的修改只能是在SearchAlphaBeta()函数中进行,search_data是作为每一层共享的局部结构变量,每一层都有一个search_data结构体,它们是不同的,search_data传入SearchAlphaBeta()后为pData指针

val = CallAlphaBeta1(pJunqi,depth-1,alpha,beta,pData->iDir);
        if( val>pData->mxVal ){pData->mxVal = val;...}

从上面的代码可以看到,第2层的分数为10000,只能是val为10000,所以第3层的分数为-10000(这里上一层是下一层分数的负值),但是第4层的分数为直接评估局面后的分数不可能是10000,这样第3层的val不为-10000,而search_data.mxVal的初始值为-10000,必然会被更改,那为什么没有被更改呢,其实顺着这个思路往下走,就能得到答案,而我当时思路比较混乱,想不到那么深,第一选择是把bug重现出来,通过调试器来分析。

既然在"alpha1: -38 depth 2"这行信息出问题,那么我可以在这行打印后添加代码pJunqi->move=1把bug重现出来,但是还是没有重现出来,打印的信息里并没有出现“best cnt 1 val -10000 per 256”,也没有出现崩溃。这说明pJunqi->move置1的时机选择不对,需要再稍微让代码运行一段时间,在某个结点置1才能重现,这种情况让重现变得比较困难。

再仔细想一下,pJunqi->move影响的是TimeOut函数,TimeOut会结束搜索,为什么一开始就结束搜索不会有问题,只有过一段时间才有问题呢,想不明白,先看看在"alpha1: -38 depth 2"和“best cnt 1 val -10000 per 256”这2处代码中间调用了几个TimeOut,在开始的地方添加如下代码

    if( cnt==2 && search_data.mxVal==-38 ){pJunqi->debugFlag = 1;pJunqi->debugCnt = 0;}

在TimeOut中添加如下代码

 if( pJunqi->debugFlag ){pJunqi->debugCnt++;log_a("debugCnt %d",pJunqi->debugCnt);}

这时打印后发现pJunqi->debugCnt有3000多次,把代码改成如下

 if( pJunqi->debugFlag ){pJunqi->debugCnt++;if( pJunqi->debugCnt>100 ){pJunqi->bMove = 1;}}

这时后bug终于复现了,而且是每次都出现,接下来就容易多了,用pJunqi->debugTest记录递归层数,打印如下信息

log_a("debugCnt %d %d",pJunqi->debugCnt,pJunqi->debugTest);

pJunqi->debugTest的值在96之前都是3和4,第96次为2,所以把条件设为pJunqi->debugCnt>95,通过单步运行就可以知道在val = CallAlphaBeta(pJunqi,depth-1,alpha,beta,iDir);得到val的值后有时并不会立即去更新search_data.mxVal,因为发生碰撞后有3种情况,需要算平均值,所以执行goto continue_search;跳过mxVal的更新继续下一次搜索,而TimeOut刚好在continue_search后面,而这时TimeOut返回1,直接跳出循环,导致mxVal没被更新,停留在-10000,也就是说第3层刚出现TimeOut时,正在搜索的棋刚好是碰撞时才会复现这个问题。解决的方法很简单,因为SearchAlphaBeta只是搜索单步棋的所有可能,所以在TimeOut跳出循环使没有意义的,把break去掉,只要保留pData->cut=1就可以了。

接下来分析出现崩溃的问题,既然可以复现,通过之前文章介绍的的方法:

C语言调试内存访问出错而引起的程序崩溃问题

可以迅速定位到是在SetBestMove函数里pResult的值为0,导致访问非法的邻接表pJunqi->aBoard[p1.x][p1.y].pAdjList,这是传入的search_data.pBest值为0导致的,pBest是一个指针,其地址有2个来源,一个是搜索最佳变例的aBestMove[0].pHead->result[4]这是一个数组,存放四种行棋的结果,一个是正常搜索时每层的最佳着法,存放在 pJunqi->pMoveList里,不管哪种情况都不会出现内存被提前释放,那么接下来思考的是search_data.pBest的值在哪里被修改了,反复看代码,只有在以下地方被修改

        if( val>pData->mxVal ){pData->mxVal = val;if( aBestMove[cnt-1].mxPerFlag1 ){UpdateBestMove(aBestMove,p,depth,cnt,isHashVal);pData->pBest = &p->move;if( cnt==1 ){PrintBestMove(aBestMove,alpha,depth);}}//更新alpha值if( val>alpha ){            pData->alpha = val;}}

然而调试时发现这里并未被修改,然后通过打印信息反复确定修改的地方,定位在了下面的代码

        if( val>=beta ){if( -INFINITY==pData->mxVal && aBestMove[cnt-1].mxPerFlag1 ){UpdateBestMove(aBestMove,p,depth,cnt,isHashVal);if( cnt==1 ){PrintBestMove(aBestMove,alpha,depth);}}pData->mxVal = val;pData->cut = 1;break;}

也就是在UpdateBestMove处被修改了,这下终于明白了,search_data.pBest虽然没被修改,但存放的是aBestMove[0].pHead->result[4]数组中的其中一个地址,在更新最佳变例时,这个值被修改了,但是search_data.pBest指向的地址却没有被修改,所以更新后这个地址存放的值可能是空值。那么beta是10000,为什么这个条件会进来的,通过上面的分析可知道,在第2层时,遇到碰撞的着法会导致search_data.mxVal未被更新,停留在初始值-10000,从而返回到第一层为10000。最后的解决办法很简单,在 UpdateBestMove下面加一句pData->pBest = &p->move;即可

3.源代码

https://github.com/pfysw/JunQi

四国军棋引擎开发(9)子力概率判断分析相关推荐

  1. 四国军棋引擎开发(1)随机下棋

    现在开始来开发四国军棋的引擎,所谓引擎就是根据当前的局面给出最佳的下法,而界面只是一个显示的功能.目前由玩家控制自家和对家的棋,由引擎控制上家和下家的棋. 1.通信 和界面类似,socke通信放在一个 ...

  2. 四国军棋引擎开发(7)概率分析与搜索优化

    1.概率分析 四国军棋属于不完全信息博弈,我们是看不到敌方的棋子,但是可以通过棋子间的碰撞来判断敌方的子力分布情况和棋子大小的概率. 当棋子产生碰撞后,可能的判决结果有吃子.打兑.撞死3种结果,有时还 ...

  3. 四国军棋引擎开发(5)着法生成与棋谱分析

    1.着法生成 软件下棋时需要搜索大量的局面并对局面进行评估从而选出最好的着法,每一次行棋时生成所有可行的着法,每个着法产生后对应一个新的局面,然后下一家在新的局面基础上再生成所有着法. 军棋软件和普通 ...

  4. 四国军棋引擎开发(6)alpha-beta剪枝算法

    在讲alpha-beta剪枝算法之前先要了解最大最小算法,在棋类游戏中,给每一个局面打一个分数,轮到自己下时会选择有利于自己的下法,即选择局面分数高的,而对手会选择更加不利于自己的局面,即分数最低的. ...

  5. 四国军棋引擎开发(4)子力判断和局面评估初步

    1.子力判断 子力判断在局面评估中起着非常重要的作用,在前一篇文章中已经介绍了子力判断的部分,那时相对还比较粗糙,这次会更细致的分析并优化上一次的不足. pLineup->type用来代表棋子的 ...

  6. 四国军棋引擎开发(10)局面评估优化

    这次对局面评估做了一些优化,棋力有了一些提升,可以定为2.1版本,测试结果如下: 引擎A vs 引擎B 战绩(胜:负:和) 1.1 vs 1.0 8:2:0 1.2 vs 1.1 8:2:0 1.2 ...

  7. 四国军棋引擎开发(11)多线程搜索

    由于现在没有什么好的办法优化剪枝来增加搜索深度,所以现在通过不同的方法进行搜索,最后综合各种搜索方法的结果选择最佳着法.每一种搜索方法是独立的,所以单独放在一个线程里搜索,如果CPU是多核的,操作系统 ...

  8. 四国军棋引擎开发(12)关键步加深搜索

    调了很久终于能够更新一个版本了,这东西是越来越难调了,每一次输棋都要处理茫茫多的复杂逻辑,而且有些bug隐藏在递归的最深处很难定位,真希望软件可以像人一样自己学会想算法调代码做验证. 这次更新大的框架 ...

  9. 四国军棋引擎开发(8)主要变例提取

    alpha-beta剪枝算法对着法的搜索顺序有比较高的要求,最好是先搜索好的着法再搜索坏的着法,这样就可以最大程度的进行剪枝. 在搜索前我们当然不知道着法的优劣,如果知道了那就不需要搜索了,但我们搜索 ...

最新文章

  1. 【spring boot】 使用 RestTemplate
  2. SQL语言基础:常用的数据查询语句
  3. python语法基础整理_Python基础
  4. 四川中职计算机专业考的学校,四川中职学校哪家专业
  5. WF的性能特征(一)
  6. 学习HanNLP2015年12月4日 16:24:53
  7. Arduino-1602-LiquidCrystal库
  8. 畅玩新方式 | Google Play 游戏 Beta 版在 PC 上发布
  9. 面试算法 香槟塔 ,算法:暴力算法
  10. 【因果推断的统计方法】观察性研究和可忽略性
  11. Vim的ZZ、ZQ和Ctrl-Z,提高浏览效率
  12. 计算机网络:路由协议分类——IGP和EGP
  13. Python二级--奖学金评选2
  14. NLP逻辑回归模型(LR)实现分类问题实例详解
  15. UE4插件与编辑器Slate
  16. 大写日期(大写日期10月前要写0吗)
  17. 计算机应用模块等级考试大纲,全国计算机等级考试大纲 年版.doc
  18. 生日祝福html_说祝福 | 祝4月的你生日快乐
  19. 因式分解,输出素数因子及其指数
  20. 单片机实验——熟悉单片机开发环境和指令系统

热门文章

  1. kubernetes-service详解
  2. 补充番外4:创建Koroutine协程仓库并提交代码,vscode关联远方仓库
  3. 中投公司副总经理谢平:互联网金融风险更低
  4. 文献管理软件citavi(西塔维)安装教程
  5. Linux系统下设置redis的密码 与 redis 命令
  6. deepinv2 添加打印机_【小教程】如何在deepin操作系统中配置打印机
  7. 2022湖南多校对抗赛第六场
  8. NPOI以文本流的方式导出多个EXCEL,打开其中文件报“Excel 已完成文件级验证和修复.....或丢弃”
  9. 人工智能学习之使用百度大脑在线AI Studio实现人体皮影戏
  10. 差错控制编码(原书第2版)/shu lin/daniel j.costello/jr