300 lol

This sketch is an interactive survival game where players tap a green food circle to defend against three escalating disasters: a UFO attack at 100 taps, a microwave explosion at 200 taps, and a zombie invasion at 300 taps. Each event is a near-death experience that must be survived through different mechanics—avoiding the UFO's beam, watching the pizza explode, and finding a key to escape.

🧪 Try This!

Experiment with the code by making these changes:

  1. Make the food bigger
  2. Trigger the UFO attack earlier — UFO appears at 100 taps by default—lower this threshold to fight it after just 30 taps, speeding up the game.
  3. Make the zombie run twice as fast — Higher ZOMBIE_SPEED means less time to find the key and escape before it reaches you.
  4. Turn the UFO red — The UFO's color changes to red—a more menacing saucer.
  5. Change the background to dark blue — A darker background makes all the game elements stand out more visually.
  6. Add more pizza slices to the explosion — When the microwave explodes, more slices scatter everywhere for visual chaos.
Prefer the full editor? Open it there →

📖 About This Sketch

This sketch creates an interactive survival game that combines game state management, animation sequencing, and user interaction. Players tap a green food circle at the bottom of the screen, counting up towards 300 taps. At 100 taps, a UFO descends and zaps the player; at 200 taps, a microwave appears and a pizza explodes inside it; at 300 taps, a zombie shambles in and the player must find a key to escape through a door. The visual chaos escalates through each event while the core mechanic—tapping to progress—remains the same.

The sketch demonstrates advanced p5.js techniques organized around a large switch-statement game loop that handles eight distinct phases. You will learn how to structure complex, multi-phase games using state variables, how to sequence animations smoothly over time using millis() and lerp, how to integrate sound design with visual events, and how to build interactive UI elements like clickable keys and doors. The code also shows how to manage collision detection, object arrays, and responsive canvas sizing.

⚙️ How It Works

  1. When the sketch loads, setup() initializes all game elements—the food circle, UFO, microwave, pizza, zombie, key, and door—positioning them off-screen or in their starting locations. Audio oscillators are created for the UFO zap and microwave hum sound effects.
  2. Every frame, draw() first displays the game instructions (which block all other drawing until dismissed by a tap). Once instructions are hidden, the food count is displayed and the large switch statement begins, routing execution to the appropriate phase handler based on the current gamePhase variable.
  3. Phase 0 is the main tapping phase: players tap the green food circle to increment foodCount. When foodCount reaches 100 and the UFO is not already active, ufoActive is set to true and the UFO event begins, cycling through three sub-states (appearing, attacking, leaving) using elapsed time and animation timing.
  4. When foodCount reaches 200 and the UFO is inactive, gamePhase transitions to 1, triggering the microwave sequence: the microwave slides up (phase 1), the pizza slides into it (phase 2), the plate spins and internal lights flicker during cooking (phase 3), and then a white flash and screen shake occur as pizza slices are generated and fall to the floor (phase 4). After the explosion, the game returns to phase 0.
  5. When foodCount reaches 300, gamePhase becomes 6 and the zombie event starts: the zombie slides in from the right edge, a key appears on-screen, and the player must tap the key (causing it to move to the door handle) and then tap the door to escape. If the zombie reaches the center before escape, the game transitions to phase 8 (game over). If the player reaches the door first, phase 7 (escape successful) is triggered.
  6. Sounds are triggered at key moments: the UFO zap plays when the beam hits, the microwave hum plays during cooking, and explosion noise plays when the microwave detonates. All animations use elapsed time calculations to ensure they play at the correct speed regardless of frame rate.

🎓 Concepts You'll Learn

Game state management with switch statementsAnimation timing using millis() and lerp/mapEvent sequencing and phase transitionsSound design with p5.sound oscillators and noiseCollision detection for tap targetsCanvas responsiveness and window resizingArray-based object animation (pizza slices)Conditional rendering based on game phase

📝 Code Breakdown

setup()

setup() runs once when the sketch starts. It initializes every variable that the game needs, positions all game objects, and creates the audio oscillators. Notice how all positions are calculated relative to canvas dimensions (width and height) rather than hard-coded numbers—this makes the game work on any screen size.

function setup() {
  createCanvas(windowWidth, windowHeight);
  // Tip: Use windowWidth/windowHeight for responsive canvas

  // Initialize food position and size
  foodSize = min(width, height) * 0.1;
  foodX = width / 2;
  foodY = height * 0.7; // Place food towards the bottom

  // Initialize player position and size (still needed for UFO targeting)
  playerSize = min(width, height) * 0.08;
  playerX = width / 2;
  playerY = height * 0.85;

  // Initialize UFO size and start off-screen
  ufoSize = min(width, height) * 0.2;
  ufoX = width / 2; // Correctly centered initially
  ufoY = -ufoSize; // Start above the canvas
  ufoActive = false;
  ufoEventState = 0;
  ufoKilledOnce = false; // Ensure it's false at setup

  // Initialize microwave and pizza
  microwaveSize = min(width, height) * 0.4;
  microwaveX = width / 2;
  microwaveY = height + microwaveSize / 2; // Start off-screen below
  microwaveExplodedOnce = false; // Ensure it's false at setup

  pizzaSize = foodSize * 1.5;
  pizzaX = width / 2;
  pizzaY = height * 0.7; // Initial position near food

  // Initialize zombie, key, and door (off-screen or initial state)
  zombieSize = min(width, height) * 0.15;
  zombieX = width + zombieSize / 2; // Start off-screen right
  zombieY = height * 0.85; // Same Y as player
  zombieEventState = 0;

  keySize = min(width, height) * 0.05;
  keyX = random(width * 0.2, width * 0.8);
  keyY = random(height * 0.3, height * 0.7);
  keyFound = false;

  doorWidth = min(width, height) * 0.1;
  doorHeight = doorWidth * 1.5;
  doorX = width * 0.1; // Place door on left side
  doorY = height * 0.85 - doorHeight / 2; // Align with player Y
  doorOpen = false;
  escapeComplete = false;

  textAlign(CENTER, CENTER);
  textSize(min(width, height) * 0.04);
  
  userStartAudio(); // Important for touch devices: start audio context on user gesture

  // Initialize audio objects
  zapOsc = new p5.Oscillator('sine'); // For UFO zap
  zapOsc.amp(0);
  zapOsc.freq(440);

  microwaveHumOsc = new p5.Oscillator('sine'); // For microwave hum
  microwaveHumOsc.amp(0);
  microwaveHumOsc.freq(1000);

  explosionNoise = new p5.Noise('white'); // For microwave explosion
  explosionNoise.amp(0);
}
Line-by-line explanation (11 lines)

🔧 Subcomponents:

calculation Responsive Canvas createCanvas(windowWidth, windowHeight);

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

calculation Food Initialization foodSize = min(width, height) * 0.1;

Scales food size relative to the smaller canvas dimension for responsive design

calculation Audio Oscillator Setup zapOsc = new p5.Oscillator('sine');

Creates a sine wave oscillator for the UFO zap sound effect

createCanvas(windowWidth, windowHeight);
Creates a canvas that fills the entire browser window, making the game responsive to different screen sizes
foodSize = min(width, height) * 0.1;
Calculates the food's diameter as 10% of the smaller canvas dimension, ensuring it scales proportionally on any screen
foodX = width / 2;
Places the food horizontally centered on the canvas
foodY = height * 0.7;
Places the food vertically at 70% down the screen—low enough for comfortable tapping
playerX = width / 2;
Stores the conceptual player position at the center (used for zombie collision, but the player is not drawn)
ufoY = -ufoSize;
Positions the UFO completely above the canvas so it can animate downward when the event starts
microwaveY = height + microwaveSize / 2;
Positions the microwave completely below the canvas so it can slide upward during its event
zombieX = width + zombieSize / 2;
Positions the zombie completely off-screen to the right, ready to slide in when gamePhase becomes 6
keyX = random(width * 0.2, width * 0.8);
Places the key at a random horizontal position between 20% and 80% across the screen
userStartAudio();
Initializes the audio context—required on touch devices before any sound can play
zapOsc = new p5.Oscillator('sine');
Creates a sine wave oscillator that will produce the UFO zap sound when triggered

draw()

draw() is the heartbeat of the sketch—it runs 60 times per second. This frame, it handles instruction display, food rendering, and all eight game phases through a massive switch statement. Each phase has its own animation timing logic using millis() to measure elapsed time, ensuring animations play at the correct speed regardless of frame rate. The UFO and zombie events are sub-state machines: the UFO has three internal states (appearing, attacking, leaving), and the zombie has two (appearing, moving). Understanding this structure is key to building games with multiple events.

🔬 This shake effect randomizes the screen position for 500ms. What happens if you change random(-10, 10) to random(-20, 20)? Or change 500 to 2000?

      if (elapsedExplosion < 500) {
        explosionShakeOffset = random(-10, 10);
      } else {
        explosionShakeOffset = 0;
      }
function draw() {
  background(220); // Light gray background

  // Apply explosion shake offset
  translate(explosionShakeOffset, explosionShakeOffset);

  // --- Display Instructions ---
  if (showInstructions) {
    fill(0);
    textSize(min(width, height) * 0.025); // Smaller text for instructions
    let lineHeight = textSize() * 1.2;
    let startY = height * 0.1;
    for (let i = 0; i < instructionsText.length; i++) {
      text(instructionsText[i], width / 2, startY + i * lineHeight);
    }
    // Don't draw food or other game elements while instructions are shown
    return; // Exit draw loop early
  }

  // --- Draw Food ---
  // Only draw and allow tapping food if gamePhase is 0, not during UFO event, and not game over
  if (gamePhase === 0 && !ufoActive && !gameOver) {
    fill(50, 200, 50); // Green color
    noStroke();
    ellipse(foodX, foodY, foodSize, foodSize); // Food is a circle
    foodTappable = true;
  } else {
    foodTappable = false; // Disable food tapping during UFO event or other phases
  }

  // --- Display Food Count ---
  fill(0); // Black text
  textSize(min(width, height) * 0.03);
  text(`Food Count: ${foodCount}/${ZOMBIE_TAP_TARGET}`, width / 2, height * 0.05);

  // --- Game Logic ---
  switch (gamePhase) {
    case 0: // Tapping food and UFO event at 100 taps, transitions to microwave at 200, zombie at 300
      // Trigger UFO event at 100 taps (only once per game)
      if (!ufoActive && foodCount >= UFO_TAP_TARGET && !ufoKilledOnce && !gameOver) {
        ufoActive = true;
        ufoEventState = 1; // Start UFO appearing
        ufoEventStartTime = millis();
        // Reset UFO position for the event
        ufoX = width / 2;
        ufoY = -ufoSize;
        zapOsc.amp(0); // Ensure zap sound is off
      }

      if (ufoActive) {
        // Handle UFO event phases (appearing, attacking, leaving)
        switch (ufoEventState) {
          case 1: // Appearing
            let appearDuration = 2000;
            let elapsedAppear = millis() - ufoEventStartTime;
            if (elapsedAppear < appearDuration) {
              let t = elapsedAppear / appearDuration;
              ufoY = map(t, 0, 1, -ufoSize, height * 0.2);
            } else {
              ufoY = height * 0.2;
              ufoEventState = 2; // Start attacking
              ufoEventStartTime = millis();
            }
            drawUFO();
            break;

          case 2: // Attacking
            let attackDuration = 3000;
            let elapsedAttack = millis() - ufoEventStartTime;

            drawUFO();

            if (elapsedAttack < 1500) {
              ufoEventBeamHeight = map(elapsedAttack, 0, 1500, 0, playerY - ufoY);
              fill(255, 0, 0);
              noStroke();
              rect(ufoX - ufoSize * 0.05, ufoY, ufoSize * 0.1, ufoEventBeamHeight);
            } else if (elapsedAttack >= 1500 && elapsedAttack < 1600) {
              ufoEventBeamHeight = 0;
              ufoEventFlashAlpha = 255;
              // Play zap sound
              zapOsc.start();
              zapOsc.amp(0.5, 0.05);
              zapOsc.freq(880, 0.05);
              // Set the flag that the UFO kill has occurred
              ufoKilledOnce = true; 
            } else if (elapsedAttack >= 1600) {
              ufoEventFlashAlpha = map(elapsedAttack, 1600, attackDuration, 255, 0);
              zapOsc.amp(0, 0.5);
              if (elapsedAttack >= attackDuration - 500) {
                zapOsc.stop(0.5);
              }
            }

            if (elapsedAttack >= attackDuration) {
              ufoEventState = 3; // Start leaving
              ufoEventStartTime = millis();
            }
            break;

          case 3: // Leaving
            let leaveDuration = 2000;
            let elapsedLeave = millis() - ufoEventStartTime;
            if (elapsedLeave < leaveDuration) {
              let t = elapsedLeave / leaveDuration;
              ufoY = map(t, 0, 1, height * 0.2, -ufoSize);
            } else {
              ufoY = -ufoSize;
              ufoEventState = 0; // Reset UFO event state
              ufoActive = false; // UFO event is over
            }
            drawUFO(); // Draw UFO while leaving
            break;
        }
        // Draw UFO event flash if active
        if (ufoEventFlashAlpha > 0) {
          fill(255, ufoEventFlashAlpha);
          rect(0, 0, width, height);
        }
      }

      // Transition to microwave phase ONLY if UFO event is NOT active AND foodCount >= MICROWAVE_TAP_TARGET
      // AND escapeComplete is false (to avoid microwave after escaping zombie)
      // AND microwaveExplodedOnce is false (to only explode once)
      if (!ufoActive && foodCount >= MICROWAVE_TAP_TARGET && foodCount < ZOMBIE_TAP_TARGET && !gameOver && !escapeComplete && !microwaveExplodedOnce) {
        gamePhase = 1; // Start microwave appearing
        phaseStartTime = millis();
      }
      // Transition to zombie phase ONLY if UFO event is NOT active AND foodCount >= ZOMBIE_TAP_TARGET
      if (!ufoActive && foodCount >= ZOMBIE_TAP_TARGET && !gameOver && !escapeComplete) {
        gamePhase = 6; // Start zombie event
        phaseStartTime = millis();
        zombieEventState = 1; // Zombie starts appearing
      }
      break;

    case 1: // Microwave Appears
      let appearDuration = 2000; // 2 seconds
      let elapsedAppear = millis() - phaseStartTime;

      // Microwave moves up
      if (elapsedAppear < appearDuration) {
        let t = elapsedAppear / appearDuration;
        microwaveY = map(t, 0, 1, height + microwaveSize / 2, height * 0.6);
      } else {
        microwaveY = height * 0.6;
        gamePhase = 2; // Pizza enters microwave
        phaseStartTime = millis();
      }

      drawMicrowave();
      drawPizza(pizzaX, pizzaY, pizzaSize);
      break;

    case 2: // Pizza Enters Microwave
      let enterDuration = 1500; // 1.5 seconds
      let elapsedEnter = millis() - phaseStartTime;

      // Pizza moves into microwave opening
      if (elapsedEnter < enterDuration) {
        let t = elapsedEnter / enterDuration;
        pizzaX = map(t, 0, 1, width / 2, microwaveX);
        pizzaY = map(t, 0, 1, height * 0.7, microwaveY - microwaveSize * 0.15); // Adjust for microwave opening
      } else {
        pizzaInMicrowave = true;
        microwaveDoorOpen = false; // Close door
        gamePhase = 3; // Start cooking
        phaseStartTime = millis();
        // Start microwave hum
        microwaveHumOsc.start();
        microwaveHumOsc.amp(0.3, 0.1);
      }

      drawMicrowave();
      if (!pizzaInMicrowave) {
        drawPizza(pizzaX, pizzaY, pizzaSize);
      }
      break;

    case 3: // Microwave Cooking
      let cookDuration = 3000; // 3 seconds
      let elapsedCook = millis() - phaseStartTime;

      microwavePlateAngle += 0.05; // Spin plate
      microwaveInternalLightAlpha = map(sin(millis() * 0.01), -1, 1, 100, 200); // Flickering light

      if (elapsedCook >= cookDuration) {
        gamePhase = 4; // Explosion!
        phaseStartTime = millis();
        // Stop microwave hum
        microwaveHumOsc.amp(0, 0.5); // Fade out
        microwaveHumOsc.stop(0.5); // Stop after fade

        // Play explosion sound
        explosionNoise.start();
        explosionNoise.amp(1, 0.05); // Quick attack to max volume
        explosionNoise.amp(0, 0.5); // Fade out
        explosionNoise.stop(0.5); // Stop after fade

        // Initiate screen shake
        explosionShakeOffset = 10;
        // Generate pizza slices
        generateSpilledPizzaSlices();
      }

      drawMicrowave();
      // Pizza is inside, not drawn separately
      break;

    case 4: // Explosion!
      let explosionDuration = 1000; // 1 second for flash and shake
      let elapsedExplosion = millis() - phaseStartTime;

      // Flash
      if (elapsedExplosion < 100) { // Instant full white flash
        explosionFlashAlpha = 255;
      } else if (elapsedExplosion < explosionDuration) { // Flash fades
        explosionFlashAlpha = map(elapsedExplosion, 100, explosionDuration, 255, 0);
      } else {
        explosionFlashAlpha = 0;
      }

      // Shake effect
      if (elapsedExplosion < 500) {
        explosionShakeOffset = random(-10, 10);
      } else {
        explosionShakeOffset = 0;
      }

      drawMicrowave(); // Still draw microwave, maybe slightly broken visually later
      drawSpilledPizzaSlices(); // Draw falling pizza

      if (elapsedExplosion >= explosionDuration) {
        gamePhase = 0; // Return to tapping phase after explosion
        // No gameOver = true here!
        microwaveExplodedOnce = true; // Set flag so it doesn't explode again
      }
      break;

    case 5: // Game Over - Pizza Spilled (This phase will now be skipped, but keeping it for reference)
      explosionShakeOffset = 0; // Ensure no shake
      drawMicrowave(); // Microwave remains
      drawSpilledPizzaSlices(); // Pizza remains spilled

      fill(255, 0, 0); // Red text
      textSize(min(width, height) * 0.08);
      text("GAME OVER", width / 2, height / 2);
      textSize(min(width, height) * 0.04);
      text("Your microwave exploded! The pizza spilled on the floor.", width / 2, height / 2 + min(width, height) * 0.06);
      fill(100); // Gray text for announcement
      textSize(min(width, height) * 0.025);
      text("More coming soon like 400, 500, 600, 700, 800, 900 and more!", width / 2, height / 2 + min(width, height) * 0.12);
      break;

    case 6: // Zombie Event - Find key and escape
      drawKey();
      drawDoor();
      drawZombie();

      // Zombie movement logic
      switch (zombieEventState) {
        case 1: // Appearing
          let zombieAppearDuration = 1000;
          let elapsedZombieAppear = millis() - zombieEventStartTime;
          if (elapsedZombieAppear < zombieAppearDuration) {
            let t = elapsedZombieAppear / zombieAppearDuration;
            zombieX = map(t, 0, 1, width + zombieSize / 2, width * 0.8);
          } else {
            zombieX = width * 0.8;
            zombieEventState = 2; // Start moving
          }
          break;

        case 2: // Moving
          zombieX -= ZOMBIE_SPEED;
          // Check if zombie reached player's conceptual position
          if (zombieX <= playerX) {
            gamePhase = 8; // Zombie Game Over
            gameOver = true;
          }
          break;
      }

      // If key is found, move it to the door handle
      if (keyFound) {
        keyX = doorX + doorWidth / 2;
        keyY = doorY;
      }

      // Check for tapping the door
      if (doorOpen) {
        gamePhase = 7;
        escapeComplete = true; // Mark escape as complete
      }
      break;

    case 7: // Escape Successful
      fill(0, 200, 0); // Green text
      textSize(min(width, height) * 0.08);
      text("YOU ESCAPED!", width / 2, height / 2);
      textSize(min(width, height) * 0.04);
      text("You found the key and got away from the zombie!", width / 2, height / 2 + min(width, height) * 0.06);
      fill(100); // Gray text for announcement
      textSize(min(width, height) * 0.025);
      text("More coming soon like 400, 500, 600, 700, 800, 900 and more!", width / 2, height / 2 + min(width, height) * 0.12);
      break;

    case 8: // Zombie Game Over
      fill(255, 0, 0); // Red text
      textSize(min(width, height) * 0.08);
      text("GAME OVER", width / 2, height / 2);
      textSize(min(width, height) * 0.04);
      text("The zombie got you! You couldn't find the key in time.", width / 2, height / 2 + min(width, height) * 0.06);
      fill(100); // Gray text for announcement
      textSize(min(width, height) * 0.025);
      text("More coming soon like 400, 500, 600, 700, 800, 900 and more!", width / 2, height / 2 + min(width, height) * 0.12);
      gameOver = true; // Set game over for this path
      break;
  }

  // Draw explosion flash if active (for microwave explosion)
  if (explosionFlashAlpha > 0) {
    fill(255, explosionFlashAlpha);
    rect(0, 0, width, height);
  }

  // Reset shake offset for next frame
  translate(-explosionShakeOffset, -explosionShakeOffset);
}
Line-by-line explanation (15 lines)

🔧 Subcomponents:

conditional Instruction Screen if (showInstructions) { ... return; }

Displays game rules and blocks all other drawing until the player taps to dismiss

conditional Food Rendering if (gamePhase === 0 && !ufoActive && !gameOver) { ... }

Only draws the green food circle during the tapping phase when UFO is not active

switch-case Game Phase Router switch (gamePhase) { ... }

Routes all game logic through eight distinct phases based on the current gamePhase value

switch-case UFO Event Sub-States switch (ufoEventState) { case 1: ... case 2: ... case 3: ... }

Manages UFO appearing, attacking, and leaving animations within phase 0

background(220);
Clears the canvas with light gray each frame, erasing previous drawings and creating the animation effect
translate(explosionShakeOffset, explosionShakeOffset);
Shifts all subsequent drawing by a random offset to create the screen-shake effect during explosions
if (showInstructions) { ... return; }
If instructions are still showing, display them and return early—this prevents any game elements from drawing until the player dismisses the instructions
if (gamePhase === 0 && !ufoActive && !gameOver) {
Only draw the food if we're in the tapping phase, the UFO is not attacking, and the game hasn't ended
ellipse(foodX, foodY, foodSize, foodSize);
Draws a green circle at the food's position—this is the target the player taps
text(`Food Count: ${foodCount}/${ZOMBIE_TAP_TARGET}`, width / 2, height * 0.05);
Displays the current tap count out of 300, updated every frame so the player sees their progress
switch (gamePhase) { ... }
Routes execution to one of eight case handlers based on the current phase number—each handles a distinct game state
if (!ufoActive && foodCount >= UFO_TAP_TARGET && !ufoKilledOnce && !gameOver) {
Checks if the UFO attack should trigger: food count reached 100, UFO hasn't attacked yet, and no UFO is currently active
ufoActive = true;
Sets the flag that the UFO event is happening, preventing food tapping and triggering the UFO animation sequence
ufoEventBeamHeight = map(elapsedAttack, 0, 1500, 0, playerY - ufoY);
Calculates the beam height using map, scaling elapsed time (0 to 1500ms) to the distance from UFO to player
zapOsc.start();
Starts playing the UFO zap sound at the moment the beam hits the player
if (!ufoActive && foodCount >= MICROWAVE_TAP_TARGET && !microwaveExplodedOnce) {
Transitions to the microwave phase once the UFO event is over, foodCount reaches 200, and the microwave hasn't exploded yet
drawMicrowave();
Calls the helper function to draw the microwave at its current position and state
fill(255, explosionFlashAlpha);
Draws a semi-transparent white rectangle with fading alpha, creating the flash effect during explosions
translate(-explosionShakeOffset, -explosionShakeOffset);
Undoes the shake translation to reset the canvas to its normal position before the next frame

drawUFO()

drawUFO() is a pure drawing helper—it takes the global variables ufoX, ufoY, and ufoSize and draws a UFO shape at that position. It's called from the draw() function whenever the UFO event is active. Notice how every position and size is calculated relative to ufoSize, so if you change ufoSize in setup(), the entire UFO scales proportionally.

🔬 These three lines draw the UFO's three lights. What happens if you duplicate these lines to add a fourth light? Try copying one ellipse line and changing ufoX - ufoSize * 0.3 to ufoX * 0.5 to place it in a different spot.

  fill(255, 255, 0, 150); // Yellow lights
  ellipse(ufoX - ufoSize * 0.3, ufoY + ufoSize * 0.1, ufoSize * 0.05);
  ellipse(ufoX + ufoSize * 0.3, ufoY + ufoSize * 0.1, ufoSize * 0.05);
  ellipse(ufoX, ufoY + ufoSize * 0.15, ufoSize * 0.05);
function drawUFO() {
  fill(150); // Gray saucer body
  ellipse(ufoX, ufoY, ufoSize, ufoSize * 0.4);
  fill(100, 100, 255, 180); // Blue, semi-transparent dome
  ellipse(ufoX, ufoY - ufoSize * 0.1, ufoSize * 0.6, ufoSize * 0.3);
  fill(255, 255, 0, 150); // Yellow lights
  ellipse(ufoX - ufoSize * 0.3, ufoY + ufoSize * 0.1, ufoSize * 0.05);
  ellipse(ufoX + ufoSize * 0.3, ufoY + ufoSize * 0.1, ufoSize * 0.05);
  ellipse(ufoX, ufoY + ufoSize * 0.15, ufoSize * 0.05);
}
Line-by-line explanation (8 lines)
fill(150);
Sets the fill color to gray for the saucer body
ellipse(ufoX, ufoY, ufoSize, ufoSize * 0.4);
Draws a wide, flat ellipse—the main saucer body. Width is ufoSize, height is 40% of ufoSize
fill(100, 100, 255, 180);
Sets fill to semi-transparent blue (alpha 180) for the dome, making it slightly see-through
ellipse(ufoX, ufoY - ufoSize * 0.1, ufoSize * 0.6, ufoSize * 0.3);
Draws a smaller ellipse above the saucer to create the UFO's dome
fill(255, 255, 0, 150);
Sets fill to semi-transparent yellow for the UFO's lights
ellipse(ufoX - ufoSize * 0.3, ufoY + ufoSize * 0.1, ufoSize * 0.05);
Draws a small yellow light on the left side of the saucer
ellipse(ufoX + ufoSize * 0.3, ufoY + ufoSize * 0.1, ufoSize * 0.05);
Draws a small yellow light on the right side of the saucer
ellipse(ufoX, ufoY + ufoSize * 0.15, ufoSize * 0.05);
Draws a small yellow light in the center bottom of the saucer

drawMicrowave()

drawMicrowave() demonstrates how to build complex shapes from simpler elements: the body, door, handle, buttons, and internal plate are all rectangles and ellipses layered together. The use of push() and pop() creates a local transformation context so the door and plate rotations don't affect other elements. The conditional plate rendering shows how game state (gamePhase) drives what gets drawn.

🔬 These lines draw the control panel on the left with buttons labeled 1, 2, and START. What happens if you add another text() line to add a button labeled '3'? Try text("3", -w * 0.38, h * 0.05).

  // Buttons/Controls
  fill(150);
  stroke(80);
  strokeWeight(1);
  rect(-w * 0.35, 0, w * 0.2, h * 0.8, 5); // Control panel
  fill(0);
  textSize(min(w, h) * 0.1);
  text("1", -w * 0.38, -h * 0.25);
  text("2", -w * 0.32, -h * 0.25);
  text("START", -w * 0.35, h * 0.25);
function drawMicrowave() {
  push();
  rectMode(CENTER);
  translate(microwaveX, microwaveY);

  let w = microwaveSize;
  let h = microwaveSize * 0.7;

  // Body
  fill(100);
  stroke(50);
  strokeWeight(3);
  rect(0, 0, w, h, 10); // Rounded rectangle

  // Door (interactive part)
  fill(50);
  stroke(20);
  strokeWeight(2);
  if (microwaveDoorOpen) {
    rect(w * 0.2, 0, w * 0.6, h * 0.8, 5); // Open door (shifted right)
  } else {
    rect(0, 0, w * 0.6, h * 0.8, 5); // Closed door (centered)
  }

  // Door handle
  fill(150);
  noStroke();
  rect(w * 0.28, -h * 0.25, 5, h * 0.15); // Handle on the right side of the door

  // Buttons/Controls
  fill(150);
  stroke(80);
  strokeWeight(1);
  rect(-w * 0.35, 0, w * 0.2, h * 0.8, 5); // Control panel
  fill(0);
  textSize(min(w, h) * 0.1);
  text("1", -w * 0.38, -h * 0.25);
  text("2", -w * 0.32, -h * 0.25);
  text("START", -w * 0.35, h * 0.25);

  // Internal plate (only visible when door is open or cooking)
  if (microwaveDoorOpen || gamePhase === 3) {
    push();
    if (gamePhase === 3) {
      rotate(microwavePlateAngle);
      // microwaveInternalLightAlpha is updated in draw()
    } else {
      microwaveInternalLightAlpha = 0;
    }
    fill(200, microwaveInternalLightAlpha); // Internal light
    ellipse(0, 0, w * 0.5, w * 0.5 * 0.05); // Plate base
    fill(180);
    ellipse(0, 0, w * 0.45); // Plate
    pop();
  }

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

🔧 Subcomponents:

conditional Door Open/Closed Logic if (microwaveDoorOpen) { ... } else { ... }

Shifts the door visually between open and closed positions based on the microwaveDoorOpen flag

conditional Plate Cooking Animation if (microwaveDoorOpen || gamePhase === 3) { ... }

Only draws the spinning plate when the door is open or during the cooking phase (gamePhase 3)

push();
Saves the current drawing settings (fills, strokes, transforms) so changes inside this function don't affect other drawings
rectMode(CENTER);
Tells p5.js to treat rectangle coordinates as the center point rather than the top-left corner
translate(microwaveX, microwaveY);
Shifts the origin point to the microwave's position, so all subsequent drawing happens relative to it
let w = microwaveSize;
Creates a local shorthand variable for microwave width to make calculations easier to read
rect(0, 0, w, h, 10);
Draws the microwave body as a rounded rectangle—the last parameter (10) sets the corner radius
if (microwaveDoorOpen) { rect(w * 0.2, 0, w * 0.6, h * 0.8, 5); } else { rect(0, 0, w * 0.6, h * 0.8, 5); }
If the door is open, it's shifted right by w * 0.2. If closed, it's centered at x = 0. This creates a visual opening/closing animation
rotate(microwavePlateAngle);
Rotates the internal plate by the current angle value, which increases each frame during cooking to create a spinning effect
fill(200, microwaveInternalLightAlpha);
Draws the internal light with alpha fading based on microwaveInternalLightAlpha, which flickers during cooking
pop();
Restores the drawing settings saved by push(), so transformations and fills only apply to the microwave

drawPizza(x, y, size)

drawPizza() is a parametric drawing function—it takes x, y, and size arguments and draws a pizza at any position and scale. The pepperoni loop is a classic technique: divide a full circle (TWO_PI) into N equal angles, then use cos() and sin() to place objects evenly around a circle. This pattern appears in many sketches for creating radial symmetry.

🔬 This loop places 8 pepperoni in a circle. What happens if you change r = size * 0.3 to r = size * 0.5? How does the pepperoni placement change?

  // Pepperoni
  fill(180, 0, 0); // Red pepperoni
  for (let i = 0; i < 8; i++) {
    let angle = TWO_PI / 8 * i;
    let r = size * 0.3;
    ellipse(r * cos(angle), r * sin(angle), size * 0.1);
  }
function drawPizza(x, y, size) {
  push();
  rectMode(CENTER);
  translate(x, y);

  // Crust
  fill(200, 150, 100); // Crust color
  stroke(150, 100, 50);
  strokeWeight(2);
  ellipse(0, 0, size, size);

  // Sauce
  fill(200, 50, 50); // Red sauce
  noStroke();
  ellipse(0, 0, size * 0.9, size * 0.9);

  // Cheese
  fill(255, 200, 0); // Yellow cheese
  noStroke();
  ellipse(0, 0, size * 0.8, size * 0.8);

  // Pepperoni
  fill(180, 0, 0); // Red pepperoni
  for (let i = 0; i < 8; i++) {
    let angle = TWO_PI / 8 * i;
    let r = size * 0.3;
    ellipse(r * cos(angle), r * sin(angle), size * 0.1);
  }
  pop();
}
Line-by-line explanation (11 lines)

🔧 Subcomponents:

for-loop Pepperoni Circle Placement for (let i = 0; i < 8; i++) { ... }

Evenly distributes 8 pepperoni circles around the pizza at equal angles using a loop

push();
Saves drawing settings so the pizza's transformations don't affect other parts of the sketch
rectMode(CENTER);
Sets rectangle mode to CENTER (though this function actually uses ellipses, the call ensures consistency)
translate(x, y);
Moves the origin to the pizza's position so all pizza parts are drawn relative to it
ellipse(0, 0, size, size);
Draws the outermost circle (crust) using the pizza's size parameter
ellipse(0, 0, size * 0.9, size * 0.9);
Draws a slightly smaller red circle (sauce) underneath the crust
ellipse(0, 0, size * 0.8, size * 0.8);
Draws an even smaller yellow circle (cheese) on top of the sauce
for (let i = 0; i < 8; i++) {
Loops 8 times, once for each pepperoni circle to place around the pizza
let angle = TWO_PI / 8 * i;
Calculates the angle for the current pepperoni: TWO_PI (360°) divided by 8 gives 45° increments
let r = size * 0.3;
Sets the radius at which pepperoni are placed—30% of the pizza's size from the center
ellipse(r * cos(angle), r * sin(angle), size * 0.1);
Uses cos and sin with the angle to calculate x,y position on a circle, then draws a small pepperoni at that spot
pop();
Restores the drawing settings saved by push()

generateSpilledPizzaSlices()

generateSpilledPizzaSlices() is called once when the microwave explodes. It creates an array of slice objects, each with unique properties: position, angle, fall speed, rotation speed, and random colors. This demonstrates how to use loops to generate many similar objects with slight variations, and how to store structured data (objects with properties) in an array. The slices are created in a radial pattern (all angles around a circle), which is why they explode outward.

🔬 These three lines add randomness to each slice's behavior. What happens if you change random(1, 3) to random(5, 10)? How much faster do the slices fall?

    let sliceFallSpeed = random(1, 3);
    let sliceRotateSpeed = random(-0.1, 0.1);
    let sliceSize = random(pizzaSize * 0.5, pizzaSize * 0.7);
function generateSpilledPizzaSlices() {
  pizzaSlices = [];
  let numSlices = 10;
  for (let i = 0; i < numSlices; i++) {
    let angle = TWO_PI / numSlices * i;
    let r = pizzaSize * 0.3;
    let sliceX = microwaveX + r * cos(angle);
    let sliceY = microwaveY + r * sin(angle);
    let sliceAngle = angle + PI / 2; // Point slice outwards
    let sliceFallSpeed = random(1, 3);
    let sliceRotateSpeed = random(-0.1, 0.1);
    let sliceSize = random(pizzaSize * 0.5, pizzaSize * 0.7);
    let sliceColor = color(random(180, 220), random(10, 80), random(10, 80)); // Reddish sauce
    let sliceCrustColor = color(random(180, 220), random(130, 170), random(80, 120)); // Crust
    let sliceCheeseColor = color(random(230, 255), random(180, 220), random(0, 50)); // Cheese

    pizzaSlices.push({
      x: sliceX,
      y: sliceY,
      size: sliceSize,
      angle: sliceAngle,
      fallSpeed: sliceFallSpeed,
      rotateSpeed: sliceRotateSpeed,
      color: sliceColor,
      crustColor: sliceCrustColor,
      cheeseColor: sliceCheeseColor,
      landed: false
    });
  }
}
Line-by-line explanation (11 lines)

🔧 Subcomponents:

for-loop Pizza Slice Generation for (let i = 0; i < numSlices; i++) { ... pizzaSlices.push(...); }

Creates 10 slice objects with randomized properties and adds them to the pizzaSlices array

pizzaSlices = [];
Empties the array to reset all slices before generating new ones
let numSlices = 10;
Sets how many pizza slices to create—10 is a good amount for a visible explosion
let angle = TWO_PI / numSlices * i;
Calculates an angle for each slice so they radiate outward from the microwave in all directions
let r = pizzaSize * 0.3;
Sets the initial distance of each slice from the microwave center—30% of pizza size
let sliceX = microwaveX + r * cos(angle);
Calculates the x starting position using cos(angle) to place the slice in the radial pattern
let sliceY = microwaveY + r * sin(angle);
Calculates the y starting position using sin(angle) to complete the radial placement
let sliceAngle = angle + PI / 2;
Rotates the slice by 90° (PI/2) extra so it points outward from the explosion center rather than upward
let sliceFallSpeed = random(1, 3);
Randomly varies how fast each slice falls (1 to 3 pixels per frame) for natural, chaotic motion
let sliceRotateSpeed = random(-0.1, 0.1);
Randomly varies how fast each slice spins as it falls, creating tumbling motion
let sliceColor = color(random(180, 220), random(10, 80), random(10, 80));
Generates a randomized reddish color for each slice's sauce, varying brightness slightly for visual interest
pizzaSlices.push({ x: sliceX, ... });
Creates an object with all slice properties and adds it to the pizzaSlices array so it can be drawn and animated

drawSpilledPizzaSlices()

drawSpilledPizzaSlices() updates and renders each slice object every frame. It demonstrates the update-then-draw pattern: first, physics (position, rotation, landing detection) are updated; then the slice is drawn with all its layers (crust, sauce, cheese, pepperoni). Notice how slice.landed acts as a flag to turn off physics once a slice stops moving—this prevents unnecessary calculations and keeps the animation stable once slices land.

🔬 This block is the physics engine for each slice. What happens if you remove slice.fallSpeed = 0 and slice.rotateSpeed = 0? How will the slices behave after landing?

    if (!slice.landed) {
      slice.y += slice.fallSpeed;
      slice.angle += slice.rotateSpeed;
      if (slice.y >= height * 0.95) { // Land on the floor
        slice.y = height * 0.95;
        slice.fallSpeed = 0;
        slice.rotateSpeed = 0;
        slice.landed = true;
      }
    }
function drawSpilledPizzaSlices() {
  for (let slice of pizzaSlices) {
    if (!slice.landed) {
      slice.y += slice.fallSpeed;
      slice.angle += slice.rotateSpeed;
      if (slice.y >= height * 0.95) { // Land on the floor
        slice.y = height * 0.95;
        slice.fallSpeed = 0;
        slice.rotateSpeed = 0;
        slice.landed = true;
      }
    }

    push();
    rectMode(CORNER);
    translate(slice.x, slice.y);
    rotate(slice.angle);

    let sliceW = slice.size;
    let sliceH = slice.size * 0.7;

    // Crust
    fill(slice.crustColor);
    noStroke();
    triangle(-sliceW / 2, sliceH / 2, sliceW / 2, sliceH / 2, 0, -sliceH / 2);

    // Sauce
    fill(slice.color);
    noStroke();
    triangle(-sliceW * 0.4, sliceH * 0.4, sliceW * 0.4, sliceH * 0.4, 0, -sliceH * 0.4);

    // Cheese
    fill(slice.cheeseColor);
    noStroke();
    triangle(-sliceW * 0.3, sliceH * 0.3, sliceW * 0.3, sliceH * 0.3, 0, -sliceH * 0.3);

    // Pepperoni (some random dots)
    fill(180, 0, 0);
    for (let i = 0; i < 2; i++) {
      ellipse(random(-sliceW * 0.2, sliceW * 0.2), random(-sliceH * 0.2, sliceH * 0.2), sliceW * 0.08);
    }

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

🔧 Subcomponents:

conditional Slice Landing Detection if (slice.y >= height * 0.95) { slice.landed = true; }

Stops updating a slice's position and rotation once it reaches the floor

for-loop Slice Iteration for (let slice of pizzaSlices) { ... }

Loops through all pizza slices in the array and updates and draws each one

for (let slice of pizzaSlices) {
Loops through each slice object in the pizzaSlices array, one iteration per slice
if (!slice.landed) {
Only update physics (falling, rotating) if the slice hasn't landed yet
slice.y += slice.fallSpeed;
Moves the slice downward each frame by adding its fallSpeed to its y position
slice.angle += slice.rotateSpeed;
Rotates the slice by adding its rotateSpeed to its angle each frame
if (slice.y >= height * 0.95) { slice.landed = true; }
Once the slice reaches near the bottom of the screen (95% down), mark it as landed and stop all movement
translate(slice.x, slice.y);
Moves the drawing origin to the slice's current position
rotate(slice.angle);
Rotates the drawing so the slice appears at its current rotation angle
triangle(-sliceW / 2, sliceH / 2, sliceW / 2, sliceH / 2, 0, -sliceH / 2);
Draws a triangle with the base at the bottom and point at the top—the basic slice shape

drawZombie()

drawZombie() builds a creature from simple rectangles and circles layered together: body, head, eyes, mouth, arms, and legs. All dimensions scale with zombieSize, so the zombie can be made larger or smaller while maintaining proportions. The red eyes are the key to making it look menacing. This is a great example of how complex characters can be created from primitive shapes.

function drawZombie() {
  push();
  rectMode(CENTER);
  translate(zombieX, zombieY);

  let w = zombieSize;
  let h = zombieSize * 1.2;

  // Body
  fill(80, 150, 80); // Greenish body
  noStroke();
  rect(0, -h * 0.1, w * 0.6, h * 0.8, 10);

  // Head
  fill(100, 180, 100); // Lighter green head
  ellipse(0, -h * 0.6, w * 0.5, w * 0.5);

  // Eyes
  fill(255, 0, 0); // Red eyes
  ellipse(-w * 0.1, -h * 0.6, w * 0.08);
  ellipse(w * 0.1, -h * 0.6, w * 0.08);

  // Mouth
  fill(0);
  rect(0, -h * 0.5, w * 0.3, 5);

  // Arms
  fill(80, 150, 80);
  rect(-w * 0.4, -h * 0.1, w * 0.2, h * 0.6, 5);
  rect(w * 0.4, -h * 0.1, w * 0.2, h * 0.6, 5);

  // Legs
  fill(80, 150, 80);
  rect(-w * 0.15, h * 0.3, w * 0.15, h * 0.4, 5);
  rect(w * 0.15, h * 0.3, w * 0.15, h * 0.4, 5);

  pop();
}
Line-by-line explanation (12 lines)
push();
Saves the drawing state so zombie transformations don't affect other elements
translate(zombieX, zombieY);
Moves the origin to the zombie's position so all zombie parts are drawn relative to it
rect(0, -h * 0.1, w * 0.6, h * 0.8, 10);
Draws the zombie's body as a rounded rectangle—the main torso
ellipse(0, -h * 0.6, w * 0.5, w * 0.5);
Draws a circle for the zombie's head, positioned above the body
ellipse(-w * 0.1, -h * 0.6, w * 0.08);
Draws the left red eye on the head
ellipse(w * 0.1, -h * 0.6, w * 0.08);
Draws the right red eye on the head
rect(0, -h * 0.5, w * 0.3, 5);
Draws a small horizontal rectangle for the mouth, giving the zombie a menacing expression
rect(-w * 0.4, -h * 0.1, w * 0.2, h * 0.6, 5);
Draws the left arm as a rounded rectangle
rect(w * 0.4, -h * 0.1, w * 0.2, h * 0.6, 5);
Draws the right arm as a rounded rectangle
rect(-w * 0.15, h * 0.3, w * 0.15, h * 0.4, 5);
Draws the left leg as a rounded rectangle, positioned below the body
rect(w * 0.15, h * 0.3, w * 0.15, h * 0.4, 5);
Draws the right leg as a rounded rectangle
pop();
Restores the drawing settings so zombie transformations don't affect subsequent drawings

drawKey()

drawKey() creates a simple golden key from three shapes: a circle (head), a rectangle (shaft), and two rectangles (teeth). The key needs to be visually distinct from the background to be easily tappable. The use of rectMode(CENTER) and translate() ensures the key is positioned correctly at keyX, keyY.

function drawKey() {
  push();
  rectMode(CENTER);
  translate(keyX, keyY);

  let w = keySize;
  let h = keySize * 0.5;

  fill(255, 223, 0); // Gold color
  noStroke();

  // Head of the key
  ellipse(0, 0, w, w);

  // Shaft of the key
  rect(w * 0.3, 0, w * 1.5, h * 0.4);

  // Teeth of the key
  rect(w * 0.8, h * 0.2, w * 0.4, h * 0.4);
  rect(w * 0.8, -h * 0.2, w * 0.2, h * 0.4);

  pop();
}
Line-by-line explanation (7 lines)
push();
Saves drawing settings so key transformations don't affect other drawings
translate(keyX, keyY);
Moves the origin to the key's position so all key parts are drawn relative to it
ellipse(0, 0, w, w);
Draws a circle for the key's head (the ring end you hold)
rect(w * 0.3, 0, w * 1.5, h * 0.4);
Draws a long rectangle for the key's shaft extending to the right
rect(w * 0.8, h * 0.2, w * 0.4, h * 0.4);
Draws a small rectangle for the upper tooth of the key, creating a notch at the end
rect(w * 0.8, -h * 0.2, w * 0.2, h * 0.4);
Draws another small rectangle for the lower tooth, completing the key's distinctive serrated end
pop();
Restores the drawing settings

drawDoor()

drawDoor() draws an escape hatch with a door panel, handle, keyhole, and EXIT text. It's a static prop that the player must reach to complete the zombie phase. The keyhole detail makes it clear that the key found earlier is needed to open it.

function drawDoor() {
  push();
  rectMode(CENTER);
  translate(doorX, doorY);

  fill(139, 69, 19); // Brown color
  stroke(50);
  strokeWeight(3);
  rect(0, 0, doorWidth, doorHeight, 5);

  // Door handle
  fill(200);
  noStroke();
  ellipse(doorWidth * 0.3, 0, doorWidth * 0.15);

  // Keyhole
  fill(0);
  rect(doorWidth * 0.3, 0, 5, doorHeight * 0.1);
  ellipse(doorWidth * 0.3, -doorHeight * 0.05, doorWidth * 0.05);

  // Text on door
  fill(255);
  textSize(min(doorWidth, doorHeight) * 0.15);
  text("EXIT", 0, -doorHeight * 0.4);

  pop();
}
Line-by-line explanation (8 lines)
push();
Saves drawing settings so door transformations don't affect other drawings
translate(doorX, doorY);
Moves the origin to the door's position
rect(0, 0, doorWidth, doorHeight, 5);
Draws the main door panel as a brown rounded rectangle
ellipse(doorWidth * 0.3, 0, doorWidth * 0.15);
Draws a small gray circle for the door handle
rect(doorWidth * 0.3, 0, 5, doorHeight * 0.1);
Draws a thin vertical line for the keyhole slot
ellipse(doorWidth * 0.3, -doorHeight * 0.05, doorWidth * 0.05);
Draws a small circle for the keyhole opening
text("EXIT", 0, -doorHeight * 0.4);
Writes "EXIT" text on the door to indicate it's the escape route
pop();
Restores the drawing settings

touchStarted()

touchStarted() is p5.js's universal input handler for both mouse clicks and touch taps. It demonstrates collision detection using dist() to measure distance from the tap to target objects. Notice how the function uses early returns (return false) to stop processing once an action is handled—this prevents accidentally tapping multiple targets on the same gesture. The gamePhase checks show how input handling changes based on game state: in phase 0, the food is tappable; in phase 6, the key and door are tappable instead.

🔬 This block increments foodCount when the food is tapped. What would happen if you changed foodCount++ to foodCount += 10? How would that change the game's pacing?

    if (gamePhase === 0) {
      if (foodTappable && d < foodSize / 2) {
        foodCount++;
        return false;
      }
    }
function touchStarted() {
  if (!gameOver && !escapeComplete) { // Only allow interaction if game isn't over or escaped
    // If instructions are showing, tap anywhere to dismiss them
    if (showInstructions) {
      showInstructions = false;
      return false; // Prevent food tapping on the same tap
    }

    let d = dist(mouseX, mouseY, foodX, foodY); // Still used for food tapping in phase 0

    if (gamePhase === 0) {
      if (foodTappable && d < foodSize / 2) {
        foodCount++;
        return false;
      }
    } else if (gamePhase === 6) { // Zombie event phase
      // Check for tapping the key
      let dk = dist(mouseX, mouseY, keyX, keyY);
      if (dk < keySize / 2 && !keyFound) {
        keyFound = true;
        // Optionally, make key move to a "ready to use" position
        keyX = doorX + doorWidth * 0.3; // Move key to door handle position
        keyY = doorY;
        return false;
      }

      // Check for tapping the door
      if (keyFound && mouseX > doorX - doorWidth / 2 && mouseX < doorX + doorWidth / 2 &&
          mouseY > doorY - doorHeight / 2 && mouseY < doorY + doorHeight / 2) {
        doorOpen = true;
        escapeComplete = true;
        gamePhase = 7; // Transition to Win phase
        return false;
      }
    }
  } else {
    // Optional: Reset game on tap after game over or escape
    // resetGame();
    // return false;
  }
}
Line-by-line explanation (11 lines)

🔧 Subcomponents:

conditional Instruction Dismissal if (showInstructions) { showInstructions = false; return false; }

Allows player to dismiss the instruction screen by tapping anywhere

conditional Food Tap Detection if (gamePhase === 0) { if (foodTappable && d < foodSize / 2) { foodCount++; } }

Detects when the player taps the food circle and increments the tap counter

conditional Key Tap Detection let dk = dist(mouseX, mouseY, keyX, keyY); if (dk < keySize / 2 && !keyFound) { keyFound = true; }

Detects when the player taps the golden key during the zombie phase

conditional Door Tap Detection if (keyFound && mouseX > doorX - doorWidth / 2 && ... && mouseY < doorY + doorHeight / 2) { escapeComplete = true; }

Detects when the player taps the door after finding the key, triggering escape

function touchStarted() {
This special p5.js function is called whenever the user taps or clicks the canvas (works on touch and mouse)
if (!gameOver && !escapeComplete) {
Only process taps if the game is still running (not over and not already escaped)
if (showInstructions) { showInstructions = false; return false; }
If instructions are showing, hide them and stop processing the tap so it doesn't accidentally tap the food
let d = dist(mouseX, mouseY, foodX, foodY);
Calculates the distance between the tap position (mouseX, mouseY) and the food circle center
if (gamePhase === 0) { if (foodTappable && d < foodSize / 2) { foodCount++; } }
If in the tapping phase and the tap is within the food circle (distance less than radius), increment foodCount
} else if (gamePhase === 6) {
If in the zombie phase, handle key and door tapping instead of food tapping
let dk = dist(mouseX, mouseY, keyX, keyY);
Calculates distance from tap to key position
if (dk < keySize / 2 && !keyFound) { keyFound = true; }
If the tap is within the key circle and the key hasn't been found yet, set keyFound to true
if (keyFound && mouseX > doorX - doorWidth / 2 && mouseX < doorX + doorWidth / 2 && mouseY > doorY - doorHeight / 2 && mouseY < doorY + doorHeight / 2) {
Checks if the tap is within the rectangular bounds of the door AND the key has been found. This is a rectangle collision check.
doorOpen = true; escapeComplete = true; gamePhase = 7;
Opens the door, marks the escape as complete, and transitions to the win phase
return false;
Returning false prevents the browser from processing the tap as a normal click, preventing unwanted page interactions

windowResized()

windowResized() is called automatically whenever the browser window changes size (for example, rotating a phone or resizing a desktop window). It recalculates every position and size variable to ensure the game remains responsive and centered on any screen. Notice how it uses constrain() and map() to preserve animation timing during resize—if the pizza is mid-animation when the window resizes, it recalculates the animation progress so the movement stays smooth. This is important for games that should work on any device.

function windowResized() {
  resizeCanvas(windowWidth, windowHeight);
  // Recalculate positions based on new size
  foodSize = min(width, height) * 0.1;
  foodX = width / 2;
  foodY = height * 0.7;

  // Player position (for conceptual targeting, but not drawn)
  playerSize = min(width, height) * 0.08;
  playerX = width / 2;
  playerY = height * 0.85;

  microwaveSize = min(width, height) * 0.4;
  microwaveX = width / 2;
  // Adjust microwaveY based on its current phase
  if (gamePhase === 0 || gamePhase === 1) { // If still appearing or not yet in place
    microwaveY = height + microwaveSize / 2; // Off-screen below
  } else {
    microwaveY = height * 0.6; // Final position
  }

  pizzaSize = foodSize * 1.5;
  // Adjust pizzaX, pizzaY based on its current phase
  if (gamePhase === 0 || gamePhase === 1) {
    pizzaX = width / 2;
    pizzaY = height * 0.7;
  } else if (gamePhase === 2) {
    // If pizza is entering, recalculate its current interpolation
    let enterDuration = 1500;
    let elapsedEnter = millis() - phaseStartTime;
    let t = constrain(elapsedEnter / enterDuration, 0, 1);
    pizzaX = map(t, 0, 1, width / 2, microwaveX);
    pizzaY = map(t, 0, 1, height * 0.7, microwaveY - microwaveSize * 0.15);
  } else if (gamePhase >= 3) {
    pizzaInMicrowave = true; // Assume it's inside
  }
  
  // Recalculate pizza slice positions for game over phase
  if (gamePhase === 5) {
    generateSpilledPizzaSlices(); // Regenerate slices relative to new microwave pos
  }

  // UFO event specific recalculations (if active)
  ufoSize = min(width, height) * 0.2;
  ufoX = width / 2;
  if (ufoActive) {
    if (ufoEventState === 1 || ufoEventState === 2) {
      ufoY = height * 0.2; // Keep UFO in view during its event
    } else if (ufoEventState === 3) {
      // If leaving, recalculate current position based on elapsed time
      let leaveDuration = 2000;
      let elapsedLeave = millis() - ufoEventStartTime;
      let t = constrain(elapsedLeave / leaveDuration, 0, 1);
      ufoY = map(t, 0, 1, height * 0.2, -ufoSize);
    }
  } else {
    ufoY = -ufoSize; // Keep UFO off-screen when not active
  }

  // Zombie event specific recalculations
  zombieSize = min(width, height) * 0.15;
  if (zombieEventState === 0) {
    zombieX = width + zombieSize / 2; // Off-screen right
  } else if (zombieEventState === 1) {
    // If appearing, recalculate current position based on elapsed time
    let zombieAppearDuration = 1000;
    let elapsedZombieAppear = millis() - zombieEventStartTime;
    let t = constrain(elapsedZombieAppear / zombieAppearDuration, 0, 1);
    zombieX = map(t, 0, 1, width + zombieSize / 2, width * 0.8);
  } else if (zombieEventState === 2) {
    zombieX = width * 0.8; // Set to final appearing position
  }
  zombieY = height * 0.85;

  keySize = min(width, height) * 0.05;
  if (!keyFound) { // Only randomize key position if not yet found
    keyX = random(width * 0.2, width * 0.8);
    keyY = random(height * 0.3, height * 0.7);
  } else {
    keyX = doorX + doorWidth * 0.3; // Key stays with door once found
    keyY = doorY;
  }

  doorWidth = min(width, height) * 0.1;
  doorHeight = doorWidth * 1.5;
  doorX = width * 0.1; // Place door on left side
  doorY = height * 0.85 - doorHeight / 2; // Align with player Y
}
Line-by-line explanation (7 lines)
resizeCanvas(windowWidth, windowHeight);
Resizes the canvas to match the new window dimensions
foodSize = min(width, height) * 0.1;
Recalculates food size based on the new canvas dimensions to maintain proportional scaling
if (gamePhase === 0 || gamePhase === 1) { microwaveY = height + microwaveSize / 2; }
If the microwave hasn't appeared yet, keep it off-screen below. Otherwise, position it at its final location.
if (gamePhase === 2) { let t = constrain(elapsedEnter / enterDuration, 0, 1); pizzaX = map(t, 0, 1, width / 2, microwaveX); }
If the pizza is currently entering the microwave, recalculate its position based on elapsed time so the animation stays smooth after resize
if (gamePhase === 5) { generateSpilledPizzaSlices(); }
If pizza slices are on the floor, regenerate them relative to the new microwave position
if (ufoActive) { if (ufoEventState === 1 || ufoEventState === 2) { ufoY = height * 0.2; } }
If the UFO is actively appearing or attacking, adjust its position based on the new screen height
if (!keyFound) { keyX = random(width * 0.2, width * 0.8); }
If the key hasn't been found yet, place it at a new random position on the resized screen

📦 Key Variables

foodCount number

Tracks the total number of times the player has tapped the food—increments toward 100 (UFO), 200 (microwave), and 300 (zombie) to trigger events

let foodCount = 0;
foodX, foodY, foodSize number

Stores the position and diameter of the green tappable food circle at the bottom of the screen

let foodX = width / 2; let foodY = height * 0.7; let foodSize = min(width, height) * 0.1;
gamePhase number

An integer (0-8) that tracks which game state is active: 0=tapping, 1-5=microwave sequence, 6=zombie, 7=escaped, 8=game over

let gamePhase = 0;
ufoActive, ufoEventState boolean, number

ufoActive is true during the UFO attack event. ufoEventState (0-3) tracks UFO appearing, attacking, or leaving

let ufoActive = false; let ufoEventState = 0;
microwaveDoorOpen, pizzaInMicrowave boolean

Flags tracking whether the microwave door is open and whether the pizza is inside cooking

let microwaveDoorOpen = true; let pizzaInMicrowave = false;
pizzaSlices array of objects

Stores an array of pizza slice objects with properties (x, y, angle, fallSpeed, color, etc.) used during the explosion animation

let pizzaSlices = [];
zombieEventState, keyFound, escapeComplete number, boolean, boolean

zombieEventState (0-2) tracks zombie appearing or moving. keyFound and escapeComplete flag the zombie phase progress

let zombieEventState = 0; let keyFound = false; let escapeComplete = false;
zapOsc, microwaveHumOsc, explosionNoise p5.Oscillator, p5.Oscillator, p5.Noise

Audio objects that produce the UFO zap, microwave hum, and explosion sounds when triggered during their respective events

let zapOsc = new p5.Oscillator('sine');
showInstructions boolean

Flag that controls whether the instruction screen is displayed—set to false when the player taps to dismiss it

let showInstructions = true;

🔧 Potential Improvements (6)

Here are some ways this code could be enhanced:

BUG draw() UFO event and food tapping

If the UFO event is triggered mid-frame, the food remains drawn and tappable for one frame before foodTappable is set to false, allowing an extra tap to register during the UFO appearance animation.

💡 Move foodTappable = false into the case 0 block before the UFO check, so food is disabled immediately when the UFO is activated rather than one frame later.

PERFORMANCE draw() instructionsText rendering

The instructions text loop iterates through all 28+ lines of text every frame and calls text() for each line, even though the instructions rarely change. This is wasteful.

💡 Cache the instructions as a single string with newlines, or only redraw instructions when showInstructions changes (using a flag or checking the previous state).

STYLE touchStarted() collision detection

The food uses dist() for circular collision (d < foodSize / 2), but the door uses rectangle bounds checking with four separate comparisons. Inconsistent collision methods make code harder to maintain.

💡 Create a reusable collision detection helper function: circleCollision(px, py, cx, cy, radius) and rectCollision(px, py, rx, ry, rw, rh) for clarity.

FEATURE touchStarted() end of function

The commented-out code suggests a game reset feature was planned but is incomplete. Players cannot restart after game over or escape without reloading the page.

💡 Implement a resetGame() function that resets all variables (foodCount, gamePhase, gameOver, escapeComplete, keyFound, etc.) and uncomment the reset logic. Add instructions to press a button to restart.

BUG windowResized() pizza entering phase

When windowResized() is called during gamePhase 2 (pizza entering), it uses constrain() to clamp elapsed time between 0 and 1, but the clamping can cause the pizza to jump if the animation is partially complete and the window resizes.

💡 Store the progress ratio (t) rather than recalculating from elapsed time, or accept that resizing mid-animation will cause a visual jump and document this limitation.

PERFORMANCE draw() game loop switch statement

The switch statement spans over 400 lines and contains nested conditionals for UFO sub-states, making draw() very long and hard to read. The function tries to do too much.

💡 Extract each phase (case 0, case 1, etc.) into separate functions like updatePhase0(), updatePhase6(), etc., so draw() becomes a dispatcher that calls the relevant update function.

🔄 Code Flow

Code flow showing setup, draw, drawufo, drawmicrowave, drawpizza, generatespilledpizzaslices, drawspilledpizzaslices, drawzombie, drawkey, drawdoor, touchstarted, windowresized

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

graph TD start[Start] --> setup[setup] setup --> canvas-creation[Responsive Canvas (calculation)] setup --> food-init[Food Initialization (calculation)] setup --> audio-init[Audio Oscillator Setup (calculation)] setup --> draw[draw loop] click setup href "#fn-setup" click canvas-creation href "#sub-canvas-creation" click food-init href "#sub-food-init" click audio-init href "#sub-audio-init" draw --> instruction-display[Instruction Screen (conditional)] instruction-display -->|tap to dismiss| draw click instruction-display href "#sub-instruction-display" draw --> food-drawing[Food Rendering (conditional)] food-drawing -->|draw food circle| draw click food-drawing href "#sub-food-drawing" draw --> phase-switch[Game Phase Router (switch-case)] phase-switch -->|gamePhase 0| ufo-event-logic[UFO Event Sub-States (switch-case)] ufo-event-logic -->|appearing| drawufo[drawUFO()] drawufo --> draw click ufo-event-logic href "#sub-ufo-event-logic" click drawufo href "#fn-drawufo" phase-switch -->|gamePhase 3| plate-rotation[Plate Cooking Animation (conditional)] plate-rotation --> drawmicrowave[drawMicrowave()] drawmicrowave --> draw click plate-rotation href "#sub-plate-rotation" click drawmicrowave href "#fn-drawmicrowave" phase-switch -->|gamePhase 4| drawpizza[drawPizza()] drawpizza --> pepperoni-loop[Pepperoni Circle Placement (for-loop)] pepperoni-loop --> draw click drawpizza href "#fn-drawpizza" click pepperoni-loop href "#sub-pepperoni-loop" phase-switch -->|gamePhase 5| generatespilledpizzaslices[generateSpilledPizzaSlices()] generatespilledpizzaslices --> slice-creation-loop[Pizza Slice Generation (for-loop)] slice-creation-loop --> drawspilledpizzaslices[drawSpilledPizzaSlices()] drawspilledpizzaslices --> foreach-loop[Slice Iteration (for-loop)] foreach-loop --> landing-check[Slice Landing Detection (conditional)] landing-check --> drawspilledpizzaslices click generatespilledpizzaslices href "#fn-generatespilledpizzaslices" click slice-creation-loop href "#sub-slice-creation-loop" click drawspilledpizzaslices href "#fn-drawspilledpizzaslices" click foreach-loop href "#sub-foreach-loop" click landing-check href "#sub-landing-check" phase-switch -->|gamePhase 6| drawkey[drawKey()] drawkey --> key-tap[Key Tap Detection (conditional)] key-tap --> drawdoor[drawDoor()] drawdoor --> door-tap[Door Tap Detection (conditional)] door-tap --> draw click drawkey href "#fn-drawkey" click key-tap href "#sub-key-tap" click drawdoor href "#fn-drawdoor" click door-tap href "#sub-door-tap" draw --> windowresized[windowResized()] windowresized --> draw click windowresized href "#fn-windowresized"

Preview

300 lol - p5.js creative coding sketch preview
Sketch Preview
Code flow diagram showing the structure of 300 lol - Code flow showing setup, draw, drawufo, drawmicrowave, drawpizza, generatespilledpizzaslices, drawspilledpizzaslices, drawzombie, drawkey, drawdoor, touchstarted, windowresized
Code Flow Diagram