四国军棋界面开发(4) 行棋规则和工兵路径

现在开始来实现界面开发过程中最关键的部分,也就是行棋路线的合法性判断,这里工兵路径的判定是一个主要的难点,在此基础上再加上线路只能走直线的条件就可以得到其他棋子在铁道上的行棋规则。

1.路径箭头

每一次行棋,都需要在行棋的线路上加上箭头来表示行棋路径,先获取8个方向上的箭头素材
这里写图片描述
把图中的小箭头裁剪下来放在pJunqi->paArrowPixbuf的数组里,此后在需要添加路径时通过调用GetArrowImage(pJunqi, pSrc, pDst);来获取路径箭头。pSrc是原位置, pDst是目标位置,获取横坐标与纵坐标的差值,再通过一个映射数组aMap[3][3]来得到所需要的箭头。

路径的结构是一个双向的循环链表,之所以定义双向循环链表是为了方便的确定尾部元素并进行添加和删除操作。pArrow就是路径结点上的方向箭头,isHead标记链表头部。 每新增一个路径结点都会向链表尾部插入元素。

struct GraphPath
{
    BoardChess *pChess;
    GtkWidget *pArrow;
    GraphPath *pNext;
    GraphPath *pPrev;
    u8 isHead;
};

目前定义了3条路径作用如下:

    //0:显示在屏幕上的路径,1:当前确定的最优路径,2:其他尝试的路径
    GraphPath  *pPath[3];

2.棋盘和铁路的表示

之前定义了BoardChess ChessPos[4][30];BoardChess NineGrid[9];来表示棋盘上每一个位置的相关信息。现在需要把这些位置映射到一个17*17的棋盘上,pChess->point.x记录了横坐标,pChess->point.y表示纵坐标,为了与qq四国军棋的复盘文件兼容,取下家最右边为横坐标0,对家最上面为纵坐标0。于是自家布局左上角的映射坐标为(10,11)。

这里要注意的是九宫格虽然只有3*3的格子,但却是用5*5的格子来表示,2个相邻的九宫格之间其实是隐含着一个格子的。

现在我们再来定义一个17*17的数组BoardGraph aBoard[17][17],aBoard定义了一个图的数据结构,通过上面的映射坐标,我们就可以找到在图上的对应位置。之后初始化铁路图时会对遍历铁路格子的上下左右寻找邻居,因为所有的铁路都不在边上,不用担心溢出的问题。

BoardGraph结构定义了图上的每个结点,该结构包含一个邻接表pAdjList,还有passCnt用来记录在寻找路径时该顶点经过的次数。

typedef struct BoardGraph
{
    AdjNode *pAdjList;
    int passCnt;
}BoardGraph;

在初始化时每个铁道上的位置都会对isRailway变量置1,下面是代码是对每一家的铁路做标记,当然九宫格上的点都是铁路,也要初始化。

void SetBoardRailway(Junqi *pJunqi, enum ChessDir dir, int i)
{
    if(i<25)
    {
        if( (i/5==0||i/5==4) || ((i%5==0||i%5==4)) )
        {
            pJunqi->ChessPos[dir][i].isRailway = 1;
        }
    }
}

接下来遍历棋盘上的每个位置,如果是铁路,则需要初始化相应的邻接表,这里有3种情况

  1. 如红色方框所示,相邻的铁路只可能出现在相邻一格的上下左右
  2. 如黄色所示,相邻的铁路除了出现在相邻一格的上下左右,还可能出现在相邻2格的上下左右位置,即2个相邻的九宫格
  3. 最后如绿色方框内,2相邻方阵的角上还有一个斜向的铁路,需要额外初始化。
    四国军棋界面开发(4) 行棋规则和工兵路径_第1张图片

这时候铁路就可以用一张包含铁路顶点和顶点的邻接表的图来表示,这在寻找铁路的路径时非常有用。

3.非铁路的行棋

首先地雷和军棋不能移动,营里有棋不能向营里移动,大本营里的棋子不能移动,这些都很简单只需做一个判断就可以了。

非铁路棋子可以移动的充要条件是棋子处于相邻状态,这里我们注意到营与周边的格子都是相邻的,如果不是营,则只与上下左右的格子相邻,所以判断代码如下:

    //如果是相邻的格子(包括斜相邻)
    if( ((pDst->point.x-pSrc->point.x)>=-1 && (pDst->point.x-pSrc->point.x)<=1) &&
            ((pDst->point.y-pSrc->point.y)>=-1 && (pDst->point.y-pSrc->point.y)<=1) )
    {
        //营与周围的格子都是相邻的
        if( pSrc->isCamp || pDst->isCamp )
        {
            rc = 1;
        }
        //非斜相邻
        else if( pDst->point.x==pSrc->point.x || pDst->point.y==pSrc->point.y)
        {
            rc = 1;
        }
        if(rc)
        {
            AddPathArrow(pJunqi, pSrc, pDst, 1);
        }
    }

4.工兵的路径

接下来只剩下铁路上的棋子移动,我们先来讲工兵路径的移动。实现函数为GetRailPath,传入参数为原顶点和目标顶点。

u8 GetRailPath(
        Junqi *pJunqi,
        BoardGraph *pSrc,
        BoardGraph *pDst,
        enum RailType type);

一开始先遍历原顶点的每个邻接表元素,即遍历与该顶点相邻的铁路顶点。

    for(p=pSrc->pAdjList->pNext; p!=NULL; p=p->pNext)
    {
    }

遍历的结果有以下几种:

if(邻居是目标顶点)
{
    if(原结点之前被遍历过)
    {
        该结点已经添加到pPath[2]的末尾,要删除该结点路径
    }
    把结点的新路径加入到pPath[2]的路径里
    if(目标结点未被遍历过)
    {
        更新当前路径长度
        把pPath[2]的路径复制到pPath[1]
    }
    else if(之前的路径非最短路径)
    {
        更新当前路径长度
        清除pPath[1]的路径
        把pPath[2]的路径复制到pPath[1]
    }
    此后把该结点从pPath[2]的末尾移除,去寻找其他路径,返回1
}
else if(邻居位置上已经有棋子)
{
    此路不通,继续下一次遍历
}
else if(该邻居已经被遍历到,并且之前的路径比当前短)
{
    此时再从这点找下去已经没什么意义,继续下一次遍历
}
else
{
    此时该邻居既不是障碍物,之前也没有被遍历过
    如果之前这点已经被较长的路径遍历到过,需要把这点从pPath[2]移除
    把该点的新路径添加到pPath[2]里
    此时递归调用GetRailPath()函数,沿着这条路径继续走下去,即
    GetRailPath(pJunqi,当前邻居,目标结点);
}

要注意最后遍历完毕,函数返回时,如果已经在pPath[2]里添加了当前结点,需要移除

    if(pathFlag)
    {
        RemovePathTail(pJunqi, 2);
    }

通过以上遍历和递归调用,即可找到工兵的最短路径,工兵会沿着最短的路径行棋,而不会绕远路,效果如下
四国军棋界面开发(4) 行棋规则和工兵路径_第2张图片
四国军棋界面开发(4) 行棋规则和工兵路径_第3张图片

4.铁路行棋

实现了工兵行棋的判定后,铁路行棋就非常简单了,铁路无非是3种:横向、竖直、弯道,对每一种铁路做一个判定,不符合条件继续下一次行棋。

        else if( type!=GONGB_RAIL && !IsSameRail(pJunqi, pSrc, pVertex, type))
        {
            continue;
        }

判断代码如下,弯道铁路需要在初始化时对eCurveRail做标记是哪一条弯道铁路

    switch(type)
    {
    case HORIZONTAL_RAIL:
        if( pSrcChess->point.x==pDstChess->point.x )
            rc = 1;
        break;
    case VERTICAL_RAIL:
        if( pSrcChess->point.y==pDstChess->point.y )
            rc = 1;
        break;
    case CURVE_RAIL:
        assert( pSrcChess->eCurveRail>0 );
        if( pSrcChess->eCurveRail==pDstChess->eCurveRail  )
            rc = 1;
        break;
    default:
        break;
    }

最后在铁路上的行棋代码如下,有4种行棋方式,工兵、横向铁路、竖直铁路、弯道铁路,注意工兵的判断一定要放在最前面

    if( !rc && pSrc->isRailway && pDst->isRailway )
    {
        pVertex1 = &pJunqi->aBoard[pSrc->point.x][pSrc->point.y];
        pVertex2 = &pJunqi->aBoard[pDst->point.x][pDst->point.y];

        if( pSrc->type==GONGB )
        {
            rc = GetRailPath(pJunqi, pVertex1, pVertex2, GONGB_RAIL);
        }
        else if( pDst->point.x==pSrc->point.x )
        {
            rc = GetRailPath(pJunqi, pVertex1, pVertex2, HORIZONTAL_RAIL);
        }
        else if( pDst->point.y==pSrc->point.y )
        {
            rc = GetRailPath(pJunqi, pVertex1, pVertex2, VERTICAL_RAIL);
        }
        else if( pDst->eCurveRail>0 && pDst->eCurveRail==pSrc->eCurveRail )
        {
            rc = GetRailPath(pJunqi, pVertex1, pVertex2, CURVE_RAIL);
        }
    }

这样就完成了对所有行棋情况的判断

5.源代码

https://github.com/pfysw/JunQi

你可能感兴趣的:(四国军棋,数据结构,四国军棋开发)