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);
})