Добрый день. В этой статье я опишу написание бота для онлайн 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 startedn");
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 addedn";
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. Waitingn";
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 startedn";
$self->$get_town_data;
GrepolisBotModules::Log::echo 0, "Town ".$self->{'id'}." data gettings finishedn";
$self->$build_something;
GrepolisBotModules::Log::echo 0, "Town ".$self->{'id'}." build startedn";
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>Yousreceive:sd+sresources</h4><ul><li>(d+)swood</li><li>d+srock</li><li>d+ssilverscoins</li></ul>/;
$self->{'resources_by_request'} = $1;
if($resp->{'json'}->{'html'} =~ /<h4>Upgradeslevels((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";
$self->$get_farm_data;
};
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 finishedn";
};
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 startedn";
bless $self, $class;
$self->$get_farm_data;
GrepolisBotModules::Log::echo 0, "Farm ".$self->{'id'}." data gettings finishedn";
$self->$tick;
GrepolisBotModules::Log::echo 0, "Farm ".$self->{'id'}." ticker startedn";
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, которые мне удалось обнаружить.
Автор: peinguin
нужна полная автоматизация )))
нужна полная автоматизация влоть до того чтобы войска сами воевали и тп.