spiral ›

eco

Cargo.toml

See /f0blog/eco/ for more info and /software/p5js/eco/ for a JavaScript version.

//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
//ported 2020-09-14 to rust nannou

//TODO:
//* set framerate (app.set_loop_mode(LoopMode::rate_fps(model.fps));)
//* make dim a vector (width, height)
//* simple gui

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

use nannou::prelude::*;
use nannou::rand::seq::SliceRandom;
use nannou::rand::thread_rng;

const DIM: usize = 100;

#[derive(Debug, PartialEq, Clone, Copy)]
enum CellType {
    Empty,
    Plant,
    Herbivore,
    Carnivore,
}

#[derive(Debug, PartialEq, Clone, Copy)]
struct Cell {
    cell_type: CellType,
    energy: i32,
}
impl Cell {
    fn clear(&mut self) {
        self.cell_type = CellType::Empty;
        self.energy = 0;
    }
}

fn main() {
    nannou::app(model).update(update).run();
}

struct Model {
    fps: f32,
    world: Vec<Cell>,
    plant_init_chance: f32,
    herbivore_init_chance: f32,
    carnivore_init_chance: f32,
    plant_energy_init: i32,
    herbivore_energy_init: i32,
    carnivore_energy_init: i32,
    herbivore_energy_loss: i32,
    carnivore_energy_loss: i32,
    herbivore_energy_baby: i32,
    carnivore_energy_baby: i32,
}
fn model(app: &App) -> Model {
    app.new_window()
    .title("eco")
    .size(800, 600)
    .key_pressed(key_pressed)
    .view(view)
    .build()
    .unwrap();
    
    // settings
    let fps = 60.0;
    let plant_init_chance = 0.2; // percentage chance
    let herbivore_init_chance = 0.15;
    let carnivore_init_chance = 0.05;
    let plant_energy_init = 25; // initial energy
    let herbivore_energy_init = 25;
    let carnivore_energy_init = 25;
    let herbivore_energy_loss = 10; // energy decrease per generation
    let carnivore_energy_loss = 10;
    let herbivore_energy_baby = 250; // threshold for spawning baby
    let carnivore_energy_baby = 250;

    let mut model = Model {
        fps,
        world: Vec::with_capacity(DIM * DIM),
        plant_init_chance,
        herbivore_init_chance,
        carnivore_init_chance,
        plant_energy_init,
        herbivore_energy_init,
        carnivore_energy_init,
        herbivore_energy_loss,
        carnivore_energy_loss,
        herbivore_energy_baby,
        carnivore_energy_baby,
    };
    create_world(&mut model);
    model
}

fn key_pressed(_app: &App, model: &mut Model, key: Key) {
    match key {
        Key::Space => {
            create_world(model);
        }
        Key::Add => {   // NOTE: does not work on swedish keyboard layout
            model.fps = (model.fps + 3.0).min(120.0);
        }
        Key::Minus => {
            model.fps = (model.fps - 3.0).max(1.0);
        }
        _ => {}
    }
}

fn create_world(model: &mut Model) {
    model.world.clear();
    for _x in 0..DIM {
        for _y in 0..DIM {
            let q = random_f32();
            let t;
            let e;
            if q < model.plant_init_chance {
                t = CellType::Plant;
                e = model.plant_energy_init;
            } else if q < (model.plant_init_chance + model.herbivore_init_chance) {
                t = CellType::Herbivore;
                e = model.herbivore_energy_init;
            } else if q < (model.plant_init_chance + model.herbivore_init_chance + model.carnivore_init_chance) {
                t = CellType::Carnivore;
                e = model.carnivore_energy_init;
            } else {
                t = CellType::Empty;
                e = 0;
            }
            model.world.push(Cell {
                cell_type: t,
                energy: e,
            });
        }
    }
}

// unbounded version - wraps around borders
fn neighbours(x: usize, y: usize) -> Vec<(usize, usize)> {
    let left = if x == 0 { DIM - 1 } else { x - 1 };
    let right = if x == DIM - 1 { 0 } else { x + 1 };
    let top = if y == 0 { DIM - 1 } else { y - 1 };
    let bottom = if y == DIM - 1 { 0 } else { y + 1 };
    vec![
    (left, top),
    (x, top),
    (right, top),
    (left, y),
    (right, y),
    (left, bottom),
    (x, bottom),
    (right, bottom),
    ]
}

// bounded version - respects borders
fn neighbours2(x: usize, y: usize) -> Vec<(usize, usize)> {
    let left = if x == 0 { 0 } else { x - 1 };
    let right = if x == DIM - 1 { DIM - 1 } else { x + 1 };
    let top = if y == 0 { 0 } else { y - 1 };
    let bottom = if y == DIM - 1 { DIM - 1 } else { y +1 };
    let mut result = vec![
    (left, top),
    (x, top),
    (right, top),
    (left, y),
    (right, y),
    (left, bottom),
    (x, bottom),
    (right, bottom),
    ];
    result.sort_unstable();
    result.dedup();
    result.retain(|t| !(t.0 == x && t.1 == y));
    result
}

fn neighbor_indices(index: usize) -> Vec<usize> {
    let x = index / DIM;
    let y = index % DIM;
    let coordinates = neighbours(x, y); // NOTE: here one can try neighbours2
    coordinates.iter().map(|c| c.0 * DIM + c.1).collect()
}

fn all_of_type(cell_type: CellType, world: &Vec<Cell>) -> Vec<usize> {
    let mut result = Vec::new();
    for (i, cell) in world.iter().enumerate() {
        if cell.cell_type == cell_type {
            result.push(i);
        }
    }
    result
}

fn neighbours_of_type(index: usize, cell_type: CellType, world: &Vec<Cell>) -> Vec<usize> {
    let mut result = Vec::new();
    for i in neighbor_indices(index) {
        if world[i].cell_type == cell_type {
            result.push(i);
        }
    }
    result
}

fn three_or_more_plants(world: &Vec<Cell>, index: usize) -> bool {
    let mut count: u8 = 0;
    for ndex in neighbor_indices(index) {
        if world[ndex].cell_type == CellType::Plant {
            count += 1;
            if count >= 3 {
                return true;
            }
        }
    }
    false
}

fn update(_app: &App, model: &mut Model, _update: Update) {        
    let mut rng = thread_rng();
    
    // rule #1: for every empty cell
    let empty_cells = all_of_type(CellType::Empty, &model.world);
    for index in empty_cells {
        if three_or_more_plants(&model.world, index) {
            model.world[index] = Cell { // grow new plant
                cell_type: CellType::Plant,
                energy: model.plant_energy_init,
            };
        }
    }
    
    // rule #2: for every herbivore
    let mut herbivores = all_of_type(CellType::Herbivore, &model.world);
    herbivores.shuffle(&mut rng);
    for index in herbivores {
        model.world[index].energy -= model.herbivore_energy_loss;
        if model.world[index].energy <= 0 {
            model.world[index].clear(); // dies if no energy left
        } else {
            let neighbour_plants = neighbours_of_type(index, CellType::Plant, &model.world);
            if let Some(&plant_index) = neighbour_plants.choose(&mut rng) { // find food (plant)
                model.world[index].energy += model.world[plant_index].energy;   // move to and eat nearby food
                model.world[plant_index] = model.world[index];
                model.world[index].clear();
                if model.world[plant_index].energy > model.herbivore_energy_baby {
                    model.world[index] = Cell { // enough energy spawns baby
                        cell_type: CellType::Herbivore,
                        energy: model.herbivore_energy_init,
                    }
                }
            } else {
                let neighbour_empty = neighbours_of_type(index, CellType::Empty, &model.world);
                if let Some(&empty_index) = neighbour_empty.choose(&mut rng) {
                    model.world[empty_index] = model.world[index];  // move to random empty space
                    model.world[index].clear();
                }
            }
        }
    }
    
    // rule#3: for every carnivore
    let mut carnivore = all_of_type(CellType::Carnivore, &model.world);
    carnivore.shuffle(&mut rng);
    for index in carnivore {
        model.world[index].energy -= model.carnivore_energy_loss;
        if model.world[index].energy <= 0 {
            model.world[index].clear(); // dies if no energy left
        } else {
            let neighbour_herbivores = neighbours_of_type(index, CellType::Herbivore, &model.world);
            if let Some(&herbivore_index) = neighbour_herbivores.choose(&mut rng) { // find food (herbivore)
                model.world[index].energy += model.world[herbivore_index].energy;   // move to and eat nearby food
                model.world[herbivore_index] = model.world[index];
                model.world[index].clear();
                if model.world[herbivore_index].energy > model.carnivore_energy_baby {
                    model.world[index] = Cell { // enough energy spawns baby
                        cell_type: CellType::Carnivore,
                        energy: model.carnivore_energy_init,
                    }
                }
            } else {
                let neighbour_empty = neighbours_of_type(index, CellType::Empty, &model.world);
                if let Some(&empty_index) = neighbour_empty.choose(&mut rng) {
                    model.world[empty_index] = model.world[index];  // move to random empty space
                    model.world[index].clear();
                } else {
                    let neighbour_plants = neighbours_of_type(index, CellType::Plant, &model.world);
                    if let Some(&plant_index) = neighbour_plants.choose(&mut rng) {
                        let plant = model.world[plant_index];
                        model.world[plant_index] = model.world[index];  // move through random plant
                        model.world[index] = plant;
                    }
                }
            }
        }
    }
}

fn view(app: &App, model: &Model, frame: Frame) {
    frame.clear(WHITE);
    let win = app.window_rect();
    let size_x = win.w() / DIM as f32;
    let size_y = win.h() / DIM as f32;
    let lower_left = pt2(win.w() - size_x, win.h() - size_y) * -0.5;
    let mut draw = app.draw().xy(lower_left);
    for (index, cell) in model.world.iter().enumerate() {
        let color = match cell.cell_type {
            CellType::Empty => rgb(1.0, 1.0, 1.0),
            CellType::Plant => rgb(0.0, 1.0, 0.0),
            CellType::Herbivore => rgb(1.0, 0.0, 0.0),
            CellType::Carnivore => rgb(0.0, 0.0, 1.0),
        };
        let x = (index / DIM) as f32 * size_x;
        let y = (index % DIM) as f32 * size_y;
        draw.rect().color(color).w_h(size_x * 0.9, size_y * 0.9).x_y(x, y);
    }
    
    let r = Rect::from_w_h(50.0, 15.0).top_left_of(win.pad(8.0));
    draw = draw.xy(-lower_left + r.xy());
    draw.rect().wh(r.wh()).color(rgba(1.0, 1.0, 1.0, 0.5));
    let text = format!("fps: {:.1}", app.fps());
    draw.text(&text).wh(r.wh()).left_justify().align_text_top().color(BLACK);
    draw.to_frame(app, &frame).unwrap();
}