Writing a bot for Grepolis

    imageGood afternoon. In this article, I will describe the writing of a bot for the online mmo strategy game Grepolis. Please note that the use of such programs is prohibited by the rules of the game, it is banned for this, and not without reason. I just have a hobby of writing bots for games. And writing is not prohibited. Who cares about the logic and implementation, please, under cat.

    As always, at the beginning is a link to the source code .

    I like Grepolis, a good game. But in order to survive there, you need to collect tribute from the villages every 5 minutes. And I’m busy all day doing the main work, so the main goal of writing the bot was to collect tribute. Then auto-improvement of villages was added (then they give more profit), auto-construction (so that at night, when I sleep, the line of construction would not be empty). As I understand it, these functions bring the bulk of the game’s administration revenue. Probably because players who use bots are banned so often.

    The bot will not play by itself; only auxiliary functions are assigned to it. And in general, it is better not to throw it alone for a long time - they can ban it.

    To be honest, this is the second version of the bot, as well as the first one written in Perl. The first version of the crown once every five minutes collected resources, was 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, re-collected all the information. I did not know how to determine the list of farms and cities and in general was not labor.

    So the second version was born. Now to work you just need to specify sid (taken from cookies through dev-tools, for example) and the server on which you play. A list of cities / farms is built automatically. Although I have not tried it on two cities yet, I have only one city, but keep an eye on 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 unnecessary requests.

    The game itself has also been updated, if earlier the client was sent basically just html code, now separate objects have appeared, although sending another json object to the json field of the object is still a move. For instance:
    {
      '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":"Perl<\\/a>","same_island":true,"current_town_link":"Perl<\\/a>","current_player_link":"Pingvein<\\/a>"}}',
      'time' => 1383837485
    }
    

    I created the file “install_libraries.sh” for those with debian / ubuntu to resolve all the dependencies. Others are encouraged to use cpan or the repositories of their distribution. All of my code is single-threaded, because it will be strange if the bot sends 2 requests at the same time. And IO :: Async :: Loop manages this thread. 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 triggers will simply be added to the main loop. I have everything on the timer, but the code that initializes the application is given in the "run" method. Please note that I try to calculate the timer time based on the rand function, so as not to falter. 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 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.

    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;
    

    When initializing, it reads the resources it has, searches for farms from which it may require tribute, and the volume of the warehouse. Also pay attention to the "build_something" procedure. 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'} =~ /

    You\sreceive:\s\d+\sresources<\/h4>
    • (\d+)\swood<\/li>
    • \d+\srock<\/li>
    • \d+\ssilver\scoins<\/li><\/ul>/; $self->{'resources_by_request'} = $1; if($resp->{'json'}->{'html'} =~ /

      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, upon initialization, reads its level and the amount of resources allocated every 5 minutes. To save on queries, I check if the city really needs these resources, if not, check if the current settlement can be improved so that it gives more resources at a time. After the resources are requested from the settlement, I check the notifications, and based on them I set the values ​​of the resources to the city so as not to send separate requests for this. After each improvement, information about the settlement is updated. I’ll write about one fragment from the module that 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 for the introduction of captcha. In fact, I plan to read requests before the need to enter captcha to limit the activity of the bot. Another “night mode” is 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, then requests will not be sent anyway.

    In the game, the first city is universal, but then you need to divide it into cities, which are a marine attacking army, a marine defense and a land attacking / defense. Depending on the type of city, it carries out the construction of various buildings to save the free population, and different scientific policies. I will be glad to see comments on whether it is worth implementing auto-construction of armies, buildings, auto-research depending on the city, or leaving the bot as a simple auto-collector. I also wonder if the scheduled sending function will be useful.

    I will be happy to answer in the comments about the features of the Grepolis server that I managed to find.

    Also popular now: