spencerkuan.co
Genetic Evolution Simulation

Genetic Evolution Simulation

Speed:

Creature maximum age:

Creatures can fall off sides

Basic Explanation

The Board

The board is a 64 by 64 box grid. Objects can only occupy one square on the grid. No two things can occupy the same position on the board.

Creatures are represented on the canvas by blue and purple squares. The blue creatures and the purple creatures are two different genders of the same species. Other than color and gender, the two genders of creatures are identical.

Food is represented on the board by green squares.

White squares are empty spots on the board. A white square means neither food nor a creature occupies that spot.

Creature Movement

Every frame, a creature moves in one of 9 different ways. After every movement, a creature loses 1 food point. The possible directions a creature can move include:

  1. Move up
  2. Move up and right
  3. Move right
  4. Move right and down
  5. Move down
  6. Move down and left
  7. Move left
  8. Move left and up
  9. Don’t move

When moving, if the square the creature is trying to is not empty, one of several things can happen.

Procreation

When two creatures procreate, an offspring is created on a square not occupied by another creature and is adjacent to the creature that moved last. If no such square exists, the two creatures do not procreate. The two creatures also do not procreate if either creature has less than 25 food points. The new creature inherits behavioral genes and physical genes from both parents. Some of these genes are mutated based on the mutation rate. After procreating, both parents lose 25 food points each. The new creature starts with 50 food points.

Death

If a creature at any time has less than zero food points or is older than the maximum age, that creature dies. Dead creatures are removed from the board.

Neural Networks and Behavioral Genes

Creature’s actions are based on a neural network. I used Synaptic.js to achieve this. The neural network follows a multilayer Perceptron architecture with an input layer 8 nodes, a single hidden layer of 3 nodes, and an output layer of 9 nodes. An explanation of how neural networks work by the University of Wisconson-Madison can be found here. The weights and biases of the neural networks controlling the creature’s actions are not determined from backpropagation. Rather, they are encoded in a creature’s behavioral genes. The neural network of a creature receives eight inputs, one for each square next to the creature. Each input is a number between 0 and 1 based on what kind of object is on that square. The distinct types of objects a creature’s neural network can recognize include: creature of the same gender, creature of a different gender, food, nothing, square that is not on the board. The neural network outputs nine different numbers. The direction that the creature moves is determined by which output has the highest value.

Physical Genes

The only physical trait currently associated with a creature's genes is the number of children that creature has. A creature has two different gene pairs that deturmine this. Each gene pair acts as a 1 or 0. The total of these two numbers gives how many children a given creature will have every time it procreates. Having two (or more) children takes twice as many food points (or more) away from the parents.

Food

Every frame, one-fifteenth of the amount of food that was in the previous frame grows. For example, if there were 150 squares containing food in the previous frame, 10 empty squares are filled with food in the next frame. The food does not have genes, and cannot move.

Simulation Controls

Various things about the simulation can be manipulated to observe effects on the creatures over time. If all of the creatures die, you can click the reset button to reset the simulation. Resetting the simulation will not change the values of the simulation settings.

Graphs

I included graphs of the populations of both the food and the creatures so relationships between the two could be observed. These graphs record the current populations every 10 frames and delete data points older than 2,500 frames to reduce the times your computer crashes or becomes unresponsive from the simulation.

Things to Notice

Population Cycles

Over time, the creature’s population fluctuates through cycles of increased population with lots of food, and then lower population due to over population and not enough food. The population of the food also fluctuates inversely to the creature population. This was not explicitly coded into the simulation, but rather emerged while I was coding the simulation.

Creature Behavior Evolution

The creature’s tend to run in patterns at the beginning of the simulation because of the random weight and bias initialization of the creature’s neural networks. Over time, the creatures do this less because creatures that don’t just run in straight lines or circles but rather react to their environment are more likely to survive.

About the Creation of this Simulation

Apart from the graphs and Synaptic.js, I coded this simulation from scratch in JavaScript(the actual simulation), HTML(the website), and CSS(to style the website). At the time of writing this, the simulation has taken me about 50 hours to make, with lots of experimentation. The JavaScript code itself is about 500 lines long. The original intent was to create a simulation of Darwinian evolution, which I think I did create. However, it is somewhat hard to observe this. I think the end result is now more of a simulation of a contained environment, with population cycles and interactions between the food and the creatures. I learned a lot of things and had a lot of fun creating this simulation. I’m probably going to continue to work on it after the quarter end.

JavaScript Code

Here is the actual JavaScript code for the simulation:

//Get the food slider stuff
const ageSlider = document.getElementById("ageSlider");
const ageLabel = document.getElementById("ageLabel");
ageLabel.innerHTML = ageSlider.value;
var maxAge = ageSlider.value;

//get the speed slider stuff
const speedSlider = document.getElementById("speedSlider");
const speedLabel = document.getElementById("speedLabel");
speedLabel.innerHTML = speedSlider.value;

//get the reset button
const resetButton = document.getElementById("resetButton");

//get the fall off sides check box
const fallBox = document.getElementById("fallBox");
var fallOff = false;

//get chart div
const popChart = document.getElementById("populationChart");

Plotly.plot(popChart, [{
  y: [],
  type: 'scatter',
}],{
  title: "Creature Population"
}); 

const popChart2 = document.getElementById("populationChart2");

Plotly.plot(popChart2, [{
  y: [],
  type: 'scatter',
}],{
  title: "Plant Population"
});

const allPhysicalGenes = [0,1];
const physGeneKey = [1,0];

var physAlleleKey = [];
for(var i = 0; i < 9; i++){
  physAlleleKey.push(randInt(2));
}


const mutationProb = 0.01;
const physicalChromosomeLength = 2;

//define canvas
const canvas = document.getElementById("myCanvas");
const ctx = canvas.getContext("2d");

//define canvas size
const boardSize = 64;
const squares = boardSize*boardSize;
const squareSize = canvas.width/boardSize;

//define the board
var board = new Array(boardSize);
for(var i = 0; i < board.length; i++){
    board[i] = new Array(boardSize).fill(0);
}                  

//define different directions
const directions = [[-1,-1],[-1,0],[-1,1],[0,-1],[0,1],[1,-1],[1,0],[1,1]];

//define random item chooser with no repeats
function randomNoRepeats(array) {
    var copy = array.slice(0);
    if (copy.length < 1) { 
    copy = array.slice(0); 
  }

    var index = Math.floor(randInt(copy.length));
    var item = copy[index];
    copy.splice(index, 1);
    return item;
}

//random integer function
function randInt(max){
  return Math.floor(Math.random()*(max));
}

//define a function to get all indexes from an array
function getAllIndexes(arr, val) {
    var indexes = [], i;
    for(i = 0; i < arr.length; i++)
        if (arr[i] === val)
            indexes.push(i);
    return indexes;
}

//define the array of organisms
var creatures = [];
var foods = [];

var Food = function(positionX, positionY){
  //define position
  this.x = positionX;
  this.y = positionY;

  //add food to the board
  board[this.x][this.y] = this;
  foods.push(this)
};

Food.prototype.draw = function(){
  //draw the food
  ctx.fillStyle = "green";
  ctx.fillRect(this.x*(squareSize), this.y*squareSize, squareSize, squareSize);
};

Food.prototype.die = function(){
  //delete the food from the board
  board[this.x][this.y] = 0;
  foods.splice(foods.indexOf(this),1);
};

var Creature = function(physicalGenes, behaviorGenes, positionX, positionY){
  //define variables
  this.pgenes = physicalGenes;
  this.bgenes = behaviorGenes;
  this.x = positionX;
  this.y = positionY;
  this.age = 0;
  this.gender = randInt(2);
  this.food = 50;
  this.lastDirection = randInt(8);

  //decode the physical genes
  this.decodedPGenes = [];
  for(var i = 0; i < this.pgenes.length; i++){
    if(this.pgenes[i][0]===this.pgenes[i][1]){
      this.decodedPGenes.push(this.pgenes[i][0]);
    }else{
      this.decodedPGenes.push(1);
    }
      
  }

  this.numberOfChildren = this.decodedPGenes[0]+this.decodedPGenes[1];

  //decode the behavior genes
  var decodedInputWeights = [];
  var decodedInputBiases = [];
  var decodedHiddenWeights = [];
  var decodedHiddenBiases = [];

  for(var i = 0; i < 78; i++){
    const val = this.bgenes[i][0]+this.bgenes[i][1];  
    if(i<12){
      decodedInputWeights.push(val);
    }else if(i<24){
      decodedInputBiases.push(val);
    }else if(i<51){
      decodedInputWeights.push(val);
    }else if(i<78){
      decodedHiddenBiases.push(val);
    }
  }

  //create the neural net
  this.network = new synaptic.Architect.Perceptron(8,3,9);

  var a = 0;
  for (key in this.network.layers.input.connectedTo[0].connections) {
      if (this.network.layers.input.connectedTo[0].connections.hasOwnProperty(key)) {
          this.network.layers.input.connectedTo[0].connections[key].weight = decodedInputWeights[a];
          this.network.layers.input.connectedTo[0].connections[key].to.bias = decodedInputBiases[a];

          //this.network.layers.input.connectedTo[0].connections[key].weight = Math.random()-0.5;
          //this.network.layers.input.connectedTo[0].connections[key].to.bias = Math.random()-0.5;
          a++;
      } 
  }

  a = 0;
  for (key in this.network.layers.hidden[0].connectedTo[0].connections) {
      if (this.network.layers.hidden[0].connectedTo[0].connections.hasOwnProperty(key)) {
          this.network.layers.hidden[0].connectedTo[0].connections[key].weight = decodedHiddenWeights[a];
          this.network.layers.hidden[0].connectedTo[0].connections[key].to.bias = decodedHiddenBiases[a];
          //this.network.layers.hidden[0].connectedTo[0].connections[key].weight = Math.random()-0.5;
          //this.network.layers.hidden[0].connectedTo[0].connections[key].to.bias = Math.random()-0.5;
          a++;
      } 
  }

  //add creature to board and organism arrays
  board[this.x][this.y] = this;
  creatures.push(this);

};

Creature.prototype.draw = function(){
  //draw the creature
  if(this.gender===1){
    ctx.fillStyle = "purple";
  }else if(this.gender===0){
    ctx.fillStyle = "blue";
  }
  ctx.fillRect(this.x*(squareSize), this.y*squareSize, squareSize, squareSize);
} ;
Creature.prototype.update = function(){
    var dw = [];

    if(dw.includes(NaN)){
        console.log("Error!!! One of a creatures genes are undefined!");
  }   
  
  var inputs = [];

  const ajacents = [[-1,0],[0, -1],[1, 0],[0, 1]];
  for(var i = 0; i < directions.length; i++){
    const i2 = (i+this.lastDirection)%8;
    if(this.x+directions[i2][0]>=0&&this.x+directions[i2][0]=0&&this.y+directions[i2]maxAge || this.food < 0){
    this.die();
  }
  this.age+=1; 
  this.food-=1;
};
Creature.prototype.move = function(dx,dy){
  //Update the creature's position if another creature doesn't occupy that spot
  if(this.x+dx=0&&this.y+dy=0){
    if(!(board[(this.x+dx)][(this.y+dy)] instanceof Creature)){
      if(board[(this.x+dx)][(this.y+dy)] instanceof Food){
        this.food+=10;
        board[(this.x+dx)][(this.y+dy)].die();
      }
      board[this.x+dx][this.y+dy] = this;
      board[this.x][this.y] = 0;
      this.x+=dx;
      this.y+=dy;
    }else if(board[(this.x+dx)][(this.y+dy)] instanceof Creature && board[(this.x+dx)][(this.y+dy)].gender === (1-this.gender)){
       this.procreate(this, board[(this.x+dx)][(this.y+dy)]);
    }
  }else if(fallOff){
        this.die();
    }
    this.food-=1; 
};

Creature.prototype.die = function(){
  //remove the creature from the board
  board[this.x][this.y] = 0;
  creatures.splice(creatures.indexOf(this),1);
};

Creature.prototype.procreate = function(creature){
  if(this.food>=25*this.numberOfChildren&&creature.food>=25*this.numberOfChildren){
    for(var k = 0; k < this.numberOfChildren; k++){
      var x = -1;
      var y = -1;
      for(var i = 0; i < directions.length; i++){
        const d = randomNoRepeats(directions);
        if(this.x+d[0]=0&&this.y+d[1]=0){
        if(!(board[this.x+d[0]][this.y+d[1]] instanceof Creature)){
            x = this.x+d[0];
            y = this.y+d[1];
          }
        }
          }
          if(x!==-1 && y!==-1){
          if(board[x][y]!==0){
            board[x][y].die();
          }
          new Creature(this.calcGenes(this.pgenes, creature.pgenes), this.calcGenes(this.bgenes, creature.bgenes, function(g){return g+(Math.random()-0.5)}), x, y);
          this.food -= 25;
              creature.food -= 25;
          }
    }
  }
};

Creature.prototype.calcGenes = function(genes1, genes2, possibleGeneFunction){
  const geneFunction = possibleGeneFunction||function(g){return allPhysicalGenes[randInt(allPhysicalGenes.length)]};

  //calculate the genes
  var newGenes = [];
  for(var i = 0; i < genes1.length; i++){
    //figure out which alleles are inherited
    var thisAllele = genes1[i][randInt(2)];
    var thatAllele = genes2[i][randInt(2)]

    //mutate the genes using the mutation probability
    if(Math.random() <= mutationProb){
      thisAllele = geneFunction(thisAllele);
    }

    if(Math.random() <= mutationProb){
      thatAllele = geneFunction(thatAllele);
    }

    //add them to the new string of genes
    newGenes.push([thisAllele, thatAllele]);
  }
  return newGenes;
};

var updateBoard = function(){
  //update creatures
    for(var i = 0; i < creatures.length; i++){
      creatures[i].update();
    }

  //draw board
    for(var a  = 0; a < board.length; a++){
        for(var b = 0; b < board.length; b++){
          if(board[a][b]!==0){
            board[a][b].draw();
          }
        }
    }
    
};

var makeCreatures = function(){
  for(var i = 0; i < squares/20; i++){
    var a = randInt(boardSize);
    var b = randInt(boardSize);
    if(board[a][b]!== 0){
      board[a][b].die();
    }

    var bgenes = [];
    for(var m = 0; m < 78; m++){
      bgenes.push([Math.random()/10-0.05, Math.random()/10-0.05]);
    }

    var pgenes = [];
    for(var m = 0; m < physicalChromosomeLength; m++){
      pgenes.push([randInt(2), randInt(2)]);
    }
    new Creature(pgenes,bgenes, a, b);
  } 
}

var makeFood = function(){
  //make food
  for(var i = 0; i < squares*2; i++){
    var a = randInt(boardSize);
    var b = randInt(boardSize);
    if(board[a][b]=== 0){
      new Food(a,b);
    }
    
  }
}

var restart = function(){
  clearInterval(Mainloop);
  Mainloop = setInterval(draw,1000-speedSlider.value);
  board = [];
    board = new Array(boardSize);
  for(var i = 0; i < board.length; i++){
      board[i] = new Array(boardSize).fill(0);
  }
  creatures = [];
    foods = [];
    counter = 0;

  //make food
  makeFood();

  //make creaures
    makeCreatures();
    
  Plotly.newPlot(popChart, [{
    y: [],
    type: 'scatter',
  }],{
    title: "Creature Population"
  }); 


  Plotly.newPlot(popChart2, [{
    y: [],
    type: 'scatter',
  }],{
    title: "Plant Population"
  });
};


//make food
makeFood();

//make creaures
makeCreatures();

counter = 0;

//create animation loop
var draw = function(){
  //clear the board
  ctx.clearRect(0,0,canvas.width,canvas.height);
  
  //draw the board
  updateBoard();

  //add more food
  const foodPop = foods.length/10;
    for(var i = 0; i < (foodPop); i++){
      const a = randInt(boardSize);
      const b = randInt(boardSize);
      if(board[a][b]===0){
        new Food(a, b);
    }
    }

    if(counter===0){
        Plotly.extendTraces(popChart, { y:[[creatures.length]]}, [0], 250);
        Plotly.extendTraces(popChart2, { y:[[foods.length]]}, [0], 250);
    }
    counter++;
    counter %= 10;

    if(creatures.length===0){
        clearInterval(Mainloop);
        allDead();
    }
}

var allDead = function(){
  ctx.fillStyle = "rgba(50,50,50,0.7)";
  ctx.fillRect(0,0,boardSize*squareSize,boardSize*squareSize);
  ctx.font = "30px Arial";
  ctx.fillStyle = "#fff";
  ctx.textAlign = "center";
  ctx.fillText("All of the creatures died, click 'Reset' to restart.", canvas.width/2, canvas.height/2);
}

//start animation loop
var Mainloop = setInterval(draw,1000-speedSlider.value);


ageSlider.oninput = function(){
  ageLabel.innerHTML = this.value;
  maxAge = this.value; 
}

speedSlider.oninput = function(){
  speedLabel.innerHTML = speedSlider.value;
  clearInterval(Mainloop);
  Mainloop = setInterval(draw, 1000-this.value);
}

resetButton.onclick = function(){
    restart();
}

fallBox.onchange = function(){
  fallOff = this.checked;
}