四国军棋界面开发(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相邻方阵的角上还有一个斜向的铁路,需要额外初始化。
    这里写图片描述

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

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.铁路行棋

实现了工兵行棋的判定后,铁路行棋就非常简单了,铁路无非是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

四国军棋中的工兵和飞行棋是可以穿越河流和山地的,因此它们的移动路线可能非常复杂。回溯法可以帮助我们找到最短的工兵和飞行棋移动路线。 具体步骤如下: 1. 设计状态空间:将所有可行的移动路径表示为一张,每个节点表示一种状态(例如,一个位置和一个方向),每个边表示一种转移(例如,从一个位置到相邻的位置)。 2. 定义状态转移函数:确定每个状态的所有可能后继状态,即从当前状态出发,可以移动到哪些新状态。 3. 设计搜索策略:选择一种搜索策略来遍历状态空间,例如深度优先搜索、广度优先搜索或最佳优先搜索。 4. 实现回溯算法:按照搜索策略遍历状态空间,并记录每个状态的路径和已经访问过的状态。当找到目标状态时,回溯到起始状态并输出最短路径。 需要注意的是,在回溯算法中,我们需要考虑以下几个问题: 1. 如何表示状态:在四国军棋中,每个棋子有一个位置和一个方向,因此我们可以用一个元组 (x, y, d) 来表示一个状态,其中 x 和 y 表示棋子的位置,d 表示棋子的方向。 2. 如何定义转移函数:在四国军棋中,工兵和飞行棋有不同的移动方式,因此我们需要分别定义它们的转移函数。例如,工兵可以向前、向左、向右移动一格,而飞行棋可以直线飞行到任意位置。 3. 如何处理障碍物:在四国军棋中,河流和山地是障碍物,工兵和飞行棋可以穿越它们。因此,在转移函数中需要特别处理这些情况,以确保不会被障碍物阻挡。 4. 如何剪枝:由于状态空间非常庞大,搜索过程可能会非常耗时。因此,我们需要设计一些剪枝策略,例如限制搜索深度或排除一些无效的状态。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值