Snail game What it should be

This is a full multi-screen browser game where a player-controlled snail eats mint candies for points while avoiding obstacles, grabbing speed-boost treats, and using a limited-use pickaxe to smash through walls. It includes a title menu, map-select screen, instructions, an avatar shop with a timed 'secret snail' unlock, and an admin cheat panel, with progress persisted via localStorage.

🧪 Try This!

Experiment with the code by making these changes:

  1. Make the snail zoom — Raising the snail's base speed makes every map feel much faster and more frantic to navigate.
  2. Award more points per mint — Increasing the score awarded per mint makes the player unlock the next map much faster.
  3. Flood the map with candy — Spawning far more candies turns the level into a speed-boost frenzy right from the start.
  4. Extend the speed boost — Making the candy's speed boost last much longer keeps the snail fast for a big chunk of the level.
Prefer the full editor? Open it there →

📖 About This Sketch

This sketch is a complete arcade-style game built entirely in p5.js: a snail steered with WASD or arrow keys eats mint candies for score, dodges grey rectangle obstacles, grabs spinning speed-boost candies, and can pick up a pickaxe to smash obstacles out of the way. Layered on top of the gameplay is a full menu system - a title screen, map-select screen, instructions page, avatar shop, and an admin/cheat panel - all driven by a single 'gameState' variable and a big switch statement inside draw(). It uses classes (Obstacle, Mint, Candy, Pickaxe, Key, Snail) for object-oriented organization, vector math for movement, circle-rectangle collision detection, p5.Oscillator/p5.Envelope for a 'yum' sound effect, and localStorage to remember which maps and avatars the player has unlocked between visits.

The code is organized around one giant state machine: gameState determines which draw function runs each frame (drawStartScreen, drawMapSelectScreen, updateGame/drawGame, etc.), and mousePressed()/keyPressed() route input to whichever screen is currently active. By studying it you'll learn how to structure a multi-screen p5.js application, how class inheritance can be used so items like Candy and Pickaxe reuse a shared Mint class's collision and respawn logic, how to fake simple physics-style pickups with distance checks, and how to persist game progress across browser sessions with localStorage.

⚙️ How It Works

  1. When the page loads, preload() defines all six maps and their obstacle layouts, then attempts to load saved progress (unlocked maps, chosen avatar, secret-snail status) from localStorage before setup() creates the canvas and starts a triangle-wave oscillator used for the 'yum' eating sound.
  2. Every frame, draw() clears the background and then checks the global gameState variable in a switch statement, calling a different function depending on whether the player is on the start menu, map-select screen, shop, admin panel, or actually playing.
  3. During 'play' state, updateGame() moves the snail based on held keys, checks distance-based collisions against the mint, obstacles, candies, the pickaxe, and the key, and updateGame() also advances timers for animations like blinking, walking bounce, eating mouth, and the temporary speed boost.
  4. drawGame() then renders everything - mint, obstacles, candies, pickaxe, key, and the snail itself (whose appearance depends on currentSnailDesign) - plus HUD text for score, map name, and any active power-up banners.
  5. mousePressed() and keyPressed() detect clicks/keys against the current screen's button positions to transition gameState between menus, start a new game via startGame(), or trigger gameplay actions like using the pickaxe (the 'E' key).
  6. Reaching a map's target score calls unlockNextMap(), which increases unlockedMapsCount and immediately calls saveGameProgress() to write the new unlock (and avatar/secret-snail choices) to localStorage so progress survives a page reload.

🎓 Concepts You'll Learn

Game state machine (switch statement)Class inheritance (Mint -> Candy/Pickaxe/Key/Chest)Circle-rectangle collision detectionp5.Vector for position and velocitylocalStorage persistencep5.Oscillator and p5.Envelope for soundUI hit-testing for buttons with mouseX/mouseY

📝 Code Breakdown

preload()

preload() runs before setup() in p5.js and is meant for loading assets. Here it's repurposed to prepare data (maps) and restore saved state before the canvas even exists.

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 of level data (obstacles, target scores) before anything else needs it.
loadGameProgress();
Reads any previously saved progress from the browser's localStorage so unlocked maps/avatars persist across visits.

setup()

setup() runs once. Here it prepares both the canvas and an audio synthesis chain (oscillator + envelope) that will be reused every time the snail eats a mint.

function setup() {
  createCanvas(windowWidth, windowHeight);
  // Game now starts in the 'start' (menu) state.

  // Initialize yum sound
  yumSound = new p5.Oscillator();
  yumSound.setType('triangle'); // A triangle wave can be less harsh than sine
  yumSound.freq(440);
  yumSound.amp(0); // Start with 0 amplitude
  yumSound.start();

  yumEnvelope = new p5.Envelope();
  yumEnvelope.setADSR(0.01, 0.1, 0.5, 0.1); // Attack, Decay, Sustain, Release
  yumEnvelope.setRange(0.5, 0); // Max amp, Min amp
}
Line-by-line explanation (7 lines)
createCanvas(windowWidth, windowHeight);
Makes the canvas fill the entire browser window instead of a fixed size.
yumSound = new p5.Oscillator();
Creates a tone generator from the p5.sound library that will produce the 'yum' eating sound.
yumSound.setType('triangle');
Chooses a triangle waveform, which sounds softer than a default sine or square wave.
yumSound.amp(0);
Starts the oscillator silent - its volume will be controlled by the envelope instead.
yumSound.start();
Starts the oscillator running continuously in the background (silent until the envelope opens it up).
yumEnvelope = new p5.Envelope();
An envelope shapes volume over time (Attack-Decay-Sustain-Release) to make a short percussive blip instead of a constant tone.
yumEnvelope.setADSR(0.01, 0.1, 0.5, 0.1);
Sets how quickly the sound rises, decays, sustains, and releases - a fast attack and short release make it feel like a quick 'yum'.

triggerYumSound()

This function demonstrates chaining setTimeout() calls to sequence sound effects over time without blocking the draw loop, since JavaScript timers run independently of p5.js's frame updates.

function triggerYumSound() {
  // Play three "yum" sounds with a short delay
  yumSound.freq(440);
  yumEnvelope.play(yumSound);

  setTimeout(() => {
    yumSound.freq(490); // Slightly higher pitch for next "yum"
    yumEnvelope.play(yumSound);
  }, yumDelay);

  setTimeout(() => {
    yumSound.freq(550); // Even higher pitch for third "yum"
    yumEnvelope.play(yumSound);
  }, yumDelay * 2);
}
Line-by-line explanation (3 lines)

🔧 Subcomponents:

calculation Second Yum Timing setTimeout(() => { ... }, yumDelay);

Schedules the second, higher-pitched yum sound to play after a short delay

calculation Third Yum Timing setTimeout(() => { ... }, yumDelay * 2);

Schedules the third, highest-pitched yum sound to play after twice the delay

yumSound.freq(440);
Sets the oscillator's pitch to 440Hz (musical note A4) for the first 'yum'.
yumEnvelope.play(yumSound);
Triggers the ADSR envelope, briefly opening the volume to produce one short blip.
setTimeout(() => { ... }, yumDelay);
Waits yumDelay milliseconds, then raises the pitch and plays again - creating a rising 'yum-yum-yum' effect.

defineMaps()

This function is pure data setup - it demonstrates storing level design as an array of plain objects, which is a common pattern for making games data-driven instead of hard-coding drawing calls for each level.

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 }, // Key now top-left, more accessible
    },
    {
      name: "Map 2: Rocky Foothills",
      targetScore: 10, // Score needed to unlock Map 3
      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, // Score needed to unlock Map 4
      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, // Score needed to unlock Map 5
      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, // Last map, no more to unlock
      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, // Score needed to unlock the next map (or set to Infinity if last)
      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)

🔧 Subcomponents:

calculation Relative Obstacle Coordinates { x: 0.2, y: 0.3, w: 100, h: 50 }

Obstacle x/y are stored as fractions (0.0-1.0) of the canvas so maps automatically adapt to any window size

maps = [ ... ];
Builds an array of level objects, each with a name, a targetScore needed to unlock the next map, and a list of obstacle rectangles.
{ x: 0.2, y: 0.3, w: 100, h: 50 }
x and y are stored as fractions of the canvas width/height (not pixels) so the layout scales correctly if the browser window is resized.
targetScore: Infinity, // Last map, no more to unlock
Using Infinity as the target score means this map can never be 'beaten' to unlock a next one - it's the final map.
unlockedMapsCount = constrain(unlockedMapsCount, 1, maps.length);
Clamps the unlocked map count so it never exceeds how many maps actually exist, protecting against corrupted save data.

draw()

draw() is intentionally tiny here - it delegates almost everything to state-specific functions. This 'state machine' pattern is the backbone of nearly every menu-driven game and keeps draw() easy to read even as the game grows many screens.

🔬 This case runs both the game logic and the drawing every frame. What happens if you comment out updateGame() but leave drawGame() - does the game freeze completely, or does it still animate?

    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 Game State Router switch (gameState) { ... }

Chooses which screen-drawing function to call based on the current gameState string

background(220); // Clear background each frame
Repaints the whole canvas a light grey every frame so nothing from the previous frame lingers.
switch (gameState) {
Looks at the single global string gameState and branches to the matching case - this is the entire game's screen router.
case 'play': updateGame(); drawGame(); break;
During actual gameplay, both the logic update (movement, collisions) and the rendering happen every frame, in that order.

startGame()

startGame() is the game's 'reset button' - it rebuilds every game object from scratch based on the currently selected map and avatar, which is why it can safely be called both when starting a new map and when restarting after death.

🔬 This loop spawns 3 speed-boost candies. What happens if you change 3 to 15 - does the map feel too easy?

  for(let i = 0; i < 3; i++) { // You can adjust the number of candies
    candies.push(new Candy());
    candies[i].respawn();
  }
function startGame() {
  score = 0;
  snail = new Snail(width / 2, height / 2, currentSnailDesign); // Create snail at center with chosen design
  mint = new Mint(); // Create initial mint (formerly food)
  mint.respawn(); // Ensure mint 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 mint
  for(let i = 0; i < 3; i++) { // You can adjust the number of candies
    candies.push(new Candy());
    candies[i].respawn();
  }

  // Initialize pickaxe item on the map
  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
  snail.pickaxeUses = 0; // Snail starts with 0 pickaxe uses

  // Reset animation states
  snail.isBlinking = false;
  snail.blinkTimer = 0;
  snail.nextBlinkTime = millis() + random(2000, 5000);
  snail.walkOffset = 0;
  snail.walkRotation = 0;
  snail.isEating = false; // Reset eating state

  gameState = 'play'; // Set game state to play
}
Line-by-line explanation (6 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 Obstacle objects positioned in pixels

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

Creates three Candy objects and places each at a random non-overlapping spot

score = 0;
Resets the score counter every time a map is (re)started.
snail = new Snail(width / 2, height / 2, currentSnailDesign);
Creates a brand-new Snail object in the center of the canvas using whichever avatar design the player picked in the shop.
obstacles.push(new Obstacle(width * obsData.x, height * obsData.y, obsData.w, obsData.h));
Multiplies the map's relative x/y (0 to 1) by the current canvas width/height, so obstacles land in the right place no matter the window size.
for(let i = 0; i < 3; i++) { ... }
Loops three times to create three Candy speed-boost items and place each at a valid random spot via respawn().
pickaxe = new Pickaxe(); pickaxe.respawn();
Creates a single pickaxe pickup and drops it somewhere on the map that doesn't overlap other objects.
gameState = 'play';
Switches the state machine into gameplay mode, so draw() will start calling updateGame()/drawGame() next frame.

isSpotOccupied()

This helper is called by every item's respawn() method (Mint, Candy, Pickaxe, Key) to keep new spawn points from overlapping other game objects, avoiding unfair 'spawned inside a wall' situations.

function isSpotOccupied(x, y, size, excludeItem = null) {
  // Check snail
  if (snail && 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 (only if not held by snail and not the item itself)
  if (pickaxe && !snail.hasPickaxe && 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 (3 lines)

🔧 Subcomponents:

for-loop Circle-Rectangle Overlap Check for (let obs of obstacles) { ... }

Finds the closest point on each obstacle rectangle to (x,y) and tests if that distance is within the item's radius

if (snail && dist(x, y, snail.pos.x, snail.pos.y) < size / 2 + snail.size / 2) {
Only checks the snail if it exists yet, then tests whether the candidate spot is too close to the snail's current position.
if (x < obs.x) testX = obs.x; else if (x > obs.x + obs.w) testX = obs.x + obs.w;
Clamps the test point to the nearest edge of the obstacle rectangle - this is the standard trick for circle-vs-rectangle distance checks.
if (distance <= size / 2) { return true; }
If the closest point on the obstacle is nearer than the item's radius, the spot is considered occupied and the function returns immediately.

updateGame()

updateGame() is the heart of the gameplay loop - it's a sequence of independent collision checks (mint, obstacles, candies, pickaxe, key) each using the same distance-based math, showing how a handful of simple checks combine to create a full arcade game.

🔬 This distance check decides how close the snail must get to 'eat' the mint. What happens if you replace snail.size / 2 + mint.size / 2 with a big fixed number like 150?

  if (dist(snail.pos.x, snail.pos.y, mint.x, mint.y) < snail.size / 2 + mint.size / 2) {
    score++; // Increase score
    mint.respawn(); // Move mint to a new random location
function updateGame() {
  snail.move(); // Move the snail
  snail.checkBoundaries(); // Keep snail within canvas

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

    // Trigger sound and mouth animation
    triggerYumSound();
    snail.isEating = true;
    snail.eatingTimer = millis();

    // 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 item (on the ground)
  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
    snail.pickaxeUses = snail.maxPickaxeUses; // Reset pickaxe uses
    console.log("Snail picked up a pickaxe! Uses:", snail.pickaxeUses);
  }

  // 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();

  // Update eating animation timer
  if (snail.isEating && millis() > snail.eatingTimer + snail.eatingDuration) {
    snail.isEating = false; // End eating animation
  }
}
Line-by-line explanation (6 lines)

🔧 Subcomponents:

conditional Mint Collision & Scoring if (dist(snail.pos.x, snail.pos.y, mint.x, mint.y) < snail.size / 2 + mint.size / 2) { ... }

Detects when the snail reaches the mint, increases score, respawns the mint, and checks for map unlocks

for-loop Obstacle Collision Check for (let obs of obstacles) { if (snail.collidesWith(obs)) { ... } }

Ends the game the moment the snail touches any obstacle, unless God Mode is active

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

Loops backward through candies so items can be safely removed/respawned mid-loop, and activates the speed boost on pickup

snail.move();
Reads the currently held keys and updates the snail's velocity and position.
snail.checkBoundaries();
Clamps the snail's position so it can never move off the edge of the canvas.
if (dist(snail.pos.x, snail.pos.y, mint.x, mint.y) < snail.size / 2 + mint.size / 2) {
A simple circle-circle collision test: if the distance between snail and mint centers is less than the sum of their radii, they're touching.
if (currentMapIndex < maps.length - 1 && score >= maps[currentMapIndex].targetScore) {
Checks whether the player has hit the score needed to unlock the next map, and that there is a next map to unlock.
for (let i = candies.length - 1; i >= 0; i--) {
Iterating backward through the candies array is a safe pattern when you might remove (splice) items during the loop.
if (snail.isEating && millis() > snail.eatingTimer + snail.eatingDuration) {
Checks whether enough time has passed since the eating animation started, and if so, ends it.

drawGame()

drawGame() is purely about rendering - it never changes game state, following a clean separation between updateGame() (logic) and drawGame() (visuals) that makes the code easier to debug.

🔬 This banner only appears while speedBoostActive is true. What happens if you change ceil() to floor() - does the countdown ever briefly show '0s' before disappearing?

  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);
  }
function drawGame() {
  // Draw mint (formerly food)
  mint.show();

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

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

  // Draw pickaxe item on the map if the snail doesn't have it
  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);
  }

  // Display Limited Item notification
  if (!secretSnailUnlocked && millis() > secretSnailAvailabilityTimer && millis() < secretSnailAvailabilityTimer + secretSnailDuration) {
    fill(255, 100, 0); // Orange text
    textSize(20);
    textAlign(LEFT);
    text("Limited Item! Secret Snail available!", 10, height - 20);
  }
}
Line-by-line explanation (5 lines)

🔧 Subcomponents:

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

Renders every obstacle rectangle for the current map

conditional Speed Boost Banner if (snail.speedBoostActive) { ... }

Shows a green countdown message while the candy speed boost is active

mint.show();
Draws the mint candy target at its current position.
for (let obs of obstacles) { obs.show(); }
Loops through every obstacle in the current map and draws each grey rectangle.
if (pickaxe && !snail.hasPickaxe) { pickaxe.show(); }
Only draws the pickaxe on the ground if it exists and the snail hasn't already picked it up.
let timeRemaining = ceil((snail.speedBoostTimer - millis()) / 1000);
Converts the remaining milliseconds of the speed boost into whole seconds for a clean countdown display.
if (!secretSnailUnlocked && millis() > secretSnailAvailabilityTimer && millis() < secretSnailAvailabilityTimer + secretSnailDuration) {
Checks a three-part time window: the secret snail hasn't been claimed yet, its reveal delay has passed, and its limited-time availability hasn't expired.

drawStartScreen()

This function shows the classic p5.js push()/translate()/pop() pattern for drawing multiple similarly-shaped UI buttons without recalculating absolute coordinates for every rect() and text() call.

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;

  // Check if Secret Snail is available to update Avatar Shop button text
  let isSecretSnailAvailable = !secretSnailUnlocked && millis() > secretSnailAvailabilityTimer && millis() < secretSnailAvailabilityTimer + secretSnailDuration;
  let avatarShopButtonText = isSecretSnailAvailable ? "Avatar Shop (Limited Item!)" : "Avatar Shop";

  // 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 Button
  push();
  translate(width / 2, startY + buttonHeight + spacing + buttonHeight / 2);
  fill(50, 100, 150); // Blue
  noStroke();
  rect(-buttonWidth / 2, -buttonHeight / 2, buttonWidth, buttonHeight, 10);
  fill(255);
  textSize(32);
  text("How to Play", 0, 0);
  pop();

  // Avatar Shop Button (replaces old Change Avatar)
  push();
  translate(width / 2, startY + 2 * (buttonHeight + spacing) + buttonHeight / 2);
  fill(150, 50, 150); // Purple
  noStroke();
  rect(-buttonWidth / 2, -buttonHeight / 2, buttonWidth, buttonHeight, 10);
  fill(255);
  textSize(24);
  text(avatarShopButtonText, 0, 0);
  pop();

  // Admin Panel Button
  push();
  translate(width / 2, startY + 3 * (buttonHeight + spacing) + buttonHeight / 2);
  fill(200, 100, 0); // Orange
  noStroke();
  rect(-buttonWidth / 2, -buttonHeight / 2, buttonWidth, buttonHeight, 10);
  fill(255);
  textSize(24);
  text("Admin Panel", 0, 0);
  pop();

  // Reset Progress Button
  push();
  translate(width / 2, startY + 4 * (buttonHeight + spacing) + buttonHeight / 2);
  fill(200, 0, 0); // Red
  noStroke();
  rect(-buttonWidth / 2, -buttonHeight / 2, buttonWidth, buttonHeight, 10);
  fill(255);
  textSize(24);
  text("Reset Game Progress", 0, 0);
  pop();
}
Line-by-line explanation (4 lines)

🔧 Subcomponents:

calculation Secret Snail Availability Flag let isSecretSnailAvailable = !secretSnailUnlocked && millis() > secretSnailAvailabilityTimer && millis() < secretSnailAvailabilityTimer + secretSnailDuration;

Determines whether to show the '(Limited Item!)' label on the Avatar Shop button

let startY = height / 2 - 80;
Anchors all the menu buttons relative to the vertical center of the screen so they stay centered at any window size.
push(); translate(width / 2, startY + buttonHeight / 2);
push()/translate() move the drawing origin to each button's center, so the rect() and text() calls below can use simple local coordinates like 0,0.
rect(-buttonWidth / 2, -buttonHeight / 2, buttonWidth, buttonHeight, 10);
Draws a rounded rectangle (the 5th argument is corner radius) centered on the translated origin.
pop();
Restores the previous drawing state (position, fill, etc.) so the next button starts fresh.

drawMapSelectScreen()

Notice this function checks mouseX/mouseY for clicks directly inside a draw-phase function, which is unusual - normally click handling belongs in mousePressed(). This is flagged in the improvements section below.

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 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;
      }
    }

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

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

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

    if (i >= unlockedMapsCount) {
      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 and Handle Each Map Button for (let i = 0; i < maps.length; i++) { ... }

Draws one button per map, greying out locked ones, and also handles clicks directly inside the draw function

if (mouseX > buttonX && mouseX < buttonX + buttonWidth && mouseY > buttonY && mouseY < buttonY + buttonHeight) {
A simple rectangle hit-test: checks if the current mouse position falls within this button's bounds.
if (i < unlockedMapsCount) { currentMapIndex = i; startGame(); return; }
Only lets the player select maps that have actually been unlocked; clicking a locked map does nothing.
fill(i < unlockedMapsCount ? 50 : 150); // Green for unlocked, grey for locked
Uses a ternary expression to pick between a darker (unlocked) or lighter (locked) fill color in one line.

drawInstructionsScreen()

This is mostly static text laid out with manual pixel offsets - a good example of how quickly plain text() calls can become hard to maintain, and a candidate for extracting a small helper function that draws a line of text and returns the next y position.

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 little mint treats to increase your score.", width * 0.1, height * 0.3); // Updated text
  text("Avoid grey obstacles – 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("- W A S D Keys: Move the snail up, left, down, or right.", width * 0.1, height * 0.45 + 30);
  text("- Arrow Keys: Also move the snail up, down, left, or right.", width * 0.1, height * 0.45 + 60);

  text("Special Items:", width * 0.1, height * 0.7);
  text("- Candies: Collect colorful candies for a temporary speed boost!", width * 0.1, height * 0.7 + 30);
  text("- Pickaxe: Pick up the pickaxe icon. It gives you " + snail.maxPickaxeUses + " uses. Press 'E' to remove nearby obstacles!", width * 0.1, height * 0.7 + 60);
  text("The pickaxe item respawns only when you run out of uses.", width * 0.1, height * 0.7 + 90);
  text("- Secret Snail: A rare avatar that appears for a limited time! Check the Avatar Shop!", width * 0.1, height * 0.7 + 120);

  // Back to Menu Button
  let buttonY = height * 0.85; // Adjusted Y position
  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 (2 lines)
text("- Pickaxe: Pick up the pickaxe icon. It gives you " + snail.maxPickaxeUses + " uses. Press 'E' to remove nearby obstacles!", width * 0.1, height * 0.7 + 60);
Reads a live value straight from the snail object to keep the instructions text accurate even if maxPickaxeUses is tuned elsewhere - but this will crash if snail hasn't been created yet (see improvements).
let buttonY = height * 0.85;
Positions the Back to Menu button near the bottom of the screen using a percentage of the canvas height so it adapts to window size.

drawShopScreen()

This function demonstrates using millis()-based time windows to gate a limited-time in-game reward, and how a single variable (secretSnailUnlocked) combined with two timestamps can express four distinct UI states.

🔬 This chain of else-if statements decides which status text to show. What happens if you swap the order so the 'Expired' check comes before the 'Claimed' check?

      if (secretSnailUnlocked) {
        stockText = "Claimed";
        buttonColor = color(50, 150, 50); // Green if claimed
      } else if (millis() > secretSnailAvailabilityTimer + secretSnailDuration) {
        stockText = "Expired";
        buttonColor = color(200, 50, 50); // Red if expired
      } else if (millis() > secretSnailAvailabilityTimer) {
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 < 4; i++) { // Assuming 4 designs (0, 1, 2, 3 for secret snail)
    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();

    if (i === 3) { // This is the secret snail slot
      let stockText = "Secret Snail";
      let buttonColor = color(150); // Grey by default

      if (secretSnailUnlocked) {
        stockText = "Claimed";
        buttonColor = color(50, 150, 50); // Green if claimed
      } else if (millis() > secretSnailAvailabilityTimer + secretSnailDuration) {
        stockText = "Expired";
        buttonColor = color(200, 50, 50); // Red if expired
      } else if (millis() > secretSnailAvailabilityTimer) {
        // Item is available
        stockText = "Claim Now! (Free)";
        buttonColor = color(0, 150, 200); // Blue if available
      } else {
        // Item not yet available
        let timeToAvailable = ceil((secretSnailAvailabilityTimer - millis()) / 1000);
        stockText = "Available in " + timeToAvailable + "s";
      }

      fill(buttonColor);
      noStroke();
      rect(x - avatarSize * 0.8 / 2, y + avatarSize * 0.8 / 2 + 10, avatarSize * 0.8, 30, 5);
      fill(255);
      textSize(16);
      textAlign(CENTER, CENTER);
      text(stockText, x, y + avatarSize * 0.8 / 2 + 25);
    }
  }

  // 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 Each Avatar Slot for (let i = 0; i < 4; i++) { ... }

Draws all four avatar previews, highlighting the currently selected one and adding special status text for the secret slot

conditional Secret Snail Status Text if (secretSnailUnlocked) { ... } else if (millis() > secretSnailAvailabilityTimer + secretSnailDuration) { ... }

Chooses which of four possible status labels (Claimed / Expired / Claim Now / countdown) to show for the limited-time avatar

let startX = width / 2 - (avatarSize * 3 + spacing * 2) / 2 + avatarSize / 2;
Calculates the x position of the first avatar so that all 4 avatars end up evenly centered as a group on screen.
if (i === currentSnailDesign) { noFill(); stroke(255, 200, 0);
Draws a gold ring around whichever avatar is currently selected, giving clear visual feedback.
let timeToAvailable = ceil((secretSnailAvailabilityTimer - millis()) / 1000);
Calculates a live countdown in seconds until the secret snail becomes claimable.

drawSnailAvatar()

This is a smaller, static-pose version of the same drawing logic used in Snail.show() - notice how much code is duplicated between the two. This duplication is flagged in the improvements section as a good refactoring opportunity.

🔬 This loop layers 20 shrinking ellipses to fake a gradient. What happens visually if you lower the loop count from 20 to 5?

      for (let i = 0; i < 20; i++) {
        let c = lerpColor(color(100, 100, 255), color(200, 100, 255), i / 20);
        fill(c);
        ellipse(0, -size * 0.1, size - i * (size / 20), (size * 1.2) - i * (size * 1.2 / 20));
      }
function drawSnailAvatar(x, y, size, designIndex) {
  push();
  translate(x, y);

  switch (designIndex) {
    case 0: // Default Snail
      fill(139, 69, 19); // Brown
      ellipse(0, -size * 0.1, size, size * 1.2);
      fill(124, 252, 0); // Green
      ellipse(0, size * 0.25, size * 1.5, size * 0.7);
      fill(0);
      ellipse(-size * 0.4, -size * 0.4, size * 0.2, size * 0.2);
      ellipse(size * 0.4, -size * 0.4, size * 0.2, size * 0.2);
      fill(255);
      ellipse(-size * 0.4, -size * 0.4, size * 0.1, size * 0.1);
      ellipse(size * 0.4, -size * 0.4, size * 0.1, size * 0.1);
      fill(255, 0, 0);
      ellipse(0, size * 0.1, size * 0.3, size * 0.2);
      break;
    case 1: // Blue Snail
      fill(50, 50, 200);
      ellipse(0, -size * 0.1, size * 1.1, size * 1.3);
      fill(100, 100, 255);
      ellipse(0, size * 0.25, size * 1.4, size * 0.8);
      fill(255);
      ellipse(-size * 0.3, -size * 0.3, size * 0.25, size * 0.25);
      ellipse(size * 0.3, -size * 0.3, size * 0.25, size * 0.25);
      fill(0);
      ellipse(-size * 0.3, -size * 0.3, size * 0.1, size * 0.1);
      ellipse(size * 0.3, -size * 0.3, size * 0.1, size * 0.1);
      fill(100);
      rect(0 - size * 0.4 / 2, size * 0.1, size * 0.4, size * 0.1);
      break;
    case 2: // Striped Snail
      fill(139, 69, 19);
      ellipse(0, -size * 0.1, size, size * 1.2);
      fill(255, 200, 0);
      rect(-size / 2, -size * 0.375, size, size * 0.05);
      rect(-size / 2, -size * 0.125, size, size * 0.05);
      rect(-size / 2, size * 0.125, size, size * 0.05);
      fill(200, 100, 50);
      ellipse(0, size * 0.25, size * 1.5, size * 0.7);
      fill(0);
      ellipse(-size * 0.4, -size * 0.4, size * 0.15, size * 0.15);
      ellipse(size * 0.4, -size * 0.4, size * 0.15, size * 0.15);
      fill(255, 0, 0);
      ellipse(-size * 0.4, -size * 0.4, size * 0.05, size * 0.05);
      ellipse(size * 0.4, -size * 0.4, size * 0.05, size * 0.05);
      fill(0);
      ellipse(0, size * 0.15, size * 0.2, size * 0.1, 0, PI);
      break;
    case 3: // Secret Snail (New Avatar)
      noStroke();
      for (let i = 0; i < 20; i++) {
        let c = lerpColor(color(100, 100, 255), color(200, 100, 255), i / 20);
        fill(c);
        ellipse(0, -size * 0.1, size - i * (size / 20), (size * 1.2) - i * (size * 1.2 / 20));
      }
      fill(255);
      ellipse(-size * 0.2, -size * 0.3, size * 0.1, size * 0.1);
      ellipse(size * 0.3, -size * 0.2, size * 0.08, size * 0.08);
      ellipse(-size * 0.1, size * 0.1, size * 0.06, size * 0.06);
      fill(255, 255, 200, 150);
      ellipse(0, size * 0.25, size * 1.5, size * 0.7);
      fill(255, 255, 255, 200);
      ellipse(0, size * 0.25, size * 1.2, size * 0.6);
      fill(255, 255, 0);
      ellipse(-size * 0.4, -size * 0.4, size * 0.3, size * 0.3);
      ellipse(size * 0.4, -size * 0.4, size * 0.3, size * 0.3);
      fill(0);
      ellipse(-size * 0.4, -size * 0.4, size * 0.1, size * 0.1);
      ellipse(size * 0.4, -size * 0.4, size * 0.1, size * 0.1);
      fill(0);
      ellipse(0, size * 0.15, size * 0.2, size * 0.1);
      break;
  }

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

🔧 Subcomponents:

switch-case Avatar Design Selector switch (designIndex) { case 0: ... case 3: ... }

Draws a completely different shell/body/eye combination depending on which avatar design index is passed in

for-loop Secret Snail Gradient Shell for (let i = 0; i < 20; i++) { let c = lerpColor(...); ... }

Draws 20 progressively smaller, color-blended ellipses to fake a smooth gradient on the secret snail's shell

translate(x, y);
Moves the drawing origin to the avatar's center so all shapes below can be drawn relative to (0,0).
switch (designIndex) {
Selects which set of shapes to draw based on the numeric avatar design index (0-3).
let c = lerpColor(color(100, 100, 255), color(200, 100, 255), i / 20);
lerpColor blends smoothly between two colors; dividing i by 20 produces a value from 0 to ~1 across the loop, creating a gradient effect.

drawAdminScreen()

This screen only draws buttons - the actual toggling logic lives in mousePressed(). It's a good example of keeping visual state (what color/label to show) driven directly by the same global variables that gameplay code checks.

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;

  // Unlock All Maps Button
  push();
  translate(width / 2, startY + buttonHeight / 2);
  fill(50, 50, 150); // Dark Blue
  noStroke();
  rect(-buttonWidth / 2, -buttonHeight / 2, buttonWidth, buttonHeight, 10);
  fill(255);
  textSize(28);
  text("Unlock All Maps", 0, 0);
  pop();

  // 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();

  // Toggle Permanent Speed Boost Button
  push();
  translate(width / 2, startY + 2 * (buttonHeight + spacing) + buttonHeight / 2);
  fill(permanentSpeedBoost ? 50 : 100, permanentSpeedBoost ? 50 : 100, permanentSpeedBoost ? 200 : 100); // Blue if active, grey otherwise
  noStroke();
  rect(-buttonWidth / 2, -buttonHeight / 2, buttonWidth, buttonHeight, 10);
  fill(255);
  textSize(28);
  text("Perma Boost: " + (permanentSpeedBoost ? "ON" : "OFF"), 0, 0);
  pop();

  // Add 100 Score Button
  push();
  translate(width / 2, startY + 3 * (buttonHeight + spacing) + buttonHeight / 2);
  fill(50, 150, 50); // Green
  noStroke();
  rect(-buttonWidth / 2, -buttonHeight / 2, buttonWidth, buttonHeight, 10);
  fill(255);
  textSize(28);
  text("Add 100 Score", 0, 0);
  pop();

  // Back to Menu Button
  push();
  translate(width / 2, startY + 4 * (buttonHeight + spacing) + buttonHeight / 2);
  fill(200, 100, 0); // Orange
  noStroke();
  rect(-buttonWidth / 2, -buttonHeight / 2, buttonWidth, buttonHeight, 10);
  fill(255);
  textSize(32);
  text("Back to Menu", 0, 0);
  pop();
}
Line-by-line explanation (2 lines)
fill(godMode ? 200 : 100, godMode ? 50 : 100, godMode ? 50 : 100); // Red if active, grey otherwise
Uses three ternary expressions to build an RGB color that turns red when God Mode is on and grey when it's off, all in one fill() call.
text("God Mode: " + (godMode ? "ON" : "OFF"), 0, 0);
Concatenates a live boolean state into the button's label text so it always reflects the current toggle state.

drawMapUnlockedScreen()

A simple transitional screen shown briefly whenever the player hits a map's target score - handled entirely with text() calls and no buttons, since any key or click advances the state.

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 (1 lines)
text("You unlocked: " + maps[currentMapIndex].name, width / 2, height / 2);
Displays the name of the map the player just unlocked by reading it straight from the maps array.

drawGameOverScreen()

Notice this screen's instructions reference click zones (like 'Select a Map') that are actually handled by hardcoded Y-coordinate ranges in mousePressed(), rather than drawn as real buttons - a fragile pattern worth improving.

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 (1 lines)
text("Final Score: " + score, width / 2, height / 2);
Displays the score the player had when they hit an obstacle, read directly from the global score variable.

keyPressed()

keyPressed() is a built-in p5.js function that fires once per key press (not every frame like keyIsDown checks in Snail.move()), making it ideal for one-shot actions like using an item or dismissing a screen.

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.pickaxeUses > 0) {
      snail.usePickaxe(); // Use the pickaxe
    } else {
      console.log("No pickaxe to use or no uses left!");
    }
  }
}
Line-by-line explanation (3 lines)

🔧 Subcomponents:

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

Triggers the pickaxe's obstacle-smashing ability when the player presses E during gameplay

if (gameState === 'mapUnlocked' || gameState === 'gameOver' || gameState === 'instructions' || gameState === 'shop' || gameState === 'admin') {
A single condition covers five different screens, all of which should return to the start menu on any keypress.
} else if (gameState === 'play' && (key === 'e' || key === 'E') ) {
Checks both lowercase and uppercase 'E' so the shortcut works regardless of caps lock or shift state.
if (snail.hasPickaxe && snail.pickaxeUses > 0) { snail.usePickaxe();
Only calls usePickaxe() if the snail actually has one with uses remaining, preventing wasted calls.

mousePressed()

mousePressed() is p5.js's built-in click handler. This function shows the brute-force way to build clickable UI: manually computing each button's rectangle and comparing it against mouseX/mouseY - functional, but repetitive enough that a reusable 'button' helper (see improvements) would simplify it a lot.

🔬 What happens if you delete the 'return;' at the end of this block? Since the next buttons are checked with 'else if'-free separate 'if' statements below, could clicking God Mode also accidentally trigger the Perma Boost button underneath it?

    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;
    }
function mousePressed() {
  // Start audio context on first mouse press (required by web audio APIs)
  if (getAudioContext().state !== 'running') {
    userStartAudio();
  }

  if (gameState === 'start') {
    let startY = height / 2 - 80;
    let buttonHeight = 50;
    let spacing = 15;
    let buttonWidth = 300;
    let buttonX = width / 2 - buttonWidth / 2;

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

    let howToPlayY = startY + buttonHeight + spacing;
    if (mouseX > buttonX && mouseX < buttonX + buttonWidth &&
        mouseY > howToPlayY && mouseY < howToPlayY + buttonHeight) {
      gameState = 'instructions';
      return;
    }

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

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

    let resetY = startY + 4 * (buttonHeight + spacing);
    if (mouseX > buttonX && mouseX < buttonX + buttonWidth &&
        mouseY > resetY && mouseY < resetY + buttonHeight) {
      resetGameProgress();
      return;
    }
  }
  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();
          return;
        }
      }
    }
  }
  else if (gameState === 'instructions') {
    let buttonY = height * 0.85;
    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 === 'shop') {
    let avatarSize = 100;
    let spacing = 50;
    let startX = width / 2 - (avatarSize * 3 + spacing * 2) / 2 + avatarSize / 2;
    let avatarY = height * 0.5;

    let secretSnailX = startX + 3 * (avatarSize + spacing);
    let secretSnailY = avatarY + avatarSize * 0.8 / 2 + 10;
    let secretSnailButtonWidth = avatarSize * 0.8;
    let secretSnailButtonHeight = 30;

    if (mouseX > secretSnailX - secretSnailButtonWidth / 2 && mouseX < secretSnailX + secretSnailButtonWidth / 2 &&
        mouseY > secretSnailY && mouseY < secretSnailY + secretSnailButtonHeight) {
      if (!secretSnailUnlocked && millis() > secretSnailAvailabilityTimer && millis() < secretSnailAvailabilityTimer + secretSnailDuration) {
        changeSnailAvatar(3);
        secretSnailUnlocked = true;
        saveGameProgress();
        console.log("Secret Snail claimed!");
        return;
      }
    }

    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);
        return;
      }
    }

    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;

    if (mouseX > buttonX && mouseX < buttonX + buttonWidth &&
        mouseY > startY && mouseY < startY + buttonHeight) {
      unlockAllMaps();
      return;
    }

    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;
    }

    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;
    }

    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;
    }

    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') {
    if (mouseY > height / 2 + 30 && mouseY < height / 2 + 70) {
      startGame();
    }
    else if (mouseY > height / 2 + 70 && mouseY < height / 2 + 100) {
      gameState = 'mapSelect';
    }
  }
}
Line-by-line explanation (5 lines)

🔧 Subcomponents:

conditional Start Audio Context if (getAudioContext().state !== 'running') { userStartAudio(); }

Browsers block audio until a user gesture; this unlocks sound the first time the player clicks anywhere

conditional Start Menu Button Hit-Tests if (gameState === 'start') { ... }

Checks the mouse position against five stacked button rectangles to route to mapSelect, instructions, shop, admin, or reset

conditional God Mode Toggle 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; }

Flips the global godMode boolean whenever the God Mode button is clicked in the admin panel

if (getAudioContext().state !== 'running') { userStartAudio(); }
Web browsers require a user gesture (like a click) before audio can play; this line unlocks the audio engine on the very first click.
if (gameState === 'start') { ... }
The whole function is one long if/else-if chain that checks which screen is active, then does hit-testing only for that screen's buttons.
if (mouseX > buttonX && mouseX < buttonX + buttonWidth && mouseY > howToPlayY && mouseY < howToPlayY + buttonHeight) {
A rectangle hit-test repeated for every button: is the mouse within the button's left/right and top/bottom bounds?
if (dist(mouseX, mouseY, x, y) < avatarSize / 2) {
For the circular avatar icons, a distance check (rather than rectangle bounds) is used to detect clicks anywhere inside the circle.
godMode = !godMode;
The ! operator flips a boolean - clicking the God Mode button toggles it from true to false or vice versa.

saveGameProgress()

localStorage lets a web page save small amounts of data in the browser that persists even after the tab or browser is closed. Since it only stores strings, JSON.stringify()/JSON.parse() are used to convert objects to and from text.

function saveGameProgress() {
  const progress = {
    unlockedMapsCount: unlockedMapsCount,
    currentSnailDesign: currentSnailDesign, // Save the current snail design
    secretSnailUnlocked: secretSnailUnlocked, // Save secret snail unlocked status
    secretSnailAvailabilityTimer: secretSnailAvailabilityTimer // Save availability timer
  };
  localStorage.setItem(localStorageKey, JSON.stringify(progress));
  console.log("Game progress saved:", progress);
}
Line-by-line explanation (2 lines)
const progress = { unlockedMapsCount: unlockedMapsCount, ... };
Bundles several separate global variables into one plain object so they can be saved together as a single record.
localStorage.setItem(localStorageKey, JSON.stringify(progress));
JSON.stringify() converts the object into a text string, which is the only format localStorage can store; setItem() writes it under a named key.

loadGameProgress()

This function demonstrates safe deserialization - always checking `!== undefined` before trusting a loaded field guards against crashes when the save format changes between versions of the game.

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
    secretSnailUnlocked = progress.secretSnailUnlocked !== undefined ? progress.secretSnailUnlocked : false; // Load secret snail status
    secretSnailAvailabilityTimer = progress.secretSnailAvailabilityTimer !== undefined ? progress.secretSnailAvailabilityTimer : millis() + secretSnailAppearanceDelay; // Load timer, default if not found

    // Ensure unlockedMapsCount doesn't exceed available maps
    unlockedMapsCount = constrain(unlockedMapsCount, 1, maps.length);
    console.log("Game progress loaded:", progress);
  } else {
    // If no saved progress, initialize availability timer for the first time
    secretSnailAvailabilityTimer = millis() + secretSnailAppearanceDelay;
    console.log("No saved game progress found. Initializing new game.");
  }
}
Line-by-line explanation (3 lines)

🔧 Subcomponents:

conditional Has Saved Data? if (savedProgress) { ... } else { ... }

Branches between restoring previously saved values and initializing fresh defaults for a first-time player

const savedProgress = localStorage.getItem(localStorageKey);
Attempts to read a previously saved string from the browser's storage; returns null if nothing was ever saved.
const progress = JSON.parse(savedProgress);
Converts the saved text string back into a real JavaScript object with usable properties.
currentSnailDesign = progress.currentSnailDesign !== undefined ? progress.currentSnailDesign : 0;
A defensive default: if an older save file didn't include this field, fall back to 0 instead of leaving it undefined.

unlockNextMap()

This small function ties gameplay achievement (reaching a score target) directly to persistent progression, and shows why saving immediately after state changes (rather than only on exit) is important for web games.

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 unlocks a new map if there are still locked maps remaining - prevents counting past the end of the maps array.
unlockedMapsCount++;
Increases the number of playable maps by one.
saveGameProgress();
Immediately persists the new unlock count so it isn't lost if the browser is closed right after unlocking.

resetGameProgress()

This is the 'nuclear option' button, wiping all localStorage-backed progress and re-initializing every relevant global variable back to its starting value in one place.

function resetGameProgress() {
  unlockedMapsCount = 1; // Reset to only Map 1 unlocked
  currentSnailDesign = 0; // Reset snail design to default
  secretSnailUnlocked = false; // Reset secret snail status
  secretSnailAvailabilityTimer = millis() + secretSnailAppearanceDelay; // Reset availability timer
  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, and Secret Snail is available again.");
}
Line-by-line explanation (3 lines)
unlockedMapsCount = 1;
Resets progression so only the first map is playable again.
secretSnailAvailabilityTimer = millis() + secretSnailAppearanceDelay;
Restarts the 30-second countdown for the secret snail as if the player just loaded the game for the first time.
startGame(); gameState = 'mapSelect';
Interestingly this calls startGame() (which sets gameState to 'play') and then immediately overwrites gameState back to 'mapSelect' - a slightly redundant but harmless sequence.

changeSnailAvatar()

This function only updates a variable and saves it - it deliberately doesn't touch the live 'snail' object, since the comment explains the snail only exists during gameplay and will pick up the new design next time startGame() runs.

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 avatar design is currently selected.
saveGameProgress();
Immediately writes the chosen avatar to localStorage so it's remembered on the next visit.

unlockAllMaps()

A simple one-line cheat function exposed through the admin panel - a good example of how cheat/debug tools can reuse the same save/load pipeline as normal gameplay progression.

function unlockAllMaps() {
  unlockedMapsCount = maps.length;
  saveGameProgress();
  console.log("All maps unlocked!");
}
Line-by-line explanation (2 lines)
unlockedMapsCount = maps.length;
Sets the unlocked count equal to the total number of maps, instantly unlocking everything.
saveGameProgress();
Persists the cheat immediately so all maps stay unlocked after a page refresh.

windowResized()

windowResized() is a built-in p5.js callback that fires automatically whenever the browser window changes size, which is essential for the game's fullscreen canvas approach here - though as the comment admits, calling startGame() on every resize event has real downsides (see improvements).

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 resizes the canvas to match the new browser window dimensions.
startGame();
Rebuilds the entire game (score, snail, obstacles, items) from scratch so obstacle positions - stored as fractions of width/height - land correctly at the new size.

📦 Key Variables

gameState string

Tracks which screen is currently active ('start', 'mapSelect', 'play', etc.) and drives the switch statement in draw()

let gameState = 'start';
snail object

Holds the player's Snail instance (position, velocity, avatar design, power-up states) once a game is started

let snail;
mint object

The single Mint (food) item the player is currently trying to reach for points

let mint;
obstacles array

List of Obstacle objects for the current map that end the game on collision

let obstacles = [];
score number

Tracks the player's current point total, used both for display and for unlocking new maps

let score = 0;
candies array

List of Candy speed-boost pickups currently on the map

let candies = [];
pickaxe object

The single Pickaxe item that can be picked up to smash obstacles

let pickaxe;
keyItem object

An optional Key item defined on some maps; currently mostly vestigial from a removed door mechanic

let keyItem;
maps array

Stores all level definitions (name, target score, obstacle layout) built once in defineMaps()

let maps = [];
currentMapIndex number

Index into the maps array indicating which level is currently being played

let currentMapIndex = 0;
unlockedMapsCount number

How many maps the player has unlocked so far; saved/loaded via localStorage

let unlockedMapsCount = 1;
localStorageKey string

The fixed key name used to read/write this game's save data in the browser's localStorage

const localStorageKey = 'snailGameProgress';
currentSnailDesign number

Index of the avatar design the player has chosen in the shop

let currentSnailDesign = 0;
godMode boolean

Admin cheat flag; when true the snail ignores obstacle collisions

let godMode = false;
permanentSpeedBoost boolean

Admin cheat flag; when true the snail always moves at a boosted speed

let permanentSpeedBoost = false;
yumSound object

A p5.Oscillator used to synthesize the 'yum' eating sound effect

let yumSound;
yumEnvelope object

A p5.Envelope that shapes the volume of yumSound into a short blip each time it plays

let yumEnvelope;
secretSnailUnlocked boolean

Tracks whether the current player has already claimed the limited-time secret snail avatar

let secretSnailUnlocked = false;
secretSnailAvailabilityTimer number

A millis() timestamp marking when the secret snail becomes claimable in the shop

let secretSnailAvailabilityTimer = 0;

🔧 Potential Improvements (5)

Here are some ways this code could be enhanced:

BUG drawInstructionsScreen()

This screen reads `snail.maxPickaxeUses` directly, but `snail` is only created inside startGame(). If a player clicks 'How to Play' from the start menu before ever starting a game, `snail` is undefined and this line will throw a runtime error.

💡 Reference a constant (like the Snail class's default maxPickaxeUses value stored outside the class) instead of a live snail instance, or guard the text with `snail ? snail.maxPickaxeUses : 3`.

BUG windowResized()

Every resize event calls startGame(), which resets score to 0 and respawns all items - so a player who accidentally resizes their browser mid-game loses all progress on that map.

💡 Only reposition existing obstacles/items proportionally to the new width/height instead of fully rebuilding the game state, or at minimum preserve the current score.

STYLE drawMapSelectScreen() and mousePressed()

Click-hit-testing for the map buttons is duplicated almost verbatim in both drawMapSelectScreen() (inside the draw loop) and mousePressed(). Handling clicks inside a draw function is also unconventional and easy to lose track of.

💡 Move all click detection into mousePressed() only, and have drawMapSelectScreen() purely render buttons based on state - store button rectangles in an array so both functions (or just one) can reuse the same layout data.

PERFORMANCE mousePressed()

Button positions (startY, buttonWidth, spacing, etc.) are recalculated as local variables every single click across five separate branches, duplicating layout math that's already computed in the corresponding draw function.

💡 Define a shared array of button descriptors (label, x, y, w, h, action) once per screen and loop over it for both drawing and click detection, removing the need to keep two copies of the same coordinates in sync.

FEATURE Snail.usePickaxe() / drawGame()

There's no visual or audio feedback distinguishing a successful pickaxe hit from a wasted swing (both just log to the console), so players have no in-game cue about whether their 'E' press worked.

💡 Add a brief particle effect or flash animation on the destroyed obstacle, and a short sound cue, so pickaxe usage feels responsive without needing to check the browser console.

🔄 Code Flow

Code flow showing preload, setup, triggeryumsound, 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 --> preload[preload] preload --> definemaps[definemaps] preload --> progress-exists-check[progress-exists-check] progress-exists-check -->|yes| loadgameprogress[loadgameprogress] progress-exists-check -->|no| startgame[startgame] setup --> draw[draw loop] draw --> state-switch[state-switch] state-switch -->|start| drawstartscreen[drawstartscreen] state-switch -->|mapSelect| drawmapselectscreen[drawmapselectscreen] state-switch -->|instructions| drawinstructionsscreen[drawinstructionsscreen] state-switch -->|shop| drawshopscreen[drawshopscreen] state-switch -->|admin| drawadminscreen[drawadminscreen] state-switch -->|mapUnlocked| drawmapunlockedscreen[drawmapunlockedscreen] state-switch -->|gameOver| drawgameoverscreen[drawgameoverscreen] state-switch -->|game| updategame[updategame] updategame --> mint-collision[mint-collision] mint-collision -->|collected| triggeryumsound[triggeryumsound] updategame --> obstacle-collision-loop[obstacle-collision-loop] updategame --> candy-collision-loop[candy-collision-loop] updategame --> obstacle-check-loop[obstacle-check-loop] updategame --> obstacle-loop[obstacle-loop] updategame --> drawgame[drawgame] drawgame --> obstacle-draw-loop[obstacle-draw-loop] drawgame --> boost-banner[boost-banner] drawgame --> drawsnailavatar[drawsnailavatar] drawmapselectscreen --> map-button-loop[map-button-loop] drawmapselectscreen --> secret-snail-check[secret-snail-check] drawinstructionsscreen --> menu-key-shortcut[menu-key-shortcut] drawshopscreen --> avatar-loop[avatar-loop] avatar-loop --> avatar-design-switch[avatar-design-switch] avatar-loop --> secret-snail-state[secret-snail-state] drawadminscreen --> admin-toggle[admin-toggle] drawadminscreen --> unlockallmaps[unlockallmaps] drawgameoverscreen --> start-menu-buttons[start-menu-buttons] keypressed --> pickaxe-key[pickaxe-key] windowresized --> startgame click preload href "#fn-preload" click setup href "#fn-setup" click definemaps href "#fn-definemaps" click loadgameprogress href "#fn-loadgameprogress" click startgame href "#fn-startgame" click draw href "#fn-draw" click state-switch href "#sub-state-switch" click updategame href "#fn-updategame" click mint-collision href "#sub-mint-collision" click triggeryumsound href "#fn-triggeryumsound" click obstacle-collision-loop href "#sub-obstacle-collision-loop" click candy-collision-loop href "#sub-candy-collision-loop" click obstacle-check-loop href "#sub-obstacle-check-loop" click obstacle-loop href "#sub-obstacle-loop" click drawgame href "#fn-drawgame" click obstacle-draw-loop href "#sub-obstacle-draw-loop" click boost-banner href "#sub-boost-banner" click drawsnailavatar href "#fn-drawsnailavatar" click map-button-loop href "#sub-map-button-loop" click secret-snail-check href "#sub-secret-snail-check" click menu-key-shortcut href "#sub-menu-key-shortcut" click avatar-loop href "#sub-avatar-loop" click avatar-design-switch href "#sub-avatar-design-switch" click secret-snail-state href "#sub-secret-snail-state" click admin-toggle href "#sub-admin-toggle" click unlockallmaps href "#fn-unlockallmaps" click start-menu-buttons href "#sub-start-menu-buttons"

❓ Frequently Asked Questions

What visual elements are featured in the Snail game sketch?

The Snail game sketch visually represents a whimsical environment where players control a snail navigating through various maps filled with obstacles and collectible items.

How can players interact with the Snail game in this p5.js sketch?

Players can interact with the Snail game by navigating the snail through different maps, collecting mint and speed boost candies, while avoiding obstacles to increase their score.

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

The sketch showcases concepts such as game state management, object-oriented programming through class definitions, and dynamic interaction with local storage for saving game progress.

Preview

Snail game What it should be - p5.js creative coding sketch preview
Sketch Preview
Code flow diagram showing the structure of Snail game What it should be - Code flow showing preload, setup, triggeryumsound, 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