AI Ecosystem Oracle - xelsed.ai

This sketch simulates a living ecosystem of colorful blob creatures that wander, eat food, grow, split into offspring, and die of old age. A text box lets you type commands like 'feast' or 'plague' to an AI oracle, which calls OpenAI's API to interpret your intent and unleash an effect on the whole population, complete with a spoken prophecy.

🧪 Try This!

Experiment with the code by making these changes:

  1. Make creatures frantic — Boosting the starting speed makes every creature dart around much faster, giving the ecosystem a chaotic, energetic feel.
  2. Flood the world with food — Lowering how many frames pass between food spawns makes green pellets appear constantly instead of trickling in.
  3. Recolor the food — Food pellets are always drawn green - swap the fill color to make them stand out differently against the creatures.
Prefer the full editor? Open it there →

📖 About This Sketch

This sketch grows a small population of circular creatures into a churning artificial-life simulation: each creature wanders randomly, hunts down green food pellets, grows when it eats, and splits into two smaller offspring once it's big enough - all driven by a Creature class, distance-based collision checks with dist(), and constant array manipulation with push() and splice(). What makes it unusual is the 'oracle' layer on top: typed commands are sent to OpenAI's chat completions API with fetch() and async/await, the AI returns structured JSON describing an action and intensity, and that JSON is used to trigger sweeping effects - plagues that kill creatures, feasts that flood the world with food, mutations that scramble colors and speeds, or calm periods that heal everyone.

The code is organized into a Creature class (constructor, display, move, eat, split), a p5.js setup()/draw() animation loop that spawns and updates the population every frame, a UI-handling layer that reads the command input box and talks to OpenAI, and an applyOracleAction() function that translates the AI's JSON response into concrete changes to the simulation using a switch statement. Studying it will teach you how to structure an OOP-style simulation in p5.js, how frame-based timers work, and how to bridge an external AI API into a real-time animation.

⚙️ How It Works

  1. When the sketch loads, setup() creates a full-window canvas, decodes an obfuscated OpenAI API key, wires up the command input and button, and spawns an initial population of 10 creatures plus a batch of food pellets scattered randomly around the canvas.
  2. Every frame, draw() clears the background, then loops backward through the creatures array so each creature can move randomly, check for nearby food to eat, grow if it ate, split into two smaller offspring once big enough, and lose a small amount of health - dying and being removed once health hits zero.
  3. New food pellets appear on a timer (every 30 frames) up to a maximum count, and are drawn as small green circles alongside the creatures each frame.
  4. When you type a command and click submit, handleGodCommand() sends your text to sendToOpenAI(), which calls the OpenAI chat completions API asking it to return a JSON object with an action ('plague', 'feast', 'mutate', or 'calm'), an intensity from 1-10, and a prophetic message.
  5. Once the AI responds, the oracle's message is shown on screen and spoken aloud with the Web Speech API, and for the next 5 seconds applyOracleAction() runs every frame inside draw(), using a switch statement to kill creatures during a plague, spawn extra food during a feast, randomize colors/speeds during a mutation, or heal and slow everyone during a calm.
  6. After the action's timer expires, the oracle state resets to nothing, the ecosystem returns to its normal autonomous behavior, and the simulation keeps running until the next command is typed.

🎓 Concepts You'll Learn

Object-oriented design with ES6 classesAsync/await and the fetch APIStructured JSON responses from an AI modelArray iteration, push() and splice() for population managementFrame-based timers using frameCountDOM manipulation with p5.js select()/html()Web Speech API for text-to-speech

📝 Code Breakdown

getApiKey()

This is a light obfuscation trick, not real encryption - anyone can open devtools, call getApiKey(), and read the plaintext key. Real projects should never ship secret API keys in client-side code; instead, route requests through your own backend server that holds the key privately.

function getApiKey() {
  return atob(encoded).split('').map(c => String.fromCharCode(c.charCodeAt(0) ^ key)).join('');
}
Line-by-line explanation (3 lines)
return atob(encoded)
atob() decodes the base64-encoded string 'encoded' back into raw text.
.split('').map(c => String.fromCharCode(c.charCodeAt(0) ^ key))
Splits the decoded text into individual characters and XORs each character's code with a fixed 'key' number, reversing a simple obfuscation cipher.
.join('')
Joins the un-XORed characters back into a single string - the final decoded API key.

Creature constructor()

The constructor runs automatically whenever you write 'new Creature(...)'. It's where you set up all the instance properties (x, y, size, color, speed, generation, health) that every method below will read and modify.

  constructor(x, y, size, color, speed, generation) {
    this.x = x;
    this.y = y;
    this.size = size;
    this.color = color;
    this.speed = speed;
    this.generation = generation || 1;
    this.health = 100; // Added health for plague action
  }
Line-by-line explanation (6 lines)
this.x = x; this.y = y;
Stores the creature's position on the canvas.
this.size = size;
Stores the creature's diameter, which grows as it eats and determines when it can split.
this.color = color;
Stores the p5.js color object used to draw this creature.
this.speed = speed;
Stores how far the creature can randomly move each frame.
this.generation = generation || 1;
Tracks how many splits deep this creature is - defaults to 1 if no value is passed.
this.health = 100;
Gives every creature full health, which decays over time and can be reduced by a plague or restored by eating/calm.

display()

display() is called once per creature per frame from draw(). Separating movement logic (move()) from drawing logic (display()) is a common pattern that keeps each method focused on one job.

  display() {
    fill(this.color);
    noStroke();
    circle(this.x, this.y, this.size);
  }
Line-by-line explanation (3 lines)
fill(this.color);
Sets the fill color for the next shape to this creature's stored color.
noStroke();
Removes the outline so the creature renders as a smooth solid circle.
circle(this.x, this.y, this.size);
Draws the creature as a circle at its current position with a diameter equal to its size.

move()

move() is a classic 'random walk' - a foundational technique in creative coding where small random steps accumulate into organic-looking motion. It's the same idea behind Perlin noise walks, just without smoothing.

🔬 This is a 'random walk' - each frame the creature nudges in a totally random direction. What happens if you replace one of these lines with a fixed value like this.x += this.speed (no randomness)? Would creatures still look alive?

    this.x += random(-this.speed, this.speed);
    this.y += random(-this.speed, this.speed);
  move() {
    // Simple random walk
    this.x += random(-this.speed, this.speed);
    this.y += random(-this.speed, this.speed);

    // Keep creature within canvas bounds
    this.x = constrain(this.x, this.size / 2, width - this.size / 2);
    this.y = constrain(this.y, this.size / 2, height - this.size / 2);
  }
Line-by-line explanation (4 lines)

🔧 Subcomponents:

calculation Random Walk Step this.x += random(-this.speed, this.speed);

Nudges the creature's position by a random amount each frame, creating jittery wandering motion.

calculation Boundary Clamp this.x = constrain(this.x, this.size / 2, width - this.size / 2);

Prevents the creature from drifting off-screen by clamping its position to stay within the canvas edges.

this.x += random(-this.speed, this.speed);
Adds a random value between -speed and +speed to x, so faster creatures jump around more erratically.
this.y += random(-this.speed, this.speed);
Does the same random nudge for the vertical position.
this.x = constrain(this.x, this.size / 2, width - this.size / 2);
Clamps x so the creature's edge never goes past the left or right side of the canvas.
this.y = constrain(this.y, this.size / 2, height - this.size / 2);
Clamps y the same way for the top and bottom edges.

eat()

This method demonstrates circle-vs-circle collision detection, one of the most common techniques in 2D games and simulations, plus the safe pattern of looping backward when removing array items mid-loop.

🔬 This is the collision + growth logic. What happens visually if you change 'foodSize * 0.1' to 'foodSize * 1' so creatures grow ten times faster per bite?

      if (d < this.size / 2 + foodSize / 2) {
        this.size += foodSize * 0.1; // Grow creature slowly
        this.health = constrain(this.health + 5, 0, 100); // Regain health
        foodArray.splice(i, 1); // Remove eaten food pellet
        return true; // Food was eaten
      }
  eat(foodArray) {
    for (let i = foodArray.length - 1; i >= 0; i--) {
      let pellet = foodArray[i];
      let d = dist(this.x, this.y, pellet.x, pellet.y);
      if (d < this.size / 2 + foodSize / 2) {
        this.size += foodSize * 0.1; // Grow creature slowly
        this.health = constrain(this.health + 5, 0, 100); // Regain health
        foodArray.splice(i, 1); // Remove eaten food pellet
        return true; // Food was eaten
      }
    }
    return false; // No food eaten
  }
Line-by-line explanation (7 lines)

🔧 Subcomponents:

for-loop Food Collision Loop for (let i = foodArray.length - 1; i >= 0; i--) {

Checks every remaining food pellet, iterating backward so splicing items out mid-loop doesn't skip any.

conditional Distance Collision Check if (d < this.size / 2 + foodSize / 2) {

Determines if the creature's edge overlaps the food pellet's edge, meaning they've touched.

for (let i = foodArray.length - 1; i >= 0; i--) {
Loops through the food array backward - important because splice() removes items and shifts indexes, so going backward avoids skipping entries.
let d = dist(this.x, this.y, pellet.x, pellet.y);
Calculates the straight-line distance between the creature and this food pellet using p5.js's dist() function.
if (d < this.size / 2 + foodSize / 2) {
Checks if the two circles overlap by comparing distance against the sum of their radii - a standard circle-collision test.
this.size += foodSize * 0.1;
Grows the creature slightly every time it successfully eats.
this.health = constrain(this.health + 5, 0, 100);
Restores 5 health points, capped at 100, rewarding creatures that keep eating.
foodArray.splice(i, 1);
Removes the eaten pellet from the food array so it disappears and can't be eaten twice.
return true;
Stops the function immediately once one pellet is eaten this frame, and reports success back to the caller.

split()

split() is the core of this simulation's population growth - it models simple asexual reproduction where a creature that has eaten enough divides into two offspring, similar to cell division or amoeba behavior.

🔬 What happens if you lower the multiplier from 2 to 1.2? Creatures would need much less food to reproduce - will the ecosystem explode in population or stay balanced?

    const splitThreshold = initialCreatureSize * 2;
    if (this.size > splitThreshold && creatures.length < maxCreatures) {
  split() {
    const splitThreshold = initialCreatureSize * 2;
    if (this.size > splitThreshold && creatures.length < maxCreatures) {
      // Create two new smaller creatures
      let newSize = this.size / 2;
      let newSpeed = this.speed * 0.9; // New creatures are slightly slower
      let newGeneration = this.generation + 1;

      // New creatures appear slightly offset from parent
      creatures.push(new Creature(
        this.x + random(-5, 5),
        this.y + random(-5, 5),
        newSize,
        this.color, // Inherit color
        newSpeed,
        newGeneration
      ));
      creatures.push(new Creature(
        this.x + random(-5, 5),
        this.y + random(-5, 5),
        newSize,
        this.color, // Inherit color
        newSpeed,
        newGeneration
      ));

      // Reset parent size or remove it (removing for more dynamic population)
      return true; // Creature split
    }
    return false; // Creature did not split
  }
Line-by-line explanation (6 lines)

🔧 Subcomponents:

conditional Split Eligibility Check if (this.size > splitThreshold && creatures.length < maxCreatures) {

Only allows a creature to split once it has grown big enough and the population hasn't hit the cap.

const splitThreshold = initialCreatureSize * 2;
Defines how big a creature must grow before it can reproduce - twice the starting size.
if (this.size > splitThreshold && creatures.length < maxCreatures) {
Checks both that the creature is big enough AND that adding more creatures won't exceed the population cap.
let newSize = this.size / 2;
Each offspring starts at half the parent's size, so total mass is conserved.
let newSpeed = this.speed * 0.9;
Offspring are slightly slower than their parent, a small built-in variation between generations.
creatures.push(new Creature(...));
Creates and adds a brand new Creature object to the shared creatures array, offset randomly from the parent's position.
return true; // Creature split
Tells the caller (in draw()) that a split happened, so it knows to remove the original parent creature.

setup()

setup() runs exactly once when the page loads. It's the right place to size the canvas, connect to DOM elements with select(), and populate arrays with starting data before the animation loop begins.

function setup() {
  createCanvas(windowWidth, windowHeight);
  apiKey = getApiKey(); // Decode API key

  // Initialize UI elements
  godCommandInput = select('#godCommandInput');
  submitCommandButton = select('#submitCommandButton');
  creatureCountSpan = select('#creatureCountSpan');
  generationSpan = select('#generationSpan');
  oracleMessageDisplay = select('#oracleMessageDisplay');

  submitCommandButton.mousePressed(handleGodCommand);

  // Initial creature population
  for (let i = 0; i < initialPopulation; i++) {
    creatures.push(new Creature(
      random(width),
      random(height),
      initialCreatureSize,
      color(random(255), random(255), random(255)), // Random initial color
      initialCreatureSpeed,
      1
    ));
  }

  // Initial food pellets
  for (let i = 0; i < maxFood / 2; i++) {
    spawnFood();
  }
}
Line-by-line explanation (7 lines)

🔧 Subcomponents:

for-loop Initial Population Loop for (let i = 0; i < initialPopulation; i++) {

Creates the starting batch of creatures at random positions with random colors.

for-loop Initial Food Loop for (let i = 0; i < maxFood / 2; i++) {

Fills half the food capacity so the world isn't empty when the simulation begins.

createCanvas(windowWidth, windowHeight);
Makes the canvas fill the entire browser window.
apiKey = getApiKey();
Decodes the obfuscated OpenAI key once at startup and stores it for later fetch() calls.
godCommandInput = select('#godCommandInput');
Grabs a reference to the HTML text input so its value can be read later.
submitCommandButton.mousePressed(handleGodCommand);
Registers a click handler so clicking the button runs handleGodCommand().
for (let i = 0; i < initialPopulation; i++) {
Loops 10 times (initialPopulation) to spawn the starting creatures.
creatures.push(new Creature(...));
Creates each creature at a random x/y position with a random RGB color and adds it to the global creatures array.
for (let i = 0; i < maxFood / 2; i++) { spawnFood(); }
Calls spawnFood() enough times to fill half the maximum food capacity before the simulation starts.

draw()

draw() is the heartbeat of every p5.js sketch, running roughly 60 times per second. Here it does double duty: running the ecosystem's autonomous rules (movement, eating, splitting, dying) AND checking whether a time-limited oracle effect should still be active.

🔬 This removes a parent once it splits into two children. What happens if you comment out this whole block, so parents stay in the world alongside their offspring instead of being replaced?

    if (creature.split()) {
      creatures.splice(i, 1); // Remove the parent creature after it splits
    }
function draw() {
  background(220); // Clear background

  // Apply oracle action if active
  if (oracleAction && frameCount - lastOracleActionFrame < oracleActionDuration) {
    applyOracleAction();
  } else {
    // Clear oracle state if duration passed
    if (oracleAction) {
      oracleMessage = "";
      oracleAction = "";
      oracleIntensity = 0;
      oracleMessageDisplay.html("");
      console.log("Oracle action ended.");
    }
  }

  // Update and display creatures
  for (let i = creatures.length - 1; i >= 0; i--) {
    let creature = creatures[i];
    creature.move();
    creature.eat(food);
    creature.display();

    // Check for splitting
    if (creature.split()) {
      creatures.splice(i, 1); // Remove the parent creature after it splits
    }

    // Creatures lose health over time, die if health reaches 0
    creature.health -= 0.1;
    if (creature.health <= 0) {
      creatures.splice(i, 1); // Remove dead creature
    }
  }

  // Spawn new food pellets
  if (frameCount % foodSpawnRate === 0 && food.length < maxFood) {
    spawnFood();
  }

  // Display food pellets
  displayFood();

  // Update UI stats
  creatureCountSpan.html(`Creatures: ${creatures.length}`);
  let maxGen = creatures.reduce((max, c) => max(max, c.generation), 1);
  generationSpan.html(`Generation: ${maxGen}`);

  // Display oracle message
  oracleMessageDisplay.html(oracleMessage);
}
Line-by-line explanation (9 lines)

🔧 Subcomponents:

conditional Oracle Action Timer if (oracleAction && frameCount - lastOracleActionFrame < oracleActionDuration) {

Keeps applying the oracle's effect for 5 seconds, then clears it once time is up.

for-loop Creature Update Loop for (let i = creatures.length - 1; i >= 0; i--) {

Moves, feeds, draws, splits, and ages every creature each frame, looping backward so removals are safe.

conditional Split Removal Check if (creature.split()) {

Removes the parent creature from the array once it has successfully split into two offspring.

conditional Health Death Check if (creature.health <= 0) {

Removes a creature once its health has dropped to zero or below.

conditional Food Spawn Timer if (frameCount % foodSpawnRate === 0 && food.length < maxFood) {

Spawns a new food pellet only on specific frames, throttling how quickly food appears.

background(220);
Repaints the whole canvas light gray every frame, erasing the previous frame's drawings so motion looks smooth instead of leaving trails.
if (oracleAction && frameCount - lastOracleActionFrame < oracleActionDuration) {
Checks if an oracle action is currently active and still within its 5-second window; if so, applyOracleAction() runs again this frame.
for (let i = creatures.length - 1; i >= 0; i--) {
Loops through every creature backward, which is essential because creatures get removed (spliced) from the array inside this loop.
creature.move(); creature.eat(food); creature.display();
Each frame, every creature wanders a bit, checks if it's touching food, and gets drawn to the screen.
if (creature.split()) { creatures.splice(i, 1); }
If the creature just split into two offspring, the original parent creature is removed from the array.
creature.health -= 0.1;
Every creature slowly loses health each frame, simulating natural aging or hunger.
if (creature.health <= 0) { creatures.splice(i, 1); }
Removes creatures whose health has run out, so old or starved creatures eventually disappear.
if (frameCount % foodSpawnRate === 0 && food.length < maxFood) {
Uses the modulo operator to spawn food only every 'foodSpawnRate' frames, and only if there's still room under the max food limit.
let maxGen = creatures.reduce((max, c) => max(max, c.generation), 1);
Attempts to find the highest generation number among all creatures using Array.reduce().

handleGodCommand()

This function is the bridge between the DOM (the HTML button/input) and the simulation logic - it's triggered by mousePressed() registered in setup(), a common pattern for connecting UI events to p5.js code.

function handleGodCommand() {
  const commandText = godCommandInput.value();
  if (commandText.trim() !== "" && !isOracleThinking) {
    sendToOpenAI(commandText);
  }
}
Line-by-line explanation (3 lines)

🔧 Subcomponents:

conditional Empty/Busy Guard if (commandText.trim() !== "" && !isOracleThinking) {

Prevents sending empty commands or firing a second API request while one is already in progress.

const commandText = godCommandInput.value();
Reads whatever text the user currently has typed in the input box using p5.js's .value() method.
if (commandText.trim() !== "" && !isOracleThinking) {
Only proceeds if the text isn't blank (after trimming whitespace) and the oracle isn't already processing a previous command.
sendToOpenAI(commandText);
Kicks off the async API call with the user's typed command.

sendToOpenAI()

This function shows the standard async/await pattern for calling a web API from JavaScript: build the request, await the response, check for errors, parse the JSON body, and update your app's state - all wrapped in try/catch/finally so failures don't crash the sketch.

async function sendToOpenAI(commandText) {
  isOracleThinking = true;
  oracleMessageDisplay.html("Oracle is contemplating the command...");
  console.log("Sending command to OpenAI:", commandText);

  try {
    const response = await fetch('https://api.openai.com/v1/chat/completions', {
      method: 'POST',
      headers: {
        'Authorization': `Bearer ${apiKey}`,
        'Content-Type': 'application/json'
      },
      body: JSON.stringify({
        model: "gpt-3.5-turbo", // Using gpt-3.5-turbo for cost-effectiveness
        messages: [
          {
            role: "system",
            content: "You are an ancient oracle overseeing a digital ecosystem. Your task is to interpret a god's command and return a JSON object. The JSON should contain an 'action' (one of 'plague', 'feast', 'mutate', 'calm'), an 'intensity' (an integer from 1 to 10), and a 'message' (a short, evocative prophecy or statement related to the action)."
          },
          {
            role: "user",
            content: `God's command: ${commandText}`
          }
        ],
        response_format: { type: "json_object" } // Request JSON output
      })
    });

    if (!response.ok) {
      const errorData = await response.json();
      throw new Error(`OpenAI API error: ${response.status} ${response.statusText} - ${errorData.error.message}`);
    }

    const data = await response.json();
    console.log("OpenAI Response:", data);

    const rawContent = data.choices[0].message.content;
    let parsedResponse;
    try {
      parsedResponse = JSON.parse(rawContent);
    } catch (parseError) {
      console.error("Failed to parse OpenAI JSON response:", rawContent, parseError);
      oracleMessage = "Oracle response was unclear. Try again.";
      oracleMessageDisplay.html(oracleMessage);
      speechSynthesis.speak(new SpeechSynthesisUtterance(oracleMessage));
      return;
    }

    oracleAction = parsedResponse.action;
    oracleIntensity = constrain(parsedResponse.intensity, 1, 10);
    oracleMessage = parsedResponse.message;
    lastOracleActionFrame = frameCount; // Start action timer

    oracleMessageDisplay.html(oracleMessage);
    speechSynthesis.speak(new SpeechSynthesisUtterance(oracleMessage));

  } catch (error) {
    console.error("Error communicating with OpenAI:", error);
    oracleMessage = `Oracle is silent... (Error: ${error.message})`;
    oracleMessageDisplay.html(oracleMessage);
    speechSynthesis.speak(new SpeechSynthesisUtterance(oracleMessage));
  } finally {
    isOracleThinking = false;
  }
}
Line-by-line explanation (12 lines)

🔧 Subcomponents:

calculation OpenAI Fetch Request const response = await fetch('https://api.openai.com/v1/chat/completions', {

Sends the player's typed command to OpenAI's chat model and awaits a structured JSON reply.

conditional HTTP Error Check if (!response.ok) {

Detects a failed HTTP request (like a bad key or rate limit) and throws a descriptive error.

conditional JSON Parse Safety Net } catch (parseError) {

Catches cases where the AI's reply isn't valid JSON, showing a fallback message instead of crashing.

isOracleThinking = true;
Sets a flag so handleGodCommand() won't let the player submit another command while this one is processing.
const response = await fetch('https://api.openai.com/v1/chat/completions', {...})
Sends an HTTP POST request to OpenAI's API and pauses this function (thanks to await) until a response arrives, without freezing the rest of the sketch.
'Authorization': `Bearer ${apiKey}`
Attaches the decoded API key as a bearer token so OpenAI knows who is making the request and can bill the account.
response_format: { type: "json_object" }
Tells the OpenAI model to guarantee its reply is valid JSON, rather than free-form text, making it safe to parse.
if (!response.ok) { throw new Error(...); }
If the server responds with an error status (like 401 or 429), this throws an error that gets caught below and shown to the player.
const rawContent = data.choices[0].message.content;
Digs into OpenAI's response structure to pull out the actual text (JSON string) the model generated.
parsedResponse = JSON.parse(rawContent);
Converts the AI's JSON string into a real JavaScript object with .action, .intensity, and .message properties.
oracleAction = parsedResponse.action;
Stores the AI-chosen action ('plague', 'feast', 'mutate', or 'calm') so draw() knows what effect to apply.
oracleIntensity = constrain(parsedResponse.intensity, 1, 10);
Clamps the AI's intensity value to a safe 1-10 range in case the model returns something out of bounds.
lastOracleActionFrame = frameCount;
Records the current frame number, starting a 5-second countdown for how long this effect stays active.
speechSynthesis.speak(new SpeechSynthesisUtterance(oracleMessage));
Uses the browser's built-in Web Speech API to read the oracle's prophecy out loud.
} finally { isOracleThinking = false; }
Always resets the 'thinking' flag when the function finishes, whether it succeeded or failed, so new commands can be submitted.

applyOracleAction()

This function is where the AI's abstract JSON decision becomes a concrete simulation effect. The switch statement pattern here is a great example of translating a small set of string commands into very different, intensity-scaled behaviors.

🔬 This loop is what makes 'plague' commands devastating. What happens if you remove the '|| killCount > 0' condition, so creatures only die from health loss (no instant kill quota)?

      for (let i = creatures.length - 1; i >= 0; i--) {
        let creature = creatures[i];
        creature.health -= plagueSeverity;
        if (creature.health <= 0 || killCount > 0) {
          creatures.splice(i, 1);
          killCount--;
        }
      }
function applyOracleAction() {
  if (creatures.length === 0) return; // No creatures to act upon

  switch (oracleAction) {
    case 'plague':
      // Reduce health and potentially remove creatures
      let plagueSeverity = oracleIntensity * 0.05; // Health reduction per frame
      let killCount = floor(oracleIntensity * 0.01 * creatures.length); // Percentage to kill immediately

      for (let i = creatures.length - 1; i >= 0; i--) {
        let creature = creatures[i];
        creature.health -= plagueSeverity;
        if (creature.health <= 0 || killCount > 0) {
          creatures.splice(i, 1);
          killCount--;
        }
      }
      // Reduce food as well
      for (let i = 0; i < oracleIntensity; i++) {
        if (food.length > 0) food.splice(floor(random(food.length)), 1);
      }
      break;

    case 'feast':
      // Spawn more food and make creatures grow faster
      const feastFoodMultiplier = oracleIntensity * 2;
      for (let i = 0; i < feastFoodMultiplier; i++) {
        spawnFood();
      }
      // Creatures grow slightly faster during feast
      creatures.forEach(c => c.size += 0.1);
      break;

    case 'mutate':
      // Randomly change color or speed of a subset of creatures
      const mutateCount = floor(oracleIntensity * 0.1 * creatures.length);
      for (let i = 0; i < mutateCount; i++) {
        let creature = random(creatures);
        if (creature) {
          if (random() < 0.5) { // Mutate color
            creature.color = color(random(255), random(255), random(255));
          } else { // Mutate speed
            creature.speed = constrain(creature.speed + random(-0.5, 0.5), 0.5, 3);
          }
          creature.size = constrain(creature.size + random(-2, 2), 10, 50); // Mutate size slightly
        }
      }
      break;

    case 'calm':
      // Stabilize the ecosystem: reduce speed, increase health slightly
      creatures.forEach(c => {
        c.speed = constrain(c.speed * 0.98, 0.8, initialCreatureSpeed * 1.2); // Gradually calm speed
        c.health = constrain(c.health + oracleIntensity * 0.1, 0, 100); // Slowly heal
      });
      // Ensure steady food supply
      if (food.length < maxFood / 2) {
        spawnFood();
      }
      break;

    default:
      console.warn("Unknown oracle action:", oracleAction);
  }
}
Line-by-line explanation (11 lines)

🔧 Subcomponents:

switch-case Oracle Action Switch switch (oracleAction) {

Routes to a different block of behavior depending on which action string the AI returned.

for-loop Plague Kill Loop for (let i = creatures.length - 1; i >= 0; i--) {

Drains health from every creature and removes a percentage of the population immediately, scaled by oracle intensity.

for-loop Mutation Loop for (let i = 0; i < mutateCount; i++) {

Picks random creatures and randomizes their color, speed, or size to simulate genetic mutation.

if (creatures.length === 0) return;
Bails out early if there's nothing left to affect, avoiding wasted work or errors on an empty array.
switch (oracleAction) {
Branches into different behavior depending on the string value the AI chose: 'plague', 'feast', 'mutate', or 'calm'.
let plagueSeverity = oracleIntensity * 0.05;
Scales how much health is drained per frame based on how intense the AI decided the plague should be.
let killCount = floor(oracleIntensity * 0.01 * creatures.length);
Calculates how many creatures to instantly kill this frame, as a percentage of the total population.
if (creature.health <= 0 || killCount > 0) { creatures.splice(i, 1); killCount--; }
Removes a creature either because its health ran out, or because it's one of the 'quota' of creatures being culled this frame.
const feastFoodMultiplier = oracleIntensity * 2;
Determines how many extra food pellets to spawn during a feast, scaled by intensity.
creatures.forEach(c => c.size += 0.1);
Makes every creature grow a little extra each frame while a feast is active.
let creature = random(creatures);
Picks one random creature from the array using p5.js's random() overload that works on arrays.
creature.color = color(random(255), random(255), random(255));
Assigns the mutated creature a completely new random RGB color.
c.speed = constrain(c.speed * 0.98, 0.8, initialCreatureSpeed * 1.2);
Gradually slows every creature toward a calm baseline speed during a 'calm' action.
default: console.warn("Unknown oracle action:", oracleAction);
Logs a warning if the AI ever returns an action string that isn't one of the four expected values.

spawnFood()

This function shows that not everything needs to be a class - food pellets are simple {x, y} objects since they don't need behavior like move() or display() of their own (drawing happens separately in displayFood()).

function spawnFood() {
  if (food.length < maxFood) {
    food.push({
      x: random(width),
      y: random(height)
    });
  }
}
Line-by-line explanation (2 lines)
if (food.length < maxFood) {
Only adds new food if the total count hasn't reached the maximum, preventing unbounded array growth.
food.push({ x: random(width), y: random(height) });
Creates a plain JavaScript object with random x/y coordinates and adds it to the food array - notice food pellets aren't a class, just simple objects.

displayFood()

This function is a clean example of separating data (the food array, filled by spawnFood()) from rendering (displayFood()), which makes it easy to change how food looks without touching how it's created.

function displayFood() {
  fill(0, 200, 0); // Green color for food
  noStroke();
  food.forEach(pellet => {
    circle(pellet.x, pellet.y, foodSize);
  });
}
Line-by-line explanation (2 lines)

🔧 Subcomponents:

for-loop Food Drawing Loop food.forEach(pellet => {

Draws every food pellet currently in the array as a small green circle.

fill(0, 200, 0);
Sets the fill color to green for all food pellets drawn afterward.
food.forEach(pellet => { circle(pellet.x, pellet.y, foodSize); });
Loops through the food array using forEach() and draws a small circle at each pellet's stored x/y position.

windowResized()

windowResized() is a special p5.js function that p5 calls for you automatically - you never call it yourself. It's the standard way to keep a full-window sketch responsive to browser resizing.

function windowResized() {
  resizeCanvas(windowWidth, windowHeight);
}
Line-by-line explanation (1 lines)
resizeCanvas(windowWidth, windowHeight);
A built-in p5.js callback that fires automatically whenever the browser window is resized, keeping the canvas filling the whole window.

📦 Key Variables

creatures array

Holds every active Creature object in the simulation; the whole ecosystem lives in this array.

let creatures = [];
food array

Holds simple {x, y} objects representing food pellets that creatures can eat.

let food = [];
apiKey string

Stores the decoded OpenAI API key used to authenticate fetch() requests.

let apiKey;
initialPopulation number

How many creatures exist at the very start of the simulation.

const initialPopulation = 10;
maxCreatures number

The population cap - once reached, creatures can no longer split into new offspring.

const maxCreatures = 200;
initialCreatureSize number

The starting diameter of new creatures, and the basis for the size threshold needed to split.

const initialCreatureSize = 20;
initialCreatureSpeed number

The starting movement speed of new creatures.

const initialCreatureSpeed = 1.5;
foodSpawnRate number

How many frames pass between automatic food spawns.

const foodSpawnRate = 30;
maxFood number

The maximum number of food pellets allowed on screen at once.

const maxFood = 150;
foodSize number

The diameter used to draw and collision-check food pellets.

const foodSize = 5;
oracleMessage string

The current prophecy text returned by the AI, shown in the UI.

let oracleMessage = "";
oracleAction string

The current active action name ('plague', 'feast', 'mutate', or 'calm') that draw() applies each frame.

let oracleAction = "";
oracleIntensity number

How strong the AI wants the current action to be, on a 1-10 scale, used to scale effects.

let oracleIntensity = 0;
lastOracleActionFrame number

Records the frameCount when the current oracle action began, used to time out the effect after 5 seconds.

let lastOracleActionFrame = -Infinity;
oracleActionDuration number

How many frames (5 seconds worth) an oracle action stays active before resetting.

const oracleActionDuration = 5 * 60;
isOracleThinking boolean

A flag preventing multiple simultaneous OpenAI requests while one is already in flight.

let isOracleThinking = false;

🔧 Potential Improvements (5)

Here are some ways this code could be enhanced:

BUG draw() - `let maxGen = creatures.reduce((max, c) => max(max, c.generation), 1);`

The reduce callback's parameter is named 'max', which shadows the p5.js/JS Math max reference being called as `max(max, c.generation)`. Since 'max' is now a number (the accumulator), calling it as a function throws a TypeError every frame this line runs, which would break the generation display (and possibly the whole draw loop, depending on error handling).

💡 Rename the parameter to avoid shadowing, e.g. `creatures.reduce((acc, c) => Math.max(acc, c.generation), 1)`.

BUG draw() - creature update loop

When a creature splits, `creatures.splice(i, 1)` removes it and the array shifts down, but the code then continues to run `creature.health -= 0.1;` and a second `creatures.splice(i, 1)` check on the same index `i` - which now refers to a completely different creature that just shifted into that slot. This can incorrectly remove an unrelated, healthy creature the same frame a split happens.

💡 After a successful split, use `continue` to skip the rest of that loop iteration: `if (creature.split()) { creatures.splice(i, 1); continue; }`.

BUG Top of file - `getApiKey()` / OpenAI key handling

The OpenAI API key is only lightly obfuscated with XOR and base64 in client-side code, then sent directly from the browser to OpenAI. Anyone can open dev tools, call getApiKey(), or read the Network tab to steal the key and rack up charges on the developer's account.

💡 Move the fetch() call to a small backend server (or serverless function) that holds the real API key privately, and have the sketch call that server instead of OpenAI directly.

PERFORMANCE Creature.eat()

Every creature checks distance against every food pellet each frame (an O(creatures × food) check), which can get slow as both arrays grow toward their maximums (200 creatures × 150 food = up to 30,000 distance checks per frame).

💡 For larger populations, consider spatial partitioning (a grid) so creatures only check food pellets in nearby cells instead of the entire array.

FEATURE applyOracleAction()

If the AI ever returns an action string outside 'plague'/'feast'/'mutate'/'calm' (e.g. due to a model hallucination), the switch statement's default case just logs a console warning and does nothing visible to the player.

💡 Show a friendly on-screen message like 'The oracle's words were unclear' when the default case triggers, so players get feedback instead of silence.

🔄 Code Flow

Code flow showing getapikey, constructor, display, move, eat, split, setup, draw, handlegodcommand, sendtoopenai, applyoracleaction, spawnfood, displayfood, windowresized

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

graph TD start[Start] --> setup[setup] setup --> draw[draw loop] draw --> creature-update-loop[Creature Update Loop] creature-update-loop --> move[random-walk] creature-update-loop --> boundary-constrain creature-update-loop --> eat[food-loop] food-loop --> collision-check collision-check -->|Collision Detected| eat collision-check -->|No Collision| food-loop creature-update-loop --> split[split-condition] split-condition -->|Eligible| split split-condition -->|Not Eligible| creature-update-loop creature-update-loop --> death-check death-check -->|Health <= 0| death-check death-check -->|Health > 0| creature-update-loop draw --> oracle-timer-check oracle-timer-check -->|Active| applyoracleaction oracle-timer-check -->|Inactive| draw draw --> food-spawn-timer food-spawn-timer -->|Spawn Food| spawnfood food-spawn-timer -->|Do Not Spawn| draw click setup href "#fn-setup" click draw href "#fn-draw" click creature-update-loop href "#sub-creature-update-loop" click move href "#sub-random-walk" click boundary-constrain href "#sub-boundary-constrain" click eat href "#fn-eat" click food-loop href "#sub-food-loop" click collision-check href "#sub-collision-check" click split href "#fn-split" click split-condition href "#sub-split-condition" click death-check href "#sub-death-check" click oracle-timer-check href "#sub-oracle-timer-check" click applyoracleaction href "#fn-applyoracleaction" click food-spawn-timer href "#sub-food-spawn-timer" click spawnfood href "#fn-spawnfood"

❓ Frequently Asked Questions

What visual experience does the AI Ecosystem Oracle - XeLseDai sketch provide?

This sketch creates a vibrant, animated ecosystem filled with colorful creatures that evolve and interact within a dynamic environment.

How can users interact with the XeLseDai sketch to influence the ecosystem?

Users can type commands like 'feast' or 'plague' to affect the simulation, prompting the AI oracle to respond with prophecies about the species' survival.

What creative coding concepts are showcased in the AI Ecosystem Oracle - XeLseDai sketch?

The sketch demonstrates concepts such as procedural animation, random movement algorithms, and AI-based interaction through the integration of the OpenAI API.

Preview

AI Ecosystem Oracle - xelsed.ai - p5.js creative coding sketch preview
Sketch Preview
Code flow diagram showing the structure of AI Ecosystem Oracle - xelsed.ai - Code flow showing getapikey, constructor, display, move, eat, split, setup, draw, handlegodcommand, sendtoopenai, applyoracleaction, spawnfood, displayfood, windowresized
Code Flow Diagram