eco
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: f64,
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 = 6.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().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().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();
}