【读者群征文活动】

随便聊聊军棋

不知道各位开发人员,有没有四国军棋的爱好者?本人是一位四国军棋爱好者,一直玩四暗。玩军棋大概有 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 可以查看全部源代码。

欢迎提出宝贵意见。谢谢。