Games-Lacuna-Client

 view release on metacpan or  search on metacpan

lib/Games/Lacuna/Client/Governor.pm  view on Meta::CPAN

package Games::Lacuna::Client::Governor;
{
  $Games::Lacuna::Client::Governor::VERSION = '0.003';
}
use strict;
use warnings;
use English qw(-no_match_vars);
no warnings 'uninitialized'; # Yes, I count on undef to be zero.  Cue admonishments.

use Games::Lacuna::Client::PrettyPrint qw(trace message warning action ptime phours);
use Games::Lacuna::Client::Types;
use List::Util qw(sum max min);
use List::MoreUtils qw(any part uniq);
use Hash::Merge qw(merge);
use JSON qw(to_json from_json);
require YAML::Any;
use Carp;

use Data::Dumper;

sub new {
    my ($self, $client, $config_opt) = @_;

    my $config;
    if (not ref $config_opt) {
        open my $fh, '<', $config_opt or die "Couldn't open $config_opt";
        $config = YAML::Any::Load( do { local $/; <$fh> } );
        close $fh;
    }
    else {
        $config = $config_opt;  # We passed in a literal hashref for config. Right?
    }

    return bless {
        client => $client,
        config => $config,
    },$self;
}

sub run {
    my $self = shift;
    my $client = $self->{client};
    my $config = $self->{config};

    my $data = $client->empire->view_species_stats();
    $self->{status} = $data->{status};
    my $planets        = $self->{status}->{empire}->{planets};
    my $home_planet_id = $self->{status}->{empire}->{home_planet_id};
    $self->{planet_names} = { map { $_ => $planets->{$_} } keys %$planets };

    my $do_keepalive = 1;
    my $start_time = time();

    $self->load_cache();

    do {
        my @priorities;
        if ( $self->{config}->{dry_run} ) {
            message("Starting dry run, actions are not actually taking place...");
        }
        $do_keepalive = 0;
        for my $pid ( keys %$planets ) {
            next if ( time() < $self->{next_action}->{$pid} );
            trace( "Examining " . $planets->{$pid} ) if ( $self->{config}->{verbosity}->{trace} );
            my $colony_config = merge( $config->{colony}->{ $planets->{$pid} } || {}, $config->{colony}->{_default_} );

            ### Fix the merge w/ priority lists.
            ### Normally Hash::Merge just concats two lists.
            ### We want our more specific array to override.
            if( my $colony_priorities = $config->{colony}{$planets->{$pid}}{priorities} ){
                $colony_config->{priorities} = $colony_priorities;
            }

            next if ( not exists $colony_config->{priorities} or $colony_config->{exclude} );
            $self->{current}->{planet_id} = $pid;
            $self->{current}->{config}    = $colony_config;
            push @priorities, @{$colony_config->{priorities} || [] };
            $self->govern();
        }

        ### Do post_$priority actions.
        for my $priority (uniq @priorities) {
            $self->do_post_priority($priority);
        }

        Games::Lacuna::Client::PrettyPrint::ship_report($self->{ship_info},$self->{config}->{ship_info_sort}) if defined $self->{ship_info};
        trace(sprintf("%d RPC calls this run",$self->{client}->{total_calls})) if ($self->{config}->{verbosity}->{trace});
        if ( $self->{config}->{dry_run} ) {
            message("Dry run complete.");
            return;
        }
        my $next_action_in = min( grep { $_ > time } values %{ $self->{next_action} } ) - time;
        if ( defined $next_action_in && ( $next_action_in + time ) < ( $config->{keepalive} + $start_time ) ) {
            if ( $next_action_in <= 0 ) {
                $do_keepalive = 0;
            }
            else {
                my $nat_time = ptime($next_action_in);
                trace("Expecting to govern again in $nat_time or so, sleeping...") if ($self->{config}->{verbosity}->{trace});
                sleep( $next_action_in + 5 );
                $do_keepalive = 1;
            }
        }
    } while ($do_keepalive);

    $self->write_cache();
}

sub govern {
    my $self = shift;
    my ($pid, $cfg) = @{$self->{current}}{qw(planet_id config)};
    my $client = $self->{client};

    my $result  = $self->{client}->body( id => $pid )->get_buildings();
    my $surface_image = $result->{body}->{surface_image};
    $surface_image =~ s/^surface-//g;
    my $details = $result->{buildings};
    my $status  = $result->{status}->{body};
    $self->{status}->{$pid} = $status;

    Games::Lacuna::Client::PrettyPrint::show_bar('*');
    message("Governing ".$status->{name}) if ($self->{config}->{verbosity}->{message});
    Games::Lacuna::Client::PrettyPrint::show_status($status) if ($self->{config}->{verbosity}->{summary});
    Games::Lacuna::Client::PrettyPrint::surface($surface_image,$details) if ($self->{config}->{verbosity}->{surface_map});
    $self->{cache}->{body}->{$pid} = $details;
    for my $bid (keys %{$self->{cache}->{body}->{$pid}}) {
        $self->{cache}->{body}->{$pid}->{$bid}->{pretty_type} =
            Games::Lacuna::Client::Buildings::type_from_url( $self->{cache}->{body}->{$pid}->{$bid}->{url} );
    }

    if ($self->{config}->{verbosity}->{production}) {
        my @buildings = map { $self->building_details($pid,$_) } keys %$details;
        # We need to get the details from view_platform at the mining ministry, if it exists, and add it into the details.
        for (@buildings) {
            if ($_->{name} eq 'Mining Ministry') {
                my $platforms = $client->building( id => $_->{id}, type => 'MiningMinistry' )->view_platforms->{platforms};
                $_->{ore_hour} += sum( map { my $p=$_; sum(map { $p->{$_ } } grep {/_hour$/} keys %$p) } @$platforms);
            }
        }
        Games::Lacuna::Client::PrettyPrint::production_report(@buildings);
    }


    $status->{happiness_capacity} = $cfg->{resource_profile}->{happiness}->{storage_target} || 1;

    for my $res (qw(food ore water energy happiness waste)) {
        my ( $amount, $capacity, $rate ) = @{$status}{
            $res eq 'happiness' ? 'happiness' : "$res\_stored",
            "$res\_capacity",
            "$res\_hour"
        };
        $rate += 0.00001;
        my $remaining            = $capacity - $amount;
        $status->{full}->{$res}  = $remaining / $rate;
        $status->{empty}->{$res} = $amount / ( -1 * $rate );
    }

    $self->{current}->{status} = $status;

    # Check the size of the build queue
    my $max_queue = 1;
    my ($dev_ministry) = $self->find_buildings('Development');
    if ($dev_ministry) {
        $max_queue = $self->building_details($pid,$dev_ministry->{building_id})->{level} + 1;

lib/Games/Lacuna/Client/Governor.pm  view on Meta::CPAN


This module implements a rudimentary configurable automaton for maintaining your colonies.
Currently, this means automation of upgrade and recycling tasks, but more is planned.
The intent is that the automation should be highly configurable, which of course has a cost
of a complex configuration file.

This script makes an effort to do its own crude caching of building data in order to minimize
the number of RPC calls per invocation.  In order to build its cache on first run, this script
will call ->view() on every building in your empire.  This is expensive.  However, after the
first run, you can expect the script to run between 1-5 calls per colony.  In my tests the
script currently makes about 10-20 calls per invocation for an empire with 4 colonies.
Running on an hourly cron job, this is acceptable for me.

The building data for any particular building does get refreshed from the server if the
script thinks it looks fishy, for example, if it doesn't have any data for it, or if
the building's level has changed from what is in the cache.

This module has absolutely no tests associated with it.  Use at your own risk.  I'm only
trying to be helpful.  Be kind, please rewind.  Etc. Etc.


=head1 DEPENDENCIES

I depend on Hash::Merge and List::MoreUtils to make the magic happen.  Please provide them.
I also depend on Games::Lacuna::Client (of course), and Games::Lacuna::Client::PrettyPrint,
which was published to this distribution at the same time as me.

=head1 Methods

=head2 new

Takes exactly 2 arguments, the client object built by Games::Lacuna::Client->new, and a
path to a YAML configuration file, described in the L<CONFIGURATION FILE> section below.

=head2 run

Runs the governor script according to configuration.  Note: old behavior which permitted
an argument to force a scan of all buildings has been removed as superfluous and wasteful.

=head1 CONFIGURATION FILE

It's a multi-level data structure.  See F<examples/governor.yml>.

=head2 cache_dir

This is a directory which must be writeable to you.  I will write my
building cache data here.

=head2 cache_duration

This is the maximum permitted age of the cache file, in seconds, before
a refresh is required.  Note the age of the cache file is updated with
each run, so this value may be set high enough that a refresh is never
forced.  Refreshes are pulled on a per-building basis.

=head2 dry_run

If this is true, Governor goes through the motions but does not actually
trigger any actions (such as upgrades, recycling jobs, or pushes).  The
output shows the actions as they would have taken place.  Enabling dry_run
disables keepalive behavior.

=head2 keepalive

This is the window of time, in seconds, to try to keep the governor alive
if more actions are possible.  Basically, if any governed colony's build
queue will be empty before the keepalive window expires, the script will
not terminate, but will instead sleep and wait for that build queue to empty
before once again governing that colony.  Setting this to 0 will
effectively disable this behavior.

=head2 push_max_travel_time

This is the maximum time, in hours, that a push should take (one-way) to
be considered a valid candidate.  This can be used to prevent pushes between
very distant colonies.  If not defined, there is no restriction.

=head2 push_minimum_load

This is a proportion, i.e. 0.5 for 50%.  It indicates the minimum amount
of used cargo space to require before a ship will be sent on a push.
E.g., if set to 0.25, a ship must be at least 25% full of its maximum
cargo capacity or it will not be considered eligible for a push.

=head2 push_ships_named

If defined, ship names must match this substring (case-insensitive) to
be eligible to be used for pushes.  This is an easy to to tell the governor
which ships it can utilize.

=head2 verbosity

Not all of the 'verbosity' keys are currently implemented.  If any are
true, messages of that type are output to standard output.

=head3 action

Messages notifying you that some action has taken place.

=head3 construction

Outputs a construction report for each colony (not yet implemented)

=head3 message

Messages which are informational in nature.  One level above trace.

=head3 production

Outputs a production report for each colony (not yet implemented)

=head3 pushes

Outputs a colony resource push analysis (not yet implemented)

=head3 storage

Outputs a storage report for each colony (not yet implemented)

=head3 summary

Outputs a resource summary for each colony

=head3 surface_map

Too much time on my hands.  Outputs an ASCII version of the planet surface map.



( run in 1.775 second using v1.01-cache-2.11-cpan-0d23b851a93 )