思路:
1、如何让棋子自己走动?
这是很多人,包括我首先想到的一个问题。解决这个问题的办法比较多,我用的是非常简单的一个办法,投递鼠标消息即可。(对于其它的拖动式的象棋未曾试验)发送棋盘对应的窗口鼠标按下按起的消息,WM_LBUTTONDOWN / WM_LBUTTONUP。传送棋子相对于窗口的坐标点信息。
2、如何定位棋子坐标点信息?
嘿嘿,这也简单,自己画线不就行了?pDC->Line(int x, int y); 得到窗口的句柄与设备环境了以后想干啥不行?设定不同于棋盘颜色的线条,慢慢逼近棋盘线条,让测试线与棋盘线完全重合,然后测试棋子。
3、如何测试棋子?
这是最关键的部分,也是外挂运行的核心技术咯,简单说来就是屏幕取色!(从分析QQ*棋王中得到的,希望源作者看到后不要见怪,毕竟是技术共享嘛)我觉得QQ*棋王这一点做得十分好:
取每个棋子的十一个点(当然,你自己也可以定义),上路三点,中路五点,下路三点,正好符合棋子是圆的特性。一般说来,两个相同的子的十一点肯定相等,也就是说红车1=红车2,红马1=红马2等等。
4、如何调用引擎?
这也是外挂的核心部分,有了它以后外挂才能有自己的“思想”,判断该怎么走子,应该走哪个子!建议大家有兴趣的话可以去参考一下UCI(Universal Chess Interface)国际象棋通用引擎协议以及Fen表示棋局的方法。我们用的是中国象棋,为什么要学UCI呢,因为中国象棋引擎协议UCCI就是从UCI改进而来的,属于UCI的子集。
呵呵,做一个外挂不是那么容易的!象棋旋风的引擎是采用的UCI协议通信,车马相士将等都用字母进行表示,一个棋盘用一个Fen字串表示:
rnbakabnr/9/1c5c1/p1p1p1p1p/9/9/P1P1P1P1P/1C5C1/9/RNBAKABNR
这就代表了一个棋盘,里面是所有棋子未走动时的情况。(大写代表红方,小写代表黑方)
下面的命令是从象棋旋风中而来(只针对象棋旋风):
uci //初始引擎 显示引擎的版本作者等内容
ucinewgame //重新开启一个新的局面 引擎不会返回任何内容
isready //查看引擎是否已经准备就绪 引擎返回readyok 表明准备好了
position <fen> //设置引擎当前的局面信息 引擎不会返回任何内容
go depth <xx> //思考的深度,深度越高,棋力越强,但越耗时。
//引擎会不断地返回思考的步骤info ….. curmove …. bestmove ….
stop //停止思考,立即给出当前最好的招法!
quit //退出引擎
引擎以bestmove 的方式给出最优招法,我的方法是建立匿名双管道,用一线程监视引擎信息,然后再用另一线程控制棋盘扫描等信息。
5、如何进行棋盘扫描?
用两个字符数组,保存棋盘所有交叉点的棋子情况,扫描每个交叉点的11个像素点的颜色值,从而存储布局情况,无子则为NULL。第一次走子时,将两个数据都初始化相同棋面数据。无论是否轮到自己走子,都尝试走子!(如果不该自己走子,也不会走子成功)然后就是棋盘扫描线程的工作了:
不断地扫描棋盘,比较当前的棋面数组是否与前一棋面相同,不同的话也要进行分析:
a不同的地方只能有两个点,即走子后为空的点和别一个被棋子填充的点。
b如果有三个以上不同的点存在的话,就不作任何处理,继续扫描(当棋盘被其它窗口挡住的时候,就会出现三个或以上的点不相等的情况)
其中出现a的情况时还得进一步判断,是不是自己走子,如果是自己走子的话,继续扫描,否则指挥走子!
6、如何判断是不是自己走子?
嘿嘿,这个方法也是比较经典的。扫描对家的棋盘线上三路的九宫格,也就是将帅所在的王宫位置,得到对家的子是红帅还是黑将,从而可以确定自己是红方还是黑方。扫描的棋盘对比时,如果发现是与已方相同的子,则忽略,继续扫描,否则就指挥走子!
7、如何让棋子自动将对方将死?
象棋旋风的引擎虽然十分强大,水平已达到专业级水平,但是有一个毛病:当走子到最后,将对方完全“将死”的情况下,引擎不会指挥将对方的将或帅吃掉,这就需要自己想办法了!自己写函数判断了:(在每将让引擎思考的时候,先得判断当前是否是能将对方将死的情况,是的话直接指挥将死对方,否则继续让引擎思考)
1、对于车比较横直线与竖直线在对方的将帅之间是否有棋子阻挡,有则吃不了,否则直接指挥吃掉对方的将帅!
2、对于炮比较横直线与竖直线在对方的将帅之间是否只有一个棋子阻挡,有则能将对方将帅吃掉,否则就是不能吃子情况。
3、对于马比较八个R型方向是否能到达对方将帅的位置,且马的上下左右四面是否有子阻挡,有则不能吃子,无则可以直接将对方将帅吃掉。
4、对于兵或卒比较前方和左右三方是否对到达对方将帅的位置(后方不作判断,兵不能后退,义无反顾的小兵啊!),是的话直接将对方吃掉!
8、如何才能通用?
采用与QQ棋王相同的原理,用XML方式表示棋子,但是加以了改进:用窗口列表的方式来显示每个结点以及相关的属性(下图是其中的一部分):
其中P表示主窗口,C表示子窗口,如果有相同类名及标题名的兄弟窗口,就用B表示。””表示标题或类名为空。这样就能比较方便地只通过修改XML外部文件从而达到不修改源软件程序而能通用的目的。
数据参考结构,其实这才是实现重点
#define CHESS_X_MAX 9 //象棋棋盘X方向最大为9列
#define CHESS_Y_MAX 10 //象棋棋盘Y方向最大为10行
#define CHESS_N_POS 90 //象棋棋盘有90个坐标点
#define CHESS_N_PIECE 32 //象棋总共有32个棋子
#define CHESS_PERPOINT_COLOR 11 //每一个点取11点像素值以确定棋子
struct _ChessPiece{ //保存所有棋子的颜色信息
TCHAR chesscode; //棋子的fen码
COLORREF color[CHESS_PERPOINT_COLOR]; //棋子的颜色数组
}; //总共的棋子数组
struct _ChessBoard{
TCHAR PrevChessBoard[CHESS_N_POS]; //前一局面
TCHAR CurChessBoard[CHESS_N_POS]; //当前局面
};
struct _ChessStep{
TCHAR ChessCode; //走子的棋子信息,大写为红子,小写为黑子,0为无效
TCHAR cMyCode; //自己擎红还是擎黑, w为红,b为黑, 0为无效
int StartStep; //棋子的起始步,对应数组索引,下同
int EndStep;
};
struct _ChessMate{ //储存将军状态下将死对方的起始点与终点,对应数组索引。
int nStartPos;
int nEndPos;
};
struct _ChessPosition{ //对棋盘与棋子定位
HWND hCtrlWnd; //棋盘控件对应句柄
int nLeftTopX; //棋盘的左上角x坐标
int nLeftTopY; //棋盘的左上角y坐标
int nXYStep; //棋盘XY方向的步长
};
struct _ChessWndFindInfo{ //保存查找游戏窗口控件句柄的相关变量,通过标题与类名获取
CString cRelation; //表示窗口关系,p表示顶层窗口,c表示上一窗口的子窗口,b表示上一窗口的兄弟窗口。
CString stitle; //存储当前窗口的标题名
CString sclass; //存储当前窗口的类名。
};
class CChessData
{
public:
CChessData();
virtual ~CChessData();
static _ChessPiece ChessPiece[CHESS_N_PIECE];
static _ChessBoard ChessBoard;
static _ChessStep ChessStep;
static _ChessMate ChessMate;
static _ChessPosition ChessPosition;
static _ChessWndFindInfo *pChessFindInfo;
static long nStep2FindWnd; //查找到棋盘句柄要经过的步数。
static CString sPieceFile; //保存棋子颜色信息的XML文件名
static HWND hChessBoard; //保存棋盘句柄
};