Simple MineCraft Game (Louis)

This sketch creates a 2D sandbox mining game inspired by Minecraft where players explore a procedurally generated world, mine blocks, fight skeletons and spiders, and collect materials to win. The player navigates terrain physics, manages a hotbar inventory, and survives day/night cycles with enemy spawns.

🧪 Try This!

Experiment with the code by making these changes:

  1. Speed up the player — Increase MOVE_SPEED to 6 so the player moves 6 pixels/frame instead of 3, making the game feel more responsive.
  2. Make jumping easier — Increase JUMP_SPEED to 18 so the player jumps higher and can reach more areas of the world.
  3. Reduce win objective to 10 of each — Change targetQuantityPerBlock from 20 to 10, making it twice as fast to complete and win the game.
  4. Reduce skeleton damage — Change skeletonDamage from 0.7 to 0.2 so skeletons are much less threatening and you have more time to react.
  5. Double skeleton spawn rate — Change SKELETON_SPAWN_INTERVAL from 180 to 90 frames, making skeletons spawn twice as fast at night for a harder challenge.
  6. Make diamonds way more common — Change diamond weight in pickOreType() from 5.5 to 30, making diamonds almost as common as iron instead of rare.
Prefer the full editor? Open it there →

📖 About This Sketch

This sketch creates a playable 2D mining sandbox where players dig, build, and survive in a procedurally generated world. It combines Perlin noise terrain generation, tile-based collision detection, character physics (gravity and jumping), inventory management, and a day/night cycle that spawns hostile mobs. Skeletons shoot arrows and spiders can climb walls—both enemies damage the player on contact. The visual style mimics Minecraft with a pixelated font (Press Start 2P) and colored rectangular blocks.

The code is organized into world generation (Perlin noise terrain with ore spawning), player physics (movement, jumping, gravity), enemy AI (skeleton pathfinding and shooting, spider wall-climbing), a UI hotbar for block selection, a game state machine (PLAYING/WIN/LOSE), and mouse/keyboard input handling. By reading it you will learn how to build a complete game loop with multiple systems: procedural generation, physics simulation, collision detection, enemy behavior trees, and win/lose conditions.

⚙️ How It Works

  1. When the sketch loads, setup() generates a 200×60 tile world using Perlin noise, placing grass and dirt on the surface, stone underground with scattered ores (iron, gold, redstone, copper, diamond), and random trees. The player spawns on the surface and initial skeletons and spiders are placed randomly on solid ground.
  2. Every frame, the draw loop updates the player's position using WASD/arrow keys, applies gravity, checks collisions with tiles and enemies, then renders the camera-following view of the world, player, and hostile mobs.
  3. The player mines blocks by left-clicking (breaking blocks within 5 tiles, damaging nearby enemies) or places selected blocks by right-clicking. The hotbar (bottom UI) displays 10 inventory slots tied to number keys 1–0; pressing 'F' breaks/places the block directly in front of the player.
  4. A day/night cycle lasts ~60 seconds. During night (50–90% of the cycle), skeletons and spiders spawn every 2–4 seconds up to a maximum. Skeletons move toward the player and shoot arrows when within 200 pixels; spiders climb walls when blocked by terrain and can reach high areas.
  5. The win condition is collecting 20 of each material (grass, dirt, stone, wood, leaves, and all five ore types). The lose condition is falling below the loseHeight threshold or health dropping to 0 from enemy contact or arrow hits. Player health regenerates slowly after 3 seconds without damage.

🎓 Concepts You'll Learn

Procedural world generation with Perlin noiseTile-based grid collision detectionPhysics simulation (gravity, velocity, acceleration)Enemy pathfinding and behavior treesDay/night cycle and time-based eventsInventory and hotbar UI managementGame state machine (PLAYING, WIN, LOSE)Off-screen graphics buffer for performanceAudio synthesis with p5.sound oscillators and noise

📝 Code Breakdown

setup()

setup() runs once at sketch start and initializes all game state: canvas, world, entities, UI elements, and fonts. This is where you set up anything that should persist for the entire game session.

function setup() {
  createCanvas(windowWidth, windowHeight);
  worldGraphics = createGraphics(WORLD_WIDTH * TILE_SIZE, WORLD_HEIGHT * TILE_SIZE);
  noiseSeed(floor(random(100000)));
  initWorld();
  spawnSkeletons();
  spawnSpiders();
  currentDay = 1;

  textFont(minecraftFont);

  restartButton = createButton('Play Again');
  restartButton.style('font-size', '24px');
  restartButton.style('padding', '15px 30px');
  restartButton.style('background-color', '#4CAF50');
  restartButton.style('color', 'white');
  restartButton.style('border', 'none');
  restartButton.style('border-radius', '8px');
  restartButton.style('cursor', 'pointer');
  restartButton.hide();
  restartButton.mousePressed(resetGame);
}
Line-by-line explanation (8 lines)

🔧 Subcomponents:

function-call Create Canvas and Graphics Buffer createCanvas(windowWidth, windowHeight);

Sets up the display canvas to fit the window and creates an off-screen buffer for efficient world rendering

function-call Initialize World and Entities initWorld(); spawnSkeletons(); spawnSpiders();

Generates the procedural terrain, places initial enemies, and sets up the game world

createCanvas(windowWidth, windowHeight);
Creates a p5.js canvas that fills the browser window; allows fullscreen responsive design
worldGraphics = createGraphics(WORLD_WIDTH * TILE_SIZE, WORLD_HEIGHT * TILE_SIZE);
Creates an off-screen graphics buffer (6400×1920 pixels at 32px per tile) to draw the entire world once, then copy it efficiently each frame instead of redrawing all tiles
noiseSeed(floor(random(100000)));
Seeds the Perlin noise generator with a random number so each game generates a unique terrain
initWorld();
Builds the terrain array, places blocks, spawns the player, and draws the world to the graphics buffer
spawnSkeletons();
Creates the initial batch of skeleton enemies at random locations on solid ground
spawnSpiders();
Creates the initial batch of spider enemies at random locations on solid ground
textFont(minecraftFont);
Applies the loaded Press Start 2P font to all text drawn on the canvas
restartButton.hide();
Hides the restart button initially; it only appears when the game ends in WIN or LOSE state

draw()

The draw loop runs 60 times per second and is the heartbeat of the game. It splits into two phases: update (modify game state) and render (draw visuals). This separation makes code cleaner and prevents visual glitches.

function draw() {
  background(135, 206, 235);

  if (gameState === 'PLAYING') {
    updateDayNightCycle();
    handleInput();
    updatePlayer();
    updateSkeletons();
    updateSpiders();
    updatePlayerHealth();
    updateNightSpawns();
    updateArrows();
    updateCamera();

    image(worldGraphics, -cameraX, -cameraY);

    drawPlayer();
    drawSkeletons();
    drawSpiders();
    drawArrows();
    drawCursorHighlight();

    drawDayNightOverlay();

    updateGameStatus();
  }

  drawUI();
  drawEndScreen();
}
Line-by-line explanation (16 lines)

🔧 Subcomponents:

conditional Update Phase updateDayNightCycle(); handleInput(); updatePlayer(); updateSkeletons(); updateSpiders(); updatePlayerHealth(); updateNightSpawns(); updateArrows(); updateCamera();

On each frame, processes all game logic: time, input, physics, enemies, health, spawning, and camera position

function-call Render Phase image(worldGraphics, -cameraX, -cameraY); drawPlayer(); drawSkeletons(); drawSpiders(); drawArrows(); drawCursorHighlight();

Draws the world from the off-screen buffer, then layers player, enemies, arrows, and UI elements on top

background(135, 206, 235);
Clears the canvas to sky blue every frame; prevents visual trails from the previous frame
if (gameState === 'PLAYING') {
Only updates game logic if the game is in PLAYING state; skips updates when game is won or lost
updateDayNightCycle();
Advances the time of day counter and checks if a new day has begun
handleInput();
Reads keyboard input (WASD/arrows) and updates the player's horizontal velocity based on keys held down
updatePlayer();
Applies gravity to the player, updates position based on velocity, and checks collisions with tiles
updateSkeletons();
Moves each skeleton toward the player, handles jumping/wall collisions, checks damage on player contact, and handles skeleton shooting
updateSpiders();
Moves each spider toward the player, handles wall-climbing behavior, and checks damage on player contact
updatePlayerHealth();
Regenerates player health slowly if enough time has passed since the last damage taken
updateNightSpawns();
During night time, periodically spawns new skeletons and spiders up to their maximum counts
updateArrows();
Moves all arrows fired by skeletons, checks collisions with tiles and the player, and removes off-screen arrows
updateCamera();
Positions the camera to follow the player while keeping it within world bounds
image(worldGraphics, -cameraX, -cameraY);
Draws the pre-rendered world buffer at an offset by the camera position, creating the scrolling effect
drawDayNightOverlay();
Darkens the screen with a semi-transparent overlay during dusk and night
updateGameStatus();
Checks win and lose conditions, transitions game state if needed
drawUI();
Renders the hotbar, instructions panel, objectives counter, and health bar overlay
drawEndScreen();
If game is won or lost, draws the end screen with message and positions the restart button

initWorld()

initWorld() is the procedural generation engine. It uses Perlin noise to create organic terrain, randomly places ores, and spawns trees. The playerPlaced array is key: it distinguishes between naturally generated blocks and player-placed ones so that only mined natural blocks count toward the win condition.

🔬 The 0.12 is the noise scale—this controls how smooth or bumpy the terrain is. What happens if you change it to 0.05 (bumpier) or 0.2 (smoother)? Try both and observe the terrain.

    let h = floor(noise(x * 0.12) * 10) + baseHeight;
    h = constrain(h, 12, WORLD_HEIGHT - 6);

🔬 ORE_SPAWN_CHANCE is currently 0.25 (25%). What happens if you change it to 0.5 to make the world half ore? Or 0.1 to make ores scarce?

        if (random() < ORE_SPAWN_CHANCE) {
          blockId = pickOreType();
        }
function initWorld() {
  world = new Array(WORLD_WIDTH);
  playerPlaced = new Array(WORLD_WIDTH);
  for (let x = 0; x < WORLD_WIDTH; x++) {
    world[x] = new Array(WORLD_HEIGHT).fill(AIR);
    playerPlaced[x] = new Array(WORLD_HEIGHT).fill(false);
  }

  const baseHeight = 30;
  for (let x = 0; x < WORLD_WIDTH; x++) {
    let h = floor(noise(x * 0.12) * 10) + baseHeight;
    h = constrain(h, 12, WORLD_HEIGHT - 6);

    for (let y = h; y < WORLD_HEIGHT; y++) {
      let blockId = STONE;
      if (y > h + 3) {
        if (random() < ORE_SPAWN_CHANCE) {
          blockId = pickOreType();
        }
      }
      world[x][y] = blockId;
      playerPlaced[x][y] = false;
    }

    if (h - 1 >= 0) {
      world[x][h - 1] = GRASS;
      playerPlaced[x][h - 1] = false;
    }
    if (h - 2 >= 0) {
      world[x][h - 2] = DIRT;
      playerPlaced[x][h - 2] = false;
    }
    if (h - 3 >= 0) {
      world[x][h - 3] = DIRT;
      playerPlaced[x][h - 3] = false;
    }

    if (random() < 0.08 && h - 4 > 0) {
      const trunkHeight = 4;
      for (let t = 0; t < trunkHeight; t++) {
        const ty = h - 2 - t;
        if (ty >= 0) {
          world[x][ty] = WOOD;
          playerPlaced[x][ty] = false;
        }
      }

      const leafY = h - 2 - trunkHeight;
      for (let lx = -2; lx <= 2; lx++) {
        for (let ly = -2; ly <= 1; ly++) {
          const tx = x + lx;
          const ty = leafY + ly;
          if (tx >= 0 && tx < WORLD_WIDTH && ty >= 0 && ty < WORLD_HEIGHT) {
            if (dist(lx, ly, 0, 0) < 2.6 && world[tx][ty] === AIR) {
              world[tx][ty] = LEAVES;
              playerPlaced[tx][ty] = false;
            }
          }
        }
      }
    }
  }

  const spawnX = floor(WORLD_WIDTH / 2);
  let spawnY = 0;
  let foundSpawnSurface = false;
  for (let y = 0; y < WORLD_HEIGHT && !foundSpawnSurface; y++) {
    if (isSolidTile(spawnX, y)) {
      spawnY = y - 1;
      foundSpawnSurface = true;
    }
  }
  if (spawnY < 0) spawnY = 0;

  player.x = spawnX * TILE_SIZE;
  player.y = spawnY * TILE_SIZE - player.h;

  drawWorldGraphics();
}
Line-by-line explanation (15 lines)

🔧 Subcomponents:

for-loop 2D Array Initialization for (let x = 0; x < WORLD_WIDTH; x++) { world[x] = new Array(WORLD_HEIGHT).fill(AIR); playerPlaced[x] = new Array(WORLD_HEIGHT).fill(false); }

Creates two 2D arrays: one storing block types, one tracking whether each block was placed by the player (for win condition counting)

for-loop Perlin Noise Terrain Generation for (let x = 0; x < WORLD_WIDTH; x++) { let h = floor(noise(x * 0.12) * 10) + baseHeight; h = constrain(h, 12, WORLD_HEIGHT - 6);

Uses Perlin noise to generate smooth, natural-looking terrain height variation across the world

conditional Ore Spawning if (y > h + 3) { if (random() < ORE_SPAWN_CHANCE) { blockId = pickOreType(); } }

Deep underground (more than 3 blocks below surface), randomly replaces stone with ore types at 25% probability

conditional Random Tree Generation if (random() < 0.08 && h - 4 > 0) { const trunkHeight = 4; for (let t = 0; t < trunkHeight; t++) { ... } const leafY = h - 2 - trunkHeight; for (let lx = -2; lx <= 2; lx++) { ... }

At 8% chance per column, spawns a tree with a 4-block trunk and a circular leaves cluster at the top

world = new Array(WORLD_WIDTH);
Creates the first dimension of the 2D world array (200 columns)
world[x] = new Array(WORLD_HEIGHT).fill(AIR);
For each column, creates a second dimension (60 rows) and fills with AIR (block ID 0)
let h = floor(noise(x * 0.12) * 10) + baseHeight;
Uses Perlin noise scaled by 0.12 to create smooth terrain; the result is offset from a base height of 30 to keep terrain visible
h = constrain(h, 12, WORLD_HEIGHT - 6);
Clamps the terrain height between 12 and 54 so it stays within the world bounds and leaves room for digging
for (let y = h; y < WORLD_HEIGHT; y++) {
Fills all tiles from the surface down to the bottom with stone (underground)
if (y > h + 3) {
Only spawns ores deeper than 3 blocks below the surface, preventing ores from appearing near grass
if (random() < ORE_SPAWN_CHANCE) {
25% chance per deep stone block to become an ore instead of remaining stone
blockId = pickOreType();
Calls a function that randomly selects an ore type weighted by rarity (diamond is rarest)
if (h - 1 >= 0) { world[x][h - 1] = GRASS;
Places grass on the surface (one block above the stone layer)
if (random() < 0.08 && h - 4 > 0) {
8% chance to spawn a tree if there is enough vertical space (at least 4 blocks above surface)
if (dist(lx, ly, 0, 0) < 2.6 && world[tx][ty] === AIR) {
Places leaves in a circle (distance < 2.6 from center) only in empty air blocks, creating a natural tree crown
const spawnX = floor(WORLD_WIDTH / 2);
Spawns the player at the world center horizontally
for (let y = 0; y < WORLD_HEIGHT && !foundSpawnSurface; y++) {
Scans downward from the top to find the first solid block (the terrain surface)
if (isSolidTile(spawnX, y)) {
When a solid block is found, places the player on top of it
drawWorldGraphics();
Renders the entire world to the off-screen graphics buffer for efficient repeated use

updatePlayer()

updatePlayer() implements the core physics loop: apply gravity, move the player based on velocity, then use tile-based collision detection to prevent passing through solid blocks. Separating horizontal and vertical collision allows the player to slide along walls and walk on uneven terrain naturally.

🔬 This checks a single column of tiles (tileRight) to stop horizontal movement. What happens if you change tileRight to tileRight + 1 to check one tile further ahead? The player should pass partially through walls.

  if (player.vx > 0) {
    const right = newX + player.w;
    const top = player.y;
    const bottom = player.y + player.h - 1;
    const tileRight = floor(right / TILE_SIZE);
    for (let ty = floor(top / TILE_SIZE); ty <= floor(bottom / TILE_SIZE); ty++) {
      if (isSolidTile(tileRight, ty)) {
function updatePlayer() {
  player.vy += GRAVITY;
  player.vy = min(player.vy, 18);
  player.onGround = false;

  let newX = player.x + player.vx;
  newX = constrain(newX, 0, WORLD_WIDTH * TILE_SIZE - player.w);

  if (player.vx > 0) {
    const right = newX + player.w;
    const top = player.y;
    const bottom = player.y + player.h - 1;
    const tileRight = floor(right / TILE_SIZE);
    for (let ty = floor(top / TILE_SIZE); ty <= floor(bottom / TILE_SIZE); ty++) {
      if (isSolidTile(tileRight, ty)) {
        newX = tileRight * TILE_SIZE - player.w;
        player.vx = 0;
        break;
      }
    }
  } else if (player.vx < 0) {
    const left = newX;
    const top = player.y;
    const bottom = player.y + player.h - 1;
    const tileLeft = floor(left / TILE_SIZE);
    for (let ty = floor(top / TILE_SIZE); ty <= floor(bottom / TILE_SIZE); ty++) {
      if (isSolidTile(tileLeft, ty)) {
        newX = (tileLeft + 1) * TILE_SIZE;
        player.vx = 0;
        break;
      }
    }
  }
  player.x = newX;

  let newY = player.y + player.vy;

  if (player.vy > 0) {
    let bottom = newY + player.h;
    if (bottom >= WORLD_HEIGHT * TILE_SIZE) {
      newY = WORLD_HEIGHT * TILE_SIZE - player.h;
      player.vy = 0;
      player.onGround = true;
      if (gameState === 'PLAYING') {
        gameState = 'LOSE';
        restartButton.show();
      }
    } else {
      const left = player.x;
      const right = player.x + player.w - 1;
      const tileBottom = floor(bottom / TILE_SIZE);
      for (let tx = floor(left / TILE_SIZE); tx <= floor(right / TILE_SIZE); tx++) {
        if (isSolidTile(tx, tileBottom)) {
          newY = tileBottom * TILE_SIZE - player.h;
          player.vy = 0;
          player.onGround = true;
          break;
        }
      }
    }
  } else if (player.vy < 0) {
    let top = newY;
    if (top <= 0) {
      newY = 0;
      player.vy = 0;
    } else {
      const left = player.x;
      const right = player.x + player.w - 1;
      const tileTop = floor(top / TILE_SIZE);
      for (let tx = floor(left / TILE_SIZE); tx <= floor(right / TILE_SIZE); tx++) {
        if (isSolidTile(tx, tileTop)) {
          newY = (tileTop + 1) * TILE_SIZE;
          player.vy = 0;
          break;
        }
      }
    }
  }

  player.y = newY;
}
Line-by-line explanation (12 lines)

🔧 Subcomponents:

calculation Gravity Application player.vy += GRAVITY; player.vy = min(player.vy, 18);

Accelerates the player downward each frame and caps falling speed at 18 pixels/frame to prevent unrealistic behavior

conditional Horizontal Collision Detection if (player.vx > 0) { const right = newX + player.w; const tileRight = floor(right / TILE_SIZE); for (let ty = floor(top / TILE_SIZE); ty <= floor(bottom / TILE_SIZE); ty++) { if (isSolidTile(tileRight, ty)) { newX = tileRight * TILE_SIZE - player.w; player.vx = 0; break; } } }

When moving right, checks tiles at the player's right edge; if solid, stops movement and places player flush against the wall

conditional Vertical Collision Detection if (player.vy > 0) { let bottom = newY + player.h; const tileBottom = floor(bottom / TILE_SIZE); for (let tx = floor(left / TILE_SIZE); tx <= floor(right / TILE_SIZE); tx++) { if (isSolidTile(tx, tileBottom)) { newY = tileBottom * TILE_SIZE - player.h; player.vy = 0; player.onGround = true; break; } } }

When falling, checks tiles below the player's bottom edge; if solid, stops fall, sets onGround flag, and positions player on top of the block

player.vy += GRAVITY;
Increases vertical velocity downward by 0.8 each frame, simulating constant gravitational acceleration
player.vy = min(player.vy, 18);
Caps downward velocity at 18 so the player doesn't fall too fast and pass through terrain
player.onGround = false;
Resets onGround to false each frame; it is set back to true only if collision with solid tile below is detected
let newX = player.x + player.vx;
Calculates where the player would be after moving horizontally; collision checks will adjust this if needed
const tileRight = floor(right / TILE_SIZE);
Converts the player's right edge pixel position to a tile grid coordinate
for (let ty = floor(top / TILE_SIZE); ty <= floor(bottom / TILE_SIZE); ty++) {
Loops through all tiles along the player's vertical span at the collision point (top to bottom)
if (isSolidTile(tileRight, ty)) {
Checks if the tile at this position contains a solid block (not air)
newX = tileRight * TILE_SIZE - player.w;
Repositions the player so the right edge is flush against the left edge of the solid tile (no overlap)
player.vx = 0;
Stops horizontal movement to prevent the player from passing through the wall
if (bottom >= WORLD_HEIGHT * TILE_SIZE) {
Checks if the player has fallen past the bottom of the world (instant lose condition)
if (isSolidTile(tx, tileBottom)) {
Checks if the tile below the player is solid; if so, land the player on it
player.onGround = true;
Sets onGround to true so the player can jump on the next keypress

updateSkeletons()

updateSkeletons() implements enemy AI: collision damage, simple 1D pathfinding toward the player, wall-jump traversal, and ranged attack cooldowns. The loop processes each skeleton's physics (gravity, collision) and behavior independently, making the skeleton army feel like individuals reacting to the player.

🔬 The shootTimer increments every frame and shoots when it reaches shootCooldown (120 frames = 2 seconds). What happens if you change shootCooldown to 30? Skeletons shoot 4x faster.

    skel.shootTimer++;
    if (skel.shootTimer >= skel.shootCooldown) {
      const distToPlayer = dist(skel.x + skel.w / 2, skel.y + skel.h / 2, player.x + player.w / 2, player.y + player.h / 2);
      if (distToPlayer <= SKELETON_SHOOT_RANGE) {
function updateSkeletons() {
  for (let i = skeletons.length - 1; i >= 0; i--) {
    let skel = skeletons[i];

    if (rectsOverlap(skel.x, skel.y, skel.w, skel.h, player.x, player.y, player.w, player.h)) {
      playerHealth -= skeletonDamage;
      lastDamageFrame = frameCount;
      if (playerHealth <= 0) {
        gameState = 'LOSE';
        restartButton.show();
        return;
      }
    }

    skel.vy += GRAVITY;
    skel.vy = min(skel.vy, 18);
    skel.onGround = false;

    if (skel.x < player.x) {
      skel.vx = skeletonMoveSpeed;
    } else if (skel.x > player.x) {
      skel.vx = -skeletonMoveSpeed;
    } else {
      skel.vx = 0;
    }

    let newSkX = skel.x + skel.vx;
    newSkX = constrain(newSkX, 0, WORLD_WIDTH * TILE_SIZE - skel.w);

    if (skel.vx > 0) {
      const right = newSkX + skel.w;
      const top = skel.y;
      const bottom = skel.y + skel.h - 1;
      const tileRight = floor(right / TILE_SIZE);
      let collidedRight = false;
      for (let ty = floor(top / TILE_SIZE); ty <= floor(bottom / TILE_SIZE) && !collidedRight; ty++) {
        if (isSolidTile(tileRight, ty)) {
          newSkX = tileRight * TILE_SIZE - skel.w;
          skel.vx = 0;
          if (skel.onGround) {
            skel.vy = -skeletonJumpSpeed;
            skel.onGround = false;
          }
          collidedRight = true;
        }
      }
    } else if (skel.vx < 0) {
      const left = newSkX;
      const top = skel.y;
      const bottom = skel.y + skel.h - 1;
      const tileLeft = floor(left / TILE_SIZE);
      let collidedLeft = false;
      for (let ty = floor(top / TILE_SIZE); ty <= floor(bottom / TILE_SIZE) && !collidedLeft; ty++) {
        if (isSolidTile(tileLeft, ty)) {
          newSkX = (tileLeft + 1) * TILE_SIZE;
          skel.vx = 0;
          if (skel.onGround) {
            skel.vy = -skeletonJumpSpeed;
            skel.onGround = false;
          }
          collidedLeft = true;
        }
      }
    }
    skel.x = newSkX;

    let newSkY = skel.y + skel.vy;

    if (skel.vy > 0) {
      let bottom = newSkY + skel.h;
      if (bottom >= WORLD_HEIGHT * TILE_SIZE) {
        skeletons.splice(i, 1);
        continue;
      } else {
        const left = skel.x;
        const right = skel.x + skel.w - 1;
        const tileBottom = floor(bottom / TILE_SIZE);
        for (let tx = floor(left / TILE_SIZE); tx <= floor(right / TILE_SIZE); tx++) {
          if (isSolidTile(tx, tileBottom)) {
            newSkY = tileBottom * TILE_SIZE - skel.h;
            skel.vy = 0;
            skel.onGround = true;
            break;
          }
        }
      }
    } else if (skel.vy < 0) {
      let top = newSkY;
      if (top <= 0) {
        newSkY = 0;
        skel.vy = 0;
      } else {
        const left = skel.x;
        const right = skel.x + skel.w - 1;
        const tileTop = floor(top / TILE_SIZE);
        for (let tx = floor(left / TILE_SIZE); tx <= floor(right / TILE_SIZE); tx++) {
          if (isSolidTile(tx, tileTop)) {
            newSkY = (tileTop + 1) * TILE_SIZE;
            skel.vy = 0;
            break;
          }
        }
      }
    }
    skel.y = newSkY;

    skel.shootTimer++;
    if (skel.shootTimer >= skel.shootCooldown) {
      const distToPlayer = dist(skel.x + skel.w / 2, skel.y + skel.h / 2, player.x + player.w / 2, player.y + player.h / 2);
      if (distToPlayer <= SKELETON_SHOOT_RANGE) {
        spawnArrow(skel.x + skel.w / 2, skel.y + skel.h / 2, player.x + player.w / 2, player.y + player.h / 2);
        skel.shootTimer = 0;
      }
    }

    if (skel.health <= 0) {
      skeletons.splice(i, 1);
    }
  }
}
Line-by-line explanation (13 lines)

🔧 Subcomponents:

conditional Damage on Contact if (rectsOverlap(skel.x, skel.y, skel.w, skel.h, player.x, player.y, player.w, player.h)) { playerHealth -= skeletonDamage; lastDamageFrame = frameCount;

If skeleton overlaps player hitbox, deal damage and record the frame for health regeneration delay

conditional Simple Pathfinding Toward Player if (skel.x < player.x) { skel.vx = skeletonMoveSpeed; } else if (skel.x > player.x) { skel.vx = -skeletonMoveSpeed; } else { skel.vx = 0; }

Moves skeleton left or right toward the player's x position; basic 1D pathfinding

conditional Wall Jump on Blocked Movement if (skel.onGround) { skel.vy = -skeletonJumpSpeed; skel.onGround = false; }

When a skeleton hits a wall while on the ground, automatically jumps to traverse obstacles

conditional Skeleton Shooting Behavior skel.shootTimer++; if (skel.shootTimer >= skel.shootCooldown) { const distToPlayer = dist(skel.x + skel.w / 2, skel.y + skel.h / 2, player.x + player.w / 2, player.y + player.h / 2); if (distToPlayer <= SKELETON_SHOOT_RANGE) { spawnArrow(skel.x + skel.w / 2, skel.y + skel.h / 2, player.x + player.w / 2, player.y + player.h / 2); skel.shootTimer = 0; } }

Increments a cooldown timer; when cooldown expires and player is within range, fires an arrow toward the player and resets timer

for (let i = skeletons.length - 1; i >= 0; i--) {
Loops through skeletons backwards so removing an element (splice) doesn't skip indices
if (rectsOverlap(skel.x, skel.y, skel.w, skel.h, player.x, player.y, player.w, player.h)) {
Checks if the skeleton's bounding box overlaps the player's bounding box (collision)
playerHealth -= skeletonDamage;
Subtracts 0.7 health per frame of contact; rapid damage when skeletons touch the player
lastDamageFrame = frameCount;
Records which frame the damage occurred so health regeneration knows when to begin (after 180 frames of no damage)
if (skel.x < player.x) {
If skeleton is to the left of player, move right (toward player)
skel.vx = skeletonMoveSpeed;
Sets horizontal velocity to +1 pixel per frame (right)
if (skel.onGround) {
Only jump if currently standing on solid ground; prevents mid-air jumping
skel.vy = -skeletonJumpSpeed;
Sets upward velocity to -7.2 to jump over obstacles when blocked horizontally
const distToPlayer = dist(skel.x + skel.w / 2, skel.y + skel.h / 2, player.x + player.w / 2, player.y + player.h / 2);
Calculates Euclidean distance from skeleton center to player center
if (distToPlayer <= SKELETON_SHOOT_RANGE) {
Only shoots if player is within 200 pixels; prevents arrows from across the map
spawnArrow(skel.x + skel.w / 2, skel.y + skel.h / 2, player.x + player.w / 2, player.y + player.h / 2);
Creates a new arrow at the skeleton's center aimed at the player's center
if (skel.health <= 0) {
If health reaches 0 (from player breaking blocks nearby), remove skeleton from array
skeletons.splice(i, 1);
Removes the skeleton at index i, deleting it from the game world

updateArrows()

updateArrows() simulates projectile motion: each arrow moves by its velocity, checks collisions with terrain and player, and removes itself when hit or off-screen. The camera offset is critical for accurate collision detection since arrow coordinates are in screen space but tile checks need world space.

function updateArrows() {
  for (let i = arrows.length - 1; i >= 0; i--) {
    let arrow = arrows[i];
    arrow.x += arrow.vx;
    arrow.y += arrow.vy;

    const tileX = floor((arrow.x + cameraX) / TILE_SIZE);
    const tileY = floor((arrow.y + cameraY) / TILE_SIZE);
    if (isSolidTile(tileX, tileY)) {
      arrows.splice(i, 1);
      continue;
    }

    if (rectsOverlap(arrow.x - ARROW_W / 2, arrow.y - ARROW_H / 2, ARROW_W, ARROW_H, player.x, player.y, player.w, player.h)) {
      playerHealth -= arrow.damage;
      lastDamageFrame = frameCount;
      if (playerHealth <= 0) {
        gameState = 'LOSE';
        restartButton.show();
      }
      arrows.splice(i, 1);
      continue;
    }

    if (arrow.x < -100 || arrow.x > WORLD_WIDTH * TILE_SIZE + 100 || arrow.y < -100 || arrow.y > WORLD_HEIGHT * TILE_SIZE + 100) {
      arrows.splice(i, 1);
    }
  }
}
Line-by-line explanation (9 lines)

🔧 Subcomponents:

calculation Arrow Movement arrow.x += arrow.vx; arrow.y += arrow.vy;

Updates arrow position based on its velocity vector each frame

conditional Tile Collision Detection const tileX = floor((arrow.x + cameraX) / TILE_SIZE); const tileY = floor((arrow.y + cameraY) / TILE_SIZE); if (isSolidTile(tileX, tileY)) { arrows.splice(i, 1); continue; }

Converts arrow world position to tile coordinates; if tile is solid, removes arrow (collision with terrain)

conditional Player Collision Detection if (rectsOverlap(arrow.x - ARROW_W / 2, arrow.y - ARROW_H / 2, ARROW_W, ARROW_H, player.x, player.y, player.w, player.h)) { playerHealth -= arrow.damage;

Checks if arrow hitbox overlaps player hitbox; if so, deals damage and removes arrow

for (let i = arrows.length - 1; i >= 0; i--) {
Loops backwards through arrows so splicing (removal) doesn't skip indices
arrow.x += arrow.vx;
Moves arrow horizontally based on its velocity (set when spawned toward player)
arrow.y += arrow.vy;
Moves arrow vertically based on its velocity (slight downward arc due to gravity)
const tileX = floor((arrow.x + cameraX) / TILE_SIZE);
Converts arrow screen position to world tile coordinate using camera offset and tile size
if (isSolidTile(tileX, tileY)) {
Checks if the tile at arrow's position is solid (terrain hit)
arrows.splice(i, 1);
Removes the arrow from the array and continues to next iteration (skip remaining checks)
if (rectsOverlap(arrow.x - ARROW_W / 2, arrow.y - ARROW_H / 2, ARROW_W, ARROW_H, player.x, player.y, player.w, player.h)) {
Checks if arrow's bounding box (16×4 pixels centered on arrow position) overlaps player's bounding box
playerHealth -= arrow.damage;
Subtracts 10 health per arrow hit
if (arrow.x < -100 || arrow.x > WORLD_WIDTH * TILE_SIZE + 100 || arrow.y < -100 || arrow.y > WORLD_HEIGHT * TILE_SIZE + 100) {
Removes arrows that have traveled far off-screen (cleanup to prevent memory leaks)

drawUI()

drawUI() renders all overlays: the hotbar for inventory, the instructions panel, the objectives counter, and the health bar. It uses text alignment, color mapping, and layout math to center and position UI elements responsively.

function drawUI() {
  push();

  const barH = UI_BAR_H;
  noStroke();
  fill(0, 0, 0, 130);
  rect(0, height - barH, width, barH);

  const slots = inventory.length;
  const slotSize = 40;
  const gap = 12;
  const totalW = slots * slotSize + (slots - 1) * gap;
  const startX = (width - totalW) / 2;
  const startY = height - barH + (barH - slotSize) / 2;

  textAlign(CENTER, CENTER);
  textSize(16);

  for (let i = 0; i < slots; i++) {
    const x = startX + i * (slotSize + gap);
    const y = startY;

    stroke(255);
    strokeWeight(1);
    fill(50, 50, 50, 220);
    rect(x, y, slotSize, slotSize, 4);

    const blockId = inventory[i];
    const c = getBlockColor(blockId);
    if (blockId !== AIR) {
      noStroke();
      if (c.length === 1) fill(c[0]);
      else fill(c[0], c[1], c[2]);
      rect(x + 6, y + 6, slotSize - 12, slotSize - 12, 3);
    }

    fill(255);
    noStroke();
    let label;
    if (i < 9) {
      label = (i + 1).toString();
    } else {
      label = '0';
    }
    text(label, x + slotSize / 2, y + slotSize / 2);
  }

  const panelW = 380;
  const panelH = 105;
  fill(0, 0, 0, 180);
  stroke(255);
  strokeWeight(1);
  rect(10, 10, panelW, panelH, 8);
  fill(255);
  textAlign(LEFT, TOP);
  textSize(12);
  text(
    `Day: ${currentDay}\n` +
    "Move: A/D or ←/→\n" +
    "Jump: W / Space / ↑\n" +
    "1–0: choose hotbar slot\n" +
    "F: use block in front\n" +
    "Mouse: Left=break, Right=place",
    18,
    18
  );

  fill(255);
  textAlign(LEFT, TOP);
  textSize(16);
  const materialNames = {
    [GRASS]:        "Grass",
    [DIRT]:         "Dirt",
    [STONE]:        "Stone",
    [WOOD]:         "Wood",
    [LEAVES]:       "Leaves",
    [IRON_ORE]:     "Iron",
    [REDSTONE_ORE]: "Redstone",
    [COPPER_ORE]:   "Copper",
    [GOLD_ORE]:     "Gold",
    [DIAMOND_ORE]:  "Diamond"
  };

  const objectiveOrder = [
    GRASS,
    DIRT,
    STONE,
    WOOD,
    LEAVES,
    IRON_ORE,
    REDSTONE_ORE,
    COPPER_ORE,
    GOLD_ORE,
    DIAMOND_ORE
  ];

  let yOffset = 220;
  for (let blockId of objectiveOrder) {
    const name = materialNames[blockId];
    const count = inventoryCounts[blockId];

    if (count >= targetQuantityPerBlock) {
      fill(0, 255, 0);
    } else {
      fill(255);
    }
    text(`${name}: ${count}/${targetQuantityPerBlock}`, 10, yOffset);
    yOffset += 20;
  }

  const healthBarWidth = 100;
  const healthBarHeight = 10;
  const healthBarX = 10;
  const healthBarY = yOffset + 10;

  fill(255, 0, 0);
  stroke(255);
  strokeWeight(1);
  rect(healthBarX, healthBarY, healthBarWidth, healthBarHeight);

  fill(0, 255, 0);
  noStroke();
  const currentHealthWidth = map(playerHealth, 0, playerMaxHealth, 0, healthBarWidth);
  rect(healthBarX, healthBarY, currentHealthWidth, healthBarHeight);

  fill(255);
  textAlign(LEFT, CENTER);
  textSize(14);
  text(`Health: ${floor(playerHealth)}/${playerMaxHealth}`, healthBarX + healthBarWidth + 5, healthBarY + healthBarHeight / 2);

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

🔧 Subcomponents:

for-loop Hotbar Slot Rendering for (let i = 0; i < slots; i++) { const x = startX + i * (slotSize + gap); const y = startY; stroke(255); strokeWeight(1); fill(50, 50, 50, 220); rect(x, y, slotSize, slotSize, 4); const blockId = inventory[i]; const c = getBlockColor(blockId); if (blockId !== AIR) { noStroke(); if (c.length === 1) fill(c[0]); else fill(c[0], c[1], c[2]); rect(x + 6, y + 6, slotSize - 12, slotSize - 12, 3); } fill(255); noStroke(); let label = i < 9 ? (i + 1).toString() : '0'; text(label, x + slotSize / 2, y + slotSize / 2); }

Draws 10 inventory slots at the bottom, each showing a block preview and keyboard number (1–0)

for-loop Objectives Progress Display let yOffset = 220; for (let blockId of objectiveOrder) { const name = materialNames[blockId]; const count = inventoryCounts[blockId]; if (count >= targetQuantityPerBlock) { fill(0, 255, 0); } else { fill(255); } text(`${name}: ${count}/${targetQuantityPerBlock}`, 10, yOffset); yOffset += 20; }

Lists all 10 materials with their collected count; turns green when target is reached

const barH = UI_BAR_H;
Uses the constant UI_BAR_H (60 pixels) for the hotbar height
fill(0, 0, 0, 130);
Sets a semi-transparent black background for the hotbar UI (alpha 130 out of 255)
rect(0, height - barH, width, barH);
Draws the background rectangle at the bottom of the screen
const totalW = slots * slotSize + (slots - 1) * gap;
Calculates the total width of all hotbar slots (10 × 40px + 9 × 12px gap)
const startX = (width - totalW) / 2;
Centers the hotbar horizontally by calculating left offset
for (let i = 0; i < slots; i++) {
Loops through all 10 inventory slots
rect(x, y, slotSize, slotSize, 4);
Draws a 40×40 pixel slot background with 4-pixel rounded corners
const c = getBlockColor(blockId);
Gets the RGB color for the block type from a lookup function
if (blockId !== AIR) {
Only draws a block preview if the slot contains a non-air block
rect(x + 6, y + 6, slotSize - 12, slotSize - 12, 3);
Draws a smaller block preview inset inside the slot (6px margin on all sides)
let label = i < 9 ? (i + 1).toString() : '0';
Sets label to '1'–'9' for slots 0–8, or '0' for slot 9 (keyboard shortcut)
if (count >= targetQuantityPerBlock) {
If objective count reaches 20, color the text green to show completion
fill(0, 255, 0);
Green text to indicate objective completed
const currentHealthWidth = map(playerHealth, 0, playerMaxHealth, 0, healthBarWidth);
Scales health bar width proportionally to remaining health (0–100 maps to 0–100 pixels)

handleWorldTap()

handleWorldTap() is the core interaction system: it converts mouse coordinates to tiles, handles breaking (remove block, unlock ore, count objective, damage enemies) and placing (add block, mark as player-placed). The camera offset is crucial for accurate tile lookup, and the playerPlaced array distinguishes natural vs. constructed blocks for win condition logic.

🔬 This only increments the objective count if the block was naturally generated (!playerPlaced) AND hasn't reached the target yet. What happens if you remove the playerPlaced check? Player-placed blocks now count toward win condition, making the game easier.

      if (!playerPlaced[tileX][tileY] && inventoryCounts.hasOwnProperty(brokenBlockId)) {
        if (inventoryCounts[brokenBlockId] < targetQuantityPerBlock) {
          inventoryCounts[brokenBlockId]++;
        }
      }
function handleWorldTap(px, py) {
  const tileX = floor((px + cameraX) / TILE_SIZE);
  const tileY = floor((py + cameraY) / TILE_SIZE);
  if (!isInWorld(tileX, tileY)) return;

  const playerTileX = floor((player.x + player.w / 2) / TILE_SIZE);
  const playerTileY = floor((player.y + player.h / 2) / TILE_SIZE);

  const distanceX = abs(playerTileX - tileX);
  const distanceY = abs(playerTileY - tileY);

  if (world[tileX][tileY] !== AIR) {
    if (max(distanceX, distanceY) <= 5) {
      const brokenBlockId = world[tileX][tileY];

      ensureBlockInInventory(brokenBlockId);

      world[tileX][tileY] = AIR;
      playBlockBreakSound();

      if (!playerPlaced[tileX][tileY] && inventoryCounts.hasOwnProperty(brokenBlockId)) {
        if (inventoryCounts[brokenBlockId] < targetQuantityPerBlock) {
          inventoryCounts[brokenBlockId]++;
        }
      }

      worldGraphics.noStroke();
      worldGraphics.fill(135, 206, 235);
      worldGraphics.rect(tileX * TILE_SIZE, tileY * TILE_SIZE, TILE_SIZE, TILE_SIZE);

      for (let skel of skeletons) {
        if (
          rectsOverlap(
            tileX * TILE_SIZE,
            tileY * TILE_SIZE,
            TILE_SIZE,
            TILE_SIZE,
            skel.x - TILE_SIZE,
            skel.y - TILE_SIZE,
            skel.w + TILE_SIZE * 2,
            skel.h + TILE_SIZE * 2
          )
        ) {
          skel.health -= playerAttackDamage;
        }
      }

      for (let s of spiders) {
        if (
          rectsOverlap(
            tileX * TILE_SIZE,
            tileY * TILE_SIZE,
            TILE_SIZE,
            TILE_SIZE,
            s.x - TILE_SIZE,
            s.y - TILE_SIZE,
            s.w + TILE_SIZE * 2,
            s.h + TILE_SIZE * 2
          )
        ) {
          s.health -= playerAttackDamage;
        }
      }
    }
  } else {
    const blockId = inventory[selectedIndex];
    if (blockId !== AIR) {
      const bx = tileX * TILE_SIZE;
      const by = tileY * TILE_SIZE;
      if (!rectsOverlap(bx, by, TILE_SIZE, TILE_SIZE, player.x, player.y, player.w, player.h)) {
        world[tileX][tileY] = blockId;
        playerPlaced[tileX][tileY] = true;
        playBlockPlaceSound();

        worldGraphics.noStroke();
        worldGraphics.fill(getBlockColor(blockId));
        worldGraphics.rect(bx, by, TILE_SIZE, TILE_SIZE);
      }
    }
  }
}
Line-by-line explanation (14 lines)

🔧 Subcomponents:

calculation Screen to Tile Coordinate Conversion const tileX = floor((px + cameraX) / TILE_SIZE); const tileY = floor((py + cameraY) / TILE_SIZE);

Converts mouse screen coordinates to world tile coordinates using camera offset

conditional Break Block Logic if (world[tileX][tileY] !== AIR) { if (max(distanceX, distanceY) <= 5) { const brokenBlockId = world[tileX][tileY]; ensureBlockInInventory(brokenBlockId); world[tileX][tileY] = AIR; playBlockBreakSound(); if (!playerPlaced[tileX][tileY] && inventoryCounts.hasOwnProperty(brokenBlockId)) { if (inventoryCounts[brokenBlockId] < targetQuantityPerBlock) { inventoryCounts[brokenBlockId]++; } }

Removes block from world, unlocks ore in inventory if needed, increments objective count if naturally generated, and clears tile in graphics buffer

for-loop Enemy Damage on Block Break for (let skel of skeletons) { if ( rectsOverlap( tileX * TILE_SIZE, tileY * TILE_SIZE, TILE_SIZE, TILE_SIZE, skel.x - TILE_SIZE, skel.y - TILE_SIZE, skel.w + TILE_SIZE * 2, skel.h + TILE_SIZE * 2 ) ) { skel.health -= playerAttackDamage; } }

Checks if enemies are near the broken block and applies damage to them (AoE attack)

conditional Place Block Logic } else { const blockId = inventory[selectedIndex]; if (blockId !== AIR) { const bx = tileX * TILE_SIZE; const by = tileY * TILE_SIZE; if (!rectsOverlap(bx, by, TILE_SIZE, TILE_SIZE, player.x, player.y, player.w, player.h)) { world[tileX][tileY] = blockId; playerPlaced[tileX][tileY] = true;

If tile is empty (AIR), place the selected block unless it would overlap the player

const tileX = floor((px + cameraX) / TILE_SIZE);
Converts mouse x position from screen space to world tile grid using camera offset and tile size
if (!isInWorld(tileX, tileY)) return;
Early exit if clicked tile is outside world bounds
const playerTileX = floor((player.x + player.w / 2) / TILE_SIZE);
Converts player center position to tile coordinates for distance checking
const distanceX = abs(playerTileX - tileX);
Calculates horizontal distance in tiles between player and target
if (max(distanceX, distanceY) <= 5) {
Uses Chebyshev distance (max of horizontal/vertical) to check if tile is within 5-tile reach
const brokenBlockId = world[tileX][tileY];
Stores the block type before removing it (used to unlock ore in inventory)
ensureBlockInInventory(brokenBlockId);
If ore, adds it to the hotbar slot for that ore type so player can place it later
world[tileX][tileY] = AIR;
Removes the block from the world array
if (!playerPlaced[tileX][tileY] && inventoryCounts.hasOwnProperty(brokenBlockId)) {
Only counts toward objective if block was naturally generated (not player-placed) and is a tracked material
worldGraphics.rect(tileX * TILE_SIZE, tileY * TILE_SIZE, TILE_SIZE, TILE_SIZE);
Updates the graphics buffer by drawing a sky-blue square where the block was (erases it visually)
rectsOverlap(tileX * TILE_SIZE, tileY * TILE_SIZE, TILE_SIZE, TILE_SIZE, skel.x - TILE_SIZE, skel.y - TILE_SIZE, skel.w + TILE_SIZE * 2, skel.h + TILE_SIZE * 2)
Checks if the broken tile overlaps an expanded enemy hitbox (tile ± 1 tile margin) for area damage
skel.health -= playerAttackDamage;
Subtracts 25 health from enemy near the broken block
if (!rectsOverlap(bx, by, TILE_SIZE, TILE_SIZE, player.x, player.y, player.w, player.h)) {
Only places block if it doesn't overlap the player's hitbox (prevents trapping yourself)
playerPlaced[tileX][tileY] = true;
Marks this block as player-placed so it doesn't count toward win objectives if mined later

pickOreType()

pickOreType() implements weighted random selection: each ore type has a probability weight, and a random number in the total range is mapped to an ore type. This pattern (weights → cumulative thresholds → random comparison) is used everywhere in game dev for loot drops, spawns, and procedural generation. Higher weights = more common.

🔬 These are the ore spawn rates. What happens if you make all weights equal (e.g., all 25)? Ores spawn equally often. Try setting wDiamond = 50 equal to wIron—diamonds become as common as iron.

  const wIron     = 50;
  const wRedstone = 40;
  const wCopper   = 35;
  const wGold     = 20;
  const wDiamond  = 5.5;
  const total = wIron + wRedstone + wCopper + wGold + wDiamond;
function pickOreType() {
  const wIron     = 50;
  const wRedstone = 40;
  const wCopper   = 35;
  const wGold     = 20;
  const wDiamond  = 5.5;
  const total = wIron + wRedstone + wCopper + wGold + wDiamond;

  const r = random(total);
  if (r < wIron) return IRON_ORE;
  if (r < wIron + wRedstone) return REDSTONE_ORE;
  if (r < wIron + wRedstone + wCopper) return COPPER_ORE;
  if (r < wIron + wRedstone + wCopper + wGold) return GOLD_ORE;
  return DIAMOND_ORE;
}
Line-by-line explanation (12 lines)

🔧 Subcomponents:

calculation Weighted Random Selection Setup const wIron = 50; const wRedstone = 40; const wCopper = 35; const wGold = 20; const wDiamond = 5.5; const total = wIron + wRedstone + wCopper + wGold + wDiamond;

Defines probability weights for each ore type and calculates their total

const wIron = 50;
Iron ore has weight 50—highest chance to spawn (50 out of 150.5 total)
const wRedstone = 40;
Redstone weight 40—slightly less common than iron
const wCopper = 35;
Copper weight 35—less common than redstone
const wGold = 20;
Gold weight 20—rare ore, 1/7 chance vs iron
const wDiamond = 5.5;
Diamond weight 5.5—rarest ore, about 1/9 the frequency of iron
const total = wIron + wRedstone + wCopper + wGold + wDiamond;
Sum of all weights (150.5) used as the range for random selection
const r = random(total);
Generates random number 0 to 150.5
if (r < wIron) return IRON_ORE;
If random value is 0–50 (50/150.5 ≈ 33%), return iron
if (r < wIron + wRedstone) return REDSTONE_ORE;
If 50–90 (40/150.5 ≈ 27%), return redstone
if (r < wIron + wRedstone + wCopper) return COPPER_ORE;
If 90–125 (35/150.5 ≈ 23%), return copper
if (r < wIron + wRedstone + wCopper + wGold) return GOLD_ORE;
If 125–145 (20/150.5 ≈ 13%), return gold
return DIAMOND_ORE;
If 145–150.5 (5.5/150.5 ≈ 4%), return diamond (rarest)

📦 Key Variables

world 2D array of numbers

Stores block IDs (0–11) at each tile coordinate [x][y]; represents the world terrain and all blocks

let world; // initialized in initWorld()
playerPlaced 2D array of booleans

Tracks whether each block was placed by the player (true) or naturally generated (false); used to determine if mined blocks count toward win objective

let playerPlaced; // initialized in initWorld()
player object

Stores player state: position (x, y), dimensions (w, h), velocity (vx, vy), and physics flag (onGround)

let player = { x: 0, y: 0, w: 22, h: 32, vx: 0, vy: 0, onGround: false };
cameraX number

Horizontal camera offset in pixels; used to pan the world view to follow the player

let cameraX = 0;
gameState string

Tracks game status: 'PLAYING' (active), 'WIN' (collected all objectives), 'LOSE' (fell off world or health to 0)

let gameState = 'PLAYING';
skeletons array of objects

Stores active skeleton enemy instances with position, velocity, health, and shooting state

let skeletons = [];
spiders array of objects

Stores active spider enemy instances with position, velocity, health, and climbing state

let spiders = [];
arrows array of objects

Stores active arrow projectiles fired by skeletons; each has position, velocity, and damage

let arrows = [];
inventory array of numbers (block IDs)

Hotbar slots (10 total) storing block types player can place; slots 0–4 start filled, slots 5–9 unlock when ores are mined

let inventory = new Array(HOTBAR_SIZE).fill(AIR); inventory[SLOT_DIRT] = DIRT;
inventoryCounts object mapping block IDs to counts

Tracks total number of each material type mined; used to check win condition (20 of each material)

let inventoryCounts = { [GRASS]: 0, [DIRT]: 0, [STONE]: 0, ... };
playerHealth number

Current player health (0–100); decreases on enemy contact or arrow hit, regenerates slowly after 3 seconds without damage

let playerHealth = playerMaxHealth;
timeOfDay number

Current time in the day/night cycle (0 to DAY_LENGTH_FRAMES); used to determine light level and spawn night mobs

let timeOfDay = 0;
currentDay number

Tracks which day/cycle the player is on; displays in UI and increments when timeOfDay wraps around

let currentDay = 1;
selectedIndex number

Index of the currently selected hotbar slot (0–9); determines which block type is placed on mouse click

let selectedIndex = 0;

🔧 Potential Improvements (6)

Here are some ways this code could be enhanced:

BUG updateArrows() and collision detection

Arrow collision uses arrow.x + cameraX to compute world tile position, but arrows are stored in screen space. If the camera is far offset, this can cause arrows to collide with wrong tiles. The coordinate system is inconsistent.

💡 Store arrows in world-space coordinates from spawn. Update the arrow spawn and rendering to work in world space consistently, then apply camera offset only during drawing (drawArrows).

PERFORMANCE updateSkeletons() and updateSpiders() collision detection

Every skeleton and spider runs horizontal and vertical collision checks against all tiles in its range every frame. With many enemies, this becomes expensive. No spatial partitioning or tile caching.

💡 Cache the set of solid tiles near each enemy, or use a simple spatial grid to query only nearby tiles instead of checking all tiles in the creature's bounding box.

STYLE Block constants (GRASS, DIRT, STONE, etc.)

Block IDs are magic numbers scattered throughout. The code uses both the constant names and the numbers interchangeably, making it hard to add new block types.

💡 Create a BlockType object that maps names to IDs and properties (color, solid, droppable) so blocks can be defined in one place and referenced consistently.

FEATURE Hotbar system

Ore blocks only appear in hotbar slots after mining; but there is no way to place natural stone or other blocks that start the game. All pre-game blocks are fixed to specific hotbar slots.

💡 Allow players to click hotbar slots to open a mini-crafting menu, or allow dynamic assignment of any mined block to any empty hotbar slot by right-clicking the slot.

BUG rectsOverlap() enemy area damage in handleWorldTap()

The collision box for area damage expands by TILE_SIZE on all sides (skel.x - TILE_SIZE to skel.x + skel.w + TILE_SIZE), which is asymmetric and can hit enemies far away or miss nearby ones if they're at tile edges.

💡 Use a fixed radius (e.g., 2 tiles) around the broken block center to determine which enemies take damage, calculated as distance from block to enemy center.

FEATURE Game state and difficulty

Difficulty is fixed; no easy/normal/hard modes or day/night speed adjustment.

💡 Add a difficulty menu in the start state that adjusts MOVE_SPEED, SKELETON_SPAWN_INTERVAL, and enemy health for different skill levels.

🔄 Code Flow

Code flow showing setup, draw, initworld, updateplayer, updateskeletons, updatearrows, drawui, handleworldtap, pickoretype

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

graph TD start[Start] --> setup[setup] setup --> createcanvas[create-canvas] createcanvas --> initworld[init-world] initworld --> draw[draw loop] draw --> updatephase[update-phase] updatephase --> updateplayer[updateplayer] updateplayer --> gravity[gravity] gravity --> horizontalcollision[horizontal-collision] horizontalcollision --> verticalcollision[vertical-collision] verticalcollision --> damagecheck[damage-check] updatephase --> updateskeletons[updateskeletons] updateskeletons --> pathfinding[pathfinding] pathfinding --> walljump[wall-jump] updateskeletons --> shootinglogic[shooting-logic] shootinglogic --> updatearrows[updatearrows] updatearrows --> movement[movement] movement --> tilecollision[tile-collision] tilecollision --> playercollision[player-collision] draw --> renderphase[render-phase] renderphase --> drawui[drawui] drawui --> hotbar[hotbar] drawui --> objectives[objectives] draw --> handleworldtap[handleworldtap] handleworldtap --> tileconversion[tile-conversion] tileconversion --> breakblock[break-block] breakblock --> enemydamage[enemy-damage] breakblock --> placeblock[place-block] draw --> pickoretype[pickoretype] pickoretype --> weightcalc[weight-calc] click setup href "#fn-setup" click createcanvas href "#sub-create-canvas" click initworld href "#fn-initworld" click draw href "#fn-draw" click updatephase href "#sub-update-phase" click updateplayer href "#fn-updateplayer" click gravity href "#sub-gravity" click horizontalcollision href "#sub-horizontal-collision" click verticalcollision href "#sub-vertical-collision" click damagecheck href "#sub-damage-check" click updateskeletons href "#fn-updateskeletons" click pathfinding href "#sub-pathfinding" click walljump href "#sub-wall-jump" click shootinglogic href "#sub-shooting-logic" click updatearrows href "#fn-updatearrows" click movement href "#sub-movement" click tilecollision href "#sub-tile-collision" click playercollision href "#sub-player-collision" click renderphase href "#sub-render-phase" click drawui href "#fn-drawui" click hotbar href "#sub-hotbar" click objectives href "#sub-objectives" click handleworldtap href "#fn-handleworldtap" click tileconversion href "#sub-tile-conversion" click breakblock href "#sub-break-block" click enemydamage href "#sub-enemy-damage" click placeblock href "#sub-place-block" click pickoretype href "#fn-pickoretype" click weightcalc href "#sub-weight-calc"

❓ Frequently Asked Questions

What visual experience does the Simple MineCraft Game sketch provide?

The sketch creates a 2D Minecraft-like sandbox environment featuring a tile grid where players can explore and interact with various blocks such as dirt, stone, and ores.

How can users interact with the Simple MineCraft Game sketch?

Users can interact with the sketch by placing and breaking blocks using keyboard controls or touch controls on mobile devices, allowing for a dynamic gameplay experience.

What creative coding concepts are demonstrated in the Simple MineCraft Game sketch?

This sketch showcases concepts such as tile-based world generation, simple physics for player movement, and interactive block manipulation, all implemented using p5.js.

Preview

Simple MineCraft Game (Louis) - p5.js creative coding sketch preview
Sketch Preview
Code flow diagram showing the structure of Simple MineCraft Game (Louis) - Code flow showing setup, draw, initworld, updateplayer, updateskeletons, updatearrows, drawui, handleworldtap, pickoretype
Code Flow Diagram