四國軍棋引擎開發(5)著法生成與棋譜分析
1.著法生成
軟體下棋時需要搜尋大量的局面並對局面進行評估從而選出最好的著法,每一次行棋時生成所有可行的著法,每個著法產生後對應一個新的局面,然後下一家在新的局面基礎上再生成所有著法。
軍棋軟體和普通的象棋軟體的著法生成有所不同,象棋是明棋,每一步行棋的結果都是確定的,而軍棋則不同,軍棋是暗棋,在產生子力的碰撞後可能會有不同的結果,所以還要根據之前的子力判斷分析生成可能的碰撞結果。
在下棋時引擎給出著法後是不知道判決結果,判決結果是由介面給出,搜尋時的著法格式也和判決的格式相同,產生著法後會把著法輸入到PlayResult()函式產生一個新的局面。每一個局面搜尋到的所有著法都放在一個雙向連結串列裡,當全部搜尋完畢後再清除連結串列。
struct MoveList
{
MoveResultData move;
MoveList *pNext;
MoveList *pPre;
int value;
u8 isHead;
};
搜尋時變數行棋方的30個棋子,碰到營、地雷、軍旗則跳過。根據每一個棋子再遍歷棋盤上的所有129個位置(4家的棋子再加九宮格),找出合法的著法。
for(i=0; i<30; i++)
{
pLineup = &pJunqi->Lineup[pJunqi->eTurn][i];
if( pLineup->bDead )
{
continue;
}
pSrc = pLineup->pChess;
if(pLineup->type!=NONE && pLineup->type!=JUNQI && pLineup->type!=DILEI )
{
for(j=0; j<129; j++)
{
... ...
}
}
}
這裡需要注意的是如果敵方是暗棋,則有可能是工兵,工兵的行棋路線和其他棋子是不一樣的,如果有些行棋只能是工兵可以走,那麼這步棋就要把這個暗子當作工兵來處理,處理完畢後再恢復原來的型別。
temp[0] = pSrc->type;
//只有敵方的棋才可能是暗棋,只考慮沒碰撞過的
//pSrc->pLineup->type是dark,那麼pSrc->type必然是dark,
//反之則不一定,反之則不一定,pSrc->type只是確定有無棋子,而不管棋子是什麼
if( pSrc->pLineup->type==DARK && pSrc->isRailway &&
pJunqi->aInfo[pSrc->pLineup->iDir].aTypeNum[GONGB]<3 )
{
//暫時設定棋子的位置為工兵,獲取工兵路線
//棋子的型別pSrc->pLineup->type還是dark沒有變
assert( pSrc->type==DARK );
pSrc->type = GONGB;
}
pDst = GetValideMove(pJunqi, pSrc, j);
pSrc->type = temp[0];
temp[1] = pSrc->pLineup->mx_type;
temp[2] = pSrc->pLineup->type;
if( pDst!=NULL )
{
if( pSrc->pLineup->type==DARK )
{
//如果得到的是工兵路線,則按工兵處理
if( !IsEnableMove(pJunqi, pSrc, pDst) )
{
assert( pSrc->pLineup->type==DARK );
pSrc->pLineup->type = GONGB;
pSrc->pLineup->mx_type = GONGB;
}
}
//根據不同的判決結果把著法新增到連結串列中
AddMoveToList(pJunqi, pHead, pSrc, pDst);
}
//恢復型別
pSrc->pLineup->mx_type = temp[1];
pSrc->pLineup->type = temp[2];
有了著法後必須生成相應的判決結果,判決的結果可能是移動、吃棋、打兌、撞死,敵方司令死後亮棋的位置,是否軍旗被扛。然後並不是每一種判決都是可能的,還要根據當前局面所產生的所有資訊來過濾掉不可能的結果,比如我方37撞死後,再拿36去碰肯定還是撞死不可能是吃棋或打兌。移動比較好確定,如果目標位置無子那就是移動,吃棋、打兌、撞死的情況比較複雜,針對這3種情況需要分別寫一個過濾函式。
- IsPossibleEat(pJunqi,pSrc,pDst)
- IsPossibleBomb(pJunqi,pSrc,pDst)
- IsPossibleKilled(pJunqi,pSrc,pDst)
在生成判決結果時,遍歷每一種判決,如果沒有附加的資訊那麼最後的移動結果就是著法和判決結果
//temp是著法格式,其中包含判決結果,再和pSrc、pDst一起生成最後的移動結果,並插入連結串列
InsertMoveList(pHead,pSrc,pDst,&temp);
當然遇到司令和軍旗時需要新增額外的可能結果,這些也需要分別處理
- AddJunqiMove(pJunqi,pSrc,pDst,&temp);
- AddCommanderMove(pJunqi,pSrc,pDst,&temp);
- AddCommanderKilled(pJunqi,pSrc,pDst,&temp);
過濾的細節和確定可能的判決結果這些事情非常繁瑣,這裡就不展開細講了,具體見原始碼。最後還有一點值得注意的是,著法和判決結果是合在一起存放的,著法可以選擇,但判決結果是不能選擇的,不可以把不同的判決結果拿來做alpha-beta剪枝,由於相同著法不同判決結果在連結串列中都是相鄰的,到時在搜尋時可以把所有相同著法得到的分值取平均值再進行剪枝。
2.棋譜分析
在局面評估和搜尋時需要除錯大量局面,所以需要有一個棋譜分析的功能,由介面將局面傳送給引擎分析,總不能一步一步擺棋吧,那樣就太麻煩了。
一開始需要在介面上做一個選單項,在“設定->分析”裡,當點選分析選單時(只在覆盤時有效)會向引擎傳送COMM_READY指令來複位引擎,此時處於覆盤狀態,pJunqi->bReplay為1,要先清0,否則無法通訊。在收到引擎的COMM_OK指令回覆後,接著傳送COMM_REPLAY指令通知引擎開始覆盤,即呼叫ShowReplayStep()函式將局面從開始走到當前局面,把每一步判決結果傳送給引擎,pJunqi->bAnalyse需要置1,表示現在是分析狀態,每一步走棋不要儲存棋譜。
case COMM_OK:
if( pJunqi->bAnalyse )
{
SendHeader(pJunqi, pJunqi->eFirstTurn, COMM_REPLAY);
ShowReplayStep(pJunqi, 0);
pJunqi->bAnalyse = 0;
pJunqi->bReplay = 1;
}
break;
if(!pJunqi->bReplay && !pJunqi->bAnalyse)
{
AddMoveRecord(pJunqi, pSrc, pDst);
}
這裡還修改了覆盤時的一個小問題,之前覆盤時每下一步棋都從頭下到尾,這樣越到後面感覺就越遲鈍,所以在ShowReplayStep()裡設定一個標誌位,代表是不是下一步,如果是下一步則在當前局面的基礎上行棋,否則從頭開始。
if( !next_flag )
{
preStep = 0;
ReSetChessBoard(pJunqi);
}
for(i=preStep; i<pJunqi->iRpStep; i++)
{
... ...
}
在引擎這邊通訊和行棋處理分別在2個執行緒裡,通過訊息佇列來通訊,之前還在想會不會UDP通訊太快導致訊息佇列溢位,目前測試不存在這種情況,佇列滿了後mq_send似乎會阻塞在那裡直到從佇列中取走資料後才解除阻塞,但是阻塞在那裡UDP應該會丟包,目前測試沒出現丟包,我也不知道為什麼。
while(1)
{
recvbytes=recvfrom(socket_fd, buf, REC_LEN, 0,NULL ,NULL);
DealRecData(pJunqi, buf, recvbytes);
mq_send(pJunqi->qid, (char*)buf, recvbytes, 0);
}
此外在除錯時遇到一個問題,這兩個執行緒都有資料列印,經常出現2個執行緒列印的資料混在一起,看不清列印資訊。所以做了一個列印函式的介面,把列印的資料都通過訊息傳送到一個執行緒裡列印。
void SafePrint(const char *zFormat, ...)
{
va_list ap;
char zBuf[50];
int len;
Junqi* pJunqi = gJunqi;
PrintMsg *pData;
va_start(ap,zFormat);
len = vsprintf(zBuf, zFormat, ap);
pData = (PrintMsg *)malloc(len+1);
pData->type = PRINT_MSG;
memcpy(pData->data,zBuf,len);
mq_send(pJunqi->print_qid, (char*)pData, len+1, 0);
free(pData);
va_end(ap);
}
void *print_thread(void *arg)
{
int len;
u8 aBuf[REC_LEN];
Junqi* pJunqi = (Junqi*)arg;
PrintMsg *pData;
while (1)
{
len = mq_receive(pJunqi->print_qid, (char *)aBuf, REC_LEN, NULL);
if ( len > 0)
{
pData = (PrintMsg *)aBuf;
switch(pData->type)
{
case PRINT_MSG:
aBuf[len] = '\0';
printf("%s",pData->data);
break;
case MEMOUT_MSG:
memout(pData->data,len-1);
break;
default:
break;
}
}
}
pthread_detach(pthread_self());
return NULL;
}