Chance @ Life

Exploring my exsistentialism through an interactive simulation of biological fertilization.

Year:
2025
Tools:
ChatGPT, P5.js

The Assignment

"Create a sketch that would simulate an existing natural system - look at physics, biology, and other natural sciences for good examples. Start with the environment. Examine the agents in the system, their relationships to each other and the environment they are in. The look at how this system would develop over time. What are the rules that you are going to come up with, what are the parameters you are going to feed into them and what effect will the changes have on the development of the system."

What I made

The "Chance of Life Simulator" is a p5.js sketch designed to model a natural biological system: the process of fertilization. The project's core goal is to visualize the extreme improbability of a specific individual's existence (represented by a single "blue" agent) against a backdrop of millions of competitors and a hostile, randomized environment.The simulation is an agent-based system that runs continuously, logging the parameters of each "attempt" to the console. It only pauses when the target agent ("you") successfully wins the "race," demonstrating the sheer number of failed attempts that were statistically more likely to occur.

System Design: The Agents

The system is comprised of two primary agent classes, Sperm and Egg, which interact with each other and the environment.

The Egg Agent (Passive Target)
The Egg is a simple, passive agent that acts as the "finish line." Its state (position and size) and behavior (display) are defined in its class:

/**
 * The Egg class (the target).
 * A simple, passive agent.
 */
class Egg {
  constructor() {
    this.pos = createVector(width / 2, 80);
    this.size = TARGET_SIZE; // Uses the global parameter
  }

  /**
   * Draws the egg with a simple glow effect.
   */
  display() {
    noStroke();
    // Outer glow
    fill(255, 200, 150, 50);
    ellipse(this.pos.x, this.pos.y, this.size + 20);
    // Inner glow
    fill(255, 200, 150, 100);
    ellipse(this.pos.x, this.pos.y, this.size + 10);
    // Core
    fill(255, 220, 200);
    ellipse(this.pos.x, this.pos.y, this.size);
  }
}

The Sperm Agent (Active Competitor)
The Sperm is the core, active agent in the simulation. Its complex state and behaviors are defined in its class.

/**
 * The Sperm class (the agent).
 * Represents one of the millions of competitors.
 */
class Sperm {
  constructor(isYou) {
    this.pos = createVector(random(width), height - 20);

    // 1. Set Speed (Velocity)
    // Speed is based on the global Vigor and Diversity parameters
    let minSpeed = ENV_VIGOR - POPULATION_DIVERSITY;
    let maxSpeed = ENV_VIGOR + POPULATION_DIVERSITY;
    if (minSpeed < 0.5) minSpeed = 0.5; // Prevent moving backwards
    this.vel = createVector(0, -random(minSpeed, maxSpeed));

    // 2. Set State
    this.isYou = isYou;
    this.isAlive = true;
    this.isOffScreen = false;
    this.color = isYou ? color(0, 150, 255) : color(255, 50, 50, 150);

    // 3. Set visual properties
    this.history = []; // Stores past positions for the tail
    this.tailLength = 7;
    // Used by Perlin noise to create a unique, organic wiggle
    this.noiseOffset = random(1000);

    // 4. Check against immune system
    if (random(100) < ENV_IMMUNE_STRENGTH) {
      this.isAlive = false;
      this.color = color(100); // Culled sperm are gray
    }
  }

  /**
   * Updates the sperm's position.
   */
  move() {
    // Dead, off-screen, or winning sperm don't move
    if (!this.isAlive || this.isOffScreen) return;

    // 1. Calculate wiggle
    // Perlin noise creates a smooth, random "organic" movement
    let wiggleAmount = map(ENV_VIGOR, 2, 5, 1, 4, true);
    let wiggleSpeed = map(ENV_VIGOR, 2, 5, 0.08, 0.15, true);
    let wiggle = map(noise(this.noiseOffset), 0, 1, -wiggleAmount, wiggleAmount);

    // 2. Apply forces
    this.pos.x += wiggle;
    this.pos.add(this.vel); // Apply main velocity
    this.noiseOffset += wiggleSpeed; // Increment noise for next frame

    // 3. Check boundaries
    if (this.pos.y < -20 || this.pos.x < -20 || this.pos.x > width + 20) {
      this.isOffScreen = true;
    }

    // 4. Update tail history
    this.history.push(this.pos.copy());
    if (this.history.length > this.tailLength) {
      this.history.splice(0, 1); // Remove the oldest point
    }
  }

  /**
   * Draws the sperm (head and tail).
   */
  display() {
    // Don't draw if it's fully off-screen
    if (this.isOffScreen) return;
    
    // Draw the tail first, but only if alive
    if (this.isAlive) {
      beginShape();
      noFill();
      stroke(this.color);
      strokeWeight(3);
      for (let i = 0; i < this.history.length; i++) {
        let pos = this.history[i];
        
        // Map 'i' to amplitude, so the tip (i=0) wiggles most
        // and the base (i=length-1) wiggles least.
        let amplitude = map(i, 0, this.history.length - 1, 6, 0);
        
        // Use sin() for a fast, flicking visual-only wiggle
        let visualWiggle = sin(frameCount * 0.6 + i * 0.5) * amplitude;
        
        vertex(pos.x + visualWiggle, pos.y);
      }
      endShape();
    }

    // Draw the head (a simple ellipse)
    fill(this.color);
    noStroke();
    ellipse(this.pos.x, this.pos.y, 8, 8);
  }

  /**
   * Checks if this sperm has successfully fertilized the target.
   */
  checkWin(target) {
    // Can't win if dead or off-screen
    if (!this.isAlive || this.isOffScreen) return false;

    // Standard distance check
    let d = dist(this.pos.x, this.pos.y, target.pos.x, target.pos.y);
    if (d < target.size / 2) {
      return true;
    }
    return false;
  }
}

The Environment and Systemic Forces

The environment is defined by global parameters that exert pressure on all agents. These forces are re-randomized at the start of every new attempt inside the startNewAttempt() function.

// --- State & Global Variables ---
// ... (omitted for brevity) ...

// Environmental Parameters
let ENV_VIGOR;
let POPULATION_DIVERSITY;
let TARGET_SIZE;
let ENV_IMMUNE_STRENGTH;
let numSperm;

// ... (omitted for brevity) ...

/**
 * Resets all parameters and agents for a new attempt.
 */
function startNewAttempt() {
  attempts++;

  // 1. Set all environmental parameters for this new attempt
  ENV_VIGOR = random(2.0, 5.0);
  POPULATION_DIVERSITY = random(0.1, 2.0);
  TARGET_SIZE = random(20, 80);
  ENV_IMMUNE_STRENGTH = immuneSlider.value(); // Read from slider
  numSperm = floor(random(1, 501));

  // ... (rest of setup) ...
}

Simulation Loop and States

The simulation operates as a continuous state machine, managed by the main draw() loop and the startNewAttempt() function.

- setup():
Initializes the canvas and UI, then calls startNewAttempt() for the first time.
- draw(): Acts as the state manager, checking if a winner exists. If not, it runs updateSimulation(). If so, it runs drawEndScreen().
- startNewAttempt():
Resets all parameters and agents to begin the "Start" state.
- updateSimulation():
Contains the logic for the "Race" state, checking for win/fail conditions.
- drawEndScreen():
Manages the "Failure" and "Success" states, either auto-restarting or pausing.

// --- Main p5.js Functions ---

function setup() {
  createCanvas(600, 600);

  // --- Create UI Elements (Slider) ---
  if (!immuneSlider) {
    // ... (slider creation) ...
  }

  // Start the first simulation attempt
  startNewAttempt();
}

/**
 * The main simulation loop.
 * It's a "state machine" that either updates the race or draws the end screen.
 */
function draw() {
  background(0, 0, 30);

  // 1. Draw the environment and UI
  drawEnvironment();
  drawUI();

  // 2. Check the simulation state
  if (winner) {
    // If we have a winner (or failure), draw the end screen
    drawEndScreen();
  } else {
    // Otherwise, keep the race running
    updateSimulation();
  }
}

/**
 * Runs one frame of the "race" logic.
 * Moves, draws, and checks all sperm.
 */
function updateSimulation() {
  let anyoneAliveAndOnScreen = false;

  for (let s of sperm) {
    s.move();
    s.display();

    // Check if anyone is still in the race
    if (s.isAlive && !s.isOffScreen) {
      anyoneAliveAndOnScreen = true;
    }

    // Check if this sperm won
    if (s.checkWin(egg)) {
      // ... (handle win logic) ...
      break; 
    }
  }

  // Check for a "population lost" fail state
  if (!winner && !anyoneAliveAndOnScreen) {
    // ... (handle population lost logic) ...
  }
}

/**
 * Draws the end-of-race screen (Success or Failure).
 */
function drawEndScreen() {
  // ... (draws all sperm) ...

  if (winner.isYou) {
    // --- SUCCESS SCREEN ---
    // ... (draws success text and animation) ...
  } else {
    // --- FAILURE (Auto-Restart) SCREEN ---
    autoRestartTimer--;
    if (autoRestartTimer <= 0) {
      startNewAttempt(); // Restart the simulation
    }
    // ... (draws failure text) ...
  }
}

User Interaction and Data Collection

Direct Control: The user's primary control is the HTML slider, which is created in setup() and read in startNewAttempt(). A mouse click is also used to reset the simulation after success.

function setup() {
  // ...
  if (!immuneSlider) {
    immuneSliderLabel = createSpan('Immune Strength (0-100%): ');
    // ...
    immuneSlider = createSlider(0, 100, 30, 1); 
    // ...
  }
  // ...
}

function startNewAttempt() {
  // ...
  ENV_IMMUNE_STRENGTH = immuneSlider.value(); // Read from slider
  // ...
}

function mousePressed() {
  // Check if the simulation is paused on the success screen
  if (winner && winner.isYou) {
    console.log("--- RESETTING SIMULATION ---");
    attempts = 0; // Reset counter
    startNewAttempt(); // Start over
  }
}

Data Collection: Data is output to the browser console. startNewAttempt() logs the parameters for the new attempt, and updateSimulation() logs the result (success or failure).

function startNewAttempt() {
  // ...
  // Log these new conditions to the console
  console.log(`--- Attempt #${attempts} ---`);
  console.log(`  Parameters: Pop: ${numSperm}, Immunity: ${ENV_IMMUNE_STRENGTH}%, Vigor: ${nfs(ENV_VIGOR, 1, 1)}, Diversity: ${nfs(POPULATION_DIVERSITY, 1, 1)}, Target: ${floor(TARGET_SIZE)}`);
  // ...
}

function updateSimulation() {
  // ...
  for (let s of sperm) {
    // ...
    if (s.checkWin(egg)) {
      // ...
      if (winner.isYou) {
        // Success!
        console.log(`  Result: SUCCESS! 'You' were born.`);
        console.log(`  Total Attempts: ${attempts}`);
        console.log('--------------------');
      } else {
        // Failure
        console.log(`  Result: FAILED. A 'Red' sperm won.`);
      }
      break; // Stop the loop
    }
  }
  // Check for a "population lost" fail state
  if (!winner && !anyoneAliveAndOnScreen) {
    // ...
    console.log(`  Result: FAILED. Population lost.`);
  }
}

Final sketch!