Good (Remix)

This sketch simulates a bustling emoji restaurant where you control a customizable player character, manage a personal house, and watch AI customers, robbers, and police officers interact in real-time. You can generate food, decorate your house, drag characters around, and watch an emergent comedy of restaurant chaos unfold.

🧪 Try This!

Experiment with the code by making these changes:

  1. Spawn with 10× more customers — The restaurant will become instantly crowded and chaotic. Customers will have trouble finding seats and will wander a lot.
  2. Make the player tiny — You'll be a small character that can fit through tight spaces between other people—your size becomes a game mechanic.
  3. Double the robber speed — Robbers will become nearly impossible to catch—they'll steal food and escape before police can react.
  4. Make customers eat twice as long — Customers will occupy tables for much longer, filling seats and preventing new customers from finding places.
  5. Create 5 robbers instead of 1 — Chaos erupts—multiple robbers steal food simultaneously while the single police officer can't keep up.
  6. Shrink the police detection range — Police will become nearly blind and rarely spot robbers, letting thieves run rampant.
Prefer the full editor? Open it there →

📖 About This Sketch

This sketch creates a dynamic emoji restaurant simulation where you play as a customizable character with your own house, while AI-driven customers, robbers, and police officers populate the restaurant floor. The visual appeal comes from watching dozens of simple emoji characters navigate complex behaviors—customers arrive, find tables, eat, and pay; robbers steal food and flee; police chase and jail them. It uses multiple p5.js techniques including real-time collision detection, state machines, emoji rendering, interactive click-to-move controls, keyboard input, and touch support for mobile.

The code is organized into a main sketch file with multiple interconnected classes: Customer, PlayerControlledCustomer, Robber, PoliceOfficer, BoatPerson, Food, Table, and HouseItem. By reading it, you'll learn how to structure complex interactive systems using object-oriented programming, how to manage AI state machines with switch statements, how to handle multiple input methods (mouse, keyboard, touch), and how to coordinate dozens of interacting objects in real-time.

⚙️ How It Works

  1. When the sketch loads, setup() prompts you to customize your player character (color, size, emoji icon), creates a restaurant with four tables and a kitchen area, spawns initial AI customers, one robber, one police officer, and one boat person, and initializes your house on the left side of the screen.
  2. Every frame, draw() either shows the house interior (if you're inside) or the restaurant. In the restaurant, it renders the kitchen, tables with chairs, a jail area, and displays all characters and food items.
  3. Each AI character runs an independent state machine—customers cycle through ARRIVING → SEEKING_TABLE → MOVING_TO_CHAIR → EATING → PAYING → REMOVE. Robbers hunt for food to steal. Police officers patrol and chase robbers, then jail them.
  4. You control the player character with arrow keys or WASD for smooth keyboard movement, or click anywhere to walk there. Click the 'Enter House' prompt to enter your house where you can add and drag decorative items, then click 'Exit House' to return to the restaurant.
  5. Collision avoidance keeps characters from stacking, and interaction checks (distance-based) determine when prompts appear. When the player is in the house, all AI characters pause, creating a safe refuge.
  6. Food spawns when you click in the kitchen or on a table. Robbers seek and steal it. Police catch robbers and send them to jail for 400 frames. Customers eat and pay. The simulation respawns customers, robbers, and police as needed to maintain population limits.

🎓 Concepts You'll Learn

Object-oriented programming and class inheritanceState machines and AI behaviorCollision detection and avoidanceInteractive input handling (mouse, keyboard, touch)Distance-based proximity detectionArray management and entity lifecycleEmoji rendering and text positioningReal-time simulation and frame-based animation

📝 Code Breakdown

setup()

setup() runs exactly once when the sketch first loads. It's where you initialize the canvas, prompt for player input, create all the initial game objects, and set up game state. Changes here affect the entire simulation.

🔬 This loop spawns 19 customers off the left edge of the screen. What happens if you change numInitialAICustomers to 5? To 50? How does crowd size affect the chaos?

  for (let i = 0; i < numInitialAICustomers; i++) {
    customers.push(new Customer(random(-100, -50), random(height))); 
  }
function setup() {
  createCanvas(windowWidth, windowHeight);
  textFont('Arial'); 
  textAlign(CENTER, CENTER); 
  rectMode(CENTER); 

  houseArea.height = windowHeight;
  houseEntrance.y = windowHeight / 2;

  let colorInput = prompt("Choose your character's color (e.g., 'red', '#00FF00', or RGB like '255,100,0'):", playerCustomColor);
  if (colorInput) playerCustomColor = colorInput;

  let sizeInput = prompt("Choose your character's size (e.g., '30', '50'):", playerCustomSize);
  if (sizeInput && !isNaN(sizeInput)) playerCustomSize = float(sizeInput);

  let iconInput = prompt("Choose your character's emoji icon (e.g., '😊', '🚀', '👑'):", playerCustomIcon);
  if (iconInput) playerCustomIcon = iconInput;

  initializeTables(); 

  player = new PlayerControlledCustomer(houseEntrance.x, houseEntrance.y, playerCustomColor, playerCustomSize, playerCustomIcon);
  customers.push(player);

  for (let i = 0; i < numInitialAICustomers; i++) {
    customers.push(new Customer(random(-100, -50), random(height))); 
  }
  for (let i = 0; i < numRobbers; i++) {
    customers.push(new Robber(random(-100, -50), random(height))); 
  }
  for (let i = 0; i < numPoliceOfficers; i++) {
    customers.push(new PoliceOfficer(random(-100, -50), random(height))); 
  }
  for (let i = 0; i < numBoatPeople; i++) {
    customers.push(new BoatPerson(random(-100, -50), random(height))); 
  }
}
Line-by-line explanation (11 lines)

🔧 Subcomponents:

calculation Canvas and Text Setup createCanvas(windowWidth, windowHeight);

Creates a full-screen canvas and configures text rendering defaults

conditional Player Customization Prompts let colorInput = prompt("Choose your character's color...", playerCustomColor);

Asks the player to customize their character's appearance before the game starts

for-loop AI Character Spawning for (let i = 0; i < numInitialAICustomers; i++) { customers.push(new Customer(random(-100, -50), random(height))); }

Creates initial AI customers, robbers, police officers, and boat people and adds them to the customers array

createCanvas(windowWidth, windowHeight);
Creates a p5.js canvas that fills the entire browser window, so the simulation scales to any screen size
textFont('Arial');
Sets the font for all text rendering to Arial, a clean, readable sans-serif font
textAlign(CENTER, CENTER);
Configures text alignment so text centers at the x,y coordinates you provide—easier for centering emojis and speech bubbles
rectMode(CENTER);
Makes rect() draw from the center point instead of the top-left, matching how circle() works and simplifying layout calculations
houseArea.height = windowHeight;
Sets the house interior's height to fill the whole screen, so it scales responsively
let colorInput = prompt("Choose your character's color...", playerCustomColor);
Opens a browser prompt where you type your player's color name, hex code, or RGB values before the game starts
if (colorInput) playerCustomColor = colorInput;
If you entered a color, saves it; otherwise keeps the default '#FF00FF' (bright magenta)
initializeTables();
Calls a helper function that creates the four restaurant tables and positions them evenly across the screen
player = new PlayerControlledCustomer(houseEntrance.x, houseEntrance.y, playerCustomColor, playerCustomSize, playerCustomIcon);
Creates your player character with your custom color, size, and emoji icon, positioned at the house entrance
customers.push(player);
Adds your player to the customers array so they're updated and drawn every frame like any other character
for (let i = 0; i < numInitialAICustomers; i++) { customers.push(new Customer(random(-100, -50), random(height))); }
Spawns 19 AI customers at random positions off the left edge, so they 'arrive' walking in from off-screen

draw()

draw() runs continuously at 60 frames per second. It's the heartbeat of the simulation, updating every character's position and state each frame, managing spawning and cleanup, and rendering everything. Notice it uses two branches (inHouse vs. restaurant) to create two distinct game modes.

🔬 This code pauses AI characters when you're in the house—it stops calling person.update() and only displays them. What do you think would happen if you deleted the 'continue;' statement so update() still runs while you're inside?

    for (let i = customers.length - 1; i >= 0; i--) {
      let person = customers[i];
      if (person !== player && inHouse) {
        if (person.speechTimer > 0) person.speechTimer--;
        person.display();
        continue; 
      }
function draw() {
  if (inHouse) {
    background(100, 50, 0); 
    drawHouseInterior();
    player.update(); 
    player.display();
  } else {
    background(240, 230, 200); 

    // Draw Jail Area (Top Right)
    push();
    fill(150);
    stroke(0);
    strokeWeight(2);
    rect(width - 60, 60, 80, 80);
    fill(0);
    noStroke();
    textSize(16);
    text("JAIL", width - 60, 115);
    pop();

    fill(180);
    rect(kitchenPos.x + kitchenPos.width / 2, kitchenPos.y + kitchenPos.height / 2, kitchenPos.width, kitchenPos.height);
    fill(0);
    textSize(18);
    textAlign(CENTER, CENTER);
    text("Kitchen", kitchenPos.x + kitchenPos.width / 2, kitchenPos.y + kitchenPos.height / 2);

    for (let table of tables) {
      table.display();
    }

    for (let i = customers.length - 1; i >= 0; i--) {
      let person = customers[i];
      if (person !== player && inHouse) {
        if (person.speechTimer > 0) person.speechTimer--;
        person.display();
        continue; 
      }

      person.update();

      if (person instanceof Customer && person.remove) {
        customers.splice(i, 1);
        continue;
      }

      person.display();

      if ((person.state === 'LEAVING' || person.state === 'FLEEING') && person.x > width + 50) {
        customers.splice(i, 1);
      }
    }

    // Draw Jail Bars ON TOP of characters so the robber looks like they are inside
    push();
    stroke(0);
    strokeWeight(4);
    for(let i = -30; i <= 30; i += 15) {
      line(width - 60 + i, 20, width - 60 + i, 100);
    }
    pop();

    const currentAICustomerCount = customers.filter(p => p instanceof Customer && !(p instanceof Robber) && !(p instanceof PoliceOfficer) && !(p instanceof BoatPerson) && p !== player).length;
    if (frameCount - lastCustomerArrival > customerArrivalInterval && currentAICustomerCount < maxAICustomers) {
      customers.push(new Customer(random(-100, -50), random(height)));
      lastCustomerArrival = frameCount;
    }

    const currentRobberCount = customers.filter(p => p instanceof Robber).length;
    if (currentRobberCount < numRobbers) {
      customers.push(new Robber(random(-100, -50), random(height)));
    }

    const currentPoliceOfficerCount = customers.filter(p => p instanceof PoliceOfficer).length;
    if (currentPoliceOfficerCount < numPoliceOfficers) {
      customers.push(new PoliceOfficer(random(-100, -50), random(height)));
    }

    const currentBoatPersonCount = customers.filter(p => p instanceof BoatPerson).length;
    if (currentBoatPersonCount < numBoatPeople) {
      customers.push(new BoatPerson(random(-100, -50), random(height)));
    }

    for (let i = foodItems.length - 1; i >= 0; i--) {
      let food = foodItems[i];
      food.display();
      if (food.isEaten() && !food.inKitchen) {
        foodItems.splice(i, 1);
      }
    }

    if (dist(player.x, player.y, houseEntrance.x, houseEntrance.y) < houseEntrance.size / 2 + player.size / 2) {
      drawInteractionPrompt("Enter House", houseEntrance.x, houseEntrance.y - 70);
    }
  }
}
Line-by-line explanation (22 lines)

🔧 Subcomponents:

conditional House Interior Display if (inHouse) { background(100, 50, 0); drawHouseInterior(); player.update(); player.display(); }

When player is inside, show house interior, pause all AI, and only update/display the player

calculation Restaurant Background and Layout background(240, 230, 200);

Clears the screen each frame with a light beige color, preventing motion trails

calculation Jail Area Rendering rect(width - 60, 60, 80, 80);

Draws the jail box in the top-right corner where captured robbers are sent

for-loop Character Update and Display Loop for (let i = customers.length - 1; i >= 0; i--) { person.update(); person.display(); }

Updates all characters' state machines and displays them each frame; removes characters when they leave or are eaten

conditional Spawn and Respawn Logic if (frameCount - lastCustomerArrival > customerArrivalInterval && currentAICustomerCount < maxAICustomers)

Maintains customer population by spawning new ones on a timer, up to the max limit

for-loop Food Display and Cleanup for (let i = foodItems.length - 1; i >= 0; i--) { food.display(); if (food.isEaten() && !food.inKitchen) foodItems.splice(i, 1); }

Displays all food and removes eaten items to prevent memory leaks

conditional House Entrance Prompt if (dist(player.x, player.y, houseEntrance.x, houseEntrance.y) < houseEntrance.size / 2 + player.size / 2)

Shows 'Enter House' prompt when player is close enough to the house entrance

if (inHouse) {
Checks if the player is inside the house; if true, show house interior instead of restaurant
background(100, 50, 0);
Fills the screen with a warm brown color, creating a cozy house atmosphere
drawHouseInterior();
Calls a helper function that renders the house interior—couch, rug, decorations, and buttons
player.update();
Updates the player's position based on keyboard/click input while inside the house
} else {
Otherwise, render the restaurant view
background(240, 230, 200);
Clears the screen with a light beige/cream color, the restaurant floor color
rect(width - 60, 60, 80, 80);
Draws the jail area as a gray box in the top-right corner (50 pixels from the right edge, 60 pixels down)
text("JAIL", width - 60, 115);
Labels the jail box so players know what it is
fill(180); rect(kitchenPos.x + kitchenPos.width / 2, kitchenPos.y + kitchenPos.height / 2, kitchenPos.width, kitchenPos.height);
Draws the kitchen area as a light gray rectangle in the upper portion of the restaurant
for (let table of tables) { table.display(); }
Iterates through all four restaurant tables and renders each one with its chairs
for (let i = customers.length - 1; i >= 0; i--) {
Loops backward through the customers array (from end to start), so you can safely remove items during iteration
if (person !== player && inHouse) { if (person.speechTimer > 0) person.speechTimer--; person.display(); continue; }
If AI characters are paused because player is in house, just decrement their speech timer and display them without updating their movement
person.update();
Runs the character's state machine, updating position, eating timers, animation, and state transitions
if (person instanceof Customer && person.remove) { customers.splice(i, 1); continue; }
If a customer has paid and is marked for removal, delete them from the array immediately
person.display();
Renders the character's body, head, eyes, mouth, emoji icon, and speech bubble
if ((person.state === 'LEAVING' || person.state === 'FLEEING') && person.x > width + 50) { customers.splice(i, 1); }
When a customer or robber moves off the right side of the screen (past width + 50), remove them to prevent infinite array growth
for(let i = -30; i <= 30; i += 15) { line(width - 60 + i, 20, width - 60 + i, 100); }
Draws vertical jail bars on top of characters so jailed robbers appear trapped behind bars
const currentAICustomerCount = customers.filter(p => p instanceof Customer && !(p instanceof Robber) && !(p instanceof PoliceOfficer) && !(p instanceof BoatPerson) && p !== player).length;
Counts how many regular (non-robber, non-police, non-boat) AI customers exist by filtering the customers array
if (frameCount - lastCustomerArrival > customerArrivalInterval && currentAICustomerCount < maxAICustomers) {
If enough frames have passed since the last customer arrived AND we haven't hit the max customer count, spawn a new one
for (let i = foodItems.length - 1; i >= 0; i--) { food.display(); if (food.isEaten() && !food.inKitchen) foodItems.splice(i, 1); }
Displays all food items and removes eaten ones (except kitchen items, which stay invisible until picked up)
if (dist(player.x, player.y, houseEntrance.x, houseEntrance.y) < houseEntrance.size / 2 + player.size / 2) {
Checks if player is close enough to the house entrance (within a radius equal to half the entrance size plus half the player's size)
drawInteractionPrompt("Enter House", houseEntrance.x, houseEntrance.y - 70);
Shows a clickable 'Enter House' prompt above the entrance when the player is nearby

initializeTables()

initializeTables() is a helper function that creates the four restaurant tables and positions them in a symmetric 2x2 grid. It's called in setup() to create the initial layout and in windowResized() to rebuild the grid when the screen size changes.

🔬 This creates a 2x2 grid of tables. What if you changed the third argument (2) to 4 in each line? Each table would have 4 chairs instead of 2—would more seating help customers find seats faster?

  tables.push(new Table(restaurantOffset + (width - restaurantOffset) / 3, height / 3, 2));
  tables.push(new Table(restaurantOffset + 2 * (width - restaurantOffset) / 3, height / 3, 2));
  tables.push(new Table(restaurantOffset + (width - restaurantOffset) / 3, 2 * height / 3, 2));
  tables.push(new Table(restaurantOffset + 2 * (width - restaurantOffset) / 3, 2 * height / 3, 2));
function initializeTables() {
  tables = []; 
  tables.push(new Table(restaurantOffset + (width - restaurantOffset) / 3, height / 3, 2));
  tables.push(new Table(restaurantOffset + 2 * (width - restaurantOffset) / 3, height / 3, 2));
  tables.push(new Table(restaurantOffset + (width - restaurantOffset) / 3, 2 * height / 3, 2));
  tables.push(new Table(restaurantOffset + 2 * (width - restaurantOffset) / 3, 2 * height / 3, 2));
}
Line-by-line explanation (5 lines)

🔧 Subcomponents:

calculation 2x2 Table Grid Layout tables.push(new Table(restaurantOffset + (width - restaurantOffset) / 3, height / 3, 2));

Places tables in a 2x2 grid pattern, evenly distributed across the restaurant area

tables = [];
Clears the tables array, removing any old tables—useful when the window resizes
tables.push(new Table(restaurantOffset + (width - restaurantOffset) / 3, height / 3, 2));
Creates a table at 1/3 from the left of the restaurant space and 1/3 down from the top, with 2 chairs, and adds it to the array
tables.push(new Table(restaurantOffset + 2 * (width - restaurantOffset) / 3, height / 3, 2));
Creates a table at 2/3 from the left of the restaurant space and 1/3 down—the top-right table of the 2x2 grid
tables.push(new Table(restaurantOffset + (width - restaurantOffset) / 3, 2 * height / 3, 2));
Creates a table at 1/3 from the left and 2/3 down—the bottom-left table
tables.push(new Table(restaurantOffset + 2 * (width - restaurantOffset) / 3, 2 * height / 3, 2));
Creates a table at 2/3 from the left and 2/3 down—the bottom-right table, completing the 2x2 grid

drawHouseInterior()

drawHouseInterior() renders the interior of the player's house when inHouse is true. It displays fixed furniture (couch, rug, door) and user-placed items, then checks proximity to show interaction prompts. This function is called every frame from draw().

function drawHouseInterior() {
  fill(0);
  textSize(24);
  textAlign(CENTER, CENTER);
  text("Your Cozy Home!", width / 2, height / 4);

  // --- Draw Couch & Rug ---
  push();
  fill(180, 60, 60); // Cozy red rug
  noStroke();
  ellipse(width / 4, height / 2 + 30, 180, 70); 
  
  // Couch emoji
  textSize(100);
  text("🛋️", width / 4, height / 2 - 10);
  pop();

  // --- Draw user-placed house items ---
  for (let item of houseItems) {
    item.display();
  }

  // --- Draw Trash Bin ---
  textSize(40);
  text("🗑️", 40, height - 40);

  // --- Draw 'Add Item' Button ---
  drawInteractionPrompt("📦 Add Item", width / 2, height - 50);

  // Simple "door" to exit
  fill(139, 69, 19); 
  noStroke();
  rect(width / 2, height / 2, 80, 120);
  fill(0);
  ellipse(width / 2 + 30, height / 2, 10, 10); 

  if (dist(player.x, player.y, width / 2, height / 2) < 40 + player.size / 2) {
    drawInteractionPrompt("Exit House", width / 2, height / 2 - 80);
  }
}
Line-by-line explanation (11 lines)

🔧 Subcomponents:

calculation Couch and Rug Display text("🛋️", width / 4, height / 2 - 10);

Renders a decorative couch emoji and red rug ellipse to make the house feel lived-in

for-loop Display User-Placed Items for (let item of houseItems) { item.display(); }

Renders all player-placed decorative items (plants, TV, pets, etc.) at their dragged positions

calculation Exit Door Rendering rect(width / 2, height / 2, 80, 120);

Draws a brown rectangular door that represents the exit to the restaurant

conditional Exit Prompt Detection if (dist(player.x, player.y, width / 2, height / 2) < 40 + player.size / 2)

Shows 'Exit House' prompt when player is close enough to the door

text("Your Cozy Home!", width / 2, height / 4);
Displays the house title at the top-center of the screen
fill(180, 60, 60); // Cozy red rug
Sets the fill color to a warm reddish-brown before drawing the rug
ellipse(width / 4, height / 2 + 30, 180, 70);
Draws a red oval rug on the floor to add visual warmth and coziness to the house
textSize(100); text("🛋️", width / 4, height / 2 - 10);
Renders a large couch emoji above the rug at quarter-way across the screen
for (let item of houseItems) { item.display(); }
Loops through all player-placed decorative items and renders each one at its current position
textSize(40); text("🗑️", 40, height - 40);
Displays a trash bin emoji in the bottom-left corner, where players can drag items to delete them
drawInteractionPrompt("📦 Add Item", width / 2, height - 50);
Shows a clickable button that lets the player spawn new decorative items
fill(139, 69, 19); noStroke(); rect(width / 2, height / 2, 80, 120);
Draws a brown wooden door at the center-right of the screen
ellipse(width / 2 + 30, height / 2, 10, 10);
Draws a small black circle (doorknob) on the right side of the door
if (dist(player.x, player.y, width / 2, height / 2) < 40 + player.size / 2) {
Checks if player is within 40 pixels (plus half the player's size) of the door center
drawInteractionPrompt("Exit House", width / 2, height / 2 - 80);
Shows an 'Exit House' prompt above the door when the player is nearby

mousePressed()

mousePressed() runs once every time you click the mouse. It's the main interaction hub—it detects whether you clicked the house door, the Add Item button, a house item, the Enter House prompt, a character (to drag), a table, the kitchen, or empty floor (to move the player). It then takes appropriate action.

🔬 This rectangular hit detection makes it easy to click a character. What if you changed it to a circular distance check like `if (dist(mouseX, mouseY, person.x, person.y) < person.size / 2)`? How would that change what's clickable?

    for (let person of customers) {
      let minX = person.x - person.size / 4;
      let maxX = person.x + person.size / 4;
      let minY = person.y - person.size * 0.6 - person.size * 0.4;
      let maxY = person.y + person.size / 2;

      if (mouseX >= minX && mouseX <= maxX && mouseY >= minY && mouseY <= maxY) {
function mousePressed() {
  if (inHouse) {
    // 1. Check Exit Door
    if (dist(mouseX, mouseY, width / 2, height / 2) < 40) { 
      inHouse = false;
      player.x = houseEntrance.x;
      player.y = houseEntrance.y;
      player.targetX = null; player.targetY = null;
      player.setSpeech("Back to work! 🏃");
      return false;
    } 
    
    // 2. Check "Add Item" Button
    let btnMsg = "📦 Add Item";
    let btnW = textWidth(btnMsg) + 20;
    if (mouseX > width/2 - btnW/2 && mouseX < width/2 + btnW/2 && 
        mouseY > height - 50 - 15 && mouseY < height - 50 + 15) {
      houseItems.push(new HouseItem(width/2, height/2 + 80, random(houseEmojis)));
      return false;
    }

    // 3. Check picking up a house item
    for (let i = houseItems.length - 1; i >= 0; i--) {
      let item = houseItems[i];
      if (dist(mouseX, mouseY, item.x, item.y) < item.size / 1.5) {
        selectedHouseItem = item;
        return false;
      }
    }

    // 4. Click to move player
    player.targetX = mouseX;
    player.targetY = mouseY;
    
  } else {
    if (dist(mouseX, mouseY, houseEntrance.x, houseEntrance.y - 70) < (textWidth("Enter House") + 20) / 2 + 15) { 
      inHouse = true;
      player.x = width / 2; 
      player.y = height / 2;
      player.targetX = null; player.targetY = null;
      player.setSpeech("Ah, my home! 🏠");
      return false;
    }

    for (let person of customers) {
      let minX = person.x - person.size / 4;
      let maxX = person.x + person.size / 4;
      let minY = person.y - person.size * 0.6 - person.size * 0.4;
      let maxY = person.y + person.size / 2;

      if (mouseX >= minX && mouseX <= maxX && mouseY >= minY && mouseY <= maxY) {
        selectedCharacter = person;
        selectedCharacter.originalState = selectedCharacter.state;
        selectedCharacter.state = 'DRAGGED';
        selectedCharacter.setSpeech("Whee! 🎢");
        return false;
      }
    }

    let clickedOnTable = false;
    for (let table of tables) {
      if (mouseX >= table.x - table.width / 2 && mouseX <= table.x + table.width / 2 &&
          mouseY >= table.y - table.height / 2 && mouseY <= table.y + table.height / 2) {
        let food = new Food(table.x, table.y, random(foodEmojis), false);
        foodItems.push(food);
        table.foodOnTableCenter = food;
        clickedOnTable = true;
        break;
      }
    }

    if (!clickedOnTable) {
      if (mouseX >= kitchenPos.x && mouseX <= kitchenPos.x + kitchenPos.width &&
          mouseY >= kitchenPos.y && mouseY <= kitchenPos.y + kitchenPos.height) {
        foodItems.push(new Food(kitchenPos.x + random(kitchenPos.width), kitchenPos.y + random(kitchenPos.height), random(foodEmojis), true));
      } else {
        player.targetX = mouseX;
        player.targetY = mouseY;
      }
    }
  }
  return false;
}
Line-by-line explanation (30 lines)

🔧 Subcomponents:

conditional Exit Door Click Detection if (dist(mouseX, mouseY, width / 2, height / 2) < 40)

When inside house, checks if player clicked the door to exit

conditional Add Item Button Click if (mouseX > width/2 - btnW/2 && mouseX < width/2 + btnW/2 && mouseY > height - 50 - 15 && mouseY < height - 50 + 15)

Detects click on 'Add Item' button and spawns a new decorative item

for-loop Pick Up House Item Loop for (let i = houseItems.length - 1; i >= 0; i--) { if (dist(mouseX, mouseY, item.x, item.y) < item.size / 1.5) { selectedHouseItem = item; return false; } }

Checks each house item to see if player clicked it for dragging

conditional Enter House Button Click if (dist(mouseX, mouseY, houseEntrance.x, houseEntrance.y - 70) < (textWidth("Enter House") + 20) / 2 + 15)

Detects click on 'Enter House' prompt when in restaurant view

for-loop Drag Character Detection Loop for (let person of customers) { if (mouseX >= minX && mouseX <= maxX && mouseY >= minY && mouseY <= maxY) { selectedCharacter = person; } }

Checks each character to see if player clicked them for manual dragging

for-loop Click on Table Detection for (let table of tables) { if (mouseX >= table.x - table.width / 2 && mouseX <= table.x + table.width / 2 && ...) { foodItems.push(new Food(table.x, table.y, ...)); clickedOnTable = true; break; } }

Detects if player clicked on a table to spawn food on it

conditional Click on Kitchen Detection if (mouseX >= kitchenPos.x && mouseX <= kitchenPos.x + kitchenPos.width && ...)

Detects if player clicked in the kitchen area to spawn food there

calculation Player Click-to-Move player.targetX = mouseX; player.targetY = mouseY;

Sets the player's movement target when clicking on empty floor

if (inHouse) {
First, check if the player is inside the house—this branch handles house interactions
if (dist(mouseX, mouseY, width / 2, height / 2) < 40) {
Check if the mouse click is within 40 pixels of the door's center (a simple circular click target)
inHouse = false;
Exit the house by setting the flag to false
player.x = houseEntrance.x; player.y = houseEntrance.y;
Teleport the player to the house entrance outside so they re-enter the restaurant at that spot
player.targetX = null; player.targetY = null;
Cancel any pending click-to-move so the player stops moving
player.setSpeech("Back to work! 🏃");
Sets the player's speech bubble to show they're heading back to the restaurant
let btnMsg = "📦 Add Item"; let btnW = textWidth(btnMsg) + 20;
Calculates the pixel width of the button text plus padding to create a clickable area
if (mouseX > width/2 - btnW/2 && mouseX < width/2 + btnW/2 && mouseY > height - 50 - 15 && mouseY < height - 50 + 15) {
Checks if the click is inside the rectangular bounds of the 'Add Item' button
houseItems.push(new HouseItem(width/2, height/2 + 80, random(houseEmojis)));
Creates a new random decorative item and adds it to the houseItems array
for (let i = houseItems.length - 1; i >= 0; i--) {
Loops backward through house items (so you can remove them during iteration if needed)
if (dist(mouseX, mouseY, item.x, item.y) < item.size / 1.5) { selectedHouseItem = item; return false; }
Checks if the click is on an item; if so, select it for dragging and exit the function
player.targetX = mouseX; player.targetY = mouseY;
If no interactions matched, treat the click as a movement command—set the player's walk target
} else {
Otherwise, handle restaurant clicks (not in house)
if (dist(mouseX, mouseY, houseEntrance.x, houseEntrance.y - 70) < (textWidth("Enter House") + 20) / 2 + 15) {
Check if the click is on the 'Enter House' prompt (calculated radius based on text width)
inHouse = true;
Enter the house by setting the flag to true
player.x = width / 2; player.y = height / 2;
Teleport the player to the center of the house interior
player.setSpeech("Ah, my home! 🏠");
Sets a cozy speech bubble as the player enters their house
for (let person of customers) {
Loop through all characters to check if any were clicked for manual dragging
let minX = person.x - person.size / 4; let maxX = person.x + person.size / 4;
Calculate the left and right bounds of the character's clickable area (horizontal slice of their body)
let minY = person.y - person.size * 0.6 - person.size * 0.4; let maxY = person.y + person.size / 2;
Calculate the top and bottom bounds of the character's clickable area (from head to mid-body)
if (mouseX >= minX && mouseX <= maxX && mouseY >= minY && mouseY <= maxY) {
Check if the click is inside these bounds—a simple rectangular hit detection for the character
selectedCharacter = person; selectedCharacter.originalState = selectedCharacter.state; selectedCharacter.state = 'DRAGGED';
Select the character, save their current state, and set their state to 'DRAGGED' so they'll follow the mouse
selectedCharacter.setSpeech("Whee! 🎢");
Sets a fun speech bubble indicating the character is being dragged
let clickedOnTable = false; for (let table of tables) {
Track whether a table was clicked and loop through all tables to check
if (mouseX >= table.x - table.width / 2 && mouseX <= table.x + table.width / 2 && mouseY >= table.y - table.height / 2 && mouseY <= table.y + table.height / 2) {
Check if the click is inside this table's rectangular bounds
let food = new Food(table.x, table.y, random(foodEmojis), false); foodItems.push(food); table.foodOnTableCenter = food;
Create food at the table's center and add it to both the global foodItems array and the table's own reference
clickedOnTable = true; break;
Mark that a table was clicked and stop checking other tables
if (!clickedOnTable) { if (mouseX >= kitchenPos.x && mouseX <= kitchenPos.x + kitchenPos.width && ...) {
If no table was clicked, check if the click was in the kitchen area instead
foodItems.push(new Food(kitchenPos.x + random(kitchenPos.width), kitchenPos.y + random(kitchenPos.height), random(foodEmojis), true));
Spawn food at a random position inside the kitchen and mark it as inKitchen=true
} else { player.targetX = mouseX; player.targetY = mouseY; }
If the click was on empty floor (not table, not kitchen), treat it as a player movement command

keyPressed()

keyPressed() fires once when a key is first pressed. It sets movement flags to true so the PlayerControlledCustomer's update() method can read them each frame. It also cancels mouse-based movement and shows the 'Zoom' speech bubble.

🔬 This maps arrow keys and WASD to movement flags. What if you added another case like `case 32: player.jump = true; break;` to respond to the spacebar (keyCode 32)? The player wouldn't jump (unless you coded that), but the flag would be set—how might you use it?

    switch (keyCode) {
      case LEFT_ARROW: case 65: player.left = true; break;
      case RIGHT_ARROW: case 68: player.right = true; break;
      case UP_ARROW: case 87: player.up = true; break;
      case DOWN_ARROW: case 83: player.down = true; break;
    }
function keyPressed() {
  if (player && player.state !== 'DRAGGED') {
    switch (keyCode) {
      case LEFT_ARROW: case 65: player.left = true; break;
      case RIGHT_ARROW: case 68: player.right = true; break;
      case UP_ARROW: case 87: player.up = true; break;
      case DOWN_ARROW: case 83: player.down = true; break;
    }
    if ([LEFT_ARROW, RIGHT_ARROW, UP_ARROW, DOWN_ARROW, 65, 68, 87, 83].includes(keyCode)) {
      player.targetX = null; player.targetY = null; // Cancel mouse movement
      player.setSpeech("Zoom! 💨"); 
      return false;
    }
  }
}
Line-by-line explanation (9 lines)

🔧 Subcomponents:

switch-case Arrow Keys and WASD Mapping switch (keyCode) { case LEFT_ARROW: case 65: player.left = true; break; ... }

Maps both arrow keys and WASD to boolean movement flags on the player

calculation Cancel Mouse Movement player.targetX = null; player.targetY = null;

When keyboard input is detected, cancels any pending click-to-move so keyboard takes priority

if (player && player.state !== 'DRAGGED') {
Only process keyboard input if the player exists and is not currently being dragged by the mouse
switch (keyCode) {
Use a switch statement to match the pressed key code (LEFT_ARROW, RIGHT_ARROW, etc.) to actions
case LEFT_ARROW: case 65: player.left = true; break;
If the left arrow or 'A' key (keyCode 65) is pressed, set player.left to true so they start moving left
case RIGHT_ARROW: case 68: player.right = true; break;
If the right arrow or 'D' key (keyCode 68) is pressed, set player.right to true
case UP_ARROW: case 87: player.up = true; break;
If the up arrow or 'W' key (keyCode 87) is pressed, set player.up to true
case DOWN_ARROW: case 83: player.down = true; break;
If the down arrow or 'S' key (keyCode 83) is pressed, set player.down to true
if ([LEFT_ARROW, RIGHT_ARROW, UP_ARROW, DOWN_ARROW, 65, 68, 87, 83].includes(keyCode)) {
Check if the pressed key is one of the movement keys by searching an array of key codes
player.targetX = null; player.targetY = null;
Cancel any pending click-to-move targets so keyboard movement takes priority over mouse clicks
player.setSpeech("Zoom! 💨");
Show a fun speech bubble indicating the player is zooming around with keyboard input

keyReleased()

keyReleased() fires once when a key is released. It sets movement flags to false, which allows smooth keyboard movement—the player keeps moving in the pressed direction(s) and stops when the key is released. It also clears the speech bubble when the player becomes completely still.

function keyReleased() {
  if (player && player.state !== 'DRAGGED') {
    switch (keyCode) {
      case LEFT_ARROW: case 65: player.left = false; break;
      case RIGHT_ARROW: case 68: player.right = false; break;
      case UP_ARROW: case 87: player.up = false; break;
      case DOWN_ARROW: case 83: player.down = false; break;
    }
    if (!player.left && !player.right && !player.up && !player.down && player.targetX === null) {
      player.setSpeech("");
    }
    if ([LEFT_ARROW, RIGHT_ARROW, UP_ARROW, DOWN_ARROW, 65, 68, 87, 83].includes(keyCode)) {
      return false;
    }
  }
}
Line-by-line explanation (8 lines)

🔧 Subcomponents:

switch-case Release Movement Flags switch (keyCode) { case LEFT_ARROW: case 65: player.left = false; break; ... }

Sets movement flags to false when keys are released so the player stops moving in that direction

conditional Clear Speech When Still if (!player.left && !player.right && !player.up && !player.down && player.targetX === null) { player.setSpeech(""); }

When all movement stops, clears the 'Zoom' speech bubble to clean up the display

if (player && player.state !== 'DRAGGED') {
Only process key releases if the player exists and is not being dragged
switch (keyCode) {
Match the released key to set the corresponding movement flag to false
case LEFT_ARROW: case 65: player.left = false; break;
When left arrow or 'A' is released, set player.left to false so the player stops moving left
case RIGHT_ARROW: case 68: player.right = false; break;
When right arrow or 'D' is released, set player.right to false
case UP_ARROW: case 87: player.up = false; break;
When up arrow or 'W' is released, set player.up to false
case DOWN_ARROW: case 83: player.down = false; break;
When down arrow or 'S' is released, set player.down to false
if (!player.left && !player.right && !player.up && !player.down && player.targetX === null) {
Check if ALL movement flags are false (no keys pressed) AND no mouse target is set (not click-moving)
player.setSpeech("");
Clear the speech bubble by setting it to an empty string, removing the 'Zoom' message

Customer (class)

The Customer class is the foundation of the simulation. It implements a state machine with 5 states (ARRIVING → SEEKING_TABLE → MOVING_TO_CHAIR → EATING → PAYING → remove). The update() method drives the state transitions each frame, while display() renders the character's body, eyes, mouth, and speech. Subclasses like PlayerControlledCustomer, Robber, PoliceOfficer, and BoatPerson extend it with custom behaviors.

🔬 This code searches for an empty chair and books it immediately when found. What if you added a chance to ignore a found chair and keep wandering, like `if (random(1) < 0.3) break;` (30% chance to skip)? How would random picky behavior change the simulation?

      case 'SEEKING_TABLE':
        let foundChair = false;
        for (let table of tables) {
          let chairIndex = table.findEmptyChair();
          if (chairIndex !== -1) {
            this.targetChair = table.chairs[chairIndex];
            this.targetChair.customer = this;
            this.targetChair.table = table; 
            this.state = 'MOVING_TO_CHAIR';
            foundChair = true;
            break;
          }
        }
class Customer {
  constructor(x, y) {
    this.x = x;
    this.y = y;
    this.size = random(30, 45); 
    this.color = color(random(100, 200), random(100, 200), random(100, 200)); 
    this.speed = random(1.5, 2.5); 
    this.state = 'ARRIVING'; 
    this.targetChair = null; 
    this.mouthOpen = false; 
    this.eatingTimer = 0; 
    this.eatingDuration = random(180, 360); 
    this.speechText = ""; 
    this.speechTimer = 0; 
    this.setSpeech(random(["I'm hungry!", "Table for one?", "Looking for a seat!"])); 
    this.paid = false; 
    this.payingDuration = random(30, 60); 
    this.remove = false; 
    this.wanderAngle = random(TWO_PI);
    this.wanderTimer = 0;
    this.originalState = this.state; 
  }

  setSpeech(text) {
    this.speechText = text;
    this.speechTimer = 90; 
  }

  display() {
    push();
    translate(this.x, this.y);
    fill(this.color);
    noStroke();
    rect(0, 0, this.size / 2, this.size); 
    fill(this.color); 
    ellipse(0, -this.size / 2, this.size * 0.8);
    fill(0);
    ellipse(-this.size * 0.15, -this.size * 0.6, this.size * 0.1);
    ellipse(this.size * 0.15, -this.size * 0.6, this.size * 0.1);
    fill(255);
    stroke(0);
    if (this.mouthOpen) {
      arc(0, -this.size * 0.4, this.size * 0.3, this.size * 0.2, 0, PI); 
      this.eatingTimer--;
      if (this.eatingTimer <= 0) this.mouthOpen = false;
    } else {
      line(-this.size * 0.15, -this.size * 0.4, this.size * 0.15, -this.size * 0.4); 
    }

    if (this.speechText && this.speechTimer > 0) {
      textSize(12); 
      textAlign(CENTER, BOTTOM); 
      fill(255);
      stroke(0);
      let calculatedTextWidth = textWidth(this.speechText);
      let padding = 10;
      let bubbleWidth = calculatedTextWidth + padding * 2;
      let bubbleHeight = 20 + padding; 
      let bubbleX = 0;
      let bubbleY = -this.size * 1.1;

      rect(bubbleX, bubbleY - bubbleHeight / 2, bubbleWidth, bubbleHeight, 5);
      triangle(bubbleX - 5, bubbleY, bubbleX + 5, bubbleY, bubbleX, bubbleY - 10);

      fill(0);
      noStroke();
      text(this.speechText, bubbleX, bubbleY - padding);
    }
    pop();
  }

  update() {
    if (this.state === 'DRAGGED' || this.state === 'IN_JAIL' || (inHouse && this !== player)) {
      return;
    }
    if (this.speechTimer > 0) this.speechTimer--;
    else this.speechText = ""; 

    let previousState = this.state; 

    switch (this.state) {
      case 'ARRIVING':
        this.x += this.speed;
        if (this.x >= restaurantOffset + 200) this.state = 'SEEKING_TABLE';
        break;

      case 'SEEKING_TABLE':
        let foundChair = false;
        for (let table of tables) {
          let chairIndex = table.findEmptyChair();
          if (chairIndex !== -1) {
            this.targetChair = table.chairs[chairIndex];
            this.targetChair.customer = this;
            this.targetChair.table = table; 
            this.state = 'MOVING_TO_CHAIR';
            foundChair = true;
            break;
          }
        }
        if (!foundChair) {
          this.wander();
          if (!this.speechText) this.setSpeech(random(["Looking for a table...", "Is there an empty chair?", "Busy place!", "A table, please!"])); 
        }
        break;

      case 'MOVING_TO_CHAIR':
        if (this.targetChair) {
          let dx = this.targetChair.x - this.x;
          let dy = this.targetChair.y - this.y;
          let angle = atan2(dy, dx);
          this.x += cos(angle) * this.speed;
          this.y += sin(angle) * this.speed;

          if (dist(this.x, this.y, this.targetChair.x, this.targetChair.y) < 5) {
            this.x = this.targetChair.x; 
            this.y = this.targetChair.y;
            this.state = 'EATING';
            this.targetChair.occupied = true; 
            this.eat(); 
          }
        }
        break;

      case 'EATING':
        this.eatingDuration--;
        if (this.eatingDuration % 60 === 0) {
          this.mouthOpen = true;
          this.eatingTimer = 20; 
        }
        if (this.eatingDuration <= 0) {
          this.state = 'PAYING'; 
          this.setSpeech("💰 Paying..."); 
          this.payingDuration = random(30, 60); 
        }
        break;

      case 'PAYING':
        this.payingDuration--;
        if (this.payingDuration <= 0) {
          this.paid = true; 
          this.remove = true;
          this.targetChair.occupied = false; 
          this.targetChair.customer = null; 
          if (this.targetChair.food) {
            this.targetChair.food.setEaten(); 
            this.targetChair.food = null; 
          }
          this.setSpeech(random(["That was good!", "See you again!", "Goodbye!", "Thanks for the meal!", "Going back home!"])); 
        }
        break;
    }

    this.avoidCollisions();
    this.x = constrain(this.x, restaurantOffset, width);
    this.y = constrain(this.y, 0, height);

    if (this.state !== previousState) {
      switch (this.state) {
        case 'SEEKING_TABLE': this.setSpeech(random(["Looking for a table...", "Is there an empty chair?", "Busy place!", "A table, please!"])); break;
        case 'MOVING_TO_CHAIR': this.setSpeech(random(["Found one!", "Coming right over!", "Almost there!", "Perfect!"])); break;
        case 'EATING': this.setSpeech(random(["Mmm, delicious!", "Yummy!", "This is great!", "So good!"])); break;
      }
    }
  }

  eat() {
    if (this.targetChair && this.targetChair.table && this.targetChair.table.foodOnTableCenter && !this.targetChair.table.foodOnTableCenter.isEaten()) {
      this.targetChair.table.foodOnTableCenter.setEaten(); 
      this.targetChair.table.foodOnTableCenter = null; 
    } else {
      let food = new Food(this.targetChair.x, this.targetChair.y + this.size * 0.6, random(foodEmojis), false);
      foodItems.push(food);
      this.targetChair.food = food; 
    }
    this.mouthOpen = true;
    this.eatingTimer = 20;
  }

  wander() {
    this.wanderTimer--;
    if (this.wanderTimer <= 0) {
      this.wanderAngle = random(TWO_PI);
      this.wanderTimer = random(60, 180); 
    }
    this.x += cos(this.wanderAngle) * (this.speed * 0.5);
    this.y += sin(this.wanderAngle) * (this.speed * 0.5);
  }

  avoidCollisions() {
    const avoidanceForce = 0.5; 
    const avoidanceRadius = 40; 

    for (let other of customers) {
      if (other !== this && other.state !== 'IN_JAIL' && !(other instanceof Customer && other.remove)) {
        let d = dist(this.x, this.y, other.x, other.y);
        if (d < avoidanceRadius) {
          let angle = atan2(this.y - other.y, this.x - other.x);
          this.x += cos(angle) * avoidanceForce;
          this.y += sin(angle) * avoidanceForce;
        }
      }
    }

    for (let table of tables) {
      let dx = this.x - table.x;
      let dy = this.y - table.y;
      if (abs(dx) < table.width / 2 + this.size / 2 && abs(dy) < table.height / 2 + this.size / 2) {
        let angle = atan2(dy, dx);
        this.x += cos(angle) * avoidanceForce * 2; 
        this.y += sin(angle) * avoidanceForce * 2;
      }
    }
  }
}
Line-by-line explanation (27 lines)

🔧 Subcomponents:

switch-case Customer State Machine switch (this.state) { case 'ARRIVING': ... case 'SEEKING_TABLE': ... case 'MOVING_TO_CHAIR': ... case 'EATING': ... case 'PAYING': ... }

Implements a 5-state lifecycle: customers arrive, find tables, move to chairs, eat, pay, and leave

for-loop Find Empty Chair Loop for (let table of tables) { let chairIndex = table.findEmptyChair(); ... }

Searches all tables for an available chair when in SEEKING_TABLE state

calculation Wandering Behavior this.wander();

When no table is found, customer wanders randomly around the restaurant

calculation Vector-Based Movement to Chair let dx = this.targetChair.x - this.x; let dy = this.targetChair.y - this.y; let angle = atan2(dy, dx); this.x += cos(angle) * this.speed; this.y += sin(angle) * this.speed;

Moves customer toward their target chair using angle and trigonometry

calculation Eating Animation with Mouth if (this.eatingDuration % 60 === 0) { this.mouthOpen = true; this.eatingTimer = 20; }

Every 60 frames, opens the customer's mouth briefly to simulate chewing

for-loop Collision Avoidance Loop for (let other of customers) { if (other !== this && other.state !== 'IN_JAIL' && !(other instanceof Customer && other.remove)) { ... } }

Pushes the customer away from nearby other characters to prevent overlap

constructor(x, y) {
Initializes a new Customer at position (x, y) with randomized appearance and behavior
this.size = random(30, 45);
Gives each customer a random size between 30 and 45 pixels for visual variety
this.color = color(random(100, 200), random(100, 200), random(100, 200));
Assigns a random RGB color in the mid-brightness range so customers are visually distinct
this.state = 'ARRIVING';
Sets the initial state to ARRIVING, so the customer starts by walking into the restaurant
this.setSpeech(random(["I'm hungry!", "Table for one?", "Looking for a seat!"]));
Sets an initial speech bubble with a random greeting
setSpeech(text) { this.speechText = text; this.speechTimer = 90; }
Helper method to set speech text and a 90-frame timer so the bubble fades after 1.5 seconds (60fps / 90 frames)
display() { push(); translate(this.x, this.y); ... pop(); }
Renders the customer's body, head, eyes, mouth, and speech bubble at their position
rect(0, 0, this.size / 2, this.size);
Draws a rectangle (the body) from the local origin
ellipse(0, -this.size / 2, this.size * 0.8);
Draws a circular head above the body
ellipse(-this.size * 0.15, -this.size * 0.6, this.size * 0.1);
Draws the left eye as a small dark circle
if (this.mouthOpen) { arc(0, -this.size * 0.4, this.size * 0.3, this.size * 0.2, 0, PI); } else { line(...); }
Draws either an open arc (mouth eating) or a simple line (closed mouth) based on mouthOpen flag
if (this.speechText && this.speechTimer > 0) { ... rect(...); triangle(...); text(...); }
If there's text and time left, renders a speech bubble with a white rounded rectangle, a triangle pointer, and black text
update() { if (this.state === 'DRAGGED' || this.state === 'IN_JAIL' || (inHouse && this !== player)) { return; } }
Skips update if the customer is being dragged, in jail, or paused in the house—preserves their state
if (this.speechTimer > 0) this.speechTimer--; else this.speechText = "";
Decrements the speech timer and clears the text when it reaches 0
case 'ARRIVING': this.x += this.speed; if (this.x >= restaurantOffset + 200) this.state = 'SEEKING_TABLE'; break;
Moves the customer right until they're past the left edge of the restaurant, then switch to looking for a table
case 'SEEKING_TABLE': for (let table of tables) { let chairIndex = table.findEmptyChair(); ... }
Loops through tables looking for an empty chair; if found, set it as the target and transition to MOVING_TO_CHAIR
if (!foundChair) { this.wander(); ... }
If no table found, wander randomly and complain with speech
case 'MOVING_TO_CHAIR': let dx = this.targetChair.x - this.x; let dy = this.targetChair.y - this.y; let angle = atan2(dy, dx);
Calculates the direction vector from the customer to the chair, then converts it to an angle using atan2
this.x += cos(angle) * this.speed; this.y += sin(angle) * this.speed;
Moves the customer by the speed amount in the calculated direction using trigonometry
if (dist(this.x, this.y, this.targetChair.x, this.targetChair.y) < 5) { this.x = this.targetChair.x; this.y = this.targetChair.y; this.state = 'EATING'; this.eat(); }
When the customer reaches the chair (within 5 pixels), snap them to the chair, start eating, and call the eat() method
case 'EATING': this.eatingDuration--; if (this.eatingDuration % 60 === 0) { this.mouthOpen = true; this.eatingTimer = 20; }
Decrements eating time; every 60 frames (once per second), opens the mouth briefly
if (this.eatingDuration <= 0) { this.state = 'PAYING'; ... }
When eating time runs out, transition to the PAYING state
case 'PAYING': this.payingDuration--; if (this.payingDuration <= 0) { this.paid = true; this.remove = true; ... }
Decrements payment time; when done, mark the customer as paid and ready to remove
this.avoidCollisions(); this.x = constrain(this.x, restaurantOffset, width); this.y = constrain(this.y, 0, height);
Prevents overlap with other characters and keeps the customer on-screen within the restaurant bounds
eat() { if (this.targetChair && this.targetChair.table && this.targetChair.table.foodOnTableCenter && !this.targetChair.table.foodOnTableCenter.isEaten()) { ... } else { ... } }
Tries to use food already on the table; if not found, creates new food. Opens mouth to chew.
wander() { this.wanderTimer--; if (this.wanderTimer <= 0) { this.wanderAngle = random(TWO_PI); this.wanderTimer = random(60, 180); } ... }
Implements Boid-like wandering: occasionally picks a new random angle and moves in that direction at half speed
avoidCollisions() { for (let other of customers) { if (other !== this && other.state !== 'IN_JAIL' && !(other instanceof Customer && other.remove)) { let d = dist(this.x, this.y, other.x, other.y); if (d < avoidanceRadius) { ... } } } }
Checks distance to all other characters; if too close, pushes this character away using the angle between them

Robber (class)

Robber is a subclass that inherits most behavior from Customer but implements a completely different AI goal: stealing food instead of eating it. It has 5 states (ENTERING, SEEKING_LOOT, STEALING, FLEEING, IN_JAIL) that create a robber-vs-police dynamic. When caught, they're locked in the jail box and must wait before escaping.

🔬 This code prioritizes stealing from occupied chairs first. What if you changed it so robbers skip occupied chairs and ONLY steal kitchen or table-center food with `if (chair.occupied) continue;`? How would this make the game fairer to customers?

      case 'SEEKING_LOOT':
        for (let table of tables) {
          for (let chair of table.chairs) {
            if (chair.occupied && chair.food && !chair.food.isEaten()) {
              this.targetFood = chair.food;
              this.targetChair = chair; 
              this.state = 'STEALING';
              this.setSpeech("Aha! Found some loot!");
              return;
class Robber extends Customer {
  constructor(x, y) {
    super(x, y); 
    this.color = color(50); 
    this.speed = random(2.5, 4); 
    this.state = 'ENTERING'; 
    this.maskEmoji = "🥷"; 
    this.targetFood = null; 
    this.jailTimer = 0; 
    this.setSpeech("Mwahahaha! Time to feast!"); 
    this.originalState = this.state; 
  }

  setSpeech(text) {
    // Only speak normally if not in jail
    if (this.state !== 'IN_JAIL') super.setSpeech(text);
  }

  display() {
    // We REMOVED the "if in jail return" line so we can see him behind bars!
    super.display(); 
    push();
    translate(this.x, this.y);
    textSize(this.size * 0.8);
    textAlign(CENTER, CENTER);
    text(this.maskEmoji, 0, -this.size * 0.6); 
    pop();
  }

  update() {
    if (this.speechTimer > 0) this.speechTimer--;
    else this.speechText = ""; 

    let previousState = this.state; 

    switch (this.state) {
      case 'IN_JAIL':
        // Snap inside the jail box
        this.x = width - 20; 
        this.y = 100;
        this.jailTimer--;
        
        // Complain while in jail!
        if (this.jailTimer % 100 === 0) {
          super.setSpeech(random(["Let me out!", "I'm innocent!", "I want my lawyer!", "It wasn't me!"]));
        }

        if (this.jailTimer <= 0) {
          this.state = 'ENTERING'; 
          this.x = random(-100, -50); 
          this.y = random(height);
          this.setSpeech(random(["I'm back!", "Fresh out!", "Time for round two!"])); 
        }
        return; 

      case 'ENTERING':
        this.x += this.speed;
        if (this.x >= restaurantOffset + 200) this.state = 'SEEKING_LOOT';
        break;

      case 'SEEKING_LOOT':
        for (let table of tables) {
          for (let chair of table.chairs) {
            if (chair.occupied && chair.food && !chair.food.isEaten()) {
              this.targetFood = chair.food;
              this.targetChair = chair; 
              this.state = 'STEALING';
              this.setSpeech("Aha! Found some loot!");
              return; 
            }
          }
          if (table.foodOnTableCenter && !table.foodOnTableCenter.isEaten()) {
            this.targetFood = table.foodOnTableCenter;
            this.targetChair = { table: table }; 
            this.state = 'STEALING';
            this.setSpeech("Aha! Found some table loot!");
            return; 
          }
        }
        for (let food of foodItems) {
          if (food.inKitchen && !food.isEaten()) {
            this.targetFood = food;
            this.state = 'STEALING';
            this.setSpeech("Kitchen loot! Mwahahaha!");
            return; 
          }
        }
        this.wander();
        if (!this.speechText) this.setSpeech(random(["Where's the loot?", "I smell food...", "This place needs a good robbing!"]));
        break;

      case 'STEALING':
        if (this.targetFood) {
          let dx = this.targetFood.x - this.x;
          let dy = this.targetFood.y - this.y;
          let angle = atan2(dy, dx);
          this.x += cos(angle) * this.speed;
          this.y += sin(angle) * this.speed;

          if (dist(this.x, this.y, this.targetFood.x, this.targetFood.y) < this.size / 2 + this.targetFood.size / 4) {
            this.takeFood();
            this.state = 'FLEEING';
            this.setSpeech("Got it! See ya!");
          }
        } else {
          this.state = 'SEEKING_LOOT';
          this.setSpeech("My loot is gone! 😠");
        }
        break;

      case 'FLEEING':
        this.x += this.speed * 2; 
        this.y += random(-1, 1) * this.speed * 0.5; 
        this.y = constrain(this.y, 0, height); 
        break;
    }

    this.avoidCollisions();
    // Don't constrain if in jail, to allow them to sit in the box at top right
    if (this.state !== 'IN_JAIL') {
      this.x = constrain(this.x, restaurantOffset, width);
      this.y = constrain(this.y, 0, height);
    }

    if (this.state !== previousState) {
      switch (this.state) {
        case 'ENTERING': this.setSpeech("Mwahahaha! Time to feast!"); break;
        case 'SEEKING_LOOT': this.setSpeech(random(["Where's the loot?", "I smell food...", "This place needs a good robbing!"])); break;
        case 'STEALING': this.setSpeech("Got it! See ya!"); break;
        case 'FLEEING': this.setSpeech(random(["Run!", "Catch me if you can!", "Later, losers!"])); break;
      }
    }
  }

  takeFood() {
    if (this.targetFood) {
      this.targetFood.setEaten(); 
      if (this.targetChair && this.targetChair.food === this.targetFood) this.targetChair.food = null;
      if (this.targetChair && this.targetChair.table && this.targetChair.table.foodOnTableCenter === this.targetFood) {
        this.targetChair.table.foodOnTableCenter = null;
      }
      this.targetFood = null; 
    }
  }
}
Line-by-line explanation (17 lines)

🔧 Subcomponents:

conditional IN_JAIL State Handler case 'IN_JAIL': this.x = width - 20; this.y = 100; this.jailTimer--; if (this.jailTimer % 100 === 0) { super.setSpeech(...); } if (this.jailTimer <= 0) { this.state = 'ENTERING'; ... }

Snaps the robber into the jail box, decrements time, shows complaints, and releases them when time expires

for-loop Seek Loot Loop (tables then kitchen) for (let table of tables) { for (let chair of table.chairs) { if (chair.occupied && chair.food && !chair.food.isEaten()) { ... } } ... } for (let food of foodItems) { if (food.inKitchen && !food.isEaten()) { ... } }

Searches tables for occupied chairs with food, then table centers, then the kitchen

calculation Chase and Steal Food if (this.targetFood) { let dx = this.targetFood.x - this.x; ... if (dist(...) < ...) { this.takeFood(); this.state = 'FLEEING'; } }

Moves toward target food using trigonometry and steals it when close enough

class Robber extends Customer {
Robber inherits from Customer, reusing display(), wander(), and avoidCollisions() but overriding update() and adding stealing logic
this.state = 'ENTERING';
Robbers start in ENTERING state, walking into the restaurant like customers but with theft on their mind
this.maskEmoji = "🥷";
Stores the ninja emoji to display above the robber's head
setSpeech(text) { if (this.state !== 'IN_JAIL') super.setSpeech(text); }
Overrides setSpeech() to allow only jailed robbers to complain—others speak normally
case 'IN_JAIL': this.x = width - 20; this.y = 100;
Force the robber to stay at the jail position (top-right corner) by updating their x and y each frame
this.jailTimer--;
Decrements the jail timer, counting down the frames until release
if (this.jailTimer % 100 === 0) { super.setSpeech(random(["Let me out!", "I'm innocent!", "I want my lawyer!", "It wasn't me!"])); }
Every 100 frames (about 1.67 seconds), the jailed robber complains using parent's setSpeech so it always shows in jail
if (this.jailTimer <= 0) { this.state = 'ENTERING'; this.x = random(-100, -50); this.y = random(height); ... }
When jail time expires, release the robber by teleporting them off-screen on the left and transitioning to ENTERING
case 'ENTERING': this.x += this.speed; if (this.x >= restaurantOffset + 200) this.state = 'SEEKING_LOOT'; break;
Robber walks right from off-screen until they're deep in the restaurant, then start hunting for food
for (let table of tables) { for (let chair of table.chairs) { if (chair.occupied && chair.food && !chair.food.isEaten()) { ... } } }
First search priority: occupied chairs with food (steal from customers who are eating)
if (table.foodOnTableCenter && !table.foodOnTableCenter.isEaten()) { this.targetFood = table.foodOnTableCenter; ... }
Second priority: food sitting on table centers (food you clicked to place)
for (let food of foodItems) { if (food.inKitchen && !food.isEaten()) { this.targetFood = food; ... } }
Third priority: food still in the kitchen (freshly made items)
case 'STEALING': if (this.targetFood) { let dx = this.targetFood.x - this.x; let dy = this.targetFood.y - this.y; let angle = atan2(dy, dx); this.x += cos(angle) * this.speed; this.y += sin(angle) * this.speed;
Move toward the target food using the same angle-based movement as Customer uses for chairs
if (dist(this.x, this.y, this.targetFood.x, this.targetFood.y) < this.size / 2 + this.targetFood.size / 4) {
Check if robber has reached the food (distance-based collision detection)
this.takeFood(); this.state = 'FLEEING';
Steal the food and immediately switch to FLEEING state to escape before police catch them
case 'FLEEING': this.x += this.speed * 2; this.y += random(-1, 1) * this.speed * 0.5;
Robber moves right at double speed and wiggles vertically to show panic/evasion
takeFood() { if (this.targetFood) { this.targetFood.setEaten(); if (this.targetChair && this.targetChair.food === this.targetFood) this.targetChair.food = null; ... }
Marks the food as eaten and removes all references to it so it disappears and can't be eaten again

PoliceOfficer (class)

PoliceOfficer is a subclass that implements law-enforcement AI with 4 states (PATROLLING, PURSUING, APPREHENDING, LEAVING). They patrol randomly, detect robbers within 300 pixels, chase and catch them, then escort them to jail for 400 frames. This creates an emergent narrative of crime and justice within the simulation.

class PoliceOfficer extends Customer {
  constructor(x, y) {
    super(x, y); 
    this.color = color(0, 0, 150); 
    this.speed = random(4.5, 6); 
    this.state = 'PATROLLING'; 
    this.iconEmoji = "🚓"; 
    this.targetRobber = null; 
    this.apprehendTimer = 0; 
    this.setSpeech(random(["All quiet here...", "Patrolling the area.", "Looking out for trouble."])); 
    this.originalState = this.state; 
  }

  display() {
    super.display(); 
    push();
    translate(this.x, this.y);
    textSize(this.size * 0.8);
    textAlign(CENTER, CENTER);
    text(this.iconEmoji, 0, -this.size * 0.6); 
    pop();
  }

  update() {
    if (this.speechTimer > 0) this.speechTimer--;
    else this.speechText = ""; 

    let previousState = this.state; 

    switch (this.state) {
      case 'PATROLLING':
        this.wander();
        for (let person of customers) {
          if (person !== this && person instanceof Robber && person.state !== 'FLEEING' && person.state !== 'ENTERING' && person.state !== 'IN_JAIL') {
            let d = dist(this.x, this.y, person.x, person.y);
            // Increased detection range significantly so they catch robbers better!
            if (d < 300) { 
              this.targetRobber = person;
              this.state = 'PURSUING';
              this.setSpeech("🚨 Robber spotted! Get back here!");
              break; 
            }
          }
        }
        break;

      case 'PURSUING':
        if (this.targetRobber) {
          let dx = this.targetRobber.x - this.x;
          let dy = this.targetRobber.y - this.y;
          let angle = atan2(dy, dx);
          this.x += cos(angle) * this.speed;
          this.y += sin(angle) * this.speed;

          if (dist(this.x, this.y, this.targetRobber.x, this.targetRobber.y) < this.size / 2 + this.targetRobber.size / 2) {
            this.state = 'APPREHENDING';
            this.setSpeech("🚓 Gotcha! You're under arrest!");
            this.targetRobber.setSpeech("Caught! 😫"); 
            this.apprehendTimer = 120; 
          }
        } else {
          this.state = 'PATROLLING';
          this.setSpeech("Lost them... Back to patrolling.");
        }
        break;

      case 'APPREHENDING':
        this.apprehendTimer--;
        if (this.apprehendTimer <= 0) {
          this.state = 'LEAVING'; 
          this.setSpeech("Taking them to jail!");
          if (this.targetRobber) {
            this.targetRobber.state = 'IN_JAIL'; 
            this.targetRobber.jailTimer = 400; // Longer jail time
            this.targetRobber.setSpeech("Nooooo! 😭 I'll be back!"); 
            this.targetRobber = null; 
          }
        }
        break;

      case 'LEAVING': 
        this.x += this.speed; 
        break;
    }

    this.avoidCollisions();
    this.x = constrain(this.x, restaurantOffset, width);
    this.y = constrain(this.y, 0, height);

    if (this.state !== previousState) {
      switch (this.state) {
        case 'PATROLLING': this.setSpeech(random(["All quiet here...", "Patrolling the area.", "Looking out for trouble."])); break;
        case 'PURSUING': this.setSpeech(random(["🚨 Robber spotted! Get back here!", "Stop right there!", "Chase is on!"])); break;
        case 'APPREHENDING': this.setSpeech(random(["🚓 Gotcha! You're under arrest!", "Hands behind your back!", "Case closed!"])); break;
        case 'LEAVING': this.setSpeech(random(["Taking them to jail!", "Justice served!", "Another one bites the dust."])); break;
      }
    }
  }
}
Line-by-line explanation (15 lines)

🔧 Subcomponents:

for-loop Patrol and Detect Robbers for (let person of customers) { if (person !== this && person instanceof Robber && person.state !== 'FLEEING' && person.state !== 'ENTERING' && person.state !== 'IN_JAIL') { if (d < 300) { ... } } }

Patrols by wandering and checks if any active robbers are within 300 pixel detection range

calculation Chase Robber to Apprehension let dx = this.targetRobber.x - this.x; let dy = this.targetRobber.y - this.y; let angle = atan2(dy, dx); this.x += cos(angle) * this.speed; this.y += sin(angle) * this.speed; if (dist(...) < ...) { this.state = 'APPREHENDING'; ... }

Chases robber using angle-based movement and triggers apprehension when close enough

conditional Hold Robber and Send to Jail case 'APPREHENDING': this.apprehendTimer--; if (this.apprehendTimer <= 0) { this.state = 'LEAVING'; if (this.targetRobber) { this.targetRobber.state = 'IN_JAIL'; this.targetRobber.jailTimer = 400; } }

Holds the robber for 120 frames, then sends them to jail for 400 frames and leaves

class PoliceOfficer extends Customer {
PoliceOfficer inherits from Customer but overrides update() with law-enforcement AI
this.color = color(0, 0, 150);
Makes the police officer a dark blue color for easy visual identification
this.speed = random(4.5, 6);
Police are faster than regular customers but not as fast as fleeing robbers, creating interesting chase dynamics
this.state = 'PATROLLING';
Police start by wandering around looking for trouble
case 'PATROLLING': this.wander();
In PATROLLING state, move randomly around the restaurant using inherited wander() method
for (let person of customers) { if (person !== this && person instanceof Robber && person.state !== 'FLEEING' && person.state !== 'ENTERING' && person.state !== 'IN_JAIL') {
Loop through all characters, looking for robbers that are ACTIVELY stealing (not fleeing, entering, or already jailed)
let d = dist(this.x, this.y, person.x, person.y); if (d < 300) {
Calculate distance to the robber; if within 300 pixels (large radius for reliable detection), spot them
this.targetRobber = person; this.state = 'PURSUING';
Lock onto the robber and transition to PURSUING state to chase them
case 'PURSUING': if (this.targetRobber) { let dx = this.targetRobber.x - this.x; let dy = this.targetRobber.y - this.y; let angle = atan2(dy, dx);
In PURSUING state, calculate direction to the robber using the same trigonometric approach as Customer uses for chairs
this.x += cos(angle) * this.speed; this.y += sin(angle) * this.speed;
Move toward the robber at police speed
if (dist(this.x, this.y, this.targetRobber.x, this.targetRobber.y) < this.size / 2 + this.targetRobber.size / 2) {
Check if police has reached the robber (collision with sum of their radii)
this.state = 'APPREHENDING'; this.setSpeech("🚓 Gotcha! You're under arrest!"); this.targetRobber.setSpeech("Caught! 😫"); this.apprehendTimer = 120;
Transition to APPREHENDING and set a 120-frame timer while both show dramatic speech
case 'APPREHENDING': this.apprehendTimer--; if (this.apprehendTimer <= 0) { this.state = 'LEAVING';
In APPREHENDING, count down; when timer reaches 0, transition to LEAVING
if (this.targetRobber) { this.targetRobber.state = 'IN_JAIL'; this.targetRobber.jailTimer = 400;
Send the robber to JAIL for 400 frames (about 6.67 seconds), a significant time-out
case 'LEAVING': this.x += this.speed;
In LEAVING state, move right off-screen to exit the simulation (will be auto-removed when x > width)

PlayerControlledCustomer (class)

PlayerControlledCustomer is the player character, inheriting from Customer but completely overriding update() to respond to keyboard and mouse input. It supports both continuous keyboard movement (WASD/arrows) and click-to-move. It also normalizes diagonal movement so diagonal and cardinal speeds are equal, handles house vs. restaurant boundaries, and collides with other characters and tables.

🔬 This code checks flags set by keyPressed(). What if you added a check like `if (this.left && this.right) moveX = 0;` to cancel horizontal movement if both left and right keys are pressed? Try it and see if it feels better or worse.

    if (this.left || this.right || this.up || this.down) {
      if (this.left) moveX -= this.speed;
      if (this.right) moveX += this.speed;
      if (this.up) moveY -= this.speed;
      if (this.down) moveY += this.speed;
      isMoving = true;
    }
class PlayerControlledCustomer extends Customer {
  constructor(x, y, customColor, customSize, customIcon) {
    super(x, y);
    this.color = color(customColor); 
    this.size = customSize; 
    this.speed = random(4, 6); 
    this.state = 'PLAYER_CONTROLLED'; 
    this.iconEmoji = customIcon; 
    this.left = false;
    this.right = false;
    this.up = false;
    this.down = false;
    this.targetX = null;
    this.targetY = null;
    this.setSpeech("I'm the player!"); 
    this.originalState = this.state; 
  }

  display() {
    super.display(); 
    push();
    translate(this.x, this.y);
    fill(255);
    noStroke();
    textSize(this.size * 0.5);
    text(this.iconEmoji, 0, -this.size * 0.9); 
    pop();
  }

  update() {
    if (this.state === 'DRAGGED' || this.state === 'IN_JAIL') return;

    if (this.speechTimer > 0) this.speechTimer--;

    let moveX = 0;
    let moveY = 0;
    let isMoving = false;

    if (this.left || this.right || this.up || this.down) {
      if (this.left) moveX -= this.speed;
      if (this.right) moveX += this.speed;
      if (this.up) moveY -= this.speed;
      if (this.down) moveY += this.speed;
      isMoving = true;
    } else if (this.targetX !== null && this.targetY !== null) {
      // CLICK TO MOVE Logic
      let d = dist(this.x, this.y, this.targetX, this.targetY);
      if (d > this.speed) {
        let angle = atan2(this.targetY - this.y, this.targetX - this.x);
        moveX = cos(angle) * this.speed;
        moveY = sin(angle) * this.speed;
        isMoving = true;
        if (this.speechText !== "Zoom! 💨") this.setSpeech("Zoom! 💨");
      } else {
        this.x = this.targetX;
        this.y = this.targetY;
        this.targetX = null;
        this.targetY = null;
        this.setSpeech(""); // stop zooming
      }
    }

    if (!isMoving && this.speechTimer <= 0) {
      this.speechText = "";
    }

    if ((this.left || this.right || this.up || this.down) && moveX !== 0 && moveY !== 0) {
      let mag = sqrt(moveX * moveX + moveY * moveY);
      moveX = (moveX / mag) * this.speed;
      moveY = (moveY / mag) * this.speed;
    }

    this.x += moveX;
    this.y += moveY;

    this.avoidCollisions();

    if (inHouse) {
      // Free movement anywhere in the house window
      this.x = constrain(this.x, 0, width);
      this.y = constrain(this.y, 0, height);
    } else {
      this.x = constrain(this.x, houseArea.x + houseArea.width - 10, width);
      this.y = constrain(this.y, 0, height);
    }
  }

  avoidCollisions() {
    const avoidanceForce = 0.5;
    const avoidanceRadius = 40;

    for (let other of customers) {
      if (other !== this && other.state !== 'IN_JAIL' && !(other instanceof Customer && other.remove)) {
        let d = dist(this.x, this.y, other.x, other.y);
        if (d < avoidanceRadius) {
          let angle = atan2(this.y - other.y, this.x - other.x);
          this.x += cos(angle) * avoidanceForce;
          this.y += sin(angle) * avoidanceForce;
        }
      }
    }

    if (!inHouse) {
      for (let table of tables) {
        let dx = this.x - table.x;
        let dy = this.y - table.y;
        if (abs(dx) < table.width / 2 + this.size / 2 && abs(dy) < table.height / 2 + this.size / 2) {
          let angle = atan2(dy, dx);
          this.x += cos(angle) * avoidanceForce * 2;
          this.y += sin(angle) * avoidanceForce * 2;
        }
      }
    }
  }
}
Line-by-line explanation (17 lines)

🔧 Subcomponents:

conditional Keyboard Movement Input if (this.left || this.right || this.up || this.down) { if (this.left) moveX -= this.speed; if (this.right) moveX += this.speed; if (this.up) moveY -= this.speed; if (this.down) moveY += this.speed; isMoving = true; }

Checks movement flags and accumulates velocity in moveX/moveY

conditional Click-to-Move Logic else if (this.targetX !== null && this.targetY !== null) { let d = dist(this.x, this.y, this.targetX, this.targetY); if (d > this.speed) { ... } else { ... } }

Alternatively, if no keys pressed, move toward a click target using angle-based movement

calculation Normalize Diagonal Movement if ((this.left || this.right || this.up || this.down) && moveX !== 0 && moveY !== 0) { let mag = sqrt(moveX * moveX + moveY * moveY); moveX = (moveX / mag) * this.speed; moveY = (moveY / mag) * this.speed; }

Prevents faster diagonal movement by normalizing velocity when moving in multiple directions

conditional Boundary Constraints (House vs. Restaurant) if (inHouse) { this.x = constrain(this.x, 0, width); this.y = constrain(this.y, 0, height); } else { this.x = constrain(this.x, houseArea.x + houseArea.width - 10, width); ... }

Keeps player on-screen, with different bounds for house interior vs. restaurant

class PlayerControlledCustomer extends Customer {
Inherits from Customer but completely overrides update() to support player input instead of AI
this.color = color(customColor);
Uses the player's custom color from setup() prompts
this.size = customSize;
Uses the player's custom size from setup()
this.state = 'PLAYER_CONTROLLED';
Sets state to a unique value that tells draw() and mousePressed() this is the controllable character
this.left = false; this.right = false; this.up = false; this.down = false;
Initializes movement flags that keyPressed() and keyReleased() will toggle
this.targetX = null; this.targetY = null;
Initializes click-to-move targets (null means no pending click)
display() { super.display(); ... text(this.iconEmoji, 0, -this.size * 0.9); }
Calls parent display() to draw the body, then adds the player's custom emoji icon above the head
if (this.state === 'DRAGGED' || this.state === 'IN_JAIL') return;
Skip update if being dragged or jailed, preserving movement state
if (this.left || this.right || this.up || this.down) { if (this.left) moveX -= this.speed; ... isMoving = true; }
If any movement key is pressed, calculate the direction by checking all four flags, then set isMoving to true
} else if (this.targetX !== null && this.targetY !== null) {
If no keys pressed but a click target exists, use click-to-move instead
let d = dist(this.x, this.y, this.targetX, this.targetY); if (d > this.speed) {
Calculate distance to target; if farther than one step, keep moving
let angle = atan2(this.targetY - this.y, this.targetX - this.x); moveX = cos(angle) * this.speed; moveY = sin(angle) * this.speed;
Calculate angle to target and move one step toward it using trigonometry
} else { this.x = this.targetX; this.y = this.targetY; this.targetX = null; this.targetY = null; this.setSpeech(""); }
When close enough, snap to target, clear the target, and stop the 'Zoom' speech
if (!isMoving && this.speechTimer <= 0) { this.speechText = ""; }
If not moving and speech timer expired, clear any remaining speech
if ((this.left || this.right || this.up || this.down) && moveX !== 0 && moveY !== 0) { let mag = sqrt(moveX * moveX + moveY * moveY); moveX = (moveX / mag) * this.speed; moveY = (moveY / mag) * this.speed; }
When holding two keys at once (like left+up), the raw movement would be faster diagonally. This normalizes the velocity so diagonal movement is the same speed as cardinal movement
this.x += moveX; this.y += moveY;
Apply the calculated movement to the player's position
if (inHouse) { this.x = constrain(this.x, 0, width); this.y = constrain(this.y, 0, height); } else { this.x = constrain(this.x, houseArea.x + houseArea.width - 10, width); ... }
Different boundary logic: in house, can move freely; in restaurant, can't go left into the house area

Table (class)

Table is a simple data structure that holds position, dimensions, and a list of chair objects. Each table in the sketch has 2 chairs initially (created in initializeTables()). Tables are clickable to place food, and customers search tables for empty chairs to sit in.

class Table {
  constructor(x, y, numChairs) {
    this.x = x;
    this.y = y;
    this.width = 120;
    this.height = 80;
    this.chairs = [];
    this.foodOnTableCenter = null; 

    for (let i = 0; i < numChairs; i++) {
      let chairX = this.x + (i === 0 ? -this.width / 2 - 20 : this.width / 2 + 20);
      let chairY = this.y;
      this.chairs.push({ x: chairX, y: chairY, occupied: false, customer: null, food: null, table: this }); 
    }
  }

  display() {
    fill(139, 69, 19); 
    stroke(0);
    rect(this.x, this.y, this.width, this.height, 5); 

    for (let chair of this.chairs) {
      fill(100); 
      noStroke();
      ellipse(chair.x, chair.y, 30, 30);
      if (chair.occupied) {
        fill(255, 0, 0, 100); 
        ellipse(chair.x, chair.y, 30, 30);
      }
    }

    if (this.foodOnTableCenter && !this.foodOnTableCenter.isEaten()) {
      this.foodOnTableCenter.display();
    }
  }

  findEmptyChair() {
    for (let i = 0; i < this.chairs.length; i++) {
      if (!this.chairs[i].occupied) return i; 
    }
    return -1; 
  }
}
Line-by-line explanation (10 lines)

🔧 Subcomponents:

for-loop Chair Positioning Loop for (let i = 0; i < numChairs; i++) { let chairX = this.x + (i === 0 ? -this.width / 2 - 20 : this.width / 2 + 20); ... this.chairs.push(...); }

Creates chairs: first chair on the left side of the table, second on the right side

for-loop Display Chairs Loop for (let chair of this.chairs) { fill(100); ellipse(chair.x, chair.y, 30, 30); if (chair.occupied) { fill(255, 0, 0, 100); ellipse(chair.x, chair.y, 30, 30); } }

Renders each chair as a gray circle, highlighted in red+transparent if occupied

constructor(x, y, numChairs) {
Creates a table at (x, y) with a specified number of chairs
this.width = 120; this.height = 80;
Standard table dimensions (120 pixels wide, 80 pixels tall)
for (let i = 0; i < numChairs; i++) {
Loop to create and position each chair
let chairX = this.x + (i === 0 ? -this.width / 2 - 20 : this.width / 2 + 20);
Position chairs: first chair (i=0) goes on the left side, second on the right. The ternary operator (? :) decides based on i
this.chairs.push({ x: chairX, y: chairY, occupied: false, customer: null, food: null, table: this });
Each chair is a simple object with position, occupied flag, and references to the customer and food using it
fill(139, 69, 19); rect(this.x, this.y, this.width, this.height, 5);
Renders the table surface as a brown rectangle with rounded corners (the 5 is the corner radius)
for (let chair of this.chairs) { fill(100); ellipse(chair.x, chair.y, 30, 30); }
Renders each chair as a gray circle
if (chair.occupied) { fill(255, 0, 0, 100); ellipse(chair.x, chair.y, 30, 30); }
If the chair is occupied, draw a semi-transparent red circle on top to show it's taken
if (this.foodOnTableCenter && !this.foodOnTableCenter.isEaten()) { this.foodOnTableCenter.display(); }
If there's food on the table and it hasn't been eaten, display it
findEmptyChair() { for (let i = 0; i < this.chairs.length; i++) { if (!this.chairs[i].occupied) return i; } return -1; }
Helper that returns the index of the first unoccupied chair, or -1 if all are occupied

Food (class)

Food is the simplest class in the sketch. It's just an emoji at a position that can be eaten. When created in the kitchen, inKitchen=true; when clicked on a table, inKitchen=false. Customers and robbers target and consume food items.

class Food {
  constructor(x, y, emoji, inKitchen = false) {
    this.x = x;
    this.y = y;
    this.emoji = emoji;
    this.size = random(20, 36); 
    this.eaten = false;
    this.inKitchen = inKitchen; 
  }
  display() {
    if (!this.eaten) {
      textSize(this.size);
      textAlign(CENTER, CENTER);
      text(this.emoji, this.x, this.y);
    }
  }
  isEaten() { return this.eaten; }
  setEaten() { this.eaten = true; }
}
Line-by-line explanation (7 lines)
constructor(x, y, emoji, inKitchen = false) {
Creates food at (x, y) with a random emoji and optional inKitchen flag (defaults to false)
this.size = random(20, 36);
Gives each food item a random size for visual variety
this.eaten = false;
Starts as uneaten; will be set to true when a customer eats it or a robber steals it
this.inKitchen = inKitchen;
Tracks whether this food is in the kitchen (true) or on a table (false)
display() { if (!this.eaten) { textSize(this.size); text(this.emoji, this.x, this.y); } }
Only renders the emoji if the food hasn't been eaten yet
isEaten() { return this.eaten; }
Simple getter to check if this food has been consumed
setEaten() { this.eaten = true; }
Marks this food as eaten; it will no longer be displayed and will be removed from the foodItems array on the next frame

HouseItem (class)

HouseItem is the simplest class—just an emoji that can be dragged around in the house. Players create new ones by clicking 'Add Item', then drag them or trash them via mousePressed(), mouseDragged(), and mouseReleased() handlers.

class HouseItem {
  constructor(x, y, emoji) {
    this.x = x;
    this.y = y;
    this.emoji = emoji;
    this.size = 50; 
  }
  display() {
    push();
    textSize(this.size);
    textAlign(CENTER, CENTER);
    text(this.emoji, this.x, this.y);
    pop();
  }
}
Line-by-line explanation (3 lines)
constructor(x, y, emoji) {
Creates a decorative item at (x, y) with a random emoji from the houseEmojis array
this.size = 50;
All house items are 50 pixels in text size
display() { push(); textSize(this.size); textAlign(CENTER, CENTER); text(this.emoji, this.x, this.y); pop(); }
Renders the emoji at its position with centered alignment, wrapped in push/pop to preserve state

drawInteractionPrompt()

drawInteractionPrompt() is a simple helper that renders a white rounded button with centered text. It's used for 'Enter House', 'Exit House', 'Add Item', and similar UI prompts. It calculates button width dynamically based on text length.

function drawInteractionPrompt(msg, x, y) {
  push();
  textSize(14);
  textAlign(CENTER, CENTER);
  fill(255);
  stroke(0);
  let w = textWidth(msg) + 20;
  let h = 30;
  rect(x, y, w, h, 5);
  fill(0);
  noStroke();
  text(msg, x, y);
  pop();
}
Line-by-line explanation (4 lines)
let w = textWidth(msg) + 20;
Calculates the button width based on text width plus 20 pixels of padding
let h = 30;
Fixed button height
rect(x, y, w, h, 5);
Draws a white rounded rectangle (the button background)
text(msg, x, y);
Renders the message text in black on top of the button

windowResized()

windowResized() fires whenever the browser window is resized. It updates the canvas, house, and table positions so the layout scales responsively to different screen sizes.

function windowResized() {
  resizeCanvas(windowWidth, windowHeight);
  houseArea.height = windowHeight; 
  houseEntrance.y = windowHeight / 2; 
  initializeTables(); 
}
Line-by-line explanation (4 lines)
resizeCanvas(windowWidth, windowHeight);
Resizes the p5.js canvas to match the new browser window size
houseArea.height = windowHeight;
Updates the house interior's height to fill the new window
houseEntrance.y = windowHeight / 2;
Re-centers the house entrance vertically on the new window
initializeTables();
Rebuilds the four restaurant tables so they're re-positioned for the new canvas dimensions

📦 Key Variables

customers array

Master array holding all characters: Player, Customers, Robbers, Police, BoatPeople. Updated every frame with new spawns and removals.

let customers = [];
foodItems array

Array of all Food objects in the restaurant. Managed by spawn (click), consumption (customer/robber), and cleanup (draw loop).

let foodItems = [];
tables array

Array of the four restaurant Table objects, each with 2 chairs. Initialized in setup() and rebuilt on window resize.

let tables = [];
houseItems array

Array of player-placed decorative items (plants, TV, pets, etc.) in the house. Created, dragged, and deleted by the player.

let houseItems = [];
player object

Reference to the PlayerControlledCustomer object. The player character you control with keyboard and mouse.

let player;
inHouse boolean

Flag indicating whether the player is inside their house (true) or in the restaurant (false). Switches draw() between two modes.

let inHouse = false;
selectedCharacter object

Temporarily holds a reference to a character being dragged by the mouse. Set in mousePressed(), used in mouseDragged(), cleared in mouseReleased().

let selectedCharacter = null;
selectedHouseItem object

Temporarily holds a reference to a house item being dragged. Set in mousePressed(), updated in mouseDragged(), cleared in mouseReleased().

let selectedHouseItem = null;
kitchenPos object

Object storing kitchen position and dimensions: {x, y, width, height}. Used for rendering and collision detection.

const kitchenPos = { x: restaurantOffset + 100, y: 150, width: 150, height: 100 };
houseArea object

Object storing house area dimensions: {x, y, width, height}. Updated on window resize.

let houseArea = { x: 0, y: 0, width: restaurantOffset - 20, height: 0 };
lastCustomerArrival number

Tracks the frameCount of the last customer spawn, used to enforce customerArrivalInterval timing.

let lastCustomerArrival = 0;
restaurantOffset number

The x-coordinate that separates the house area (left) from the restaurant area (right). Set to 200.

const restaurantOffset = 200;

🔧 Potential Improvements (8)

Here are some ways this code could be enhanced:

PERFORMANCE draw() character update loop

The loop iterates backward but still checks instanceof and various conditions repeatedly for every character every frame—this is O(n) per frame

💡 Cache instanceof checks or separate arrays for different character types (customerArray, robberArray, policeArray) to avoid repeated instanceof calls

BUG Customer.avoidCollisions()

Collision avoidance applies force but doesn't prevent characters from overlapping if forces accumulate; they can get stuck in each other

💡 Use a proper separation vector and clamp minimum distance, or add a hard 'no-go zone' that teleports characters apart if they get too close

BUG Robber.update() STEALING state

If multiple robbers target the same food and one steals it first, the second robber's targetFood reference becomes stale (pointing to eaten food) and they'll chase a ghost

💡 Add a check each frame: `if (this.targetFood.isEaten()) { this.targetFood = null; this.state = 'SEEKING_LOOT'; }`

STYLE Global variables setup

Many configuration constants (numInitialAICustomers, maxAICustomers, etc.) are scattered throughout the file—hard to tweak without editing code

💡 Create a config object at the top: `const CONFIG = { numInitialAICustomers: 19, maxAICustomers: 25, ... }` and reference CONFIG.numInitialAICustomers everywhere

FEATURE draw() main loop

When player is in house, AI characters pause but their animation/visuals stop too—creates a jarring freeze effect

💡 Continue playing idle animations (mouth chewing, wandering in place) even when paused, so they feel 'alive' in the background

BUG PlayerControlledCustomer.update() click-to-move

If you click a target and then press a movement key before arriving, the speech bubble shows 'Zoom' but doesn't update if you click again—speech state confusion

💡 Clear targetX/targetY and update speech immediately when any movement key is pressed

PERFORMANCE draw() food item cleanup

Every frame, foodItems loops backward checking isEaten()—O(n) per frame. Kitchen food stays in the array invisibly forever.

💡 Add a 'lifespan' timer to kitchen food so it auto-removes after 60 seconds, or lazy-clean the array only when it exceeds a threshold size

STYLE mousePressed() and touchStarted()

Nearly identical code duplicated between mousePressed and touchStarted—violates DRY (Don't Repeat Yourself)

💡 Extract common logic into a shared `handleInput(x, y)` function and call it from both handlers

🔄 Code Flow

Code flow showing setup, draw, initializetables, drawhouseinterior, mousepressed, keypressed, keyreleased, customer, robber, policeofficer, playercontrolledcustomer, table, food, houseitem, drawinteractionprompt, windowresized

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

graph TD start[Start] --> setup[setup] setup --> canvasinit[canvas-init] setup --> playercustomization[player-customization] setup --> initializetables[initializetables] setup --> spawnai[spawn-ai] setup --> windowresized[windowresized] setup --> draw[draw loop] click setup href "#fn-setup" click canvasinit href "#sub-canvas-init" click playercustomization href "#sub-player-customization" click initializetables href "#fn-initializetables" click spawnai href "#sub-spawn-ai" click windowresized href "#fn-windowresized" draw --> restaurantbackground[restaurant-background] draw --> houseinteriorbranch[house-interior-branch] draw --> jailrendering[jail-rendering] draw --> characterupdateloop[character-update-loop] draw --> foodcleanup[food-cleanup] draw --> spawnrespawnlogic[spawn-respawn-logic] draw --> houseprompt[house-prompt] draw --> tablegrid[table-grid] draw --> couchrug[couch-rug] draw --> houseitemsloop[house-items-loop] draw --> exitdoor[exit-door] draw --> exitprompt[exit-prompt] click draw href "#fn-draw" click restaurantbackground href "#sub-restaurant-background" click houseinteriorbranch href "#sub-house-interior-branch" click jailrendering href "#sub-jail-rendering" click characterupdateloop href "#sub-character-update-loop" click foodcleanup href "#sub-food-cleanup" click spawnrespawnlogic href "#sub-spawn-respawn-logic" click houseprompt href "#sub-house-prompt" click tablegrid href "#sub-table-grid" click couchrug href "#sub-couch-rug" click houseitemsloop href "#sub-house-items-loop" click exitdoor href "#sub-exit-door" click exitprompt href "#sub-exit-prompt" characterupdateloop --> customerstate[state-machine] characterupdateloop --> dragcharacterloop[drag-character-loop] characterupdateloop --> collisionavoidance[collision-avoidance] click customerstate href "#sub-state-machine" click dragcharacterloop href "#sub-drag-character-loop" click collisionavoidance href "#sub-collision-avoidance" customerstate --> seekingtableloop[seeking-table-loop] customerstate --> wanderlogic[wander-logic] customerstate --> distancemovement[distance-movement] customerstate --> eatinganimation[eating-animation] click seekingtableloop href "#sub-seeking-table-loop" click wanderlogic href "#sub-wander-logic" click distancemovement href "#sub-distance-movement" click eatinganimation href "#sub-eating-animation" spawnrespawnlogic --> keyboardmovement[keyboard-movement] spawnrespawnlogic --> clicktomove[click-to-move] click keyboardmovement href "#sub-keyboard-movement" click clicktomove href "#sub-click-to-move" houseinteriorbranch --> drawhouseinterior[drawhouseinterior] houseinteriorbranch --> exitprompt[exit-prompt] exitprompt --> houseexitcheck[house-exit-check] exitprompt --> additemcheck[add-item-check] click drawhouseinterior href "#fn-drawhouseinterior" click houseexitcheck href "#sub-house-exit-check" click additemcheck href "#sub-add-item-check" houseitemsloop --> pickupitemloop[pickup-item-loop] click pickupitemloop href "#sub-pickup-item-loop" click tablegrid href "#sub-table-grid" tablegrid --> chairpositioning[chair-positioning] chairpositioning --> displayloop[display-loop] click chairpositioning href "#sub-chair-positioning" click displayloop href "#sub-display-loop" jailrendering --> jailstate[jail-state] click jailstate href "#sub-jail-state" patrollingstate[patrolling-state] --> pursuingstate[pursuing-state] pursuingstate --> apprehendingstate[apprehending-state] click patrollingstate href "#sub-patrolling-state" click pursuingstate href "#sub-pursuing-state" click apprehendingstate href "#sub-apprehending-state"

❓ Frequently Asked Questions

What kind of visual experience does the Good (Remix) sketch provide?

The Good (Remix) sketch creates a vibrant and interactive emoji restaurant filled with animated characters, customizable player avatars, and a personal house for decoration.

How can users interact with the Good (Remix) sketch?

Users can customize their character, navigate the restaurant using clicks or keyboard inputs, enter and exit their house, and decorate it with items by dragging and dropping.

What creative coding techniques are showcased in the Good (Remix) sketch?

The sketch demonstrates player-controlled character mechanics, AI behavior for customers and robbers, and dynamic house decoration features, all within a playful simulation environment.

Preview

Good (Remix) - p5.js creative coding sketch preview
Sketch Preview
Code flow diagram showing the structure of Good (Remix) - Code flow showing setup, draw, initializetables, drawhouseinterior, mousepressed, keypressed, keyreleased, customer, robber, policeofficer, playercontrolledcustomer, table, food, houseitem, drawinteractionprompt, windowresized
Code Flow Diagram