Простой торговый бот для The Settlers Online

в 18:58, , рубрики: perl, xdotool, Песочница, метки: , ,

Давным давно, еще в те времена когда на персональных компьютерах жил MsDOS довелось играть в игру Settlers II. Игра меня тронула, и я с удовольствием провел наедине с ней несколько дней. Много позже прошел ее повторно, а затем и еще раз, и каждый раз несмотря на древность этой игры с удовольствием проводил время играя в нее. Не так давно увидел рекламу онлайн игры The Settlers Online и поддавшись ностальгии зарегистрировался в ней. Первым впечатлением был восторг, настолько все было похоже на полюбившееся мне Settlers II. Но радужная эйфория быстро прошла. Я не буду рассказывать в этой статье о всех плюсах и минусах, расскажу только об одном минусе — торговле. О самой игре более подробно вы можете прочитать в статье The Settlers: теперь Online.

Причина

Торговля в игре реализована так, что требует постоянного присутствия. Любой торговый лот выставляется всего на 10 минут, а затем становится недоступен для других игроков. Как следствие, для того что-бы торговля принесла хоть сколько нибудь ощутимую прибыль необходимо провести в торговом интерфейсе не один час.

Следствие

Так и родилась идея написать бота который будет выставлять лот автоматически.

Пишем бота

Так как на моем десктопе установлен Linux, в качестве инструментов разработки был выбран язык программирования Perl, и утилита управления курсором мыши xdotool.

И так приступим.
бот будет состоять из 2-х файлов:

  • simple.trade.bot.pl — непосредственно сам бот.
  • trade.guns.pl — файл с координатами кнопок и дополнительными данными для торговли выбранным товаром (в моем случае это были пушки продаваемые за футбольные мячи).

Начнем с файла trade.guns.pl. В этом файле содержится список HASH (именованных) массивов содержащих координаты кнопок которые необходимо нажать чтобы выставить лот, и другую дополнительную информацию.

(
    { x=>'288',         y=>'852'},        # Кнопка [добавить]. Инициирует интерфейс выставления торгового лота
    { x=>'790',         y=>'667'},        # Кнопка [выбрать]. Интерфейс выбора продаваемого товара.
    { x=>'1126',        y=>'658'},        # Кнопка [искусные]. Выбор категории продаваемого товара.
    { x=>'871',         y=>'594'},        # Кнопка [пушки]. Непосредственно выбор самого товара.
    {                                     # Действия с бегунком выставляющим количество продаваемого товара.
                                          # Приведенные ниже данные соответствуют 100 единицам товара.
                                          # По умолчанию выставляется 400 единиц.
        x=>'983',       y=>'742',         # Координаты нажатия ЛКМ.
        dx=>'874',      dy=>'750',        # Координаты отжатия ЛКМ.
                                          # Получаем 99 единиц.
        inc=>{                            # Данный элемент содержит:
            x=>'1010',  y=>'745',         # Координаты кнопки увеличивающей значение бегунка на 1.
            inc=>'1',                     # На сколько увеличить значение бегунка.
            rnd=>'0',                     # Если не 0 то рандомное значение от 0 до rnd добавляется к inc.
            timer => [                    # Содержит поправку цены в зависимости от времени суток.
                { sh=>0, eh=>24, inc=>0 } # В данном случае поправка равна 0 на весь день.
            ]
        }
    }, 
    { x=>'1064',        y=>'733',       rx=>'20',       ry=>'10'}, # Кнопка [ОК]. Завершает выбор товара для продажи.
    { x=>'1131',        y=>'668',       rx=>'10',       ry=>'2'},  # Кнопка [выбрать]. Интерфейс выбора покупаемого товара.
                                                                   # Так как необходимый товар находится в категории по умолчанию
                                                                   # выбор категории опущен.
    { x=>'1016',        y=>'548',       rx=>'20',       ry=>'10'}, # Кнопка [мячики]. Непосредственно выбор покупаемого товара.
    {                                     # Действия с бегунком выставляющим количество покупаемого товара.
                                          # Приведенные ниже данные соответствуют 100 единицам товара.
                                          # По умолчанию выставляется 400 единиц.
        x=>'988',       y=>'743',         # Координаты нажатия ЛКМ.
        dx=>'804',      dy=>'746',        # Координаты отжатия ЛКМ.
                                          # Получаем 1 единицу.
        inc=>{                            # Данный элемент содержит:
            x=>'1010',  y=>'745',         # Координаты кнопки увеличивающей значение бегунка на 1.
            inc=>'19',                    # На сколько увеличить значение бегунка.
            rnd=>'15',                    # Если не 0 то рандомное значение от 0 до rnd добавляется к inc.
            timer => [                    # Содержит поправку цены в зависимости от времени суток.
                                          # Если поправок несколько - то каждая последующая обладает более высоким приоритетом.
                { sh=>0, eh=>24, inc=>0 },# 0 на весь день.
                { sh=>0, eh=>8, inc=>8  },# +8 c 00.00 до 08.00.
                { sh=>8, eh=>9, inc=>4  },# +4 c 08.00 до 09.00.
                { sh=>9, eh=>10, inc=>2 } # +2 c 09.00 до 10.00.
            ]
        }
    }, 
    { x=>'1083',        y=>'736',       rx=>'20',       ry=>'10' }, # Кнопка [ОК]. Завершает выбор товара для покупки.
    { x=>'961',         y=>'596',       rx=>'14',       ry=>'12' }  # Кнопка [применить]. Отправляет лот на торг.
);

В более детальном описании помимо того что дано в комментариях данный файл не нуждается. Поэтому приступим к рассмотрению simple.trade.bot.pl. Это и есть сам бот.

#!/usr/bin/perl

# После запуска скрипт дает пользователю 20 секунд для того чтобы он открыл окно с игрой и зашел в торговый интерфейс
sleep 20;

# Получаем список из файла с координатами в массив
my <hh user=g_trade> = do "trade.guns.pl";

# Далее главный цикл программы. Он бесконечен.
# Для прерывания работы программы перейдите в консоль и нажмите [Ctrl]+c.
while(){
    # Пишем в консоль о постановке нового лота на торги.
    print "NEXT TRADEn";      

    # Данный цикл последовательно проходит все элементы из массива с координатами.
    for my $l_cur ( <hh user=g_trade> ){
        sleep(1);
        if( defined $l_cur->{dx} ){ 
            # Если в текущем элементе определен ключ dx значит данный элемент - оперирует бегунком
            # выставляющим колличество товара.
            # Расчитываем количество выставляемого товара ($l_count).

            # Добавляем статическую поправку к количеству товара.
            my  $l_count=$l_cur->{inc}{inc};
            # Получаем текущее время. В ячейке №2 - часы.
            my <hh user=l_curtime>=localtime(time);
            # Перемещаем курсор в координаты x, y текущего элемента.
            move($l_cur);
            # Получаем поправку к цене зависящую от времени суток.
            my $l_inc=0;
                # Последовательно перебираем массив с поправками к цене.
                for my $l_timer ( @{$l_cur->{inc}{timer}} ){
                    # Если текущее время попадает в заданный интервал, запоминаеми поправку.
                    $l_inc=$l_timer->{inc} if $l_curtime[2] >= $l_timer->{sh} && $l_curtime[2] <= $l_timer->{eh};
                }                                                                                                                                                                                                                                                              
            # Расчитываем рандомную поправку к количеству товара.                                                                                                                                                                                                              
            my $l_rnd=int(rand($l_cur->{inc}{rnd}));                                                                                                                                                                                                                           
            # Добавляем вычисленые поправки к общему количеству товара.                                                                                                                                                                                                        
            $l_count+=$l_inc;                                                                                                                                                                                                                                                  
            $l_count+=$l_rnd;                                                                                                                                                                                                                                                  
            # Выводим в консоль данные о количестве товара и состовляющие из которых это количество складывается.                                                                                                                                                              
            print "tSET: [$l_count] [$l_cur->{inc}{inc} + $l_up + $l_rnd($l_cur->{inc}{rnd})]n";                                                                                                                                                                            
            # Увеличеваем значение бегунка на величину поправки.                                                                                                                                                                                                               
            while ($l_count){                                                                                                                                                                                                                                                  
                $l_count--;                                                                                                                                                                                                                                                    
                clicktoxy($l_cur->{inc});                                                                                                                                                                                                                                      
                usleep(80);                                                                                                                                                                                                                                                    
            }                                                                                                                                                                                                                                                                  
                                                                                                                                                                                                                                                                               
        }else{                                                                                                                                                                                                                                                                 
            # Если ключ dx не определен - значит это просто клик мыши по заданным в текущем елементе координатам.                                                                                                                                                              
            clicktoxy($l_cur);                                                                                                                                                                                                                                                 
        }                                                                                                                                                                                                                                                                      
    }                                                                                                                                                                                                                                                                          
    # Лот выставлен. Ожидаем 11 минут + рандомно от 0 до 4 минут (человек ведь никогда не ставит лот секунда в секунду).                                                                                                                                                       
    my $l_time=11+int(rand(4));                                                                                                                                                                                                                                                
    my $count=0;                                                                                                                                                                                                                                                               
    # По прошествии каждой минуты пошевеливаем курсором мыши чтобы система не уснула.                                                                                                                                                                                          
    for($count=0;$count<$l_time;$count++){                                                                                                                                                                                                                                     
        sleep 60;                                                                                                                                                                                                                                                              
        mousemove(400,500);                                                                                                                                                                                                                                                    
        sleep 1;                                                                                                                                                                                                                                                               
        mousemove(1400,500);
        sleep 1;
    }
    # Ждем еще рандомное количество секунд от 0 до 60 
    sleep(int(rand(60)));
    # Цикл возвращается в начало и выставляется новый лот
}



sub usleep{
    # Функция дает задержку в млилисекундах (Не работает в Ms Windows).
    my $l_ptr=shift;
    $l_ptr*=1000;
    `usleep $l_ptr`;
}


sub move{
    # Функция принимает текущий элемент и перемещает курсор мыши по заданным в элементе координатам.
    my $l_coord=shift;
    mousemove($l_coord->{x},$l_coord->{y});
    mousedown(1);
    usleep(600);
    mousemove($l_coord->{dx},$l_coord->{dy});
    mouseup(1);
}

sub click{
    # Функция производит клик ЛКМ в текущей позиции курсора.
    mousedown(1);
    mouseup(1);
}

sub clicktoxy{
    # Функция принимает текущий элемент, перемещает курсор мыши по заданным в элементе координатам
    # и производит клик ЛКМ.
    my $l_coord=shift;
    mousemove($l_coord->{x},$l_coord->{y});
    mousedown(1);
    mouseup(1);
    usleep(300);
}

sub mousedown{
    # Функция отдает команду утилите xdotool имитировать нажатие кнопки мыши.
    # Принимает значения:
    # 1 - левая кнопка мыши (ЛКМ)
    # 2 - правая кнопка мыши (ПКМ)
    my $l_key = shift;
    if( $l_key ){
        `xdotool mousedown $l_key`;
    }
}

sub mouseup{
    # Функция отдает команду утилите xdotool имитировать отжатие кнопки мыши.
    # Принимает значения:
    # 1 - левая кнопка мыши (ЛКМ)
    # 2 - правая кнопка мыши (ПКМ)
    my $l_key = shift;
    if( $l_key ){
        `xdotool mouseup $l_key`;
    }
}


sub mousemove{
    # Функция отдает команду утилите xdotool переместить курсор мыши по координатам x, y.
    my $l_x = shift;
    my $l_y = shift;
    my $l_com='xdotool mousemove';
    $l_com.=" $l_x $l_y";
    `$l_com`;
}

Бот готов.

В завершение

Данный бот очень простой. Он пока еще не имеет множество необходимых возможностей. Вот краткий список идей как сделать бота более совершенным:

  • Научить бота видеть с экрана. Для этого можно использовать ImageMagick.
  • Научить бота настраиваться под разные разрешения экрана и разные браузеры. Поиск границ и кнопок с помощью утилиты compare из состава ImageMagick.
  • Научить бота читать с экрана. Для этого можно использовать ImageMagick + gocr.
  • Научить бота принимать сделки. Но так как в игре практикуется передача обманных сделок — бот прежде чем принять сделку должен ее проверить. Для этого можно использовать ImageMagick + gocr.
  • Научить бота знать заранее все лоты выставленные в торговом интерфейсе. Лоты передаются браузеру в открытом виде как XML. Для перехвата можно использовать perl модуль Net::Pcap. Для поиска в торговом интерфейсе выбранного на этапе перехвата пакетов товара можно использовать ImageMagick + gocr.

Следует учесть

  • Данный бот написан под Linux. Для того чтобы заставить его работать под Ms Windows необходимо использовать вместо xdotool аналог для Ms Windows. Возможно подойдет утилита autoit. Также в Ms Windows необходимо будет установить Perl.
  • Файл с координатами trade.guns.pl рассчитан на конкретный товар (100 пушек меняем на 20 — 34 мяча), на мое разрешение экрана и мой браузер. Для использования бота вам нужно будет вычислить свои координаты кнопок.
  • По установленным в игре правилам за использование даже такого простого бота наказание бан без права восстановления. пункт 6.4.

Автор: lastuniverse

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js