survive 10 years in cave with cheese launcher

This sketch creates an endless dungeon crawler where you navigate procedurally-generated mazes, dodge hostile enemies, and blast them with a cheese launcher. Each floor gets harder as you descend deeper, picking up loot upgrades along the way.

🧪 Try This!

Experiment with the code by making these changes:

  1. Make the game permanently harder — Change the enemy spawn interval so new foes appear more frequently, raising the challenge
  2. Double the fire rate from the start — Lower the base cooldown so your cheese launcher fires much faster without waiting for upgrades
  3. Make cheese wedges live longer — Increase the projectile lifetime so bullets travel farther before disappearing, letting you reach distant enemies
  4. Ramp difficulty even harder per floor — Enemies scale much faster with depth—each floor down makes them significantly stronger
  5. Greedy loot: grab gold and ignore healing — Reduce how much HP loot heals so you're tempted to rush for damage upgrades instead
Prefer the full editor? Open it there →

📖 About This Sketch

This sketch builds a complete roguelike game: you control a player character moving through randomly-generated dungeon layouts, fire projectiles at enemies using the cheese launcher, grab glowing loot orbs for permanent upgrades, and descend endless floors by reaching the exit. It combines procedural dungeon generation with collision detection, enemy AI, timed entity spawning, and a full upgrade progression system—everything you need to understand how modern indie games work under the hood.

The code is organized into game entities (Player, Enemy, Bullet, Loot classes), a dungeon generator using random walk, an update loop that handles collision and spawning, and drawing functions that render the maze and UI. By studying it, you will learn how to architect a multi-system game: coordinate movement and shooting, spawn enemies and loot on a timer, detect collisions between projectiles and enemies, and manage game state across multiple floors.

⚙️ How It Works

  1. When the sketch loads, setup() initializes the canvas and calls initGame(), which generates a new dungeon layout using a drunkard's walk algorithm that carves random floors out of solid walls, then spawns initial enemies and loot on floor tiles.
  2. Every frame, the draw loop updates all game entities: the player moves with WASD keys and fires cheese wedges toward the mouse, enemies chase the player and deal contact damage, bullets travel and expire after 1.5 seconds or hitting walls, and loot items pulse visually waiting to be picked up.
  3. Collision detection runs continuously: bullets hitting enemies deal damage and disappear, the player picking up loot applies stat boosts, and the player touching the glowing exit tile triggers a floor descent, generating a fresh dungeon and incrementing the difficulty.
  4. Timed spawning keeps the challenge alive: every 15 seconds a new enemy appears at a random floor tile (respecting a minimum distance from the player), and every 10 seconds a new loot item materializes, up to hard caps of 35 enemies and 20 items to prevent performance collapse.
  5. The UI layer displays your health bar, current floor number, enemy count, and on-screen messages for damage taken, upgrades gained, or floor transitions; when the player's health hits zero, game state flips to 'dead' and pressing R triggers a full reset with a fresh dungeon and inventory.

🎓 Concepts You'll Learn

Procedural generation with random walkCollision detection between entitiesTimed spawning systemsGame state managementClass-based architecture for game objectsGrid-to-world coordinate mappingEnemy AI and pathfindingUpgrade progression and stat scaling with depth

📝 Code Breakdown

setup()

setup() runs once when p5.js starts. Here we prepare the canvas, set visual styles, and call initGame() to begin the game loop.

function setup() {
  createCanvas(windowWidth, windowHeight);
  textFont('monospace');
  initGame(true);
  lastTime = millis() / 1000;
}
Line-by-line explanation (4 lines)
createCanvas(windowWidth, windowHeight);
Creates a full-screen canvas that stretches to your entire window
textFont('monospace');
Sets the font for all UI text to monospace, giving it a retro computer game feel
initGame(true);
Generates the first dungeon, spawns the player and initial enemies/loot, and resets all game variables
lastTime = millis() / 1000;
Records the current time in seconds so the draw loop can calculate deltaTime (dt) for smooth, frame-rate-independent movement

generateDungeon()

Procedural generation creates unique layouts every run. This uses the drunkard's walk algorithm: start a random walker at the center and let it shuffle around carving floors, creating natural-looking organic mazes. This is the same technique used in classic roguelikes like Rogue and NetHack.

🔬 This loop randomly walks and carves. What if you change floor(random(4)) to always pick direction 0 (up) or direction 3 (right) only? What shape of maze would you get?

  while (floorCount < targetFloor && steps < maxSteps) {
    steps++;
    const dir = floor(random(4)); // random int 0–3
    if (dir === 0 && r > 1) r--;             // up
    else if (dir === 1 && r < ROWS - 2) r++; // down
    else if (dir === 2 && c > 1) c--;        // left
    else if (dir === 3 && c < COLS - 2) c++; // right
function generateDungeon() {
  dungeon = [];
  floorTiles = [];

  for (let r = 0; r < ROWS; r++) {
    const row = [];
    for (let c = 0; c < COLS; c++) {
      row.push(1); // all walls
    }
    dungeon.push(row);
  }

  // Start the walker near the center
  const startRow = floor(ROWS / 2);
  const startCol = floor(COLS / 2);
  let r = startRow;
  let c = startCol;
  dungeon[r][c] = 0;

  let floorCount = 1;
  const targetFloor = floor(ROWS * COLS * 0.4);
  const maxSteps = ROWS * COLS * 20;
  let steps = 0;

  while (floorCount < targetFloor && steps < maxSteps) {
    steps++;
    const dir = floor(random(4)); // random int 0–3
    if (dir === 0 && r > 1) r--;             // up
    else if (dir === 1 && r < ROWS - 2) r++; // down
    else if (dir === 2 && c > 1) c--;        // left
    else if (dir === 3 && c < COLS - 2) c++; // right

    if (dungeon[r][c] === 1) {
      dungeon[r][c] = 0;
      floorCount++;
    }
  }

  // Collect floor tiles and choose spawn/exit
  for (let rr = 0; rr < ROWS; rr++) {
    for (let cc = 0; cc < COLS; cc++) {
      if (dungeon[rr][cc] === 0) {
        floorTiles.push({ row: rr, col: cc });
      }
    }
  }

  spawnCell = { row: startRow, col: startCol };

  // Choose exit as farthest floor tile from spawn
  let best = null;
  let bestDist2 = -1;
  for (const t of floorTiles) {
    const dr = t.row - spawnCell.row;
    const dc = t.col - spawnCell.col;
    const d2 = dr * dr + dc * dc;
    if (d2 > bestDist2) {
      bestDist2 = d2;
      best = t;
    }
  }
  exitCell = best || spawnCell;

  spawnPos = gridToWorld(spawnCell.col, spawnCell.row);
  exitPos = gridToWorld(exitCell.col, exitCell.row);
}
Line-by-line explanation (8 lines)

🔧 Subcomponents:

for-loop Dungeon Grid Initialization for (let r = 0; r < ROWS; r++) { const row = []; for (let c = 0; c < COLS; c++) { row.push(1); // all walls } dungeon.push(row); }

Creates a 2D array where every cell starts as 1 (wall); this is the solid block we will carve through

while-loop Drunkard's Walk Floor Generation while (floorCount < targetFloor && steps < maxSteps) { steps++; const dir = floor(random(4)); // random int 0–3 if (dir === 0 && r > 1) r--; // up else if (dir === 1 && r < ROWS - 2) r++; // down else if (dir === 2 && c > 1) c--; // left else if (dir === 3 && c < COLS - 2) c++; // right if (dungeon[r][c] === 1) { dungeon[r][c] = 0; floorCount++; } }

A walker starts at the center and randomly stumbles up, down, left, or right, carving floor tiles (0s) wherever it goes until 40% of the map is floor

for-loop Farthest Exit Selection for (const t of floorTiles) { const dr = t.row - spawnCell.row; const dc = t.col - spawnCell.col; const d2 = dr * dr + dc * dc; if (d2 > bestDist2) { bestDist2 = d2; best = t; } }

Finds the floor tile that is farthest away from the spawn point and designates it as the exit, ensuring the player must traverse the entire dungeon

dungeon = [];
Clears the old dungeon array before building a fresh one
floorTiles = [];
Resets the list of walkable tiles; we will refill it after the walk completes
row.push(1); // all walls
Each cell is initialized to 1, representing a solid wall tile
const dir = floor(random(4)); // random int 0–3
Picks a random direction 0–3: 0=up, 1=down, 2=left, 3=right for the drunkard's walk
if (dungeon[r][c] === 1) { dungeon[r][c] = 0; floorCount++; }
If the walker steps on a wall, carve it into a floor (0) and increment the floor counter; if it steps on existing floor, nothing happens (no double-counting)
floorTiles.push({ row: rr, col: cc });
After the walk, iterate the entire dungeon and collect all floor tile coordinates into an array for later spawning
spawnCell = { row: startRow, col: startCol };
The spawn point is always the center where the walk began
exitCell = best || spawnCell;
The exit is the farthest floor tile found, or the spawn if something went wrong; this ensures the player must explore

draw()

The draw loop runs 60 times per second (by default). Each call calculates deltaTime so movement is consistent: a player moving at 100 pixels per second travels 100 * dt pixels every frame, whether the frame rate dips or soars.

function draw() {
  const now = millis() / 1000;
  let dt = now - lastTime;
  if (!isFinite(dt) || dt < 0) dt = 0;
  dt = min(dt, 0.05); // clamp to avoid big jumps when tab inactive
  lastTime = now;

  background(5);

  if (gameState === 'playing') {
    updateGame(dt);
  } else {
    // Still tick message timer so it can fade out on death screen
    messageTimer -= dt;
  }

  drawDungeon();
  drawEntities();
  drawUI();
}
Line-by-line explanation (6 lines)

🔧 Subcomponents:

calculation Frame-Rate-Independent Delta Time const now = millis() / 1000; let dt = now - lastTime; if (!isFinite(dt) || dt < 0) dt = 0; dt = min(dt, 0.05); // clamp to avoid big jumps when tab inactive lastTime = now;

Calculates how many seconds have passed since the last frame (dt); this makes all movement and timers independent of frame rate, ensuring consistent gameplay on slow and fast computers

conditional Game State Branching if (gameState === 'playing') { updateGame(dt); } else { // Still tick message timer so it can fade out on death screen messageTimer -= dt; }

When playing, update all entities; when dead, only tick the message timer so death text fades gracefully

const now = millis() / 1000;
Gets the current time in seconds (p5.js millis() returns milliseconds, so we divide by 1000)
let dt = now - lastTime;
Delta time is the difference between now and the last frame; this is how many seconds elapsed
if (!isFinite(dt) || dt < 0) dt = 0;
Safety check: if dt is infinite or negative (e.g., page was in background), set it to 0 to avoid broken calculations
dt = min(dt, 0.05); // clamp to avoid big jumps when tab inactive
Caps dt at 0.05 seconds (50 milliseconds) so that if the page lags or you switch tabs and come back, movement doesn't suddenly teleport
background(5);
Fills the entire canvas with a very dark gray (almost black), erasing everything from the previous frame
drawDungeon(); drawEntities(); drawUI();
Draws the dungeon walls, then all living game entities (loot, enemies, bullets, player), then the UI layer (health bar, floor number, messages)

updateGame(dt)

updateGame() is the heartbeat of the gameplay loop. It orchestrates all collision checks, timed spawning, and state transitions. Understanding this function teaches you the core pattern used in every game: update positions, check collisions, apply effects, remove dead entities, and reset timers.

🔬 These lines clean up dead entities. What happens if you comment them out so dead entities stay in the arrays? Would the game still work? Would it get slower?

  // Remove dead bullets and enemies
  bullets = bullets.filter(b => !b.dead);
  enemies = enemies.filter(e => !e.dead);
function updateGame(dt) {
  if (!player) return;

  player.update(dt);

  for (const e of enemies) {
    e.update(dt);
  }

  for (const b of bullets) {
    b.update(dt);
  }

  // Bullet-enemy collisions
  for (const b of bullets) {
    if (b.dead) continue;
    for (const e of enemies) {
      if (e.dead) continue;
      const d = dist(b.x, b.y, e.x, e.y);
      if (d < e.radius + b.radius * 0.5) {
        e.takeDamage(b.damage);
        b.dead = true;
        break;
      }
    }
  }

  // Remove dead bullets and enemies
  bullets = bullets.filter(b => !b.dead);
  enemies = enemies.filter(e => !e.dead);

  // Loot pickup
  for (const l of lootItems) {
    if (l.picked) continue;
    const d = dist(player.x, player.y, l.x, l.y);
    if (d < player.radius + l.radius * 0.8) {
      l.apply(player);
    }
  }
  lootItems = lootItems.filter(l => !l.picked);

  // Check exit (floor change)
  const exitDist = dist(player.x, player.y, exitPos.x, exitPos.y);
  if (exitDist < tileSize * 0.5) {
    depth++;
    showMessage(`Descending to floor ${depth}...`, 2);
    initGame(false); // keep upgrades
  }

  // Timed spawns
  lootSpawnTimer -= dt;
  if (lootSpawnTimer <= 0) {
    spawnLootAtRandomTile();
    lootSpawnTimer = lootSpawnInterval;
  }

  enemySpawnTimer -= dt;
  if (enemySpawnTimer <= 0) {
    spawnEnemyAtRandomTile();
    enemySpawnTimer = enemySpawnInterval;
  }

  // Message timer
  messageTimer -= dt;
}
Line-by-line explanation (14 lines)

🔧 Subcomponents:

for-loop Entity Update Loop player.update(dt); for (const e of enemies) { e.update(dt); } for (const b of bullets) { b.update(dt); }

Calls update() on every entity (player, enemies, bullets) to move them and handle their internal logic

for-loop Bullet-Enemy Collision Detection // Bullet-enemy collisions for (const b of bullets) { if (b.dead) continue; for (const e of enemies) { if (e.dead) continue; const d = dist(b.x, b.y, e.x, e.y); if (d < e.radius + b.radius * 0.5) { e.takeDamage(b.damage); b.dead = true; break; } } }

Nested loop checks every bullet against every enemy; if their distance is less than the sum of their radii, they collide—deal damage and mark the bullet as dead

for-loop Loot Pickup Detection // Loot pickup for (const l of lootItems) { if (l.picked) continue; const d = dist(player.x, player.y, l.x, l.y); if (d < player.radius + l.radius * 0.8) { l.apply(player); } } lootItems = lootItems.filter(l => !l.picked);

Checks if the player overlaps any loot item; if so, calls apply() to grant the upgrade and filters out picked loot

conditional Exit Proximity Detection // Check exit (floor change) const exitDist = dist(player.x, player.y, exitPos.x, exitPos.y); if (exitDist < tileSize * 0.5) { depth++; showMessage(`Descending to floor ${depth}...`, 2); initGame(false); // keep upgrades }

When the player gets within one half-tile of the exit, increment depth, show a message, and generate a new dungeon while keeping all upgrades

conditional Timed Enemy and Loot Spawning // Timed spawns lootSpawnTimer -= dt; if (lootSpawnTimer <= 0) { spawnLootAtRandomTile(); lootSpawnTimer = lootSpawnInterval; } enemySpawnTimer -= dt; if (enemySpawnTimer <= 0) { spawnEnemyAtRandomTile(); enemySpawnTimer = enemySpawnInterval; }

Countdown timers tick down each frame; when either reaches zero, spawn a new loot or enemy at a random floor tile and reset the timer

if (!player) return;
Safety check: if player doesn't exist yet, exit early to avoid errors
player.update(dt);
Updates the player's position based on keyboard input and handles shooting logic
for (const e of enemies) { e.update(dt); }
Loops through all enemies and updates each one (movement, attacking, health check)
for (const b of bullets) { b.update(dt); }
Loops through all bullets and updates each one (movement, lifetime, wall collision)
const d = dist(b.x, b.y, e.x, e.y);
p5.js's dist() function calculates the Euclidean distance between the bullet and enemy
if (d < e.radius + b.radius * 0.5) {
If distance is less than the sum of their radii, they overlap—a collision occurred
e.takeDamage(b.damage); b.dead = true; break;
Damage the enemy, mark the bullet as dead (so it disappears), and break to stop checking this bullet against other enemies
bullets = bullets.filter(b => !b.dead);
Removes all dead bullets from the array using filter(), keeping only living ones
lootItems = lootItems.filter(l => !l.picked);
Removes all picked-up loot from the array, keeping only uncollected items
if (exitDist < tileSize * 0.5) {
If the player is within half a tile of the exit, they successfully reached it and can descend
depth++;
Increments the current floor number, making enemies stronger and spawning more loot on the new floor
initGame(false); // keep upgrades
Generates a new dungeon but passes false so the player's upgrades (damage, speed, fire rate) are retained
lootSpawnTimer -= dt;
Countdown the loot spawn timer by deltaTime each frame
if (lootSpawnTimer <= 0) {
When the timer expires, spawn new loot and reset the timer to its full interval

spawnEnemyAtRandomTile()

Timed spawning is essential to maintaining difficulty while keeping the game fair. This function runs every 15 seconds (configurable) and adds enemies as long as you're below the cap. The distance check prevents rage-inducing spawns right on top of you—a UX pattern used in modern roguelikes.

🔬 This loop searches 40 times for a safe spawn tile. What happens if you change 40 to 1? What if you change it to 1000? How does this trade time (CPU work) for fairness (avoiding cheap spawns)?

  // Try to find a tile not too close to the player
  let pos = null;
  const minDist = tileSize * 4;
  for (let i = 0; i < 40; i++) {
    const t = random(eligible);
    const candidate = gridToWorld(t.col, t.row);
    if (!player || dist(player.x, player.y, candidate.x, candidate.y) > minDist) {
      pos = candidate;
      break;
    }
  }
function spawnEnemyAtRandomTile() {
  if (enemies.length >= MAX_ENEMIES) return;
  if (floorTiles.length === 0) return;

  const eligible = floorTiles.filter(t =>
    !(t.row === spawnCell.row && t.col === spawnCell.col) &&
    !(t.row === exitCell.row && t.col === exitCell.col)
  );
  if (eligible.length === 0) return;

  // Try to find a tile not too close to the player
  let pos = null;
  const minDist = tileSize * 4;
  for (let i = 0; i < 40; i++) {
    const t = random(eligible);
    const candidate = gridToWorld(t.col, t.row);
    if (!player || dist(player.x, player.y, candidate.x, candidate.y) > minDist) {
      pos = candidate;
      break;
    }
  }
  // Fallback: just pick any eligible tile
  if (!pos) {
    const t = random(eligible);
    pos = gridToWorld(t.col, t.row);
  }

  enemies.push(new Enemy(pos.x, pos.y, depth));
}
Line-by-line explanation (9 lines)

🔧 Subcomponents:

for-loop Filter Safe Spawn Tiles const eligible = floorTiles.filter(t => !(t.row === spawnCell.row && t.col === spawnCell.col) && !(t.row === exitCell.row && t.col === exitCell.col) );

Removes the spawn and exit tiles from the list of possible spawn locations so enemies don't materialize on top of the player or exit

for-loop Find Safe Distance Spawn for (let i = 0; i < 40; i++) { const t = random(eligible); const candidate = gridToWorld(t.col, t.row); if (!player || dist(player.x, player.y, candidate.x, candidate.y) > minDist) { pos = candidate; break; } }

Tries 40 times to find a floor tile that is at least minDist (4 tile widths) away from the player; this prevents cheap spawns directly on you

if (enemies.length >= MAX_ENEMIES) return;
If there are already MAX_ENEMIES on the floor, don't spawn more—exit early to avoid lag
if (floorTiles.length === 0) return;
If there are no floor tiles (dungeon generation failed), bail out
const eligible = floorTiles.filter(t => ...
filter() creates a new array containing only tiles that are NOT the spawn or exit
const minDist = tileSize * 4;
Enemies must spawn at least 4 tile widths away from the player—enough distance to give you reaction time
for (let i = 0; i < 40; i++) {
Loop 40 times trying to find a safe spawn location
const t = random(eligible);
Pick a random tile from the eligible list
const candidate = gridToWorld(t.col, t.row);
Convert the grid coordinates to world (pixel) coordinates
if (!player || dist(...) > minDist) {
If there's no player yet OR if the candidate is far enough from the player, accept this spawn and break
enemies.push(new Enemy(pos.x, pos.y, depth));
Create a new Enemy at the chosen position with health/damage scaled by current floor depth

Player class (constructor & methods)

The Player class encapsulates all player behavior: input handling, movement, shooting, collision detection, and damage. It's a great example of object-oriented game design: each entity is responsible for its own update() and draw() logic, making the code modular and reusable.

🔬 This block normalizes movement. What if you comment out the two division lines (dx /= len; dy /= len)? How would diagonal movement feel different—would it be faster or slower?

    if (dx !== 0 || dy !== 0) {
      const len = Math.hypot(dx, dy);
      dx /= len;
      dy /= len;

      const step = this.moveSpeed * dt;
      this.tryMove(dx * step, dy * step);
    }
class Player {
  constructor(x, y) {
    this.x = x;
    this.y = y;
    this.radius = tileSize * 0.35;
    this.baseSpeed = tileSize * 6;
    this.speedMultiplier = 1;

    this.maxHealth = 100;
    this.health = this.maxHealth;
    this.damage = 25;

    this.baseFireCooldown = 0.3; // seconds between shots
    this.fireCooldownMultiplier = 1; // lower = faster
    this.fireTimer = 0;
  }

  get moveSpeed() {
    return this.baseSpeed * this.speedMultiplier;
  }

  get fireCooldown() {
    return this.baseFireCooldown * this.fireCooldownMultiplier;
  }

  update(dt) {
    // Movement - WASD / arrow keys
    let dx = 0;
    let dy = 0;
    if (keyIsDown(65) || keyIsDown(37)) dx -= 1; // A or Left
    if (keyIsDown(68) || keyIsDown(39)) dx += 1; // D or Right
    if (keyIsDown(87) || keyIsDown(38)) dy -= 1; // W or Up
    if (keyIsDown(83) || keyIsDown(40)) dy += 1; // S or Down

    if (dx !== 0 || dy !== 0) {
      const len = Math.hypot(dx, dy);
      dx /= len;
      dy /= len;

      const step = this.moveSpeed * dt;
      this.tryMove(dx * step, dy * step);
    }

    // Shooting - hold mouse button (Cheese Launcher)
    this.fireTimer -= dt;
    if (mouseIsPressed && this.fireTimer <= 0) {
      this.shoot();
      this.fireTimer = this.fireCooldown;
    }
  }

  tryMove(mx, my) {
    // move on x-axis then y-axis with wall collision
    let newX = this.x + mx;
    let newY = this.y;

    if (!this.collidesWithWalls(newX, newY)) {
      this.x = newX;
    }

    newX = this.x;
    newY = this.y + my;

    if (!this.collidesWithWalls(newX, newY)) {
      this.y = newY;
    }
  }

  collidesWithWalls(px, py) {
    const r = this.radius * 0.8;
    const samples = [
      { x: px - r, y: py },
      { x: px + r, y: py },
      { x: px,     y: py - r },
      { x: px,     y: py + r }
    ];
    for (const s of samples) {
      if (isWallAtWorld(s.x, s.y)) return true;
    }
    return false;
  }

  shoot() {
    const dx = mouseX - this.x;
    const dy = mouseY - this.y;
    const len = Math.hypot(dx, dy);
    if (len < 0.001) return;

    const speed = tileSize * 18;
    const vx = (dx / len) * speed;
    const vy = (dy / len) * speed;
    bullets.push(new Bullet(this.x, this.y, vx, vy, this.damage));
  }

  takeDamage(amount) {
    if (gameState !== 'playing') return;
    this.health -= amount;
    showMessage(`Ouch! -${amount} HP`, 1.5);
    if (this.health <= 0) {
      this.health = 0;
      gameState = 'dead';
      showMessage('You died! Press R to restart.', 5);
    }
  }

  draw() {
    push();
    noStroke();
    // Body
    fill(80, 200, 255);
    ellipse(this.x, this.y, this.radius * 2, this.radius * 2);

    // Facing line towards mouse (cheese launcher aim)
    const dx = mouseX - this.x;
    const dy = mouseY - this.y;
    const len = Math.hypot(dx, dy) || 1;
    const fx = this.x + (dx / len) * this.radius;
    const fy = this.y + (dy / len) * this.radius;
    stroke(0);
    strokeWeight(2);
    line(this.x, this.y, fx, fy);
    pop();
  }
}
Line-by-line explanation (13 lines)

🔧 Subcomponents:

conditional WASD/Arrow Movement Input // Movement - WASD / arrow keys let dx = 0; let dy = 0; if (keyIsDown(65) || keyIsDown(37)) dx -= 1; // A or Left if (keyIsDown(68) || keyIsDown(39)) dx += 1; // D or Right if (keyIsDown(87) || keyIsDown(38)) dy -= 1; // W or Up if (keyIsDown(83) || keyIsDown(40)) dy += 1; // S or Down

Reads which keys are held down and accumulates movement direction; supports both WASD and arrow keys

calculation Direction Normalization if (dx !== 0 || dy !== 0) { const len = Math.hypot(dx, dy); dx /= len; dy /= len; const step = this.moveSpeed * dt; this.tryMove(dx * step, dy * step); }

Normalizes movement to a unit vector (length 1) so diagonal movement is the same speed as cardinal movement; prevents bias when moving diagonally

for-loop Multi-Point Collision Detection const samples = [ { x: px - r, y: py }, { x: px + r, y: py }, { x: px, y: py - r }, { x: px, y: py + r } ]; for (const s of samples) { if (isWallAtWorld(s.x, s.y)) return true; }

Samples four points around the player's position (left, right, top, bottom); if any hits a wall, return true—prevents clipping through narrow passages

conditional Fire Rate Cooldown // Shooting - hold mouse button (Cheese Launcher) this.fireTimer -= dt; if (mouseIsPressed && this.fireTimer <= 0) { this.shoot(); this.fireTimer = this.fireCooldown; }

Only allows a shot when the fire cooldown timer has expired; this prevents spam and creates balanced weapon pacing

this.radius = tileSize * 0.35;
Player radius is 35% of a tile width, making the player a medium-sized sprite relative to the dungeon
this.baseSpeed = tileSize * 6;
Base movement speed is 6 tile widths per second; multipliers can scale this for loot upgrades
if (keyIsDown(65) || keyIsDown(37)) dx -= 1; // A or Left
keyCode 65 is A, 37 is left arrow; both move the player left. p5.js's keyIsDown() checks if a key is currently held
const len = Math.hypot(dx, dy);
hypot() calculates the length of the movement vector; used to normalize diagonal movement
dx /= len; dy /= len;
Divides both components by the vector length to normalize them; now moving diagonally is the same speed as moving cardinal
const step = this.moveSpeed * dt;
Calculates the pixel distance to move this frame: speed (pixels/second) × deltaTime (seconds) = pixels this frame
const r = this.radius * 0.8;
Collision check uses a slightly smaller radius (80%) than the visual radius to allow squeezing through tight gaps
const dx = mouseX - this.x; const dy = mouseY - this.y;
Vector from player to mouse—the direction to shoot the cheese wedge
const len = Math.hypot(dx, dy); if (len < 0.001) return;
If the mouse is essentially on the player, the length is near zero; return to avoid division-by-zero errors
const speed = tileSize * 18;
Cheese wedges travel at 18 tile widths per second—much faster than the player to enable ranging
const vx = (dx / len) * speed; const vy = (dy / len) * speed;
Normalizes the direction and scales it by the projectile speed to get velocity components
fill(80, 200, 255);
Player body is a cyan-ish blue: RGB(80, 200, 255)
const fx = this.x + (dx / len) * this.radius;
The line extends from the player center to a point on the edge facing the mouse—a visual aim indicator

Enemy class (constructor & methods)

Enemies use a simple but effective pursuit AI: calculate direction to player, normalize it, apply movement speed, and move. The difficulty scaling ensures the game gets harder as you progress: each floor, enemies gain speed, health, and damage. This creates a fun power fantasy—you're getting stronger from loot, but enemies are catching up.

🔬 This is the enemy's AI—it chases the player by moving toward them. What if you changed `player.x` to `player.x + 100`? The enemy would aim at a point ahead of the player—could this be useful for leading shots or dodging?

    const dx = player.x - this.x;
    const dy = player.y - this.y;
    const distToPlayer = Math.hypot(dx, dy) || 1;

    // Chase player
    const step = this.speed * dt;
    const mx = (dx / distToPlayer) * step;
    const my = (dy / distToPlayer) * step;
    this.tryMove(mx, my);
class Enemy {
  constructor(x, y, depth) {
    this.x = x;
    this.y = y;
    this.radius = tileSize * 0.3;
    this.speed = tileSize * (1.8 + depth * 0.15);
    this.maxHealth = 35 + depth * 12;
    this.health = this.maxHealth;
    this.damage = 8 + depth * 2;

    this.attackCooldown = 0.7;
    this.attackTimer = 0;

    this.dead = false;
  }

  update(dt) {
    if (this.dead || gameState !== 'playing') return;
    this.attackTimer -= dt;

    const dx = player.x - this.x;
    const dy = player.y - this.y;
    const distToPlayer = Math.hypot(dx, dy) || 1;

    // Chase player
    const step = this.speed * dt;
    const mx = (dx / distToPlayer) * step;
    const my = (dy / distToPlayer) * step;
    this.tryMove(mx, my);

    // Contact damage
    const distPlayer = Math.hypot(player.x - this.x, player.y - this.y);
    if (distPlayer < this.radius + player.radius * 0.8) {
      if (this.attackTimer <= 0) {
        player.takeDamage(this.damage);
        this.attackTimer = this.attackCooldown;
      }
    }
  }

  tryMove(mx, my) {
    let newX = this.x + mx;
    let newY = this.y;

    if (!this.collidesWithWalls(newX, newY)) {
      this.x = newX;
    }
    newX = this.x;
    newY = this.y + my;
    if (!this.collidesWithWalls(newX, newY)) {
      this.y = newY;
    }
  }

  collidesWithWalls(px, py) {
    const r = this.radius * 0.8;
    const samples = [
      { x: px - r, y: py },
      { x: px + r, y: py },
      { x: px,     y: py - r },
      { x: px,     y: py + r }
    ];
    for (const s of samples) {
      if (isWallAtWorld(s.x, s.y)) return true;
    }
    return false;
  }

  takeDamage(amount) {
    this.health -= amount;
    if (this.health <= 0) {
      this.dead = true;
    }
  }

  draw() {
    if (this.dead) return;
    push();
    noStroke();
    // Body
    fill(220, 80, 80);
    ellipse(this.x, this.y, this.radius * 2, this.radius * 2);

    // Health bar
    const barW = this.radius * 1.6;
    const barH = 4;
    const hpRatio = constrain(this.health / this.maxHealth, 0, 1);
    const bx = this.x - barW / 2;
    const by = this.y - this.radius - 8;

    stroke(0);
    strokeWeight(1);
    fill(40);
    rect(bx, by, barW, barH);
    noStroke();
    fill(0, 220, 0);
    rect(bx, by, barW * hpRatio, barH);
    pop();
  }
}
Line-by-line explanation (10 lines)

🔧 Subcomponents:

calculation Difficulty Scaling by Floor this.speed = tileSize * (1.8 + depth * 0.15); this.maxHealth = 35 + depth * 12; this.damage = 8 + depth * 2;

Enemies get faster, tougher, and hit harder as you descend deeper floors, creating escalating difficulty

calculation Pathfinding Toward Player const dx = player.x - this.x; const dy = player.y - this.y; const distToPlayer = Math.hypot(dx, dy) || 1; // Chase player const step = this.speed * dt; const mx = (dx / distToPlayer) * step; const my = (dy / distToPlayer) * step; this.tryMove(mx, my);

Calculates direction to player, normalizes it, scales by movement speed, and moves the enemy toward the player—simple pursuer AI

conditional Melee Attack on Contact // Contact damage const distPlayer = Math.hypot(player.x - this.x, player.y - this.y); if (distPlayer < this.radius + player.radius * 0.8) { if (this.attackTimer <= 0) { player.takeDamage(this.damage); this.attackTimer = this.attackCooldown; } }

If the enemy touches the player, deals damage on a cooldown (0.7 seconds) to prevent overkill damage

this.speed = tileSize * (1.8 + depth * 0.15);
Base speed 1.8 tile widths per second, plus 0.15× per floor depth—enemies get 15% faster each floor
this.maxHealth = 35 + depth * 12;
Base health 35, plus 12 HP per floor—enemies gain 12 health each descent, requiring more shots to kill
this.damage = 8 + depth * 2;
Base damage 8, plus 2 per floor—enemies hit harder as you go deeper
if (this.dead || gameState !== 'playing') return;
Dead enemies don't update; also skip if the game is not actively playing
const distToPlayer = Math.hypot(dx, dy) || 1;
Distance from enemy to player; defaults to 1 if zero to avoid division by zero
const mx = (dx / distToPlayer) * step;
Normalizes the direction by dividing by distance, then scales by movement distance this frame
if (distPlayer < this.radius + player.radius * 0.8) {
Collision check: if overlap between enemy radius and player's slightly-padded radius, they are touching
if (this.attackTimer <= 0) {
Only attack if the cooldown has expired; prevents enemies from overkilling instantly
const hpRatio = constrain(this.health / this.maxHealth, 0, 1);
Calculates the fraction of health remaining; constrain() clamps it to [0, 1] for visual health bar
rect(bx, by, barW * hpRatio, barH);
Draws the green bar proportional to remaining health; as health decreases, the bar shrinks

Bullet class (constructor & methods)

Bullets are simple but powerful: straight-line projectiles with a lifespan. The angle calculation and rotation make the visual wedge point where it's flying, enhancing the visual feedback. The wall collision check prevents cheese from passing through stone—you must aim carefully.

🔬 This draws the cheese wedge as a triangle. What happens if you change the vertices to make a square (four vertices at the corners)? Or if you scale r differently, like r * 2 instead of r * 1.4?

  draw() {
    if (this.dead) return;
    push();
    translate(this.x, this.y);
    rotate(this.angle);

    // Cheese wedge body
    fill(255, 230, 120);  // cheese color
    stroke(200, 170, 80);
    strokeWeight(1.5);

    const r = this.radius * 1.4;
    beginShape();
    vertex(-r, -r);
    vertex(r * 1.2, 0);
    vertex(-r, r);
    endShape(CLOSE);
class Bullet {
  constructor(x, y, vx, vy, damage) {
    this.x = x;
    this.y = y;
    this.vx = vx;
    this.vy = vy;
    this.damage = damage;
    this.radius = tileSize * 0.12;
    this.life = 1.5; // seconds
    this.dead = false;

    // Angle of travel for rotating the cheese wedge
    this.angle = Math.atan2(vy, vx);
  }

  update(dt) {
    if (this.dead) return;
    this.life -= dt;
    if (this.life <= 0) {
      this.dead = true;
      return;
    }
    this.x += this.vx * dt;
    this.y += this.vy * dt;

    // Hit wall?
    if (isWallAtWorld(this.x, this.y)) {
      this.dead = true;
    }
  }

  draw() {
    if (this.dead) return;
    push();
    translate(this.x, this.y);
    rotate(this.angle);

    // Cheese wedge body
    fill(255, 230, 120);  // cheese color
    stroke(200, 170, 80);
    strokeWeight(1.5);

    const r = this.radius * 1.4;
    beginShape();
    vertex(-r, -r);
    vertex(r * 1.2, 0);
    vertex(-r, r);
    endShape(CLOSE);

    // Cheese holes
    noStroke();
    fill(230, 200, 100);
    ellipse(-r * 0.3, -r * 0.3, r * 0.4, r * 0.4);
    ellipse(-r * 0.2, r * 0.2, r * 0.3, r * 0.3);

    pop();
  }
}
Line-by-line explanation (8 lines)

🔧 Subcomponents:

conditional Projectile Lifetime Expiry this.life -= dt; if (this.life <= 0) { this.dead = true; return; }

Bullets live for 1.5 seconds max; after that they disappear to prevent them accumulating forever

conditional Wall Collision Detection // Hit wall? if (isWallAtWorld(this.x, this.y)) { this.dead = true; }

If the bullet hits a wall, mark it dead so it disappears immediately instead of passing through

this.angle = Math.atan2(vy, vx);
atan2(y, x) calculates the angle of the velocity vector in radians; used to rotate the cheese wedge sprite to face the direction it's flying
this.life = 1.5; // seconds
Bullets live for 1.5 seconds; after that they automatically die to prevent a cloud of old projectiles
this.x += this.vx * dt; this.y += this.vy * dt;
Updates position by velocity each frame: new_pos = old_pos + (velocity × deltaTime)
translate(this.x, this.y);
Moves the coordinate system to the bullet's position so we draw the cheese wedge centered on it
rotate(this.angle);
Rotates the canvas so the cheese wedge points in the direction it's traveling
const r = this.radius * 1.4;
The cheese wedge is drawn with a scale factor 1.4× the base radius
vertex(-r, -r); vertex(r * 1.2, 0); vertex(-r, r);
Three vertices of a triangle: left-top, right-center (pointed), left-bottom—forms a wedge shape pointing right
ellipse(-r * 0.3, -r * 0.3, r * 0.4, r * 0.4);
Draws a small hole in the top-left of the cheese; visual flavor that makes it recognizable as cheese

Loot class (constructor & methods)

Loot is the progression system: each pickup grants a permanent multiplicative or additive boost. The five upgrade types (heal, max health, damage, speed, fire rate) create strategic choices—do you grab healing or rush for speed? Color and pulsing animation make them visually appealing and discoverable.

🔬 This pulsing effect multiplies the radius by (1.5 + pulse). What if you change 0.004 to 0.01? Or change 0.5 + 0.5 * sin(...) to just sin(...) by itself? How would the animation look different?

    const pulse = 0.5 + 0.5 * sin(millis() * 0.004);
    fill(red(c), green(c), blue(c), 200);
    ellipse(this.x, this.y, this.radius * (1.5 + pulse), this.radius * (1.5 + pulse));
class Loot {
  constructor(x, y, type) {
    this.x = x;
    this.y = y;
    this.type = type;
    this.radius = tileSize * 0.2;
    this.picked = false;
  }

  apply(player) {
    if (this.picked) return;
    this.picked = true;

    if (this.type === 'heal') {
      const heal = floor(player.maxHealth * 0.6);
      player.health = min(player.maxHealth, player.health + heal);
      showMessage(`Healed +${heal} HP`, 2);
    } else if (this.type === 'maxHealth') {
      player.maxHealth += 25;
      player.health = player.maxHealth;
      showMessage('Max HP +25 (fully healed!)', 2.5);
    } else if (this.type === 'damage') {
      player.damage += 10;
      showMessage('Cheese damage +10', 2);
    } else if (this.type === 'speed') {
      player.speedMultiplier *= 1.15;
      showMessage('Speed +15%', 2);
    } else if (this.type === 'firerate') {
      player.fireCooldownMultiplier *= 0.85;
      player.fireCooldownMultiplier = max(player.fireCooldownMultiplier, 0.4);
      showMessage('Cheese fire rate +15%', 2);
    }
  }

  draw() {
    if (this.picked) return;
    push();
    noStroke();
    let c;
    if (this.type === 'heal') {
      c = color(80, 220, 120);
    } else if (this.type === 'maxHealth') {
      c = color(0, 180, 255);
    } else if (this.type === 'damage') {
      c = color(255, 150, 0);
    } else if (this.type === 'speed') {
      c = color(80, 220, 255);
    } else if (this.type === 'firerate') {
      c = color(220, 120, 255);
    } else {
      c = color(255);
    }
    const pulse = 0.5 + 0.5 * sin(millis() * 0.004);
    fill(red(c), green(c), blue(c), 200);
    ellipse(this.x, this.y, this.radius * (1.5 + pulse), this.radius * (1.5 + pulse));
    pop();
  }
}
Line-by-line explanation (7 lines)

🔧 Subcomponents:

switch-case Upgrade Type Dispatch if (this.type === 'heal') { const heal = floor(player.maxHealth * 0.6); player.health = min(player.maxHealth, player.health + heal); showMessage(`Healed +${heal} HP`, 2); } else if (this.type === 'maxHealth') { player.maxHealth += 25; player.health = player.maxHealth; showMessage('Max HP +25 (fully healed!)', 2.5); } else if (this.type === 'damage') { player.damage += 10; showMessage('Cheese damage +10', 2); } else if (this.type === 'speed') { player.speedMultiplier *= 1.15; showMessage('Speed +15%', 2); } else if (this.type === 'firerate') { player.fireCooldownMultiplier *= 0.85; player.fireCooldownMultiplier = max(player.fireCooldownMultiplier, 0.4); showMessage('Cheese fire rate +15%', 2); }

Each loot type grants a different permanent upgrade; the applied effect persists across floors for the rest of the run

calculation Sine Wave Pulsing Animation const pulse = 0.5 + 0.5 * sin(millis() * 0.004); ellipse(this.x, this.y, this.radius * (1.5 + pulse), this.radius * (1.5 + pulse));

Uses a sine wave to create a smooth pulsing effect that attracts attention and signals 'pick me up!'

if (this.picked) return;
Safety check: if loot is already picked, don't apply it again
const heal = floor(player.maxHealth * 0.6);
Healing loot restores 60% of the player's max health (scales with player upgrades)
player.health = min(player.maxHealth, player.health + heal);
Adds healing but caps health at maxHealth using min()
player.speedMultiplier *= 1.15;
Speed loot multiplies movement speed by 1.15×—a 15% increase that stacks multiplicatively
player.fireCooldownMultiplier = max(player.fireCooldownMultiplier, 0.4);
Fire rate loot scales down cooldown (lower is faster); max() ensures it never drops below 0.4 to prevent breaking the game
let c; if (this.type === 'heal') { c = color(80, 220, 120);
Each loot type has a distinct color for visual recognition: heal is green, maxHealth is blue, damage is orange, speed is cyan, firerate is magenta
const pulse = 0.5 + 0.5 * sin(millis() * 0.004);
sin() oscillates from -1 to 1; scaled to 0.5–1.5, creating a smooth pulsing multiplier from 50% to 150% of radius

drawDungeon()

Drawing the dungeon is just two nested loops iterating the 2D array and rendering rectangles. The grid-to-world coordinate conversion happens inside the loop, making it easy to position tiles correctly. The pulsing exit indicator gives visual feedback about the goal.

function drawDungeon() {
  noStroke();
  rectMode(CORNER);

  for (let r = 0; r < ROWS; r++) {
    for (let c = 0; c < COLS; c++) {
      const x = xOffset + c * tileSize;
      const y = yOffset + r * tileSize;

      if (dungeon[r][c] === 1) {
        fill(10);
      } else {
        fill(40);
      }
      rect(x, y, tileSize, tileSize);
    }
  }

  // Highlight spawn and exit tiles
  if (spawnCell) {
    const sp = gridToWorld(spawnCell.col, spawnCell.row);
    push();
    noStroke();
    fill(120, 180, 255, 120);
    ellipse(sp.x, sp.y, tileSize * 0.7, tileSize * 0.7);
    pop();
  }

  if (exitCell) {
    const ex = gridToWorld(exitCell.col, exitCell.row);
    const pulse = 0.5 + 0.5 * sin(millis() * 0.004);
    push();
    noStroke();
    fill(80, 220, 255, 220);
    ellipse(ex.x, ex.y, tileSize * (0.9 + 0.3 * pulse), tileSize * (0.9 + 0.3 * pulse));
    pop();
  }
}
Line-by-line explanation (6 lines)

🔧 Subcomponents:

for-loop Dungeon Tile Grid Rendering for (let r = 0; r < ROWS; r++) { for (let c = 0; c < COLS; c++) { const x = xOffset + c * tileSize; const y = yOffset + r * tileSize; if (dungeon[r][c] === 1) { fill(10); } else { fill(40); } rect(x, y, tileSize, tileSize); } }

Loops through every grid cell and draws a rectangle; walls (1) are dark gray (10), floors (0) are light gray (40)

rectMode(CORNER);
Sets rect() to draw from the top-left corner (rather than from center), making grid alignment straightforward
const x = xOffset + c * tileSize;
Converts grid column to world x coordinate: xOffset positions the grid, then c * tileSize steps across
if (dungeon[r][c] === 1) {
Checks if this cell is a wall (value 1) or floor (value 0)
const sp = gridToWorld(spawnCell.col, spawnCell.row);
Converts spawn grid coordinates to world coordinates for drawing the highlight
fill(120, 180, 255, 120);
Semi-transparent cyan (alpha 120) shows the spawn point gently
const pulse = 0.5 + 0.5 * sin(millis() * 0.004); ellipse(ex.x, ex.y, tileSize * (0.9 + 0.3 * pulse), tileSize * (0.9 + 0.3 * pulse));
Exit pulses between 0.9 and 1.2 tile sizes to draw attention—tells the player 'go here to descend'

📦 Key Variables

tileSize number

Pixel width/height of one dungeon tile; scales all game objects proportionally based on screen size

let tileSize = 32;
COLS, ROWS number

Grid dimensions (number of columns and rows) calculated from screen size and tileSize

let COLS = 20; let ROWS = 12;
dungeon array

2D array where each cell is 1 (wall) or 0 (floor), defining the maze layout

let dungeon = [[1, 1, 1], [1, 0, 1], [1, 1, 1]];
floorTiles array

List of {row, col} objects representing all walkable floor cells; used for spawning enemies and loot

let floorTiles = [{row: 5, col: 7}, {row: 5, col: 8}];
spawnCell, exitCell object

Grid coordinates of the player spawn point and exit to next floor; collected as {row, col}

let spawnCell = {row: 10, col: 15};
player object (Player instance)

The main character; stores position, health, upgrades, and handles movement/shooting

let player = new Player(200, 300);
enemies, bullets, lootItems array

Arrays of active Enemy, Bullet, and Loot objects; updated every frame and filtered when entities die

let enemies = []; let bullets = []; let lootItems = [];
depth number

Current floor number; increments when exiting, scales enemy difficulty

let depth = 1;
gameState string

Either 'playing' or 'dead'; controls whether the game updates or shows the death screen

let gameState = 'playing';
lootSpawnInterval, enemySpawnInterval number

Target time in seconds between automatic spawns; lower = more frequent spawning

let lootSpawnInterval = 10;
lootSpawnTimer, enemySpawnTimer number

Running countdown timers in seconds; tick down each frame, trigger spawn when reaching zero

let lootSpawnTimer = 10;

🔧 Potential Improvements (7)

Here are some ways this code could be enhanced:

BUG Player.shoot()

If the mouse is exactly on the player (distance ~0), len will be near zero, and normalization will produce NaN, creating broken bullets

💡 The code already checks `if (len < 0.001) return;` which prevents this—good defensive coding, but could add an assertion or warning for debugging

PERFORMANCE updateGame() bullet-enemy collision

Nested loops check every bullet against every enemy—O(n²) complexity. With 35 enemies and many bullets, this grows quadratically

💡 Use spatial partitioning (divide the dungeon into a grid) to only check nearby pairs, or use a 1D collision array sorted by position

BUG Enemy.update() contact damage

If the player has very low health and an enemy touches them, multiple frames of contact can deliver damage faster than the cooldown resets, leading to unexpected overkill

💡 Store the last frame when damage was dealt and skip if fewer than attackCooldown seconds have passed, rather than relying on the timer

STYLE Global variables

Many global variables (dungeon, enemies, bullets, lootItems, etc.) could be grouped into a gameState object for clarity

💡 Create a const gameData = { dungeon, enemies, bullets, ... } to namespace related data and reduce global pollution

FEATURE Loot system

All loot types are equally likely; some upgrades (heal) may feel weak compared to damage, discouraging diversity

💡 Implement weighted random selection: rarer upgrades (maxHealth) appear less often but give bigger boosts, creating more meaningful choices

PERFORMANCE generateDungeon() random walk

The walk can take many iterations to fill targetFloor cells; on slow devices or large canvases, this might stutter

💡 Pre-generate dungeons offline or cache common layouts; alternatively, cap maxSteps more aggressively to trade dungeon size for speed

FEATURE Game loop

No pause or menu; the game is always on—players can't take a break

💡 Add a spacebar-to-pause feature and a main menu state to let players breathe between runs

🔄 Code Flow

Code flow showing setup, generatedungeon, draw, updategame, spawnenemyatrandomtile, player, enemy, bullet, loot, drawdungeon

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

graph TD start[Start] --> setup[setup] setup --> generatedungeon[generatedungeon] setup --> draw[draw loop] draw --> deltatime-calc[deltatime-calc] draw --> state-conditional[state-conditional] state-conditional -->|Playing| entity-updates[entity-updates] entity-updates --> input-handling[input-handling] input-handling --> movement-normalization[movement-normalization] entity-updates --> collision-detection[collision-detection] collision-detection --> loot-pickup[loot-pickup] entity-updates --> exit-check[exit-check] draw --> drawdungeon[drawdungeon] drawdungeon --> tile-grid-draw[tile-grid-draw] tile-grid-draw --> pulsing-animation[pulsing-animation] draw --> timed-spawning[timed-spawning] timed-spawning --> eligible-filter[eligible-filter] eligible-filter --> distance-check[distance-check] click setup href "#fn-setup" click generatedungeon href "#fn-generatedungeon" click draw href "#fn-draw" click deltatime-calc href "#sub-deltatime-calc" click state-conditional href "#sub-state-conditional" click entity-updates href "#sub-entity-updates" click input-handling href "#sub-input-handling" click movement-normalization href "#sub-movement-normalization" click collision-detection href "#sub-collision-detection" click loot-pickup href "#sub-loot-pickup" click exit-check href "#sub-exit-check" click drawdungeon href "#fn-drawdungeon" click tile-grid-draw href "#sub-tile-grid-draw" click pulsing-animation href "#sub-pulsing-animation" click timed-spawning href "#sub-timed-spawning" click eligible-filter href "#sub-eligible-filter" click distance-check href "#sub-distance-check"

❓ Frequently Asked Questions

What visual experience does the 'Survive 10 Years in Cave with Cheese Launcher' sketch provide?

The sketch creates a vibrant and dynamic dungeon environment where players navigate through maze-like corridors filled with colorful enemies and glowing loot.

How can players interact with the cheese launcher game?

Players can move their character using the keyboard and aim or fire the cheese launcher with their mouse to defeat enemies and collect loot.

What creative coding techniques are demonstrated in this p5.js sketch?

This sketch showcases procedural generation for creating unique dungeon layouts and timed spawning for enemies and loot, enhancing gameplay variability.

Preview

survive 10 years in cave with cheese launcher - p5.js creative coding sketch preview
Sketch Preview
Code flow diagram showing the structure of survive 10 years in cave with cheese launcher - Code flow showing setup, generatedungeon, draw, updategame, spawnenemyatrandomtile, player, enemy, bullet, loot, drawdungeon
Code Flow Diagram