Speed:
Creature maximum age:
Creatures can fall off sides
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.
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:
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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; }