hg clone https://cavac.at/public/mercurial/JavaScript-V8/
cd JavaScript-V8
perl Makefile.PL
make test install distclean
####
package FileSlurp;
#---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 base qw(Exporter);
our @EXPORT_OK = qw(slurpTextFile slurpBinFile writeBinFile slurpBinFilehandle slurpBinFilePart);
use File::Binary;
sub slurpTextFile {
my $fname = shift;
# Read in file in binary mode, slurping it into a single scalar.
# We have to make sure we use binmode *and* turn on the line termination variable completly
# to work around the multiple idiosynchrasies of Perl on Windows
open(my $fh, "<", $fname) or croak($ERRNO);
local $INPUT_RECORD_SEPARATOR = undef;
binmode($fh);
my $data = <$fh>;
close($fh);
# Convert line endings to a single format. This certainly is not perfect,
# but it works in my case. So i don't f...ing care.
$data =~ s/\015\012/\012/go;
$data =~ s/\012\015/\012/go;
$data =~ s/\015/\012/go;
# Split the lines, which also removes the linebreaks
my @datalines = split/\012/, $data;
return @datalines;
}
sub slurpBinFile {
my $fname = shift;
# Read in file in binary mode, slurping it into a single scalar.
# We have to make sure we use binmode *and* turn on the line termination variable completly
# to work around the multiple idiosynchrasies of Perl on Windows
open(my $fh, "<", $fname) or croak($ERRNO);
local $INPUT_RECORD_SEPARATOR = undef;
binmode($fh);
my $data = <$fh>;
close($fh);
return $data;
}
sub slurpBinFilePart {
my ($fname, $start, $len) = @_;
# Read in file in binary mode, slurping it into a single scalar.
# We have to make sure we use binmode *and* turn on the line termination variable completly
# to work around the multiple idiosynchrasies of Perl on Windows
my $fb = File::Binary->new($fname);
$fb->seek($start);
my $data = $fb->get_bytes($len);
$fb->close();
return $data;
}
sub slurpBinFilehandle {
my $fh = shift;
# Read in file in binary mode, slurping it into a single scalar.
# We have to make sure we use binmode *and* turn on the line termination variable completly
# to work around the multiple idiosynchrasies of Perl on Windows
local $INPUT_RECORD_SEPARATOR = undef;
binmode($fh);
my $data = <$fh>;
close($fh);
return $data;
}
sub writeBinFile {
my ($fname, $data) = @_;
# Read in file in binary mode, slurping it into a single scalar.
# We have to make sure we use binmode *and* turn on the line termination variable completly
# to work around the multiple idiosynchrasies of Perl on Windows
open(my $fh, ">", $fname) or croak($ERRNO);
local $INPUT_RECORD_SEPARATOR = undef;
binmode($fh);
print $fh $data;
close($fh);
return 1;
}
1;
__END__
####
// Theo Jansen's STRANDBEEST
//
// Code adapted from user heltonbiker at Stack Overflow
//
function fmod(a, b) {
if (a < 0) {
return b - (-a) % b
} else {
return a % b
}
}
function fromPoint(p, d, theta) {
return p.add(Vector.create([Math.cos(theta), Math.sin(theta)]).x(d))
}
function radians(d) {
return d * Math.PI / 180
}
// Return 2-dimensional vector cross product of p and q.
function cross2(p, q) {
var P = p.elements
var Q = q.elements
return P[0] * Q[1] - P[1] * Q[0]
}
// Return a point R that's distance l1 from p1, and distance l2 from p2,
// and p1-p2-R is clockwise.
function inter(p1, l1, p2, l2) {
var D = p2.subtract(p1) // Vector from p1 to p2.
var d = D.modulus() // Dist from p2 to p1.
var a = (l1*l1 - l2*l2 + d*d) / (2*d) // Dist from p1 to radical line.
var M = p1.add(D.x(a / d)) // Intersection of D w/radical line
var h = Math.sqrt(l1*l1 - a*a) // Distance from M to R1 or R2.
var R = D.x(h / d)
var r = Vector.create([-R.elements[1], R.elements[0]])
// There are two results, but only one (the correct side of the
// line) must be chosen
var R1 = M.add(r)
if (cross2(D, R1.subtract(p1)) < 0) {
return M.subtract(r)
} else {
return R1
}
}
function Beest() {
this.angle = 0;
this.lines = ["AC", "CD", "BD", "BE", "CE", "DF",
"BF", "FG", "EG", "GH", "EH"];
this.magic = ["Bx", "By"].concat(this.lines);
this.update();
}
Beest.prototype = {
constructor: Beest,
update: function () {
var text = ""
for (var i = 0; i < this.magic.length; ++i) {
var m = this.magic[i]
this[m] = parseFloat(params[m]);
}
this.footprint = [];
this.linkageBroken = false;
this.analyzedFootprint = false;
this.tolerance = 2; // Range of values of Y that count as "ground"
this.Ymax = 0;
this.liftheight = 35;
this.lifttolerance = 15;
this.maxliftheight = 60;
this.maxlifttolerance = 20;
},
addPoint: function (label, p) {
p.angle = this.angle
p.label = label
this.points.push(p)
this[label] = p
},
footprintGrounded: function (i) {
return (Math.abs(this.Ymax - this.footprint[i].elements[1])
< this.tolerance)
},
footprintLifted: function (i) {
return (Math.abs((this.Ymax - this.liftheight) - this.footprint[i].elements[1])
< this.lifttolerance)
},
analyzeFootprint: function () {
var f = this.footprint;
this.Ymax = 0; // Extremal value of Y: counts as "ground"
this.Ymin = 1000000;
for (var i = 0; i < f.length; ++i) {
this.Ymax = Math.max(this.Ymax, f[i].elements[1]);
this.Ymin = Math.min(this.Ymin, f[i].elements[1]);
}
var groundAngle = 0; // Angle spent on the ground.
var liftAngle = 0;
var minVx = 1e10;
var maxVx = -1e10;
for (var i = 0; i < f.length; ++i) {
if (this.footprintGrounded(i)) {
var j = (i + 1) % f.length
var a = f[j].angle
var b = f[i].angle
var dt
if (a < b) {
dt = b - a
} else {
dt = a - b - 360
}
groundAngle += dt
if (dt > 0) {
var vx = (f[j].elements[0] - f[i].elements[0]) / dt
minVx = Math.min(minVx, vx)
maxVx = Math.max(maxVx, vx)
}
}
if (this.footprintLifted(i)) {
var j = (i + 1) % f.length
var a = f[j].angle
var b = f[i].angle
var dt
if (a < b) {
dt = b - a
} else {
dt = a - b - 360
}
liftAngle += dt
}
}
this.analyzedFootprint = true
var text = ""
for (var i = 0; i < this.magic.length; ++i) {
var m = this.magic[i]
text += m + "=" + this[m] + "; "
}
text += "groundScore: " + (groundAngle / 360.0).toFixed(3);
text += "; dragScore: " + (Math.max(- maxVx + minVx)).toFixed(3);
text += "; liftScore: " + (liftAngle / 360.0).toFixed(3);
var maxliftscore = (this.Ymax - this.Ymin - this.maxliftheight) / this.maxlifttolerance;
if(maxliftscore < 0.0) {
maxliftscore = 0.0;
}
//write(text);
setFinished('ground', (groundAngle / 360.0), 'drag', (Math.max(- maxVx + minVx)), 'lift', (liftAngle / 360.0), 'maxlift', maxliftscore);
isFinished = 1;
},
tick: function (dt) {
this.angle += speed * dt;
this.points = []
this.addPoint("A", Vector.create([0,0]))
this.addPoint("B", Vector.create([this.Bx, -this.By]))
this.addPoint("C", fromPoint(this.A, this.AC, radians(this.angle)))
this.addPoint("D", inter(this.C, this.CD, this.B, this.BD))
this.addPoint("E", inter(this.B, this.BE, this.C, this.CE))
this.addPoint("F", inter(this.D, this.DF, this.B, this.BF))
this.addPoint("G", inter(this.F, this.FG, this.E, this.EG))
this.addPoint("H", inter(this.G, this.GH, this.E, this.EH))
if (isNaN(this.H.elements[0]) || isNaN(this.H.elements[1])) {
this.linkageBroken = true;
setFailed("Broken Linkage");
isFinished = 1;
} else {
this.footprint.push(this.H)
}
var footprintComplete = false
while (this.footprint[0].angle - 360 > this.angle) {
this.footprint.shift()
footprintComplete = true
}
if (!this.analyzedFootprint
&& !this.linkageBroken
&& footprintComplete)
{
this.analyzeFootprint()
}
},
}
var speed = -60; // Speed of crank rotation, degrees/sec.
var lastFrame;
var beest;
var isFinished = 0;
function beestTick() {
var t = (new Date()).getTime()
var dt = Math.min(1.0 / 30, (t - lastFrame) / 1000.0)
lastFrame = t
beest.tick(params.dt)
}
lastFrame = (new Date()).getTime();
beest = new Beest();
while(!isFinished) {
beestTick();
}
####
#/usr/bin/env perl
#---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---
BEGIN {
push @INC, '.';
}
use Evolver;
use FileSlurp qw(slurpBinFile writeBinFile);
use Array::Contains;
my $bestscore = -10000;
my $beest = Evolver->new();
$beest->resetStates;
my $saving = 0;
if(contains('--save', \@ARGV)) {
print "Will save fittest to fittest.dat\n";
$saving = 1;
}
if(contains('--load', \@ARGV)) {
if(!-f 'fittest.dat') {
croak("File fittest.dat not found");
}
my $data = slurpBinFile('fittest.dat');
$beest->crossPolinate($data);
}
foreach my $arg (@ARGV) {
if($arg =~ /\-\-(.*)\=(.*)/) {
$beest->config($1 => $2);
}
}
while(1) {
my ($generation, $newbestscore) = $beest->evolve;
if($newbestscore > $bestscore) {
print "** New best fittness score $bestscore\n";
my $fittest = $beest->getFittest;
$bestscore = $newbestscore;
if($saving) {
writeBinFile('fittest.dat', $fittest);
}
}
}
sub doSpacePad {
my ($val, $len) = @_;
croak("$val longer than $len") if(length($val) > $len);
#return $val if(length($val) == $len);
$val .= ' ' x ($len - length($val));
return $val;
}
sub doTrim {
my ($val) = @_;
$val =~ s/\ +$//;
return $val;
}
####
use JavaScript::V8;
...
my $vectorjs = slurpBinFile('sylvester.js');
my $strandbeestjs = slurpBinFile('strandbeest.js');
my $js = $vectorjs . ' ' . $strandbeestjs;
...
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");
}
...
####
#/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;
}