四国军棋引擎开发(2)简单的事件驱动模型下棋

这次在随机乱下的基础上加上了一些简单的处理,如进营、炸棋、吃子等功能,在和敌方棋子产生碰撞之后会获取敌方棋子大小的一些信息,目前采用的是事件驱动模型,当下完一步棋界面返回结果后会判断是否触发了相关事件,有事件发生则处理相关事件,没有事件发生则仍然是随机下棋。

1.事件驱动模型

首先定义一个各种事件的枚举变量,目前的事件有工兵吃子,摸暗棋,进营,明确吃子,炸棋。定义如下:

enum MoveEvent{
    GONGB_EVENT,
    DARK_EVENT,
    CAMP_EVENT,
    EAT_EVENT,
    BOMB_EVENT
};

事件的优先级由小到大,要改变优先级只需要调整枚举变量的顺序,其他地方不用变。

定义了事件的枚举变量后,当然还要把事件与相应的处理函数进行绑定,并将其放在一个数组里面:

EventHandle eventArr[] = {
    { ComeInCamp, CAMP_EVENT },
    { ProBombEvent, BOMB_EVENT },
    { ProEatEvent, EAT_EVENT },
    { ProEatEvent, GONGB_EVENT },
    { ProEatEvent, DARK_EVENT }
};

在每一次轮到本方行棋时会进行事件检测:

void CheckMoveEvent(
    Engine *pEngine,
    BoardChess *pSrc,
    BoardChess *pDst,
    MoveResultData* pResult)
{
    int type = pResult->result;
    if( type==MOVE || type==EAT  )
    {
        if( pDst->pLineup->iDir%2!=ENGINE_DIR%2 )
        {
            CheckCampEvent(pEngine,pDst);
        }
    }
    //在里面检查是否是引擎方
    CheckBombEvent(pEngine);
    CheckEatEvent(pEngine);
}

如果检测到了有相应的事件发生,则处理事件,否则仍然接着调用随机下棋模块

u8 DealEvent(Engine *pEngine)
{
    u8 isMove = 0;
    int i;
    u8 eventFlag = 0;
    u8 eventId = 0;
    u8 index;

    for(i=0; i<sizeof(eventArr)/sizeof(eventArr[0]); i++)
    {
        if( TESTBIT(aEventBit, eventArr[i].eventId) )
        {
            if( eventArr[i].eventId>=eventId )
            {
                //处理优先级最高的事件
                eventId = eventArr[i].eventId;
                index = i;
            }
            eventFlag = 1;
        }
    }

    if( eventFlag )
    {
        isMove = eventArr[index].xEventFun(pEngine);
    }

    return isMove;
}

2.进营事件

进营的检测是这样的,当敌方有棋子移动后,锁定这个棋子,观察这个棋子的周围是否有本方的营,如果有则触发进营事件。如下图所示,就触发了一个进营事件:
这里写图片描述
当然此时紫方下了轮到绿方下,绿方并不能处理这个事件,然后再轮到蓝方下,蓝方下完后也可能触发进营事件,所以现在保留2个进营事件,防止之前的进营事件被覆盖:

    //遍历该棋子的所有邻居
    for(i=0; i<9; i++)
    {
        if( i==4 ) continue;
        x = pChess->point.x+1-i%3;
        y = pChess->point.y+i/3-1;
        if(x<0||x>16||y<0||y>16) continue;

        if( pJunqi->aBoard[x][y].pAdjList )
        {

            pNbr = pJunqi->aBoard[x][y].pAdjList->pChess;

            if( pNbr->isCamp && pNbr->type==NONE )
            {
                    //找到营将其放在pEngine->pCamp[0]里
                    if( TESTBIT(aEventBit, CAMP_EVENT) )
                    {
                        //如果之前有进营事件未处理,先备份起来
                        pEngine->pCamp[1] = pEngine->pCamp[0];
                    }
                    pEngine->pCamp[0] = pNbr;
                    SETBIT(aEventBit, CAMP_EVENT);
            }
        }
    }

在处理的时候先进第一个营,如果第一个营不能进,再看看第2个营能不能进,如果这2个进营的事件都已经完成则清除进营事件标志位

u8 ComeInCamp(Engine *pEngine)
{
    u8 isMove;
    //先进第一个营
    isMove = MoveInCamp(pEngine, pEngine->pCamp[0]);
    if( isMove )
    {
        pEngine->pCamp[0] = pEngine->pCamp[1];
        //没有进营事件时,清除该标志位
        if( pEngine->pCamp[1]==NULL )
        {
            CLEARBIT(aEventBit, CAMP_EVENT);
        }
        else
        {
            pEngine->pCamp[1] = NULL;
        }
    }
    //如果本方没有棋子可以进第一个营,则进第2个营
    else if( pEngine->pCamp[1] )
    {
        isMove = MoveInCamp(pEngine, pEngine->pCamp[1]);
        if( isMove )
        {
            pEngine->pCamp[1]=NULL;
        }
    }

    return isMove;
}

3.炸棋

炸棋事件的检测很简单,遍历敌方的棋子,看看自己的炸弹有没有对着大棋,如果对着大棋,则触发炸棋事件:

    for(i=0; i<30; i++)
    {
        pLineup = &pJunqi->Lineup[pJunqi->eTurn][i];
        if( pLineup->type==ZHADAN && !pLineup->bDead )
        {
            //检查2家敌方的棋有没有棋炸
            isMove = CanBombChess(pEngine, pLineup->pChess, (ENGINE_DIR+1)%4);
            if( isMove )
            {
                break;
            }
            else
            {
                isMove=CanBombChess(pEngine, pLineup->pChess, (ENGINE_DIR+3)%4);
                if( isMove ) break;
            }
        }
    }

如果有棋炸则记下来,要炸的棋子必须大于旅长

        //由于定义的关系,枚举变量从司令到工兵依次变大,所以这里不要搞混淆了
        if( pLineup->type>=SILING && pLineup->type<=LVZH )
        {
            if( IsEnableMove(pJunqi, pBomb, pLineup->pChess) )
            {
                //本方炸弹
                pEngine->pBomb[0] = pBomb;
                //要炸的棋
                pEngine->pBomb[1] = pLineup->pChess;
                SETBIT(aEventBit, BOMB_EVENT);
                isBomb = 1;

                break;
            }
        }

在处理事件时很简单,直接发送从pBomb[0]到pBomb[1]的移动即可

SendMove(pJunqi, pEngine->pBomb[0], pEngine->pBomb[1]);

3.吃子

吃子与炸棋类似,也就是看自己从工兵到司令的棋有没有对着地方的棋,如果有则触发吃子事件,这里有3种情况,分别是工兵挖雷,摸暗棋,吃明确小于自己的棋,其中前2种优先级较低,第3种优先级最高

        if( IsEnableMove(pJunqi, pSrc, pLineup->pChess) )
        {
            if( pSrc->pLineup->type!=GONGB )
            {
                //吃暗棋
                if( pLineup->type==DARK )
                {
                    pEngine->pEat[0] = pSrc;
                    pEngine->pEat[1] = pLineup->pChess;
                    SETBIT(aEventBit, DARK_EVENT);
                    isEat = 2;
                }
                //吃小于自己的棋
                else if( pSrc->pLineup->type<=pLineup->mx_type )
                {
                    pEngine->pEat[0] = pSrc;
                    pEngine->pEat[1] = pLineup->pChess;
                    SETBIT(aEventBit, EAT_EVENT);
                    isEat = 1;
                    break;
                }
            }
            //工兵只能吃底线的2排棋
            else if( pLineup->index>=20 && !pLineup->isNotLand)
            {
                pEngine->pEat[0] = pSrc;
                pEngine->pEat[1] = pLineup->pChess;
                SETBIT(aEventBit, GONGB_EVENT);
                isEat = 3;
            }

        }

4.判断大小

在每一次碰撞后都会记录下敌方棋子的大小,pLineup->type为棋子最小的可能性,初始化为dark,如果吃掉本方棋子,则至少比吃掉的棋高一级,pLineup->mx_type为对方最大棋子的可能性,初始化为司令,如果对方司令死掉或已走明,则最大为军长,如果军长也死掉或走明,则最大为师长,依次类推。

      //... ...
            //如果吃掉本方的棋不是地雷或军棋,则比吃掉棋的级别大一级
            if( pDst->type!=DILEI && pDst->type!=JUNQI )
            {
                pSrc->pLineup->type = pDst->type-1;
            }

关于司令的判断有2种,一种是收到亮军旗的判决,说明司令死了,另一种是吃掉军长。关于炸弹的判断,如果一方亮军棋了,另一方没亮说明是炸弹,如果地雷打兑也说明是炸弹。暗棋打兑后会将pLineup->bBomb标志位置1,将来再判断最大棋子的时候会用到。

关于地雷的判断,后2排的棋子移动则不是地雷,被工兵飞过也不是地雷,此时pLineup->isNotLand标志位置1,此后工兵不会再去飞这个子。如果撞死的类型超过了最大的类型,说明撞到地雷了

    if( type==KILLED )
    {
        pSrc->pLineup->bDead = 1;
        //自己的棋撞死
        if( pSrc->pLineup->iDir%2==ENGINE_DIR%2 )
        {
            if( pSrc->type==GONGB )
            {
                pDst->pLineup->isNotLand = 1;
            }
            pDst->pLineup->type = pSrc->type-1;
            //撞死的子超过最大可能类型
            if( pDst->pLineup->type<pDst->pLineup->mx_type )
            {
                pDst->pLineup->type = DILEI;
            }
        }

关于工兵的判断,首先暗棋移动非法,会沿着铁路转弯,普通棋不会转弯,可以判断为工兵

    if( pSrc->pLineup->type==DARK )
    {
        if( !IsEnableMove(pJunqi, pSrc, pDst) )
        {
            pSrc->pLineup->type = GONGB;
        }
    }

此外,地雷被挖说明是工兵,排长吃的子也是工兵。

每一次行棋判决后,都会重新预测每个棋子的最大可能性,由AdjustMaxType()函数完成。在这里先统计每种已经碰撞过的棋子种类的数量和暗棋打兑的数量

    u8 aTypeNum[14] = {0};
    u8 aBombNum[14] = {0};
    for(i=0; i<30; i++)
    {
        pLineup = &pJunqi->Lineup[iDir][i];
        if( pLineup->type==NONE || pLineup->type==DARK )
        {
            continue;
        }
        //pLineup->typ为碰撞后最小的棋子可能性
        //如吃掉我方团长,则至少是旅长,此时pLineup->type就是旅长
        aTypeNum[pLineup->type]++;
        //如果是打兑的可能是炸弹,也可能是真的打兑
        if( pLineup->bBomb )
        {
            aBombNum[pLineup->type]++;
        }
    }

接下来看最大类型的棋明棋是否到达最大数量如果达到最大数量则重新预测最大棋子,比如我预测某个棋子是军长,但是现在已明的棋中至少大于军长的棋有2个,那么说明这个棋的最大类型只能是师长,依次类推

    for(i=0; i<30; i++)
    {
        pLineup = &pJunqi->Lineup[iDir][i];
        if( pLineup->type==NONE || pLineup->type==DARK )
        {
            continue;
        }
        //pLineup->mx_type为最大类型,pLineup->type为最小类型
        while( pLineup->mx_type<pLineup->type )
        {
            //获取级别不小于pLineup->mx_type的所以类型,如果没到最大数量,则不用调整
            if( GetTypeNum(aBombNum,aTypeNum,pLineup->mx_type)<aMaxTypeNum[pLineup->mx_type] )
            {
                break;
            }
            //否则级别降1,继续调整
            else
            {
                pLineup->mx_type++;
            }
        }
    }

统计该级别所有明棋的数量时要注意考虑炸弹的情况,首先累计所有大于该级别的数量之和,再减去暗棋打兑的部分。但是如果有3个暗棋打兑,由于最多只有2个炸弹,那么不应该减3而应该减1,如果一个炸弹明了,则打兑中最多只有一个炸弹,所以应该减1,相关代码如下

int GetTypeNum(u8 *aBombNum, u8 *aTpyeNum, int type)
{
    int i;
    int sum = 0;
    int sum1 = 0;
    for(i=SILING; i<=type; i++)
    {
        sum += aTpyeNum[i];
        //sum1为暗棋打兑部分
        sum1 += aBombNum[i];
    }
    if( aTpyeNum[GONGB]>3 )
    {
        //工兵的数量大于3,说明有一个炸被兵飞掉了
        aTpyeNum[ZHADAN] += aTpyeNum[GONGB]-3;
    }
    assert( aTpyeNum[ZHADAN]<=2 );
    //减去暗棋打兑数量和暗炸数量的极小值
    sum -= (sum1<(2-aTpyeNum[ZHADAN]))?sum1:(2-aTpyeNum[ZHADAN]);
    return sum;
}

5.总结

目前这种方法存在很大的缺陷,棋盘上的各种情况太多,无法一一用事件处理来进行判断,在某些情况下事件的优先级是不一样的。而且同一时间吃子的情况有很多,该吃哪个不好界定,躲避对方大棋、防守、抓棋、摸棋等行棋走位都不好处理。所以这种事件驱动的方法实现起来非常繁琐,而且就算增加的事件再多效果也不一定好,所以暂时不打算在这种方法深入下去。接下来打算直接上α-β剪枝算法,先把棋力快速提高到一个正常人的水平再说。

源代码如下
https://github.com/pfysw/JunQi

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值