Sketch 2026-05-17 06:18

This sketch simulates a population of 120 AI-controlled cars learning to drive around a procedurally generated circular track using genetic algorithms and neural networks. Over generations, the population evolves better driving behaviors through natural selection, with the camera following the best performer while inferior drivers are replaced by offspring of champions.

🧪 Try This!

Experiment with the code by making these changes:

  1. Speed up evolution by increasing population size — More cars mean more genetic diversity and faster learning, but slower frame rate. Try POP_SIZE = 200 to see evolution accelerate.
  2. Make the leader car bright red instead of cyan — Change the color of the best car to stand out differently. Try RGB values for red, orange, or any color you want.
  3. Shorten generation time to speed up breeding — 30 seconds per generation takes time to watch. Try MAX_GEN_TIME = 300 for 5-second generations and watch evolution happen faster.
  4. Reward checkpoints much more heavily — Change the fitness formula to exponentially favor progress. Replace the 2 in pow(2, car.score) with 3 or 5 to make checkpoint count dominate.
  5. Increase mutation rate for bolder evolution — Mutations drive exploration. Try 0.1 (10% mutation rate) instead of 0.05 to see faster, more chaotic learning.
  6. Make the track tighter by increasing base radius variation — Change the -baseR*0.4 to -baseR*0.2 to reduce bumps and curves, creating a smoother track for easier learning.
Prefer the full editor? Open it there →

📖 About This Sketch

This sketch creates a mesmerizing genetic algorithm visualization where 120 cars with simple neural network 'brains' compete to navigate a massive procedurally generated circular track. Each car starts with nearly-random weights and gradually improves through crossover and mutation—the hallmarks of evolutionary computation. The visualizer displays ray-casts from the leading car and a grid-based camera system that follows the best performer, creating an intuitive window into how artificial selection shapes behavior over time.

The code demonstrates several advanced p5.js and programming concepts working in concert: procedural track generation using Perlin noise, collision detection with line-segment intersection, a genetic algorithm with fitness selection, neural network decision-making, and smooth camera following. By reading through it, you'll learn how to build population-based simulations, breed digital creatures, and visualize learning in real time—skills that unlock entire genres of interactive art and games.

⚙️ How It Works

  1. When the sketch loads, buildTrack() generates a unique circular race track using Perlin noise to vary the radius and width of the track, creating natural-looking curves and tight sections. This track is stored as walls (collision boundaries) and checkpoints (progress markers). initPopulation() then creates 120 cars, each with a small DNA object containing 8 randomly initialized neural network weights between -0.05 and 0.05.
  2. Every frame, the draw loop runs the simulation at speeds 1x to 10x normal by looping through multiple update calls per rendered frame. Each car calls update(), which uses its neural network to decide how much to turn based on sensor inputs from a look() function that casts seven rays forward at different angles, measuring distances to walls.
  3. The car's think() method multiplies each ray distance by its corresponding DNA weight, sums them, and outputs a turn amount. The car moves forward, checks for wall collisions (which cause scraping and speed loss), and detects checkpoint crossings (which increment its score). A fitness formula rewards checkpoints exponentially and penalizes wall hits.
  4. After 900 frames (15 seconds at 1x speed), nextGeneration() calculates fitness for all cars. Parents are selected probabilistically by fitness using a roulette wheel algorithm. Two parents produce a child via DNA crossover (each parent contributes half their genes at a random split point), which is then mutated by tiny random amounts (5% mutation rate).
  5. The new generation replaces the old one, and the cycle repeats. Over many generations, weights drift from ±0.05 toward values that successfully navigate the track. The camera follows the highest-scoring car, displaying its ray-casts in neon cyan when it's the leader, creating a visual guide to how the winning strategy evolved.

🎓 Concepts You'll Learn

Genetic AlgorithmsNeural NetworksFitness SelectionProcedural GenerationCollision DetectionCamera FollowingPopulation Simulation

📝 Code Breakdown

setup()

setup() runs once when the sketch starts. It's where you initialize the canvas, load external resources, and set up variables that won't change during the simulation. Notice that this sketch uses both p5.js functions (like createCanvas) and custom functions (like buildTrack) to organize the startup logic.

function setup() {
  createCanvas(windowWidth, windowHeight);
  trackSeed = random(10000);
  
  speedSlider = select('#speedSlider');
  newTrackBtn = select('#newTrackBtn');
  newTrackBtn.mousePressed(() => {
    trackSeed = random(10000);
    generation = 1;
    highestScore = 0;
    genTimer = 0;
    buildTrack();
    initPopulation();
  });

  buildTrack();
  initPopulation();
  
  camX = startPos.x;
  camY = startPos.y;
}
Line-by-line explanation (7 lines)

🔧 Subcomponents:

calculation Canvas Creation createCanvas(windowWidth, windowHeight);

Creates a full-screen canvas that scales to the window size

calculation Track Randomization Seed trackSeed = random(10000);

Generates a seed number to ensure procedurally generated tracks are reproducible

conditional New Track Button Handler newTrackBtn.mousePressed(() => { trackSeed = random(10000); generation = 1; highestScore = 0; genTimer = 0; buildTrack(); initPopulation(); });

Resets the simulation when user clicks 'Generate New Track' button

createCanvas(windowWidth, windowHeight);
Creates a canvas sized to fill the entire browser window, allowing full-screen visualization
trackSeed = random(10000);
Picks a random seed number between 0-10000 that will be used by Perlin noise to generate the track consistently
speedSlider = select('#speedSlider');
Grabs the HTML slider element so the code can read how fast the user wants the simulation to run
newTrackBtn.mousePressed(() => { ... });
Creates a callback function that runs whenever the user clicks the 'Generate New Track' button, resetting everything
buildTrack();
Generates the circular track with walls and checkpoints using Perlin noise
initPopulation();
Creates 120 new cars with random, nearly-zero neural network weights
camX = startPos.x;
Positions the camera at the starting checkpoint so you're looking at the race start

draw()

draw() is the heartbeat of the sketch—it runs 60 times per second. This function contains three major sections: (1) the simulation logic that updates all cars and manages generations, (2) the camera tracking system that keeps the leader centered, and (3) the rendering code that draws the track, cars, and UI. Notice how it uses nested loops to update multiple cars in a single frame, and how it uses conditional checks (like checking if a wall is on-screen) to optimize performance on a massive track.

🔬 The code draws faded cars first, then the leader on top. What happens if you comment out the first loop so you ONLY see the leader car? What do you lose?

  // Draw regular cars (faded so we can see the leader)
  for (let car of population) {
    if (car !== bestCar) car.show(false);
  }
  
  // Highlight leader on top
  if (bestCar) bestCar.show(true);

🔬 This loop draws vertical grid lines. What happens if you change gridSize from 200 to 50? To 500? How does grid density affect the visual feedback?

  let gridSize = 200;
  let startGridX = floor((camX - width/2) / gridSize) * gridSize;
  let startGridY = floor((camY - height/2) / gridSize) * gridSize;
  for (let x = startGridX; x < camX + width/2 + gridSize; x += gridSize) {
    line(x, camY - height/2, x, camY + height/2);
  }
function draw() {
  background(20);
  
  // --- LOGIC ---
  let simSpeed = speedSlider.value();
  for (let s = 0; s < simSpeed; s++) {
    for (let car of population) {
      car.update(walls, checkpoints);
    }
    
    // Time-based generation reset instead of death-based
    genTimer++;
    if (genTimer >= MAX_GEN_TIME) {
      nextGeneration();
      genTimer = 0;
    }
  }

  // Find the best car to follow (based on checkpoints + how close they are to the next one)
  let bestCar = null;
  let maxProgress = -Infinity;
  for (let car of population) {
    let nextCP = checkpoints[car.targetCheckpoint];
    let cpCenter = p5.Vector.lerp(nextCP.a, nextCP.b, 0.5);
    let distToCP = p5.Vector.dist(car.pos, cpCenter);
    
    // Formula to accurately track the car furthest along the track
    let currentProgress = (car.score * 10000) - distToCP;
    
    if (currentProgress > maxProgress) {
      maxProgress = currentProgress;
      bestCar = car;
    }
  }
  
  if (bestCar) {
    camX = lerp(camX, bestCar.pos.x, 0.1);
    camY = lerp(camY, bestCar.pos.y, 0.1);
  } else {
    camX = lerp(camX, startPos.x, 0.05);
    camY = lerp(camY, startPos.y, 0.05);
  }

  // --- RENDERING ---
  push();
  translate(width / 2 - camX, height / 2 - camY);

  // Moving background grid
  stroke(255, 255, 255, 15);
  strokeWeight(1);
  let gridSize = 200;
  let startGridX = floor((camX - width/2) / gridSize) * gridSize;
  let startGridY = floor((camY - height/2) / gridSize) * gridSize;
  for (let x = startGridX; x < camX + width/2 + gridSize; x += gridSize) {
    line(x, camY - height/2, x, camY + height/2);
  }
  for (let y = startGridY; y < camY + height/2 + gridSize; y += gridSize) {
    line(camX - width/2, y, camX + width/2, y);
  }

  // Draw Track Walls
  stroke(255);
  strokeWeight(3);
  let viewPadding = 500;
  for (let w of walls) {
    if (max(w.a.x, w.b.x) > camX - width/2 - viewPadding && 
        min(w.a.x, w.b.x) < camX + width/2 + viewPadding &&
        max(w.a.y, w.b.y) > camY - height/2 - viewPadding && 
        min(w.a.y, w.b.y) < camY + height/2 + viewPadding) {
      line(w.a.x, w.a.y, w.b.x, w.b.y);
    }
  }
  
  // Draw Checkpoints
  stroke(255, 255, 255, 20);
  strokeWeight(1);
  for (let cp of checkpoints) {
    if (max(cp.a.x, cp.b.x) > camX - width/2 - viewPadding && 
        min(cp.a.x, cp.b.x) < camX + width/2 + viewPadding) {
      line(cp.a.x, cp.a.y, cp.b.x, cp.b.y);
    }
  }

  // Draw regular cars (faded so we can see the leader)
  for (let car of population) {
    if (car !== bestCar) car.show(false);
  }
  
  // Highlight leader on top
  if (bestCar) bestCar.show(true);

  pop();

  // Draw UI Stats
  noStroke();
  fill(255);
  textSize(16);
  textAlign(RIGHT);
  text(`Generation: ${generation}`, width - 20, 30);
  text(`Record Checkpoints: ${highestScore}`, width - 20, 50);
  
  // Show timer instead of alive count
  let secondsLeft = floor((MAX_GEN_TIME - genTimer) / 60);
  text(`Gen Time Left: ${secondsLeft}s`, width - 20, 70);
  
  // Creator Watermark!
  fill(0, 255, 200, 180); // Neon cyan
  textSize(20);
  text("Made by Corbun", width - 20, height - 25);
}
Line-by-line explanation (17 lines)

🔧 Subcomponents:

for-loop Multi-Step Simulation for (let s = 0; s < simSpeed; s++) { for (let car of population) { car.update(walls, checkpoints); } ... }

Runs the physics and AI update for every car, multiple times per frame based on simulation speed slider

conditional Generation Timer Check if (genTimer >= MAX_GEN_TIME) { nextGeneration(); genTimer = 0; }

After 900 frames, breeds a new generation and resets the timer

calculation Smooth Camera Tracking camX = lerp(camX, bestCar.pos.x, 0.1); camY = lerp(camY, bestCar.pos.y, 0.1);

Smoothly moves the camera toward the leader's position using linear interpolation

conditional Render Optimization if (max(w.a.x, w.b.x) > camX - width/2 - viewPadding && ...) { line(w.a.x, w.a.y, w.b.x, w.b.y); }

Only draws walls and checkpoints that are visible on screen or near the viewport edge

background(20);
Clears the entire canvas with dark gray (RGB 20,20,20) each frame, erasing the previous frame
let simSpeed = speedSlider.value();
Reads the slider value (1-10) to determine how many simulation steps to run this frame
for (let s = 0; s < simSpeed; s++) {
Outer loop that repeats the entire simulation update 1-10 times per frame depending on slider
for (let car of population) {
Inner loop iterates over all 120 cars in the population
car.update(walls, checkpoints);
Updates each car's position, rotation, and collision state based on its neural network decisions
genTimer++;
Increments a counter that tracks how many frames have passed in the current generation
if (genTimer >= MAX_GEN_TIME) {
When the timer reaches 900 frames, it's time to breed a new generation
nextGeneration();
Selects parents by fitness, creates offspring through crossover and mutation, and replaces the population
let currentProgress = (car.score * 10000) - distToCP;
Calculates each car's progress as a combination of checkpoints passed and distance to the next checkpoint
camX = lerp(camX, bestCar.pos.x, 0.1);
Smoothly interpolates the camera's x position toward the leader, moving 10% of the way each frame
push();
Saves the current drawing state (translation, rotation, fill, etc.) before applying camera transforms
translate(width / 2 - camX, height / 2 - camY);
Moves the world so the leader is always at screen center; subtracting camX/Y inverts the movement
for (let x = startGridX; x < camX + width/2 + gridSize; x += gridSize) {
Draws vertical grid lines that move with the camera, creating the illusion of the track moving beneath the cars
if (max(w.a.x, w.b.x) > camX - width/2 - viewPadding && ...) {
Only draws walls that are inside or near the camera's visible area, saving performance on large tracks
if (car !== bestCar) car.show(false);
Draws all non-leader cars with faded colors so they don't obscure the leader
if (bestCar) bestCar.show(true);
Draws the leader car on top with bright cyan color and ray-cast visualization lines
text(`Generation: ${generation}`, width - 20, 30);
Displays the current generation number in the top-right corner of the screen

buildTrack()

buildTrack() creates a completely unique circular racetrack each time it's called, using Perlin noise for natural variation. The track is stored as a collection of line segments (walls) and perpendicular lines (checkpoints). This function demonstrates procedural generation—the art of using algorithms and randomness to create content that looks hand-designed. The use of noiseSeed() means the same seed always generates the same track, allowing reproducibility for testing.

function buildTrack() {
  walls = [];
  checkpoints = [];
  trackPts = [];
  noiseSeed(trackSeed);
  
  let numSegments = 350; 
  let baseR = 4000;      
  
  for (let i = 0; i < numSegments; i++) {
    let a = map(i, 0, numSegments, 0, TWO_PI);
    let xoff = cos(a) * 3 + 3; 
    let yoff = sin(a) * 3 + 3;
    
    let rOut = baseR + map(noise(xoff, yoff), 0, 1, -baseR*0.4, baseR*0.4);
    let trackWidth = map(noise(xoff + 10, yoff + 10), 0, 1, 300, 700); 
    let rIn = rOut - trackWidth;
    
    trackPts.push({
      out: createVector(cos(a) * rOut, sin(a) * rOut),
      in: createVector(cos(a) * rIn, sin(a) * rIn)
    });
  }

  for (let i = 0; i < numSegments; i++) {
    let next = (i + 1) % numSegments;
    walls.push({ a: trackPts[i].out, b: trackPts[next].out });
    walls.push({ a: trackPts[i].in, b: trackPts[next].in });
    checkpoints.push({ a: trackPts[i].in, b: trackPts[i].out });
  }

  let cp0 = checkpoints[0];
  let cp1 = checkpoints[1];
  let mid0 = p5.Vector.lerp(cp0.a, cp0.b, 0.5);
  let mid1 = p5.Vector.lerp(cp1.a, cp1.b, 0.5);
  
  startPos = mid0.copy();
  startAngle = p5.Vector.sub(mid1, mid0).heading();
}
Line-by-line explanation (14 lines)

🔧 Subcomponents:

for-loop Perlin Noise Track Points for (let i = 0; i < numSegments; i++) { let a = map(i, ..., let rOut = baseR + map(noise(xoff, yoff), ..., trackPts.push(...) }

Creates 350 points around a circle, using Perlin noise to vary radius and width, generating natural-looking bumps and tight sections

for-loop Wall and Checkpoint Creation for (let i = 0; i < numSegments; i++) { let next = (i + 1) % numSegments; walls.push(...); checkpoints.push(...); }

Connects consecutive track points to create inner and outer walls, and adds checkpoints perpendicular to the track

calculation Starting Position and Angle startPos = mid0.copy(); startAngle = p5.Vector.sub(mid1, mid0).heading();

Calculates where cars spawn and what direction they should face based on the first two checkpoints

walls = [];
Empties the walls array so we start fresh when regenerating the track
noiseSeed(trackSeed);
Seeds the Perlin noise generator with the trackSeed, ensuring the same seed always produces the same track shape
let numSegments = 350;
Sets how many points to sample around the circle—350 creates a smooth track with high detail
let baseR = 4000;
Sets the average radius of the track in pixels; larger values create bigger tracks
let a = map(i, 0, numSegments, 0, TWO_PI);
Maps the loop index (0-350) to an angle in radians (0-2π), so we sample 350 points around a full circle
let rOut = baseR + map(noise(xoff, yoff), 0, 1, -baseR*0.4, baseR*0.4);
Uses 2D Perlin noise to vary the outer radius by ±40% of baseR, creating natural-looking curves and bumpy sections
let trackWidth = map(noise(xoff + 10, yoff + 10), 0, 1, 300, 700);
Uses a different Perlin noise sample to vary track width between 300 and 700 pixels, making some sections tight and others wide
let rIn = rOut - trackWidth;
Calculates the inner radius by subtracting the track width from the outer radius
trackPts.push({ out: createVector(cos(a) * rOut, sin(a) * rOut), in: createVector(cos(a) * rIn, sin(a) * rIn) });
Converts polar coordinates (angle and radius) to Cartesian coordinates (x, y) and stores both outer and inner points
let next = (i + 1) % numSegments;
Gets the index of the next point in the circle, wrapping around to 0 when i reaches 349
walls.push({ a: trackPts[i].out, b: trackPts[next].out });
Creates a wall segment connecting the current and next outer points
checkpoints.push({ a: trackPts[i].in, b: trackPts[i].out });
Creates a checkpoint perpendicular to the track, going from inner radius to outer radius
let mid0 = p5.Vector.lerp(cp0.a, cp0.b, 0.5);
Finds the midpoint of the first checkpoint—this is where cars will spawn
startAngle = p5.Vector.sub(mid1, mid0).heading();
Calculates the angle from the first checkpoint to the second, so cars spawn facing the correct direction

Car.update()

update() runs once per frame for each car and is the core of the simulation. It orchestrates four key steps: (1) getting sensor inputs, (2) using the neural network to decide a turn, (3) moving the car and checking for collisions, and (4) checking if the car reached a checkpoint. This is where the 'live' behavior emerges from the DNA weights—the neural network weights determine turn decisions, which accumulate into driving patterns like 'stay toward the middle' or 'follow the left wall.'

🔬 This entire block handles collisions. What happens if you comment out the `this.pos = oldPos.copy()` line, so the car can pass through walls but still loses speed? Does it make the track harder or easier for the AI to learn?

    // Bouncing/Scraping Logic instead of dying
    let hit = false;
    for (let w of nearWalls) {
      if (checkIntersect(oldPos.x, oldPos.y, this.pos.x, this.pos.y, w.a.x, w.a.y, w.b.x, w.b.y)) {
        hit = true;
        break;
      }
    }

    if (hit) {
      this.pos = oldPos.copy(); // Block movement
      this.currentSpeed *= 0.5; // Scrape wall / lose speed
      this.wallHits++;
    } else {
      this.currentSpeed = lerp(this.currentSpeed, this.maxSpeed, 0.05); // Accelerate smoothly
    }
  update(wallsArr, checkpoints) {
    let nearWalls = this.getNearbyWalls();
    let inputs = this.look(nearWalls);
    this.think(inputs);
    
    let oldPos = this.pos.copy();
    let velocity = p5.Vector.fromAngle(this.angle).mult(this.currentSpeed);
    this.pos.add(velocity);

    // Bouncing/Scraping Logic instead of dying
    let hit = false;
    for (let w of nearWalls) {
      if (checkIntersect(oldPos.x, oldPos.y, this.pos.x, this.pos.y, w.a.x, w.a.y, w.b.x, w.b.y)) {
        hit = true;
        break;
      }
    }

    if (hit) {
      this.pos = oldPos.copy(); // Block movement
      this.currentSpeed *= 0.5; // Scrape wall / lose speed
      this.wallHits++;
    } else {
      this.currentSpeed = lerp(this.currentSpeed, this.maxSpeed, 0.05); // Accelerate smoothly
    }

    // Checkpoints
    let nextCP = checkpoints[this.targetCheckpoint];
    if (checkIntersect(oldPos.x, oldPos.y, this.pos.x, this.pos.y, nextCP.a.x, nextCP.a.y, nextCP.b.x, nextCP.b.y)) {
      this.score++;
      if (this.score > highestScore) highestScore = this.score;
      this.targetCheckpoint = (this.targetCheckpoint + 1) % checkpoints.length;
    }
  }
Line-by-line explanation (18 lines)

🔧 Subcomponents:

calculation Sensor and Neural Network Input let inputs = this.look(nearWalls); this.think(inputs);

Gets ray-cast sensor data and feeds it into the neural network to decide turn angle

calculation Physics Update let velocity = p5.Vector.fromAngle(this.angle).mult(this.currentSpeed); this.pos.add(velocity);

Converts the car's angle and speed into velocity, moves the car one step

for-loop Wall Collision Checking for (let w of nearWalls) { if (checkIntersect(...)) { hit = true; break; } }

Tests whether the car's movement path crossed any walls

conditional Collision Handling if (hit) { this.pos = oldPos.copy(); this.currentSpeed *= 0.5; this.wallHits++; } else { this.currentSpeed = lerp(...) }

On collision, blocks movement and slows the car; otherwise, accelerates smoothly

conditional Checkpoint Crossing if (checkIntersect(oldPos.x, oldPos.y, this.pos.x, this.pos.y, nextCP.a.x, nextCP.a.y, nextCP.b.x, nextCP.b.y)) { this.score++; ... this.targetCheckpoint = ... }

Detects when the car crosses the next checkpoint, increments score, advances to the next checkpoint

let nearWalls = this.getNearbyWalls();
Gets only the nearby walls around the car's current checkpoint, not all 700+ walls, for efficiency
let inputs = this.look(nearWalls);
Casts seven rays from the car in different directions and measures distances to walls, returning an array of sensor readings
this.think(inputs);
Feeds the sensor readings into the neural network to compute a turn angle
let oldPos = this.pos.copy();
Saves the current position so we can check if the movement crosses a wall
let velocity = p5.Vector.fromAngle(this.angle).mult(this.currentSpeed);
Creates a velocity vector pointing in the car's direction, scaled by its current speed
this.pos.add(velocity);
Updates the car's position by adding the velocity
let hit = false;
Initializes a flag that will be set to true if the car hit a wall
for (let w of nearWalls) {
Loops through nearby walls to test if the car's path crossed any of them
if (checkIntersect(oldPos.x, oldPos.y, this.pos.x, this.pos.y, w.a.x, w.a.y, w.b.x, w.b.y)) {
Tests if the line segment from the old position to the new position crosses the wall segment
hit = true;
If an intersection is found, set hit to true and stop checking other walls
this.pos = oldPos.copy();
Revert the position back to before the movement, effectively blocking the car from passing through the wall
this.currentSpeed *= 0.5;
Cut the car's speed in half when it hits a wall, simulating friction or damage
this.wallHits++;
Increment the wall hit counter, which reduces fitness when calculating the next generation
this.currentSpeed = lerp(this.currentSpeed, this.maxSpeed, 0.05);
If no wall was hit, gradually accelerate toward max speed at 5% per frame—smooth, gradual acceleration
let nextCP = checkpoints[this.targetCheckpoint];
Gets the checkpoint the car is currently trying to reach
if (checkIntersect(oldPos.x, oldPos.y, this.pos.x, this.pos.y, nextCP.a.x, nextCP.a.y, nextCP.b.x, nextCP.b.y)) {
Checks if the car's path crossed the checkpoint line
this.score++;
Increment the car's checkpoint counter
this.targetCheckpoint = (this.targetCheckpoint + 1) % checkpoints.length;
Move to the next checkpoint in the list, wrapping back to 0 after the last one

Car.look()

look() is the car's 'sensory system'—it simulates seven eyes looking in different directions. Each eye casts a ray from the car toward a wall and measures the distance to the closest obstacle in that direction. These seven measurements become the inputs to the neural network. This is inspired by real insect behavior and lidar sensors in autonomous vehicles. The closer a wall is, the higher the input value (mapped to 1.0), which influences the neural network's turn decision.

🔬 This loop finds the CLOSEST wall intersection. What if you removed the `if (d < recordDist)` check so the ray returns the FIRST wall it hits instead of the closest? Does that change how cars learn to drive?

      for (let w of nearWalls) {
        let pt = checkIntersect(this.pos.x, this.pos.y, endPoint.x, endPoint.y, w.a.x, w.a.y, w.b.x, w.b.y);
        if (pt) {
          let d = dist(this.pos.x, this.pos.y, pt.x, pt.y);
          if (d < recordDist) {
            recordDist = d;
            closestPt = pt;
          }
        }
      }
  look(nearWalls) {
    let inputs = [];
    this.rays = [];
    let angles = [-PI/2, -PI/3, -PI/6, 0, PI/6, PI/3, PI/2];
    let maxDist = 350; 

    for (let i = 0; i < angles.length; i++) {
      let rayAngle = this.angle + angles[i];
      let endPoint = p5.Vector.add(this.pos, p5.Vector.fromAngle(rayAngle).mult(maxDist));
      let recordDist = maxDist;
      let closestPt = endPoint;

      for (let w of nearWalls) {
        let pt = checkIntersect(this.pos.x, this.pos.y, endPoint.x, endPoint.y, w.a.x, w.a.y, w.b.x, w.b.y);
        if (pt) {
          let d = dist(this.pos.x, this.pos.y, pt.x, pt.y);
          if (d < recordDist) {
            recordDist = d;
            closestPt = pt;
          }
        }
      }
      this.rays.push(closestPt);
      inputs[i] = map(recordDist, 0, maxDist, 1, 0); 
    }
    return inputs;
  }
Line-by-line explanation (13 lines)

🔧 Subcomponents:

calculation Seven Sensor Directions let angles = [-PI/2, -PI/3, -PI/6, 0, PI/6, PI/3, PI/2];

Defines seven ray angles: far left (-90°), left (-60°), left-of-center (-30°), straight ahead (0°), right-of-center (30°), right (60°), far right (90°)

for-loop Cast Each Ray for (let i = 0; i < angles.length; i++) { let rayAngle = this.angle + angles[i]; ... for (let w of nearWalls) { ... } }

For each of the seven angles, casts a ray and finds the closest wall intersection

for-loop Find Closest Wall for (let w of nearWalls) { let pt = checkIntersect(...); if (pt) { let d = dist(...); if (d < recordDist) { ... } } }

Tests each nearby wall to see if the ray crosses it, keeping track of the closest intersection point

let inputs = [];
Creates an empty array to store the seven sensor readings
this.rays = [];
Empties the rays array so we store fresh ray endpoints for visualization
let angles = [-PI/2, -PI/3, -PI/6, 0, PI/6, PI/3, PI/2];
Defines seven ray angles in radians: 90° to the left, 60° left, 30° left, straight ahead, 30° right, 60° right, 90° right
let maxDist = 350;
Maximum distance a ray will search for walls—if no wall is found within 350 pixels, the input is 0
let rayAngle = this.angle + angles[i];
Adds the offset angle to the car's current heading angle, so rays are always relative to where the car is facing
let endPoint = p5.Vector.add(this.pos, p5.Vector.fromAngle(rayAngle).mult(maxDist));
Calculates the end point of the ray by extending maxDist pixels from the car in the rayAngle direction
let recordDist = maxDist;
Initializes the closest distance to maxDist; if a wall is found, this will be updated to the shorter distance
for (let w of nearWalls) {
Loops through all nearby walls to find if any intersect this ray
let pt = checkIntersect(this.pos.x, this.pos.y, endPoint.x, endPoint.y, w.a.x, w.a.y, w.b.x, w.b.y);
Tests if the ray (from car position to endPoint) intersects the wall segment
let d = dist(this.pos.x, this.pos.y, pt.x, pt.y);
Calculates the distance from the car to the intersection point
if (d < recordDist) {
If this intersection is closer than the previous closest, update recordDist and closestPt
this.rays.push(closestPt);
Stores the endpoint of this ray for visualization when the leader car is drawn
inputs[i] = map(recordDist, 0, maxDist, 1, 0);
Maps distance to wall (0 to 350) into a sensor input (1 to 0): close walls = high input, far walls = low input

Car.think()

think() implements a simple single-layer neural network with seven inputs (sensor readings) and eight weights (seven sensor weights plus one bias). Each frame, it computes a weighted sum of the inputs and outputs a turn angle. This is the 'brain' that evolution optimizes—by adjusting the 8 DNA weights, the genetic algorithm teaches the car how to respond to sensor data. The beauty of this approach is its simplicity: just a few multiplications and an addition, yet it's powerful enough to learn driving.

🔬 This is a simple neural network: seven inputs, seven weights, one bias, one output. What happens if you remove the `sum += this.dna.genes[inputs.length]` line (the bias)? Can the network still learn to drive without it?

    let sum = 0;
    for (let i = 0; i < inputs.length; i++) {
      sum += inputs[i] * this.dna.genes[i];
    }
    sum += this.dna.genes[inputs.length];
    
    let turn = constrain(sum, -1, 1);
  think(inputs) {
    let sum = 0;
    for (let i = 0; i < inputs.length; i++) {
      sum += inputs[i] * this.dna.genes[i];
    }
    sum += this.dna.genes[inputs.length];
    
    let turn = constrain(sum, -1, 1);
    this.angle += turn * 0.18; 
  }
Line-by-line explanation (6 lines)

🔧 Subcomponents:

for-loop Neural Network Computation for (let i = 0; i < inputs.length; i++) { sum += inputs[i] * this.dna.genes[i]; }

Multiplies each sensor input by its corresponding DNA weight and adds them together—this is a neuron

calculation Neural Network Bias sum += this.dna.genes[inputs.length];

Adds a bias weight (the 8th gene) to allow the neuron to output values even when all inputs are zero

calculation Apply Turn to Angle this.angle += turn * 0.18;

Adds the turn amount to the car's current angle, rotating it slightly each frame

let sum = 0;
Initializes a variable to accumulate the weighted sum of sensor inputs
for (let i = 0; i < inputs.length; i++) {
Loops through all seven sensor inputs
sum += inputs[i] * this.dna.genes[i];
Multiplies each sensor input by its DNA weight and adds to the sum—this is the core of neural network computation
sum += this.dna.genes[inputs.length];
Adds the bias weight (the 8th gene) to the sum; this allows output even when inputs are zero
let turn = constrain(sum, -1, 1);
Clamps the output to between -1 (hard left) and 1 (hard right)
this.angle += turn * 0.18;
Adds the turn amount (scaled by 0.18 radians per frame) to the car's current angle, rotating it

Car.show()

show() renders the car on the canvas. Non-leader cars are drawn in faded random colors; the leader is drawn in bright cyan with cyan rays extending from its center to show what it 'sees' with its seven sensors. This visualization is crucial for understanding the learning process—you can literally watch the cyan rays and see how the leader's sensors react to walls and curves.

  show(isBest) {
    push();
    translate(this.pos.x, this.pos.y);
    rotate(this.angle);
    
    rectMode(CENTER);
    noStroke();
    
    if (isBest) {
      fill(0, 255, 200);
      strokeWeight(1);
      for (let i = 0; i < this.rays.length; i++) {
        stroke(0, 255, 200, 100);
        push();
        rotate(-this.angle);
        line(0, 0, this.rays[i].x - this.pos.x, this.rays[i].y - this.pos.y);
        pop();
      }
      
      push();
      rotate(-this.angle);
      fill(255);
      noStroke();
      textSize(12);
      textAlign(CENTER);
      text("Leader", 0, -20);
      pop();
    } else {
      fill(this.color);
    }
    
    rect(0, 0, 20, 10, 3);
    fill(isBest ? 255 : 150);
    rect(5, 0, 4, 8);      
    pop();
  }
Line-by-line explanation (14 lines)

🔧 Subcomponents:

calculation Car Body Drawing rect(0, 0, 20, 10, 3); fill(isBest ? 255 : 150); rect(5, 0, 4, 8);

Draws the car as two rectangles: a large body and a smaller 'headlight' to show direction

for-loop Sensor Ray Visualization for (let i = 0; i < this.rays.length; i++) { stroke(0, 255, 200, 100); push(); rotate(-this.angle); line(0, 0, this.rays[i].x - this.pos.x, this.rays[i].y - this.pos.y); pop(); }

Draws cyan lines from the leader car to each sensor endpoint, showing what it 'sees'

push();
Saves the current drawing state so transforms don't affect other drawings
translate(this.pos.x, this.pos.y);
Moves the origin to the car's position, so the car is drawn at (0,0) relative to itself
rotate(this.angle);
Rotates the coordinate system so the car is drawn facing the correct direction
if (isBest) {
If this is the leader car, draw it in bright cyan with ray visualization
fill(0, 255, 200);
Sets the fill color to bright cyan (RGB: 0, 255, 200)
for (let i = 0; i < this.rays.length; i++) {
Loops through all seven sensor rays
rotate(-this.angle);
Undoes the car's rotation so rays are drawn in world coordinates, not car-relative
line(0, 0, this.rays[i].x - this.pos.x, this.rays[i].y - this.pos.y);
Draws a line from the car center to the ray endpoint, showing sensor distance and direction
text("Leader", 0, -20);
Draws the label 'Leader' above the best car so you know which one is winning
} else {
If this is a regular (non-leader) car, draw it in a faded color
fill(this.color);
Use the car's pre-assigned random color with transparency
rect(0, 0, 20, 10, 3);
Draws the main car body as a 20x10 rectangle with rounded corners (3px radius)
fill(isBest ? 255 : 150);
If leader, use white for the headlight; otherwise use medium gray
rect(5, 0, 4, 8);
Draws a small 'headlight' rectangle pointing forward, indicating the car's heading

nextGeneration()

nextGeneration() is where evolution happens. It calculates how 'fit' each car is based on performance (checkpoints + proximity), then uses that fitness to probabilistically select parents. Fitter cars have more offspring, weaker cars fewer. Children inherit hybrid DNA from both parents, then mutations add random tweaks. Over many generations, this creates evolutionary pressure that pushes the population toward better driving—a stunning demonstration of how natural selection creates complex behavior from simple rules.

🔬 This loop breeds all 120 new cars from fitter parents. What if you change the mutation rate from 0.05 to 0.0 (no mutations)? Without random exploration, can the population still evolve, or does it get stuck?

  for (let i = 0; i < POP_SIZE; i++) {
    let parentA = selectParent(sumFitness);
    let parentB = selectParent(sumFitness);
    
    let childDNA = parentA.dna.crossover(parentB.dna);
    childDNA.mutate(0.05); // 5% chance to mutate a tiny bit
    
    newPopulation.push(new Car(childDNA));
  }
function nextGeneration() {
  let sumFitness = 0;
  
  for (let car of population) {
    let nextCP = checkpoints[car.targetCheckpoint];
    let cpCenter = p5.Vector.lerp(nextCP.a, nextCP.b, 0.5);
    let distToCP = p5.Vector.dist(car.pos, cpCenter);

    // Fitness rewards checkpoint captures highly, but also rewards micro-distances
    car.fitness = pow(2, car.score) * 1000 + (2000 / (distToCP + 1));
    
    // Penalize scraping walls so cars that drive straight down the middle win
    car.fitness = max(1, car.fitness - (car.wallHits * 2));
    sumFitness += car.fitness;
  }

  let newPopulation = [];
  for (let i = 0; i < POP_SIZE; i++) {
    let parentA = selectParent(sumFitness);
    let parentB = selectParent(sumFitness);
    
    let childDNA = parentA.dna.crossover(parentB.dna);
    childDNA.mutate(0.05); // 5% chance to mutate a tiny bit
    
    newPopulation.push(new Car(childDNA));
  }
  
  population = newPopulation;
  generation++;
}
Line-by-line explanation (17 lines)

🔧 Subcomponents:

for-loop Fitness Evaluation for (let car of population) { let nextCP = checkpoints[car.targetCheckpoint]; ... car.fitness = pow(2, car.score) * 1000 + (2000 / (distToCP + 1)); car.fitness = max(1, car.fitness - (car.wallHits * 2)); sumFitness += car.fitness; }

Calculates how 'fit' each car is based on checkpoints passed and distance to the next one, penalizing wall hits

for-loop Create New Generation for (let i = 0; i < POP_SIZE; i++) { let parentA = selectParent(sumFitness); let parentB = selectParent(sumFitness); let childDNA = parentA.dna.crossover(parentB.dna); childDNA.mutate(0.05); newPopulation.push(new Car(childDNA)); }

Selects pairs of parents by fitness, breeds offspring through crossover and mutation, fills the new generation

let sumFitness = 0;
Initializes a counter to sum all fitness values; used for probabilistic parent selection
for (let car of population) {
Loops through all 120 cars to evaluate each one
let nextCP = checkpoints[car.targetCheckpoint];
Gets the checkpoint the car is currently aiming for
let cpCenter = p5.Vector.lerp(nextCP.a, nextCP.b, 0.5);
Finds the midpoint of the checkpoint line
let distToCP = p5.Vector.dist(car.pos, cpCenter);
Calculates how far the car is from the next checkpoint
car.fitness = pow(2, car.score) * 1000 + (2000 / (distToCP + 1));
Exponentially rewards checkpoints (2^score * 1000) and adds bonus for proximity to the next checkpoint
car.fitness = max(1, car.fitness - (car.wallHits * 2));
Subtracts 2 points for each wall hit, but keeps fitness at minimum 1 so all cars have a chance to reproduce
sumFitness += car.fitness;
Adds this car's fitness to the total, used for probabilistic selection
let newPopulation = [];
Creates an empty array to hold the new generation of cars
for (let i = 0; i < POP_SIZE; i++) {
Loops 120 times to create 120 new cars
let parentA = selectParent(sumFitness);
Selects one parent car probabilistically based on fitness (fitter cars more likely to be chosen)
let parentB = selectParent(sumFitness);
Selects another parent car the same way
let childDNA = parentA.dna.crossover(parentB.dna);
Creates a child DNA by mixing genes from both parents at a random split point
childDNA.mutate(0.05);
Has a 5% chance per gene to nudge each weight by a small random amount
newPopulation.push(new Car(childDNA));
Creates a new car with the hybrid DNA and adds it to the new generation
population = newPopulation;
Replaces the entire population with the new generation
generation++;
Increments the generation counter displayed on screen

selectParent()

selectParent() implements roulette wheel selection, a classic genetic algorithm technique. Imagine a spinning wheel where each car occupies a slice proportional to its fitness. The function picks a random point on that wheel and returns the car it lands on. High-fitness cars get bigger slices, so they're selected more often. This is why evolution works—fitter cars breed more frequently, spreading their good genes through the population.

function selectParent(sumFitness) {
  let r = random(sumFitness);
  let index = 0;
  while (r > 0 && index < population.length) {
    r -= population[index].fitness;
    index++;
  }
  index--;
  return population[index];
}
Line-by-line explanation (7 lines)

🔧 Subcomponents:

while-loop Fitness Wheel Selection while (r > 0 && index < population.length) { r -= population[index].fitness; index++; }

Implements roulette wheel selection: iterates through cars, subtracting their fitness from a random number until it goes negative, selecting the car where that happens

let r = random(sumFitness);
Picks a random number between 0 and the total fitness of all cars
let index = 0;
Starts searching from the first car
while (r > 0 && index < population.length) {
Loop while the random value is still positive and we haven't exceeded the population
r -= population[index].fitness;
Subtracts the current car's fitness from the random number
index++;
Move to the next car
index--;
Step back one index because the while loop incremented one too many times
return population[index];
Return the selected parent car

checkIntersect()

checkIntersect() uses the parametric line-segment intersection algorithm. It treats each line segment as a parametric equation and solves for the intersection point. This is used for both wall collision detection and checkpoint crossing detection. Understanding this function teaches you computational geometry—skills valuable far beyond p5.js.

function checkIntersect(x1, y1, x2, y2, x3, y3, x4, y4) {
  let den = (x1 - x2) * (y3 - y4) - (y1 - y2) * (x3 - x4);
  if (den === 0) return null;
  
  let t = ((x1 - x3) * (y3 - y4) - (y1 - y3) * (x3 - x4)) / den;
  let u = -((x1 - x2) * (y1 - y3) - (y1 - y2) * (x1 - x3)) / den;
  
  if (t >= 0 && t <= 1 && u >= 0 && u <= 1) {
    return createVector(x1 + t * (x2 - x1), y1 + t * (y2 - y1));
  }
  return null;
}
Line-by-line explanation (6 lines)

🔧 Subcomponents:

conditional Parallel Line Check let den = (x1 - x2) * (y3 - y4) - (y1 - y2) * (x3 - x4); if (den === 0) return null;

Calculates the denominator; if zero, the lines are parallel and don't intersect

calculation Parametric Intersection let t = ((x1 - x3) * (y3 - y4) - (y1 - y3) * (x3 - x4)) / den; let u = -((x1 - x2) * (y1 - y3) - (y1 - y2) * (x1 - x3)) / den;

Computes parameters t and u; if both are in [0,1], the segments intersect within their bounds

let den = (x1 - x2) * (y3 - y4) - (y1 - y2) * (x3 - x4);
Calculates the determinant; if zero, the two line segments are parallel
if (den === 0) return null;
If parallel, there's no intersection, so return null
let t = ((x1 - x3) * (y3 - y4) - (y1 - y3) * (x3 - x4)) / den;
Calculates the parameter t, representing how far along segment 1 the intersection occurs (0=start, 1=end)
let u = -((x1 - x2) * (y1 - y3) - (y1 - y2) * (x1 - x3)) / den;
Calculates the parameter u for segment 2
if (t >= 0 && t <= 1 && u >= 0 && u <= 1) {
Check if both parameters are in [0,1], meaning the intersection point is within both segments
return createVector(x1 + t * (x2 - x1), y1 + t * (y2 - y1));
Calculate and return the exact intersection point on segment 1

DNA.crossover()

crossover() implements genetic crossover—when two parents produce a child by mixing their genes. The function picks a random split point and takes genes from one parent up to that point, then genes from the other parent after it. This mimics biological reproduction and helps spread successful combinations of genes through the population.

🔬 This loop splits genes at one point. What if you picked TWO random split points and alternated between parents (like chromosomal crossing over)? Would that create better or worse offspring?

    for (let i = 0; i < this.genes.length; i++) {
      if (i > midPoint) newGenes[i] = this.genes[i];
      else newGenes[i] = partner.genes[i];
    }
  crossover(partner) {
    let newGenes = [];
    let midPoint = floor(random(this.genes.length));
    for (let i = 0; i < this.genes.length; i++) {
      if (i > midPoint) newGenes[i] = this.genes[i];
      else newGenes[i] = partner.genes[i];
    }
    return new DNA(newGenes);
  }
Line-by-line explanation (6 lines)

🔧 Subcomponents:

calculation Random Split Point let midPoint = floor(random(this.genes.length));

Picks a random position in the gene array to split between parent genes

for-loop Gene Inheritance Loop for (let i = 0; i < this.genes.length; i++) { if (i > midPoint) newGenes[i] = this.genes[i]; else newGenes[i] = partner.genes[i]; }

Copies genes from parent A up to the split point, then genes from parent B after the split

let newGenes = [];
Creates an empty array to hold the child's genes
let midPoint = floor(random(this.genes.length));
Picks a random position (0-8) as the split point
for (let i = 0; i < this.genes.length; i++) {
Loops through all 8 genes
if (i > midPoint) newGenes[i] = this.genes[i];
After the split point, inherit from parent A (this)
else newGenes[i] = partner.genes[i];
Before the split point, inherit from parent B (partner)
return new DNA(newGenes);
Create and return a new DNA object with the hybrid genes

DNA.mutate()

mutate() introduces random variation into genes by nudging them with small random amounts. Mutation is crucial for evolution—without it, the population can only recombine existing genes and gets stuck in local optima. The tiny mutation steps (±0.05) mean learning is very slow, which is intentional—it forces evolution to find robust solutions rather than lucky accidents.

  mutate(rate) {
    for (let i = 0; i < this.genes.length; i++) {
      if (random(1) < rate) {
        // Microscopic mutation steps! It will take a long time to learn.
        this.genes[i] += random(-0.05, 0.05); 
        this.genes[i] = constrain(this.genes[i], -1, 1);
      }
    }
  }
Line-by-line explanation (4 lines)

🔧 Subcomponents:

for-loop Per-Gene Mutation for (let i = 0; i < this.genes.length; i++) { if (random(1) < rate) { this.genes[i] += random(-0.05, 0.05); this.genes[i] = constrain(this.genes[i], -1, 1); } }

For each gene, has a 5% chance to nudge it by a small random amount within -1 to 1

for (let i = 0; i < this.genes.length; i++) {
Loops through all 8 genes
if (random(1) < rate) {
Rolls a random number; if it's less than the mutation rate (0.05 = 5%), proceed
this.genes[i] += random(-0.05, 0.05);
If mutation occurs, add a small random nudge between -0.05 and +0.05 to the gene
this.genes[i] = constrain(this.genes[i], -1, 1);
Clamp the gene to stay within [-1, 1], preventing it from exploding to extreme values

initPopulation()

initPopulation() creates the initial population of 120 cars, each with random, nearly-zero neural network weights. This is the starting point for evolution—every car is equally dumb at first, and natural selection gradually shapes them into competent drivers over many generations.

function initPopulation() {
  population = [];
  for (let i = 0; i < POP_SIZE; i++) {
    population.push(new Car());
  }
}
Line-by-line explanation (3 lines)
population = [];
Empties the population array so we start with a blank slate
for (let i = 0; i < POP_SIZE; i++) {
Loops 120 times (POP_SIZE = 120)
population.push(new Car());
Creates a new Car with random DNA (nearly-zero weights) and adds it to the population

windowResized()

windowResized() is a p5.js lifecycle function that runs automatically whenever the browser window is resized. It ensures the canvas adapts to the new viewport size, maintaining full-screen visualization.

function windowResized() {
  resizeCanvas(windowWidth, windowHeight);
}
Line-by-line explanation (1 lines)
resizeCanvas(windowWidth, windowHeight);
Rescales the canvas to fill the new window dimensions whenever the browser is resized

📦 Key Variables

walls array

Stores all line segments that form the inner and outer boundaries of the race track; used for collision detection

let walls = [];
checkpoints array

Stores vertical line segments perpendicular to the track; cars increment their score when crossing these

let checkpoints = [];
trackPts array

Intermediate storage for the 350 points (inner and outer radii) that define the track shape during generation

let trackPts = [];
population array

Stores the 120 Car objects in the current generation; replaced each generation with offspring from fitter parents

let population = [];
POP_SIZE number

Constant that sets the population size to 120 cars per generation

const POP_SIZE = 120;
generation number

Tracks which generation is currently simulating; increments each time a new population is bred

let generation = 1;
highestScore number

Global record: the maximum number of checkpoints any car has ever passed, used to track population progress

let highestScore = 0;
genTimer number

Counts frames in the current generation; when it reaches MAX_GEN_TIME (900), a new generation is bred

let genTimer = 0;
MAX_GEN_TIME number

Constant: number of frames each generation survives before breeding (900 = 15 seconds at 60fps)

const MAX_GEN_TIME = 900;
trackSeed number

Random seed used by Perlin noise to generate the track shape; same seed produces identical tracks

let trackSeed;
camX number

Current x-coordinate of the camera; smoothly follows the best car's x position

let camX = 0;
camY number

Current y-coordinate of the camera; smoothly follows the best car's y position

let camY = 0;
startPos p5.Vector

The spawn position where all cars start each generation

let startPos;
startAngle number

The initial rotation angle cars spawn with, pointing toward the first checkpoint

let startAngle;
speedSlider HTML element

Reference to the simulation speed slider (1-10x); controls how many update steps run per frame

let speedSlider;
newTrackBtn HTML element

Reference to the 'Generate New Track' button; allows user to reset and create a different track

let newTrackBtn;

🔧 Potential Improvements (6)

Here are some ways this code could be enhanced:

PERFORMANCE draw() - wall rendering

The code checks every wall's bounds against the camera view every frame. With 700+ walls, this becomes expensive.

💡 Pre-compute which walls are visible based on track topology, or use spatial hashing to quickly find walls near the camera.

BUG look() - ray casting

If a ray intersects multiple walls at the same distance, only the first found is recorded. This can create inconsistent sensor readings depending on wall order.

💡 Sort all ray-wall intersections by distance and always return the closest one, regardless of wall array order.

FEATURE nextGeneration() - fitness calculation

All cars have equal chance to reproduce once selected as a parent, even if fitness varies wildly. Elitism could preserve the best.

💡 Implement elitism by always copying the top 5-10% best cars unchanged into the next generation, reducing loss of good solutions.

STYLE checkIntersect()

The line-segment intersection algorithm has hard-to-understand variable names (x1, y1, x2, y2, x3, y3, x4, y4) and no comments explaining the math.

💡 Rename parameters to (p1x, p1y, p2x, p2y, p3x, p3y, p4x, p4y) and add comments explaining the parametric intersection math for clarity.

BUG Car.think() - neural network

The neural network has no activation function (like tanh or sigmoid), so outputs can theoretically exceed [-1, 1] before constrain() clamps them. This wastes the mutation space.

💡 Apply a sigmoid or tanh activation function before constrain() to use the full mutation range more effectively.

FEATURE Car.update() - collision response

When a car hits a wall, it only loses speed and is blocked. It doesn't rotate away from the wall, so it gets stuck easily.

💡 On collision, nudge the car's angle slightly away from the wall normal to help it escape corners and learn to avoid walls.

🔄 Code Flow

Code flow showing setup, draw, buildtrack, update, look, think, show, nextgeneration, selectparent, checkintersect, crossover, mutate, initpopulation, windowresized

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

graph TD start[Start] --> setup[setup] setup --> canvas-setup[Canvas Creation] setup --> track-seed[Track Randomization Seed] setup --> initpopulation[Init Population] setup --> draw[draw loop] click setup href "#fn-setup" click canvas-setup href "#sub-canvas-setup" click track-seed href "#sub-track-seed" click initpopulation href "#fn-initpopulation" draw --> simulation-loop[Multi-Step Simulation] draw --> generation-timer[Generation Timer Check] draw --> best-car-search[Best Car Selection] draw --> camera-follow[Smooth Camera Tracking] draw --> frustum-culling[Render Optimization] click draw href "#fn-draw" click simulation-loop href "#sub-simulation-loop" click generation-timer href "#sub-generation-timer" click best-car-search href "#sub-best-car-search" click camera-follow href "#sub-camera-follow" click frustum-culling href "#sub-frustum-culling" simulation-loop --> update[update] update --> sensor-input[Sensor and Neural Network Input] update --> movement-logic[Physics Update] update --> collision-detection[Wall Collision Checking] update --> checkpoint-detection[Checkpoint Crossing] click update href "#fn-update" click sensor-input href "#sub-sensor-input" click movement-logic href "#sub-movement-logic" click collision-detection href "#sub-collision-detection" click checkpoint-detection href "#sub-checkpoint-detection" sensor-input --> ray-angles[Seven Sensor Directions] sensor-input --> ray-casting-loop[Cast Each Ray] click ray-angles href "#sub-ray-angles" click ray-casting-loop href "#sub-ray-casting-loop" ray-casting-loop --> wall-intersection-check[Find Closest Wall] click wall-intersection-check href "#sub-wall-intersection-check" movement-logic --> collision-response[Collision Handling] click collision-response href "#sub-collision-response" collision-detection --> collision-response generation-timer --> nextgeneration[nextGeneration] nextgeneration --> fitness-calculation[Fitness Evaluation] nextgeneration --> breeding-loop[Create New Generation] click nextgeneration href "#fn-nextgeneration" click fitness-calculation href "#sub-fitness-calculation" click breeding-loop href "#sub-breeding-loop" breeding-loop --> selectparent[selectParent] breeding-loop --> crossover[crossover] breeding-loop --> mutate[mutate] click selectparent href "#fn-selectparent" click crossover href "#fn-crossover" click mutate href "#fn-mutate" selectparent --> fitness-wheel[Fitness Wheel Selection] click fitness-wheel href "#sub-fitness-wheel" crossover --> split-point[Random Split Point] crossover --> crossover-loop[Gene Inheritance Loop] click split-point href "#sub-split-point" click crossover-loop href "#sub-crossover-loop" mutate --> mutation-loop[Per-Gene Mutation] click mutation-loop href "#sub-mutation-loop" draw --> show[show] show --> car-body[Car Body Drawing] show --> ray-visualization[Sensor Ray Visualization] click show href "#fn-show" click car-body href "#sub-car-body" click ray-visualization href "#sub-ray-visualization" windowresized[windowResized] --> setup click windowresized href "#fn-windowresized"

❓ Frequently Asked Questions

What visual effects can I expect from the MASSIVE AI Driving Simulation sketch?

The sketch creates a dynamic simulation where multiple AI-driven cars navigate a procedurally generated track, showcasing their movements and interactions with walls and checkpoints against a dark background.

How can I interact with the MASSIVE AI Driving Simulation sketch?

Users can adjust the simulation speed using a slider and generate a new track by clicking a button, allowing for varied experiences in observing AI learning.

What creative coding concepts does this p5.js sketch illustrate?

This sketch demonstrates concepts of evolutionary algorithms and simulation, where AI agents adapt and evolve over generations based on their performance in navigating a track.

Preview

Sketch 2026-05-17 06:18 - p5.js creative coding sketch preview
Sketch Preview
Code flow diagram showing the structure of Sketch 2026-05-17 06:18 - Code flow showing setup, draw, buildtrack, update, look, think, show, nextgeneration, selectparent, checkintersect, crossover, mutate, initpopulation, windowresized
Code Flow Diagram