Sketch 2026-03-06 00:48

This is a stealth puzzle game where you guide a mischievous goose through a garden, dodging gardeners' vision cones, splashing in ponds, and collecting items to complete missions. You can honk to distract guards, watch a mini-map radar, and advance through increasingly difficult levels by stealing items, dunking objects in lakes, or scaring targets.

🧪 Try This!

Experiment with the code by making these changes:

  1. Make gardeners see further — Increase the vision range so guards can spot you from further away, making stealth harder.
  2. Speed up the goose — Increase movement speed to 7, making you move faster and easier to escape guards but harder to steer precisely.
  3. Make honks deafen guards — Change the honk hearing range to 300 pixels so honks only distract guards that are closer, making the distraction mechanic less powerful.
  4. Slow down while holding items more — Change the held item speed penalty from 3.5 to 2, making it much harder to escape with a stolen item.
  5. Make gardeners persistent chasers — Increase the chase distance before giving up from 400 to 800, so guards will chase you further across the map.
Prefer the full editor? Open it there →

📖 About This Sketch

This sketch creates a fully playable stealth game where you pilot a goose through a garden avoiding gardeners. The visual appeal comes from the hand-drawn gardener characters with animated vision cones, the scrolling camera that follows the goose, particles that burst on victory, and a tactical mini-map showing enemies, targets, and terrain. The code demonstrates advanced p5.js techniques including vector-based AI pathfinding, collision detection, a field-of-view system that determines what guards can see, audio synthesis with the p5.sound library, and a complete game state machine.

The sketch is organized into a setup that initializes the canvas and audio, a draw loop that updates all game objects and renders the world with a camera offset, and five classes—Goose, Gardener, Item, Particle, and utility functions for levels and UI. By studying it, you will learn how to build state machines (PATROL, INVESTIGATE, CHASE, RETURN), implement vision-based enemy AI using angle and distance math, create a honk mechanic that broadcasts to nearby NPCs, render a minimap that scales world coordinates, and orchestrate win conditions across multiple task types. It's a masterclass in object-oriented game design.

⚙️ How It Works

  1. When the sketch loads, setup() creates the canvas, initializes audio oscillators and envelopes for sound effects, spawns the goose at the origin, and calls startLevel(1) which generates a random task (STEAL, DUNK, or SCARE), picks a target item type, spawns that item and decoys around the map, and positions one or more gardener guards near the target.
  2. Every frame, draw() clears the background, translates the camera to center on the goose, draws the world (grid, blanket home, pond lake), updates and renders all game objects in depth-sort order, and renders the UI overlay and mini-map. The camera follow and depth sort create the illusion of a 2D top-down world.
  3. The goose responds to WASD or arrow keys to move slower when holding an item, honks with the spacebar or H to emit sound that gardeners can hear up to 600 pixels away, and clicks to pick up nearby items or drop held ones. When you honk, each gardener within hearing range transitions from PATROL to INVESTIGATE state.
  4. Gardeners patrol their home point, maintain a 60-degree field of view with a 250-pixel range, and continuously check if the goose is visible using angle-difference math. If they see the goose, they enter CHASE state and move at full speed toward it. If they hear a honk, they enter INVESTIGATE state and walk to the sound source, allowing the player to lure them away from the target.
  5. When a gardener catches the goose (distance < 40), they force a drop and push the goose back, then return home. When the target item reaches the home blanket (STEAL), the pond (DUNK), or the goose honks near the target (SCARE), triggerVictory() plays a victory sound, spawns confetti particles, and counts down before advancing to the next level with increased difficulty.
  6. The mini-map in the top-right corner scales world coordinates by a factor proportional to canvas size, showing the pond (blue), blanket (pink), target item (gold), gardeners (red), and goose (white) so the player can plan routes and avoid guards.

🎓 Concepts You'll Learn

State machines (PATROL, INVESTIGATE, CHASE, RETURN)Vision cone detection using angles and distanceDistraction and NPC AI behaviorCamera following and depth sortingMini-map radar scaling and renderingGame state management (PLAYING, VICTORY)Audio synthesis with p5.soundParticle systems for visual feedbackVector math and collision detectionLevel generation and difficulty scaling

📝 Code Breakdown

setup()

setup() runs once when the sketch launches. It's where you initialize the canvas size, pre-compute colors, set up audio, and spawn the core game objects. Think of it as the 'load game' function.

function setup() {
  createCanvas(windowWidth, windowHeight);
  
  grassColor = color(108, 194, 74);
  pathColor = color(214, 203, 144);
  waterColor = color(100, 150, 255);
  
  setupAudio();

  // Initialize World
  goose = new Goose(0, 0);
  blanketPos = createVector(0, 0);
  pondPos = createVector(-400, 200);

  startLevel(1);

  userStartAudio();
}
Line-by-line explanation (10 lines)

🔧 Subcomponents:

function-call Canvas setup createCanvas(windowWidth, windowHeight);

Creates a full-window canvas that scales with the browser

assignment Color palette definition grassColor = color(108, 194, 74);

Stores reusable colors for the background and world elements

function-call Audio initialization setupAudio();

Initializes oscillators and envelopes for sound effects

assignment World object creation goose = new Goose(0, 0);

Creates the goose player at the origin and sets landmark positions

createCanvas(windowWidth, windowHeight);
Creates a canvas that fills the entire browser window; windowWidth and windowHeight update when the window resizes
grassColor = color(108, 194, 74);
Stores an RGB green color in a global variable so it can be reused for the background without hardcoding the numbers
pathColor = color(214, 203, 144);
Stores a sandy beige color for later use (currently unused in the code, but prepared for future path graphics)
waterColor = color(100, 150, 255);
Stores a light blue color for the pond that will be drawn later
setupAudio();
Calls the setupAudio function to initialize p5.sound oscillators and envelopes for honk and victory sounds
goose = new Goose(0, 0);
Creates a new Goose object at coordinates (0, 0); this is the player character you control
blanketPos = createVector(0, 0);
Sets the home/goal location (the blanket) at the origin; items brought here count as 'stolen'
pondPos = createVector(-400, 200);
Sets the pond/lake location 400 pixels left and 200 pixels down from the blanket; items dunked here complete DUNK tasks
startLevel(1);
Generates the first level: picks a random task, spawns the target item and decoys, and positions gardener guards
userStartAudio();
p5.sound requires user interaction to start audio; this call primes the system so sounds play when honk() is called

setupAudio()

This function uses p5.sound to create three oscillators with different waveforms and an envelope to shape the honk sound. Oscillators generate continuous sine/square/sawtooth waves; envelopes control how volume changes over time. This is the foundation of all synthesized sounds in the sketch.

function setupAudio() {
  osc = new p5.Oscillator('sawtooth');
  env = new p5.Envelope();
  env.setADSR(0.01, 0.1, 0.5, 0.1);
  env.setRange(0.5, 0); 
  osc.start(); osc.amp(0);

  winOsc = new p5.Oscillator('sine');
  winOsc.start(); winOsc.amp(0);
  
  angOsc = new p5.Oscillator('square');
  angOsc.start(); angOsc.amp(0);
}
Line-by-line explanation (9 lines)

🔧 Subcomponents:

assignment Honk oscillator setup osc = new p5.Oscillator('sawtooth');

Creates a sawtooth wave generator for the goose's honk sound—harsh, buzzy tone

assignment Honk envelope setup env.setADSR(0.01, 0.1, 0.5, 0.1);

Defines Attack, Decay, Sustain, Release timing to shape the honk sound's volume over time

assignment Victory oscillator setup winOsc = new p5.Oscillator('sine');

Creates a sine wave oscillator for the victory chime—smooth, musical tone

assignment Angry gardener oscillator setup angOsc = new p5.Oscillator('square');

Creates a square wave oscillator for the gardener's angry sound during chase—harsh, alarm-like tone

osc = new p5.Oscillator('sawtooth');
Creates a sawtooth wave oscillator; sawtooth is a bright, buzzy waveform perfect for a goose honk
env = new p5.Envelope();
Creates an Envelope object that will shape how the honk sound starts, peaks, and fades out
env.setADSR(0.01, 0.1, 0.5, 0.1);
Sets the ADSR times: Attack 10ms (instant), Decay 100ms (fall to sustain), Sustain 500ms (hold level), Release 100ms (fade out)
env.setRange(0.5, 0);
Sets the envelope to ramp from volume 0.5 (peak) to 0 (silent); the ADSR times define how fast this ramp happens
osc.start(); osc.amp(0);
Starts the oscillator running continuously in the background at zero volume; we'll trigger the honk sound by playing the envelope later
winOsc = new p5.Oscillator('sine');
Creates a sine wave oscillator for victory sounds; sine is smooth and musical
winOsc.start(); winOsc.amp(0);
Starts the victory oscillator running at zero volume; it will be triggered when you complete a task
angOsc = new p5.Oscillator('square');
Creates a square wave oscillator for angry gardener sounds; square is bright and harsh like an alarm
angOsc.start(); angOsc.amp(0);
Starts the angry oscillator running silently; it plays in short bursts when a gardener chases you

startLevel(n)

startLevel() is the level generator. It uses difficulty scaling (higher level = more enemies, bigger map), randomization (random task type, random item type), and spatial logic (spawn items in a circle, avoid the pond) to create varied, progressively harder challenges. This is the engine behind replayability.

🔬 This block locks harder tasks to level 3+. What happens if you change 'level > 2' to 'level > 0' so DUNK and SCARE appear on level 1? Or 'level > 100' so they never appear?

  let missionRoll = random();
  let taskType = "STEAL";
  if (level > 2) {
    if (missionRoll < 0.5) taskType = "STEAL";
    else if (missionRoll < 0.8) taskType = "DUNK";
    else taskType = "SCARE";
  }
function startLevel(n) {
  level = n;
  gameState = "PLAYING";
  items = []; 
  gardeners = [];
  
  // Difficulty
  let spawnRadius = 300 + (level * 60); 
  let numGardeners = level; // 1 gardener level 1, 2 level 2, etc.
  let types = ['bell', 'pumpkin', 'rake', 'radio', 'key', 'boot', 'mug', 'hat'];
  
  // Task Generation
  let missionRoll = random();
  let taskType = "STEAL";
  if (level > 2) {
    if (missionRoll < 0.5) taskType = "STEAL";
    else if (missionRoll < 0.8) taskType = "DUNK";
    else taskType = "SCARE";
  }

  let targetType = random(types);
  currentTask = {
    type: taskType,
    target: targetType,
    desc: generateTaskDescription(taskType, targetType)
  };

  // Spawn Target
  let tAngle = random(TWO_PI);
  let tx = cos(tAngle) * spawnRadius;
  let ty = sin(tAngle) * spawnRadius;
  // Ensure not in lake
  if (dist(tx, ty, pondPos.x, pondPos.y) < 150) tx += 200;
  
  items.push(new Item(tx, ty, targetType));
  
  // Spawn Gardeners near the target to guard it
  for(let i=0; i<numGardeners; i++) {
     gardeners.push(new Gardener(tx + random(-100, 100), ty + random(-100, 100)));
  }

  // Spawn Decoys
  let decoyCount = 4 + level;
  for (let i = 0; i < decoyCount; i++) {
    let rType = random(types);
    if(rType === targetType) continue; // Don't duplicate target type if possible
    let r = spawnRadius + random(-200, 200); 
    let angle = random(TWO_PI);
    items.push(new Item(cos(angle)*r, sin(angle)*r, rType));
  }
}
Line-by-line explanation (16 lines)

🔧 Subcomponents:

calculation Difficulty scaling let spawnRadius = 300 + (level * 60);

Higher levels place items further from start, making the map larger and enemies more spread out

conditional Task type randomization if (level > 2) {

Only offers DUNK and SCARE tasks on level 3+; earlier levels are simpler STEAL-only

calculation Target item spawn let tAngle = random(TWO_PI);

Picks a random direction around the origin to place the target item at distance spawnRadius

conditional Pond collision check if (dist(tx, ty, pondPos.x, pondPos.y) < 150) tx += 200;

If the target spawns in the pond, shifts it 200 pixels to the side so it's reachable

for-loop Gardener placement loop for(let i=0; i<numGardeners; i++) {

Places one or more gardeners around the target item as guards

for-loop Decoy item loop for (let i = 0; i < decoyCount; i++) {

Scatters fake items around the map to confuse the player

level = n;
Stores the level number passed in (1, 2, 3, etc.) as a global variable
gameState = "PLAYING";
Sets the game state back to PLAYING so the draw loop executes win-condition checks
items = [];
Clears the items array from the previous level so we start fresh with new items
gardeners = [];
Clears the gardeners array so old guards don't carry over to the new level
let spawnRadius = 300 + (level * 60);
Calculates how far from the blanket to place items; level 1 is 360 pixels, level 2 is 420, etc., making the map bigger and harder
let numGardeners = level;
Level 1 gets 1 gardener, level 2 gets 2, level 3 gets 3, etc., increasing difficulty
let types = ['bell', 'pumpkin', 'rake', 'radio', 'key', 'boot', 'mug', 'hat'];
Defines the pool of 8 different item types that can be randomly selected as targets or decoys
let missionRoll = random();
Generates a random number 0–1 to decide which task type the player gets
if (level > 2) {
On level 3 and higher, unlock harder task types (DUNK and SCARE); before that, only STEAL
let targetType = random(types);
Picks one random item type from the types array; this is what the player must find
let tAngle = random(TWO_PI);
Generates a random angle 0–360 degrees to place the target in a random direction
let tx = cos(tAngle) * spawnRadius;
Converts the angle and spawnRadius into an x-coordinate using cosine (standard polar-to-cartesian conversion)
let ty = sin(tAngle) * spawnRadius;
Converts the angle and spawnRadius into a y-coordinate using sine
if (dist(tx, ty, pondPos.x, pondPos.y) < 150) tx += 200;
If the target spawned inside the pond (distance < 150 pixels), shift the x-coordinate 200 pixels to move it out
for(let i=0; i<numGardeners; i++) {
Loops once per gardener (level determines count) and places each slightly offset from the target's position
for (let i = 0; i < decoyCount; i++) {
Loops to spawn 4 + level decoy items at various distances; more decoys on higher levels to confuse the player

draw()

The draw loop is the heartbeat of the game—60 times per second it clears, updates, sorts, and renders everything. The camera translate makes the world follow the player; the depth sort ensures visual layers look correct (things further down the screen appear on top). Understanding this function is key to all interactive p5.js sketches.

🔬 The translate line is the camera. What happens if you change it to translate(0, 0) or translate(width / 2, height / 2) instead? The camera will stop following the goose—where does the goose end up on screen?

  push();
  // Camera Follow
  translate(width / 2 - goose.pos.x, height / 2 - goose.pos.y);

  drawWorld();

🔬 What if you remove the sort line entirely? Objects will draw in the order they appear in the array (items first, then goose, then gardeners). How does the visual depth feel different? Try commenting out just the sort line and see—gardeners will sometimes appear behind items and the goose.

  // Draw Sort Order
  let drawList = [...items, goose, ...gardeners];
  drawList.sort((a, b) => a.pos.y - b.pos.y);
function draw() {
  background(grassColor);

  push();
  // Camera Follow
  translate(width / 2 - goose.pos.x, height / 2 - goose.pos.y);

  drawWorld(); 
  
  if (gameState === "PLAYING") {
    checkWinCondition();
  } else if (gameState === "VICTORY") {
    runVictoryTimer();
  }

  // Draw Sort Order
  let drawList = [...items, goose, ...gardeners];
  drawList.sort((a, b) => a.pos.y - b.pos.y);

  for (let obj of drawList) {
    if (obj instanceof Goose) {
      obj.update();
      obj.show();
    } else if (obj instanceof Gardener) {
      obj.update();
      obj.show(); // Draw gardener body
      // We draw view cones separately or in show, handled inside class
    } else {
      obj.show();
    }
  }
  
  // Confetti
  for (let i = particles.length - 1; i >= 0; i--) {
    particles[i].update();
    particles[i].show();
    if (particles[i].finished()) particles.splice(i, 1);
  }

  drawGuideArrow();
  pop();

  drawUI();
  drawMiniMap();
}
Line-by-line explanation (26 lines)

🔧 Subcomponents:

transformation Camera offset translation translate(width / 2 - goose.pos.x, height / 2 - goose.pos.y);

Shifts the entire world so the goose appears centered on screen while the camera follows it

conditional Game state logic if (gameState === "PLAYING") {

Runs win-condition checks during gameplay; skips them during victory animation

sort Depth sorting by Y coordinate drawList.sort((a, b) => a.pos.y - b.pos.y);

Sorts objects by Y position (top to bottom) so objects further down the screen draw on top, creating proper 3D perspective

for-loop Update and render loop for (let obj of drawList) {

Iterates through all game objects, updates their state (movement, animation), and draws them to the screen

for-loop Particle system loop for (let i = particles.length - 1; i >= 0; i--) {

Updates and renders confetti particles; removes finished ones from the array

background(grassColor);
Fills the entire canvas with the grass color, erasing the previous frame so objects don't trail
push();
Saves the current transformation state (scaling, rotation, translation) so we can restore it later
translate(width / 2 - goose.pos.x, height / 2 - goose.pos.y);
Shifts the entire world coordinate system so the goose's position maps to the center of the screen; the world moves as the goose moves
drawWorld();
Calls the drawWorld function to render the grid, blanket, and pond
if (gameState === "PLAYING") {
If the game is in PLAYING state, check if the player has won; during VICTORY, this is skipped
checkWinCondition();
Calls the function that tests whether the current task is complete (item at blanket, pond, or honked)
} else if (gameState === "VICTORY") {
If the game is in VICTORY state, run the victory timer instead
let drawList = [...items, goose, ...gardeners];
Creates a new array combining all items, the goose, and all gardeners using spread operator (...) to copy
drawList.sort((a, b) => a.pos.y - b.pos.y);
Sorts the list by Y position ascending (smallest Y first); objects with larger Y values draw later and appear on top, creating proper depth
for (let obj of drawList) {
Loops through each object in the sorted draw list
if (obj instanceof Goose) {
Checks if the current object is a Goose; if true, update and render it
obj.update();
Calls the goose's update method to process input, move it, and update held items
obj.show();
Calls the goose's show method to draw it to the canvas
} else if (obj instanceof Gardener) {
Checks if the object is a Gardener
obj.update();
Calls the gardener's update method to run AI (vision checks, state transitions, movement)
obj.show();
Calls the gardener's show method to draw the character and vision cone
} else {
For any other object (items), skip update and only render
obj.show();
Draws items directly without updating them (items don't move on their own unless held)
for (let i = particles.length - 1; i >= 0; i--) {
Loops backwards through the particle array (important for safe removal during iteration)
particles[i].update();
Updates the particle's position and velocity (particles slow down and fade)
particles[i].show();
Draws the particle to the screen
if (particles[i].finished()) particles.splice(i, 1);
If the particle's life has expired, removes it from the array (splice at index i removes one element)
drawGuideArrow();
Calls the function to draw a compass arrow pointing to the next target (appears when target is far away)
pop();
Restores the transformation state saved by push(), exiting the camera-centered coordinate system
drawUI();
Calls the function to render the UI overlay (task description, level, controls)
drawMiniMap();
Calls the function to render the mini-map in the top-right corner

checkWinCondition()

This function defines the win conditions for each of the three task types. It runs every frame during PLAYING state, constantly checking if you've satisfied the goal. The three task types demonstrate conditional branching and distance-based collision logic—core concepts in game design.

🔬 The STEAL check requires goose.heldItem !== target—meaning the goose must have DROPPED the item. What happens if you remove that condition (change to just 'if (d < 80)')? The goose could win while still carrying the item. Is that easier or harder?

  if (currentTask.type === "STEAL") {
    let d = dist(target.pos.x, target.pos.y, blanketPos.x, blanketPos.y);
    if (d < 80 && goose.heldItem !== target) triggerVictory(blanketPos);
  }
function checkWinCondition() {
  let target = items.find(i => i.type === currentTask.target);
  if (!target) return; 

  if (currentTask.type === "STEAL") {
    let d = dist(target.pos.x, target.pos.y, blanketPos.x, blanketPos.y);
    if (d < 80 && goose.heldItem !== target) triggerVictory(blanketPos);
  }
  else if (currentTask.type === "DUNK") {
    let d = dist(target.pos.x, target.pos.y, pondPos.x, pondPos.y);
    if (d < 100 && goose.heldItem !== target) triggerVictory(pondPos);
  }
  else if (currentTask.type === "SCARE") {
    let d = dist(goose.pos.x, goose.pos.y, target.pos.x, target.pos.y);
    if (d < 120 && goose.heldItem !== target && goose.honkFrame > 15) triggerVictory(target.pos);
  }
}
Line-by-line explanation (11 lines)

🔧 Subcomponents:

calculation Target item lookup let target = items.find(i => i.type === currentTask.target);

Searches the items array for the one item whose type matches the current task target

conditional STEAL task completion if (currentTask.type === "STEAL") {

Tests if the target item is close enough to the blanket home position

conditional DUNK task completion else if (currentTask.type === "DUNK") {

Tests if the target item is close enough to the pond

conditional SCARE task completion else if (currentTask.type === "SCARE") {

Tests if the goose is close to the target AND honked recently (honkFrame > 15)

let target = items.find(i => i.type === currentTask.target);
Uses the find method to search the items array for the first item whose type property matches currentTask.target; returns undefined if not found
if (!target) return;
If no target item was found (e.g., it was already picked up or the level is broken), exit the function early
if (currentTask.type === "STEAL") {
If the current task is to STEAL an item, check the steal win condition
let d = dist(target.pos.x, target.pos.y, blanketPos.x, blanketPos.y);
Calculates the distance between the target item's position and the blanket's position
if (d < 80 && goose.heldItem !== target) triggerVictory(blanketPos);
If the distance is less than 80 pixels AND the goose is not holding the target (item was dropped at the blanket), trigger victory
else if (currentTask.type === "DUNK") {
If the task is to DUNK an item in the pond, check the dunk win condition
let d = dist(target.pos.x, target.pos.y, pondPos.x, pondPos.y);
Calculates the distance between the target item and the pond's center
if (d < 100 && goose.heldItem !== target) triggerVictory(pondPos);
If the distance is less than 100 pixels AND the item is not in the goose's beak, the item is in the pond—victory!
else if (currentTask.type === "SCARE") {
If the task is to SCARE the target, check the scare win condition
let d = dist(goose.pos.x, goose.pos.y, target.pos.x, target.pos.y);
Calculates the distance between the goose and the target item (not the blanket or pond)
if (d < 120 && goose.heldItem !== target && goose.honkFrame > 15) triggerVictory(target.pos);
Victory if: goose is within 120 pixels of the target, NOT holding it, AND honkFrame > 15 (meaning a honk happened at least 5 frames ago, confirming intent)

drawMiniMap()

The mini-map is a radar view of the world scaled to fit a small window. The key insight is coordinate transformation: converting world positions (which can be thousands of pixels apart) to map positions (150×150) using multiplication by a scale factor. This is a fundamental technique in game development for displaying large worlds.

🔬 The scaleF factor determines how much the world shrinks to fit the map. What if you change worldSize from 2000 to 500? The scale factor becomes larger, and everything on the map will be bigger. How does this change your tactical awareness?

  let scaleF = mapSize / worldSize; // Scale world to map

  // Map BG
  fill(0, 100); noStroke();
  rect(mapX, mapY, mapSize, mapSize, 10);
  
  // Center (0,0 of map)
  let cx = mapX + mapSize/2;
  let cy = mapY + mapSize/2;
function drawMiniMap() {
  let mapSize = 150;
  let mapX = width - mapSize - 20;
  let mapY = height - mapSize - 20;
  let scaleF = mapSize / worldSize; // Scale world to map

  // Map BG
  fill(0, 100); noStroke();
  rect(mapX, mapY, mapSize, mapSize, 10);
  
  // Center (0,0 of map)
  let cx = mapX + mapSize/2;
  let cy = mapY + mapSize/2;
  
  // Helper to map coordinates
  function mapPoint(x, y) {
    return {
      x: cx + (x * scaleF),
      y: cy + (y * scaleF)
    };
  }

  // Draw Pond
  let mp = mapPoint(pondPos.x, pondPos.y);
  fill(100, 150, 255); ellipse(mp.x, mp.y, 30 * scaleF * 4); // Scaled pond

  // Draw Blanket
  let mb = mapPoint(blanketPos.x, blanketPos.y);
  fill(255, 200, 200); rect(mb.x - 5, mb.y - 5, 10, 10);

  // Draw Target Item (Gold)
  for(let item of items) {
    if(item.type === currentTask.target) {
      let mi = mapPoint(item.pos.x, item.pos.y);
      fill(255, 215, 0); ellipse(mi.x, mi.y, 6, 6);
    }
  }

  // Draw Gardeners (Red)
  for(let g of gardeners) {
    let mg = mapPoint(g.pos.x, g.pos.y);
    fill(255, 0, 0); ellipse(mg.x, mg.y, 5, 5);
  }

  // Draw Goose (White)
  let mg = mapPoint(goose.pos.x, goose.pos.y);
  fill(255); ellipse(mg.x, mg.y, 6, 6);
  
  // Label
  fill(255); textAlign(CENTER, BOTTOM); textSize(10);
  text("MAP", cx, mapY);
}
Line-by-line explanation (25 lines)

🔧 Subcomponents:

calculation Map size and position let mapSize = 150;

Sets the mini-map's width and height in pixels

calculation World-to-map scaling let scaleF = mapSize / worldSize;

Converts world coordinates to map coordinates; dividing by worldSize (2000) shrinks everything proportionally

function Coordinate transformation helper function mapPoint(x, y) {

Converts a world position (x, y) into a screen position on the mini-map

drawing Pond rendering fill(100, 150, 255); ellipse(mp.x, mp.y, 30 * scaleF * 4);

Draws a blue circle representing the pond at the scaled position

for-loop Gardener marker loop for(let g of gardeners) {

Loops through all gardeners and draws red dots for each on the map

let mapSize = 150;
Sets the mini-map to a 150×150 pixel square
let mapX = width - mapSize - 20;
Positions the map 20 pixels from the right edge of the canvas (width - 150 - 20)
let mapY = height - mapSize - 20;
Positions the map 20 pixels from the bottom edge (height - 150 - 20)
let scaleF = mapSize / worldSize;
Calculates a scale factor: 150 / 2000 ≈ 0.075; multiplying any world coordinate by this shrinks it to map size
fill(0, 100); noStroke();
Sets the fill to semi-transparent black and removes stroke for a dark background
rect(mapX, mapY, mapSize, mapSize, 10);
Draws a dark rounded rectangle as the map's background
let cx = mapX + mapSize/2;
Calculates the map's center X coordinate (left edge + half the map width)
let cy = mapY + mapSize/2;
Calculates the map's center Y coordinate (top edge + half the map height)
function mapPoint(x, y) {
Defines a local helper function that converts world coordinates to map coordinates
return { x: cx + (x * scaleF), y: cy + (y * scaleF) };
Returns an object with scaled x and y; the map center (cx, cy) is offset by the scaled world position
let mp = mapPoint(pondPos.x, pondPos.y);
Converts the pond's world position to map coordinates
fill(100, 150, 255); ellipse(mp.x, mp.y, 30 * scaleF * 4);
Draws a blue circle at the pond's map position; the diameter (30 * scaleF * 4) is scaled to match the map
let mb = mapPoint(blanketPos.x, blanketPos.y);
Converts the blanket's world position to map coordinates
fill(255, 200, 200); rect(mb.x - 5, mb.y - 5, 10, 10);
Draws a light pink 10×10 square at the blanket's map position
for(let item of items) {
Loops through all items on the map
if(item.type === currentTask.target) {
Filters to only the target item (ignores decoys)
let mi = mapPoint(item.pos.x, item.pos.y);
Converts the target item's world position to map coordinates
fill(255, 215, 0); ellipse(mi.x, mi.y, 6, 6);
Draws a small gold circle for the target item on the map
for(let g of gardeners) {
Loops through all gardeners
let mg = mapPoint(g.pos.x, g.pos.y);
Converts each gardener's position to map coordinates
fill(255, 0, 0); ellipse(mg.x, mg.y, 5, 5);
Draws a small red circle for each gardener on the map
let mg = mapPoint(goose.pos.x, goose.pos.y);
Converts the goose's position to map coordinates
fill(255); ellipse(mg.x, mg.y, 6, 6);
Draws a white circle for the goose on the map
fill(255); textAlign(CENTER, BOTTOM); textSize(10);
Sets the text color to white, centered alignment, and small font size
text("MAP", cx, mapY);
Renders the label 'MAP' centered above the map box

Gardener class

The Gardener class is the brain of the game's AI. It uses a state machine (PATROL, INVESTIGATE, CHASE, RETURN) to model realistic guard behavior. The vision cone system uses angle and distance math to determine if a target is visible—this is a fundamental pattern in game AI. The honk distraction mechanic shows how to override AI behavior with player interaction, a key element of stealth game design.

🔬 These two lines control when gardeners start and stop chasing. What happens if you change the second condition to dToGoose > 200? Gardeners will give up the chase sooner, making the game easier. Or change to dToGoose > 800 to have them chase you across the entire map.

    // State Transitions
    if (canSee) {
      this.state = "CHASE";
    } else if (this.state === "CHASE" && dToGoose > 400) {
      this.state = "RETURN"; // Lost him
    }
class Gardener {
  constructor(x, y) {
    this.homePos = createVector(x, y);
    this.pos = createVector(x, y);
    this.vel = createVector(0, 0);
    this.state = "PATROL"; // PATROL, INVESTIGATE, CHASE, RETURN
    this.angle = 0;
    
    // AI Params
    this.fovAngle = PI / 3; // 60 degrees view cone
    this.fovDist = 250;
    this.patrolRadius = 200;
    this.patrolTarget = this.pickPatrolPoint();
    this.investigateTarget = null;
    this.waitTimer = 0;
  }
  
  pickPatrolPoint() {
    let angle = random(TWO_PI);
    let r = random(50, this.patrolRadius);
    return createVector(this.homePos.x + cos(angle)*r, this.homePos.y + sin(angle)*r);
  }
  
  hearHonk(honkPos) {
    // Distraction Mechanic!
    // If hearing a honk, stop patrolling and go check it out
    if (this.state !== "CHASE") {
      this.state = "INVESTIGATE";
      this.investigateTarget = honkPos.copy();
      this.waitTimer = 120; // Will wait at the sound location
      
      // Visual feedback
      fill(255); text("?", this.pos.x, this.pos.y - 60);
    }
  }

  update() {
    let dToGoose = dist(this.pos.x, this.pos.y, goose.pos.x, goose.pos.y);
    
    // --- VISION CHECK ---
    // Vector to goose
    let toGoose = p5.Vector.sub(goose.pos, this.pos);
    let angleToGoose = toGoose.heading();
    let angleDiff = abs(angleToGoose - this.angle);
    if (angleDiff > PI) angleDiff = TWO_PI - angleDiff;
    
    // Can I see the goose?
    let canSee = (dToGoose < this.fovDist && angleDiff < this.fovAngle / 2);
    
    // State Transitions
    if (canSee) {
      this.state = "CHASE";
    } else if (this.state === "CHASE" && dToGoose > 400) {
      this.state = "RETURN"; // Lost him
    }

    // --- BEHAVIOR ---
    let target = this.pos;
    let speed = 2;

    if (this.state === "PATROL") {
      target = this.patrolTarget;
      speed = 1.5;
      if (dist(this.pos.x, this.pos.y, target.x, target.y) < 10) {
        if (random() < 0.02) this.patrolTarget = this.pickPatrolPoint();
      }
    } 
    else if (this.state === "INVESTIGATE") {
      target = this.investigateTarget;
      speed = 2.5; // Walk faster to sound
      if (dist(this.pos.x, this.pos.y, target.x, target.y) < 20) {
        // Arrived at sound source
        this.waitTimer--;
        if (this.waitTimer <= 0) this.state = "RETURN";
      }
    }
    else if (this.state === "RETURN") {
      target = this.homePos;
      speed = 2;
      if (dist(this.pos.x, this.pos.y, target.x, target.y) < 10) {
        this.state = "PATROL";
      }
    }
    else if (this.state === "CHASE") {
      target = goose.pos;
      speed = 4.3; // Faster than goose carrying item
      
      // Angry Sound
      if (frameCount % 15 === 0) {
        angOsc.freq(random(100, 150));
        angOsc.amp(0.1, 0.05);
        angOsc.amp(0, 0.2);
      }
    }

    // Move
    let dir = p5.Vector.sub(target, this.pos);
    if (dir.mag() > 1) {
      dir.setMag(speed);
      this.pos.add(dir);
      // Smooth Rotation
      let targetA = dir.heading();
      let diff = targetA - this.angle;
      if (diff > PI) diff -= TWO_PI; if (diff < -PI) diff += TWO_PI;
      this.angle += diff * 0.1;
    }
    
    // Collision (Catch logic)
    if (dToGoose < 40) {
      goose.dropItem();
      // Shove goose
      let push = p5.Vector.sub(goose.pos, this.pos).setMag(15);
      goose.pos.add(push);
      this.state = "RETURN"; // Job done
    }
  }
  
  show() {
    push();
    translate(this.pos.x, this.pos.y);
    
    // Draw View Cone (Visual feedback for stealth)
    noStroke();
    if (this.state === "CHASE") fill(255, 0, 0, 50); // Red alert
    else if (this.state === "INVESTIGATE") fill(255, 255, 0, 50); // Yellow suspicious
    else fill(200, 200, 255, 30); // Blue passive
    
    arc(0, 0, this.fovDist*2, this.fovDist*2, this.angle - this.fovAngle/2, this.angle + this.fovAngle/2, PIE);

    rotate(this.angle);
    
    // Body
    fill(50, 50, 150);
    rectMode(CENTER);
    rect(0, 0, 40, 50, 10);
    
    // Hat
    fill(210, 180, 140);
    ellipse(0, 0, 50, 50); 
    fill(180, 150, 110);
    ellipse(0, 0, 25, 25); 
    
    // Hands
    stroke(50, 50, 150); strokeWeight(8);
    line(10, 0, 25, 10);
    line(10, 0, 25, -10);
    
    pop();
    
    // Status Icon
    if (this.state === "CHASE") {
      fill(255, 0, 0); textAlign(CENTER); text("!", this.pos.x, this.pos.y - 40);
    } else if (this.state === "INVESTIGATE") {
      fill(255, 255, 0); textAlign(CENTER); text("?", this.pos.x, this.pos.y - 40);
    }
  }
}
Line-by-line explanation (69 lines)

🔧 Subcomponents:

function Gardener constructor constructor(x, y) {

Initializes a new gardener with position, state, and AI parameters

function Patrol point picker pickPatrolPoint() {

Generates a random point within the patrolRadius for the gardener to walk toward

function Honk distraction handler hearHonk(honkPos) {

Called when a honk is within hearing range; transitions to INVESTIGATE state

conditional Vision cone detection let canSee = (dToGoose < this.fovDist && angleDiff < this.fovAngle / 2);

Tests whether the goose is within the gardener's vision cone (distance and angle)

conditional State-based behavior selection if (this.state === "PATROL") {

Selects different movement speed and target based on current state

conditional Collision and catch logic if (dToGoose < 40) {

If the gardener touches the goose, the goose drops its item and is pushed back

constructor(x, y) {
Initializes a new Gardener; x and y are the starting position
this.homePos = createVector(x, y);
Stores the home position where the gardener starts and returns to after losing the goose
this.pos = createVector(x, y);
Current position; updates as the gardener moves
this.state = "PATROL";
Initial state is PATROL; the gardener will walk around its home area looking for intruders
this.fovAngle = PI / 3;
Field of view angle: PI / 3 radians = 60 degrees; the cone is half this width on each side (30°)
this.fovDist = 250;
How far the gardener can see ahead; the vision cone extends 250 pixels from the gardener
pickPatrolPoint() {
Helper method that generates a new random patrol target within patrolRadius of home
let angle = random(TWO_PI);
Picks a random direction (0 to 360 degrees)
let r = random(50, this.patrolRadius);
Picks a random distance between 50 and 200 pixels from home
return createVector(this.homePos.x + cos(angle)*r, this.homePos.y + sin(angle)*r);
Uses polar-to-cartesian conversion to return a random point in a circle around home
hearHonk(honkPos) {
Called when the goose honks within hearing range; makes the gardener investigate the sound
if (this.state !== "CHASE") {
Only distracted if not already chasing (once you're seen, honks don't help)
this.state = "INVESTIGATE";
Changes state to INVESTIGATE to walk toward the sound
this.investigateTarget = honkPos.copy();
Stores the sound source position as the investigation target
this.waitTimer = 120;
Will wait 120 frames (2 seconds) at the sound source before giving up and returning home
let dToGoose = dist(this.pos.x, this.pos.y, goose.pos.x, goose.pos.y);
Calculates the distance between the gardener and the goose
let toGoose = p5.Vector.sub(goose.pos, this.pos);
Creates a vector pointing from the gardener toward the goose
let angleToGoose = toGoose.heading();
Calculates the angle of that vector (direction to the goose in radians)
let angleDiff = abs(angleToGoose - this.angle);
Finds the angular difference between where the gardener is facing and where the goose is
if (angleDiff > PI) angleDiff = TWO_PI - angleDiff;
Normalizes the angle to the shorter arc (e.g., -10° becomes 350°, but we want 10°)
let canSee = (dToGoose < this.fovDist && angleDiff < this.fovAngle / 2);
Vision check: goose is in range (distance < 250) AND within the view cone (angle difference < 30°)
if (canSee) {
If the goose is visible, immediately enter CHASE state
this.state = "CHASE";
Sets state to CHASE so the gardener will pursue at full speed
} else if (this.state === "CHASE" && dToGoose > 400) {
If already chasing but the goose gets more than 400 pixels away, give up
this.state = "RETURN";
Changes to RETURN state to head back to home
if (this.state === "PATROL") {
Patrol behavior: slowly walk toward patrol targets in a circle around home
target = this.patrolTarget;
Walk toward the current patrol point
speed = 1.5;
Move slowly while patrolling
if (dist(this.pos.x, this.pos.y, target.x, target.y) < 10) {
When close enough to the patrol target (within 10 pixels)
if (random() < 0.02) this.patrolTarget = this.pickPatrolPoint();
2% chance per frame to pick a new patrol point (so gardeners don't get stuck on one spot)
else if (this.state === "INVESTIGATE") {
Investigate behavior: walk toward the honk sound at medium speed
target = this.investigateTarget;
Walk toward the sound source
speed = 2.5;
Walk faster to investigate than normal patrol
if (dist(this.pos.x, this.pos.y, target.x, target.y) < 20) {
When close enough to the sound source (within 20 pixels)
this.waitTimer--;
Decrement the wait timer each frame
if (this.waitTimer <= 0) this.state = "RETURN";
After waiting 120 frames with no sign of trouble, give up investigating and head home
else if (this.state === "RETURN") {
Return behavior: walk back to home base
target = this.homePos;
Walk toward the starting position
if (dist(this.pos.x, this.pos.y, target.x, target.y) < 10) {
When close enough to home
this.state = "PATROL";
Resume patrolling around home
else if (this.state === "CHASE") {
Chase behavior: pursue the goose at high speed
target = goose.pos;
Always move toward the goose's current position
speed = 4.3;
Move faster than the goose (4.3 vs. 5 base, but goose slows to 3.5 when carrying items)
if (frameCount % 15 === 0) {
Every 15 frames, play an angry sound
angOsc.freq(random(100, 150));
Set a random frequency in the 100–150 Hz range (low, angry)
angOsc.amp(0.1, 0.05);
Ramp the amplitude to 0.1 over 50ms
angOsc.amp(0, 0.2);
Fade to silence over 200ms
let dir = p5.Vector.sub(target, this.pos);
Calculate the direction vector from the gardener to the target
if (dir.mag() > 1) {
Only move if the gardener is more than 1 pixel away from the target (avoid jittering)
dir.setMag(speed);
Normalize the direction vector and scale it to the current speed
this.pos.add(dir);
Add the direction vector to the position, moving the gardener one step toward the target
let targetA = dir.heading();
Calculate the angle the gardener should face (toward the target)
let diff = targetA - this.angle;
Find the angular difference between current facing and desired facing
if (diff > PI) diff -= TWO_PI; if (diff < -PI) diff += TWO_PI;
Normalize the angle difference to the shortest arc (e.g., 350° becomes -10°)
this.angle += diff * 0.1;
Smoothly rotate toward the target angle, moving 10% of the way each frame
if (dToGoose < 40) {
If the gardener is touching the goose (within 40 pixels)
goose.dropItem();
Force the goose to drop whatever it's carrying
let push = p5.Vector.sub(goose.pos, this.pos).setMag(15);
Calculate a vector pushing the goose away from the gardener with magnitude 15
goose.pos.add(push);
Apply the push, shoving the goose back
this.state = "RETURN";
After catching the goose, return to home base
show() {
Renders the gardener and its vision cone to the canvas
if (this.state === "CHASE") fill(255, 0, 0, 50);
Draw the vision cone red and semi-transparent when chasing (alert state)
else if (this.state === "INVESTIGATE") fill(255, 255, 0, 50);
Draw it yellow when investigating (suspicious state)
else fill(200, 200, 255, 30);
Draw it light blue when patrolling (passive state)
arc(0, 0, this.fovDist*2, this.fovDist*2, this.angle - this.fovAngle/2, this.angle + this.fovAngle/2, PIE);
Draw the vision cone as a pie slice with width fovDist and angle fovAngle, centered on the gardener's facing angle
rotate(this.angle);
Rotate the coordinate system so the gardener's body points in the direction they're facing
ellipse(0, 0, 50, 50);
Draw a large ellipse (hat) as the head
line(10, 0, 25, 10);
Draw the left arm reaching out
text("!", this.pos.x, this.pos.y - 40);
Draw a red '!' above the gardener when chasing (alert indicator)

Goose class

The Goose class handles player input, movement, item carrying, and interaction. The smooth angle rotation (0.15 easing) creates natural feeling movement instead of snapping. The honk broadcast demonstrates how to trigger events across multiple objects. The try-Interact function shows how to find the closest object within a range—a pattern you'll use in many games.

🔬 This code slows the goose when carrying an item. What happens if you remove the second line or change 3.5 to 5? The goose will move at full speed even while holding the item—does the game become too easy?

    let moveSpeed = 5;
    if (this.heldItem) moveSpeed = 3.5; // Heavy item penalty
class Goose {
  constructor(x, y) {
    this.pos = createVector(x, y);
    this.angle = 0;
    this.waddle = 0;
    this.heldItem = null;
    this.honkFrame = 0; 
  }
  
  update() {
    let input = createVector(0,0);
    if(keyIsDown(87)||keyIsDown(UP_ARROW)) input.y--;
    if(keyIsDown(83)||keyIsDown(DOWN_ARROW)) input.y++;
    if(keyIsDown(65)||keyIsDown(LEFT_ARROW)) input.x--;
    if(keyIsDown(68)||keyIsDown(RIGHT_ARROW)) input.x++;
    
    let moveSpeed = 5;
    if (this.heldItem) moveSpeed = 3.5; // Heavy item penalty
    
    if(input.mag()>0) {
      input.setMag(moveSpeed);
      this.pos.add(input);
      let targetA = input.heading();
      let diff = targetA - this.angle;
      if (diff > PI) diff -= TWO_PI;
      if (diff < -PI) diff += TWO_PI;
      this.angle += diff * 0.15;
      this.waddle += 0.3;
    } else {
      this.waddle = lerp(this.waddle, 0, 0.2);
    }
    
    if(this.heldItem) {
      let offset = p5.Vector.fromAngle(this.angle).mult(40);
      this.heldItem.pos.x = this.pos.x + offset.x;
      this.heldItem.pos.y = this.pos.y + offset.y - 10;
    }
  }
  
  honk() {
    this.honkFrame = 20; 
    osc.freq(random(350, 450)); env.play(osc); osc.freq(300, 0.1);
    
    // NOTIFY GARDENERS (Distraction Mechanic)
    for(let g of gardeners) {
      let d = dist(this.pos.x, this.pos.y, g.pos.x, g.pos.y);
      if (d < 600) { // Hearing range
        g.hearHonk(this.pos);
      }
    }
  }
  
  dropItem() {
    this.heldItem = null;
  }
  
  tryInteract(x, y) {
    if(this.heldItem) {
      this.dropItem();
    } else {
      let beak = p5.Vector.fromAngle(this.angle).mult(40).add(this.pos);
      let closest = null; let minD = 70;
      for(let i of items) {
        let d = dist(beak.x, beak.y, i.pos.x, i.pos.y);
        if(d < minD) { minD = d; closest = i; }
      }
      if(closest) this.heldItem = closest;
    }
  }
  
  show() {
    push();
    translate(this.pos.x, this.pos.y);
    if(this.honkFrame > 0) {
      this.honkFrame--;
      push(); rotate(this.angle); stroke(0); strokeWeight(2);
      line(45, -10, 60, -20); line(50, 0, 70, 0); line(45, 10, 60, 20);
      pop();
    }
    rotate(this.angle + sin(this.waddle)*0.1);
    noStroke();
    fill(255, 165, 0); 
    ellipse(10, 15 + sin(this.waddle)*10, 15, 15);
    ellipse(10, -15 - sin(this.waddle)*10, 15, 15);
    fill(255); ellipse(0, 0, 70, 50); ellipse(30, 0, 40, 35); 
    fill(255, 165, 0); triangle(40, -5, 40, 5, 60, 0);
    fill(0); ellipse(40, -8, 4, 4); ellipse(40, 8, 4, 4);
    pop();
  }
}
Line-by-line explanation (65 lines)

🔧 Subcomponents:

function Goose constructor constructor(x, y) {

Initializes the goose with position, facing angle, waddle animation state, and item holding

conditional Keyboard input processing if(keyIsDown(87)||keyIsDown(UP_ARROW)) input.y--;

Detects WASD and arrow keys to build a movement input vector

conditional Carrying item speed penalty if (this.heldItem) moveSpeed = 3.5;

Reduces movement speed when holding an item, making stealth harder but giving guards more time to react

calculation Smooth angle interpolation this.angle += diff * 0.15;

Gradually rotates the goose toward the direction it's moving instead of snapping instantly

calculation Held item positioning if(this.heldItem) {

Updates the held item's position to follow the goose, offset in front of its beak

for-loop Honk notification broadcast for(let g of gardeners) {

Notifies all gardeners within 600 pixels that a honk was heard

for-loop Closest item detection for(let i of items) {

Finds the item closest to the goose's beak to determine what can be picked up

constructor(x, y) {
Initializes a new Goose at position (x, y)
this.pos = createVector(x, y);
Stores the goose's position as a p5.Vector
this.angle = 0;
Starts facing angle 0 (facing right)
this.waddle = 0;
Waddle animation state (0–1 range); oscillates as the goose walks to create bobbing motion
this.heldItem = null;
No item is being held initially
this.honkFrame = 0;
Honk animation counter; > 0 means the honk lines are currently visible
let input = createVector(0,0);
Creates a vector to accumulate keyboard input; x and y will be -1, 0, or 1 depending on which keys are pressed
if(keyIsDown(87)||keyIsDown(UP_ARROW)) input.y--;
If W or UP arrow is pressed, decrement y to move up (negative Y is up in p5)
if(keyIsDown(83)||keyIsDown(DOWN_ARROW)) input.y++;
If S or DOWN arrow is pressed, increment y to move down
if(keyIsDown(65)||keyIsDown(LEFT_ARROW)) input.x--;
If A or LEFT arrow is pressed, decrement x to move left
if(keyIsDown(68)||keyIsDown(RIGHT_ARROW)) input.x++;
If D or RIGHT arrow is pressed, increment x to move right
let moveSpeed = 5;
Default movement speed: 5 pixels per frame when not carrying anything
if (this.heldItem) moveSpeed = 3.5;
If holding an item, reduce speed to 3.5 pixels per frame (30% slower), making it easier for guards to catch you
if(input.mag()>0) {
If the input vector has any magnitude (at least one key was pressed)
input.setMag(moveSpeed);
Normalize the input vector and scale it to moveSpeed, creating uniform motion in any direction
this.pos.add(input);
Add the movement vector to the goose's position, moving it one frame's worth
let targetA = input.heading();
Calculate the angle the input vector is pointing (the direction to move)
let diff = targetA - this.angle;
Find the angular difference between the goose's current facing and the desired facing
if (diff > PI) diff -= TWO_PI;
If the angle difference is > 180°, subtract 360° to get the shorter arc (e.g., 270° becomes -90°)
if (diff < -PI) diff += TWO_PI;
If the angle difference is < -180°, add 360° to normalize it
this.angle += diff * 0.15;
Smoothly rotate toward the target angle by moving 15% of the way each frame (easing animation)
this.waddle += 0.3;
Increment the waddle counter; the show() function will use sin(this.waddle) to bob the goose up and down
} else {
If no movement input (no keys pressed)
this.waddle = lerp(this.waddle, 0, 0.2);
Smooth the waddle back to 0 by interpolating 20% of the way toward 0 each frame (stops the bobbing)
if(this.heldItem) {
If the goose is carrying an item
let offset = p5.Vector.fromAngle(this.angle).mult(40);
Create a vector pointing in the direction the goose is facing, 40 pixels long (the beak offset)
this.heldItem.pos.x = this.pos.x + offset.x;
Position the item's X coordinate 40 pixels in front of the goose
this.heldItem.pos.y = this.pos.y + offset.y - 10;
Position the item's Y coordinate 40 pixels in front and 10 pixels above (so it looks like the goose is holding it in its beak)
honk() {
Plays the honk sound and notifies nearby gardeners
this.honkFrame = 20;
Sets honkFrame to 20, making the honk lines visible for the next 20 frames
osc.freq(random(350, 450));
Set the honk oscillator to a random frequency between 350 and 450 Hz (goose-like pitch)
env.play(osc);
Play the honk envelope (triggers the ADSR attack-decay-sustain-release sequence)
osc.freq(300, 0.1);
Glide the frequency down to 300 Hz over 100ms for a more realistic honk tail
for(let g of gardeners) {
Loop through all gardeners on the map
let d = dist(this.pos.x, this.pos.y, g.pos.x, g.pos.y);
Calculate the distance from the goose to the gardener
if (d < 600) {
If the gardener is within 600 pixels (hearing range)
g.hearHonk(this.pos);
Call the gardener's hearHonk method, triggering the distraction
dropItem() {
Releases whatever the goose is carrying
this.heldItem = null;
Set heldItem to null, severing the connection; the item stays at its current position
tryInteract(x, y) {
Called when the player clicks; either drops the held item or picks up a nearby item
if(this.heldItem) {
If already holding something
this.dropItem();
Release it
} else {
If not holding anything
let beak = p5.Vector.fromAngle(this.angle).mult(40).add(this.pos);
Calculate the position of the goose's beak (40 pixels in front in the facing direction)
let closest = null; let minD = 70;
Initialize closest item to null and minimum distance to 70 pixels (the interaction range)
for(let i of items) {
Loop through all items on the map
let d = dist(beak.x, beak.y, i.pos.x, i.pos.y);
Calculate the distance from the beak to the item
if(d < minD) { minD = d; closest = i; }
If this item is closer than the previous closest, update closest and minD
if(closest) this.heldItem = closest;
After the loop, if we found an item within 70 pixels, pick it up
show() {
Draws the goose to the screen
push();
Save the current transformation state
translate(this.pos.x, this.pos.y);
Move the origin to the goose's position
if(this.honkFrame > 0) {
If honkFrame is > 0, the goose is currently honking
this.honkFrame--;
Decrement honkFrame so the animation will end after 20 frames
push(); rotate(this.angle); stroke(0); strokeWeight(2);
Save state, rotate to face direction, and set line properties
line(45, -10, 60, -20); line(50, 0, 70, 0); line(45, 10, 60, 20);
Draw three honk lines radiating from the goose's beak to show sound waves
pop();
Restore the transformation state
rotate(this.angle + sin(this.waddle)*0.1);
Rotate to the goose's facing angle, plus a small wobble based on the waddle animation
fill(255, 165, 0);
Set the fill color to orange (goose color)
ellipse(10, 15 + sin(this.waddle)*10, 15, 15);
Draw the bottom leg, bobbing up and down with the waddle (sine wave oscillation)
ellipse(10, -15 - sin(this.waddle)*10, 15, 15);
Draw the top leg, bobbing in the opposite direction
fill(255); ellipse(0, 0, 70, 50);
Draw a large white ellipse for the goose's body
ellipse(30, 0, 40, 35);
Draw a smaller white ellipse for the goose's head
fill(255, 165, 0); triangle(40, -5, 40, 5, 60, 0);
Draw an orange triangle for the goose's beak pointing forward
fill(0); ellipse(40, -8, 4, 4); ellipse(40, 8, 4, 4);
Draw two small black circles for the goose's eyes

Item class

The Item class is simple but demonstrates an important pattern: storing a type identifier and using it to customize the visual representation. Each item type has a unique combination of shapes and colors, making them visually distinct on the map. This is how the player learns what each item looks like and can strategically plan which ones to collect.

class Item {
  constructor(x, y, type) {
    this.pos = createVector(x, y);
    this.type = type;
  }
  
  show() {
    push();
    translate(this.pos.x, this.pos.y);
    noStroke();
    fill(0, 50); ellipse(0, 5, 25, 10);
    
    if(this.type === 'bell') {
      fill(255, 215, 0); ellipse(0,0,20,20);
    } else if(this.type === 'pumpkin') {
      fill(255, 100, 0); ellipse(0,0,35,30); fill(0,100,0); rect(-2,-15,4,8);
    } else if(this.type === 'rake') {
      fill(100); rect(-20,0,40,4); stroke(100,50,0); strokeWeight(3); line(0,0,0,40);
    } else if(this.type === 'radio') {
      fill(200,50,50); rect(-15,-10,30,20); fill(50); ellipse(0,0,10,10);
    } else if(this.type === 'key') {
      fill(200,200,0); ellipse(0,0,10,10); rect(5,-2,15,4); rect(15,2,4,4);
    } else if(this.type === 'boot') {
      fill(100, 50, 0); rect(-5, -10, 15, 20); ellipse(0, 10, 20, 10);
    } else if(this.type === 'mug') {
      fill(255); rect(-5, -8, 10, 16); ellipse(0, 0, 5, 10);
    } else if(this.type === 'hat') {
      fill(50); ellipse(0,0,25,25); rect(-8,-12,16,10);
    }
    pop();
  }
}
Line-by-line explanation (32 lines)

🔧 Subcomponents:

function Item constructor constructor(x, y, type) {

Initializes an item with a position and a type string

drawing Item shadow fill(0, 50); ellipse(0, 5, 25, 10);

Draws a semi-transparent dark ellipse beneath every item to suggest it's sitting on the ground

conditional Item type rendering switch if(this.type === 'bell') {

Each item type has a unique visual representation drawn with basic shapes

constructor(x, y, type) {
Initializes a new Item; x and y are its position, type is a string like 'bell', 'pumpkin', etc.
this.pos = createVector(x, y);
Stores the item's position as a p5.Vector
this.type = type;
Stores the item type string, used to determine what to draw in show()
show() {
Draws the item to the screen based on its type
push();
Save the current transformation state so we don't affect other drawings
translate(this.pos.x, this.pos.y);
Move the origin to the item's position
noStroke();
Disable outline drawing for cleaner appearance
fill(0, 50); ellipse(0, 5, 25, 10);
Draw a semi-transparent black ellipse slightly below the item; this is a shadow to ground the item visually
if(this.type === 'bell') {
If the item type is 'bell'
fill(255, 215, 0); ellipse(0,0,20,20);
Draw a gold circle (the bell)
} else if(this.type === 'pumpkin') {
If the item type is 'pumpkin'
fill(255, 100, 0); ellipse(0,0,35,30);
Draw an orange ellipse for the pumpkin body
fill(0,100,0); rect(-2,-15,4,8);
Draw a small green rectangle for the stem on top
} else if(this.type === 'rake') {
If the item type is 'rake'
fill(100); rect(-20,0,40,4);
Draw a dark gray rectangle for the rake's head (the part with teeth)
stroke(100,50,0); strokeWeight(3); line(0,0,0,40);
Draw a brown line for the rake's handle extending downward
} else if(this.type === 'radio') {
If the item type is 'radio'
fill(200,50,50); rect(-15,-10,30,20);
Draw a reddish rectangle for the radio's body
fill(50); ellipse(0,0,10,10);
Draw a dark gray circle for the radio's speaker/dial
} else if(this.type === 'key') {
If the item type is 'key'
fill(200,200,0); ellipse(0,0,10,10);
Draw a yellow circle for the key's head
rect(5,-2,15,4); rect(15,2,4,4);
Draw rectangles for the key's shaft and teeth
} else if(this.type === 'boot') {
If the item type is 'boot'
fill(100, 50, 0); rect(-5, -10, 15, 20);
Draw a brown rectangle for the boot's cuff
ellipse(0, 10, 20, 10);
Draw an ellipse for the boot's sole
} else if(this.type === 'mug') {
If the item type is 'mug'
fill(255); rect(-5, -8, 10, 16);
Draw a white rectangle for the mug's body
ellipse(0, 0, 5, 10);
Draw an ellipse for the mug's handle
} else if(this.type === 'hat') {
If the item type is 'hat'
fill(50); ellipse(0,0,25,25);
Draw a dark gray circle for the hat's crown
rect(-8,-12,16,10);
Draw a rectangle for the hat's brim
pop();
Restore the transformation state

Particle class

The Particle class is a simple but effective visual effect system. Particles are short-lived objects that spawn, move, decelerate, and fade out. They're created in bursts (spawning 50 at once when you win) to create celebratory confetti. The key techniques are: random velocity for varied directions, velocity decay for realistic physics, and life-based alpha fading for smooth disappearance. You'll use particles in almost every visual effects system you build.

class Particle {
  constructor(x, y) {
    this.pos = createVector(x, y);
    this.vel = p5.Vector.random2D().mult(random(2, 8));
    this.life = 255;
    this.color = color(random(255), random(255), random(255));
  }
  update() {
    this.pos.add(this.vel);
    this.vel.mult(0.95);
    this.life -= 5;
  }
  show() {
    noStroke();
    fill(red(this.color), green(this.color), blue(this.color), this.life);
    ellipse(this.pos.x, this.pos.y, 8, 8);
  }
  finished() { return this.life < 0; }
}
Line-by-line explanation (14 lines)

🔧 Subcomponents:

function Particle constructor constructor(x, y) {

Initializes a particle with a random velocity, bright color, and full life

calculation Random direction and speed this.vel = p5.Vector.random2D().mult(random(2, 8));

Generates a random direction (p5.Vector.random2D()) and scales it to a random speed between 2 and 8 pixels per frame

function Particle update update() {

Moves the particle and applies physics (deceleration and fading)

constructor(x, y) {
Initializes a particle at position (x, y)
this.pos = createVector(x, y);
Stores the particle's starting position
this.vel = p5.Vector.random2D().mult(random(2, 8));
Creates a random 2D velocity vector (direction) and scales it to a random speed between 2 and 8 pixels per frame, so particles burst outward in all directions at varying speeds
this.life = 255;
Sets the particle's life (alpha/opacity) to 255 (fully opaque)
this.color = color(random(255), random(255), random(255));
Generates a random RGB color for the particle; each particle is a different random color
update() {
Called every frame to update the particle's state
this.pos.add(this.vel);
Adds the velocity to the position, moving the particle one step
this.vel.mult(0.95);
Multiplies velocity by 0.95 each frame, slowing it down (5% loss per frame); creates a decelerating effect
this.life -= 5;
Decreases the life (opacity) by 5 each frame; after 51 frames, life reaches 0 and the particle fades out
show() {
Draws the particle to the screen
noStroke();
Disables outlines for a cleaner look
fill(red(this.color), green(this.color), blue(this.color), this.life);
Sets the fill color to the particle's stored color, using red(), green(), blue() to extract channels, and this.life as the alpha (opacity)
ellipse(this.pos.x, this.pos.y, 8, 8);
Draws an 8×8 circle at the particle's current position
finished() { return this.life < 0; }
Returns true if life has expired (< 0), signaling that this particle should be removed from the array

📦 Key Variables

goose Goose object

The player character; stores position, angle, held item, and handles input and animation

let goose = new Goose(0, 0);
gardeners array of Gardener objects

Array of all guards on the map; each has position, state (PATROL/INVESTIGATE/CHASE/RETURN), and vision cone

let gardeners = [];
items array of Item objects

Array of all collectable items on the map; includes the target item and decoys

let items = [];
particles array of Particle objects

Array of short-lived visual effect particles; spawned on victory to create confetti animation

let particles = [];
level number

Current level number (1, 2, 3, etc.); increases difficulty and determines enemy count and map size

let level = 1;
currentTask object

Object storing the current mission: { type: 'STEAL'|'DUNK'|'SCARE', target: itemType, desc: description }

let currentTask = { type: 'STEAL', target: 'bell', desc: 'Steal the BELL' };
gameState string

Controls the main game loop; 'PLAYING' runs win checks, 'VICTORY' counts down before next level

let gameState = 'PLAYING';
worldSize number

The overall map boundary in pixels (2000); used for mini-map scaling calculations

let worldSize = 2000;
blanketPos p5.Vector

Position of the home base where stolen items are delivered to complete STEAL tasks

let blanketPos = createVector(0, 0);
pondPos p5.Vector

Position of the pond/lake where items are dunked to complete DUNK tasks

let pondPos = createVector(-400, 200);

🔧 Potential Improvements (5)

Here are some ways this code could be enhanced:

BUG Gardener.update() vision check

The vision cone check uses angleDiff < this.fovAngle / 2, but fovAngle is already 60 degrees (PI/3), so the actual effective cone is 30 degrees. This is correct but unintuitive—the code comments should clarify that fovAngle is the full cone width, not half.

💡 Add a comment explaining: // fovAngle is full cone width; we compare half it to the angle difference

PERFORMANCE draw() depth sort

The drawList is sorted every single frame (60 times per second), even though the objects may not change depth order significantly between frames. This is an O(n log n) operation that could be expensive with many objects.

💡 Consider sorting only when necessary (e.g., every 10 frames) or use a space-partitioning system for larger games. For this sketch, it's fine, but it's good practice to note.

STYLE Gardener class hearHonk()

The line fill(255); text("?", this.pos.x, this.pos.y - 60) is unreachable—it's never executed because the status icon is drawn in show(). This is dead code.

💡 Remove the unreachable text() call in hearHonk(); the visual feedback is already in show().

FEATURE Game progression

All levels use the same pond and blanket positions, making later levels feel repetitive. Higher-difficulty levels could randomize or relocate these landmarks.

💡 In startLevel(), randomize blanketPos and pondPos based on level number to change the map layout each level.

BUG Goose.tryInteract()

The interaction range (70 pixels) is fixed and doesn't account for the goose's facing direction—you can pick up items behind the goose if they're within 70 pixels of the beak position. This can feel unintuitive.

💡 Consider adding a direction check: only pick up items that are within a forward-facing cone, not in all directions.

🔄 Code Flow

Code flow showing setup, setupaudio, startlevel, draw, checkwincondition, drawminimap, gardener, goose, item, particle

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

graph TD start[Start] --> setup[setup] setup --> canvas-init[canvas-init] setup --> color-init[color-init] setup --> audio-setup[audio-setup] setup --> world-init[world-init] setup --> honk-osc[honk-osc] setup --> honk-env[honk-env] setup --> win-osc[win-osc] setup --> angry-osc[angry-osc] setup --> startlevel[startlevel] startlevel --> difficulty-calc[difficulty-calc] startlevel --> task-type-roll[task-type-roll] task-type-roll -->|if level >= 3| target-spawn[target-spawn] task-type-roll -->|else| target-spawn[target-spawn] target-spawn --> pond-avoidance[pond-avoidance] pond-avoidance --> gardener-spawn-loop[gardener-spawn-loop] gardener-spawn-loop --> decoy-spawn-loop[decoy-spawn-loop] setup --> draw[draw loop] draw --> camera-follow[camera-follow] draw --> state-check[state-check] state-check --> checkwincondition[checkwincondition] checkwincondition -->|if win condition met| draw checkwincondition -->|else| draw draw --> depth-sort[depth-sort] draw --> object-update-loop[object-update-loop] object-update-loop --> particle-loop[particle-loop] particle-loop --> drawminimap[drawminimap] drawminimap --> map-dimensions[map-dimensions] drawminimap --> scale-factor[scale-factor] drawminimap --> mappoint-helper[mappoint-helper] drawminimap --> pond-draw[pond-draw] drawminimap --> gardener-loop[gardener-loop] click setup href "#fn-setup" click startlevel href "#fn-startlevel" click draw href "#fn-draw" click checkwincondition href "#fn-checkwincondition" click drawminimap href "#fn-drawminimap" click canvas-init href "#sub-canvas-init" click color-init href "#sub-color-init" click audio-setup href "#sub-audio-setup" click world-init href "#sub-world-init" click honk-osc href "#sub-honk-osc" click honk-env href "#sub-honk-env" click win-osc href "#sub-win-osc" click angry-osc href "#sub-angry-osc" click difficulty-calc href "#sub-difficulty-calc" click task-type-roll href "#sub-task-type-roll" click target-spawn href "#sub-target-spawn" click pond-avoidance href "#sub-pond-avoidance" click gardener-spawn-loop href "#sub-gardener-spawn-loop" click decoy-spawn-loop href "#sub-decoy-spawn-loop" click camera-follow href "#sub-camera-follow" click state-check href "#sub-state-check" click depth-sort href "#sub-depth-sort" click object-update-loop href "#sub-object-update-loop" click particle-loop href "#sub-particle-loop" click map-dimensions href "#sub-map-dimensions" click scale-factor href "#sub-scale-factor" click mappoint-helper href "#sub-mappoint-helper" click pond-draw href "#sub-pond-draw" click gardener-loop href "#sub-gardener-loop"

❓ Frequently Asked Questions

What visual elements are featured in the p5.js goose game sketch?

The sketch creates a vibrant garden environment filled with lush grass, water ponds, and various characters like gardeners, all designed to enhance the playful stealth theme.

How can players interact with the game and what actions can they perform?

Players control a mischievous goose, using honking to distract gardeners, navigating the garden while avoiding their vision cones, and completing tasks to progress through levels.

What creative coding concepts are showcased in this p5.js sketch?

The sketch demonstrates advanced NPC AI with vision cones and hearing, a mini-map radar system, and a distraction mechanic, all contributing to an engaging stealth gameplay experience.

Preview

Sketch 2026-03-06 00:48 - p5.js creative coding sketch preview
Sketch Preview
Code flow diagram showing the structure of Sketch 2026-03-06 00:48 - Code flow showing setup, setupaudio, startlevel, draw, checkwincondition, drawminimap, gardener, goose, item, particle
Code Flow Diagram