├── img ├── bg.jpg ├── bg1.jpg └── map.jpg ├── poster.jpg ├── README.md ├── model ├── Obstacle.js └── Organism.js ├── index.html └── sketch.js /img/bg.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ljmocic/evolutionary-game-of-life/HEAD/img/bg.jpg -------------------------------------------------------------------------------- /img/bg1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ljmocic/evolutionary-game-of-life/HEAD/img/bg1.jpg -------------------------------------------------------------------------------- /img/map.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ljmocic/evolutionary-game-of-life/HEAD/img/map.jpg -------------------------------------------------------------------------------- /poster.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ljmocic/evolutionary-game-of-life/HEAD/poster.jpg -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # evolutionary-game-of-life 2 | 3 | ## Live preview 4 | * [demo](https://goo.gl/Uir7yJ) 5 | * [video](https://goo.gl/YB6Q3e) 6 | 7 | ## Poster 8 | ![poster.jpg](https://github.com/ljmocic/evolutionary-game-of-life/blob/master/poster.jpg) 9 | 10 | -------------------------------------------------------------------------------- /model/Obstacle.js: -------------------------------------------------------------------------------- 1 | function Obstacle(position, width, height) { 2 | 3 | this.position = position; 4 | this.width = width; 5 | this.height = height; 6 | 7 | this.draw = function () { 8 | stroke(0); 9 | fill(183); 10 | strokeWeight(1); 11 | rectMode(CORNER); 12 | rect(this.position.x, this.position.y, this.width - 10, this.height - 10); 13 | } 14 | 15 | this.contains = function (spot, radius) { 16 | if (spot.x > this.position.x && spot.x < this.position.x + width 17 | && spot.y > this.position.y && spot.y < this.position.y + height) { 18 | return true; 19 | } else { 20 | return false; 21 | } 22 | } 23 | 24 | } -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 |
15 |
16 |
17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | -------------------------------------------------------------------------------- /model/Organism.js: -------------------------------------------------------------------------------- 1 | function Organism() { 2 | 3 | this.fitness = 0; 4 | 5 | // position 6 | this.position = createVector(random(frameWidth - 20) + 10, random(frameHeight - 20) + 10); 7 | this.velocity = createVector(random(this.maxSpeed), random(this.maxSpeed)); 8 | this.acceleration = createVector(0, 0); 9 | this.desiredPosition = createVector(random(frameWidth - 20) + 10, random(frameHeight - 20) + 10); 10 | this.maxForce = random(1); 11 | this.maxSpeed = random(10); 12 | 13 | // health 14 | this.health = 100; 15 | 16 | // body 17 | this.radius = 10; 18 | this.sight = random(5, 35); 19 | 20 | // color 21 | this.colorR = random(255); 22 | this.colorG = random(255); 23 | this.colorB = random(255); 24 | 25 | this.calculateFitness = function () { 26 | var score = 0; 27 | 28 | score += this.health; 29 | 30 | //score += Math.abs(Math.pow(this.velocity.x, 2) + Math.pow(this.velocity.y, 2)); 31 | score += this.maxForce; 32 | score += this.maxSpeed; 33 | score += this.radius; 34 | score += this.sight; 35 | 36 | if (score > maxFitness) { 37 | maxFitness = score; 38 | } 39 | 40 | this.fitness = score; 41 | }; 42 | 43 | this.randomMutation = function () { 44 | 45 | var randomNumber = Math.floor(random(6)); 46 | 47 | if (randomNumber < 1) { 48 | if (random(1) < 0.5) { 49 | this.sight = this.sight * (random(1) + 1); 50 | } 51 | else { 52 | this.sight = this.sight * random(1); 53 | } 54 | } 55 | else if (randomNumber < 2) { 56 | if (random(1) < 0.5) { 57 | this.radius = this.radius * (random(1) + 1); 58 | } 59 | else { 60 | this.radius = this.radius * random(1); 61 | } 62 | } 63 | else if (randomNumber < 4) { 64 | if (random(1) < 0.5) { 65 | this.maxForce = this.maxForce * (random(1) + 1); 66 | } 67 | else { 68 | this.maxForce = this.maxForce * random(1); 69 | } 70 | } 71 | else if (randomNumber < 5) { 72 | if (random(1) < 0.5) { 73 | this.maxSpeed = this.maxSpeed * (random(1) + 1); 74 | } 75 | else { 76 | this.maxSpeed = this.maxSpeed * random(1); 77 | } 78 | } 79 | 80 | }; 81 | 82 | this.eat = function () { 83 | 84 | // eat food 85 | var minDistance = Infinity; 86 | var minIndex = -1; 87 | 88 | for (var i = 0; i < food.length; i++) { 89 | 90 | var distance = int(dist(this.position.x, this.position.y, food[i].x, food[i].y)); 91 | 92 | if (distance < minDistance) { 93 | minDistance = distance; 94 | minIndex = i; 95 | } 96 | 97 | } 98 | 99 | if (minDistance < this.radius) { 100 | food.splice(minIndex, 1); 101 | this.health += 50; 102 | } 103 | 104 | // drink water 105 | minDistance = Infinity; 106 | minIndex = -1; 107 | 108 | for (var i = 0; i < water.length; i++) { 109 | 110 | var distance = int(dist(this.position.x, this.position.y, water[i].x, water[i].y)); 111 | 112 | if (distance < minDistance) { 113 | minDistance = distance; 114 | minIndex = i; 115 | } 116 | 117 | } 118 | 119 | if (minDistance < this.radius) { 120 | water.splice(minIndex, 1); 121 | this.health += 10; 122 | } 123 | 124 | // poison 125 | minDistance = Infinity; 126 | minIndex = -1; 127 | 128 | for (var i = 0; i < poison.length; i++) { 129 | 130 | var distance = int(dist(this.position.x, this.position.y, poison[i].x, poison[i].y)); 131 | 132 | if (distance < minDistance) { 133 | minDistance = distance; 134 | minIndex = i; 135 | } 136 | 137 | } 138 | 139 | if (minDistance < this.radius) { 140 | poison.splice(minIndex, 1); 141 | this.health -= 30; 142 | } 143 | 144 | }; 145 | 146 | this.seek = function () { 147 | 148 | var minDistance = Infinity; 149 | var minIndex = -1; 150 | 151 | var goal; 152 | var desire; 153 | var target; 154 | 155 | // if it sees food, it should follow it 156 | for (var i = 0; i < food.length; i++) { 157 | 158 | var distance = int(dist(this.position.x, this.position.y, food[i].x, food[i].y)); 159 | 160 | if (distance < minDistance) { 161 | minDistance = distance; 162 | minIndex = i; 163 | } 164 | 165 | } 166 | 167 | if (minDistance < this.sight) { 168 | goal = food[minIndex]; 169 | 170 | // want to eat food so bad 171 | desire = 1; 172 | 173 | target = p5.Vector.sub(goal, this.position); 174 | 175 | // normalize then multiply by maximum speed 176 | target.setMag(this.maxSpeed * desire); 177 | 178 | // make it closer to the target 179 | this.desiredPosition = p5.Vector.sub(target, this.velocity); 180 | this.desiredPosition.limit(this.maxForce); 181 | 182 | // move towards target, as fast as you can 183 | this.acceleration.add(this.desiredPosition); 184 | } 185 | // randomize moving 186 | else if (frameCount % floor(random(30)) == 0) { 187 | var randomMovement = 7; 188 | var randX = this.velocity.x + random(randomMovement) - randomMovement / 2; 189 | var randY = this.velocity.y + random(randomMovement) - randomMovement / 2; 190 | this.acceleration = createVector(randX, randY); 191 | } 192 | }; 193 | 194 | this.move = function () { 195 | 196 | // avoid obstacle 197 | for (var i = 0; i < obstacles.length; i++) { 198 | if (obstacles[i].contains(this.position, this.radius)) { 199 | this.acceleration.add(createVector(random(20) - 10, random(20) - 10)); 200 | } 201 | } 202 | 203 | // update position 204 | this.velocity.add(this.acceleration); // add force if there is any 205 | this.acceleration.mult(0); // reset acceleration 206 | this.velocity.limit(this.maxSpeed); 207 | this.position.add(this.velocity); 208 | 209 | // let wall be a portal to the other side 210 | if (this.position.x < 5) { 211 | this.position.x = width - 5; 212 | } 213 | if (width - this.position.x < 5) { 214 | this.position.x = 5; 215 | } 216 | if (this.position.y < 5) { 217 | this.position.y = height - 5; 218 | } 219 | if (height - this.position.y < 5) { 220 | this.position.y = 5; 221 | } 222 | 223 | // limit boundaries classic way 224 | // this.position.x = constrain(this.position.x, 0, width - 1); 225 | // this.position.y = constrain(this.position.y, 0, height - 1); 226 | }; 227 | 228 | this.render = function () { 229 | 230 | var theta = this.velocity.heading() + PI / 2; 231 | push(); 232 | // act like it is in the left upper corner 233 | translate(this.position.x, this.position.y); 234 | rotate(theta); 235 | 236 | // draw an organism 237 | fill(this.colorR, this.colorG, this.colorB); 238 | stroke(this.colorG); 239 | beginShape(this.colorB); 240 | ellipse(0, 0, this.radius); 241 | 242 | // draw sensor 243 | fill(255, 255, 255, 30); 244 | stroke(0, 255, 0); 245 | arc(0, 0, this.sight, this.sight, PI + PI / 4, PI + 3 * PI / 4); 246 | endShape(CLOSE); 247 | pop(); 248 | 249 | }; 250 | 251 | this.update = function () { 252 | this.calculateFitness(); 253 | this.eat(); 254 | this.seek(); 255 | this.move(); 256 | this.render(); 257 | this.health -= 1; 258 | if(this.health > 100) { 259 | this.health = 100; 260 | } 261 | }; 262 | } 263 | -------------------------------------------------------------------------------- /sketch.js: -------------------------------------------------------------------------------- 1 | var population = new Array(); 2 | var food = new Array(); 3 | var water = new Array(); 4 | var poison = new Array(); 5 | var obstacles = new Array(); 6 | 7 | var elitism = 5; 8 | 9 | var generation = 0; 10 | var populationSize = 30; 11 | var mutationRate = 0.9; 12 | var numberOfFood = 200; 13 | var numberOfWater = 50; 14 | var numberOfPoison = 30; 15 | var numberOfObstacles = 30; 16 | var maxFitness = 0; 17 | 18 | var randomFoodGeneration = 15; 19 | var randomWaterGeneration = 3; 20 | var randomPoisonGeneration = 3; 21 | 22 | var deathEnabled = false; 23 | 24 | var frameWidth = 883; 25 | var frameHeight = 550; 26 | 27 | // start server 28 | // command: python -m http.server 29 | // adress: http://localhost:8000/ 30 | /* 31 | var bg; 32 | 33 | function preload() { 34 | bg = loadImage("model/bg1.jpg"); 35 | } 36 | */ 37 | 38 | function setup() { 39 | createCanvas(frameWidth, frameHeight); 40 | frameRate(60); 41 | 42 | initElements(); 43 | } 44 | 45 | function draw() { 46 | 47 | // every 5 seconds generating new population 48 | if (frameCount % 150 == 0) { 49 | runGeneticAlgorithm(); 50 | } 51 | 52 | // remove all elements from last frame 53 | clear(); 54 | 55 | // in case of server running, enable background 56 | //background(bg); 57 | background(50); 58 | 59 | removeDead(); 60 | 61 | generateElements(); 62 | 63 | drawElements(); 64 | 65 | refreshParameters(); 66 | } 67 | 68 | function initElements() { 69 | for (var i = 0; i < populationSize; i++) { 70 | population.push(new Organism()); 71 | } 72 | 73 | for (var i = 0; i < numberOfFood; i++) { 74 | food.push(createVector(random(frameWidth - 20) + 10, random(frameHeight - 20) + 10)); 75 | } 76 | 77 | for (var i = 0; i < numberOfWater; i++) { 78 | water.push(createVector(random(frameWidth - 20) + 10, random(frameHeight - 20) + 10)); 79 | } 80 | 81 | for (var i = 0; i < numberOfPoison; i++) { 82 | poison.push(createVector(random(frameWidth - 20) + 10, random(frameHeight - 20) + 10)); 83 | } 84 | 85 | /* 86 | for (var i = 0; i < numberOfObstacles; i++) { 87 | obstacles.push(new Obstacle(createVector(random(frameWidth - 20) + 10, random(frameHeight - 20) + 10), random(30) + 10, random(30) + 10)); 88 | } 89 | */ 90 | 91 | } 92 | 93 | function drawElements() { 94 | // draw food 95 | for (var i = 0; i < food.length; i++) { 96 | fill(0, 255, 0); 97 | noStroke(); 98 | ellipse(food[i].x, food[i].y, 5); 99 | } 100 | 101 | // water 102 | for (var i = 0; i < water.length; i++) { 103 | fill(0, 191, 255); 104 | noStroke(); 105 | ellipse(water[i].x, water[i].y, 5); 106 | } 107 | 108 | // poison 109 | for (var i = 0; i < poison.length; i++) { 110 | fill(255, 0, 0); 111 | noStroke(); 112 | ellipse(poison[i].x, poison[i].y, 5); 113 | } 114 | 115 | // obstacles 116 | for (var i = 0; i < obstacles.length; i++) { 117 | obstacles[i].draw(); 118 | } 119 | } 120 | 121 | function generateElements() { 122 | // random food generation 123 | if (random(1) < 0.3) { 124 | for (var i = 0; i < randomFoodGeneration; i++) { 125 | food.push(createVector(random(frameWidth - 20) + 10, random(frameHeight - 20) + 10)); 126 | } 127 | } 128 | 129 | // random water generation 130 | if (random(1) < 0.3) { 131 | for (var i = 0; i < randomFoodGeneration; i++) { 132 | if(random(1) < 0.1) { 133 | water.push(createVector(random(frameWidth - 20) + 10, random(frameHeight - 20) + 10)); 134 | } 135 | } 136 | } 137 | 138 | // random poison generation 139 | if (random(1) < 0.3) { 140 | for (var i = 0; i < randomPoisonGeneration; i++) { 141 | if(random(1) < 0.1) { 142 | poison.push(createVector(random(frameWidth - 20) + 10, random(frameHeight - 20) + 10)); 143 | } 144 | } 145 | } 146 | } 147 | 148 | function removeDead() { 149 | for (var i = population.length - 1; i > 0; i--) { 150 | population[i].update(); 151 | 152 | // death 153 | if (deathEnabled == true) { 154 | if (population[i].health <= 0) { 155 | population.splice(i, 1); 156 | } 157 | } 158 | } 159 | } 160 | 161 | function runGeneticAlgorithm() { 162 | var bestInPopulation = new Array(); 163 | 164 | if(population.length < 10) { 165 | alert("Population too small! Please reset simulation"); 166 | return; 167 | } 168 | 169 | // elitism 170 | var tempElitism = 0; 171 | if(elitism > population.length) { 172 | tempElitism = population.length; 173 | } 174 | else { 175 | tempElitism = elitism; 176 | } 177 | for (var i = 0; i < tempElitism; i++) { 178 | 179 | var maxPopulationFitness = 0; 180 | 181 | var bestChromosome = population[0]; 182 | var bestChromosomeIndex = 0; 183 | 184 | for (var j = 0; j < population.length; j++) { 185 | population[j].calculateFitness(); 186 | if (population[j].fitness > maxPopulationFitness) { 187 | maxPopulationFitness = population[j].fitness; 188 | bestChromosome = population[j]; 189 | bestChromosomeIndex = j; 190 | } 191 | } 192 | 193 | population.splice(bestChromosomeIndex, 1); 194 | bestInPopulation.push(bestChromosome); 195 | } 196 | 197 | var newPopulation = new Array(); 198 | for (var j = 0; j < tempElitism; j++) { 199 | newPopulation.push(bestInPopulation[j]); 200 | } 201 | 202 | // crossover 203 | while (newPopulation.length < populationSize) { 204 | 205 | var parent1 = population[Math.floor(random(population.length))]; 206 | var parent2 = population[Math.floor(random(population.length))]; 207 | var child = new Organism(); 208 | 209 | if (random(1) < 0.5) { 210 | child.radius = parent1.radius; 211 | } 212 | else { 213 | child.radius = parent2.radius; 214 | } 215 | 216 | if (random(1) < 0.5) { 217 | child.sight = parent1.sight; 218 | } 219 | else { 220 | child.sight = parent2.sight; 221 | } 222 | 223 | if (random(1) < 0.5) { 224 | child.maxForce = parent1.maxForce; 225 | } 226 | else { 227 | child.maxForce = parent2.maxForce; 228 | } 229 | 230 | if (random(1) < 0.5) { 231 | child.maxSpeed = parent1.maxSpeed; 232 | } 233 | else { 234 | child.maxSpeed = parent2.maxSpeed; 235 | } 236 | 237 | // mutation 238 | child.randomMutation(); 239 | 240 | if(child.radius > child.sight) { 241 | child.sight = child.radius + 5; 242 | } 243 | 244 | newPopulation.push(child); 245 | } 246 | 247 | population.splice(0, population.length); 248 | population = newPopulation.slice(); 249 | generation++; 250 | //alert("new population is in the game"); 251 | } 252 | 253 | // html controllers 254 | function resetSimulation() { 255 | population = new Array(); 256 | food = new Array(); 257 | water = new Array(); 258 | poison = new Array(); 259 | obstacles = new Array(); 260 | 261 | initElements(); 262 | } 263 | 264 | function refreshParameters() { 265 | 266 | // update info 267 | document.getElementById("fitness").innerHTML = "Max fitness: " + maxFitness; 268 | document.getElementById("population").innerHTML = "Generation: " + generation; 269 | 270 | randomFoodGeneration = document.getElementById("randomFoodGeneration").value; 271 | document.getElementById("randomFoodGenerationOutput").innerHTML = randomFoodGeneration; 272 | 273 | populationSize = document.getElementById("populationSize").value; 274 | document.getElementById("populationSizeOutput").innerHTML = populationSize; 275 | 276 | elitism = document.getElementById("elitism").value; 277 | document.getElementById("elitismOutput").innerHTML = elitism; 278 | 279 | 280 | mutationRate = document.getElementById("mutationRate").value * 0.01; 281 | document.getElementById("mutationRateOutput").innerHTML = mutationRate.toFixed(2); 282 | 283 | deathEnabled = document.getElementById("deathCheckbox").checked; 284 | } 285 | --------------------------------------------------------------------------------
Population size:
Random food generation:
Elitism:
Mutation rate:
Death enabled