#/usr/bin/env perl package Evolver; #---AUTOPRAGMASTART--- use 5.012; use strict; use warnings; use diagnostics; use mro 'c3'; use English qw( -no_match_vars ); use Carp; our $VERSION = 1.7; no if $] >= 5.017011, warnings => 'experimental::smartmatch'; use Fatal qw( close ); #---AUTOPRAGMAEND--- use JavaScript::V8; use AI::Genetic; use FileSlurp qw[slurpBinFile]; use Time::HiRes qw(time); use Data::Dumper; my $vectorjs = slurpBinFile('sylvester.js'); my $strandbeestjs = slurpBinFile('strandbeest.js'); my $js = $vectorjs . ' ' . $strandbeestjs; my $precision = 10; my $crosspolinationcount = 3; my $popsize = 20; my $crosspopulated = 0; my @genetics = ( { name => 'Bx', min => -44, max => -32, }, { name => 'By', min => -12, max => -5, }, { name => 'AC', min => 8, max => 18, }, { name => 'CD', min => 40, max => 60, }, { name => 'BD', min => 30, max => 50, }, { name => 'BE', min => 30, max => 50, }, { name => 'CE', min => 50, max => 70, }, { name => 'DF', min => 45, max => 65, }, { name => 'BF', min => 30, max => 50, }, { name => 'FG', min => 30, max => 50, }, { name => 'EG', min => 28, max => 45, }, { name => 'GH', min => 55, max => 75, }, { name => 'EH', min => 40, max => 60, }, ); sub new { my ($proto, %config) = @_; my $class = ref($proto) || $proto; my $self = {}; bless $self, $class; $self->{generation} = 0; return $self; } sub config { my($self, %config) = @_; if(defined($config{population_size})) { $popsize = $config{population_size}; print "Setting population size to $popsize\n"; } if(defined($config{crosspolination_count})) { $crosspolinationcount = $config{crosspolination_count}; print "Settin crosspolination count to $crosspolinationcount\n"; } } sub resetStates { my ($self) = @_; my $evolver = AI::Genetic->new( -fitness => \&getFitness, -type => 'rangevector', -population => $popsize, -crossover => 0.95, -mutation => 0.10, ); my @initargs; foreach my $genetic (@genetics) { my @pair = ($genetic->{min} * $precision, $genetic->{max} * $precision); push @initargs, \@pair; } $evolver->init(\@initargs); $self->{evolver} = $evolver; $self->{generation} = 0; return; } sub evolve { my ($self) = @_; my $starttime = time; $self->{evolver}->evolve('rouletteUniform', 1); my $endtime = time; my $timetaken = $endtime - $starttime; $timetaken = int($timetaken*100)/100; my ($top) = $self->{evolver}->getFittest(1); my $score = $top->score(); if($crosspopulated) { $self->{evolver}->size($popsize); # Reset size } $self->{generation}++; print "Generation ", $self->{generation}, " in $timetaken seconds: Population size $popsize, Best fit: $score\n"; return ($self->{generation}, $score); } sub getFittest { my ($self) = @_; my @fittest = $self->{evolver}->getFittest($crosspolinationcount); my $i = 0; my @serialparts; foreach my $top (@fittest) { my @temp; my $score = $top->score(); push @temp, $score; my @genes = $top->genes(); foreach my $gene (@genes) { $gene /= $precision; push @temp, $gene; } push @serialparts, join('|', @temp); } my $serialized = join('#', @serialparts); return $serialized; } sub crossPolinate { my ($self, $extragenes) = @_; my @serialparts = split/\#/, $extragenes; foreach my $serialpart (@serialparts) { my ($score, @genes) = split/\|/, $serialpart; $self->{evolver}->inject(1, \@genes); } print "Injected ", scalar @serialparts, " crosspolinator\n"; $crosspopulated = 1; return; } sub getFitness { my ($genes) = @_; my $self; my %params = ( dt => 0.01, # virtual time tick ); for(my $i = 0; $i < scalar @genetics; $i++) { $params{$genetics[$i]->{name}} = $genes->[$i] / 10; } my $ok = -1; my $errortype = 'MATH_ERROR'; my %scores; my $ctx = JavaScript::V8::Context->new(); $ctx->bind_function(write => sub { print @_ }); $ctx->bind_function(setFailed => sub { $ok = 0; $errortype = shift @_; }); $ctx->bind_function(setFinished => sub { $ok = 1; %scores = @_; }); $ctx->bind(params => \%params); $ctx->eval($js); my $error = $@; my $total = -1000; # Default: Failed! if(defined($error)) { print("SCRIPT ERROR: $error\n"); } elsif($ok == -1) { print("Script didn't call setFailed() or setFinished()\n"); } if(!$ok) { #print "Strandbeest feet failed: $errortype\n"; } else { $total = 0; #print 'ground score: ', $scores{ground}, "\n"; $total += abs(0.5 - $scores{ground}); # We want close to 50% #print 'drag score: ', $scores{drag}, "\n"; $total += abs($scores{drag}); # We want close to 0 #print 'lift score: ', $scores{lift}, "\n"; $total += abs(0.2 - $scores{lift}) * 2; # We want close to 20% at optimum target height #print 'maxlift score: ', $scores{maxlift}, "\n"; $total += abs($scores{maxlift}); # We want close to 0 if(!$total) { $total = 1; # No distance to prefered values } else { $total = -$total; } } #print "Total fitness score: $total (the higher the better)\n"; return $total; }