四国军棋引擎开发(1)随机下棋

现在开始来开发四国军棋的引擎,所谓引擎就是根据当前的局面给出最佳的下法,而界面只是一个显示的功能。目前由玩家控制自家和对家的棋,由引擎控制上家和下家的棋。

1.通信

和界面类似,socke通信放在一个单独的线程里,但是只接收数据不处理数据,接收到的数据通过消息队列发送给引擎模块处理:

    while(1)
    {
        recvbytes=recvfrom(socket_fd, buf, 200, 0,NULL ,NULL);
        mq_send(pJunqi->qid, (char*)buf, recvbytes, 0);

    }

引擎也是一个单独的线程,一直等待通信线程传来的数据。

    while (1)
    {
        len = mq_receive(pJunqi->qid, (char *)aBuf, REC_LEN, NULL);
        if ( len > 0)
        {
            ProRecMsg(pJunqi, aBuf);
        }
    }

当界面先于引擎启动的时候,引擎会主动发送COMM_READY命令来请求初始化。

2.基本框架

当界面发送初始化命令过来时,引擎会初始化一些棋盘上固定的东西,如邻接表、铁路、九宫格等,这些只在引擎第一次启动时初始化,此后不再改变,而棋子和布阵每次新开局时都会重新初始化。

    static int isInitBoard = 0;
    ... ...
    case COMM_INIT:
        InitLineup(pJunqi, data, isInitBoard);
        InitChess(pJunqi, data);
        if( !isInitBoard )
        {
            isInitBoard = 1;
            InitBoard(pJunqi);
        }
        SendHeader(pJunqi, pHead->iDir, COMM_OK);
        break;

在接收到COMM_START命令后引擎开始下棋,轮到自己下时发送行棋命令给界面,界面会把判定的结果重新发回给引擎。每收到一次界面的判定结果,更换下棋的方位。引擎虽然不能显示,但是自身维护着各棋子的状态,每收到一次行棋结果或事件的命令都会改变棋子的相应状态。

    case COMM_EVNET:
        event = *((u8*)&pHead[1]);
        ProMoveEvent(pJunqi, pHead->iDir, event);
        SendHeader(pJunqi, pHead->iDir, COMM_OK);
        break;
    case COMM_MOVE:
        assert( pHead->iDir==pJunqi->eTurn );
        data = (u8*)&pHead[1];
        ProMoveResult(pJunqi, pHead->iDir, data);
        SendHeader(pJunqi, pHead->iDir, COMM_OK);
        break;
void ProMoveResult(Junqi* pJunqi, u8 iDir, u8 *data)
{
    ... ...
    PlayResult(pJunqi, pSrc, pDst, pResult);
    ChessTurn(pJunqi);
}

3.决策模块

这个是引擎的核心,也就是根据当前的局面,找出获胜概率最大的下法。由于现在刚开始开发,先从最开始的随机决策做起,慢慢的会增加一些其他提高胜率的算法。引擎控制上家和下家,轮到引擎时发送行棋命令。

    if( !pJunqi->bStart || pJunqi->bStop )
    {
        return;
    }

    if( pJunqi->eTurn%2==1 )
    {
        sleep(1);
        if( pJunqi->aInfo[pJunqi->eTurn].bDead )
        {
            ChessTurn(pJunqi);
        }
        SendRandMove(pJunqi);
    }

轮到某一家下棋时,引擎会遍历这一家所有活着的棋子,随机选中一颗,然后再查看这颗棋子能否行棋,如果不能行棋再换下一颗棋子,无棋可走时发送跳过,此时界面已经判负了,将会发送投降的命令过来。

    rand = random_()%30;
    for(i=0;  i<30; i++)
    {
        pLineup = &pJunqi->Lineup[pJunqi->eTurn][(rand+i)%30];
        if( pLineup->bDead )
        {
            continue;
        }
        pSrc = pLineup->pChess;

        if(pLineup->type!=NONE && pLineup->type!=JUNQI && pLineup->type!=DILEI )
        {
            pDst = GetMoveDst(pJunqi, pSrc);
            if( pDst!=NULL )
            {
                SendMove(pJunqi, pSrc, pDst);
                return;
            }
        }
    }
    SendEvent(pJunqi, pJunqi->eTurn, JUMP_EVENT);

下面是和我下棋的截图,当然现在很弱智,随便乱送都能赢
这里写图片描述

4.源代码

https://github.com/pfysw/JunQi

一个多线程引发的问题

接下来说明一下解决的一个非常棘手的程序崩溃问题,曾一度调的都要放弃。在软件和界面联调时,经常出现莫名奇妙的界面显示异常,也经常出现GTK内部函数的断言错误,还时不时的出现内存引起的程序崩溃而退出。

这个问题不是必现,出现的概率是很高,但是又琢磨不透。自己手工测试很多次,加上打印信息,基本上可以确定是在通信线程执行行棋(即PlayResult)时出现的,但是没发现什么规律,也想不通为什么会这样。

为了增加单位时间内的测试次数,修改代码让引擎自己和自己下,这样几秒就能下一盘棋,测试时间变短了很多。为了确定不是自己代码逻辑的问题,在PlayResult里把界面相关的东西都屏蔽掉,此时概率减小了很多,但是还是会出现崩溃,但是有一个重大的发现,崩溃基本都是出现在最后一方棋被吃光无棋可走5次跳过的时候,此时与界面相关的只有DestroyAllChess()函数,这个函数销毁战败方的棋子。

在下面代码处加入打印信息,查看到底是哪颗棋子的问题

    for(i=0; i<30; i++)
    {
        if( pJunqi->Lineup[iDir][i].type!=NONE )
        {
            for(j=1; j<4; j++)
            {
                printf("desroy %d %d",i,j);
                gtk_widget_destroy(pJunqi->Lineup[iDir][i].pImage[j]);
            }
        }
    }

发现竟然不能复现了,此时很容易联想到是gtk_widget_destroy执行太快的问题,所以在后面加上1ms的延时函数Sleep(1),此时貌似问题解决了,但是经过10多次测试,还是有10分之一的概率会出现崩溃。为了确认的确是这个函数的问题,而不是其他问题,让引擎只发送跳过指令,测试结果是加了延时崩溃概率变小,不加延时立即崩溃,此时可以确认是这里出现的问题。

但是我们发现再新建菜单的回调函数里也有DestroyAllChess(),无论测试多少次都不会出现崩溃。

void ReSetChessBoard(Junqi *pJunqi)
{

    for(int i=0; i<4; i++)
    {
        if( !pJunqi->aInfo[i].bDead )
        {
            DestroyAllChess(pJunqi, i);
        }
        SetChess(pJunqi,i);
    }

    ......
}

由于SetChess(pJunqi,i);会重新画上棋子,为了保持相同的测试情境,把SetChess(pJunqi,i)去掉,这时点”新建”时,棋子会消失,但是略微有延迟感,也就是说gtk_widget_destroy执行后不是立即产生效果,而是在之后才产生效果,在后面加Sleep(2000)延时2s,这证实了我的猜想。但是为什么在通信线程里执行DestroyAllChess()也延时2s,棋子也还是立即消失呢?仔细想一下就知道了,gtk_widget_destroy和真正执行销毁控件的函数其实是在一个线程里,而其他线程执行gtk_widget_destroy后延时只是在自己的线程里延时,并不阻塞控件的销毁。

有了以上分析,解决的方案是怎么把界面有关的东西都放在主线程里执行,而不是在另外的线程里执行。一开始是做一个button按钮,绑定回调函数,然后通过g_signal_emit_by_name发送按钮消息,但是仍然没效果。怀疑是button的优先级太高导致的问题,改为做一个名为”通信“的空菜单绑定回调函数,同样没有效果。然后我把这个菜单显示到通信栏里用鼠标点击就没问题,仔细想一下难道g_signal_emit_by_name并不是向主线程发送消息吗?经过设断点调试发现竟然还在通信线程里,也就是说g_signal_emit_by_name仅仅相当于一个函数调用,而不是模拟鼠标发送事件。

上面方案行不通我又想到了主线程里有一个idle函数,通过g_idle_add() 来添加,通过断点调试确认添加的idle的确是在主线程里。如果idle函数返回0则只执行一次,返回1在空闲时反复执行。现在终于得到了最终的解决方案,把通信处理函数DealRecData()放在idle里,并且返回0,每次收到数据时就调用g_idle_add(),由于通信数据在2个线程里,所以要加上互斥锁对数据进行保护,这样就解决了这个程序崩溃的问题。

相关推荐
©️2020 CSDN 皮肤主题: 编程工作室 设计师:CSDN官方博客 返回首页