terra is a super customizable library for creating and analyzing biological simulations. It's open-source and licensed under MIT.

Usage

Including terra

Getting started is as easy as including the script!

<script src="//cdn.jsdelivr.net/terra/latest/mainfile"></script>

terra can also be used as a module with most popular module systems: ?

// CommonJS
var terra = require('./terra.min.js');
// ...

// AMD
define(['./terra.min.js'] , function (terra) {
    return function () { //... };
});

// ...and if you're not using a module system, it'll be in
window.terra;

If you manage dependencies with Bower, you're in luck!

bower install terra

Creating creatures

Let's create a simple creature using the registerCreature method. Each creature requires a type.

terra.registerCreature({
  type: 'firstCreature'
});

This creature is valid, but it's pretty boring. To make a more interesting creature, let's override some of the default attributes and methods.

terra.registerCreature({
  type: 'secondCreature',
  color: [120, 0, 240],
  sustainability: 6,
  reproduceLv: 1
});

We've just created a purple creature that only eats if 6 or more plants are around it. These creatures basically seek out an edge or corner and die there.

Creating the environment

To run a simulation, we'll need to create an environment. Let's make a 25x25 grid, populate 10% of the space with our lonely purple creature, and fill the rest with simple plants.

var ex1 = new terra.Terrarium(25, 25, {id: 'ex1'});
ex1.grid = ex1.makeGridWithDistribution([['secondCreature', 10], ['simplePlant', 90]]);

Running a simulation

Terrariums have a few methods that allow you to interact with the simulation. Let's animate it and see how it does for the first 300 steps.

ex1.animate(300);

That's all there is to it! Though it's possible to generate complex behaviours by simply overriding default values, the real fun comes when you realize that creatures are entirely customizable.

Examples

Conway's Game of Life ?

var gameOfLife = new terra.Terrarium(25, 25, {
  trails: 0.9,
  periodic: true,
  background: [22, 22, 22]
});

terra.registerCA({
  type: 'GoL',
  colorFn: function () { return this.alive ? this.color + ',1' : '0,0,0,0'; },
  process: function (neighbors, x, y) {
    var surrounding = neighbors.filter(function (spot) {
      return spot.creature.alive;
    }).length;
    this.alive = surrounding === 3 || surrounding === 2 && this.alive;
    return true;
  }
}, function () {
  this.alive = Math.random() < 0.5;
});

gameOfLife.grid = gameOfLife.makeGrid('GoL');
gameOfLife.animate();

Cyclic Cellular Automaton ?

var cyclic = new terra.Terrarium(100, 100);

terra.registerCA({
  type: 'cyclic',
  colors: ['255,0,0,1', '255,96,0,1', '255,191,0,1', '223,255,0,1', '128,255,0,1', '32,255,0,1', '0,255,64,1', '0,255,159,1', '0,255,255,1', '0,159,255,1', '0,64,255,1', '32,0,255,1', '127,0,255,1', '223,0,255,1', '255,0,191,1', '255,0,96,1'],
  colorFn: function () { return this.colors[this.state];},
  process: function (neighbors, x, y) {
    var next = (this.state + 1) % 16;
    var changing = neighbors.some(function (spot) {
      return spot.creature.state === next;
    });
    if (changing) this.state = next;
    return true;
  }
}, function () {
  this.state = Math.floor(Math.random() * 16);
});

cyclic.grid = cyclic.makeGrid('cyclic');
cyclic.animate();

Brutes and Bullies

// the demo running at the top of this page
var bbTerrarium = new terra.Terrarium(25, 25);

terra.registerCreature({
  type: 'plant',
  color: [0, 120, 0],
  size: 10,
  initialEnergy: 5,
  maxEnergy: 20,
  wait: function() {
    // photosynthesis :)
    this.energy += 1;
  },
  move: false,
  reproduceLv: 0.65
});

terra.registerCreature({
  type: 'brute',
  color: [0, 255, 255],
  maxEnergy: 50,
  initialEnergy: 10,
  size: 20
});

terra.registerCreature({
  type: 'bully',
  color: [241, 196, 15],
  initialEnergy: 20,
  reproduceLv: 0.6,
  sustainability: 3
});

bbTerrarium.grid = bbTerrarium.makeGridWithDistribution([['plant', 50], ['brute', 5], ['bully', 5]]);
bbTerrarium.animate();

Rule 146

var elementary = new terra.Terrarium(150, 150);

terra.registerCA({
  type: 'elementary',
  alive: false,
  ruleset: [1, 0, 0, 1, 0, 0, 1, 0].reverse(), // rule 146
  colorFn: function () { return this.alive ? this.color + ',1' : '0,0,0,0'; },
  process: function (neighbors, x, y) {
    if (this.age === y) {
      var index = neighbors.filter(function (neighbor) { return neighbor.coords.y === y - 1;
      }).map(function (neighbor) { return neighbor.creature.alive ? 1 : 0; });
      index = parseInt(index.join(''), 2);
      this.alive = isNaN(index) ? !x : this.ruleset[index];
    }
    return true;
  }
});

elementary.grid = elementary.makeGrid('elementary');
elementary.animate();

If you come up with a cool example, let me know! I'll add it to this list and credit you.

Creatures

Creatures are registered with

terra.registerCreature(options, init)
or
terra.registerCA(options, init)
for cellular automata.

The following methods and attributes can be passed in an object as the first argument:

Required

Optional

The second argument to the terra.registerCreature and terra.registerCA is the init function. This function is run within a creature's constructor and allows you to set different attributes for individual creatures. For example, in the Cyclic Cellular Automaton example above we see the following init function:

function () {
  this.state = Math.floor(Math.random() * 16);
});

Whenever a new creature is created of type 'cyclic', it will be randomly assigned a state of 0 to 15.

Terrarium

Terrariums are where the action happens. They're initialized with the following constructor:

//new terra.Terrarium(width, height, {options});
//example: create a 4x4 terrarium called #myTerrarium after element #bugStory
var t = new terra.Terrarium(4, 4, {
  id: 'myTerrarium',
  cellSize: 15,
  insertAfter: document.getElementById('bugStory')
});

Required

Optional

Once initialized, terrariums have a few exposed methods. Using our terrarium t that we just created:

//initial setup
var a = 'a', b = 'b';
terra.registerCreature({type: a});
terra.registerCreature({type: b});

/* makeGrid(content)
 *
 * Returns a grid populated using a function, 2-d array, or uniform type */
// example: fill the terrarium with the 'b' creature type
t.grid = t.makeGrid(b);

// example: fill the terrarium with a checkerboard pattern
t.grid = t.makeGrid(function (x, y) {
  return (x + y) % 2 ? a : b;
});

// example: fill the terrarium's left half with 'a', right half with 'b'
t.grid = t.makeGrid([
  [a, a, b, b],
  [a, a, b, b],
  [a, a, b, b],
  [a, a, b, b]
]);

/* makeGridWithDistribution(distribution)
 *
 * Returns a grid populated randomly with a set creature distribution, where distribution
 *   is an array of arrays of the form [string 'creatureName', float fillPercent] */
// example: fill the terrarium randomly with approximately half 'a' and half 'b'
t.grid = t.makeGridWithDistribution([[a, 50], [b, 50]]);

/* step(steps)
 *
 * Returns the next step of the simulation, or the grid after <steps> steps if specified */
// example: advance the terrarium 10 steps in the future
t.grid = t.step(10);

/* draw()
 *
 * Updates the terrarium's canvas to reflect the current grid */
// example: display all the work we've done above
t.draw();

/* animate(steps, fn)
 * animate(steps)
 *
 * Starts animating the simulation. The simulation will stop after <steps> steps
 *   if specified, and call <fn> as a callback once the animation finishes. */
// example: animate the terrarium
t.animate();

/* stop()
 *
 * stops a currently running simulation */
// example: stop the animation that we just started
t.stop();

Still want more? Check out the source on GitHub!