Snail game

This sketch is a full arcade game where a snail glides around eating red food, dodging grey obstacles, and collecting speed-boosting candy and a screen-clearing pickaxe. It wraps the game in a complete menu system - start screen, map select, instructions, avatar shop, and an admin cheat panel - with progress saved to the browser's localStorage.

🧪 Try This!

Experiment with the code by making these changes:

  1. Turbo snail — Raising the snail's base speed makes it feel dramatically faster under both arrow-key and mouse control.
  2. Giant snail — The snail's size drives both its drawing scale and its collision radius, so a bigger snail is easier to hit but also easier to see.
  3. Neon obstacles
  4. Midnight mode — The background() call at the top of draw() paints behind every single screen in the game, so darkening it gives the whole game a night-time feel.
Prefer the full editor? Open it there →

📖 About This Sketch

This sketch turns p5.js into a small arcade game: a snail steered by arrow keys or the mouse must eat food to raise its score, avoid grey obstacles that end the game, and grab candy and pickaxe pickups scattered across six unlockable maps. Visually it mixes hand-drawn character sprites (three different snail designs built from ellipses and arcs), a spinning star-shaped candy drawn with a rotating triangle loop, and a full set of menu screens with clickable buttons. Under the hood it leans on ES6 classes, circle-rectangle collision math, p5.Vector-based steering, and a `gameState` string that acts as a finite state machine to switch between screens.

The code is organized into class definitions (Obstacle, Food and its subclasses Candy/Pickaxe/Key/Chest, and Snail) followed by the standard p5.js lifecycle functions (preload, setup, draw) and a long list of helper functions - one drawing function per screen (start, map select, instructions, shop, admin, map-unlocked, game-over) plus game logic functions like startGame(), updateGame(), and isSpotOccupied(). By studying it you will learn how to structure a multi-screen game with a single state variable, how inheritance lets several pickup types share collision and respawn logic, how to steer a character toward the mouse with vector subtraction, and how to persist progress between sessions with localStorage.

⚙️ How It Works

  1. When the page loads, preload() builds the list of six maps and tries to load any previously saved progress (unlocked maps, chosen snail skin) from localStorage before anything is drawn.
  2. setup() creates a full-window canvas and calls startGame(), which resets the score, places the snail in the center, builds that map's obstacles, and scatters food, three candies, and one pickaxe.
  3. Every frame, draw() clears the background and then looks at the global gameState variable to decide which single function to run - a menu screen, the shop, the admin panel, or updateGame()+drawGame() during actual play.
  4. While playing, updateGame() moves the snail with either arrow keys or the mouse, then checks distances between the snail and every food, obstacle, candy, pickaxe and key to trigger scoring, game-over, speed boosts, or item pickups.
  5. Reaching a map's target score calls unlockNextMap() and switches to a celebratory 'mapUnlocked' screen; hitting an obstacle (unless God Mode is on) switches to 'gameOver'; both screens return to the menu on any key press or click.
  6. mousePressed() and keyPressed() translate clicks and key taps into gameState changes and gameplay actions (like pressing 'E' to swing the picked-up pickaxe), while saveGameProgress()/loadGameProgress() keep unlocked maps and the chosen avatar stored in the browser between visits.

🎓 Concepts You'll Learn

Game state machine (switch on a string variable)ES6 classes and inheritanceCircle-rectangle collision detectionVector-based mouse steering (p5.Vector)localStorage persistenceProcedural sprite drawing with ellipses/arcs/beginShape

📝 Code Breakdown

class Obstacle

Obstacle is the simplest class in the sketch - just a rectangle with a position and size. Other classes like Snail and Food later test collisions against Obstacle instances using circle-rectangle math.

class Obstacle {
  constructor(x, y, w, h) {
    this.x = x;
    this.y = y;
    this.w = w;
    this.h = h;
  }

  // Draw the obstacle
  show() {
    fill(100); // Grey
    rect(this.x, this.y, this.w, this.h);
  }
}
Line-by-line explanation (3 lines)
constructor(x, y, w, h)
Stores the obstacle's top-left position and its width/height as plain properties - simple data, no fancy math.
fill(100); // Grey
Sets the fill color to a mid-grey before drawing the rectangle.
rect(this.x, this.y, this.w, this.h);
Draws the obstacle as a rectangle using its stored position and size.

class Food

Food is the base class that Candy, Pickaxe, Key, and Chest all extend with `extends Food`, so they automatically inherit respawn() and collidesWith() without rewriting that logic - a good example of code reuse through inheritance.

🔬 This loop tries 100 random spots before giving up. What happens visually if you lower 100 down to something tiny like 2 on a map with lots of obstacles - would food start spawning on top of obstacles more often?

    while (attempts < 100) { // Limit attempts to prevent infinite loops on very dense maps
      this.x = random(this.size / 2, width - this.size / 2);
      this.y = random(this.size / 2, height - this.size / 2);
class Food {
  constructor() {
    this.size = 20;
    // Initial respawn handled in startGame()
  }

  // Move food to a new random location, avoiding obstacles, the snail, and other items
  respawn() {
    let attempts = 0;
    let foundSpot = false;
    while (attempts < 100) { // Limit attempts to prevent infinite loops on very dense maps
      this.x = random(this.size / 2, width - this.size / 2);
      this.y = random(this.size / 2, height - this.size / 2);

      // Pass the current item to isSpotOccupied so it doesn't check itself
      if (!isSpotOccupied(this.x, this.y, this.size, this)) {
        foundSpot = true;
        break; // Found a valid spot
      }
      attempts++;
    }

    if (!foundSpot) {
        // Fallback: if no valid spot found after 100 attempts, just place it randomly.
        // This might still overlap, but prevents an infinite loop and keeps the game running.
        this.x = random(this.size / 2, width - this.size / 2);
        this.y = random(this.size / 2, height - this.size / 2);
    }
  }

  // Check collision with an Obstacle (circle-rectangle collision)
  collidesWith(obstacle) {
    let testX = this.x;
    let testY = this.y;

    if (this.x < obstacle.x) testX = obstacle.x;
    else if (this.x > obstacle.x + obstacle.w) testX = obstacle.x + obstacle.w;
    if (this.y < obstacle.y) testY = obstacle.y;
    else if (this.y > obstacle.y + obstacle.h) testY = obstacle.y + obstacle.h;

    let distX = this.x - testX;
    let distY = this.y - testY;
    let distance = sqrt((distX * distX) + (distY * distY));

    return distance <= this.size / 2;
  }

  // Draw the food
  show() {
    fill(255, 0, 0); // Red
    ellipse(this.x, this.y, this.size, this.size);
  }
}
Line-by-line explanation (9 lines)

🔧 Subcomponents:

while-loop Retry Until Clear Spot Found while (attempts < 100) {

Keeps picking new random coordinates up to 100 times until it finds a spot that doesn't overlap the snail, obstacles, or other items.

this.size = 20;
Every piece of food is a 20-pixel circle by default.
while (attempts < 100) {
Tries up to 100 random positions looking for one that is clear of everything else.
this.x = random(this.size / 2, width - this.size / 2);
Picks a random x position that keeps the whole circle inside the canvas (not just its center).
if (!isSpotOccupied(this.x, this.y, this.size, this)) {
Asks the global helper function whether this random spot overlaps anything; passing `this` means the food doesn't accidentally collide with itself.
foundSpot = true; break;
As soon as a clear spot is found, the loop stops early instead of wasting more attempts.
if (!foundSpot) { ... }
A safety fallback: if 100 tries all failed (very cluttered map), just place it randomly anyway rather than freezing the game.
let distance = sqrt((distX * distX) + (distY * distY));
Classic distance formula used to measure how far the food's center is from the closest edge of a rectangle obstacle.
return distance <= this.size / 2;
If that distance is smaller than the food's radius, the circle is overlapping the rectangle - a collision.
ellipse(this.x, this.y, this.size, this.size);
Draws the food as a filled circle at its stored position.

class Candy

Candy demonstrates inheritance with a twist: it calls `super.respawn()` to reuse the parent's positioning logic, then adds its own extra behavior (picking new random colors) on top.

🔬 This loop draws a 5-pointed star. What happens if you change the loop's `5` to `8` and also change `TWO_PI / 5` to `TWO_PI / 8`?

    for (let i = 0; i < 5; i++) {
      fill(i % 2 === 0 ? this.color1 : this.color2);
      rotate(TWO_PI / 5);
      triangle(0, -this.size / 2, -this.size / 4, 0, this.size / 4, 0);
    }
class Candy extends Food { // Candy inherits collidesWith and respawn logic from Food
  constructor() {
    super(); // Call Food's constructor
    this.size = 15; // Slightly smaller than food
    this.colors = [color(255, 100, 100), color(100, 255, 100), color(100, 100, 255), color(255, 255, 100)];
    this.color1 = random(this.colors);
    this.color2 = random(this.colors);
  }

  respawn() {
    super.respawn(); // Use Food's respawn logic
    this.color1 = random(this.colors); // Get new random colors
    this.color2 = random(this.colors);
  }

  show() {
    push();
    translate(this.x, this.y);
    rotate(frameCount * 0.05); // Spin the candy

    // Draw a star-like shape or a simple colorful circle
    noStroke();
    for (let i = 0; i < 5; i++) {
      fill(i % 2 === 0 ? this.color1 : this.color2);
      rotate(TWO_PI / 5);
      triangle(0, -this.size / 2, -this.size / 4, 0, this.size / 4, 0);
    }
    pop();
  }
}
Line-by-line explanation (7 lines)

🔧 Subcomponents:

for-loop Draw 5 Star Points for (let i = 0; i < 5; i++) {

Rotates around the candy's center 5 times, drawing a triangle each time, alternating between two colors to build a spinning star shape.

class Candy extends Food {
Candy reuses Food's respawn() and collidesWith() logic through inheritance, only overriding respawn() slightly and replacing show().
this.colors = [color(255, 100, 100), color(100, 255, 100), color(100, 100, 255), color(255, 255, 100)];
A palette of four candy colors to randomly choose from.
rotate(frameCount * 0.05); // Spin the candy
Because frameCount keeps increasing every frame, this constantly rotates the candy a little more each frame, making it spin continuously.
for (let i = 0; i < 5; i++) {
Loops 5 times to draw the 5 points of a star.
fill(i % 2 === 0 ? this.color1 : this.color2);
Alternates between two colors on each loop iteration using the modulo operator, creating a two-tone pattern.
rotate(TWO_PI / 5);
Rotates the drawing coordinate system by 1/5th of a full circle before each triangle, spacing the 5 points evenly.
triangle(0, -this.size / 2, -this.size / 4, 0, this.size / 4, 0);
Draws one pointed triangle shape relative to the (already translated and rotated) origin.

class Pickaxe

beginShape()/vertex()/endShape(CLOSE) lets you build custom polygons point-by-point, which is how the sketch draws the diamond-shaped pickaxe head instead of using a built-in shape function.

class Pickaxe extends Food { // Pickaxe also inherits collidesWith and respawn logic from Food
  constructor() {
    super(); // Call Food's constructor
    this.size = 25; // Size of the pickaxe item on the ground
  }

  // Draw the pickaxe item
  show() {
    push();
    translate(this.x, this.y);
    rotate(PI / 6); // Angle it slightly

    // Handle
    fill(150, 100, 50); // Brown handle
    rect(-10, -12, 20, 25);

    // Head
    fill(120); // Grey head
    beginShape();
    vertex(0, -18);
    vertex(-10, -5);
    vertex(-10, 5);
    vertex(0, 18);
    vertex(10, 5);
    vertex(10, -5);
    endShape(CLOSE);

    pop();
  }
}
Line-by-line explanation (3 lines)
class Pickaxe extends Food {
Reuses Food's placement and collision logic - only the visuals differ.
rotate(PI / 6); // Angle it slightly
Tilts the whole drawing by 30 degrees so the pickaxe looks like it's lying at an angle rather than perfectly upright.
beginShape(); ... endShape(CLOSE);
Builds a custom polygon (the axe head) point by point using vertex(), then CLOSE connects the last point back to the first to seal the shape.

class Key

Key is currently defined and drawn but never spawned or checked for collisions in the active game loop (the door/key puzzle was removed) - it's a good example of leftover code you might revive or clean up.

class Key extends Food {
  constructor(x, y) {
    super();
    this.x = x;
    this.y = y;
    this.size = 20; // Size of the key item
  }

  show() {
    push();
    translate(this.x, this.y);
    fill(255, 200, 0); // Yellow
    ellipse(0, 0, this.size * 0.7, this.size * 0.7); // Key head
    rect(this.size * 0.2, -this.size * 0.2, this.size * 0.3, this.size * 0.1); // Key shaft
    rect(this.size * 0.2, this.size * 0.1, this.size * 0.1, this.size * 0.1); // Key teeth 1
    rect(this.size * 0.4, -this.size * 0.1, this.size * 0.2, this.size * 0.1); // Key teeth 2
    pop();
  }
}
Line-by-line explanation (3 lines)
constructor(x, y) { super(); this.x = x; this.y = y; ... }
Unlike Food's other subclasses, Key takes a fixed x/y position at creation time (from the current map's data) instead of only being placed by respawn().
ellipse(0, 0, this.size * 0.7, this.size * 0.7); // Key head
Draws the round part of the key using a proportion of its overall size.
rect(...); // Key shaft / teeth
Three small rectangles build the key's shaft and teeth, all sized relative to `this.size` so the whole key scales together.

class Chest

Like Key, Chest is fully drawn but not currently spawned or checked in updateGame()/drawGame() - the door-and-chest puzzle it belonged to is commented out elsewhere in the file.

class Chest extends Food {
  constructor(x, y) {
    super();
    this.x = x;
    this.y = y;
    this.size = 30; // Size of the chest item
  }

  show() {
    push();
    translate(this.x, this.y);
    fill(139, 69, 19); // Brown chest body
    rect(-this.size / 2, -this.size / 2, this.size, this.size * 0.8);
    fill(255, 215, 0); // Gold band
    rect(-this.size / 2, -this.size * 0.1, this.size, this.size * 0.2);
    fill(100); // Lock
    ellipse(0, 0, this.size * 0.15, this.size * 0.15);
    pop();
  }
}
Line-by-line explanation (3 lines)
rect(-this.size / 2, -this.size / 2, this.size, this.size * 0.8);
Draws the main chest body centered on the translated origin.
fill(255, 215, 0); // Gold band ... rect(...)
Layers a thin gold rectangle over the middle of the chest to look like a metal band.
ellipse(0, 0, this.size * 0.15, this.size * 0.15);
A small circle in the very center represents the chest's lock.

class Snail

Snail is the most complex class in the sketch, combining p5.Vector movement, timer-based power-ups (using millis()), inventory flags (hasPickaxe/hasKey), and a switch-based multi-design renderer - a great single-class case study in how much behavior an object can hold.

🔬 This block makes the mouse take over steering the instant it moves at all. What happens if you wrap the whole block in `if (mouseIsPressed)` so the mouse only steers while the button is held down?

    if (mouseIsPressed || mouseX !== pmouseX || mouseY !== pmouseY) {
      let target = createVector(mouseX, mouseY); // Target position is mouse
      let steer = p5.Vector.sub(target, this.pos); // Calculate steering force
      steer.setMag(currentSpeed); // Set speed
      this.vel = steer; // Apply velocity
    }

🔬 The `break;` stops the loop after destroying just one obstacle. What happens if you remove that `break;` line - could a single pickaxe swing clear out every obstacle within range at once?

    for (let i = obstacles.length - 1; i >= 0; i--) {
      let obs = obstacles[i];
      // Simple distance check to the obstacle's center, could be improved for rect-circle interaction
      let obsCenterX = obs.x + obs.w / 2;
      let obsCenterY = obs.y + obs.h / 2;

      if (dist(this.pos.x, this.pos.y, obsCenterX, obsCenterY) < hitRange) {
        obstacles.splice(i, 1); // Remove the obstacle
        obstacleHit = true;
        break; // Only hit one obstacle per use
      }
    }
class Snail {
  constructor(x, y, designIndex = 0) {
    this.pos = createVector(x, y); // Position vector
    this.vel = createVector(0, 0); // Velocity vector
    this.originalSpeed = 3; // Base movement speed
    this.speed = this.originalSpeed; // Current movement speed
    this.size = 40; // Overall size of the snail
    this.designIndex = designIndex; // Avatar design index

    this.speedBoostActive = false;
    this.speedBoostTimer = 0;
    this.speedBoostDuration = 5000; // 5 seconds
    this.speedBoostMultiplier = 2; // Double speed

    this.hasPickaxe = false; // New property for pickaxe
    this.hasKey = false; // New property for key (kept for potential future use or score)
    this.permanentSpeedBoostMultiplier = 1.5; // Multiplier for permanent speed boost
  }

  move() {
    let currentSpeed = this.speed;
    if (permanentSpeedBoost) {
      currentSpeed = this.originalSpeed * this.permanentSpeedBoostMultiplier;
    }

    // Arrow key control
    if (keyIsDown(LEFT_ARROW)) {
      this.vel.x = -currentSpeed;
    } else if (keyIsDown(RIGHT_ARROW)) {
      this.vel.x = currentSpeed;
    } else {
      this.vel.x = 0;
    }

    if (keyIsDown(UP_ARROW)) {
      this.vel.y = -currentSpeed;
    } else if (keyIsDown(DOWN_ARROW)) {
      this.vel.y = currentSpeed;
    } else {
      this.vel.y = 0;
    }

    // Mouse control (overrides arrow keys if mouse is moved or clicked)
    if (mouseIsPressed || mouseX !== pmouseX || mouseY !== pmouseY) {
      let target = createVector(mouseX, mouseY); // Target position is mouse
      let steer = p5.Vector.sub(target, this.pos); // Calculate steering force
      steer.setMag(currentSpeed); // Set speed
      this.vel = steer; // Apply velocity
    }

    this.pos.add(this.vel); // Update position based on velocity
  }

  // Keep the snail within canvas boundaries
  checkBoundaries() {
    this.pos.x = constrain(this.pos.x, this.size / 2, width - this.size / 2);
    this.pos.y = constrain(this.pos.y, this.size / 2, height - this.size / 2);
  }

  activateSpeedBoost() {
    this.speedBoostActive = true;
    this.speedBoostTimer = millis() + this.speedBoostDuration;
    this.speed = this.originalSpeed * this.speedBoostMultiplier;
  }

  updateSpeedBoost() {
    if (this.speedBoostActive && millis() > this.speedBoostTimer) {
      this.speedBoostActive = false;
      this.speed = this.originalSpeed; // Revert to original speed
    }
  }

  // Use the pickaxe to remove a nearby obstacle
  usePickaxe() {
    if (!this.hasPickaxe) return; // Can't use if no pickaxe

    let hitRange = this.size * 1.5; // How close the snail needs to be to hit an obstacle
    let obstacleHit = false;

    // Check for nearby obstacles
    for (let i = obstacles.length - 1; i >= 0; i--) {
      let obs = obstacles[i];
      // Simple distance check to the obstacle's center, could be improved for rect-circle interaction
      let obsCenterX = obs.x + obs.w / 2;
      let obsCenterY = obs.y + obs.h / 2;

      if (dist(this.pos.x, this.pos.y, obsCenterX, obsCenterY) < hitRange) {
        obstacles.splice(i, 1); // Remove the obstacle
        obstacleHit = true;
        break; // Only hit one obstacle per use
      }
    }

    if (obstacleHit) {
      this.hasPickaxe = false; // Pickaxe is consumed
      console.log("Obstacle removed!");
    } else {
      console.log("No obstacle found within pickaxe range.");
    }
  }

  // Draw the snail based on designIndex
  show() {
    push();
    translate(this.pos.x, this.pos.y);

    switch (this.designIndex) {
      case 0: // Default Snail
        // (drawing code for brown/green snail with ellipses, arcs)
        break;
      case 1: // Blue Snail
        // (drawing code for blue snail variant)
        break;
      case 2: // Striped Snail
        // (drawing code for striped snail variant)
        break;
    }

    // Draw pickaxe icon if snail has it
    if (this.hasPickaxe) { /* ... */ }

    // Draw key icon if snail has it
    if (this.hasKey) { /* ... */ }

    pop();
  }

  // Check collision with an Obstacle (circle-rectangle collision)
  collidesWith(obstacle) {
    let testX = this.pos.x;
    let testY = this.pos.y;

    // Which edge is closest?
    if (this.pos.x < obstacle.x) testX = obstacle.x;
    else if (this.pos.x > obstacle.x + obstacle.w) testX = obstacle.x + obstacle.w;
    if (this.pos.y < obstacle.y) testY = obstacle.y;
    else if (this.pos.y > obstacle.y + obstacle.h) testY = obstacle.y + obstacle.h;

    // Calculate the distance from closest edge
    let distX = this.pos.x - testX;
    let distY = this.pos.y - testY;
    let distance = sqrt((distX * distX) + (distY * distY));

    // If the distance is less than the snail's radius, they are colliding
    return distance <= this.size / 2;
  }
}
Line-by-line explanation (15 lines)

🔧 Subcomponents:

conditional Arrow Key Movement if (keyIsDown(LEFT_ARROW)) { this.vel.x = -currentSpeed; }

Sets horizontal/vertical velocity based on which arrow keys are currently held down.

conditional Mouse Steering Override if (mouseIsPressed || mouseX !== pmouseX || mouseY !== pmouseY) {

Whenever the mouse moves or is clicked, it overrides arrow-key velocity by steering the snail directly toward the cursor using vector subtraction.

for-loop Find Nearby Obstacle to Destroy for (let i = obstacles.length - 1; i >= 0; i--) {

Loops backward through obstacles (safe for removing items mid-loop) looking for the first one within pickaxe range to delete.

switch-case Choose Avatar Drawing switch (this.designIndex) {

Picks which of the three hand-drawn snail appearances to render based on the player's chosen skin.

this.pos = createVector(x, y); // Position vector
Uses a p5.Vector to store x/y together as one object, which makes movement math (adding velocity, steering) much simpler than tracking two separate numbers.
let currentSpeed = this.speed;
Starts each frame's movement calculation from the snail's current speed (normal or candy-boosted).
if (permanentSpeedBoost) { currentSpeed = this.originalSpeed * this.permanentSpeedBoostMultiplier; }
If the admin cheat is enabled, it overrides the speed calculation entirely with a permanent multiplier.
if (keyIsDown(LEFT_ARROW)) { this.vel.x = -currentSpeed; }
Checks in real time whether the left arrow key is held, and if so sets negative horizontal velocity to move left.
if (mouseIsPressed || mouseX !== pmouseX || mouseY !== pmouseY) {
Detects any mouse activity (click or movement) by comparing the current mouse position to its previous-frame position (pmouseX/pmouseY).
let steer = p5.Vector.sub(target, this.pos);
Subtracting the snail's position from the mouse's position gives a vector pointing from the snail toward the mouse.
steer.setMag(currentSpeed);
Resizes that direction vector so its length equals the snail's speed, regardless of how far away the mouse is - this keeps movement speed constant.
this.pos.add(this.vel); // Update position based on velocity
Actually moves the snail by adding its velocity vector to its position vector, once per frame.
this.pos.x = constrain(this.pos.x, this.size / 2, width - this.size / 2);
Clamps the snail's x position so it can never leave the canvas, leaving room for half its own size.
this.speedBoostTimer = millis() + this.speedBoostDuration;
Records the exact future timestamp (current time plus boost duration) when the speed boost should expire.
if (this.speedBoostActive && millis() > this.speedBoostTimer) {
Every frame, checks whether real time has passed the stored expiry timestamp to know when to turn the boost off.
if (!this.hasPickaxe) return;
Guard clause - immediately exits the function if the snail doesn't currently hold a pickaxe, so nothing else in the function runs.
if (dist(this.pos.x, this.pos.y, obsCenterX, obsCenterY) < hitRange) {
Uses p5's built-in dist() to measure distance to each obstacle's center and compares it to the pickaxe's reach.
obstacles.splice(i, 1);
Removes exactly one obstacle from the array at index i, permanently deleting it from the map.
return distance <= this.size / 2;
The same circle-vs-rectangle collision test used elsewhere in the game: true if the snail's circular body overlaps the obstacle's rectangle.

preload()

preload() runs once before setup() and is the right place to load data (files, saved state) that setup() or draw() will depend on immediately.

function preload() {
  // Define maps *before* loading game progress so maps.length is correct
  defineMaps();
  // Try to load game progress from localStorage before setup
  loadGameProgress();
}
Line-by-line explanation (2 lines)
defineMaps();
Builds the `maps` array first, so its length is known before anything tries to read saved progress against it.
loadGameProgress();
Reads any previously saved unlocked-maps count and chosen snail skin from the browser's localStorage.

setup()

setup() runs exactly once. Here it's kept minimal - just sizing the canvas and delegating all real initialization to the reusable startGame() function, which can also be called again later (e.g., from windowResized()).

function setup() {
  createCanvas(windowWidth, windowHeight);
  startGame(); // Start the game with the current map
}
Line-by-line explanation (2 lines)
createCanvas(windowWidth, windowHeight);
Makes the canvas fill the entire browser window, so the game always matches the viewer's screen size.
startGame();
Immediately initializes the snail, food, obstacles, candies and pickaxe and sets gameState to 'play' - though the player actually first sees the start menu because mousePressed()/keyPressed() haven't changed gameState yet... in this sketch startGame() runs right away, so the game begins in 'play' state until the player later chooses 'start' from a menu click flow.

defineMaps()

Storing level data as an array of objects (rather than hard-coding each map's logic separately) is a common pattern that makes it easy to add a 7th map just by appending one more object to the array.

function defineMaps() {
  maps = [
    {
      name: "Map 1: Grassy Plains",
      targetScore: 5, // Score needed to unlock Map 2
      obstacles: [
        { x: 0.2, y: 0.3, w: 100, h: 50 },
        { x: 0.7, y: 0.6, w: 80, h: 80 },
        { x: 0.5, y: 0.1, w: 150, h: 30 },
      ],
      key: { x: 0.2, y: 0.2 },
    },
    {
      name: "Map 2: Rocky Foothills",
      targetScore: 10,
      obstacles: [
        { x: 0.1, y: 0.2, w: 120, h: 60 },
        { x: 0.4, y: 0.5, w: 70, h: 100 },
        { x: 0.8, y: 0.3, w: 90, h: 40 },
        { x: 0.6, y: 0.8, w: 110, h: 80 },
      ]
    },
    {
      name: "Map 3: Sandy Desert",
      targetScore: 15,
      obstacles: [
        { x: 0.3, y: 0.1, w: 180, h: 40 },
        { x: 0.6, y: 0.4, w: 50, h: 150 },
        { x: 0.2, y: 0.7, w: 100, h: 100 },
        { x: 0.8, y: 0.9, w: 130, h: 60 },
      ]
    },
    {
      name: "Map 4: Misty Forest",
      targetScore: 20,
      obstacles: [
        { x: 0.1, y: 0.1, w: 100, h: 100 },
        { x: 0.9, y: 0.1, w: 100, h: 100 },
        { x: 0.1, y: 0.9, w: 100, h: 100 },
        { x: 0.9, y: 0.9, w: 100, h: 100 },
        { x: 0.5, y: 0.5, w: 150, h: 150 },
      ]
    },
    {
      name: "Map 5: Icy Tundra",
      targetScore: Infinity,
      obstacles: [
        { x: 0.15, y: 0.15, w: 80, h: 80 },
        { x: 0.85, y: 0.15, w: 80, h: 80 },
        { x: 0.15, y: 0.85, w: 80, h: 80 },
        { x: 0.85, y: 0.85, w: 80, h: 80 },
        { x: 0.4, y: 0.4, w: 60, h: 60 },
        { x: 0.6, y: 0.6, w: 60, h: 60 },
        { x: 0.4, y: 0.6, w: 60, h: 60 },
        { x: 0.6, y: 0.4, w: 60, h: 60 },
      ]
    },
    {
      name: "Map 6: Asteroid Field",
      targetScore: 25,
      obstacles: [
        { x: 0.2, y: 0.2, w: 70, h: 70 },
        { x: 0.8, y: 0.2, w: 70, h: 70 },
        { x: 0.2, y: 0.8, w: 70, h: 70 },
        { x: 0.8, y: 0.8, w: 70, h: 70 },
        { x: 0.5, y: 0.5, w: 100, h: 100 },
        { x: 0.3, y: 0.4, w: 40, h: 120 },
        { x: 0.7, y: 0.6, w: 120, h: 40 },
      ]
    }
  ];

  // Ensure unlockedMapsCount doesn't exceed the number of defined maps
  unlockedMapsCount = constrain(unlockedMapsCount, 1, maps.length);
}
Line-by-line explanation (4 lines)
maps = [ { name: ..., targetScore: ..., obstacles: [...] }, ... ];
Builds an array of plain JavaScript objects, one per map, each holding a display name, the score needed to unlock the next map, and a list of obstacle definitions.
{ x: 0.2, y: 0.3, w: 100, h: 50 }
Obstacle x/y are stored as fractions (0.0 to 1.0) of the canvas size rather than fixed pixels, so obstacles reposition correctly if the window is resized.
targetScore: Infinity, // Last map, no more to unlock
Using JavaScript's built-in Infinity value means this map's score target can never actually be reached, effectively marking it as the final map.
unlockedMapsCount = constrain(unlockedMapsCount, 1, maps.length);
Safety check to make sure the unlocked-maps counter never goes below 1 or above however many maps actually exist (protects against corrupted save data).

draw()

This switch statement is the heart of the sketch's state machine: exactly one gameState string decides which whole screen appears, which is a simple but powerful pattern for organizing any multi-screen p5.js project.

🔬 This case is the only one that calls updateGame(). What happens if you comment out the updateGame(); call but leave drawGame(); - would the snail still respond to your arrow keys?

    case 'play':
      updateGame();
      drawGame();
      break;
function draw() {
  background(220); // Clear background each frame

  // Handle game logic based on the current state
  switch (gameState) {
    case 'start':
      drawStartScreen();
      break;
    case 'mapSelect':
      drawMapSelectScreen();
      break;
    case 'instructions':
      drawInstructionsScreen();
      break;
    case 'shop': // New shop state
      drawShopScreen();
      break;
    case 'admin': // New admin state
      drawAdminScreen();
      break;
    case 'play':
      updateGame();
      drawGame();
      break;
    case 'mapUnlocked':
      drawMapUnlockedScreen();
      break;
    case 'gameOver':
      drawGameOverScreen();
      break;
  }
}
Line-by-line explanation (3 lines)

🔧 Subcomponents:

switch-case Start Menu case 'start': drawStartScreen(); break;

Shows the main menu when gameState is 'start'.

switch-case Active Gameplay case 'play': updateGame(); drawGame(); break;

The only case that both updates game logic and draws the playable scene every frame.

switch-case Game Over Screen case 'gameOver': drawGameOverScreen(); break;

Shows the final score and restart instructions after a collision ends the game.

background(220); // Clear background each frame
Repaints the entire canvas light grey at the start of every frame, erasing whatever was drawn last frame so nothing leaves a trail.
switch (gameState) {
Looks at the single global gameState string and runs only the code block matching its current value - this is the entire game's screen-switching logic in one place.
case 'play': updateGame(); drawGame(); break;
Only while actually playing does the game update positions/collisions (updateGame) and then render everything (drawGame); every other state just draws a static-ish menu.

startGame()

startGame() is called from multiple places (setup, selecting a map, restarting after game over, and windowResized) - centralizing all initialization in one reusable function avoids duplicating this setup logic everywhere it's needed.

function startGame() {
  score = 0;
  snail = new Snail(width / 2, height / 2, currentSnailDesign); // Create snail at center with chosen design
  food = new Food(); // Create initial food
  food.respawn(); // Ensure food doesn't spawn on an obstacle or the snail

  // Load obstacles for the current map based on currentMapIndex
  obstacles = [];
  if (maps[currentMapIndex]) {
    for (let obsData of maps[currentMapIndex].obstacles) {
      // Convert relative coordinates to absolute based on current canvas size
      obstacles.push(new Obstacle(width * obsData.x, height * obsData.y, obsData.w, obsData.h));
    }
    // Initialize key if defined for the current map
    if (maps[currentMapIndex].key) {
      keyItem = new Key(width * maps[currentMapIndex].key.x, height * maps[currentMapIndex].key.y);
      keyItem.respawn(); // Ensure it doesn't overlap
    } else {
      keyItem = null; // No key for this map
    }
  }

  // Initialize candies
  candies = [];
  // Spawn a few candies initially, less frequent than food
  for(let i = 0; i < 3; i++) { // You can adjust the number of candies
    candies.push(new Candy());
    candies[i].respawn();
  }

  // Initialize pickaxe
  pickaxe = new Pickaxe();
  pickaxe.respawn(); // Spawn the pickaxe on the map

  snail.hasKey = false; // Snail starts without a key
  snail.hasPickaxe = false; // Snail starts without a pickaxe

  gameState = 'play'; // Set game state to play
}
Line-by-line explanation (7 lines)

🔧 Subcomponents:

for-loop Build Obstacles From Map Data for (let obsData of maps[currentMapIndex].obstacles) {

Converts each map's relative (0-1) obstacle coordinates into real pixel positions for the current canvas size and creates Obstacle objects.

for-loop Spawn Starting Candies for(let i = 0; i < 3; i++) { // You can adjust the number of candies

Creates exactly 3 Candy objects and places each at a valid random position.

score = 0;
Resets the score every time a map is (re)started.
snail = new Snail(width / 2, height / 2, currentSnailDesign);
Creates a brand-new Snail object centered on the canvas, using whichever skin the player picked in the shop.
obstacles = [];
Clears out any obstacles from a previous map before loading the new ones.
obstacles.push(new Obstacle(width * obsData.x, height * obsData.y, obsData.w, obsData.h));
Multiplies the map's stored 0-1 fractions by the actual canvas width/height to get real pixel coordinates, then creates the Obstacle there.
for(let i = 0; i < 3; i++) { candies.push(new Candy()); candies[i].respawn(); }
Creates 3 candies and immediately gives each one a random valid position via respawn().
pickaxe = new Pickaxe(); pickaxe.respawn();
Creates the single pickaxe item for this map and places it somewhere that doesn't overlap anything else.
gameState = 'play'; // Set game state to play
The final line switches the game into active play mode so draw()'s switch statement starts running updateGame()/drawGame().

isSpotOccupied()

isSpotOccupied() is the shared 'is this spot safe?' function used by every item's respawn() method, showing how a single reusable helper function can serve many different classes.

🔬 This loop checks every single obstacle every time any item tries to respawn. On a map with hundreds of obstacles, what do you think would happen to performance, and how could you speed it up (hint: only check obstacles near x,y)?

  // Check obstacles
  for (let obs of obstacles) {
    if (!obs) continue; // Skip if null
    let testX = x;
    let testY = y;
function isSpotOccupied(x, y, size, excludeItem = null) {
  // Check snail
  if (dist(x, y, snail.pos.x, snail.pos.y) < size / 2 + snail.size / 2) {
    return true;
  }

  // Check obstacles
  for (let obs of obstacles) {
    if (!obs) continue; // Skip if null
    let testX = x;
    let testY = y;
    if (x < obs.x) testX = obs.x;
    else if (x > obs.x + obs.w) testX = obs.x + obs.w;
    if (y < obs.y) testY = obs.y;
    else if (y > obs.y + obs.h) testY = obs.y + obs.h;
    let distX = x - testX;
    let distY = y - testY;
    let distance = sqrt((distX * distX) + (distY * distY));
    if (distance <= size / 2) {
      return true;
    }
  }

  // Check candies
  for (let candy of candies) {
    if (candy === excludeItem) continue;
    if (dist(x, y, candy.x, candy.y) < size / 2 + candy.size / 2) {
      return true;
    }
  }

  // Check pickaxe
  if (pickaxe && pickaxe !== excludeItem) {
    if (dist(x, y, pickaxe.x, pickaxe.y) < size / 2 + pickaxe.size / 2) {
      return true;
    }
  }

  // Check keyItem
  if (keyItem && keyItem !== excludeItem) {
    if (dist(x, y, keyItem.x, keyItem.y) < size / 2 + keyItem.size / 2) {
      return true;
    }
  }

  return false;
}
Line-by-line explanation (4 lines)

🔧 Subcomponents:

for-loop Check Every Obstacle for (let obs of obstacles) {

Runs the circle-rectangle distance test against each obstacle in turn, returning true the moment any one of them overlaps the candidate spot.

for-loop Check Every Candy for (let candy of candies) {

Makes sure a new item doesn't spawn on top of an existing candy (unless that candy is the item being excluded, e.g. respawning itself).

function isSpotOccupied(x, y, size, excludeItem = null) {
Takes a candidate location and size, plus an optional item to ignore (so an object doesn't collide with its own old position while respawning).
if (dist(x, y, snail.pos.x, snail.pos.y) < size / 2 + snail.size / 2) { return true; }
Adding both radii together and comparing to the distance between centers is the standard circle-circle overlap test.
if (candy === excludeItem) continue;
Skips checking against the very item that's currently trying to respawn, otherwise it would always think its old spot is occupied by itself.
return false;
If none of the checks above found an overlap, the spot is confirmed clear and safe to use.

updateGame()

updateGame() is where all the actual gameplay rules live - it never draws anything itself, only reads input, checks distances, and changes state variables like score and gameState, which drawGame() then visualizes.

🔬 This whole block is skipped entirely when godMode is true. What happens if you delete the `if (!godMode) {` wrapper (and its closing brace) so obstacles always end the game, even with God Mode on in the admin panel?

  if (!godMode) {
    for (let obs of obstacles) {
      if (snail.collidesWith(obs)) {
        gameState = 'gameOver'; // End game on collision
        break; // No need to check other obstacles
      }
    }
  }
function updateGame() {
  snail.move(); // Move the snail
  snail.checkBoundaries(); // Keep snail within canvas

  // Check collision between snail and food
  if (dist(snail.pos.x, snail.pos.y, food.x, food.y) < snail.size / 2 + food.size / 2) {
    score++; // Increase score
    food.respawn(); // Move food to a new random location

    // Check if score is enough to unlock the next map
    if (currentMapIndex < maps.length - 1 && score >= maps[currentMapIndex].targetScore) {
      unlockNextMap(); // Unlock the next map
      gameState = 'mapUnlocked'; // Transition to map unlocked screen
      return; // Skip further game updates this frame
    }
  }

  // Check collision between snail and obstacles
  // Only end game if not in God Mode
  if (!godMode) {
    for (let obs of obstacles) {
      if (snail.collidesWith(obs)) {
        gameState = 'gameOver'; // End game on collision
        break; // No need to check other obstacles
      }
    }
  }

  // Check collision between snail and candies
  for (let i = candies.length - 1; i >= 0; i--) {
    let candy = candies[i];
    if (dist(snail.pos.x, snail.pos.y, candy.x, candy.y) < snail.size / 2 + candy.size / 2) {
      candies.splice(i, 1); // Remove collected candy
      candy.respawn(); // Respawn a new candy
      candies.push(candy); // Add it back to the array

      snail.activateSpeedBoost(); // Activate speed boost
      break; // Only collect one candy per frame
    }
  }

  // Check collision between snail and pickaxe
  if (pickaxe && !snail.hasPickaxe && dist(snail.pos.x, snail.pos.y, pickaxe.x, pickaxe.y) < snail.size / 2 + pickaxe.size / 2) {
    snail.hasPickaxe = true; // Snail picks up pickaxe
    pickaxe.respawn(); // Respawn the pickaxe immediately
    console.log("Snail picked up a pickaxe!");
  }

  // Check collision between snail and key (if it exists)
  if (keyItem && dist(snail.pos.x, snail.pos.y, keyItem.x, keyItem.y) < snail.size / 2 + keyItem.size / 2) {
    snail.hasKey = true; // Snail picks up key
    keyItem.respawn(); // Key is consumed and respawns
    console.log("Snail picked up a key!");
  }

  // Update speed boost timer
  snail.updateSpeedBoost();
}
Line-by-line explanation (7 lines)

🔧 Subcomponents:

conditional Food Collection & Map Unlock if (dist(snail.pos.x, snail.pos.y, food.x, food.y) < snail.size / 2 + food.size / 2) {

Increments score and respawns food when eaten, and checks whether that new score is enough to unlock the next map.

for-loop Obstacle Collision (unless God Mode) for (let obs of obstacles) {

Ends the game the moment the snail touches any obstacle, unless godMode is currently enabled.

for-loop Candy Pickup for (let i = candies.length - 1; i >= 0; i--) {

Detects when the snail touches a candy, removes and instantly respawns it elsewhere, and activates the speed boost.

snail.move(); // Move the snail
Delegates all velocity/steering calculations to the Snail class's own move() method.
if (dist(snail.pos.x, snail.pos.y, food.x, food.y) < snail.size / 2 + food.size / 2) {
A simple circle-circle collision check: adds both radii and compares to the actual distance between the snail and the food.
if (currentMapIndex < maps.length - 1 && score >= maps[currentMapIndex].targetScore) {
Only tries to unlock a next map if one exists (not already on the last map) and the score requirement for the current map has been reached.
if (!godMode) { for (let obs of obstacles) { if (snail.collidesWith(obs)) { gameState = 'gameOver'; break; } } }
The entire obstacle-collision check is skipped when godMode is true, making the snail invincible - a classic cheat-toggle pattern.
candies.splice(i, 1); ... candies.push(candy);
Removes the eaten candy from its old array position and pushes the same object back in after giving it a new random position - reusing the object instead of creating a new one.
snail.activateSpeedBoost();
Tells the Snail object to start its temporary speed boost timer.
snail.updateSpeedBoost();
Called every frame at the end to check whether an active speed boost has expired and should be turned off.

drawGame()

drawGame() is purely visual - it never changes game variables, only reads them and draws shapes/text. Keeping update logic (updateGame) and drawing logic (drawGame) separate is a common, easy-to-debug pattern.

function drawGame() {
  // Draw food
  food.show();

  // Draw obstacles
  for (let obs of obstacles) {
    obs.show();
  }

  // Draw candies
  for (let candy of candies) {
    candy.show();
  }

  // Draw pickaxe if not held by snail
  if (pickaxe && !snail.hasPickaxe) {
    pickaxe.show();
  }

  // Draw key if it exists
  if (keyItem) {
    keyItem.show();
  }

  // Draw snail
  snail.show();

  // Display current score
  fill(0);
  textSize(24);
  textAlign(LEFT);
  text("Score: " + score, 10, 30);

  // Display current map name
  textAlign(RIGHT);
  text(maps[currentMapIndex].name, width - 10, 30);

  // Display speed boost timer
  if (snail.speedBoostActive) {
    fill(0, 150, 0); // Green text
    textSize(20);
    textAlign(CENTER);
    let timeRemaining = ceil((snail.speedBoostTimer - millis()) / 1000);
    text("SPEED BOOST! " + timeRemaining + "s", width / 2, height - 20);
  }

  // Display God Mode status
  if (godMode) {
    fill(255, 0, 0); // Red text
    textSize(20);
    textAlign(CENTER);
    text("GOD MODE!", width / 2, height - 45);
  }

  // Display Permanent Speed Boost status
  if (permanentSpeedBoost) {
    fill(0, 0, 255); // Blue text
    textSize(20);
    textAlign(CENTER);
    text("PERMA BOOST!", width / 2, height - 70);
  }
}
Line-by-line explanation (4 lines)

🔧 Subcomponents:

for-loop Draw All Obstacles for (let obs of obstacles) { obs.show(); }

Calls each obstacle's own show() method so every one gets drawn.

conditional Speed Boost HUD Text if (snail.speedBoostActive) {

Displays a countdown of remaining speed-boost seconds only while the boost is active.

food.show(); / obs.show(); / candy.show(); / snail.show();
Every game object knows how to draw itself - drawGame() simply calls show() on each one in the right order (background items first, snail last, so it's drawn on top).
if (pickaxe && !snail.hasPickaxe) { pickaxe.show(); }
Only draws the pickaxe lying on the ground if it exists AND the snail hasn't already picked it up - once collected, it moves onto the snail's body icon instead.
text("Score: " + score, 10, 30);
String concatenation combines the label with the current numeric score to build the HUD text in the top-left corner.
let timeRemaining = ceil((snail.speedBoostTimer - millis()) / 1000);
Converts the stored millisecond expiry timestamp into a whole number of seconds remaining, rounding up with ceil() so it doesn't show '0s' too early.

drawStartScreen()

This function shows a reusable layout pattern: define shared spacing variables once (startY, buttonHeight, spacing, buttonWidth), then use push()/translate()/pop() to draw each button relative to its own center instead of recalculating absolute coordinates every time.

function drawStartScreen() {
  fill(0);
  textSize(48);
  textAlign(CENTER, CENTER);
  text("Snail Game", width / 2, height / 2 - 160);

  let startY = height / 2 - 80;
  let buttonHeight = 50;
  let spacing = 15;
  let buttonWidth = 300;

  // Start Game Button
  push();
  translate(width / 2, startY + buttonHeight / 2);
  fill(50, 150, 50); // Green
  noStroke();
  rect(-buttonWidth / 2, -buttonHeight / 2, buttonWidth, buttonHeight, 10);
  fill(255);
  textSize(32);
  text("Start Game", 0, 0);
  pop();

  // (How to Play, Avatar Shop, Admin Panel, and Reset Progress buttons follow the same push/translate/rect/text/pop pattern at increasing Y offsets)
}
Line-by-line explanation (4 lines)

🔧 Subcomponents:

calculation Button Position Math let startY = height / 2 - 80;

Computes shared spacing values (startY, buttonHeight, spacing, buttonWidth) that every button on this screen reuses to stay evenly stacked and centered.

textAlign(CENTER, CENTER);
Makes all subsequent text() calls center both horizontally and vertically on the given coordinate, which is why button labels look neatly centered inside their rectangles.
push(); translate(width / 2, startY + buttonHeight / 2); ... pop();
Temporarily moves the coordinate system's origin to each button's center so the rectangle and its label can be drawn using simple 0,0-relative coordinates, then restores the original coordinate system with pop().
rect(-buttonWidth / 2, -buttonHeight / 2, buttonWidth, buttonHeight, 10);
Draws a rounded rectangle (the last argument, 10, is the corner radius) centered on the translated origin.
text("Start Game", 0, 0);
Draws the button's label at the translated origin, which is now the button's own center.

drawMapSelectScreen()

Generating one button per array element inside a for-loop (rather than writing separate code for each map) is a key technique for making UI scale automatically as data grows.

🔬 What happens if you swap the ternary's values so locked maps are colored 50 and unlocked maps are 150 - would that make locked maps look 'more available' by mistake?

    fill(isUnlocked ? 50 : 150); // Green for unlocked, grey for locked
    noStroke();
    rect(-buttonWidth / 2, -buttonHeight / 2, buttonWidth, buttonHeight, 10); // Rounded rectangle
function drawMapSelectScreen() {
  fill(0);
  textSize(40);
  textAlign(CENTER, CENTER);
  text("Select a Map", width / 2, height * 0.2);

  let startY = height * 0.3;
  let buttonHeight = 50;
  let spacing = 15;
  let buttonWidth = 400; // Fixed width for map buttons

  for (let i = 0; i < maps.length; i++) {
    let mapName = maps[i].name;
    let targetScore = maps[i].targetScore;
    let buttonY = startY + i * (buttonHeight + spacing);
    let isUnlocked = i < unlockedMapsCount;

    push();
    translate(width / 2, buttonY + buttonHeight / 2); // Center buttons

    fill(isUnlocked ? 50 : 150); // Green for unlocked, grey for locked
    noStroke();
    rect(-buttonWidth / 2, -buttonHeight / 2, buttonWidth, buttonHeight, 10); // Rounded rectangle

    fill(255); // White text
    textSize(isUnlocked ? 28 : 24);
    text(mapName, 0, -5); // Adjusted Y for vertical centering

    if (!isUnlocked) {
      textSize(18);
      fill(220); // Lighter text for locked message
      text("Locked - Score " + targetScore + " on previous map", 0, 15);
    }
    pop();
  }
}
Line-by-line explanation (3 lines)

🔧 Subcomponents:

for-loop Draw One Button Per Map for (let i = 0; i < maps.length; i++) {

Generates a button for every map in the array automatically, so adding a 7th map to defineMaps() creates a new button here for free.

let isUnlocked = i < unlockedMapsCount;
A map is unlocked if its index is smaller than the count of unlocked maps - so if unlockedMapsCount is 3, maps 0, 1, and 2 are unlocked.
fill(isUnlocked ? 50 : 150); // Green for unlocked, grey for locked
Uses a ternary operator to pick between two fill values in one line depending on whether the map is unlocked.
if (!isUnlocked) { ... text("Locked - Score " + targetScore + " on previous map", 0, 15); }
Only locked maps get an extra line of text explaining what score is needed on the previous map to unlock this one.

drawInstructionsScreen()

This screen is pure text and one button - a good demonstration of how much of a p5.js UI is just carefully positioned text() and rect() calls rather than anything more exotic.

function drawInstructionsScreen() {
  fill(0);
  textSize(40);
  textAlign(CENTER, CENTER);
  text("How to Play", width / 2, height * 0.2);

  textSize(24);
  textAlign(LEFT);
  text("Goal: Collect red food items to increase your score.", width * 0.1, height * 0.3);
  text("Avoid grey obstacles \u2013 bumping into one ends the game!", width * 0.1, height * 0.3 + 30);
  text("Reach the target score on each map to unlock the next challenge.", width * 0.1, height * 0.3 + 60);

  text("Controls:", width * 0.1, height * 0.45);
  text("- **Arrow Keys:** Move the snail up, down, left, or right.", width * 0.1, height * 0.45 + 30);
  text("- **Mouse:** Move your mouse to guide the snail directly.", width * 0.1, height * 0.45 + 60);
  text("(Mouse control overrides arrow key control)", width * 0.1, height * 0.45 + 90);

  text("Special Items:", width * 0.1, height * 0.6);
  text("- **Candies:** Collect colorful candies for a temporary speed boost!", width * 0.1, height * 0.6 + 30);
  text("- **Pickaxe:** Pick up the pickaxe icon. Press **'E'** to remove nearby obstacles!", width * 0.1, height * 0.6 + 60);

  // Back to Menu Button
  let buttonY = height * 0.8;
  let buttonHeight = 50;
  let buttonWidth = 300;
  push();
  translate(width / 2, buttonY + buttonHeight / 2);
  fill(50, 150, 50); // Green
  noStroke();
  rect(-buttonWidth / 2, -buttonHeight / 2, buttonWidth, buttonHeight, 10);
  fill(255);
  textSize(32);
  text("Back to Menu", 0, 0);
  pop();
}
Line-by-line explanation (3 lines)
textAlign(LEFT);
Switches text alignment back to left-aligned for the instructional paragraphs, since the title above uses CENTER alignment.
text("...", width * 0.1, height * 0.3 + 30);
Each line of instructions is positioned using a fraction of the canvas width/height plus a fixed pixel offset, so the text block stays roughly proportional on different screen sizes.
// Back to Menu Button ... pop();
Reuses the same push/translate/rect/text/pop button pattern seen in drawStartScreen() to return to the main menu.

drawShopScreen()

drawShopScreen() reuses drawSnailAvatar() rather than duplicating the snail-drawing code - a good example of factoring out shared visuals into their own helper function so both the shop and (potentially) other screens can use it.

function drawShopScreen() {
  fill(0);
  textSize(40);
  textAlign(CENTER, CENTER);
  text("Avatar Shop", width / 2, height * 0.2);

  let avatarSize = 100;
  let spacing = 50;
  let startX = width / 2 - (avatarSize * 3 + spacing * 2) / 2 + avatarSize / 2;
  let avatarY = height * 0.5;

  // Draw each avatar option
  for (let i = 0; i < 3; i++) { // Assuming 3 designs (0, 1, 2)
    let x = startX + i * (avatarSize + spacing);
    let y = avatarY;

    // Highlight if this is the current design
    if (i === currentSnailDesign) {
      noFill();
      stroke(255, 200, 0); // Gold border
      strokeWeight(4);
      ellipse(x, y, avatarSize + 10, avatarSize + 10);
    }

    // Draw the snail avatar
    push();
    translate(x, y);
    drawSnailAvatar(0, 0, avatarSize * 0.8, i); // Draw smaller version of snail
    pop();
  }

  // Back to Menu Button
  let buttonY = height * 0.8;
  let buttonHeight = 50;
  let buttonWidth = 300;
  push();
  translate(width / 2, buttonY + buttonHeight / 2);
  fill(50, 150, 50); // Green
  noStroke();
  rect(-buttonWidth / 2, -buttonHeight / 2, buttonWidth, buttonHeight, 10);
  fill(255);
  textSize(32);
  text("Back to Menu", 0, 0);
  pop();
}
Line-by-line explanation (3 lines)

🔧 Subcomponents:

for-loop Draw the 3 Avatar Choices for (let i = 0; i < 3; i++) { // Assuming 3 designs (0, 1, 2)

Draws each of the three snail skin options side by side, adding a gold ring around whichever one is currently selected.

let startX = width / 2 - (avatarSize * 3 + spacing * 2) / 2 + avatarSize / 2;
Calculates the x-position of the first avatar so that all 3 avatars plus their spacing end up perfectly centered as a group on screen.
if (i === currentSnailDesign) { ... ellipse(x, y, avatarSize + 10, avatarSize + 10); }
Draws an outlined circle slightly larger than the avatar itself around whichever design matches the player's currently saved choice, to show it's selected.
drawSnailAvatar(0, 0, avatarSize * 0.8, i);
Calls the shared helper function to draw a scaled-down preview of snail design `i`, reusing the exact same drawing code used for the in-game snail.

drawSnailAvatar()

This function is almost identical to Snail.show()'s drawing code, but extracted as a standalone function so it can be called both for the small shop previews and (via the same math, scaled up) for the full-size in-game snail - notice how the Snail class's own show() method duplicates this logic rather than calling drawSnailAvatar(), which is an opportunity to reduce repeated code.

function drawSnailAvatar(x, y, size, designIndex) {
  push();
  translate(x, y);

  switch (designIndex) {
    case 0: // Default Snail
      // (draws brown/green snail scaled to `size`)
      break;
    case 1: // Blue Snail
      // (draws blue snail scaled to `size`)
      break;
    case 2: // Striped Snail
      // (draws striped snail scaled to `size`)
      break;
  }

  pop();
}
Line-by-line explanation (2 lines)

🔧 Subcomponents:

switch-case Pick Which Design to Draw switch (designIndex) {

Selects one of three sets of ellipse/rect/arc drawing calls based on which snail skin index is requested.

push(); translate(x, y);
Moves the origin to the requested (x, y) so every shape inside can be drawn relative to 0,0 regardless of where the avatar actually appears on screen.
switch (designIndex) { case 0: ... case 1: ... case 2: ... }
Each case contains its own sequence of fill()/ellipse()/rect()/arc() calls, all using `size` as a scaling factor so the same drawing code works at any requested size.

drawAdminScreen()

This screen shows how to reflect a boolean game variable directly in the UI - the button's color and label both update automatically based on the current value of godMode/permanentSpeedBoost, without needing separate 'on' and 'off' button graphics.

function drawAdminScreen() {
  fill(0);
  textSize(40);
  textAlign(CENTER, CENTER);
  text("Admin Panel", width / 2, height * 0.2);

  let startY = height * 0.3;
  let buttonHeight = 50;
  let spacing = 15;
  let buttonWidth = 300;

  // Toggle God Mode Button
  push();
  translate(width / 2, startY + buttonHeight + spacing + buttonHeight / 2);
  fill(godMode ? 200 : 100, godMode ? 50 : 100, godMode ? 50 : 100); // Red if active, grey otherwise
  noStroke();
  rect(-buttonWidth / 2, -buttonHeight / 2, buttonWidth, buttonHeight, 10);
  fill(255);
  textSize(28);
  text("God Mode: " + (godMode ? "ON" : "OFF"), 0, 0);
  pop();

  // (Unlock All Maps, Perma Boost, Add Score, and Back to Menu buttons follow the same pattern)
}
Line-by-line explanation (2 lines)

🔧 Subcomponents:

conditional God Mode Button Color fill(godMode ? 200 : 100, godMode ? 50 : 100, godMode ? 50 : 100); // Red if active, grey otherwise

Changes the button's fill color based on the current boolean state of godMode, giving instant visual feedback for whether the cheat is on.

fill(godMode ? 200 : 100, godMode ? 50 : 100, godMode ? 50 : 100);
Three separate ternary expressions build an RGB color: red-ish when godMode is true, grey when false - all computed inline as the fill() arguments.
text("God Mode: " + (godMode ? "ON" : "OFF"), 0, 0);
Converts the boolean godMode into a readable 'ON'/'OFF' string directly inside the string concatenation.

drawMapUnlockedScreen()

This is a purely informational screen with no buttons of its own - any key or click anywhere is enough to move on, which is handled by the input functions checking `gameState === 'mapUnlocked'`.

function drawMapUnlockedScreen() {
  fill(0);
  textSize(48);
  textAlign(CENTER, CENTER);
  text("Map Unlocked!", width / 2, height / 2 - 50);
  textSize(32);
  text("You unlocked: " + maps[currentMapIndex].name, width / 2, height / 2);
  textSize(24);
  text("Press any key or click to continue to Map Select", width / 2, height / 2 + 50);
}
Line-by-line explanation (2 lines)
text("You unlocked: " + maps[currentMapIndex].name, width / 2, height / 2);
Reads the just-unlocked map's name directly from the maps array to personalize the celebration message.
text("Press any key or click to continue to Map Select", width / 2, height / 2 + 50);
Tells the player how to leave this screen - handled elsewhere by keyPressed()/mousePressed() checking for this exact gameState.

drawGameOverScreen()

Notice this screen doesn't draw any actual buttons - the clickable regions are defined separately in mousePressed() using hard-coded height/2-based ranges, which is a fragile pattern worth studying (see the improvements section).

function drawGameOverScreen() {
  fill(0);
  textSize(48);
  textAlign(CENTER, CENTER);
  text("Game Over!", width / 2, height / 2 - 50);
  textSize(32);
  text("Final Score: " + score, width / 2, height / 2);
  textSize(24);
  text("Press any key or click to restart current map", width / 2, height / 2 + 50);
  text("Click 'Select a Map' to choose another", width / 2, height / 2 + 80);
}
Line-by-line explanation (2 lines)
text("Final Score: " + score, width / 2, height / 2);
Displays whatever the score variable was at the moment the obstacle collision happened.
text("Press any key or click to restart current map", width / 2, height / 2 + 50);
This text describes behavior that mousePressed() implements by checking mouseY ranges relative to height/2, so the click zone and this text need to stay in sync manually.

keyPressed()

keyPressed() is a p5.js event function that fires once per keypress (unlike keyIsDown() used inside Snail.move(), which is checked continuously every frame) - this distinction matters for actions that should happen exactly once, like swinging a pickaxe.

function keyPressed() {
  if (gameState === 'mapUnlocked' || gameState === 'gameOver' || gameState === 'instructions' || gameState === 'shop' || gameState === 'admin') {
    gameState = 'start'; // Default key press action from menus/shop/admin/game over
  } else if (gameState === 'play' && (key === 'e' || key === 'E')) {
    if (snail.hasPickaxe) {
      snail.usePickaxe(); // Use the pickaxe
    } else {
      console.log("No pickaxe to use!");
    }
  }
}
Line-by-line explanation (4 lines)

🔧 Subcomponents:

conditional Any Key Returns to Menu if (gameState === 'mapUnlocked' || gameState === 'gameOver' || gameState === 'instructions' || gameState === 'shop' || gameState === 'admin') {

On several non-gameplay screens, ANY keypress sends the player back to the start menu.

conditional 'E' Key Uses Pickaxe } else if (gameState === 'play' && (key === 'e' || key === 'E')) {

Only while actually playing does pressing E (uppercase or lowercase) attempt to use a held pickaxe.

function keyPressed() {
A built-in p5.js function that automatically runs once every time any key is pressed down.
if (gameState === 'mapUnlocked' || ... ) { gameState = 'start'; }
Checks the current screen against a list of states using multiple `||` (OR) comparisons, and if it matches any of them, jumps back to the main menu.
} else if (gameState === 'play' && (key === 'e' || key === 'E')) {
The built-in `key` variable holds the actual character typed; checking both cases handles Caps Lock being on or off.
if (snail.hasPickaxe) { snail.usePickaxe(); }
Only calls the pickaxe-use logic if the snail actually has one to use, preventing wasted attempts.

mousePressed()

mousePressed() shows two different hit-testing techniques side by side: rectangle bounds checks (mouseX/mouseY compared to button edges) for square buttons, and dist() checks for circular avatar icons - choosing the right test for each shape's geometry.

🔬 These click zones are hard-coded pixel ranges that must match where drawGameOverScreen() happens to draw its text. What happens if you widen the 'restart' zone to 30-100 - does it start overlapping the 'select a map' zone below it?

  else if (gameState === 'gameOver') {
    // Check if clicked on "Restart current map" area (approx)
    if (mouseY > height / 2 + 30 && mouseY < height / 2 + 70) {
      startGame();
    }
    // Check if clicked on "Select a Map" area
    else if (mouseY > height / 2 + 70 && mouseY < height / 2 + 100) {
      gameState = 'mapSelect';
    }
  }
function mousePressed() {
  if (gameState === 'start') {
    let startY = height / 2 - 80;
    let buttonHeight = 50;
    let spacing = 15;
    let buttonWidth = 300;
    let buttonX = width / 2 - buttonWidth / 2;

    // Start Game Button
    if (mouseX > buttonX && mouseX < buttonX + buttonWidth &&
        mouseY > startY && mouseY < startY + buttonHeight) {
      gameState = 'mapSelect';
      return;
    }

    // (How to Play, Avatar Shop, Admin Panel, and Reset Progress buttons checked the same way at increasing Y offsets)
  }
  else if (gameState === 'mapSelect') {
    let startY = height * 0.3;
    let buttonHeight = 50;
    let spacing = 15;
    let buttonWidth = 400;

    for (let i = 0; i < maps.length; i++) {
      let buttonY = startY + i * (buttonHeight + spacing);
      let buttonX = width / 2 - buttonWidth / 2;

      if (mouseX > buttonX && mouseX < buttonX + buttonWidth &&
          mouseY > buttonY && mouseY < buttonY + buttonHeight) {
        if (i < unlockedMapsCount) {
          currentMapIndex = i;
          startGame(); // Start playing the selected map
          return;
        }
      }
    }
  }
  else if (gameState === 'shop') {
    let avatarSize = 100;
    let spacing = 50;
    let startX = width / 2 - (avatarSize * 3 + spacing * 2) / 2 + avatarSize / 2;
    let avatarY = height * 0.5;

    for (let i = 0; i < 3; i++) {
      let x = startX + i * (avatarSize + spacing);
      let y = avatarY;
      if (dist(mouseX, mouseY, x, y) < avatarSize / 2) {
        changeSnailAvatar(i); // Select this avatar design
        return;
      }
    }
  }
  else if (gameState === 'gameOver') {
    // Check if clicked on "Restart current map" area (approx)
    if (mouseY > height / 2 + 30 && mouseY < height / 2 + 70) {
      startGame();
    }
    // Check if clicked on "Select a Map" area
    else if (mouseY > height / 2 + 70 && mouseY < height / 2 + 100) {
      gameState = 'mapSelect';
    }
  }
}
Line-by-line explanation (5 lines)

🔧 Subcomponents:

conditional Start Menu Buttons if (gameState === 'start') {

Tests the click position against each button's rectangle bounds on the main menu, switching gameState depending on which one was hit.

for-loop Map Select Buttons for (let i = 0; i < maps.length; i++) {

Checks the click against every map button and, if that map is unlocked, sets currentMapIndex and starts it.

for-loop Avatar Click Detection for (let i = 0; i < 3; i++) {

Uses a simple distance check (dist) instead of rectangle bounds to detect clicks on the circular avatar icons.

conditional Game Over Click Zones else if (gameState === 'gameOver') {

Uses hard-coded vertical pixel ranges (rather than shared button variables) to detect clicks on the 'restart' vs 'select a map' text prompts.

function mousePressed() {
A built-in p5.js function that fires once every time the mouse button is clicked.
if (mouseX > buttonX && mouseX < buttonX + buttonWidth && mouseY > startY && mouseY < startY + buttonHeight) {
The classic 'is the point inside this rectangle' test: checks that the mouse's x is between the button's left/right edges AND its y is between the top/bottom edges.
if (i < unlockedMapsCount) { currentMapIndex = i; startGame(); return; }
Only lets the player actually start a map if its index is within the unlocked range; clicking a locked map button does nothing.
if (dist(mouseX, mouseY, x, y) < avatarSize / 2) { changeSnailAvatar(i); return; }
For the round avatar icons, a distance check (rather than a rectangle test) correctly detects clicks anywhere inside the circle.
if (mouseY > height / 2 + 30 && mouseY < height / 2 + 70) { startGame(); }
On the Game Over screen, clicking within this specific vertical band restarts the same map - these numbers must be manually kept in sync with where drawGameOverScreen() actually draws its text.

saveGameProgress()

localStorage persists data even after the browser tab is closed, which is how this sketch remembers which maps you've unlocked and which snail skin you picked the next time you visit the page.

function saveGameProgress() {
  const progress = {
    unlockedMapsCount: unlockedMapsCount,
    currentSnailDesign: currentSnailDesign // Save the current snail design
  };
  localStorage.setItem(localStorageKey, JSON.stringify(progress));
  console.log("Game progress saved:", progress);
}
Line-by-line explanation (2 lines)
const progress = { unlockedMapsCount: unlockedMapsCount, currentSnailDesign: currentSnailDesign };
Bundles the two pieces of data worth remembering between visits into a single plain object.
localStorage.setItem(localStorageKey, JSON.stringify(progress));
localStorage can only store strings, so JSON.stringify() converts the progress object into a text string before saving it under a named key in the browser.

loadGameProgress()

This function is the mirror image of saveGameProgress() - together they form a simple save/load pair that many browser games use instead of a server-side database.

function loadGameProgress() {
  const savedProgress = localStorage.getItem(localStorageKey);
  if (savedProgress) {
    const progress = JSON.parse(savedProgress);
    unlockedMapsCount = progress.unlockedMapsCount;
    currentSnailDesign = progress.currentSnailDesign !== undefined ? progress.currentSnailDesign : 0; // Load design, default to 0 if not found
    // Ensure unlockedMapsCount doesn't exceed available maps
    unlockedMapsCount = constrain(unlockedMapsCount, 1, maps.length);
    console.log("Game progress loaded:", progress);
  } else {
    console.log("No saved game progress found.");
  }
}
Line-by-line explanation (3 lines)
const savedProgress = localStorage.getItem(localStorageKey);
Retrieves the raw saved string (or null if nothing was ever saved) using the same key it was saved under.
if (savedProgress) { const progress = JSON.parse(savedProgress); ... }
Only tries to parse and use the data if something was actually found - JSON.parse() turns the stored text back into a real JavaScript object.
currentSnailDesign = progress.currentSnailDesign !== undefined ? progress.currentSnailDesign : 0;
A defensive check: older saved data might not have a currentSnailDesign field at all, so this falls back to 0 rather than storing `undefined`.

unlockNextMap()

Calling saveGameProgress() right after every meaningful change (rather than only when the player quits) ensures progress is never lost even if the tab crashes.

function unlockNextMap() {
  if (unlockedMapsCount < maps.length) {
    unlockedMapsCount++;
    saveGameProgress(); // Save progress immediately
    console.log("Unlocked Map:", maps[unlockedMapsCount - 1].name);
  }
}
Line-by-line explanation (3 lines)
if (unlockedMapsCount < maps.length) {
Only increases the unlocked count if there's actually another map left to unlock.
unlockedMapsCount++;
Increments the counter by one, immediately making the next map selectable on the Map Select screen.
saveGameProgress();
Immediately writes the new unlocked count to localStorage so the unlock isn't lost if the browser tab closes.

resetGameProgress()

Notice the order: startGame() sets gameState to 'play' internally, but the very next line overwrites it to 'mapSelect' - a reminder that later assignments to the same variable always win.

function resetGameProgress() {
  unlockedMapsCount = 1; // Reset to only Map 1 unlocked
  currentSnailDesign = 0; // Reset snail design to default
  saveGameProgress(); // Save this reset state
  currentMapIndex = 0; // Go back to Map 1
  startGame(); // Start Map 1
  gameState = 'mapSelect'; // Return to map select screen
  console.log("Game progress reset. Only Map 1 is now unlocked.");
}
Line-by-line explanation (2 lines)
unlockedMapsCount = 1;
Reverts progress back to only the very first map being unlocked.
startGame(); gameState = 'mapSelect';
Restarts Map 1's game elements first, but then immediately overrides gameState to 'mapSelect' so the player lands on the map picker rather than jumping straight into play.

changeSnailAvatar()

This function only updates data - it deliberately does NOT try to update a live Snail object, because (as the comments explain) no Snail object necessarily exists yet while browsing the shop; startGame() reads currentSnailDesign fresh whenever a new Snail is created.

function changeSnailAvatar(designIndex) {
  currentSnailDesign = designIndex;
  // The 'snail' object only exists during 'play' state.
  // When changing avatar in 'shop' state, 'snail' is null.
  // 'startGame()' will correctly create the snail with 'currentSnailDesign'
  // when the player starts a new game.
  saveGameProgress(); // Save the new design
  console.log("Snail design changed to:", currentSnailDesign);
}
Line-by-line explanation (2 lines)
currentSnailDesign = designIndex;
Updates the global variable that tracks which snail skin is currently chosen.
saveGameProgress();
Immediately persists the new choice so it's remembered the next time the game loads, even if the player never enters 'play' state again this session.

unlockAllMaps()

This is the simplest of the admin cheat functions - a single-line change to a counter that the Map Select screen already knows how to interpret, since it was built to compare against `unlockedMapsCount` from the start.

function unlockAllMaps() {
  unlockedMapsCount = maps.length;
  saveGameProgress();
  console.log("All maps unlocked!");
}
Line-by-line explanation (2 lines)
unlockedMapsCount = maps.length;
Sets the unlocked count to the total number of maps that exist, instantly unlocking every single one.
saveGameProgress();
Saves this cheat's effect so all maps remain unlocked even after the page is refreshed.

windowResized()

windowResized() is a built-in p5.js function that automatically fires whenever the browser window changes size, which is essential for this sketch since createCanvas(windowWidth, windowHeight) means the canvas is always exactly the size of the browser.

function windowResized() {
  resizeCanvas(windowWidth, windowHeight);
  // Re-initialize game elements to adapt to the new canvas size.
  // This will reload the obstacles for the current map with correct coordinates.
  // Note: Rapidly resizing the window can cause multiple 'startGame()' calls,
  // which might also trigger 'Long task detected' warnings.
  startGame();
}
Line-by-line explanation (2 lines)
resizeCanvas(windowWidth, windowHeight);
A built-in p5.js function that changes the canvas's actual pixel dimensions to match the browser window whenever it's resized.
startGame();
Because obstacle positions are stored as fractions of width/height, the simplest way to keep them correctly placed after a resize is to just rebuild everything from scratch - though this comes at the cost of losing the player's current score and progress mid-game.

📦 Key Variables

gameState string

The single source of truth for which screen is currently active ('start', 'mapSelect', 'play', 'gameOver', etc.); draw()'s switch statement reads this every frame.

let gameState = 'start';
snail object

Holds the current Snail instance the player controls, including its position, velocity, speed, and inventory flags.

let snail;
food object

The single red food item the player is chasing to increase score.

let food;
obstacles array

Holds all Obstacle objects for the currently loaded map; the snail dies if it touches any of them (unless godMode is on).

let obstacles = [];
score number

Tracks how many food items have been eaten this run; compared against each map's targetScore to unlock the next map.

let score = 0;
candies array

Holds the Candy objects scattered on the map that grant a temporary speed boost when eaten.

let candies = [];
pickaxe object

The single Pickaxe item on the map (or held by the snail); used to destroy a nearby obstacle when the player presses 'E'.

let pickaxe;
keyItem object

An optional Key item defined on some maps; picked up and stored but not currently tied to any door mechanic in the active game logic.

let keyItem;
maps array

The master list of all playable map definitions (name, target score, obstacle layout), built once by defineMaps().

let maps = [];
currentMapIndex number

Which entry in the maps array is currently being played.

let currentMapIndex = 0;
unlockedMapsCount number

How many maps (starting from map 1) the player has unlocked so far; loaded from and saved to localStorage.

let unlockedMapsCount = 1;
localStorageKey string

The constant key name used to store and retrieve saved progress in the browser's localStorage.

const localStorageKey = 'snailGameProgress';
currentSnailDesign number

Index (0, 1, or 2) of the snail skin the player has chosen in the Avatar Shop.

let currentSnailDesign = 0;
godMode boolean

Admin cheat flag - when true, the snail becomes invincible to obstacle collisions.

let godMode = false;
permanentSpeedBoost boolean

Admin cheat flag - when true, overrides the snail's speed with a permanent multiplier instead of relying on candy pickups.

let permanentSpeedBoost = false;

🔧 Potential Improvements (4)

Here are some ways this code could be enhanced:

BUG windowResized()

Resizing the browser window calls startGame(), which fully resets score, obstacles, candies, and the pickaxe - so a player who resizes their window mid-game instantly loses their progress on that attempt.

💡 Instead of calling startGame(), reposition existing obstacles/items proportionally to the new width/height without resetting score or recreating the snail, or at least preserve `score` across the resize.

BUG mousePressed() gameOver branch

The clickable zones for 'Restart' and 'Select a Map' on the Game Over screen are hard-coded pixel ranges (height/2+30 to +70, etc.) that must manually match where drawGameOverScreen() happens to draw its text - if one is edited without the other, clicks will silently stop working.

💡 Store each button's y-position and height in shared variables (or a small array of button definitions) that both the draw function and mousePressed() read from, so layout changes automatically stay in sync with click detection.

FEATURE Key and Chest classes

The Key and Chest classes (and the door puzzle they belonged to) are fully implemented and drawn but never spawned or checked for collisions anywhere in updateGame()/drawGame() - it's dead code that adds confusion.

💡 Either remove the unused Key/Chest classes and related commented-out door code entirely, or finish reintroducing the door-and-chest puzzle as a bonus objective on later maps.

PERFORMANCE isSpotOccupied()

Every time any item respawns, this function loops through every obstacle, candy, the pickaxe, and the key with a full distance calculation, and respawn() itself can call it up to 100 times per attempt loop - on maps with many obstacles this adds up.

💡 Add an early-exit rough distance check (e.g., skip obstacles whose bounding circle is clearly far from the candidate point) before doing the precise circle-rectangle math, or reduce the max attempt count.

🔄 Code Flow

Code flow showing obstacle, food, candy, pickaxe, key, chest, snail, preload, setup, defineMaps, draw, startgame, isspotoccupied, updategame, drawgame, drawstartscreen, drawmapselectscreen, drawinstructionsscreen, drawshopscreen, drawsnailavatar, drawadminscreen, drawmapunlockedscreen, drawgameoverscreen, keypressed, mousepressed, savegameprogress, loadgameprogress, unlocknextmap, resetgameprogress, changesnailavatar, unlockallmaps, windowresized

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

graph TD start[Start] --> setup[setup] setup --> draw[draw loop] draw --> draw-case-start[draw-case-start] draw-case-start --> mouse-start-branch[mouse-start-branch] mouse-start-branch --> draw-case-play[draw-case-play] draw-case-play --> updategame[updategame] updategame --> update-food-check[update-food-check] update-food-check --> update-candy-loop[update-candy-loop] update-candy-loop --> update-obstacle-loop[update-obstacle-loop] update-obstacle-loop --> drawgame[drawgame] drawgame --> drawgame-obstacle-loop[drawgame-obstacle-loop] drawgame-obstacle-loop --> drawgame-boost-hud[drawgame-boost-hud] draw-case-play --> draw-case-gameover[draw-case-gameover] draw-case-gameover --> mouse-gameover-branch[mouse-gameover-branch] draw-case-gameover --> keypressed-menu-check[keypressed-menu-check] draw-case-start --> start-button-layout[start-button-layout] start-button-layout --> drawmapselectscreen[drawmapselectscreen] drawmapselectscreen --> mapselect-loop[mapselect-loop] mapselect-loop --> mouse-mapselect-loop[mouse-mapselect-loop] draw-case-start --> drawinstructionsscreen[drawinstructionsscreen] draw-case-start --> drawshopscreen[drawshopscreen] drawshopscreen --> shop-avatar-loop[shop-avatar-loop] shop-avatar-loop --> mouse-shop-loop[mouse-shop-loop] draw-case-start --> drawadminscreen[drawadminscreen] drawadminscreen --> admin-godmode-button[admin-godmode-button] draw-case-play --> snail-arrow-keys[snail-arrow-keys] draw-case-play --> snail-mouse-override[snail-mouse-override] draw-case-play --> snail-pickaxe-loop[snail-pickaxe-loop] draw-case-play --> snail-design-switch[snail-design-switch] setup --> startgame[startgame] startgame --> startgame-obstacle-loop[startgame-obstacle-loop] startgame-obstacle-loop --> startgame-candy-loop[startgame-candy-loop] startgame-candy-loop --> food-respawn-loop[food-respawn-loop] food-respawn-loop --> isspotoccupied[isspotoccupied] isspotoccupied --> isspot-obstacle-loop[isspot-obstacle-loop] isspot-obstacle-loop --> isspot-candy-loop[isspot-candy-loop] click setup href "#fn-setup" click draw href "#fn-draw" click updategame href "#fn-updategame" click drawgame href "#fn-drawgame" click draw-case-start href "#sub-draw-case-start" click draw-case-play href "#sub-draw-case-play" click draw-case-gameover href "#sub-draw-case-gameover" click startgame href "#fn-startgame" click food-respawn-loop href "#sub-food-respawn-loop" click isspotoccupied href "#fn-isspotoccupied" click isspot-obstacle-loop href "#sub-isspot-obstacle-loop" click isspot-candy-loop href "#sub-isspot-candy-loop" click update-food-check href "#sub-update-food-check" click update-candy-loop href "#sub-update-candy-loop" click update-obstacle-loop href "#sub-update-obstacle-loop" click drawgame-obstacle-loop href "#sub-drawgame-obstacle-loop" click drawgame-boost-hud href "#sub-drawgame-boost-hud" click start-button-layout href "#sub-start-button-layout" click mapselect-loop href "#sub-mapselect-loop" click shop-avatar-loop href "#sub-shop-avatar-loop" click admin-godmode-button href "#sub-admin-godmode-button" click snail-arrow-keys href "#sub-snail-arrow-keys" click snail-mouse-override href "#sub-snail-mouse-override" click snail-pickaxe-loop href "#sub-snail-pickaxe-loop" click snail-design-switch href "#sub-snail-design-switch" click mouse-start-branch href "#sub-mouse-start-branch" click mouse-mapselect-loop href "#sub-mouse-mapselect-loop" click mouse-shop-loop href "#sub-mouse-shop-loop" click mouse-gameover-branch href "#sub-mouse-gameover-branch" click keypressed-menu-check href "#sub-keypressed-menu-check"

❓ Frequently Asked Questions

What visual elements can users expect to see in the Snail game sketch?

The Snail game sketch features a playful environment with a snail character, obstacles, and items like food and speed boost candies, all rendered in a simple graphical style.

How can players interact with the Snail game sketch?

Players can navigate the snail through different maps, collect food and candies for speed boosts, and avoid obstacles to increase their score.

What creative coding concepts are showcased in the Snail game sketch?

The sketch demonstrates object-oriented programming through class definitions for game elements, as well as state management for different game phases.

Preview

Snail game - p5.js creative coding sketch preview
Sketch Preview
Code flow diagram showing the structure of Snail game - Code flow showing obstacle, food, candy, pickaxe, key, chest, snail, preload, setup, defineMaps, draw, startgame, isspotoccupied, updategame, drawgame, drawstartscreen, drawmapselectscreen, drawinstructionsscreen, drawshopscreen, drawsnailavatar, drawadminscreen, drawmapunlockedscreen, drawgameoverscreen, keypressed, mousepressed, savegameprogress, loadgameprogress, unlocknextmap, resetgameprogress, changesnailavatar, unlockallmaps, windowresized
Code Flow Diagram