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
-
string type
Creature type, to be used later in makeGrid( ) or makeGridWithDistribution( ).
Optional
-
int actionRadius
A creature's vision and movement range for each step.
- Default: 1
-
char character
ASCII character used to visually represent a creature.
- Default: undefined (fills cell)
-
int [3] color
RGB components of a creature's display color.
- Range: [0, 255]
- Default: random
-
function colorFn
How a creature's color is determined at each step.
- Returns: string of comma-separated RGBA components.
-
int efficiency
Conversion ratio of food to energy. Food energy × efficiency = gained energy.
- Default: 0.7
-
float initialEnergy
Energy level that a creature has at the start of its life.
- Range: (0, maxEnergy]
- Default: 50
-
function isDead
Determines whether a creature should be removed at the beginning of a step.
- Returns: boolean
- Default: Return true for creatures with energy <= 0. Return false always for cellular automata.
-
float maxEnergy
Maximum energy that a creature can have; excess energy is discarded.
- Default: 100
- Minimum: 0
-
function move
How a creature moves.
- Parameters: {coords, creature} [] neighbors
- Default: Look for edible creatures; if none are found, look for open spaces to move to; if none are found, wait.
- Returns: {x, y, creature, successFn} || false
-
float moveLv
- Default: 0
- Range: [0, 1]
Percentage of a creature's max energy below which it will stop moving (used in the default process method).
-
function process
Main entry point for behavior; called for each creature on each iteration.
- Parameters: {coords, creature} [] neighbors, int x, int y
- Default: Creatures reproduce if energy is sufficient, otherwise move if energy is sufficient, otherwise wait. Cellular automata do nothing by default.
- Returns:
- true: indicates that a change has occurred in the creature; if no creatures return a truthy value, the simulation terminates
- false
- {x, y, creature, observed}: a creature in a new position; observed acts like true and false above and allows watching for specific conditions
-
function reproduce
How a creature reproduces.
- Parameters: {coords, creature} [] neighbors
- Default: Look for neighboring open space; if any exists, randomly place a new creature and lose energy equal to the child's initialEnergy.
- Returns: {x, y, creature, successFn, failureFn} || false
-
float reproduceLv
Percentage of a creature's max energy above which it will reproduce (used in the default process method).
- Default: 0.7
- Range: [0, 1]
-
int size
A creature's size; by default, creatures can only eat creatures smaller than them.
- Default: 50
-
int sustainability
Number of visible food sources needed before a creature will eat.
- Default: 2
- Range: (0, 16 × actionRadius - 8]
-
function wait
What happens when a creature waits.
- Default: Creatures lose 5 energy. No effect for cellular automata.
-
* *
The best part about creatures is that you can add whatever you want to them! In addition to overriding any of the above properties, creatures will accept any methods and properties you throw at 'em.
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
-
int width
Number of cells in the x-direction.
-
int height
Number of cells in the y-direction.
Optional
-
string id
id assigned to the generated canvas.
-
int cellSize
Pixel width of each cell.
- Default: 10
-
string insertAfter
id of the element to insert the canvas after.
- Default: canvas is appended to
document.body
- Default: canvas is appended to
-
boolean periodic
Determines if boundaries wrap around; a creature at the top of a periodic map would see the bottom cells as though they were adjacent.
- Default: false
-
string neighborhood
Defines neighborhood type as either
moore
orvonNeumann
.- Default: moore
-
float trails
Allows for "trails", which visualize system history. A value of 1 shows all past state; trails fade faster as we approach 0.
- Range: [0, 1]
- Default: canvas is appended to
document.body
- Dependencies: "background" option is required if trails is set.
-
int [3] background
RGB components of the canvas' background.
- Range: [0, 255]
- Default: transparent
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!