该程序是个迷你版本象棋外挂。
最近翻下箱底,想不到目前还可以运行。
演示:
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居然还可以运行。不得不佩服微软的操作系统兼容性很好。
最后,使用棋软虐人是无道德的,靠棋迷们自律了。