📜 ⬆️ ⬇️

Writing a bot for Grepolis

image Good day. In this article I will describe the writing of a bot for the online mmo strategy of the game Grepolis. Note that the rules of the game use of such programs is prohibited, for it is banned, and not without reason. I just have a hobby to write bots for games. And writing is not prohibited. To whom the logic and implementation are interesting, I ask under kat.

As always, at the beginning of the link to the source code .

I like Grepolis, a good game. But in order to survive there, you need to collect tribute from villages every 5 minutes. And I am busy all day at the main job, so the main purpose of writing a bot was to collect tribute. Then the auto-improvement of villages was added (then more profit is given), autobuilding (so that at night, when I sleep, the construction queue is not empty). As I understand it, these functions bring the bulk of the revenue of the administration of the game. Probably because the players that use bots, so often banyat.

By itself, the bot will not play, it is assigned only auxiliary functions. And in general, it is better not to leave him alone for a long time - they can be banned.
')
Honestly speaking, this is the second version of the bot, like the first one written in Perl. The first version of the crown once every five minutes to collect resources, built and completed. And it worked well, I got into the top players, everything was fine. But then the game got tired and I scored, when after a while I started playing again - I was banned. Apparently, they added bot detection features. So you need to change the approach. The fact is that the old version did not know anything about what was before it, and it re-collected all the information. She was not able to determine the list of farms and cities and in general was not hard.

So the second version was born. Now all you need to do is just to specify sid (taken from cookies through dev-tools, for example) and the server you are playing on. Build a list of cities / farms automatically. Although I have not tried two cities yet, I still have only one city, but watch out for the github repository - fixes will be released immediately. The peculiarity of the new version is that it remembers as much data as possible and tries to send less extra requests.

The game itself was also updated, if previously the client was sent basically just html code, now separate objects have appeared, although sending another json object's object to the json field as a line is another move. For example:
{ '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 } 

I created the install_libraries.sh file for those who have debian / ubuntu to resolve all dependencies. Others are encouraged to use cpan or the repositories of their distribution. All my code is single-threaded, because it will be strange if the bot simultaneously sends 2 requests. And the flow is managed by “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; 

In this module, event initiators are simply added to the main loop. I have everything on a timer, but the code that initializes the application is given in the "run" method. Please note that I try to calculate the time of the timer on the basis of the rand function, so as not to burn. The main file is 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)); }; 

We read the config, set the csrfToken for subsequent requests, and the current city. Support for several cities will appear as soon as I capture a new city. I promise to do it as quickly as I can.

The module for the city, 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; 

During initialization, he reads out the resources he has, searches for farms from which tribute can demand, and the volume of the warehouse. Also pay attention to the procedure "build_something". I didn’t really think about any special construction strategy, so you can change the priority of construction as you see fit. Module for "farms" (the so-called peasant settlements) 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"; $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 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; 

The farm, when initialized, reads its level and the amount of resources given every 5 minutes. To save on requests, I check if the city really needs these resources, if not, I check if the current settlement can be improved so that it gives more resources at a time. After resources are requested from the settlement, I check the notifications, and on the basis of them I set the resource values ​​to the city in order not to send individual requests for this. After each improvement, information about the settlement is updated. I will also write about one fragment from the module, which is responsible for sending requests to the server, 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; } } } } 

I check the notifications to highlight one that interests me. Namely, a request to introduce a captcha. Actually, I plan to read requests before the need arises to enter a captcha to limit bot activity. A "night mode" is also planned - so that the bot does not send requests at night. Although, if the warehouses are full and the construction line is full of long tasks, requests will not be sent anyway.

In the game, the first city is universal, but then it is necessary to divide into cities that there are a naval attacking army, a naval defensive and a land attacking / defensive. Depending on the type of city, it is used to build various buildings, to save free population, and different scientific policies. I will be glad to see the comments, whether it is worthwhile to implement the autobuilding of armies, buildings, auto research depending on the city or leave the bot as a simple auto collector. I also wonder if the schedule sending function will be useful.

I am pleased to answer in the comments about the features of the Grepolis server that I was able to detect.

Source: https://habr.com/ru/post/201272/


All Articles