Написание бота для онлайн игры
Не так давно, просматривая ленту на Хабре, наткнулся на такую вот статью. Бегло просмотрев её, я решил сделать подобное на своём родном языке Java. Сейчас постараюсь в точности описать ход своих мыслей и идей. Приступим.
Первым делом нужно было узнать, умеет ли Java работать с мышкой? Интуиция выдала стопроцентный положительный результат, затем, через десять секунд был введён запрос в Google и получено подтверждение - в таких делах наш помощник это класс java.awt.Robot. Он умеет получать изображение с экрана, эмулировать нажатия клавиш и управлять мышью. То что нужно. Для начала решил освоить этот класс, для этого написал метод, который "набирал" переданный ему текст. Исходный код этого метода выглядит так:
/**
* Автоматическое написание сообщения
* @param text "печатаемый текст"
*/
public void writeMessage(String text) {
for (char symbol : text.toCharArray()) {
boolean needShiftPress = Character.isUpperCase(symbol) && Character.isLetter(symbol);
if(needShiftPress) {
robot.keyPress(KeyEvent.VK_SHIFT);
}
int event = KeyEvent.getExtendedKeyCodeForChar(symbol);
try {
robot.keyPress(event);
} catch (Exception e) {}
if(needShiftPress) {
robot.keyRelease(KeyEvent.VK_SHIFT);
}
}
}
Здесь всё просто: проходим по тексту символ за символом и проверяем, строчная или прописная буква перед нами. В зависимости от этого "нажимаем" SHIFT, а потом "отпускаем".
int event = KeyEvent.getExtendedKeyCodeForChar(symbol); кодирует символ в его код, для передачи непосредственно в метод нажатия клавиши в классе Robot.
Вот так инициализируется наш робот в конструкторе:
Вдоволь наигравшись, я решил всё-таки перейти к делу. Нашел на facebook'е игру Diamond Dash, которая и послужила учителем моего бота. Первым делом я произвёл замеры области игры на экране. Узнал смещение, откуда начинают рисоваться блоки. Узнал размер одного блока - 40 пикселей. Размер поля - 10 на 9 блоков.
robot = new Robot(); // секунда на то, чтоб свернуть приложение robot.delay(1000);
Вдоволь наигравшись, я решил всё-таки перейти к делу. Нашел на facebook'е игру Diamond Dash, которая и послужила учителем моего бота. Первым делом я произвёл замеры области игры на экране. Узнал смещение, откуда начинают рисоваться блоки. Узнал размер одного блока - 40 пикселей. Размер поля - 10 на 9 блоков.
Недолго думая, была выведена формула, по которой можно было бы обращаться к любому блоку:
xClick = startx + xClick * BRICK_SIZE + BRICK_SIZE/2;
yClick = starty + yClick * BRICK_SIZE + BRICK_SIZE/2;
Где startx / starty - смещение игрового поля от левого верхнего угла экрана, xClick / yClick - индекс нажимаемого блока, BRICK_SIZE/2 позволяет нам кликать мышкой ровно по центру блока.
В итоге был написан метод, кликающий по выбранному блоку:
Сначала мы высчитываем позицию, куда нужно переместить курсор, затем перемещаем его в это место. Далее нажимаем левую кнопку мыши, держим её 150 мс. и потом отпускаем. Вроде всё просто и прозрачно.
Вскоре, после написания еще одного метода, программа уже была работоспособна и пригодна для первого теста. Я не стал заморачиваться и сделал случайный выбор нажимаемого блока.
Количество кликов фиксировано, иначе мы не смогли бы пошевелить нормально мышкой, так как программа перехватывала бы все наши действия.
Честно говоря, я ожидал худшего результата, но после запуска, мой рандомный бот набрал около 200000 очков, что уже превышает игру "ручками". Довольно-таки неплохо, но на этом глупо было останавливаться. Я решил продолжить, и сделать распознавание необходимых блоков. Алгоритм прост:
1. Получаем скриншот экрана с помощью Robot.
2. Сканируем изображение и идентифицируем цвет каждого блока.
3. Сопоставляем каждому цвету свой целочисленный индекс.
4. Ищем, какие блоки удовлетворяют нашему условию.
5. Кликаем по блоку.
Скриншот необходимой области экрана получать очень просто - задаем размеры и готово:
С идентификацией немного сложнее, особенно тем, кто мало знаком со структурой изображений. Нужно получить цвет пикселя, и сравнить его с одним из пяти представленных в игре цветов. Здесь помог опыт реализации чувствительности для заливки. Весь код распознавания изображения и перевода его в числовые данные представлен здесь:
В массиве brickColors записаны цвета блоков в формате RGB. Далее проходим по центру всех блоков и получаем их цвета. Затем этот полученный цвет сравниваем с шаблоном (массив brickColors) на предмет соответствия с погрешностью в COLOR_TOLERANCE значений цвета. Если цвет блока близок к цвету одного из шаблонов, то этому блоку присваивается соответствующий номер.
В итоге для той картинки, которая представлена выше, функция предоставит следующий массив:
2334232212
5224155525
1141331153
5323525524
1453254424
4325433433
2431224521
4331445413
5343152525
Как видно, алгоритм довольно точно преобразовывает графическую информацию в логическую.
Следующим немаловажным шагом было написание алгоритма поиска необходимых блоков, по которым нужно кликать. Здесь я решил не делать лишних телодвижений, а просто рассчитать всевозможные комбинации, которые приводили бы к успеху. То есть имеем текущий блок с координатой [x,y], для которого нужно решить, находится ли он в блоке из более трёх таких же квадратиков. Значит нужно просмотреть все блоки вокруг и определить, какие из них такого же цвета. Я решил воспользоваться для этого двумерным массивом boolean, размером 3x3. Алгоритм заполнения этого массива такой:
1. Получаем индекс блока [x, y].
2. Если индекс нулевой, значит что-то в этом блоке не так: либо там серые квадратики, которые не нажимаются, либо там еще пусто. Поэтому блоки с нулевым индексом кликать не будем.
3. Сравниваем индексы блоков, стоящих вокруг блока [x, y]. Те блоки, индексы которых совпадают, будут в массиве выглядеть как true, все остальные - false. Это поможет нам в дальнейшем, чтобы узнать комбинации необходимых блоков.
После этого создаём шаблон с размещением правильных блоков. Так как вариантов может быть несколько, то воспользуемся трёхмерным массивом.
Правильные комбинации будут выглядеть так:
Сначала идёт значение y, а потом x. Так как [1,1] у нас всегда true, то проверять его не стоит.
Теперь можно написать функцию, которая подсказывает нам, стоит ли кликать по блоку [x, y] или нет.
Ну что ж, теперь самое время связать наши "кирпичики":
Демонстрация работы программы:
Исходный код можно скачать здесь.
xClick = startx + xClick * BRICK_SIZE + BRICK_SIZE/2;
yClick = starty + yClick * BRICK_SIZE + BRICK_SIZE/2;
Где startx / starty - смещение игрового поля от левого верхнего угла экрана, xClick / yClick - индекс нажимаемого блока, BRICK_SIZE/2 позволяет нам кликать мышкой ровно по центру блока.
В итоге был написан метод, кликающий по выбранному блоку:
/**
* Кликнуть по нужному блоку
* @param xClick
* @param yClick
*/
private void clickBlock(int xClick, int yClick) {
xClick = startx + xClick * BRICK_SIZE + BRICK_SIZE/2;
yClick = starty + yClick * BRICK_SIZE + BRICK_SIZE/2;
robot.mouseMove(xClick, yClick);
robot.mousePress(InputEvent.BUTTON1_MASK);
robot.delay(150);
robot.mouseRelease(InputEvent.BUTTON1_MASK);
}
Сначала мы высчитываем позицию, куда нужно переместить курсор, затем перемещаем его в это место. Далее нажимаем левую кнопку мыши, держим её 150 мс. и потом отпускаем. Вроде всё просто и прозрачно.
Вскоре, после написания еще одного метода, программа уже была работоспособна и пригодна для первого теста. Я не стал заморачиваться и сделал случайный выбор нажимаемого блока.
/**
* Автоклик
* @param numOfClicks количество кликов
*/
public void click(int numOfClicks) {
for (int i = 0; i < numOfClicks; i++) {
clickBlock(random.nextInt(10), random.nextInt(9));
}
}
Количество кликов фиксировано, иначе мы не смогли бы пошевелить нормально мышкой, так как программа перехватывала бы все наши действия.
Честно говоря, я ожидал худшего результата, но после запуска, мой рандомный бот набрал около 200000 очков, что уже превышает игру "ручками". Довольно-таки неплохо, но на этом глупо было останавливаться. Я решил продолжить, и сделать распознавание необходимых блоков. Алгоритм прост:
1. Получаем скриншот экрана с помощью Robot.
2. Сканируем изображение и идентифицируем цвет каждого блока.
3. Сопоставляем каждому цвету свой целочисленный индекс.
4. Ищем, какие блоки удовлетворяют нашему условию.
5. Кликаем по блоку.
Скриншот необходимой области экрана получать очень просто - задаем размеры и готово:
/*
* Получение картинки размером [width x height] с экрана с позиции [x, y]
*/
public BufferedImage getImage(int x, int y, int width, int height) {
return robot.createScreenCapture(new Rectangle(x, y, width, height));
}
С идентификацией немного сложнее, особенно тем, кто мало знаком со структурой изображений. Нужно получить цвет пикселя, и сравнить его с одним из пяти представленных в игре цветов. Здесь помог опыт реализации чувствительности для заливки. Весь код распознавания изображения и перевода его в числовые данные представлен здесь:
/*
* Преобразование картинки в массив со значениями цветов.
* ID цвета будем брать на основе преимущества цветовой компоненты в пикселе.
*/
public int[][] getBricksID(BufferedImage image) {
int[] brickColors = new int[] {
0xFF0000, // RED
0x00FF00, // GREEN
0x0000FF, // BLUE
0xFFFF00, // YELLOW
0xFF00FF, // MAGENTA
};
for(int y = 0; y < brickId.length; y++) {
for (int x = 0; x < brickId[0].length; x++) {
// Сначала берём цвета пикселей
int color = image.getRGB(x * BRICK_SIZE + BRICK_SIZE/2+BRICK_SIZE/4,
y * BRICK_SIZE + BRICK_SIZE/4);
// Проходим по всем цветам блоков и выбираем наиболее подходящий цвет
brickId[y][x] = 0;
for (int id = 0; id < brickColors.length; id++) {
if(isEquals(color, brickColors[id], COLOR_TOLERANCE)) {
brickId[y][x] = id + 1;
}
}
}
}
return brickId;
}
/**
* Проверка, соответствуют ли цвета друг другу
* @param color1 первый цвет
* @param color2 второй цвет
* @param tolerance чувствительность
* @return true - соответствуют, false - нет
*/
private boolean isEquals(int color1, int color2, int tolerance) {
if (tolerance < 2) {
return color1 == color2;
}
int r1 = (color1 >> 16) & 0xff;
int g1 = (color1 >> 8) & 0xff;
int b1 = color1 & 0xff;
int r2 = (color2 >> 16) & 0xff;
int g2 = (color2 >> 8) & 0xff;
int b2 = color2 & 0xff;
return (Math.abs(r1 - r2) <= tolerance) &&
(Math.abs(g1 - g2) <= tolerance) &&
(Math.abs(b1 - b2) <= tolerance);
}
В массиве brickColors записаны цвета блоков в формате RGB. Далее проходим по центру всех блоков и получаем их цвета. Затем этот полученный цвет сравниваем с шаблоном (массив brickColors) на предмет соответствия с погрешностью в COLOR_TOLERANCE значений цвета. Если цвет блока близок к цвету одного из шаблонов, то этому блоку присваивается соответствующий номер.
В итоге для той картинки, которая представлена выше, функция предоставит следующий массив:
2334232212
5224155525
1141331153
5323525524
1453254424
4325433433
2431224521
4331445413
5343152525
Как видно, алгоритм довольно точно преобразовывает графическую информацию в логическую.
Следующим немаловажным шагом было написание алгоритма поиска необходимых блоков, по которым нужно кликать. Здесь я решил не делать лишних телодвижений, а просто рассчитать всевозможные комбинации, которые приводили бы к успеху. То есть имеем текущий блок с координатой [x,y], для которого нужно решить, находится ли он в блоке из более трёх таких же квадратиков. Значит нужно просмотреть все блоки вокруг и определить, какие из них такого же цвета. Я решил воспользоваться для этого двумерным массивом boolean, размером 3x3. Алгоритм заполнения этого массива такой:
1. Получаем индекс блока [x, y].
2. Если индекс нулевой, значит что-то в этом блоке не так: либо там серые квадратики, которые не нажимаются, либо там еще пусто. Поэтому блоки с нулевым индексом кликать не будем.
3. Сравниваем индексы блоков, стоящих вокруг блока [x, y]. Те блоки, индексы которых совпадают, будут в массиве выглядеть как true, все остальные - false. Это поможет нам в дальнейшем, чтобы узнать комбинации необходимых блоков.
После этого создаём шаблон с размещением правильных блоков. Так как вариантов может быть несколько, то воспользуемся трёхмерным массивом.
![]() |
| Комбинации, удовлетворяющие условию. |
private static final int[][][] good = {
{ {0, 1}, {2, 1} },
{ {1, 0}, {1, 2} },
{ {0, 0}, {0, 1} },
{ {1, 0}, {2, 0} },
{ {2, 1}, {2, 2} },
{ {1, 2}, {0, 2} },
{ {0, 1}, {0, 2} },
{ {1, 2}, {2, 2} },
{ {2, 0}, {2, 1} },
{ {0, 0}, {1, 0} }
};
Сначала идёт значение y, а потом x. Так как [1,1] у нас всегда true, то проверять его не стоит.
Теперь можно написать функцию, которая подсказывает нам, стоит ли кликать по блоку [x, y] или нет.
/**
* Поиск смежных одноцветных блоков
* @param x координата блока по-горизонтали
* @param y координата блока по-вертикали
* @return стоит ли кликать по текущему блоку?
*/
private boolean searchArea(int x, int y) {
int curID = getId(x, y);
if (curID == 0) return false;
// Совпадают ли вокруг блока (x, y) индексы.
boolean[][] indexEquals = {
{ getId(x-1, y-1) == curID, getId(x, y-1) == curID, getId(x+1, y-1) == curID },
{ getId(x-1, y) == curID, true, getId(x+1, y) == curID },
{ getId(x-1, y+1) == curID, getId(x, y+1) == curID, getId(x+1, y+1) == curID }
};
// Проверка соответствия нашей комбинации искомой
for (int i = 0; i < good.length; i++) {
if ( (indexEquals[ good[i][0][0] ][ good[i][0][1] ]) &&
(indexEquals[ good[i][1][0] ][ good[i][1][1] ])) {
return true;
}
}
return false;
}
Ну что ж, теперь самое время связать наши "кирпичики":
/**
* "Умный" клик
*/
public void click() {
// Проходим снизу вверх, так как внизу блоки всегда есть
// Причем крайние блоки не трогаем
for(int y = brickId.length - 1; y > 0; y--) {
BufferedImage screen = getImage(startx, starty, 10 * BRICK_SIZE, 9 * BRICK_SIZE);
brickId = getBricksID(screen);
for (int x = 0; x < brickId[0].length; x++) {
if( ((getId(x, y) != 0) && searchArea(x, y))) {
clickBlock(x, y);
}
}
}
}
Демонстрация работы программы:
Исходный код можно скачать здесь.


О_о
ОтветитьУдалитьОсобенно интересен интеллектуальный блок по сбору окружающей инфы и принятию решения на основе матрицы. Здесь все как бы открыто и на поверхности, но практике весьма повозиться нужно.
ОтветитьУдалитьПримерно такие вопросы были, когда писал на своем покойном сиеменсе игру-имитацию "Жизнь", на МБэйсике, только там все-таки задача попроще была.
А что за среда программирования на скринах? Я не прогер, различить не могу :-)
Это Netbeans IDE. Моя родная среда, так сказать :)
ОтветитьУдалить