Snail game (Remix)

This is a complete snail-themed collection game where players guide a customizable snail around obstacle-filled maps to collect food and reach score targets to unlock new levels. The game features multiple difficulty maps, speed-boosting candies, pickaxes to destroy obstacles, inventory systems, and an admin panel for cheats.

🧪 Try This!

Experiment with the code by making these changes:

  1. Make the snail twice as fast — The snail feels sluggish? Doubling originalSpeed instantly makes gameplay more action-packed and challenging.
  2. Add a fourth map with more obstacles — Extend the difficulty curve by inserting a new map between Map 2 and Map 3—great practice for editing game data.
  3. Make candies give more speed — Speed boosts feel underwhelming? Increase the speedBoostMultiplier from 2 to 3 so candy pickups feel more dramatic.
  4. Extend the speed boost timer — Speed boosts fade too quickly? Changing 5000ms to 8000ms gives players 8 seconds of enhanced speed instead of 5.
  5. Start every map with more candies — More speed boosts = more fun. Change the for loop to spawn 5 candies instead of 3 at the start of each map.
  6. Change the pickaxe key to spacebar — E key feels awkward? Try using the spacebar to activate pickaxes for a more accessible control scheme.
Prefer the full editor? Open it there →

📖 About This Sketch

This sketch creates a full-featured collection game where a player-controlled snail navigates around grey obstacles to collect red food items. The visual appeal comes from colorful snail avatar designs, spinning rainbow candies, and a progressive difficulty curve across six thematic maps (Grassy Plains through Icy Tundra). The code showcases several advanced p5.js techniques: object-oriented class design (Snail, Food, Candy, Pickaxe, Key, Obstacle classes), state management for menu navigation, collision detection between circles and rectangles, mouse and keyboard input, and persistent data storage using localStorage.

The code is organized into distinct sections: class definitions for game entities, setup and draw loop management, game state handlers (start menu, map selection, gameplay, shop, admin panel), input processing via keyPressed and mousePressed, and utility functions for saving/loading progress. By studying it, you will learn how professional game developers structure complex interactive experiences—particularly how to use classes to represent game objects, how state machines control screen flow, and how to separate visual rendering from game logic.

⚙️ How It Works

  1. When the sketch loads, preload() calls defineMaps() to set up all six maps with their obstacle layouts, then loadGameProgress() retrieves saved progress from the browser's localStorage so the player resumes where they left off.
  2. setup() creates the canvas and calls startGame(), which initializes the snail at the center, spawns food and collectible items (candies, pickaxe, key) at random non-overlapping positions, and loads obstacles for the current map.
  3. The draw() loop runs at 60 fps and switches behavior based on gameState: it draws menus, the shop, the admin panel, or (if 'play') calls updateGame() to move the snail and check collisions, then drawGame() to render all visual elements.
  4. In the 'play' state, updateGame() handles snail movement (arrow keys or mouse control), checks for food collection (increasing score), detects obstacles (ending the game unless god mode is on), and manages collectible items: candies trigger a temporary speed boost, pickaxes can be used with 'E' to destroy nearby obstacles, and keys can be collected for future door mechanics.
  5. When the player reaches the target score for a map, unlockNextMap() increments unlockedMapsCount and transitions to a congratulatory screen. Clicking on the Map Select screen lets players choose unlocked maps to play.
  6. Mouse clicks on buttons call the appropriate state transitions: Start Game → Map Select, How to Play → Instructions, Avatar Shop → customizable snail designs (saved to localStorage), Admin Panel → cheats like God Mode or permanent speed boosts, and Reset Game Progress → full restart.

🎓 Concepts You'll Learn

Object-Oriented Programming (Classes)State Machine & Game FlowCollision Detection (Circle-Rectangle)Keyboard & Mouse InputLocalStorage PersistenceClass InheritanceVector MathConditional Logic

📝 Code Breakdown

preload()

preload() is called before setup() and is the perfect place to load data that must exist before the sketch initializes. Here it ensures maps are defined and prior progress is restored before any game objects are created.

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();
Populates the global maps array with all six levels before loading saved data—this must happen first so we know how many maps exist
loadGameProgress();
Retrieves the player's previous progress (unlocked maps, avatar choice) from the browser's localStorage so it persists across sessions

setup()

setup() runs once when the sketch launches. It prepares the canvas and calls startGame() to populate all game objects. If you resize the browser window, windowResized() re-calls startGame() to adapt the layout.

function setup() {
  createCanvas(windowWidth, windowHeight);
  startGame(); // Start the game with the current map
}
Line-by-line explanation (2 lines)
createCanvas(windowWidth, windowHeight);
Creates a canvas that fills the entire browser window—windowWidth and windowHeight are p5.js variables that automatically detect the browser size
startGame();
Initializes the snail, food, obstacles, candies, and pickaxe for the current map, then sets gameState to 'play' so the game begins

draw()

The draw() function runs every frame (60 times per second by default). By using a switch statement on gameState, the sketch cleanly separates menu rendering, gameplay logic, and shop interfaces—each state is independent, making the code easy to extend with new screens or features.

🔬 This switch statement is the heart of the game's navigation. What happens if you change case 'start': to case 'mapSelect': so the game starts on the map selection screen instead of the main menu?

  switch (gameState) {
    case 'start':
      drawStartScreen();
      break;
    case 'mapSelect':
      drawMapSelectScreen();
      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 Game State Dispatcher switch (gameState) {

Routes all rendering and logic to the correct handler based on what screen the player is on

background(220); // Clear background each frame
Fills the canvas with light grey (RGB 220) every frame—this erases the previous frame so new visuals appear cleanly instead of stacking on top
switch (gameState) {
A switch statement checks the current gameState string ('start', 'play', 'shop', etc.) and branches to the appropriate drawing or update function
case 'play': updateGame(); drawGame(); break;
When the game is being played, updateGame() handles snail movement and collision checks, then drawGame() renders all visual elements; break exits the switch so other cases don't run

defineMaps()

defineMaps() creates all level data as plain JavaScript objects. Each map's obstacle positions use normalized coordinates (0.0 to 1.0) so they adapt when the window resizes. By editing this function, you can quickly create new maps or adjust difficulty without touching game logic.

🔬 These three lines define Map 1's obstacles. Relative positions (0.2 = 20% across the canvas) scale with window resizing. What happens if you change the first obstacle's x from 0.2 to 0.8 and its y from 0.3 to 0.7? Where does it move to?

        { 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 },
function defineMaps() {
  maps = [
    {
      name: "Map 1: Grassy Plains",
      targetScore: 5, // Score needed to unlock Map 2
      obstacles: [
        // Obstacle positions (x, y) are relative to canvas width/height (0.0 to 1.0)
        // Width (w) and height (h) are absolute pixel values for simplicity
        { 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 }, // Key now top-left, more accessible
    },
    {
      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 (5 lines)
maps = [
Initializes the global maps array with six level objects, each containing a name, target score to unlock the next level, and obstacle layout
name: "Map 1: Grassy Plains",
A human-readable name displayed in the map select screen and on the HUD during gameplay
targetScore: 5, // Score needed to unlock Map 2
The number of food items the player must collect on this map to unlock the next map; Map 5 uses Infinity (no unlock) because it's the final map
{ x: 0.2, y: 0.3, w: 100, h: 50 },
An obstacle object: x and y are normalized coordinates (0.0 to 1.0 of canvas width/height) so obstacles scale with window resizing; w and h are fixed pixel dimensions
unlockedMapsCount = constrain(unlockedMapsCount, 1, maps.length);
Ensures the stored unlockedMapsCount never exceeds the actual number of maps defined (clamping between 1 and maps.length)

startGame()

startGame() is the initialization function called when the player selects a map or the game restarts. It resets all game objects, loads the selected map's obstacles, and transitions to the 'play' state. It's also called by windowResized() to adapt when the browser is resized.

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 (6 lines)

🔧 Subcomponents:

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

Iterates through the current map's obstacle definitions and creates actual Obstacle objects at absolute pixel positions

for-loop Spawn Initial Candies for(let i = 0; i < 3; i++) {

Creates three candy objects and spawns them at random non-overlapping locations on the map

score = 0;
Resets the player's score to zero for this map
snail = new Snail(width / 2, height / 2, currentSnailDesign);
Creates a new Snail object at the center of the canvas using the player's chosen avatar design (stored in currentSnailDesign)
food = new Food(); // Create initial food food.respawn();
Creates the initial red food item and respawns it to ensure it doesn't overlap obstacles, candies, or the snail
for (let obsData of maps[currentMapIndex].obstacles) { obstacles.push(new Obstacle(width * obsData.x, height * obsData.y, obsData.w, obsData.h));
Converts the current map's normalized obstacle positions (0.0 to 1.0) into absolute pixel positions by multiplying by canvas width/height, then creates Obstacle objects
for(let i = 0; i < 3; i++) { candies.push(new Candy()); candies[i].respawn();
Spawns three speed-boost candies at random locations on the map
gameState = 'play'; // Set game state to play
Changes the game state so the draw() loop will call updateGame() and drawGame() instead of menu screens

updateGame()

updateGame() is called every frame when gameState is 'play'. It handles all collision detection and game logic: eating food, hitting obstacles, collecting items, and managing state transitions. It runs before drawGame(), so all position and state changes are finalized before rendering.

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 (9 lines)

🔧 Subcomponents:

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

Detects when the snail touches the food, increments score, respawns food, and checks for map unlock

conditional Obstacle Collision Detection if (!godMode) { for (let obs of obstacles) {

Loops through obstacles checking if the snail collides with any (unless god mode is active)

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

Checks all candies for collision with the snail; if touched, removes it, respawns it, and activates the speed boost

snail.move();
Calls the Snail's move() method to process keyboard and mouse input and update its velocity and position
snail.checkBoundaries();
Ensures the snail stays within the canvas using constrain() to prevent it from escaping off-screen
if (dist(snail.pos.x, snail.pos.y, food.x, food.y) < snail.size / 2 + food.size / 2) {
Uses dist() to calculate the distance between the snail center and food center; collision occurs when this distance is less than the sum of their radii (circle-circle collision)
score++;
Increments the score by 1 each time food is collected
if (currentMapIndex < maps.length - 1 && score >= maps[currentMapIndex].targetScore) {
Checks two conditions: the current map is not the last map, AND the score meets the target—if both are true, the next map is unlocked
if (!godMode) {
Only checks for obstacle collisions if god mode is off; if godMode is true, the snail passes through all obstacles
for (let i = candies.length - 1; i >= 0; i--) {
Loops backward through the candies array (from last to first) so removing items doesn't skip entries
candies.splice(i, 1);
Removes the candy at index i from the array without disrupting the loop
snail.activateSpeedBoost();
Calls the Snail's activateSpeedBoost() method to start the temporary speed boost timer and set snail.speed to originalSpeed * speedBoostMultiplier

drawGame()

drawGame() runs every frame during gameplay to render all visual elements. It calls show() on every game object (food, obstacles, candies, pickaxe, key, snail) and displays overlays like the score, map name, and active cheat/boost status. The order matters: items drawn first appear behind items drawn later.

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 (8 lines)
food.show();
Calls the Food object's show() method to render the red circle at its current position
for (let obs of obstacles) { obs.show(); }
Iterates through all obstacles and draws each grey rectangle by calling their show() method
for (let candy of candies) { candy.show(); }
Draws all candies—each rotates and displays colorful star-like shapes
if (pickaxe && !snail.hasPickaxe) { pickaxe.show(); }
Only draws the pickaxe on the map if it exists AND the snail doesn't have it (once picked up, it hides from the map and appears as an icon on the snail)
snail.show();
Renders the snail at its current position using the chosen avatar design; also draws pickaxe/key icons if the snail is holding them
text("Score: " + score, 10, 30);
Displays the current score in the top-left corner, updated every frame
text(maps[currentMapIndex].name, width - 10, 30);
Displays the current map's name (e.g., 'Map 1: Grassy Plains') in the top-right corner
let timeRemaining = ceil((snail.speedBoostTimer - millis()) / 1000); text("SPEED BOOST! " + timeRemaining + "s", width / 2, height - 20);
Calculates how many seconds remain on the speed boost by subtracting the current time (millis()) from the boost's expiration time, then rounds up with ceil() and displays it

Snail class

The Snail class is the game's protagonist. It encapsulates position, velocity, speed, avatar design, and interaction logic (collision detection, pickaxe use). The move() method reads input (arrow keys override mouse control), and show() renders one of three designs. Study this class to understand how objects in p5.js can be self-contained entities with their own data and methods.

🔬 This code handles left/right movement. The snail only moves if an arrow key is held down, and stops if no key is pressed. What happens if you change the last else to keep moving even without input? Try changing else { this.vel.x = 0; } to remove it so the snail coasts forward?

    if (keyIsDown(LEFT_ARROW)) {
      this.vel.x = -currentSpeed;
    } else if (keyIsDown(RIGHT_ARROW)) {
      this.vel.x = currentSpeed;
    } else {
      this.vel.x = 0;
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
        // Shell
        fill(139, 69, 19); // Brown
        ellipse(0, -5, this.size, this.size * 1.2);

        // Body
        fill(124, 252, 0); // Green
        ellipse(0, 10, this.size * 1.5, this.size * 0.7);

        // Eyes
        fill(0);
        ellipse(-this.size * 0.4, -this.size * 0.4, this.size * 0.2, this.size * 0.2);
        ellipse(this.size * 0.4, -this.size * 0.4, this.size * 0.2, this.size * 0.2);

        // Pupils
        fill(255);
        ellipse(-this.size * 0.4, -this.size * 0.4, this.size * 0.1, this.size * 0.1);
        ellipse(this.size * 0.4, -this.size * 0.4, this.size * 0.1, this.size * 0.1);

        // Mouth
        fill(255, 0, 0);
        arc(0, this.size * 0.1, this.size * 0.3, this.size * 0.2, 0, PI);
        break;

      case 1: // Blue Snail
        // Shell
        fill(50, 50, 200); // Dark Blue
        ellipse(0, -5, this.size * 1.1, this.size * 1.3); // Slightly larger shell

        // Body
        fill(100, 100, 255); // Light Blue
        ellipse(0, 10, this.size * 1.4, this.size * 0.8);

        // Eyes
        fill(255); // White eyes
        ellipse(-this.size * 0.3, -this.size * 0.3, this.size * 0.25, this.size * 0.25);
        ellipse(this.size * 0.3, -this.size * 0.3, this.size * 0.25, this.size * 0.25);

        // Pupils
        fill(0); // Black pupils
        ellipse(-this.size * 0.3, -this.size * 0.3, this.size * 0.1, this.size * 0.1);
        ellipse(this.size * 0.3, -this.size * 0.3, this.size * 0.1, this.size * 0.1);

        // Mouth
        fill(100); // Grey mouth
        arc(0, this.size * 0.1, this.size * 0.4, this.size * 0.1, 0, PI);
        break;

      case 2: // Striped Snail
        // Shell
        fill(139, 69, 19); // Brown
        ellipse(0, -5, this.size, this.size * 1.2);
        fill(255, 200, 0); // Yellow stripes
        rect(-this.size / 2, -15, this.size, 5);
        rect(-this.size / 2, -5, this.size, 5);
        rect(-this.size / 2, 5, this.size, 5);

        // Body
        fill(200, 100, 50); // Orange-brown body
        ellipse(0, 10, this.size * 1.5, this.size * 0.7);

        // Eyes
        fill(0);
        ellipse(-this.size * 0.4, -this.size * 0.4, this.size * 0.15, this.size * 0.15);
        ellipse(this.size * 0.4, -this.size * 0.4, this.size * 0.15, this.size * 0.15);

        // Pupils
        fill(255, 0, 0); // Red pupils
        ellipse(-this.size * 0.4, -this.size * 0.4, this.size * 0.05, this.size * 0.05);
        ellipse(this.size * 0.4, -this.size * 0.4, this.size * 0.05, this.size * 0.05);

        // Mouth
        fill(0);
        arc(0, this.size * 0.15, this.size * 0.2, this.size * 0.1, 0, PI);
        break;
    }

    // Draw pickaxe icon if snail has it
    if (this.hasPickaxe) {
      push();
      translate(this.size * 0.7, -this.size * 0.7); // Position relative to snail
      rotate(PI / 4); // Angle the pickaxe
      fill(150, 100, 50); // Handle
      rect(-10, -5, 20, 5);
      fill(120); // Head
      beginShape();
      vertex(0, -10);
      vertex(-5, 0);
      vertex(-10, 5);
      vertex(0, 18);
      vertex(10, 5);
      vertex(10, -5);
      endShape(CLOSE);
      pop();
    }

    // Draw key icon if snail has it
    if (this.hasKey) {
      push();
      translate(this.size * 0.7, this.size * 0.7); // Position relative to snail
      fill(255, 200, 0); // Yellow
      ellipse(0, 0, this.size * 0.4, this.size * 0.4);
      rect(this.size * 0.1, -this.size * 0.2, this.size * 0.3, this.size * 0.1);
      rect(this.size * 0.1, this.size * 0.1, this.size * 0.1, this.size * 0.1);
      pop();
    }

    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 (18 lines)

🔧 Subcomponents:

calculation Move Method - Input & Steering move() {

Reads keyboard and mouse input and calculates the snail's new velocity each frame

switch-case Show Method - Avatar Rendering switch (this.designIndex) {

Renders the snail in one of three designs based on the player's choice from the shop

for-loop Use Pickaxe - Obstacle Destruction for (let i = obstacles.length - 1; i >= 0; i--) {

Finds and removes nearby obstacles when the snail uses the pickaxe

this.pos = createVector(x, y);
Creates a p5.Vector to store the snail's 2D position, which is easier to work with than separate x and y variables
this.vel = createVector(0, 0);
Creates a velocity vector; each frame, this.pos.add(this.vel) moves the snail by the velocity amount
this.originalSpeed = 3;
The base speed—used as a reference point when calculating speed boosts so the snail returns to this speed when boosts expire
this.speedBoostDuration = 5000; // 5 seconds
How long the speed boost lasts in milliseconds (5000ms = 5 seconds)
move() {
This method reads keyboard and mouse input every frame and calculates the snail's velocity
if (keyIsDown(LEFT_ARROW)) { this.vel.x = -currentSpeed;
If the left arrow is held, set velocity.x to negative speed (move left); keyIsDown() detects continuous holding
if (mouseIsPressed || mouseX !== pmouseX || mouseY !== pmouseY) {
Mouse control activates if the mouse button is pressed OR the mouse moved (pmouseX/pmouseY store last frame's position)
let steer = p5.Vector.sub(target, this.pos);
Calculates a vector FROM the snail's current position TO the mouse cursor; this is the steering direction
steer.setMag(currentSpeed);
Sets the steering vector's magnitude (length) to currentSpeed, so the snail always moves at the same rate regardless of distance to cursor
this.pos.add(this.vel);
Adds the velocity vector to the position vector, moving the snail by velocity.x and velocity.y pixels this frame
checkBoundaries() { this.pos.x = constrain(this.pos.x, this.size / 2, width - this.size / 2);
Uses constrain() to keep the snail's x position between the left and right edges (accounting for the snail's size so it doesn't half-disappear)
activateSpeedBoost() { this.speedBoostActive = true; this.speedBoostTimer = millis() + this.speedBoostDuration;
Starts the speed boost: sets the flag to true and records the expiration time as the current time plus the boost duration
updateSpeedBoost() { if (this.speedBoostActive && millis() > this.speedBoostTimer) { this.speedBoostActive = false; this.speed = this.originalSpeed;
Checks if the boost has expired by comparing the current time (millis()) to the stored expiration time; if expired, deactivates the boost and reverts to original speed
usePickaxe() { if (!this.hasPickaxe) return;
Early exit: if the snail doesn't have the pickaxe, the function stops immediately
if (dist(this.pos.x, this.pos.y, obsCenterX, obsCenterY) < hitRange) { obstacles.splice(i, 1);
If an obstacle's center is within hitRange pixels, removes it from the obstacles array using splice()
switch (this.designIndex) {
Routes drawing logic to one of three snail designs based on the designIndex (0, 1, or 2)
if (this.hasPickaxe) { push(); translate(this.size * 0.7, -this.size * 0.7);
Only draws the pickaxe icon if the snail has it; translate() positions it relative to the snail's center
collidesWith(obstacle) { let testX = this.pos.x; let testY = this.pos.y; if (this.pos.x < obstacle.x) testX = obstacle.x;
Finds the closest point on the obstacle's rectangle to the snail's center—this is the foundation of circle-rectangle collision detection

keyPressed()

keyPressed() is called once per key press (not continuously like keyIsDown). It's ideal for one-time actions like activating items or transitioning screens. The key variable holds the last key pressed, and keyCode stores its numeric ID for special keys like arrow keys.

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 Pickaxe Activation } else if (gameState === 'play' && (key === 'e' || key === 'E')) {

Pressing E during gameplay activates the pickaxe (if the snail has one)

if (gameState === 'mapUnlocked' || gameState === 'gameOver' || gameState === 'instructions' || gameState === 'shop' || gameState === 'admin') {
Checks if any key is pressed while on a menu/end screen by testing multiple game states with logical OR (||)
gameState = 'start';
Pressing any key returns to the main menu from any of these screens
} else if (gameState === 'play' && (key === 'e' || key === 'E')) {
Only during gameplay, if the 'E' key is pressed (uppercase or lowercase), attempt to use the pickaxe
snail.usePickaxe();
Calls the Snail's usePickaxe() method to search for nearby obstacles and destroy one

mousePressed()

mousePressed() handles all click detection across every menu and screen. Notice how it uses different collision logic: rectangles (> < comparisons) for most buttons and circles (dist()) for avatar selection. The function branches based on gameState to check only the relevant buttons for the current screen.

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 Button
    let howToPlayY = startY + buttonHeight + spacing;
    if (mouseX > buttonX && mouseX < buttonX + buttonWidth &&
        mouseY > howToPlayY && mouseY < howToPlayY + buttonHeight) {
      gameState = 'instructions';
      return;
    }

    // Avatar Shop Button
    let avatarShopY = startY + 2 * (buttonHeight + spacing);
    if (mouseX > buttonX && mouseX < buttonX + buttonWidth &&
        mouseY > avatarShopY && mouseY < avatarShopY + buttonHeight) {
      gameState = 'shop';
      return;
    }

    // Admin Panel Button
    let adminPanelY = startY + 3 * (buttonHeight + spacing);
    if (mouseX > buttonX && mouseX < buttonX + buttonWidth &&
        mouseY > adminPanelY && mouseY < adminPanelY + buttonHeight) {
      gameState = 'admin';
      return;
    }

    // Reset Progress Button
    let resetY = startY + 4 * (buttonHeight + spacing);
    if (mouseX > buttonX && mouseX < buttonX + buttonWidth &&
        mouseY > resetY && mouseY < resetY + buttonHeight) {
      resetGameProgress();
      return;
    }
  }
  else if (gameState === 'mapSelect') {
    // Check if a map button was clicked
    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 === 'instructions') {
    let buttonY = height * 0.8;
    let buttonHeight = 50;
    let buttonWidth = 300;
    let buttonX = width / 2 - buttonWidth / 2;

    // Back to Menu Button
    if (mouseX > buttonX && mouseX < buttonX + buttonWidth &&
        mouseY > buttonY && mouseY < buttonY + buttonHeight) {
      gameState = 'start';
      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;

    // Check if an avatar was clicked
    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;
      }
    }

    // Check if Back to Menu Button was clicked
    let buttonY = height * 0.8;
    let buttonHeight = 50;
    let buttonWidth = 300;
    let buttonX = width / 2 - buttonWidth / 2;

    if (mouseX > buttonX && mouseX < buttonX + buttonWidth &&
        mouseY > buttonY && mouseY < buttonY + buttonHeight) {
      gameState = 'start';
      return;
    }
  }
  else if (gameState === 'admin') {
    let startY = height * 0.3;
    let buttonHeight = 50;
    let spacing = 15;
    let buttonWidth = 300;
    let buttonX = width / 2 - buttonWidth / 2;

    // Unlock All Maps Button
    if (mouseX > buttonX && mouseX < buttonX + buttonWidth &&
        mouseY > startY && mouseY < startY + buttonHeight) {
      unlockAllMaps();
      return;
    }

    // Toggle God Mode Button
    let godModeY = startY + buttonHeight + spacing;
    if (mouseX > buttonX && mouseX < buttonX + buttonWidth &&
        mouseY > godModeY && mouseY < godModeY + buttonHeight) {
      godMode = !godMode;
      console.log("God Mode:", godMode ? "ON" : "OFF");
      return;
    }

    // Toggle Permanent Speed Boost Button
    let permaBoostY = startY + 2 * (buttonHeight + spacing);
    if (mouseX > buttonX && mouseX < buttonX + buttonWidth &&
        mouseY > permaBoostY && mouseY < permaBoostY + buttonHeight) {
      permanentSpeedBoost = !permanentSpeedBoost;
      console.log("Permanent Speed Boost:", permanentSpeedBoost ? "ON" : "OFF");
      return;
    }

    // Add 100 Score Button
    let addScoreY = startY + 3 * (buttonHeight + spacing);
    if (mouseX > buttonX && mouseX < buttonX + buttonWidth &&
        mouseY > addScoreY && mouseY < addScoreY + buttonHeight) {
      score += 100;
      console.log("Score increased to:", score);
      return;
    }

    // Back to Menu Button
    let buttonY = startY + 4 * (buttonHeight + spacing);
    if (mouseX > buttonX && mouseX < buttonX + buttonWidth &&
        mouseY > buttonY && mouseY < buttonY + buttonHeight) {
      gameState = 'start';
      return;
    }
  }
  else if (gameState === 'mapUnlocked') {
    gameState = 'mapSelect';
  }
  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)
function mousePressed() {
Called once when the mouse button is pressed; contains all button click detection for the entire game
if (mouseX > buttonX && mouseX < buttonX + buttonWidth && mouseY > buttonY && mouseY < buttonY + buttonHeight) {
Rectangle collision test: checks if the mouse position is inside the button's rectangular bounds using simple > and < comparisons
if (dist(mouseX, mouseY, x, y) < avatarSize / 2) {
Circle collision test for the avatar shop: checks if the mouse is within avatarSize/2 pixels of the avatar center using dist()
changeSnailAvatar(i);
Calls changeSnailAvatar() to save the player's avatar choice to localStorage
return;
Exits the function early so no other button logic runs—prevents multiple buttons from triggering if they overlap

saveGameProgress()

localStorage is a browser API that stores small amounts of data (up to ~5MB) that persists even after the page closes. By saving progress here and loading it in loadGameProgress(), players can resume exactly where they left off.

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 (3 lines)
const progress = {
Creates a JavaScript object containing the data to save: unlockedMapsCount (which maps are unlocked) and currentSnailDesign (avatar choice)
localStorage.setItem(localStorageKey, JSON.stringify(progress));
Converts the object to a JSON string and stores it in the browser's localStorage using localStorageKey ('snailGameProgress' from the global variable) as the key
console.log("Game progress saved:", progress);
Logs to the browser console for debugging—helps confirm the save was successful

loadGameProgress()

loadGameProgress() runs in preload() before setup(), so the player's saved progress is available immediately when the game starts. This function demonstrates defensive programming: it checks if data exists before using it and provides defaults for missing properties.

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 (5 lines)
const savedProgress = localStorage.getItem(localStorageKey);
Retrieves the saved progress string from localStorage using the same key used to save it; returns null if nothing was saved
if (savedProgress) {
Checks if savedProgress exists (is not null) before trying to parse it—prevents errors if no save exists
const progress = JSON.parse(savedProgress);
Converts the JSON string back into a JavaScript object so we can access its properties
currentSnailDesign = progress.currentSnailDesign !== undefined ? progress.currentSnailDesign : 0;
Uses a ternary operator to load the avatar design if it exists, or default to 0 if the property was never saved (for older save files)
unlockedMapsCount = constrain(unlockedMapsCount, 1, maps.length);
Safety check: ensures the loaded unlockedMapsCount doesn't exceed the actual number of defined maps

📦 Key Variables

gameState string

Tracks which screen/mode the game is in: 'start', 'mapSelect', 'play', 'shop', 'admin', 'instructions', 'gameOver', 'mapUnlocked'. The draw() switch statement uses this to render the correct screen.

let gameState = 'start';
snail object (Snail instance)

The main player character. Contains position, velocity, size, avatar design, speed boost state, and inventory (pickaxe, key).

let snail; // Initialized in startGame()
food object (Food instance)

The red collectible item that increases the player's score. Respawns at a new random location each time it's collected.

let food; // Initialized in startGame()
obstacles array of Obstacle objects

An array of grey rectangular obstacles loaded from the current map's definition. Hitting an obstacle ends the game (unless god mode is on).

let obstacles = [];
score number

The player's current score (number of food items collected on the current map). Increments by 1 each time food is collected.

let score = 0;
candies array of Candy objects

An array of colorful spinning candies that grant temporary speed boosts when collected. Respawns after being collected.

let candies = [];
pickaxe object (Pickaxe instance)

A tool that can be picked up and used (by pressing E) to destroy nearby obstacles. Only one pickaxe exists on the map at a time.

let pickaxe;
keyItem object (Key instance) or null

A collectible key item (kept for future door mechanics or scoring). Respawns after being picked up.

let keyItem;
maps array of objects

An array of all six game levels, each containing a name, target score to unlock the next level, obstacle layout, and optional key position.

let maps = [];
currentMapIndex number

The index of the currently selected/playing map (0 for Map 1, 5 for Map 6). Used to load obstacles and determine which level to display.

let currentMapIndex = 0;
unlockedMapsCount number

How many maps the player has unlocked (1 to 6). Used to determine which maps appear as clickable in the map select screen. Saved to localStorage.

let unlockedMapsCount = 1;
currentSnailDesign number

The player's chosen snail avatar (0 for default brown, 1 for blue, 2 for striped). Saved to localStorage and used when creating new Snail objects.

let currentSnailDesign = 0;
godMode boolean

Admin cheat flag. When true, the snail is invincible to obstacles. Toggled via the Admin Panel.

let godMode = false;
permanentSpeedBoost boolean

Admin cheat flag. When true, the snail always moves 1.5x faster. Toggled via the Admin Panel.

let permanentSpeedBoost = false;

🔧 Potential Improvements (7)

Here are some ways this code could be enhanced:

BUG Snail.move()

Mouse control completely overrides arrow keys even when the mouse hasn't moved—any click keeps the snail following the cursor, making arrow key control unavailable.

💡 Change the condition to check if the mouse is near the snail or if a specific button is held: if (mouseIsPressed && dist(mouseX, mouseY, this.pos.x, this.pos.y) < 200) instead of just checking if the mouse position changed.

PERFORMANCE isSpotOccupied()

The respawn loops (in Food, Candy, Pickaxe, Key classes) call isSpotOccupied() up to 100 times each. On dense maps with many items, this creates O(n*m) collision checks, making spawn logic slow.

💡 Cache or optimize isSpotOccupied() by early-exiting for obvious collisions, or create a spatial grid that divides the canvas into zones to avoid checking distant obstacles.

STYLE mousePressed()

The function contains duplicate button-checking logic (rectangle collision tests) repeated 20+ times across different screen states, making it hard to maintain and easy to introduce bugs.

💡 Extract button-checking into a reusable helper function: function checkRectClick(x, y, w, h) { return mouseX > x && mouseX < x + w && mouseY > y && mouseY < y + h; } and use it throughout the function.

BUG drawGame() and updateGame()

The speed boost timer display uses ceil() to round up, but millis() can return negative time remaining briefly—if the speed boost expires mid-frame, timeRemaining becomes negative, showing nonsensical values.

💡 Add a safety check: let timeRemaining = max(0, ceil((snail.speedBoostTimer - millis()) / 1000)); to ensure it never displays negative seconds.

FEATURE Snail class & updateGame()

The pickaxe has no visual feedback or sound when it successfully destroys an obstacle—the player only sees the obstacle vanish silently.

💡 Add a visual effect (brief explosion circle) or flash the screen briefly when usePickaxe() succeeds, or draw a temporary particle effect at the destroyed obstacle's location.

STYLE defineMaps()

Map obstacles are defined with hardcoded normalized coordinates, making it hard to visualize the layout without running the game. Adding a comment with the approximate layout would help.

💡 Add ASCII art or a brief comment describing each map's layout: // Map 1: 3 obstacles forming a narrow corridor down the middle

BUG windowResized()

Rapidly resizing the window calls startGame() repeatedly, which resets the score and respawns all items—frustrating if the player is mid-game.

💡 Only call startGame() if the canvas size change is significant (e.g., if abs(width - oldWidth) > 100), or debounce the resize event to avoid calling startGame() more than once per 500ms.

🔄 Code Flow

Code flow showing preload, setup, draw, definemaps, startgame, updategame, drawgame, snail, keypressed, mousepressed, savegameprogress, loadgameprogress

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

graph TD start[Start] --> setup[setup] setup --> draw[draw loop] draw --> state-switch[Game State Dispatcher] state-switch -->|gameState = menu| menu-escape[Menu Escape to Home] state-switch -->|gameState = play| updategame[updateGame] state-switch -->|gameState = shop| mousepressed[mousePressed] updategame --> move-method[Move Method] move-method --> obstacle-collision[Obstacle Collision Detection] obstacle-collision -->|collision detected| updategame obstacle-collision -->|no collision| candy-loop[ candy-loop] candy-loop -->|candy collected| food-collision[Food Collection & Scoring] food-collision -->|food collected| updategame candy-loop -->|candy collected| updategame candy-loop -->|no candy collected| updategame updategame --> drawgame[drawGame] drawgame --> show-method[Show Method] show-method --> drawgame drawgame --> updategame click setup href "#fn-setup" click draw href "#fn-draw" click state-switch href "#sub-state-switch" click menu-escape href "#sub-menu-escape" click updategame href "#fn-updategame" click move-method href "#sub-move-method" click obstacle-collision href "#sub-obstacle-collision" click candy-loop href "#sub-candy-loop" click food-collision href "#sub-food-collision" click show-method href "#sub-show-method"

❓ Frequently Asked Questions

What visual elements does the Snail game sketch feature?

The Snail game sketch features a colorful snail character navigating through various maps filled with obstacles, food items, and speed boost candies.

How can players interact with the Snail game?

Players can control the snail's movement, collect food and candies, and navigate through obstacles while progressing through different maps.

What creative coding concepts are showcased in this snail game sketch?

The sketch demonstrates game state management, object-oriented programming through class definitions, and local storage for saving game progress.

Preview

Snail game (Remix) - p5.js creative coding sketch preview
Sketch Preview
Code flow diagram showing the structure of Snail game (Remix) - Code flow showing preload, setup, draw, definemaps, startgame, updategame, drawgame, snail, keypressed, mousepressed, savegameprogress, loadgameprogress
Code Flow Diagram