Не давно наткнулся на простенькую игрушку, где необходимо стрелять шариком в группы одного цвета. Хотя в игры я играю очень редко, минут 30 я с ней посидел.
Захотелось автоматизировать этот процесс. Знаний для игры не требуется, да игр таких много.
Описываю процесс написания бота к данной игре.
Идея была следующая:
Шаг 1. получаем картинку с экрана
Шаг 2. перенос цветов шариков в матрицу
Шаг 3. высчитывание позиции и клик мышкой для выполнения хода
Для написания программы я немного колебался между Qt и Delphi, но так как хотелось всё сделать побыстрей решил сделать на Delphi.
Теперь можно реализовывать Шаг 1.
Для получения картинки задаём приблизительное положение окна с игрой и копируем этот участок в TImage, ранее расположенный на форме.
procedure MakeFrame();
var
bmp:TBitmap;
rect:TRect;
begin
rect.Left := 50;
rect.Top:= 150;
rect.Right:=550; // Screen.Width
rect.Bottom:=550; // Screen.Height
bmp := TBitmap.Create;
bmp.Width := Screen.Width;
bmp.Height := Screen.Height;
BitBlt(bmp.Canvas.Handle, 0, 0, rect.Right, rect.Bottom,
GetDC(0), rect.Left, rect.Top, SRCCOPY);
Form1.Image1.Width := rect.Right;
Form1.Image1.Height := rect.Bottom;
Form1.Image1.Picture.Assign(bmp);
bmp.Free;
end;
Если поместить данный код в таймер, двигая окно с игрой можно расположить его так, что копироваться будет именно изображение игры.
Шаг2. Необходимо узнать цвета шариков. Как видно их всего 3 варианта, прям RGB.
Высота между рядами а также диаметры шариков постоянные, поэтому мы можем составить сетку положения шариков.
for I:=0 to rows do
begin
offset:=10; // сдвиг
if (I mod 2) <> 0 then offset:=offset+(36 div 2);
for J:=0 to cols do
begin
mat[i,j].Y := 10+Round(I*36); // позиция шарика
mat[i,j].X := offset+Round(J*36);
// пулятель (было не охото создавать ещё переменную и повторять код)
if (I=rows) and (J=cols) then begin
mat[i,j].Y := 519;
mat[i,j].X := 262;
end;
// обработка полученного пикселя
end;
end;
Чтобы узнать цвет шарика сравним его пиксель с пороговым значением каждого из цветов RGB
rgb:=Form1.Image1.Canvas.Pixels[ mat[i,j].X, mat[i,j].Y-4 ];
mat[i,j].color := 0;
mat[i,j].summa := 0;
if GetRValue(rgb) > 230 then mat[i,j].color:=1;
if GetGValue(rgb) > 230 then mat[i,j].color:=2;
if GetBValue(rgb) > 230 then mat[i,j].color:=3;
И чтобы убедиться что всё в порядке нарисуем на шарике маленький прямоугольник с полученным цветом
if mat[i,j].color = 0 then Form1.Image1.Canvas.Brush.Color := clWhite;
if mat[i,j].color = 1 then Form1.Image1.Canvas.Brush.Color := clRed;
if mat[i,j].color = 2 then Form1.Image1.Canvas.Brush.Color := clGreen;
if mat[i,j].color = 3 then Form1.Image1.Canvas.Brush.Color := clBlue;
Form1.Image1.Canvas.Rectangle(
mat[i,j].X-5, mat[i,j].Y-5, mat[i,j].X+5, mat[i,j].Y+5
);
Получается такая симпатичная картина.
На этом работа с сеткой заканчивается и начинается работа с полученной матрицей
Шаг 3.
Проверим все шарики на возможность пальнуть в него, для этого нужно проверить, чтобы на пути к нему мы не столкнулись с другим шариком.
Проверка происходит через вложенные циклы (по шарикам и для каждого проверяем остальные)
Само условие проверки получается при системы из трёх неравенств.
{ A=1, B=(x0-y0)/(y1-y0), C=-x0-By0 }
|Ax3+By3+C| / sqrt(A^2+b^2) <= radius
/// теперь найдём шар куда пулять
for I:=0 to rows-1 do
for J:=0 to cols do
begin
if (mat[I,J].color = 0) and (I <> 0) then continue;
mat[i,j].allow:=1;
// куда хотим пулять
LineMaxX:=mat[I,J].X;
LineMaxY:=mat[I,J].Y;
// откуда хотим пулять
LineMinX:=mat[rows,cols].X;
LineMinY:=mat[rows,cols].Y;
mat[I,J].dist := sqrt(
sqr(mat[I,J].X-mat[rows,cols].X)+
sqr(mat[I,J].Y-mat[rows,cols].Y));
for II:=I+1 to rows-1 do
for JJ:=0 to cols do
begin
if mat[II,JJ].color = 0 then continue; // если шарика нет
LineMiddleX:=mat[II,JJ].X;
LineMiddleY:=mat[II,JJ].Y;
// не множко не красивый код но переписывалось
// с решенных уравнений на бумажке
ka:=1;
kb:=(LineMinX-LineMaxX)/(LineMaxY-LineMinY);
kc:=-LineMinX-kb*LineMinY;
kz:=
abs(ka*LineMiddleX + kb*LineMiddleY+kc)/
sqrt(sqr(ka)+sqr(kb));
if kz < 39 then mat[i,j].allow:=0;
//Form1.Memo1.Lines.Add(FloatToStr(kz));
end;
Для наглядности проведём линии к шарикам, к которым можно пулять.
if mat[i,j].allow = 1 then begin
Form1.Image1.Canvas.Pen.Width:= 2;
Form1.Image1.Canvas.Pen.Color:= clWhite;
Form1.Image1.Canvas.MoveTo(Round(LineMinX),Round(LineMinY));
Form1.Image1.Canvas.LineTo(Round(LineMaxX),Round(LineMaxY));
end;
Нужно из разрешенных выбрать шарик с одинаковым цветом.
for I:=0 to rows-1 do
for J:=0 to cols do
begin
if mat[i,j].allow = 1 then
begin
Form1.Image1.Canvas.Pen.Width:= 2;
Form1.Image1.Canvas.Pen.Color:= clWhite;
Form1.Image1.Canvas.MoveTo(Round(LineMinX),Round(LineMinY));
Form1.Image1.Canvas.LineTo(Round(LineMaxX),Round(LineMaxY)); // линия
if mat[i,j].color=mat[rows,cols].color // если цвета совпали
then begin
MouseX:=mat[i,j].X+50;
MouseY:=mat[i,j].Y+250; // шарик + смещение по экрану
break;
end;
end;
end;
И сделать клик мышкой на экране через WinApi
if Form1.CheckBox2.Checked then
begin
GetCursorPos(MouseL);
SetCursorPos(MouseX,MouseY);
mouse_event(MOUSEEVENTF_LEFTDOWN,MouseX,MouseY,0,0);// - нажать левой кнопки
mouse_event(MOUSEEVENTF_LEFTUP,MouseX,MouseY,0,0);// - отпустить левую кнопку
SetCursorPos(MouseL.X,MouseL.Y);
end;
Осталось всё выполнить в таймере и идти пить чай.
Видео работы программы
Можно улучшить алгаритм например находить группы с большим числом шариков или искать линии отрезающие части поля.
Имена функций и переменные может названы не красиво, но это можно исправить.
Если нужны исходники, то они тут.
Автор: darkoff