Pull to refresh

Бот для браузерки Angry Pets

Reading time6 min
Views11K
Предупреждение: бот прост до безобразия. Вы вряд ли научитесь чему-то новому. Статья just for fun.

Решил, что спустя год можно поглядеть, чем френды вконтакте развлекаются. Кроме традиционных тонн политоты, картинок с котятами и прочих непотребств обнаружил ссылку на браузерку Angry Pets. В браузерки никогда не играл, поэтому решил посмотреть, что это за зверь такой. Выяснилось следующее: картинки кавайнейшие (милые котики, пингвинчики и белочки бьют друг другу морды), донат анальнейший (за деньги доход умножают в 8 раз), сюжет, стратегия, безбажность, мануал и прочие привычные удобства отсутствуют.

Игра чуть менее, чем полностью, состоит из следующих нехитрых операций: построить здание, подождать 10 минут, построить юнитов, подождать 10 минут, найти жертву, отправить юниты в атаку, подождать 10 минут, найти жертву, отправить юниты в атаку, подождать 10 минут… (10 минут на высоких уровнях растут экспоненциально. За ускорение — отдельная плата.)

Что делать с такой игрой? Правильно, писать бота. Не играть же в это, честное слово.



Ставим задачу:

  • Бот должен уметь атаковать заданные списком города по расписанию.
  • Боту можно задавать, какими юнитами атаковать.
  • Бот должен не сразу палиться. :)

Чтобы бот не спалился сразу, во-первых, жертвами для избиения младенцев фарма выбираем игроков, долго не выходивших в онлайн, чтобы они на нас не пожаловались куда следует; во-вторых, имитируем действия пользователя, а не отправляем голые AJAX запросы; в-третьих, задержку между действиями делаем не нулевой, а случайной.

На радость авторам ботов скрипты на сайте не обфусцированы. Единственные упакованные скрипты — это jQuery и иже. И да, есть jQuery, поэтому пользуемся всеми благами цивилизации.

Большинство действий в игре приводят к AJAX запросам. Нажимаешь элемент, уходит запрос, приходит или практически полный экран, или отдельное окошко. Исследуя в отладчике ссылки, видим, что они выглядят как
<a href="/10193192/city/view/10009466" data-link-handled="1" onclick="Main.goToUrl(this);return false;"></a>
где метод Main.goToUrl принимает либо строки, либо элементы-ссылки. У большинства игровых ссылок в начале стоит префикс с идентификатором профиля игрока. Сохраняем его.

    profilePath: window.location.pathname.match(/^\/\d+\//)[0],

Как сделать ожидание выполнения AJAX запроса? В jQuery есть славная функция $.ajaxSuccess, в которую можно передать коллбэк, вызываемый после каждого успешного запроса. В него сваливаются событие, объект XMLHttpRequest и аргументы вызова $.ajax. Соответственно, при получении заданного УРЛа вызываем наш коллбэк.

    ajaxCallbacks: {},

    run: function ()
    {
        ...
        $('html').ajaxSuccess(Bot.ajaxSuccess);
        ...
    },

    ajaxSuccess: function (e, xhr, settings)
    {
        var ajaxUrl = null, ajaxCallback = null;
        $.each(Bot.ajaxCallbacks, function (url, callback)
        {
            var fullUrl = Bot.profilePath + url;
            if (settings.url.substr(0, fullUrl.length) == fullUrl) {
                ajaxUrl = url;
                ajaxCallback = callback;
            }
        });

        if (ajaxCallback) {
            Bot.ajaxCallbacks[ajaxUrl] = null;
            setTimeout(ajaxCallback, Bot.getClickDelay());
        }
    },

    waitForAjax: function (pageUrl, gotoPage, success)
    {
        Bot.ajaxCallbacks[pageUrl] = success;
        gotoPage();
    },

Ну и чисто для единообразия к waitForAjax добавляем waitForAction, когда нужно не ждать AJAX, а просто сделать задержку.

    waitForAction: function (action, success)
    {
        action();
        setTimeout(success, Bot.getClickDelay());
    },

Как простой неавтоматизированный смертный часто фармит? Жмёт по кнопке почты, переход в логи, выбирает недавно атакованный город, жмёт «Атаковать», выбирает юниты, жмёт «Набить морду».



Вот и будем повторять эту операцию по кругу. Конечно, в логах нужный город на нужной странице будет не всегда, но писать сложную логику перелистывания без особой мотивации — откровенно лень.

Выбираем жертву и формируем ссылки…
    attackNext: function ()
    {
        if (Bot.targetCities.length == 0)
            return;
        if (!Bot.targetCities[Bot.currentTargetCity])
            Bot.currentTargetCity = 0;
        var targetCity = Bot.targetCities[Bot.currentTargetCity++],
            targetCityUrl = 'city/view/' + targetCity,
            attackCityUrl = 'attack/' + targetCity;

Переходим по ссылкам и жмём кнопки…
        Bot.waitForAjax('pm/inbox', function ()
        {
            Main.goToUrl(Bot.profilePath + 'pm/inbox');
        }, function ()
        {
            Bot.waitForAjax('pm/logs', function ()
            {
                Main.goToUrl(Bot.profilePath + 'pm/logs');
            }, function ()
            {
                Bot.waitForAjax(targetCityUrl, function ()
                {
                    Main.goToUrl(Bot.profilePath + targetCityUrl);
                }, function ()
                {
                    Bot.waitForAjax(attackCityUrl, function ()
                    {
                        $('button[onclick^="Attack.showAttackAlert"]').click();
                    }, function ()
                    {

Либо выбираем конкретные юниты (доступное количество — в атрибуте инпута max), либо выбираем все (отдельная кнопка), а затем самое важное: нажать «Атаковать».
                        Bot.waitForAction(function ()
                        {
                            var count = 0;
                            $.each(Bot.attackUnits, function (unitType, unitNum)
                            {
                                var ctl = $('input[name="units[' + unitType + ']"]');
                                ctl.val(Math.min(ctl.attr('max'), unitNum)).change();
                                count++;
                            });
                            if (count == 0) {
                                $('span[onclick^="Attack.ChooseEveryone"]').click();
                            }
                        }, function ()
                        {
                            $('button[type=submit]').click();
                            setTimeout(Bot.attackNext, Bot.getAttackInterval());
                        })

Попапы-слои при переходе по AJAX-страницам закрываются сами, можно не заморачиваться.

Собственно, всё. Только красивый список атакуемых городов можно прикрутить, чтобы видеть, кого мочим.

Полный код:
window.Bot = {

    attackInterval: /*5.5*/8 * 60 * 1000, // 8 min
    attackIntervalRandom: 1.2 * 60 * 1000, // 1.2 min

    clickDelay: 3 * 1000, // 3 sec
    clickDelayRandom: 4 * 1000, // 4 sec

    targetCities: [
        //12345678
    ],

    attackUnits: {
        //101: 99
    },

    profilePath: window.location.pathname.match(/^\/\d+\//)[0],

    currentTargetCity: 0,

    ajaxCallbacks: {},

    run: function ()
    {
        var box = '<div style="position: absolute; background: #fff; padding: 10px; border-radius: 10px; left: 20px; top: 20px; z-index: 666666">';
        $.each(Bot.targetCities, function (_, cityId)
        {
            box += '<a class="bot-target-city" data-link-handled="1" onclick="Main.goToUrl(this);return false;"' +
                ' id="bot-target-city-' + cityId + '"' +
                ' href="' + Bot.profilePath + 'city/view/' + cityId + '">' +
                cityId + '</a><br>';
        });
        box += '</div>';
        $('body').append(box);

        $('html').ajaxSuccess(Bot.ajaxSuccess);
        Bot.attackNext();
    },

    ajaxSuccess: function (e, xhr, settings)
    {
        var ajaxUrl = null, ajaxCallback = null;
        $.each(Bot.ajaxCallbacks, function (url, callback)
        {
            var fullUrl = Bot.profilePath + url;
            if (settings.url.substr(0, fullUrl.length) == fullUrl) {
                ajaxUrl = url;
                ajaxCallback = callback;
            }
        });

        if (ajaxCallback) {
            Bot.ajaxCallbacks[ajaxUrl] = null;
            setTimeout(ajaxCallback, Bot.getClickDelay());
        }
        else {
            console.log('Not recognized ' + settings.url);
        }
    },

    waitForAjax: function (pageUrl, gotoPage, success)
    {
        Bot.ajaxCallbacks[pageUrl] = success;
        gotoPage();
    },

    waitForAction: function (action, success)
    {
        action();
        setTimeout(success, Bot.getClickDelay());
    },

    getAttackInterval: function ()
    {
        return parseInt(Bot.attackInterval + Math.random() * Bot.attackIntervalRandom);
    },

    getClickDelay: function ()
    {
        return parseInt(Bot.clickDelay + Math.random() * Bot.clickDelayRandom);
    },

    attackNext: function ()
    {
        if (Bot.targetCities.length == 0)
            return;
        if (!Bot.targetCities[Bot.currentTargetCity])
            Bot.currentTargetCity = 0;
        var targetCity = Bot.targetCities[Bot.currentTargetCity++],
            targetCityUrl = 'city/view/' + targetCity,
            attackCityUrl = 'attack/' + targetCity;

        $('a.bot-target-city').css({ fontWeight: 'normal' });
        $('a#bot-target-city-' + targetCity).css({ fontWeight: 'bold' });

        Bot.waitForAjax('pm/inbox', function ()
        {
            Main.goToUrl(Bot.profilePath + 'pm/inbox');
        }, function ()
        {
            Bot.waitForAjax('pm/logs', function ()
            {
                Main.goToUrl(Bot.profilePath + 'pm/logs');
            }, function ()
            {
                Bot.waitForAjax(targetCityUrl, function ()
                {
                    Main.goToUrl(Bot.profilePath + targetCityUrl);
                }, function ()
                {
                    Bot.waitForAjax(attackCityUrl, function ()
                    {
                        $('button[onclick^="Attack.showAttackAlert"]').click();
                    }, function ()
                    {
                        Bot.waitForAction(function ()
                        {
                            var count = 0;
                            $.each(Bot.attackUnits, function (unitType, unitNum)
                            {
                                var ctl = $('input[name="units[' + unitType + ']"]');
                                ctl.val(Math.min(ctl.attr('max'), unitNum)).change();
                                count++;
                            });
                            if (count == 0) {
                                $('span[onclick^="Attack.ChooseEveryone"]').click();
                            }
                        }, function ()
                        {
                            $('button[type=submit]').click();
                            setTimeout(Bot.attackNext, Bot.getAttackInterval());
                        })
                    });
                });
            });
        });
    }
};
Bot.run();


Инструкция по применению:

  • В коде массив targetCities заполнить идентификаторами атакуемых городов. При просмотре страницы города идентификатор — это число в самом конце.
  • В коде объект attackUnits заполнить парами тип юнита — количество юнитов. Идентификаторы типов любезно предоставлены разработчиками (101 из примера — кошачий «боец»).
  • Задержки настроить в соответствии со своими возможностями: attackInterval — интервал между атаками, clickDelay — задержка при кликах, значения Random — диапазон случайно добавляемой задержки.
  • Зайти в игру.
  • Запустить код из отладочной консоли.
  • Если всё верно, то слева появится список идентификаторов атакуемых городов, а скрипт приступит к атаке на первый город. Текущий город выделяется жирным.

Вообще, меня забавляет наглость хозяев игры. «Абонентская плата» в виде Озверина (без которого играть ещё более уныло) — это 180 руб/мес (WoW, в котором есть всё, чего нет в этой игре, стоит всего лишь в 2 раза дороже). Ресурсов можно покупать на до 120 руб/день (день!), увеличивая скорость добычи ресурсов в 8 раз (соревнование кошельков, неплатящие вообще идут лесом). Просторы для вымогательства доната на ускорялки, детали для ракеты, бонусы для атаки, прохождение квестов (ага, заплатил 300 руб — квест прошёл) и прочую мелочь — вообще безграничны. Обмен ресурсов, вступление в клан и прочее — только за деньги. У них в оплате «выбор игроков» — это монетки на 1500 руб. И это всё прикрывается железным аргументом: «Часть дохода идёт в WWF! Вы спасаете милых пушистых зверьков! Будете возмущаться — забаним нафиг!»

И ведь люди платят. Я фигею.
Tags:
Hubs:
+30
Comments17

Articles

Change theme settings