#!/usr/bin/perl
use strict;
use warnings;
use FindBin;
use lib "$FindBin::Bin/../lib";
use List::Util (qw(first sum));
use Games::Lacuna::Client ();
use Games::Lacuna::Client::Types qw(:resource);
use Getopt::Long (qw(GetOptions));
use YAML::Any (qw(LoadFile Dump));
use POSIX qw( floor );
my $cfg_file;
if ( @ARGV && $ARGV[0] !~ /^--/) {
$cfg_file = shift @ARGV;
}
else {
$cfg_file = 'lacuna.yml';
}
unless ( $cfg_file and -e $cfg_file ) {
$cfg_file = eval{
require File::HomeDir;
require File::Spec;
my $dist = File::HomeDir->my_dist_config('Games-Lacuna-Client');
File::Spec->catfile(
$dist,
'login.yml'
) if $dist;
};
unless ( $cfg_file and -e $cfg_file ) {
die "Did not provide a config file";
}
}
my $from;
my $to;
my $ship_type;
my $ship_name;
my $fill_ratio = 0.5;
my $min_level = 100_000;
my $max_ships;
my $verbose;
my $dryrun;
my $debug;
GetOptions(
'from=s' => \$from,
'to=s' => \$to,
'ship_type|ship-type=s' => \$ship_type,
'ship_name|ship-type=s' => \$ship_name,
'fill_ratio|fill-ratio=s' => \$fill_ratio,
'min_level|min-level=i' => \$min_level,
'max_ships|max-ships=i' => \$max_ships,
'verbose' => \$verbose,
'dryrun' => \$dryrun,
'debug' => \$debug,
);
usage() if !$from || !$to;
my @foods = food_types;
my @ores = ore_types;
my $client = Games::Lacuna::Client->new(
cfg_file => $cfg_file,
#debug => 1,
);
my $empire = $client->empire->get_status->{empire};
my $planets = $empire->{planets};
# reverse hash, to key by name instead of id
my %planets_by_name = map { lc( $planets->{$_} ), $_ } keys %$planets;
my $to_id = $planets_by_name{ lc $to }
or die "to planet not found";
# Load planet data
my $body = $client->body( id => $planets_by_name{ lc $from } );
my $result = $body->get_buildings;
my $buildings = $result->{buildings};
# Find the TradeMin
my $trade_min_id = first {
$buildings->{$_}->{name} eq 'Trade Ministry'
} keys %$buildings;
my $trade_min = $client->building( id => $trade_min_id, type => 'Trade' );
my @ships = @{ $trade_min->get_trade_ships($to_id)->{ships} };
if ($ship_type) {
@ships = grep
{
$_->{type} =~ m/$ship_type/i;
}
@ships;
}
if ($ship_name) {
@ships = grep
{
$_->{name} =~ m/$ship_name/i;
}
@ships;
}
if (!@ships) {
warn "no suitable ships found\n";
exit;
}
@ships = sort {
$b->{hold_size} <=> $a->{hold_size}
|| $b->{speed} <=> $a->{speed}
} @ships;
my $resources = $trade_min->get_stored_resources->{resources};
for my $key (@foods, @ores, 'water', 'energy') {
$resources->{$key} ||= 0;
}
my $ship_count = 1;
my $last_hold_size;
for my $ship (@ships) {
if ( $last_hold_size && $last_hold_size <= $ship->{hold_size} ) {
next;
}
elsif ( $last_hold_size ) {
# smaller hold-size, so we'll give it a try
undef $last_hold_size;
}
my @items = trade_items( $ship, $resources );
if (!@items) {
warn "insufficient items to fill ship\n";
$last_hold_size = $ship->{hold_size};
next;
}
my $return;
if ( $dryrun ) {
$return->{ship} = {
name => $ship->{name},
hold_size => $ship->{hold_size},
date_arrives => 'DRY RUN',
};
}
else {
$return = $trade_min->push_items(
$to_id,
\@items,
{
ship_id => $ship->{id},
}
);
}
printf "Pushed from '%s' to '%s' using '%s' size '%d', arriving '%s'\n",
$from,
$to,
$return->{ship}{name},
$return->{ship}{hold_size},
$return->{ship}{date_arrives};
if ($verbose) {
print Dump(\@items);
}
last if $max_ships && $ship_count == $max_ships;
$ship_count++;
}
exit;
sub trade_items {
my ( $ship, $resources ) = @_;
my ( $food, $ore, $water, $energy ) = resource_totals( $resources );
my $total = sum( $food, $ore, $water, $energy );
if ($debug) {
warn <<DEBUG;
Total available to push: $total
DEBUG
}
my $food_percent = $food ? ($food / $total) : 0;
my $ore_percent = $ore ? ($ore / $total) : 0;
my $water_percent = $water ? ($water / $total) : 0;
my $energy_percent = $energy ? ($energy / $total) : 0;
if ($debug) {
my $food = sprintf "%.2f", $food_percent * 100;
my $ore = sprintf "%.2f", $ore_percent * 100;
my $water = sprintf "%.2f", $water_percent * 100;
my $energy = sprintf "%.2f", $energy_percent * 100;
warn <<DEBUG;
Percentages to push:
food: $food\%
ore: $ore\%
water: $water\%
energy: $energy\%
DEBUG
}
my $trade = {};
my $hold = $ship->{hold_size};
my $max_push = $hold > $total ? $total
: $hold;
subtotals( $max_push, $trade, $resources, $food_percent, \@foods );
subtotals( $max_push, $trade, $resources, $ore_percent, \@ores );
subtotals( $max_push, $trade, $resources, $water_percent, ['water'] );
subtotals( $max_push, $trade, $resources, $energy_percent, ['energy'] );
if ($debug) {
my $food = 0;
my $ore = 0;
my $water = $trade->{water};
my $energy = $trade->{energy};
map { $food += $trade->{$_} } @foods;
map { $ore += $trade->{$_} } @ores;
warn <<DEBUG;
Totals after calculating individual resources (foods, ores):
food: $food
ore: $ore
water: $water
energy: $energy
DEBUG
}
# don't go to zero in any resource
for my $type ( @foods, @ores, 'water', 'energy' ) {
next if !$trade->{$type};
if ( ( $resources->{$type} - $trade->{$type} ) == 0 ) {
--$trade->{$type};
}
}
if ($debug) {
my $food = 0;
my $ore = 0;
my $water = $trade->{water};
my $energy = $trade->{energy};
map { $food += $trade->{$_} } @foods;
map { $ore += $trade->{$_} } @ores;
warn <<DEBUG;
Totals ensuring none drop to zero:
food: $food
ore: $ore
water: $water
energy: $energy
DEBUG
}
my $total_trade = sum( values %$trade );
if ($debug) {
warn <<DEBUG;
Total resources to push: $total_trade
Ship hold size: $hold
DEBUG
}
if ( ( $total_trade / $hold ) < $fill_ratio ) {
# ship not full enough
return;
}
# new totals for next ship
map {
$resources->{$_} -= $trade->{$_}
}
@foods, @ores, 'water', 'energy';
if ($debug) {
my $food = 0;
my $ore = 0;
my $water = $resources->{water} || 0;
my $energy = $resources->{energy} || 0;
map { $food += $resources->{$_}||0 } @foods;
map { $ore += $resources->{$_}||0 } @ores;
warn <<DEBUG;
Remaining after push:
food: $food
ore: $ore
water: $water
energy: $energy
DEBUG
}
return map {
+{
type => $_,
quantity => $trade->{$_},
}
}
grep {
$trade->{$_}
}
keys %$trade;
}
sub resource_totals {
my ( $resources ) = @_;
my $food = sum( @{$resources}{ @foods } );
my $ore = sum( @{$resources}{ @ores } );
my $water = $resources->{water};
my $energy = $resources->{energy};
if ($debug) {
warn <<DEBUG;
On planet:
food: $food
ore: $ore
water: $water
energy: $energy
DEBUG
}
$food = ( ($food - $min_level) > 0 ) ? ($food - $min_level) : 0;
$ore = ( ($ore - $min_level) > 0 ) ? ($ore - $min_level) : 0;
$water = ( ($water - $min_level) > 0 ) ? ($water - $min_level) : 0;
$energy = ( ($energy - $min_level) > 0 ) ? ($energy - $min_level) : 0;
if ($debug) {
warn <<DEBUG;
Available above min_level:
food: $food
ore: $ore
water: $water
energy: $energy
DEBUG
}
return $food, $ore, $water, $energy;
}
sub subtotals {
my ( $hold, $trade, $resources, $percent, $types ) = @_;
$hold *= $percent;
my $total_available = sum( @{$resources}{@$types} );
if ( $total_available == 0 ) {
@{$trade}{@$types} = ( 0 x scalar @$types );
}
elsif ( $total_available <= $hold ) {
@{$trade}{@$types} = @{$resources}{@$types};
}
else {
# more available than the ship can carry
my $ratio = $hold / $total_available;
@{$trade}{@$types} = map {
floor( $_ * $ratio )
} @{$resources}{@$types};
}
return;
}
sub usage {
die <<"END_USAGE";
Usage: $0 CONFIG_FILE
--from PLANET_NAME
--to PLANET_NAME
--ship_type SHIP_TYPE
--fill_ratio FILL_RATIO
--min_level MIN_LEVEL
--max_ships MAX_SHIPS
--dryrun
--verbose
Pushes all resources above a configurable level, from one colony to another.
Resources are pushed in proportion to the stored levels.
CONFIG_FILE defaults to 'lacuna.yml'
SHIP_TYPE is a regex used to decide which ships to use to push.
By default, is not set, so all trade ships will be used.
FILL_RATIO defaults to 0.5, meaning a ship is only sent if it can be filled 50%
MIN_LEVEL defaults to 100,000, meaning at least that many units of each of
food, ore, water and energy will be left on the sending planet.
MAX_SHIPS is not set by default. If set, limits the number of ships used to
push resources.
END_USAGE
}