Как всегда, в начале ссылка на исходный код.
Мне нравится 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/
Добавить комментарий