Chance @ Life
Exploring my exsistentialism through an interactive simulation of biological fertilization.

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.`);
}
}