Написание бота для Grepolis

от автора

image Добрый день. В этой статье я опишу написание бота для онлайн mmo strategy игры Grepolis. Учтите, что правилами игры использование подобных програм запрещено, за это банят, и не безпричинно. Просто у меня хобби писать боты для игр. А писать не запрещено. Кому интересны логика и реализация, прошу под кат.

Как всегда, в начале ссылка на исходный код.

Мне нравится Grepolis, хорошая игра. Но чтобы там выжить, нужно каждые 5 минут собирать дань с деревень. А я весь день занят на основной работе, поэтому главной целью написания бота было как-раз собирать дань. Потом добавилось автоулучшение деревень(тогда больше прибыли дают), автопостройка(чтобы ночью, когда я сплю, очередь постройки не пустела). Насколько я понял, эти функции приносят основную часть дохода администрации игры. Наверное потому игроков, что используют ботов, так часто банят.

Сам по себе бот играть не будет, на него возложены только вспомогательные функции. И вообще, лучше не бросайте его одного на длительное время — могут забанить.

Чесно говоря это уже вторая по счету версия бота, как и первая написана на Perl. Первая версия по крону раз в пять минут собирала ресурсы, строилась и завершалась. И это неплохо работало, я попал в топ игроков, все было хорошо. Но потом игра надоела и я забил, когда через время снова начал играть — меня забанили. Видимо, добавили функций обнаружения ботов. Значит нужно менять подход. Дело в том, что старая версия не знала ничего о том, что было до нее, заново собирала всю информацию. Не умела определять список ферм и городов и вообще была не тру.

Так родилась вторая версия. Теперь для работы нужно всего-лишь указать sid(берется из кукисов через dev-tools, например) и сервер, на котором играете. Строится список городов/ферм автоматически. Хотя на двух городах еще не пробовал, у меня пока только один город, но следите за github репозиторием — фиксы будут выходить незамедлительно. Особенность новой версии в том, что она запоминает как можно больше данных и старается слать поменьше лишних запросов.

Сама игра тоже обновилась, если раньше клиенту посылался в основном просто html код, то теперь появились отдельные объекты, хотя присылать в поле json объекта другой json об’ект как строку тот еще ход. Например:

{   'type' => 'backbone',   'param_id' => 13980,   'subject' => 'Units',   'id' => 4414096,   'param_str' => '{"Units":{"id":13980,"home_town_id":5391,"current_town_id":5391,"sword":23,"slinger":21,"archer":5,"hoplite":10,"rider":0,"chariot":0,"catapult":0,"minotaur":0,"manticore":0,"zyklop":0,"harpy":0,"medusa":0,"centaur":0,"pegasus":0,"cerberus":0,"fury":0,"griffin":0,"calydonian_boar":0,"godsent":34,"big_transporter":0,"bireme":0,"attack_ship":0,"demolition_ship":0,"small_transporter":0,"trireme":0,"colonize_ship":0,"sea_monster":0,"militia":0,"heroes":null,"home_town_link":"<a href=\\"#eyJpZCI6NTM5MSwiaXgiOjUxMSwiaXkiOjYyMywidHAiOiJ0b3duIiwibmFtZSI6IlBlcmwifQ==\\" class=\\"gp_town_link\\">Perl<\\/a>","same_island":true,"current_town_link":"<a href=\\"#eyJpZCI6NTM5MSwiaXgiOjUxMSwiaXkiOjYyMywidHAiOiJ0b3duIiwibmFtZSI6IlBlcmwifQ==\\" class=\\"gp_town_link\\">Perl<\\/a>","current_player_link":"<a  href=\\"#eyJuYW1lIjoiUGluZ3ZlaW4iLCJpZCI6e319\\" class=\\"gp_player_link\\">Pingvein<\\/a>"}}',   'time' => 1383837485 } 

Я создал файл «install_libraries.sh» для тех у кого debian/ubuntu, чтобы разрешить все зависимости. Другим же предлагается использовать cpan или репозитории своего дистрибутива. Весь код у меня однопоточный, потому что странно будет, если бот одновременно пошлет 2 запроса. И потоком этим заведует «IO::Async::Loop». Async.pm:

package GrepolisBotModules::Async;  use GrepolisBotModules::Log;  use IO::Async::Timer::Countdown; use IO::Async::Loop;  my $loop = IO::Async::Loop->new;  sub delay{ 	my($delay, $callback) = @_;  	GrepolisBotModules::Log::echo 1, "Start delay $delay \n";  	my $timer = IO::Async::Timer::Countdown->new( 		delay => $delay, 		on_expire => $callback, 	); 	  	$timer->start; 	$loop->add( $timer ); }  sub run{ 	$loop->later(shift); 	$loop->run; }  1; 

В этом модуле просто добавятся инициаторы событий в главный цикл. У меня все по таймеру, но код, что инициализирует приложение додается в методе «run». Обратите внимание, что я стараюсь время таймера высчитывать на основании функции rand, чтобы не палится. Главный файл — grepolis_bot.pl:

#!/usr/bin/perl  use strict; use warnings;  use Config::IniFiles;  use GrepolisBotModules::Request; use GrepolisBotModules::Town; use GrepolisBotModules::Async; use GrepolisBotModules::Log;  use utf8;  my $cfg = Config::IniFiles->new( -file => "config.ini" ); my $config = {     security => {         sid    => $cfg->val( 'security', 'sid' ),         server => $cfg->val( 'security', 'server' )     },     global => {         log    => $cfg->val( 'global', 'log' ),     } }; undef $cfg;  my $Towns = [];  GrepolisBotModules::Async::run sub{      GrepolisBotModules::Request::init($config->{'security'});     GrepolisBotModules::Log::init($config->{'global'});      GrepolisBotModules::Log::echo(0, "Program started\n");      my $game = GrepolisBotModules::Request::base_request('http://'.$config->{'security'}->{'server'}.'.grepolis.com/game');      $game =~ /"csrfToken":"([^"]+)",/;     GrepolisBotModules::Request::setH($1);     $game =~ /"townId":(\d+),/;     GrepolisBotModules::Log::echo 1, "Town $1 added\n";     push($Towns, new GrepolisBotModules::Town($1)); }; 

Считываем конфиг, устанавливаем csrfToken для последующих запросов, и текущий город. Поддержка нескольких городов появится как только я захвачу новый город. Обещаю сделать это так быстро как только смогу.

Модуль для города, Town.pm:

package GrepolisBotModules::Town;  use strict; use warnings;  use GrepolisBotModules::Request; use GrepolisBotModules::Farm; use GrepolisBotModules::Log;  use JSON;  my $get_town_data = sub {     my( $self ) = @_;      my $resp = JSON->new->allow_nonref->decode(         GrepolisBotModules::Request::request(                 'town_info',                 'go_to_town',                 $self->{'id'},                 undef,                 0             )         );      $self->{'max_storage'} = $resp->{'json'}->{'max_storage'};      $resp = JSON->new->allow_nonref->decode(         GrepolisBotModules::Request::request(                 'data',                 'get',                 $self->{'id'},                 '{"types":[{"type":"backbone"},{"type":"map","param":{"x":0,"y":0}}]}',                 1             )         );      foreach my $arg (@{$resp->{'json'}->{'backbone'}->{'collections'}}) {         if(             defined $arg->{'model_class_name'} &&             $arg->{'model_class_name'} eq 'Town'         ){             my $town = pop($arg->{'data'});             $self->setResources($town->{'last_iron'}, $town->{'last_stone'}, $town->{'last_wood'});         }     }      foreach my $data (@{$resp->{'json'}->{'map'}->{'data'}->{'data'}->{'data'}} ) {         foreach my $key (keys %{$data->{'towns'}}) {             if(                 defined $data->{'towns'}->{$key}->{'relation_status'} &&                 $data->{'towns'}->{$key}->{'relation_status'} == 1             ){                 my $village = new GrepolisBotModules::Farm($data->{'towns'}->{$key}->{'id'}, $self);                 push($self->{'villages'}, $village);             }         }     } };  my $build_something;  $build_something = sub {     my $self = shift;      GrepolisBotModules::Log::echo 0, "Build request ".$self->{'id'}."\n";     my $response_body = GrepolisBotModules::Request::request('building_main', 'index', $self->{'id'}, '{"town_id":"'.$self->{'id'}.'"}', 0);      $response_body =~ m/({.*})/;      my %hash = ( JSON->new->allow_nonref->decode( $1 )->{'json'}->{'html'} =~ /BuildingMain.buildBuilding\('([^']+)',\s(\d+)\)/g );     my $to_build = '';          if(defined $hash{'main'} && $hash{'main'}<25){         $to_build = 'main';     }elsif(defined $hash{'academy'}){         $to_build = 'academy';     }elsif(defined $hash{'farm'}){         $to_build = 'farm';     }elsif(defined $hash{'barracks'}){         $to_build = 'barracks';     }elsif(defined $hash{'storage'}){         $to_build = 'storage';     }elsif(defined $hash{'docks'}){         $to_build = 'docks';     }elsif(defined $hash{'stoner'}){         $to_build = 'stoner';     }elsif(defined $hash{'lumber'}){         $to_build = 'lumber';     }elsif(defined $hash{'ironer'}){         $to_build = 'ironer';     }     if($to_build ne ''){         my $response_body = GrepolisBotModules::Request::request(             'building_main',             'build',             $self->{'id'},             '{"building":"'.$to_build.'","level":5,"wnd_main":{"typeinforefid":0,"type":9},"wnd_index":0,"town_id":"'.$self->{'id'}.'"}',             1         );     }      my $time_wait = undef;      my $json = JSON->new->allow_nonref->decode($response_body);     if(defined $json->{'notifications'}){         foreach my $arg (@{$json->{'notifications'}}) {             if(                 $arg->{'type'} eq 'backbone' &&                 $arg->{'subject'} eq 'BuildingOrder'             ){                 my $order = JSON->new->allow_nonref->decode($arg->{'param_str'})->{'BuildingOrder'};                 $time_wait = $order->{'to_be_completed_at'} - $order->{'created_at'};             }         }     }      if(defined $time_wait){         GrepolisBotModules::Log::echo 0, "Town ".$self->{'id'}." build ".$to_build."\n";         GrepolisBotModules::Async::delay( $time_wait + int(rand(60)), sub {$self->$build_something} );     }else{         GrepolisBotModules::Log::echo 0, "Town ".$self->{'id'}." can not build. Waiting\n";         GrepolisBotModules::Async::delay( 600 + int(rand(300)), sub {$self->$build_something} );     } };  sub setResources{     my $self = shift;     my $iron = shift;     my $stone = shift;     my $wood = shift;      $self->{'iron'} = $iron;     $self->{'wood'} = $wood;     $self->{'stone'} = $stone;      GrepolisBotModules::Log::echo 1, "Town ".$self->{'id'}." resources updates iron-".$self->{'iron'}.", stone-".$self->{'stone'}.", wood-".$self->{'wood'}."\n"; }  sub needResources{     my $self = shift;     my $resources_by_request = shift;      if(         $self->{'iron'} + $resources_by_request < $self->{'max_storage'} ||         $self->{'wood'} + $resources_by_request < $self->{'max_storage'} ||         $self->{'stone'} + $resources_by_request < $self->{'max_storage'}     ){         return 1;     }     return 0; }  sub toUpgradeResources{     my $self = shift;      return {         wood => int($self->{'iron'}/5),         stone => int($self->{'wood'}/5),         iron => int($self->{'stone'}/5),     }; }  sub getId{     my $self = shift;     return $self->{'id'}; }  sub new {     my $class = shift;     my $self = {         id => shift,         villages => [],         max_storage => undef,         iron => undef,         wood => undef,         stone => undef      };      bless $self, $class;          GrepolisBotModules::Log::echo 0, "Town ".$self->{'id'}." init started\n";      $self->$get_town_data;     GrepolisBotModules::Log::echo 0, "Town ".$self->{'id'}." data gettings finished\n";     $self->$build_something;     GrepolisBotModules::Log::echo 0, "Town ".$self->{'id'}." build started\n";      return $self; }  1;  

Он при инициализации считывает ресурсы, которыми располагает, ищет фермы, с которых может требовать дань, и объем склада. Так же обратите внимание на процедуру «build_something». Я особо не задумывался над какой-то особой стратегией постройки, поэтому можете изменить приоритет строительства так, как посчитаете нужным. Модуль для «ферм» (так называемых крестьянских поселений) Farm.pm:

package GrepolisBotModules::Farm;  use GrepolisBotModules::Request; use GrepolisBotModules::Log;  use JSON;  my $get_farm_data = sub { 	 	my $self = shift;      my $resp = JSON->new->allow_nonref->decode(         GrepolisBotModules::Request::request(                 'farm_town_info',                 'claim_info',                 $self->{'town'}->getId,                 '{"id":"'.$self->{'id'}.'"}',                 0             )         );      $self->{'name'} = $resp->{'json'}->{'json'}->{'farm_town_name'};     $resp->{'json'}->{'html'} =~ /<h4>You\sreceive:\s\d+\sresources<\/h4><ul><li>(\d+)\swood<\/li><li>\d+\srock<\/li><li>\d+\ssilver\scoins<\/li><\/ul>/;     $self->{'resources_by_request'} = $1;     if($resp->{'json'}->{'html'} =~ /<h4>Upgrade\slevel\s\((\d)\/6\)<\/h4>/ ){         $self->{'level'} = $1;     }else{         die('Level not found');     } };  my $upgrade = sub{ 	my $self = shift;  	my $donate = $self->{'town'}->toUpgradeResources();      $json = '{"target_id":'.$self->{'id'}.',"wood":'.$donate->{'wood'}.',"stone":'.$donate->{'stone'}.',"iron":'.$donate->{'iron'}.',"town_id":"'.$self->{'town'}->getId().'"}';     my $response_body = GrepolisBotModules::Request::request('farm_town_info', 'send_resources', $self->{'town'}->getId(), $json, 1);     GrepolisBotModules::Log::echo 1, "Village send request. Town ID ".$self->{'town'}->getId()." Village ID ".$self->{'id'}."\n"; }; my $claim = sub{ 	my $self = shift; 	$json = '{"target_id":"'.$self->{'id'}.'","claim_type":"normal","time":300,"town_id":"'.$self->{'town'}->getId.'"}';     my $response_body = GrepolisBotModules::Request::request('farm_town_info', 'claim_load', $self->{'town'}->getId, $json, 1);      my $json = JSON->new->allow_nonref->decode($response_body)->{'json'};     if(defined $json->{'notifications'}){         foreach my $arg (@{$json->{'notifications'}}) {             if(                 $arg->{'type'} eq 'backbone' &&                 $arg->{'subject'} eq 'Town'             ){                 my $town = JSON->new->allow_nonref->decode($arg->{'param_str'})->{'Town'};                 $self->{'town'}->setResources($town->{'last_iron'}, $town->{'last_stone'}, $town->{'last_wood'});             }         }     }      GrepolisBotModules::Log::echo 1, "Farm ".$self->{'id'}." claim finished\n"; };  my $needUpgrade = sub { 	my $self = shift; 	if($self->{'level'} < 6){ 		return true; 	}else{ 		return false; 	} };  my $tick; $tick = sub {  	my $self = shift;  	if($self->{'town'}->needResources($self->{'resources_by_request'})){ 		$self->$claim(); 	    GrepolisBotModules::Async::delay( 360 + int(rand(240)), sub { $self->$tick} ); 	}elsif($self->$needUpgrade()){ 		$self->$upgrade(); 		GrepolisBotModules::Async::delay( 600 + int(rand(240)), sub { $self->$tick} );     } };  sub new {     my $class = shift;      my $self = {         id => shift,         name => undef,         resources_by_request => undef,         town => shift,         level => undef     };     GrepolisBotModules::Log::echo 0, "Farm ".$self->{'id'}." init started\n";     bless $self, $class;          $self->$get_farm_data;     GrepolisBotModules::Log::echo 0, "Farm ".$self->{'id'}." data gettings finished\n";     $self->$tick;     GrepolisBotModules::Log::echo 0, "Farm ".$self->{'id'}." ticker started\n";      return $self; }  1; 

Ферма, при инициализации считывает свой уровень и количество ресурсов, отдаваемых каждые 5 минут. Чтобы сэкономить на запросах, я проверяю, действительно нужны ли городу эти ресурсы, если нет, проверяю, можно ли улучшить текущее поселение, чтобы оно давало больше ресурсов за раз. После того, как с поселения затребованы ресурсы, я проверяю уведомления, и их на основании задаю городу значения ресурсов, чтобы не посылать для этого отдельных запросов. Еще напишу про один фрагмент из модуля, что отвечает за посылку запросов на сервер, Request.pm:

if($response_body =~ /^{/){     my $json = JSON->new->allow_nonref->decode( $response_body )->{'json'};     if(defined $json->{'notifications'}){         foreach my $arg (@{$json->{'notifications'}}) {             if(                 (                     $arg->{'type'} ne 'building_finished' &&                     $arg->{'type'} ne 'newreport' &&                     (                         $arg->{'type'} ne 'backbone' ||                         $arg->{'type'} eq 'backbone' &&                          (                             !(defined $arg->{'subject'}) ||                             (                                 $arg->{'subject'} ne 'BuildingOrder' &&                                 $arg->{'subject'} ne 'Town' &&                                 $arg->{'subject'} ne 'PlayerRanking' &&                                 $arg->{'subject'} ne 'Buildings' &&                                 $arg->{'subject'} ne 'IslandQuest' &&                                 $arg->{'subject'} ne 'TutorialQuest'                             )                         )                     )                 )             ){                 GrepolisBotModules::Log::dump 5, $arg;             }         }     } } 

Я проверяю нотификации, чтобы выделить одну, интересующую меня. А именно запрос на введение капчи. Вообще-то я планирую считать запросы до возникновения необходимости вводить капчу, чтобы ограничивать активность бота. Еще планируется «ночной режим» — чтобы бот не отсылал в ночное время запросы. Хотя, если склады будут полны а очередь строительства полна долгими заданиями, то запросы и так слаться не будут.

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

С удовольствием отвечу в комментариях о особенностях сервера Grepolis, которые мне удалось обнаружить.

ссылка на оригинал статьи http://habrahabr.ru/post/201272/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *