среда, 14 сентября 2011 г.

Написание бота для онлайн игры

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

robot = new Robot();
// секунда на то, чтоб свернуть приложение
robot.delay(1000);

Вдоволь наигравшись, я решил всё-таки перейти к делу. Нашел на facebook'е игру Diamond Dash, которая и послужила учителем моего бота. Первым делом я произвёл замеры области игры на экране. Узнал смещение, откуда начинают рисоваться блоки. Узнал размер одного блока - 40 пикселей. Размер поля - 10 на 9 блоков.
Игровое поле Diamond Dash
 Недолго думая, была выведена формула, по которой можно было бы обращаться к любому блоку:

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);
            }
        }
    }

}


Демонстрация работы программы:

Исходный код можно скачать здесь.

3 комментария:

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

    ОтветитьУдалить
  2. Это Netbeans IDE. Моя родная среда, так сказать :)

    ОтветитьУдалить