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