QQ 四國軍棋佈局編輯器

Jeffrey Y.發表於2015-09-12

【讀者群徵文活動】

隨便聊聊軍棋

不知道各位開發人員,有沒有四國軍棋的愛好者?本人是一位四國軍棋愛好者,一直玩四暗。玩軍棋大概有 6~7 年的時間了,兒時的東西到了上班期間才慢慢的找回。

從玩遊戲之初到現在,各種軍棋的遊戲平臺都摸過一遍,感覺下來,無論是從遊戲的介面上、操作的動態效果上,還是方便性,我覺得騰訊的四國軍棋平臺是做的最好的(簡潔、清爽)。其它的平臺不是過於花哨,就是遲鈍的響應和互動。

現在很多原來中游、聯眾平臺裡面的高手也有部分轉到了騰訊遊戲平臺,比如:張碩閃電、聯眾老菜鳥、老頭臉上飛紅等等。騰訊平臺也有很多大師級人物,比如:欠你幸福、蚊子、戲子無義(2014年個人軍棋賽冠軍)等。

萌生開發此工具想法的原因

經常在網上下軍棋的人都知道,我們都需要很多佈局以備使用。當征戰的時候,在遊戲介面上直接 import 一個指定的佈局檔案可以快速的調整作戰佈局,而不用臨時擺棋。就拿騰訊的四國軍棋平臺來說,臨時擺棋:一來,浪費一桌四個玩家的時間,而且如果和你坐一桌的人有藍鑽,很有可能當你調整到一半的時候就被踢了,前面的調整前功盡棄;二來,如果你調整的慢,還沒等你調整完,都有可能因長時間未“完成排程”而導致系統超時,把你踢出棋桌。我身邊碰到不少的棋手就是這樣的。

目前為止,騰訊都沒有一個工具來方便而又直接的檢視、編輯自己的佈局檔案。我不知道其它有沒有第三方的工具去做這件事情。出於興趣愛好,以及為了方便大家佈局的編輯調整,自己在網上查詢了一點資料後就開始著手開發這個小工具了,騰訊的四國軍棋佈局檔案的字尾名是 JQL(PS:可能是 Jun Qi Layout 的縮寫)。

投入使用和更新

“QQ 四國軍棋佈局編輯器(Jql Editor)”我大概早在一年半以前就寫成了第一個版本,後來在公司的四國軍棋俱樂部中,以及比賽中,釋出給大家使用,反響不錯,大家都挺喜歡的。

之後,又對其進行了功能上的強化和豐富,包括Drag & Drop、Undo-Redo、Copy As Image、Y-Flip 等方面的功能。然後在一些 QQ 軍棋群裡面分享給一些棋手,方便他們使用。

在最新的版本里(3.0.0),又對其動了一次“手術”。這次改動把原先使用的 Dev Express 元件去除了,原先使用該元件的目的是為了美化介面和豐富樣式,後來經過思慮,鑑於 Windows 7+ 的介面都做的不錯,所以決定移除這個UI上的錦上添花。其次,將介面底部的具有一定厚度的按鈕組整個去掉,換成 Context Menu,使得 UI 看上去更加的簡潔。

QQ 四國軍棋佈局編輯器一覽

下面先讓大家看一下這款工具的總體介面(一共有四種顏色):

橙色佈局介面 紫色佈局介面 綠色佈局介面 藍色佈局介面

每次開啟這個小工具的時候,預設是以這樣大體上對稱的佈局將25個棋子攤在臺面上的。

在這張圖中,大家所看到的棋子均採自QQ四國軍棋,從中進行擷取、修改和拼接。墨綠色的棋盤也同樣來源於QQ四國軍棋,通過PS對QQ四國軍棋的棋盤進行技術處理,通過截邊、著色、漸進等處理獲取這個迷你佈局盤面。如下圖所示。話說,由於本人PS技術能力有限,在這圖片的製作上,花了不少功夫,其中還包括後面會看到的獲得焦點棋子(Focused Chessman)及選中棋子(Selected Chessman)的高亮發光效果。

佈局棋盤

所有操作均放在了右鍵上下文選單上,當右鍵選單開啟就會一目瞭然。下面是截圖。

右鍵選單功能

這個佈局編輯器程式的幾個關鍵點在於:

  • GDI+ 圖形影像的繪製和處理

  • 棋子位置的判斷和計算

  • 二進位制JQL檔案的解析

  • 滑鼠及鍵盤事件的響應和處理

  • 合理隨機佈陣的生成演算法

核心類的設計

在 QQ 四國軍棋佈局編輯器中,有四個核心的類,如下圖:

核心類類設計圖

(1) JqlChessman:全盤 25 個棋子物件,盤面上的“主角”,被它們的大管家 JqlChessmanCollection 管著。

(2) JqlChessmanCollection:棋盤上所有棋子的容器。負責整體處理棋子位置的關係、特殊位置的判斷、棋子之間的交換等操作。

(3) JqlChessmanSwap:兩棋子之間的交換和整個盤面的翻轉。

(4) JqlInfo:讀取並解析 JQL 檔案的類,鎮守程式的入口(資料的讀取、校驗和解析),如果沒有它,上面三位都只是打醬油的。

列舉的設計

列舉設計圖

一個工具無論大小,都會有列舉型別的存在,這款工具也不例外。

(1) ChessmanSwapType:棋子間的交換或者整個盤面的左右翻轉。

(2) SpecialPosition:指示軍棋佈局中的特殊位置,玩軍棋的人都知道,軍棋有幾個限定擺放位置的棋子:地雷、炸彈、軍旗。其中“軍旗”只能放在最後一排的兩個大本營裡面(Base),“地雷”只能放在最後兩排(LastTwoRows),“炸彈”不能放在第一排(FirstRow)。另外還有五個特殊的位置,那就是“行營”(Bunk),在 QQ 軍棋裡面,就是橙色圈圈代表的位置,行營在佈局中是用不到的,而且滑鼠點選這些位置也應該直接忽略,初始佈陣時,行營是不能放子的。那除了這些以外,剩餘的其他位置就是非特殊(NonSpecial)位置。下面再貼一下棋盤,讓大家看起來直觀一點:

佈局棋盤

(3) ChessmanType:顧名思義,棋子型別,為了給每一個軍銜取英文名,我在網上搜尋了很多,各有說法,最終我得出以下對應的英文軍銜。 (注:Cmdr = Commander)

棋子英文

棋子型別直接決定了介面的顯示和佈局的內容。

(4) ChessmanColor:子顏色這個類在當前這個小工具中是一個錦上添花的東西,因為對於這個佈局而言,我完全可以只用一種顏色,但是考慮到每個人喜好的顏色不同,就擴充套件了更換棋子顏色的這麼個功能。但是因為考慮到這個工具目前只針對騰訊的四國軍棋,所以介面的棋子和顏色均取自 QQ 的四國軍棋遊戲。 有人可能會問,為什麼不讓使用者自己選擇顏色呢?原因是如果這麼做,我就無法使用 QQ 的軍棋圖片了,需要使用 Label 去做一個 Chessman Control,這樣才能自定義 background color,衡量了一下,覺得沒有必要,雖然 25 個 Label 放在臺面上可以省掉麻煩的 Mouse Move 事件,但是這樣一來,就有幾個缺點:

  • 無法對焦點棋子進行高亮光環特效顯示。
  • 棋子的美觀上不如圖片來的好。

JQL 佈局檔案分析

JQL 檔案是一個二進位制檔案,用 UltraEdit 或者 Notepad++(HexEditor 外掛)開啟,可以看到二進位制文字,該文字一共50個位元組:

  1. 前20個位元組是一串固定的文字,類似於“QQ 四國軍棋遊戲”這樣的字串的二進位制byte陣列。

  2. 後 30 個位元組,每一個位元組代表一個棋子面值(包括25個棋子 + 5個行營),棋子的字面值就是軍銜等級,對應如下:(行營是00)。PS:在騰訊該款遊戲的設計裡,並未用到 01。

棋子級別二進位制程式碼

前面提到,處理檔案主要由 JqlFile 來負責,這裡麵包含三個主要的屬性:ChessmanCollectionHeaderBytesLayoutBytes

HeaderBytes 用來儲存二進位制 JQL 檔案開頭部分的一段說明字串;LayoutBytes 用來儲存佈局的二進位制位元組陣列,讀取這個 byte 陣列之後,會將每一個byte通過一定的規則轉化成 Chessman 物件(一個 byte 就是一個 chessman,後面會說到這個檔案的結構),進而形成ChessmanCollection 物件。下面的程式碼是讀取檔案的內容:

HeaderBytes = new byte[JqlConsts.JqlFileHeaderLength];
Array.Copy(bytes, 0, HeaderBytes, 0, JqlConsts.JqlFileHeaderLength);

LayoutBytes = new byte[JqlConsts.JqlFileLayoutLength];
Array.Copy(bytes, JqlConsts.JqlFileHeaderLength, LayoutBytes, 0, JqlConsts.JqlFileLayoutLength);

ChessmanCollection = new JqlChessmanCollection(this);

佈局檔案二進位制顯示

與棋子位置相關的判斷和計算

首先,是對於某個棋子位置的檢測,這個位置的檢測在 CheckPosition 方法裡面:

internal static SpecialPosition CheckPosition(int row, int col)
{
    if (!IsValidPosition(row, col)) return SpecialPosition.None;
    // 判斷是不是第一排
    if (row == 0) return SpecialPosition.FirstRow;
    // 判斷是不是大本營
    if (row == JqlConsts.LayoutRowCount - 1 && (col == 1 || col == 3)) 
        return SpecialPosition.Base;
    // 判斷是不是最後兩排
    if (row == JqlConsts.LayoutRowCount - 2 || row == JqlConsts.LayoutRowCount - 1) 
        return SpecialPosition.LastTwoRows;
    // 判斷是不是行營
    if (row > 0 && row < JqlConsts.LayoutColCount && 
        col > 0 && col < JqlConsts.LayoutColCount)
    {
        if (row == col || row + col == 4)
        {
            return SpecialPosition.Bunker;
        }
    }
    // 以上都不是,那就是非特殊位置
    return SpecialPosition.NonSpecial;
}

在這個方法裡面,需要說明的是:大本營的判斷必須放在後兩排的判斷前面,因為大本營本身也是屬於後兩排的。

接下來 CanSwap 方法會呼叫 CheckPosition 方法來獲取當前 Mouse Click 或者 Mouse Over 的位置型別,隨後,判斷待交換的兩個棋子能否交換,比如:第二排的炸彈和第一排的工兵交換,這就是不合理的交換。程式碼比較長,就不貼了,有興趣的可以看看原始碼。這裡放一張特殊位置圖,大家可以參考比對一下:

棋子特殊位置

和棋子相關的一些演算法方法中涉及到一些常量:

  • 佈局矩形區域相對於窗體客戶區的左上角水平、垂直偏移量

    public static readonly int OffsetLeft = 30;
    public static readonly int OffsetTop = 30;
    
  • 佈局的行、列數

    public static readonly int LayoutColCount = 5;
    public static readonly int LayoutRowCount = 6;
    
  • 棋子和棋子之間的水平、垂直間距

    public static readonly int HoriMargin = 3;
    public static readonly int VertMargin = 20;
    

棋子位置偏移量

高亮發光邊的簡單實現

在介面上,滑鼠可以直觀的選擇兩個棋子進行交換。如下圖,當選中一個棋子時,其邊緣呈現藍色發光邊;當滑鼠懸浮經過棋子,即呈現黃色發光邊。

選中棋子和焦點棋子

如何高亮顯示,主要就是如何判定滑鼠是否移動到棋子的客戶區範圍之上。

private Point GetPositionInLayout(Point mouseLocation)
{
    int horiSpan = JqlConsts.ChessmanWidth + JqlConsts.HoriMargin;
    int vertSpan = JqlConsts.ChessmanHeight + JqlConsts.VertMargin;
    int x = (mouseLocation.X - JqlConsts.OffsetLeft) / horiSpan;
    int y = (mouseLocation.Y - JqlConsts.OffsetTop) / vertSpan;
    return new Point(x, y);
}

上面方法用來將滑鼠的位置轉化成一個6*5矩陣中的位置,即哪一個棋子。接著,對該位置進行上述的特殊位置判斷,此處僅判斷是不是行營,如果不是則需要有發光效果,所以需要獲取棋子的矩形客戶區。

private Rectangle GetChessmanRectangleInBoard(Point layoutPosition)
{
    layoutPosition.X = layoutPosition.X * (JqlConsts.ChessmanWidth + JqlConsts.HoriMargin) 
                       + JqlConsts.OffsetLeft;
    layoutPosition.Y = layoutPosition.Y * (JqlConsts.ChessmanHeight + JqlConsts.VertMargin) 
                       + JqlConsts.OffsetTop;
    return new Rectangle(layoutPosition.X, layoutPosition.Y, 
                         JqlConsts.ChessmanWidth, JqlConsts.ChessmanHeight);
}

現在可以判斷當前滑鼠是不是在棋子位。

private bool IsMouseOverChessman(Point mouseLocation, out Point positionInLayout)
{
    positionInLayout = GetPositionInLayout(mouseLocation);
    SpecialPosition position = JqlChessmanCollection.CheckPosition(
                               positionInLayout.Y, positionInLayout.X);
    if (position == SpecialPosition.None || position == SpecialPosition.Bunker)
    {
        return false;
    }
    Rectangle rect = GetChessmanRectangleInBoard(positionInLayout);
    return rect.Contains(mouseLocation);
}

最後需要將發光效果應用到棋子所在的矩形上,通過下面的方法可以根據陣列中的位置確定具體的棋子矩形客戶區的位置:

private Rectangle GetFrameRectangleInBoard(Point layoutPosition)
{
    layoutPosition.X = layoutPosition.X * (JqlConsts.ChessmanWidth + JqlConsts.HoriMargin) 
                       + JqlConsts.OffsetLeft;
    layoutPosition.Y = layoutPosition.Y * (JqlConsts.ChessmanHeight + JqlConsts.VertMargin) 
                       + JqlConsts.OffsetTop;
    layoutPosition.Offset(-10, -10);
    return new Rectangle(layoutPosition.X, layoutPosition.Y, 
                         JqlConsts.FrameWidth, JqlConsts.FrameHeight);
}

介面上所有圖形的繪製均放在 Paint 事件中處理,在該事件中,除繪製棋子以外,另外需要判斷是否有焦點棋子,如有,則需繪製高亮邊框。

隨機合理佈局的生成

隨機生成佈局,首先,需要生成軍旗的位置,因為軍旗就倆大本營可以選擇,不是左邊就是右邊。其次,需要生成地雷的位置,因為地雷的位置的限制也是僅次於軍旗的,地雷只能放在最後兩排;然後,我們需要隨機生成炸彈的位置,炸彈不能放第一排,是僅次於地雷的。之後隨機生成其他子力的佈局位。

接著當全部擺放完25個棋子之後,需要對除軍旗外的另一個大本營的子力進行微調,比如司令放到了大本營,這顯然不是一個合理的佈局,當然也不是沒有人這麼放過,但是通常來說不是很合理。所以需要將這個子力和某一個排長進行互換。

最後,得到一個較為合理的軍棋佈局,軍棋的佈局和打法變化多端,地雷下放司令、炸彈的,一線司令後面跟炸彈的,都屢見不鮮,所以除了上述的情況以外,出其不意的奇葩佈陣也會有意想不到的效果。

總結

這個工具可以在 這裡 下載到,有興趣的可以使用,該工具總的來說不是很大,主要就是用來處理軍棋佈局,方便玩家遊戲,使用 .NET Reflector 可以檢視全部原始碼。

歡迎提出寶貴意見。謝謝。

相關文章