Let’s take yesterday’s Worm Coral and turn it up to 11!
Now we have:
- Whenever a worm gets stuck, it will ‘backtrack’: it will instead expand from the previous position recursively
That means that the initial 10 worms should always be able to fill the entire world! Even if one closes off an area, that one can eventually fill it up:
I like how occasionally you get one spindly bit (usually early in the run) that another goes through. It reminds me of Blokus It does take a while.
In addition, I wanted to play a bit with simulationism:
- Worms can potentially
changeColor
each frame - Every
framesPerGeneration
check if each worm diesdeathChance
or spawns a child worm (spawnChance
) - If a worm dies, it is removed from the simulation
- If a worm spawns, it creates a new child at it’s current location
- If
spawnIncludesHistory
is set, the child can backtrack into the parent’s history - If
spawnVariesColor
is set, the child will (potentially, it’s random) have a slightly different color
- If
Let’s check it out!
let gui;
let params = {
minimumWormCount: 10,
minimumWormCountMin: 1,
colorChange: false,
framesPerGeneration: 10,
deathChance: 0.0,
deathChanceMin: 0,
deathChanceMax: 1,
deathChanceStep: 0.01,
spawnChance: 0.0,
spawnChanceMin: 0,
spawnChanceMax: 1,
spawnChanceStep: 0.01,
spawnIncludesHistory: true,
spawnVariesColor: false,
};
let visited;
let worms;
class Worm {
constructor() {
this.points = [{
x: int(random(width)),
y: int(random(height))
}];
this.color = color(
random(256),
random(256),
random(256)
);
this.age = 0;
}
update() {
this.age++;
// Dead point (should be removed)
if (this.points.length == 0) {
return;
}
var oldX = this.points[this.points.length - 1].x;
var oldY = this.points[this.points.length - 1].y;
// Calculate available points
var possible = [];
for (var newX = oldX - 1; newX <= oldX + 1; newX++) {
for (var newY = oldY - 1; newY <= oldY + 1; newY++) {
if (newX < 0 || newX >= width || newY < 0 || newY >= height) {
continue;
} else if (!visited[newX][newY]) {
possible.push({x: newX, y: newY});
}
}
}
// If we have none, we are stuck; backtrack and try to move again
if (possible.length == 0) {
this.points.pop();
this.update();
return;
}
// Otherwise, move to one of them
var target = possible[Math.floor(random() * possible.length)];
this.points.push(target);
visited[target.x][target.y] = true;
// Update color if requested
if (params.colorChange) {
this.color = color(
red(this.color) + random(-10, 10),
green(this.color) + random(-10, 10),
blue(this.color) + random(-10, 10)
);
}
}
draw() {
var p = this.points[this.points.length - 1];
fill(this.color);
noStroke();
rect(p.x, p.y, 1, 1);
}
spawn() {
var child = new Worm();
if (params.spawnIncludesHistory) {
child.points = [...this.points];
} else {
child.points = [this.points[this.points.length - 1]];
}
if (params.spawnVariesColor) {
child.color = color(
red(this.color) + random(-10, 10),
green(this.color) + random(-10, 10),
blue(this.color) + random(-10, 10)
);
} else {
child.color = this.color;
}
return child;
}
}
function setup() {
createCanvas(400, 400);
gui = createGuiPanel();
gui.addObject(params);
gui.setPosition(420, 0);
background(0);
reset();
}
function reset() {
background(0);
visited = [];
for (var x = 0; x < width; x++) {
visited[x] = [];
for (var y = 0; y < height; y++) {
visited[x][y] = false;
}
}
worms = [];
}
function draw() {
while (worms.length < params.minimumWormCount) {
worms.push(new Worm());
}
worms.forEach((worm) => worm.update());
worms = worms.filter((worm) => worm.points.length > 0);
if (frameCount % params.framesPerGeneration == 0) {
worms = worms.filter((worm) => random() > params.deathChance);
worms.forEach((worm) => {
if (random() < params.spawnChance) {
worms.push(worm.spawn());
}
});
}
worms.forEach((worm) => worm.draw());
// If all points are visited, stop the simulation
if (visited.every((row) => row.every((el) => el))) {
noLoop();
}
}
There are a few particular settings I’m a fan of:
spawnChance
= 1 is basically a flood fill and makes something close to a voronoi diagram.
High spawnChance
with color change is basically beautiful spaghetti:
Setting the minimumWormCount
to 1 and spawning new, colorful worms is delightlful to look at. You can also tell where they started (bottom left):
Setting deathChance
= 1 means that each worm goes exactly the framesPerGeneration
count (unless it gets stuck). Tons of small worms!
And setting both death and spawn is fun to watch, it’s like it’s grabbing out for you! (I need to figure out how to do gifs).
Also, I’ve added the ability to save/share settings to the p5js
shortcode I’m writing by way of the URI fragment. Every time you change a setting, it saves:
if (typeof params !== "undefined") {
for (var el of document.querySelectorAll('input')) {
if (el.id && el.id.startsWith('qs_')) {
el.addEventListener('change', () => {
parent.location.hash = btoa(JSON.stringify(params));
});
}
}
}
And whenever you load the page, it loads the fragment (if present):
setup = () => {
// Load saved settings from the browser's hash fragment
if (parent.location.hash && typeof params !== "undefined") {
try {
var settings = JSON.parse(atob(parent.location.hash.substring(1)));
Object.keys(params).forEach((key) => params[key] = settings[key] || params[key]);
} catch(ex) {
}
}
oldSetup();
...
}
It loads first so it runs before the original setup
, otherwise the settings will be loaded but the GUI will not automatically refresh.
Pretty cool, no?