四國軍棋界面開發(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

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章