四國軍棋引擎開發(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;
}
上面的考慮還是挺粗糙的,例如有炸和無炸的影響,有沒有令子,有些棋根本無法與對方大子接觸所以暗價值很小,而有些子可以迫使對方令子偏線或騙對方工兵,暗價值非常大,有些子暗打兌讓敵方誤以為本方少炸也產生了很大的暗價值,這些都是後期需要考慮的事情。