1. 程式人生 > >四國軍棋引擎開發(4)子力判斷和局面評估初步

四國軍棋引擎開發(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