Level game ok

This is an incremental level-based game where players hunt for an impossible apple positioned 10 million feet away—so distant it appears smaller than a pixel. As difficulty increases across 1000 levels, the apple shrinks and hints become more limited, punctuated by wild boss events like zombie attacks, fruit chases, and cake-throwing at animals.

🧪 Try This!

Experiment with the code by making these changes:

  1. Increase background music volume — The game's ambient bass hum is very quiet; increase it to make the atmosphere more immersive and feel the deep rumble throughout gameplay.
  2. Make the apple much bigger — Instead of shrinking from 30px to 5px across 1000 levels, keep it large to make all levels forgiving and reveal how the game's mechanics work without frustration.
  3. Speed up the food police car on Level 10 — The police car moves slowly to give players time to watch the event unfold; increase its speed to make the Level 10 event more urgent and dramatic.
  4. Double the hint circle size — Hints start huge on Level 1 but shrink across levels; doubling the shrink-end value (50 to 100) keeps hints useful even on late levels.
  5. Change the apple's red color to gold — The apple is drawn during setup() onto a graphics buffer; change the fill color to create a completely different looking fruit.
  6. Add more location types for apple placement — The apple randomly appears in one of seven locations; add a new location name (e.g., 'MOUNTAIN' or 'OCEAN') to expand variety.
Prefer the full editor? Open it there →

📖 About This Sketch

This sketch creates an elaborate incremental game built around a deceptively simple premise: find a tiny apple on an infinite canvas. It's a masterclass in p5.js game design, combining canvas panning with translate-based coordinate systems, dynamic difficulty scaling, sound synthesis via p5.sound oscillators, and object-oriented collision detection. The apple starts at a reasonable size but shrinks to near-invisibility as levels climb toward 1000, forcing players to rely on hint circles that also shrink with time. Special boss events on Levels 10, 20, 30, and 1000 replace the search with action sequences—a food-police car arrests the apple, a zombie hunts it, a hostile fruit chases the player, and finally, the player throws cakes at adorable animals.

The code is organized into a setup() that initializes canvas, audio, and buttons, a massive draw() loop that handles level-specific logic and rendering, and a suite of helper functions that manage panning, interactions, and level state. You will learn how to build progressively difficult games, implement sound synthesis for audio feedback, structure game events around level numbers, use object-oriented design (Zombie, Bullet, Fruit, ExitDoor, Cake, Animal classes), and architect state machines that branch based on game conditions—all skills that power indie games.

⚙️ How It Works

  1. When the sketch loads, setup() creates a fullscreen canvas, initializes p5.sound oscillators for background music and sound effects, draws an apple graphic onto a buffer (so it can be scaled to any size), and sets up two UI buttons (Hint and Restart). It then calls restartGame() which resets all variables and calls setupLevel() to begin Level 1.
  2. setupLevel() randomly places the tiny apple at one of seven location types (HORIZON, SKY, SUN, MOON, CLOUD, SPACE, or HOUSE) and calculates difficulty parameters that map linearly from level 1 to 1000: the apple shrinks from 30px to 5px, the hint circle shrinks from 200px to 50px, and the panning limits expand to encourage exploration.
  3. Every frame, draw() clears the sky-blue background, applies a translate() transform based on user panning, draws location-specific scenery (sun, moon, clouds, stars, or house), and renders the grass. The apple is drawn using the pre-made buffer image, scaled to currentAppleSize.
  4. Players interact via mouse or touch: dragging pans the camera (constrained by level), and clicking/tapping on the apple's position (within a 100-pixel hitbox) triggers checkAppleInteraction(), which plays a sound and sets foundApple = true. On normal levels, the character 'You' animates off-screen and the next level loads automatically.
  5. Special boss levels override the search: Level 10 spawns a food-police car that drives across the screen to arrest the apple and carry it away in a jail cell. Level 20 spawns a zombie that walks toward the apple while the apple shoots bullets to defend itself. Level 30 triggers a fruit-chase event where a hostile fruit pursues the player, who must navigate to an exit door. Level 1000 (after completing all 999 prior levels) activates a cake-throwing minigame where the player splats adorable animals with desserts.
  6. Throughout, p5.sound oscillators provide musical feedback: background sine wave (50 Hz), triumphant triangle wave (440 Hz) when the apple is found, and specialized sounds for each boss event. When all 1000 levels are complete, the cake event ends and a final win message displays.

🎓 Concepts You'll Learn

Canvas panning and translate-based coordinate transformationIncremental difficulty scaling across game levelsSound synthesis with p5.sound oscillatorsObject-oriented game design (classes for Zombie, Bullet, Fruit, etc.)Collision detection and state machine event logicResponsive UI and button managementTouch and mouse input handling

📝 Code Breakdown

setup()

setup() runs once when the page loads. It prepares the canvas, initializes all audio oscillators, creates UI buttons, and calls restartGame() to start Level 1. All p5.sound oscillators must be created and started here before they can be used in draw().

function setup() {
  createCanvas(windowWidth, windowHeight);
  appleBuffer = createGraphics(50, 50);
  appleBuffer.noStroke();
  appleBuffer.fill(220, 20, 60);
  appleBuffer.ellipse(25, 30, 40, 40);
  appleBuffer.stroke(139, 69, 19);
  appleBuffer.strokeWeight(2);
  appleBuffer.line(25, 10, 25, 20);
  appleBuffer.noStroke();
  appleBuffer.fill(34, 139, 34);
  appleBuffer.ellipse(35, 15, 15, 8);
  userStartAudio();
  backgroundMusic = new p5.Oscillator('sine');
  backgroundMusic.freq(50);
  backgroundMusic.amp(0.05);
  backgroundMusic.start();
  foundAppleSound = new p5.Oscillator('triangle');
  foundAppleSound.freq(440);
  foundAppleSound.amp(0);
  foundAppleSound.start();
  appleShootingSound = new p5.Oscillator('square');
  appleShootingSound.freq(1000);
  appleShootingSound.amp(0);
  appleShootingSound.start();
  zombieHitSound = new p5.Oscillator('sine');
  zombieHitSound.freq(150);
  zombieHitSound.amp(0);
  zombieHitSound.start();
  zombieDeathSound = new p5.Oscillator('sawtooth');
  zombieDeathSound.freq(100);
  zombieDeathSound.amp(0);
  zombieDeathSound.start();
  zombieSpawnSound = new p5.Oscillator('triangle');
  zombieSpawnSound.freq(80);
  zombieSpawnSound.amp(0);
  zombieSpawnSound.start();
  fruitChaseSound = new p5.Oscillator('sawtooth');
  fruitChaseSound.freq(80);
  fruitChaseSound.amp(0);
  fruitChaseSound.start();
  doorOpenSound = new p5.Oscillator('sine');
  doorOpenSound.freq(600);
  doorOpenSound.amp(0);
  doorOpenSound.start();
  playerCaughtSound = new p5.Oscillator('triangle');
  playerCaughtSound.freq(250);
  playerCaughtSound.amp(0);
  playerCaughtSound.start();
  cakeSplatSound = new p5.Oscillator('square');
  cakeSplatSound.amp(0);
  cakeSplatSound.start();
  animalOuchSound = new p5.Oscillator('triangle');
  animalOuchSound.amp(0);
  animalOuchSound.start();
  hintButton = createButton('Give me a Hint!');
  hintButton.position(width / 2 - hintButton.width / 2, height - 50);
  hintButton.mousePressed(showHint);
  hintButton.touchStarted(showHint);
  restartButton = createButton('Restart Game');
  restartButton.position(width / 2 - restartButton.width / 2, height / 2 + 50);
  restartButton.mousePressed(restartGame);
  restartButton.touchStarted(restartGame);
  restartButton.hide();
  restartGame();
}
Line-by-line explanation (10 lines)

🔧 Subcomponents:

calculation Canvas Creation createCanvas(windowWidth, windowHeight);

Creates a fullscreen canvas that fills the entire browser window

calculation Apple Graphics Buffer appleBuffer = createGraphics(50, 50);

Pre-renders a detailed apple image (50×50 pixels) that can be scaled down to tiny sizes without quality loss

calculation Audio Initialization userStartAudio();

Enables audio context in the browser—required before p5.sound oscillators will work

for-loop Sound Oscillator Setup backgroundMusic = new p5.Oscillator('sine');

Creates 9 different oscillator objects for background music and various sound effects throughout the game

calculation UI Button Creation hintButton = createButton('Give me a Hint!');

Creates and positions two interactive buttons for hints and game restart

createCanvas(windowWidth, windowHeight);
Creates a canvas that fills the entire browser window; windowWidth and windowHeight are p5.js built-in variables that update when the window resizes
appleBuffer = createGraphics(50, 50);
createGraphics() creates an invisible drawing surface, like a 50×50 pixel canvas, where we can draw detailed graphics once and reuse them at any scale
appleBuffer.ellipse(25, 30, 40, 40);
Draws a red circle onto the buffer to form the apple's body; all subsequent appleBuffer calls draw on this private surface
userStartAudio();
Initializes the p5.sound library and activates the browser's audio context, which is required before oscillators can play sound
backgroundMusic = new p5.Oscillator('sine');
Creates a new oscillator that produces a sine wave (smooth, pure tone); the first of many sound generators
backgroundMusic.freq(50); backgroundMusic.amp(0.05); backgroundMusic.start();
Sets the oscillator's frequency to 50 Hz (very deep bass), volume to 0.05 (quiet), and starts it playing continuously
hintButton = createButton('Give me a Hint!');
Creates a clickable/touchable button that calls showHint() when pressed
hintButton.position(width / 2 - hintButton.width / 2, height - 50);
Positions the button at the bottom-center of the screen by calculating its center point
restartButton.hide();
The restart button is hidden initially and only shown when the player loses a level
restartGame();
Calls the game initialization function, which sets level to 1 and calls setupLevel() to begin playing

draw()

draw() is the heart of the sketch—it runs 60 times per second. Each call, it clears the background, applies the camera pan via translate(), draws scenery and the apple, handles level-specific boss events with if-else chains, and displays UI text. This massive function demonstrates how game logic branches based on game state (level, foundApple, specific event flags) to create different gameplay experiences.

🔬 This line draws the apple only if many conditions are true—it hides during boss events. What happens if you remove the '&& !(level === 10 && appleArrested)' clause? Can the apple be seen while the police car is chasing it?

  if (!foundApple && level <= MAX_LEVELS && !(level === 10 && appleArrested) && !(level === 20 && appleEaten) && !cakeEventActive && !level30EventTriggered) {
    image(appleBuffer, tinyAppleX, tinyAppleY, currentAppleSize, currentAppleSize);
  }
function draw() {
  background(135, 206, 235);
  fill(0);
  textSize(20);
  textAlign(LEFT, TOP);
  text(`Level: ${level}/${MAX_LEVELS}`, 20, 20);
  push();
  translate(translateX, translateY);
  switch (currentLocationType) {
    case 'SUN':
      drawSun();
      break;
    case 'MOON':
      drawMoon();
      break;
    case 'CLOUD':
      drawClouds();
      break;
    case 'SPACE':
      drawStars();
      break;
    case 'HOUSE':
      let houseCenterX = (width * 0.2 + width * 0.8) / 2;
      let houseCenterY = height * 0.65;
      drawHouse(houseCenterX, houseCenterY, 120, 80);
      break;
  }
  noStroke();
  fill(34, 139, 34);
  rect(0, height * 0.7, width, height * 0.3);
  if (level === 10) {
    if (foodPoliceActive) {
      drawFoodPoliceCar(foodPoliceX, tinyAppleY);
      foodPoliceX -= FOOD_POLICE_SPEED;
      if (foodPoliceX < tinyAppleX) {
        appleArrested = true;
        foodPoliceActive = false;
        jailActive = true;
        jailX = tinyAppleX;
        jailY = tinyAppleY;
        jailCarryingApple = true;
        foundApple = true;
      }
    }
    if (jailActive) {
      drawJail(jailX, jailY, jailCarryingApple);
      jailY += FOOD_POLICE_SPEED;
      if (jailY > height + 50) {
        jailActive = false;
        jailCarryingApple = false;
        nextLevel();
        return;
      }
    }
  } else if (level === 20) {
    if (level20ZombieEventTriggered) {
      if (zombieActive) {
        zombie.move();
        zombie.draw();
        if (millis() - lastShotTime > 500) {
          bullets.push(new Bullet(tinyAppleX, tinyAppleY, zombie.x, zombie.y - zombie.size * 0.6));
          lastShotTime = millis();
          if (appleShootingSound) {
            appleShootingSound.amp(0.2, 0.05);
            appleShootingSound.freq(1000, 0.05);
            setTimeout(() => appleShootingSound.amp(0, 0.1), 100);
          }
        }
        for (let i = bullets.length - 1; i >= 0; i--) {
          bullets[i].move();
          bullets[i].draw();
          if (!bullets[i].isAlive) {
            bullets.splice(i, 1);
          }
        }
        for (let i = bullets.length - 1; i >= 0; i--) {
          let b = bullets[i];
          if (b.isAlive && zombie.isAlive) {
            let d = dist(b.x, b.y, zombie.x, zombie.y - zombie.size * 0.6);
            if (d < zombie.size * 0.4) {
              zombie.takeDamage();
              b.isAlive = false;
              bullets.splice(i, 1);
              if (!zombie.isAlive) {
                zombieDefeated = true;
                zombieActive = false;
              }
            }
          }
        }
        let d = dist(zombie.x, zombie.y - zombie.size * 0.6, tinyAppleX, tinyAppleY);
        if (d < currentAppleSize / 2 + zombie.size * 0.4) {
          appleEaten = true;
          zombieActive = false;
          if (zombieHitSound) {
            zombieHitSound.amp(0.5, 0.1);
            zombieHitSound.freq(50, 0.1);
            setTimeout(() => zombieHitSound.amp(0, 0.5), 1000);
          }
        }
      } else if (zombieDefeated) {
        textSize(48);
        textAlign(CENTER, TOP);
        text("You defeated the zombie!", width / 2, height / 2 - 50);
        textSize(24);
        text("The apple is safe! Moving to the next level.", width / 2, height / 2 + 10);
        nextLevel();
        return;
      } else if (appleEaten) {
        textSize(48);
        textAlign(CENTER, TOP);
        text("The zombie ate the apple!", width / 2, height / 2 - 50);
        textSize(24);
        text("Game Over. The impossible apple was defeated.", width / 2, height / 2 + 10);
        noLoop();
        restartButton.show();
      }
    } else {
      text("You are standing on the grass, looking out.", width / 2, 20);
      text("Somewhere on the horizon, 10,000,000 feet away, is a single apple.", width / 2, 60);
      text("It's so far, it would appear as a speck smaller than a pixel to the naked eye.", width / 2, 100);
      text("It's impossible to find. But wait... you hear a groan?", width / 2, 140);
      text("Can you spot it?", width / 2, 180);
    }
  } else if (level === 30) {
    if (level30EventTriggered) {
      hintButton.hide();
      if (exitDoor) exitDoor.draw();
      youX = mouseX;
      youY = mouseY;
      if (touches.length > 0) {
        youX = touches[0].x;
        youY = touches[0].y;
      }
      youX = constrain(youX, 20, width - 20);
      youY = constrain(youY, 20, height - 20);
      if (fruitActive && fruit) {
        fruit.targetX = youX;
        fruit.targetY = youY;
        fruit.move();
        fruit.draw();
        let d = dist(fruit.x, fruit.y, youX, youY);
        if (d < fruit.size / 2 + 20) {
          fruitCaughtPlayer = true;
          fruitActive = false;
          if (playerCaughtSound) {
            playerCaughtSound.amp(0.5, 0.1);
            playerCaughtSound.freq(map(random(), 0, 1, 200, 300), 0.1);
            setTimeout(() => playerCaughtSound.amp(0, 0.5), 1000);
          }
        }
      }
      if (exitDoor && !playerEscaped) {
        if (
          youX > exitDoor.x - exitDoor.width / 2 &&
          youX < exitDoor.x + exitDoor.width / 2 &&
          youY > exitDoor.y - exitDoor.height / 2 &&
          youY < exitDoor.y + exitDoor.height / 2
        ) {
          playerEscaped = true;
          fruitActive = false;
          if (doorOpenSound) {
            doorOpenSound.amp(0.4, 0.1);
            doorOpenSound.freq(map(random(), 0, 1, 500, 700), 0.1);
            setTimeout(() => doorOpenSound.amp(0, 0.5), 500);
          }
        }
      }
      if (playerEscaped) {
        textSize(48);
        textAlign(CENTER, TOP);
        text("You escaped the hungry fruit!", width / 2, height / 2 - 50);
        textSize(24);
        text("The impossible apple is safe! Moving to the next level.", width / 2, height / 2 + 10);
        nextLevel();
        return;
      } else if (fruitCaughtPlayer) {
        textSize(48);
        textAlign(CENTER, TOP);
        text("The fruit ate you!", width / 2, height / 2 - 50);
        textSize(24);
        text("Game Over. The impossible apple was defeated.", width / 2, height / 2 + 10);
        noLoop();
        restartButton.show();
        return;
      }
      fill(0);
      textSize(24);
      textAlign(CENTER, TOP);
      text("ESCAPE THE HUNGRY FRUIT!", width / 2, 20);
      text("Find the EXIT door!", width / 2, 60);
    } else {
      text("You are standing on the grass, looking out.", width / 2, 20);
      text("Somewhere on the horizon, 10,000,000 feet away, is a single apple.", width / 2, 60);
      text("It's so far, it would appear as a speck smaller than a pixel to the naked eye.", width / 2, 100);
      text("It's impossible to find. But wait... is that a fruit with eyes?", width / 2, 140);
      text("Can you spot it?", width / 2, 180);
    }
  }
  if (!foundApple && level <= MAX_LEVELS && !(level === 10 && appleArrested) && !(level === 20 && appleEaten) && !cakeEventActive && !level30EventTriggered) {
    image(appleBuffer, tinyAppleX, tinyAppleY, currentAppleSize, currentAppleSize);
  }
  if (hintActive) {
    noFill();
    stroke(255, 0, 0);
    strokeWeight(3);
    ellipse(tinyAppleX, tinyAppleY, currentHintCircleSize, currentHintCircleSize);
    if (millis() - hintTimer > currentHintDuration) {
      hintActive = false;
    }
  }
  if (youX > -50 && level <= MAX_LEVELS && !cakeEventActive && !level30EventTriggered) {
    fill(100);
    noStroke();
    ellipse(youX, youY, 40, 20);
    fill(0);
    textSize(16);
    textAlign(CENTER, CENTER);
    text("You", youX, youY + 10);
  }
  pop();
  fill(0);
  textSize(24);
  textAlign(CENTER, TOP);
  if (level > MAX_LEVELS && !cakeEventActive) {
    textSize(48);
    text("YOU WON THE GAME!", width / 2, height / 2 - 50);
    textSize(24);
    text("Thanks for playing! You found all impossible apples and had some cake fun!", width / 2, height / 2 + 10);
    noLoop();
    restartButton.show();
  } else if (foundApple && level !== 10 && level !== 20 && level !== 30) {
    if (youX > -50) {
      youX -= 2;
    } else {
      textSize(48);
      text("You win!", width / 2, height / 2 - 50);
      textSize(24);
      text("You ate the impossible apple and left!", width / 2, height / 2 + 10);
      nextLevel();
      return;
    }
  }
  if (cakeEventActive) {
    hintButton.hide();
    restartButton.hide();
    for (let animal of targetAnimals) {
      animal.move();
      animal.draw();
    }
    for (let i = thrownCakes.length - 1; i >= 0; i--) {
      thrownCakes[i].move();
      thrownCakes[i].draw();
      if (!thrownCakes[i].isActive) {
        thrownCakes.splice(i, 1);
      }
    }
    for (let i = thrownCakes.length - 1; i >= 0; i--) {
      let cake = thrownCakes[i];
      if (!cake.isActive) continue;
      for (let j = targetAnimals.length - 1; j >= 0; j--) {
        let animal = targetAnimals[j];
        if (animal.isHit) continue;
        let d = dist(cake.x, cake.y, animal.x, animal.y);
        if (d < cake.size / 2 + animal.size / 2) {
          animal.isHit = true;
          animal.splatTimer = millis();
          cake.isActive = false;
          if (cakeSplatSound) {
            cakeSplatSound.amp(0.6, 0.1);
            cakeSplatSound.freq(map(random(), 0, 1, 100, 200), 0.1);
            setTimeout(() => cakeSplatSound.amp(0, 0.5), 500);
          }
          if (animalOuchSound) {
            animalOuchSound.amp(0.4, 0.05);
            animalOuchSound.freq(map(random(), 0, 1, 400, 600), 0.05);
            setTimeout(() => animalOuchSound.amp(0, 0.2), 200);
          }
          thrownCakes.splice(i, 1);
          break;
        }
      }
    }
    if (hasCake) {
      push();
      translate(mouseX, mouseY);
      noStroke();
      fill(139, 69, 19);
      rect(-15, -15, 30, 15, 5);
      fill(255);
      ellipse(0, -15, 30, 10);
      fill(200, 0, 0);
      ellipse(0, -15 - 5, 10, 10);
      pop();
    }
    fill(0);
    textSize(32);
    textAlign(CENTER, TOP);
    text("Congratulations! You found all impossible apples!", width / 2, 20);
    textSize(24);
    if (hasCake) {
      text("Tap to throw the cake at a dog, cat, or bunny for fun!", width / 2, 60);
    } else {
      text("More cakes are coming!", width / 2, 60);
      if (thrownCakes.length === 0 && targetAnimals.some(a => !a.isHit)) {
        setTimeout(() => { hasCake = true; }, 1000);
      }
    }
    if (targetAnimals.every(a => a.isHit)) {
      cakeEventActive = false;
      level = MAX_LEVELS + 1;
    }
  }
}
Line-by-line explanation (7 lines)

🔧 Subcomponents:

calculation Clear Background background(135, 206, 235);

Clears the screen with sky blue every frame, erasing previous drawings

calculation Level Counter Display text(`Level: ${level}/${MAX_LEVELS}`, 20, 20);

Shows current progress in the top-left corner

calculation Apply Camera Pan push(); translate(translateX, translateY);

Saves the current drawing state and applies the camera pan transformation to all subsequent drawings until pop()

switch-case Location-Specific Scenery switch (currentLocationType) { case 'SUN': drawSun(); break; ... }

Draws background elements (sun, moon, clouds, stars, house) based on where the apple was randomly placed

calculation Grass Ground Layer rect(0, height * 0.7, width, height * 0.3);

Draws a green rectangle covering the bottom 30% of the screen to represent grass

conditional Level 10 Food Police Event if (level === 10) { ... }

Handles the boss event on Level 10 where a police car arrests the apple and carries it away in a jail

conditional Level 20 Zombie Event else if (level === 20) { ... }

Handles the boss event on Level 20 where a zombie hunts the apple while it shoots bullets in self-defense

conditional Level 30 Fruit Chase Event else if (level === 30) { ... }

Handles the boss event on Level 30 where a hostile fruit chases the player toward an exit door

conditional Apple Rendering if (!foundApple && level <= MAX_LEVELS && ...) { image(appleBuffer, ...); }

Draws the tiny apple only when it hasn't been found yet, checking multiple conditions to skip rendering during events

conditional Hint Circle Display if (hintActive) { ellipse(tinyAppleX, tinyAppleY, ...); }

Shows a red circle around the apple when the player presses the Hint button; disappears after currentHintDuration milliseconds

conditional You Character Rendering if (youX > -50 && level <= MAX_LEVELS && ...) { ellipse(youX, youY, 40, 20); ... }

Draws the 'You' character and its label; animates off-screen to the left after finding the apple

conditional Win Condition and Level Advance if (level > MAX_LEVELS && !cakeEventActive) { ... } else if (foundApple && level !== 10 && ...) { ... }

Displays win messages and automatically advances to the next level once the character leaves the screen

conditional Level 1000 Cake Event if (cakeEventActive) { ... }

Activates after all 1000 levels are complete, allowing the player to throw cakes at animals

background(135, 206, 235);
Clears the entire canvas with sky blue (RGB 135, 206, 235) every frame, erasing the previous frame's drawings
push();
Saves the current transformation matrix (position, rotation, scale) to a stack; allows us to reset it later with pop()
translate(translateX, translateY);
Shifts the coordinate system by translateX and translateY, which are updated when the player drags to pan the camera
image(appleBuffer, tinyAppleX, tinyAppleY, currentAppleSize, currentAppleSize);
Draws the pre-made apple image at the apple's position, scaled to currentAppleSize (shrinks from 30px to 5px across 1000 levels)
if (hintActive) { noFill(); stroke(255, 0, 0); ellipse(tinyAppleX, tinyAppleY, currentHintCircleSize, currentHintCircleSize); }
When the player clicks 'Give me a Hint!', draws a red circle around the apple for currentHintDuration milliseconds (3000ms on Level 1, 1000ms on Level 1000)
youX -= 2;
After finding the apple, the 'You' character moves left by 2 pixels per frame to animate off-screen; when youX < -50, the level automatically advances
pop();
Restores the previously saved transformation matrix, so text drawn after this point is no longer affected by the camera pan

setupLevel()

setupLevel() is called once per level to initialize all variables and randomly place the apple. It demonstrates how to use map() to scale difficulty across a large number of levels, how to use switch-case statements to handle different location types, and how to use trigonometry (angle and distance) to generate random positions within circular regions. This is the backbone of the game's progression system.

function setupLevel() {
  foundApple = false;
  hintActive = false;
  translateX = 0;
  translateY = 0;
  youX = width / 2;
  youY = height * 0.7 + 10;
  level10EventTriggered = false;
  appleArrested = false;
  foodPoliceActive = false;
  foodPoliceX = width + 100;
  level20ZombieEventTriggered = false;
  zombieActive = false;
  zombieDefeated = false;
  appleEaten = false;
  zombie = null;
  bullets = [];
  lastShotTime = 0;
  level30EventTriggered = false;
  fruitActive = false;
  exitDoorActive = false;
  fruit = null;
  exitDoor = null;
  fruitCaughtPlayer = false;
  playerEscaped = false;
  jailActive = false;
  jailCarryingApple = false;
  jailX = 0;
  jailY = 0;
  currentLocationType = random(['HORIZON', 'SKY', 'SUN', 'MOON', 'CLOUD', 'SPACE', 'HOUSE']);
  let angle;
  let r;
  switch (currentLocationType) {
    case 'HORIZON':
      tinyAppleX = random(width);
      tinyAppleY = height * 0.7;
      break;
    case 'SKY':
      tinyAppleX = random(width);
      tinyAppleY = random(height * 0.1, height * 0.6);
      break;
    case 'SUN':
      let sunCenterX = random(width * 0.2, width * 0.8);
      let sunCenterY = random(height * 0.1, height * 0.4);
      let sunRadius = 50;
      angle = random(TWO_PI);
      r = random(sunRadius);
      tinyAppleX = sunCenterX + r * cos(angle);
      tinyAppleY = sunCenterY + r * sin(angle);
      break;
    case 'MOON':
      let moonCenterX = random(width * 0.2, width * 0.8);
      let moonCenterY = random(height * 0.1, height * 0.4);
      let moonRadius = 40;
      angle = random(TWO_PI);
      r = random(moonRadius);
      tinyAppleX = moonCenterX + r * cos(angle);
      tinyAppleY = moonCenterY + r * sin(angle);
      break;
    case 'CLOUD':
      tinyAppleX = random(width);
      tinyAppleY = random(height * 0.1, height * 0.6);
      break;
    case 'SPACE':
      tinyAppleX = random(width);
      tinyAppleY = random(0, height * 0.3);
      break;
    case 'HOUSE':
      let houseCenterX = random(width * 0.2, width * 0.8);
      let houseCenterY = height * 0.65;
      let houseWidth = 120;
      let houseHeight = 80;
      tinyAppleX = random(houseCenterX - houseWidth / 3, houseCenterX + houseWidth / 3);
      tinyAppleY = random(houseCenterY - houseHeight / 3, houseCenterY + houseHeight / 3);
      break;
  }
  if (level === 30) {
    let doorX, doorY;
    let edge = random(['top', 'bottom', 'left', 'right']);
    const DOOR_SIZE = 60;
    switch (edge) {
      case 'top':
        doorX = random(DOOR_SIZE / 2, width - DOOR_SIZE / 2);
        doorY = DOOR_SIZE / 2;
        break;
      case 'bottom':
        doorX = random(DOOR_SIZE / 2, width - DOOR_SIZE / 2);
        doorY = height - DOOR_SIZE / 2;
        break;
      case 'left':
        doorX = DOOR_SIZE / 2;
        doorY = random(DOOR_SIZE / 2, height - DOOR_SIZE / 2);
        break;
      case 'right':
        doorX = width - DOOR_SIZE / 2;
        doorY = random(DOOR_SIZE / 2, height - DOOR_SIZE / 2);
        break;
    }
    exitDoor = new ExitDoor(doorX, doorY, DOOR_SIZE * 0.8, DOOR_SIZE * 1.2);
  }
  currentAppleSize = map(level, 1, MAX_LEVELS, 30, 5);
  currentHintCircleSize = map(level, 1, MAX_LEVELS, 200, 50);
  currentHintDuration = map(level, 1, MAX_LEVELS, 3000, 1000);
  currentPanLimitX = map(level, 1, MAX_LEVELS, width / 2, width);
  currentPanLimitY = map(level, 1, MAX_LEVELS, height / 4, height / 2);
}
Line-by-line explanation (7 lines)

🔧 Subcomponents:

calculation Game State Reset foundApple = false; hintActive = false;

Clears all flags related to the previous level so the new level starts fresh

calculation Camera Position Reset translateX = 0; translateY = 0;

Resets the camera pan to the origin, so the player starts viewing the center of the world

calculation You Character Reset youX = width / 2; youY = height * 0.7 + 10;

Places 'You' at the bottom-center of the screen, ready to search for the apple

calculation Boss Event Flags Reset level10EventTriggered = false; ... level30EventTriggered = false;

Ensures all boss event states are cleared before the level begins

calculation Random Location Type currentLocationType = random(['HORIZON', 'SKY', 'SUN', 'MOON', 'CLOUD', 'SPACE', 'HOUSE']);

Randomly picks one of seven location types to place the apple in a thematic setting

switch-case Apple Position Based on Location switch (currentLocationType) { case 'HORIZON': ... }

Places the apple randomly within the chosen location's bounds (on the horizon, inside a sun, inside a house, etc.)

conditional Level 30 Exit Door Setup if (level === 30) { ... exitDoor = new ExitDoor(...); }

On Level 30, creates an exit door object and places it randomly on one of the four canvas edges

calculation Difficulty Scaling via Map currentAppleSize = map(level, 1, MAX_LEVELS, 30, 5);

Uses the map() function to scale difficulty parameters linearly from level 1 to 1000: apple shrinks, hints shrink, hints disappear faster, and panning limits expand

foundApple = false;
Resets the found-apple flag so the apple is visible and needs to be found again on the new level
currentLocationType = random(['HORIZON', 'SKY', 'SUN', 'MOON', 'CLOUD', 'SPACE', 'HOUSE']);
Picks a random location type from the array; each level's apple appears in a different thematic setting
switch (currentLocationType) { case 'SUN': ... }
Uses a switch statement to place the apple differently depending on which location type was randomly chosen
angle = random(TWO_PI); r = random(sunRadius); tinyAppleX = sunCenterX + r * cos(angle); tinyAppleY = sunCenterY + r * sin(angle);
For SUN and MOON locations, generates a random angle and distance from the sun/moon's center, then uses trigonometry (cos and sin) to position the apple randomly within that circle
currentAppleSize = map(level, 1, MAX_LEVELS, 30, 5);
Uses map() to scale the apple's size linearly: on level 1 it's 30 pixels, on level 1000 it's 5 pixels—making higher levels much harder to find
currentHintDuration = map(level, 1, MAX_LEVELS, 3000, 1000);
Hint circles shrink in duration: 3 seconds on level 1, only 1 second on level 1000, forcing players to use hints more strategically
currentPanLimitX = map(level, 1, MAX_LEVELS, width / 2, width);
Higher levels allow more panning freedom; level 1 allows panning only half the width, level 1000 allows panning the entire width—encouraging exploration

checkAppleInteraction(x, y)

checkAppleInteraction() is the gateway to all major game events. It's called when the player clicks or taps, and it determines what happens next: on most levels, the apple is found and the win animation plays. But on special boss levels (10, 20, 30), it branches to trigger entirely different gameplay sequences. This function demonstrates event-driven game design and state management.

function checkAppleInteraction(x, y) {
  if (foundApple || level > MAX_LEVELS || cakeEventActive) return;
  const hitboxRadius = 100;
  let d = dist(x, y, tinyAppleX, tinyAppleY);
  if (d < hitboxRadius) {
    foundAppleSound.amp(0.5, 0.1);
    foundAppleSound.freq(880, 0.1);
    setTimeout(() => {
      foundAppleSound.amp(0, 0.5);
      foundAppleSound.freq(440, 0.5);
    }, 200);
    if (level === 10) {
      if (!level10EventTriggered) {
        level10EventTriggered = true;
        foodPoliceActive = true;
        foodPoliceX = width + 100;
      }
    } else if (level === 20) {
      if (!level20ZombieEventTriggered) {
        level20ZombieEventTriggered = true;
        zombieActive = true;
        foundApple = true;
        let spawnX, spawnY;
        let edge = random(['top', 'bottom', 'left', 'right']);
        switch (edge) {
          case 'top':
            spawnX = random(width);
            spawnY = -100;
            break;
          case 'bottom':
            spawnX = random(width);
            spawnY = height + 100;
            break;
          case 'left':
            spawnX = -100;
            spawnY = random(height);
            break;
          case 'right':
            spawnX = width + 100;
            spawnY = random(height);
            break;
        }
        zombie = new Zombie(spawnX, spawnY, tinyAppleX, tinyAppleY);
        bullets = [];
        lastShotTime = millis();
        if (zombieSpawnSound) {
          zombieSpawnSound.amp(0.3, 0.1);
          zombieSpawnSound.freq(80, 0.1);
          setTimeout(() => zombieSpawnSound.amp(0, 0.5), 1000);
        }
      }
    } else if (level === 30) {
      if (!level30EventTriggered) {
        level30EventTriggered = true;
        fruitActive = true;
        foundApple = true;
        let spawnX, spawnY;
        let edge = random(['top', 'bottom', 'left', 'right']);
        switch (edge) {
          case 'top':
            spawnX = random(width);
            spawnY = -100;
            break;
          case 'bottom':
            spawnX = random(width);
            spawnY = height + 100;
            break;
          case 'left':
            spawnX = -100;
            spawnY = random(height);
            break;
          case 'right':
            spawnX = width + 100;
            spawnY = random(height);
            break;
        }
        fruit = new Fruit(spawnX, spawnY, youX, youY);
        if (fruitChaseSound) {
          fruitChaseSound.amp(0.3, 0.1);
          fruitChaseSound.freq(map(random(), 0, 1, 60, 90), 0.1);
          setTimeout(() => fruitChaseSound.amp(0, 0.5), 1000);
        }
      }
    } else {
      foundApple = true;
    }
  }
}
Line-by-line explanation (7 lines)

🔧 Subcomponents:

conditional Early Exit Guards if (foundApple || level > MAX_LEVELS || cakeEventActive) return;

Prevents further execution if the apple is already found, the game is won, or the cake event is active—stops the player from re-triggering events

calculation Distance Calculation let d = dist(x, y, tinyAppleX, tinyAppleY);

Uses dist() to compute the straight-line distance from the click/tap position to the apple's position

conditional Hitbox Collision Detection if (d < hitboxRadius) { ... }

Checks if the distance is less than 100 pixels; if so, the apple is considered 'found' and the appropriate event is triggered

calculation Found Apple Sound foundAppleSound.amp(0.5, 0.1); foundAppleSound.freq(880, 0.1);

Plays a triumphant triangle wave by ramping up the oscillator's amplitude and frequency over 0.1 seconds

conditional Level 10 Branch if (level === 10) { ... }

On Level 10, triggers the food police event but does NOT immediately set foundApple = true, allowing the apple to remain visible while the police car chases it

conditional Level 20 Branch else if (level === 20) { ... }

On Level 20, sets foundApple = true and spawns a zombie from a random edge to hunt the apple

conditional Level 30 Branch else if (level === 30) { ... }

On Level 30, sets foundApple = true and spawns a hostile fruit from a random edge to chase the player

conditional Default Case (All Other Levels) else { foundApple = true; }

On levels 1–9, 11–19, 21–29, and 31–999, simply sets foundApple = true to trigger the win animation

if (foundApple || level > MAX_LEVELS || cakeEventActive) return;
Exits early if the apple is already found, the game is won, or the cake event is active—prevents double-triggering
const hitboxRadius = 100;
Defines a 100-pixel radius around the apple's position as the 'tap zone'—currently generous to allow players to find the apple easily
let d = dist(x, y, tinyAppleX, tinyAppleY);
Calculates the Euclidean distance from the click/tap position (x, y) to the apple (tinyAppleX, tinyAppleY)
foundAppleSound.amp(0.5, 0.1); foundAppleSound.freq(880, 0.1);
Ramps the oscillator's amplitude to 0.5 over 0.1 seconds and its frequency to 880 Hz over 0.1 seconds, creating a rising tone
setTimeout(() => { foundAppleSound.amp(0, 0.5); foundAppleSound.freq(440, 0.5); }, 200);
After 200ms, ramps the amplitude and frequency back down over 0.5 seconds, creating a descending tone—a 'ding' sound effect
zombie = new Zombie(spawnX, spawnY, tinyAppleX, tinyAppleY);
Creates a new Zombie object at a random spawn position on one of the four edges, targeting the apple's position
fruit = new Fruit(spawnX, spawnY, youX, youY);
Creates a new Fruit object at a random spawn position, targeting the player's current position

nextLevel()

nextLevel() is called when the player finds the apple and wins the level. It increments the level counter and either sets up the next level or triggers the final cake event if all 1000 levels are complete. This function is intentionally simple and demonstrates the power of delegation: setupLevel() and triggerCakeEvent() handle the complexity.

function nextLevel() {
  level++;
  if (level > MAX_LEVELS) {
    triggerCakeEvent();
  } else {
    setupLevel();
  }
}
Line-by-line explanation (2 lines)

🔧 Subcomponents:

calculation Increment Level Counter level++;

Advances the level number by 1

conditional Check for Game Completion if (level > MAX_LEVELS) { triggerCakeEvent(); } else { setupLevel(); }

If the player has completed all 1000 levels, triggers the cake event; otherwise, sets up the next level

level++;
Increments the global level variable by 1, moving from the current level to the next
if (level > MAX_LEVELS) { triggerCakeEvent(); } else { setupLevel(); }
Checks if the new level exceeds 1000; if so, the game is won and the cake event activates; otherwise, the new level's parameters are initialized

showHint()

showHint() is called when the player clicks the 'Give me a Hint!' button. It simply activates a hint flag and records the time, then draw() handles the actual rendering and duration logic. This separation of concerns keeps the code clean.

function showHint() {
  if (!foundApple && level <= MAX_LEVELS) {
    hintActive = true;
    hintTimer = millis();
  }
}
Line-by-line explanation (3 lines)

🔧 Subcomponents:

conditional Guard Condition if (!foundApple && level <= MAX_LEVELS) { ... }

Ensures the hint button only works if the apple hasn't been found yet and the game isn't won

calculation Activate Hint Display hintActive = true; hintTimer = millis();

Sets the hint flag to true and records the current millisecond time, so draw() knows when the hint expires

if (!foundApple && level <= MAX_LEVELS) {
Only allows the hint to be shown if the apple hasn't been found (!foundApple) and the game is still in progress (level <= MAX_LEVELS)
hintActive = true;
Sets the flag that tells draw() to render the red hint circle around the apple
hintTimer = millis();
Records the current time in milliseconds so draw() can calculate when the hint duration expires

restartGame()

restartGame() is called by the Restart button when the player loses or completes the game. It comprehensively resets all game state back to Level 1, hides UI, and resumes the draw loop. This demonstrates the importance of a master reset function to prevent state leakage between playthroughs.

function restartGame() {
  level = 1;
  foundApple = false;
  hintActive = false;
  cakeEventActive = false;
  hasCake = false;
  thrownCakes = [];
  targetAnimals = [];
  restartButton.hide();
  loop();
  setupLevel();
}
Line-by-line explanation (6 lines)

🔧 Subcomponents:

calculation Reset to Level 1 level = 1;

Sets the level counter back to 1

calculation Reset Game Flags foundApple = false; hintActive = false; cakeEventActive = false;

Clears all major game state flags to ensure a clean slate

calculation Reset Cake Event State hasCake = false; thrownCakes = []; targetAnimals = [];

Clears all cake-event-specific arrays and flags

calculation Hide UI and Resume Drawing restartButton.hide(); loop();

Hides the restart button and resumes the draw loop if it was stopped

level = 1;
Resets the level counter to 1
foundApple = false; hintActive = false; cakeEventActive = false;
Clears major game state flags
hasCake = false; thrownCakes = []; targetAnimals = [];
Resets cake-event-specific state: empties the thrown-cakes array and the animals array
restartButton.hide();
Hides the restart button if it's visible
loop();
Resumes the draw loop; noLoop() was called when the player lost, so loop() re-enables 60 fps drawing
setupLevel();
Calls setupLevel() to initialize Level 1's parameters and randomly place the apple

triggerCakeEvent()

triggerCakeEvent() is called after all 1000 levels are complete. It activates a bonus minigame where the player throws cakes at adorable animals to splat them. The function spawns 5 random animals at the bottom of the screen and sets up the state needed for the cake event logic in draw().

function triggerCakeEvent() {
  cakeEventActive = true;
  hasCake = true;
  thrownCakes = [];
  targetAnimals = [];
  for (let i = 0; i < 5; i++) {
    let animalType = random(['dog', 'cat', 'bunny']);
    let x = random(50, width - 50);
    let y = random(height * 0.75, height - 50);
    targetAnimals.push(new Animal(x, y, animalType));
  }
}
Line-by-line explanation (6 lines)

🔧 Subcomponents:

calculation Activate Cake Event cakeEventActive = true; hasCake = true;

Sets flags to enable the cake-throwing minigame

for-loop Spawn Animals for (let i = 0; i < 5; i++) { ... }

Creates 5 random animals (dogs, cats, or bunnies) at random positions in the lower part of the screen

cakeEventActive = true;
Sets the flag that enables the cake event logic in draw()
hasCake = true;
Gives the player an initial cake to throw
for (let i = 0; i < 5; i++) { ... }
Loops 5 times to create 5 animals
let animalType = random(['dog', 'cat', 'bunny']);
Picks a random animal type from the array
let x = random(50, width - 50); let y = random(height * 0.75, height - 50);
Generates a random x position and a random y position in the lower part of the screen (the grass area)
targetAnimals.push(new Animal(x, y, animalType));
Creates a new Animal object and adds it to the targetAnimals array

mousePressed()

mousePressed() is p5.js's built-in callback that fires when the mouse button is pressed. This function handles two simultaneous features: if the cake event is active and the player has a cake, it creates a cake object and throws it at the mouse position; otherwise, it initiates camera panning. The function returns false to prevent default browser behavior.

function mousePressed() {
  if (cakeEventActive && hasCake) {
    hasCake = false;
    thrownCakes.push(new Cake(mouseX, mouseY, mouseX, mouseY));
    if (cakeSplatSound) {
      cakeSplatSound.amp(0.4, 0.05);
      cakeSplatSound.freq(map(random(), 0, 1, 300, 500), 0.05);
      setTimeout(() => cakeSplatSound.amp(0, 0.2), 200);
    }
  }
  panning = true;
  lastMouseX = mouseX;
  lastMouseY = mouseY;
  userStartAudio();
  return false;
}
Line-by-line explanation (5 lines)

🔧 Subcomponents:

conditional Cake Throw Logic if (cakeEventActive && hasCake) { ... }

Handles cake throwing during the cake event—creates a new Cake object and plays a sound

calculation Initialize Panning panning = true; lastMouseX = mouseX; lastMouseY = mouseY;

Starts panning by recording the initial mouse position and setting the panning flag

calculation Ensure Audio Context userStartAudio();

Guarantees the audio context is active (sometimes needed if the user hasn't interacted with audio yet)

if (cakeEventActive && hasCake) {
Only allows cake throwing if the cake event is active and the player is holding a cake
thrownCakes.push(new Cake(mouseX, mouseY, mouseX, mouseY));
Creates a new Cake object at the mouse position (the start and end positions are the same, so it just spawns at the click)
cakeSplatSound.freq(map(random(), 0, 1, 300, 500), 0.05);
Maps a random number (0–1) to a frequency range (300–500 Hz) to vary the cake-throw sound pitch
panning = true;
Enables panning mode, which allows the player to drag to pan the camera
return false;
Prevents the browser's default behavior (like text selection) when the mouse is pressed

mouseDragged()

mouseDragged() is called every frame while the mouse button is held down. It applies smooth camera panning by adding the mouse's movement delta to the translation variables, then constrains the result to prevent panning beyond the current level's limits. This creates the illusion of a moveable camera that explores the world.

function mouseDragged() {
  if (panning) {
    translateX += mouseX - lastMouseX;
    translateY += mouseY - lastMouseY;
    translateX = constrain(translateX, -currentPanLimitX, currentPanLimitX);
    translateY = constrain(translateY, -currentPanLimitY, currentPanLimitY);
    lastMouseX = mouseX;
    lastMouseY = mouseY;
  }
  return false;
}
Line-by-line explanation (4 lines)

🔧 Subcomponents:

calculation Calculate Pan Delta translateX += mouseX - lastMouseX; translateY += mouseY - lastMouseY;

Adds the difference between the current and last mouse position to the translation values, creating smooth panning

calculation Constrain Pan Range translateX = constrain(translateX, -currentPanLimitX, currentPanLimitX);

Limits how far the player can pan in each direction based on the current level's difficulty

if (panning) {
Only executes panning logic if panning is active (which was set to true in mousePressed())
translateX += mouseX - lastMouseX;
Adds the mouse's horizontal movement since the last frame to the camera's x position
translateX = constrain(translateX, -currentPanLimitX, currentPanLimitX);
Clamps the translation to a range: on level 1, panning is limited to half the width; on level 1000, it's unlimited
lastMouseX = mouseX;
Updates lastMouseX to the current mouse position for the next frame's delta calculation

mouseReleased()

mouseReleased() is called when the mouse button is released. It stops panning and checks for apple interaction. The key insight here is the coordinate transformation: since the world is panned by translateX and translateY, clicks must be un-panned (mouseX - translateX) to find the apple's true world position. This is essential for hit detection in a panned/zoomed game world.

function mouseReleased() {
  panning = false;
  checkAppleInteraction(mouseX - translateX, mouseY - translateY);
  return false;
}
Line-by-line explanation (2 lines)

🔧 Subcomponents:

calculation Stop Panning panning = false;

Disables panning mode so mouseDragged() stops updating the camera

calculation Check Apple Interaction checkAppleInteraction(mouseX - translateX, mouseY - translateY);

Converts the click position from screen coordinates to world coordinates by subtracting the pan offset, then checks for apple interaction

panning = false;
Stops the panning mode that was enabled in mousePressed()
checkAppleInteraction(mouseX - translateX, mouseY - translateY);
Converts the click's screen-space coordinates to world-space coordinates by subtracting the camera pan, then checks if the apple was tapped

touchStarted()

touchStarted() mirrors mousePressed() but for touchscreen input. The touches array contains information about all active touch points; touches[0] refers to the first (primary) touch. This function handles cake throwing on touch and initializes touch-based panning.

function touchStarted() {
  if (cakeEventActive && hasCake && touches.length > 0) {
    hasCake = false;
    thrownCakes.push(new Cake(touches[0].x, touches[0].y, touches[0].x, touches[0].y));
    if (cakeSplatSound) {
      cakeSplatSound.amp(0.4, 0.05);
      cakeSplatSound.freq(map(random(), 0, 1, 300, 500), 0.05);
      setTimeout(() => cakeSplatSound.amp(0, 0.2), 200);
    }
  }
  if (touches.length > 0) {
    panning = true;
    lastTouchX = touches[0].x;
    lastTouchY = touches[0].y;
    userStartAudio();
  }
  return false;
}
Line-by-line explanation (3 lines)

🔧 Subcomponents:

conditional Touch-Based Cake Throw if (cakeEventActive && hasCake && touches.length > 0) { ... }

Throws a cake when the player touches the screen during the cake event

conditional Initialize Touch Panning if (touches.length > 0) { panning = true; ... }

Starts panning mode on touch

if (cakeEventActive && hasCake && touches.length > 0) {
Checks if the cake event is active, the player has a cake, and there's at least one touch point
thrownCakes.push(new Cake(touches[0].x, touches[0].y, touches[0].x, touches[0].y));
Creates a cake at the first touch point's position
lastTouchX = touches[0].x; lastTouchY = touches[0].y;
Records the initial touch position for calculating the drag delta in touchMoved()

touchMoved()

touchMoved() mirrors mouseDragged() but for touch input. It continuously updates the camera pan based on touch movement, constrained to the current level's pan limits. This allows smooth panning on mobile devices.

function touchMoved() {
  if (panning && touches.length > 0) {
    translateX += touches[0].x - lastTouchX;
    translateY += touches[0].y - lastTouchY;
    translateX = constrain(translateX, -currentPanLimitX, currentPanLimitX);
    translateY = constrain(translateY, -currentPanLimitY, currentPanLimitY);
    lastTouchX = touches[0].x;
    lastTouchY = touches[0].y;
  }
  return false;
}
Line-by-line explanation (2 lines)

🔧 Subcomponents:

calculation Calculate Touch Pan Delta translateX += touches[0].x - lastTouchX; translateY += touches[0].y - lastTouchY;

Updates the camera position based on the touch movement since the last frame

if (panning && touches.length > 0) {
Executes only if panning is active and there's at least one active touch
translateX += touches[0].x - lastTouchX;
Adds the touch's horizontal movement delta to the camera's x position

touchEnded()

touchEnded() mirrors mouseReleased() for touch input. It stops panning and checks for apple interaction using the last recorded touch position converted to world coordinates. This ensures that taps on the apple are detected correctly even when the camera is panned.

function touchEnded() {
  panning = false;
  checkAppleInteraction(lastTouchX - translateX, lastTouchY - translateY);
  return false;
}
Line-by-line explanation (2 lines)

🔧 Subcomponents:

calculation Stop Touch Panning panning = false;

Disables touch panning when the touch ends

calculation Check Apple on Touch Release checkAppleInteraction(lastTouchX - translateX, lastTouchY - translateY);

Checks if the touch released on the apple by converting screen coordinates to world coordinates

panning = false;
Stops touch panning when the touch is released
checkAppleInteraction(lastTouchX - translateX, lastTouchY - translateY);
Converts the touch's final position to world coordinates and checks for apple interaction

windowResized()

windowResized() is called by p5.js whenever the browser window is resized. This function demonstrates responsive design: it resizes the canvas, recalculates the apple's position (so it scales to the new dimensions), repositions UI buttons, and adjusts camera pan limits. This ensures the game remains playable at any screen size.

function windowResized() {
  resizeCanvas(windowWidth, windowHeight);
  if (level <= MAX_LEVELS && level !== 20 && level !== 30 && !cakeEventActive) {
    setupLevel();
  }
  hintButton.position(width / 2 - hintButton.width / 2, height - 50);
  restartButton.position(width / 2 - restartButton.width / 2, height / 2 + 50);
  if (foundApple) {
    youX = width / 2;
    youY = height * 0.7 + 10;
  }
  if (level <= MAX_LEVELS) {
    currentPanLimitX = map(level, 1, MAX_LEVELS, width / 2, width);
    currentPanLimitY = map(level, 1, MAX_LEVELS, height / 4, height / 2);
  }
}
Line-by-line explanation (4 lines)

🔧 Subcomponents:

calculation Resize Canvas resizeCanvas(windowWidth, windowHeight);

Adjusts the canvas size to match the new window dimensions

conditional Conditionally Reset Level if (level <= MAX_LEVELS && level !== 20 && level !== 30 && !cakeEventActive) { setupLevel(); }

Re-runs setupLevel() to recalculate apple position and difficulty based on new canvas size, but skips during active events

calculation Reposition UI Buttons hintButton.position(...); restartButton.position(...);

Re-centers the hint and restart buttons on the resized canvas

calculation Recalculate Pan Limits currentPanLimitX = map(level, 1, MAX_LEVELS, width / 2, width);

Adjusts the camera pan limits based on the new canvas dimensions

resizeCanvas(windowWidth, windowHeight);
Resizes the p5.js canvas to match the browser window's new dimensions
if (level <= MAX_LEVELS && level !== 20 && level !== 30 && !cakeEventActive) {
Only resets the level if the game is in a normal state; skips reset during zombie, fruit chase, or cake events to avoid disrupting active gameplay
hintButton.position(width / 2 - hintButton.width / 2, height - 50);
Centers the hint button horizontally and places it near the bottom of the resized canvas
currentPanLimitX = map(level, 1, MAX_LEVELS, width / 2, width);
Recalculates the panning limit based on the new width, ensuring the pan range scales with the canvas

📦 Key Variables

appleBuffer p5.Renderer

A pre-rendered graphics surface (50×50 pixels) containing a detailed apple image that can be scaled down to any size without quality loss

appleBuffer = createGraphics(50, 50);
tinyAppleX number

The x-coordinate of the apple's position in world space

let tinyAppleX = random(width);
tinyAppleY number

The y-coordinate of the apple's position in world space

let tinyAppleY = height * 0.7;
APPLE_DISTANCE_FEET number (constant)

A flavor constant representing the conceptual distance to the apple (10 million feet); doesn't affect gameplay but reinforces the game's theme

const APPLE_DISTANCE_FEET = 10_000_000;
foundApple boolean

Tracks whether the apple has been found on the current level; when true, the character animates off-screen and the next level begins

let foundApple = false;
translateX number

The camera's horizontal pan offset; updated when the player drags to pan left/right

let translateX = 0;
translateY number

The camera's vertical pan offset; updated when the player drags to pan up/down

let translateY = 0;
panning boolean

Flag indicating whether the player is currently dragging to pan the camera

let panning = false;
hintActive boolean

Flag indicating whether a hint circle is currently displayed around the apple

let hintActive = false;
hintTimer number

The millisecond timestamp when the hint was activated; used to calculate hint duration

let hintTimer = 0;
youX number

The horizontal position of the 'You' character; animates left after finding the apple or follows the mouse during Level 30

let youX = width / 2;
youY number

The vertical position of the 'You' character; normally fixed on the grass but becomes dynamic during Level 30 (fruit chase)

let youY = height * 0.7 + 10;
level number

The current level number; ranges from 1 to 1000, and tracks progression through the game

let level = 1;
currentAppleSize number

The apple's diameter in pixels; shrinks from 30px on level 1 to 5px on level 1000, increasing difficulty

let currentAppleSize = 30;
currentHintCircleSize number

The diameter of the hint circle in pixels; shrinks from 200px to 50px across levels to challenge players more

let currentHintCircleSize = 200;
currentHintDuration number (milliseconds)

How long the hint circle remains visible; decreases from 3000ms on level 1 to 1000ms on level 1000

let currentHintDuration = 3000;
currentPanLimitX number

Maximum pixels the camera can pan horizontally; increases from half-width on level 1 to full-width on level 1000

let currentPanLimitX = 0;
currentPanLimitY number

Maximum pixels the camera can pan vertically; increases from quarter-height on level 1 to half-height on level 1000

let currentPanLimitY = 0;
level10EventTriggered boolean

Flag preventing the Level 10 food-police event from triggering multiple times

let level10EventTriggered = false;
foodPoliceActive boolean

Flag indicating whether the food police car is currently on screen during Level 10

let foodPoliceActive = false;
foodPoliceX number

The x-coordinate of the food police car; moves left toward the apple during Level 10

let foodPoliceX = 0;
level20ZombieEventTriggered boolean

Flag preventing the Level 20 zombie event from triggering multiple times

let level20ZombieEventTriggered = false;
zombieActive boolean

Flag indicating whether the zombie is currently on screen and moving during Level 20

let zombieActive = false;
zombie Zombie object

The zombie instance created on Level 20; stores position, health, and movement state

let zombie = null;
bullets array of Bullet objects

Stores all active bullets shot by the apple during Level 20 zombie combat

let bullets = [];
lastShotTime number

The millisecond timestamp of the last bullet fired; used to rate-limit apple shots to one every 500ms

let lastShotTime = 0;
level30EventTriggered boolean

Flag preventing the Level 30 fruit-chase event from triggering multiple times

let level30EventTriggered = false;
fruitActive boolean

Flag indicating whether the hostile fruit is currently chasing the player during Level 30

let fruitActive = false;
fruit Fruit object

The hostile fruit instance created on Level 30; tracks position and targets the player

let fruit = null;
exitDoor ExitDoor object

The exit door instance created on Level 30; the player must reach this to escape the fruit

let exitDoor = null;
cakeEventActive boolean

Flag indicating whether the Level 1000 cake-throwing event is currently active

let cakeEventActive = false;
hasCake boolean

Flag indicating whether the player is currently holding a cake during the cake event

let hasCake = false;
thrownCakes array of Cake objects

Stores all cakes currently flying through the air during the cake event

let thrownCakes = [];
targetAnimals array of Animal objects

Stores all animals that the player can splat with cakes during the cake event

let targetAnimals = [];
backgroundMusic p5.Oscillator

A sine wave oscillator playing a deep 50 Hz bass tone throughout the game

let backgroundMusic = new p5.Oscillator('sine');
foundAppleSound p5.Oscillator

A triangle wave oscillator that plays a triumphant sound when the apple is found

let foundAppleSound = new p5.Oscillator('triangle');
currentLocationType string

The current level's apple location type; one of 'HORIZON', 'SKY', 'SUN', 'MOON', 'CLOUD', 'SPACE', or 'HOUSE'

let currentLocationType = 'HORIZON';

🔧 Potential Improvements (9)

Here are some ways this code could be enhanced:

BUG checkAppleInteraction()

The hitbox radius is set to 100 pixels—extremely forgiving and defeats the challenge of finding a tiny apple on late levels. A difficulty-scaled hitbox would make late-game progression genuinely difficult.

💡 Replace 'const hitboxRadius = 100;' with 'const hitboxRadius = map(level, 1, MAX_LEVELS, 100, 15);' to scale the hitbox down as the apple shrinks.

PERFORMANCE draw()

The draw() function is monolithic (~300 lines) with deeply nested conditionals and switch-case blocks. This makes the code hard to follow and modify, and adds cognitive overhead each frame.

💡 Extract each level's event logic into a separate function: drawLevel10Event(), drawLevel20Event(), drawLevel30Event(), drawCakeEvent(). Call these from draw() with if-else branches to improve clarity and maintainability.

BUG windowResized()

The function skips calling setupLevel() during zombie, fruit chase, and cake events with the condition 'level !== 20 && level !== 30 && !cakeEventActive'. If a player resizes the window during Level 20 or 30, the pan limits won't recalculate, potentially locking the camera in an unexpected state.

💡 Always recalculate currentPanLimitX and currentPanLimitY at the end of windowResized(), even during active events: extract the calculation into a separate function and call it unconditionally.

STYLE Zombie and Fruit classes

The Zombie class has a fixed speed (1.5) and size (60), while the Fruit class has fixed speed (2) and size (50). These constants are hardcoded, making it hard to tune difficulty across levels or experiment with balance.

💡 Move these to constants at the top of the sketch (e.g., const ZOMBIE_SPEED = 1.5; const FRUIT_SPEED = 2;) or pass them as constructor parameters to allow level-based scaling.

FEATURE Level progression

All 1000 levels are identical except for apple size and difficulty scaling. There are no intermediate narrative beats, no reward milestones (e.g., every 100 levels), and no visual variety beyond location type.

💡 Add milestone events: e.g., at Level 100, 200, 300, etc., trigger a special message or unlock a new feature. Add variety by randomly selecting enemy types on certain levels or rotating background colors.

BUG touchEnded()

The function uses 'lastTouchX' and 'lastTouchY' for apple interaction, but these variables store the last position during touchMoved(). If the player taps without dragging, lastTouchX/Y may not equal the actual tap position, causing mis-detections.

💡 Store the tap's initial position (from touchStarted()) in separate variables, e.g., 'tapStartX' and 'tapStartY', and use those for apple interaction in touchEnded().

STYLE Oscillator initialization

Nine separate oscillators are created in setup() for different sounds (background music, zombie sounds, cake sounds, etc.). The code is repetitive and hard to maintain if new sounds are added.

💡 Create an object to organize sounds: const sounds = { background: new p5.Oscillator('sine'), foundApple: new p5.Oscillator('triangle'), ... } and initialize all oscillators in a helper function.

FEATURE draw() cake event block

After all animals are hit in the cake event, the game immediately advances with 'level = MAX_LEVELS + 1;'. There's no celebration, no score display, no indication of how well the player performed.

💡 Add a 'cake event result' screen showing how many animals were splatted, how many cakes were thrown, and allowing the player to replay the cake event or return to the main menu with the restart button.

PERFORMANCE draw() cake event collision detection

The nested loop checking cake-animal collisions ('for (let i = ...) for (let j = ...)') has O(n×m) complexity. With 10+ animals and many cakes in flight, this becomes expensive.

💡 Use spatial partitioning (e.g., divide the screen into a grid) or a spatial hash to reduce collision checks from O(n×m) to O(n + m).

🔄 Code Flow

Code flow showing setup, draw, setupLevel, checkAppleInteraction, nextLevel, showHint, restartGame, triggerCakeEvent, mousePressed, mouseDragged, mouseReleased, touchStarted, touchMoved, touchEnded, windowResized

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

graph TD start[Start] --> setup[setup] setup --> draw[draw loop] click setup href "#fn-setup" click draw href "#fn-draw" setup --> canvas-creation[Canvas Creation] setup --> audio-init[Audio Initialization] setup --> oscillators-init[Sound Oscillator Setup] setup --> ui-buttons[UI Button Creation] setup --> restartGame[restartGame] click canvas-creation href "#sub-canvas-creation" click audio-init href "#sub-audio-init" click oscillators-init href "#sub-oscillators-init" click ui-buttons href "#sub-ui-buttons" draw --> background[Clear Background] draw --> camera-transform[Apply Camera Pan] draw --> level-display[Level Counter Display] draw --> location-scenery[Location-Specific Scenery] draw --> grass[Grass Ground Layer] draw --> apple-render[Apple Rendering] draw --> hint-circle[Hint Circle Display] draw --> you-character[You Character Rendering] draw --> win-logic[Win Condition and Level Advance] draw --> cake-event[Level 1000 Cake Event] click background href "#sub-background" click camera-transform href "#sub-camera-transform" click level-display href "#sub-level-display" click location-scenery href "#sub-location-scenery" click grass href "#sub-grass" click apple-render href "#sub-apple-render" click hint-circle href "#sub-hint-circle" click you-character href "#sub-you-character" click win-logic href "#sub-win-logic" click cake-event href "#sub-cake-event" draw --> checkAppleInteraction[checkAppleInteraction] checkAppleInteraction --> early-exit[Early Exit Guards] checkAppleInteraction --> distance-check[Distance Calculation] distance-check --> hitbox-collision[Hitbox Collision Detection] hitbox-collision --> sound-effect[Found Apple Sound] hitbox-collision --> level-10-branch[Level 10 Branch] hitbox-collision --> level-20-branch[Level 20 Branch] hitbox-collision --> level-30-branch[Level 30 Branch] hitbox-collision --> default-case[Default Case] click checkAppleInteraction href "#fn-checkappleinteraction" click early-exit href "#sub-early-exit" click distance-check href "#sub-distance-check" click hitbox-collision href "#sub-hitbox-collision" click sound-effect href "#sub-sound-effect" click level-10-branch href "#sub-level-10-branch" click level-20-branch href "#sub-level-20-branch" click level-30-branch href "#sub-level-30-branch" click default-case href "#sub-default-case" checkAppleInteraction --> nextLevel[nextLevel] nextLevel --> increment-level[Increment Level Counter] nextLevel --> max-check[Check for Game Completion] max-check -->|Complete| triggerCakeEvent[triggerCakeEvent] max-check -->|Not Complete| setupLevel[setupLevel] click nextLevel href "#fn-nextlevel" click increment-level href "#sub-increment-level" click max-check href "#sub-max-check" click triggerCakeEvent href "#fn-triggercakeevent" setupLevel --> state-reset[Game State Reset] setupLevel --> camera-reset[Camera Position Reset] setupLevel --> character-reset[You Character Reset] setupLevel --> event-flags-reset[Boss Event Flags Reset] setupLevel --> location-selection[Random Location Type] location-selection --> apple-placement[Apple Position Based on Location] apple-placement -->|Level 30| door-setup-l30[Level 30 Exit Door Setup] apple-placement --> difficulty-mapping[Difficulty Scaling via Map] click setupLevel href "#fn-setuplevel" click state-reset href "#sub-state-reset" click camera-reset href "#sub-camera-reset" click character-reset href "#sub-character-reset" click event-flags-reset href "#sub-event-flags-reset" click location-selection href "#sub-location-selection" click apple-placement href "#sub-apple-placement" click door-setup-l30 href "#sub-door-setup-l30" click difficulty-mapping href "#sub-difficulty-mapping" draw --> mousePressed[mousePressed] mousePressed --> pan-init[Initialize Panning] mousePressed --> audio-context[Ensure Audio Context] mousePressed --> apple-check[Check Apple Interaction] click mousePressed href "#fn-mousepressed" click pan-init href "#sub-pan-init" click audio-context href "#sub-audio-context" click apple-check href "#sub-apple-check" draw --> mouseDragged[mouseDragged] mouseDragged --> pan-delta[Calculate Pan Delta] mouseDragged --> pan-constrain[Constrain Pan Range] click mouseDragged href "#fn-mousedragged" click pan-delta href "#sub-pan-delta" click pan-constrain href "#sub-pan-constrain" draw --> mouseReleased[mouseReleased] mouseReleased --> stop-pan[Stop Panning] mouseReleased --> apple-check[Check Apple Interaction] click mouseReleased href "#fn-mousereleased" click stop-pan href "#sub-stop-pan" draw --> touchStarted[touchStarted] touchStarted --> touch-pan-init[Initialize Touch Panning] touchStarted --> audio-context[Ensure Audio Context] click touchStarted href "#fn-touchstarted" click touch-pan-init href "#sub-touch-pan-init" draw --> touchMoved[touchMoved] touchMoved --> touch-delta[Calculate Touch Pan Delta] click touchMoved href "#fn-touchmoved" click touch-delta href "#sub-touch-delta" draw --> touchEnded[touchEnded] touchEnded --> stop-touch-pan[Stop Touch Panning] touchEnded --> touch-apple-check[Check Apple on Touch Release] click touchEnded href "#fn-touchended" click stop-touch-pan href "#sub-stop-touch-pan" click touch-apple-check href "#sub-touch-apple-check" draw --> windowResized[windowResized] windowResized --> resize-canvas[Resize Canvas] windowResized --> reposition-buttons[Reposition UI Buttons] windowResized --> recalc-pan-limits[Recalculate Pan Limits] windowResized --> reset-level-safe[Reset Level Safe] click windowResized href "#fn-windowresized" click resize-canvas href "#sub-resize-canvas" click reposition-buttons href "#sub-reposition-buttons" click recalc-pan-limits href "#sub-recalc-pan-limits" click reset-level-safe href "#sub-reset-level-safe"

❓ Frequently Asked Questions

What visual elements are present in the Level game ok sketch?

The sketch features a detailed apple graphic, a character representing 'You', and various interactive elements like a food police car and a zombie that appear at different levels.

How can users interact with the Level game ok sketch?

Users can pan the screen, tap on the apple to progress through levels, and activate hints to assist in gameplay.

What creative coding techniques does the Level game ok sketch demonstrate?

The sketch showcases dynamic level progression, state management for events, and graphical panning effects to enhance user experience.

Preview

Level game ok - p5.js creative coding sketch preview
Sketch Preview
Code flow diagram showing the structure of Level game ok - Code flow showing setup, draw, setupLevel, checkAppleInteraction, nextLevel, showHint, restartGame, triggerCakeEvent, mousePressed, mouseDragged, mouseReleased, touchStarted, touchMoved, touchEnded, windowResized
Code Flow Diagram