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

1.子力判断

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

pLineup->type用来代表棋子的类型,这里是用枚举变量来表示,要注意级别大的变量值小,如40的值是5,39的值是6,排长的值12,工兵的值是13,所以在级别比较的时候要注意区分变量的大小和级别的大小。本方棋子的类型是明确的,敌方棋子的类型未知所以一开始用DARK表示,但是撞过之后,我们就知道其最大的类型或最小的类型。比如我方37吃掉一个子,那么这个子最大也就是36,如果对方吃掉我方一个38,那么这个棋最小也有39。这时我们用pLineup->type表示最小的估算类型,pLineup->mx_type表示最大的估算类型。

但是如果棋下到中局后会产生一些碰撞,一些暗子就会根据已有已经明了的棋产生一个估算区间,这时新的碰撞后产生的估算不能超出上一次计算的范围。例如双方40打兑,39和2个38都吃过37,那么可以断定其他子最大37,这时如果其他子被本方39吃掉,那么就不能判断它最大38,而应仍然判断为37。

                //这里假定pSrc是本方的子吃掉对方的子pDst
                //现在pDst->pLineup->mx_type是37
                //pSrc->pLineup->type是39
                //以下条件会阻止pDst->pLineup->mx_type被更新
                if( pDst->pLineup->mx_type < pSrc->pLineup->type+1 )
                {
                    pDst->pLineup->mx_type = pSrc->pLineup->type+1;
                }

接下来我们再看一下除了基本碰撞之外的判断,后2排属于雷区,如果动棋或者被工兵飞过,那么可以判断不是地雷

    if( pSrc->pLineup->index>=20 )
    {
        pSrc->pLineup->isNotLand = 1;
    }

    //前面条件是工兵撞死
        if( pSrc->type==GONGB && pDst->pLineup->index>=20 )
        {
            pDst->pLineup->isNotLand = 1;
        }

如果出现碰撞,并且是1线以下的棋,则标记不是炸弹,因为1线以下的棋没摸过是有炸弹的可能。

        if( pDst->pLineup->index>=5 )
        {
            pDst->pLineup->isNotBomb = 1;
        }

如果自家的棋撞死了,经过之前的精确评估后发现这个子的最大可能不会比撞死的大,那么可以断定是地雷

            if( pSrc->pLineup->type<=pDst->pLineup->mx_type )
            {
                assert( pDst->pLineup->index>=20 );
                pDst->pLineup->type = DILEI;
            }

有了这些基本的信息后,我们就要对子力进行计算,如敌方这个棋的最大可能性,地雷还剩几个,炸弹还剩几个。

基本思路就是先假定这个这个子的最大可能性是司令,比如这个子吃掉了36,那么最小37,先查询现在大于等于司令的棋有几个,如果已经有1个了那么不可能是司令,再接着查大于等于39的棋有几个,如果有2个了,说明不可能是39。这里要考虑炸弹和地雷的影响,如果是后2排的棋不要算进去,这样可以排除地雷的影响。如果是是暗棋打兑,会把pLineup->bBomb置1,统计暗棋打兑的数量,根据剩余的炸弹数量,减去较小的,这样可以排除炸弹的影响。很多东西都很难描述,还是直接通过代码来解释吧,子力计算的函数都在AdjustMaxType()中实现。

//计算大于某个级别的数量总和,比如要计算大于37的棋的数量
//就要把当前已知40、39、38、37的数量加起来,再排除炸弹的影响
void GetTypeNum(u8 *aBombNum, u8 *aTpyeNum, u8 *aTypeNumSum)
{
    int i;
    int sum = 0;
    int sum1 = 0;
    int nBomb = 0;
    int sub;

    for(i=SILING; i<=GONGB; i++)
    {
        sum += aTpyeNum[i];
        sum1 += aBombNum[i];
        //这里排除炸弹的影响
        aTypeNumSum[i] = sum - ((sum1<(2-aTpyeNum[ZHADAN]))?sum1:(2-aTpyeNum[ZHADAN]));
        //高于当前级别的数量已超出最大值,那么超出的部分必定是炸弹
        if( (sub=sum-aMaxTypeNum[i])>nBomb )
        {
            nBomb = sub;
        }
    }
    aTpyeNum[ZHADAN] += nBomb;
    assert( aTpyeNum[ZHADAN]<=2 );
}

int GetMaxType(int mx_type, int type, u8 *aTypeNumSum)
{
    enum ChessType tmp;

    tmp = mx_type;
    while( tmp<type )
    {
        //大于等于tmp的数量已经到最大值,所以mx_type已经不可能是tmp
        //那这里为什么不退出而要继续搜索呢,这里还是举个例子
        //司令死掉,有3个子吃掉37,而大于等于39的子并没有到最大数量
        //那么是否可以判断最大就是39了呢,显然不是,后面发现,大于等于
        //38的数量也到了最大值,所以当前这个子最大只可能是37
        if( aTypeNumSum[tmp]>=aMaxTypeNum[tmp] )
        {
            mx_type = ++tmp;
        }
        else
        {
            ++tmp;
        }
    }
    return mx_type;
}

//待优化
void AdjustMaxType(Junqi *pJunqi, int iDir)
{
    int i;
    ChessLineup *pLineup;
    u8 *aTypeNum = pJunqi->aInfo[iDir].aTypeNum;
    u8 aBombNum[14] = {0};
    u8 aTypeNumSum[14] = {0};
    enum ChessType tmp;


    memset(aTypeNum, 0, 14);
    for(i=0; i<30; i++)
    {
        pLineup = &pJunqi->Lineup[iDir][i];
        if( pLineup->type==NONE || pLineup->type==DARK )
        {
            continue;
        }
        //疑似地雷的棋,不要把pLineup->type统计进去
        if( pLineup->index>=20 && !pLineup->isNotLand )
        {
            if( pLineup->type!=DILEI )
                continue;
        }
        //计算该子类型的总和
        aTypeNum[pLineup->type]++;
        //计算该子暗打兑的数量,打兑当中有些是炸弹,需要在后续判断排除
        if( pLineup->bBomb )
        {
            aBombNum[pLineup->type]++;
        }
    }
    //工兵大于3,说明多余的飞了炸
    if( aTypeNum[GONGB]>3 )
    {
        log_b("gongb zhad %d %d",aTypeNum[GONGB], aTypeNum[ZHADAN]);
        aTypeNum[ZHADAN] += aTypeNum[GONGB]-3;
        assert( aTypeNum[ZHADAN]<=2 );
    }
    //获取某个级别以上的数量总和,保存在aTypeNumSum里
    //这里是先把aTypeNumSum都算好,因为aTypeNumSum是固定的
    //如果后面再循环中算则重复了
    GetTypeNum(aBombNum,aTypeNum,aTypeNumSum);
    //这里先计算好暗子的最大可能性
    tmp = GetMaxType(SILING, GONGB, aTypeNumSum);

    for(i=0; i<30; i++)
    {
        pLineup = &pJunqi->Lineup[iDir][i];
        //NONE ~ SILING
        if( pLineup->type<=SILING && pLineup->type!=DARK )
        {
            continue;
        }
        //当现在估计的子力比之前算的小的话才更新
        if( pLineup->type==DARK )
        {
            if( pLineup->mx_type<tmp )
            {
                pLineup->mx_type = tmp;
            }
        }
        else
        {
            assert( pLineup->type>SILING );
            //这里计算吃过子的棋的最大可能
            pLineup->mx_type = GetMaxType(pLineup->mx_type,
                    pLineup->type, aTypeNumSum);
            //这里计算疑似地雷的棋,举个例子,对方司令已经死了
            //此时38撞雷,我们还不能判断是地雷,也可能是39,
            //如果又有另一个子吃了38,那么可以判断是地雷
            //这里比当前棋级别大的数量已经为最大值
            //后2排的pLineup->type是没有统计到aTypeNumSum里的,所以可以断定为地雷
            if( aTypeNumSum[pLineup->type]==aMaxTypeNum[pLineup->type] )
            {
                //后2排疑似地雷的type不会统计到aTypeNumSum里
                if( pLineup->index>=20 && !pLineup->isNotLand )
                {
                    if( pLineup->type != DILEI)
                    {
                        pLineup->type = DILEI;
                        aTypeNum[DILEI]++;
                    }
                }
            }
        }
}

2.局面评估

局面评估对于α-β剪枝算法非常重要,如果局面评估不准确,那么很容易漏算好的招法。由于局面评估涉及到的东西比较复杂,现在很难说清到底什么在局面评估中起着关键作用,所以现在就是根据子力判断建立一个基本的评估框架,可能现在对局面的评估很不准确,需要后续和搜索算法一起调试,优化评价结构和子力价值的评估分数。

首先军棋中的每一个子都有一个价值,除了基本价值外还有暗价值,比如二线以下的小子可以装炸弹来吓唬司令,如果军旗位没明可以利用假旗玩空城计,最后2排的棋属于雷区不到最后时刻最好不要动,动了就暴露不是地雷,这些都是暗价值。

现在把所有相关的价值定义在一个结构体里

typedef struct Value_Parameter_t
{

    int vAllChess;//一家所有棋的子价值
    u8  vChess[14];//14个作战子力类型的价值
    u8  vDarkLand;//后2排的暗价值,装地雷
    u8  vDarkBomb;//非1线棋的暗价值,装炸弹
    u8  vDarkJunqi;//假旗位的暗价值,装军旗
}Value_Parameter;

在pEngine对象里定义一个Value_Parameter成员变量,之所以不用宏定义是为了后面考虑让这些价值分数动态变化。在创建pEngine对象时会初始化相关价值分数,现在只是随便定个分数,当然可能非常不准确,会在后期调整。

void InitValuePara(Value_Parameter *p)
{
    p->vAllChess = 1600;
    p->vChess[SILING] = 100;
    p->vChess[JUNZH] = 90;
    p->vChess[SHIZH] = 80;
    p->vChess[LVZH] = 70;
    p->vChess[TUANZH] = 60;
    p->vChess[YINGZH] = 50;
    p->vChess[LIANZH] = 40;
    p->vChess[PAIZH] = 30;
    p->vChess[GONGB] = 55;
    p->vChess[DILEI] = 60;
    p->vChess[ZHADAN] = 65;
    p->vDarkLand =  10;
    p->vDarkBomb =  4;
    p->vDarkJunqi = 10;
}

评分现在很简单,就是变量4家的棋,计算每个子的分数。如果自家的棋死了则减去相应的分数,如果是地方的棋死了则加上相应的分数。基本框架如下

    for(i=0; i<4; i++)
    {
        if( !pJunqi->aInfo[i].bDead)
        {
            ... ...
            for(j=0; j<30; j++)
            {
               ... ...
               死了的棋,加减每个子的基本分数,
               相应的子还要计算相应暗价值的分
               活着的子看isNotLand和isNotBomb标志位
               来加减每个子的暗价值分数
               如果2炸都没了,那么每个子的vDarkBomb分数都要减掉
               不管isNotBomb有没有置位
            }
        }
        else
        {
            //加减p->vAllChess 
        }
    }

对方的棋不明,所以是Lineup->type和pLineup->mx_type的价值取评价值,如果是暗打兑,则打兑子力和炸弹价值取评价值。如果是被暗吃,则取pLineup->mx_type价值分数的一半

                                if( pLineup->bBomb )
                                {
                                    value += (pVal->vChess[pLineup->type]+
                                            pVal->vChess[ZHADAN])/2;
                                }
                                else if( pLineup->type==GONGB || pLineup->type==DILEI )
                                {
                                    value += pVal->vChess[pLineup->type];
                                }
                                else
                                {
                                    value += (pVal->vChess[pLineup->type]+
                                            pVal->vChess[pLineup->mx_type])/2;
                                }

上面的考虑还是挺粗糙的,例如有炸和无炸的影响,有没有令子,有些棋根本无法与对方大子接触所以暗价值很小,而有些子可以迫使对方令子偏线或骗对方工兵,暗价值非常大,有些子暗打兑让敌方误以为本方少炸也产生了很大的暗价值,这些都是后期需要考虑的事情。

3.源代码

https://github.com/pfysw/JunQi

猜你喜欢

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