Написание бота для Diamond Dash
от aNNiMON
Не так давно, просматривая ленту на Хабре, наткнулся на такую вот статью. Бегло просмотрев её, я решил сделать подобное на своём родном языке Java. Сейчас постараюсь в точности описать ход своих мыслей и идей. Приступим.
Первым делом нужно было узнать, умеет ли Java работать с мышкой? Интуиция выдала стопроцентный положительный результат, затем, через десять секунд был введён запрос в Google и получено подтверждение - в таких делах наш помощник это класс java.awt.Robot. Он умеет получать изображение с экрана, эмулировать нажатия клавиш и управлять мышью. То что нужно. Для начала решил освоить этот класс, для этого написал метод, который "набирал" переданный ему текст. Исходный код этого метода выглядит так:Здесь всё просто: проходим по тексту символ за символом и проверяем, строчная или прописная буква перед нами. В зависимости от этого "нажимаем" SHIFT, а потом "отпускаем". кодирует символ в его код, для передачи непосредственно в метод нажатия клавиши в классе Robot.
Вот так инициализируется наш робот в конструкторе:
Вдоволь наигравшись, я решил всё-таки перейти к делу. Нашел на facebook'е игру Diamond Dash, которая и послужила учителем моего бота. Первым делом я произвёл замеры области игры на экране. Узнал смещение, откуда начинают рисоваться блоки. Узнал размер одного блока - 40 пикселей. Размер поля - 10 на 9 блоков.
Недолго думая, была выведена формула, по которой можно было бы обращаться к любому блоку:
Где 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] или нет.
Ну что ж, теперь самое время связать наши "кирпичики":
Демонстрация работы программы:
Исходный код можно скачать здесь.
Первым делом нужно было узнать, умеет ли 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);
- }
- }
- }
- int event = KeyEvent.getExtendedKeyCodeForChar(symbol);
Вот так инициализируется наш робот в конструкторе:
- 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;
В итоге был написан метод, кликающий по выбранному блоку:
- /**
- * Кликнуть по нужному блоку
- * @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);
- }
Вскоре, после написания еще одного метода, программа уже была работоспособна и пригодна для первого теста. Я не стал заморачиваться и сделал случайный выбор нажимаемого блока.
- /**
- * Автоклик
- * @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} }
- };
Теперь можно написать функцию, которая подсказывает нам, стоит ли кликать по блоку [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);
- }
- }
- }
- }
Демонстрация работы программы:
Исходный код можно скачать здесь.