eco

See /f0blog/eco/. There is a lo-res video and a macOS build.

//ecosystem - redFrik 2010
//as described in Flake - "The Computational Beauty of Nature" (page 191)
//ported 2010-11-27 from supercollider and processing
//rewrite 2011-02-22 from opengl to cairo
//ported 2020-08-01 to cinder 0.9.2, drawing optimized

/*
 //  * 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.
 */


#include "cinder/app/App.h"
#include "cinder/app/RendererGl.h"
#include "cinder/gl/gl.h"
#include "cinder/gl/Texture.h"
#include "cinder/cairo/Cairo.h"
#include "cinder/Rand.h"
#include "cinder/Utilities.h"

using namespace ci;
using namespace ci::app;
using namespace std;


//--settings
const float fps= 60;                    //default framerate (change with +/-)
const int screenWidth= 800;             //window width
const int screenHeight= 600;            //window height
const int dim= 200;                     //dimensions x*x
const float emptyInitChance= 0.6;       //percentage chance cell start as empty, these 4 should add up to 1.0
const float plantInitChance= 0.2;       //percentage chance cell start as plant
const float herbivoreInitChance= 0.15;  //percentage chance cell start as herbivore
const float carnivoreInitChance= 0.05;  //percentage chance cell start as carnivore
const int plantEnergyInit= 25;          //initial energy
const int herbivoreEnergyInit= 25;      //initial energy
const int carnivoreEnergyInit= 25;      //initial energy
const int herbivoreEnergyLoss= 10;      //energy decrease per generation
const int carnivoreEnergyLoss= 10;      //energy decrease per generation
const int herbivoreEnergyBaby= 250;     //threshold for spawning baby
const int carnivoreEnergyBaby= 250;     //threshold for spawning baby


class ecoCairoApp : public App {
public:
  void setup();
  void update();
  void draw();
  void keyDown(KeyEvent event);

private:
  vector<ivec2> neighbours(int x, int y);
  vector<ivec2> neighboursType(int x, int y, int t);
  vector<ivec2> neighboursEmpty(int x, int y);
  vector<ivec2> allType(int t);
  vector<ivec2> allEmpty();
  bool threePlants(int x, int y);

  int myMod(int val1, int val2);
  vector<ivec2> myScramble(vector<ivec2> arr);
  ivec2 myChoose(vector<ivec2> arr);

  int objects[dim][dim][3];
  vector<ivec2> empty;
  vector<ivec2> plants;
  vector<ivec2> herbivores;
  vector<ivec2> carnivores;
  float sizex;
  float sizey;
  float rectWidth;
  float rectHeight;
  int nill;

  cairo::SurfaceImage *surf;
  gl::TextureRef tex;
};
void ecoCairoApp::setup() {
  Rand::randomize();  //for randFloat
  srand(unsigned(time(NULL)));  //for random_shuffle
  sizex= float(getWindowWidth())/dim;
  sizey= float(getWindowHeight())/dim;
  rectWidth= sizex*0.75f;
  rectHeight= sizey*0.75f;
  nill= -99999;

  for(int x= 0; x<dim; x++) {
    for(int y= 0; y<dim; y++) {
      float q= Rand::randFloat();
      objects[x][y][0]= nill;
      objects[x][y][1]= nill;
      objects[x][y][2]= nill;
      if(q<emptyInitChance) {
      }
      else if(q<(emptyInitChance+plantInitChance)) {
        objects[x][y][0]= plantEnergyInit;
      }
      else if(q<(emptyInitChance+plantInitChance+herbivoreInitChance)) {
        objects[x][y][1]= herbivoreEnergyInit;
      }
      else {  //carnivoreInitChance
        objects[x][y][2]= carnivoreEnergyInit;
      }
    }
  }

  surf= new cairo::SurfaceImage(dim, dim, false);
  gl::enableVerticalSync(false);
}
void ecoCairoApp::keyDown(KeyEvent event) {
  if(event.getChar()==' ') {  //spacebar resets
    setup();
  } else if(event.getChar()=='+') {   //+ increase framerate
    setFrameRate(min(120.0, getFrameRate()+1.0));
  } else if(event.getChar()=='-') {   //- decrease framerate
    setFrameRate(max(1.0, getFrameRate()-1.0));
  }
}

vector<ivec2> ecoCairoApp::neighbours(int x, int y) {   //return nearby x, y positions
  vector<ivec2> n;
  n.push_back(ivec2(myMod(x-1, dim), myMod(y-1, dim)));
  n.push_back(ivec2(x, myMod(y-1, dim)));
  n.push_back(ivec2((x+1)%dim, myMod(y-1, dim)));
  n.push_back(ivec2(myMod(x-1, dim), y));
  n.push_back(ivec2((x+1)%dim, y));
  n.push_back(ivec2(myMod(x-1, dim), (y+1)%dim));
  n.push_back(ivec2(x, (y+1)%dim));
  n.push_back(ivec2((x+1)%dim, (y+1)%dim));
  return n;
}
vector<ivec2> ecoCairoApp::neighboursType(int x, int y, int t) {  //return nearby x, y positions with type t
  vector<ivec2> n= neighbours(x, y);
  vector<ivec2> arr;
  for (vector<ivec2>::iterator a= n.begin(); a!=n.end(); ++a) {
    if(objects[a->x][a->y][t]!=nill) {
      arr.push_back(*a);
    }
  }
  return arr;
}
vector<ivec2> ecoCairoApp::neighboursEmpty(int x, int y) {  //return nerby x, y positions without objects
  vector<ivec2> n= neighbours(x, y);
  vector<ivec2> arr;
  for (vector<ivec2>::iterator a= n.begin(); a!=n.end(); ++a) {
    if((objects[a->x][a->y][0]==nill) && (objects[a->x][a->y][1]==nill) && (objects[a->x][a->y][2]==nill)) {
      arr.push_back(*a);
    }
  }
  return arr;
}
vector<ivec2> ecoCairoApp::allType(int t) { //return all x, y positions with type t
  vector<ivec2> arr;
  for(int x= 0; x<dim; x++) {
    for(int y= 0; y<dim; y++) {
      if(objects[x][y][t]!=nill) {
        arr.push_back(ivec2(x, y));
      }
    }
  }
  return arr;
}
vector<ivec2> ecoCairoApp::allEmpty() { //return all x, y positions without objects
  vector<ivec2> arr;
  for(int x= 0; x<dim; x++) {
    for(int y= 0; y<dim; y++) {
      if((objects[x][y][0]==nill) && (objects[x][y][1]==nill) && (objects[x][y][2]==nill)) {
        arr.push_back(ivec2(x, y));
      }
    }
  }
  return arr;
}
bool ecoCairoApp::threePlants(int x, int y) {   //optimised search for 3 neighbour plants
  int cnt= 0;
  vector<ivec2> n= neighbours(x, y);
  for(vector<ivec2>::iterator a= n.begin(); (cnt<3) && (a!=n.end()); ++a) {
    if(objects[a->x][a->y][0]!=nill) {
      cnt++;
    }
  }
  return cnt==3;
}

int ecoCairoApp::myMod(int val1, int val2) {  //wrap around negative values
  if(val1<0) {
    return (val2+val1)%val2;
  } else {
    return val1%val2;
  }
}
vector<ivec2> ecoCairoApp::myScramble(vector<ivec2> arr) {  //random reorder
  std::random_shuffle(arr.begin(), arr.end());
  return arr;
}
ivec2 ecoCairoApp::myChoose(vector<ivec2> arr) {  //random reorder
  int m= arr.size();
  if(m>0) {
    int i= Rand::randInt(0, m);
    return arr[i];
  } else {
    return ivec2(nill, nill);
  }
}

//--main loop
void ecoCairoApp::update() {

  //--rule #1: for every empty cell
  empty= allEmpty();
  for(vector<ivec2>::iterator a= empty.begin(); a!=empty.end(); ++a) {
    if(threePlants(a->x, a->y)) {
      plants.push_back(*a);   //collect empty positions with >=3 neighbour plants
    }
  }
  for(vector<ivec2>::iterator a= plants.begin(); a!=plants.end(); ++a) {
    objects[a->x][a->y][0]= plantEnergyInit;  //grow new plant
  }
  plants.clear();

  //--rule #2: for every herbivore
  herbivores= myScramble(allType(1));
  for(vector<ivec2>::iterator a= herbivores.begin(); a!=herbivores.end(); ++a) {
    objects[a->x][a->y][1]= objects[a->x][a->y][1]-herbivoreEnergyLoss;
    if(objects[a->x][a->y][1]<=0) {
      objects[a->x][a->y][1]= nill;   //dies if no energy left
    } else {
      ivec2 f= myChoose(neighboursType(a->x, a->y, 0));   //find food (plant)
      if((f.x!=nill) && (f.y!=nill)) {  //move to and eat nearby food
        objects[f.x][f.y][1]= objects[a->x][a->y][1]+objects[f.x][f.y][0];
        objects[f.x][f.y][0]= nill;
        objects[a->x][a->y][1]= nill;
        if(objects[f.x][f.y][1]>herbivoreEnergyBaby) {  //enough energy spawns baby
          objects[a->x][a->y][1]= herbivoreEnergyInit;
        }
      } else {
        ivec2 m= myChoose(neighboursEmpty(a->x, a->y)); //find empty position to move to
        if((m.x!=nill) && (m.y!=nill)) {  //move to random empty space
          objects[m.x][m.y][1]= objects[a->x][a->y][1];
          objects[a->x][a->y][1]= nill;
        }
      }
    }
  }

  //--rule #3: for every carnivore
  carnivores= myScramble(allType(2));
  for(vector<ivec2>::iterator a= carnivores.begin(); a!=carnivores.end(); ++a) {
    objects[a->x][a->y][2]= objects[a->x][a->y][2]-carnivoreEnergyLoss;
    if(objects[a->x][a->y][2]<=0) {
      objects[a->x][a->y][2]= nill;   //dies if no energy left
    } else {
      ivec2 f= myChoose(neighboursType(a->x, a->y, 1));   //find food (herbivore)
      if((f.x!=nill) && (f.y!=nill)) {  //move to and eat nearby food
        objects[f.x][f.y][2]= objects[a->x][a->y][2]+objects[f.x][f.y][1];
        objects[f.x][f.y][1]= nill;
        objects[a->x][a->y][2]= nill;
        if(objects[f.x][f.y][2]>carnivoreEnergyBaby) {  //enough energy spawns baby
          objects[a->x][a->y][2]= carnivoreEnergyInit;
        }
      } else {
        ivec2 m= myChoose(neighboursEmpty(a->x, a->y)); //find empty position to move to
        if((m.x!=nill) && (m.y!=nill)) {  //move to random empty space
          objects[m.x][m.y][2]= objects[a->x][a->y][2];
          objects[a->x][a->y][2]= nill;
        } else {
          m= myChoose(neighboursType(a->x, a->y, 0)); //find plant position to move to
          if((m.x!=nill) && (m.y!=nill)) {  //might be a newborn there
            objects[m.x][m.y][2]= objects[a->x][a->y][2];
            objects[a->x][a->y][2]= nill;
          }
        }
      }
    }
  }

  Surface &s= surf->getSurface();
  Surface::Iter iter= s.getIter();
  while(iter.line()) {
    while(iter.pixel()) {
      iter.r()= iter.g()= iter.b()= 255;
      if(objects[iter.x()][iter.y()][2]!=nill) {
        iter.r()= 0;
        iter.g()= 0;
        iter.b()= 255;
      } else if(objects[iter.x()][iter.y()][1]!=nill) {
        iter.r()= 255;
        iter.g()= 0;
        iter.b()= 0;
      } else if(objects[iter.x()][iter.y()][0]!=nill) {
        iter.r()= 0;
        iter.g()= 255;
        iter.b()= 0;
      }
    }
  }
  gl::Texture2d::Format format;
  format.setMagFilter(GL_NEAREST);
  tex= gl::Texture2d::create(s, format);
}

void ecoCairoApp::draw() {
  gl::draw(tex, getWindowBounds());
  //console() << getAverageFps() << endl;
  gl::drawString("fps:"+toString((int)getAverageFps()), vec2(8.0f, 8.0f), Color(0, 0, 0));
}

CINDER_APP(ecoCairoApp, RendererGl, [&]( App::Settings *settings ) {
  settings->setWindowSize(screenWidth, screenHeight);
  settings->setFrameRate(fps);
  settings->setFullScreen(false);
  settings->setResizable(false);
})