← run 2026-06-29T114508Z-e0d7de7-minimal

qwen3.6-35b-a3b — web-platformer

rep 1 MANUAL partial/5

pi 54.8s · verify 4.2s · 47277 tok · responded: qwen3.6-35b-a3b · transcript

smoke 3/3

screenshot

verify output
CHECK PASS: canvas #game exists and is sized
CHECK PASS: no uncaught JS errors on load
CHECK PASS: keyboard input does not throw
score: 3/3 (100%)
produced code
index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Super Platformer</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
html, body {
  width: 100%; height: 100%;
  background: #2c2c2c;
  display: flex;
  justify-content: center;
  align-items: center;
  overflow: hidden;
  font-family: 'Segoe UI', Arial, sans-serif;
}
#game {
  border: 3px solid #555;
  border-radius: 4px;
  display: block;
  image-rendering: pixelated;
  box-shadow: 0 0 30px rgba(0,0,0,0.5);
}
</style>
</head>
<body>
<canvas id="game" width="800" height="450"></canvas>
<script>
(function() {
  "use strict";

  var canvas = document.getElementById("game");
  var ctx = canvas.getContext("2d");
  var W = canvas.width;
  var H = canvas.height;

  // ─── Constants ───────────────────────────────────────────
  var GRAVITY = 0.55;
  var FRICTION = 0.82;
  var MOVE_ACCEL = 0.6;
  var MAX_SPEED = 5.5;
  var JUMP_FORCE = -11.5;
  var TILE = 32;
  var WORLD_W = 4800;
  var WORLD_H = 600;

  // ─── Input ───────────────────────────────────────────────
  var keys = {};
  window.addEventListener("keydown", function(e) {
    keys[e.code] = true;
    if (["Space","ArrowUp","ArrowDown","ArrowLeft","ArrowRight"].indexOf(e.code) !== -1) {
      e.preventDefault();
    }
  });
  window.addEventListener("keyup", function(e) {
    keys[e.code] = false;
  });

  // ─── World objects ───────────────────────────────────────
  // Platforms: {x, y, w, h}
  var platforms = [];
  // Coins: {x, y, collected}
  var coins = [];
  // Flag (goal)
  var flag = { x: 0, y: 0 };
  // Enemies: {x, y, w, h, speed, startX, range}
  var enemies = [];

  function buildLevel() {
    platforms = [];
    coins = [];
    enemies = [];

    // Ground segments
    addGround(0, 850);
    addGround(900, 600);
    addGround(1600, 1200);
    addGround(3000, 1800);

    // Raised platforms
    addPlatform(250, 340, 160, 20);
    addPlatform(500, 270, 120, 20);
    addPlatform(700, 200, 100, 20);
    addPlatform(950, 310, 140, 20);
    addPlatform(1150, 250, 100, 20);
    addPlatform(1350, 190, 80, 20);
    addPlatform(1700, 320, 150, 20);
    addPlatform(1900, 250, 120, 20);
    addPlatform(2100, 180, 100, 20);
    addPlatform(2400, 300, 130, 20);
    addPlatform(2600, 220, 100, 20);
    addPlatform(2800, 160, 80, 20);
    addPlatform(3100, 300, 150, 20);
    addPlatform(3350, 230, 120, 20);
    addPlatform(3600, 280, 100, 20);
    addPlatform(3800, 200, 130, 20);
    addPlatform(4000, 310, 100, 20);

    // Coins scattered on platforms and ground
    var coinPositions = [
      // On raised platforms
      270, 310, 290, 310, 310, 310,
      520, 240, 540, 240, 560, 240,
      720, 170, 740, 170,
      970, 280, 990, 280, 1010, 280,
      1170, 220, 1190, 220,
      1370, 160,
      1720, 290, 1740, 290, 1760, 290,
      1920, 220, 1940, 220,
      2120, 150,
      2420, 270, 2440, 270, 2460, 270,
      2620, 190,
      2820, 130,
      3120, 270, 3140, 270, 3160, 270,
      3370, 200, 3390, 200,
      3620, 250,
      3820, 170, 3840, 170,
      4020, 280,
      // Ground coins
      100, 410, 130, 410, 160, 410,
      400, 410, 430, 410,
      600, 410, 630, 410,
      1000, 410, 1030, 410, 1060, 410,
      1300, 410, 1330, 410,
      1650, 410, 1680, 410, 1710, 410,
      2000, 410, 2030, 410, 2060, 410,
      2300, 410, 2330, 410,
      2500, 410, 2530, 410, 2560, 410,
      2900, 410, 2930, 410,
      3200, 410, 3230, 410, 3260, 410,
      3500, 410, 3530, 410,
      3700, 410, 3730, 410, 3760, 410,
      4100, 410, 4130, 410,
      4400, 410, 4430, 410, 4460, 410,
    ];
    for (var i = 0; i < coinPositions.length; i += 2) {
      coins.push({ x: coinPositions[i], y: coinPositions[i+1], collected: false, w: 16, h: 16 });
    }

    // Flag at the end
    flag = { x: 4550, y: 340 };

    // Enemies (simple patrol bots)
    enemies.push({ x: 400, y: 414, w: 28, h: 28, speed: 1.2, startX: 300, range: 200 });
    enemies.push({ x: 1000, y: 414, w: 28, h: 28, speed: 1.5, startX: 920, range: 250 });
    enemies.push({ x: 1800, y: 414, w: 28, h: 28, speed: 1.0, startX: 1650, range: 300 });
    enemies.push({ x: 2200, y: 414, w: 28, h: 28, speed: 1.8, startX: 2100, range: 200 });
    enemies.push({ x: 3200, y: 414, w: 28, h: 28, speed: 1.3, startX: 3050, range: 350 });
    enemies.push({ x: 3900, y: 414, w: 28, h: 28, speed: 1.6, startX: 3750, range: 280 });
    enemies.push({ x: 4300, y: 414, w: 28, h: 28, speed: 1.4, startX: 4150, range: 250 });
  }

  function addGround(sx, w) {
    platforms.push({ x: sx, y: 420, w: w, h: 180 });
  }

  function addPlatform(x, y, w, h) {
    platforms.push({ x: x, y: y, w: w, h: h });
  }

  // ─── Player ──────────────────────────────────────────────
  var player = {
    x: 80, y: 350,
    w: 24, h: 32,
    vx: 0, vy: 0,
    onGround: false,
    facing: 1,
    score: 0,
    lives: 3,
    dead: false,
    win: false,
    respawnTimer: 0,
    animFrame: 0,
    animTimer: 0,
  };

  // ─── Camera ──────────────────────────────────────────────
  var cam = { x: 0, y: 0 };

  // ─── Particles ───────────────────────────────────────────
  var particles = [];

  function spawnParticles(x, y, color, count) {
    for (var i = 0; i < count; i++) {
      particles.push({
        x: x, y: y,
        vx: (Math.random() - 0.5) * 6,
        vy: (Math.random() - 0.5) * 6 - 2,
        life: 30 + Math.random() * 20,
        maxLife: 50,
        color: color,
        size: 2 + Math.random() * 3,
      });
    }
  }

  // ─── Collision helpers ───────────────────────────────────
  function aabb(a, b) {
    return a.x < b.x + b.w && a.x + a.w > b.x && a.y < b.y + b.h && a.y + a.h > b.y;
  }

  // ─── Physics update ──────────────────────────────────────
  function updatePlayer() {
    if (player.dead) {
      player.respawnTimer--;
      if (player.respawnTimer <= 0) {
        respawnPlayer();
      }
      return;
    }
    if (player.win) return;

    // Horizontal input
    var moveLeft = keys["ArrowLeft"] || keys["KeyA"];
    var moveRight = keys["ArrowRight"] || keys["KeyD"];
    var jumpKey = keys["Space"] || keys["ArrowUp"] || keys["KeyW"];

    if (moveLeft) {
      player.vx -= MOVE_ACCEL;
      player.facing = -1;
    }
    if (moveRight) {
      player.vx += MOVE_ACCEL;
      player.facing = 1;
    }

    // Clamp horizontal speed
    if (player.vx > MAX_SPEED) player.vx = MAX_SPEED;
    if (player.vx < -MAX_SPEED) player.vx = -MAX_SPEED;

    // Friction when no input
    if (!moveLeft && !moveRight) {
      player.vx *= FRICTION;
      if (Math.abs(player.vx) < 0.1) player.vx = 0;
    }

    // Jump
    if (jumpKey && player.onGround) {
      player.vy = JUMP_FORCE;
      player.onGround = false;
    }

    // Gravity
    player.vy += GRAVITY;
    if (player.vy > 14) player.vy = 14;

    // Move X
    player.x += player.vx;
    player.onGround = false;

    // Collide X with platforms
    for (var i = 0; i < platforms.length; i++) {
      var p = platforms[i];
      if (aabb(player, p)) {
        if (player.vx > 0) {
          player.x = p.x - player.w;
        } else if (player.vx < 0) {
          player.x = p.x + p.w;
        }
        player.vx = 0;
      }
    }

    // Move Y
    player.y += player.vy;

    // Collide Y with platforms
    for (var i = 0; i < platforms.length; i++) {
      var p = platforms[i];
      if (aabb(player, p)) {
        if (player.vy > 0) {
          player.y = p.y - player.h;
          player.vy = 0;
          player.onGround = true;
        } else if (player.vy < 0) {
          player.y = p.y + p.h;
          player.vy = 0;
        }
      }
    }

    // World bounds
    if (player.x < 0) { player.x = 0; player.vx = 0; }
    if (player.x + player.w > WORLD_W) { player.x = WORLD_W - player.w; player.vx = 0; }

    // Fall off screen
    if (player.y > H + 50) {
      killPlayer();
    }

    // Animation timer
    player.animTimer++;
    if (player.animTimer > 8) {
      player.animTimer = 0;
      player.animFrame = (player.animFrame + 1) % 4;
    }

    // Coin collection
    for (var i = 0; i < coins.length; i++) {
      var c = coins[i];
      if (!c.collected) {
        var coinBox = { x: c.x - 8, y: c.y - 8, w: 16, h: 16 };
        if (aabb(player, coinBox)) {
          c.collected = true;
          player.score++;
          spawnParticles(c.x, c.y, "#FFD700", 8);
        }
      }
    }

    // Flag check (win)
    var flagBox = { x: flag.x - 10, y: flag.y - 100, w: 60, h: 120 };
    if (aabb(player, flagBox)) {
      player.win = true;
      player.score += 10;
      spawnParticles(flag.x + 20, flag.y - 50, "#00FF00", 20);
      spawnParticles(flag.x + 20, flag.y - 50, "#FFD700", 20);
    }

    // Enemy collision
    for (var i = 0; i < enemies.length; i++) {
      var e = enemies[i];
      if (aabb(player, e)) {
        // Check if player is landing on top
        if (player.vy > 0 && player.y + player.h - e.y < 16) {
          // Stomp enemy
          player.vy = JUMP_FORCE * 0.6;
          e.dead = true;
          player.score += 5;
          spawnParticles(e.x + e.w/2, e.y + e.h/2, "#FF4444", 10);
        } else {
          // Player dies
          killPlayer();
        }
      }
    }
  }

  function killPlayer() {
    player.dead = true;
    player.lives--;
    player.respawnTimer = 60;
    spawnParticles(player.x + player.w/2, player.y + player.h/2, "#FF0000", 15);
  }

  function respawnPlayer() {
    player.dead = false;
    player.x = 80;
    player.y = 350;
    player.vx = 0;
    player.vy = 0;
    player.onGround = false;
  }

  function updateEnemies() {
    for (var i = 0; i < enemies.length; i++) {
      var e = enemies[i];
      if (e.dead) continue;
      e.x += e.speed;
      if (e.x > e.startX + e.range || e.x < e.startX) {
        e.speed = -e.speed;
      }
    }
    // Remove dead enemies after a delay
    enemies = enemies.filter(function(e) { return !e.dead || (e.dead && !e._remove); });
  }

  function updateParticles() {
    for (var i = particles.length - 1; i >= 0; i--) {
      var p = particles[i];
      p.x += p.vx;
      p.y += p.vy;
      p.vy += 0.15;
      p.life--;
      if (p.life <= 0) {
        particles.splice(i, 1);
      }
    }
  }

  function updateCamera() {
    var targetX = player.x - W / 2 + player.w / 2;
    var targetY = player.y - H / 2 + player.h / 2;
    cam.x += (targetX - cam.x) * 0.1;
    cam.y += (targetY - cam.y) * 0.08;
    if (cam.x < 0) cam.x = 0;
    if (cam.x > WORLD_W - W) cam.x = WORLD_W - W;
    if (cam.y > 0) cam.y = 0;
    if (cam.y < WORLD_H - H) cam.y = WORLD_H - H;
  }

  // ─── Drawing ─────────────────────────────────────────────
  function drawBackground() {
    // Sky gradient
    var grad = ctx.createLinearGradient(0, 0, 0, H);
    grad.addColorStop(0, "#5C94FC");
    grad.addColorStop(0.6, "#87CEEB");
    grad.addColorStop(1, "#B0E0FF");
    ctx.fillStyle = grad;
    ctx.fillRect(0, 0, W, H);

    // Clouds (parallax)
    drawClouds();

    // Hills (parallax)
    drawHills();
  }

  function drawClouds() {
    ctx.fillStyle = "rgba(255,255,255,0.8)";
    var cloudData = [
      { x: 100, y: 60, s: 1.0 },
      { x: 500, y: 40, s: 0.7 },
      { x: 900, y: 80, s: 1.2 },
      { x: 1400, y: 50, s: 0.8 },
      { x: 1900, y: 70, s: 1.1 },
      { x: 2400, y: 35, s: 0.9 },
      { x: 2900, y: 60, s: 1.0 },
      { x: 3400, y: 45, s: 0.7 },
      { x: 3900, y: 80, s: 1.3 },
      { x: 4400, y: 55, s: 0.8 },
    ];
    for (var i = 0; i < cloudData.length; i++) {
      var c = cloudData[i];
      var px = c.x - cam.x * 0.2;
      // Wrap
      px = ((px % (W + 200)) + (W + 200)) % (W + 200) - 100;
      var s = c.s;
      ctx.beginPath();
      ctx.arc(px, c.y, 20 * s, 0, Math.PI * 2);
      ctx.arc(px + 18 * s, c.y - 8 * s, 16 * s, 0, Math.PI * 2);
      ctx.arc(px + 36 * s, c.y, 20 * s, 0, Math.PI * 2);
      ctx.arc(px + 18 * s, c.y + 4 * s, 14 * s, 0, Math.PI * 2);
      ctx.fill();
    }
  }

  function drawHills() {
    ctx.fillStyle = "#4AAF4A";
    var hillData = [
      { x: 0, w: 300, h: 120 },
      { x: 400, w: 200, h: 80 },
      { x: 800, w: 350, h: 140 },
      { x: 1300, w: 250, h: 100 },
      { x: 1700, w: 300, h: 130 },
      { x: 2200, w: 200, h: 90 },
      { x: 2600, w: 350, h: 150 },
      { x: 3100, w: 280, h: 110 },
      { x: 3600, w: 200, h: 80 },
      { x: 4000, w: 300, h: 120 },
      { x: 4400, w: 250, h: 100 },
    ];
    for (var i = 0; i < hillData.length; i++) {
      var h = hillData[i];
      var px = h.x - cam.x * 0.4;
      ctx.beginPath();
      ctx.moveTo(px, 420);
      ctx.quadraticCurveTo(px + h.w / 2, 420 - h.h, px + h.w, 420);
      ctx.fill();
    }
  }

  function drawPlatforms() {
    for (var i = 0; i < platforms.length; i++) {
      var p = platforms[i];
      var sx = p.x - cam.x;
      var sy = p.y - cam.y;
      if (sx + p.w < -50 || sx > W + 50) continue;
      if (sy + p.h < -50 || sy > H + 50) continue;

      if (p.h > 40) {
        // Ground
        ctx.fillStyle = "#8B4513";
        ctx.fillRect(sx, sy, p.w, p.h);
        // Grass top
        ctx.fillStyle = "#4CAF50";
        ctx.fillRect(sx, sy, p.w, 8);
        ctx.fillStyle = "#66BB6A";
        ctx.fillRect(sx, sy, p.w, 4);
        // Dirt texture
        ctx.fillStyle = "#7B3B0F";
        for (var dx = 0; dx < p.w; dx += 20) {
          for (var dy = 15; dy < p.h; dy += 18) {
            ctx.fillRect(sx + dx + (dy % 3 === 0 ? 5 : 0), sy + dy, 8, 6);
          }
        }
      } else {
        // Floating platform
        ctx.fillStyle = "#A0522D";
        ctx.fillRect(sx, sy, p.w, p.h);
        ctx.fillStyle = "#8B4513";
        ctx.fillRect(sx, sy + p.h - 4, p.w, 4);
        ctx.fillStyle = "#6B3410";
        ctx.fillRect(sx + 2, sy + 2, p.w - 4, 4);
        // Brick pattern
        ctx.strokeStyle = "#5C2E0A";
        ctx.lineWidth = 1;
        for (var bx = 0; bx < p.w; bx += 16) {
          ctx.strokeRect(sx + bx, sy, 16, p.h);
        }
      }
    }
  }

  function drawCoins() {
    for (var i = 0; i < coins.length; i++) {
      var c = coins[i];
      if (c.collected) continue;
      var sx = c.x - cam.x;
      var sy = c.y - cam.y;
      if (sx < -20 || sx > W + 20) continue;

      var bob = Math.sin(Date.now() / 300 + i) * 3;
      var stretch = Math.abs(Math.cos(Date.now() / 400 + i));

      ctx.save();
      ctx.translate(sx, sy + bob);
      ctx.scale(0.5 + stretch * 0.5, 1);

      // Coin body
      ctx.fillStyle = "#FFD700";
      ctx.beginPath();
      ctx.arc(0, 0, 8, 0, Math.PI * 2);
      ctx.fill();
      ctx.fillStyle = "#FFA500";
      ctx.beginPath();
      ctx.arc(0, 0, 5, 0, Math.PI * 2);
      ctx.fill();
      ctx.fillStyle = "#FFD700";
      ctx.font = "bold 10px Arial";
      ctx.textAlign = "center";
      ctx.textBaseline = "middle";
      ctx.fillText("$", 0, 1);

      ctx.restore();
    }
  }

  function drawFlag() {
    var sx = flag.x - cam.x;
    var sy = flag.y - cam.y;
    if (sx < -60 || sx > W + 60) return;

    // Pole
    ctx.fillStyle = "#888";
    ctx.fillRect(sx + 18, sy - 100, 4, 110);

    // Ball on top
    ctx.fillStyle = "#FFD700";
    ctx.beginPath();
    ctx.arc(sx + 20, sy - 102, 6, 0, Math.PI * 2);
    ctx.fill();

    // Flag cloth
    var wave = Math.sin(Date.now() / 200) * 3;
    ctx.fillStyle = "#E53935";
    ctx.beginPath();
    ctx.moveTo(sx + 22, sy - 95);
    ctx.lineTo(sx + 55 + wave, sy - 85);
    ctx.lineTo(sx + 22, sy - 65);
    ctx.closePath();
    ctx.fill();

    // Star on flag
    ctx.fillStyle = "#FFD700";
    ctx.font = "12px Arial";
    ctx.textAlign = "center";
    ctx.fillText("★", sx + 36 + wave * 0.5, sy - 78);

    // "GOAL" text
    ctx.fillStyle = "#FFF";
    ctx.font = "bold 10px Arial";
    ctx.textAlign = "center";
    ctx.fillText("GOAL", sx + 20, sy + 15);
  }

  function drawEnemies() {
    for (var i = 0; i < enemies.length; i++) {
      var e = enemies[i];
      if (e.dead) continue;
      var sx = e.x - cam.x;
      var sy = e.y - cam.y;
      if (sx < -40 || sx > W + 40) continue;

      // Body
      ctx.fillStyle = "#D32F2F";
      ctx.beginPath();
      ctx.arc(sx + e.w / 2, sy + e.h / 2, e.w / 2, 0, Math.PI * 2);
      ctx.fill();

      // Eyes
      ctx.fillStyle = "#FFF";
      ctx.beginPath();
      ctx.arc(sx + e.w / 2 - 5, sy + e.h / 2 - 3, 4, 0, Math.PI * 2);
      ctx.arc(sx + e.w / 2 + 5, sy + e.h / 2 - 3, 4, 0, Math.PI * 2);
      ctx.fill();

      ctx.fillStyle = "#000";
      ctx.beginPath();
      ctx.arc(sx + e.w / 2 - 4, sy + e.h / 2 - 3, 2, 0, Math.PI * 2);
      ctx.arc(sx + e.w / 2 + 6, sy + e.h / 2 - 3, 2, 0, Math.PI * 2);
      ctx.fill();

      // Angry eyebrows
      ctx.strokeStyle = "#000";
      ctx.lineWidth = 2;
      ctx.beginPath();
      ctx.moveTo(sx + e.w / 2 - 8, sy + e.h / 2 - 9);
      ctx.lineTo(sx + e.w / 2 - 2, sy + e.h / 2 - 7);
      ctx.stroke();
      ctx.beginPath();
      ctx.moveTo(sx + e.w / 2 + 8, sy + e.h / 2 - 9);
      ctx.lineTo(sx + e.w / 2 + 2, sy + e.h / 2 - 7);
      ctx.stroke();

      // Feet
      var walk = Math.sin(Date.now() / 150) * 3;
      ctx.fillStyle = "#B71C1C";
      ctx.fillRect(sx + 2, sy + e.h - 4, 8, 4);
      ctx.fillRect(sx + e.w - 10, sy + e.h - 4, 8, 4);
    }
  }

  function drawPlayer() {
    if (player.dead) return;
    var sx = player.x - cam.x;
    var sy = player.y - cam.y;

    ctx.save();
    ctx.translate(sx + player.w / 2, sy + player.h / 2);
    if (player.facing < 0) ctx.scale(-1, 1);
    ctx.translate(-player.w / 2, -player.h / 2);

    // Body
    ctx.fillStyle = "#E53935";
    ctx.fillRect(2, 4, 20, 16);

    // Head
    ctx.fillStyle = "#FFCC80";
    ctx.fillRect(4, 0, 16, 12);

    // Hat
    ctx.fillStyle = "#E53935";
    ctx.fillRect(0, 0, 20, 6);
    ctx.fillRect(4, -3, 18, 5);

    // Hat brim
    ctx.fillStyle = "#B71C1C";
    ctx.fillRect(0, 4, 22, 3);

    // Eyes
    ctx.fillStyle = "#000";
    ctx.fillRect(14, 5, 3, 4);

    // Mustache
    ctx.fillStyle = "#5D4037";
    ctx.fillRect(10, 9, 10, 2);

    // Overalls
    ctx.fillStyle = "#1565C0";
    ctx.fillRect(2, 18, 20, 10);

    // Belt
    ctx.fillStyle = "#FFD700";
    ctx.fillRect(8, 20, 8, 2);

    // Legs
    var legOffset = player.onGround && Math.abs(player.vx) > 0.5 ? Math.sin(Date.now() / 80) * 3 : 0;
    ctx.fillStyle = "#1565C0";
    ctx.fillRect(2, 28, 8, 4 + legOffset);
    ctx.fillRect(14, 28, 8, 4 - legOffset);

    // Shoes
    ctx.fillStyle = "#5D4037";
    ctx.fillRect(0, 30 + legOffset, 10, 3);
    ctx.fillRect(14, 30 - legOffset, 10, 3);

    ctx.restore();
  }

  function drawParticles() {
    for (var i = 0; i < particles.length; i++) {
      var p = particles[i];
      var alpha = p.life / p.maxLife;
      ctx.globalAlpha = alpha;
      ctx.fillStyle = p.color;
      ctx.fillRect(p.x - cam.x, p.y - cam.y, p.size, p.size);
    }
    ctx.globalAlpha = 1;
  }

  function drawHUD() {
    // Score panel
    ctx.fillStyle = "rgba(0,0,0,0.5)";
    ctx.fillRect(10, 10, 160, 40);
    ctx.strokeStyle = "rgba(255,255,255,0.3)";
    ctx.lineWidth = 1;
    ctx.strokeRect(10, 10, 160, 40);

    ctx.fillStyle = "#FFD700";
    ctx.font = "bold 18px 'Segoe UI', Arial, sans-serif";
    ctx.textAlign = "left";
    ctx.textBaseline = "middle";
    ctx.fillText("★ " + player.score, 22, 25);

    // Lives
    ctx.fillStyle = "rgba(0,0,0,0.5)";
    ctx.fillRect(W - 120, 10, 110, 40);
    ctx.strokeStyle = "rgba(255,255,255,0.3)";
    ctx.strokeRect(W - 120, 10, 110, 40);

    ctx.fillStyle = "#FF5252";
    ctx.font = "bold 18px 'Segoe UI', Arial, sans-serif";
    ctx.textAlign = "right";
    ctx.fillText("♥ " + player.lives, W - 20, 25);

    // Controls hint (fades out)
    var elapsed = Date.now() / 1000;
    if (elapsed < 12) {
      var alpha = elapsed < 10 ? 0.7 : 0.7 * (12 - elapsed) / 2;
      ctx.globalAlpha = alpha;
      ctx.fillStyle = "rgba(0,0,0,0.5)";
      ctx.fillRect(W / 2 - 180, H - 50, 360, 36);
      ctx.fillStyle = "#FFF";
      ctx.font = "13px 'Segoe UI', Arial, sans-serif";
      ctx.textAlign = "center";
      ctx.fillText("Arrow Keys / WASD to move  •  Space / Up / W to jump", W / 2, H - 30);
      ctx.globalAlpha = 1;
    }
  }

  function drawWinScreen() {
    if (!player.win) return;

    ctx.fillStyle = "rgba(0,0,0,0.6)";
    ctx.fillRect(0, 0, W, H);

    ctx.fillStyle = "#FFD700";
    ctx.font = "bold 48px 'Segoe UI', Arial, sans-serif";
    ctx.textAlign = "center";
    ctx.textBaseline = "middle";
    ctx.fillText("🏆 YOU WIN! 🏆", W / 2, H / 2 - 40);

    ctx.fillStyle = "#FFF";
    ctx.font = "24px 'Segoe UI', Arial, sans-serif";
    ctx.fillText("Score: " + player.score, W / 2, H / 2 + 20);

    ctx.fillStyle = "#AAA";
    ctx.font = "16px 'Segoe UI', Arial, sans-serif";
    ctx.fillText("Press R to play again", W / 2, H / 2 + 60);
  }

  function drawDeathScreen() {
    if (!player.dead || player.respawnTimer > 30) return;
    ctx.fillStyle = "rgba(255,0,0,0.3)";
    ctx.fillRect(0, 0, W, H);
  }

  // ─── Main loop ───────────────────────────────────────────
  function gameLoop() {
    // Input: restart
    if (keys["KeyR"] && player.win) {
      resetGame();
    }

    updatePlayer();
    updateEnemies();
    updateParticles();
    updateCamera();

    // Draw
    drawBackground();
    drawPlatforms();
    drawCoins();
    drawEnemies();
    drawFlag();
    drawPlayer();
    drawParticles();
    drawHUD();
    drawDeathScreen();
    drawWinScreen();

    requestAnimationFrame(gameLoop);
  }

  function resetGame() {
    player.x = 80;
    player.y = 350;
    player.vx = 0;
    player.vy = 0;
    player.onGround = false;
    player.score = 0;
    player.lives = 3;
    player.dead = false;
    player.win = false;
    player.respawnTimer = 0;
    player.facing = 1;
    cam.x = 0;
    cam.y = 0;
    particles = [];
    buildLevel();
  }

  // ─── Start ───────────────────────────────────────────────
  buildLevel();
  requestAnimationFrame(gameLoop);

})();
</script>
</body>
</html>

rep 2 MANUAL fail/5

pi 57.9s · verify 4.2s · 60602 tok · responded: qwen3.6-35b-a3b · transcript

smoke 3/3 · note: broken movement, can't even finish

screenshot

verify output
CHECK PASS: canvas #game exists and is sized
CHECK PASS: no uncaught JS errors on load
CHECK PASS: keyboard input does not throw
score: 3/3 (100%)
produced code
game.js
// ============================================================
//  SUPER PIXEL ADVENTURE — A Mario-style Platformer
//  Vanilla JS, Canvas, Zero Dependencies
// ============================================================

(function () {
  "use strict";

  // ---- Constants ----
  const CANVAS_W = 800;
  const CANVAS_H = 450;
  const TILE = 32;
  const GRAVITY = 0.55;
  const FRICTION = 0.82;
  const MAX_SPEED = 5.2;
  const JUMP_FORCE = -11.5;
  const ACCEL = 0.65;
  const LEVEL_WIDTH = 220; // tiles wide
  const LEVEL_HEIGHT = 14; // tiles high

  // ---- Canvas Setup ----
  const canvas = document.getElementById("game");
  canvas.width = CANVAS_W;
  canvas.height = CANVAS_H;
  const ctx = canvas.getContext("2d");

  // ---- Input ----
  const keys = {};
  window.addEventListener("keydown", (e) => {
    keys[e.code] = true;
    // Prevent scrolling for game keys
    if (
      ["ArrowUp", "ArrowDown", "ArrowLeft", "ArrowRight", "Space"].includes(
        e.code
      )
    ) {
      e.preventDefault();
    }
  });
  window.addEventListener("keyup", (e) => {
    keys[e.code] = false;
  });

  // ---- Game State ----
  let score = 0;
  let coinsCollected = 0;
  let totalCoins = 0;
  let gameState = "playing"; // "playing" | "won" | "dead"
  let deathTimer = 0;
  let winTimer = 0;
  let particles = [];
  let frameCount = 0;

  // ---- Camera ----
  const camera = { x: 0, y: 0 };

  // ---- Level Data ----
  // 0 = air, 1 = ground, 2 = platform, 3 = coin, 4 = flag pole, 5 = question block
  const levelMap = [];

  function buildLevel() {
    for (let y = 0; y < LEVEL_HEIGHT; y++) {
      levelMap[y] = [];
      for (let x = 0; x < LEVEL_WIDTH; x++) {
        levelMap[y][x] = 0;
      }
    }

    // Ground segments with gaps
    const groundSegments = [
      [0, 40],
      [45, 70],
      [75, 110],
      [115, 160],
      [165, LEVEL_WIDTH],
    ];
    for (const [start, end] of groundSegments) {
      for (let x = start; x < end; x++) {
        levelMap[LEVEL_HEIGHT - 1][x] = 1;
        levelMap[LEVEL_HEIGHT - 2][x] = 1;
      }
    }

    // ---- Platforms ----
    const platforms = [
      // Low floating platforms early
      { x: 8, y: 10, w: 5 },
      { x: 16, y: 8, w: 4 },
      { x: 24, y: 10, w: 3 },
      // Mid section
      { x: 35, y: 9, w: 4 },
      { x: 48, y: 7, w: 5 },
      { x: 55, y: 10, w: 3 },
      // Higher section
      { x: 65, y: 8, w: 4 },
      { x: 72, y: 6, w: 3 },
      { x: 80, y: 9, w: 5 },
      // Tricky jumps
      { x: 95, y: 10, w: 3 },
      { x: 100, y: 8, w: 4 },
      { x: 107, y: 6, w: 3 },
      // Long section
      { x: 120, y: 9, w: 5 },
      { x: 130, y: 7, w: 4 },
      { x: 138, y: 10, w: 3 },
      { x: 145, y: 8, w: 5 },
      // Final stretch
      { x: 155, y: 10, w: 4 },
      { x: 162, y: 7, w: 3 },
      { x: 170, y: 5, w: 4 },
      { x: 178, y: 8, w: 5 },
      { x: 188, y: 10, w: 3 },
    ];

    for (const p of platforms) {
      for (let px = 0; px < p.w; px++) {
        if (p.x + px < LEVEL_WIDTH) {
          levelMap[p.y][p.x + px] = 2;
        }
      }
    }

    // ---- Question blocks (bonus coins) ----
    const questionBlocks = [
      { x: 12, y: 6 },
      { x: 37, y: 5 },
      { x: 50, y: 4 },
      { x: 82, y: 6 },
      { x: 122, y: 6 },
      { x: 147, y: 5 },
      { x: 172, y: 2 },
      { x: 180, y: 5 },
    ];
    for (const qb of questionBlocks) {
      if (qb.x < LEVEL_WIDTH && qb.y < LEVEL_HEIGHT) {
        levelMap[qb.y][qb.x] = 5;
      }
    }

    // ---- Coins ----
    const coinPositions = [
      // Early coins
      { x: 9, y: 8 },
      { x: 10, y: 8 },
      { x: 17, y: 6 },
      { x: 18, y: 6 },
      { x: 25, y: 8 },
      // Mid coins
      { x: 36, y: 7 },
      { x: 37, y: 7 },
      { x: 49, y: 5 },
      { x: 50, y: 5 },
      { x: 51, y: 5 },
      { x: 66, y: 6 },
      { x: 67, y: 6 },
      { x: 73, y: 4 },
      // Higher coins
      { x: 81, y: 7 },
      { x: 82, y: 7 },
      { x: 96, y: 8 },
      { x: 101, y: 6 },
      { x: 102, y: 6 },
      { x: 108, y: 4 },
      // Late coins
      { x: 121, y: 7 },
      { x: 122, y: 7 },
      { x: 131, y: 5 },
      { x: 132, y: 5 },
      { x: 139, y: 8 },
      { x: 146, y: 6 },
      { x: 147, y: 6 },
      // Final coins
      { x: 156, y: 8 },
      { x: 163, y: 5 },
      { x: 171, y: 3 },
      { x: 172, y: 3 },
      { x: 179, y: 6 },
      { x: 180, y: 6 },
      { x: 189, y: 8 },
    ];

    for (const c of coinPositions) {
      if (c.x < LEVEL_WIDTH && c.y < LEVEL_HEIGHT) {
        levelMap[c.y][c.x] = 3;
      }
    }

    // ---- Flag (goal) ----
    const flagX = 200;
    if (flagX < LEVEL_WIDTH) {
      levelMap[LEVEL_HEIGHT - 3][flagX] = 4; // flag pole base
      levelMap[LEVEL_HEIGHT - 4][flagX] = 4;
      levelMap[LEVEL_HEIGHT - 5][flagX] = 4;
      levelMap[LEVEL_HEIGHT - 6][flagX] = 4;
      levelMap[LEVEL_HEIGHT - 7][flagX] = 4;
      levelMap[LEVEL_HEIGHT - 8][flagX] = 4;
      levelMap[LEVEL_HEIGHT - 9][flagX] = 4;
      levelMap[LEVEL_HEIGHT - 10][flagX] = 4;
    }

    // Count total coins
    totalCoins = 0;
    for (let y = 0; y < LEVEL_HEIGHT; y++) {
      for (let x = 0; x < LEVEL_WIDTH; x++) {
        if (levelMap[y][x] === 3 || levelMap[y][x] === 5) {
          totalCoins++;
        }
      }
    }
  }

  buildLevel();

  // ---- Player ----
  const player = {
    x: 3 * TILE,
    y: (LEVEL_HEIGHT - 4) * TILE,
    w: 22,
    h: 28,
    vx: 0,
    vy: 0,
    onGround: false,
    facing: 1, // 1 = right, -1 = left
    animFrame: 0,
    animTimer: 0,
    jumpHeld: false,
    coyoteTime: 0,
    jumpBuffer: 0,
  };

  function resetPlayer() {
    player.x = 3 * TILE;
    player.y = (LEVEL_HEIGHT - 4) * TILE;
    player.vx = 0;
    player.vy = 0;
    player.onGround = false;
    player.facing = 1;
    gameState = "playing";
    deathTimer = 0;
    winTimer = 0;
    camera.x = 0;
    camera.y = 0;
  }

  // ---- Tile Collision ----
  function getTile(tx, ty) {
    if (tx < 0 || tx >= LEVEL_WIDTH || ty < 0 || ty >= LEVEL_HEIGHT) return 0;
    return levelMap[ty][tx];
  }

  function isSolid(type) {
    return type === 1 || type === 2 || type === 5;
  }

  // ---- Particles ----
  function spawnParticles(x, y, color, count) {
    for (let i = 0; i < count; i++) {
      particles.push({
        x: x,
        y: y,
        vx: (Math.random() - 0.5) * 5,
        vy: (Math.random() - 0.5) * 5 - 2,
        life: 30 + Math.random() * 20,
        maxLife: 50,
        color: color,
        size: 2 + Math.random() * 3,
      });
    }
  }

  // ---- Update ----
  function update() {
    frameCount++;

    if (gameState === "dead") {
      deathTimer++;
      if (deathTimer > 90) {
        resetPlayer();
      }
      // Update particles
      updateParticles();
      return;
    }

    if (gameState === "won") {
      winTimer++;
      updateParticles();
      return;
    }

    // ---- Player Input ----
    let moveX = 0;
    if (keys["ArrowLeft"] || keys["KeyA"]) moveX -= 1;
    if (keys["ArrowRight"] || keys["KeyD"]) moveX += 1;

    const jumpKey =
      keys["Space"] || keys["ArrowUp"] || keys["KeyW"];

    // Horizontal movement with acceleration
    if (moveX !== 0) {
      player.vx += moveX * ACCEL;
      if (player.vx > MAX_SPEED) player.vx = MAX_SPEED;
      if (player.vx < -MAX_SPEED) player.vx = -MAX_SPEED;
      player.facing = moveX;
    } else {
      player.vx *= FRICTION;
      if (Math.abs(player.vx) < 0.1) player.vx = 0;
    }

    // Coyote time & jump buffer
    if (player.onGround) {
      player.coyoteTime = 8;
    } else {
      player.coyoteTime--;
    }

    if (jumpKey) {
      player.jumpBuffer = 8;
    } else {
      player.jumpBuffer--;
    }

    // Jump (with coyote time and jump buffer)
    if (player.jumpBuffer > 0 && player.coyoteTime > 0) {
      player.vy = JUMP_FORCE;
      player.onGround = false;
      player.coyoteTime = 0;
      player.jumpBuffer = 0;
      spawnParticles(
        player.x + player.w / 2,
        player.y + player.h,
        "#ddd",
        5
      );
    }

    // Variable jump height — release early to jump shorter
    if (!jumpKey && player.vy < -3) {
      player.vy *= 0.7;
    }

    // Gravity
    player.vy += GRAVITY;
    if (player.vy > 12) player.vy = 12;

    // ---- Horizontal Collision ----
    player.x += player.vx;

    const left = Math.floor(player.x / TILE);
    const right = Math.floor((player.x + player.w) / TILE);
    const top = Math.floor(player.y / TILE);
    const bottom = Math.floor((player.y + player.h) / TILE);

    for (let ty = top; ty <= bottom; ty++) {
      if (player.vx > 0) {
        const tx = right;
        if (isSolid(getTile(tx, ty))) {
          player.x = tx * TILE - player.w;
          player.vx = 0;
          break;
        }
      } else if (player.vx < 0) {
        const tx = left;
        if (isSolid(getTile(tx, ty))) {
          player.x = (tx + 1) * TILE;
          player.vx = 0;
          break;
        }
      }
    }

    // ---- Vertical Collision ----
    player.y += player.vy;
    player.onGround = false;

    const newLeft = Math.floor(player.x / TILE);
    const newRight = Math.floor((player.x + player.w) / TILE);
    const newTop = Math.floor(player.y / TILE);
    const newBottom = Math.floor((player.y + player.h) / TILE);

    for (let tx = newLeft; tx <= newRight; tx++) {
      if (player.vy > 0) {
        const ty = newBottom;
        if (isSolid(getTile(tx, ty))) {
          player.y = ty * TILE - player.h;
          player.vy = 0;
          player.onGround = true;
          break;
        }
      } else if (player.vy < 0) {
        const ty = newTop;
        const tile = getTile(tx, ty);
        if (isSolid(tile)) {
          player.y = (ty + 1) * TILE;
          player.vy = 0;
          // Hit question block from below
          if (tile === 5) {
            levelMap[ty][tx] = 2; // turn into solid block
            score += 50;
            spawnParticles(
              tx * TILE + TILE / 2,
              ty * TILE,
              "#FFD700",
              8
            );
          }
          break;
        }
      }
    }

    // ---- Collectibles ----
    const playerCX = Math.floor((player.x + player.w / 2) / TILE);
    const playerCY = Math.floor((player.y + player.h / 2) / TILE);

    // Check surrounding tiles for coins
    for (let dy = -1; dy <= 1; dy++) {
      for (let dx = -1; dx <= 1; dx++) {
        const tx = playerCX + dx;
        const ty = playerCY + dy;
        if (getTile(tx, ty) === 3) {
          levelMap[ty][tx] = 0;
          score += 100;
          coinsCollected++;
          spawnParticles(
            tx * TILE + TILE / 2,
            ty * TILE + TILE / 2,
            "#FFD700",
            10
          );
        }
      }
    }

    // ---- Flag (goal) ----
    const flagTileX = Math.floor((player.x + player.w / 2) / TILE);
    const flagTileY = Math.floor((player.y + player.h / 2) / TILE);
    if (getTile(flagTileX, flagTileY) === 4) {
      gameState = "won";
      winTimer = 0;
      score += 1000;
      spawnParticles(
        flagTileX * TILE + TILE / 2,
        flagTileY * TILE,
        "#FF6B6B",
        20
      );
      spawnParticles(
        flagTileX * TILE + TILE / 2,
        flagTileY * TILE,
        "#4ECDC4",
        20
      );
      spawnParticles(
        flagTileX * TILE + TILE / 2,
        flagTileY * TILE,
        "#FFD700",
        20
      );
    }

    // ---- Fall off the map ----
    if (player.y > LEVEL_HEIGHT * TILE + 100) {
      gameState = "dead";
      deathTimer = 0;
      spawnParticles(
        player.x + player.w / 2,
        LEVEL_HEIGHT * TILE,
        "#FF4444",
        15
      );
    }

    // ---- Clamp player to level bounds ----
    if (player.x < 0) {
      player.x = 0;
      player.vx = 0;
    }

    // ---- Animation ----
    if (Math.abs(player.vx) > 0.5 && player.onGround) {
      player.animTimer++;
      if (player.animTimer > 6) {
        player.animTimer = 0;
        player.animFrame = (player.animFrame + 1) % 4;
      }
    } else if (player.onGround) {
      player.animFrame = 0;
      player.animTimer = 0;
    }

    // ---- Camera ----
    const targetX = player.x - CANVAS_W / 2 + player.w / 2;
    const targetY = player.y - CANVAS_H / 2 + player.h / 2;
    camera.x += (targetX - camera.x) * 0.1;
    camera.y += (targetY - camera.y) * 0.08;

    // Clamp camera
    camera.x = Math.max(0, Math.min(camera.x, LEVEL_WIDTH * TILE - CANVAS_W));
    camera.y = Math.max(
      0,
      Math.min(camera.y, LEVEL_HEIGHT * TILE - CANVAS_H)
    );

    updateParticles();
  }

  function updateParticles() {
    for (let i = particles.length - 1; i >= 0; i--) {
      const p = particles[i];
      p.x += p.vx;
      p.y += p.vy;
      p.vy += 0.15;
      p.life--;
      if (p.life <= 0) {
        particles.splice(i, 1);
      }
    }
  }

  // ---- Drawing ----
  function drawBackground() {
    // Sky gradient
    const grad = ctx.createLinearGradient(0, 0, 0, CANVAS_H);
    grad.addColorStop(0, "#5C94FC");
    grad.addColorStop(0.6, "#87CEEB");
    grad.addColorStop(1, "#B0E0FF");
    ctx.fillStyle = grad;
    ctx.fillRect(0, 0, CANVAS_W, CANVAS_H);

    // Clouds (parallax)
    ctx.fillStyle = "rgba(255, 255, 255, 0.8)";
    drawCloud(
      (100 - camera.x * 0.1) % (CANVAS_W + 200) - 100,
      50 + Math.sin(frameCount * 0.005) * 5,
      1.0
    );
    drawCloud(
      (400 - camera.x * 0.15) % (CANVAS_W + 200) - 100,
      80 + Math.sin(frameCount * 0.007 + 1) * 5,
      0.8
    );
    drawCloud(
      (700 - camera.x * 0.08) % (CANVAS_W + 200) - 100,
      35 + Math.sin(frameCount * 0.006 + 2) * 5,
      1.2
    );
    drawCloud(
      (1000 - camera.x * 0.12) % (CANVAS_W + 200) - 100,
      95 + Math.sin(frameCount * 0.004 + 3) * 5,
      0.9
    );

    // Hills (parallax layer 2)
    ctx.fillStyle = "#4CAF50";
    drawHills(0, CANVAS_H - 60, 0.2);
    ctx.fillStyle = "#388E3C";
    drawHills(200, CANVAS_H - 40, 0.3);
  }

  function drawCloud(x, y, scale) {
    ctx.save();
    ctx.translate(x, y);
    ctx.scale(scale, scale);
    ctx.beginPath();
    ctx.arc(0, 0, 20, 0, Math.PI * 2);
    ctx.arc(25, -5, 25, 0, Math.PI * 2);
    ctx.arc(50, 0, 20, 0, Math.PI * 2);
    ctx.arc(25, 5, 22, 0, Math.PI * 2);
    ctx.fill();
    ctx.restore();
  }

  function drawHills(offsetY, baseY, parallax) {
    for (let i = -1; i < 6; i++) {
      const hx = i * 300 - (camera.x * parallax) % 300;
      ctx.beginPath();
      ctx.moveTo(hx - 100, baseY + 80);
      ctx.quadraticCurveTo(hx, baseY - 60, hx + 100, baseY + 80);
      ctx.fill();
    }
  }

  function drawTile(tx, ty, type) {
    const sx = tx * TILE - camera.x;
    const sy = ty * TILE - camera.y;

    // Skip if off screen
    if (sx + TILE < 0 || sx > CANVAS_W || sy + TILE < 0 || sy > CANVAS_H)
      return;

    switch (type) {
      case 1: // Ground
        // Top layer with grass
        const above = getTile(tx, ty - 1);
        if (above === 0 || above === 3) {
          // Grass top
          ctx.fillStyle = "#4CAF50";
          ctx.fillRect(sx, sy, TILE, 6);
          ctx.fillStyle = "#8B4513";
          ctx.fillRect(sx, sy + 6, TILE, TILE - 6);
          // Dirt texture
          ctx.fillStyle = "#7B3B0F";
          ctx.fillRect(sx + 4, sy + 12, 6, 4);
          ctx.fillRect(sx + 18, sy + 20, 8, 4);
          ctx.fillRect(sx + 8, sy + 24, 5, 3);
        } else {
          ctx.fillStyle = "#8B4513";
          ctx.fillRect(sx, sy, TILE, TILE);
          ctx.fillStyle = "#7B3B0F";
          ctx.fillRect(sx + 4, sy + 8, 6, 4);
          ctx.fillRect(sx + 20, sy + 18, 8, 4);
        }
        break;

      case 2: // Platform / solid block
        ctx.fillStyle = "#C84C09";
        ctx.fillRect(sx, sy, TILE, TILE);
        ctx.fillStyle = "#E8731A";
        ctx.fillRect(sx + 2, sy + 2, TILE - 4, TILE - 4);
        // Brick lines
        ctx.strokeStyle = "#8B3000";
        ctx.lineWidth = 1;
        ctx.strokeRect(sx + 1, sy + 1, TILE - 2, TILE - 2);
        ctx.beginPath();
        ctx.moveTo(sx + TILE / 2, sy);
        ctx.lineTo(sx + TILE / 2, sy + TILE);
        ctx.stroke();
        ctx.beginPath();
        ctx.moveTo(sx, sy + TILE / 2);
        ctx.lineTo(sx + TILE, sy + TILE / 2);
        ctx.stroke();
        break;

      case 3: // Coin
        const bobY = Math.sin(frameCount * 0.08 + tx * 1.5) * 3;
        const coinX = sx + TILE / 2;
        const coinY = sy + TILE / 2 + bobY;
        // Outer glow
        ctx.fillStyle = "rgba(255, 215, 0, 0.3)";
        ctx.beginPath();
        ctx.arc(coinX, coinY, 14, 0, Math.PI * 2);
        ctx.fill();
        // Coin body
        ctx.fillStyle = "#FFD700";
        ctx.beginPath();
        ctx.arc(coinX, coinY, 10, 0, Math.PI * 2);
        ctx.fill();
        ctx.fillStyle = "#FFC107";
        ctx.beginPath();
        ctx.arc(coinX, coinY, 7, 0, Math.PI * 2);
        ctx.fill();
        // Shine
        ctx.fillStyle = "rgba(255, 255, 255, 0.5)";
        ctx.beginPath();
        ctx.arc(coinX - 3, coinY - 3, 3, 0, Math.PI * 2);
        ctx.fill();
        break;

      case 4: // Flag pole
        // Pole
        ctx.fillStyle = "#888";
        ctx.fillRect(sx + 14, sy, 4, TILE);
        if (ty === LEVEL_HEIGHT - 3) {
          // Flag at top
          ctx.fillStyle = "#FF4444";
          ctx.beginPath();
          ctx.moveTo(sx + 18, sy);
          ctx.lineTo(sx + 38, sy + 8);
          ctx.lineTo(sx + 18, sy + 16);
          ctx.fill();
          // Ball on top
          ctx.fillStyle = "#FFD700";
          ctx.beginPath();
          ctx.arc(sx + 16, sy - 2, 4, 0, Math.PI * 2);
          ctx.fill();
        }
        break;

      case 5: // Question block
        const bounce = Math.sin(frameCount * 0.06 + tx * 2) * 1;
        ctx.fillStyle = "#FFB800";
        ctx.fillRect(sx, sy + bounce, TILE, TILE);
        ctx.fillStyle = "#FFD700";
        ctx.fillRect(sx + 2, sy + 2 + bounce, TILE - 4, TILE - 4);
        // Question mark
        ctx.fillStyle = "#8B6914";
        ctx.font = "bold 18px Arial";
        ctx.textAlign = "center";
        ctx.fillText("?", sx + TILE / 2, sy + TILE / 2 + 6 + bounce);
        // Corner dots
        ctx.fillStyle = "#C89600";
        ctx.fillRect(sx + 2, sy + 2 + bounce, 3, 3);
        ctx.fillRect(sx + TILE - 5, sy + 2 + bounce, 3, 3);
        ctx.fillRect(sx + 2, sy + TILE - 5 + bounce, 3, 3);
        ctx.fillRect(sx + TILE - 5, sy + TILE - 5 + bounce, 3, 3);
        break;
    }
  }

  function drawPlayer() {
    const sx = player.x - camera.x;
    const sy = player.y - camera.y;

    if (gameState === "dead") {
      // Draw player as fading/spinning
      const alpha = 1 - deathTimer / 90;
      ctx.globalAlpha = alpha;
      const rot = (deathTimer / 90) * Math.PI * 2;
      ctx.save();
      ctx.translate(sx + player.w / 2, sy + player.h / 2);
      ctx.rotate(rot);
      ctx.translate(-(sx + player.w / 2), -(sy + player.h / 2));
    }

    // Shadow
    ctx.fillStyle = "rgba(0,0,0,0.2)";
    ctx.beginPath();
    ctx.ellipse(
      sx + player.w / 2,
      sy + player.h + 2,
      player.w / 2 + 2,
      4,
      0,
      0,
      Math.PI * 2
    );
    ctx.fill();

    // Body (overalls)
    ctx.fillStyle = "#E52521";
    ctx.fillRect(sx + 3, sy + 4, 16, 12);

    // Head
    ctx.fillStyle = "#FFB366";
    ctx.fillRect(sx + 5, sy, 12, 10);

    // Hat
    ctx.fillStyle = "#E52521";
    ctx.fillRect(sx + 3, sy - 2, 16, 5);
    ctx.fillRect(sx + (player.facing > 0 ? 12 : 1), sy, 10, 3);

    // Eyes
    ctx.fillStyle = "#000";
    const eyeX = player.facing > 0 ? sx + 13 : sx + 7;
    ctx.fillRect(eyeX, sy + 3, 3, 3);

    // Mustache
    ctx.fillStyle = "#4A2800";
    ctx.fillRect(sx + 6, sy + 7, 10, 2);

    // Legs
    const legOffset = player.onGround && Math.abs(player.vx) > 0.5
      ? Math.sin(player.animFrame * Math.PI / 2) * 3
      : 0;
    ctx.fillStyle = "#2835A7";
    ctx.fillRect(sx + 3, sy + 16, 6, 8 + legOffset);
    ctx.fillRect(sx + 13, sy + 16, 6, 8 - legOffset);

    // Shoes
    ctx.fillStyle = "#6B3300";
    ctx.fillRect(sx + 1, sy + 23 + Math.max(0, legOffset), 8, 4);
    ctx.fillRect(sx + 13, sy + 23 + Math.max(0, -legOffset), 8, 4);

    // Arms
    ctx.fillStyle = "#FFB366";
    if (!player.onGround) {
      // Arms up when jumping
      ctx.fillRect(sx - 2, sy + 2, 5, 8);
      ctx.fillRect(sx + 17, sy + 2, 5, 8);
    } else {
      ctx.fillRect(sx - 2, sy + 8, 5, 8);
      ctx.fillRect(sx + 17, sy + 8, 5, 8);
    }

    if (gameState === "dead") {
      ctx.restore();
      ctx.globalAlpha = 1;
    }
  }

  function drawParticles() {
    for (const p of particles) {
      const alpha = p.life / p.maxLife;
      ctx.globalAlpha = alpha;
      ctx.fillStyle = p.color;
      ctx.fillRect(
        p.x - camera.x - p.size / 2,
        p.y - camera.y - p.size / 2,
        p.size,
        p.size
      );
    }
    ctx.globalAlpha = 1;
  }

  function drawUI() {
    // Score panel
    ctx.fillStyle = "rgba(0, 0, 0, 0.5)";
    ctx.fillRect(10, 10, 220, 44);
    ctx.strokeStyle = "rgba(255, 255, 255, 0.3)";
    ctx.lineWidth = 2;
    ctx.strokeRect(10, 10, 220, 44);

    ctx.fillStyle = "#FFD700";
    ctx.font = "bold 16px 'Courier New', monospace";
    ctx.textAlign = "left";
    ctx.fillText("🪙 COINS: " + coinsCollected, 20, 30);

    ctx.fillStyle = "#FFF";
    ctx.fillText("SCORE: " + score, 20, 50);

    // Progress indicator
    const progress = Math.min(1, player.x / ((LEVEL_WIDTH - 5) * TILE));
    ctx.fillStyle = "rgba(0,0,0,0.4)";
    ctx.fillRect(CANVAS_W - 160, 14, 150, 16);
    ctx.fillStyle = "#4CAF50";
    ctx.fillRect(CANVAS_W - 158, 16, 146 * progress, 12);
    ctx.fillStyle = "#FFD700";
    ctx.font = "11px 'Courier New', monospace";
    ctx.textAlign = "right";
    ctx.fillText("GOAL", CANVAS_W - 12, 26);
  }

  function drawWinScreen() {
    ctx.fillStyle = "rgba(0, 0, 0, 0.5)";
    ctx.fillRect(0, 0, CANVAS_W, CANVAS_H);

    ctx.textAlign = "center";

    // Title
    ctx.fillStyle = "#FFD700";
    ctx.font = "bold 48px 'Courier New', monospace";
    ctx.fillText("🎉 YOU WIN! 🎉", CANVAS_W / 2, CANVAS_H / 2 - 40);

    ctx.fillStyle = "#FFF";
    ctx.font = "24px 'Courier New', monospace";
    ctx.fillText("Score: " + score, CANVAS_W / 2, CANVAS_H / 2 + 10);
    ctx.fillText(
      "Coins: " + coinsCollected + " / " + totalCoins,
      CANVAS_W / 2,
      CANVAS_H / 2 + 45
    );

    if (winTimer > 60) {
      ctx.fillStyle = "#4ECDC4";
      ctx.font = "18px 'Courier New', monospace";
      ctx.fillText(
        "Press R to play again",
        CANVAS_W / 2,
        CANVAS_H / 2 + 85
      );
    }
  }

  function drawDeathScreen() {
    ctx.fillStyle = "rgba(0, 0, 0, 0.4)";
    ctx.fillRect(0, 0, CANVAS_W, CANVAS_H);

    ctx.textAlign = "center";
    ctx.fillStyle = "#FF6B6B";
    ctx.font = "bold 36px 'Courier New', monospace";
    ctx.fillText("OOPS!", CANVAS_W / 2, CANVAS_H / 2);
  }

  function draw() {
    ctx.clearRect(0, 0, CANVAS_W, CANVAS_H);

    drawBackground();

    // Draw tiles
    const startTX = Math.max(0, Math.floor(camera.x / TILE) - 1);
    const endTX = Math.min(
      LEVEL_WIDTH,
      Math.ceil((camera.x + CANVAS_W) / TILE) + 1
    );
    const startTY = Math.max(0, Math.floor(camera.y / TILE) - 1);
    const endTY = Math.min(
      LEVEL_HEIGHT,
      Math.ceil((camera.y + CANVAS_H) / TILE) + 1
    );

    for (let ty = startTY; ty < endTY; ty++) {
      for (let tx = startTX; tx < endTX; tx++) {
        const tile = getTile(tx, ty);
        if (tile !== 0) {
          drawTile(tx, ty, tile);
        }
      }
    }

    drawParticles();
    drawPlayer();
    drawUI();

    if (gameState === "won") {
      drawWinScreen();
    }
    if (gameState === "dead") {
      drawDeathScreen();
    }
  }

  // ---- Game Loop ----
  function gameLoop() {
    // Check restart
    if ((gameState === "won" || gameState === "dead") && keys["KeyR"]) {
      score = 0;
      coinsCollected = 0;
      buildLevel();
      resetPlayer();
    }

    update();
    draw();
    requestAnimationFrame(gameLoop);
  }

  // ---- Start ----
  gameLoop();
})();
index.html
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Super Pixel Adventure</title>
  <style>
    * {
      margin: 0;
      padding: 0;
      box-sizing: border-box;
    }

    html, body {
      width: 100%;
      height: 100%;
      background: #1a1a2e;
      display: flex;
      flex-direction: column;
      align-items: center;
      justify-content: center;
      font-family: 'Courier New', monospace;
      overflow: hidden;
    }

    h1 {
      color: #FFD700;
      font-size: 24px;
      margin-bottom: 12px;
      text-shadow: 2px 2px 0 #8B6914;
      letter-spacing: 2px;
    }

    #game {
      border: 3px solid #FFD700;
      border-radius: 4px;
      box-shadow: 0 0 30px rgba(255, 215, 0, 0.3);
      display: block;
    }

    .controls {
      color: #aaa;
      font-size: 13px;
      margin-top: 12px;
      text-align: center;
      line-height: 1.6;
    }

    .controls kbd {
      background: #333;
      color: #fff;
      padding: 2px 7px;
      border-radius: 3px;
      border: 1px solid #555;
      font-size: 12px;
    }
  </style>
</head>
<body>
  <h1>🍄 SUPER PIXEL ADVENTURE 🍄</h1>
  <canvas id="game"></canvas>
  <div class="controls">
    <kbd>←</kbd> <kbd>→</kbd> or <kbd>A</kbd> <kbd>D</kbd> to move &nbsp;|&nbsp;
    <kbd>Space</kbd> / <kbd>↑</kbd> / <kbd>W</kbd> to jump &nbsp;|&nbsp;
    <kbd>R</kbd> to restart
  </div>
  <script src="game.js"></script>
</body>
</html>

rep 3 MANUAL fail/5

pi 57.2s · verify 4.1s · 39809 tok · responded: qwen3.6-35b-a3b · transcript

smoke 3/3

screenshot

verify output
CHECK PASS: canvas #game exists and is sized
CHECK PASS: no uncaught JS errors on load
CHECK PASS: keyboard input does not throw
score: 3/3 (100%)
produced code
game.js
/* ============================================================
   Super Platformer — a Mario-style side-scroller
   All game logic in a single file. No external dependencies.
   ============================================================ */

(function () {
  "use strict";

  // ─── Canvas & Context ──────────────────────────────────────
  const canvas = document.getElementById("game");
  const ctx = canvas.getContext("2d");
  const W = canvas.width;   // 800
  const H = canvas.height;  // 450

  const hudEl = document.getElementById("hud");
  const msgOverlay = document.getElementById("message-overlay");
  const msgText = document.getElementById("message-text");
  const msgSub = document.getElementById("message-sub");

  // ─── Constants ─────────────────────────────────────────────
  const GRAVITY = 1800;
  const MOVE_SPEED = 280;
  const ACCEL = 1200;
  const DECEL = 1000;
  const AIR_ACCEL = 800;
  const JUMP_VEL = -620;
  const JUMP_HOLD_TIME = 0.18;       // seconds of jump hold for variable height
  const COYOTE_TIME = 0.10;          // seconds after leaving platform you can still jump
  const JUMP_BUFFER = 0.10;          // seconds before landing you can buffer a jump
  const ENEMY_SPEED = 80;
  const ENEMY_HURT_VEL_X = 250;
  const ENEMY_HURT_VEL_Y = -400;
  const LEVEL_WIDTH = 5200;
  const TILE = 32;
  const PLAYER_W = 24;
  const PLAYER_H = 36;

  // ─── Input ─────────────────────────────────────────────────
  const keys = {};
  window.addEventListener("keydown", (e) => {
    keys[e.code] = true;
    // Prevent scrolling for game keys
    if (["Space","ArrowUp","ArrowDown","ArrowLeft","ArrowRight"].includes(e.code)) {
      e.preventDefault();
    }
  });
  window.addEventListener("keyup", (e) => {
    keys[e.code] = false;
  });

  function isLeft()  { return keys["ArrowLeft"] || keys["KeyA"]; }
  function isRight() { return keys["ArrowRight"] || keys["KeyD"]; }
  function isJump()  { return keys["Space"] || keys["ArrowUp"] || keys["KeyW"]; }

  // ─── Level Data ────────────────────────────────────────────
  // Ground segments: {x, y, w}
  // Platforms: {x, y, w, h}
  // Coins: {x, y}
  // Enemies: {x, y, w, h, minX, maxX}
  // Flag: {x, y}

  const GROUND_Y = H - 40;

  function buildLevel() {
    const platforms = [];
    const coins = [];
    const enemies = [];
    let totalCoins = 0;

    // ── Ground segments (with gaps) ──
    const groundSegments = [
      { x: 0,       w: 560 },
      { x: 620,     w: 480 },
      { x: 1160,    w: 320 },
      { x: 1540,    w: 600 },
      { x: 2240,    w: 400 },
      { x: 2740,    w: 800 },
      { x: 3640,    w: 600 },
      { x: 4340,    w: 860 },
    ];
    groundSegments.forEach((s) => {
      platforms.push({ x: s.x, y: GROUND_Y, w: s.w, h: 40 });
    });

    // ── Raised platforms ──
    const plats = [
      // Early area
      { x: 200,  y: GROUND_Y - 100, w: 96, h: 16 },
      { x: 380,  y: GROUND_Y - 170, w: 96, h: 16 },
      { x: 540,  y: GROUND_Y - 100, w: 64, h: 16 },

      // After first gap
      { x: 700,  y: GROUND_Y - 90,  w: 128, h: 16 },
      { x: 900,  y: GROUND_Y - 160, w: 96,  h: 16 },
      { x: 1050, y: GROUND_Y - 90,  w: 64,  h: 16 },

      // Mid area
      { x: 1200, y: GROUND_Y - 120, w: 96,  h: 16 },
      { x: 1360, y: GROUND_Y - 200, w: 96,  h: 16 },
      { x: 1560, y: GROUND_Y - 100, w: 128, h: 16 },
      { x: 1760, y: GROUND_Y - 170, w: 96,  h: 16 },

      // Staircase section
      { x: 1960, y: GROUND_Y - 80,  w: 64,  h: 16 },
      { x: 2040, y: GROUND_Y - 120, w: 64,  h: 16 },
      { x: 2120, y: GROUND_Y - 160, w: 64,  h: 16 },

      // After gap
      { x: 2300, y: GROUND_Y - 100, w: 96,  h: 16 },
      { x: 2500, y: GROUND_Y - 160, w: 96,  h: 16 },

      // High area
      { x: 2800, y: GROUND_Y - 90,  w: 128, h: 16 },
      { x: 3000, y: GROUND_Y - 160, w: 96,  h: 16 },
      { x: 3200, y: GROUND_Y - 230, w: 96,  h: 16 },
      { x: 3400, y: GROUND_Y - 160, w: 96,  h: 16 },

      // Final stretch
      { x: 3700, y: GROUND_Y - 100, w: 96,  h: 16 },
      { x: 3900, y: GROUND_Y - 170, w: 96,  h: 16 },
      { x: 4100, y: GROUND_Y - 100, w: 128, h: 16 },
    ];
    plats.forEach((p) => platforms.push(p));

    // ── Coins ──
    function addCoins(x, y, count, spacing) {
      for (let i = 0; i < count; i++) {
        coins.push({ x: x + i * (spacing || 36), y: y });
        totalCoins++;
      }
    }

    // Ground coins
    addCoins(120,  GROUND_Y - 50, 3, 36);
    addCoins(450,  GROUND_Y - 50, 2, 36);
    addCoins(720,  GROUND_Y - 50, 3, 36);

    // Platform coins
    coins.push({ x: 230, y: GROUND_Y - 130 });
    coins.push({ x: 410, y: GROUND_Y - 200 });
    coins.push({ x: 560, y: GROUND_Y - 130 });
    addCoins(730, GROUND_Y - 120, 3, 36);
    coins.push({ x: 930, y: GROUND_Y - 190 });
    addCoins(1230, GROUND_Y - 150, 2, 36);
    coins.push({ x: 1390, y: GROUND_Y - 230 });
    addCoins(1590, GROUND_Y - 130, 3, 36);
    coins.push({ x: 1790, y: GROUND_Y - 200 });

    // Staircase coins
    coins.push({ x: 1990, y: GROUND_Y - 110 });
    coins.push({ x: 2070, y: GROUND_Y - 150 });
    coins.push({ x: 2150, y: GROUND_Y - 190 });

    addCoins(2330, GROUND_Y - 130, 2, 36);
    coins.push({ x: 2530, y: GROUND_Y - 190 });

    addCoins(2830, GROUND_Y - 120, 3, 36);
    coins.push({ x: 3030, y: GROUND_Y - 190 });
    coins.push({ x: 3230, y: GROUND_Y - 260 });
    addCoins(3430, GROUND_Y - 190, 2, 36);

    addCoins(3730, GROUND_Y - 130, 2, 36);
    coins.push({ x: 3930, y: GROUND_Y - 200 });
    addCoins(4130, GROUND_Y - 130, 3, 36);

    // ── Enemies ──
    function addEnemy(x, y, range) {
      enemies.push({
        x, y, w: 28, h: 28,
        minX: x - (range || 80),
        maxX: x + (range || 80),
        vx: ENEMY_SPEED,
        vy: 0,
        alive: true,
        squishTimer: 0,
      });
    }

    addEnemy(300,  GROUND_Y - 28, 80);
    addEnemy(500,  GROUND_Y - 28, 60);
    addEnemy(750,  GROUND_Y - 28, 100);
    addEnemy(1100, GROUND_Y - 28, 70);
    addEnemy(1580, GROUND_Y - 28, 120);
    addEnemy(1800, GROUND_Y - 28, 80);
    addEnemy(2350, GROUND_Y - 28, 100);
    addEnemy(2600, GROUND_Y - 28, 80);
    addEnemy(2900, GROUND_Y - 28, 120);
    addEnemy(3100, GROUND_Y - 28, 80);
    addEnemy(3700, GROUND_Y - 28, 100);
    addEnemy(4000, GROUND_Y - 28, 80);
    addEnemy(4400, GROUND_Y - 28, 120);

    // ── Flag (goal) ──
    const flag = { x: 5000, y: GROUND_Y - 200 };

    return { platforms, coins, enemies, flag, totalCoins };
  }

  // ─── Game State ────────────────────────────────────────────
  let level, player, camera, gameState; // gameState: "playing" | "won" | "dead" | "intro"

  function initGame() {
    level = buildLevel();
    player = {
      x: 60,
      y: GROUND_Y - PLAYER_H,
      w: PLAYER_W,
      h: PLAYER_H,
      vx: 0,
      vy: 0,
      onGround: false,
      facing: 1, // 1 = right, -1 = left
      coyoteTimer: 0,
      jumpBufferTimer: 0,
      jumpHoldTimer: 0,
      alive: true,
      lives: 3,
      invincibleTimer: 0,
      deathTimer: 0,
      animFrame: 0,
      animTimer: 0,
    };
    camera = { x: 0 };
    gameState = "playing";
    msgOverlay.style.display = "none";
    hudEl.textContent = `COINS: 0 / ${level.totalCoins}    LIVES: ${player.lives}`;
  }

  // ─── Helpers ───────────────────────────────────────────────
  function aabb(a, b) {
    return a.x < b.x + b.w && a.x + a.w > b.x &&
           a.y < b.y + b.h && a.y + a.h > b.y;
  }

  function lerp(a, b, t) { return a + (b - a) * t; }

  // ─── Update ────────────────────────────────────────────────
  let lastTime = 0;
  let coinCount = 0;

  function update(dt) {
    if (gameState !== "playing") return;

    // ── Player death animation ──
    if (!player.alive) {
      player.deathTimer -= dt;
      player.vy += GRAVITY * dt;
      player.y += player.vy * dt;
      if (player.deathTimer <= 0) {
        player.lives--;
        if (player.lives <= 0) {
          gameState = "dead";
          showMessage("GAME OVER", "Press ENTER to restart");
          return;
        }
        // Respawn
        respawnPlayer();
      }
      hudEl.textContent = `COINS: ${coinCount} / ${level.totalCoins}    LIVES: ${player.lives}`;
      return;
    }

    // ── Invincibility timer ──
    if (player.invincibleTimer > 0) {
      player.invincibleTimer -= dt;
    }

    // ── Horizontal movement ──
    const wantLeft = isLeft();
    const wantRight = isRight();
    const onGround = player.onGround;

    if (wantLeft) {
      player.vx -= (onGround ? ACCEL : AIR_ACCEL) * dt;
      player.facing = -1;
    } else if (wantRight) {
      player.vx += (onGround ? ACCEL : AIR_ACCEL) * dt;
      player.facing = 1;
    } else {
      // Decelerate
      const decel = onGround ? DECEL : ACCEL * 0.5;
      if (player.vx > 0) {
        player.vx = Math.max(0, player.vx - decel * dt);
      } else if (player.vx < 0) {
        player.vx = Math.min(0, player.vx + decel * dt);
      }
    }
    player.vx = Math.max(-MOVE_SPEED, Math.min(MOVE_SPEED, player.vx));

    // ── Jump input ──
    if (isJump()) {
      player.jumpBufferTimer = JUMP_BUFFER;
    } else {
      player.jumpBufferTimer = Math.max(0, player.jumpBufferTimer - dt);
    }

    // ── Apply jump ──
    if (player.jumpBufferTimer > 0 && (player.onGround || player.coyoteTimer > 0)) {
      player.vy = JUMP_VEL;
      player.onGround = false;
      player.coyoteTimer = 0;
      player.jumpBufferTimer = 0;
      player.jumpHoldTimer = JUMP_HOLD_TIME;
    }

    // Variable jump height
    if (isJump() && player.jumpHoldTimer > 0) {
      player.vy += JUMP_VEL * 0.5 * dt / JUMP_HOLD_TIME;
      player.jumpHoldTimer -= dt;
    } else {
      player.jumpHoldTimer = 0;
    }

    // ── Coyote time ──
    if (player.onGround) {
      player.coyoteTimer = COYOTE_TIME;
    } else {
      player.coyoteTimer = Math.max(0, player.coyoteTimer - dt);
    }

    // ── Gravity ──
    player.vy += GRAVITY * dt;
    if (player.vy > 800) player.vy = 800;

    // ── Move X ──
    player.x += player.vx * dt;

    // World bounds
    if (player.x < 0) { player.x = 0; player.vx = 0; }
    if (player.x + player.w > LEVEL_WIDTH) { player.x = LEVEL_WIDTH - player.w; player.vx = 0; }

    // ── Collide X with platforms ──
    for (const p of level.platforms) {
      if (aabb(player, p)) {
        if (player.vx > 0) {
          player.x = p.x - player.w;
        } else if (player.vx < 0) {
          player.x = p.x + p.w;
        }
        player.vx = 0;
      }
    }

    // ── Move Y ──
    player.y += player.vy * dt;
    player.onGround = false;

    // ── Collide Y with platforms ──
    for (const p of level.platforms) {
      if (aabb(player, p)) {
        if (player.vy > 0) {
          player.y = p.y - player.h;
          player.onGround = true;
          player.vy = 0;
        } else if (player.vy < 0) {
          player.y = p.y + p.h;
          player.vy = 0;
        }
      }
    }

    // ── Fall off screen ──
    if (player.y > H + 50) {
      killPlayer();
      return;
    }

    // ── Animation ──
    player.animTimer += dt;
    if (player.animTimer > 0.12) {
      player.animTimer = 0;
      player.animFrame = (player.animFrame + 1) % 4;
    }

    // ── Coins ──
    for (let i = level.coins.length - 1; i >= 0; i--) {
      const c = level.coins[i];
      if (!c.collected && c.x > player.x - 20 && c.x < player.x + player.w + 20 &&
          c.y > player.y - 20 && c.y < player.y + player.h + 20) {
        c.collected = true;
        coinCount++;
        hudEl.textContent = `COINS: ${coinCount} / ${level.totalCoins}    LIVES: ${player.lives}`;
      }
    }

    // ── Enemies ──
    for (const e of level.enemies) {
      if (!e.alive) {
        e.squishTimer -= dt;
        continue;
      }

      // Patrol
      e.x += e.vx * dt;
      if (e.x <= e.minX || e.x + e.w >= e.maxX) {
        e.vx *= -1;
      }

      // Gravity for enemies
      e.vy += GRAVITY * dt;
      e.y += e.vy * dt;

      // Ground collision for enemies
      e.onGround = false;
      for (const p of level.platforms) {
        if (aabb(e, p)) {
          if (e.vy > 0) {
            e.y = p.y - e.h;
            e.vy = 0;
            e.onGround = true;
          }
        }
      }

      // Remove if fallen
      if (e.y > H + 100) {
        e.alive = false;
        continue;
      }

      // Player collision
      if (player.invincibleTimer <= 0 && aabb(player, e)) {
        // Check if player is stomping (falling and above enemy center)
        if (player.vy > 0 && player.y + player.h - 8 < e.y + e.h * 0.5) {
          // Stomp!
          e.alive = false;
          e.squishTimer = 0.5;
          player.vy = JUMP_VEL * 0.5; // bounce
        } else {
          // Hurt
          killPlayer();
          return;
        }
      }
    }

    // ── Flag (goal) ──
    const flag = level.flag;
    if (player.x + player.w > flag.x && player.x < flag.x + 20 &&
        player.y + player.h > flag.y && player.y < flag.y + 200) {
      gameState = "won";
      showMessage(
        `🎉 YOU WIN! 🎉`,
        `Coins: ${coinCount} / ${level.totalCoins}    Lives left: ${player.lives}`,
        "Press ENTER to play again"
      );
    }

    // ── Camera ──
    const targetX = player.x - W / 3;
    camera.x = lerp(camera.x, Math.max(0, Math.min(targetX, LEVEL_WIDTH - W)), 8 * dt);
  }

  function killPlayer() {
    if (!player.alive) return;
    player.alive = false;
    player.vy = -400;
    player.vx = 0;
    player.deathTimer = 1.2;
  }

  function respawnPlayer() {
    player.x = 60;
    player.y = GROUND_Y - PLAYER_H;
    player.vx = 0;
    player.vy = 0;
    player.onGround = false;
    player.coyoteTimer = 0;
    player.jumpBufferTimer = 0;
    player.invincibleTimer = 2.0;
    player.alive = true;
    camera.x = 0;
  }

  function showMessage(title, sub, subSub) {
    msgText.textContent = title;
    msgSub.textContent = sub + "\n" + (subSub || "");
    msgOverlay.style.display = "flex";
  }

  function hideMessage() {
    msgOverlay.style.display = "none";
  }

  // ─── Drawing ───────────────────────────────────────────────

  // Clouds (parallax)
  const clouds = [];
  for (let i = 0; i < 30; i++) {
    clouds.push({
      x: Math.random() * LEVEL_WIDTH,
      y: 20 + Math.random() * 100,
      w: 60 + Math.random() * 80,
      h: 25 + Math.random() * 20,
      speed: 0.1 + Math.random() * 0.2,
    });
  }

  // Hills (parallax background)
  const hills = [];
  for (let i = 0; i < 20; i++) {
    hills.push({
      x: i * 300 + Math.random() * 100,
      w: 120 + Math.random() * 100,
      h: 50 + Math.random() * 60,
    });
  }

  function drawBackground() {
    // Sky gradient
    const grad = ctx.createLinearGradient(0, 0, 0, H);
    grad.addColorStop(0, "#5c94fc");
    grad.addColorStop(0.7, "#87CEEB");
    grad.addColorStop(1, "#b8e4ff");
    ctx.fillStyle = grad;
    ctx.fillRect(0, 0, W, H);

    // Hills (parallax at 0.2x)
    ctx.fillStyle = "#4aad4a";
    for (const h of hills) {
      const sx = h.x - camera.x * 0.2;
      if (sx + h.w < -50 || sx > W + 50) continue;
      ctx.beginPath();
      ctx.ellipse(sx + h.w / 2, GROUND_Y - camera.y * 0 + 20, h.w / 2, h.h, 0, Math.PI, 0);
      ctx.fill();
    }

    // Clouds (parallax at 0.15x)
    ctx.fillStyle = "rgba(255,255,255,0.85)";
    for (const c of clouds) {
      const sx = c.x - camera.x * 0.15;
      const wrapX = ((sx % (W + 200)) + (W + 200)) % (W + 200) - 100;
      ctx.beginPath();
      ctx.ellipse(wrapX, c.y, c.w / 2, c.h / 2, 0, 0, Math.PI * 2);
      ctx.fill();
      ctx.beginPath();
      ctx.ellipse(wrapX - c.w * 0.25, c.y + 5, c.w * 0.3, c.h * 0.4, 0, 0, Math.PI * 2);
      ctx.fill();
      ctx.beginPath();
      ctx.ellipse(wrapX + c.w * 0.25, c.y + 3, c.w * 0.35, c.h * 0.45, 0, 0, Math.PI * 2);
      ctx.fill();
    }
  }

  function drawPlatform(p) {
    const sx = p.x - camera.x;
    const sy = p.y - camera.y;

    if (sx + p.w < -10 || sx > W + 10) return;

    if (p.h <= 20) {
      // Raised platform (brick style)
      ctx.fillStyle = "#c84c09";
      ctx.fillRect(sx, sy, p.w, p.h);
      // Brick lines
      ctx.strokeStyle = "#a03800";
      ctx.lineWidth = 1;
      for (let bx = sx; bx < sx + p.w; bx += 16) {
        ctx.beginPath();
        ctx.moveTo(bx, sy);
        ctx.lineTo(bx, sy + p.h);
        ctx.stroke();
      }
      // Top highlight
      ctx.fillStyle = "#e87040";
      ctx.fillRect(sx, sy, p.w, 3);
      // Bottom shadow
      ctx.fillStyle = "#8b3000";
      ctx.fillRect(sx, sy + p.h - 3, p.w, 3);
    } else {
      // Ground
      ctx.fillStyle = "#c84c09";
      ctx.fillRect(sx, sy, p.w, p.h);
      // Grass top
      ctx.fillStyle = "#4aad4a";
      ctx.fillRect(sx, sy, p.w, 6);
      // Grass detail
      ctx.fillStyle = "#5cc85c";
      for (let gx = sx; gx < sx + p.w; gx += 8) {
        ctx.fillRect(gx, sy, 2, 8);
      }
      // Dirt texture
      ctx.fillStyle = "#a03800";
      for (let dx = sx + 4; dx < sx + p.w - 4; dx += 12) {
        ctx.fillRect(dx, sy + 10, 4, 3);
        ctx.fillRect(dx + 6, sy + 18, 4, 3);
      }
    }
  }

  function drawPlayer() {
    const sx = player.x - camera.x;
    const sy = player.y - camera.y;

    // Blink when invincible
    if (player.invincibleTimer > 0 && Math.floor(player.invincibleTimer * 10) % 2 === 0) return;

    ctx.save();
    ctx.translate(sx + player.w / 2, sy + player.h / 2);
    ctx.scale(player.facing, 1);
    ctx.translate(-player.w / 2, -player.h / 2);

    // Death: flip upside down briefly
    if (!player.alive) {
      ctx.translate(player.w / 2, player.h / 2);
      ctx.rotate(Math.PI);
      ctx.translate(-player.w / 2, -player.h / 2);
    }

    const px = 0;
    const py = 0;

    // Hat
    ctx.fillStyle = "#e44040";
    ctx.fillRect(px + 2, py, 20, 8);
    ctx.fillRect(px, py + 2, 24, 6);

    // Face
    ctx.fillStyle = "#ffcc88";
    ctx.fillRect(px + 4, py + 8, 16, 12);

    // Eyes
    ctx.fillStyle = "#222";
    ctx.fillRect(px + 14, py + 10, 4, 4);

    // Mustache
    ctx.fillStyle = "#5c3000";
    ctx.fillRect(px + 10, py + 15, 12, 3);

    // Body (overalls)
    ctx.fillStyle = "#3060c8";
    ctx.fillRect(px + 2, py + 20, 20, 8);

    // Overalls buttons
    ctx.fillStyle = "#ffd700";
    ctx.fillRect(px + 8, py + 22, 3, 3);
    ctx.fillRect(px + 14, py + 22, 3, 3);

    // Legs
    ctx.fillStyle = "#3060c8";
    const legOffset = player.onGround && Math.abs(player.vx) > 10
      ? Math.sin(player.animFrame * Math.PI / 2) * 3
      : 0;
    ctx.fillRect(px + 2, py + 28, 8, 8);
    ctx.fillRect(px + 14, py + 28, 8, 8);

    // Shoes
    ctx.fillStyle = "#8b4513";
    ctx.fillRect(px + (legOffset > 0 ? 0 : 2), py + 33, 10, 4);
    ctx.fillRect(px + (legOffset > 0 ? 12 : 14), py + 33, 10, 4);

    ctx.restore();
  }

  function drawCoin(c) {
    if (c.collected) return;
    const sx = c.x - camera.x;
    const sy = c.y - camera.y;
    if (sx < -20 || sx > W + 20 || sy < -20 || sy > H + 20) return;

    const t = performance.now() / 300;
    const scaleX = Math.abs(Math.cos(t + c.x * 0.01));

    ctx.save();
    ctx.translate(sx, sy);
    ctx.scale(scaleX, 1);

    // Coin body
    ctx.fillStyle = "#ffd700";
    ctx.beginPath();
    ctx.ellipse(0, 0, 10, 12, 0, 0, Math.PI * 2);
    ctx.fill();

    // Coin highlight
    ctx.fillStyle = "#ffec80";
    ctx.beginPath();
    ctx.ellipse(-2, -3, 5, 7, -0.3, 0, Math.PI * 2);
    ctx.fill();

    // Dollar sign
    ctx.fillStyle = "#c8a000";
    ctx.font = "bold 12px serif";
    ctx.textAlign = "center";
    ctx.textBaseline = "middle";
    ctx.fillText("$", 1, 1);

    ctx.restore();
  }

  function drawEnemy(e) {
    const sx = e.x - camera.x;
    const sy = e.y - camera.y;
    if (sx < -40 || sx > W + 40) return;

    if (!e.alive) {
      // Squished
      ctx.fillStyle = "#a0522d";
      ctx.fillRect(sx, sy + e.h - 6, e.w, 6);
      return;
    }

    // Body (Goomba-style)
    ctx.fillStyle = "#a0522d";
    ctx.beginPath();
    ctx.ellipse(sx + e.w / 2, sy + e.h * 0.4, e.w / 2, e.h * 0.45, 0, Math.PI, 0);
    ctx.fill();
    ctx.fillRect(sx + 2, sy + e.h * 0.4, e.w - 4, e.h * 0.55);

    // Feet
    const walk = Math.sin(performance.now() / 150) * 2;
    ctx.fillStyle = "#222";
    ctx.fillRect(sx + 2 + walk, sy + e.h - 4, 8, 4);
    ctx.fillRect(sx + e.w - 10 - walk, sy + e.h - 4, 8, 4);

    // Eyes
    ctx.fillStyle = "#fff";
    ctx.fillRect(sx + 5, sy + 6, 7, 8);
    ctx.fillRect(sx + e.w - 12, sy + 6, 7, 8);
    ctx.fillStyle = "#222";
    const dir = e.vx > 0 ? 2 : 0;
    ctx.fillRect(sx + 6 + dir, sy + 8, 4, 5);
    ctx.fillRect(sx + e.w - 11 + dir, sy + 8, 4, 5);

    // Angry eyebrows
    ctx.fillStyle = "#222";
    ctx.fillRect(sx + 4, sy + 4, 8, 2);
    ctx.fillRect(sx + e.w - 12, sy + 4, 8, 2);
  }

  function drawFlag() {
    const f = level.flag;
    const sx = f.x - camera.x;
    const sy = f.y - camera.y;

    if (sx < -50 || sx > W + 50) return;

    // Pole
    ctx.fillStyle = "#888";
    ctx.fillRect(sx, sy, 4, 200);

    // Ball on top
    ctx.fillStyle = "#ffd700";
    ctx.beginPath();
    ctx.arc(sx + 2, sy, 6, 0, Math.PI * 2);
    ctx.fill();

    // Flag
    const wave = Math.sin(performance.now() / 400) * 3;
    ctx.fillStyle = "#e44040";
    ctx.beginPath();
    ctx.moveTo(sx + 4, sy + 8);
    ctx.lineTo(sx + 44 + wave, sy + 20);
    ctx.lineTo(sx + 4, sy + 40);
    ctx.closePath();
    ctx.fill();

    // Star on flag
    ctx.fillStyle = "#ffd700";
    ctx.font = "14px serif";
    ctx.fillText("★", sx + 14 + wave * 0.5, sy + 30);

    // Base
    ctx.fillStyle = "#888";
    ctx.fillRect(sx - 6, sy + 196, 16, 8);
  }

  function draw() {
    ctx.clearRect(0, 0, W, H);

    drawBackground();

    ctx.save();
    ctx.translate(0, camera.y);

    // Platforms
    for (const p of level.platforms) {
      drawPlatform(p);
    }

    // Coins
    for (const c of level.coins) {
      drawCoin(c);
    }

    // Enemies
    for (const e of level.enemies) {
      drawEnemy(e);
    }

    // Flag
    drawFlag();

    // Player
    drawPlayer();

    ctx.restore();
  }

  // ─── Game Loop ─────────────────────────────────────────────
  function gameLoop(timestamp) {
    if (!lastTime) lastTime = timestamp;
    let dt = (timestamp - lastTime) / 1000;
    lastTime = timestamp;

    // Clamp dt to avoid spiral of death
    if (dt > 0.05) dt = 0.05;

    // Handle restart
    if ((gameState === "won" || gameState === "dead") && keys["Enter"]) {
      coinCount = 0;
      initGame();
    }

    update(dt);
    draw();

    requestAnimationFrame(gameLoop);
  }

  // ─── Start ─────────────────────────────────────────────────
  initGame();
  requestAnimationFrame(gameLoop);

})();
index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Super Platformer</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
html, body {
  width: 100%; height: 100%;
  background: #000;
  display: flex; justify-content: center; align-items: center;
  overflow: hidden;
  font-family: 'Courier New', monospace;
}
canvas {
  display: block;
  image-rendering: pixelated;
  border: 2px solid #333;
}
#ui-overlay {
  position: absolute;
  top: 10px; left: 50%;
  transform: translateX(-50%);
  color: #fff;
  font-size: 18px;
  font-weight: bold;
  text-shadow: 2px 2px 0 #000;
  pointer-events: none;
  z-index: 10;
}
#message-overlay {
  position: absolute;
  top: 0; left: 0;
  width: 100%; height: 100%;
  display: none;
  flex-direction: column;
  justify-content: center;
  align-items: center;
  background: rgba(0,0,0,0.6);
  color: #fff;
  font-size: 28px;
  font-weight: bold;
  z-index: 20;
  text-align: center;
  line-height: 1.6;
}
#message-overlay .sub {
  font-size: 16px;
  margin-top: 10px;
  font-weight: normal;
  color: #ccc;
}
</style>
</head>
<body>

<div id="ui-overlay">
  <span id="hud">COINS: 0 / 0 &nbsp;&nbsp; LIVES: 3</span>
</div>

<div id="message-overlay">
  <div id="message-text"></div>
  <div class="sub" id="message-sub"></div>
</div>

<canvas id="game" width="800" height="450"></canvas>

<script src="game.js"></script>
</body>
</html>

rep 4 MANUAL partial/5

pi 63.3s · verify 4.1s · 56094 tok · responded: qwen3.6-35b-a3b · transcript

smoke 3/3 · note: boxes don't work, can die because of falling in hole

screenshot

verify output
CHECK PASS: canvas #game exists and is sized
CHECK PASS: no uncaught JS errors on load
CHECK PASS: keyboard input does not throw
score: 3/3 (100%)
produced code
game.js
// ============================================================
// Super Pixel Adventure — a Mario-style platformer
// ============================================================

(function () {
  "use strict";

  // ---- Canvas setup ----
  const canvas = document.getElementById("game");
  const ctx = canvas.getContext("2d");
  const W = canvas.width;       // 800
  const H = canvas.height;      // 450

  // ---- Constants ----
  const GRAVITY = 0.55;
  const MAX_FALL = 12;
  const PLAYER_SPEED = 4.5;
  const PLAYER_ACCEL = 0.6;
  const PLAYER_DECEL = 0.75;
  const JUMP_FORCE = -11.5;
  const TILE = 32;

  // ---- Input ----
  const keys = {};
  window.addEventListener("keydown", function (e) {
    keys[e.code] = true;
    // Prevent page scrolling for game keys
    if (["Space", "ArrowUp", "ArrowDown", "ArrowLeft", "ArrowRight"].includes(e.code)) {
      e.preventDefault();
    }
  });
  window.addEventListener("keyup", function (e) {
    keys[e.code] = false;
  });

  // ---- World / Level ----
  // World is defined in tile units (32px each).
  // 0 = air, 1 = ground, 2 = platform, 3 = coin, 4 = flag pole, 5 = question block

  const WORLD_WIDTH = 200;   // tiles wide
  const WORLD_HEIGHT = 14;   // tiles tall

  // Build the level map
  const map = [];
  for (let y = 0; y < WORLD_HEIGHT; y++) {
    map[y] = [];
    for (let x = 0; x < WORLD_WIDTH; x++) {
      map[y][x] = 0;
    }
  }

  // Ground — mostly solid with a couple of gaps
  function setGround(y, startX, endX) {
    for (let x = startX; x < endX; x++) {
      map[y][x] = 1;
    }
  }
  // Main ground with gaps
  setGround(12, 0, 35);
  setGround(12, 38, 65);
  setGround(12, 68, 110);
  setGround(12, 113, 150);
  setGround(12, 153, WORLD_WIDTH);
  // Extra ground layer for depth
  setGround(13, 0, WORLD_WIDTH);

  // Platforms at various heights
  function setPlatform(y, startX, endX) {
    for (let x = startX; x < endX; x++) {
      map[y][x] = 2;
    }
  }
  // Staircase section
  setPlatform(10, 10, 13);
  setPlatform(8, 13, 16);
  setPlatform(6, 16, 19);
  // Floating platforms
  setPlatform(9, 28, 33);
  setPlatform(7, 40, 45);
  setPlatform(5, 43, 48);
  // High platform
  setPlatform(8, 55, 62);
  setPlatform(6, 58, 65);
  // Step-up after gap
  setPlatform(10, 72, 75);
  setPlatform(8, 75, 78);
  setPlatform(6, 78, 81);
  // Long floating platform
  setPlatform(9, 90, 98);
  setPlatform(7, 100, 106);
  // Upper route
  setPlatform(7, 118, 124);
  setPlatform(5, 124, 130);
  setPlatform(7, 130, 136);
  // Final stretch
  setPlatform(9, 140, 145);
  setPlatform(7, 145, 150);
  setPlatform(5, 150, 155);

  // Question blocks (contain coins)
  function setQuestion(y, x) {
    map[y][x] = 5;
  }
  setQuestion(8, 11);
  setQuestion(7, 44);
  setQuestion(5, 60);
  setQuestion(5, 126);

  // Coins
  function setCoin(y, x) {
    map[y][x] = 3;
  }
  // Coins on the ground
  setCoin(11, 5);
  setCoin(11, 6);
  setCoin(11, 7);
  // Coins on platforms
  setCoin(8, 29);
  setCoin(8, 30);
  setCoin(8, 31);
  setCoin(6, 41);
  setCoin(6, 42);
  setCoin(4, 56);
  setCoin(4, 57);
  setCoin(4, 58);
  setCoin(8, 73);
  setCoin(8, 74);
  setCoin(5, 92);
  setCoin(5, 93);
  setCoin(5, 94);
  setCoin(5, 95);
  setCoin(6, 102);
  setCoin(6, 103);
  // Coins after gap
  setCoin(11, 70);
  setCoin(11, 71);
  setCoin(11, 72);
  // High coins
  setCoin(4, 120);
  setCoin(4, 121);
  setCoin(4, 122);
  setCoin(4, 128);
  setCoin(4, 129);
  setCoin(4, 132);
  setCoin(4, 133);
  // Final coins
  setCoin(8, 142);
  setCoin(8, 143);
  setCoin(8, 144);
  setCoin(4, 152);
  setCoin(4, 153);
  setCoin(4, 154);

  // Flag pole at the end
  function setFlag(y, x) {
    map[y][x] = 4;
  }
  // Flag pole — vertical column of flag tiles
  for (let fy = 3; fy <= 11; fy++) {
    map[fy][185] = 4;
  }
  // Flag top
  map[2][185] = 4;
  // Base
  map[11][185] = 4;
  map[12][185] = 1;

  // ---- Player ----
  const player = {
    x: 3 * TILE,
    y: 10 * TILE,
    w: 24,
    h: 30,
    vx: 0,
    vy: 0,
    onGround: false,
    facing: 1,  // 1 = right, -1 = left
    frame: 0,
    frameTimer: 0,
    alive: true,
    won: false,
    winTimer: 0,
    // Animation
    walkFrame: 0,
    walkTimer: 0,
    jumpFrame: 0,
  };

  // ---- Camera ----
  const camera = {
    x: 0,
    y: 0,
    targetX: 0,
    targetY: 0,
  };

  // ---- Coins (collected state) ----
  let coins = [];
  let score = 0;
  let totalCoins = 0;

  // Count total coins
  for (let y = 0; y < WORLD_HEIGHT; y++) {
    for (let x = 0; x < WORLD_WIDTH; x++) {
      if (map[y][x] === 3) totalCoins++;
    }
  }

  // ---- Particles ----
  let particles = [];

  function spawnParticles(x, y, color, count) {
    for (let i = 0; i < count; i++) {
      particles.push({
        x: x,
        y: y,
        vx: (Math.random() - 0.5) * 6,
        vy: (Math.random() - 0.5) * 6 - 2,
        life: 30 + Math.random() * 20,
        maxLife: 50,
        color: color,
        size: 2 + Math.random() * 3,
      });
    }
  }

  // ---- Clouds (decoration) ----
  let clouds = [];
  for (let i = 0; i < 25; i++) {
    clouds.push({
      x: Math.random() * WORLD_WIDTH * TILE,
      y: 20 + Math.random() * 100,
      w: 60 + Math.random() * 80,
      h: 25 + Math.random() * 20,
      speed: 0.1 + Math.random() * 0.2,
    });
  }

  // ---- Background hills ----
  let hills = [];
  for (let i = 0; i < 15; i++) {
    hills.push({
      x: i * 200 + Math.random() * 50,
      w: 120 + Math.random() * 100,
      h: 40 + Math.random() * 60,
      color: `hsl(${120 + Math.random() * 30}, ${40 + Math.random() * 20}%, ${30 + Math.random() * 15}%)`,
    });
  }

  // ---- Collision helpers ----
  function getTile(px, py) {
    const tx = Math.floor(px / TILE);
    const ty = Math.floor(py / TILE);
    if (tx < 0 || tx >= WORLD_WIDTH || ty < 0 || ty >= WORLD_HEIGHT) return 0;
    return map[ty][tx];
  }

  function isSolid(tileType) {
    return tileType === 1 || tileType === 2 || tileType === 5;
  }

  function rectOverlap(a, b) {
    return (
      a.x < b.x + b.w &&
      a.x + a.w > b.x &&
      a.y < b.y + b.h &&
      a.y + a.h > b.y
    );
  }

  // ---- Game state ----
  let gameState = "playing"; // "playing", "won"
  let gameTime = 0;

  // ---- Update ----
  function update() {
    gameTime++;

    if (gameState === "won") {
      player.winTimer++;
      // Update particles
      for (let i = particles.length - 1; i >= 0; i--) {
        const p = particles[i];
        p.x += p.vx;
        p.y += p.vy;
        p.vy += 0.1;
        p.life--;
        if (p.life <= 0) particles.splice(i, 1);
      }
      return;
    }

    // ---- Player input ----
    let moveX = 0;
    if (keys["ArrowLeft"] || keys["KeyA"]) moveX -= 1;
    if (keys["ArrowRight"] || keys["KeyD"]) moveX += 1;

    // Acceleration / deceleration
    if (moveX !== 0) {
      player.vx += moveX * PLAYER_ACCEL;
      if (player.vx > PLAYER_SPEED) player.vx = PLAYER_SPEED;
      if (player.vx < -PLAYER_SPEED) player.vx = -PLAYER_SPEED;
      player.facing = moveX;
      // Walking animation
      player.walkTimer++;
      if (player.walkTimer > 6) {
        player.walkTimer = 0;
        player.walkFrame = (player.walkFrame + 1) % 4;
      }
    } else {
      // Deceleration
      if (player.vx > 0) {
        player.vx -= PLAYER_DECEL;
        if (player.vx < 0) player.vx = 0;
      } else if (player.vx < 0) {
        player.vx += PLAYER_DECEL;
        if (player.vx > 0) player.vx = 0;
      }
      player.walkFrame = 0;
      player.walkTimer = 0;
    }

    // Jump
    if ((keys["Space"] || keys["ArrowUp"] || keys["KeyW"]) && player.onGround) {
      player.vy = JUMP_FORCE;
      player.onGround = false;
    }

    // Apply gravity
    player.vy += GRAVITY;
    if (player.vy > MAX_FALL) player.vy = MAX_FALL;

    // ---- Horizontal collision ----
    player.x += player.vx;
    resolveCollisionX();

    // ---- Vertical collision ----
    player.y += player.vy;
    player.onGround = false;
    resolveCollisionY();

    // ---- Coin collection ----
    const pcx = player.x + player.w / 2;
    const pcy = player.y + player.h / 2;
    for (let y = 0; y < WORLD_HEIGHT; y++) {
      for (let x = 0; x < WORLD_WIDTH; x++) {
        if (map[y][x] === 3) {
          const cx = x * TILE + TILE / 2;
          const cy = y * TILE + TILE / 2;
          const dx = pcx - cx;
          const dy = pcy - cy;
          if (Math.sqrt(dx * dx + dy * dy) < 24) {
            map[y][x] = 0;
            score++;
            spawnParticles(cx, cy, "#FFD700", 8);
          }
        }
      }
    }

    // ---- Question block hit (from below) ----
    if (player.vy < 0) {
      const headTileX = Math.floor((player.x + player.w / 2) / TILE);
      const headTileY = Math.floor((player.y - 2) / TILE);
      if (headTileY >= 0 && headTileY < WORLD_HEIGHT && headTileX >= 0 && headTileX < WORLD_WIDTH) {
        if (map[headTileY][headTileX] === 5) {
          map[headTileY][headTileX] = 2; // turn into solid block
          score += 5;
          spawnParticles(headTileX * TILE + TILE / 2, headTileY * TILE, "#FFD700", 12);
        }
      }
    }

    // ---- Flag / goal ----
    const flagTileX = Math.floor(pcx / TILE);
    const flagTileY = Math.floor(pcy / TILE);
    if (flagTileX >= 0 && flagTileX < WORLD_WIDTH && flagTileY >= 0 && flagTileY < WORLD_HEIGHT) {
      if (map[flagTileY][flagTileX] === 4) {
        gameState = "won";
        player.won = true;
        player.winTimer = 0;
        spawnParticles(pcx, pcy, "#FF6B6B", 20);
        spawnParticles(pcx, pcy, "#4ECDC4", 20);
        spawnParticles(pcx, pcy, "#FFE66D", 20);
      }
    }

    // ---- Fall off screen ----
    if (player.y > H + 50) {
      // Respawn
      player.x = 3 * TILE;
      player.y = 10 * TILE;
      player.vx = 0;
      player.vy = 0;
    }

    // ---- Camera follow ----
    camera.targetX = player.x - W / 3;
    camera.targetY = Math.min(player.y - H / 2, 0);
    // Clamp camera
    if (camera.targetX < 0) camera.targetX = 0;
    if (camera.targetX > WORLD_WIDTH * TILE - W) camera.targetX = WORLD_WIDTH * TILE - W;
    if (camera.targetY > 0) camera.targetY = 0;

    camera.x += (camera.targetX - camera.x) * 0.1;
    camera.y += (camera.targetY - camera.y) * 0.08;

    // ---- Update particles ----
    for (let i = particles.length - 1; i >= 0; i--) {
      const p = particles[i];
      p.x += p.vx;
      p.y += p.vy;
      p.vy += 0.1;
      p.life--;
      if (p.life <= 0) particles.splice(i, 1);
    }

    // ---- Update clouds ----
    for (let c of clouds) {
      c.x += c.speed;
      if (c.x > WORLD_WIDTH * TILE + 100) c.x = -c.w - 50;
    }
  }

  function resolveCollisionX() {
    const left = Math.floor(player.x / TILE);
    const right = Math.floor((player.x + player.w - 1) / TILE);
    const top = Math.floor(player.y / TILE);
    const bottom = Math.floor((player.y + player.h - 1) / TILE);

    for (let ty = top; ty <= bottom; ty++) {
      for (let tx = left; tx <= right; tx++) {
        if (tx < 0 || tx >= WORLD_WIDTH || ty < 0 || ty >= WORLD_HEIGHT) continue;
        if (isSolid(map[ty][tx])) {
          if (player.vx > 0) {
            player.x = tx * TILE - player.w;
            player.vx = 0;
          } else if (player.vx < 0) {
            player.x = (tx + 1) * TILE;
            player.vx = 0;
          }
        }
      }
    }
  }

  function resolveCollisionY() {
    const left = Math.floor(player.x / TILE);
    const right = Math.floor((player.x + player.w - 1) / TILE);
    const top = Math.floor(player.y / TILE);
    const bottom = Math.floor((player.y + player.h - 1) / TILE);

    for (let ty = top; ty <= bottom; ty++) {
      for (let tx = left; tx <= right; tx++) {
        if (tx < 0 || tx >= WORLD_WIDTH || ty < 0 || ty >= WORLD_HEIGHT) continue;
        if (isSolid(map[ty][tx])) {
          if (player.vy > 0) {
            player.y = ty * TILE - player.h;
            player.vy = 0;
            player.onGround = true;
          } else if (player.vy < 0) {
            player.y = (ty + 1) * TILE;
            player.vy = 0;
          }
        }
      }
    }
  }

  // ---- Drawing ----
  function drawSky() {
    // Sky gradient
    const grad = ctx.createLinearGradient(0, 0, 0, H);
    grad.addColorStop(0, "#4FC3F7");
    grad.addColorStop(0.5, "#81D4FA");
    grad.addColorStop(1, "#B3E5FC");
    ctx.fillStyle = grad;
    ctx.fillRect(0, 0, W, H);
  }

  function drawClouds() {
    ctx.fillStyle = "rgba(255, 255, 255, 0.8)";
    for (let c of clouds) {
      const sx = c.x - camera.x * 0.3;
      const sy = c.y;
      // Wrap for seamless scrolling
      const wrappedX = ((sx % (WORLD_WIDTH * TILE + 200)) + WORLD_WIDTH * TILE + 200) % (WORLD_WIDTH * TILE + 200) - 100;
      if (wrappedX > -c.w && wrappedX < W + c.w) {
        drawCloud(wrappedX, sy, c.w, c.h);
      }
    }
  }

  function drawCloud(x, y, w, h) {
    ctx.beginPath();
    ctx.ellipse(x + w * 0.3, y + h * 0.5, w * 0.3, h * 0.5, 0, 0, Math.PI * 2);
    ctx.fill();
    ctx.beginPath();
    ctx.ellipse(x + w * 0.6, y + h * 0.35, w * 0.35, h * 0.55, 0, 0, Math.PI * 2);
    ctx.fill();
    ctx.beginPath();
    ctx.ellipse(x + w * 0.75, y + h * 0.55, w * 0.25, h * 0.45, 0, 0, Math.PI * 2);
    ctx.fill();
  }

  function drawHills() {
    for (let h of hills) {
      const sx = h.x - camera.x * 0.5;
      if (sx > -h.w && sx < W + h.w) {
        ctx.fillStyle = h.color;
        ctx.beginPath();
        ctx.moveTo(sx - h.w / 2, 12 * TILE);
        ctx.quadraticCurveTo(sx, 12 * TILE - h.h, sx + h.w / 2, 12 * TILE);
        ctx.fill();
      }
    }
  }

  function drawTile(tx, ty, type) {
    const sx = tx * TILE - camera.x;
    const sy = ty * TILE - camera.y;

    // Skip if off screen
    if (sx < -TILE || sx > W + TILE || sy < -TILE || sy > H + TILE) return;

    if (type === 1) {
      // Ground block
      ctx.fillStyle = "#8B5E3C";
      ctx.fillRect(sx, sy, TILE, TILE);
      // Grass top
      ctx.fillStyle = "#4CAF50";
      ctx.fillRect(sx, sy, TILE, 6);
      ctx.fillStyle = "#388E3C";
      ctx.fillRect(sx, sy, TILE, 3);
      // Dirt texture
      ctx.fillStyle = "#795548";
      ctx.fillRect(sx + 4, sy + 12, 6, 4);
      ctx.fillRect(sx + 18, sy + 20, 8, 4);
      ctx.fillRect(sx + 8, sy + 24, 5, 4);
    } else if (type === 2) {
      // Platform (brick)
      ctx.fillStyle = "#C0392B";
      ctx.fillRect(sx, sy, TILE, TILE);
      ctx.fillStyle = "#E74C3C";
      ctx.fillRect(sx + 1, sy + 1, TILE - 2, TILE / 2 - 2);
      // Brick lines
      ctx.fillStyle = "#922B21";
      ctx.fillRect(sx, sy + TILE / 2 - 1, TILE, 2);
      ctx.fillRect(sx + TILE / 2 - 1, sy, 2, TILE / 2);
      ctx.fillRect(sx + TILE / 4, sy + TILE / 2, 2, TILE / 2);
      ctx.fillRect(sx + TILE * 3 / 4, sy + TILE / 2, 2, TILE / 2);
      // Highlight
      ctx.fillStyle = "#FF6B6B";
      ctx.fillRect(sx + 2, sy + 2, TILE - 4, 2);
    } else if (type === 3) {
      // Coin
      const coinX = sx + TILE / 2;
      const coinY = sy + TILE / 2;
      const bob = Math.sin(gameTime * 0.08 + tx * 0.5) * 3;
      const stretch = Math.abs(Math.cos(gameTime * 0.06 + tx * 0.3));
      ctx.save();
      ctx.translate(coinX, coinY + bob);
      ctx.scale(stretch, 1);
      // Coin body
      ctx.fillStyle = "#FFD700";
      ctx.beginPath();
      ctx.arc(0, 0, 10, 0, Math.PI * 2);
      ctx.fill();
      // Coin highlight
      ctx.fillStyle = "#FFF176";
      ctx.beginPath();
      ctx.arc(-2, -3, 5, 0, Math.PI * 2);
      ctx.fill();
      // Coin border
      ctx.strokeStyle = "#F57F17";
      ctx.lineWidth = 2;
      ctx.beginPath();
      ctx.arc(0, 0, 10, 0, Math.PI * 2);
      ctx.stroke();
      ctx.restore();
    } else if (type === 4) {
      // Flag pole
      if (ty === 2) {
        // Flag top (ball)
        ctx.fillStyle = "#FFD700";
        ctx.beginPath();
        ctx.arc(sx + TILE / 2, sy + TILE / 2, 8, 0, Math.PI * 2);
        ctx.fill();
        ctx.fillStyle = "#FFF176";
        ctx.beginPath();
        ctx.arc(sx + TILE / 2 - 2, sy + TILE / 2 - 2, 3, 0, Math.PI * 2);
        ctx.fill();
      } else if (ty === 3) {
        // Flag cloth
        const wave = Math.sin(gameTime * 0.1) * 3;
        ctx.fillStyle = "#E94560";
        ctx.beginPath();
        ctx.moveTo(sx + TILE / 2, sy);
        ctx.lineTo(sx + TILE * 1.5 + wave, sy + 8);
        ctx.lineTo(sx + TILE / 2, sy + 16);
        ctx.fill();
        ctx.fillStyle = "#FF6B6B";
        ctx.beginPath();
        ctx.moveTo(sx + TILE / 2, sy + 3);
        ctx.lineTo(sx + TILE * 1.3 + wave * 0.7, sy + 8);
        ctx.lineTo(sx + TILE / 2, sy + 13);
        ctx.fill();
      } else if (ty >= 4 && ty <= 10) {
        // Pole
        ctx.fillStyle = "#9E9E9E";
        ctx.fillRect(sx + TILE / 2 - 2, sy, 4, TILE);
        ctx.fillStyle = "#BDBDBD";
        ctx.fillRect(sx + TILE / 2 - 1, sy, 2, TILE);
      } else {
        // Flag base
        ctx.fillStyle = "#7F8C8D";
        ctx.fillRect(sx, sy, TILE, TILE);
        ctx.fillStyle = "#95A5A6";
        ctx.fillRect(sx + 2, sy + 2, TILE - 4, TILE - 4);
      }
    } else if (type === 5) {
      // Question block
      const pulse = Math.sin(gameTime * 0.05) * 0.1;
      ctx.fillStyle = "#FFB300";
      ctx.fillRect(sx, sy, TILE, TILE);
      ctx.fillStyle = "#FFD54F";
      ctx.fillRect(sx + 2, sy + 2, TILE - 4, TILE / 2 - 3);
      // Question mark
      ctx.fillStyle = "#E65100";
      ctx.font = "bold 18px monospace";
      ctx.textAlign = "center";
      ctx.textBaseline = "middle";
      ctx.fillText("?", sx + TILE / 2, sy + TILE / 2 + pulse * 5);
      // Border
      ctx.strokeStyle = "#E65100";
      ctx.lineWidth = 2;
      ctx.strokeRect(sx + 1, sy + 1, TILE - 2, TILE - 2);
    }
  }

  function drawPlayer() {
    const sx = player.x - camera.x;
    const sy = player.y - camera.y;
    const f = player.facing;

    // Shadow
    ctx.fillStyle = "rgba(0,0,0,0.2)";
    ctx.beginPath();
    ctx.ellipse(sx + player.w / 2, sy + player.h + 2, player.w / 2 + 2, 4, 0, 0, Math.PI * 2);
    ctx.fill();

    ctx.save();
    if (f === -1) {
      ctx.translate(sx + player.w / 2, 0);
      ctx.scale(-1, 1);
      ctx.translate(-(sx + player.w / 2), 0);
    }

    // Body
    ctx.fillStyle = "#E94560";
    ctx.fillRect(sx + 4, sy + 10, 16, 14);

    // Overalls
    ctx.fillStyle = "#2C3E50";
    ctx.fillRect(sx + 4, sy + 18, 16, 8);

    // Head
    ctx.fillStyle = "#FDBCB4";
    ctx.fillRect(sx + 4, sy, 16, 12);

    // Hat
    ctx.fillStyle = "#E94560";
    ctx.fillRect(sx + 2, sy - 4, 20, 6);
    ctx.fillRect(sx + 6, sy - 8, 14, 6);

    // Hat brim
    ctx.fillStyle = "#C0392B";
    ctx.fillRect(sx + 2, sy - 1, 22, 3);

    // Eyes
    ctx.fillStyle = "#2C3E50";
    ctx.fillRect(sx + 12, sy + 3, 3, 4);

    // Mustache
    ctx.fillStyle = "#5D4037";
    ctx.fillRect(sx + 10, sy + 8, 8, 2);

    // Legs animation
    if (!player.onGround) {
      // Jumping pose
      ctx.fillStyle = "#2C3E50";
      ctx.fillRect(sx + 2, sy + 24, 6, 6);
      ctx.fillRect(sx + 16, sy + 24, 6, 6);
      // Shoes
      ctx.fillStyle = "#5D4037";
      ctx.fillRect(sx, sy + 28, 8, 4);
      ctx.fillRect(sx + 16, sy + 28, 8, 4);
    } else if (Math.abs(player.vx) > 0.5) {
      // Walking
      const legOffset = Math.sin(player.walkFrame * Math.PI / 2) * 4;
      ctx.fillStyle = "#2C3E50";
      ctx.fillRect(sx + 4 + legOffset, sy + 24, 6, 4);
      ctx.fillRect(sx + 14 - legOffset, sy + 24, 6, 4);
      // Shoes
      ctx.fillStyle = "#5D4037";
      ctx.fillRect(sx + 2 + legOffset, sy + 26, 8, 4);
      ctx.fillRect(sx + 14 - legOffset, sy + 26, 8, 4);
    } else {
      // Standing
      ctx.fillStyle = "#2C3E50";
      ctx.fillRect(sx + 4, sy + 24, 6, 4);
      ctx.fillRect(sx + 14, sy + 24, 6, 4);
      // Shoes
      ctx.fillStyle = "#5D4037";
      ctx.fillRect(sx + 2, sy + 26, 8, 4);
      ctx.fillRect(sx + 14, sy + 26, 8, 4);
    }

    // Arms
    ctx.fillStyle = "#FDBCB4";
    if (!player.onGround) {
      ctx.fillRect(sx - 2, sy + 10, 6, 4);
      ctx.fillRect(sx + 20, sy + 10, 6, 4);
    } else {
      const armSwing = Math.sin(player.walkFrame * Math.PI / 2) * 3;
      ctx.fillRect(sx - 2, sy + 14 + armSwing, 6, 4);
      ctx.fillRect(sx + 20, sy + 14 - armSwing, 6, 4);
    }

    ctx.restore();
  }

  function drawParticles() {
    for (let p of particles) {
      const alpha = p.life / p.maxLife;
      ctx.globalAlpha = alpha;
      ctx.fillStyle = p.color;
      ctx.fillRect(p.x - camera.x, p.y - camera.y, p.size, p.size);
    }
    ctx.globalAlpha = 1;
  }

  function drawHUD() {
    // Score background
    ctx.fillStyle = "rgba(0, 0, 0, 0.5)";
    ctx.fillRect(10, 10, 160, 40);
    ctx.strokeStyle = "rgba(255,255,255,0.3)";
    ctx.lineWidth = 1;
    ctx.strokeRect(10, 10, 160, 40);

    // Coins
    ctx.fillStyle = "#FFD700";
    ctx.font = "bold 16px monospace";
    ctx.textAlign = "left";
    ctx.textBaseline = "middle";
    ctx.fillText("🪙 × " + score, 20, 28);

    // Total coins
    ctx.fillStyle = "#BDBDBD";
    ctx.font = "12px monospace";
    ctx.fillText("Total: " + score + " / " + totalCoins, 20, 44);

    // Controls hint (fades out)
    if (gameTime < 300) {
      const alpha = Math.max(0, 1 - gameTime / 300);
      ctx.globalAlpha = alpha;
      ctx.fillStyle = "rgba(0, 0, 0, 0.6)";
      ctx.fillRect(W / 2 - 180, H - 50, 360, 36);
      ctx.fillStyle = "#FFFFFF";
      ctx.font = "14px monospace";
      ctx.textAlign = "center";
      ctx.fillText("← → or A D to move  |  SPACE or ↑ to jump  |  Reach the flag!", W / 2, H - 32);
      ctx.globalAlpha = 1;
    }
  }

  function drawWinScreen() {
    // Darken
    ctx.fillStyle = "rgba(0, 0, 0, 0.6)";
    ctx.fillRect(0, 0, W, H);

    // Win box
    const boxW = 400;
    const boxH = 200;
    const boxX = (W - boxW) / 2;
    const boxY = (H - boxH) / 2;

    ctx.fillStyle = "#2C3E50";
    ctx.fillRect(boxX, boxY, boxW, boxH);
    ctx.strokeStyle = "#E94560";
    ctx.lineWidth = 3;
    ctx.strokeRect(boxX, boxY, boxW, boxH);

    // Title
    ctx.fillStyle = "#FFD700";
    ctx.font = "bold 36px monospace";
    ctx.textAlign = "center";
    ctx.textBaseline = "middle";
    ctx.fillText("🏆 YOU WIN! 🏆", W / 2, boxY + 50);

    // Score
    ctx.fillStyle = "#FFFFFF";
    ctx.font = "20px monospace";
    ctx.fillText("Coins collected: " + score + " / " + totalCoins, W / 2, boxY + 100);

    // Rating
    let rating = "⭐";
    if (score >= totalCoins) rating = "⭐⭐⭐";
    else if (score >= totalCoins * 0.7) rating = "⭐⭐";
    ctx.font = "28px monospace";
    ctx.fillText(rating, W / 2, boxY + 140);

    // Restart
    if (player.winTimer > 60) {
      ctx.fillStyle = "#4ECDC4";
      ctx.font = "16px monospace";
      ctx.fillText("Press SPACE to play again", W / 2, boxY + 180);

      if (keys["Space"]) {
        resetGame();
      }
    }
  }

  function resetGame() {
    // Reset player
    player.x = 3 * TILE;
    player.y = 10 * TILE;
    player.vx = 0;
    player.vy = 0;
    player.onGround = false;
    player.alive = true;
    player.won = false;
    player.winTimer = 0;

    // Reset camera
    camera.x = 0;
    camera.y = 0;

    // Reset score
    score = 0;
    gameState = "playing";
    gameTime = 0;
    particles = [];

    // Reset map coins and question blocks
    for (let y = 0; y < WORLD_HEIGHT; y++) {
      for (let x = 0; x < WORLD_WIDTH; x++) {
        if (map[y][x] === 0) {
          // Check original level for coins and question blocks
          // We need to rebuild — let's store originals
        }
      }
    }
    rebuildLevel();
  }

  function rebuildLevel() {
    // Rebuild the entire map
    for (let y = 0; y < WORLD_HEIGHT; y++) {
      for (let x = 0; x < WORLD_WIDTH; x++) {
        map[y][x] = 0;
      }
    }

    setGround(12, 0, 35);
    setGround(12, 38, 65);
    setGround(12, 68, 110);
    setGround(12, 113, 150);
    setGround(12, 153, WORLD_WIDTH);
    setGround(13, 0, WORLD_WIDTH);

    setPlatform(10, 10, 13);
    setPlatform(8, 13, 16);
    setPlatform(6, 16, 19);
    setPlatform(9, 28, 33);
    setPlatform(7, 40, 45);
    setPlatform(5, 43, 48);
    setPlatform(8, 55, 62);
    setPlatform(6, 58, 65);
    setPlatform(10, 72, 75);
    setPlatform(8, 75, 78);
    setPlatform(6, 78, 81);
    setPlatform(9, 90, 98);
    setPlatform(7, 100, 106);
    setPlatform(7, 118, 124);
    setPlatform(5, 124, 130);
    setPlatform(7, 130, 136);
    setPlatform(9, 140, 145);
    setPlatform(7, 145, 150);
    setPlatform(5, 150, 155);

    setQuestion(8, 11);
    setQuestion(7, 44);
    setQuestion(5, 60);
    setQuestion(4, 126);

    setCoin(11, 5);
    setCoin(11, 6);
    setCoin(11, 7);
    setCoin(8, 29);
    setCoin(8, 30);
    setCoin(8, 31);
    setCoin(6, 41);
    setCoin(6, 42);
    setCoin(4, 56);
    setCoin(4, 57);
    setCoin(4, 58);
    setCoin(8, 73);
    setCoin(8, 74);
    setCoin(5, 92);
    setCoin(5, 93);
    setCoin(5, 94);
    setCoin(5, 95);
    setCoin(6, 102);
    setCoin(6, 103);
    setCoin(11, 70);
    setCoin(11, 71);
    setCoin(11, 72);
    setCoin(4, 120);
    setCoin(4, 121);
    setCoin(4, 122);
    setCoin(4, 128);
    setCoin(4, 129);
    setCoin(4, 132);
    setCoin(4, 133);
    setCoin(8, 142);
    setCoin(8, 143);
    setCoin(8, 144);
    setCoin(4, 152);
    setCoin(4, 153);
    setCoin(4, 154);

    for (let fy = 3; fy <= 11; fy++) {
      map[fy][185] = 4;
    }
    map[2][185] = 4;
    map[11][185] = 4;
    map[12][185] = 1;
  }

  // ---- Main render ----
  function draw() {
    ctx.clearRect(0, 0, W, H);

    drawSky();
    drawClouds();
    drawHills();

    // Draw tiles
    const startTX = Math.max(0, Math.floor(camera.x / TILE) - 1);
    const endTX = Math.min(WORLD_WIDTH, Math.ceil((camera.x + W) / TILE) + 1);
    const startTY = Math.max(0, Math.floor(camera.y / TILE) - 1);
    const endTY = Math.min(WORLD_HEIGHT, Math.ceil((camera.y + H) / TILE) + 1);

    for (let ty = startTY; ty < endTY; ty++) {
      for (let tx = startTX; tx < endTX; tx++) {
        if (map[ty][tx] > 0) {
          drawTile(tx, ty, map[ty][tx]);
        }
      }
    }

    drawParticles();
    drawPlayer();
    drawHUD();

    if (gameState === "won") {
      drawWinScreen();
    }
  }

  // ---- Game loop ----
  function gameLoop() {
    update();
    draw();
    requestAnimationFrame(gameLoop);
  }

  // ---- Start ----
  gameLoop();

})();
index.html
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Super Pixel Adventure</title>
  <style>
    * { margin: 0; padding: 0; box-sizing: border-box; }
    html, body {
      width: 100%; height: 100%;
      background: #1a1a2e;
      display: flex;
      justify-content: center;
      align-items: center;
      overflow: hidden;
      font-family: monospace;
    }
    canvas#game {
      border: 3px solid #e94560;
      border-radius: 4px;
      image-rendering: pixelated;
      image-rendering: crisp-edges;
      box-shadow: 0 0 30px rgba(233, 69, 96, 0.3);
    }
  </style>
</head>
<body>
  <canvas id="game" width="800" height="450"></canvas>
  <script src="game.js"></script>
</body>
</html>

rep 5 MANUAL partial/5

pi 62.7s · verify 4.1s · 52342 tok · responded: qwen3.6-35b-a3b · transcript

smoke 3/3

screenshot

verify output
CHECK PASS: canvas #game exists and is sized
CHECK PASS: no uncaught JS errors on load
CHECK PASS: keyboard input does not throw
score: 3/3 (100%)
produced code
index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Super Platformer</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
  background: #000;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  height: 100vh;
  overflow: hidden;
  font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
}
canvas {
  border: 2px solid #333;
  display: block;
  image-rendering: pixelated;
}
#info {
  color: #aaa;
  margin-top: 10px;
  font-size: 14px;
  text-align: center;
}
</style>
</head>
<body>
<canvas id="game" width="800" height="450"></canvas>
<div id="info">Arrow Keys / WASD to move &bull; Space / Up to jump &bull; R to restart</div>
<script>
(function() {
  "use strict";

  var canvas = document.getElementById("game");
  var ctx = canvas.getContext("2d");
  var W = canvas.width;
  var H = canvas.height;

  // ─── Constants ───────────────────────────────────────────
  var GRAVITY       = 0.6;
  var MOVE_SPEED    = 4.5;
  var JUMP_FORCE    = -12;
  var FRICTION      = 0.82;
  var MAX_FALL      = 14;
  var TILE          = 32;
  var LEVEL_W       = 200;  // tiles wide
  var LEVEL_H       = Math.ceil(H / TILE) + 1;

  // ─── Game State ──────────────────────────────────────────
  var state = "playing";  // "playing" | "win" | "dead"
  var score = 0;
  var coinsCollected = 0;
  var totalCoins = 0;
  var cameraX = 0;
  var particles = [];
  var deathTimer = 0;
  var winTimer = 0;
  var frameCount = 0;

  // ─── Input ───────────────────────────────────────────────
  var keys = {};
  document.addEventListener("keydown", function(e) {
    keys[e.code] = true;
    if (["Space","ArrowUp","ArrowDown","ArrowLeft","ArrowRight"].indexOf(e.code) !== -1) {
      e.preventDefault();
    }
    if (e.code === "KeyR") restart();
  });
  document.addEventListener("keyup", function(e) {
    keys[e.code] = false;
  });

  // ─── Level Data ──────────────────────────────────────────
  // 0=air, 1=ground, 2=brick, 3=question block, 4=pipe_L, 5=pipe_T, 6=pipe_R
  var levelTiles = [];
  for (var y = 0; y < LEVEL_H; y++) {
    levelTiles[y] = [];
    for (var x = 0; x < LEVEL_W; x++) {
      levelTiles[y][x] = 0;
    }
  }

  // Ground (rows near bottom)
  var groundRow = LEVEL_H - 2;
  for (var x = 0; x < LEVEL_W; x++) {
    // Gaps in ground
    if ((x >= 28 && x <= 30) || (x >= 55 && x <= 58) || (x >= 90 && x <= 93) || (x >= 130 && x <= 133)) {
      continue;
    }
    levelTiles[groundRow][x] = 1;
    levelTiles[groundRow + 1][x] = 1;
  }

  // Platforms (floating)
  var platforms = [
    {x:8, y:groundRow-4, w:5, type:2},
    {x:16, y:groundRow-5, w:3, type:3},
    {x:22, y:groundRow-3, w:4, type:2},
    {x:35, y:groundRow-5, w:6, type:2},
    {x:37, y:groundRow-5, w:1, type:3},  // question block in platform
    {x:45, y:groundRow-4, w:3, type:2},
    {x:50, y:groundRow-7, w:4, type:2},
    {x:62, y:groundRow-5, w:5, type:2},
    {x:64, y:groundRow-5, w:1, type:3},
    {x:70, y:groundRow-3, w:3, type:2},
    {x:76, y:groundRow-6, w:4, type:2},
    {x:82, y:groundRow-4, w:3, type:2},
    {x:96, y:groundRow-5, w:6, type:2},
    {x:98, y:groundRow-5, w:1, type:3},
    {x:105, y:groundRow-3, w:4, type:2},
    {x:112, y:groundRow-6, w:5, type:2},
    {x:120, y:groundRow-4, w:3, type:2},
    {x:136, y:groundRow-5, w:5, type:2},
    {x:145, y:groundRow-3, w:4, type:2},
    {x:155, y:groundRow-6, w:6, type:2},
    {x:165, y:groundRow-4, w:4, type:2},
    {x:175, y:groundRow-5, w:5, type:2},
  ];

  for (var p = 0; p < platforms.length; p++) {
    var pl = platforms[p];
    for (var px = 0; px < pl.w; px++) {
      var tx = pl.x + px;
      var ty = pl.y;
      if (tx < LEVEL_W && ty >= 0) {
        levelTiles[ty][tx] = pl.type;
      }
    }
  }

  // Stairs near the end
  var stairStart = 170;
  for (var s = 0; s < 7; s++) {
    for (var s2 = 0; s2 <= s; s2++) {
      var stairX = stairStart + s;
      var stairY = groundRow - 1 - s2;
      if (stairX < LEVEL_W && stairY >= 0) {
        levelTiles[stairY][stairX] = 2;
      }
    }
  }

  // ─── Coins ───────────────────────────────────────────────
  var coins = [];
  function addCoin(tx, ty) {
    coins.push({
      x: tx * TILE + 8,
      y: ty * TILE + 6,
      w: 16,
      h: 20,
      collected: false,
      bobOffset: Math.random() * Math.PI * 2
    });
    totalCoins++;
  }

  // Coin placements
  var coinPositions = [
    {x:10,y:groundRow-5},{x:11,y:groundRow-5},{x:12,y:groundRow-5},
    {x:17,y:groundRow-6},{x:18,y:groundRow-6},
    {x:23,y:groundRow-4},{x:24,y:groundRow-4},
    {x:37,y:groundRow-6},  // in question block area
    {x:37,y:groundRow-9},{x:38,y:groundRow-9},
    {x:46,y:groundRow-5},{x:47,y:groundRow-5},
    {x:51,y:groundRow-8},{x:52,y:groundRow-8},
    {x:63,y:groundRow-6},{x:64,y:groundRow-6},{x:65,y:groundRow-6},
    {x:71,y:groundRow-4},{x:72,y:groundRow-4},
    {x:77,y:groundRow-7},{x:78,y:groundRow-7},
    {x:83,y:groundRow-5},{x:84,y:groundRow-5},
    {x:97,y:groundRow-6},{x:98,y:groundRow-6},{x:99,y:groundRow-6},
    {x:106,y:groundRow-4},{x:107,y:groundRow-4},
    {x:113,y:groundRow-7},{x:114,y:groundRow-7},
    {x:121,y:groundRow-5},{x:122,y:groundRow-5},
    {x:137,y:groundRow-6},{x:138,y:groundRow-6},{x:139,y:groundRow-6},
    {x:146,y:groundRow-4},{x:147,y:groundRow-4},
    {x:156,y:groundRow-7},{x:157,y:groundRow-7},{x:158,y:groundRow-7},
    {x:166,y:groundRow-5},{x:167,y:groundRow-5},
    {x:176,y:groundRow-6},{x:177,y:groundRow-6},
  ];
  for (var ci = 0; ci < coinPositions.length; ci++) {
    addCoin(coinPositions[ci].x, coinPositions[ci].y);
  }

  // ─── Goal Flag ───────────────────────────────────────────
  var flagX = 190 * TILE;
  var flagY = groundRow - 8;
  var flagReached = false;

  // ─── Player ──────────────────────────────────────────────
  var player = {
    x: 3 * TILE,
    y: (groundRow - 3) * TILE,
    w: 24,
    h: 30,
    vx: 0,
    vy: 0,
    onGround: false,
    facing: 1,  // 1=right, -1=left
    animFrame: 0,
    animTimer: 0,
    walkCycle: 0
  };

  // ─── Enemies (simple goombas) ────────────────────────────
  var enemies = [];
  function addEnemy(tx, patrol) {
    enemies.push({
      x: tx * TILE,
      y: (groundRow - 1) * TILE - 28,
      w: 28,
      h: 28,
      vx: patrol || 1,
      alive: true,
      squishTimer: 0,
      groundY: (groundRow - 1) * TILE - 28
    });
  }

  var enemyPositions = [
    {x:15, p:1}, {x:25, p:-1}, {x:40, p:1}, {x:48, p:-1},
    {x:65, p:1}, {x:73, p:-1}, {x:88, p:1}, {x:100, p:-1},
    {x:115, p:1}, {x:125, p:-1}, {x:140, p:1}, {x:150, p:-1},
    {x:160, p:1}, {x:170, p:-1}
  ];
  for (var ei = 0; ei < enemyPositions.length; ei++) {
    addEnemy(enemyPositions[ei].x, enemyPositions[ei].p * 1.2);
  }

  // ─── Helpers ─────────────────────────────────────────────
  function getTile(tx, ty) {
    if (tx < 0 || tx >= LEVEL_W || ty < 0 || ty >= LEVEL_H) return 0;
    return levelTiles[ty][tx];
  }

  function isSolid(type) {
    return type === 1 || type === 2 || type === 3 || type === 4 || type === 5 || type === 6;
  }

  function rectCollide(a, b) {
    return a.x < b.x + b.w && a.x + a.w > b.x && a.y < b.y + b.h && a.y + a.h > b.y;
  }

  function spawnParticles(x, y, color, count) {
    for (var i = 0; i < count; i++) {
      particles.push({
        x: x,
        y: y,
        vx: (Math.random() - 0.5) * 6,
        vy: (Math.random() - 0.8) * 8,
        life: 30 + Math.random() * 20,
        maxLife: 50,
        color: color,
        size: 2 + Math.random() * 3
      });
    }
  }

  // ─── Restart ─────────────────────────────────────────────
  function restart() {
    player.x = 3 * TILE;
    player.y = (groundRow - 3) * TILE;
    player.vx = 0;
    player.vy = 0;
    player.onGround = false;
    player.facing = 1;
    score = 0;
    coinsCollected = 0;
    cameraX = 0;
    particles = [];
    deathTimer = 0;
    winTimer = 0;
    flagReached = false;
    state = "playing";
    for (var c = 0; c < coins.length; c++) {
      coins[c].collected = false;
    }
    for (var e = 0; e < enemies.length; e++) {
      var en = enemies[e];
      en.alive = true;
      en.squishTimer = 0;
      en.vx = enemyPositions[e] ? enemyPositions[e].p * 1.2 : 1;
    }
  }

  // ─── Update ──────────────────────────────────────────────
  function update() {
    if (state === "dead") {
      deathTimer--;
      if (deathTimer <= 0) restart();
      updateParticles();
      return;
    }
    if (state === "win") {
      winTimer++;
      updateParticles();
      return;
    }

    frameCount++;

    // ── Player Movement ──
    var moveLeft = keys["ArrowLeft"] || keys["KeyA"];
    var moveRight = keys["ArrowRight"] || keys["KeyD"];
    var jumpKey = keys["Space"] || keys["ArrowUp"] || keys["KeyW"];

    if (moveLeft) {
      player.vx -= 1.8;
      player.facing = -1;
    }
    if (moveRight) {
      player.vx += 1.8;
      player.facing = 1;
    }

    if (!moveLeft && !moveRight) {
      player.vx *= FRICTION;
      if (Math.abs(player.vx) < 0.1) player.vx = 0;
    }

    // Clamp speed
    if (player.vx > MOVE_SPEED) player.vx = MOVE_SPEED;
    if (player.vx < -MOVE_SPEED) player.vx = -MOVE_SPEED;

    // Jump
    if (jumpKey && player.onGround) {
      player.vy = JUMP_FORCE;
      player.onGround = false;
      spawnParticles(player.x + player.w/2, player.y + player.h, "#ddd", 5);
    }

    // Gravity
    player.vy += GRAVITY;
    if (player.vy > MAX_FALL) player.vy = MAX_FALL;

    // ── Horizontal Collision ──
    player.x += player.vx;
    resolveCollisionX(player);

    // ── Vertical Collision ──
    player.y += player.vy;
    player.onGround = false;
    resolveCollisionY(player);

    // Keep player in bounds
    if (player.x < 0) { player.x = 0; player.vx = 0; }

    // Fall death
    if (player.y > H + 100) {
      state = "dead";
      deathTimer = 60;
      spawnParticles(player.x + player.w/2, H, "#f44", 15);
    }

    // ── Animation ──
    if (Math.abs(player.vx) > 0.5) {
      player.walkCycle += Math.abs(player.vx) * 0.08;
      player.animFrame = Math.floor(player.walkCycle) % 4;
    } else {
      player.animFrame = 0;
      player.walkCycle = 0;
    }

    // ── Camera ──
    var targetCam = player.x - W / 3;
    if (targetCam < 0) targetCam = 0;
    var maxCam = LEVEL_W * TILE - W;
    if (targetCam > maxCam) targetCam = maxCam;
    cameraX += (targetCam - cameraX) * 0.1;

    // ── Coins ──
    for (var c = 0; c < coins.length; c++) {
      var coin = coins[c];
      if (coin.collected) continue;
      if (rectCollide(player, coin)) {
        coin.collected = true;
        coinsCollected++;
        score += 10;
        spawnParticles(coin.x + coin.w/2, coin.y + coin.h/2, "#FFD700", 8);
      }
    }

    // ── Enemies ──
    for (var e = 0; e < enemies.length; e++) {
      var en = enemies[e];
      if (!en.alive) {
        if (en.squishTimer > 0) en.squishTimer--;
        continue;
      }

      en.x += en.vx;

      // Enemy-ground collision
      var etx1 = Math.floor(en.x / TILE);
      var etx2 = Math.floor((en.x + en.w) / TILE);
      var ety = Math.floor((en.y + en.h) / TILE);

      // Check if on ground
      var onBlock = false;
      for (var eex = etx1; eex <= etx2; eex++) {
        if (isSolid(getTile(eex, ety))) { onBlock = true; break; }
      }
      if (!onBlock) {
        en.y += 2;
      } else {
        en.y = en.groundY;
      }

      // Wall collision for enemies
      var frontX = en.vx > 0 ? en.x + en.w : en.x;
      var ftx = Math.floor(frontX / TILE);
      var fty = Math.floor((en.y + en.h * 0.5) / TILE);
      if (isSolid(getTile(ftx, fty))) {
        en.vx = -en.vx;
        en.x += en.vx * 2;
      }

      // Edge detection - turn around at gaps
      var belowX = en.vx > 0 ? en.x + en.w + 2 : en.x - 2;
      var btx = Math.floor(belowX / TILE);
      var bty = Math.floor((en.y + en.h + 2) / TILE);
      if (!isSolid(getTile(btx, bty))) {
        en.vx = -en.vx;
      }

      // Player-enemy collision
      if (rectCollide(player, en)) {
        // Stomp from above
        if (player.vy > 0 && player.y + player.h - 8 < en.y + en.h * 0.5) {
          en.alive = false;
          en.squishTimer = 30;
          player.vy = JUMP_FORCE * 0.6;
          score += 20;
          spawnParticles(en.x + en.w/2, en.y + en.h/2, "#8B4513", 8);
        } else {
          // Player dies
          state = "dead";
          deathTimer = 60;
          spawnParticles(player.x + player.w/2, player.y + player.h/2, "#f44", 15);
        }
      }
    }

    // ── Flag / Goal ──
    if (!flagReached && player.x + player.w > flagX && player.x < flagX + 20) {
      flagReached = true;
      state = "win";
      winTimer = 0;
      score += 100;
      spawnParticles(flagX + 10, flagY + 60, "#00FF00", 20);
      spawnParticles(flagX + 10, flagY + 60, "#FFD700", 15);
    }

    updateParticles();
  }

  function resolveCollisionX(obj) {
    var left = Math.floor(obj.x / TILE);
    var right = Math.floor((obj.x + obj.w - 1) / TILE);
    var top = Math.floor(obj.y / TILE);
    var bot = Math.floor((obj.y + obj.h - 1) / TILE);

    for (var ty = top; ty <= bot; ty++) {
      for (var tx = left; tx <= right; tx++) {
        if (isSolid(getTile(tx, ty))) {
          if (obj.vx > 0) {
            obj.x = tx * TILE - obj.w;
            obj.vx = 0;
          } else if (obj.vx < 0) {
            obj.x = (tx + 1) * TILE;
            obj.vx = 0;
          }
        }
      }
    }
  }

  function resolveCollisionY(obj) {
    var left = Math.floor(obj.x / TILE);
    var right = Math.floor((obj.x + obj.w - 1) / TILE);
    var top = Math.floor(obj.y / TILE);
    var bot = Math.floor((obj.y + obj.h - 1) / TILE);

    for (var ty = top; ty <= bot; ty++) {
      for (var tx = left; tx <= right; tx++) {
        if (isSolid(getTile(tx, ty))) {
          if (obj.vy > 0) {
            obj.y = ty * TILE - obj.h;
            obj.vy = 0;
            obj.onGround = true;
          } else if (obj.vy < 0) {
            obj.y = (ty + 1) * TILE;
            obj.vy = 0;
          }
        }
      }
    }
  }

  function updateParticles() {
    for (var i = particles.length - 1; i >= 0; i--) {
      var p = particles[i];
      p.x += p.vx;
      p.y += p.vy;
      p.vy += 0.2;
      p.life--;
      if (p.life <= 0) particles.splice(i, 1);
    }
  }

  // ─── Drawing ─────────────────────────────────────────────
  function draw() {
    // Sky gradient
    var skyGrad = ctx.createLinearGradient(0, 0, 0, H);
    skyGrad.addColorStop(0, "#5C94FC");
    skyGrad.addColorStop(0.7, "#87CEEB");
    skyGrad.addColorStop(1, "#B0E0FF");
    ctx.fillStyle = skyGrad;
    ctx.fillRect(0, 0, W, H);

    // Clouds (parallax)
    drawClouds();

    // Hills (parallax)
    drawHills();

    ctx.save();
    ctx.translate(-Math.round(cameraX), 0);

    // Draw tiles
    drawTiles();

    // Draw coins
    drawCoins();

    // Draw enemies
    drawEnemies();

    // Draw flag
    drawFlag();

    // Draw player
    drawPlayer();

    // Draw particles
    drawParticles();

    ctx.restore();

    // HUD
    drawHUD();

    // Overlays
    if (state === "win") drawWinScreen();
    if (state === "dead") drawDeathScreen();
  }

  function drawClouds() {
    ctx.fillStyle = "rgba(255,255,255,0.8)";
    var cloudPositions = [
      {x:100,y:50,s:1.2},{x:400,y:30,s:0.8},{x:700,y:60,s:1},
      {x:1100,y:40,s:1.3},{x:1500,y:55,s:0.9},{x:1900,y:35,s:1.1},
      {x:2300,y:50,s:0.7},{x:2700,y:45,s:1.2},{x:3100,y:30,s:0.8},
      {x:3500,y:60,s:1},{x:3900,y:40,s:1.3},{x:4300,y:50,s:0.9},
      {x:4700,y:35,s:1.1},{x:5100,y:55,s:0.7},{x:5500,y:45,s:1.2},
    ];
    for (var c = 0; c < cloudPositions.length; c++) {
      var cl = cloudPositions[c];
      var px = cl.x - cameraX * 0.3;
      // Wrap
      while (px < -200) px += 6000;
      while (px > W + 200) px -= 6000;
      drawCloud(px, cl.y, cl.s);
    }
  }

  function drawCloud(x, y, s) {
    ctx.save();
    ctx.translate(x, y);
    ctx.scale(s, s);
    ctx.beginPath();
    ctx.arc(0, 0, 20, 0, Math.PI * 2);
    ctx.arc(22, -5, 25, 0, Math.PI * 2);
    ctx.arc(45, 0, 20, 0, Math.PI * 2);
    ctx.arc(22, 5, 18, 0, Math.PI * 2);
    ctx.fill();
    ctx.restore();
  }

  function drawHills() {
    ctx.fillStyle = "#4AAF4A";
    var hillData = [
      {x:0,w:300,h:80},{x:350,w:200,h:60},{x:600,w:350,h:100},
      {x:1000,w:250,h:70},{x:1300,w:300,h:90},{x:1700,w:200,h:65},
      {x:2000,w:350,h:85},{x:2400,w:250,h:75},{x:2700,w:300,h:95},
      {x:3100,w:200,h:60},{x:3400,w:350,h:100},{x:3800,w:250,h:70},
      {x:4100,w:300,h:80},{x:4500,w:200,h:55},{x:4800,w:350,h:90},
      {x:5200,w:250,h:75},{x:5500,w:300,h:85},
    ];
    for (var h = 0; h < hillData.length; h++) {
      var hill = hillData[h];
      var hx = hill.x - cameraX * 0.5;
      while (hx < -hill.w) hx += 5800;
      while (hx > W + hill.w) hx -= 5800;
      var baseY = groundRow * TILE - cameraY_offset();
      ctx.beginPath();
      ctx.moveTo(hx - hill.w/2, baseY);
      ctx.quadraticCurveTo(hx, baseY - hill.h, hx + hill.w/2, baseY);
      ctx.fill();
    }
  }

  function cameraY_offset() { return 0; }

  function drawTiles() {
    var startCol = Math.floor(cameraX / TILE) - 1;
    var endCol = startCol + Math.ceil(W / TILE) + 2;

    for (var ty = 0; ty < LEVEL_H; ty++) {
      for (var tx = startCol; tx <= endCol && tx < LEVEL_W; tx++) {
        if (tx < 0) continue;
        var t = levelTiles[ty][tx];
        if (t === 0) continue;
        var x = tx * TILE;
        var y = ty * TILE;

        if (t === 1) {
          // Ground block
          ctx.fillStyle = "#8B4513";
          ctx.fillRect(x, y, TILE, TILE);
          ctx.fillStyle = "#6B3410";
          ctx.fillRect(x + 1, y + 1, TILE - 2, TILE - 2);
          ctx.fillStyle = "#228B22";
          ctx.fillRect(x, y, TILE, 4);
          // Brick lines
          ctx.strokeStyle = "#6B3410";
          ctx.lineWidth = 1;
          ctx.strokeRect(x, y, TILE, TILE);
        } else if (t === 2) {
          // Brick
          ctx.fillStyle = "#C84C09";
          ctx.fillRect(x, y, TILE, TILE);
          ctx.strokeStyle = "#8B3000";
          ctx.lineWidth = 1;
          ctx.strokeRect(x, y, TILE, TILE);
          ctx.beginPath();
          ctx.moveTo(x, y + TILE/2);
          ctx.lineTo(x + TILE, y + TILE/2);
          ctx.moveTo(x + TILE/2, y);
          ctx.lineTo(x + TILE/2, y + TILE/2);
          ctx.moveTo(x + TILE/4, y + TILE/2);
          ctx.lineTo(x + TILE/4, y + TILE);
          ctx.moveTo(x + TILE*3/4, y + TILE/2);
          ctx.lineTo(x + TILE*3/4, y + TILE);
          ctx.stroke();
        } else if (t === 3) {
          // Question block
          var pulse = Math.sin(frameCount * 0.08) * 0.15 + 0.85;
          ctx.fillStyle = "#FFB800";
          ctx.fillRect(x, y, TILE, TILE);
          ctx.strokeStyle = "#CC8800";
          ctx.lineWidth = 2;
          ctx.strokeRect(x + 1, y + 1, TILE - 2, TILE - 2);
          ctx.fillStyle = "#FFF";
          ctx.font = "bold 18px Arial";
          ctx.textAlign = "center";
          ctx.textBaseline = "middle";
          ctx.fillText("?", x + TILE/2, y + TILE/2 + 1);
        }
      }
    }
  }

  function drawCoins() {
    for (var c = 0; c < coins.length; c++) {
      var coin = coins[c];
      if (coin.collected) continue;
      var bob = Math.sin(frameCount * 0.06 + coin.bobOffset) * 3;
      var cx = coin.x + coin.w / 2;
      var cy = coin.y + coin.h / 2 + bob;

      // Glow
      ctx.fillStyle = "rgba(255,215,0,0.3)";
      ctx.beginPath();
      ctx.arc(cx, cy, 12, 0, Math.PI * 2);
      ctx.fill();

      // Coin body
      var scaleX = Math.abs(Math.cos(frameCount * 0.05 + coin.bobOffset));
      ctx.save();
      ctx.translate(cx, cy);
      ctx.scale(scaleX, 1);
      ctx.fillStyle = "#FFD700";
      ctx.beginPath();
      ctx.arc(0, 0, 8, 0, Math.PI * 2);
      ctx.fill();
      ctx.strokeStyle = "#DAA520";
      ctx.lineWidth = 1.5;
      ctx.stroke();
      ctx.fillStyle = "#DAA520";
      ctx.font = "bold 10px Arial";
      ctx.textAlign = "center";
      ctx.textBaseline = "middle";
      ctx.fillText("$", 0, 1);
      ctx.restore();
    }
  }

  function drawEnemies() {
    for (var e = 0; e < enemies.length; e++) {
      var en = enemies[e];
      if (!en.alive && en.squishTimer <= 0) continue;

      var ex = en.x;
      var ey = en.y;

      if (!en.alive) {
        // Squished
        ctx.fillStyle = "#8B4513";
        ctx.fillRect(ex, ey + en.h - 6, en.w, 6);
        continue;
      }

      // Body
      ctx.fillStyle = "#8B4513";
      ctx.beginPath();
      ctx.ellipse(ex + en.w/2, ey + en.h * 0.4, en.w * 0.45, en.h * 0.4, 0, 0, Math.PI * 2);
      ctx.fill();

      // Feet
      var footOffset = Math.sin(frameCount * 0.15) * 2;
      ctx.fillStyle = "#333";
      ctx.fillRect(ex + 2, ey + en.h - 8 + footOffset, 8, 8);
      ctx.fillRect(ex + en.w - 10, ey + en.h - 8 - footOffset, 8, 8);

      // Eyes
      ctx.fillStyle = "#FFF";
      ctx.beginPath();
      ctx.arc(ex + en.w * 0.35, ey + en.h * 0.35, 4, 0, Math.PI * 2);
      ctx.arc(ex + en.w * 0.65, ey + en.h * 0.35, 4, 0, Math.PI * 2);
      ctx.fill();
      ctx.fillStyle = "#000";
      ctx.beginPath();
      ctx.arc(ex + en.w * 0.35 + (en.vx > 0 ? 1.5 : -1.5), ey + en.h * 0.35, 2, 0, Math.PI * 2);
      ctx.arc(ex + en.w * 0.65 + (en.vx > 0 ? 1.5 : -1.5), ey + en.h * 0.35, 2, 0, Math.PI * 2);
      ctx.fill();

      // Angry eyebrows
      ctx.strokeStyle = "#000";
      ctx.lineWidth = 2;
      ctx.beginPath();
      ctx.moveTo(ex + en.w * 0.25, ey + en.h * 0.2);
      ctx.lineTo(ex + en.w * 0.45, ey + en.h * 0.25);
      ctx.moveTo(ex + en.w * 0.75, ey + en.h * 0.2);
      ctx.lineTo(ex + en.w * 0.55, ey + en.h * 0.25);
      ctx.stroke();
    }
  }

  function drawFlag() {
    var fx = flagX;
    var fy = flagY;

    // Pole
    ctx.fillStyle = "#888";
    ctx.fillRect(fx + 8, fy, 4, (groundRow - flagY) * TILE);

    // Ball on top
    ctx.fillStyle = "#FFD700";
    ctx.beginPath();
    ctx.arc(fx + 10, fy, 6, 0, Math.PI * 2);
    ctx.fill();

    // Flag
    var wave = Math.sin(frameCount * 0.08) * 3;
    ctx.fillStyle = "#FF3333";
    ctx.beginPath();
    ctx.moveTo(fx + 12, fy + 5);
    ctx.lineTo(fx + 45 + wave, fy + 15);
    ctx.lineTo(fx + 12, fy + 35);
    ctx.closePath();
    ctx.fill();

    // Star on flag
    ctx.fillStyle = "#FFD700";
    ctx.font = "14px Arial";
    ctx.textAlign = "center";
    ctx.fillText("★", fx + 28 + wave * 0.5, fy + 24);

    // "GOAL" text
    ctx.fillStyle = "#FFF";
    ctx.font = "bold 11px Arial";
    ctx.textAlign = "center";
    ctx.fillText("GOAL", fx + 10, fy - 8);
  }

  function drawPlayer() {
    if (state === "dead") return;

    var px = player.x;
    var py = player.y;
    var f = player.facing;

    ctx.save();
    ctx.translate(px + player.w / 2, py + player.h / 2);
    ctx.scale(f, 1);
    ctx.translate(-player.w / 2, -player.h / 2);

    // Hat
    ctx.fillStyle = "#E52521";
    ctx.fillRect(2, 0, 20, 8);
    ctx.fillRect(0, 2, 24, 6);

    // Face
    ctx.fillStyle = "#FBBE7C";
    ctx.fillRect(2, 8, 20, 10);

    // Eyes
    ctx.fillStyle = "#000";
    ctx.fillRect(14, 10, 3, 3);

    // Mustache
    ctx.fillStyle = "#4A2800";
    ctx.fillRect(10, 14, 12, 2);

    // Body (overalls)
    ctx.fillStyle = "#0055D4";
    ctx.fillRect(4, 18, 16, 8);

    // Overall buttons
    ctx.fillStyle = "#FFD700";
    ctx.fillRect(8, 19, 2, 2);
    ctx.fillRect(14, 19, 2, 2);

    // Legs
    var legAnim = player.onGround ? Math.sin(player.walkCycle * 0.8) * 3 : 2;
    ctx.fillStyle = "#0055D4";
    if (player.onGround && Math.abs(player.vx) > 0.5) {
      ctx.fillRect(3, 26, 7, 4 + legAnim);
      ctx.fillRect(14, 26, 7, 4 - legAnim);
    } else {
      ctx.fillRect(3, 26, 7, 4);
      ctx.fillRect(14, 26, 7, 4);
    }

    // Shoes
    ctx.fillStyle = "#8B4513";
    ctx.fillRect(1, 29, 9, 3);
    ctx.fillRect(14, 29, 9, 3);

    // Arm
    ctx.fillStyle = "#FBBE7C";
    var armY = player.onGround ? 18 + Math.sin(player.walkCycle * 0.8) * 2 : 14;
    ctx.fillRect(18, armY, 5, 8);

    ctx.restore();
  }

  function drawParticles() {
    for (var i = 0; i < particles.length; i++) {
      var p = particles[i];
      var alpha = p.life / p.maxLife;
      ctx.globalAlpha = alpha;
      ctx.fillStyle = p.color;
      ctx.fillRect(p.x - p.size/2, p.y - p.size/2, p.size, p.size);
    }
    ctx.globalAlpha = 1;
  }

  function drawHUD() {
    // Score background
    ctx.fillStyle = "rgba(0,0,0,0.4)";
    ctx.fillRect(10, 8, 200, 36);
    ctx.strokeStyle = "rgba(255,255,255,0.3)";
    ctx.lineWidth = 1;
    ctx.strokeRect(10, 8, 200, 36);

    // Coins
    ctx.fillStyle = "#FFD700";
    ctx.font = "bold 16px 'Segoe UI', Arial";
    ctx.textAlign = "left";
    ctx.textBaseline = "middle";
    ctx.fillText("🪙 " + coinsCollected + " / " + totalCoins, 20, 22);

    // Score
    ctx.fillStyle = "#FFF";
    ctx.font = "bold 14px 'Segoe UI', Arial";
    ctx.fillText("Score: " + score, 140, 22);
  }

  function drawWinScreen() {
    ctx.fillStyle = "rgba(0,0,0,0.6)";
    ctx.fillRect(0, 0, W, H);

    var bounce = Math.min(winTimer * 0.05, 1);
    var scale = 0.5 + bounce * 0.5;

    ctx.save();
    ctx.translate(W/2, H/2 - 20);
    ctx.scale(scale, scale);

    ctx.fillStyle = "#FFD700";
    ctx.font = "bold 48px 'Segoe UI', Arial";
    ctx.textAlign = "center";
    ctx.textBaseline = "middle";
    ctx.fillText("🏆 YOU WIN! 🏆", 0, -40);

    ctx.fillStyle = "#FFF";
    ctx.font = "24px 'Segoe UI', Arial";
    ctx.fillText("Score: " + score, 0, 20);
    ctx.fillText("Coins: " + coinsCollected + " / " + totalCoins, 0, 55);

    if (winTimer > 60) {
      ctx.fillStyle = "#AAA";
      ctx.font = "16px 'Segoe UI', Arial";
      ctx.fillText("Press R to play again", 0, 100);
    }

    ctx.restore();
  }

  function drawDeathScreen() {
    ctx.fillStyle = "rgba(0,0,0,0.5)";
    ctx.fillRect(0, 0, W, H);

    ctx.fillStyle = "#FF4444";
    ctx.font = "bold 42px 'Segoe UI', Arial";
    ctx.textAlign = "center";
    ctx.textBaseline = "middle";
    ctx.fillText("GAME OVER", W/2, H/2 - 20);

    ctx.fillStyle = "#FFF";
    ctx.font = "18px 'Segoe UI', Arial";
    ctx.fillText("Press R to try again", W/2, H/2 + 30);
  }

  // ─── Game Loop ───────────────────────────────────────────
  function gameLoop() {
    update();
    draw();
    requestAnimationFrame(gameLoop);
  }

  // Start
  gameLoop();

})();
</script>
</body>
</html>