Top Hat AI

This sketch creates a grid-based building game where tiny top-hatted AI robots fly around dropping colored blocks to construct walls, towers, pyramids, houses, cars, and even pixel-art recreations of uploaded photos. Players click UI buttons to send blueprints, summon lumberjack AIs to chop everything down, toggle a pseudo-3D isometric view, or explore inside a furnished house.

🧪 Try This!

Experiment with the code by making these changes:

  1. Slow-motion falling blocks — Lowering the per-frame fall speed makes every dropped or gravity-affected block drift down dramatically slower.
  2. Give every AI a giant head — The computer head is a single rounded rectangle, so enlarging it makes every AI look comically oversized.
  3. Make idle AIs restless — Lowering the idle timer threshold makes AIs place random blocks far more often instead of waiting patiently.
Prefer the full editor? Open it there →

📖 About This Sketch

This sketch turns the screen into a scrolling block grid patrolled by friendly AI builders wearing top hats. Each AI reads tasks from a shared queue and flies to the right column to drop a block, while gravity settles floating blocks and a lumberjack mode can summon extra AIs to chop everything back down. It leans heavily on object-based 'factory' functions, shared task queues, a simple finite-state machine for game screens, and even pixel-by-pixel image analysis to turn an uploaded photo into a blueprint of blocks.

The code is organized around a handful of big ideas: createAI() manufactures independent AI objects with their own update()/draw() methods, queueStructure() and buildFromImage() fill a shared globalTaskQueue with block-drop instructions, and drawGame() ties together grid physics, AI behavior, and UI rendering every frame. Studying it will teach you how to manage multiple animated agents sharing global state, how to fake gravity and 3D with plain 2D drawing calls, and how to structure a multi-screen p5.js app using a gameState string.

⚙️ How It Works

  1. When the sketch loads, setup() builds a full-window canvas, initializes an empty 2D grid sized to blockSize, sets up the UI buttons, and spawns the first permanent AI worker on a HOME title screen.
  2. Clicking Play switches gameState to 'PLAY', and every frame draw() calls drawGame(), which clears the background, draws grid lines, applies gravity to any floating blocks, and updates + draws every AI in the ais array.
  3. Clicking a blueprint button (Wall, Tower, House, Car, Build Image...) calls sendBlueprint() or buildFromImage(), which fills globalTaskQueue with column/color instructions; AIs in BUILD mode pull tasks off this queue one at a time and fly to the correct column to drop a block.
  4. Dropped blocks become 'falling' objects that animate downward until they land in the grid array, at which point playSFX() plays a pitch based on how high they landed.
  5. If a car or house is built, the game automatically drives an AI to board the car (letting the player steer with the wheels) or walk it through the house door into an INSIDE_HOUSE view with its own furnished grid.
  6. Clicking 'Chop All' pushes every occupied cell into globalDestroyQueue, flips existing AIs into angry DESTROY mode, and spawns ten temporary lumberjack AIs that fly in, chop blocks one by one, then fly off-screen and remove themselves.

🎓 Concepts You'll Learn

Object factory functions & closuresShared task queuesFinite state machines (game screens & AI modes)Faked 3D isometric rendering with quad()Grid-based collision & gravity simulationPixel-level image analysis with loadPixels()Procedural audio with p5.sound PolySynth

📝 Code Breakdown

setup()

setup() runs exactly once. Here it wires together three separate systems - the grid, the UI, and the AI swarm - before the animation loop in draw() ever runs.

function setup() {
  createCanvas(windowWidth, windowHeight);
  initGrid();
  setupUI();
  ais.push(createAI(width / 2, 120, false)); // Spawn the first permanent AI
}
Line-by-line explanation (4 lines)
createCanvas(windowWidth, windowHeight);
Makes the canvas fill the entire browser window so the grid can scroll across the whole viewport.
initGrid();
Calculates how many columns/rows fit and builds the empty 2D grid arrays that store block data.
setupUI();
Creates the list of clickable buttons and the hidden file input used for image uploads.
ais.push(createAI(width / 2, 120, false)); // Spawn the first permanent AI
Manufactures the first permanent AI object using the createAI factory and adds it to the ais array so it starts updating and drawing.

createAI(startX, startY, isTempAI)

createAI() is a factory function - it returns a fresh JavaScript object each time it's called, complete with its own update() and draw() methods that close over 'this' properties like x, y, and mode. This lets you spawn as many independent, self-contained AI agents as you like just by calling the function again.

🔬 PI/3 is how far the axe rotates forward mid-swing. What happens if you change it to a full PI, making the axe whip almost all the way around?

        if (this.swingTimer > 0) {
          rotate(PI / 3); // Swing animation
          this.swingTimer--;
        } else {
          rotate(-PI / 6); // Hold ready
        }
function createAI(startX, startY, isTempAI = false) {
  return {
    x: startX,
    y: startY, 
    tx: startX,
    ty: startY, 
    targetCol: 0,
    targetRow: 0,
    targetType: 2,
    timer: random(0, 20),
    phaseOffset: random(TWO_PI), 
    isMovingToDrop: false,
    
    isTemp: isTempAI, 
    mode: isTempAI ? 'DESTROY' : 'IDLE', 
    carDir: 1,
    hasDestroyTarget: false,
    swingTimer: 0,
    
    update: function() {
      let activeGrid = gameState === 'INSIDE_HOUSE' ? insideGrid : grid;
      let activeFalling = gameState === 'INSIDE_HOUSE' ? insideFallingBlocks : fallingBlocks;
      let moveSpeed = (globalTaskQueue.length > 0 || this.mode === 'DRIVING' || this.mode === 'DESTROY' || this.mode === 'LEAVING') ? 0.25 : 0.05;
      
      this.x = lerp(this.x, this.tx, moveSpeed);
      this.y = lerp(this.y, this.ty, moveSpeed);
      
      // --- LUMBERJACK DESTROY MODE ---
      if (this.mode === 'DESTROY') {
        if (!this.hasDestroyTarget) {
          if (globalDestroyQueue.length > 0) {
            let t = globalDestroyQueue.pop();
            this.targetCol = t.c;
            this.targetRow = t.r;
            
            // Safe array check to prevent out-of-bounds crash
            if (activeGrid[this.targetCol] && activeGrid[this.targetCol][this.targetRow] !== undefined) {
              this.tx = this.targetCol * blockSize + blockSize / 2;
              this.ty = this.targetRow * blockSize + blockSize / 2;
              this.hasDestroyTarget = true;
            }
          } else {
            // Queue is empty. Optimize checking to prevent massive CPU lag!
            let hasBlocks = false;
            for (let c = 0; c < cols; c++) {
              if (!activeGrid[c]) continue;
              for (let r = 0; r < rows; r++) {
                if (activeGrid[c][r] > 0) { hasBlocks = true; break; }
              }
              if (hasBlocks) break;
            }

            if (hasBlocks && activeFalling.length === 0) {
              // Repopulate queue safely
              let remaining = [];
              for(let c=0; c<cols; c++) {
                if(!activeGrid[c]) continue; 
                for(let r=0; r<rows; r++) {
                  if(activeGrid[c][r] > 0) remaining.push({c, r});
                }
              }
              globalDestroyQueue = shuffle(remaining);
            } else if (!hasBlocks && activeFalling.length === 0) {
              // EVERYTHING IS DESTROYED! Time to leave or idle.
              this.mode = this.isTemp ? 'LEAVING' : 'IDLE';
              if (!this.isTemp) {
                this.ty = 120;
                currentTaskName = "Idle";
              } else {
                this.tx = random(-200, width + 200);
                this.ty = -200; // Fly away into the sky!
              }
            }
          }
        } else {
          // We have a target, fly to it!
          if (dist(this.x, this.y, this.tx, this.ty) < 15) {
            this.swingTimer = 10;
            // Add safe navigation to prevent crash
            if (activeGrid[this.targetCol] && activeGrid[this.targetCol][this.targetRow] > 0) {
              activeGrid[this.targetCol][this.targetRow] = 0; // Destroy block
              playSFX('break');
            }
            this.hasDestroyTarget = false; 
          }
          
          // Failsafe: if block is already gone or grid changed
          if (activeGrid[this.targetCol] && activeGrid[this.targetCol][this.targetRow] <= 0) {
            this.hasDestroyTarget = false;
          } else if (!activeGrid[this.targetCol]) {
            this.hasDestroyTarget = false;
          }
        }
      } 
      // --- LEAVING MODE (For Temp AIs) ---
      else if (this.mode === 'LEAVING') {
         // Logic is handled in drawGame() where they are removed safely
      }
      // --- NORMAL BEHAVIORS ---
      else {
        if (this.isMovingToDrop && abs(this.x - this.tx) < 5) {
          dropBlock(this.targetCol, this.targetType); 
          this.isMovingToDrop = false;
          this.timer = 0;
        }

        let dropDelay = 3;
        if (this.mode === 'BUILD') {
          if (globalTaskQueue.length > 150) dropDelay = 0; 
          else if (globalTaskQueue.length > 40) dropDelay = 1; 
        }

        if (!this.isMovingToDrop && globalReceivingData === 0 && this.mode === 'BUILD') {
          this.timer++;
          if (globalTaskQueue.length > 0) {
            if (this.timer > dropDelay) {
              let task = globalTaskQueue.shift(); 
              this.targetCol = task.col;
              // Bounds clamp just in case
              if (this.targetCol >= cols) this.targetCol = cols - 1;
              if (this.targetCol < 0) this.targetCol = 0;
              
              this.targetType = task.type;
              this.tx = this.targetCol * blockSize + blockSize / 2;
              this.isMovingToDrop = true;
            }
          } else {
            this.mode = 'IDLE';
            currentTaskName = "Idle";
          }
        }
        
        if (this.mode === 'IDLE') {
          this.ty = 120;
          this.timer++;
          if (this.timer > 80 && globalTaskQueue.length === 0) {
            this.targetCol = floor(random(cols));
            this.targetType = 2;
            this.tx = this.targetCol * blockSize + blockSize / 2;
            this.isMovingToDrop = true;
            this.timer = random(0, 20);
          }
        }
      }
    },
    
    draw: function() {
      push();
      translate(this.x, this.y + sin(frameCount * 0.1 + this.phaseOffset) * 8);

      // --- AXE RENDERING ---
      if (this.mode === 'DESTROY' || this.swingTimer > 0) {
        push();
        translate(20, 0); // Put in right hand
        if (this.swingTimer > 0) {
          rotate(PI / 3); // Swing animation
          this.swingTimer--;
        } else {
          rotate(-PI / 6); // Hold ready
        }
        fill(100, 50, 0); rect(-2, -15, 4, 30, 2); // Handle
        fill(180); arc(5, -10, 16, 22, -PI/2, PI/2); // Blade
        pop();
      }

      if (globalReceivingData > 0 && this.mode !== 'DESTROY') {
        let alpha = map(globalReceivingData, 0, 30, 255, 0);
        stroke(0, 255, 255, alpha);
        strokeWeight(3); noFill();
        arc(0, -60, 20, 20, PI + QUARTER_PI, TWO_PI - QUARTER_PI);
        arc(0, -50, 40, 40, PI + QUARTER_PI, TWO_PI - QUARTER_PI);
      }

      // Top Hat
      fill(30); noStroke(); rectMode(CENTER);
      rect(0, -25, 60, 6, 3);
      rect(0, -45, 36, 40, 2);
      fill(220, 50, 50); rect(0, -30, 36, 8);

      // Computer Head
      fill(230); stroke(100); strokeWeight(3);
      rect(0, 0, 46, 46, 8);

      // Eyes
      noStroke();
      let isBusy = globalTaskQueue.length > 0 || this.mode === 'DRIVING' || this.mode === 'DESTROY';
      fill(this.mode === 'DESTROY' ? color(255, 50, 50) : (isBusy ? color(0, 200, 255) : color(50, 255, 100)));
      
      // Angry eyes if destroying
      if (this.mode === 'DESTROY') {
        push();
        translate(-12, -5); rotate(PI/8); rect(0, 0, 12, 4);
        pop();
        push();
        translate(12, -5); rotate(-PI/8); rect(0, 0, 12, 4);
        pop();
      } else {
        circle(-12, -5, 10); circle(12, -5, 10);
      }

      // Mouth
      stroke(100); strokeWeight(3); noFill();
      if (this.mode === 'DESTROY') arc(0, 10, 16, 10, PI, TWO_PI); // Angry mouth
      else if (isBusy) circle(0, 10, 8); 
      else arc(0, 8, 16, 12, 0, PI);
      pop();
    }
  };
}
Line-by-line explanation (8 lines)

🔧 Subcomponents:

conditional Refill Destroy Queue if (hasBlocks && activeFalling.length === 0) {

Rescans the whole grid and rebuilds the destroy queue once it runs empty but blocks are still standing.

conditional Everything Destroyed Check } else if (!hasBlocks && activeFalling.length === 0) {

Once no blocks remain, switches permanent AIs back to IDLE and sends temporary lumberjacks flying off-screen to be removed.

conditional Take Next Build Task if (globalTaskQueue.length > 0) {

Pulls the next column/type task off the shared build queue once the per-AI drop delay timer allows it.

conditional Idle Random Placement if (this.timer > 80 && globalTaskQueue.length === 0) {

Gives an idle AI something to do occasionally by placing a random orange block, keeping the scene alive.

conditional Draw & Swing Axe if (this.mode === 'DESTROY' || this.swingTimer > 0) {

Draws the lumberjack axe in the AI's hand and animates a forward swing whenever swingTimer is counting down.

conditional Angry vs Friendly Eyes if (this.mode === 'DESTROY') {

Swaps round friendly eyes for angled angry rectangles whenever the AI is in chopping mode.

let moveSpeed = (globalTaskQueue.length > 0 || this.mode === 'DRIVING' || this.mode === 'DESTROY' || this.mode === 'LEAVING') ? 0.25 : 0.05;
AIs move faster (0.25 lerp amount) whenever there's work to do, and slower (0.05) when idle, making busy AIs feel snappier.
this.x = lerp(this.x, this.tx, moveSpeed);
Smoothly slides the AI's current x position toward its target x (tx) a little each frame instead of teleporting.
let t = globalDestroyQueue.pop();
Grabs one block coordinate off the shared destroy queue to claim as this lumberjack's next chopping target.
if (dist(this.x, this.y, this.tx, this.ty) < 15) {
Once the AI is close enough to its target block, it triggers the chop: playing the swing animation and deleting the block.
let task = globalTaskQueue.shift();
Takes the oldest task from the front of the build queue (col + block type) for this AI to fulfil next.
if (this.mode === 'IDLE') {
When there's no work queued, the AI hovers near the top of the screen and occasionally drops a random block just for flavor.
translate(this.x, this.y + sin(frameCount * 0.1 + this.phaseOffset) * 8);
Moves the drawing origin to the AI's position, adding a gentle sine-wave bob so each AI bounces slightly out of sync with the others.
fill(this.mode === 'DESTROY' ? color(255, 50, 50) : (isBusy ? color(0, 200, 255) : color(50, 255, 100)));
Picks the eye color: red when destroying, cyan when busy building/driving, green when calmly idle.

setupUI()

Storing buttons as an array of plain objects (a 'data-driven UI') means adding, removing, or relabeling buttons never requires touching the drawing or click-handling code - only this list.

function setupUI() {
  btns = [
    { label: "🧱 Wall", type: 'wall' },
    { label: "🗼 Tower", type: 'tower' },
    { label: "🔺 Pyramid", type: 'pyramid' },
    { label: "🏠 House", type: 'house' },
    { label: "🚗 Car", type: 'car' },
    { label: "🖼️ Build Image", type: 'upload' },
    { label: "🔨 Delete: OFF", type: 'delete' },
    { label: "🪓 Chop All", type: 'clear' },
    { label: "🚪 Exit House", type: 'exit' },
    { label: "🤖 Add AI", type: 'add_ai' },
    { label: "🏙️ Build City", type: 'build_city' },
    { label: "🧊 Make 3D: OFF", type: 'toggle_3d' }
  ];
  
  updateUILayout();
  fileInput = createFileInput(handleFile);
  fileInput.hide();
}
Line-by-line explanation (4 lines)
btns = [ ... ]
Defines every possible button as a plain data object with a label and a type string, letting the rest of the code decide which ones are visible and what each one does.
updateUILayout();
Calculates x/y positions and visibility for whichever buttons apply to the current game screen.
fileInput = createFileInput(handleFile);
Creates a hidden HTML file picker that calls handleFile() whenever the user chooses an image.
fileInput.hide();
Hides the default browser file input element since it's triggered programmatically by clicking the 'Build Image' button instead.

updateUILayout()

This function recalculates layout every time the game state or mode changes, a common p5.js pattern for responsive UI: rather than hardcoding pixel positions, compute them from current window size and content.

🔬 This loop wraps buttons to a new row when they'd cross 'width - 70'. What happens if you change that to 'width - 400', forcing far fewer buttons per row?

  for (let b of activeBtns) {
    b.w = textWidth(b.label) + 40; b.h = 36;
    if (bx + b.w > width - 70) { bx = 15; by += 45; }
    b.x = bx; b.y = by; bx += b.w + 10; b.visible = true;
  }
function updateUILayout() {
  let activeBtns = [];
  if (gameState === 'PLAY') {
    if (advancedMode) activeBtns = btns.filter(b => ['upload', 'delete', 'clear', 'add_ai', 'build_city', 'toggle_3d'].includes(b.type));
    else activeBtns = btns.filter(b => ['wall', 'tower', 'pyramid', 'house', 'car', 'upload', 'delete', 'clear'].includes(b.type));
  } else if (gameState === 'INSIDE_HOUSE') {
    activeBtns = btns.filter(b => ['exit', 'delete', 'clear'].includes(b.type));
  }
  
  let bx = 15, by = 45;
  for (let b of activeBtns) {
    b.w = textWidth(b.label) + 40; b.h = 36;
    if (bx + b.w > width - 70) { bx = 15; by += 45; }
    b.x = bx; b.y = by; bx += b.w + 10; b.visible = true;
  }
  for (let b of btns) if (!activeBtns.includes(b)) b.visible = false;
  uiPanelHeight = activeBtns.length > 0 ? by + 50 : 0; 
}
Line-by-line explanation (4 lines)

🔧 Subcomponents:

conditional Pick Buttons For Current Mode if (advancedMode) activeBtns = btns.filter(b => ['upload', 'delete', 'clear', 'add_ai', 'build_city', 'toggle_3d'].includes(b.type));

Chooses a different subset of buttons depending on whether the player is in Advanced Mode, Basic Mode, or inside the house.

for-loop Position Buttons In Rows for (let b of activeBtns) {

Lines buttons up left to right, wrapping to a new row whenever they would run off the edge of the window.

activeBtns = btns.filter(b => ['wall', 'tower', 'pyramid', 'house', 'car', 'upload', 'delete', 'clear'].includes(b.type));
Keeps only the buttons whose 'type' string is in this allow-list, hiding everything else for Basic Mode.
b.w = textWidth(b.label) + 40; b.h = 36;
Sizes each button just wide enough to fit its label text plus 40px of padding.
if (bx + b.w > width - 70) { bx = 15; by += 45; }
If the next button would overflow past the window width (minus room for the gear icon), start a new row instead.
uiPanelHeight = activeBtns.length > 0 ? by + 50 : 0;
Stores how tall the button panel ended up, so the game grid and click detection know where playable space begins below the UI.

handleFile(file)

createFileInput()'s callback receives a file object; loadImage() is asynchronous, so the arrow function img => buildFromImage(img) only runs once the browser has finished decoding the picture.

function handleFile(file) {
  if (file.type === 'image') loadImage(file.data, img => buildFromImage(img));
}
Line-by-line explanation (1 lines)
if (file.type === 'image') loadImage(file.data, img => buildFromImage(img));
Checks the uploaded file is actually an image, then asynchronously loads it and passes the finished p5.Image into buildFromImage() once ready.

buildFromImage(img)

This function shows how to turn any image into game data: shrink it with createGraphics(), read its pixels[] array, and translate each pixel into a task your simulation already understands (a colored block in a column).

🔬 A pixel only becomes a block when its alpha channel is above 50 (out of 255). What happens if you raise that threshold to 220, so only near-fully-opaque pixels count?

      if (pg.pixels[i+3] > 50) {
        let cId = nextColorId++; customColors[cId] = color(pg.pixels[i], pg.pixels[i+1], pg.pixels[i+2]);
        globalTaskQueue.push({ col: actualCol, type: cId });
      } else {
function buildFromImage(img) {
  currentTaskName = "Analyzing Image Blueprint...";
  globalTaskQueue = []; globalReceivingData = 30; 
  ais.forEach(a => { if(!a.isTemp) { a.mode = 'BUILD'; a.ty = 80; }});
  
  let maxW = min(cols - 2, 32), maxH = min(rows - 4, 32);
  let aspect = img.width / img.height;
  let w = maxW, h = floor(w / aspect);
  if (h > maxH) { h = maxH; w = floor(h * aspect); }
  w = max(1, w); h = max(1, h);

  let pg = createGraphics(w, h);
  pg.image(img, 0, 0, w, h); pg.loadPixels();
  let cx = floor(cols / 2), startCol = cx - floor(w / 2);

  for (let x = 0; x < w; x++) {
    let actualCol = startCol + x;
    if (actualCol < 0 || actualCol >= cols) continue;
    for (let y = h - 1; y >= 0; y--) {
      let i = (x + y * w) * 4;
      if (pg.pixels[i+3] > 50) {
        let cId = nextColorId++; customColors[cId] = color(pg.pixels[i], pg.pixels[i+1], pg.pixels[i+2]);
        globalTaskQueue.push({ col: actualCol, type: cId });
      } else {
        let needsSupport = false;
        for (let yy = y - 1; yy >= 0; yy--) if (pg.pixels[(x + yy * w) * 4 + 3] > 50) { needsSupport = true; break; }
        if (needsSupport) globalTaskQueue.push({ col: actualCol, type: 99 });
      }
    }
  }
  pg.remove(); currentTaskName = "Building Masterpiece...";
}
Line-by-line explanation (6 lines)

🔧 Subcomponents:

calculation Fit Image To Grid let w = maxW, h = floor(w / aspect);

Shrinks the uploaded image down to a small pixel grid (max 32x32) while keeping its original proportions.

for-loop Scan Image Columns for (let x = 0; x < w; x++) {

Walks across the shrunk image column by column, deciding which pixels become colored blocks.

conditional Opaque Pixel Becomes Block if (pg.pixels[i+3] > 50) {

Treats any sufficiently opaque pixel as a solid, uniquely colored block and remembers its exact RGB as a custom color.

for-loop Ghost Support Blocks for (let yy = y - 1; yy >= 0; yy--) if (pg.pixels[(x + yy * w) * 4 + 3] > 50) { needsSupport = true; break; }

Adds an invisible support block (type 99) under transparent gaps so floating parts of the image don't collapse due to grid gravity.

let maxW = min(cols - 2, 32), maxH = min(rows - 4, 32);
Caps the image's block-resolution at 32x32 so it never overflows the grid or creates an enormous task queue.
let pg = createGraphics(w, h);
Creates an off-screen tiny canvas just big enough to hold the downscaled image.
pg.image(img, 0, 0, w, h); pg.loadPixels();
Draws the uploaded photo into the tiny canvas (resizing it) then loads its raw pixel color data into pg.pixels[].
let i = (x + y * w) * 4;
Pixel arrays store 4 numbers per pixel (red, green, blue, alpha), so this converts an (x, y) coordinate into the correct starting index.
let cId = nextColorId++; customColors[cId] = color(pg.pixels[i], pg.pixels[i+1], pg.pixels[i+2]);
Generates a brand-new unique block-type id and stores the pixel's exact color in the customColors lookup, so any type number over 100 means 'look up this custom color'.
globalTaskQueue.push({ col: actualCol, type: cId });
Queues a build task telling an AI to drop a block of this new custom color into the correct column.

initAudio()

Web browsers require a user gesture (like a click) before allowing sound, which is why initAudio() is only called from inside handleInput() rather than from setup().

function initAudio() {
  if (!audioInitialized) {
    userStartAudio(); bgSynth = new p5.PolySynth(); sfxSynth = new p5.PolySynth(); 
    audioInitialized = true; outputVolume(0.4, 0.5); 
  }
}
Line-by-line explanation (3 lines)
userStartAudio();
Unlocks the browser's audio engine, which most browsers block until the user interacts with the page (like a click).
bgSynth = new p5.PolySynth(); sfxSynth = new p5.PolySynth();
Creates two separate synthesizers - one for background music chords, one for short sound effects - so they don't interfere with each other.
outputVolume(0.4, 0.5);
Sets the overall master volume to 40%, ramping to that level over 0.5 seconds to avoid an audio pop.

playSFX(type, data)

This function centralizes every sound effect the game makes, using a string 'type' as a lightweight event name - a common pattern for keeping audio logic in one place instead of scattering synth.play() calls everywhere.

🔬 Right now higher rows (closer to the top) map to higher notes. What happens if you swap the map() output range so higher blocks play LOWER notes instead?

    else if (type === 'land') {
      let pitchScale = ['C3', 'D3', 'E3', 'G3', 'A3', 'C4', 'D4', 'E4', 'G4', 'A4', 'C5', 'D5', 'E5'];
      let idx = constrain(floor(map(data, rows - 1, 0, 0, pitchScale.length - 1)), 0, pitchScale.length - 1);
      sfxSynth.play(pitchScale[idx], 0.05, 0, 0.1);
    } 
function playSFX(type, data) {
  if (!audioInitialized) return;

  // CRITICAL FIX: Audio Engine Crash Guard & Rate Limiter!
  // If 10 lumberjacks chop blocks at the exact same millisecond, PolySynth overloads and crashes the browser.
  // We limit the same sound from playing more than once every 2 frames to prevent this.
  if (frameCount - sfxRateLimits[type] < 2) return;
  sfxRateLimits[type] = frameCount;

  try {
    if (type === 'receive') sfxSynth.play(random(['C6', 'E6', 'G6']), 0.05, 0, 0.05);
    else if (type === 'drop') sfxSynth.play('C5', 0.01, 0, 0.05);
    else if (type === 'break') sfxSynth.play('G2', 0.1, 0, 0.1);
    else if (type === 'door') sfxSynth.play('E4', 0.1, 0, 0.3);
    else if (type === 'land') {
      let pitchScale = ['C3', 'D3', 'E3', 'G3', 'A3', 'C4', 'D4', 'E4', 'G4', 'A4', 'C5', 'D5', 'E5'];
      let idx = constrain(floor(map(data, rows - 1, 0, 0, pitchScale.length - 1)), 0, pitchScale.length - 1);
      sfxSynth.play(pitchScale[idx], 0.05, 0, 0.1);
    } 
    else if (type === 'clear') sfxSynth.play('C2', 0.3, 0, 1.0);
  } catch (err) {
    // If the p5.sound library overloads, catch the crash silently instead of freezing the game!
    console.warn("Audio engine guarded a crash!");
  }
}
Line-by-line explanation (4 lines)

🔧 Subcomponents:

conditional Prevent Sound Spam if (frameCount - sfxRateLimits[type] < 2) return;

Stops the exact same sound effect from firing more than once every 2 frames, protecting PolySynth from crashing when many events happen at once.

conditional Choose Sound By Type if (type === 'receive') sfxSynth.play(random(['C6', 'E6', 'G6']), 0.05, 0, 0.05);

Picks which note and envelope to play depending on the event name passed in (drop, break, land, door, etc.).

if (frameCount - sfxRateLimits[type] < 2) return;
Skips playing this sound if the same sound type already played less than 2 frames ago, avoiding audio engine overload.
sfxRateLimits[type] = frameCount;
Remembers the current frame as 'the last time this sound type played' for next time's rate check.
let idx = constrain(floor(map(data, rows - 1, 0, 0, pitchScale.length - 1)), 0, pitchScale.length - 1);
Maps the row a block landed on (0 = top, rows-1 = bottom) to an index into a musical scale, so blocks landing higher up play higher notes.
} catch (err) { console.warn("Audio engine guarded a crash!"); }
Wraps the actual sound-playing calls in a try/catch so an unexpected audio library error just logs a warning instead of freezing the whole game.

playMusic()

Using frameCount % N is the standard p5.js trick for 'do this every N frames' without needing a separate timer or setInterval.

function playMusic() {
  if (!audioInitialized) return;
  if (frameCount % 180 === 0) {
    for (let note of chords[chordIndex]) {
      try { bgSynth.play(note, 0.03, 0, 2.5); } catch(e) {}
    }
    chordIndex = (chordIndex + 1) % chords.length;
  }
}
Line-by-line explanation (3 lines)

🔧 Subcomponents:

conditional Every 180 Frames if (frameCount % 180 === 0) {

Uses the modulo operator on frameCount to trigger a new background chord roughly every 3 seconds at 60fps.

if (frameCount % 180 === 0) {
frameCount increases by 1 every frame; checking if it's evenly divisible by 180 makes this code run only once every 180 frames.
for (let note of chords[chordIndex]) { try { bgSynth.play(note, 0.03, 0, 2.5); } catch(e) {} }
Plays every note in the current chord (an array of note names) simultaneously with a soft volume and a 2.5 second sustain.
chordIndex = (chordIndex + 1) % chords.length;
Advances to the next chord in the progression, wrapping back to the first chord after the last one using modulo.

initGrid()

This is a classic 2D array initialization pattern in JavaScript: an outer array of columns, each holding an inner array of rows, giving grid[c][r] access to any cell.

🔬 This nested loop builds a cols x rows grid of zeros. Since cols and rows both depend on blockSize, what happens visually if you make blockSize much smaller before this runs?

  for (let i = 0; i < cols; i++) {
    grid[i] = []; insideGrid[i] = [];
    for (let j = 0; j < rows; j++) { grid[i][j] = 0; insideGrid[i][j] = 0; }
  }
function initGrid() {
  cols = floor(width / blockSize); rows = floor(height / blockSize);
  grid = []; fallingBlocks = []; insideGrid = []; insideFallingBlocks = [];
  hasFurnishedHouse = false; customColors = {}; nextColorId = 100;
  for (let i = 0; i < cols; i++) {
    grid[i] = []; insideGrid[i] = [];
    for (let j = 0; j < rows; j++) { grid[i][j] = 0; insideGrid[i][j] = 0; }
  }
}
Line-by-line explanation (3 lines)

🔧 Subcomponents:

for-loop Build 2D Grid Arrays for (let i = 0; i < cols; i++) {

Creates a 2D array (columns of rows) initialized entirely to 0, meaning every cell starts empty.

cols = floor(width / blockSize); rows = floor(height / blockSize);
Calculates how many whole grid cells fit across the canvas width and height based on blockSize.
grid = []; fallingBlocks = []; insideGrid = []; insideFallingBlocks = [];
Resets both the outdoor and indoor worlds to empty, wiping any previously built structures.
for (let j = 0; j < rows; j++) { grid[i][j] = 0; insideGrid[i][j] = 0; }
Fills every row of the current column with 0, the code for 'empty cell', in both the outside and inside grids.

queueStructure(type, cx)

This function generates blueprints procedurally instead of storing a pixel-perfect image of each shape - a wall, tower, pyramid, house, or car are all just math describing which columns get how many blocks.

🔬 Each row loses 2 columns of width (baseWidth - i*2). What happens if you change that to i*4, making the pyramid's sides much steeper and pointier?

  if (type === 'pyramid') {
    let baseWidth = min(11, cols - 2); if (baseWidth % 2 === 0) baseWidth--; 
    for (let i = 0; i < floor(baseWidth / 2) + 1; i++) {
      let w = baseWidth - (i * 2);
      for (let j = 0; j < w; j++) pushToQueue(cx - floor(w / 2) + j, 2);
    }
  }
function queueStructure(type, cx) {
  let pushToQueue = (col, bType) => globalTaskQueue.push({ col: constrain(col, 0, cols - 1), type: bType });
  let addColumnStack = (colOffset, count, bType) => { for(let i=0; i<count; i++) pushToQueue(cx + colOffset, bType); };

  if (type === 'pyramid') {
    let baseWidth = min(11, cols - 2); if (baseWidth % 2 === 0) baseWidth--; 
    for (let i = 0; i < floor(baseWidth / 2) + 1; i++) {
      let w = baseWidth - (i * 2);
      for (let j = 0; j < w; j++) pushToQueue(cx - floor(w / 2) + j, 2);
    }
  } else if (type === 'tower') {
    for (let i = 0; i < min(12, rows - 2); i++) { pushToQueue(cx - 1, 2); pushToQueue(cx, 2); pushToQueue(cx + 1, 2); }
  } else if (type === 'wall') {
    let w = min(15, cols - 2);
    for (let i = 0; i < 4; i++) { for (let j = 0; j < w; j++) pushToQueue(cx - floor(w/2) + j, 2); }
  } else if (type === 'house') {
    let t = 4;
    addColumnStack(-4, 3, t); addColumnStack(-3, 4, t); addColumnStack(-2, 5, t);
    addColumnStack(-1, 6, t); addColumnStack(0,  7, t); addColumnStack(1,  6, t);
    addColumnStack(2,  5, t); addColumnStack(3,  4, t); addColumnStack(4,  3, t);
  } else if (type === 'car') {
    pushToQueue(cx - 2, 30); pushToQueue(cx - 2, 30);
    pushToQueue(cx - 1, 31); pushToQueue(cx - 1, 30); pushToQueue(cx - 1, 32);
    pushToQueue(cx, 30); pushToQueue(cx, 30); pushToQueue(cx, 30);
    pushToQueue(cx + 1, 31); pushToQueue(cx + 1, 30); pushToQueue(cx + 1, 32);
    pushToQueue(cx + 2, 30); pushToQueue(cx + 2, 30);
  }
}
Line-by-line explanation (3 lines)

🔧 Subcomponents:

for-loop Build Pyramid Rows for (let i = 0; i < floor(baseWidth / 2) + 1; i++) {

Queues each horizontal row of the pyramid, shrinking the row width by 2 columns every step up.

for-loop Stack Tower Blocks for (let i = 0; i < min(12, rows - 2); i++) { pushToQueue(cx - 1, 2); pushToQueue(cx, 2); pushToQueue(cx + 1, 2); }

Queues three columns of blocks repeated for each floor of the tower, capped at 12 stories or the available rows.

for-loop Build Wall Layers for (let i = 0; i < 4; i++) { for (let j = 0; j < w; j++) pushToQueue(cx - floor(w/2) + j, 2); }

Repeats a full-width row of blocks 4 times to build a wall 4 blocks tall.

let pushToQueue = (col, bType) => globalTaskQueue.push({ col: constrain(col, 0, cols - 1), type: bType });
A small helper closure that safely adds a build task, clamping the column so it never targets a column off the edge of the grid.
let addColumnStack = (colOffset, count, bType) => { for(let i=0; i<count; i++) pushToQueue(cx + colOffset, bType); };
Another helper that queues 'count' blocks of the same type into one column, used to build the sloped roof of the house shape.
let baseWidth = min(11, cols - 2); if (baseWidth % 2 === 0) baseWidth--;
Picks an odd base width (so the pyramid has a clear center column) no wider than 11 blocks or the available grid width.

sendBlueprint(type)

sendBlueprint() is the single entry point every 'send a build task' button goes through - it resets shared state (queue, AI modes) consistently before delegating the actual shape math to queueStructure().

function sendBlueprint(type) {
  globalTaskQueue = []; globalReceivingData = 30; 
  ais.forEach(a => { if(!a.isTemp) { a.mode = 'BUILD'; a.ty = 120; }});
  if (type === 'build_city') {
    currentTaskName = "Constructing Mega City...";
    let cityTypes = ['tower', 'house', 'pyramid', 'tower', 'car'], offsets = [-14, -6, 2, 10, 16];
    for (let i = 0; i < cityTypes.length; i++) queueStructure(cityTypes[i], floor(cols/2) + offsets[i]);
    globalTaskQueue = shuffle(globalTaskQueue);
  } else {
    currentTaskName = { 'pyramid': "Building Pyramid...", 'tower': "Building Tower...", 'wall': "Building Wall...", 'house': "Building House...", 'car': "Building Car..." }[type];
    queueStructure(type, floor(cols / 2));
    if (['pyramid', 'tower', 'wall'].includes(type)) globalTaskQueue = shuffle(globalTaskQueue);
  }
}
Line-by-line explanation (4 lines)

🔧 Subcomponents:

for-loop Queue Every City Building for (let i = 0; i < cityTypes.length; i++) queueStructure(cityTypes[i], floor(cols/2) + offsets[i]);

Places five different structures side by side by pairing each structure type with a horizontal offset from the grid center.

globalTaskQueue = []; globalReceivingData = 30;
Cancels any in-progress build and starts the 'receiving data' arc animation above the AI's head for 30 frames.
ais.forEach(a => { if(!a.isTemp) { a.mode = 'BUILD'; a.ty = 120; }});
Switches every permanent (non-temporary) AI into BUILD mode and moves them into position to start dropping blocks.
let cityTypes = ['tower', 'house', 'pyramid', 'tower', 'car'], offsets = [-14, -6, 2, 10, 16];
Defines a lineup of five buildings and how far left/right of center each one should be placed.
globalTaskQueue = shuffle(globalTaskQueue);
Randomizes the order blocks get built in, so structures visibly grow from scattered pieces rather than row-by-row.

queueFurniture()

This function reuses the same pushToQueue-and-column-offset pattern as queueStructure(), showing how a small set of ideas (columns, offsets, block types) can describe very different scenes.

function queueFurniture() {
  globalTaskQueue = []; globalReceivingData = 30;
  ais.forEach(a => { if(!a.isTemp) { a.mode = 'BUILD'; a.ty = 80; }});
  currentTaskName = "Building Furniture...";
  let cx = floor(cols / 2);
  let pushToQueue = (col, bType) => globalTaskQueue.push({ col: constrain(col, 0, cols - 1), type: bType });
  pushToQueue(cx, 40); pushToQueue(cx - 1, 40); pushToQueue(cx, 40); pushToQueue(cx + 1, 40); 
  pushToQueue(cx - 3, 40); pushToQueue(cx - 3, 40); pushToQueue(cx + 3, 40); pushToQueue(cx + 3, 40); 
  for(let i = 1; i <= 4; i++) pushToQueue(i, 40); 
  pushToQueue(1, 42); 
  for(let i = 2; i <= 4; i++) pushToQueue(i, 41); 
  let rx = cols - 4;
  pushToQueue(rx, 40); pushToQueue(rx+1, 40); pushToQueue(rx, 43); pushToQueue(rx+1, 44); 
  pushToQueue(rx, 40); pushToQueue(rx+1, 40); pushToQueue(rx, 45); pushToQueue(rx+1, 43); 
  hasFurnishedHouse = true;
}
Line-by-line explanation (4 lines)

🔧 Subcomponents:

for-loop Queue Bookshelf Column for(let i = 1; i <= 4; i++) pushToQueue(i, 40);

Stacks brown 'wood' blocks up the left side of the room to form a bookshelf-like structure.

let cx = floor(cols / 2);
Finds the horizontal center column of the room so furniture pieces can be positioned relative to it.
pushToQueue(cx, 40); pushToQueue(cx - 1, 40); pushToQueue(cx, 40); pushToQueue(cx + 1, 40);
Queues a small brown table/couch shape centered in the room using block type 40.
let rx = cols - 4;
Finds a column near the right edge of the room to place a second piece of furniture (like a TV or chair set).
hasFurnishedHouse = true;
Marks the house as already furnished so this function won't run again every time the player re-enters.

shuffle(array)

shuffle() is a generic utility with no game-specific knowledge - it's called on globalTaskQueue and globalDestroyQueue so structures appear to build and get chopped down in a random, organic-looking order rather than row by row.

🔬 This is the classic Fisher-Yates shuffle used to randomize build order. What do you think would happen visually if sendBlueprint() never called shuffle() at all - would structures still look the same once finished?

  while (currentIndex != 0) {
    randomIndex = floor(random(currentIndex)); currentIndex--;
    [array[currentIndex], array[randomIndex]] = [array[randomIndex], array[currentIndex]];
  }
function shuffle(array) {
  let currentIndex = array.length, randomIndex;
  while (currentIndex != 0) {
    randomIndex = floor(random(currentIndex)); currentIndex--;
    [array[currentIndex], array[randomIndex]] = [array[randomIndex], array[currentIndex]];
  }
  return array;
}
Line-by-line explanation (3 lines)

🔧 Subcomponents:

while-loop Fisher-Yates Shuffle while (currentIndex != 0) {

Repeatedly swaps the current last unshuffled element with a random earlier element until the whole array is randomly reordered.

let currentIndex = array.length, randomIndex;
Starts scanning from the very end of the array, working backwards.
randomIndex = floor(random(currentIndex)); currentIndex--;
Picks a random index somewhere from 0 up to (but not including) the current position, then moves the current position one step earlier.
[array[currentIndex], array[randomIndex]] = [array[randomIndex], array[currentIndex]];
Uses array destructuring to swap the two elements in place without needing a temporary variable.

dropBlock(c, type)

dropBlock() only decides WHERE a block will land and creates the falling animation object; the actual falling motion and final placement happen later inside drawGame()'s falling-blocks loop.

🔬 This searches from the bottom row upward to find the first empty cell, mimicking gravity. What would happen if you started the search at 0 (the top) and searched downward instead?

  let targetR = rows - 1;
  while (targetR >= 0 && activeGrid[c][targetR] !== 0) targetR--;
function dropBlock(c, type) {
  let activeGrid = gameState === 'INSIDE_HOUSE' ? insideGrid : grid;
  let activeFalling = gameState === 'INSIDE_HOUSE' ? insideFallingBlocks : fallingBlocks;
  if (c < 0 || c >= cols) return;
  if (!activeGrid[c]) return; // Extra Safety
  
  let targetR = rows - 1;
  while (targetR >= 0 && activeGrid[c][targetR] !== 0) targetR--;
  if (targetR >= 0) {
    activeGrid[c][targetR] = -1; 
    activeFalling.push({ c: c, r: targetR, y: -blockSize, type: type });
    playSFX('drop');
  }
}
Line-by-line explanation (4 lines)

🔧 Subcomponents:

while-loop Find Lowest Empty Row while (targetR >= 0 && activeGrid[c][targetR] !== 0) targetR--;

Scans upward from the bottom of the column to find the first empty cell a new block can land in, like real gravity.

let activeGrid = gameState === 'INSIDE_HOUSE' ? insideGrid : grid;
Chooses which world's grid to affect depending on whether the player is currently inside the house or outside.
let targetR = rows - 1;
Starts checking from the bottom-most row of the grid, since blocks always fall to the lowest available spot.
activeGrid[c][targetR] = -1;
Temporarily marks the destination cell as -1 ('reserved but not yet landed') so no other block claims the same spot while this one is still falling.
activeFalling.push({ c: c, r: targetR, y: -blockSize, type: type });
Adds a new falling-block object starting above the top of the screen (y: -blockSize) that will animate down to its target row.

deleteBlock(c, r)

This function powers the manual Delete Mode where clicking or dragging over blocks removes them one at a time, using lastDeletedCell to avoid duplicate deletions during a single drag gesture.

function deleteBlock(c, r) {
  let activeGrid = gameState === 'INSIDE_HOUSE' ? insideGrid : grid;
  if (c >= 0 && c < cols && r >= 0 && r < rows) {
    if (activeGrid[c] && activeGrid[c][r] > 0 && (lastDeletedCell.c !== c || lastDeletedCell.r !== r)) {
      activeGrid[c][r] = 0; playSFX('break'); lastDeletedCell = { c: c, r: r };
    }
  }
}
Line-by-line explanation (3 lines)
if (c >= 0 && c < cols && r >= 0 && r < rows) {
Guards against deleting a cell outside the grid's bounds, which would otherwise throw an error.
(lastDeletedCell.c !== c || lastDeletedCell.r !== r)
Prevents repeatedly deleting the same cell over and over while the mouse is dragged across it in one continuous motion.
activeGrid[c][r] = 0; playSFX('break'); lastDeletedCell = { c: c, r: r };
Clears the cell back to empty, plays a breaking sound, and remembers this cell so it isn't immediately re-deleted.

getBlockColor(type)

Using numeric 'type' codes to represent different block kinds (like a mini enum) is a lightweight alternative to full classes - getBlockColor() is the single place that translates those codes into visuals.

function getBlockColor(type) {
  if (type === 1) return color(60, 160, 255);       
  else if (type === 2) return color(255, 120, 60);  
  else if (type === 4) return color(240, 180, 40);  
  else if (type === 30) return color(210, 40, 40);  
  else if (type === 31) return color(30, 30, 30);   
  else if (type === 32) return color(180, 230, 255);
  else if (type === 40) return color(139, 69, 19);  
  else if (type === 41) return color(240, 240, 245);
  else if (type === 42) return color(200, 50, 50);  
  else if (type === 43) return color(100, 200, 100);
  else if (type === 44) return color(100, 100, 200);
  else if (type === 45) return color(200, 150, 50); 
  else if (type === 99) return color(200, 240, 255, 60); 
  else if (type >= 100 && customColors[type]) return customColors[type]; 
  return color(255);
}
Line-by-line explanation (4 lines)

🔧 Subcomponents:

conditional Type Number To Color else if (type >= 100 && customColors[type]) return customColors[type];

Falls back to a stored custom color for any type number 100 or above, which is how image-uploaded blocks get their exact photo colors.

if (type === 1) return color(60, 160, 255);
Type 1 is a blue block color, though it's rarely queued directly by the current UI.
else if (type === 2) return color(255, 120, 60);
Type 2 is the default orange block used by walls, towers, pyramids, and idle random drops.
else if (type >= 100 && customColors[type]) return customColors[type];
Any type 100+ came from an uploaded image; this looks up its exact RGB color that buildFromImage() stored earlier.
return color(255);
A safe fallback: if no type matches, just return plain white instead of crashing.

drawBlock(x, y, type)

drawBlock() shows a clever trick for faking 3D in a 2D canvas: instead of using WEBGL, it draws extra shaded quad() shapes offset from the flat square, which reads as a cube when many blocks are drawn in the right order.

🔬 dy is negative, pushing the top face upward to fake depth. What happens visually if you flip dy to a positive number, like 8?

  if (is3DMode) {
    let dx = 8, dy = -8;
    let cTop = type === 99 ? color(255, 255, 255, 80) : color(red(baseCol)+30, green(baseCol)+30, blue(baseCol)+30);
    fill(cTop); quad(x, y, x + dx, y + dy, x + blockSize + dx, y + dy, x + blockSize, y);
function drawBlock(x, y, type) {
  let baseCol = getBlockColor(type);
  if (type === 99) { fill(200, 240, 255, 60); stroke(255, 255, 255, 30); strokeWeight(is3DMode ? 1 : 2); } 
  else { fill(baseCol); stroke(20, 30, 45); strokeWeight(is3DMode ? 2 : 3); }
  
  if (is3DMode) {
    let dx = 8, dy = -8;
    let cTop = type === 99 ? color(255, 255, 255, 80) : color(red(baseCol)+30, green(baseCol)+30, blue(baseCol)+30);
    fill(cTop); quad(x, y, x + dx, y + dy, x + blockSize + dx, y + dy, x + blockSize, y);
    let cRight = type === 99 ? color(200, 240, 255, 40) : color(red(baseCol)-30, green(baseCol)-30, blue(baseCol)-30);
    fill(cRight); quad(x + blockSize, y, x + blockSize + dx, y + dy, x + blockSize + dx, y + blockSize + dy, x + blockSize, y + blockSize);
    fill(baseCol); rect(x, y, blockSize, blockSize, 2);
  } else { rect(x, y, blockSize, blockSize, 6); }
  
  noStroke();
  if (type === 31) { fill(130); circle(x + blockSize/2, y + blockSize/2, 18); fill(40); circle(x + blockSize/2, y + blockSize/2, 8); } 
  else if (type === 32) { fill(255, 255, 255, 100); quad(x + 5, y + 5, x + 15, y + 5, x + 10, y + blockSize - 5, x + 5, y + blockSize - 5); } 
  else if (type === 99) { fill(255, 255, 255, 60); rect(x + 5, y + 5, blockSize - 10, blockSize / 2, 3); } 
  else { fill(255, 255, 255, 40); rect(x + 5, y + 5, blockSize - 10, blockSize / 3, 3); }
}
Line-by-line explanation (4 lines)

🔧 Subcomponents:

conditional Draw Isometric Top & Side Faces if (is3DMode) {

Draws two extra slanted quad() faces (lighter top, darker side) offset from the flat square to fake a 3D cube look.

conditional Per-Type Decoration if (type === 31) { fill(130); circle(x + blockSize/2, y + blockSize/2, 18); fill(40); circle(x + blockSize/2, y + blockSize/2, 8); }

Adds small decorative details on top of certain block types, like a wheel hubcap for car-wheel blocks.

let baseCol = getBlockColor(type);
Looks up the correct base color for this block's type number before drawing anything.
let dx = 8, dy = -8;
Defines the offset used to shear the top and side faces, creating the illusion of looking at a cube from an angle.
fill(cTop); quad(x, y, x + dx, y + dy, x + blockSize + dx, y + dy, x + blockSize, y);
Draws a parallelogram above the block's flat top edge, lightened by +30 to simulate a light source hitting the top face.
fill(baseCol); rect(x, y, blockSize, blockSize, 2);
Draws the flat front face of the block on top of the isometric shading so it still reads clearly in the grid.

draw()

This tiny dispatcher function is the entire 'screen manager' for the whole app - a common and simple way to build multi-screen p5.js sketches using one string variable (gameState) instead of separate HTML pages.

function draw() {
  if (gameState === 'HOME') drawHome();
  else if (gameState === 'CREDITS') drawCredits();
  else if (gameState === 'PLAY' || gameState === 'INSIDE_HOUSE') drawGame();
}
Line-by-line explanation (3 lines)

🔧 Subcomponents:

conditional Game State Dispatcher if (gameState === 'HOME') drawHome();

Routes each frame's rendering to a different function depending on which 'screen' the player is currently on.

if (gameState === 'HOME') drawHome();
Draws the title screen with the bouncing AI logo and Play/Credits buttons.
else if (gameState === 'CREDITS') drawCredits();
Draws the simple credits screen with a Back button.
else if (gameState === 'PLAY' || gameState === 'INSIDE_HOUSE') drawGame();
Draws the main gameplay - the grid, blocks, AIs, and UI - whether the player is outside or inside the house.

drawHome()

Drawing the same dark text slightly offset behind a lighter copy is a cheap, GPU-free way to fake a drop shadow in plain 2D p5.js.

function drawHome() {
  cursor(ARROW); background(20, 30, 45); ais[0].x = width / 2; ais[0].y = height / 2 - 120; ais[0].draw();
  textAlign(CENTER, CENTER); textSize(64); textStyle(BOLD); fill(0, 150); text("Tophat AI", width / 2 + 4, height / 2 - 26); 
  fill(255); text("Tophat AI", width / 2, height / 2 - 30); textStyle(NORMAL); 
  drawMenuButton("Play", width / 2 - 100, height / 2 + 40, 200, 50);
  drawMenuButton("Credits", width / 2 - 100, height / 2 + 110, 200, 50);
}
Line-by-line explanation (4 lines)
ais[0].x = width / 2; ais[0].y = height / 2 - 120; ais[0].draw();
Forces the very first AI to sit centered above the title text and draws it, reusing the same draw() method used in-game.
fill(0, 150); text("Tophat AI", width / 2 + 4, height / 2 - 26);
Draws a semi-transparent black copy of the title slightly offset, creating a drop-shadow effect before the main white text.
fill(255); text("Tophat AI", width / 2, height / 2 - 30);
Draws the crisp white title text directly on top of its shadow copy.
drawMenuButton("Play", width / 2 - 100, height / 2 + 40, 200, 50);
Draws a 200x50 clickable button labeled 'Play' centered horizontally below the title.

drawCredits()

A minimal screen that demonstrates reusing drawMenuButton() across multiple game states instead of duplicating button-drawing code.

function drawCredits() {
  cursor(ARROW); background(20, 30, 45); textAlign(CENTER, CENTER); textSize(40); fill(255);
  text("Made by Corbun", width / 2, height / 2 - 50); drawMenuButton("Back", width / 2 - 100, height / 2 + 50, 200, 50);
}
Line-by-line explanation (3 lines)
background(20, 30, 45);
Fills the whole canvas with the same dark navy background used on the home screen for visual consistency.
text("Made by Corbun", width / 2, height / 2 - 50);
Draws the credits text centered on screen using the alignment set on the previous line.
drawMenuButton("Back", width / 2 - 100, height / 2 + 50, 200, 50);
Reuses the same button-drawing helper as the home screen to keep a consistent look.

drawGame()

drawGame() is the heart of the sketch, running gravity simulation, automatic car/house behaviors, block rendering, falling-block animation, and AI updates all in a single, carefully ordered frame - a good study in how a complex p5.js draw() loop can stay organized with clear comment sections.

🔬 This loop runs every single frame checking all columns for floating blocks. What do you predict would happen to performance and stacking if you deleted this whole gravity block?

  for (let c = 0; c < cols; c++) {
    if (!activeGrid[c]) continue;
    for (let r = rows - 2; r >= 0; r--) {
      if (activeGrid[c][r] > 0 && activeGrid[c][r+1] === 0) {
        activeFalling.push({ c: c, r: r+1, y: r * blockSize, type: activeGrid[c][r] });
        activeGrid[c][r] = 0; activeGrid[c][r+1] = -1; 
      }
    }
  }
function drawGame() {
  let isInside = (gameState === 'INSIDE_HOUSE');
  let activeGrid = isInside ? insideGrid : grid;
  let activeFalling = isInside ? insideFallingBlocks : fallingBlocks;
  
  if (isInside) { background(45, 30, 25); fill(70, 45, 30); noStroke(); rect(0, height - 2*blockSize, width, 2*blockSize); } 
  else { background(20, 30, 45); }
  
  playMusic(); 
  if (globalReceivingData > 0) globalReceivingData--;
  
  stroke(255, 255, 255, 10); strokeWeight(1);
  for (let i = 0; i <= cols; i++) line(i * blockSize, 0, i * blockSize, height);
  for (let j = 0; j <= rows; j++) line(0, j * blockSize, width, j * blockSize);

  // Gravity
  for (let c = 0; c < cols; c++) {
    if (!activeGrid[c]) continue;
    for (let r = rows - 2; r >= 0; r--) {
      if (activeGrid[c][r] > 0 && activeGrid[c][r+1] === 0) {
        activeFalling.push({ c: c, r: r+1, y: r * blockSize, type: activeGrid[c][r] });
        activeGrid[c][r] = 0; activeGrid[c][r+1] = -1; 
      }
    }
  }

  // Auto-drive & Board House logic
  if (!isInside && ais.some(a => !a.isTemp && a.mode !== 'DESTROY')) {
    carExists = false; let carCenterX = 0, carTopY = height, carBlockCount = 0;
    let houseBlockCount = 0, houseCenterX = 0, houseDoorY = 0;
    for (let c = 0; c < cols; c++) {
      if (!activeGrid[c]) continue;
      for (let r = 0; r < rows; r++) {
        let type = activeGrid[c][r];
        if (type >= 30 && type <= 32) { carExists = true; carCenterX += (c * blockSize + blockSize/2); carTopY = min(carTopY, r * blockSize); carBlockCount++; }
        if (type === 4) { houseBlockCount++; houseCenterX += (c * blockSize + blockSize/2); houseDoorY = max(houseDoorY, r * blockSize); }
      }
    }
    
    let houseExists = houseBlockCount >= 10; 
    if (globalTaskQueue.length === 0 && activeFalling.length === 0) {
      if (carExists && carBlockCount > 0) {
        carCenterX /= carBlockCount;
        let driver = ais.find(a => !a.isTemp && (a.mode === 'DRIVING' || a.mode === 'BOARDING_CAR'));
        if (!driver) driver = ais.find(a => !a.isTemp);
        if (driver && driver.mode !== 'BOARDING_CAR' && driver.mode !== 'DRIVING') { driver.mode = 'BOARDING_CAR'; currentTaskName = "Boarding Car..."; }
        if (driver && driver.mode === 'BOARDING_CAR') {
          driver.tx = carCenterX; driver.ty = carTopY - 25;
          if (abs(driver.x - driver.tx) < 5 && abs(driver.y - driver.ty) < 5) { driver.mode = 'DRIVING'; currentTaskName = "Driving Car! 🚗"; }
        }
        if (driver && driver.mode === 'DRIVING') {
          driver.tx = carCenterX; driver.ty = carTopY - 25;
          if (frameCount % 15 === 0) { let moved = moveCar(driver.carDir); if (!moved) driver.carDir *= -1; }
        }
      } else if (houseExists && !hasFurnishedHouse) {
        let leader = ais.find(a => !a.isTemp);
        houseCenterX /= houseBlockCount;
        if (leader && leader.mode !== 'BOARDING_HOUSE') { leader.mode = 'BOARDING_HOUSE'; currentTaskName = "Entering House..."; }
        if (leader && leader.mode === 'BOARDING_HOUSE') {
          leader.tx = houseCenterX; leader.ty = houseDoorY - blockSize/2;
          if (abs(leader.x - leader.tx) < 5 && abs(leader.y - leader.ty) < 5) {
            gameState = 'INSIDE_HOUSE'; playSFX('door');
            ais.forEach(a => { if(!a.isTemp){ a.y = 100; a.tx = random(width/3, (width/3)*2); }});
            updateUILayout(); queueFurniture(); 
          }
        }
      } else {
        ais.forEach(a => { if (!a.isTemp && (a.mode === 'BOARDING_CAR' || a.mode === 'DRIVING' || a.mode === 'BOARDING_HOUSE')) { a.mode = 'IDLE'; a.ty = 120; currentTaskName = "Idle"; }});
      }
    }
  }

  // Draw Static Blocks
  if (is3DMode) {
    for (let j = 0; j < rows; j++) { 
      for (let i = cols - 1; i >= 0; i--) {
        if (activeGrid[i] && activeGrid[i][j] > 0) drawBlock(i * blockSize, j * blockSize, activeGrid[i][j]); 
      }
    }
  } else {
    for (let i = 0; i < cols; i++) { 
      if (!activeGrid[i]) continue;
      for (let j = 0; j < rows; j++) {
        if (activeGrid[i][j] > 0) drawBlock(i * blockSize, j * blockSize, activeGrid[i][j]); 
      }
    }
  }

  // Draw Falling Blocks
  if (is3DMode) activeFalling.sort((a, b) => { if (abs(a.y - b.y) > 5) return a.y - b.y; return b.c - a.c; });
  for (let i = activeFalling.length - 1; i >= 0; i--) {
    let b = activeFalling[i]; let targetY = b.r * blockSize; b.y += 22; 
    if (b.y >= targetY) { 
      b.y = targetY; 
      if (activeGrid[b.c]) activeGrid[b.c][b.r] = b.type; // Safely place block
      activeFalling.splice(i, 1); 
      playSFX('land', b.r); 
    } else { 
      drawBlock(b.c * blockSize, b.y, b.type); 
    }
  }

  // UPDATE AND DRAW AIs (Reverse order so temp AIs can properly remove themselves)
  for (let i = ais.length - 1; i >= 0; i--) {
    let a = ais[i];
    a.update();
    a.draw();
    if (a.isTemp && a.mode === 'LEAVING' && a.y < -150) {
      ais.splice(i, 1); // Delete the temporary lumberjack!
    }
  }

  drawUI();

  if (deleteMode && mouseY > uiPanelHeight) {
    noCursor(); push(); textSize(36); textAlign(CENTER, CENTER); text("🔨", mouseX, mouseY - 10); pop();
  } else cursor(ARROW);
}
Line-by-line explanation (7 lines)

🔧 Subcomponents:

for-loop Apply Gravity if (activeGrid[c][r] > 0 && activeGrid[c][r+1] === 0) {

Scans every column for a block with empty space directly below it and turns that block into a falling block, simulating gravity.

for-loop Detect Car & House Blocks if (type >= 30 && type <= 32) { carExists = true; carCenterX += (c * blockSize + blockSize/2); carTopY = min(carTopY, r * blockSize); carBlockCount++; }

Scans the whole grid each frame to check whether a car or a house has been built, and computes their center position.

conditional Board Car / Enter House if (carExists && carBlockCount > 0) {

Automatically walks an AI to a built car (to drive it back and forth) or a built house (to walk inside and switch game states).

for-loop Animate Falling Blocks for (let i = activeFalling.length - 1; i >= 0; i--) {

Moves every currently-falling block down toward its target row each frame, placing it into the grid once it arrives.

for-loop Update & Draw Every AI for (let i = ais.length - 1; i >= 0; i--) {

Runs each AI's update() and draw() methods in reverse order so temporary lumberjacks can safely remove themselves from the array mid-loop.

let activeGrid = isInside ? insideGrid : grid;
Chooses which of the two parallel worlds (outdoor vs. indoor) to read and modify this frame.
if (globalReceivingData > 0) globalReceivingData--;
Counts down the 'receiving blueprint' animation timer every frame until it reaches zero.
activeFalling.push({ c: c, r: r+1, y: r * blockSize, type: activeGrid[c][r] });
When gravity detects a floating block, it's converted into a falling-block object that will animate down one row.
let houseExists = houseBlockCount >= 10;
Considers a house 'built' once at least 10 roof/wall blocks (type 4) exist, a simple heuristic rather than checking exact shape.
if (frameCount % 15 === 0) { let moved = moveCar(driver.carDir); if (!moved) driver.carDir *= -1; }
Every 15 frames, tries to move the car one column in its current direction; if it's blocked, it reverses direction next time.
let b = activeFalling[i]; let targetY = b.r * blockSize; b.y += 22;
Moves the falling block's pixel y-position down by 22 pixels this frame, animating its fall toward its target row.
if (a.isTemp && a.mode === 'LEAVING' && a.y < -150) { ais.splice(i, 1); }
Once a temporary lumberjack has flown far enough above the screen, it's permanently removed from the ais array to free memory.

drawUI()

drawUI() shows a pattern common in immediate-mode UI: every frame, re-check mouse position against every button's stored rectangle and pick a fill color accordingly, rather than using persistent DOM button elements.

🔬 Hover states brighten a color channel from 150/50 up to 200/50 on the clear/exit buttons. What happens if you swap those numbers so hovering makes the button DARKER instead of brighter?

  for (let b of btns) {
    if (!b.visible) continue;
    let isHover = isInside(mouseX, mouseY, b.x, b.y, b.w, b.h);
    if (b.type === 'clear' || b.type === 'exit') fill(isHover ? 200 : 150, 50, 50);
function drawUI() {
  textSize(18); textAlign(LEFT, TOP);
  fill(0, 150); text("Status: " + currentTaskName, 17, 17);
  fill(255); text("Status: " + currentTaskName, 15, 15);

  if (carExists && ais.some(a => a.mode === 'DRIVING') && gameState === 'PLAY') {
    fill(0, 150); text("Tophat AI is taking the car for a spin!", 17, 42); fill(255, 200, 0); text("Tophat AI is taking the car for a spin!", 15, 40);
  } else if (gameState === 'INSIDE_HOUSE') {
    fill(0, 150); text("Cozy House Interior 🏠", 17, 42); fill(255, 200, 100); text("Cozy House Interior 🏠", 15, 40);
  }

  for (let b of btns) {
    if (!b.visible) continue;
    let isHover = isInside(mouseX, mouseY, b.x, b.y, b.w, b.h);
    if (b.type === 'clear' || b.type === 'exit') fill(isHover ? 200 : 150, 50, 50);
    else if (b.type === 'upload' || b.type === 'add_ai') fill(isHover ? 100 : 50, 200, 100);
    else if (b.type === 'build_city') fill(isHover ? 150 : 100, 50, 200);
    else if (b.type === 'toggle_3d') fill(is3DMode ? 255 : (isHover ? 150 : 100), is3DMode ? 200 : 100, is3DMode ? 50 : 255);
    else if (b.type === 'delete') fill(deleteMode ? 220 : (isHover ? 80 : 50), deleteMode ? 80 : (isHover ? 150 : 100), deleteMode ? 80 : 255);
    else fill(isHover ? 80 : 50, isHover ? 150 : 100, 255);
    
    stroke(255, 100); strokeWeight(2); rect(b.x, b.y, b.w, b.h, 6);
    fill(is3DMode && b.type === 'toggle_3d' ? 0 : 255); noStroke(); textAlign(CENTER, CENTER); textSize(14);
    text(b.label, b.x + b.w / 2, b.y + b.h / 2);
  }

  if (gameState === 'PLAY') {
    let gearHover = isInside(mouseX, mouseY, width - 55, 10, 40, 40);
    fill(gearHover ? 80 : 40, 150); stroke(255, 100); strokeWeight(2); rect(width - 55, 10, 40, 40, 8);
    textSize(24); textAlign(CENTER, CENTER); noStroke(); fill(255); text("⚙️", width - 35, 30);
  }
}
Line-by-line explanation (4 lines)

🔧 Subcomponents:

for-loop Draw Each Visible Button for (let b of btns) {

Iterates over every button, skipping hidden ones, and colors each differently based on its type and hover state.

fill(0, 150); text("Status: " + currentTaskName, 17, 17);
Draws a semi-transparent black shadow copy of the status text slightly offset, again faking a drop shadow.
let isHover = isInside(mouseX, mouseY, b.x, b.y, b.w, b.h);
Checks whether the mouse is currently over this specific button, used to brighten its color.
if (b.type === 'clear' || b.type === 'exit') fill(isHover ? 200 : 150, 50, 50);
Colors destructive/exit-style buttons reddish, brightening the red channel further on hover.
fill(deleteMode ? 220 : (isHover ? 80 : 50), deleteMode ? 80 : (isHover ? 150 : 100), deleteMode ? 80 : 255);
The Delete button turns solid red whenever deleteMode is active, overriding the normal hover-blue coloring to clearly signal the mode is on.

moveCar(dir)

moveCar() treats a scattered group of grid cells as one rigid object by validating every block's destination before committing any change - a useful technique whenever you need to move a multi-cell shape as a unit.

🔬 The car is allowed to drive over its own block types (30-32) but nothing else. What happens if you remove that exception so ANY occupied cell - even another part of the same car - stops it from moving?

  for (let b of carBlocks) {
    let nc = b.c + dir; 
    if (nc < 0 || nc >= cols) { canMove = false; break; }
    if (!grid[nc]) { canMove = false; break; }
    let targetCell = grid[nc][b.r]; 
    if (targetCell !== 0 && !(targetCell >= 30 && targetCell <= 32)) { canMove = false; break; }
  }
function moveCar(dir) {
  let carBlocks = [];
  for (let c = 0; c < cols; c++) {
    if (!grid[c]) continue;
    for (let r = 0; r < rows; r++) {
      if (grid[c][r] >= 30 && grid[c][r] <= 32) carBlocks.push({c: c, r: r, type: grid[c][r]});
    }
  }
  if (carBlocks.length === 0) return false;

  let canMove = true;
  for (let b of carBlocks) {
    let nc = b.c + dir; 
    if (nc < 0 || nc >= cols) { canMove = false; break; }
    if (!grid[nc]) { canMove = false; break; }
    let targetCell = grid[nc][b.r]; 
    if (targetCell !== 0 && !(targetCell >= 30 && targetCell <= 32)) { canMove = false; break; }
  }

  if (canMove) {
    for (let b of carBlocks) if (grid[b.c]) grid[b.c][b.r] = 0;
    for (let b of carBlocks) if (grid[b.c + dir]) grid[b.c + dir][b.r] = b.type;
    return true; 
  }
  return false; 
}
Line-by-line explanation (3 lines)

🔧 Subcomponents:

for-loop Find All Car Blocks if (grid[c][r] >= 30 && grid[c][r] <= 32) carBlocks.push({c: c, r: r, type: grid[c][r]});

Scans the entire outdoor grid to locate every block belonging to the car (types 30-32) so they can be moved together.

for-loop Check For Obstacles if (targetCell !== 0 && !(targetCell >= 30 && targetCell <= 32)) { canMove = false; break; }

Verifies every car block has clear space to move into (either empty or another car block), preventing the car from driving through walls.

let nc = b.c + dir;
Calculates the column this particular car block would move into if the car shifts one step in direction 'dir' (+1 or -1).
if (targetCell !== 0 && !(targetCell >= 30 && targetCell <= 32)) { canMove = false; break; }
Blocks the move only if the destination cell is occupied by something that ISN'T part of the car itself, since the car's own blocks obviously occupy space it's about to vacate.
for (let b of carBlocks) if (grid[b.c]) grid[b.c][b.r] = 0;
Clears every old car block position before writing the new ones, avoiding leaving duplicate car pieces behind.

drawMenuButton(label, x, y, w, h)

A small reusable helper function - both drawHome() and drawCredits() call this instead of duplicating button-drawing code, which is a good habit anytime you draw the same shape with different labels.

function drawMenuButton(label, x, y, w, h) {
  let isHover = isInside(mouseX, mouseY, x, y, w, h);
  fill(isHover ? 80 : 50, isHover ? 150 : 100, 255);
  stroke(255, 100); strokeWeight(2); rect(x, y, w, h, 8);
  fill(255); noStroke(); textSize(24); text(label, x + w / 2, y + h / 2);
}
Line-by-line explanation (3 lines)
let isHover = isInside(mouseX, mouseY, x, y, w, h);
Checks whether the mouse cursor currently sits within this button's rectangle.
fill(isHover ? 80 : 50, isHover ? 150 : 100, 255);
Uses a blue button color that brightens slightly whenever the mouse is hovering, giving instant visual feedback.
text(label, x + w / 2, y + h / 2);
Draws the button's label text centered exactly in the middle of its rectangle.

isInside(mx, my, x, y, w, h)

This one-line helper is reused everywhere the sketch needs to know 'is the mouse over this button/rectangle?' - buttons, the gear icon, and menu buttons all call it instead of repeating the same four comparisons.

function isInside(mx, my, x, y, w, h) { return mx > x && mx < x + w && my > y && my < y + h; }
Line-by-line explanation (1 lines)
return mx > x && mx < x + w && my > y && my < y + h;
Standard rectangle hit-test: the point (mx, my) is inside the rectangle only if it's simultaneously to the right of the left edge, left of the right edge, below the top edge, and above the bottom edge.

mousePressed()

mousePressed() is a special p5.js function name that automatically runs whenever the mouse button is clicked - no need to set up your own event listener.

function mousePressed() { handleInput(); return false; }
Line-by-line explanation (2 lines)
handleInput();
Delegates all the actual click logic to handleInput(), keeping this p5.js built-in callback tiny.
return false;
Tells the browser not to perform its default behavior for this click (like text selection), which is especially important on touch devices.

touchStarted()

Handling both mousePressed() and touchStarted() with the same handleInput() call is how this sketch supports desktop and mobile input with a single code path.

function touchStarted() { handleInput(); return false; }
Line-by-line explanation (1 lines)
handleInput(); return false;
Mirrors mousePressed() so the exact same click logic works when a player taps the screen on a phone or tablet instead of clicking with a mouse.

mouseDragged()

mouseDragged() fires repeatedly while dragging, which is what makes Delete Mode feel like painting away blocks as you sweep the mouse across the grid.

function mouseDragged() { handleDrag(); return false; }
Line-by-line explanation (1 lines)
handleDrag(); return false;
Whenever the mouse moves while a button is held down, this calls handleDrag() to continuously delete blocks in Delete Mode.

touchMoved()

Together, mouseDragged() and touchMoved() ensure the same drag-to-delete gesture works identically with a mouse or a finger.

function touchMoved() { handleDrag(); return false; }
Line-by-line explanation (1 lines)
handleDrag(); return false;
The touch equivalent of mouseDragged(), letting mobile players swipe across blocks to delete them in Delete Mode.

handleDrag()

Converting pixel coordinates into grid indices with floor(mouseX / blockSize) is the fundamental technique used throughout this sketch to map mouse input onto the grid world.

function handleDrag() {
  if ((gameState === 'PLAY' || gameState === 'INSIDE_HOUSE') && deleteMode && mouseY > uiPanelHeight) {
    let clickedCol = floor(mouseX / blockSize), clickedRow = floor(mouseY / blockSize);
    deleteBlock(clickedCol, clickedRow);
  }
}
Line-by-line explanation (3 lines)
if ((gameState === 'PLAY' || gameState === 'INSIDE_HOUSE') && deleteMode && mouseY > uiPanelHeight) {
Only responds to dragging while actually in gameplay, with Delete Mode switched on, and below the UI button panel so buttons aren't accidentally triggered.
let clickedCol = floor(mouseX / blockSize), clickedRow = floor(mouseY / blockSize);
Converts the mouse's pixel coordinates into grid column/row indices by dividing by blockSize and rounding down.
deleteBlock(clickedCol, clickedRow);
Attempts to delete whatever block sits at that grid cell.

handleInput()

handleInput() is the top-level click router: it checks the current gameState first and only handles the buttons relevant to that specific screen, avoiding overlapping click zones between screens.

function handleInput() {
  initAudio(); lastDeletedCell = { c: -1, r: -1 }; 
  if (gameState === 'HOME') {
    if (isInside(mouseX, mouseY, width / 2 - 100, height / 2 + 40, 200, 50)) { gameState = 'PLAY'; ais.forEach(a => { if(!a.isTemp) { a.y = 120; a.tx = width / 2; }}); updateUILayout(); } 
    else if (isInside(mouseX, mouseY, width / 2 - 100, height / 2 + 110, 200, 50)) { gameState = 'CREDITS'; }
  } 
  else if (gameState === 'CREDITS') { if (isInside(mouseX, mouseY, width / 2 - 100, height / 2 + 50, 200, 50)) gameState = 'HOME'; } 
  else if (gameState === 'PLAY' || gameState === 'INSIDE_HOUSE') handleGameInput();
}
Line-by-line explanation (3 lines)

🔧 Subcomponents:

conditional Home Screen Buttons if (isInside(mouseX, mouseY, width / 2 - 100, height / 2 + 40, 200, 50)) { gameState = 'PLAY'; ... }

Checks whether the Play or Credits button was clicked on the home screen and switches gameState accordingly.

initAudio();
Called on every click so the very first click anywhere unlocks the browser's audio engine, satisfying the autoplay policy.
lastDeletedCell = { c: -1, r: -1 };
Resets the drag-delete tracker so a fresh click can always delete a block even if it's the same cell deleted just before.
else if (gameState === 'PLAY' || gameState === 'INSIDE_HOUSE') handleGameInput();
Once in actual gameplay, delegates all button/grid click handling to the more detailed handleGameInput() function.

handleGameInput()

This is the biggest input-handling function in the sketch: it demonstrates checking a gear icon, looping over a whole button array with a big if/else-if chain per button type, and falling back to a raw grid click when nothing else was hit.

🔬 Ten temporary lumberjacks get summoned to help demolish the build. What might happen to frame rate if you bump that number up to 100?

        if (globalDestroyQueue.length > 0) {
          globalTaskQueue = []; // Cancel builds
          globalDestroyQueue = shuffle(globalDestroyQueue); // Random chopping
          currentTaskName = "Chopping everything down! 🪓";
          hasFurnishedHouse = false;
          
          // Set normal AIs to DESTROY
          ais.forEach(a => { a.mode = 'DESTROY'; a.hasDestroyTarget = false; });
          
          // Summon 10 extra temporary lumberjacks!
          for(let i = 0; i < 10; i++) {
             ais.push(createAI(random(-100, width + 100), -50, true));
          }
        }
function handleGameInput() {
  if (gameState === 'PLAY' && isInside(mouseX, mouseY, width - 55, 10, 40, 40)) { advancedMode = !advancedMode; updateUILayout(); return; }

  for (let b of btns) {
    if (b.visible && isInside(mouseX, mouseY, b.x, b.y, b.w, b.h)) {
      if (b.type === 'delete') { deleteMode = !deleteMode; b.label = deleteMode ? "🔨 Delete: ON" : "🔨 Delete: OFF"; }
      else if (b.type === 'upload') fileInput.elt.click(); 
      else if (b.type === 'add_ai') ais.push(createAI(random(50, width-50), random(80, 150), false)); 
      else if (b.type === 'toggle_3d') { is3DMode = !is3DMode; b.label = is3DMode ? "🧊 Make 3D: ON" : "🧊 Make 3D: OFF"; }
      else if (b.type === 'exit') {
        gameState = 'PLAY'; playSFX('door');
        ais.forEach(a => { if(!a.isTemp) { a.mode = 'IDLE'; a.y = 120; }}); currentTaskName = "Idle"; updateUILayout();
      }
      else if (b.type === 'clear') {
        // LUMBERJACK SUMMONING!
        if (ais.some(a => a.mode === 'DESTROY')) return; // Prevent spamming
        
        let activeGrid = gameState === 'INSIDE_HOUSE' ? insideGrid : grid;
        globalDestroyQueue = [];
        
        for (let c = 0; c < cols; c++) {
          if (!activeGrid[c]) continue;
          for (let r = 0; r < rows; r++) {
            if (activeGrid[c][r] > 0) globalDestroyQueue.push({c: c, r: r});
          }
        }

        if (globalDestroyQueue.length > 0) {
          globalTaskQueue = []; // Cancel builds
          globalDestroyQueue = shuffle(globalDestroyQueue); // Random chopping
          currentTaskName = "Chopping everything down! 🪓";
          hasFurnishedHouse = false;
          
          // Set normal AIs to DESTROY
          ais.forEach(a => { a.mode = 'DESTROY'; a.hasDestroyTarget = false; });
          
          // Summon 10 extra temporary lumberjacks!
          for(let i = 0; i < 10; i++) {
             ais.push(createAI(random(-100, width + 100), -50, true));
          }
        } else {
           playSFX('clear'); // Already empty
        }
      } 
      else sendBlueprint(b.type);
      return; 
    }
  }

  if (mouseY > uiPanelHeight) {
    let clickedCol = floor(mouseX / blockSize), clickedRow = floor(mouseY / blockSize);
    if (deleteMode) deleteBlock(clickedCol, clickedRow); else dropBlock(clickedCol, 1);
  }
}
Line-by-line explanation (5 lines)

🔧 Subcomponents:

for-loop Check Which Button Was Clicked for (let b of btns) {

Loops through every button and, if the click landed inside a visible one, runs the matching action for its type.

conditional Summon Lumberjacks if (globalDestroyQueue.length > 0) {

Builds the destroy queue from every occupied cell, switches all AIs to DESTROY mode, and spawns 10 temporary lumberjack AIs to help chop everything down.

conditional Manual Click On Grid if (mouseY > uiPanelHeight) {

If the click wasn't on any button, treats it as a direct click on the grid - deleting or manually dropping a single block.

if (gameState === 'PLAY' && isInside(mouseX, mouseY, width - 55, 10, 40, 40)) { advancedMode = !advancedMode; updateUILayout(); return; }
Clicking the gear icon in the top-right toggles Advanced Mode (which swaps which buttons are visible) and immediately recalculates the button layout.
else if (b.type === 'upload') fileInput.elt.click();
Programmatically clicks the hidden HTML file input element, opening the browser's file picker dialog even though the real input is invisible.
if (ais.some(a => a.mode === 'DESTROY')) return; // Prevent spamming
Stops the player from spamming the Chop All button while a chopping session is already in progress, avoiding duplicate lumberjack armies.
for(let i = 0; i < 10; i++) { ais.push(createAI(random(-100, width + 100), -50, true)); }
Spawns exactly 10 temporary lumberjack AIs at random positions above the screen, each starting in DESTROY mode (set inside createAI's isTemp check).
if (deleteMode) deleteBlock(clickedCol, clickedRow); else dropBlock(clickedCol, 1);
If no button was clicked and Delete Mode is off, a direct click on the grid manually drops a blue block (type 1) into that column.

windowResized()

windowResized() is a special p5.js function that runs automatically whenever the browser window changes size, letting the sketch stay responsive across different screens and orientations.

function windowResized() { resizeCanvas(windowWidth, windowHeight); initGrid(); updateUILayout(); }
Line-by-line explanation (3 lines)
resizeCanvas(windowWidth, windowHeight);
Resizes the p5.js canvas to match the browser window's new dimensions whenever it changes (like rotating a phone).
initGrid();
Recomputes cols/rows for the new canvas size and rebuilds empty grid arrays - though this also erases any structure the player had already built.
updateUILayout();
Recalculates button positions so the UI panel still fits and wraps correctly at the new window size.

📦 Key Variables

blockSize number

The pixel width/height of every grid cell, driving cols/rows and every structure's proportions.

let blockSize = 40;
cols number

How many grid columns fit across the canvas, calculated from width and blockSize.

let cols;
rows number

How many grid rows fit down the canvas, calculated from height and blockSize.

let rows;
grid array

The outdoor world's 2D array of block type numbers (0 = empty), indexed as grid[col][row].

let grid = [];
fallingBlocks array

List of blocks currently animating downward outside, before they settle into the grid array.

let fallingBlocks = [];
insideGrid array

A separate 2D grid used only while the player is inside the house.

let insideGrid = [];
insideFallingBlocks array

Falling-block animation list for the indoor world, kept separate from the outdoor one.

let insideFallingBlocks = [];
hasFurnishedHouse boolean

Tracks whether queueFurniture() has already run, preventing duplicate furniture from being queued every time the player re-enters the house.

let hasFurnishedHouse = false;
ais array

Every active AI object (permanent workers and temporary lumberjacks), each updated and drawn every frame.

let ais = [];
globalTaskQueue array

Shared FIFO queue of build tasks ({col, type}) that any BUILD-mode AI can pull from.

let globalTaskQueue = [];
globalDestroyQueue array

Shared list of grid coordinates waiting to be chopped down by DESTROY-mode AIs.

let globalDestroyQueue = [];
globalReceivingData number

Countdown timer that shows a pulsing 'receiving blueprint' animation above AI heads right after a new build is sent.

let globalReceivingData = 0;
btns array

All possible UI buttons as data objects with label, type, and computed position/visibility.

let btns = [];
currentTaskName string

The human-readable status text shown in the top-left corner describing what the AIs are currently doing.

let currentTaskName = "Idle";
uiPanelHeight number

The pixel height of the button panel, used to keep grid clicks from being mistaken for button clicks.

let uiPanelHeight = 100;
carExists boolean

Whether a car (block types 30-32) currently exists somewhere in the outdoor grid.

let carExists = false;
advancedMode boolean

Toggles between the Basic button set (simple shapes) and the Advanced button set (upload, 3D, add AI, build city).

let advancedMode = false;
is3DMode boolean

Switches block rendering between flat 2D squares and shaded pseudo-3D isometric cubes.

let is3DMode = false;
deleteMode boolean

When true, clicking or dragging on the grid deletes blocks instead of dropping new ones.

let deleteMode = false;
lastDeletedCell object

Remembers the last cell deleted during a drag gesture so the same cell isn't deleted repeatedly in one motion.

let lastDeletedCell = { c: -1, r: -1 };
fileInput object

The hidden p5.js file input element used to let players upload an image blueprint.

let fileInput;
customColors object

Lookup table mapping generated custom color IDs (100+) to actual RGB colors sampled from uploaded images.

let customColors = {};
nextColorId number

An incrementing counter used to generate a unique block-type id for every distinct pixel color queued from an image.

let nextColorId = 100;
gameState string

The current screen/mode of the app (HOME, CREDITS, PLAY, INSIDE_HOUSE), driving which draw function runs.

let gameState = 'HOME';
audioInitialized boolean

Tracks whether the audio engine has already been unlocked, so initAudio() only sets things up once.

let audioInitialized = false;
bgSynth object

The p5.PolySynth instance used exclusively for background music chords.

let bgSynth;
sfxSynth object

The p5.PolySynth instance used exclusively for short sound effects.

let sfxSynth;
chords array

A fixed chord progression (arrays of note names) that playMusic() cycles through for ambient background music.

let chords = [['C3','E3','G3','B3']];
chordIndex number

Tracks which chord in the progression should play next.

let chordIndex = 0;
sfxRateLimits object

Stores the last frameCount each sound effect type played, used to throttle sounds and prevent audio engine crashes.

let sfxRateLimits = { 'receive': 0, 'drop': 0, 'break': 0, 'door': 0, 'land': 0, 'clear': 0 };

🔧 Potential Improvements (4)

Here are some ways this code could be enhanced:

BUG windowResized()

Calling initGrid() on every window resize completely wipes any structure the player has built, which can feel jarring if the user simply resizes their browser or rotates a mobile device mid-build.

💡 Store the old grid, compute new cols/rows, and copy overlapping cells into the resized arrays instead of resetting everything to empty.

PERFORMANCE buildFromImage()

Large or highly detailed images can queue thousands of tasks into globalTaskQueue with no upper limit besides the 32x32 downscale cap, and each unique color allocates a new customColors entry that's never cleaned up.

💡 Cap the maximum queued tasks, and consider clearing unused customColors entries when a build is cleared or replaced.

STYLE getBlockColor() and drawBlock()

Both functions use long if/else-if chains keyed on 'magic number' type codes (2, 4, 30-32, 40-45, 99), making it easy to introduce typos and hard to see the full set of block types at a glance.

💡 Replace the chains with a single lookup object like BLOCK_COLORS = {2: color(...), 4: color(...), ...} that both functions can reference.

FEATURE Overall game

There's no way to save, export, or undo a build - accidentally clicking 'Chop All' or refreshing the page permanently loses everything the player made.

💡 Add a simple save/load using localStorage to serialize the grid array to JSON, plus an undo stack for the last few dropBlock/deleteBlock actions.

🔄 Code Flow

Code flow showing setup, createai, setupui, updateuilayout, handlefile, buildfromimage, initaudio, playsfx, playmusic, initgrid, queuestructure, sendblueprint, queuefurniture, shuffle, dropblock, deleteblock, getblockcolor, drawblock, draw, drawhome, drawcredits, drawgame, drawui, movecar, drawmenubutton, isinside, mousepressed, touchstarted, mousedragged, touchmoved, handledrag, handleinput, handlegameinput, windowresized

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

graph TD start[Start] --> setup[setup] setup --> draw[draw loop] draw --> aiupdate[ai-update-loop] aiupdate --> updateuilayout[updateuilayout] aiupdate --> gravityloop[gravity-loop] aiupdate --> fallingblocksloop[falling-blocks-loop] aiupdate --> carhousescan[car-house-scan] aiupdate --> drawui[drawui] aiupdate --> drawgame[drawgame] drawgame --> buildtaskloop[build-task-loop] buildtaskloop --> idlebuild[idle-random-build] buildtaskloop --> dropblock[dropblock] buildtaskloop --> checkcanmove[check-can-move] fallingblocksloop --> findlandingrow[find-landing-row] gravityloop --> columnscan[column-scan] columnscan --> opacitycheck[opacity-check] columnscan --> supportcheck[support-check] drawui --> buttonrenderloop[button-render-loop] buttonrenderloop --> buttonhit[button-hit-test] buttonhit --> homeinput[home-input] buttonhit --> clear召唤[clear-summon] buttonhit --> gridclickfallback[grid-click-fallback] click setup href "#fn-setup" click draw href "#fn-draw" click aiupdate href "#sub-ai-update-loop" click updateuilayout href "#fn-updateuilayout" click gravityloop href "#sub-gravity-loop" click fallingblocksloop href "#sub-falling-blocks-loop" click carhousescan href "#sub-car-house-scan" click drawui href "#fn-drawui" click drawgame href "#fn-drawgame" click buildtaskloop href "#sub-build-task-loop" click idlebuild href "#sub-idle-random-build" click dropblock href "#fn-dropblock" click checkcanmove href "#sub-check-can-move" click findlandingrow href "#sub-find-landing-row" click columnscan href "#sub-column-scan" click opacitycheck href "#sub-opacity-check" click supportcheck href "#sub-support-check" click buttonrenderloop href "#sub-button-render-loop" click buttonhit href "#sub-button-hit-test" click homeinput href "#sub-home-input" click clear召唤 href "#sub-clear-summon" click gridclickfallback href "#sub-grid-click-fallback"

❓ Frequently Asked Questions

What visual experience does the Top Hat AI sketch provide?

The Top Hat AI sketch creates a dynamic and colorful pixelated world where a swarm of friendly AIs constructs and organizes vibrant block structures on a scrolling grid.

How can users interact with the Top Hat AI sketch?

Users can interact with the sketch by clicking UI buttons to switch modes, send tasks to the AI swarm, and watch as they build and manipulate structures in real-time.

What creative coding techniques are showcased in the Top Hat AI sketch?

This sketch demonstrates techniques such as swarm behavior for AI agents, grid-based building mechanics, and interactive UI design to enhance user engagement.

Preview

Top Hat AI - p5.js creative coding sketch preview
Sketch Preview
Code flow diagram showing the structure of Top Hat AI - Code flow showing setup, createai, setupui, updateuilayout, handlefile, buildfromimage, initaudio, playsfx, playmusic, initgrid, queuestructure, sendblueprint, queuefurniture, shuffle, dropblock, deleteblock, getblockcolor, drawblock, draw, drawhome, drawcredits, drawgame, drawui, movecar, drawmenubutton, isinside, mousepressed, touchstarted, mousedragged, touchmoved, handledrag, handleinput, handlegameinput, windowresized
Code Flow Diagram