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

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

这次更新大的框架没有什么改动,只是调了很多很多的bug,最主要添加的功能是在搜索时记录一些关键步,在关键步如威胁到军旗时,再加深2层,从而在残局中能调集一些子力防守。

我不打算把这个软件的棋力做到顶尖,做到能稳赢我自己就可以了。这次软件的棋力还是有点进步的,总算有一点下棋的样子了。现在和我下,如果我下的很浪的话,能偶尔赢我一两把,当然我认真下那就一点机会都没有,虽然我下棋很菜...

当然软件的问题还有很多,即使不考虑棋力因素,这里面有很多bug还要调试。限制棋力最大的关键因素还是搜索深度不够,接下来考虑的是怎么把关键步提取出来搜索推演,这是一个很大的难题。另外软件对于一些全局的价值判断不清,不能做出有效的进攻,尤其是残局时走的棋都没有针对性,有时候局面大优不去进攻,有时家里很危险还不知道防守。还有软件不会控盘,不知道怎么用大子看住线路,在子力充足时没有把对方关死的意识。软件对于行棋过程中信息的提取还有很大的问题,仅仅只是根据子力的阵亡来计算概率,但是棋子的行棋走位对于概率的影响是很大的。

目前这个版本是2.3,战绩情况如下

源代码:
https://github.com/pfysw/JunQi 

下面就来介绍这次代码的改进,当然代码改的地方太多了,这里只挑一些关键的地方简单介绍一下。

1.关键步加深搜索

一开始想到关键步加深搜索是针对某些送吃的情形,送吃不一定是坏事,有些时候是有益的,比如对家进攻一方时,我方小子送吃隔档,阻止敌方报复。还有比如炸弹在52,旗上被飞过后,敌方要从主线进攻,为了能让炸弹回防,把旗上的棋移到角上送给敌方吃,然后53的棋移到旗上,又或者敌方要夺旗时,在线路上送吃挡一挡,让我方令子回防,这些情况下送吃是有好处的。但是有时候送吃是没有必要的,只要多搜2层,就能看到即使不送吃也能回防,或者送吃也没有用。如下图所示

蓝方算到旅长要吃工兵,用排长送吃阻挡,但是即使排长送吃了也不能阻止旅长吃到工兵,这时候只要多搜2层就知道结果是一样的。要实现加深搜索的具体步骤是:

在第一层行棋后,把该棋子标记为pEngine->pFirstMove,在之后的搜索中如果敌方有棋子吃掉这个子,说明是送吃,这时把pJunqi->gFlag[FLAG_PREVENT]标志位置1,。然后再搜索到最后一层本该调用局面评估函数时,将其替换为2层的alpha-beta搜索函数,代码如下

        if( 1==pJunqi->gFlag[FLAG_PREVENT] )
        {
            pJunqi->gFlag[FLAG_PREVENT] = 2;//这里做个标记,防止标志位在加深搜索时被重复置位
            pJunqi->cnt--;
            //加深2层搜索
            val = CallAlphaBeta1(pJunqi,2,alpha,beta,iDir,isMove);
            pJunqi->cnt++;
            pJunqi->gFlag[FLAG_PREVENT] = 1;
        }

        else
        {
            val = EvalSituation(pJunqi,0);

        }

但是有个问题,出现真的走出送吃的概率比较少,而且真的送吃了也基本不是什么致命的问题。但是在每一次的搜索中基本都会出现送吃的情况,如果在第二层就搜到有棋吃掉这个子,那么后续的搜索就都增加了2层,而且有时还会出现好几个子送吃的情况,这样搜索的时间大大增加,通过测试大概增加了3到4倍的时间,用这么多的时间来解决这个问题是不明智的,所以代码中去掉了针对送吃的加深搜索。

但是上面的代码在在另外一种场景中有着重要作用,尤其是残局时,某些关键步需要加深搜索。如下图所示

实战中软件下出了错误的走法,连长挡3线,因为只搜索橙色和紫色2家4层可以搜索2个来回,图中的旅长是暗棋,此时算到旅长吃连长下底线就结束,认为不会有大的威胁,但是只要再多算2层就知道这是绝杀。如果39直接回防也是不行的,旅长已经到底线,你无法确认旅长是不是工兵,很可能被偷袭。正确的下法是连长挡1线,确认旅长不是工兵,然后军长再进中营回防,因为如果挡在3线,接下来旅长就直接进下营了,导致军长无法回防。

所以在上面的场景中只搜4层是不够的,我们把旗上、角上、底下、下营这几个位置做上标记,如果旅长在第4层到达这些位置,说明有危险,那么进行加深2层搜索,那么如果是错误的走法,将会直接算到旅长抗旗,得到一个很低的分数。因为只有搜索到第4层才决定是否加深搜索,所以相对来说消耗的时间会少很多。

2.吃子加深搜索

在搜索中,如果最后一个轮次是吃子,那么局面的评估分数会随着吃子而增加,因为是最后一个轮次所以不知道后面会怎么样。但是继续推演下去,吃子这步棋很可能是亏的,如果拿中级子力去吃可能被敌方令子报复,拿令子去吃可能会被敌方炸掉。所以这个时候就要继续搜索下去,来明确到底该不该吃,或者自己的子被吃了能不能报复回来。

我之前用hash表来记录局面来避免重复局面的搜索,但出于搜索效率的考虑,那段代码被注释掉了。现在那段hash表相关的代码正好可以简单的改造一下用来记录有没有吃子。

现在搜索总共4层,在搜索到最后一层进行局面评估,这里有一个4步棋的着法链,如下图所示,绿色小球代表每一步行棋

在每一层中遇到吃子的着法,将其记录到hash表里面

            if( p->move.result==EAT )
            {
                val = RecordMoveHash(pJunqi,&pJunqi->paHash,p,val,1);
            }

 在这里我们会设置标志变量

pEngine->eatInList = 1;//着法链中是否有吃子
pEngine->eatIndex = 0;//初始化计数变量,此后每一层没吃子,该变量都会加1

此后会递归调用CallAlphaBeta1进入下一层的搜索,递归返回后,即当前层的这 一步棋搜索完毕,将其hash表中记录的着法删除,因为我们记录的只是着法链中是否出现吃子情况

            if( p->move.result==EAT )
            {
                FreeMoveHashNode(pJunqi,p);
            }

接下来到最后一层要进行局面评估时,我们得到了一个着法链,根据着法链中是否有吃子情况来决定是否加深搜索。我们把前面关键步加深的代码写完整,为了搜索效率的考虑,已经有关键步加深的情况下,我们不再进行吃子加深搜索,在吃子加深搜索时把pJunqi->gFlag[FLAG_EAT]置1,此后不会再进到这来,直到搜索完毕。

    if( depth==0 && !pJunqi->gFlag[FLAG_EAT] )//在吃子搜索中不再进入局面评估
    {
        if( pJunqi->gFlag[FLAG_PREVENT]==2 )//在阻挡搜索中不再搜索吃子
        {
            pEngine->eatInList = 0;
        }

        if( 1==pJunqi->gFlag[FLAG_PREVENT] )
        {
            pJunqi->gFlag[FLAG_PREVENT] = 2;
            pJunqi->cnt--;
            val = CallAlphaBeta1(pJunqi,2,alpha,beta,iDir,isMove);
            pJunqi->cnt++;
            pJunqi->gFlag[FLAG_PREVENT] = 1;
            pJunqi->gFlag[FLAG_EAT] = eatFlag;
        }
        else if( SearchEat(pJunqi) )
        {
            //将吃子搜索标志位置1
            pJunqi->gFlag[FLAG_EAT] = 1;
            pJunqi->cnt--;
            val = CallAlphaBeta1(pJunqi,depth,alpha,beta,iDir,isMove);
            pJunqi->cnt++;
            pJunqi->gFlag[FLAG_EAT] = 0;
        }
        else
        {
            val = EvalSituation(pJunqi,0);
            pJunqi->gFlag[FLAG_EAT] = eatFlag;
        }
    }

要注意上面的代码是从递归的角度看的,如果进入吃子加深搜索递归了,那么之前会判断搜索是否结束

  //从最后一次吃子到当前经过了多少层
 //如果经过了4层一个轮回,就可以结束了
    pEngine->eatIndex++;
    if( pJunqi->gFlag[FLAG_EAT] )
    {

        eatFlag = 1;
        depth = 0;
        if( !SearchEat(pJunqi)  )//判断是否结束
        {
            pJunqi->gFlag[FLAG_EAT] = 0;
        }
    }

因为这是一个递归调用,搜索层数超出限定时,为了方便把 pJunqi->gFlag[FLAG_EAT]置0从而可以进入局面评估的条件,递归还没结束,所以出来之后要把pJunqi->gFlag[FLAG_EAT]重新还原回去,根据eatFlag判断当前是否处于吃子加深的搜索状态。

在吃子加深的搜索中,没必要全部着法都搜,只搜能和记录在hash表里的棋产生碰撞的行棋

    if( pJunqi->gFlag[FLAG_EAT] )
    {
        if( pDst->type!=NONE )
        {
            rc = !CheckMoveHash(pJunqi,pDst);//目标pDst不在hash表里丢弃掉
        }
        else
        {
            rc = 1;
        }
    }

3.其他

这次代码改的真的是太多,没时间把所有的都做详细说明,现在只把一些关键的地方简要说明一样,留个印象。

1.上一个版本中要对各种搜索结果求和,为了保证每一步分数的精确性,所以第一层并没有向下传入剪枝参数beta。但是现在加入了加深搜索,在4层的搜索中的时间将会超出30秒,这是不可接受的,所以只在3层搜索中获取每一步的精确分数,在第四层时照常剪枝只要获取最佳着法的分数就可以了。3层搜索获得的所有方式搜索的分数相加并排序选出最好的一步棋,在分数相同时再排序还包含了single、path、connect等搜索方式的分数,这里的最好的一步棋和之前默认搜索、左搜索、右搜索的4层搜索的最佳着法再通过ReSearchInDeep函数计算这些着法的所有4层搜索方式的分数,然后再求和选出分数最大的一步棋,这里为了安全性考虑,如果有一种着法,最低分数比其他着法低50分以上,说明这步棋有危险将其排除,这些工作都在VerifyDeepMove函数里完成。

2.在开始搜索前,根据整个局面,设置一些全局标志位,后续可以根据这些标志位做一些针对性的搜索

void CheckGLobalInfo(Engine *pEngine)
{
    //判断每家活着的棋
    //判断自家是否只剩一个子了,从而不要做出自杀的行为
    CheckLiveChessNum(pEngine);
    //判断一些没有价值的空营,这些营在局面评估时不会加分
    CheckEmptyCamp(pEngine);
    //根据棋局的进程和打兑的子力,来判断敌方是否还有炸
    CheckBebombNum(pEngine);
    //根据搜索时间,尤其是残局子力较少时可以稍微增加搜索深度
    SetMaxDepth(pEngine);
    //在优势局面僵持时,敌方还没死时,先假定一个军旗位进行进攻
    CheckDarkJunqi(pEngine);

}

3.正常情况下,如果敌方工兵没有走明,那么是不会搜索敌方工兵的行棋的,这时候如果底线空了,很可能被偷袭,所以如果敌方还有工兵的话,必须要从暗子中找出一个子来模拟工兵


        if( pSrc->pLineup->type==DARK && pSrc->isRailway &&
            pJunqi->aInfo[pSrc->pLineup->iDir].aTypeNum[GONGB]<3 && !flag
            && !pJunqi->gFlag[FLAG_EAT] && !pJunqi->gFlag[FLAG_PREVENT]  )
        {
            temp[0] = pSrc->type;
            memcpy(&tempLineup,pSrc->pLineup,sizeof(tempLineup));
            //进行模拟工兵类型
            pSrc->type = GONGB;
            pSrc->pLineup->type = GONGB;
            pSrc->pLineup->mx_type = GONGB;
            pSrc->pLineup->isNotBomb = 1;
            pSrc->pLineup->isNotLand = 1;
            pJunqi->aInfo[pSrc->pLineup->iDir].aTypeNum[GONGB]++;
            pJunqi->aInfo[pSrc->pLineup->iDir].aLiveTypeSum[GONGB]++;
            search_data.isGongB = 1;
            SearchMoveList(pJunqi,pSrc,&search_data);
            //搜完了要把工兵还原回去
            search_data.isGongB = 0;
            pJunqi->aInfo[pSrc->pLineup->iDir].aTypeNum[GONGB]--;
            pJunqi->aInfo[pSrc->pLineup->iDir].aLiveTypeSum[GONGB]--;
            memcpy(pSrc->pLineup,&tempLineup,sizeof(tempLineup));
            pSrc->type = temp[0];
            //搜到有效的工兵着法,把flag置1,下次就不要模拟了
            if( 2==search_data.isGongB )
            {
                flag = 1;
            }
        }

4.其他的还有就是在局面评估中的各种优化,在AdjustMovePercent函数里对各种着法的概率调整更加精细了,另外修复了无数杂七杂八的bug这里就不一一细说了。

猜你喜欢

转载自blog.csdn.net/pfysw/article/details/88595064