This turtle uses the naive genetic algorithm from Natural selection to evolve an initial path to one that matches the 'fittest' path as much as possible.
#genetic
Log in to post a comment.
// Path evolution. Created by Reinder Nijhoff 2019
// @reindernijhoff
//
// https://turtletoy.net/turtle/d5e96736b9
//
const mutationRate = .25; // min=0.0, max=0.5, step=0.001
const mutationDefect = 2; // min=0, max=100, step=0.1
const populationSize = 25; // min=10, max=100, step=1
const generationsPerFrame = 1; // min=1, max=100, step=1
const grid = 13; // min=1, max=15, step=1
const startPath = `M0,-37 C-10,-37 -26,-22 -30,-14 C-32,-10 -38,-3 -35,2 C-28,17 -7,39 13,29
C14,28 17,30 18,29 C59,9 37,-36 -3,-36`; // type=path, bbox=-40,-40,80,80 Click here to redraw the path
const fittestPath = `M0,-38 C0,-16 15,-2 20,18 C21,20 28,36 28,36 L26,36 C21,34 18,28 14,25
C3,17 -7,5 -20,2 C-22,1 -33,-5 -33,-5 L-32,-5 C-27,-5 -22,-5 -17,-5 C-3,-5 11,-6 25,-6 C26,-6 38,-4 38,-4
L23,1 C11,5 -7,18 -18,25 C-22,28 -28,30 -31,33 L-35,37 C-35,36 -26,22 -25,19 C-22,13 -21,6 -18,0
C-12,-12 -2,-24 -2,-37`; // type=path, bbox=-40,-40,80,80 Click here to redraw the path
let seed = 1; // min=1, max=1000, step=1
let tokens = startPath.match(/([0-9.-]+|[MLC])/g);
const target = Path(fittestPath.match(/([0-9.-]+|[MLC])/g));
function Translate(x,y) { return p => [p[0]+x, p[1]+y]; }
function Scale(s) { return p => [p[0]*s, p[1]*s]; }
function mutation(tokens) {
return tokens.map(token => {
if (isNumber(token)) {
const defect = (random()-.5)*2;
return random() < mutationRate ? token - (defect**2)*Math.sign(defect)*mutationDefect : token;
} else return token;
});
}
function drawPath(i, tokens) {
const y = i/grid|0, x = i%grid;
const path = Path(tokens);
const steps = path.length() | 0;
const turtle = new Tortoise(path.p(0));
turtle.addTransform(Scale(1.65 / grid));
turtle.addTransform(Translate((x+.5)*180/grid-90, (y+.5)*180/grid-90));
for (let i=0; i<steps; i++) {
turtle.goto(path.p( i/steps ));
}
}
//
// Individual
//
class Individual {
constructor(tokens = []) {
this.dna = tokens;
this.fitness = 0;
}
breed(p0, p1) {
let s = Math.floor(Math.random()*(this.dna.length-2))+1;
// crossover
if (Math.random() < .5) {
this.dna = [...p0.dna.slice(0,s), ...p1.dna.slice(s)];
} else {
this.dna = [...p1.dna.slice(0,s), ...p0.dna.slice(s)];
}
// mutations
this.dna = mutation(this.dna);
this.calcFitness();
}
calcFitness() {
this.fitness = 0;
const path = Path(this.dna);
const steps = 100;
for (let i=0; i<steps; i++) {
const p0 = path.p(i/steps);
const p1 = target.p(i/steps);
this.fitness += (p0[0]-p1[0])**2 + (p0[1]-p1[1])**2;
}
}
}
//
// Generation
//
class Generation {
constructor() {
this.individuals = [];
for (let i=0; i<populationSize; i++) {
this.individuals[i] = new Individual();
}
}
fillWithTokens(tokens) {
this.individuals.forEach(i => i.dna = [...tokens]);
return this.sortFittest();
}
breed(parentGeneration) {
const p0 = parentGeneration.individuals[0];
const p1 = parentGeneration.individuals[1];
this.individuals.forEach(i => i.breed(p0, p1));
return this.sortFittest();
}
sortFittest() {
this.individuals.sort( (a, b) => a.fitness - b.fitness );
return this;
}
}
//
// Selection
//
let gen0 = new Generation().fillWithTokens(tokens);
let gen1 = new Generation().fillWithTokens(tokens);
function walk(i) {
drawPath(i, gen0.individuals[0].dna);
for (let j=0; j<generationsPerFrame; j++) {
gen1.breed(gen0)
gen0.breed(gen1)
}
return i < grid*grid-1;
}
function isNumber(n) {
return !isNaN(parseFloat(n)) && isFinite(n);
}
function random() {
let r = 1103515245 * (((seed+=12345) >> 1) ^ (seed));
r = 1103515245 * (r ^ (r >> 3));
r = r ^ (r >> 16);
const mod = 1 << 20;
return (r % mod) / mod;
}
////////////////////////////////////////////////////////////////
// Modified path utility code. Created by Reinder Nijhoff 2023
// Parses a single SVG path (only M, C and L statements are
// supported). The p-method will return
// [...position, ...derivative] for a normalized point t.
//
// https://turtletoy.net/turtle/46adb0ad70
////////////////////////////////////////////////////////////////
function Path(tokens) {
class MoveTo {
constructor(p) { this.p0 = p; }
p(t, s) { return [...this.p0, 1, 0]; }
length() { return 0; }
}
class LineTo {
constructor(p0, p1) { this.p0 = p0, this.p1 = p1; }
p(t, s = 1) {
const nt = 1 - t, p0 = this.p0, p1 = this.p1;
return [
nt*p0[0] + t*p1[0],
nt*p0[1] + t*p1[1],
(p1[0] - p0[0]) * s,
(p1[1] - p0[1]) * s,
];
}
length() {
const p0 = this.p0, p1 = this.p1;
return Math.hypot(p0[0]-p1[0], p0[1]-p1[1]);
}
}
class BezierTo {
constructor(p0, c0, c1, p1) { this.p0 = p0, this.c0 = c0, this.c1 = c1, this.p1 = p1; }
p(t, s = 1) {
const nt = 1 - t, p0 = this.p0, c0 = this.c0, c1 = this.c1, p1 = this.p1;
return [
nt*nt*nt*p0[0] + 3*t*nt*nt*c0[0] + 3*t*t*nt*c1[0] + t*t*t*p1[0],
nt*nt*nt*p0[1] + 3*t*nt*nt*c0[1] + 3*t*t*nt*c1[1] + t*t*t*p1[1],
(3*nt*nt*(c0[0]-p0[0]) + 6*t*nt*(c1[0]-c0[0]) + 3*t*t*(p1[0]-c1[0])) * s,
(3*nt*nt*(c0[1]-p0[1]) + 6*t*nt*(c1[1]-c0[1]) + 3*t*t*(p1[1]-c1[1])) * s,
];
}
length() {
return this._length || (
this._length = Array.from({length:25}, (x, i) => this.p(i/25)).reduce(
(a,c,i,v) => i > 0 ? a + Math.hypot(c[0]-v[i-1][0], c[1]-v[i-1][1]) : a, 0));
}
}
class Path {
constructor(tokens) {
this.segments = [];
this.parsePath(tokens);
}
parsePath(t) {
for (let s, i=0; i<t.length;) {
switch (t[i++]) {
case 'M': this.add(new MoveTo(s=[t[i++],t[i++]]));
break;
case 'L': this.add(new LineTo(s, s=[t[i++],t[i++]]));
break;
case 'C': this.add(new BezierTo(s, [t[i++],t[i++]], [t[i++],t[i++]], s=[t[i++],t[i++]]));
break;
default: i++;
}
}
}
add(segment) {
this.segments.push(segment);
this._length = 0;
}
length() {
return this._length || (this._length = this.segments.reduce((a,c) => a + c.length(), 0));
}
p(t) {
t = Math.max(Math.min(t, 1), 0) * this.length();
for (let l=0, i=0, sl=0; i<this.segments.length; i++, l+=sl) {
sl = this.segments[i].length();
if (t > l && t <= l + sl) {
return this.segments[i].p((t-l)/sl, sl/this.length());
}
}
return this.segments[Math.min(1, this.segments.length-1)].p(0);
}
}
return new Path(tokens);
}
////////////////////////////////////////////////////////////////
// Tortoise utility code. Created by Reinder Nijhoff 2019
// https://turtletoy.net/turtle/102cbd7c4d
////////////////////////////////////////////////////////////////
function Tortoise(x, y) {
class Tortoise extends Turtle {
constructor(x, y) {
super(x, y);
this.ps = Array.isArray(x) ? [...x] : [x || 0, y || 0];
this.transforms = [];
}
addTransform(t) {
this.transforms.push(t);
this.jump(this.ps);
return this;
}
applyTransforms(p) {
if (!this.transforms) return p;
let pt = [...p];
this.transforms.map(t => { pt = t(pt); });
return pt;
}
goto(x, y) {
const p = Array.isArray(x) ? [...x] : [x, y];
const pt = this.applyTransforms(p);
if (this.isdown() && (this.pt[0]-pt[0])**2 + (this.pt[1]-pt[1])**2 > 4) {
this.goto((this.ps[0]+p[0])/2, (this.ps[1]+p[1])/2);
this.goto(p);
} else {
super.goto(pt);
this.ps = p;
this.pt = pt;
}
}
position() { return this.ps; }
}
return new Tortoise(x,y);
}