‹ cuboidsspiral ›

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

//key 'space' to reset

//TODO:
//* set framerate (app.set_loop_mode(LoopMode::rate_fps(model.fps));)
//* 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::image;
use nannou::prelude::*;
use nannou::rand::seq::SliceRandom;
use nannou::rand::thread_rng;

const WIDTH: usize = 300;
const HEIGHT: usize = 300;

#[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,
    image: image::RgbaImage,
    texture: wgpu::Texture,
}
fn model(app: &App) -> Model {
    app.new_window()
        .title("eco")
        .size(800, 600)
        .raw_event(raw)
        .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 image = image::ImageBuffer::new(WIDTH as u32, HEIGHT as u32);
    let texture = wgpu::TextureBuilder::new()
        .size([WIDTH as u32, HEIGHT as u32])
        .format(wgpu::TextureFormat::Rgba8Unorm)
        .build(app.main_window().swap_chain_device());

    let mut model = Model {
        fps,
        world: Vec::with_capacity(HEIGHT * WIDTH),
        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,
        image,
        texture,
    };
    create_world(&mut model);
    model
}

fn raw(_app: &App, model: &mut Model, event: &nannou::winit::event::WindowEvent) {
    use nannou::winit::event::*;
    match event {
        WindowEvent::ReceivedCharacter(' ') => create_world(model),
        WindowEvent::ReceivedCharacter('+') => model.fps = (model.fps + 3.0).min(120.0),
        WindowEvent::ReceivedCharacter('-') => model.fps = (model.fps - 3.0).max(1.0),
        _ => {}
    }
}

fn create_world(model: &mut Model) {
    model.world.clear();
    for _i in 0..(HEIGHT * WIDTH) {
        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 { WIDTH - 1 } else { x - 1 };
    let right = if x == WIDTH - 1 { 0 } else { x + 1 };
    let top = if y == 0 { HEIGHT - 1 } else { y - 1 };
    let bottom = if y == HEIGHT - 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 == WIDTH - 1 { WIDTH - 1 } else { x + 1 };
    let top = if y == 0 { 0 } else { y - 1 };
    let bottom = if y == HEIGHT - 1 { HEIGHT - 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(i: usize) -> Vec<usize> {
    let x = i % WIDTH;
    let y = i / WIDTH;
    let coordinates = neighbours(x, y); // NOTE: here one can try neighbours2
    coordinates.iter().map(|c| c.0 + c.1 * WIDTH).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(i: usize, cell_type: CellType, world: &Vec<Cell>) -> Vec<usize> {
    let mut result = Vec::new();
    for i in neighbor_indices(i) {
        if world[i].cell_type == cell_type {
            result.push(i);
        }
    }
    result
}

fn three_or_more_plants(world: &Vec<Cell>, i: usize) -> bool {
    let mut count: u8 = 0;
    for j in neighbor_indices(i) {
        if world[j].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 i in empty_cells {
        if three_or_more_plants(&model.world, i) {
            model.world[i] = 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 i in herbivores {
        model.world[i].energy -= model.herbivore_energy_loss;
        if model.world[i].energy <= 0 {
            model.world[i].clear(); // dies if no energy left
        } else {
            let neighbour_plants = neighbours_of_type(i, CellType::Plant, &model.world);
            if let Some(&plant_index) = neighbour_plants.choose(&mut rng) {
                // find food (plant)
                model.world[i].energy += model.world[plant_index].energy; // move to and eat nearby food
                model.world[plant_index] = model.world[i];
                model.world[i].clear();
                if model.world[plant_index].energy > model.herbivore_energy_baby {
                    model.world[i] = Cell {
                        // enough energy spawns baby
                        cell_type: CellType::Herbivore,
                        energy: model.herbivore_energy_init,
                    }
                }
            } else {
                let neighbour_empty = neighbours_of_type(i, CellType::Empty, &model.world);
                if let Some(&empty_index) = neighbour_empty.choose(&mut rng) {
                    model.world[empty_index] = model.world[i]; // move to random empty space
                    model.world[i].clear();
                }
            }
        }
    }

    // rule#3: for every carnivore
    let mut carnivore = all_of_type(CellType::Carnivore, &model.world);
    carnivore.shuffle(&mut rng);
    for i in carnivore {
        model.world[i].energy -= model.carnivore_energy_loss;
        if model.world[i].energy <= 0 {
            model.world[i].clear(); // dies if no energy left
        } else {
            let neighbour_herbivores = neighbours_of_type(i, CellType::Herbivore, &model.world);
            if let Some(&herbivore_index) = neighbour_herbivores.choose(&mut rng) {
                // find food (herbivore)
                model.world[i].energy += model.world[herbivore_index].energy; // move to and eat nearby food
                model.world[herbivore_index] = model.world[i];
                model.world[i].clear();
                if model.world[herbivore_index].energy > model.carnivore_energy_baby {
                    model.world[i] = Cell {
                        // enough energy spawns baby
                        cell_type: CellType::Carnivore,
                        energy: model.carnivore_energy_init,
                    }
                }
            } else {
                let neighbour_empty = neighbours_of_type(i, CellType::Empty, &model.world);
                if let Some(&empty_index) = neighbour_empty.choose(&mut rng) {
                    model.world[empty_index] = model.world[i]; // move to random empty space
                    model.world[i].clear();
                } else {
                    let neighbour_plants = neighbours_of_type(i, 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[i]; // move through random plant
                        model.world[i] = plant;
                    }
                }
            }
        }
    }

    for (i, col) in model.image.pixels_mut().enumerate() {
        let cell = model.world[i];
        *col = match cell.cell_type {
            CellType::Empty => image::Rgba([255, 255, 255, std::u8::MAX]),
            CellType::Plant => image::Rgba([0, 255, 0, std::u8::MAX]),
            CellType::Herbivore => image::Rgba([255, 0, 0, std::u8::MAX]),
            CellType::Carnivore => image::Rgba([0, 0, 255, std::u8::MAX]),
        }
    }
}

fn view(app: &App, model: &Model, frame: Frame) {
    frame.clear(WHITE);
    let win = app.window_rect();
    let draw = app.draw();

    model.texture.upload_data(
        app.main_window().swap_chain_device(),
        &mut *frame.command_encoder(),
        &model.image.as_flat_samples().as_slice(),
    );
    let scale = (win.h() / HEIGHT as f32).min(win.w() / WIDTH as f32);
    draw.texture(&model.texture)
        .w(WIDTH as f32 * scale)
        .h(HEIGHT as f32 * scale);

    let r = Rect::from_w_h(50.0, 15.0).top_left_of(win.pad(8.0));
    draw.rect()
        .xy(r.xy())
        .wh(r.wh())
        .color(rgba(1.0, 1.0, 1.0, 0.5));
    let text = format!("fps: {:.1}", app.fps());
    draw.text(&text)
        .xy(r.xy())
        .wh(r.wh())
        .left_justify()
        .align_text_top()
        .color(BLACK);

    draw.to_frame(app, &frame).unwrap();
}