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 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 objects = new Array(dim) // 3d array

//  --setup
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()
}

function initWorld () {
  frameRate(fps)
  for (let x = 0; x < dim; x++) {
    objects[x] = new Array(dim)
    for (let y = 0; y < dim; y++) {
      const q = random(1.0)
      objects[x][y] = []
      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)
    dim = constrain(floor(dimInput.value()), 1, 255)
    dimInput.value(dim)
    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('Dim:').position(x + button.width + 100, y + 4)
  const dimInput = createInput(dim, 'number').position(x + 50 + 128, 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 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
  const n = neighbours(x, y)
  const arr = []
  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
  const n = neighbours(x, y)
  const arr = []
  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
  const arr = []
  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
  const arr = []
  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; let cnt = 0
  const 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
  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] = 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 {
      const 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 {
        const 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
  const carnivores = myScramble(allType(2))
  for (let i = carnivores.length - 1; i >= 0; i--) {
    const 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 {
      const 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
  const sizex = width / dim
  const sizey = height / dim
  const rectWidth = sizex * 0.75
  const 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
  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, undefined]
}