AI Creature Evolution - xelsed.ai

This sketch simulates a mini ecosystem where circular creatures wander the canvas hunting for green food pellets, growing when they eat, splitting into mutated offspring when large enough, and dying when they starve. Over time genetic traits like vision radius and turning speed drift through mutation, creating an evolving population, while the Web Speech API narrates milestones out loud.

🧪 Try This!

Experiment with the code by making these changes:

  1. Make hunger brutal — Cranking up the hunger range makes creatures starve much faster, favoring genes with big vision radius and speed.
  2. Rain down food — Spawning food far more often removes scarcity, letting almost every lineage survive and split quickly.
  3. Give every creature huge eyes — Scaling up the eye size relative to body makes every creature look wide-eyed and alert, a purely cosmetic tweak to show() that doesn't touch the simulation logic.
Prefer the full editor? Open it there →

📖 About This Sketch

This sketch grows a tiny digital ecosystem: circular creatures with randomized genes roam the canvas, chase nearby green food pellets, grow when they eat, split into two mutated offspring once big enough, and shrink from constant hunger until they die. It leans on core p5.js ideas - trigonometry with cos()/sin() for movement, dist() and atan2() for sensing and steering, and the push()/translate()/rotate() transform stack for drawing each creature facing its direction of travel. On top of the p5 sketch it also calls the browser's built-in SpeechSynthesis API so the page literally talks about what's happening in the population.

The code is organized around two ES6 classes, Creature and Food, each with a constructor plus methods for updating and drawing themselves, and a global setup()/draw() pair that owns the arrays of living creatures and food. Studying it teaches you how to model autonomous agents with object-oriented classes, how simple gene objects and a mutateGenes() switch-statement can simulate evolution, and how to wire non-p5 browser APIs like speech synthesis into a creative coding sketch.

⚙️ How It Works

  1. When the page loads, setup() creates a full-window canvas, spawns 10 first-generation creatures with completely random genes, scatters 50 food pellets, and speaks an opening line of narration.
  2. Every frame, draw() clears the background, occasionally adds a new food pellet on a timer, and draws every remaining food pellet as a small green circle.
  3. For each creature, update() first applies hunger by shrinking its size, then either steers toward the nearest food pellet within its vision radius or wanders with a small random turn plus a personal movementBias, moves it forward using cos()/sin() of its angle, and wraps it around the screen edges.
  4. Back in draw(), each surviving creature is checked against every food pellet with eat() (distance-based collision); if it overlaps food it grows and the food is removed, and if its size has grown past its genetic splitSize it calls split() to produce two offspring whose genes are copies of the parent's genes run through mutateGenes(), which randomly nudges and clamps each trait.
  5. Creatures whose size drops below 5 are removed from the array (they starved), and whenever a creature reaches a brand-new highest generation the sketch calls speak() to announce it; every 10 seconds it also picks a random flavor-text line to speak while the simulation is idle.
  6. If every creature dies at once, draw() detects the empty array and calls setup() again to restart the whole simulation from generation 0, and clicking anywhere with mousePressed() drops an extra food pellet at the cursor and announces its coordinates.

🎓 Concepts You'll Learn

Object-oriented classesGenetic mutation / evolution simulationTrigonometric movement (cos/sin/atan2)Array management (splice, push, spread)Web Speech API integrationBoundary wrappingTransform stack (push/translate/rotate)

📝 Code Breakdown

Creature constructor()

The constructor is where an object's initial state is set up. Here it also implements the genetic algorithm's inheritance step: pass parentGenes in and you get evolution, leave it null and you get a randomly seeded founder.

🔬 This branch decides between fresh random genes and mutated inherited genes. What happens if you force EVERY creature (even generation 0) through mutateGenes by always taking this branch - would the population still be diverse?

    if (parentGenes) {
      // If a parent's genes are provided, mutate them for the offspring
      this.genes = this.mutateGenes(parentGenes);
    } else {
constructor(x, y, parentGenes = null, generation = 0) {
    this.x = x;
    this.y = y;
    this.size = 20; // Initial size, grows with food, shrinks with hunger
    // Randomized color for visual distinction
    this.color = color(random(100, 255), random(100, 255), random(100, 255));
    this.speed = 2; // Base movement speed
    this.angle = random(TWO_PI); // Initial movement direction
    this.generation = generation; // Track which generation this creature belongs to

    // Genes (traits)
    if (parentGenes) {
      // If a parent's genes are provided, mutate them for the offspring
      this.genes = this.mutateGenes(parentGenes);
    } else {
      // First generation, randomize genes completely
      this.genes = {
        visionRadius: random(50, 200),     // How far it can see food
        hungerRate: random(0.02, 0.1),     // Size lost per frame (hunger)
        splitSize: random(35, 50),         // Size at which it splits into offspring
        mutationRate: random(0.01, 0.1),   // Probability of a gene mutating
        movementBias: random(-0.5, 0.5),   // Preference for turning left/right (-0.5 to 0.5)
        turnSpeed: random(0.02, 0.1)       // How quickly it can turn towards food/random direction
      };
    }
  }
Line-by-line explanation (5 lines)

🔧 Subcomponents:

conditional First-gen vs offspring genes if (parentGenes) {

Decides whether a creature gets fully random starting genes or mutated copies of a parent's genes

this.color = color(random(100, 255), random(100, 255), random(100, 255));
Picks a random RGB color biased toward bright/light values so creatures are visible on the dark background
this.angle = random(TWO_PI);
Gives the creature a random starting facing direction, in radians (0 to 2*PI)
if (parentGenes) {
Checks whether this constructor was called with a parent's gene object (i.e. this is offspring, not a founding creature)
this.genes = this.mutateGenes(parentGenes);
Runs the parent's genes through the mutation function so offspring are similar to but not identical to the parent
visionRadius: random(50, 200), // How far it can see food
Randomly assigns how many pixels away this creature can detect food - this is a core trait natural selection acts on

update()

update() is the heart of the simulation's behavior: it combines a sensing step (scanning nearby food), a decision step (steer vs wander), a physics step (trig-based movement), and a survival check, all patterns you'll reuse in any agent-based simulation.

🔬 This block only fires when food is within visionRadius. What happens if you remove the visionRadius limit entirely (make minDistance start at Infinity) so creatures can always see every pellet on screen?

    if (nearestFood) {
      // If food is found, turn towards it
      let desiredAngle = atan2(nearestFood.y - this.y, nearestFood.x - this.x);
update(foodPellets) {
    // 1. Hunger: Creature slowly shrinks over time
    this.size -= this.genes.hungerRate;

    // 2. Movement: Seek nearest food or move randomly with bias
    let nearestFood = null;
    let minDistance = this.genes.visionRadius;

    // Find the nearest food pellet within vision radius
    for (let food of foodPellets) {
      let d = dist(this.x, this.y, food.x, food.y);
      if (d < minDistance) {
        minDistance = d;
        nearestFood = food;
      }
    }

    if (nearestFood) {
      // If food is found, turn towards it
      let desiredAngle = atan2(nearestFood.y - this.y, nearestFood.x - this.x);
      // Calculate the shortest angular difference
      let angleDiff = desiredAngle - this.angle;
      angleDiff = (angleDiff + PI) % TWO_PI - PI; // Normalize angle difference to -PI to PI
      // Smoothly turn towards the food, constrained by turnSpeed
      this.angle += constrain(angleDiff, -this.genes.turnSpeed, this.genes.turnSpeed);
    } else {
      // No food in sight, move randomly with a slight bias
      this.angle += random(-this.genes.turnSpeed, this.genes.turnSpeed) + this.genes.movementBias * 0.05;
    }

    // Move the creature based on its speed and angle
    this.x += this.speed * cos(this.angle);
    this.y += this.speed * sin(this.angle);

    // 3. Boundary Wrapping: If creature goes off-screen, wrap around to the other side
    if (this.x < 0) this.x = width;
    if (this.x > width) this.x = 0;
    if (this.y < 0) this.y = height;
    if (this.y > height) this.y = 0;

    // 4. Death condition: If size falls below 5, creature dies
    if (this.size < 5) {
      return false; // Creature dies
    }

    return true; // Creature is alive
  }
Line-by-line explanation (8 lines)

🔧 Subcomponents:

for-loop Find Nearest Food for (let food of foodPellets) {

Scans every food pellet to find the closest one that is still within this creature's vision radius

conditional Seek Food or Wander if (nearestFood) {

Steers the creature toward detected food, or lets it wander randomly with its personal movement bias when nothing is visible

conditional Screen Wrapping if (this.x < 0) this.x = width;

Teleports the creature to the opposite edge when it exits the canvas, so it never truly leaves the world

conditional Starvation Death Check if (this.size < 5) {

Kills the creature (signals death by returning false) once hunger has shrunk it below a viable size

this.size -= this.genes.hungerRate;
Every frame the creature loses a tiny bit of size based on its own genetic hunger rate - this is the constant pressure driving the simulation
let minDistance = this.genes.visionRadius;
Starts the search distance at the creature's maximum vision range so anything farther is ignored
let desiredAngle = atan2(nearestFood.y - this.y, nearestFood.x - this.x);
atan2 computes the angle pointing directly from the creature to the food, in radians
angleDiff = (angleDiff + PI) % TWO_PI - PI; // Normalize angle difference to -PI to PI
Wraps the angle difference so turning always takes the shortest path (never spins the long way around)
this.angle += constrain(angleDiff, -this.genes.turnSpeed, this.genes.turnSpeed);
Turns the creature toward the food but limits how much it can turn in one frame, using its genetic turnSpeed
this.x += this.speed * cos(this.angle);
Converts the creature's angle and speed into an actual x movement using trigonometry
if (this.x < 0) this.x = width;
If the creature drifts off the left edge, it reappears on the right edge (screen wrapping)
if (this.size < 5) { return false; // Creature dies }
If the creature has starved down to a tiny size, update() returns false to signal draw() should remove it

Creature show()

show() demonstrates the push()/translate()/rotate()/pop() pattern, the standard p5.js way to draw an object at its own position and orientation without manually doing trigonometry for every point.

🔬 These two circles draw the eyes on the front of the creature. What happens if you change eyeOffset to a negative value so eyes appear on the back instead?

    circle(eyeOffset, -eyeSize, eyeSize * 1.5);
    circle(eyeOffset, eyeSize, eyeSize * 1.5);
show() {
    push();
    translate(this.x, this.y);
    rotate(this.angle); // Rotate to face movement direction (for eyes)

    // Body
    noStroke();
    fill(this.color);
    circle(0, 0, this.size);

    // Eyes (two small white circles offset from the center)
    fill(255);
    let eyeOffset = this.size / 4;
    let eyeSize = this.size / 8;
    circle(eyeOffset, -eyeSize, eyeSize * 1.5);
    circle(eyeOffset, eyeSize, eyeSize * 1.5);

    pop();

    // Optional: Draw vision radius for debugging (uncomment to see)
    // noFill();
    // stroke(255, 100);
    // circle(this.x, this.y, this.genes.visionRadius * 2);
  }
Line-by-line explanation (6 lines)
push();
Saves the current drawing transform state so translate/rotate below only affect this creature
translate(this.x, this.y);
Moves the coordinate origin to the creature's position, so all shapes below are drawn relative to (0,0) at that spot
rotate(this.angle);
Rotates the coordinate system so the creature's 'face' (eyes) points in its direction of travel
circle(0, 0, this.size);
Draws the creature's body as a circle at the new local origin, sized by its current (hunger/food adjusted) size
let eyeOffset = this.size / 4;
Calculates how far forward the eyes sit, scaled to the creature's current size so eyes stay proportional as it grows or shrinks
pop();
Restores the saved transform, so later drawing (other creatures, text, etc.) isn't affected by this rotation/translation

eat()

eat() is a compact example of circle-to-circle collision detection, one of the most common patterns in 2D games and simulations.

🔬 This condition uses this.size / 2 as the creature's radius. What happens if you make food easier to grab by not dividing by 2 (using the full size as the collision radius)?

    if (d < this.size / 2 + food.size / 2) {
      this.size += 5; // Grow when eating food
      return true; // Food was eaten
    }
eat(food) {
    let d = dist(this.x, this.y, food.x, food.y);
    // Collision detection: Distance between centers is less than sum of radii
    if (d < this.size / 2 + food.size / 2) {
      this.size += 5; // Grow when eating food
      return true; // Food was eaten
    }
    return false; // Food not eaten
  }
Line-by-line explanation (3 lines)

🔧 Subcomponents:

conditional Circle Collision Check if (d < this.size / 2 + food.size / 2) {

Detects overlap between the creature's body circle and the food pellet's circle using the classic sum-of-radii distance test

let d = dist(this.x, this.y, food.x, food.y);
Measures the straight-line distance between the creature's center and the food's center
if (d < this.size / 2 + food.size / 2) {
Two circles overlap when the distance between their centers is less than the sum of their radii - this is the standard circle-collision formula
this.size += 5; // Grow when eating food
Rewards successful eating by increasing the creature's size, moving it closer to splitSize

split()

split() implements asexual reproduction with a size cost, the mechanism that lets successful (well-fed) creatures pass on and mutate their genes into the next generation.

🔬 Only two offspring are made per split. What happens visually if you create four offspring instead of two here?

      let offspring1 = new Creature(this.x + random(-5, 5), this.y + random(-5, 5), this.genes, this.generation + 1);
      let offspring2 = new Creature(this.x + random(-5, 5), this.y + random(-5, 5), this.genes, this.generation + 1);
split() {
    if (this.size > this.genes.splitSize) {
      // Create two new creatures with mutated genes and increment generation
      let offspring1 = new Creature(this.x + random(-5, 5), this.y + random(-5, 5), this.genes, this.generation + 1);
      let offspring2 = new Creature(this.x + random(-5, 5), this.y + random(-5, 5), this.genes, this.generation + 1);
      this.size /= 2; // Parent shrinks after splitting
      return [offspring1, offspring2];
    }
    return null; // Not ready to split
  }
Line-by-line explanation (4 lines)

🔧 Subcomponents:

conditional Ready-to-split check if (this.size > this.genes.splitSize) {

Only allows reproduction once a creature has grown past its own genetically determined split threshold

if (this.size > this.genes.splitSize) {
Checks whether this creature has eaten enough to be large enough to reproduce, based on its own splitSize gene
let offspring1 = new Creature(this.x + random(-5, 5), this.y + random(-5, 5), this.genes, this.generation + 1);
Creates a new Creature near the parent's position, passing the parent's genes (which get mutated inside the constructor) and incrementing the generation counter
this.size /= 2; // Parent shrinks after splitting
The parent pays an energy cost for reproducing by losing half its size
return [offspring1, offspring2];
Returns both new offspring as an array so draw() can add them to the main creatures list

mutateGenes()

This function is the genetic algorithm engine of the sketch: it copies a parent's traits, randomly perturbs some of them, and clamps results to biologically plausible ranges - the same mutate-and-clamp pattern used in real evolutionary computation code.

🔬 Mutation only happens with probability equal to mutationRate. What happens if you remove the probability check entirely so every gene mutates on every single split?

      if (random(1) < parentGenes.mutationRate) {
        switch (geneName) {
          case 'visionRadius':
            newValue += random(-20, 20);
            newValue = constrain(newValue, 50, 300); // Clamp vision radius
            break;
mutateGenes(parentGenes) {
    let newGenes = {};
    for (let geneName in parentGenes) {
      let parentValue = parentGenes[geneName];
      let newValue = parentValue;

      // Apply mutation with a certain probability (based on parent's mutationRate)
      if (random(1) < parentGenes.mutationRate) {
        switch (geneName) {
          case 'visionRadius':
            newValue += random(-20, 20);
            newValue = constrain(newValue, 50, 300); // Clamp vision radius
            break;
          case 'hungerRate':
            newValue += random(-0.01, 0.01);
            newValue = constrain(newValue, 0.01, 0.15); // Clamp hunger rate
            break;
          case 'splitSize':
            newValue += random(-5, 5);
            newValue = constrain(newValue, 30, 60); // Clamp split size
            break;
          case 'mutationRate':
            newValue += random(-0.005, 0.005);
            newValue = constrain(newValue, 0.01, 0.1); // Clamp mutation rate
            break;
          case 'movementBias':
            newValue += random(-0.1, 0.1);
            newValue = constrain(newValue, -0.5, 0.5); // Clamp movement bias
            break;
          case 'turnSpeed':
            newValue += random(-0.01, 0.01);
            newValue = constrain(newValue, 0.01, 0.15); // Clamp turn speed
            break;
        }
      }
      newGenes[geneName] = newValue;
    }
    return newGenes;
  }
Line-by-line explanation (4 lines)

🔧 Subcomponents:

for-loop Loop Over Every Gene for (let geneName in parentGenes) {

Iterates over each named trait (visionRadius, hungerRate, etc.) in the parent's genes object so every trait gets a chance to mutate

conditional Mutation Probability Check if (random(1) < parentGenes.mutationRate) {

Only mutates a gene if a random roll is below the parent's own mutationRate, so mutation frequency is itself an evolvable trait

switch-case Per-Gene Mutation Rules switch (geneName) {

Applies a different random nudge and valid range (clamp) depending on which specific gene is being mutated

for (let geneName in parentGenes) {
A for...in loop walks through every property name (key) of the genes object, like 'visionRadius', 'hungerRate', etc.
if (random(1) < parentGenes.mutationRate) {
Rolls a random number between 0 and 1; if it's less than the mutation rate, this gene will change for the offspring
newValue += random(-20, 20); newValue = constrain(newValue, 50, 300); // Clamp vision radius
Nudges the vision radius up or down by a random amount, then clamps it to a sensible range so it can't mutate into an invalid extreme
newGenes[geneName] = newValue;
Stores the (possibly mutated, possibly unchanged) value into the new gene object under the same property name

Food constructor()

A minimal constructor like this shows how simple a class can be - just storing a few properties that other methods (like show()) will later use.

constructor(x, y) {
    this.x = x;
    this.y = y;
    this.size = 5;
    this.color = color(0, 200, 0); // Green food
  }
Line-by-line explanation (2 lines)
this.size = 5;
Sets a fixed pixel size for every food pellet
this.color = color(0, 200, 0); // Green food
Gives all food pellets the same solid green color so they read clearly as 'food' against the dark background and colorful creatures

Food show()

Keeping drawing logic inside show() methods (rather than in the main draw loop) keeps each class self-contained and easy to modify independently.

show() {
    noStroke();
    fill(this.color);
    circle(this.x, this.y, this.size);
  }
Line-by-line explanation (2 lines)
noStroke();
Removes the outline so the food pellet is drawn as a clean filled dot
circle(this.x, this.y, this.size);
Draws the pellet as a small circle at its stored position and size

setup()

setup() runs once when the sketch starts (and, unusually in this sketch, is also called again later to restart the simulation). It's where you establish initial world state before the draw loop takes over.

🔬 This loop scatters 50 starting food pellets. What happens if you drop that number to 5 - will the first generation of creatures survive long enough to split?

  for (let i = 0; i < 50; i++) {
    food.push(new Food(random(width), random(height)));
  }
function setup() {
  createCanvas(windowWidth, windowHeight);
  background(0); // Dark background

  // Initialize with some first-generation creatures
  for (let i = 0; i < 10; i++) {
    creatures.push(new Creature(random(width), random(height), null, 0));
  }

  // Initialize with some food pellets
  for (let i = 0; i < 50; i++) {
    food.push(new Food(random(width), random(height)));
  }

  // Check for speech synthesis support
  if ('speechSynthesis' in window) {
    console.log("Speech synthesis is supported.");
  } else {
    console.warn("Speech synthesis not supported in this browser.");
  }

  // Initial narration
  speak("The simulation begins. Generation 0 is striving for survival.");
}
Line-by-line explanation (4 lines)

🔧 Subcomponents:

for-loop Spawn Initial Creatures for (let i = 0; i < 10; i++) {

Creates 10 founding creatures with fully random genes scattered across the canvas

for-loop Spawn Initial Food for (let i = 0; i < 50; i++) {

Scatters 50 starting food pellets randomly across the canvas

conditional Speech API Support Check if ('speechSynthesis' in window) {

Logs whether the browser supports the Web Speech API so developers can debug narration issues in the console

createCanvas(windowWidth, windowHeight);
Makes the canvas fill the entire browser window
creatures.push(new Creature(random(width), random(height), null, 0));
Adds a new Creature at a random position with no parent genes (null) and generation 0, triggering fully random gene generation
food.push(new Food(random(width), random(height)));
Adds a new Food pellet at a random canvas position to the global food array
speak("The simulation begins. Generation 0 is striving for survival.");
Calls the custom speak() helper to have the browser read out an opening line of narration

draw()

draw() is the heartbeat of any p5.js sketch, running ~60 times per second. Here it orchestrates the entire simulation lifecycle each frame: spawn, update, eat, reproduce, draw, narrate, and even restart - all patterns central to building any simulation or game loop.

🔬 This timer adds one pellet every FOOD_SPAWN_INTERVAL frames. What happens if you push several pellets at once instead of just one, flooding the world with food faster?

  foodSpawnTimer++;
  if (foodSpawnTimer >= FOOD_SPAWN_INTERVAL) {
    food.push(new Food(random(width), random(height)));
    foodSpawnTimer = 0;
  }
function draw() {
  background(0); // Clear background (dark)

  // Spawn new food periodically
  foodSpawnTimer++;
  if (foodSpawnTimer >= FOOD_SPAWN_INTERVAL) {
    food.push(new Food(random(width), random(height)));
    foodSpawnTimer = 0;
  }

  // Update and show food pellets
  for (let i = food.length - 1; i >= 0; i--) {
    food[i].show();
  }

  // Update and show creatures
  let newCreatures = []; // Temporary array to hold offspring
  let currentMaxGenAlive = 0; // Tracks the highest generation alive in the current frame

  for (let i = creatures.length - 1; i >= 0; i--) {
    let creature = creatures[i];
    currentMaxGenAlive = max(currentMaxGenAlive, creature.generation);

    if (creature.update(food)) {
      // Creature is alive, check for eating food
      for (let j = food.length - 1; j >= 0; j--) {
        if (creature.eat(food[j])) {
          food.splice(j, 1); // Remove eaten food
          break; // Creature can only eat one food per frame
        }
      }

      // Check for splitting
      let offspring = creature.split();
      if (offspring) {
        newCreatures.push(...offspring); // Add offspring to the temporary array
        // If a creature splits and creates a higher generation, update maxGenerationReached
        if (creature.generation + 1 > maxGenerationReached) {
          maxGenerationReached = creature.generation + 1;
          speak("A new generation emerges! Generation " + maxGenerationReached + " is now thriving.");
        }
      }
      creature.show(); // Show the creature if it's alive
    } else {
      // Creature died
      creatures.splice(i, 1);
    }
  }

  // Add all new offspring to the main creatures array
  creatures.push(...newCreatures);

  // Update the displayed generation count to the highest generation currently alive
  generation = currentMaxGenAlive;

  // Display generation, creature, and food counts
  fill(255);
  textSize(16);
  textAlign(LEFT, TOP);
  text("Generation: " + generation, 10, 10);
  text("Creatures: " + creatures.length, 10, 30);
  text("Food: " + food.length, 10, 50);

  // Occasional narration based on events or time
  if (millis() - lastSpeechTime > SPEECH_INTERVAL && !speechSynthesis.speaking) {
    let messages = [
      "The ecosystem evolves.",
      "Creatures adapt to their environment.",
      "Some species are thriving, others are perishing.",
      "Competition for resources intensifies.",
      "Diversity is key to survival.",
      "Observe the intricate dance of life.",
      "A new species with enhanced vision is gaining an advantage.",
      "The green food pellets are the lifeblood of this world.",
      "Survival of the fittest in action."
    ];
    speak(random(messages));
    lastSpeechTime = millis();
  }

  // If all creatures perish, restart the simulation
  if (creatures.length === 0) {
    speak("All creatures have perished. Restarting the simulation.");
    setup(); // Restart the simulation
  }
}
Line-by-line explanation (11 lines)

🔧 Subcomponents:

conditional Timed Food Spawn if (foodSpawnTimer >= FOOD_SPAWN_INTERVAL) {

Adds one new food pellet automatically once enough frames have passed

for-loop Update/Eat/Split/Draw Every Creature for (let i = creatures.length - 1; i >= 0; i--) {

Walks backward through the creatures array so items can be safely removed mid-loop while updating, feeding, reproducing, and drawing each one

for-loop Check Food Collisions for (let j = food.length - 1; j >= 0; j--) {

Checks a living creature against every remaining food pellet to see if it just ate one

conditional Extinction Restart if (creatures.length === 0) {

Detects total population collapse and restarts the whole simulation by calling setup() again

background(0); // Clear background (dark)
Repaints the whole canvas black every frame, erasing the previous frame's drawings so nothing leaves a trail
foodSpawnTimer++;
Increments a frame counter used to know when it's time to add a new food pellet automatically
for (let i = creatures.length - 1; i >= 0; i--) {
Loops backward through the array; looping backward is important here because splice() inside the loop removes items, which would skip elements if looping forward
currentMaxGenAlive = max(currentMaxGenAlive, creature.generation);
Keeps track of the highest generation number among currently living creatures, used for the on-screen display
if (creature.update(food)) {
Calls the creature's own update method; it returns true if the creature survived this frame and false if it starved
if (creature.eat(food[j])) { food.splice(j, 1); // Remove eaten food break; // Creature can only eat one food per frame }
If the creature's eat() check succeeds, removes that food pellet from the array and stops checking further food (limiting each creature to one meal per frame)
let offspring = creature.split();
Asks the creature if it's big enough to reproduce; returns an array of two offspring or null
newCreatures.push(...offspring); // Add offspring to the temporary array
Uses the spread operator to add both offspring individually into a temporary array, avoiding modifying the creatures array while it's still being looped over
creatures.push(...newCreatures);
After the loop finishes, all queued-up offspring are finally added to the main creatures array
if (millis() - lastSpeechTime > SPEECH_INTERVAL && !speechSynthesis.speaking) {
Checks whether enough real time has passed since the last spoken line, and that the browser isn't already speaking, before queuing new narration
if (creatures.length === 0) { speak("All creatures have perished. Restarting the simulation."); setup(); // Restart the simulation }
If every creature has died, this announces the extinction and calls setup() again to reseed the whole simulation

mousePressed()

mousePressed() is a p5.js event function that automatically runs whenever the mouse is clicked, letting you turn user interaction directly into changes in the simulation's world state.

function mousePressed() {
  food.push(new Food(mouseX, mouseY)); // Spawn food at mouse position
  speak("Food spawned at " + round(mouseX) + ", " + round(mouseY) + ".");
}
Line-by-line explanation (2 lines)
food.push(new Food(mouseX, mouseY)); // Spawn food at mouse position
Creates a new Food object at the current mouse coordinates and adds it to the global food array
speak("Food spawned at " + round(mouseX) + ", " + round(mouseY) + ".");
Announces the click location out loud, rounding the coordinates to whole numbers for cleaner speech

windowResized()

windowResized() is a built-in p5.js callback that fires whenever the browser window changes size, letting responsive sketches keep filling the screen.

function windowResized() {
  resizeCanvas(windowWidth, windowHeight); // Resize canvas on window resize
}
Line-by-line explanation (1 lines)
resizeCanvas(windowWidth, windowHeight); // Resize canvas on window resize
Automatically resizes the canvas to match the new browser window dimensions whenever the window is resized

speak()

speak() wraps the browser's native SpeechSynthesis API in a simple reusable helper function - a good example of integrating non-p5 Web APIs into a creative coding project.

function speak(textToSpeak) {
  if ('speechSynthesis' in window && !speechSynthesis.speaking) {
    let utterance = new SpeechSynthesisUtterance(textToSpeak);
    utterance.volume = 1; // 0 to 1
    utterance.rate = 1;   // 0.1 to 10
    utterance.pitch = 1;  // 0 to 2
    speechSynthesis.speak(utterance);
  }
}
Line-by-line explanation (4 lines)

🔧 Subcomponents:

conditional Support and Busy Check if ('speechSynthesis' in window && !speechSynthesis.speaking) {

Only attempts to speak if the browser supports the Web Speech API and isn't already speaking another line

if ('speechSynthesis' in window && !speechSynthesis.speaking) {
Guards against unsupported browsers and prevents overlapping/interrupted speech by checking nothing is currently being spoken
let utterance = new SpeechSynthesisUtterance(textToSpeak);
Creates a speech request object wrapping the text that should be read aloud, using the browser's built-in Web Speech API
utterance.rate = 1; // 0.1 to 10
Sets how fast the voice speaks; 1 is normal speed
speechSynthesis.speak(utterance);
Sends the utterance to the browser's speech engine, which plays it out loud asynchronously

📦 Key Variables

creatures array

Holds every currently living Creature object; updated, drawn, and pruned each frame in draw()

let creatures = [];
food array

Holds every currently available Food object on the canvas

let food = [];
generation number

Stores the highest generation number currently alive, shown in the on-screen HUD text

let generation = 0;
maxGenerationReached number

Tracks the highest generation that has EVER successfully split, used to detect and announce new evolutionary milestones

let maxGenerationReached = 0;
foodSpawnTimer number

Frame counter that increments every draw() call and triggers a new food pellet once it reaches FOOD_SPAWN_INTERVAL

let foodSpawnTimer = 0;
FOOD_SPAWN_INTERVAL number

Constant controlling how many frames pass between automatic food spawns

const FOOD_SPAWN_INTERVAL = 60;
lastSpeechTime number

Stores the millis() timestamp of the last idle narration line, used to space out speech events

let lastSpeechTime = 0;
SPEECH_INTERVAL number

Constant defining the minimum milliseconds between idle narration lines (10 seconds)

const SPEECH_INTERVAL = 10000;

🔧 Potential Improvements (4)

Here are some ways this code could be enhanced:

BUG draw() extinction check

When creatures.length === 0, draw() calls setup() again, but setup() never clears the existing food array or resets foodSpawnTimer/generation counters - it just pushes 10 more creatures and 50 more food on top of whatever remains (usually leftover food), so repeated extinctions cause the food array to grow unbounded over the session.

💡 Reset state explicitly at the top of setup(), e.g. `creatures = []; food = []; maxGenerationReached = 0;` before repopulating, so restarts truly start fresh.

PERFORMANCE draw() creature/food loops

For every creature, update() loops over all food to find the nearest pellet, and the eat check loops over all remaining food again - this is O(creatures * food) every single frame, which will slow down noticeably as populations and food piles grow.

💡 Use a spatial partitioning structure (like a grid) to only check food pellets near each creature, or cap the maximum number of food pellets/creatures allowed at once.

BUG draw() speech check

`!speechSynthesis.speaking` is read directly without first checking `'speechSynthesis' in window` (unlike setup(), which does check), so in a browser without the Web Speech API this line would throw a ReferenceError and silently break the draw loop.

💡 Guard this line the same way speak() does: `if ('speechSynthesis' in window && millis() - lastSpeechTime > SPEECH_INTERVAL && !speechSynthesis.speaking) { ... }`.

STYLE Creature.eat()

The parameter name `food` shadows the global `food` array variable name (also used as a loop variable name for-of in update()), which can be confusing when reading the code and risks accidental bugs if the function grows.

💡 Rename the parameter to something like `foodItem` to avoid shadowing the global array.

🔄 Code Flow

Code flow showing creatureconstructor, update, show, eat, split, mutategenes, foodconstructor, foodshow, setup, draw, mousepressed, windowresized, speak

💡 Click on function names in the diagram to jump to their code

graph TD start[Start] --> setup[setup] setup --> draw[draw loop] draw --> spawn-creatures-loop[Spawn Initial Creatures Loop] draw --> spawn-food-loop[Spawn Initial Food Loop] draw --> creature-loop[Creature Update/Eat/Split/Draw Loop] draw --> food-spawn-timer[Timed Food Spawn] draw --> restart-check[Extinction Restart Check] click setup href "#fn-setup" click draw href "#fn-draw" click spawn-creatures-loop href "#sub-spawn-creatures-loop" click spawn-food-loop href "#sub-spawn-food-loop" click creature-loop href "#sub-creature-loop" click food-spawn-timer href "#sub-food-spawn-timer" click restart-check href "#sub-restart-check" spawn-creatures-loop --> creatureconstructor[creatureconstructor] click creatureconstructor href "#fn-creatureconstructor" spawn-food-loop --> foodconstructor[foodconstructor] click foodconstructor href "#fn-foodconstructor" creature-loop --> update[update] creature-loop --> show[show] creature-loop --> eat[eat] creature-loop --> split[split] click update href "#fn-update" click show href "#fn-show" click eat href "#fn-eat" click split href "#fn-split" update --> nearest-food-loop[Find Nearest Food Loop] update --> seek-or-wander[Seek Food or Wander] update --> boundary-wrap[Screen Wrapping] update --> death-check[Starvation Death Check] click nearest-food-loop href "#sub-nearest-food-loop" click seek-or-wander href "#sub-seek-or-wander" click boundary-wrap href "#sub-boundary-wrap" click death-check href "#sub-death-check" nearest-food-loop --> eat-collision[Circle Collision Check] click eat-collision href "#sub-eat-collision" eat --> eat-collision eat --> split-check[Ready-to-Split Check] click split-check href "#sub-split-check" split --> mutategenes[mutategenes] click mutategenes href "#fn-mutategenes" mutategenes --> gene-loop[Loop Over Every Gene] mutategenes --> mutation-chance[Mutation Probability Check] click gene-loop href "#sub-gene-loop" click mutation-chance href "#sub-mutation-chance" gene-loop --> gene-branch[First-gen vs Offspring Genes] gene-loop --> gene-switch[Per-Gene Mutation Rules] click gene-branch href "#sub-gene-branch" click gene-switch href "#sub-gene-switch" show --> foodshow[foodshow] click foodshow href "#fn-foodshow" mousepressed --> speech-support-check[Speech API Support Check] click speech-support-check href "#sub-speech-support-check" speech-support-check --> speech-guard[Support and Busy Check] click speech-guard href "#sub-speech-guard" food-spawn-timer --> foodshow food-spawn-timer --> food-eat-loop[Check Food Collisions] click food-eat-loop href "#sub-food-eat-loop" creature-loop --> creature-loop creature-loop --> food-eat-loop creature-loop --> restart-check creature-loop --> show creature-loop --> update creature-loop --> eat creature-loop --> split click creature-loop href "#sub-creature-loop"

❓ Frequently Asked Questions

What visual experience does the AI Creature Evolution - XeLseDai sketch provide?

The sketch creates a vibrant and dynamic simulation of colorful creatures evolving in real-time, showcasing their unique traits and behaviors as they compete for food.

How can users engage with the AI Creature Evolution - XeLseDai sketch?

Users can interact by clicking to drop food pellets, which encourages creatures to grow, reproduce, and mutate over generations.

What creative coding concepts are illustrated in this sketch?

The sketch demonstrates concepts of evolutionary algorithms, genetic mutation, and agent-based modeling, allowing for the simulation of natural selection and adaptive behavior.

Preview

AI Creature Evolution - xelsed.ai - p5.js creative coding sketch preview
Sketch Preview
Code flow diagram showing the structure of AI Creature Evolution - xelsed.ai - Code flow showing creatureconstructor, update, show, eat, split, mutategenes, foodconstructor, foodshow, setup, draw, mousepressed, windowresized, speak
Code Flow Diagram