Написание бота для онлайн игры
Не так давно, просматривая ленту на Хабре, наткнулся на такую вот статью. Бегло просмотрев её, я решил сделать подобное на своём родном языке 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. Моя родная среда, так сказать :)
ОтветитьУдалить