Trails ›

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= 20;                          //framerate
let dim= 100;                         //dimensions x*x
let emptyInitChance= 0.6;             //percentage chance cell start as empty
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
let objects= new Array(dim);  //3d array
//--setup
function setup() {
  let div= select('#sketch');
  let cnv= createCanvas(div.width, div.height);
  cnv.parent('sketch');
  createInterface(div.width, div.position().x, div.position().y+height+10);
  initWorld();
}
function initWorld() {
  let sumChances= emptyInitChance+plantInitChance+herbivoreInitChance+carnivoreInitChance;
  let normChances= [
    emptyInitChance/sumChances,
    plantInitChance/sumChances,
    herbivoreInitChance/sumChances,
    carnivoreInitChance/sumChances
  ];
  normChances[1]= normChances[0]+normChances[1];
  normChances[2]= normChances[1]+normChances[2];
  normChances[3]= normChances[2]+normChances[3];
  frameRate(fps);
  for(let x= 0; x<dim; x++) {
    objects[x]= new Array(dim);
    for(let y= 0; y<dim; y++) {
      let q= random(1.0);
      objects[x][y]= [];
      if(q<normChances[0]) {
      }
      else if(q<normChances[1]) {
        objects[x][y][0]= plantEnergyInit;
      }
      else if(q<normChances[2]) {
        objects[x][y][1]= herbivoreEnergyInit;
      }
      else {
        objects[x][y][2]= carnivoreEnergyInit;
      }
    }
  }
}
//--gui
function createInterface(width, x, y) {
  let button= createButton('reset').position(x, y).size(50);
  button.mousePressed(() => {
    frameCount= 0;
    fps= floor(fpsInput.value());
    fpsInput.value(fps);
    dim= constrain(floor(dimInput.value()), 1, 255);
    dimInput.value(dim);
    emptyInitChance= emptyInitChanceSlider.value();
    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);
  let fpsInput= createInput(fps, "number").position(x+50+40, y).size(40);
  createSpan('Dim:').position(x+button.width+100, y+4);
  let dimInput= createInput(dim, "number").position(x+50+128, y).size(40);
  createSpan('emptyInitChance:').position(width*0.5+x, y+10);
  let emptyInitChanceSlider= createSlider(0, 1, emptyInitChance, 0.01)
  .position(width*0.5+x+120, y+10).size(100);
  createSpan('plantInitChance:').position(x, y+35);
  let plantInitChanceSlider= createSlider(0, 1, plantInitChance, 0.01)
  .position(x+120, y+35).size(100);
  createSpan('plantEnergyInit:').position(width*0.5+x, y+35);
  let plantEnergyInitSlider= createSlider(0, 100, plantEnergyInit, 1)
  .position(width*0.5+x+120, y+35).size(100);
  createSpan('herbivoreInitChance:').position(x, y+35+25);
  let 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);
  let 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);
  let 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);
  let 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);
  let 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);
  let 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);
  let 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);
  let carnivoreEnergyBabySlider= createSlider(0, 100, emptyInitChance, 1)
  .position(width*0.5+x+120, y+35+25+25+25+25).size(100);
}
function neighbours(x, y) {  //return nearby x, y positions
  let n= new Array(8);
  for(let i= 0; i<8; i++) {
    n[i]= new Uint8Array(2);
  }
  n[0][0]= myMod(x-1, dim);
  n[0][1]= myMod(y-1, dim);
  n[1][0]= x;
  n[1][1]= myMod(y-1, dim);
  n[2][0]= (x+1)%dim;
  n[2][1]= myMod(y-1, dim);
  n[3][0]= myMod(x-1, dim);
  n[3][1]= y;
  n[4][0]= (x+1)%dim;
  n[4][1]= y;
  n[5][0]= myMod(x-1, dim);
  n[5][1]= (y+1)%dim;
  n[6][0]= x;
  n[6][1]= (y+1)%dim;
  n[7][0]= (x+1)%dim;
  n[7][1]= (y+1)%dim;
  return n;
}
function neighboursType(x, y, t) {  //return nearby x, y positions with type t
  let n= neighbours(x, y);
  let arr= new Array();
  for(let i= 0; i<8; i++) {
    if(objects[n[i][0]][n[i][1]][t]!=undefined) {
      arr.push(n[i]);
    }
  }
  return arr;
}
function neighboursEmpty(x, y) {  //return nerby x, y positions without objects
  let n= neighbours(x, y);
  let arr= new Array();
  for(let i= 0; i<8; i++) {
    if((objects[n[i][0]][n[i][1]][0]==undefined) && (objects[n[i][0]][n[i][1]][1]==undefined) && (objects[n[i][0]][n[i][1]][2]==undefined)) {
      arr.push(n[i]);
    }
  }
  return arr;
}
function allType(t) { //return all x, y positions with type t
  let arr= new Array();
  for(let x= 0; x<dim; x++) {
    for(let y= 0; y<dim; y++) {
      if(objects[x][y][t]!=undefined) {
        arr.push([x, y]);
      }
    }
  }
  return arr;
}
function allEmpty() { //return all x, y positions without objects
  let arr= new Array();
  for(let x= 0; x<dim; x++) {
    for(let y= 0; y<dim; y++) {
      if((objects[x][y][0]==undefined) && (objects[x][y][1]==undefined) && (objects[x][y][2]==undefined)) {
        arr.push([x, y]);
      }
    }
  }
  return arr;
}
function threePlants(x, y) {  //optimised search for 3 neighbour plants
  let i= 0, cnt= 0;
  let n= neighbours(x, y);
  while((cnt<3) && (i<8)) {
    if(objects[n[i][0]][n[i][1]][0]!=undefined) {
      cnt++;
    }
    i++;
  }
  return cnt==3;
}
//--main loop
function draw() {
  noStroke();
  background(255);
  //--rule #1: for every empty cell
  let empty= allEmpty();
  let plants= new Array();
  for(let i= empty.length-1; i>=0; i--) {
    let 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--) {
    let a= plants[i];
    objects[a[0]][a[1]][0]= plantEnergyInit;  //grow new plant
  }
  //--rule #2: for every herbivore
  let herbivores= myScramble(allType(1));
  for(let i= herbivores.length-1; i>=0; i--) {
    let a= herbivores[i];
    objects[a[0]][a[1]][1]= objects[a[0]][a[1]][1]-herbivoreEnergyLoss;
    if(objects[a[0]][a[1]][1]<=0) {
      objects[a[0]][a[1]][1]= undefined;  //dies if no energy left
    }
    else {
      let f= myChoose(neighboursType(a[0], a[1], 0));   //find food (plant)
      if((f[0]!=undefined) && (f[1]!=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]= undefined;
        objects[a[0]][a[1]][1]= undefined;
        if(objects[f[0]][f[1]][1]>herbivoreEnergyBaby) {  //enough energy spawns baby
          objects[a[0]][a[1]][1]= herbivoreEnergyInit;
        }
      }
      else {
        let m= myChoose(neighboursEmpty(a[0], a[1])); //find position to move to
        if((m[0]!=undefined) && (m[1]!=undefined)) {  //move to random empty space
          objects[m[0]][m[1]][1]= objects[a[0]][a[1]][1];
          objects[a[0]][a[1]][1]= undefined;
        }
      }
    }
  }
  //--rule #3: for every carnivore
  let carnivores= myScramble(allType(2));
  for(let i= carnivores.length-1; i>=0; i--) {
    let a= carnivores[i];
    objects[a[0]][a[1]][2]= objects[a[0]][a[1]][2]-carnivoreEnergyLoss;
    if(objects[a[0]][a[1]][2]<=0) {  //dies if no energy left
      objects[a[0]][a[1]][2]= undefined;
    }
    else {
      let f= myChoose(neighboursType(a[0], a[1], 1)); //find food (herbivore)
      if((f[0]!=undefined) && (f[1]!=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]= undefined;
        objects[a[0]][a[1]][2]= undefined;
        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[0]!=undefined) && (m[1]!=undefined)) {  //move to random empty space
          objects[m[0]][m[1]][2]= objects[a[0]][a[1]][2];
          objects[a[0]][a[1]][2]= undefined;
        }
        else {
          m= myChoose(neighboursType(a[0], a[1], 0)); //find plant position to move to
          if((m[0]!=undefined) && (m[1]!=undefined)) {  //might be a newborn there
            objects[m[0]][m[1]][2]= objects[a[0]][a[1]][2];
            objects[a[0]][a[1]][2]= undefined;
          }
        }
      }
    }
  }
  //--render
  let sizex= width/dim;
  let sizey= height/dim;
  let rectWidth= sizex*0.75;
  let rectHeight= sizey*0.75;
  translate(sizex*0.125, sizey*0.125);
  for(let z= 0; z<3; z++) {
    fill((z==1)*255, (z==0)*255, (z==2)*255);
    for(let x= 0; x<dim; x++) {
      for(let y= 0; y<dim; y++) {
        if(objects[x][y][z]!=undefined) {
          rect(x*sizex, y*sizey, rectWidth, rectHeight);
        }
      }
    }
  }
  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
  let arrOut= new Array();
  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) {
  let m= arr.length;
  if(m>0) {
    let i= int(random(m));
    return arr[i];
  }
  return [undefined, undefined];
}