四国军棋引擎开发(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;
}

3.源代码

https://github.com/pfysw/JunQi

猜你喜欢

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