前几天逛VCKbase的时候看到了这样一篇文章:http://www.vckbase.com/document/viewdoc/?id=1415。文章一开始就介绍了基本算法,即先获得QQ连连看窗口句柄,然而获得其DC,然后对每个方格进行颜色取样,计算出每种方格的颜色特征值,构造出方格特征矩阵,在该矩阵上实现连连看的算法,模拟鼠标点击事件。
文章还给了一段关键的代码,即构造方格特征矩阵部分的代码。不过老实说,那些代码太难看了,命名混乱得很,我看了看源代码,比那个更糟,基本没注释。不过既然知道了算法,自己写也许比读那些难看的代码来得更快一些。
经过实验,作者的算法的确不错,准确率接近100%,只有4种左右的方格无法识别。但是这个不是问题,只需要将取样点增加到6个就可以达到很高的分辨能力了。但是在模拟鼠标的过程中遇到了一些麻烦。一开始使用mouse_event模拟鼠标的单击,但是未能如愿以偿,每次只能单击一个方格,无法单击第二个。后来又试了试SendInput,问题依旧。去CSDN上逛了逛,没什么收获,现在还不知道问题出在哪里,看来还得自己慢慢研究。
试验过后,觉得可以将这个思路用到类似的游戏中,比如QQ对对碰。构造特征矩阵的方法一样,只不过游戏算法不一样而已。
附:我的Code,算法有很大的改进余地。
// For SendInput() and INPUT Struct
#include "Winable.h"
//
// 一些常量和数据的定义
// 自定义消息
#define MM_DONE WM_USER + 1000
// 空格
#define GRID_BLANK (0)
// QQ连连看的游戏区方格是11行×19列
#define ROWS 11
#define COLS 19
// 游戏区相对于客户区的偏移(可以改变)
#define YCOFFSET 181
#define XCOFFSET 14
// 连连看方格的尺寸(可以改变)
#define WIDTH 31
#define HEIGHT 35
// 每个方格的取样点个数(可以改变)
#define SAMPLES 4
// 各取样点相对于方格左上角的偏移量(可以改变)
const static POINT g_Offsets[SAMPLES] = {
{15, 17},
{11, 19},
{16, 17},
{21, 17}
};
// 判断两种不同颜色的容差(可以改变)
#define TOLERANCE 15
// 方格结构
typedef struct _GRID_{
// 方格的类别ID(hash),大于等于0,0为空格。
// 它的作用很明显,只有同类的方格才可能被消除。
int iID;
// 该方格中心坐标(客户的,为了避免因为连连看窗口位置发生变化带来的问题)
CPoint pt;
// 该方格的取样点的颜色。它的作用是为了帮助确定所有方格的iID。
COLORREF cl[SAMPLES];
}GRID;
// 整个游戏区所有的方格
static GRID g_Grid[ROWS][COLS];
// 当前的方格种类ID(Hash)最大值
static int g_iGridID = 0;
// 作弊器窗口句柄。给它发送消息的时候要用到
static HWND g_hThisWnd = NULL;
// 连连看 HWND
static HWND g_hLLKWnd = NULL;
//
// 一些辅助宏
// 获得RGB个分量
#define B(X) (GetBValue(X))
#define G(X) (GetGValue(X))
#define R(X) (GetRValue(X))
// 比较大小
#define MIN(X, Y) ((X) > (Y) ? (Y) : (X))
#define MAX(X, Y) ((X) > (Y) ? (X) : (Y))
// 差的绝对值
#define ABS(X, Y) ((X) < (Y) ? (Y) - (X) : (X) - (Y))
//
// 计算两种颜色个分量的差值之和
int Difference(const COLORREF &c1, const COLORREF &c2)
{
return ABS(R(c1), R(c2))
+ ABS(G(c1), G(c2))
+ ABS(B(c1), B(c2));
}
// 在矩阵中查找和指定颜色的差别在容差范围内的方格(r, c)。如果不存在则返回 - 1,
// 否则返回0和该颜色所在方格的位置。
int FindColor(const COLORREF cl[SAMPLES], int &r, int &c)
{
int i, j, k, iDiff;
for(i = 0; i < ROWS; i ++)
{
for(j = 0 ; j < COLS; j ++)
{
// 不用和空格比较
if(g_Grid[i][j] .iID == GRID_BLANK)
continue;
iDiff = 0;
// 计算该颜色和指定颜色之间的差别
for(k = 0; k < SAMPLES; k ++)
{
iDiff += Difference(g_Grid[i][j] .cl[k], cl[k]);
}
if(iDiff <= TOLERANCE)
{
// 找到
r = i;
c = j;
return 0;
}
}
}
return - 1;
}
// 通过取样点的RGB值确定一个方格的类别ID(Hash)
int GetGridID(const COLORREF cl[SAMPLES])
{
// 根据颜色将方格分类
for(int i = 0; i < SAMPLES; i ++)
{
// 空格的RGB分量范围
// R(44~74), G(49~91), B(102~103)
if (R(cl[i]) > 44 && R(cl[i]) < 74
&& G(cl[i]) > 49 && G(cl[i]) < 91
&& B(cl[i]) > 102 && R(cl[i]) < 130)
{
}
else
{
// 是否已经存在?
int r, c;
if(FindColor(cl, r, c) == - 1)
{
// 不存在,则添加
g_iGridID ++;
return g_iGridID;
}
else
{
// 存在则返回ID
return g_Grid[r][c] .iID;
}
}
}
// 空格
return GRID_BLANK;
}
// 报告错误
void ReportError(CString strMsg)
{
AfxMessageBox(strMsg);
}
// 获得游戏方格特征矩阵
BOOL InitMatrix(HWND hWnd)
{
// 获得连连看CWnd
CWnd* pWnd = CWnd ::FromHandle(hWnd);
if(NULL == pWnd)
return FALSE;
// 连连看最小化了?
if(pWnd ->IsIconic())
{
pWnd ->ShowWindow(SW_RESTORE);
Sleep(200);
}
// 连连看没有激活?
if(pWnd ->GetActiveWindow() ->GetSafeHwnd() != hWnd)
{
pWnd ->SetForegroundWindow();
pWnd ->SetActiveWindow();
Sleep(200);
}
// 获得连连看的CDC
CDC* pDC = pWnd ->GetDC();
if(pDC == NULL)
return FALSE;
// 获得连连看游戏区左上角第一个方格的中心坐标(客户区的)
CPoint pt(0, 0);
// 获得对应所有方格的特征
for(int i = 0; i < ROWS; i ++)
{
// 方格左上角的Y坐标
pt .y = YCOFFSET + i * HEIGHT;
for(int j = 0; j < COLS; j ++)
{
// 方格左上角的X坐标
pt .x = XCOFFSET + j * WIDTH;
// 保存方格中心坐标(客户区)
g_Grid[i][j] .pt .Offset(pt .x + WIDTH / 2, pt .y + HEIGHT / 2);
// 得到该方格的取样点的RGB
for(int k = 0; k < SAMPLES; k ++)
{
// 如果用户关闭了程序窗口
if(::IsWindow(hWnd))
{
g_Grid[i][j] .cl[k] = pDC ->GetPixel(pt .x + g_Offsets[k] .x, pt .y + g_Offsets[k] .y);
}
else
{
// 窗口已经被关闭!
ReportError(_T("窗口在编码时被关闭,请检查连连看的状态,并尝试重新编码!"));
return FALSE;
}
}
// 确定该方格类别Hash
g_Grid[i][j] .iID = GetGridID(g_Grid[i][j] .cl);
TRACE(_T("%4d,"), g_Grid[i][j] .iID);
}
TRACE(_T("/r/n"));
}
return TRUE;
}
// 判断两个在同一行(列)的方格(必须同类,可以是空格)之间是否存在直线通路
BOOL GridsConnected(int pivot, int l, int h, BOOL bHorizontal = TRUE)
{
// 是否同类?
if(bHorizontal)
{
if(g_Grid[pivot][l] .iID != g_Grid[pivot][h] .iID)
return FALSE;
}
else
{
if(g_Grid[l][pivot] .iID != g_Grid[h][pivot] .iID)
return FALSE;
}
int L = MIN(l, h);
int H = MAX(l, h);
// 两个方格之间必须都是空格
for(int i = L + 1; i < H; i ++)
{
if(bHorizontal)
{
if(g_Grid[pivot][i] .iID != GRID_BLANK)
return FALSE;
}
else
{
if(g_Grid[i][pivot] .iID != GRID_BLANK)
return FALSE;
}
}
return TRUE;
}
// 输出路径
void OutputPath(int r1, int c1, int r2, int c2)
{
if(r1 == r2 && c1 == c2)
return;
//TRACE(_T(">>>(%d, %d) -> (%d, %d)./r/n"), r1, c1, r2, c2);
}
// 判断两个方格(r1, c1)和(r2, c2)(不能是空格)是否可以消除
BOOL Match(const int r1, const int c1, const int r2, const int c2)
{
// 不能是空格
if(g_Grid[r1][c1] .iID == GRID_BLANK
|| g_Grid[r2][c2] .iID == GRID_BLANK)
return FALSE;
// 必须是同一种方格
if(g_Grid[r1][c1] .iID != g_Grid[r2][c2] .iID)
return FALSE;
// 起始点的类别
const int iID = g_Grid[r1][c1] .iID;
// 它们在同一行?
if(r1 == r2)
{
if(c1 <= c2)
{
// 它们之间是否有直线通路?
if(GridsConnected(r1, c1, c2))
{
OutputPath(r1, c1, r1, c2);
return TRUE;
}
// 是否有其他通路?
for(int i = 0; i < ROWS; i ++)
{
// 已经判断过了
if(i == r1)
continue;
// 两个拐点必须是空格
if(GRID_BLANK == g_Grid[i][c1] .iID
&& GRID_BLANK == g_Grid[i][c2] .iID)
{
// 将两个拐点的类别设置为iID,然后判断是否有路径
g_Grid[i][c1] .iID = g_Grid[i][c2] .iID = iID;
if(GridsConnected(c1, i, r1, FALSE)
&& GridsConnected(i, c1, c2)
&& GridsConnected(c2, i, r1, FALSE))
{
// OK,还原类别
g_Grid[i][c1] .iID = GRID_BLANK;
g_Grid[i][c2] .iID = GRID_BLANK;
OutputPath(i, c1, r1, c1);
OutputPath(i, c1, i, c2);
OutputPath(i, c2, r1, c2);
return TRUE;
}
else
{
// 不通,则测试下一条路径,并且还原拐点类别
g_Grid[i][c1] .iID = GRID_BLANK;
g_Grid[i][c2] .iID = GRID_BLANK;
}
}
} // end for
// 不通
return FALSE;
}
else
{
return Match(r1, c2, r1, c1);
}
}
// 它们在同一列?
if(c1 == c2)
{
if(r1 <= r2)
{
if(GridsConnected(c1, r1, r2, FALSE))
{
OutputPath(r1, c1, r2, c1);
return TRUE;
}
// 是否有其他通路?
for(int i = 0; i < COLS; i ++)
{
if(i == c1)
continue;
// 必须为空格
if( GRID_BLANK == g_Grid[r1][i] .iID
&& GRID_BLANK == g_Grid[r2][i] .iID)
{
//将两个拐点的类别设置为iID,然后判断是否有路径
g_Grid[r1][i].iID = g_Grid[r2][i] .iID = iID;
if(GridsConnected(r1, i, c1)
&& GridsConnected(i, r1, r2, FALSE)
&& GridsConnected(r2, i, c1))
{
// OK, 还原类别
g_Grid[r1][i].iID = GRID_BLANK;
g_Grid[r2][i].iID = GRID_BLANK;
OutputPath(r1, c1, r1, i);
OutputPath(r1, i, r2, i);
OutputPath(r2, i, r2, c1);
return TRUE;
}
else
{
// 不通,则测试下一条路径,并且还原拐点类别
g_Grid[r1][i].iID = GRID_BLANK;
g_Grid[r2][i].iID = GRID_BLANK;
}
}
}
// 不通
return FALSE;
}
else
{
return Match(r2, c1, r1, c1);
}
}
// 不在同一列或者行
// 通过在拐点构造和方格类别相同的方格,然后判断它们是否联通(最多3条线,最少2两条
// 其中一条退化)
for(int i = 0; i < ROWS; i ++)
{
// 拐点1必须为空格
if(g_Grid[i][c1] .iID != GRID_BLANK)
continue;
// 起点和拐点1联通吗?
g_Grid[i][c1] .iID = iID;
if(GridsConnected(c1, i, r1, FALSE))
{
// 是的,拐点2必须为空格
if(g_Grid[i][c2] .iID != GRID_BLANK)
{
// 还原拐点1的类别,尝试下一条路径
g_Grid[i][c1] .iID = GRID_BLANK;
continue;
}
// 拐点2和拐点1联通吗?
g_Grid[i][c2] .iID = iID;
if(GridsConnected(i, c1, c2))
{
// 是的,然后判断拐点2和(r2, c2)是否联通
if(GridsConnected(c2, i, r2, FALSE))
{
// 是的,路径找到,还原拐点类别
g_Grid[i][c2] .iID = GRID_BLANK;
g_Grid[i][c1] .iID = GRID_BLANK;
//
OutputPath(r1, c1, i, c1);
OutputPath(i, c1, i, c2);
OutputPath(i, c2, r2, c2);
return TRUE;
}
}
// 拐点2和拐点1不通,还原拐点类别,尝试下一行
g_Grid[i][c1] .iID = GRID_BLANK;
g_Grid[i][c2] .iID = GRID_BLANK;
}
else
{
// 起点和拐点1不联通,还原拐点类别
g_Grid[i][c1] .iID = GRID_BLANK;
}
} // end for
for(i = 0; i < COLS; i ++)
{
// 拐点1必须为空格
if(g_Grid[r1][i] .iID != GRID_BLANK)
continue;
// 起点和拐点1联通吗?
g_Grid[r1][i] .iID = iID;
if(GridsConnected(r1, i, c1))
{
// 是的,拐点2 必须为空格
if(g_Grid[r2][i] .iID != GRID_BLANK)
{
// 还原拐点1的类别,尝试下一条路径
g_Grid[r1][i] .iID = GRID_BLANK;
continue;
}
// 拐点2和拐点1联通吗?
g_Grid[r2][i] .iID = iID;
if(GridsConnected(i, r1, r2, FALSE))
{
// 是的,然后判断拐点2和(r2, c2)是否联通
if(GridsConnected(r2, i, c2))
{
// 是的,路径找到,还原拐点类别
g_Grid[r1][i] .iID = GRID_BLANK;
g_Grid[r2][i] .iID = GRID_BLANK;
//
OutputPath(r1, c1, r1, i);
OutputPath(r1, i, r2, i);
OutputPath(r2, i, r2, c2);
return TRUE;
}
}
// 拐点2和拐点1不通,还原拐点类别,尝试下一行
g_Grid[r1][i] .iID = GRID_BLANK;
g_Grid[r2][i] .iID = GRID_BLANK;
}
else
{
// 起点和拐点1不通,则还原拐点类别,尝试下一条路径
g_Grid[r1][i] .iID = GRID_BLANK;
}
}
return FALSE;
}
// 模拟点击两次鼠标。实际情况有点不一样,鼠标没有点击两次,至少看起来是这样。事实上只是点击了第一次,后一次没有点击。用SendInput问题一样。
void MouseClick(const CPoint& p1, const CPoint& p2)
{
// 将客户区坐标转换为屏幕坐标
CPoint pp1(p1), pp2(p2);
::ClientToScreen(g_hLLKWnd, &pp1);
::ClientToScreen(g_hLLKWnd, &pp2);
// 点击两次鼠标
/* */
::SetCursorPos(pp1 .x, pp1 .y);
mouse_event( MOUSEEVENTF_LEFTDOWN, 0, 0, 0, 0);
mouse_event( MOUSEEVENTF_LEFTUP, 0, 0, 0, 0);
Sleep(100);
::SetCursorPos(pp2 .x, pp2 .y);
mouse_event( MOUSEEVENTF_LEFTDOWN , 0, 0, 0, 0);
mouse_event( MOUSEEVENTF_LEFTUP, 0, 0, 0, 0);
/*
INPUT clk;
ZeroMemory(&clk, sizeof(clk));
clk .type = INPUT_MOUSE;
clk . mi .dwFlags = MOUSEEVENTF_LEFTDOWN | MOUSEEVENTF_LEFTUP;
clk . mi .dwFlags = MOUSEEVENTF_LEFTDOWN | MOUSEEVENTF_LEFTUP;
::SetCursorPos(pp1 .x, pp1 .y);
VERIFY(1 == SendInput(1, &clk, sizeof(clk)));
Sleep(300);
::SetCursorPos(pp2 .x, pp2 .y);
VERIFY(1 == SendInput(1, &clk, sizeof(clk)));
Sleep(300);
*/
}