Good (Remix)

This sketch simulates a bustling emoji restaurant where you control your own customizable character with arrow keys or WASD. AI customers, robbers, police officers, and boat people populate the restaurant, interact with tables and food, while you can visit your personal house on the left side of the screen.

🧪 Try This!

Experiment with the code by making these changes:

  1. Make the player faster — Increase the player's movement speed by changing the speed range in PlayerControlledCustomer constructor—watch the player zoom across the screen
  2. Spawn more robbers and police — Increase the number of robbers and police officers to create more crime drama in the restaurant
  3. Make customers eat faster — Reduce the eating duration so customers finish their meals much quicker and leave—speeds up the restaurant turnover
  4. Darken the restaurant — Change the background color to create a moody, nighttime restaurant atmosphere
  5. Slower customer arrivals — Increase the time between customer arrivals so the restaurant is less crowded—higher numbers mean longer waits
Prefer the full editor? Open it there →

📖 About This Sketch

This sketch creates a lively emoji restaurant simulation where you play as a customizable character and interact with a world full of AI-driven agents. The scene features customers finding tables and eating, robbers stealing food, police officers chasing criminals, and boat people enjoying meals—all while you move around using keyboard controls or by dragging characters with your mouse. It showcases sophisticated p5.js techniques including ES6 class inheritance, state machine logic, collision detection and avoidance, speech bubbles, and responsive canvas resizing.

The code is organized into a setup() function that initializes your character and the restaurant, a draw() loop that updates and displays all entities 60 times per second, and seven ES6 classes that manage customers, robbers, police, boat people, the player character, food items, and tables. By studying it, you will learn how to structure complex simulations using object-oriented programming, manage state transitions for AI behavior, handle keyboard and mouse input for player control, and create emergent interactions between many independent agents.

⚙️ How It Works

  1. When the sketch loads, setup() prompts you to customize your character's color, size, and emoji icon, then initializes the restaurant with four tables, your player character at the house entrance, and spawns 19 AI customers, 1 robber, 1 police officer, and 1 boat person off-screen.
  2. Every frame, draw() checks whether you are in your house or the restaurant. If in the restaurant, it clears the background, draws the kitchen and tables, then updates and displays all characters (customers, robber, police, boat person, and you).
  3. Your player character responds to keyboard input—arrow keys or WASD—to move around. You can move to your house entrance to trigger an 'Enter House' prompt, click it to go inside, then move to the door inside to exit back to the restaurant.
  4. AI characters use a state machine: customers arrive, seek an empty chair, move to it, eat food with animated mouth-opening, pay, and leave. Robbers enter, hunt for food on tables or in the kitchen, steal it, and flee off-screen. Police officers patrol, detect robbers, pursue them, apprehend them, and send them to jail (which freezes them for 6 seconds before they re-enter).
  5. You can drag any character by clicking and holding them—they will display 'Whee! 🎢' and pause their AI. When you release them, they resume their previous behavior. Click on tables or empty canvas to spawn food items that customers and robbers can eat or steal.
  6. When you enter your house, all AI characters freeze in place and only update their speech bubbles, creating a pause mechanic. This allows you to explore your home in peace before returning to the chaos of the restaurant.

🎓 Concepts You'll Learn

ES6 class inheritanceState machine designMulti-agent simulationCollision detection and avoidancePathfinding and AI behaviorKeyboard and mouse inputSpeech bubbles and UIResponsive canvas resizing

📝 Code Breakdown

setup()

setup() runs once when the sketch starts. It initializes the canvas, collects player customization, and spawns all the initial characters into the world. The prompt() calls pause execution and wait for player input, making setup() interactive.

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

🔧 Subcomponents:

initialization Canvas and text configuration createCanvas(windowWidth, windowHeight);

Creates a full-screen canvas and sets up text alignment and drawing modes for the entire sketch

conditional Player customization dialogs let colorInput = prompt("Choose your character's color...");

Asks the player for their character's color, size, and emoji icon before spawning them into the world

for-loop Spawn AI characters for (let i = 0; i < numInitialAICustomers; i++) { customers.push(new Customer(...)); }

Creates the initial population of customers, robbers, police, and boat people off-screen to enter the restaurant

createCanvas(windowWidth, windowHeight);
Creates a canvas that fills the entire window, making the sketch responsive to screen size
textFont('Arial');
Sets the font to Arial, which works well with emoji characters and text rendering
textAlign(CENTER, CENTER);
Aligns all text to be centered horizontally and vertically, so coordinates represent the text's center point
rectMode(CENTER);
Makes all rectangles drawn with rect() use their first two arguments as the center point instead of the top-left corner
houseArea.height = windowHeight;
Sets the house area's height to match the full window height, making the house extend the full vertical span
let colorInput = prompt("Choose your character's color...");
Opens a browser dialog asking the player to enter a color name, hex code, or RGB values for their character
if (colorInput) playerCustomColor = colorInput;
If the player enters a color (doesn't cancel), updates the global playerCustomColor variable
player = new PlayerControlledCustomer(houseEntrance.x, houseEntrance.y, playerCustomColor, playerCustomSize, playerCustomIcon);
Creates the player's character at the house entrance using their customization choices, then stores a reference in the global player variable
customers.push(player);
Adds the player to the customers array so they are updated and displayed each frame along with all AI characters
for (let i = 0; i < numInitialAICustomers; i++) { customers.push(new Customer(...)); }
Loops 19 times, creating 19 AI customer objects and adding them to the customers array, all starting off-screen on the left

draw()

draw() runs 60 times per second and is the heartbeat of the sketch. This function handles the core game loop: clearing the canvas, checking game state (house vs. restaurant), updating all characters' positions and AI, and drawing everything. The backward loop through customers is a key pattern—it allows safe removal of items during iteration. The population management code demonstrates how to maintain consistent NPC counts by checking current counts against maximums.

🔬 This loop removes eaten food except items in the kitchen. What happens if you change the condition to remove ALL eaten food, even from the kitchen?

    for (let i = foodItems.length - 1; i >= 0; i--) {
      let food = foodItems[i];
      food.display();

      if (food.isEaten() && !food.inKitchen) {
        foodItems.splice(i, 1);
      }
    }
function draw() {
  if (inHouse) {
    background(100, 50, 0);
    drawHouseInterior();
    player.update();
    player.display();
  } else {
    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];
      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);
      }
    }

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

🔧 Subcomponents:

conditional House interior rendering if (inHouse) { ... } else { ... }

Checks whether the player is inside the house or in the restaurant and draws the appropriate scene

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

Iterates through all characters, updates their AI/state, and draws them; also removes characters who leave off-screen

conditional Spawn new AI characters if (frameCount - lastCustomerArrival > customerArrivalInterval && currentAICustomerCount < maxAICustomers)

Controls population limits and arrival timing—new customers arrive only if enough frames have passed and the max isn't exceeded

for-loop Display and remove eaten food for (let i = foodItems.length - 1; i >= 0; i--)

Draws all food items and removes those that have been eaten (unless they're in the kitchen, which persists)

conditional Show Enter House prompt if (dist(player.x, player.y, houseEntrance.x, houseEntrance.y) < houseEntrance.size / 2 + player.size / 2)

Checks if the player is close enough to the house entrance and draws the 'Enter House' prompt if they are

if (inHouse) {
Checks the global inHouse flag to decide whether to draw the house interior or the restaurant
background(100, 50, 0);
Clears the canvas with a brown wood-like color for the house interior
drawHouseInterior();
Calls a helper function to draw the house's door and UI
player.update();
Updates only the player's position based on keyboard input; AI characters are frozen inside the house
background(240, 230, 200);
Clears the canvas with a beige restaurant floor color
fill(180); rect(kitchenPos.x + kitchenPos.width / 2, kitchenPos.y + kitchenPos.height / 2, kitchenPos.width, kitchenPos.height);
Draws a gray rectangle representing the kitchen area at the specified position and size
for (let table of tables) { table.display(); }
Iterates through all table objects and calls their display() method to draw them and their chairs
for (let i = customers.length - 1; i >= 0; i--) {
Loops backward through the customers array (to safely remove items while iterating) and updates/displays each character
if (person !== player && inHouse) { ... continue; }
If the player enters the house, all non-player characters freeze—only their speech timer decrements and they redraw
person.update();
Calls the character's update() method to change position, state, and AI behavior
if (person instanceof Customer && person.remove) { customers.splice(i, 1); continue; }
If a customer has paid and is marked for removal, deletes them from the array immediately
if ((person.state === 'LEAVING' || person.state === 'FLEEING') && person.x > width + 50) { customers.splice(i, 1); }
Removes characters who have exited off the right edge of the screen to prevent memory buildup
const currentAICustomerCount = customers.filter(...).length;
Counts how many regular AI customers (excluding player, robbers, police, boat people) are currently in the restaurant
if (frameCount - lastCustomerArrival > customerArrivalInterval && currentAICustomerCount < maxAICustomers) {
Spawns a new customer if enough frames have passed since the last arrival AND there's room for more customers
food.display();
Draws each food item (emoji) at its position
if (food.isEaten() && !food.inKitchen) { foodItems.splice(i, 1); }
Removes eaten food from the array, except food in the kitchen (which remains for NPCs to retrieve)

initializeTables()

This helper function creates the restaurant's table layout. It uses formulas based on restaurantOffset (the house width) and the canvas dimensions to position tables responsively. When windowResized() is called, initializeTables() is invoked again to reposition tables for the new screen size. Changing the third argument (currently 2) controls how many chairs each table has.

🔬 This creates 4 tables in a 2x2 grid. What happens if you add a 5th table by copying one line and changing its position, for example to the center of the restaurant?

  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:

for-loop Create table grid tables.push(new Table(..., 2));

Spawns four tables in a 2x2 grid pattern, spacing them relative to the restaurant offset and canvas size

tables = [];
Clears the tables array so old tables don't persist if this function is called again (e.g., on window resize)
tables.push(new Table(restaurantOffset + (width - restaurantOffset) / 3, height / 3, 2));
Creates a table at the top-left position of the restaurant grid (1/3 across, 1/3 down) with 2 chairs; the positioning accounts for the house offset
tables.push(new Table(restaurantOffset + 2 * (width - restaurantOffset) / 3, height / 3, 2));
Creates a table at the top-right position (2/3 across, 1/3 down) with 2 chairs
tables.push(new Table(restaurantOffset + (width - restaurantOffset) / 3, 2 * height / 3, 2));
Creates a table at the bottom-left position (1/3 across, 2/3 down) with 2 chairs
tables.push(new Table(restaurantOffset + 2 * (width - restaurantOffset) / 3, 2 * height / 3, 2));
Creates a table at the bottom-right position (2/3 across, 2/3 down) with 2 chairs

drawHouseInterior()

This function draws the interior of the player's house when they are inside. It uses simple shapes (rect for the door, ellipse for the knob) and text to create a cozy scene. The exit prompt appears when the player is close enough to the door, mirroring the entry prompt mechanic in the restaurant.

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

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

🔧 Subcomponents:

calculation Draw exit door rect(width / 2, height / 2, 80, 120);

Draws a brown rectangle representing a door in the center of the house

conditional Show exit prompt if (dist(player.x, player.y, width / 2, height / 2) < 40 + player.size / 2)

Detects when the player is close to the door and shows the 'Exit House' prompt

fill(0); textSize(24); textAlign(CENTER, CENTER); text("Your Cozy Home!", width / 2, height / 4);
Draws black text saying 'Your Cozy Home!' centered at the top of the screen
fill(139, 69, 19); noStroke(); rect(width / 2, height / 2, 80, 120);
Draws a brown door (rectangle) centered in the middle of the screen
fill(0); ellipse(width / 2 + 30, height / 2, 10, 10);
Draws a small black circle on the right side of the door to represent a doorknob
if (dist(player.x, player.y, width / 2, height / 2) < 40 + player.size / 2)
Checks if the player is within 40 pixels plus half their size of the door's center
drawInteractionPrompt("Exit House", width / 2, height / 2 - 80);
If close to the door, displays a clickable 'Exit House' prompt above the door

drawInteractionPrompt()

This is a reusable helper function that displays clickable prompts throughout the game. It uses push() and pop() to isolate its drawing changes, ensuring that text size, alignment, and colors don't affect other parts of the sketch. The rounded corners (parameter 5) make the prompt box visually friendly.

function drawInteractionPrompt(text, x, y) {
  push();
  textSize(14);
  textAlign(CENTER, CENTER);
  fill(255);
  stroke(0);
  let w = textWidth(text) + 20;
  let h = 30;
  rect(x, y, w, h, 5);
  fill(0);
  noStroke();
  text(text, x, y);
  pop();
}
Line-by-line explanation (6 lines)

🔧 Subcomponents:

calculation Calculate and draw prompt box rect(x, y, w, h, 5);

Draws a white rounded rectangle that serves as the background for the interaction prompt

push();
Saves the current drawing settings (colors, transforms, stroke) so changes made in this function don't affect other drawings
let w = textWidth(text) + 20;
Calculates the width of the prompt box by measuring the text width and adding 20 pixels of padding
let h = 30;
Sets a fixed height of 30 pixels for the prompt box
rect(x, y, w, h, 5);
Draws a white rounded rectangle (the last parameter 5 creates rounded corners) centered at position (x, y)
text(text, x, y);
Draws the prompt text (e.g., 'Enter House' or 'Exit House') centered in the box
pop();
Restores the previously saved drawing settings, undoing all changes made within this function

mousePressed()

mousePressed() fires once every time the player clicks the mouse. This function handles multiple interactions: entering/exiting the house, dragging characters, clicking on tables to spawn food, and falling back to spawning food in the kitchen. The multiple nested conditionals and loops show a priority system—it checks house exits first, then entries, then character dragging, then table clicks. By returning false at the end, it prevents the browser from processing the click event further.

🔬 This code defines a clickable bounding box around each character. What happens if you change the size calculations to make the clickable area bigger, for example by doubling the size values like `person.size / 2` instead of `person.size / 4`?

    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) {
    if (dist(mouseX, mouseY, width / 2, height / 2) < 40) {
      inHouse = false;
      player.x = houseEntrance.x;
      player.y = houseEntrance.y;
      player.setSpeech("Back to work! 🏃");
      return false;
    }
  } 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.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 {
        foodItems.push(new Food(kitchenPos.x + random(kitchenPos.width), kitchenPos.y + random(kitchenPos.height), random(foodEmojis), true));
      }
    }
  }
  return false;
}
Line-by-line explanation (11 lines)

🔧 Subcomponents:

conditional Check for door click while in house if (inHouse) { if (dist(mouseX, mouseY, width / 2, height / 2) < 40)

Detects when the player clicks the door to exit the house

conditional Check for entry prompt click if (dist(mouseX, mouseY, houseEntrance.x, houseEntrance.y - 70) < (textWidth("Enter House") + 20) / 2 + 15)

Detects when the player clicks the 'Enter House' prompt to go inside

for-loop Find clicked character for dragging for (let person of customers) { if (mouseX >= minX && mouseX <= maxX ...) { selectedCharacter = person; ... } }

Iterates through characters to see if the click hit one, then sets it as the selected/dragged character

for-loop Check if click hit a table for (let table of tables) { if (mouseX >= table.x - table.width / 2 ...) { ... } }

Detects clicks on tables and spawns food at the table's center if clicked

conditional Spawn food in kitchen as fallback if (!clickedOnTable) { ... foodItems.push(new Food(...)) }

If no table was clicked, food is generated in the kitchen area

if (inHouse) {
Checks if the player is currently inside their house to handle house-specific interactions
if (dist(mouseX, mouseY, width / 2, height / 2) < 40) {
If in the house, checks if the click is within 40 pixels of the door in the center of the screen
inHouse = false;
Sets the flag to false, switching the game to restaurant mode
player.x = houseEntrance.x; player.y = houseEntrance.y;
Teleports the player back to the entrance of the house when exiting
if (dist(mouseX, mouseY, houseEntrance.x, houseEntrance.y - 70) < (textWidth("Enter House") + 20) / 2 + 15) {
If not in the house, checks if the click hit the 'Enter House' prompt box using distance and text width calculations
let minX = person.x - person.size / 4;
Calculates the left boundary of a character's clickable area based on their x position and size
if (mouseX >= minX && mouseX <= maxX && mouseY >= minY && mouseY <= maxY) {
Checks if the click falls within the rectangular bounding box of the character
selectedCharacter = person; selectedCharacter.state = 'DRAGGED';
Sets the clicked character as the selected/dragged character and changes their state to pause AI
for (let table of tables) { if (mouseX >= table.x - table.width / 2 && ...) {
Loops through tables and checks if the click is within each table's rectangular 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
if (!clickedOnTable) { ... foodItems.push(new Food(..., true)); }
If the click didn't hit a table, food is generated in the kitchen area instead (inKitchen = true)

mouseDragged()

mouseDragged() is called continuously while the mouse button is held down and the cursor moves. It's the key to the dragging mechanic—if a character was selected, their position updates to follow the mouse cursor every frame, creating the illusion of dragging. This works because mouseDragged runs many times per second, keeping the character synced with the mouse.

function mouseDragged() {
  if (selectedCharacter) {
    selectedCharacter.x = mouseX;
    selectedCharacter.y = mouseY;
    return false;
  }
  return false;
}
Line-by-line explanation (4 lines)
if (selectedCharacter) {
Checks if a character is currently selected (was clicked in mousePressed)
selectedCharacter.x = mouseX;
Sets the character's x position to follow the mouse's current x coordinate
selectedCharacter.y = mouseY;
Sets the character's y position to follow the mouse's current y coordinate
return false;
Prevents the browser from processing the drag event, ensuring smooth character movement without interference

mouseReleased()

mouseReleased() is called once when the mouse button is released after a drag. It handles cleanup: restoring the character's state, freeing chairs if a seated customer was dragged away, and clearing the speech bubble. The logic shows a nuanced design—if a customer was interrupted while eating, they automatically start seeking a new table instead of getting stuck.

function mouseReleased() {
  if (selectedCharacter) {
    selectedCharacter.state = selectedCharacter.originalState;

    if (selectedCharacter instanceof Customer && (selectedCharacter.originalState === 'EATING' || selectedCharacter.originalState === 'PAYING')) {
      if (selectedCharacter.targetChair) {
        selectedCharacter.targetChair.occupied = false;
        selectedCharacter.targetChair.customer = null;
        if (selectedCharacter.targetChair.food) {
          selectedCharacter.targetChair.food.setEaten();
          selectedCharacter.targetChair.food = null;
        }
        selectedCharacter.targetChair = null;
      }
      selectedCharacter.state = 'SEEKING_TABLE';
    }

    selectedCharacter.setSpeech("");
    selectedCharacter = null;
    return false;
  }
  return false;
}
Line-by-line explanation (6 lines)

🔧 Subcomponents:

conditional Restore character's previous state selectedCharacter.state = selectedCharacter.originalState;

Returns the character to whatever they were doing before being dragged

conditional Free chair if customer was dragged away if (selectedCharacter instanceof Customer && (selectedCharacter.originalState === 'EATING' || selectedCharacter.originalState === 'PAYING'))

If a customer being dragged was eating, frees their chair and resets their state to seek a new table

if (selectedCharacter) {
Checks if a character was selected and is now being released
selectedCharacter.state = selectedCharacter.originalState;
Restores the character to whatever state they were in before being dragged (e.g., 'SEEKING_TABLE', 'EATING', 'PATROLLING')
if (selectedCharacter instanceof Customer && (selectedCharacter.originalState === 'EATING' || selectedCharacter.originalState === 'PAYING')) {
Checks if the released character is a customer who was eating or paying when they were picked up
selectedCharacter.targetChair.occupied = false;
Marks the chair as no longer occupied so another customer can use it
selectedCharacter.setSpeech("");
Clears the 'Whee!' speech bubble so the character doesn't keep speaking after being released
selectedCharacter = null;
Clears the global selectedCharacter variable, ending the drag operation

keyPressed()

keyPressed() is called once every time a key is pressed. It uses a switch statement with multiple cases to handle both arrow keys and WASD, giving the player two control schemes. The movement flags (player.left, player.right, etc.) are set to true here, then checked in the player's update() method to actually move them. This design allows simultaneous keypresses—if you hold LEFT and UP, both flags are true and the player moves diagonally.

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.setSpeech("Zoom! 💨");
      return false;
    }
  }
}
Line-by-line explanation (7 lines)

🔧 Subcomponents:

switch-case Detect arrow and WASD keys switch (keyCode) { case LEFT_ARROW: case 65: ... }

Uses a switch statement to map arrow keys and WASD keys to movement direction flags

conditional Set movement speech if ([LEFT_ARROW, RIGHT_ARROW, UP_ARROW, DOWN_ARROW, 65, 68, 87, 83].includes(keyCode))

If a movement key was pressed, sets the player's speech to 'Zoom! 💨'

if (player && player.state !== 'DRAGGED') {
Checks that the player exists and is not currently being dragged before allowing keyboard input
switch (keyCode) {
Uses a switch statement to check which key was pressed based on its keyCode number
case LEFT_ARROW: case 65:
Both LEFT_ARROW and 65 (the 'A' key) map to the same action—moving left
player.left = true;
Sets the player's left flag to true, which the player's update() method checks to move the player leftward
if ([LEFT_ARROW, RIGHT_ARROW, UP_ARROW, DOWN_ARROW, 65, 68, 87, 83].includes(keyCode)) {
After the switch, checks if the pressed key was any movement key; if so, displays movement speech
player.setSpeech("Zoom! 💨");
Sets the player's speech bubble to show 'Zoom! 💨' whenever they start moving
return false;
Prevents the browser's default behavior for arrow keys (e.g., scrolling), ensuring smooth game control

keyReleased()

keyReleased() is called once when a key is released. It mirrors keyPressed() by setting movement flags back to false. The logic for clearing speech only happens when ALL movement keys are released, preserving the 'Zoom!' text while the player is moving in any direction. This creates a smooth interaction experience where the speech bubble only disappears once the player stops completely.

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.setSpeech("");
    }
    if ([LEFT_ARROW, RIGHT_ARROW, UP_ARROW, DOWN_ARROW, 65, 68, 87, 83].includes(keyCode)) {
      return false;
    }
  }
}
Line-by-line explanation (6 lines)

🔧 Subcomponents:

switch-case Set movement flags to false switch (keyCode) { case LEFT_ARROW: case 65: player.left = false; ... }

Resets direction flags when movement keys are released

conditional Clear speech when all movement stops if (!player.left && !player.right && !player.up && !player.down) { player.setSpeech(""); }

Removes the 'Zoom!' speech when the player is no longer moving

if (player && player.state !== 'DRAGGED') {
Checks that the player exists and is not being dragged before processing the key release
switch (keyCode) {
Uses the same switch logic as keyPressed() to map keys to movement directions
player.left = false;
Sets the left movement flag to false when the LEFT_ARROW or 'A' key is released
if (!player.left && !player.right && !player.up && !player.down) {
Checks if ALL movement flags are false, meaning the player has stopped moving entirely
player.setSpeech("");
Clears the speech bubble when movement stops, removing the 'Zoom!' text
if ([LEFT_ARROW, RIGHT_ARROW, UP_ARROW, DOWN_ARROW, 65, 68, 87, 83].includes(keyCode)) { return false; }
Prevents the browser's default behavior for movement keys

Customer()

The Customer class is the foundation for all human-like characters. It uses a state machine with states like 'ARRIVING', 'SEEKING_TABLE', 'EATING', and 'PAYING' to control behavior. The display() method draws the character with a rectangular body, circular head, eyes, and mouth that animates while eating. The update() method transitions between states and moves the character using trigonometry (atan2 for angles, cos/sin for movement direction). The avoidCollisions() method prevents characters from overlapping using distance checks and angle-based repulsion. Other classes like Robber and PoliceOfficer inherit from Customer and override specific methods.

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 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.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 (10 lines)

🔧 Subcomponents:

switch-case State machine for customer behavior switch (this.state) { case 'ARRIVING': ... case 'SEEKING_TABLE': ... }

Controls customer flow through states: arriving, seeking a table, moving to it, eating, paying, and leaving

conditional Animate mouth while eating if (this.eatingDuration % 60 === 0) { this.mouthOpen = true; }

Opens the customer's mouth every 60 frames while eating to create a chewing animation

calculation Avoid other characters and tables this.avoidCollisions();

Pushes the customer away from nearby characters and tables to prevent overlap and crowding

this.state = 'ARRIVING';
Initializes the customer's state to 'ARRIVING', meaning they are entering the restaurant
this.targetChair = null;
Stores a reference to the chair the customer is aiming for, initially null until they find one
this.setSpeech(random([...]))
Sets random initial speech to display when the customer appears
case 'ARRIVING': this.x += this.speed; if (this.x >= restaurantOffset + 200) { this.state = 'SEEKING_TABLE'; }
Moves the customer rightward from off-screen until they reach the restaurant area, then transitions to seeking a table
let chairIndex = table.findEmptyChair(); if (chairIndex !== -1) { ... }
Checks each table for an empty chair; if found, targets that chair and changes state to move toward it
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;
Calculates the direction to the target chair using atan2, then moves toward it using cos/sin, creating smooth diagonal movement
if (dist(this.x, this.y, this.targetChair.x, this.targetChair.y) < 5) {
Checks if the customer has reached the chair (within 5 pixels)
this.eatingDuration % 60 === 0
The modulo operator % checks if eatingDuration is divisible by 60; when true (every 60 frames), the mouth opens
this.wander();
Moves the customer randomly while they wait for a table to become available
this.avoidCollisions();
Applies forces to push the customer away from other characters and tables, preventing overlap

PlayerControlledCustomer()

PlayerControlledCustomer extends Customer, inheriting its state machine, display, and collision logic. However, it overrides the update() method to handle keyboard input instead of AI logic. The movement flag system allows smooth simultaneous input—when you hold UP and RIGHT, both flags are true and the player moves diagonally. The diagonal normalization prevents the player from moving faster when pressing multiple keys, maintaining consistent speed in all directions. The overridden avoidCollisions() method skips table avoidance while inside the house, allowing free movement in the home space.

🔬 This code normalizes diagonal movement so the player doesn't move faster diagonally. What happens if you comment out these lines so the player can move faster when pressing two keys at once?

    if (moveX !== 0 && moveY !== 0) {
      let mag = sqrt(moveX * moveX + moveY * moveY);
      moveX = (moveX / mag) * this.speed;
      moveY = (moveY / mag) * this.speed;
    }
class PlayerControlledCustomer extends Customer {
  constructor(x, y, customColor, customSize, customIcon) {
    super(x, y);
    this.color = color(customColor);
    this.size = customSize;
    this.speed = random(3, 5);
    this.state = 'PLAYER_CONTROLLED';
    this.iconEmoji = customIcon;
    this.left = false;
    this.right = false;
    this.up = false;
    this.down = false;
    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--;
    } else {
      if (!this.left && !this.right && !this.up && !this.down) {
        this.speechText = "";
      }
    }

    let moveX = 0;
    let moveY = 0;
    if (this.left) moveX -= this.speed;
    if (this.right) moveX += this.speed;
    if (this.up) moveY -= this.speed;
    if (this.down) moveY += this.speed;

    if (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) {
      this.x = constrain(this.x, houseArea.x, houseArea.x + houseArea.width);
      this.y = constrain(this.y, houseArea.y, houseArea.y + houseArea.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 (8 lines)

🔧 Subcomponents:

initialization Store player customization this.color = color(customColor); this.size = customSize; this.iconEmoji = customIcon;

Stores the player's chosen color, size, and emoji icon from setup()

initialization Initialize movement direction flags this.left = false; this.right = false; this.up = false; this.down = false;

Stores which direction keys are currently pressed; keyboard events toggle these flags

calculation Normalize diagonal movement if (moveX !== 0 && moveY !== 0) { let mag = sqrt(moveX * moveX + moveY * moveY); moveX = (moveX / mag) * this.speed;

Ensures diagonal movement doesn't move faster than horizontal/vertical movement by calculating magnitude and rescaling

conditional Different bounds inside and outside house if (inHouse) { ... } else { ... }

Constrains the player within the house when inside, or within the restaurant when outside

class PlayerControlledCustomer extends Customer {
Declares PlayerControlledCustomer as a subclass of Customer, inheriting all its methods and properties
super(x, y);
Calls the parent Customer class's constructor to initialize inherited properties
this.color = color(customColor);
Sets the player's color using the customColor parameter passed from setup()
this.left = false;
Initializes direction flags to false; keyPressed() sets them to true, keyReleased() sets them back to false
if (this.left) moveX -= this.speed;
If the left flag is true (LEFT_ARROW held), decreases moveX to move the player leftward
if (moveX !== 0 && moveY !== 0) { let mag = sqrt(moveX * moveX + moveY * moveY); moveX = (moveX / mag) * this.speed;
When moving diagonally, calculates the magnitude (length) of the movement vector and rescales it to prevent faster diagonal movement
if (inHouse) { this.x = constrain(this.x, houseArea.x, houseArea.x + houseArea.width); } else { this.x = constrain(this.x, houseArea.x + houseArea.width - 10, width);
Constrains the player to different boundaries depending on whether they are in the house or restaurant
if (!inHouse) { for (let table of tables) {
Only checks collision with tables when in the restaurant; inside the house, the player can move freely without table obstacles

Robber()

Robber is an antagonist class that inherits from Customer but behaves very differently. Instead of seeking tables and eating, robbers hunt food with a three-tier priority system and steal it. When caught by a police officer, they transition to an 'IN_JAIL' state where they become invisible and their jailTimer counts down. After release, they re-enter the restaurant and can be caught again. The override of setSpeech() prevents jailed robbers from speaking, adding to their silent imprisonment atmosphere. The FLEEING state with 2x speed and vertical jitter creates visual panic as they escape off-screen.

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) {
    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 >= 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();

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

🔧 Subcomponents:

conditional In-jail logic and timer case 'IN_JAIL': this.jailTimer--; if (this.jailTimer <= 0) { this.state = 'ENTERING'; ... }

Counts down jail time, then releases the robber back to the restaurant with a new entry

for-loop Three-tier food targeting hierarchy // First check occupied chairs, then table centers, then kitchen food

Robbers prioritize food on occupied chairs (more dramatic), then table centers, then kitchen food

calculation Fast escape with jitter case 'FLEEING': this.x += this.speed * 2; this.y += random(-1, 1) * this.speed * 0.5;

Robber moves off-screen at 2x speed with random vertical wobble, creating a panicked escape

class Robber extends Customer {
Robber inherits from Customer, gaining all customer behaviors but overriding key methods
this.state = 'ENTERING';
Robber starts with a different initial state than customers—they enter the scene as intruders
this.maskEmoji = "🥷";
Stores a ninja emoji that will be drawn over the robber's face to make them visually distinct
this.jailTimer = 0;
A timer that counts down when the robber is in jail; when it reaches 0, they escape
case 'IN_JAIL': this.jailTimer--; if (this.jailTimer <= 0) { ... this.state = 'ENTERING'; }
When jailed, the robber decrements their timer each frame. When it expires, they reset to off-screen and re-enter
if (this.state === 'IN_JAIL') { return; }
The display() method exits early if the robber is in jail, making them invisible while imprisoned
for (let table of tables) { for (let chair of table.chairs) { if (chair.occupied && chair.food ...) {
Robbers first look for food on occupied chairs (most dramatic steal), then scan table centers, then the kitchen
this.targetFood.setEaten(); if (this.targetChair && this.targetChair.food === this.targetFood) { this.targetChair.food = null; }
When the robber takes food, it marks the food as eaten and removes its reference from the chair/table
this.x += this.speed * 2; this.y += random(-1, 1) * this.speed * 0.5;
In FLEEING state, the robber moves off-screen at 2x speed and adds random vertical jitter for a panicked look

PoliceOfficer()

PoliceOfficer is a law enforcement class that hunts and apprehends robbers. Unlike customers, they have no interest in tables or food. Their state machine focuses on PATROLLING (wandering while watching), PURSUING (chasing a spotted robber), APPREHENDING (holding them in place for a brief animation), and LEAVING (escaping off-screen after making an arrest). The detection system uses distance checks to spot robbers within 150 pixels. Once caught, the robber is sentenced to 360 frames in jail. The class demonstrates how inheritance allows very different behavior from the same parent class.

🔬 The officer moves at `this.speed` toward the robber using trigonometry. What happens if you change `this.speed` to `this.speed * 0.8` to make the pursuit slower and give robbers a better chance of escape?

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

🔧 Subcomponents:

for-loop Scan for nearby robbers for (let person of customers) { if (person instanceof Robber && person.state !== 'FLEEING' ...) { if (d < 150) { ... } } }

Continuously checks if a non-fleeing robber is within 150 pixels and starts pursuit if found

calculation Chase and apprehend robber let angle = atan2(dy, dx); this.x += cos(angle) * this.speed; this.y += sin(angle) * this.speed;

Uses trigonometry to calculate the direction to the robber and moves toward them at high speed

conditional Send robber to jail this.targetRobber.state = 'IN_JAIL'; this.targetRobber.jailTimer = 360;

Transitions the caught robber to jail state and sets a 6-second timer before they can escape

class PoliceOfficer extends Customer {
Police officer inherits from Customer but uses a different state machine focused on law enforcement
this.color = color(0, 0, 150);
Sets the officer to dark blue, making them visually distinct from customers and robbers
this.speed = random(4.5, 6);
Police officers are faster than customers but potentially slower than fleeing robbers, making chases competitive
this.state = 'PATROLLING';
Police start by patrolling (wandering) instead of arriving or seeking tables
if (person instanceof Robber && person.state !== 'FLEEING' && person.state !== 'ENTERING' && person.state !== 'IN_JAIL')
Checks if the person is a Robber in a stealable/catchable state (not already fleeing, entering, or jailed)
if (d < 150) {
If the robber is within 150 pixels, starts pursuit—this is the detection range
if (dist(this.x, this.y, this.targetRobber.x, this.targetRobber.y) < this.size / 2 + this.targetRobber.size / 2)
Checks if the officer has touched the robber by comparing their size-based collision circles
this.apprehendTimer = 120;
Sets a 120-frame (2-second) timer for the apprehension animation where officer and robber stand still
this.targetRobber.state = 'IN_JAIL'; this.targetRobber.jailTimer = 360;
Transitions the robber to jail and gives them a 360-frame (6-second) timer before they can re-enter

BoatPerson()

BoatPerson is the simplest character subclass—it inherits all customer behavior (arriving, seeking tables, eating, paying) but with a unique visual theme: green color, slower speed, sailboat emoji, and nautical speech. It demonstrates how little code is needed to create distinct character varieties in an object-oriented system. Boat people behave identically to regular customers but feel thematically different. You could easily create more subclasses (Astronaut, Pirate, Zombie, etc.) using the same pattern.

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!"]));
    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();
  }
}
Line-by-line explanation (5 lines)
class BoatPerson extends Customer {
BoatPerson is a minimal subclass of Customer with only a different color, speed, and emoji
this.color = color(50, 150, 50);
Sets boat people to green, making them visually distinct while keeping the same customer behavior
this.speed = random(1.2, 2.2);
Boat people move slower than regular customers, perhaps because they're more relaxed
this.iconEmoji = "⛵";
Displays a sailboat emoji above the boat person's head
this.setSpeech(random(["Ahoy, matey!", "Table near the window, please!", "Smooth sailing today!"])):
Boat people have nautical speech options that set the tone for their theme

Food()

Food is a simple data class that stores position, emoji type, size, and eaten status. It doesn't move or have AI—it's just a visual target for customers and robbers. The inKitchen flag is important: food spawned in the kitchen persists even after being eaten (so NPCs can find more), while food on tables is removed from the array once consumed. This design choice means the kitchen is always stocked, but individual servings disappear.

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 (5 lines)
this.emoji = emoji;
Stores the food's emoji (randomly chosen from foodEmojis array when created)
this.size = random(20, 36);
Gives each food item a slightly different size, making the restaurant feel more varied
this.eaten = false;
Tracks whether the food has been consumed; when true, the food is removed or hidden
this.inKitchen = inKitchen;
A flag indicating whether the food is in the kitchen (and thus persists longer) or on a table (and is removed when eaten)
if (!this.eaten) { textSize(this.size); text(this.emoji, this.x, this.y); }
Only displays the food emoji if it hasn't been eaten; eaten food is immediately hidden

Table()

Table is a container class that manages a dining surface and the chairs around it. Each table is created with a position and a number of chairs. The constructor places chairs on opposite sides of the table using a simple ternary operator. The display() method draws the brown table and gray chairs, with red overlays on occupied seats so players can see at a glance which seats are available. The findEmptyChair() method is called by customers searching for a place to sit, returning the index of the first available chair or -1 if full. This class encapsulates all the table logic, making it easy to add more tables to the restaurant.

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

🔧 Subcomponents:

for-loop Create chairs around table for (let i = 0; i < numChairs; i++) { ... this.chairs.push({ ... }); }

Spawns chairs on opposite sides of the table (left and right) based on the numChairs parameter

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

Draws a red transparent overlay on occupied chairs so players can see which seats are taken

this.width = 120; this.height = 80;
Defines the table's fixed dimensions: 120 pixels wide and 80 pixels tall
let chairX = this.x + (i === 0 ? -this.width / 2 - 20 : this.width / 2 + 20);
Places the first chair on the left side of the table (negative x offset) and subsequent chairs on the right (positive x offset)
this.chairs.push({ x: chairX, y: chairY, occupied: false, customer: null, food: null, table: this });
Creates a chair object with position, occupation status, and references to any customer or food at that seat
fill(139, 69, 19); rect(this.x, this.y, this.width, this.height, 5);
Draws the table as a brown rounded rectangle
if (chair.occupied) { fill(255, 0, 0, 100); ellipse(chair.x, chair.y, 30, 30); }
Draws a semi-transparent red circle on top of occupied chairs to provide visual feedback
for (let i = 0; i < this.chairs.length; i++) { if (!this.chairs[i].occupied) { return i; } }
Searches through chairs and returns the index of the first unoccupied one; returns -1 if all are taken

📦 Key Variables

customers array

Stores all character objects (Customer, Robber, PoliceOfficer, BoatPerson, PlayerControlledCustomer) that populate the restaurant and house

let customers = [];
foodItems array

Stores all Food objects currently in play on the canvas, including both kitchen food and table servings

let foodItems = [];
tables array

Stores all Table objects that define dining areas with chairs in the restaurant

let tables = [];
inHouse boolean

Flag indicating whether the player is currently inside their house (true) or in the restaurant (false); when true, AI characters freeze

let inHouse = false;
player object

Reference to the PlayerControlledCustomer instance that the user controls with keyboard input

let player;
selectedCharacter object

Stores the character currently being dragged by the mouse; null when no character is selected

let selectedCharacter = null;
playerCustomColor string

The user's chosen color for their player character, entered via prompt in setup()

let playerCustomColor = "#FF00FF";
playerCustomSize number

The user's chosen size for their player character, entered via prompt in setup()

let playerCustomSize = 40;
playerCustomIcon string

The user's chosen emoji icon for their player character, entered via prompt in setup()

let playerCustomIcon = "👑";
foodEmojis array

Array of food emoji strings used to randomly generate food items throughout the sketch

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

The width of the house area on the left side of the screen; the restaurant begins at this x-coordinate

const restaurantOffset = 200;
houseArea object

An object defining the bounds of the house: x, y, width, and height for boundary checking inside the house

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

Defines the location and clickable area of the house entrance prompt where the player enters/exits

let houseEntrance = { x: restaurantOffset - 10, y: 0, size: 50 };

🔧 Potential Improvements (5)

Here are some ways this code could be enhanced:

BUG draw() customer loop

When the player enters the house, AI characters' speech timers continue to decrement in the continue block, but they never update their speechText or stop speaking

💡 Add logic to automatically clear speechText when the timer runs out for frozen characters, or consider pausing the speech timer entirely when the player is in the house

PERFORMANCE avoidCollisions() method

The collision avoidance loop runs for every character every frame, checking distances to all other characters and all tables; with many characters, this becomes O(n²) and slows the sketch

💡 Implement spatial partitioning (divide the canvas into grid cells) to only check nearby characters, or add a frequency check so collision avoidance runs every Nth frame instead of every frame

STYLE mousePressed() and touchStarted()

These two functions have nearly identical code (copy-paste duplication) for checking house interactions, character dragging, table clicks, and food spawning

💡 Extract the shared logic into a helper function like handleInteractionInput(x, y) that both mousePressed and touchStarted call, reducing code duplication and maintenance burden

FEATURE Food class

Food items lack animation or visual feedback when being eaten—they simply disappear from the foodItems array

💡 Add an 'eatAnimation' property and animate the food shrinking or moving toward the character's mouth before disappearing, or add particle effects for more visual polish

BUG PoliceOfficer.update() APPREHENDING state

When apprehending a robber, the police officer stands still but can still be dragged by the player, potentially interrupting the apprehension animation and leaving the robber in a corrupted state

💡 Skip apprehension logic if the officer is in DRAGGED state, similar to the update() guard clause, to prevent broken state transitions

🔄 Code Flow

Code flow showing setup, draw, initializetables, drawHouseInterior, drawinteractionprompt, mousepressed, mousedragged, mousereleased, keypressed, keyreleased, customer, playercontrolledcustomer, robber, policeofficer, boatperson, food, table

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

graph TD start[Start] --> setup[setup] setup --> canvas-setup[Canvas Setup] setup --> customization-prompts[Customization Prompts] setup --> character-spawning[Character Spawning] setup --> initializetables[Initialize Tables] setup --> draw[draw loop] click setup href "#fn-setup" click canvas-setup href "#sub-canvas-setup" click customization-prompts href "#sub-customization-prompts" click character-spawning href "#sub-character-spawning" click initializetables href "#fn-initializetables" draw --> house-logic[House Logic] draw --> character-update-loop[Character Update Loop] draw --> food-cleanup[Food Cleanup] draw --> ai-spawn-logic[AI Spawn Logic] draw --> house-entrance-prompt[House Entrance Prompt] draw --> exit-prompt-check[Exit Prompt Check] draw --> table-grid[Table Grid] click draw href "#fn-draw" click house-logic href "#sub-house-logic" click character-update-loop href "#sub-character-update-loop" click food-cleanup href "#sub-food-cleanup" click ai-spawn-logic href "#sub-ai-spawn-logic" click house-entrance-prompt href "#sub-house-entrance-prompt" click exit-prompt-check href "#sub-exit-prompt-check" click table-grid href "#sub-table-grid" house-logic --> door-drawing[Door Drawing] house-logic --> exit-prompt-check click door-drawing href "#sub-door-drawing" character-update-loop --> character-drag-check[Character Drag Check] character-update-loop --> collision-avoidance[Collision Avoidance] character-update-loop --> state-machine[State Machine] character-update-loop --> mouth-animation[Mouth Animation] click character-drag-check href "#sub-character-drag-check" click collision-avoidance href "#sub-collision-avoidance" click state-machine href "#sub-state-machine" click mouth-animation href "#sub-mouth-animation" ai-spawn-logic --> movement-flags[Movement Flags] ai-spawn-logic --> loot-priority[Loot Priority] click movement-flags href "#sub-movement-flags" click loot-priority href "#sub-loot-priority" food-cleanup --> food-spawn-fallback[Food Spawn Fallback] click food-spawn-fallback href "#sub-food-spawn-fallback" mousepressed[mousePressed] --> house-exit-check[House Exit Check] mousepressed --> house-entry-check[House Entry Check] mousepressed --> character-drag-check mousepressed --> table-click-check[Table Click Check] mousepressed --> food-spawn-fallback click mousepressed href "#fn-mousepressed" click house-exit-check href "#sub-house-exit-check" click house-entry-check href "#sub-house-entry-check" click table-click-check href "#sub-table-click-check" table-click-check --> chair-creation[Chair Creation] click chair-creation href "#sub-chair-creation" mousedragged[mouseDragged] --> diagonal-normalization[Diagonal Normalization] click mousedragged href "#fn-mousedragged" click diagonal-normalization href "#sub-diagonal-normalization" mousereleased[mouseReleased] --> state-restoration[State Restoration] mousereleased --> chair-freeing[Chair Freeing] click mousereleased href "#fn-mousereleased" click state-restoration href "#sub-state-restoration" click chair-freeing href "#sub-chair-freeing" keypressed[keyPressed] --> movement-key-detection[Movement Key Detection] keypressed --> movement-speech[Movement Speech] click keypressed href "#fn-keypressed" click movement-key-detection href "#sub-movement-key-detection" click movement-speech href "#sub-movement-speech" keyreleased[keyReleased] --> movement-flag-reset[Movement Flag Reset] keyreleased --> speech-clear[Speech Clear] click keyreleased href "#fn-keyreleased" click movement-flag-reset href "#sub-movement-flag-reset" click speech-clear href "#sub-speech-clear" customer[Customer Class] --> state-machine customer --> avoidCollisions[Avoid Collisions] click customer href "#fn-customer" click avoidCollisions href "#sub-collision-avoidance" playercontrolledcustomer[PlayerControlledCustomer Class] --> movement-flags playercontrolledcustomer --> avoidCollisions click playercontrolledcustomer href "#fn-playercontrolledcustomer" robber[Robber Class] --> jail-mechanic[Jail Mechanic] robber --> escape-movement[Escape Movement] click robber href "#fn-robber" click jail-mechanic href "#sub-jail-mechanic" click escape-movement href "#sub-escape-movement" policeofficer[PoliceOfficer Class] --> robber-detection[Robber Detection] policeofficer --> pursuit-logic[Pursuit Logic] policeofficer --> jail-sentencing[Jail Sentencing] click policeofficer href "#fn-policeofficer" click robber-detection href "#sub-robber-detection" click pursuit-logic href "#sub-pursuit-logic" click jail-sentencing href "#sub-jail-sentencing" boatperson[BoatPerson Class] --> avoidCollisions click boatperson href "#fn-boatperson" food[Food Class] --> inKitchen[In Kitchen Flag] click food href "#fn-food" click inKitchen href "#sub-food-spawn-fallback" table[Table Class] --> chair-creation table --> occupation-display[Occupation Display] click table href "#fn-table" click occupation-display href "#sub-occupation-display"

❓ Frequently Asked Questions

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

The Good (Remix) sketch creates a vibrant and dynamic scene of a bustling emoji restaurant filled with animated characters, including customers, robbers, and police, all interacting within a colorful environment.

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

Users can customize their player character's appearance and control movement using the Arrow Keys or WASD, allowing them to explore the restaurant and interact with various elements, including their personal house.

What creative coding concepts are demonstrated in the Good (Remix) sketch?

The sketch showcases techniques such as AI behavior for characters, player control mechanics, and collision avoidance to create a lively and interactive simulation.

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, drawinteractionprompt, mousepressed, mousedragged, mousereleased, keypressed, keyreleased, customer, playercontrolledcustomer, robber, policeofficer, boatperson, food, table
Code Flow Diagram