eco
See /f0blog/eco/
// ecosystem - redFrik 2010
// as described in Flake - 'The Computational Beauty of Nature' (page 191)
// ported 2010-11-23 from supercollider
// updated 2010-11-27 important bugfixes
// ported 2020-07-27 to p5js
/*
white = empty space
green = plant
red = herbivore
blue = carnivore
// * For every time step:
// * For every empty cell, e:
// * If e has three or more neighbors that are plants, then e will become a plant at the next time step (assuming it isn't trampled by a herbivore or carnivore).
// * For every herbivore, h (in random order):
// * Decrease energy reserves of h by a fixed amount.
// * If h has no more energy, then h dies and becomes an empty space.
// * Else, if there is a plant next to h, then h moves on top of the plant, eats it, and gains the plant's energy.
// * If h has sufficient energy reserves, then it will spawn a baby herbivore on the space that it just exited.
// * Else, h will move into a randomly selected empty space, if one exists, that is next to h's current location.
// * For every carnivore, c (in random order):
// * Decrease energy reserves of c by a fixed amount.
// * If c has no more energy, then c dies and becomes an empty space.
// * Else, if there is a herbivore next to c, then c moves on top of the herbivore, eats it, and gains the herbivore's energy.
// * If c has sufficient energy reserves, then it will spawn a baby carnivore on the space that it just exited.
// * Else, c will move into a randomly selected empty space that is next to c's current location. If there are no empty spaces, then c will move through plants.
*/
// --settings
let fps = 60 // framerate
let wwidth = 200 // world dimension
let wheight = 200 // world dimension
let plantInitChance = 0.2 // percentage chance cell start as plant
let herbivoreInitChance = 0.15 // percentage chance cell start as herbivore
let carnivoreInitChance = 0.05 // percentage chance cell start as carnivore
let plantEnergyInit = 25 // initial energy
let herbivoreEnergyInit = 25 // initial energy
let carnivoreEnergyInit = 25 // initial energy
let herbivoreEnergyLoss = 10 // energy decrease per generation
let carnivoreEnergyLoss = 10 // energy decrease per generation
let herbivoreEnergyBaby = 250 // threshold for spawning baby
let carnivoreEnergyBaby = 250 // threshold for spawning baby
const backgroundColor = [255, 255, 255]
const plantColor = [0, 255, 0]
const herbivoreColor = [255, 0, 0]
const carnivoreColor = [0, 0, 255]
// --setup
const objects = new Array(wwidth) // 3d array
let img
function setup() {
const div = select('#sketch')
const cnv = createCanvas(div.width, div.height)
cnv.parent('sketch')
createInterface(div.width, div.position().x, div.position().y + height + 10)
initWorld()
imageMode(CENTER)
img = createImage(wwidth, wheight)
img.loadPixels()
}
function initWorld() {
frameRate(fps)
for (let x = 0; x < wwidth; x++) {
objects[x] = new Array(wheight)
for (let y = 0; y < wheight; y++) {
const q = random(1.0)
objects[x][y] = new Uint32Array(3)
if (q < plantInitChance) {
objects[x][y][0] = plantEnergyInit
} else if (q < plantInitChance + herbivoreInitChance) {
objects[x][y][1] = herbivoreEnergyInit
} else if (q < plantInitChance + herbivoreInitChance + carnivoreInitChance) {
objects[x][y][2] = carnivoreEnergyInit
}
}
}
}
// --gui
function createInterface(width, x, y) {
const button = createButton('reset').position(x, y).size(50)
button.mousePressed(() => {
frameCount = 0
fps = floor(fpsInput.value())
fpsInput.value(fps)
plantInitChance = plantInitChanceSlider.value()
herbivoreInitChance = herbivoreInitChanceSlider.value()
carnivoreInitChance = carnivoreInitChanceSlider.value()
plantEnergyInit = plantEnergyInitSlider.value()
herbivoreInitChance = herbivoreInitChanceSlider.value()
herbivoreEnergyInit = herbivoreEnergyInitSlider.value()
carnivoreInitChance = carnivoreInitChanceSlider.value()
carnivoreEnergyInit = carnivoreEnergyInitSlider.value()
herbivoreEnergyLoss = herbivoreEnergyLossSlider.value()
herbivoreEnergyBaby = herbivoreEnergyBabySlider.value()
carnivoreEnergyLoss = carnivoreEnergyLossSlider.value()
carnivoreEnergyBaby = carnivoreEnergyBabySlider.value()
initWorld()
})
createSpan('FPS:').position(x + button.width + 10, y + 4)
const fpsInput = createInput(fps, 'number')
.position(x + 50 + 40, y)
.size(40)
createSpan('plantInitChance:').position(x, y + 35)
const plantInitChanceSlider = createSlider(0, 1, plantInitChance, 0.01)
.position(x + 120, y + 35)
.size(100)
createSpan('plantEnergyInit:').position(width * 0.5 + x, y + 35)
const plantEnergyInitSlider = createSlider(0, 100, plantEnergyInit, 1)
.position(width * 0.5 + x + 120, y + 35)
.size(100)
createSpan('herbivoreInitChance:').position(x, y + 35 + 25)
const herbivoreInitChanceSlider = createSlider(0, 1, herbivoreInitChance, 0.01)
.position(x + 120, y + 35 + 25)
.size(100)
createSpan('herbivoreEnergyInit:').position(width * 0.5 + x, y + 35 + 25)
const herbivoreEnergyInitSlider = createSlider(0, 100, herbivoreEnergyInit, 1)
.position(width * 0.5 + x + 120, y + 35 + 25)
.size(100)
createSpan('carnivoreInitChance:').position(x, y + 35 + 25 + 25)
const carnivoreInitChanceSlider = createSlider(0, 1, carnivoreInitChance, 0.05)
.position(x + 120, y + 35 + 25 + 25)
.size(100)
createSpan('carnivoreEnergyInit:').position(width * 0.5 + x, y + 35 + 25 + 25)
const carnivoreEnergyInitSlider = createSlider(0, 100, carnivoreEnergyInit, 1)
.position(width * 0.5 + x + 120, y + 35 + 25 + 25)
.size(100)
createSpan('herbivoreEnergyLoss:').position(x, y + 35 + 25 + 25 + 25)
const herbivoreEnergyLossSlider = createSlider(0, 100, herbivoreEnergyLoss, 1)
.position(x + 120, y + 35 + 25 + 25 + 25)
.size(100)
createSpan('herbivoreEnergyBaby:').position(width * 0.5 + x, y + 35 + 25 + 25 + 25)
const herbivoreEnergyBabySlider = createSlider(0, 500, herbivoreEnergyBaby, 1)
.position(width * 0.5 + x + 120, y + 35 + 25 + 25 + 25)
.size(100)
createSpan('carnivoreEnergyLoss:').position(x, y + 35 + 25 + 25 + 25 + 25)
const carnivoreEnergyLossSlider = createSlider(0, 100, carnivoreEnergyLoss, 1)
.position(x + 120, y + 35 + 25 + 25 + 25 + 25)
.size(100)
createSpan('carnivoreEnergyBaby:').position(width * 0.5 + x, y + 35 + 25 + 25 + 25 + 25)
const carnivoreEnergyBabySlider = createSlider(0, 100, carnivoreEnergyBaby, 1)
.position(width * 0.5 + x + 120, y + 35 + 25 + 25 + 25 + 25)
.size(100)
}
function neighbours(x, y) {
// return nearby x, y positions
const n = new Array(8)
for (let i = 0; i < 8; i++) {
n[i] = new Uint16Array(2)
}
n[0][0] = myMod(x - 1, wwidth)
n[0][1] = myMod(y - 1, wheight)
n[1][0] = x
n[1][1] = myMod(y - 1, wheight)
n[2][0] = (x + 1) % wwidth
n[2][1] = myMod(y - 1, wheight)
n[3][0] = myMod(x - 1, wwidth)
n[3][1] = y
n[4][0] = (x + 1) % wwidth
n[4][1] = y
n[5][0] = myMod(x - 1, wwidth)
n[5][1] = (y + 1) % wheight
n[6][0] = x
n[6][1] = (y + 1) % wheight
n[7][0] = (x + 1) % wwidth
n[7][1] = (y + 1) % wheight
return n
}
function neighboursType(x, y, t) {
// return nearby x, y positions with type t
const n = neighbours(x, y)
const arr = []
for (let i = 0; i < 8; i++) {
if (objects[n[i][0]][n[i][1]][t] > 0) {
arr.push(n[i])
}
}
return arr
}
function neighboursEmpty(x, y) {
// return nerby x, y positions without objects
const n = neighbours(x, y)
const arr = []
for (let i = 0; i < 8; i++) {
const energies = objects[n[i][0]][n[i][1]]
if (energies[0] === 0 && energies[1] === 0 && energies[2] === 0) {
arr.push(n[i])
}
}
return arr
}
function allType(t) {
// return all x, y positions with type t
const arr = []
for (let x = 0; x < wwidth; x++) {
for (let y = 0; y < wheight; y++) {
if (objects[x][y][t] > 0) {
arr.push([x, y])
}
}
}
return arr
}
function allEmpty() {
// return all x, y positions without objects
const arr = []
for (let x = 0; x < wwidth; x++) {
for (let y = 0; y < wheight; y++) {
const energies = objects[x][y]
if (energies[0] === 0 && energies[1] === 0 && energies[2] === 0) {
arr.push([x, y])
}
}
}
return arr
}
function threePlants(x, y) {
// optimised search for 3 neighbour plants
let i = 0
let cnt = 0
const n = neighbours(x, y)
while (cnt < 3 && i < 8) {
if (objects[n[i][0]][n[i][1]][0] > 0) {
cnt++
}
i++
}
return cnt === 3
}
// --main loop
function draw() {
background(255)
// --rule #1: for every empty cell
const empty = allEmpty()
const plants = []
for (let i = empty.length - 1; i >= 0; i--) {
const a = empty[i]
if (threePlants(a[0], a[1])) {
plants.push(a) // collect empty positions with >=3 neighbour plants
}
}
for (let i = plants.length - 1; i >= 0; i--) {
const a = plants[i]
objects[a[0]][a[1]][0] = plantEnergyInit // grow new plant
}
// --rule #2: for every herbivore
const herbivores = myScramble(allType(1))
for (let i = herbivores.length - 1; i >= 0; i--) {
const a = herbivores[i]
objects[a[0]][a[1]][1] = max(objects[a[0]][a[1]][1] - herbivoreEnergyLoss, 0)
if (objects[a[0]][a[1]][1] > 0) {
const f = myChoose(neighboursType(a[0], a[1], 0)) // find food (plant)
if (f !== undefined) {
// move to and eat nearby food
objects[f[0]][f[1]][1] = objects[a[0]][a[1]][1] + objects[f[0]][f[1]][0]
objects[f[0]][f[1]][0] = 0
objects[a[0]][a[1]][1] = 0
if (objects[f[0]][f[1]][1] > herbivoreEnergyBaby) {
// enough energy spawns baby
objects[a[0]][a[1]][1] = herbivoreEnergyInit
}
} else {
const m = myChoose(neighboursEmpty(a[0], a[1])) // find position to move to
if (m !== undefined) {
// move to random empty space
objects[m[0]][m[1]][1] = objects[a[0]][a[1]][1]
objects[a[0]][a[1]][1] = 0
}
}
}
}
// --rule #3: for every carnivore
const carnivores = myScramble(allType(2))
for (let i = carnivores.length - 1; i >= 0; i--) {
const a = carnivores[i]
objects[a[0]][a[1]][2] = max(objects[a[0]][a[1]][2] - carnivoreEnergyLoss, 0)
if (objects[a[0]][a[1]][2] > 0) {
const f = myChoose(neighboursType(a[0], a[1], 1)) // find food (herbivore)
if (f !== undefined) {
// move to and eat nearby food
objects[f[0]][f[1]][2] = objects[a[0]][a[1]][2] + objects[f[0]][f[1]][1]
objects[f[0]][f[1]][1] = 0
objects[a[0]][a[1]][2] = 0
if (objects[f[0]][f[1]][2] > carnivoreEnergyBaby) {
// enough energy spawns baby
objects[a[0]][a[1]][2] = carnivoreEnergyInit
}
} else {
let m = myChoose(neighboursEmpty(a[0], a[1])) // find empty position to move to
if (m !== undefined) {
// move to random empty space
objects[m[0]][m[1]][2] = objects[a[0]][a[1]][2]
objects[a[0]][a[1]][2] = 0
} else {
m = myChoose(neighboursType(a[0], a[1], 0)) // find plant position to move to
if (m !== undefined) {
// might be a newborn there
objects[m[0]][m[1]][2] = objects[a[0]][a[1]][2]
objects[a[0]][a[1]][2] = 0
}
}
}
}
}
// --render
for (let z = 0; z < 3; z++) {
const rgb = [plantColor, herbivoreColor, carnivoreColor][z]
for (let x = 0; x < wwidth; x++) {
for (let y = 0; y < wheight; y++) {
const i = 4 * (y * wwidth + x)
if (z === 0) {
img.pixels[i] = backgroundColor[0]
img.pixels[i + 1] = backgroundColor[1]
img.pixels[i + 2] = backgroundColor[2]
img.pixels[i + 3] = 255
}
if (objects[x][y][z] > 0) {
img.pixels[i] = rgb[0]
img.pixels[i + 1] = rgb[1]
img.pixels[i + 2] = rgb[2]
}
}
}
}
img.updatePixels()
const scale = min(width / wwidth, height / wheight)
image(img, width * 0.5, height * 0.5, scale * wwidth, scale * wheight)
fill(0)
text(`FPS:${round(frameRate())}`, 8, 18)
text(`Generation:${frameCount}`, 8, 38)
}
function myMod(val1, val2) {
// wrap around negative values
if (val1 < 0) {
return (val2 + val1) % val2
}
return val1 % val2
}
function myScramble(arr) {
// random reorder
const arrOut = []
let m = arr.length
let i
while (m > 0) {
i = int(random(m))
arrOut.push(arr[i])
arr.splice(i, 1)
m = arr.length
}
return arrOut
}
function myChoose(arr) {
const m = arr.length
if (m > 0) {
const i = int(random(m))
return arr[i]
}
return undefined
}