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