dino

This sketch creates an AI-learning dinosaur game inspired by Chrome's offline dinosaur, where the dino learns to jump over cacti and duck under birds by recording successful actions in a 'winning list'. The game features a fixed, repeating obstacle course that allows the AI to gradually improve its decision-making through reinforcement learning, getting smarter with every successful dodge.

🧪 Try This!

Experiment with the code by making these changes:

  1. Make the dino learn faster by reducing ducking probability — Birds require ducking, which is less likely to be chosen randomly. Lower the duck weight to see if the dino learns jump patterns instead.
  2. Make jumps higher so the dino clears obstacles easier — Higher jump velocity (more negative number) lets the dino reach taller obstacles and gives it more reaction time.
  3. Speed up the game difficulty increase — Higher acceleration values make the game get harder faster, forcing the dino to learn quicker patterns.
  4. Make the dino take decisions more often — Lower the decision interval so the dino can react to obstacles more frequently, speeding up learning.
  5. Change the background to dark mode — A dark background can make the game feel different and may help you see learning progress more clearly.
Prefer the full editor? Open it there →

📖 About This Sketch

This sketch builds a game where a dinosaur character learns to survive an endless obstacle course by remembering which actions worked. Unlike a typical game where you control the dino, here it makes its own decisions—and gets better at them over time. The sketch uses reinforcement learning with a 'winning list' to track successful jumps and ducks, collision detection to catch mistakes, and p5.js animation loops to create smooth movement. Each time the dino successfully avoids an obstacle using the same action it learned before, that knowledge is reinforced.

The code is organized around a game loop in draw() that updates the dino, obstacles, and collision checks each frame, plus a learning function called decideDinoAction() that chooses what the dino should do next. By studying it, you'll learn how to build a fixed obstacle course, implement a simple AI learning system, handle collision detection with hitboxes, and create classes for game objects like Dino, Cactus, and Bird. The sketch also demonstrates how to reset a game while preserving learning progress across sessions.

⚙️ How It Works

  1. When the sketch starts, setup() creates a canvas and calls initializeObstacles() to spawn a fixed sequence of cacti and birds at predetermined distances, creating a repeatable course.
  2. Every frame, draw() clears the background and updates all game elements: the ground scrolls to create motion illusion, the dino updates its physics and position, and obstacles move leftward at increasing speed.
  3. Before updating, draw() processes the outcome of the dino's action from the previous frame—if the dino collided, it dies and the game resets; if it survived, any obstacles it jumped over are recorded as successful in the winningList.
  4. The decideDinoAction() function chooses the dino's next move: if the closest approaching obstacle has a known winning jump recorded, the dino repeats that jump; otherwise, it takes a random action (jump, duck, or do nothing) at timed intervals.
  5. Collision detection uses axis-aligned bounding box (AABB) comparison between the dino's hitbox and each obstacle's hitbox, instantly ending the game on contact.
  6. The winningList grows over time, storing successful jump decisions tied to specific obstacles in the fixed course, so the dino's performance improves as it recognizes and re-executes familiar patterns.

🎓 Concepts You'll Learn

Reinforcement LearningCollision Detection (AABB)Game State ManagementPhysics Simulation (Gravity & Velocity)Classes and InheritanceFixed Pattern GenerationAnimation Loop

📝 Code Breakdown

setup()

setup() runs once when the sketch loads. It initializes the canvas size, creates all game objects, and sets up the fixed obstacle course. This is where the dino and ground get their starting state before the draw loop begins.

function setup() {
  createCanvas(windowWidth, windowHeight);

  ground = new Ground();
  dino = new Dino();

  textSize(24);
  textAlign(RIGHT, TOP);

  initializeObstacles(); // Initialize the fixed course
}
Line-by-line explanation (6 lines)
createCanvas(windowWidth, windowHeight);
Creates a canvas that fills the entire window, responsive to screen size.
ground = new Ground();
Instantiates the Ground object, which creates the scrolling ground texture that gives the illusion of forward motion.
dino = new Dino();
Creates the Dino object at its starting position, ready to receive inputs and execute actions.
textSize(24);
Sets the font size for all text drawn in the sketch (score, high score, learning list).
textAlign(RIGHT, TOP);
Aligns text to the right and top of the canvas for score display.
initializeObstacles(); // Initialize the fixed course
Populates the obstacles array with all cacti and birds from the fixedObstacleCourse array in their predetermined positions.

draw()

draw() is called 60 times per second. It's the heartbeat of the game, processing outcomes, updating physics, checking collisions, making decisions, and rendering everything. The two-frame delay between action choice and outcome evaluation ensures the dino survives at least one frame before success or failure is recorded.

🔬 This code records successful jumps. What happens if you change the condition from `targetObstacle.x < dino.x + dino.w` to `targetObstacle.x < dino.x` (stricter clearance requirement)? Will the dino learn faster or slower?

      if (currentAction.type === 'jump' && currentAction.obstaclesToEvaluate !== null) {
        // Iterate through the snapshot of obstacles that were targeted by this jump
        for (let evalObstacle of currentAction.obstaclesToEvaluate) {
          const targetObstacle = obstacles.find(o => o.id === evalObstacle.id);

          // Check if the obstacle was successfully cleared
          // (either still in array and its left edge passed dino's right edge, OR removed from array)
          if ((targetObstacle && (targetObstacle.x < dino.x + dino.w)) || !targetObstacle) { // THIS IS THE NEW CHANGE
            winningList.push({ type: 'positive_jump', obstacleCourseIndex: evalObstacle.obstacleCourseIndex });
            console.log("  WinningList updated. Added POSITIVE JUMP for obstacle:", evalObstacle.obstacleCourseIndex);
            outcomeRecorded = true;
          }
        }
      }

🔬 This loop updates and draws all obstacles. Why does it count backward (from length-1 to 0) instead of forward? What breaks if you change it to `for (let i = 0; i < obstacles.length; i++)`?

  for (let i = obstacles.length - 1; i >= 0; i--) {
    obstacles[i].update();
    obstacles[i].show();

    // Remove off-screen obstacles
    if (obstacles[i].offscreen()) {
      obstacles.splice(i, 1);
    }
  }
function draw() {
  background(220); // Light gray background

  // Check if dino was dead from the previous frame, and reset if so
  if (dino.isDead) {
    resetGame();
    return; // Exit draw() early after reset to prevent further updates in the same frame
  }

  // Game loop runs continuously
  // 1. Process the outcome of the action chosen in the *previous* frame
  // This allows us to check if the dino survived for at least one frame after the action
  if (currentAction !== null) {
    console.log("Processing action:", currentAction.type, "Targeted Obs Count:", currentAction.obstaclesToEvaluate ? currentAction.obstaclesToEvaluate.length : 0);
    if (checkCollision()) {
      // Failure: The action chosen in the previous frame led to a collision
      // Set dino to dead state for visual feedback this frame
      dino.isDead = true;
      // The winningList is NOT updated in case of failure
      currentAction = null; // Clear pending action
      console.log("  Action FAILED: Collision. Resetting next frame.");
      // Do NOT resetGame() immediately, let it render red dino this frame
    } else {
      // Success: No collision occurred. Record outcomes.
      let outcomeRecorded = false; // Flag to check if at least one outcome was recorded

      if (currentAction.type === 'jump' && currentAction.obstaclesToEvaluate !== null) {
        // Iterate through the snapshot of obstacles that were targeted by this jump
        for (let evalObstacle of currentAction.obstaclesToEvaluate) {
          const targetObstacle = obstacles.find(o => o.id === evalObstacle.id);

          // Check if the obstacle was successfully cleared
          // (either still in array and its left edge passed dino's right edge, OR removed from array)
          if ((targetObstacle && (targetObstacle.x < dino.x + dino.w)) || !targetObstacle) { // THIS IS THE NEW CHANGE
            winningList.push({ type: 'positive_jump', obstacleCourseIndex: evalObstacle.obstacleCourseIndex });
            console.log("  WinningList updated. Added POSITIVE JUMP for obstacle:", evalObstacle.obstacleCourseIndex);
            outcomeRecorded = true;
          }
        }
      }

      // If no positive jumps were recorded by this action (e.g., was 'duck', 'none', or a jump that missed)
      if (!outcomeRecorded) {
        winningList.push('neutral'); // Add 'neutral' outcomes to the winningList
        console.log("  WinningList updated. Added NEUTRAL outcome.");
      }
    }
    currentAction = null; // Clear the current action for the next decision cycle
  }

  // 2. Update and show game elements
  noStroke(); // Ensure ground has no stroke
  ground.update();
  ground.show();

  noStroke(); // Ensure dino has no stroke
  dino.update();
  dino.show(); // Show dino (will be red if just collided)

  // 3. Update and show obstacles
  for (let i = obstacles.length - 1; i >= 0; i--) {
    obstacles[i].update();
    obstacles[i].show();

    // Remove off-screen obstacles
    if (obstacles[i].offscreen()) {
      obstacles.splice(i, 1);
    }
  }

  // 4. Dino's action decision logic (now uses decideDinoAction)
  currentAction = decideDinoAction();

  // 5. Increase score and game speed
  score++;
  gameSpeed += 0.001; // Gradually increase game speed


  // Display score
  fill(0);
  noStroke(); // Ensure score text has no stroke
  text('Score: ' + score, width - 20, 20);
  text('High Score: ' + highScore, width - 20, 50);

  // Display Winning List - showing counts for positive jumps and neutral actions
  textAlign(LEFT, TOP);
  text('Winning List (Positive/Neutral):', 20, 20); // Updated title
  const positiveCount = winningList.filter(a => typeof a === 'object' && a.type === 'positive_jump').length;
  const neutralCount = winningList.filter(a => a === 'neutral').length;
  text(`- Positive Jumps: ${positiveCount}`, 20, 50);
  text(`- Neutral Actions: ${neutralCount}`, 20, 80);
  textAlign(RIGHT, TOP); // Reset alignment for score display
}
Line-by-line explanation (9 lines)

🔧 Subcomponents:

conditional Outcome Processing if (currentAction !== null) { ... }

Evaluates whether the dino's previous action was successful (no collision) or failed, then records the outcome in the winningList for learning

for-loop Obstacle Update Loop for (let i = obstacles.length - 1; i >= 0; i--) { ... }

Updates all obstacles, draws them, and removes any that have scrolled off-screen

calculation Winning List Statistics const positiveCount = winningList.filter(a => typeof a === 'object' && a.type === 'positive_jump').length;

Counts successful jumps and neutral actions in the learning list to display learning progress to the player

background(220); // Light gray background
Clears the canvas every frame with a light gray color, erasing the previous frame's drawings so motion is smooth.
if (dino.isDead) {
Checks if the dino collided in the previous frame; if so, the next reset happens at the start of this frame.
if (checkCollision()) {
Tests whether the dino's current position overlaps any obstacle using AABB hitbox collision—returns true on impact.
dino.isDead = true;
Marks the dino as dead, which causes it to render red on the next show() call and triggers a reset at the start of the next frame.
if ((targetObstacle && (targetObstacle.x < dino.x + dino.w)) || !targetObstacle) {
Checks if a targeted obstacle was successfully cleared: either it's still in the array but passed behind the dino, or it's already been removed from the array.
for (let i = obstacles.length - 1; i >= 0; i--) {
Loops backward through the obstacles array (high index to low) so removing elements doesn't skip any during the loop.
if (obstacles[i].offscreen()) {
Checks if an obstacle has scrolled far enough left to be invisible, then removes it to save memory.
currentAction = decideDinoAction();
Calls the AI decision function to determine the dino's next action based on nearby obstacles and learned patterns.
gameSpeed += 0.001; // Gradually increase game speed
Slightly increases the game speed every frame, making the game progressively harder to encourage the dino to learn faster.

decideDinoAction()

This is the AI decision engine. It implements a simple reinforcement learning strategy: first, it checks if the upcoming obstacle matches a known winning pattern and repeats that action; second, if no learned pattern applies, it explores with random actions. This balance between exploitation (repeating what works) and exploration (trying new things) is the core of the learning loop.

🔬 This loop finds the next obstacle. What if you change the condition from `obstacle.x > dino.x + dino.w` to `obstacle.x > dino.x`? How would this change which obstacle the dino targets?

  for (let obstacle of obstacles) {
    if (obstacle.x > dino.x + dino.w) {
      const distance = obstacle.x - (dino.x + dino.w);
      if (distance < minDistance) {
        minDistance = distance;
        closestObstacle = obstacle;
      }
    }
  }

🔬 This weighted random selection uses cumulative weights. What happens if you swap the weights to [0.1, 0.7, 0.2], making the dino prefer ducking? Watch the console and high score—does learning speed up or down?

    const actions = ['jump', 'duck', 'none'];
    const weights = [0.3, 0.2, 0.5];
    let choice = random(1);
    let cumulativeWeight = 0;

    for (let i = 0; i < actions.length; i++) {
      cumulativeWeight += weights[i];
      if (choice < cumulativeWeight) {
        chosenAction = actions[i];
function decideDinoAction() {
  if (!dino.isGrounded()) {
    return { type: 'none', obstaclesToEvaluate: null }; // Cannot act if not grounded
  }

  let chosenAction = null;
  let obstaclesToEvaluate = null; // Stores snapshot for evaluation

  // 1. Find the closest obstacle currently in front of the dino
  let closestObstacle = null;
  let minDistance = Infinity;
  for (let obstacle of obstacles) {
    if (obstacle.x > dino.x + dino.w) {
      const distance = obstacle.x - (dino.x + dino.w);
      if (distance < minDistance) {
        minDistance = distance;
        closestObstacle = obstacle;
      }
    }
  }

  console.log("decideDinoAction: Closest obstacle found:", closestObstacle ? closestObstacle.obstacleCourseIndex : "None");

  // 2. Prioritize Learned Positive Jumps
  if (closestObstacle) {
    // Check if a positive jump for this specific obstacleCourseIndex is in winningList
    const learnedJump = winningList.find(a => typeof a === 'object' && a.type === 'positive_jump' && a.obstacleCourseIndex === closestObstacle.obstacleCourseIndex);

    if (learnedJump) {
      dino.jump();
      chosenAction = 'jump';
      // When a learned jump is triggered, we still need to capture obstacles for evaluation
      // Capture all obstacles in front of the dino's right edge
      obstaclesToEvaluate = obstacles.filter(o => o.x > dino.x + dino.w).map(o => ({ id: o.id, obstacleCourseIndex: o.obstacleCourseIndex }));
      console.log("  Learned POSITIVE JUMP triggered for obstacle:", closestObstacle.obstacleCourseIndex, ". Evaluating", obstaclesToEvaluate.length, "obstacles.");
    }
  }

  // 3. If no learned jump was triggered, then take a new random action, respecting the interval.
  if (chosenAction === null && (frameCount - lastRandomActionFrame > randomActionInterval || obstacles.length === 0)) {
    lastRandomActionFrame = frameCount; // Reset interval for random actions

    const actions = ['jump', 'duck', 'none'];
    const weights = [0.3, 0.2, 0.5];
    let choice = random(1);
    let cumulativeWeight = 0;

    for (let i = 0; i < actions.length; i++) {
      cumulativeWeight += weights[i];
      if (choice < cumulativeWeight) {
        chosenAction = actions[i];
        console.log("  Random action chosen:", chosenAction);
        if (chosenAction === 'jump') {
          dino.jump();
          // Capture all obstacles in front for evaluation
          // Capture all obstacles in front of the dino's right edge
          obstaclesToEvaluate = obstacles.filter(o => o.x > dino.x + dino.w).map(o => ({ id: o.id, obstacleCourseIndex: o.obstacleCourseIndex }));
        } else if (chosenAction === 'duck') {
          dino.duck();
          // For duck, we don't need obstaclesToEvaluate for positive jump logic, but keep it consistent
          obstaclesToEvaluate = obstacles.filter(o => o.x > dino.x + dino.w).map(o => ({ id: o.id, obstacleCourseIndex: o.obstacleCourseIndex }));
        }
        break; // Exit loop once action is chosen
      }
    }
  }

  // 4. If still no action (e.g., repetition failed and random interval not met), default to 'none'
  if (chosenAction === null) {
    chosenAction = 'none';
    console.log("  Defaulting to 'none' action.");
  }

  return { type: chosenAction, obstaclesToEvaluate: obstaclesToEvaluate };
}
Line-by-line explanation (10 lines)

🔧 Subcomponents:

for-loop Find Closest Obstacle for (let obstacle of obstacles) { ... if (distance < minDistance) { ... } }

Scans all obstacles ahead of the dino and remembers which one is nearest—this becomes the target to check against learned patterns

conditional Learned Jump Priority const learnedJump = winningList.find(a => typeof a === 'object' && a.type === 'positive_jump' && a.obstacleCourseIndex === closestObstacle.obstacleCourseIndex);

Searches the winningList for a previously successful jump against this exact obstacle course position—the core of the learning mechanism

for-loop Weighted Random Action Selection for (let i = 0; i < actions.length; i++) { ... if (choice < cumulativeWeight) { ... } }

Uses cumulative weights to randomly pick one of three actions (jump 30%, duck 20%, do nothing 50%), biasing toward safer choices

if (!dino.isGrounded()) {
Checks if the dino is in mid-air (jumping); if so, no new action can be started until it lands.
for (let obstacle of obstacles) {
Loops through every obstacle on screen to find which one is closest in front of the dino.
if (obstacle.x > dino.x + dino.w) {
Only considers obstacles that are ahead of the dino (beyond its right edge); ignores those already passed.
const distance = obstacle.x - (dino.x + dino.w);
Calculates how far ahead each obstacle is; smaller numbers mean closer obstacles.
const learnedJump = winningList.find(a => typeof a === 'object' && a.type === 'positive_jump' && a.obstacleCourseIndex === closestObstacle.obstacleCourseIndex);
Searches the winningList array for a record of a successful jump against this specific obstacle course position—if found, the dino repeats that jump.
if (learnedJump) {
If a learned jump was found, execute it immediately instead of taking a random action.
if (chosenAction === null && (frameCount - lastRandomActionFrame > randomActionInterval || obstacles.length === 0)) {
Only takes a new random action if no learned jump was triggered AND enough frames have passed since the last random action (prevents decision spam).
const weights = [0.3, 0.2, 0.5];
These probabilities control how often each action is chosen: jump 30% of the time, duck 20%, do nothing 50%—do nothing is safest.
if (choice < cumulativeWeight) {
Cumulative weighting: if random() < 0.3, choose jump; if < 0.5, choose duck; if < 1.0, choose none—this converts weights into decision thresholds.
obstaclesToEvaluate = obstacles.filter(o => o.x > dino.x + dino.w).map(o => ({ id: o.id, obstacleCourseIndex: o.obstacleCourseIndex }));
Creates a snapshot of all obstacles currently ahead, storing their IDs and course positions so the outcome can be evaluated later even if obstacles are removed.

initializeObstacles()

This function is critical to the learning mechanism. It creates the exact same obstacle sequence every game by reading from fixedObstacleCourse, which allows the dino to learn that 'obstacle at course position 5 is a bird' and apply that knowledge across multiple games. Without this fixed pattern, learning would be impossible.

function initializeObstacles() {
  for (let i = 0; i < fixedObstacleCourse.length; i++) { // Loop with index i
    const obsDef = fixedObstacleCourse[i];
    if (obsDef.type === 'cactus') {
      obstacles.push(new Cactus(width + obsDef.xOffset, i)); // Pass index as obstacleCourseIndex
    } else if (obsDef.type === 'bird') {
      obstacles.push(new Bird(width + obsDef.xOffset, i)); // Pass index as obstacleCourseIndex
    }
  }
}
Line-by-line explanation (5 lines)

🔧 Subcomponents:

for-loop Fixed Course Iteration for (let i = 0; i < fixedObstacleCourse.length; i++) { ... }

Iterates through the predefined fixedObstacleCourse array and creates the exact same sequence of obstacles every game, ensuring repeatable patterns for learning

for (let i = 0; i < fixedObstacleCourse.length; i++) {
Loops through each obstacle definition in the fixedObstacleCourse array, creating them in order with their predetermined positions.
const obsDef = fixedObstacleCourse[i];
Retrieves the current obstacle definition object, which contains type ('cactus' or 'bird') and xOffset (distance from right edge).
if (obsDef.type === 'cactus') {
Checks if this obstacle should be a cactus; if so, creates a new Cactus instance.
obstacles.push(new Cactus(width + obsDef.xOffset, i));
Creates a cactus at x position `width + xOffset` (off-screen to the right) and passes `i` as its obstacleCourseIndex so it can be recognized as the same obstacle type in future games.
} else if (obsDef.type === 'bird') {
Checks if this obstacle should be a bird; if so, creates a new Bird instance.

resetGame()

resetGame() is called whenever the dino dies. It restores the game to starting conditions but preserves the winningList—the AI's memory of successful actions. This is the key design choice that enables learning: each game is a fresh attempt, but with accumulated knowledge from all previous games.

function resetGame() {
  gameOver = false; // Ensure this is false for the next game loop
  highScore = max(highScore, score);
  score = 0;
  gameSpeed = 5;
  obstacles = []; // Clear current obstacles
  obstacleIdCounter = 0; // Reset obstacle IDs for the new course
  initializeObstacles(); // Re-initialize the fixed course
  dino.reset(); // Reset dino's position and state, including isDead flag
  currentAction = null; // Clear any pending action
  lastRandomActionFrame = 0; // Reset random action timer
  // The winningList is NOT cleared, as per user request ("over time the winning list will grow and grow")
}
Line-by-line explanation (10 lines)
highScore = max(highScore, score);
Updates the high score by keeping whichever is larger: the old high score or the score just achieved, preserving the best performance.
score = 0;
Resets the current game score to 0, ready for the next attempt.
gameSpeed = 5;
Resets the game speed back to its starting value so the next game begins at normal difficulty (before gradual acceleration).
obstacles = [];
Clears all obstacle objects from the current game; they will be recreated by initializeObstacles().
obstacleIdCounter = 0;
Resets the unique ID counter so new obstacles in the next game get fresh IDs starting from 0.
initializeObstacles();
Repopulates the obstacles array with the exact same fixed course sequence, ensuring the next game is identical in pattern.
dino.reset();
Resets the dino's position, velocity, jumping state, ducking state, and isDead flag so it starts fresh on the ground.
currentAction = null;
Clears any pending action from the previous game so the new game starts with a clean state.
lastRandomActionFrame = 0;
Resets the random action timer so the first action in the new game can be taken immediately (or after the interval passes).
// The winningList is NOT cleared, as per user request ("over time the winning list will grow and grow")
Deliberately leaves the winningList intact across games; the dino's learned knowledge persists and grows stronger.

Dino (class)

The Dino class encapsulates all the dino's physics, animation, and state. The update() method applies gravity and detects landing, while jump() and duck() change state. This separation of concerns (physics in update, actions in jump/duck) makes the code maintainable and easy to modify.

🔬 This is the core jump physics. What happens if you change `this.velocity += this.gravity` to `this.velocity += this.gravity * 0.5` (half gravity)? Will the dino jump higher or lower, and will jumps take longer or shorter?

  update() {
    // Apply gravity
    this.velocity += this.gravity;
    this.y += this.velocity;

    // Prevent dino from going below the ground
    if (this.y >= this.initialY) {
      this.y = this.initialY;
      this.velocity = 0;
      this.isJumping = false;
class Dino {
  constructor() {
    this.w = 50; // Dino width
    this.h = 60; // Dino height (standing)
    this.duckH = 30; // Dino height (ducking)
    this.x = 50;
    this.y = height - ground.y - this.h; // Initial position on the ground
    this.velocity = 0;
    this.gravity = 0.8; // Gravity strength
    this.isJumping = false;
    this.isDucking = false;
    this.initialY = this.y; // Store initial Y for reset and ground check
    this.isDead = false; // New flag for collision feedback
  }

  jump() {
    if (!this.isJumping && !this.isDucking) {
      this.velocity = -20; // Upward velocity for jump - Increased for higher jumps
      this.isJumping = true;
    }
  }

  duck() {
    if (!this.isJumping && !this.isDucking) {
      this.isDucking = true;
      this.y += (this.h - this.duckH); // Move dino down to duck
    }
  }

  update() {
    // Apply gravity
    this.velocity += this.gravity;
    this.y += this.velocity;

    // Prevent dino from going below the ground
    if (this.y >= this.initialY) {
      this.y = this.initialY;
      this.velocity = 0;
      this.isJumping = false;
      // If dino was ducking and hit the ground, stand up
      if (this.isDucking) {
        this.y -= (this.h - this.duckH); // Move dino up to stand
        this.isDucking = false;
      }
    }
  }

  show() {
    if (this.isDead) {
      fill(255, 0, 0); // Red when dead
    } else {
      fill(50); // Dark gray dino
    }
    if (this.isDucking) {
      rect(this.x, this.y, this.w, this.duckH);
    } else {
      rect(this.x, this.y, this.w, this.h);
    }
  }

  isGrounded() {
    return this.y === this.initialY && !this.isJumping;
  }

  // Reset dino's state
  reset() {
    this.y = height - ground.y - this.h;
    this.initialY = this.y;
    this.velocity = 0;
    this.isJumping = false;
    this.isDucking = false;
    this.isDead = false; // Reset dead state
  }
}
Line-by-line explanation (12 lines)

🔧 Subcomponents:

conditional Jump Action if (!this.isJumping && !this.isDucking) { this.velocity = -20; this.isJumping = true; }

Initiates a jump by setting negative velocity (upward) only if the dino is grounded and not already ducking

calculation Physics Update this.velocity += this.gravity; this.y += this.velocity;

Applies gravity acceleration to the velocity, then moves the dino vertically—creates the falling motion during a jump

this.w = 50; // Dino width
Sets the dino's width in pixels; used for drawing and hitbox calculations.
this.h = 60; // Dino height (standing)
Sets the dino's height when standing; taller hitbox means it can hit obstacles at a greater vertical range.
this.duckH = 30; // Dino height (ducking)
Sets the dino's height when ducking; much smaller, allowing it to fit under flying birds.
this.y = height - ground.y - this.h; // Initial position on the ground
Positions the dino on the ground by calculating its Y so its bottom edge sits at ground level.
this.gravity = 0.8; // Gravity strength
Gravity constant that accelerates the dino downward when jumping; higher values make gravity stronger, shorter jumps.
if (!this.isJumping && !this.isDucking) {
Only allows jumping if the dino is not already jumping and not ducking—prevents double-jumping or jumping while crouched.
this.velocity = -20; // Upward velocity for jump - Increased for higher jumps
Sets velocity to a negative number, which moves the dino upward; larger negative values = higher jumps.
this.y += (this.h - this.duckH); // Move dino down to duck
Increases Y position (moves dino downward) by the difference between standing and ducking heights, shrinking the hitbox.
this.velocity += this.gravity;
Every frame, gravity pulls the velocity more negative (downward), creating acceleration—like real gravity pulling down.
this.y += this.velocity;
Adds velocity to position; during a jump, velocity is negative so Y decreases (dino moves up); after apex, velocity is positive (dino moves down).
if (this.y >= this.initialY) {
Checks if the dino has fallen back to ground level; if so, end the jump and reset velocity to 0.
if (this.isDead) { fill(255, 0, 0); }
Colors the dino red if it collided, providing instant visual feedback that it died.

Ground (class)

The Ground class creates the scrolling motion effect using two ground segments that loop infinitely. It doesn't move the camera; instead, it moves the ground leftward while the dino stays stationary, which tricks the eye into seeing the dino moving rightward. This is a classic game development technique.

class Ground {
  constructor() {
    this.x1 = 0;
    this.x2 = width;
    this.y = 80; // Height from the bottom of the canvas
    this.h = 10;
  }

  update() {
    this.x1 -= gameSpeed;
    this.x2 -= gameSpeed;

    if (this.x1 < -width) {
      this.x1 = width;
    }
    if (this.x2 < -width) {
      this.x2 = width;
    }
  }

  show() {
    fill(100); // Gray ground
    rect(this.x1, height - this.y, width, this.h);
    rect(this.x2, height - this.y, width, this.h);
  }

  reset() {
    this.x1 = 0;
    this.x2 = width;
  }
}
Line-by-line explanation (7 lines)

🔧 Subcomponents:

conditional Ground Wrapping if (this.x1 < -width) { this.x1 = width; }

Resets a scrolled-off-screen ground segment back to the right side, creating the illusion of an infinite repeating ground

this.x1 = 0;
First ground segment starts at the left edge of the canvas.
this.x2 = width;
Second ground segment starts at the right edge, so two ground segments are visible and seamless at the start.
this.y = 80; // Height from the bottom of the canvas
The ground is positioned 80 pixels above the bottom edge; this defines where obstacles rest and where the dino stands.
this.x1 -= gameSpeed;
Moves the first ground segment leftward by the game speed each frame, creating the scrolling illusion.
this.x2 -= gameSpeed;
Moves the second ground segment leftward at the same speed to keep the ground seamless.
if (this.x1 < -width) { this.x1 = width; }
When the first segment scrolls off the left side (position < -width), it teleports to the right side (position = width) to create a seamless loop.
rect(this.x1, height - this.y, width, this.h);
Draws a rectangle for the first ground segment at Y position `height - this.y` (80 pixels from bottom) with height this.h.

Obstacle (class)

The Obstacle base class defines common behavior for all obstacles: scrolling left, drawing, and off-screen detection. Subclasses Cactus and Bird inherit from it and add specific properties like height randomization. This inheritance pattern reduces code duplication and makes adding new obstacle types easy.

class Obstacle {
  constructor(x, y, w, h, obstacleCourseIndex) { // obstacleCourseIndex parameter added
    this.x = x; // Use provided x, not just width
    this.y = y;
    this.w = w;
    this.h = h;
    this.id = obstacleIdCounter++; // Assign a unique ID to each obstacle in the current session
    this.obstacleCourseIndex = obstacleCourseIndex; // Store the index from the fixed course
  }

  update() {
    this.x -= gameSpeed;
  }

  show() {
    stroke(0); // Add a black stroke (outline)
    strokeWeight(1); // Set stroke weight to 1 pixel
    fill(0); // Make obstacles black for better contrast
    rect(this.x, this.y, this.w, this.h);
  }

  offscreen() {
    return this.x < -this.w;
  }
}
Line-by-line explanation (6 lines)

🔧 Subcomponents:

conditional Off-Screen Detection return this.x < -this.w;

Returns true when the obstacle has scrolled completely off the left edge, triggering its removal from the array

this.id = obstacleIdCounter++;
Assigns a unique ID to this obstacle by taking the current counter value and incrementing it—ensures each obstacle can be tracked individually.
this.obstacleCourseIndex = obstacleCourseIndex;
Stores which position in the fixed obstacle course this obstacle came from, enabling the AI to recognize it as the same obstacle type across games.
this.x -= gameSpeed;
Moves the obstacle leftward by gameSpeed pixels each frame, creating the scrolling effect.
stroke(0); // Add a black stroke (outline)
Sets the stroke (outline) color to black, making obstacle edges visible.
fill(0); // Make obstacles black for better contrast
Sets the fill color to black, making obstacles stand out visually against the light gray background.
return this.x < -this.w;
Returns true if the obstacle's left edge is further left than its right edge can reach (completely off-screen)—a simple boundary check.

Cactus (class)

Cactus extends Obstacle and randomizes its dimensions each time one is created. Because each cactus is different, the dino learns to jump over cacti in general, not just one specific size. This variation is important for building robust learning.

class Cactus extends Obstacle {
  constructor(x, obstacleCourseIndex) { // obstacleCourseIndex passed from initializeObstacles
    const cactusWidth = random(20, 50);
    const cactusHeight = random(40, 80);
    super(x, height - ground.y - cactusHeight, cactusWidth, cactusHeight, obstacleCourseIndex); // Pass obstacleCourseIndex to parent
    this.type = 'cactus';
  }
}
Line-by-line explanation (4 lines)
const cactusWidth = random(20, 50);
Randomly picks a width between 20 and 50 pixels so each cactus looks slightly different.
const cactusHeight = random(40, 80);
Randomly picks a height between 40 and 80 pixels, making some cacti taller and harder to avoid without jumping.
super(x, height - ground.y - cactusHeight, cactusWidth, cactusHeight, obstacleCourseIndex);
Calls the parent Obstacle constructor with position (ground level), dimensions, and course index; Y is positioned so the cactus sits on the ground.
this.type = 'cactus';
Labels this obstacle as type 'cactus' for reference in collision logic or learning code.

Bird (class)

Bird extends Obstacle and randomizes both width and height range, as well as Y position. Birds require the dino to duck, providing variety from jump-only cacti. The random Y position tests the dino's ability to duck at the right moment.

class Bird extends Obstacle {
  constructor(x, obstacleCourseIndex) { // obstacleCourseIndex passed from initializeObstacles
    const birdWidth = random(30, 60);
    const birdHeight = 30; // Birds have a fixed height for simplicity
    // Birds can fly at different heights
    const birdY = random(height - ground.y - dino.h - 100, height - ground.y - dino.h - 30);
    super(x, birdY, birdWidth, birdHeight, obstacleCourseIndex); // Pass obstacleCourseIndex to parent
    this.type = 'bird';
  }
}
Line-by-line explanation (5 lines)
const birdWidth = random(30, 60);
Randomly picks a width between 30 and 60 pixels for variety.
const birdHeight = 30; // Birds have a fixed height for simplicity
Birds always have the same height (30 pixels), making them consistent obstacles that require ducking to avoid.
const birdY = random(height - ground.y - dino.h - 100, height - ground.y - dino.h - 30);
Places birds at random heights between 30 and 100 pixels above the dino's standing height, forcing the dino to duck at varying elevations.
super(x, birdY, birdWidth, birdHeight, obstacleCourseIndex);
Calls the parent constructor with the randomized position and dimensions, passing the course index for learning.
this.type = 'bird';
Labels this obstacle as type 'bird' for identification in game logic.

checkCollision()

AABB (Axis-Aligned Bounding Box) collision detection is the simplest form: compare two rectangles by checking if they overlap on both X and Y axes. All four inequalities must be true simultaneously for a collision. This function is called every frame in draw() to determine if the dino hit an obstacle.

🔬 This AABB collision logic checks four conditions. What if you remove one line, like `dinoHitbox.x < obstacleHitbox.x + obstacleHitbox.w`? Which direction would false collisions happen (the dino would die when it shouldn't)?

  for (let obstacle of obstacles) {
    let obstacleHitbox = {
      x: obstacle.x,
      y: obstacle.y,
      w: obstacle.w,
      h: obstacle.h
    };

    // Simple AABB (Axis-Aligned Bounding Box) collision detection
    if (dinoHitbox.x < obstacleHitbox.x + obstacleHitbox.w &&
      dinoHitbox.x + dinoHitbox.w > obstacleHitbox.x &&
      dinoHitbox.y < obstacleHitbox.y + obstacleHitbox.h &&
      dinoHitbox.y + dinoHitbox.h > obstacleHitbox.y) {
      return true; // Collision detected
    }
  }
  return false; // No collision
function checkCollision() {
  let dinoHitbox;
  if (dino.isDucking) {
    dinoHitbox = {
      x: dino.x,
      y: dino.y,
      w: dino.w,
      h: dino.duckH
    };
  } else {
    dinoHitbox = {
      x: dino.x,
      y: dino.y,
      w: dino.w,
      h: dino.h
    };
  }

  for (let obstacle of obstacles) {
    let obstacleHitbox = {
      x: obstacle.x,
      y: obstacle.y,
      w: obstacle.w,
      h: obstacle.h
    };

    // Simple AABB (Axis-Aligned Bounding Box) collision detection
    if (dinoHitbox.x < obstacleHitbox.x + obstacleHitbox.w &&
      dinoHitbox.x + dinoHitbox.w > obstacleHitbox.x &&
      dinoHitbox.y < obstacleHitbox.y + obstacleHitbox.h &&
      dinoHitbox.y + dinoHitbox.h > obstacleHitbox.y) {
      return true; // Collision detected
    }
  }
  return false; // No collision
}
Line-by-line explanation (8 lines)

🔧 Subcomponents:

conditional Hitbox Size Adjustment if (dino.isDucking) { dinoHitbox = { ... dino.duckH ... } } else { dinoHitbox = { ... dino.h ... } }

Switches the dino's hitbox height between ducking (30px) and standing (60px) to accurately represent whether it can fit under birds

conditional AABB Collision Test if (dinoHitbox.x < obstacleHitbox.x + obstacleHitbox.w && ...) { return true; }

Performs axis-aligned bounding box collision detection by checking if the dino's box overlaps any obstacle's box on both axes

if (dino.isDucking) {
Checks if the dino is currently ducking; if so, use the smaller hitbox height.
dinoHitbox = { x: dino.x, y: dino.y, w: dino.w, h: dino.duckH };
Creates a hitbox object with the ducking height (30px), making it shorter so birds can pass over without hitting.
for (let obstacle of obstacles) {
Loops through every obstacle to check collision with the dino.
let obstacleHitbox = { x: obstacle.x, y: obstacle.y, w: obstacle.w, h: obstacle.h };
Creates a hitbox object for the current obstacle with its exact position and dimensions.
if (dinoHitbox.x < obstacleHitbox.x + obstacleHitbox.w &&
First condition: dino's left edge must be before the obstacle's right edge (dino is not entirely to the right).
dinoHitbox.x + dinoHitbox.w > obstacleHitbox.x &&
Second condition: dino's right edge must be after the obstacle's left edge (dino is not entirely to the left).
dinoHitbox.y < obstacleHitbox.y + obstacleHitbox.h &&
Third condition: dino's top edge must be before the obstacle's bottom edge (dino is not entirely above).
dinoHitbox.y + dinoHitbox.h > obstacleHitbox.y) {
Fourth condition: dino's bottom edge must be after the obstacle's top edge (dino is not entirely below). All four must be true to collide.

keyPressed()

keyPressed() is a built-in p5.js function that runs once whenever a key is pressed. This sketch allows manual resets via spacebar or up arrow, though the game automatically resets when the dino dies. You could extend this to control the dino manually for testing if you wanted.

function keyPressed() {
  if (key === ' ' || keyCode === UP_ARROW) {
    resetGame();
  }
}
Line-by-line explanation (2 lines)
if (key === ' ' || keyCode === UP_ARROW) {
Checks if the user pressed either the spacebar (key === ' ') or the up arrow (keyCode === UP_ARROW).
resetGame();
If either key was pressed, calls resetGame() to restart the game, though the game also auto-resets on collision.

touchStarted()

touchStarted() is a built-in p5.js function that fires when a touch event occurs on mobile/tablet. This makes the game playable on touch devices by restarting on tap.

function touchStarted() {
  resetGame();
  return false; // Prevent default touch action
}
Line-by-line explanation (2 lines)
resetGame();
Calls resetGame() when the user taps the screen, allowing mobile players to restart without a keyboard.
return false; // Prevent default touch action
Returning false prevents p5.js and the browser from doing default touch behaviors (like scrolling), keeping the game in focus.

windowResized()

windowResized() is a built-in p5.js function that fires whenever the browser window is resized. This sketch handles resizing gracefully by resetting the canvas, ground, dino, and obstacles so the game stays playable at any screen size.

function windowResized() {
  resizeCanvas(windowWidth, windowHeight);
  ground.reset(); // Adjust ground based on new width
  dino.reset(); // Adjust dino based on new height
  // Re-initialize obstacles on resize to reposition them correctly
  obstacles = [];
  obstacleIdCounter = 0;
  initializeObstacles();
}
Line-by-line explanation (6 lines)
resizeCanvas(windowWidth, windowHeight);
Updates the p5.js canvas size to match the new window dimensions when the browser is resized.
ground.reset();
Resets the ground position to the left side since the canvas width may have changed.
dino.reset();
Resets the dino position in case the canvas height changed, recalculating where the ground level should be.
obstacles = [];
Clears all obstacles so they can be repositioned for the new canvas size.
obstacleIdCounter = 0;
Resets the obstacle ID counter so new obstacles get fresh IDs.
initializeObstacles();
Recreates all obstacles at their correct positions relative to the new canvas size.

📦 Key Variables

dino object (Dino instance)

Stores the dinosaur player object, containing its position, physics state, and methods for jumping/ducking

let dino = new Dino();
ground object (Ground instance)

Manages the scrolling ground that creates the motion illusion; contains two segments that wrap around

let ground = new Ground();
obstacles array of Obstacle objects

Stores all active cacti and birds on screen; updated each frame as obstacles scroll and are removed

let obstacles = [];
score number

Tracks the current game score (incremented each frame); resets to 0 on game over

let score = 0;
highScore number

Stores the highest score achieved across all game sessions; persists through resets

let highScore = 0;
gameSpeed number

Controls how fast obstacles and ground scroll; increases gradually each frame to raise difficulty

let gameSpeed = 5;
gameOver boolean

Legacy flag; no longer controls game-over display but is reset during game resets

let gameOver = false;
winningList array of mixed (objects and strings)

Stores the AI's learned knowledge: records of successful jumps (with obstacle course index) and neutral actions; grows across game sessions

let winningList = [];
currentAction object with type and obstaclesToEvaluate properties

Stores the dino's action choice from the current frame (jump/duck/none) and the obstacles targeted by that action, for outcome evaluation

let currentAction = null;
randomActionInterval number (frames)

Minimum frames between random action choices; prevents the dino from deciding too frequently and allows learning to stabilize

let randomActionInterval = 45;
lastRandomActionFrame number

Stores the frameCount when the last random action was performed; used to enforce the randomActionInterval

let lastRandomActionFrame = 0;
obstacleIdCounter number

Counter for assigning unique IDs to obstacles within a game session; resets to 0 each game

let obstacleIdCounter = 0;
fixedObstacleCourse array of objects

Defines the repeating obstacle pattern with type and xOffset; ensures the exact same course spawns every game

const fixedObstacleCourse = [{ type: 'cactus', xOffset: 500 }, ...];

🔧 Potential Improvements (6)

Here are some ways this code could be enhanced:

BUG decideDinoAction() learning logic

The dino only checks for learned jumps against the closest obstacle, but once a positive jump is recorded, it's stored with a course index that repeats every game. If the same obstacle position spawns twice in a row (unlikely with the current course), the dino might try to jump twice. More subtly, learned jumps are never forgotten, so past decisions could conflict with current safety needs.

💡 Add a timeout or confidence decay to learned jumps—after N games, reduce their weight. Or implement negative learning: record failed jumps so the dino avoids those decisions in the future.

PERFORMANCE decideDinoAction() and draw()

winningList is searched with `.find()` every frame and grows indefinitely. Once it contains hundreds of entries, searching becomes slow. The filter() calls for counting entries in draw() also scan the entire array each frame.

💡 Use a Map or object keyed by obstacleCourseIndex to store learned jumps—O(1) lookup instead of O(n). Cache the positive/neutral counts instead of recalculating every frame.

STYLE decideDinoAction()

The function is long (60+ lines) and mixes obstacle-finding logic, learning logic, and random selection in one block. It's hard to read and modify.

💡 Extract helper functions: `findClosestObstacle()`, `checkLearnedJump()`, and `pickRandomAction()` to improve readability and reusability.

FEATURE Learning system

The dino learns only successful jumps—it never learns that certain obstacles are best avoided with duck or none. All non-positive outcomes are lumped as 'neutral', providing no negative feedback.

💡 Track duck successes separately. When an obstacle is passed safely by ducking, record `{ type: 'positive_duck', obstacleCourseIndex }` so the dino can learn two strategies per obstacle type.

BUG checkCollision()

Hitbox detection uses exact equality: `this.y === this.initialY`. Due to floating-point arithmetic in physics updates, the dino might land slightly above or below ground, causing hitbox misalignment over time.

💡 Use a tolerance check: `Math.abs(this.y - this.initialY) < 1` instead of strict equality to account for floating-point imprecision.

FEATURE UI / Game Loop

The game has no visual indicator of what the dino is 'thinking'—users cannot see which action it chose or why. The learning is invisible except for the winning list counter.

💡 Draw a label above or below the dino showing its current action ('JUMP', 'DUCK', 'NONE') and highlight it if it's a learned action vs. random. This helps users understand the learning process.

🔄 Code Flow

Code flow showing setup, draw, decidedinoaction, initializeobstacles, resetgame, dino, ground, obstacle, cactus, bird, checkcollision, keypressed, touchstarted, windowresized

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

graph TD start[Start] --> setup[setup] setup --> draw[draw loop] draw --> collision-processing[collision-processing] draw --> obstacle-update-loop[obstacle-update-loop] draw --> winning-list-display[winning-list-display] draw --> decidedinoaction[decidedinoaction] draw --> checkcollision[checkcollision] draw --> keypressed[keypressed] draw --> touchstarted[touchstarted] draw --> windowresized[windowresized] collision-processing -->|Evaluate outcome| winning-list-display obstacle-update-loop -->|Update obstacles| draw winning-list-display -->|Count successful actions| draw decidedinoaction --> find-closest[find-closest] find-closest --> learned-jump-check[learned-jump-check] learned-jump-check --> random-action-choice[random-action-choice] random-action-choice --> draw click setup href "#fn-setup" click draw href "#fn-draw" click collision-processing href "#sub-collision-processing" click obstacle-update-loop href "#sub-obstacle-update-loop" click winning-list-display href "#sub-winning-list-display" click decidedinoaction href "#fn-decidedinoaction" click checkcollision href "#fn-checkcollision" click keypressed href "#fn-keypressed" click touchstarted href "#fn-touchstarted" click windowresized href "#fn-windowresized" learned-jump-check --> jump-method[jump-method] jump-method --> update-gravity[update-gravity] update-gravity --> scroll-logic[scroll-logic] scroll-logic --> offscreen-check[offscreen-check] offscreen-check -->|Remove obstacle| draw hitbox-selection -->|Adjust hitbox| draw aabb-check -->|Check collision| collision-processing click find-closest href "#sub-find-closest" click learned-jump-check href "#sub-learned-jump-check" click random-action-choice href "#sub-random-action-choice" click jump-method href "#sub-jump-method" click update-gravity href "#sub-update-gravity" click scroll-logic href "#sub-scroll-logic" click offscreen-check href "#sub-offscreen-check" click hitbox-selection href "#sub-hitbox-selection" click aabb-check href "#sub-aabb-check"

❓ Frequently Asked Questions

What visual elements does the dino sketch create?

The dino sketch visually represents a side-scrolling landscape featuring a dinosaur character that navigates through various obstacles like cacti and birds.

How can users interact with the dino sketch?

Users can interact with the sketch by controlling the dino's actions, such as jumping or ducking to avoid obstacles as they appear on the screen.

What creative coding concept does the dino sketch demonstrate?

The sketch demonstrates concepts of random action decisions and obstacle course generation, utilizing fixed patterns to create a consistent gameplay experience.

Preview

dino - p5.js creative coding sketch preview
Sketch Preview
Code flow diagram showing the structure of dino - Code flow showing setup, draw, decidedinoaction, initializeobstacles, resetgame, dino, ground, obstacle, cactus, bird, checkcollision, keypressed, touchstarted, windowresized
Code Flow Diagram