由连连看游戏作弊器想到的

   前几天逛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);
 */
}

评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值