象棋外挂(棋软)的设计开发,一种不寻常的棋盘棋子识别思路

体娱   2024-11-22 08:52   广东  
这个是2012年左右完成的一个个人作品,当时给其命名:象棋军师。这也是俺最喜欢的作品之一了。(下图红框程序便是)

该程序是个迷你版本象棋外挂。

最近翻下箱底,想不到目前还可以运行。

演示:

2013年在新浪博客分享过,可惜新浪博客“系统维护中”,所以,整理下,挪到这来。

大致功能就是,棋迷在下棋时候,到某个局面,太难不想思考了,点击程序“出招”,程序便思考棋局,找出最优招法,之后模拟鼠标点击事件去完成一步下棋。简单傻瓜化交互。

1、开发该软件的初因

当时最热门的象棋对战平台是QQ游戏了。

QQ象棋可以设置局时1分钟,读秒0 这种包干制的超超级棋。

会碰到这种棋友,棋下的不好,但下棋贼快,老弄对方超时胜。其实就是拼手快,还有就是网速。

所以,便产生了使用程序反击对方的想法。

程序员的特点就是这样,如果没有一个顺手的现成的工具,就自己打造一个。于是,便有了“象棋军师”程序。

完成该程序后,和别人比拼1分钟包干块棋时候,自己就是不断的点击“出招”便可(自己操作棋盘去下也行),对方如果不是也用软件,拼时间肯定必然输无疑的了,人怎么能跑得过汽车?


2、象棋外挂软件的基本功能

一个基本功能的象棋外挂软件的功能,大概需要包含这几点:

(1) 棋盘识别,象棋棋盘信息的读取;

(2) 智能引擎,AI 引擎计算最优招法;

(3) 模拟人行为,发起下棋动作;

本文重点是说(1),下面先简单说说(2)和(3),最后再说(1)。


3、博弈智能算法使用

也就是(2),调用第三方引擎,调用协议为公开的协议:UCCI(Universal Chinese Chess Protocol),协议可可参考:

https://www.xqbase.com/protocol/cchess_ucci.htm

跟踪第三方棋软主程序,发现其调用引擎方法,是使用“标准输入”和“标准输出”(即C语言中的stdin和stdout)来通讯,所以,我们的程序也是这样调用。

创建进程的时候,指定PROCESS_INFORMATION 的信息,创建两个匿名单向管道作为读写输入输出信息到PROCESS_INFORMATION中,如下:

CreatePipe(&m_hRead1, &m_hWrite1, &g_sa, 0);CreatePipe(&m_hRead2, &m_hWrite2, &g_sa, 0);// 省略了错误处理
STARTUPINFOA si = { 0 };PROCESS_INFORMATION pi = { 0 };
si.cb = sizeof(si);si.dwFlags = STARTF_USESHOWWINDOW | STARTF_USESTDHANDLES;si.hStdInput = m_hRead2;si.hStdOutput = m_hWrite1;si.hStdError = m_hWrite1;
char szPath[512] = { 0 };::GetModuleFileNameA(NULL, szPath, _countof(szPath));char *pName = strrchr(szPath, '\\');if (pName != NULL) { pName[0] = '\0';}
char szDir[512] = { 0 };strcpy(szDir, szPath);strcat_s(szPath, _countof(szPath), "\\Engine\\cyclone.exe");
::CreateProcessA(NULL, szPath, NULL, NULL, 1, 0x10, NULL, szDir, &si, &pi)


后面就可以按照UCCI协议,把象棋棋局信息写到m_hWrite2中,并且从m_hRead1中读取引擎返回的结果了。

返回的结果也是安装ucci协议的。引擎最优招法输出格式为类似:bestmove h2e2 ponder h9g7,而h2e2就是引擎返回的最优的招法。


4、模拟鼠标点击下棋

这部分较为简单,在windows 编程中,就是发起几个event 便可。

实现鼠标点击,用到的是mouse_event函数。

比如要实现一个鼠标点击(x y)的位置,那么

::SetCursorPos(x,y);
mouse_event(MOUSEEVENTF_LEFTDOWN, 0, 0, 0, 0);
mouse_event(MOUSEEVENTF_LEFTUP, 0, 0, 0, 0);

把这几个调用打包为click(x,y)函数,那么,一个下棋动作,从原来棋子从(x1,y1),移动到(x2,y2),也就是一个click(x1,y1) 后再click(x2,y2)了。

第三部分,智能引擎返回招法信息,我们把它转化为棋盘的位置,再结合棋盘位置,转化为对应的窗口坐标,之后上面两个click,我们便完成了一个下棋的动作了。

如何把引擎返回的招法,转化为窗口(棋盘)的棋子位置?这就是要结合(1)的内容了。


5、棋盘识别

棋盘识别方法,也就是(1),方法可以说比较多。比如可以通过机器学习,训练数据。

又比如可以,openCV,识别圆形,识别线条,图片相似度等方法,组合来做棋盘识别,也就是之前一篇《python 实现中国象棋棋盘识别(一)识别棋子位置》。

但是象棋军师,使用了另一种方法,一种简单粗略但有效的方法。

5.1 连连看外挂思路

当时,写QQ 游戏连连看外挂来的灵感。

先说说连连看的外挂如何识图的了。

如上图(也是俺写的一个作品)。程序读取了连连看数据,打印到外挂界面上(其实是方便调试)。

是否留意到,连连看每个格子都有6个点?

连连看数据识别,就是在小格子分散地取几个点(上图是6个),获取其颜色RGB值,计算总和。

不同小格的六个点的RGB值之和,大概率是不一样的(调试阶段调整点位置,使得不一样),它们便可分别代表连连看的小格子的物品了。这样,便可把连连看的节目,到数据的转换了。

也就是说,计算小格子的散点的RGB值和,作为物品的代表。

有了数据,再做个写个算法识别连连看的小格子哪个可以连击消除,一个连连看外挂便出来了。


5.2 象棋棋子上,散点取值刻录棋盘

但是做完连连看外挂,细细思考,貌似这个思路也可以用在象棋上啊!

对于一个象棋游戏界面,大部分情况下,窗口名、窗口大小、以及棋盘在窗口的偏移和棋子大小,都是固定的。

针对这类型游戏,棋盘棋子识别相对容易点,这个初次识别棋盘,我们称为刻录棋盘(其实就是首次识别各兵种)。

// 获取棋盘小格子的散点颜色值之和。取13个点,点距为 4/*       ·        · · ·     · · · · ·        · · ·         ·*/
DWORD CChessHelperDlg::GetDcDate(int xCell, int yCell, HDC hdc){ int x = int((float)g_cInfo.nOffsetX + (float)xCell * (float)m_nCellW); int y = (int)((float)g_cInfo.nOffsetY + (float)yCell * (float)m_nCellH); // m_nCellW、m_nCellH 分别为棋盘棋子宽和高 // g_cInfo.nOffsetX 和 g_cInfo.nOffsetY 为提前算好的棋盘起始位置
DWORD dwColor = 0; POINT pt[13] = { 0 }; pt[6].x = x; pt[6].y = y; // 其他点的初始化 略
DWORD color = 0; for (int i = 0; i < _countof(pt); i++) { color = ::GetPixel(hdc, pt[i].x, pt[i].y); dwColor += (color & 0xffffff); }
return dwColor;}

比如象棋开局时候,红方两个车在两边,读取小格子13点RGB颜色值,算出它们之和,记做K1;红马在两边内一格,读取记作K2;红相,两侧内2格,读取记作K3 ……

如果一直记录到黑方的棋盘,那么应该就是K1、K2、K3 …… K14,一共14 个数值了,区分红黑,象棋一共14个兵种嘛。

也就是说,初始化时候,棋盘9x10 小格子散点的RGB值和,作为棋子兵种的代表。

在刻录过程中,是需要必要的校验的,比如:

两边的车(的K值是否相等?

各个Kn 是否重复?

在没有棋子的格子的取值,会不会出现在K1~K14之中?

……(加上一些能想到的,简单的校验就可)

如果出现异常,说明刻录失败了,也就是首次记录棋盘信息失败了。需要调整各个散点位置,直至使得一个合法的棋盘出现。

后面在对局过程中,在实时读取棋盘上的各个格子RGB的Kn值,再结合开始刻录到的K1~K14 的信息,转化为一个合法的棋盘信息的对应兵种即可。


5.3 未知平台,未知棋盘位置和偏移的识别

对于已知的游戏,棋盘位置相对固定的棋盘,识别过程(刻录)相对还是简单的。

但还有很多象棋游戏,棋盘可能是未知的,窗口大小可能不固定的,而且,有些窗口可以拖动大小,棋盘和格子大小就不固定了。

仅靠5.1 的方案是不够的,如何做一个通用点的识别过程?

也就是在未知的窗口,未知的棋盘大小,未知的棋子大小等的情况下,如何识别出棋盘?

如果屏幕中(或者图片中),放置一个棋盘初始化好的局面,那么,扩展下5.1的方法,也是可以将棋盘信息读取出来的。

思路便是,枚举屏幕的每个小局面,也就是下面的红色框框的,不断调整其大小和挪动其位置,一直找到一个区域,满足(5.1)读取颜色数据(K1~K14),满足一个合法棋盘数据的区域即可。

如上图,红框明显是不满足5.1要求的,一直到蓝色区域才会满足。

在枚举小区域的过程中,如果一个像素一个像素的移动,一个像素一个像素地扩大区域,这个识别过程是非常慢的,但是,有很多手段可以优化,这篇就先不说了,下篇单独做一篇专门说说这个优化过程吧。

反正这个过程,就是在屏幕中,找到一个区域,使得它为9:10比率,并且小格子散点 RGB 和满足一个象棋开局的“模样”,就算完成了。


5.4 对局过程中的识别

有了前面的界面棋谱刻录过程(识别过程),也就是:棋盘在窗口的位置,棋子大小,兵种棋子的颜色Key,这些信息都有了。

在对局过程中,点击“出招”时候,在读取窗口信息,捕获图片,之后读取相应的9x10 格子颜色Key 值,再根据前面记录好的K1~K14,便可转化为一个实时对局的数据了。

有了对局数据,之后按照3,调用智能引擎,拿到最优招法,再按照4,模拟点击时间,完全下棋便可。


该作品2012年使用vc++所写压箱底很久了,最近拿运行试试,在win11居然还可以运行。不得不佩服微软的操作系统兼容性很好。

最后,使用棋软虐人是无道德的,靠棋迷们自律了。

象棋之术
热爱生活,有空下下象棋,或者读读书,或者好好户外旅游一下,别被什么束博。
 最新文章