Good

This sketch creates an interactive emoji restaurant simulation where customers arrive, find tables, eat, and pay. Special characters like robbers steal food while police officers chase them, and sailboat people enjoy meals—all controlled by clicking or touching tables to spawn food.

🧪 Try This!

Experiment with the code by making these changes:

  1. Speed up customer eating — Lower the eating duration so customers finish faster and tables turn over quicker, creating a faster-paced restaurant
  2. Make police officers way faster
  3. Double the number of starting customers — More customers on the first frame means the restaurant is immediately busy and chaotic
  4. Change the kitchen color — Make the kitchen visually distinct by changing its fill color from gray to something vibrant
  5. Add more robbers — Increase chaos and crime by spawning up to 3 robbers instead of just 1
  6. Make robbers more slippery — Reduce the police officer's detection radius so robbers have more freedom to steal before getting caught
Prefer the full editor? Open it there →

📖 About This Sketch

This sketch builds a lively emoji restaurant where dozens of animated characters navigate tables, eat food, and interact with each other. Robbers sneak in to steal dishes while police officers patrol to catch them, and regular customers line up for seats. You control the action by clicking tables to place food items. The simulation uses state machines, object-oriented programming with class inheritance, arrays of objects, and collision avoidance—fundamental techniques in interactive animation and game development.

The code is organized around seven main classes: Customer (the base class for all people), Robber, PoliceOfficer, BoatPerson, Food, and Table. The draw() loop updates every entity each frame, manages spawning of new characters, and handles removal of those who leave. By studying it, you'll learn how to build complex simulations with multiple interacting agents, manage state transitions, and structure inheritance hierarchies where specialized classes extend a general one.

⚙️ How It Works

  1. When the sketch loads, setup() creates a 400×300 canvas, initializes four tables positioned around the restaurant, and spawns 20 initial customers plus one robber, one police officer, and one boat person—all starting off-screen to the left.
  2. Every frame, draw() clears the background and redraws all tables, then loops through the customers array to update and display each person. Each person has a state (ARRIVING, SEEKING_TABLE, EATING, PAYING, etc.) that controls their behavior.
  3. Regular customers move rightward until they find an empty chair, walk to it, eat for a random duration while opening and closing their mouths, then pay and disappear. Robbers enter the restaurant, hunt for food on occupied tables or in the kitchen, grab it, and flee. Police officers patrol and chase robbers, apprehending them and sending them to jail for 6 seconds.
  4. Clicking or touching a table spawns a random food emoji at that table's center; clicking elsewhere spawns food in the kitchen. Customers eat food placed on their table, and their state machine progresses through eating and paying phases.
  5. New customers, robbers, police officers, and boat people continuously spawn off-screen as long as the current count of each type stays below their maximum limits. Characters use collision avoidance to navigate around each other and avoid table obstacles.
  6. Speech bubbles appear above characters based on their state, giving personality and narrative to the simulation. When robbers or police officers reach the screen's right edge, they're removed from the customers array.

🎓 Concepts You'll Learn

State machinesObject-oriented programming with class inheritanceCollision detection and avoidanceAnimation loopsArray management and filteringDistance-based interactionConditional state transitions

📝 Code Breakdown

setup()

setup() runs once when the sketch first loads. It initializes all the data structures (arrays for customers, food, tables) and places them in their starting positions. The use of nested loops shows how to spawn multiple objects of different types into a shared customers array—a pattern used in games and simulations everywhere.

function setup() {
  createCanvas(windowWidth, windowHeight);
  textFont('Arial');
  textAlign(CENTER, CENTER);
  rectMode(CENTER);

  tables.push(new Table(width / 3, height / 3, 2));
  tables.push(new Table(2 * width / 3, height / 3, 2));
  tables.push(new Table(width / 3, 2 * height / 3, 2));
  tables.push(new Table(2 * width / 3, 2 * height / 3, 2));

  for (let i = 0; i < numInitialCustomers; 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 (6 lines)

🔧 Subcomponents:

canvas-initialization Canvas and text setup createCanvas(windowWidth, windowHeight);

Creates a full-window canvas and configures text alignment and rectangle drawing mode for the entire sketch

for-loop Create four restaurant tables tables.push(new Table(width / 3, height / 3, 2));

Positions tables in a 2×2 grid layout, each with 2 chairs, forming the core restaurant structure

for-loop Spawn initial customers for (let i = 0; i < numInitialCustomers; i++) { customers.push(new Customer(random(-100, -50), random(height))); }

Creates 20 Customer objects starting off-screen to the left, ready to enter the restaurant

for-loop Spawn initial robbers for (let i = 0; i < numRobbers; i++) { customers.push(new Robber(random(-100, -50), random(height))); }

Creates 1 Robber object, stored in the customers array alongside regular customers

for-loop Spawn initial police officers for (let i = 0; i < numPoliceOfficers; i++) { customers.push(new PoliceOfficer(random(-100, -50), random(height))); }

Creates 1 PoliceOfficer object, ready to patrol the restaurant and catch robbers

for-loop Spawn initial boat people for (let i = 0; i < numBoatPeople; i++) { customers.push(new BoatPerson(random(-100, -50), random(height))); }

Creates 1 BoatPerson object with unique appearance and behavior, behaving like a regular customer

createCanvas(windowWidth, windowHeight);
Creates a canvas that fills the entire browser window, allowing the restaurant to scale to any screen size
textFont('Arial');
Sets the font for all text rendered in the sketch; Arial is used because it displays emojis clearly
textAlign(CENTER, CENTER);
Anchors all text at its center point rather than the default top-left; this makes positioning text and emojis intuitive
rectMode(CENTER);
Changes rectangle drawing so that rect(x, y, w, h) treats (x, y) as the center, not the top-left—critical for tables and speech bubbles
tables.push(new Table(width / 3, height / 3, 2));
Creates a Table object at one-third of the canvas width and height; all four tables use similar positioning at different quadrants
customers.push(new Customer(random(-100, -50), random(height)));
Creates a Customer at a random y-position off-screen to the left (x between -100 and -50), so they enter the restaurant smoothly

draw()

draw() is the heartbeat of the sketch—it runs 60 times per second. Every frame, it updates all entities, checks spawning conditions, removes those who've left, and redraws everything. The key lesson here is managing dynamic arrays (adding and removing items) while iterating, which is why the customer update loop goes backwards. The spawn logic shows how to maintain steady populations of different character types using filter() and conditional checks on frameCount.

🔬 This block spawns new regular customers. What happens if you change the customerArrivalInterval comparison from > to <? Or remove the currentCustomerCount check entirely?

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

🔬 This spawns new robbers whenever the count drops below the maximum. What happens if you change numRobbers to 3 here instead? Can the police officer catch them all?

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

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

  const currentCustomerCount = customers.filter(p => p instanceof Customer && !(p instanceof Robber) && !(p instanceof PoliceOfficer) && !(p instanceof BoatPerson)).length;
  if (frameCount - lastCustomerArrival > customerArrivalInterval && currentCustomerCount < maxCustomers) {
    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);
    }
  }
}
Line-by-line explanation (16 lines)

🔧 Subcomponents:

canvas-rendering Draw restaurant background and kitchen background(240, 230, 200);

Clears the canvas with a beige color every frame, erasing previous drawings to prevent trails

for-loop Display all tables for (let table of tables) { table.display(); }

Loops through the tables array and calls each table's display() method to draw it and its chairs

for-loop Update and display all people for (let i = customers.length - 1; i >= 0; i--) {

Iterates backwards through the customers array (robbers, police, boat people, and regular customers) to update state and render each one

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

Immediately removes customers who have paid and left the restaurant from the array

conditional Remove characters who exited the screen if ((person.state === 'LEAVING' || person.state === 'FLEEING') && person.x > width + 50) { customers.splice(i, 1); }

Removes police officers and robbers once they've moved far past the right edge of the canvas

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

Continuously spawns new regular customers at intervals, as long as the count stays below the maximum

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

Keeps one robber active in the simulation by spawning a replacement if the current one was jailed or escaped

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

Maintains one police officer in the restaurant by respawning if the current one leaves

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

Keeps one boat person in the restaurant by spawning a replacement after they leave

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

Renders all food items and removes eaten food from the array (unless it's in the kitchen)

background(240, 230, 200);
Fills the entire canvas with a light beige color (RGB: 240, 230, 200), clearing everything drawn in the previous frame and preventing motion trails
fill(180);
Sets the fill color to gray for the kitchen rectangle that follows
rect(kitchenPos.x + kitchenPos.width / 2, kitchenPos.y + kitchenPos.height / 2, kitchenPos.width, kitchenPos.height);
Draws the kitchen area as a gray rectangle; the position is adjusted because rectMode(CENTER) treats the first two arguments as the center point
for (let table of tables) {
Loops through each Table object in the tables array and calls its display() method
for (let i = customers.length - 1; i >= 0; i--) {
Loops backwards through the customers array (which holds Customer, Robber, PoliceOfficer, and BoatPerson objects)—going backwards is essential when splicing to avoid skipping elements
person.update();
Calls the update() method on the current person, which handles state changes, movement, and behavior logic specific to their type
if (person instanceof Customer && person.remove) {
Checks if the person is a Customer type and has the remove flag set to true (indicating they've finished paying and should leave immediately)
customers.splice(i, 1);
Removes one element from the customers array at index i, deleting that person from the simulation
person.display();
Draws the person (head, body, emoji, and speech bubble) at their current position
if ((person.state === 'LEAVING' || person.state === 'FLEEING') && person.x > width + 50) {
Removes a person if they've exited the right edge of the screen—true for police officers leaving after jailing robbers or robbers escaping
const currentCustomerCount = customers.filter(p => p instanceof Customer && !(p instanceof Robber) && !(p instanceof PoliceOfficer) && !(p instanceof BoatPerson)).length;
Counts only regular Customer objects by filtering out Robbers, PoliceOfficers, and BoatPeople (which are subclasses of Customer)
if (frameCount - lastCustomerArrival > customerArrivalInterval && currentCustomerCount < maxCustomers) {
Spawns a new customer only if enough frames have passed since the last arrival AND the current count is below the maximum
customers.push(new Customer(random(-100, -50), random(height)));
Creates a new Customer at a random y-position off-screen to the left, adding it to the customers array
lastCustomerArrival = frameCount;
Records the current frame number, so the arrival interval timer resets
food.display();
Draws the food emoji at its current position (if not eaten)
if (food.isEaten() && !food.inKitchen) {
Removes eaten food from the array, but only if it's not in the kitchen (kitchen food persists for customers to pick up)

windowResized()

windowResized() is a built-in p5.js function that p5.js calls automatically when the browser window is resized. It's essential for responsive sketches. Notice how it recalculates table positions using proportions (width / 3, height / 3) rather than fixed pixel values—this ensures the layout adapts to any screen size.

function windowResized() {
  resizeCanvas(windowWidth, windowHeight);
  tables = [];
  tables.push(new Table(width / 3, height / 3, 2));
  tables.push(new Table(2 * width / 3, height / 3, 2));
  tables.push(new Table(width / 3, 2 * height / 3, 2));
  tables.push(new Table(2 * width / 3, 2 * height / 3, 2));
}
Line-by-line explanation (3 lines)
resizeCanvas(windowWidth, windowHeight);
Resizes the canvas to match the new browser window dimensions whenever the user resizes their window
tables = [];
Clears the tables array, removing references to old tables so they can be garbage collected
tables.push(new Table(width / 3, height / 3, 2));
Recreates tables at proportional positions using the new canvas width and height, so they stay centered and balanced on any screen size

mousePressed()

mousePressed() is called by p5.js once each time the mouse button is clicked. It demonstrates rectangular collision detection (checking if a point is inside a rectangle) and how to spawn game objects in response to user input. The pattern of looping through objects, checking a condition, and breaking early is fundamental to interactive programming.

function mousePressed() {
  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) {
    foodItems.push(new Food(kitchenPos.x + random(kitchenPos.width), kitchenPos.y + random(kitchenPos.height), random(foodEmojis), true));
  }
}
Line-by-line explanation (11 lines)

🔧 Subcomponents:

conditional Check if click is on a table 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) {

Tests whether the mouse click fell within the bounds of a centered rectangle (the table)

creation Spawn food on clicked table let food = new Food(table.x, table.y, random(foodEmojis), false); foodItems.push(food); table.foodOnTableCenter = food;

Creates a food item at the table's center and links it to that table so customers can eat it

conditional Spawn food in kitchen if no table clicked if (!clickedOnTable) { foodItems.push(new Food(kitchenPos.x + random(kitchenPos.width), kitchenPos.y + random(kitchenPos.height), random(foodEmojis), true)); }

If the user clicks outside any table, spawns food in the kitchen area instead

let clickedOnTable = false;
Initializes a flag to track whether the click landed on any table
for (let table of tables) {
Loops through each table to check if the mouse click is inside any of them
if (mouseX >= table.x - table.width / 2 && mouseX <= table.x + table.width / 2 &&
Checks if the click's x-coordinate falls within the table's left and right bounds (accounting for CENTER rectMode)
mouseY >= table.y - table.height / 2 && mouseY <= table.y + table.height / 2) {
Checks if the click's y-coordinate falls within the table's top and bottom bounds
let food = new Food(table.x, table.y, random(foodEmojis), false);
Creates a new Food object at the table's center with a random emoji from the foodEmojis array; false means it's not in the kitchen
foodItems.push(food);
Adds the food to the foodItems array so it will be displayed and managed in draw()
table.foodOnTableCenter = food;
Links the food to the table, allowing customers sitting at that table to eat it
clickedOnTable = true;
Sets the flag to true so the kitchen fallback won't execute
break;
Exits the loop early since we've found the table and don't need to check the others
if (!clickedOnTable) {
If no table was clicked, spawn food in the kitchen as a fallback
foodItems.push(new Food(kitchenPos.x + random(kitchenPos.width), kitchenPos.y + random(kitchenPos.height), random(foodEmojis), true));
Creates a food item at a random position within the kitchen area and marks it as in-kitchen (true), so it won't be removed when eaten

touchStarted()

touchStarted() is p5.js's equivalent to mousePressed(), but for touch screen input (phones, tablets). It uses touchX and touchY instead of mouseX and mouseY. The logic is identical to mousePressed(), showing how to support both input methods with minimal code duplication. Returning false prevents default browser behaviors like scrolling.

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

  if (!touchedOnTable) {
    foodItems.push(new Food(kitchenPos.x + random(kitchenPos.width), kitchenPos.y + random(kitchenPos.height), random(foodEmojis), true));
  }
  return false;
}
Line-by-line explanation (5 lines)

🔧 Subcomponents:

conditional Check if touch is on a table if (touchX >= table.x - table.width / 2 && touchX <= table.x + table.width / 2 && touchY >= table.y - table.height / 2 && touchY <= table.y + table.height / 2) {

Tests whether a touch event falls within the bounds of a table using touchX and touchY

let touchedOnTable = false;
Initializes a flag to track whether the touch landed on any table
for (let table of tables) {
Loops through each table to check if the touch is inside any of them
if (touchX >= table.x - table.width / 2 && touchX <= table.x + table.width / 2 &&
Checks if the touch's x-coordinate falls within the table's left and right bounds (same logic as mousePressed)
touchY >= table.y - table.height / 2 && touchY <= table.y + table.height / 2) {
Checks if the touch's y-coordinate falls within the table's top and bottom bounds
return false;
Returns false to prevent the browser's default touch actions (scrolling, pinch-to-zoom), allowing only the game logic to respond

Customer class

The Customer class is the foundation of this simulation. It demonstrates a state machine (a pattern where an object transitions between discrete states), using a switch statement to handle different behaviors. The constructor initializes all the properties needed for a customer's lifecycle. The update() method is called every frame and handles state transitions and movement logic. The avoidCollisions() method shows how to detect and respond to proximity with other objects—a key pattern in interactive simulations. Notice how subclasses (Robber, PoliceOfficer, BoatPerson) inherit from Customer and override specific methods to create specialized behavior.

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

  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 if (this.state === 'EATING') {
      line(-this.size * 0.15, -this.size * 0.4, this.size * 0.15, -this.size * 0.4);
    } 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.speechTimer > 0) {
      this.speechTimer--;
    } else {
      this.speechText = "";
    }

    let previousState = this.state;

    switch (this.state) {
      case 'ARRIVING':
        this.x += this.speed;
        if (this.x >= 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, 0, 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 (11 lines)

🔧 Subcomponents:

method constructor(x, y) constructor(x, y) { ... }

Initializes a new Customer with position, size, color, state machine, and speech properties

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

Controls customer behavior based on current state; transitions happen when conditions are met

conditional Search for empty chair or wander let foundChair = false; for (let table of tables) { let chairIndex = table.findEmptyChair(); if (chairIndex !== -1) { ... foundChair = true; break; } } if (!foundChair) { this.wander(); }

Finds the first available chair; if none exist, the customer wanders and speaks about waiting

calculation Animate mouth opening while eating if (this.eatingDuration % 60 === 0) { this.mouthOpen = true; this.eatingTimer = 20; }

Every 60 frames (1 second), opens the customer's mouth briefly to simulate chewing

method avoidCollisions() avoidCollisions() { ... }

Repels customers away from other characters and tables to prevent overlapping

this.state = 'ARRIVING';
Sets the initial state to ARRIVING, meaning the customer is entering the restaurant from the left
this.eatingDuration = random(180, 360);
Randomizes how long each customer eats (3–6 seconds at 60 frames per second), adding variety
this.setSpeech(random(["I'm hungry!", "Table for one?", "Looking for a seat!"]));
Sets an initial speech bubble shown for 90 frames (1.5 seconds) when the customer spawns
case 'ARRIVING': this.x += this.speed;
In ARRIVING state, the customer moves right at their speed; once x >= 200, they transition to SEEKING_TABLE
let chairIndex = table.findEmptyChair();
Calls the table's findEmptyChair() method, which returns the index of an unoccupied chair or -1 if all are full
if (dist(this.x, this.y, this.targetChair.x, this.targetChair.y) < 5) {
Uses the dist() function to check if the customer has reached the chair (within 5 pixels); if so, snaps them to it and starts eating
this.eatingDuration--;
Decrements the eating timer each frame; when it reaches zero, the customer transitions to PAYING
if (this.eatingDuration % 60 === 0) {
The modulo operator (%) checks if eatingDuration is divisible by 60; every 60 frames, the customer opens their mouth
this.remove = true;
After paying, this flag is set to true, signaling draw() to remove the customer immediately from the array
let angle = atan2(dy, dx);
Calculates the angle from the customer to the target (chair or food) using arctangent; essential for directional movement
this.x += cos(angle) * this.speed;
Moves the customer toward the target by stepping along the calculated angle at their speed

Robber class (extends Customer)

The Robber class demonstrates inheritance—it extends Customer but overrides specific methods (constructor, setSpeech, display, update) to create entirely different behavior. The key pattern is super(), which calls the parent class's constructor and methods. Notice how the robber has its own state machine (ENTERING, SEEKING_LOOT, STEALING, FLEEING, IN_JAIL) that's completely separate from the customer's. The nested loop in SEEKING_LOOT shows a prioritization system: steal from occupied tables first (most dramatic), then table centers, then the kitchen. This teaches dynamic priority and search algorithms.

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!");
  }

  setSpeech(text) {
    if (this.state !== 'IN_JAIL') {
      super.setSpeech(text);
    }
  }

  display() {
    if (this.state === 'IN_JAIL') {
      return;
    }
    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':
        this.jailTimer--;
        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 >= 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();
    this.x = constrain(this.x, 0, 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 (8 lines)

🔧 Subcomponents:

method constructor(x, y) constructor(x, y) { super(x, y); this.color = color(50); ... }

Calls the parent Customer constructor then overrides color, speed, and initial state to create a darker, faster thief

nested-loops Search for food in three locations for (let table of tables) { for (let chair of table.chairs) { ... } if (table.foodOnTableCenter && !table.foodOnTableCenter.isEaten()) { ... } } for (let food of foodItems) { if (food.inKitchen && !food.isEaten()) { ... } }

Prioritizes stealing from occupied tables, then table centers, then the kitchen—creating dramatic chase scenarios

calculation Escape with food while jittering case 'FLEEING': this.x += this.speed * 2; this.y += random(-1, 1) * this.speed * 0.5;

Makes the robber sprint off-screen horizontally while wobbling vertically, creating comedic escape animation

super(x, y);
Calls the Customer constructor to initialize basic properties (position, size, color placeholder, speed placeholder)
this.color = color(50);
Overrides the random color with dark gray (50, 50, 50), making the robber visually distinct
this.state = 'ENTERING';
Sets the initial state to ENTERING (different from Customer's ARRIVING), triggering robber-specific behavior
if (this.state === 'IN_JAIL') { this.jailTimer--; if (this.jailTimer <= 0) { this.state = 'ENTERING';
When jailed, the robber counts down jailTimer each frame; when it reaches zero, they're released and re-enter the restaurant
for (let table of tables) { for (let chair of table.chairs) { if (chair.occupied && chair.food && !chair.food.isEaten()) {
Nested loops search all tables and all chairs at each table, looking for food at an occupied chair (the most dramatic theft scenario)
this.setSpeech("Aha! Found some loot!");
When the robber spots food, they announce their discovery with speech, adding personality
if (dist(this.x, this.y, this.targetFood.x, this.targetFood.y) < this.size / 2 + this.targetFood.size / 4) {
Uses distance calculation to detect collision with the target food item; the threshold accounts for both the robber's and food's sizes
this.y += random(-1, 1) * this.speed * 0.5;
Adds a random vertical wobble while fleeing (random value between -1 and 1, scaled down), creating a comedic escape

PoliceOfficer class (extends Customer)

The PoliceOfficer demonstrates a hunter-prey dynamic in a multi-agent simulation. The patrol-detect-pursue-apprehend cycle is a common AI pattern in games. Key concepts: instanceof checks to identify robbers specifically, distance-based detection radius, and state transitions that trigger different behaviors. The officer inherits from Customer but never eats or pays—they have a completely different state machine focused on law enforcement. Notice how the apprehending phase is a timed countdown, giving the player (or the robber AI) a chance to observe cause-and-effect before the robber is jailed.

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."]));
  }

  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);
            if (d < 150) {
              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 = 360;
            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, 0, 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 (6 lines)

🔧 Subcomponents:

nested-loop Detect nearby robbers while patrolling 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); if (d < 150) {

Loops through all customers and checks each Robber within 150 pixels, ignoring robbers that are already fleeing, entering, or jailed

calculation Chase robber toward target 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;

Calculates the angle to the target robber and moves toward them at maximum speed

conditional Jail the robber after catching them if (this.apprehendTimer <= 0) { this.state = 'LEAVING'; if (this.targetRobber) { this.targetRobber.state = 'IN_JAIL'; this.targetRobber.jailTimer = 360;

Transitions robber to IN_JAIL state with a 6-second (360 frame) jail timer, then the officer leaves

this.color = color(0, 0, 150);
Sets the officer's color to dark blue (RGB: 0, 0, 150), making them visually distinct from customers and robbers
this.speed = random(4.5, 6);
Officers move faster (4.5–6 pixels per frame) than regular customers, allowing them to catch robbers
if (person !== this && person instanceof Robber && person.state !== 'FLEEING' && person.state !== 'ENTERING' && person.state !== 'IN_JAIL') {
Filters the customers array for robbers that are actively in the restaurant (not fleeing, entering, or in jail)
if (d < 150) {
Only starts pursuing if the robber is within 150 pixels—a detection radius that prevents chasing robbers across the entire restaurant
if (dist(this.x, this.y, this.targetRobber.x, this.targetRobber.y) < this.size / 2 + this.targetRobber.size / 2) {
Uses distance to detect collision; when the officer reaches the robber, the apprehension state begins
this.targetRobber.jailTimer = 360;
Jails the robber for 360 frames (6 seconds at 60 fps), after which they're released to try stealing again

BoatPerson class (extends Customer)

BoatPerson is the simplest subclass—it inherits all behavior from Customer but overrides only the constructor and display() to customize appearance and initial speech. No custom update() method means boat people behave exactly like regular customers (seeking tables, eating, paying) but look different and speak differently. This demonstrates that inheritance doesn't require reimplementing everything—you can create new character types by changing only the properties you care about.

class BoatPerson extends Customer {
  constructor(x, y) {
    super(x, y);
    this.color = color(50, 150, 50);
    this.speed = random(1.2, 2.2);
    this.iconEmoji = "⛵";
    this.setSpeech(random(["Ahoy, matey!", "Table near the window, please!", "Smooth sailing today!"]));
  }

  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();
  }
}
Line-by-line explanation (5 lines)
super(x, y);
Calls the Customer constructor to initialize all base properties
this.color = color(50, 150, 50);
Sets a green color (RGB: 50, 150, 50) to give boat people a nautical, nature-themed appearance
this.speed = random(1.2, 2.2);
Slightly slower than regular customers (1.2–2.2 vs 1.5–2.5), giving them a leisurely seasoned sailor vibe
this.iconEmoji = "⛵";
Uses a sailboat emoji as the boat person's visual identifier, drawn above their head in display()
this.setSpeech(random(["Ahoy, matey!", "Table near the window, please!", "Smooth sailing today!"]));
Sets initial speech with maritime-themed phrases, adding flavor to the character

Food class

The Food class is minimal—it stores position, emoji, size, and eaten state. It's an example of a simple data container with just a few essential methods. The key design choice is the inKitchen flag: food on tables is removed when eaten, but food in the kitchen persists so multiple customers can theoretically eat from it. This teaches conditional behavior based on context.

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 (4 lines)
this.size = random(20, 36);
Randomizes the emoji size between 20 and 36 pixels, adding visual variety to food items
this.inKitchen = inKitchen;
Flags whether the food is in the kitchen (persists when eaten) or on a table (removed when eaten)
if (!this.eaten) {
Only draws the food if it hasn't been marked as eaten; once eaten, it becomes invisible
return this.eaten;
Returns the eaten state, allowing other objects to check if food is still available

Table class

The Table class demonstrates how to manage a collection of related objects (chairs) and provide utility methods (findEmptyChair) for other objects to query. Notice that chairs are plain JavaScript objects, not class instances—a design choice that keeps things simple. The ternary operator in chair positioning shows compact conditional logic. Tables also hold a reference to food placed on them, linking gameplay elements together. The visual feedback (red overlay on occupied chairs) teaches the player how to read the simulation state.

🔬 This loop places chairs: first on the left, then on the right of the table. What happens if you change the ternary operator so it always uses the right side? Or always the left? What if you add variation to chairY instead of keeping it fixed at this.y?

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

🔧 Subcomponents:

for-loop Create chairs at table sides for (let i = 0; i < numChairs; i++) { let chairX = this.x + (i === 0 ? -this.width / 2 - 20 : this.width / 2 + 20);

Positions the first chair on the left of the table and subsequent chairs on the right, creating a simple two-sided seating arrangement

conditional Show occupied chairs in red if (chair.occupied) { fill(255, 0, 0, 100); ellipse(chair.x, chair.y, 30, 30); }

Draws a red overlay on occupied chairs, providing visual feedback to the player about table capacity

loop Search for unoccupied chair findEmptyChair() { for (let i = 0; i < this.chairs.length; i++) { if (!this.chairs[i].occupied) { return i; } } return -1;

Returns the index of the first empty chair, or -1 if all chairs are full

this.width = 120;
Hard-codes the table width; all tables have the same size for consistency
let chairX = this.x + (i === 0 ? -this.width / 2 - 20 : this.width / 2 + 20);
Uses a ternary operator: if i === 0 (first chair), place it on the left side; otherwise, place it on the right side
this.chairs.push({ x: chairX, y: chairY, occupied: false, customer: null, food: null, table: this });
Creates a chair object (a plain JavaScript object, not a class) with all properties needed: position, occupation status, linked customer and food, and reference back to the table
fill(139, 69, 19);
Sets the fill to brown (139, 69, 19)—a classic wood color for tables
for (let chair of this.chairs) {
Loops through all chairs at this table and draws them
if (chair.occupied) {
If a chair is occupied, draws a semi-transparent red overlay on top to indicate a customer is sitting there
return -1;
Returns -1 as a sentinel value to indicate 'no chair found', allowing callers to check with !== -1

📦 Key Variables

customers array

Stores all Customer, Robber, PoliceOfficer, and BoatPerson objects in a single array, simplifying iteration and management

let customers = [];
foodItems array

Stores all Food objects currently in the restaurant (on tables and in the kitchen)

let foodItems = [];
tables array

Stores all Table objects, including their chairs and any food placed on them

let tables = [];
foodEmojis array

A list of 17 emoji strings (drinks, food, desserts, etc.) used to randomly generate varied food items

let foodEmojis = ["🥂", "🥤", "🍼", ...];
numInitialCustomers number

The count of regular customers created when setup() runs; set to 20 for a less overwhelming start

const numInitialCustomers = 20;
maxCustomers number

The maximum number of regular customers allowed in the restaurant at any time; caps crowding

const maxCustomers = 25;
customerArrivalInterval number

The number of frames between regular customer arrivals; 240 frames ≈ 4 seconds at 60 fps

const customerArrivalInterval = 240;
lastCustomerArrival number

Records the frameCount when the last customer arrived, used to determine when to spawn the next one

let lastCustomerArrival = 0;
kitchenPos object

Stores the position and size of the kitchen area {x, y, width, height}, used for spawning food and visual reference

const kitchenPos = { x: 100, y: 150, width: 150, height: 100 };
numRobbers number

The maximum number of robbers active in the restaurant; set to 1 to keep one stealing constantly

const numRobbers = 1;
numPoliceOfficers number

The maximum number of police officers patrolling; set to 1 to create a one-on-one pursuit dynamic

const numPoliceOfficers = 1;
numBoatPeople number

The maximum number of boat people in the restaurant; set to 1 for an occasional quirky visitor

const numBoatPeople = 1;

🔧 Potential Improvements (6)

Here are some ways this code could be enhanced:

PERFORMANCE draw() function, customer spawning logic

The filter() method is called multiple times per frame (once for regular customers, once for robbers, once for police, once for boat people), each scanning the entire customers array

💡 Cache the counts at the start of draw() or maintain separate arrays for each character type, reducing redundant array iterations

PERFORMANCE Robber.update() SEEKING_LOOT state

Double-nested loop (tables → chairs) runs every frame, repeatedly scanning all tables even when no food is targeted

💡 Add an early exit or cache the result of the first food location found, or reduce the search frequency using a timer

BUG PoliceOfficer.update() PURSUING state

If the robber is jailed or removed while the officer is pursuing, targetRobber becomes null but the officer doesn't immediately revert to patrolling until the next frame, potentially causing brief weird behavior

💡 Add a null-check before the distance calculation, or immediately transition to PATROLLING if targetRobber is null

FEATURE Customer class

Customers never split or interact meaningfully with each other beyond collision avoidance; the restaurant feels like isolated agents rather than a social space

💡 Add a 'conversing' state where nearby customers briefly speak to each other, or create a tip system that rewards watching food being delivered

STYLE Class constructors

Each constructor calls setSpeech() with random() and array brackets, which is scattered and not DRY (Don't Repeat Yourself)

💡 Create a helper method getRandomSpeech(array) and call it from constructors, centralizing the random selection logic

BUG Food.display() and foodItems array management

Food in the kitchen (inKitchen = true) persists forever even after being eaten and marked, cluttering the simulation over time

💡 Eventually remove kitchen food after it's been eaten for a while, or add a maximum lifetime to all food items to prevent accumulation

🔄 Code Flow

Code flow showing setup, draw, windowresized, mousepressed, touchstarted, customer, robber, policeofficer, boatperson, food, table

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

graph TD start[Start] --> setup[setup] setup --> draw[draw loop] draw --> canvas-setup[canvas-setup] draw --> background-draw[background-draw] draw --> table-loop[table-loop] draw --> customer-update-loop[customer-update-loop] draw --> food-display-loop[food-display-loop] draw --> removal-check[removal-check] draw --> offscreen-removal[offscreen-removal] draw --> customer-spawn-check[customer-spawn-check] draw --> robber-spawn-check[robber-spawn-check] draw --> police-spawn-check[police-spawn-check] draw --> boat-spawn-check[boat-spawn-check] setup --> table-initialization[table-initialization] setup --> customer-spawn-loop[customer-spawn-loop] setup --> robber-spawn-loop[robber-spawn-loop] setup --> police-spawn-loop[police-spawn-loop] setup --> boat-spawn-loop[boat-spawn-loop] canvas-setup -->|Creates full-window canvas| setup table-initialization -->|Creates 4 tables| setup customer-spawn-loop -->|Creates 20 Customers| setup robber-spawn-loop -->|Creates 1 Robber| setup police-spawn-loop -->|Creates 1 PoliceOfficer| setup boat-spawn-loop -->|Creates 1 BoatPerson| setup background-draw -->|Clears canvas| draw table-loop -->|Draws tables| draw customer-update-loop -->|Updates and displays customers| draw food-display-loop -->|Displays food items| draw customer-update-loop --> customer-loop[customer-loop] customer-loop --> customer-update[customer-update-loop] customer-update --> removal-check customer-update --> offscreen-removal customer-update --> customer-spawn-check customer-update --> robber-spawn-check customer-update --> police-spawn-check customer-update --> boat-spawn-check removal-check -->|Removes customers after paying| customer-update offscreen-removal -->|Removes characters offscreen| customer-update customer-spawn-check -->|Spawns new customers| customer-update robber-spawn-check -->|Respawns robbers| customer-update police-spawn-check -->|Respawns police officers| customer-update boat-spawn-check -->|Respawns boat people| customer-update food-display-loop --> food-loop[food-loop] food-loop -->|Displays and manages food items| food-display-loop click setup href "#fn-setup" click draw href "#fn-draw" click canvas-setup href "#sub-canvas-setup" click table-initialization href "#sub-table-initialization" click customer-spawn-loop href "#sub-customer-spawn-loop" click robber-spawn-loop href "#sub-robber-spawn-loop" click police-spawn-loop href "#sub-police-spawn-loop" click boat-spawn-loop href "#sub-boat-spawn-loop" click background-draw href "#sub-background-draw" click table-loop href "#sub-table-loop" click customer-update-loop href "#sub-customer-update-loop" click removal-check href "#sub-removal-check" click offscreen-removal href "#sub-offscreen-removal" click customer-spawn-check href "#sub-customer-spawn-check" click robber-spawn-check href "#sub-robber-spawn-check" click police-spawn-check href "#sub-police-spawn-check" click boat-spawn-check href "#sub-boat-spawn-check" click food-display-loop href "#sub-food-display-loop"

❓ Frequently Asked Questions

What visual elements can be seen in the p5.js sketch titled 'Good'?

The sketch creates a lively restaurant scene with customers, robbers, and police officers, represented by various food emojis and animated characters interacting within a defined space.

How can users interact with the 'Good' sketch created in p5.js?

Currently, the sketch does not include direct user interactions; it primarily showcases automated character movements and behaviors in the restaurant setting.

What creative coding concepts are illustrated in the 'Good' p5.js sketch?

The sketch demonstrates object-oriented programming by utilizing classes for different character types and managing dynamic arrays to handle multiple instances like customers and robbers.

Preview

Good - p5.js creative coding sketch preview
Sketch Preview
Code flow diagram showing the structure of Good - Code flow showing setup, draw, windowresized, mousepressed, touchstarted, customer, robber, policeofficer, boatperson, food, table
Code Flow Diagram