← run 2026-06-30T130601Z-3ea440b-minimal

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

rep 1 UNGRADED

pi 130.8s · verify 4.1s · 67622 tok · responded: qwen/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 Pixel Adventure — a compact Mario-style platformer
   Plain vanilla JS, 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

  // ---- constants -------------------------------------------------------
  const GRAVITY       = 0.55;
  const JUMP_FORCE    = -11;
  const MOVE_SPEED    = 4.2;
  const FRICTION      = 0.82;
  const MAX_FALL      = 12;
  const TILE          = 32;          // base tile size
  const PLAYER_W      = 24;
  const PLAYER_H      = 32;

  // ---- input tracking --------------------------------------------------
  const keys = {};
  window.addEventListener('keydown', 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', 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']; }

  // ---- camera ------------------------------------------------------------
  const camera = { x: 0 };

  // ---- level data --------------------------------------------------------
  // ground segments: {x, y, w}  (in world coords)
  const groundSegments = [
    { x: 0,     y: H - TILE,       w: 1200 },
    { x: 1320,  y: H - TILE,       w: 800 },
    { x: 2240,  y: H - TILE,       w: 400 },
    { x: 2720,  y: H - TILE,       w: 600 },
    { x: 3400,  y: H - TILE,       w: 1200 },
    { x: 4700,  y: H - TILE,       w: 1500 },
  ];

  // platforms: { x, y, w, type }  type: 'normal'|'brick'|'question'
  const platforms = [
    { x: 400,  y: 320, w: 96,  type: 'normal' },
    { x: 560,  y: 256, w: 64,  type: 'normal' },
    { x: 700,  y: 192, w: 128, type: 'brick' },
    { x: 960,  y: 288, w: 96,  type: 'question' },
    { x: 1080, y: 224, w: 64,  type: 'normal' },
    { x: 1500, y: 300, w: 96,  type: 'normal' },
    { x: 1640, y: 240, w: 128, type: 'brick' },
    { x: 1800, y: 180, w: 64,  type: 'normal' },
    { x: 2000, y: 260, w: 96,  type: 'question' },
    { x: 2300, y: 320, w: 128, type: 'normal' },
    { x: 2500, y: 240, w: 96,  type: 'normal' },
    { x: 2680, y: 180, w: 64,  type: 'normal' },
    { x: 3000, y: 280, w: 96,  type: 'brick' },
    { x: 3160, y: 220, w: 64,  type: 'normal' },
    { x: 3320, y: 280, w: 96,  type: 'question' },
    { x: 3600, y: 300, w: 128, type: 'normal' },
    { x: 3800, y: 240, w: 64,  type: 'normal' },
    { x: 3960, y: 180, w: 128, type: 'brick' },
    { x: 4200, y: 280, w: 96,  type: 'normal' },
    { x: 4400, y: 220, w: 64,  type: 'normal' },
    { x: 4600, y: 300, w: 96,  type: 'question' },
    { x: 4900, y: 260, w: 128, type: 'normal' },
    { x: 5100, y: 200, w: 96,  type: 'normal' },
  ];

  // coins
  const coins = [];
  const coinPositions = [
    // near start
    420, 460, 500,
    580, 620,
    720, 760, 800,
    980, 1000,
    1100,
    // mid section
    1520, 1560, 1600,
    1660, 1700, 1740,
    1820,
    2020, 2040,
    // section 3
    2320, 2360, 2400,
    2520, 2560,
    2700,
    // section 4
    3020, 3060,
    3180,
    3340, 3360, 3380,
    // final stretch
    3620, 3660, 3700, 3740,
    3820, 3860,
    3980, 4020, 4060, 4100,
    4220, 4260,
    4420,
    4620, 4640,
    // climb to flag
    4920, 4960,
    5120, 5160,
  ];

  // World y position for each coin (above various platforms)
  const coinYs = {};
  function initCoins() {
    coinPositions.forEach((cx, i) => {
      // place coins at various heights; check if near a platform
      let cy = 300; // default height
      for (const p of platforms) {
        if (cx >= p.x - 16 && cx <= p.x + p.w + 16) {
          cy = p.y - 28;
          break;
        }
      }
      // some coins in the air
      if (i % 5 === 2) cy -= 40;
      if (i % 7 === 4) cy -= 60;
      coins.push({ x: cx, y: cy, collected: false, anim: Math.random() * Math.PI * 2 });
    });
  }

  // enemies (simple goombas that patrol)
  const enemies = [];
  function initEnemies() {
    const enemyData = [
      { x: 600,  patrol: 120 },
      { x: 1100, patrol: 100 },
      { x: 1560, patrol: 140 },
      { x: 2050, patrol: 100 },
      { x: 2400, patrol: 120 },
      { x: 3100, patrol: 140 },
      { x: 3700, patrol: 160 },
      { x: 4300, patrol: 100 },
      { x: 5000, patrol: 120 },
    ];
    for (const d of enemyData) {
      // find ground height at this x
      let groundY = H - TILE;
      for (const g of groundSegments) {
        if (d.x >= g.x && d.x <= g.x + g.w) {
          groundY = g.y;
          break;
        }
      }
      // check platform
      for (const p of platforms) {
        if (d.x >= p.x && d.x <= p.x + p.w && groundY > p.y + TILE) {
          groundY = p.y;
        }
      }
      enemies.push({
        x: d.x, y: groundY - 28,
        w: 28, h: 28,
        vx: 1.2,
        patrolLeft: d.x - d.patrol,
        patrolRight: d.x + d.patrol,
        alive: true,
        squishTimer: 0,
      });
    }
  }

  // flag / goal
  const flag = { x: 5600, y: H - TILE - 160, reached: false };

  // particles
  const particles = [];

  // background elements (clouds, hills, bushes)
  const clouds = [];
  const hills = [];
  const bushes = [];
  function initBG() {
    for (let i = 0; i < 30; i++) {
      clouds.push({
        x: i * 300 + Math.random() * 200,
        y: 30 + Math.random() * 80,
        w: 60 + Math.random() * 80,
        h: 20 + Math.random() * 16,
      });
    }
    for (let i = 0; i < 20; i++) {
      hills.push({
        x: i * 500 + Math.random() * 200,
        w: 150 + Math.random() * 200,
        h: 60 + Math.random() * 80,
      });
    }
    for (let i = 0; i < 25; i++) {
      bushes.push({
        x: i * 350 + Math.random() * 200,
        y: H - TILE,
        w: 40 + Math.random() * 40,
        h: 16 + Math.random() * 12,
      });
    }
  }

  // ---- player -------------------------------------------------------------
  let player = {};
  function resetPlayer() {
    player = {
      x: 80, y: H - TILE - PLAYER_H - 2,
      w: PLAYER_W, h: PLAYER_H,
      vx: 0, vy: 0,
      onGround: false,
      facing: 1,   // 1 = right, -1 = left
      frame: 0,
      frameTimer: 0,
      jumpPressed: false,
      dead: false,
    };
  }

  // ---- game state ---------------------------------------------------------
  let score = 0;
  let totalCoins = coins.length;
  let gameState = 'playing'; // 'playing' | 'won' | 'dead'
  let deathTimer = 0;
  let winTimer = 0;

  // ---- helpers ------------------------------------------------------------
  function aabb(ax, ay, aw, ah, bx, by, bw, bh) {
    return ax < bx + bw && ax + aw > bx && ay < by + bh && ay + ah > by;
  }

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

  // ---- init ----------------------------------------------------------------
  function init() {
    initCoins();
    initEnemies();
    initBG();
    resetPlayer();
    score = 0;
    gameState = 'playing';
    particles.length = 0;
  }

  // ---- collision resolution -----------------------------------------------
  function collideX() {
    const p = player;
    // ground
    for (const g of groundSegments) {
      if (aabb(p.x, p.y, p.w, p.h, g.x, g.y, g.w, TILE)) {
        if (p.vx > 0) { p.x = g.x - p.w; }
        else if (p.vx < 0) { p.x = g.x + g.w; }
        p.vx = 0;
      }
    }
    // platforms
    for (const pl of platforms) {
      if (aabb(p.x, p.y, p.w, p.h, pl.x, pl.y, pl.w, TILE)) {
        if (p.vx > 0) { p.x = pl.x - p.w; }
        else if (p.vx < 0) { p.x = pl.x + pl.w; }
        p.vx = 0;
      }
    }
  }

  function collideY() {
    const p = player;
    p.onGround = false;

    // ground
    for (const g of groundSegments) {
      if (aabb(p.x, p.y, p.w, p.h, g.x, g.y, g.w, TILE)) {
        if (p.vy > 0) {
          p.y = g.y - p.h;
          p.vy = 0;
          p.onGround = true;
        } else if (p.vy < 0) {
          p.y = g.y + TILE;
          p.vy = 0;
        }
      }
    }

    // platforms
    for (const pl of platforms) {
      if (aabb(p.x, p.y, p.w, p.h, pl.x, pl.y, pl.w, TILE)) {
        if (p.vy > 0) {
          p.y = pl.y - p.h;
          p.vy = 0;
          p.onGround = true;
        } else if (p.vy < 0) {
          p.y = pl.y + TILE;
          p.vy = 0;
        }
      }
    }
  }

  // ---- update ---------------------------------------------------------------
  function update() {
    if (gameState === 'dead') {
      deathTimer--;
      if (deathTimer <= 0) init();
      return;
    }
    if (gameState === 'won') {
      winTimer--;
      return;
    }

    const p = player;

    // movement
    if (isLeft())  { p.vx -= 1.2; p.facing = -1; }
    if (isRight()) { p.vx += 1.2; p.facing = 1;  }

    // limit horizontal speed
    if (p.vx > MOVE_SPEED)  p.vx = MOVE_SPEED;
    if (p.vx < -MOVE_SPEED) p.vx = -MOVE_SPEED;

    // friction when no input
    if (!isLeft() && !isRight()) {
      p.vx *= FRICTION;
      if (Math.abs(p.vx) < 0.1) p.vx = 0;
    }

    // jump
    if (isJump() && p.onGround && !p.jumpPressed) {
      p.vy = JUMP_FORCE;
      p.onGround = false;
      p.jumpPressed = true;
    }
    if (!isJump()) p.jumpPressed = false;

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

    // move
    p.x += p.vx;
    p.y += p.vy;

    // collisions
    collideX();
    collideY();

    // animation frame
    p.frameTimer++;
    if (Math.abs(p.vx) > 0.5) {
      if (p.frameTimer % 8 === 0) p.frame = (p.frame + 1) % 4;
    } else {
      p.frame = 0;
    }

    // camera follow
    const targetCam = p.x - W / 3;
    camera.x += (targetCam - camera.x) * 0.1;
    if (camera.x < 0) camera.x = 0;

    // collect coins
    for (const c of coins) {
      if (c.collected) continue;
      c.anim += 0.08;
      if (aabb(p.x, p.y, p.w, p.h, c.x - 10, c.y - 10, 20, 20)) {
        c.collected = true;
        score += 100;
        addParticle(c.x, c.y, '#FFD700', 8);
      }
    }

    // enemy update & collision
    for (const e of enemies) {
      if (!e.alive) {
        e.squishTimer--;
        continue;
      }
      e.x += e.vx;
      if (e.x <= e.patrolLeft || e.x >= e.patrolRight) {
        e.vx *= -1;
      }
      // collision with player
      if (aabb(p.x, p.y, p.w, p.h, e.x, e.y, e.w, e.h)) {
        // stomp from above
        if (p.vy > 0 && p.y + p.h - 6 < e.y + e.h / 2) {
          e.alive = false;
          e.squishTimer = 30;
          p.vy = JUMP_FORCE * 0.6;
          score += 200;
          addParticle(e.x + e.w / 2, e.y + e.h / 2, '#AA4400', 10);
        } else {
          // player dies
          p.dead = true;
          gameState = 'dead';
          deathTimer = 90;
          addParticle(p.x + p.w / 2, p.y + p.h / 2, '#FF0000', 15);
        }
      }
    }

    // check goal
    if (!flag.reached && aabb(p.x, p.y, p.w, p.h, flag.x - 8, flag.y, 16, flag.h)) {
      flag.reached = true;
      gameState = 'won';
      winTimer = 180;
      addParticle(flag.x, flag.y, '#00FF00', 25);
      score += 1000;
    }

    // fall into pit
    if (p.y > H + 100) {
      gameState = 'dead';
      deathTimer = 90;
    }

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

  // ---- drawing helpers ----------------------------------------------------
  function drawSky() {
    const grad = ctx.createLinearGradient(0, 0, 0, H);
    grad.addColorStop(0, '#5C94FC');
    grad.addColorStop(0.7, '#87CEEB');
    grad.addColorStop(1, '#B0E0FF');
    ctx.fillStyle = grad;
    ctx.fillRect(0, 0, W, H);
  }

  function drawClouds() {
    ctx.fillStyle = 'rgba(255,255,255,0.9)';
    for (const c of clouds) {
      const sx = c.x - camera.x * 0.3;
      if (sx < -c.w || sx > W + c.w) continue;
      ctx.beginPath();
      ctx.ellipse(sx, c.y, c.w / 2, c.h / 2, 0, 0, Math.PI * 2);
      ctx.fill();
      ctx.beginPath();
      ctx.ellipse(sx - c.w * 0.25, c.y + 4, c.w * 0.3, c.h * 0.4, 0, 0, Math.PI * 2);
      ctx.fill();
      ctx.beginPath();
      ctx.ellipse(sx + c.w * 0.25, c.y + 4, c.w * 0.3, c.h * 0.4, 0, 0, Math.PI * 2);
      ctx.fill();
    }
  }

  function drawHills() {
    for (const h of hills) {
      const sx = h.x - camera.x * 0.5;
      if (sx < -h.w || sx > W + h.w) continue;
      const baseY = H - TILE + 4;
      ctx.fillStyle = '#4A8C3F';
      ctx.beginPath();
      ctx.ellipse(sx, baseY, h.w / 2, h.h, 0, Math.PI, 0);
      ctx.fill();
      ctx.fillStyle = '#5BA04A';
      ctx.beginPath();
      ctx.ellipse(sx, baseY, h.w / 3, h.h * 0.8, 0, Math.PI, 0);
      ctx.fill();
    }
  }

  function drawBushes() {
    for (const b of bushes) {
      const sx = b.x - camera.x;
      if (sx < -b.w || sx > W + b.w) continue;
      const by = b.y + 2;
      ctx.fillStyle = '#3A7A30';
      ctx.beginPath();
      ctx.ellipse(sx, by, b.w / 2, b.h / 2, 0, Math.PI, 0);
      ctx.fill();
      ctx.fillStyle = '#4A9A40';
      ctx.beginPath();
      ctx.ellipse(sx + 4, by - 2, b.w / 3, b.h / 2.5, 0, Math.PI, 0);
      ctx.fill();
    }
  }

  function drawGround() {
    for (const g of groundSegments) {
      const sx = g.x - camera.x;
      if (sx > W + 10 || sx + g.w < -10) continue;
      // dirt
      ctx.fillStyle = '#C84C09';
      ctx.fillRect(sx, g.y, g.w, TILE);
      // grass top
      ctx.fillStyle = '#4AAF3A';
      ctx.fillRect(sx, g.y, g.w, 8);
      // grass details
      ctx.fillStyle = '#5CC848';
      for (let gx = 0; gx < g.w; gx += 16) {
        ctx.fillRect(sx + gx, g.y, 4, 6);
      }
      // dirt texture
      ctx.fillStyle = '#A03800';
      for (let dy = 12; dy < TILE; dy += 10) {
        for (let dx = 4 + ((dy / 10) | 0) * 8; dx < g.w; dx += 16) {
          ctx.fillRect(sx + dx, g.y + dy, 6, 4);
        }
      }
    }
  }

  function drawPlatforms() {
    for (const pl of platforms) {
      const sx = pl.x - camera.x;
      if (sx > W + 10 || sx + pl.w < -10) continue;
      if (pl.type === 'question') {
        ctx.fillStyle = '#F8B800';
        ctx.fillRect(sx, pl.y, pl.w, TILE);
        ctx.fillStyle = '#DAA520';
        ctx.fillRect(sx + 2, pl.y + 2, pl.w - 4, TILE - 4);
        ctx.fillStyle = '#FFF';
        ctx.font = 'bold 18px monospace';
        ctx.textAlign = 'center';
        ctx.fillText('?', sx + pl.w / 2, pl.y + TILE - 8);
      } else if (pl.type === 'brick') {
        ctx.fillStyle = '#C84C09';
        ctx.fillRect(sx, pl.y, pl.w, TILE);
        ctx.fillStyle = '#E09060';
        for (let bx = 0; bx < pl.w; bx += 16) {
          ctx.fillRect(sx + bx + 1, pl.y + 1, 14, 14);
          ctx.fillRect(sx + bx + 1, pl.y + 17, 14, 14);
          ctx.fillStyle = '#A03800';
          ctx.fillRect(sx + bx + 1, pl.y + 15, 14, 2);
          ctx.fillStyle = '#E09060';
        }
      } else {
        ctx.fillStyle = '#8B6914';
        ctx.fillRect(sx, pl.y, pl.w, TILE);
        ctx.fillStyle = '#A07828';
        ctx.fillRect(sx + 1, pl.y + 1, pl.w - 2, TILE - 2);
        ctx.fillStyle = '#6B4F10';
        ctx.fillRect(sx, pl.y + TILE - 2, pl.w, 2);
        ctx.fillRect(sx + pl.w - 2, pl.y, 2, TILE);
      }
    }
  }

  function drawCoins() {
    for (const c of coins) {
      if (c.collected) continue;
      const sx = c.x - camera.x;
      if (sx < -20 || sx > W + 20) continue;
      const bounce = Math.sin(c.anim) * 3;
      const stretch = 0.6 + 0.4 * Math.cos(c.anim * 0.8);
      const cy = c.y + bounce;
      // coin body
      ctx.fillStyle = '#FFD700';
      ctx.beginPath();
      ctx.ellipse(sx, cy, 8 * stretch, 8, 0, 0, Math.PI * 2);
      ctx.fill();
      ctx.fillStyle = '#FFC000';
      ctx.beginPath();
      ctx.ellipse(sx, cy, 5 * stretch, 5, 0, 0, Math.PI * 2);
      ctx.fill();
      // shine
      ctx.fillStyle = '#FFF';
      ctx.beginPath();
      ctx.ellipse(sx - 2 * stretch, cy - 3, 2, 2, 0, 0, Math.PI * 2);
      ctx.fill();
    }
  }

  function drawEnemies() {
    for (const e of enemies) {
      if (!e.alive && e.squishTimer <= 0) continue;
      const sx = e.x - camera.x;
      if (sx < -40 || sx > W + 40) continue;

      if (!e.alive) {
        // squished
        ctx.fillStyle = '#8B4513';
        ctx.fillRect(sx, e.y + e.h - 8, e.w, 8);
        continue;
      }

      // body
      ctx.fillStyle = '#8B4513';
      ctx.beginPath();
      ctx.ellipse(sx + e.w / 2, e.y + e.h / 2 + 2, e.w / 2, e.h / 2 - 2, 0, 0, Math.PI * 2);
      ctx.fill();
      // feet
      const walk = Math.sin(Date.now() / 150) * 2;
      ctx.fillStyle = '#4A2008';
      ctx.fillRect(sx + 2 + walk, e.y + e.h - 6, 10, 6);
      ctx.fillRect(sx + e.w - 12 - walk, e.y + e.h - 6, 10, 6);
      // eyes
      ctx.fillStyle = '#FFF';
      ctx.fillRect(sx + 6, e.y + 6, 6, 7);
      ctx.fillRect(sx + e.w - 12, e.y + 6, 6, 7);
      ctx.fillStyle = '#000';
      const ex = e.vx > 0 ? 2 : 0;
      ctx.fillRect(sx + 7 + ex, e.y + 8, 3, 4);
      ctx.fillRect(sx + e.w - 11 + ex, e.y + 8, 3, 4);
      // eyebrows (angry)
      ctx.fillStyle = '#000';
      ctx.fillRect(sx + 5, e.y + 4, 8, 2);
      ctx.fillRect(sx + e.w - 13, e.y + 4, 8, 2);
    }
  }

  function drawFlag() {
    const sx = flag.x - camera.x;
    if (sx < -60 || sx > W + 60) return;
    // pole
    ctx.fillStyle = '#888';
    ctx.fillRect(sx - 2, flag.y, 4, flag.h);
    // ball on top
    ctx.fillStyle = '#FFD700';
    ctx.beginPath();
    ctx.arc(sx, flag.y, 6, 0, Math.PI * 2);
    ctx.fill();
    // flag
    const wave = Math.sin(Date.now() / 300) * 3;
    ctx.fillStyle = '#FF3333';
    ctx.beginPath();
    ctx.moveTo(sx + 2, flag.y + 4);
    ctx.lineTo(sx + 36 + wave, flag.y + 14);
    ctx.lineTo(sx + 2, flag.y + 28);
    ctx.closePath();
    ctx.fill();
    // star on flag
    ctx.fillStyle = '#FFD700';
    ctx.font = '12px monospace';
    ctx.fillText('★', sx + 10 + wave * 0.5, flag.y + 20);
    // "GOAL" text
    ctx.fillStyle = '#FFF';
    ctx.font = 'bold 10px monospace';
    ctx.textAlign = 'center';
    ctx.fillText('GOAL', sx, flag.y - 8);
    ctx.textAlign = 'left';
  }

  function drawPlayer() {
    if (gameState === 'dead') return;
    const p = player;
    const sx = p.x - camera.x;
    const sy = p.y;
    const f = p.facing;

    ctx.save();
    ctx.translate(sx + p.w / 2, sy + p.h);
    if (f === -1) ctx.scale(-1, 1);

    // body
    ctx.fillStyle = '#E03020';          // red shirt
    ctx.fillRect(-p.w / 2, -p.h + 8, p.w, p.h - 16);
    // overalls (blue)
    ctx.fillStyle = '#2040C0';
    ctx.fillRect(-p.w / 2 + 2, -12, p.w - 4, 10);
    // overall straps
    ctx.fillStyle = '#2040C0';
    ctx.fillRect(-p.w / 2 + 2, -p.h + 14, 5, 8);
    ctx.fillRect(p.w / 2 - 7, -p.h + 14, 5, 8);
    // overalls buttons
    ctx.fillStyle = '#FFD700';
    ctx.fillRect(-p.w / 2 + 3, -8, 3, 3);
    ctx.fillRect(p.w / 2 - 6, -8, 3, 3);

    // legs
    const legAnim = p.onGround ? Math.sin(p.frame * Math.PI / 2) * 4 : 2;
    ctx.fillStyle = '#2040C0';
    ctx.fillRect(-p.w / 2 + 1, -10, 9, 10 + legAnim);
    ctx.fillRect(p.w / 2 - 10, -10, 9, 10 - legAnim);

    // shoes
    ctx.fillStyle = '#6B3300';
    ctx.fillRect(-p.w / 2 - 1, -1 + legAnim, 11, 4);
    ctx.fillRect(p.w / 2 - 10, -1 - legAnim, 11, 4);

    // head
    ctx.fillStyle = '#FFCC88';
    ctx.fillRect(-p.w / 2 + 2, -p.h + 2, p.w - 4, 12);

    // hair/hat
    ctx.fillStyle = '#E03020';
    ctx.fillRect(-p.w / 2 + 1, -p.h, p.w - 2, 6);
    ctx.fillRect(-p.w / 2 + 3, -p.h + 1, p.w + 4, 4);

    // hat brim
    ctx.fillStyle = '#E03020';
    ctx.fillRect(-p.w / 2 + 2, -p.h + 5, p.w + 4, 3);

    // face
    ctx.fillStyle = '#FFCC88';
    ctx.fillRect(-p.w / 2 + 3, -p.h + 8, p.w - 6, 10);

    // eyes
    ctx.fillStyle = '#000';
    ctx.fillRect(-p.w / 2 + 5, -p.h + 10, 3, 4);
    ctx.fillRect(-p.w / 2 + 13, -p.h + 10, 3, 4);

    // mustache
    ctx.fillStyle = '#4A2800';
    ctx.fillRect(-p.w / 2 + 4, -p.h + 15, 8, 2);
    ctx.fillRect(-p.w / 2 + 10, -p.h + 15, 8, 2);

    // arm
    ctx.fillStyle = '#FFCC88';
    const armSwing = p.onGround ? Math.sin(p.frame * Math.PI / 2) * 3 : -2;
    ctx.fillRect(p.w / 2 - 2, -p.h + 16 + armSwing, 5, 8);

    ctx.restore();
  }

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

  function drawHUD() {
    // score background
    ctx.fillStyle = 'rgba(0,0,0,0.5)';
    ctx.fillRect(8, 8, 180, 36);
    ctx.strokeStyle = '#FFF';
    ctx.lineWidth = 1;
    ctx.strokeRect(8, 8, 180, 36);

    // coins
    ctx.fillStyle = '#FFD700';
    ctx.font = 'bold 16px monospace';
    ctx.fillText('🪙 × ' + score, 16, 31);

    // coins remaining
    const remaining = coins.filter(c => !c.collected).length;
    ctx.fillStyle = 'rgba(0,0,0,0.5)';
    ctx.fillRect(W - 160, 8, 152, 36);
    ctx.strokeStyle = '#FFF';
    ctx.strokeRect(W - 160, 8, 152, 36);
    ctx.fillStyle = '#FFF';
    ctx.textAlign = 'right';
    ctx.fillText('Coins: ' + (totalCoins - remaining) + '/' + totalCoins, W - 16, 31);
    ctx.textAlign = 'left';
  }

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

    ctx.fillStyle = '#FFD700';
    ctx.font = 'bold 48px monospace';
    ctx.textAlign = 'center';
    ctx.fillText('🎉 YOU WIN! 🎉', W / 2, H / 2 - 40);

    ctx.fillStyle = '#FFF';
    ctx.font = '24px monospace';
    ctx.fillText('Score: ' + score, W / 2, H / 2 + 10);
    ctx.fillText('Coins collected: ' + (totalCoins - coins.filter(c => !c.collected).length) + '/' + totalCoins, W / 2, H / 2 + 45);

    ctx.font = '16px monospace';
    ctx.fillStyle = '#AAA';
    ctx.fillText('Press SPACE to play again', W / 2, H / 2 + 90);

    ctx.textAlign = 'left';

    if (keys['Space']) {
      keys['Space'] = false;
      init();
    }
  }

  function drawDeathScreen() {
    ctx.fillStyle = 'rgba(100,0,0,0.4)';
    ctx.fillRect(0, 0, W, H);

    ctx.fillStyle = '#FFF';
    ctx.font = 'bold 36px monospace';
    ctx.textAlign = 'center';
    ctx.fillText('GAME OVER', W / 2, H / 2);
    ctx.font = '16px monospace';
    ctx.fillStyle = '#CCC';
    ctx.fillText('Restarting...', W / 2, H / 2 + 30);
    ctx.textAlign = 'left';
  }

  function drawStartHint() {
    if (player.x < 200 && score === 0) {
      ctx.fillStyle = 'rgba(0,0,0,0.3)';
      ctx.fillRect(W / 2 - 160, H / 2 - 40, 320, 50);
      ctx.fillStyle = '#FFF';
      ctx.font = '16px monospace';
      ctx.textAlign = 'center';
      ctx.fillText('← → or A D to move  |  Space or ↑ to jump', W / 2, H / 2 - 12);
      ctx.fillText('Collect coins and reach the red flag!', W / 2, H / 2 + 12);
      ctx.textAlign = 'left';
    }
  }

  // ---- main loop ------------------------------------------------------------
  function loop() {
    update();

    drawSky();
    drawClouds();
    drawHills();
    drawBushes();
    drawGround();
    drawPlatforms();
    drawCoins();
    drawEnemies();
    drawFlag();
    drawPlayer();
    drawParticles();
    drawStartHint();
    drawHUD();

    if (gameState === 'dead')  drawDeathScreen();
    if (gameState === 'won')   drawWinScreen();

    requestAnimationFrame(loop);
  }

  // ---- start ----------------------------------------------------------------
  init();
  loop();
})();
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; }
    body {
      background: #111;
      display: flex;
      flex-direction: column;
      align-items: center;
      justify-content: center;
      height: 100vh;
      overflow: hidden;
      font-family: 'Courier New', monospace;
    }
    canvas {
      border: 3px solid #333;
      image-rendering: pixelated;
      display: block;
    }
    #info {
      color: #888;
      margin-top: 10px;
      font-size: 13px;
      text-align: center;
    }
    #info kbd {
      background: #333;
      padding: 2px 6px;
      border-radius: 3px;
      border: 1px solid #555;
      color: #ccc;
    }
  </style>
</head>
<body>
  <canvas id="game" width="800" height="450"></canvas>
  <div id="info">
    <kbd>←</kbd> <kbd>→</kbd> or <kbd>A</kbd> <kbd>D</kbd> to move &nbsp;|&nbsp;
    <kbd>Space</kbd> <kbd>↑</kbd> or <kbd>W</kbd> to jump &nbsp;|&nbsp;
    Collect coins &amp; reach the flag!
  </div>
  <script src="game.js"></script>
</body>
</html>

rep 2 UNGRADED

pi 325.2s · verify 4.1s · 34697 tok · responded: qwen/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: #111;
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    height: 100vh;
    font-family: monospace;
    overflow: hidden;
  }
  canvas {
    border: 3px solid #444;
    image-rendering: pixelated;
    display: block;
  }
  #ui-overlay {
    position: absolute;
    top: 12px;
    left: 50%;
    transform: translateX(-50%);
    color: #fff;
    font-size: 18px;
    text-shadow: 2px 2px 0 #000;
    pointer-events: none;
    text-align: center;
    z-index: 10;
  }
  #instructions {
    color: #888;
    font-size: 13px;
    margin-top: 8px;
    text-align: center;
  }
</style>
</head>
<body>
<div id="ui-overlay">
  <span id="score-display">Coins: 0 / 0</span>
</div>
<canvas id="game" width="800" height="450"></canvas>
<div id="instructions">Arrow Keys / WASD to move &bull; Space / Up / W to jump &bull; R to restart</div>

<script>
// ============================================================
//  SUPER PLATFORMER — vanilla JS, no dependencies
// ============================================================
const canvas = document.getElementById('game');
const ctx = canvas.getContext('2d');
const scoreDisplay = document.getElementById('score-display');

const W = canvas.width;   // 800
const H = canvas.height;  // 450
const TILE = 32;

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

function keyLeft()  { return keys['ArrowLeft']  || keys['KeyA']; }
function keyRight() { return keys['ArrowRight'] || keys['KeyD']; }
function keyJump()  { return keys['Space']      || keys['ArrowUp'] || keys['KeyW']; }

// ---------- Game state ----------
let gameState = 'playing'; // 'playing' | 'won' | 'dead'
let score = 0;
let totalCoins = 0;
let deathTimer = 0;
let winTimer = 0;

// ---------- Level data ----------
// Map: each entry is a tile type at grid position (col, row)
// 0 = empty, 1 = ground, 2 = brick, 3 = question block, 4 = pipe
// We'll build the level procedurally.

function buildLevel() {
  const cols = 120;
  const rows = Math.ceil(H / TILE) + 2;
  const map = Array.from({ length: rows }, () => Array(cols).fill(0));

  // Ground — leave gaps for pits
  const groundRows = rows - 2;
  const gaps = [
    { start: 18, end: 20 },
    { start: 36, end: 39 },
    { start: 70, end: 72 },
  ];
  for (let c = 0; c < cols; c++) {
    let isGap = gaps.some(g => c >= g.start && c < g.end);
    if (!isGap) {
      map[groundRows][c] = 1;
      map[groundRows + 1][c] = 1;
    }
  }

  // Floating platforms & blocks
  const platforms = [
    // (col, row, width, type)
    [10, groundRows - 5, 5, 2],   // bricks
    [22, groundRows - 5, 3, 3],   // question blocks
    [30, groundRows - 4, 6, 2],
    [38, groundRows - 7, 3, 3],
    [50, groundRows - 5, 4, 2],
    [55, groundRows - 5, 1, 3],
    [62, groundRows - 4, 5, 2],
    [80, groundRows - 5, 3, 2],
    [85, groundRows - 8, 4, 3],
    [95, groundRows - 5, 6, 2],
  ];
  platforms.forEach(([c, r, w, t]) => {
    for (let i = 0; i < w; i++) map[r][c + i] = t;
  });

  // Pipes
  const pipes = [
    { col: 14, height: 2 },
    { col: 32, height: 3 },
    { col: 56, height: 2 },
    { col: 88, height: 4 },
  ];
  pipes.forEach(p => {
    for (let h = 0; h < p.height; h++) {
      map[groundRows - h][p.col] = 4;
      map[groundRows - h][p.col + 1] = 4;
    }
  });

  // Stairs near the end
  const stairStart = 103;
  for (let i = 0; i < 5; i++) {
    for (let h = 0; h <= i; h++) {
      map[groundRows - h][stairStart + i] = 2;
    }
  }

  // Coins on platforms
  const coinPositions = [];

  // Coins in the air above gaps
  coinPositions.push({ col: 16, row: groundRows - 4 });
  coinPositions.push({ col: 17, row: groundRows - 4 });
  coinPositions.push({ col: 18, row: groundRows - 5 });

  // Coins on platforms
  const coinOnPlatform = [
    [11, groundRows - 7], [12, groundRows - 7], [13, groundRows - 7],
    [23, groundRows - 7], [24, groundRows - 7],
    [31, groundRows - 6], [32, groundRows - 6], [33, groundRows - 6], [34, groundRows - 6],
    [39, groundRows - 9],
    [51, groundRows - 7], [52, groundRows - 7],
    [63, groundRows - 6], [64, groundRows - 6], [65, groundRows - 6],
    [81, groundRows - 7], [82, groundRows - 7],
    [86, groundRows - 10], [87, groundRows - 10],
    [96, groundRows - 7], [97, groundRows - 7], [98, groundRows - 7],
  ];
  coinPositions.push(...coinOnPlatform);

  // Stair coins
  for (let i = 0; i < 4; i++) {
    coinPositions.push({ col: stairStart + i, row: groundRows - i - 2 });
  }

  // Flag at the end
  const flagCol = 115;
  const flagRow = groundRows - 8;

  return { map, coinPositions, flagCol, flagRow, cols, groundRow: groundRows };
}

let level;

function initGame() {
  level = buildLevel();
  totalCoins = level.coinPositions.length;
  score = 0;
  gameState = 'playing';
  deathTimer = 0;
  winTimer = 0;

  // Player
  player.x = 3 * TILE;
  player.y = (level.groundRow - 2) * TILE;
  player.vx = 0;
  player.vy = 0;
  player.onGround = false;
  player.facing = 1;
  player.animFrame = 0;
  player.animTimer = 0;

  // Enemies
  enemies = [
    { x: 12 * TILE, y: (level.groundRow - 1) * TILE, vx: -1, w: TILE, h: TILE, alive: true, type: 'goomba' },
    { x: 25 * TILE, y: (level.groundRow - 1) * TILE, vx: -1, w: TILE, h: TILE, alive: true, type: 'goomba' },
    { x: 42 * TILE, y: (level.groundRow - 1) * TILE, vx: -1, w: TILE, h: TILE, alive: true, type: 'goomba' },
    { x: 44 * TILE, y: (level.groundRow - 1) * TILE, vx: -1, w: TILE, h: TILE, alive: true, type: 'goomba' },
    { x: 60 * TILE, y: (level.groundRow - 1) * TILE, vx: -1, w: TILE, h: TILE, alive: true, type: 'goomba' },
    { x: 75 * TILE, y: (level.groundRow - 1) * TILE, vx: -1, w: TILE, h: TILE, alive: true, type: 'goomba' },
    { x: 90 * TILE, y: (level.groundRow - 1) * TILE, vx: -1, w: TILE, h: TILE, alive: true, type: 'goomba' },
    { x: 110 * TILE, y: (level.groundRow - 1) * TILE, vx: -1, w: TILE, h: TILE, alive: true, type: 'goomba' },
  ];

  // Coins
  coins = level.coinPositions.map(c => ({
    x: c.col * TILE + 8,
    y: c.row * TILE + 8,
    w: 16, h: 16,
    collected: false,
    animOffset: Math.random() * Math.PI * 2,
  }));

  // Clouds and hills for background
  if (!clouds.length) {
    for (let i = 0; i < 20; i++) {
      clouds.push({
        x: i * 120 + Math.random() * 60,
        y: 20 + Math.random() * 60,
        w: 60 + Math.random() * 40,
      });
    }
    for (let i = 0; i < 15; i++) {
      hills.push({
        x: i * 200 + Math.random() * 80,
        r: 40 + Math.random() * 30,
      });
    }
  }
}

// ---------- Entities ----------
let player = {
  x: 0, y: 0, w: 24, h: 30,
  vx: 0, vy: 0,
  onGround: false,
  facing: 1,
  animFrame: 0,
  animTimer: 0,
};

const GRAVITY = 0.55;
const JUMP_FORCE = -10.5;
const MOVE_SPEED = 3.5;
const FRICTION = 0.82;
const MAX_FALL = 12;

let enemies = [];
let coins = [];
let clouds = [];
let hills = [];
let particles = [];
let cameraX = 0;

// ---------- Collision helpers ----------
function getTile(col, row) {
  if (col < 0 || col >= level.cols || row < 0 || row >= level.map.length) return 0;
  return level.map[row][col];
}

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

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;
}

function resolveCollisions() {
  player.onGround = false;
  const p = player;
  const map = level.map;
  const groundRow = level.groundRow;

  // Horizontal
  p.x += p.vx;
  // Left
  if (p.vx < 0) {
    const leftCol = Math.floor(p.x / TILE);
    const topRow = Math.floor(p.y / TILE);
    const botRow = Math.floor((p.y + p.h - 1) / TILE);
    for (let r = topRow; r <= botRow; r++) {
      if (isSolid(getTile(leftCol, r))) {
        p.x = (leftCol + 1) * TILE;
        p.vx = 0;
        break;
      }
    }
  }
  // Right
  if (p.vx > 0) {
    const rightCol = Math.floor((p.x + p.w) / TILE);
    const topRow = Math.floor(p.y / TILE);
    const botRow = Math.floor((p.y + p.h - 1) / TILE);
    for (let r = topRow; r <= botRow; r++) {
      if (isSolid(getTile(rightCol, r))) {
        p.x = rightCol * TILE - p.w;
        p.vx = 0;
        break;
      }
    }
  }

  // Vertical
  p.y += p.vy;
  // Top (hitting ceiling)
  if (p.vy < 0) {
    const topRow = Math.floor(p.y / TILE);
    const leftCol = Math.floor(p.x / TILE);
    const rightCol = Math.floor((p.x + p.w - 1) / TILE);
    for (let c = leftCol; c <= rightCol; c++) {
      if (isSolid(getTile(c, topRow))) {
        p.y = (topRow + 1) * TILE;
        p.vy = 0;
        break;
      }
    }
  }
  // Bottom (landing)
  if (p.vy >= 0) {
    const botRow = Math.floor((p.y + p.h) / TILE);
    const leftCol = Math.floor(p.x / TILE);
    const rightCol = Math.floor((p.x + p.w - 1) / TILE);
    for (let c = leftCol; c <= rightCol; c++) {
      if (isSolid(getTile(c, botRow))) {
        p.y = botRow * TILE - p.h;
        p.vy = 0;
        p.onGround = true;
        break;
      }
    }
  }
}

// ---------- Drawing helpers ----------
function drawSky() {
  const grad = ctx.createLinearGradient(0, 0, 0, H);
  grad.addColorStop(0, '#5c94fc');
  grad.addColorStop(0.7, '#87CEEB');
  grad.addColorStop(1, '#b8e0ff');
  ctx.fillStyle = grad;
  ctx.fillRect(0, 0, W, H);
}

function drawClouds(camX) {
  clouds.forEach(cl => {
    const sx = cl.x - camX * 0.3;
    if (sx > -100 && sx < W + 100) {
      ctx.fillStyle = 'rgba(255,255,255,0.9)';
      ctx.beginPath();
      ctx.ellipse(sx, cl.y, cl.w * 0.5, 14, 0, 0, Math.PI * 2);
      ctx.fill();
      ctx.beginPath();
      ctx.ellipse(sx - cl.w * 0.25, cl.y + 5, cl.w * 0.3, 10, 0, 0, Math.PI * 2);
      ctx.fill();
      ctx.beginPath();
      ctx.ellipse(sx + cl.w * 0.25, cl.y + 5, cl.w * 0.35, 11, 0, 0, Math.PI * 2);
      ctx.fill();
    }
  });
}

function drawHills(camX) {
  hills.forEach(h => {
    const sx = h.x - camX * 0.5;
    if (sx > -h.r * 2 && sx < W + h.r * 2) {
      ctx.fillStyle = '#4CAF50';
      ctx.beginPath();
      ctx.ellipse(sx, level.groundRow * TILE, h.r * 2, h.r, 0, Math.PI, 0);
      ctx.fill();
      ctx.fillStyle = '#66BB6A';
      ctx.beginPath();
      ctx.ellipse(sx, level.groundRow * TILE, h.r, h.r * 0.6, 0, Math.PI, 0);
      ctx.fill();
    }
  });
}

function drawTile(col, row, type, camX) {
  const x = col * TILE - camX;
  const y = row * TILE;
  if (x < -TILE || x > W + TILE) return;

  switch (type) {
    case 1: { // Ground
      ctx.fillStyle = '#8B4513';
      ctx.fillRect(x, y, TILE, TILE);
      ctx.fillStyle = '#5a8f3c';
      ctx.fillRect(x, y, TILE, 6);
      // Dirt texture
      ctx.fillStyle = '#7a3e10';
      ctx.fillRect(x + 4, y + 12, 6, 6);
      ctx.fillRect(x + 18, y + 20, 8, 4);
      ctx.fillRect(x + 8, y + 24, 5, 5);
      break;
    }
    case 2: { // Brick
      ctx.fillStyle = '#C84C09';
      ctx.fillRect(x, y, TILE, TILE);
      ctx.strokeStyle = '#8B3000';
      ctx.lineWidth = 1;
      // Brick lines
      ctx.strokeRect(x + 0.5, y + 0.5, 15, 15);
      ctx.strokeRect(x + 16, y + 0.5, 15, 15);
      ctx.strokeRect(x + 8, y + 16, 16, 15);
      ctx.strokeStyle = '#E8601C';
      ctx.strokeRect(x + 1, y + 1, 14, 14);
      ctx.strokeRect(x + 17, y + 1, 14, 14);
      ctx.strokeRect(x + 9, y + 17, 14, 13);
      break;
    }
    case 3: { // Question block
      const pulse = Math.sin(Date.now() / 300) * 0.15 + 0.85;
      ctx.fillStyle = `rgb(${Math.floor(230 * pulse)}, ${Math.floor(180 * pulse)}, 0)`;
      ctx.fillRect(x, y, TILE, TILE);
      ctx.strokeStyle = '#A07000';
      ctx.lineWidth = 2;
      ctx.strokeRect(x + 1, y + 1, TILE - 2, TILE - 2);
      // Question mark
      ctx.fillStyle = '#FFF';
      ctx.font = 'bold 18px monospace';
      ctx.textAlign = 'center';
      ctx.textBaseline = 'middle';
      ctx.fillText('?', x + TILE / 2, y + TILE / 2 + 1);
      break;
    }
    case 4: { // Pipe
      // Check if top
      const below = getTile(col, row + 1);
      const leftOf = getTile(col - 1, row);
      const rightOf = getTile(col + 1, row);
      const isLeftSide = (col === 0 || !isSolid(getTile(col - 1, row)));
      const isRightSide = (col + 1 >= level.cols || !isSolid(getTile(col + 1, row)));

      ctx.fillStyle = '#00AA00';
      ctx.fillRect(x, y, TILE, TILE);
      ctx.fillStyle = '#00DD00';
      ctx.fillRect(x + 3, y, 8, TILE);
      ctx.fillStyle = '#008800';
      ctx.fillRect(x + TILE - 6, y, 5, TILE);

      if (!isSolid(below) || (leftOf !== 4 && rightOf !== 4)) {
        // Pipe top rim
        ctx.fillStyle = '#00CC00';
        ctx.fillRect(x - 2, y, TILE + 4, 8);
        ctx.fillStyle = '#00FF00';
        ctx.fillRect(x - 1, y + 1, 6, 6);
        ctx.fillStyle = '#006600';
        ctx.fillRect(x + TILE - 2, y + 1, 5, 6);
      }
      break;
    }
  }
}

function drawMap(camX) {
  const startCol = Math.max(0, Math.floor(camX / TILE) - 1);
  const endCol = Math.min(level.cols, Math.ceil((camX + W) / TILE) + 1);
  const startRow = 0;
  const endRow = level.map.length;
  for (let r = startRow; r < endRow; r++) {
    for (let c = startCol; c < endCol; c++) {
      if (level.map[r][c]) drawTile(c, r, level.map[r][c], camX);
    }
  }
}

function drawPlayer(camX) {
  const p = player;
  const sx = Math.round(p.x - camX);
  const sy = Math.round(p.y);
  const f = p.facing;

  // Body
  ctx.fillStyle = '#E52521'; // Red shirt
  ctx.fillRect(sx + 4, sy + 8, 16, 12);

  // Head
  ctx.fillStyle = '#FFB366'; // Skin
  ctx.fillRect(sx + 5, sy, 14, 12);

  // Hat
  ctx.fillStyle = '#E52521';
  ctx.fillRect(sx + 3, sy - 2, 16, 6);
  ctx.fillRect(sx + (f > 0 ? 10 : 1), sy + 2, 14, 3);

  // Eyes
  ctx.fillStyle = '#000';
  const eyeX = sx + (f > 0 ? 14 : 7);
  ctx.fillRect(eyeX, sy + 4, 3, 3);

  // Mustache
  ctx.fillStyle = '#4A2800';
  ctx.fillRect(sx + (f > 0 ? 11 : 5), sy + 8, 8, 2);

  // Overalls
  ctx.fillStyle = '#0051D8'; // Blue
  ctx.fillRect(sx + 4, sy + 18, 16, 8);

  // Legs
  ctx.fillStyle = '#0051D8';
  if (!p.onGround) {
    // Jumping pose
    ctx.fillRect(sx + 2, sy + 26, 7, 5);
    ctx.fillRect(sx + 15, sy + 24, 7, 5);
  } else if (Math.abs(p.vx) > 0.3) {
    // Walking animation
    const legPhase = Math.sin(p.animFrame * 0.8);
    ctx.fillRect(sx + 3, sy + 26, 7, 4 + legPhase * 2);
    ctx.fillRect(sx + 14, sy + 26, 7, 4 - legPhase * 2);
  } else {
    ctx.fillRect(sx + 3, sy + 26, 7, 4);
    ctx.fillRect(sx + 14, sy + 26, 7, 4);
  }

  // Shoes
  ctx.fillStyle = '#6B3300';
  if (Math.abs(p.vx) > 0.3) {
    const legPhase = Math.sin(p.animFrame * 0.8);
    ctx.fillRect(sx + 2, sy + 28 + legPhase * 2, 8, 3);
    ctx.fillRect(sx + 14, sy + 28 - legPhase * 2, 8, 3);
  } else {
    ctx.fillRect(sx + 2, sy + 30, 8, 3);
    ctx.fillRect(sx + 14, sy + 30, 8, 3);
  }
}

function drawEnemy(e, camX) {
  if (!e.alive) return;
  const sx = Math.round(e.x - camX);
  const sy = Math.round(e.y);

  // Goomba body
  ctx.fillStyle = '#A0522D';
  ctx.beginPath();
  ctx.ellipse(sx + TILE / 2, sy + TILE / 2 + 2, 14, 12, 0, 0, Math.PI * 2);
  ctx.fill();

  // Feet
  ctx.fillStyle = '#000';
  const walkOff = Math.sin(Date.now() / 150) * 3;
  ctx.fillRect(sx + 3, sy + TILE - 4 + walkOff, 8, 5);
  ctx.fillRect(sx + TILE - 11, sy + TILE - 4 - walkOff, 8, 5);

  // Eyes
  ctx.fillStyle = '#FFF';
  ctx.fillRect(sx + 7, sy + 8, 6, 7);
  ctx.fillRect(sx + TILE - 13, sy + 8, 6, 7);
  ctx.fillStyle = '#000';
  ctx.fillRect(sx + 9, sy + 10, 3, 4);
  ctx.fillRect(sx + TILE - 11, sy + 10, 3, 4);

  // Angry eyebrows
  ctx.fillStyle = '#000';
  ctx.fillRect(sx + 6, sy + 6, 7, 2);
  ctx.fillRect(sx + TILE - 13, sy + 6, 7, 2);
}

function drawCoin(coin, camX) {
  if (coin.collected) return;
  const sx = Math.round(coin.x - camX);
  const sy = Math.round(coin.y + Math.sin(Date.now() / 300 + coin.animOffset) * 3);

  const stretch = Math.abs(Math.sin(Date.now() / 400 + coin.animOffset));
  const w = Math.max(4, 14 * stretch);

  ctx.fillStyle = '#FFD700';
  ctx.beginPath();
  ctx.ellipse(sx + 8, sy + 8, w / 2, 8, 0, 0, Math.PI * 2);
  ctx.fill();

  ctx.fillStyle = '#FFA500';
  ctx.beginPath();
  ctx.ellipse(sx + 8, sy + 8, w / 2 - 2, 6, 0, 0, Math.PI * 2);
  ctx.fill();

  // Shine
  if (stretch > 0.5) {
    ctx.fillStyle = '#FFF';
    ctx.fillRect(sx + 6, sy + 4, 2, 2);
  }
}

function drawFlag(camX) {
  const fc = level.flagCol;
  const fr = level.flagRow;
  const sx = fc * TILE - camX;
  const baseY = level.groundRow * TILE;

  // Pole
  ctx.fillStyle = '#888';
  ctx.fillRect(sx + 13, fr * TILE, 5, baseY - fr * TILE);

  // Ball on top
  ctx.fillStyle = '#FFD700';
  ctx.beginPath();
  ctx.arc(sx + 15, fr * TILE, 5, 0, Math.PI * 2);
  ctx.fill();

  // Flag
  const wave = Math.sin(Date.now() / 400) * 3;
  ctx.fillStyle = '#00CC00';
  ctx.beginPath();
  ctx.moveTo(sx + 18, fr * TILE + 6);
  ctx.lineTo(sx + 48 + wave, fr * TILE + 18);
  ctx.lineTo(sx + 18, fr * TILE + 32);
  ctx.closePath();
  ctx.fill();

  // Star on flag
  ctx.fillStyle = '#FFF';
  ctx.font = '12px monospace';
  ctx.textAlign = 'center';
  ctx.fillText('★', sx + 26 + wave / 2, fr * TILE + 23);
}

function drawParticles(camX) {
  particles.forEach(p => {
    ctx.fillStyle = p.color;
    ctx.globalAlpha = p.life;
    ctx.fillRect(p.x - camX, p.y, p.size, p.size);
  });
  ctx.globalAlpha = 1;
}

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

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

  ctx.fillStyle = '#FFD700';
  ctx.font = 'bold 48px monospace';
  ctx.textAlign = 'center';
  ctx.textBaseline = 'middle';
  ctx.fillText('🏆 YOU WIN! 🏆', W / 2, H / 2 - 30);

  ctx.fillStyle = '#FFF';
  ctx.font = '24px monospace';
  ctx.fillText(`Coins collected: ${score} / ${totalCoins}`, W / 2, H / 2 + 20);
  ctx.fillText(`Press R to play again`, W / 2, H / 2 + 60);
}

function drawDeathText() {
  ctx.fillStyle = 'rgba(100,0,0,0.5)';
  ctx.fillRect(0, 0, W, H);

  ctx.fillStyle = '#F00';
  ctx.font = 'bold 48px monospace';
  ctx.textAlign = 'center';
  ctx.textBaseline = 'middle';
  ctx.fillText('GAME OVER', W / 2, H / 2);

  ctx.fillStyle = '#FFF';
  ctx.font = '20px monospace';
  ctx.fillText(`Press R to retry`, W / 2, H / 2 + 40);
}

// ---------- Update ----------
function update() {
  if (gameState === 'dead') {
    deathTimer++;
    if (deathTimer > 60) {
      initGame();
    }
    // Still update particles
    updateParticles();
    return;
  }
  if (gameState === 'won') {
    winTimer++;
    updateParticles();
    return;
  }

  const p = player;

  // Movement
  if (keyLeft()) {
    p.vx -= 0.8;
    p.facing = -1;
  }
  if (keyRight()) {
    p.vx += 0.8;
    p.facing = 1;
  }
  if (!keyLeft() && !keyRight()) {
    p.vx *= FRICTION;
    if (Math.abs(p.vx) < 0.1) p.vx = 0;
  }
  p.vx = Math.max(-MOVE_SPEED, Math.min(MOVE_SPEED, p.vx));

  // Jump
  if (keyJump() && p.onGround) {
    p.vy = JUMP_FORCE;
    p.onGround = false;
  }

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

  // Animation
  if (Math.abs(p.vx) > 0.3) {
    p.animTimer++;
    if (p.animTimer > 5) {
      p.animTimer = 0;
      p.animFrame++;
    }
  }

  // Resolve collisions
  resolveCollisions();

  // Fall into pit
  if (p.y > level.map.length * TILE + 50) {
    gameState = 'dead';
    deathTimer = 0;
    spawnParticles(p.x + p.w / 2, p.y, '#F00', 15);
    return;
  }

  // Camera
  const targetCamX = p.x - W / 3;
  cameraX += (targetCamX - cameraX) * 0.1;
  cameraX = Math.max(0, Math.min(cameraX, level.cols * TILE - W));

  // Coins
  coins.forEach(coin => {
    if (coin.collected) return;
    const coinRect = { x: coin.x, y: coin.y, w: coin.w, h: coin.h };
    const playerRect = { x: p.x, y: p.y, w: p.w, h: p.h };
    if (rectOverlap(playerRect, coinRect)) {
      coin.collected = true;
      score++;
      scoreDisplay.textContent = `Coins: ${score} / ${totalCoins}`;
      spawnParticles(coin.x + 8, coin.y + 8, '#FFD700', 8);
    }
  });

  // Enemies
  enemies.forEach(e => {
    if (!e.alive) return;

    // Move
    e.x += e.vx;

    // Enemy-ground collision
    const eBotCol = Math.floor((e.y + e.h) / TILE);
    const eLeftCol = Math.floor(e.x / TILE);
    const eRightCol = Math.floor((e.x + e.w - 1) / TILE);
    let onGround = false;
    for (let c = eLeftCol; c <= eRightCol; c++) {
      if (isSolid(getTile(c, eBotCol))) {
        e.y = eBotCol * TILE - e.h;
        onGround = true;
        break;
      }
    }

    // Check wall collision — reverse direction
    const edgeCol = e.vx > 0 ? Math.floor((e.x + e.w) / TILE) : Math.floor((e.x - 1) / TILE);
    const eyeRow = Math.floor((e.y + e.h / 2) / TILE);
    if (isSolid(getTile(edgeCol, eyeRow))) {
      e.vx *= -1;
    }

    // Fall into pit — kill
    if (e.y > level.map.length * TILE + 50) {
      e.alive = false;
    }

    // Player collision
    const pRect = { x: p.x, y: p.y, w: p.w, h: p.h };
    const eRect = { x: e.x, y: e.y, w: e.w, h: e.h };
    if (rectOverlap(pRect, eRect)) {
      // Stomp from above
      if (p.vy > 0 && p.y + p.h - e.y < 16) {
        e.alive = false;
        p.vy = JUMP_FORCE * 0.5;
        spawnParticles(e.x + e.w / 2, e.y + e.h / 2, '#A0522D', 10);
        scoreDisplay.textContent = `Coins: ${score} / ${totalCoins}`;
      } else {
        // Player dies
        gameState = 'dead';
        deathTimer = 0;
        spawnParticles(p.x + p.w / 2, p.y + p.h / 2, '#E52521', 20);
      }
    }
  });

  // Flag check (win)
  const flagRect = {
    x: level.flagCol * TILE,
    y: level.flagRow * TILE,
    w: TILE,
    h: TILE * 8,
  };
  const pRect2 = { x: p.x, y: p.y, w: p.w, h: p.h };
  if (rectOverlap(pRect2, flagRect)) {
    gameState = 'won';
    winTimer = 0;
    spawnParticles(p.x + p.w / 2, p.y, '#FFD700', 30);
    spawnParticles(p.x + p.w / 2, p.y, '#00CC00', 20);
  }

  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 -= p.decay;
    if (p.life <= 0) particles.splice(i, 1);
  }
}

function restartGame() {
  clouds = [];
  hills = [];
  particles = [];
  initGame();
}

// ---------- Render ----------
function render() {
  drawSky();
  drawClouds(cameraX);
  drawHills(cameraX);
  drawMap(cameraX);
  drawFlag(cameraX);

  coins.forEach(c => drawCoin(c, cameraX));
  enemies.forEach(e => drawEnemy(e, cameraX));
  drawPlayer(cameraX);
  drawParticles(cameraX);

  if (gameState === 'won') drawWinText();
  if (gameState === 'dead') drawDeathText();
}

// ---------- Game loop ----------
function gameLoop() {
  if (keys['KeyR']) {
    restartGame();
    keys['KeyR'] = false;
  }
  update();
  render();
  requestAnimationFrame(gameLoop);
}

// ---------- Start ----------
initGame();
gameLoop();
</script>
</body>
</html>

rep 3 UNGRADED

pi 238.5s · verify 4.1s · 56355 tok · responded: qwen/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 — game.js === */
(function () {
  'use strict';

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

  /* ---------- colours ---------- */
  const C = {
    sky:       '#5c94fc',
    ground:    '#c84c0c',
    groundTop: '#fcb746',
    platform:  '#c84c0c',
    platformT: '#fcb746',
    brick:     '#c0600a',
    brickLine: '#8b3808',
    coin:      '#fcfc00',
    coinDark:  '#c4a800',
    player:    '#e00000',
    playerSkin:'#ffbb99',
    playerHat: '#e00000',
    playerPants:'#0000cc',
    flag:      '#00cc00',
    flagPole:  '#888',
    cloud:     '#ffffff',
    bush:      '#30a030',
    hill:      '#60d060',
    hillDark:  '#48a848',
    pipe:      '#00a800',
    pipeDark:  '#006800',
    scoreBg:   'rgba(0,0,0,0.45)',
    winBg:     'rgba(0,0,0,0.6)',
  };

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

  /* ---------- helper ---------- */
  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;
  }

  /* ================================================================
     LEVEL DESIGN
     ================================================================ */
  // Tile size = 32 px  — we build the level as a series of platform rects,
  // coin rects, a pipe, a flag, and decorative elements.

  const GRAVITY      = 0.55;
  const PLAYER_SPEED = 3.2;
  const JUMP_FORCE   = -10.5;
  const FRICTION     = 0.82;
  const AIR_FRICTION = 0.92;

  // The level is longer than the viewport; the camera scrolls with the player.
  // levelWidth in pixels.
  const LEVEL_WIDTH = 4800;

  /* Platforms  –  {x, y, w, h} */
  const platforms = [
    // Ground segments (with gaps)
    { x: 0,     y: H - 32, w: 900,  h: 32 },
    { x: 1000,  y: H - 32, w: 600,  h: 32 },
    { x: 1700,  y: H - 32, w: 400,  h: 32 },
    { x: 2250,  y: H - 32, w: 1200, h: 32 },
    { x: 3600,  y: H - 32, w: 1200, h: 32 },

    // Raised platforms
    { x: 320,  y: 320, w: 128, h: 16 },
    { x: 520,  y: 240, w: 128, h: 16 },
    { x: 720,  y: 176, w: 96,  h: 16 },

    { x: 1100, y: 300, w: 160, h: 16 },
    { x: 1320, y: 224, w: 128, h: 16 },

    { x: 1800, y: 300, w: 96,  h: 16 },
    { x: 1960, y: 220, w: 96,  h: 16 },
    { x: 2100, y: 152, w: 96,  h: 16 },

    { x: 2400, y: 280, w: 160, h: 16 },
    { x: 2640, y: 200, w: 128, h: 16 },
    { x: 2880, y: 140, w: 128, h: 16 },

    { x: 3700, y: 300, w: 128, h: 16 },
    { x: 3900, y: 224, w: 128, h: 16 },
    { x: 4100, y: 152, w: 160, h: 16 },
  ];

  /* Coins – {x, y}  (radius 10, shown as 20×20 rect) */
  const coins = [
    { x: 350, y: 288, taken: false },
    { x: 400, y: 288, taken: false },
    { x: 560, y: 208, taken: false },
    { x: 610, y: 208, taken: false },
    { x: 760, y: 144, taken: false },
    { x: 810, y: 144, taken: false },
    { x: 1140, y: 268, taken: false },
    { x: 1190, y: 268, taken: false },
    { x: 1360, y: 192, taken: false },
    { x: 1840, y: 268, taken: false },
    { x: 2000, y: 188, taken: false },
    { x: 2140, y: 120, taken: false },
    { x: 2440, y: 248, taken: false },
    { x: 2490, y: 248, taken: false },
    { x: 2680, y: 168, taken: false },
    { x: 2920, y: 108, taken: false },
    { x: 2970, y: 108, taken: false },
    { x: 3740, y: 268, taken: false },
    { x: 3940, y: 192, taken: false },
    { x: 4140, y: 120, taken: false },
    { x: 4200, y: 120, taken: false },
  ];

  // Decorative elements
  const clouds = [
    { x: 200,  y: 60,  s: 1.0 },
    { x: 600,  y: 40,  s: 0.7 },
    { x: 1050, y: 70,  s: 0.9 },
    { x: 1500, y: 50,  s: 0.8 },
    { x: 2000, y: 35,  s: 1.1 },
    { x: 2500, y: 65,  s: 0.6 },
    { x: 3000, y: 45,  s: 1.0 },
    { x: 3500, y: 55,  s: 0.85 },
    { x: 4000, y: 40,  s: 0.95 },
    { x: 4500, y: 75,  s: 0.7 },
  ];
  const hills = [
    { x: 100,  s: 1.2 },
    { x: 800,  s: 0.8 },
    { x: 1600, s: 1.0 },
    { x: 2400, s: 1.3 },
    { x: 3200, s: 0.9 },
    { x: 4000, s: 1.1 },
  ];
  const bushes = [
    { x: 450,  s: 0.8 },
    { x: 1200, s: 1.0 },
    { x: 1900, s: 0.7 },
    { x: 2700, s: 0.9 },
    { x: 3400, s: 0.8 },
  ];

  /* Pipe */
  const pipe = { x: 1550, y: H - 32 - 56, w: 48, h: 56 };

  /* Goal flag */
  const flag = { x: 4600, y: H - 32 - 160, w: 8, h: 160 };

  /* ================================================================
     PLAYER
     ================================================================ */
  const player = {
    x: 60,
    y: H - 32 - 48,
    w: 24,
    h: 40,
    vx: 0,
    vy: 0,
    onGround: false,
    facing: 1,  // 1 = right, -1 = left
    walkFrame: 0,
    walkTimer: 0,
    alive: true,
    won: false,
  };

  let score = 0;
  let totalCoins = coins.length;
  let camera = { x: 0 };
  let animFrame = 0;   // for coin shimmer etc.
  let winTimer = 0;    // countdown after winning

  function resetPlayer() {
    player.x = 60;
    player.y = H - 32 - 48;
    player.vx = 0;
    player.vy = 0;
    player.onGround = false;
    player.alive = false;
    player.won = false;
    player.facing = 1;
    winTimer = 0;
    for (let i = 0; i < coins.length; i++) coins[i].taken = false;
    score = 0;
  }

  /* ================================================================
     GAME LOOP
     ================================================================ */
  function update() {
    if (player.won) {
      winTimer++;
      return;
    }
    if (!player.alive) {
      // Death animation / waiting — just let gravity pull them down
      player.vy += GRAVITY;
      player.y += player.vy;
      return;
    }

    /* --- input --- */
    var left  = keys['ArrowLeft']  || keys['KeyA'];
    var right = keys['ArrowRight'] || keys['KeyD'];
    var jump  = keys['Space']      || keys['ArrowUp'] || keys['KeyW'];

    /* horizontal movement */
    if (left) {
      player.vx -= 0.8;
      player.facing = -1;
    }
    if (right) {
      player.vx += 0.8;
      player.facing = 1;
    }
    if (!left && !right) {
      player.vx *= player.onGround ? FRICTION : AIR_FRICTION;
    }
    // Clamp horizontal speed
    if (player.vx > PLAYER_SPEED)   player.vx = PLAYER_SPEED;
    if (player.vx < -PLAYER_SPEED)  player.vx = -PLAYER_SPEED;
    // Small dead-zone to stop completely
    if (Math.abs(player.vx) < 0.1) player.vx = 0;

    /* walking animation */
    if (player.onGround && Math.abs(player.vx) > 0.5) {
      player.walkTimer++;
      if (player.walkTimer > 6) {
        player.walkTimer = 0;
        player.walkFrame = (player.walkFrame + 1) % 4;
      }
    } else if (!player.onGround) {
      player.walkFrame = 1; // hang in air
    } else {
      player.walkFrame = 0;
      player.walkTimer = 0;
    }

    /* jump */
    if (jump && player.onGround) {
      player.vy = JUMP_FORCE;
      player.onGround = false;
    }

    /* gravity */
    player.vy += GRAVITY;
    if (player.vy > 14) player.vy = 14; // terminal velocity

    /* --- horizontal collision --- */
    player.x += player.vx;
    if (player.x < 0) player.x = 0;
    if (player.x + player.w > LEVEL_WIDTH) player.x = LEVEL_WIDTH - player.w;
    for (var i = 0; i < platforms.length; i++) {
      if (rectOverlap(player, platforms[i])) {
        if (player.vx > 0) {
          player.x = platforms[i].x - player.w;
        } else if (player.vx < 0) {
          player.x = platforms[i].x + platforms[i].w;
        }
        player.vx = 0;
      }
    }

    /* --- vertical collision --- */
    player.y += player.vy;
    player.onGround = false;
    for (var j = 0; j < platforms.length; j++) {
      if (rectOverlap(player, platforms[j])) {
        if (player.vy > 0) {
          // Falling — land on top
          player.y   = platforms[j].y - player.h;
          player.vy  = 0;
          player.onGround = true;
        } else if (player.vy < 0) {
          // Jumping up — hit platform bottom
          player.y   = platforms[j].y + platforms[j].h;
          player.vy  = 0;
        }
      }
    }

    /* fall off the world */
    if (player.y > H + 64) {
      player.alive = false;
    }

    /* --- coin collection --- */
    for (var c = 0; c < coins.length; c++) {
      if (!coins[c].taken) {
        var cr = coins[c];
        // Check if player overlaps the coin rect (coin is 20×20)
        var coinRect = { x: cr.x - 10, y: cr.y - 10, w: 20, h: 20 };
        if (rectOverlap(player, coinRect)) {
          coins[c].taken = true;
          score++;
        }
      }
    }

    /* --- flag (goal) --- */
    var flagRect = { x: flag.x - 4, y: flag.y, w: flag.w + 8, h: flag.h };
    if (rectOverlap(player, flagRect)) {
      player.won = true;
      winTimer = 0;
    }

    /* --- camera --- */
    var target = player.x - W / 2 + player.w / 2;
    if (target < 0) target = 0;
    if (target + W > LEVEL_WIDTH) target = LEVEL_WIDTH - W;
    // Smooth scroll
    camera.x += (target - camera.x) * 0.12;
  }

  /* ================================================================
     DRAWING
     ================================================================ */
  function drawBackground() {
    // Sky gradient
    var grad = ctx.createLinearGradient(0, 0, 0, H);
    grad.addColorStop(0, '#5c94fc');
    grad.addColorStop(1, '#98c4ff');
    ctx.fillStyle = grad;
    ctx.fillRect(0, 0, W, H);

    // Hills (parallax)
    for (var i = 0; i < hills.length; i++) {
      var h = hills[i];
      var hx = h.x - camera.x * 0.5;
      if (hx < -200 || hx > W + 200) continue;
      ctx.fillStyle = C.hill;
      ctx.beginPath();
      ctx.arc(hx, H - 32, 80 * h.s, Math.PI, 0);
      ctx.fill();
      ctx.fillStyle = C.hillDark;
      ctx.beginPath();
      ctx.arc(hx + 20 * h.s, H - 32, 50 * h.s, Math.PI, 0);
      ctx.fill();
    }

    // Clouds (parallax)
    for (var ci = 0; ci < clouds.length; ci++) {
      var cl = clouds[ci];
      var cx = cl.x - camera.x * 0.3;
      if (cx < -120 || cx > W + 120) continue;
      var s = cl.s;
      ctx.fillStyle = C.cloud;
      ctx.beginPath();
      ctx.arc(cx, cl.y, 18 * s, 0, Math.PI * 2);
      ctx.arc(cx + 22 * s, cl.y - 6 * s, 14 * s, 0, Math.PI * 2);
      ctx.arc(cx + 40 * s, cl.y + 2 * s, 16 * s, 0, Math.PI * 2);
      ctx.fill();
    }

    // Bushes (parallax)
    for (var bi = 0; bi < bushes.length; bi++) {
      var b = bushes[bi];
      var bx = b.x - camera.x * 0.7;
      if (bx < -80 || bx > W + 80) continue;
      ctx.fillStyle = C.bush;
      ctx.beginPath();
      ctx.arc(bx, H - 32, 14 * b.s, 0, Math.PI * 2);
      ctx.arc(bx + 18 * b.s, H - 32, 12 * b.s, 0, Math.PI * 2);
      ctx.arc(bx - 14 * b.s, H - 32, 10 * b.s, 0, Math.PI * 2);
      ctx.fill();
    }
  }

  function drawPlatforms() {
    for (var i = 0; i < platforms.length; i++) {
      var p = platforms[i];
      var px = p.x - camera.x;
      // Off-screen skip
      if (px + p.w < -10 || px > W + 10) continue;

      // Top border (grass-like)
      ctx.fillStyle = C.platformT;
      ctx.fillRect(px, p.y, p.w, Math.min(6, p.h));
      // Main body
      ctx.fillStyle = C.platform;
      ctx.fillRect(px, p.y + 6, p.w, p.h - 6);

      // Brick pattern for wider platforms
      if (p.h <= 16) {
        ctx.strokeStyle = C.brickLine;
        ctx.lineWidth = 1;
        for (var bx = 0; bx < p.w; bx += 32) {
          ctx.beginPath();
          ctx.moveTo(px + bx, p.y + 6);
          ctx.lineTo(px + bx, p.y + p.h);
          ctx.stroke();
        }
        // Horizontal mortar
        if (p.h > 16) {
          ctx.beginPath();
          ctx.moveTo(px, p.y + p.h / 2);
          ctx.lineTo(px + p.w, p.y + p.h / 2);
          ctx.stroke();
        }
      }
    }
  }

  function drawPipe() {
    var px = pipe.x - camera.x;
    if (px + pipe.w < -10 || px > W + 10) return;
    // Pipe body
    ctx.fillStyle = C.pipe;
    ctx.fillRect(px + 4, pipe.y + 20, pipe.w - 8, pipe.h - 20);
    // Pipe top rim
    ctx.fillStyle = C.pipeDark;
    ctx.fillRect(px, pipe.y, pipe.w, 20);
    ctx.fillStyle = C.pipe;
    ctx.fillRect(px + 2, pipe.y + 2, pipe.w - 4, 16);
    // Highlight
    ctx.fillStyle = 'rgba(255,255,255,0.15)';
    ctx.fillRect(px + 8, pipe.y + 4, 6, 14);
  }

  function drawCoins() {
    var t = Date.now() / 200;
    for (var i = 0; i < coins.length; i++) {
      if (coins[i].taken) continue;
      var c = coins[i];
      var cx = c.x - camera.x;
      if (cx < -20 || cx > W + 20) continue;
      var stretch = 0.5 + 0.5 * Math.sin(t + i * 1.7);
      var cw = 18 * stretch;
      var ch = 18;
      ctx.fillStyle = C.coin;
      ctx.beginPath();
      ctx.ellipse(cx, c.y, cw / 2, ch / 2, 0, 0, Math.PI * 2);
      ctx.fill();
      // Inner detail
      ctx.fillStyle = C.coinDark;
      ctx.beginPath();
      ctx.ellipse(cx, c.y, cw / 3, ch / 3, 0, 0, Math.PI * 2);
      ctx.fill();
      // Shimmer
      ctx.fillStyle = 'rgba(255,255,255,0.5)';
      ctx.beginPath();
      ctx.ellipse(cx - cw * 0.15, c.y - ch * 0.2, cw * 0.1, ch * 0.12, -0.3, 0, Math.PI * 2);
      ctx.fill();
    }
  }

  function drawFlag() {
    var fx = flag.x - camera.x;
    if (fx < -20 || fx > W + 20) return;
    // Pole
    ctx.fillStyle = C.flagPole;
    ctx.fillRect(fx, flag.y, flag.w, flag.h);
    // Ball on top
    ctx.fillStyle = '#fc0';
    ctx.beginPath();
    ctx.arc(fx + flag.w / 2, flag.y - 2, 6, 0, Math.PI * 2);
    ctx.fill();
    // Flag cloth
    var flap = Math.sin(Date.now() / 300) * 4;
    ctx.fillStyle = C.flag;
    ctx.beginPath();
    ctx.moveTo(fx + flag.w, flag.y + 4);
    ctx.lineTo(fx + flag.w + 36 + flap, flag.y + 16);
    ctx.lineTo(fx + flag.w + 34 + flap, flag.y + 40);
    ctx.lineTo(fx + flag.w, flag.y + 44);
    ctx.closePath();
    ctx.fill();
    // Flag star
    ctx.fillStyle = '#fff';
    ctx.font = '12px sans-serif';
    ctx.fillText('★', fx + flag.w + 12 + flap * 0.5, flag.y + 30);
  }

  function drawPlayer() {
    if (!player.alive && !player.won) return;
    var px = player.x - camera.x;
    var py = player.y;
    var f  = player.facing; // 1 = right, -1 = left

    ctx.save();
    if (f === -1) {
      ctx.translate(px + player.w, py);
      ctx.scale(-1, 1);
    } else {
      ctx.translate(px, py);
    }

    // Shadow under feet
    if (player.onGround) {
      ctx.fillStyle = 'rgba(0,0,0,0.2)';
      ctx.beginPath();
      ctx.ellipse(player.w / 2 + 4, player.h + 2, 14, 4, 0, 0, Math.PI * 2);
      ctx.fill();
    }

    // --- Body (overalls / shirt) ---
    ctx.fillStyle = C.player;
    ctx.fillRect(4, 14, 16, 18);

    // --- Head ---
    ctx.fillStyle = C.playerSkin;
    ctx.fillRect(5, 0, 14, 14);

    // --- Hat ---
    ctx.fillStyle = C.playerHat;
    ctx.fillRect(2, 0, 18, 5);
    ctx.fillRect(0, 4, 22, 4);

    // --- Eyes ---
    ctx.fillStyle = '#000';
    ctx.fillRect(12, 5, 3, 3);

    // --- Mustache ---
    ctx.fillStyle = '#3a1a00';
    ctx.fillRect(10, 9, 9, 2);

    // --- Pants ---
    ctx.fillStyle = C.playerPants;
    ctx.fillRect(4, 30, 16, 10);

    // --- Shoes ---
    ctx.fillStyle = '#6b3300';
    if (player.onGround && Math.abs(player.vx) > 0.5) {
      // Walk animation
      var legOff = [0, 3, 0, -3][player.walkFrame];
      ctx.fillRect(2, 38 + legOff, 8, 4);
      ctx.fillRect(14, 38 - legOff, 8, 4);
    } else if (!player.onGround) {
      // Jump pose — legs apart
      ctx.fillRect(0, 36, 8, 5);
      ctx.fillRect(16, 34, 8, 5);
    } else {
      ctx.fillRect(2, 38, 8, 4);
      ctx.fillRect(14, 38, 8, 4);
    }

    // --- Arms ---
    ctx.fillStyle = C.playerSkin;
    if (!player.onGround) {
      // Arms up when jumping
      ctx.fillRect(-2, 10, 5, 10);
      ctx.fillRect(21, 8, 5, 10);
    } else if (Math.abs(player.vx) > 0.5) {
      // Swing arms while walking
      var armSwing = [0, 4, 0, -4][player.walkFrame];
      ctx.fillRect(-2, 16 + armSwing, 5, 10);
      ctx.fillRect(21, 16 - armSwing, 5, 10);
    } else {
      ctx.fillRect(-2, 18, 5, 10);
      ctx.fillRect(21, 18, 5, 10);
    }

    ctx.restore();
  }

  function drawHUD() {
    // Score background
    ctx.fillStyle = C.scoreBg;
    ctx.beginPath();
    ctx.roundRect(12, 8, 150, 30, 6);
    ctx.fill();

    // Coin icon
    ctx.fillStyle = C.coin;
    ctx.beginPath();
    ctx.arc(28, 23, 7, 0, Math.PI * 2);
    ctx.fill();
    ctx.fillStyle = C.coinDark;
    ctx.beginPath();
    ctx.arc(28, 23, 4, 0, Math.PI * 2);
    ctx.fill();

    // Score text
    ctx.fillStyle = '#fff';
    ctx.font = 'bold 16px "Segoe UI", Tahoma, sans-serif';
    ctx.fillText('× ' + score, 42, 28);

    // Total coins
    ctx.fillStyle = 'rgba(255,255,255,0.7)';
    ctx.font = '13px "Segoe UI", Tahoma, sans-serif';
    ctx.fillText(totalCoins + ' coins', 100, 28);
  }

  function drawDeathScreen() {
    ctx.fillStyle = C.winBg;
    ctx.fillRect(0, 0, W, H);
    ctx.fillStyle = '#f00';
    ctx.font = 'bold 48px "Segoe UI", Tahoma, sans-serif';
    ctx.textAlign = 'center';
    ctx.fillText('Game Over', W / 2, H / 2 - 20);
    ctx.fillStyle = '#fff';
    ctx.font = '20px "Segoe UI", Tahoma, sans-serif';
    ctx.fillText('Press R to restart', W / 2, H / 2 + 30);
    ctx.textAlign = 'left';

    if (keys['KeyR']) {
      resetPlayer();
    }
  }

  function drawWinScreen() {
    ctx.fillStyle = C.winBg;
    ctx.fillRect(0, 0, W, H);

    ctx.fillStyle = '#fc0';
    ctx.font = 'bold 48px "Segoe UI", Tahoma, sans-serif';
    ctx.textAlign = 'center';
    ctx.fillText('🎉 You Win! 🎉', W / 2, H / 2 - 50);

    ctx.fillStyle = '#fff';
    ctx.font = '28px "Segoe UI", Tahoma, sans-serif';
    ctx.fillText('Coins: ' + score + ' / ' + totalCoins, W / 2, H / 2 + 10);

    if (score >= totalCoins) {
      ctx.fillStyle = '#0f0';
      ctx.font = 'bold 22px "Segoe UI", Tahoma, sans-serif';
      ctx.fillText('★ Perfect! All coins collected! ★', W / 2, H / 2 + 50);
    }

    ctx.fillStyle = '#aaa';
    ctx.font = '18px "Segoe UI", Tahoma, sans-serif';
    ctx.fillText('Press R to play again', W / 2, H / 2 + 90);
    ctx.textAlign = 'left';

    if (keys['KeyR']) {
      resetPlayer();
    }
  }

  /* ================================================================
     MAIN LOOP
     ================================================================ */
  function gameLoop() {
    animFrame++;
    update();

    drawBackground();
    drawPlatforms();
    drawPipe();
    drawCoins();
    drawFlag();
    drawPlayer();
    drawHUD();

    if (!player.alive && !player.won) {
      drawDeathScreen();
    }
    if (player.won) {
      drawWinScreen();
    }

    requestAnimationFrame(gameLoop);
  }

  /* roundRect polyfill for older browsers */
  if (!ctx.roundRect) {
    CanvasRenderingContext2D.prototype.roundRect = function (x, y, w, h, r) {
      if (typeof r === 'number') r = [r];
      var rad = r[0] || 0;
      this.moveTo(x + rad, y);
      this.lineTo(x + w - rad, y);
      this.quadraticCurveTo(x + w, y, x + w, y + rad);
      this.lineTo(x + w, y + h - rad);
      this.quadraticCurveTo(x + w, y + h, x + w - rad, y + h);
      this.lineTo(x + rad, y + h);
      this.quadraticCurveTo(x, y + h, x, y + h - rad);
      this.lineTo(x, y + rad);
      this.quadraticCurveTo(x, y, x + rad, y);
      this.closePath();
    };
  }

  /* Start */
  resetPlayer();
  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; }
body {
  background: #222;
  display: flex;
  justify-content: center;
  align-items: center;
  height: 100vh;
  overflow: hidden;
  font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
}
canvas {
  border: 2px solid #444;
  display: block;
  background: #5c94fc;
}
</style>
</head>
<body>
<canvas id="game" width="800" height="450"></canvas>
<script src="game.js"></script>
</body>
</html>

rep 4 UNGRADED

pi 61.4s · verify 4.1s · 39880 tok · responded: qwen/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 — game.js
 *  Plain JS, no frameworks, no network requests.
 * ============================================================ */

/* ───────── 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;
}

/* ───────── constants ───────── */
const CANVAS_W = 800, CANVAS_H = 450;
const TILE = 32;
const GRAVITY = 0.55;
const FRICTION = 0.82;
const MAX_SPEED = 5;
const JUMP_FORCE = -11;

/* ───────── level map ─────────
 *  S = start, # = ground, B = block/platform, C = coin, F = flag/goal
 *  G = gap (no ground).
 */
const LEVEL = [
  "                                                                                ",
  "                                                                                ",
  "                                                                                ",
  "                                                                                ",
  "                                                                                ",
  "                                                C C C                           ",
  "                                      B B B B B B B B B   C                     ",
  "                                                                        B       ",
  "         C              C C                       C B B                      F  ",
  "       B B B       B B B    B B B     C B C   B B          B B B          B B   ",
  "                                              B B   C                        ",
  "  S  G   G   G    G   G   G    G   G   G  G  G  G  G    G   G   G    G   G  G  ",
  "################################################################################",
  "################################################################################",
];

/* ───────── state ───────── */
let canvas, ctx;
let camera = { x: 0, y: 0 };
let score = 0;
let won = false;
let particles = [];

const keys = {};

/* player object */
const player = {
  x: 0, y: 0, w: 22, h: 30,
  vx: 0, vy: 0,
  onGround: false,
  facing: 1,          // 1 = right, -1 = left
  frame: 0,           // animation frame
  frameTimer: 0,
};

let platforms = [];
let coins = [];
let flag = null;
let startPos = { x: 60, y: 300 };

/* ───────── build level ───────── */
function buildLevel() {
  platforms = [];
  coins = [];
  flag = null;

  for (let row = 0; row < LEVEL.length; row++) {
    const line = LEVEL[row];
    for (let col = 0; col < line.length; col++) {
      const ch = line[col];
      const x = col * TILE;
      const y = row * TILE;

      if (ch === '#') {
        platforms.push({ x, y, w: TILE, h: TILE, type: 'ground' });
      } else if (ch === 'B') {
        platforms.push({ x, y, w: TILE, h: TILE, type: 'brick' });
      } else if (ch === 'C') {
        coins.push({ x: x + 8, y: y + 8, w: 16, h: 16, collected: false, bobPhase: Math.random() * Math.PI * 2 });
      } else if (ch === 'F') {
        flag = { x, y: y - TILE, w: 8, h: TILE * 2 };
      } else if (ch === 'S') {
        startPos = { x: x + 4, y: y - TILE };
      }
    }
  }

  player.x = startPos.x;
  player.y = startPos.y;
  player.vx = 0;
  player.vy = 0;
}

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

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);
  }
}

/* ───────── input ───────── */
document.addEventListener('keydown', e => {
  keys[e.code] = true;
  if (['Space','ArrowUp','ArrowDown','ArrowLeft','ArrowRight'].includes(e.code)) {
    e.preventDefault();
  }
  if (won && e.code === 'Space') {
    resetGame();
  }
});
document.addEventListener('keyup', e => { keys[e.code] = false; });

/* ───────── update ───────── */
function update() {
  if (won) return;

  // movement
  const left = keys['ArrowLeft'] || keys['KeyA'];
  const right = keys['ArrowRight'] || keys['KeyD'];
  const jumpKey = keys['Space'] || keys['ArrowUp'] || keys['KeyW'];

  if (left)  { player.vx -= 1.2; player.facing = -1; }
  if (right) { player.vx += 1.2; player.facing = 1; }

  player.vx *= FRICTION;
  if (Math.abs(player.vx) < 0.1) player.vx = 0;
  if (player.vx > MAX_SPEED) player.vx = MAX_SPEED;
  if (player.vx < -MAX_SPEED) player.vx = -MAX_SPEED;

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

  // gravity
  player.vy += GRAVITY;
  if (player.vy > 14) player.vy = 14; // terminal velocity

  // horizontal movement + collision
  player.x += player.vx;
  for (const p of 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;
    }
  }

  // vertical movement + collision
  player.y += player.vy;
  player.onGround = false;
  for (const p of platforms) {
    if (aabb(player, p)) {
      if (player.vy > 0) {
        player.y = p.y - player.h;
        player.onGround = true;
        player.vy = 0;
        // landing particles
        if (player.vy > 3) spawnParticles(player.x + player.w / 2, player.y + player.h, '#ccc', 4);
      } else if (player.vy < 0) {
        player.y = p.y + p.h;
        player.vy = 0;
      }
    }
  }

  // prevent going off the left side
  if (player.x < 0) { player.x = 0; player.vx = 0; }

  // fall off the world — respawn
  if (player.y > LEVEL.length * TILE + 200) {
    player.x = startPos.x;
    player.y = startPos.y;
    player.vx = 0;
    player.vy = 0;
  }

  // collect coins
  for (const c of coins) {
    if (c.collected) continue;
    c.bobPhase += 0.06;
    const bobY = c.y + Math.sin(c.bobPhase) * 3;
    if (aabb(player, { x: c.x, y: bobY, w: c.w, h: c.h })) {
      c.collected = true;
      score++;
      spawnParticles(c.x + c.w / 2, bobY + c.h / 2, '#FFD700', 10);
    }
  }

  // flag / win
  if (flag && aabb(player, { x: flag.x, y: flag.y, w: flag.w, h: flag.h })) {
    won = true;
    spawnParticles(flag.x + flag.w / 2, flag.y + flag.h / 2, '#00FF00', 30);
    spawnParticles(flag.x + flag.w / 2, flag.y + flag.h / 2, '#FFD700', 20);
  }

  // animation
  player.frameTimer++;
  if (player.frameTimer > 8) {
    player.frame = (player.frame + 1) % 4;
    player.frameTimer = 0;
  }

  // camera
  const targetX = player.x + player.w / 2 - CANVAS_W / 2;
  camera.x += (targetX - camera.x) * 0.1;
  if (camera.x < 0) camera.x = 0;

  updateParticles();
}

/* ───────── drawing helpers ───────── */
function drawRoundRect(x, y, w, h, r) {
  ctx.beginPath();
  ctx.moveTo(x + r, y);
  ctx.lineTo(x + w - r, y);
  ctx.quadraticCurveTo(x + w, y, x + w, y + r);
  ctx.lineTo(x + w, y + h - r);
  ctx.quadraticCurveTo(x + w, y + h, x + w - r, y + h);
  ctx.lineTo(x + r, y + h);
  ctx.quadraticCurveTo(x, y + h, x, y + h - r);
  ctx.lineTo(x, y + r);
  ctx.quadraticCurveTo(x, y, x + r, y);
  ctx.closePath();
}

/* ───────── draw ───────── */
function draw() {
  // sky gradient
  const skyGrad = ctx.createLinearGradient(0, 0, 0, CANVAS_H);
  skyGrad.addColorStop(0, '#5C94FC');
  skyGrad.addColorStop(0.7, '#87CEEB');
  skyGrad.addColorStop(1, '#B0E0FF');
  ctx.fillStyle = skyGrad;
  ctx.fillRect(0, 0, CANVAS_W, CANVAS_H);

  // clouds (parallax — move slower)
  ctx.fillStyle = 'rgba(255,255,255,0.85)';
  const cloudOffset = -camera.x * 0.3;
  for (let i = 0; i < 8; i++) {
    const cx = ((i * 220 + 50) + cloudOffset) % (LEVEL.length * TILE + 400) - 200;
    const cy = 40 + (i % 3) * 50;
    ctx.beginPath();
    ctx.arc(cx, cy, 25, 0, Math.PI * 2);
    ctx.arc(cx + 25, cy - 8, 20, 0, Math.PI * 2);
    ctx.arc(cx + 50, cy, 22, 0, Math.PI * 2);
    ctx.arc(cx + 20, cy + 5, 18, 0, Math.PI * 2);
    ctx.fill();
  }

  // hills (parallax — medium speed)
  ctx.fillStyle = '#3DA34A';
  const hillOffset = -camera.x * 0.5;
  for (let i = 0; i < 6; i++) {
    const hx = ((i * 350 + 100) + hillOffset) % (LEVEL.length * TILE + 500) - 250;
    const hy = LEVEL.length * TILE - 32;
    ctx.beginPath();
    ctx.ellipse(hx, hy, 120 + (i % 2) * 40, 60 + (i % 3) * 20, 0, Math.PI, 0);
    ctx.fill();
  }

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

  // platforms
  for (const p of platforms) {
    if (p.x + p.w < camera.x - TILE || p.x > camera.x + CANVAS_W + TILE) continue;

    if (p.type === 'ground') {
      // green top
      ctx.fillStyle = '#4CAF50';
      ctx.fillRect(p.x, p.y, p.w, 6);
      // brown dirt
      ctx.fillStyle = '#8B5E3C';
      ctx.fillRect(p.x, p.y + 6, p.w, p.h - 6);
      // dirt speckles
      ctx.fillStyle = '#7A5230';
      ctx.fillRect(p.x + 4, p.y + 12, 4, 4);
      ctx.fillRect(p.x + 18, p.y + 20, 3, 3);
    } else {
      // brick
      ctx.fillStyle = '#C85A3C';
      ctx.fillRect(p.x, p.y, p.w, p.h);
      ctx.fillStyle = '#A84430';
      ctx.fillRect(p.x + 1, p.y + 1, p.w - 2, 2);
      ctx.fillRect(p.x + 1, p.y + p.h / 2, p.w - 2, 2);
      ctx.fillRect(p.x + p.w / 2 - 1, p.y, 2, p.h);
    }
  }

  // coins
  for (const c of coins) {
    if (c.collected) continue;
    const bobY = c.y + Math.sin(c.bobPhase) * 3;
    // spin effect
    const scaleX = Math.abs(Math.cos(c.bobPhase * 0.7));
    ctx.save();
    ctx.translate(c.x + c.w / 2, bobY + c.h / 2);
    ctx.scale(scaleX || 0.1, 1);
    ctx.beginPath();
    ctx.arc(0, 0, 8, 0, Math.PI * 2);
    ctx.fillStyle = '#FFD700';
    ctx.fill();
    ctx.strokeStyle = '#DAA520';
    ctx.lineWidth = 2;
    ctx.stroke();
    ctx.restore();
  }

  // flag
  if (flag) {
    // pole
    ctx.fillStyle = '#888';
    ctx.fillRect(flag.x, flag.y, 4, flag.h);
    // ball on top
    ctx.beginPath();
    ctx.arc(flag.x + 2, flag.y, 5, 0, Math.PI * 2);
    ctx.fillStyle = '#FFD700';
    ctx.fill();
    // flag cloth
    const flagWave = Math.sin(Date.now() / 200) * 3;
    ctx.beginPath();
    ctx.moveTo(flag.x + 4, flag.y + 4);
    ctx.lineTo(flag.x + 34 + flagWave, flag.y + 14);
    ctx.lineTo(flag.x + 4, flag.y + 28);
    ctx.closePath();
    ctx.fillStyle = '#E53935';
    ctx.fill();
    ctx.strokeStyle = '#B71C1C';
    ctx.lineWidth = 1;
    ctx.stroke();
    // star on flag
    ctx.fillStyle = '#FFD700';
    ctx.font = '10px sans-serif';
    ctx.fillText('★', flag.x + 12 + flagWave * 0.5, flag.y + 21);
  }

  // player
  drawPlayer();

  // particles
  for (const p of particles) {
    const 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;

  ctx.restore();

  // HUD
  drawHUD();
}

/* ───────── draw player character ───────── */
function drawPlayer() {
  const px = Math.round(player.x);
  const py = Math.round(player.y);
  const 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));

  // body (red shirt)
  ctx.fillStyle = '#E53935';
  drawRoundRect(2, 12, 18, 14, 3);
  ctx.fill();

  // head
  ctx.fillStyle = '#FFCC80';
  ctx.beginPath();
  ctx.arc(11, 8, 8, 0, Math.PI * 2);
  ctx.fill();

  // hat
  ctx.fillStyle = '#E53935';
  ctx.beginPath();
  ctx.ellipse(11, 3, 9, 5, 0, Math.PI, 0);
  ctx.fill();
  // hat brim
  ctx.fillRect(2, 4, 18, 3);

  // hat logo (white circle)
  ctx.fillStyle = '#FFF';
  ctx.beginPath();
  ctx.arc(11, 2, 3, 0, Math.PI * 2);
  ctx.fill();
  ctx.fillStyle = '#E53935';
  ctx.beginPath();
  ctx.arc(11, 2, 1.5, 0, Math.PI * 2);
  ctx.fill();

  // eyes
  ctx.fillStyle = '#333';
  ctx.fillRect(13, 6, 2, 3);

  // mustache
  ctx.fillStyle = '#5D4037';
  ctx.fillRect(10, 10, 7, 2);

  // legs
  const legAnim = player.onGround ? Math.sin(player.frame * Math.PI / 2) * 4 : 3;
  ctx.fillStyle = '#1565C0';
  ctx.fillRect(3, 26, 7, 6);
  ctx.fillRect(12, 26, 7, 6);

  // shoes
  ctx.fillStyle = '#5D4037';
  const shoeOff = player.onGround ? legAnim : 0;
  ctx.fillRect(1 - shoeOff, 29, 8, 3);
  ctx.fillRect(13 + shoeOff, 29, 8, 3);

  // arms
  ctx.fillStyle = '#FFCC80';
  const armSwing = player.onGround ? Math.sin(player.frame * Math.PI / 2) * 5 : -3;
  ctx.fillRect(-2, 14 + armSwing, 5, 8);
  ctx.fillRect(19, 14 - armSwing, 5, 8);

  ctx.restore();
}

/* ───────── draw HUD ───────── */
function drawHUD() {
  // semi-transparent bar
  ctx.fillStyle = 'rgba(0,0,0,0.45)';
  drawRoundRect(10, 8, 180, 36, 8);
  ctx.fill();

  // coin icon
  ctx.fillStyle = '#FFD700';
  ctx.beginPath();
  ctx.arc(28, 26, 10, 0, Math.PI * 2);
  ctx.fill();
  ctx.fillStyle = '#DAA520';
  ctx.font = 'bold 11px sans-serif';
  ctx.fillText('$', 24, 30);

  // score
  ctx.fillStyle = '#FFF';
  ctx.font = 'bold 18px monospace';
  ctx.fillText(score + ' / ' + coins.length, 46, 32);

  // win overlay
  if (won) {
    ctx.fillStyle = 'rgba(0,0,0,0.55)';
    ctx.fillRect(0, 0, CANVAS_W, CANVAS_H);

    ctx.fillStyle = '#FFD700';
    ctx.font = 'bold 48px sans-serif';
    ctx.textAlign = 'center';
    ctx.fillText('🎉 YOU WIN! 🎉', CANVAS_W / 2, CANVAS_H / 2 - 20);

    ctx.fillStyle = '#FFF';
    ctx.font = '22px sans-serif';
    ctx.fillText('Coins collected: ' + score + ' / ' + coins.length, CANVAS_W / 2, CANVAS_H / 2 + 25);
    ctx.fillText('Press SPACE to play again', CANVAS_W / 2, CANVAS_H / 2 + 60);

    ctx.textAlign = 'left';
  }
}

/* ───────── reset ───────── */
function resetGame() {
  score = 0;
  won = false;
  particles = [];
  buildLevel();
}

/* ───────── game loop ───────── */
function gameLoop() {
  update();
  draw();
  requestAnimationFrame(gameLoop);
}

/* ───────── init ───────── */
window.addEventListener('load', () => {
  canvas = document.getElementById('game');
  if (!canvas) return;
  ctx = canvas.getContext('2d');

  canvas.width = CANVAS_W;
  canvas.height = CANVAS_H;

  buildLevel();
  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: #1a1a2e;
      display: flex;
      align-items: center;
      justify-content: center;
      font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
      overflow: hidden;
    }

    #game {
      border: 3px solid #333;
      border-radius: 8px;
      box-shadow: 0 0 30px rgba(92, 148, 252, 0.3);
      display: block;
      image-rendering: pixelated;
    }
  </style>
</head>
<body>
  <canvas id="game"></canvas>
  <script src="game.js"></script>
</body>
</html>

rep 5 UNGRADED

pi 109.2s · verify 4.2s · 41479 tok · responded: qwen/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 Pixel Platformer — game.js
   Plain vanilla JS, no frameworks, no external assets.
   ================================================================ */

(() => {
  "use strict";

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

  // ─── Constants ─────────────────────────────────────────────────
  const TILE = 32;
  const GRAVITY = 0.55;
  const FRICTION = 0.82;
  const MOVE_ACCEL = 0.6;
  const MAX_SPEED = 4.5;
  const JUMP_VEL = -11.5;
  const BOUNCE_VEL = -7;       // velocity on enemy stomp

  // ─── Level map (characters → meaning) ─────────────────────────
  //  g = ground, b = brick, q = question block, c = coin
  //  e = enemy, f = flagpole, x = flag head, p = pipe top
  //  n = pipe body, P = platform (floating), s = stone step
  //  # = wall (right edge),  . = empty
  const LEVEL_RAW = [
    "............................................................................................................................",
    "............................................................................................................................",
    "............................................................................................................................",
    "............................................................................................................................",
    "............................................................................................................................",
    "...............................................................cccc.........................................................",
    "............................................................................................................................",
    "...................bqbbqbb......................................P.....P......PP..PP............................................",
    "............................................................................................................................",
    "...........cc..................................cc.................b.....b............................................b.......",
    "............................................................................................................................",
    "..........bbbbb................................bbbb.........e...P.....P......PP..PP.........................................",
    "............................................................................................................................",
    "....................................bqbbqbb.......e.....................b..b.................................bbbbbbb.......",
    "............................................................................................................................",
    "............................................................................................................................",
    "..................................................nn..........................................nn................................",
    "..................................................pp....e...........e....pp..........e........................pp................",
    "##############################################################################################################",
  ];

  // Derived level dimensions
  const COLS = LEVEL_RAW[0].length;  // 120
  const ROWS = LEVEL_RAW.length;     // 18
  const LEVEL_W = COLS * TILE;
  const LEVEL_H = ROWS * TILE;

  // ─── Build world objects ───────────────────────────────────────
  let blocks = [];    // {x, y, type, hit}
  let coins = [];     // {x, y, collected}
  let enemies = [];   // {x, y, vx, type, alive, squishTimer}
  let particles = []; // visual juice
  let score = 0;
  let flagX = -1;     // x coordinate of the flagpole
  let levelOver = false;

  function buildLevel() {
    blocks = [];
    coins = [];
    enemies = [];
    particles = [];
    score = 0;
    flagX = -1;
    levelOver = false;

    for (let r = 0; r < ROWS; r++) {
      const row = LEVEL_RAW[r];
      for (let c = 0; c < row.length; c++) {
        const ch = row[c];
        const x = c * TILE;
        const y = r * TILE;

        if (ch === "b") {
          blocks.push({ x, y, type: "brick", hit: false });
        } else if (ch === "q") {
          blocks.push({ x, y, type: "question", hit: false });
        } else if (ch === "p") {
          blocks.push({ x, y, type: "pipe" });
        } else if (ch === "n") {
          blocks.push({ x, y, type: "pipeBody" });
        } else if (ch === "P") {
          blocks.push({ x, y, type: "platform" });
        } else if (ch === "s") {
          blocks.push({ x, y, type: "stone" });
        } else if (ch === "c") {
          coins.push({ x: x + 8, y: y + 4, w: 16, h: 24, collected: false, bobOffset: Math.random() * Math.PI * 2 });
        } else if (ch === "e") {
          enemies.push({ x, y, w: TILE, h: TILE, vx: -1.2, vy: 0, type: "goomba", alive: true, squishTimer: 0 });
        } else if (ch === "f") {
          blocks.push({ x, y, type: "flagpole" });
        } else if (ch === "x") {
          blocks.push({ x, y, type: "flag" });
          flagX = x;
        }
      }
    }
  }

  // ─── Player ────────────────────────────────────────────────────
  const player = {
    x: 80, y: 0,           // start pos
    w: 22, h: 30,
    vx: 0, vy: 0,
    onGround: false,
    facing: 1,             // 1 = right, -1 = left
    walkFrame: 0,
    walkTimer: 0,
    dead: false,
    deathTimer: 0,
    won: false,
    winTimer: 0,
  };

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

  // ─── Input ─────────────────────────────────────────────────────
  const keys = {};
  document.addEventListener("keydown", (e) => {
    keys[e.code] = true;
    // Prevent scrolling
    if (["Space", "ArrowUp", "ArrowDown", "ArrowLeft", "ArrowRight"].includes(e.code)) {
      e.preventDefault();
    }
  });
  document.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"]; }

  // ─── Collision helpers ─────────────────────────────────────────
  function aabb(ax, ay, aw, ah, bx, by, bw, bh) {
    return ax < bx + bw && ax + aw > bx && ay < by + bh && ay + ah > by;
  }

  // Get all solid blocks
  function getSolids() {
    return blocks.filter(b => b.type !== "flag" && b.type !== "flagpole");
  }

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

  function spawnCoinParticles(x, y) {
    spawnParticles(x, y, "#FFD700", 8);
  }

  function spawnBrickParticles(x, y) {
    const colors = ["#C84C09", "#E09050", "#A06030"];
    for (let i = 0; i < 10; i++) {
      particles.push({
        x: x + Math.random() * TILE,
        y: y + Math.random() * TILE,
        vx: (Math.random() - 0.5) * 7,
        vy: -Math.random() * 8 - 3,
        life: 40 + Math.random() * 20,
        maxLife: 60,
        color: colors[Math.floor(Math.random() * colors.length)],
        size: 3 + Math.random() * 4,
      });
    }
  }

  // ─── Update ────────────────────────────────────────────────────
  function update() {
    if (player.dead) {
      player.deathTimer++;
      player.vy += GRAVITY;
      player.y += player.vy;
      if (player.deathTimer > 90) {
        // Restart
        player.x = 80;
        player.y = 200;
        player.vx = 0;
        player.vy = 0;
        player.dead = false;
        player.deathTimer = 0;
        player.onGround = false;
        camera.x = 0;
        buildLevel();
      }
      return;
    }

    if (player.won) {
      player.winTimer++;
      return;
    }

    // ── Horizontal movement ──
    if (isLeft()) {
      player.vx -= MOVE_ACCEL;
      player.facing = -1;
    }
    if (isRight()) {
      player.vx += MOVE_ACCEL;
      player.facing = 1;
    }

    // Clamp speed
    player.vx = Math.max(-MAX_SPEED, Math.min(MAX_SPEED, player.vx));

    // Friction
    if (!isLeft() && !isRight()) {
      player.vx *= FRICTION;
      if (Math.abs(player.vx) < 0.1) player.vx = 0;
    }

    // Walk animation
    if (player.onGround && Math.abs(player.vx) > 0.5) {
      player.walkTimer += Math.abs(player.vx);
      if (player.walkTimer > 8) {
        player.walkTimer = 0;
        player.walkFrame = (player.walkFrame + 1) % 4;
      }
    } else if (player.onGround) {
      player.walkFrame = 0;
    }

    // Apply horizontal velocity
    player.x += player.vx;

    // Horizontal collision with solids
    const solids = getSolids();
    for (const b of solids) {
      if (aabb(player.x, player.y, player.w, player.h, b.x, b.y, TILE, TILE)) {
        if (player.vx > 0) {
          player.x = b.x - player.w;
        } else if (player.vx < 0) {
          player.x = b.x + TILE;
        }
        player.vx = 0;
      }
    }

    // ── Vertical movement ──
    player.vy += GRAVITY;
    if (player.vy > 12) player.vy = 12;

    // Jump
    if (isJump() && player.onGround) {
      player.vy = JUMP_VEL;
      player.onGround = false;
    }

    player.y += player.vy;
    player.onGround = false;

    // Vertical collision with solids
    for (const b of solids) {
      if (aabb(player.x, player.y, player.w, player.h, b.x, b.y, TILE, TILE)) {
        if (player.vy > 0) {
          // Landing on top
          player.y = b.y - player.h;
          player.vy = 0;
          player.onGround = true;
        } else if (player.vy < 0) {
          // Hitting from below
          player.y = b.y + TILE;
          player.vy = 0;

          // Hit question block or brick
          if (b.type === "question" && !b.hit) {
            b.hit = true;
            score += 50;
            spawnCoinParticles(b.x + 8, b.y);
            // Spawn floating coin text
            particles.push({
              x: b.x + 8, y: b.y - 10,
              vx: 0, vy: -1.5, life: 40, maxLife: 40,
              color: "#FFD700", size: 0, text: "+50"
            });
          } else if (b.type === "brick") {
            spawnBrickParticles(b.x, b.y);
            // Break brick
            b.type = "broken";
            score += 10;
          }
        }
      }
    }

    // ── Coins ──
    for (const c of coins) {
      if (c.collected) continue;
      if (aabb(player.x, player.y, player.w, player.h, c.x, c.y, c.w, c.h)) {
        c.collected = true;
        score += 10;
        spawnCoinParticles(c.x + 8, c.y + 12);
      }
    }

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

      // Only update if near camera
      if (Math.abs(e.x - camera.x) > W + 200) continue;

      e.vy += GRAVITY;
      e.x += e.vx;
      e.y += e.vy;

      // Ground collision for enemies
      let enemyOnGround = false;
      for (const b of solids) {
        if (b.type === "flag" || b.type === "flagpole") continue;
        if (aabb(e.x, e.y, e.w, e.h, b.x, b.y, TILE, TILE)) {
          if (e.vy > 0) {
            e.y = b.y - e.h;
            e.vy = 0;
            enemyOnGround = true;
          } else if (e.vy < 0) {
            e.y = b.y + TILE;
            e.vy = 0;
          }
          if (e.vx > 0 && aabb(e.x, e.y, e.w, e.h, b.x, b.y, TILE, TILE)) {
            e.x = b.x - e.w;
            e.vx = -e.vx;
          } else if (e.vx < 0 && aabb(e.x, e.y, e.w, e.h, b.x, b.y, TILE, TILE)) {
            e.x = b.x + TILE;
            e.vx = -e.vx;
          }
        }
      }

      // Enemy-player collision
      if (aabb(player.x, player.y, player.w, player.h, e.x + 2, e.y + 2, e.w - 4, e.h - 4)) {
        if (player.vy > 0 && player.y + player.h - e.y < 16) {
          // Stomp!
          e.alive = false;
          e.squishTimer = 30;
          player.vy = BOUNCE_VEL;
          score += 100;
          spawnParticles(e.x + e.w / 2, e.y + e.h / 2, "#CC4400", 6);
          particles.push({
            x: e.x + e.w / 2, y: e.y - 5,
            vx: 0, vy: -2, life: 40, maxLife: 40,
            color: "#FFF", size: 0, text: "+100"
          });
        } else if (!player.dead) {
          // Player hit by enemy
          playerDie();
        }
      }
    }

    // ── Flag / Goal ──
    if (flagX > 0) {
      const poleX = flagX;
      const poleTop = blocks.find(b => b.type === "flag")?.y ?? 0;
      if (player.x + player.w > poleX && player.x < poleX + TILE && player.y + player.h > poleTop) {
        player.won = true;
        player.winTimer = 0;
        score += 500;
        // Big celebration
        for (let i = 0; i < 30; i++) {
          setTimeout(() => {
            spawnParticles(
              poleX + Math.random() * TILE,
              poleTop + Math.random() * (LEVEL_H - poleTop),
              ["#FF0", "#F0F", "#0FF", "#F00", "#0F0"][Math.floor(Math.random() * 5)],
              3
            );
          }, i * 50);
        }
      }
    }

    // ── Fall death ──
    if (player.y > LEVEL_H + 64) {
      playerDie();
    }

    // ── 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.15;
      p.life--;
      if (p.life <= 0) particles.splice(i, 1);
    }

    // ── Camera ──
    const targetX = player.x - W / 3;
    camera.x += (targetX - camera.x) * 0.1;
    camera.x = Math.max(0, Math.min(LEVEL_W - W, camera.x));
  }

  function playerDie() {
    if (player.dead) return;
    player.dead = true;
    player.deathTimer = 0;
    player.vy = -10;
    player.vx = 0;
  }

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

  // Pre-render sky gradient
  const skyGrad = ctx.createLinearGradient(0, 0, 0, H);
  skyGrad.addColorStop(0, "#5C94FC");
  skyGrad.addColorStop(0.7, "#87CEEB");
  skyGrad.addColorStop(1, "#B0E0FF");

  function drawCloud(cx, cy, scale) {
    const s = scale || 1;
    ctx.fillStyle = "rgba(255,255,255,0.9)";
    ctx.beginPath();
    ctx.arc(cx, cy, 18 * s, 0, Math.PI * 2);
    ctx.arc(cx + 18 * s, cy - 6 * s, 14 * s, 0, Math.PI * 2);
    ctx.arc(cx + 34 * s, cy, 16 * s, 0, Math.PI * 2);
    ctx.arc(cx + 16 * s, cy + 4 * s, 12 * s, 0, Math.PI * 2);
    ctx.fill();
  }

  function drawHill(baseX, baseY, width, height) {
    ctx.fillStyle = "#5DA84A";
    ctx.beginPath();
    ctx.moveTo(baseX - width / 2, baseY);
    ctx.quadraticCurveTo(baseX, baseY - height, baseX + width / 2, baseY);
    ctx.fill();
    // Highlight
    ctx.fillStyle = "#6DC05A";
    ctx.beginPath();
    ctx.moveTo(baseX - width / 4, baseY);
    ctx.quadraticCurveTo(baseX, baseY - height, baseX + width / 4, baseY);
    ctx.fill();
  }

  function drawBrickBlock(bx, by) {
    ctx.fillStyle = "#C84C09";
    ctx.fillRect(bx, by, TILE, TILE);
    // Brick pattern
    ctx.strokeStyle = "#8B3000";
    ctx.lineWidth = 1;
    ctx.strokeRect(bx + 0.5, by + 0.5, TILE - 1, TILE - 1);
    ctx.beginPath();
    ctx.moveTo(bx, by + TILE / 2);
    ctx.lineTo(bx + TILE, by + TILE / 2);
    ctx.moveTo(bx + TILE / 2, by);
    ctx.lineTo(bx + TILE / 2, by + TILE / 2);
    ctx.moveTo(bx + TILE / 4, by + TILE / 2);
    ctx.lineTo(bx + TILE / 4, by + TILE);
    ctx.moveTo(bx + TILE * 3 / 4, by + TILE / 2);
    ctx.lineTo(bx + TILE * 3 / 4, by + TILE);
    ctx.stroke();
    // Highlight
    ctx.fillStyle = "rgba(255,255,255,0.15)";
    ctx.fillRect(bx + 1, by + 1, TILE - 2, 3);
  }

  function drawQuestionBlock(bx, by, hit) {
    if (hit) {
      ctx.fillStyle = "#8B6914";
      ctx.fillRect(bx, by, TILE, TILE);
      ctx.strokeStyle = "#6B4F10";
      ctx.lineWidth = 1;
      ctx.strokeRect(bx + 0.5, by + 0.5, TILE - 1, TILE - 1);
    } else {
      // Animated shimmer
      const shimmer = Math.sin(Date.now() / 300) * 0.15 + 0.85;
      ctx.fillStyle = `rgb(${Math.floor(255 * shimmer)}, ${Math.floor(200 * shimmer)}, 0)`;
      ctx.fillRect(bx, by, TILE, TILE);
      ctx.strokeStyle = "#B8860B";
      ctx.lineWidth = 1;
      ctx.strokeRect(bx + 0.5, by + 0.5, TILE - 1, TILE - 1);
      // Question mark
      ctx.fillStyle = "#FFF";
      ctx.font = "bold 18px 'Courier New', monospace";
      ctx.textAlign = "center";
      ctx.textBaseline = "middle";
      ctx.fillText("?", bx + TILE / 2, by + TILE / 2 + 1);
    }
  }

  function drawPipe(bx, by) {
    // Pipe body
    ctx.fillStyle = "#2E8B2E";
    ctx.fillRect(bx + 2, by, TILE - 4, TILE);
    // Highlight
    ctx.fillStyle = "#4CBB3D";
    ctx.fillRect(bx + 4, by, 6, TILE);
    // Outline
    ctx.strokeStyle = "#1A5C1A";
    ctx.lineWidth = 1;
    ctx.strokeRect(bx + 0.5, by + 0.5, TILE - 1, TILE - 1);
  }

  function drawPipeTop(bx, by) {
    ctx.fillStyle = "#2E8B2E";
    ctx.fillRect(bx - 2, by, TILE + 4, TILE);
    ctx.fillStyle = "#4CBB3D";
    ctx.fillRect(bx + 2, by, 8, TILE);
    ctx.strokeStyle = "#1A5C1A";
    ctx.lineWidth = 1;
    ctx.strokeRect(bx - 2.5, by + 0.5, TILE + 3, TILE - 1);
    // Highlight line
    ctx.fillStyle = "#6DD66D";
    ctx.fillRect(bx + 12, by + 2, 4, TILE - 4);
  }

  function drawPlatform(bx, by) {
    ctx.fillStyle = "#A0522D";
    ctx.fillRect(bx, by, TILE, TILE * 0.6);
    ctx.fillStyle = "#6B8E23";
    ctx.fillRect(bx, by, TILE, 4);
    ctx.strokeStyle = "#654321";
    ctx.lineWidth = 1;
    ctx.strokeRect(bx + 0.5, by + 0.5, TILE - 1, TILE * 0.6 - 1);
  }

  function drawStone(bx, by) {
    ctx.fillStyle = "#808080";
    ctx.fillRect(bx, by, TILE, TILE);
    ctx.strokeStyle = "#606060";
    ctx.lineWidth = 1;
    ctx.strokeRect(bx + 0.5, by + 0.5, TILE - 1, TILE - 1);
    ctx.fillStyle = "rgba(255,255,255,0.1)";
    ctx.fillRect(bx + 1, by + 1, TILE - 2, 3);
  }

  function drawFlagpole(bx, by) {
    ctx.fillStyle = "#666";
    ctx.fillRect(bx + TILE / 2 - 2, by, 4, TILE);
    // Ball on top
    if (by < TILE * 2) {
      ctx.fillStyle = "#FFD700";
      ctx.beginPath();
      ctx.arc(bx + TILE / 2, by + 3, 5, 0, Math.PI * 2);
      ctx.fill();
    }
  }

  function drawFlag(bx, by) {
    // Already drawn flagpole below
    // Draw the flag cloth
    const wave = Math.sin(Date.now() / 400) * 2;
    ctx.fillStyle = "#E03030";
    ctx.beginPath();
    ctx.moveTo(bx + 2, by + 4);
    ctx.lineTo(bx + TILE - 4 + wave, by + 8);
    ctx.lineTo(bx + TILE - 2 + wave, by + 18);
    ctx.lineTo(bx + 2, by + TILE - 4);
    ctx.closePath();
    ctx.fill();
    // Star on flag
    ctx.fillStyle = "#FFD700";
    ctx.font = "12px serif";
    ctx.textAlign = "center";
    ctx.fillText("★", bx + TILE / 2 + wave, by + 18);
  }

  function drawCoin(cx, cy, offset, time) {
    const bob = Math.sin(time + offset) * 3;
    const stretch = Math.abs(Math.sin(time * 3 + offset * 0.7));
    ctx.fillStyle = "#FFD700";
    ctx.beginPath();
    ctx.ellipse(cx + 8, cy + 12 + bob, 6 * stretch + 1, 10, 0, 0, Math.PI * 2);
    ctx.fill();
    ctx.fillStyle = "#FFF8DC";
    ctx.beginPath();
    ctx.ellipse(cx + 6, cy + 10 + bob, 2 * stretch + 0.5, 4, 0, 0, Math.PI * 2);
    ctx.fill();
  }

  function drawPlayer(px, py) {
    const p = player;
    const f = p.facing;

    // Body
    ctx.fillStyle = "#E03030";       // Red shirt
    ctx.fillRect(px + 4, py + 10, 14, 12);

    // Head
    ctx.fillStyle = "#FFB880";        // Skin
    ctx.fillRect(px + 5, py + 2, 12, 10);

    // Hat
    ctx.fillStyle = "#E03030";
    ctx.fillRect(px + 3, py, 14, 5);
    ctx.fillRect(px + (f > 0 ? 10 : 2), py + 2, 10, 3);

    // Eyes
    ctx.fillStyle = "#000";
    const eyeX = px + (f > 0 ? 13 : 6);
    ctx.fillRect(eyeX, py + 5, 2, 3);

    // Mustache
    ctx.fillStyle = "#4A2800";
    ctx.fillRect(px + 6, py + 9, 10, 2);

    // Overalls
    ctx.fillStyle = "#2050D0";
    ctx.fillRect(px + 3, py + 18, 16, 8);

    // Overalls buttons
    ctx.fillStyle = "#FFD700";
    ctx.fillRect(px + 7, py + 20, 2, 2);
    ctx.fillRect(px + 13, py + 20, 2, 2);

    // Legs
    const legOffset = p.onGround ? Math.sin(p.walkFrame * Math.PI / 2) * 3 : 2;
    ctx.fillStyle = "#2050D0";
    ctx.fillRect(px + 3, py + 26, 7, 4 + legOffset);
    ctx.fillRect(px + 12, py + 26, 7, 4 - legOffset);

    // Shoes
    ctx.fillStyle = "#8B4513";
    ctx.fillRect(px + 2, py + 28 + legOffset, 8, 2);
    ctx.fillRect(px + 12, py + 28 - legOffset, 8, 2);

    // Arms
    ctx.fillStyle = "#FFB880";
    if (p.onGround) {
      const armSwing = Math.sin(p.walkFrame * Math.PI / 2) * 4;
      ctx.fillRect(px + (f > 0 ? 16 : -2), py + 12 + armSwing, 4, 6);
      ctx.fillRect(px + (f > 0 ? -2 : 16), py + 12 - armSwing, 4, 6);
    } else {
      // Jumping pose - arms up
      ctx.fillRect(px + 2, py + 6, 4, 6);
      ctx.fillRect(px + 16, py + 6, 4, 6);
    }
  }

  function drawGoomba(gx, gy) {
    // Body (mushroom shape)
    ctx.fillStyle = "#A0522D";
    ctx.beginPath();
    ctx.arc(gx + 16, gy + 12, 13, Math.PI, 0);
    ctx.fill();

    // Head bottom
    ctx.fillStyle = "#DEB887";
    ctx.fillRect(gx + 4, gy + 16, 24, 12);

    // Eyes
    ctx.fillStyle = "#FFF";
    ctx.fillRect(gx + 7, gy + 10, 6, 7);
    ctx.fillRect(gx + 19, gy + 10, 6, 7);
    ctx.fillStyle = "#000";
    const pupilOffset = Math.sin(Date.now() / 200) * 1;
    ctx.fillRect(gx + 10, gy + 12 + pupilOffset, 3, 4);
    ctx.fillRect(gx + 21, gy + 12 + pupilOffset, 3, 4);

    // Angry eyebrows
    ctx.fillStyle = "#000";
    ctx.fillRect(gx + 7, gy + 8, 7, 2);
    ctx.fillRect(gx + 18, gy + 8, 7, 2);

    // Feet
    const walkBob = Math.sin(Date.now() / 100) * 2;
    ctx.fillStyle = "#000";
    ctx.fillRect(gx + 2, gy + 26 + walkBob, 8, 6);
    ctx.fillRect(gx + 22, gy + 26 - walkBob, 8, 6);
  }

  function drawSquishedGoomba(gx, gy) {
    ctx.fillStyle = "#A0522D";
    ctx.fillRect(gx + 2, gy + 26, 28, 6);
    ctx.fillStyle = "#000";
    ctx.fillRect(gx + 7, gy + 27, 3, 3);
    ctx.fillRect(gx + 19, gy + 27, 3, 3);
  }

  function drawHUD() {
    // Top bar background
    ctx.fillStyle = "rgba(0,0,0,0.4)";
    ctx.fillRect(0, 0, W, 36);

    // Coins
    ctx.fillStyle = "#FFD700";
    ctx.font = "bold 18px 'Courier New', monospace";
    ctx.textAlign = "left";
    ctx.textBaseline = "middle";
    ctx.fillText("🪙 " + score, 16, 18);

    // Level info
    ctx.fillStyle = "#FFF";
    ctx.textAlign = "center";
    ctx.fillText("WASD / Arrow Keys to move — Space / Up to jump", W / 2, 18);

    // Enemies left
    const aliveEnemies = enemies.filter(e => e.alive).length;
    ctx.textAlign = "right";
    ctx.fillStyle = "#FF6B6B";
    ctx.fillText(`👾 × ${aliveEnemies}`, W - 16, 18);
  }

  function drawGameOver() {
    ctx.fillStyle = "rgba(0,0,0,0.6)";
    ctx.fillRect(0, 0, W, H);
    ctx.fillStyle = "#FFF";
    ctx.font = "bold 48px 'Courier New', monospace";
    ctx.textAlign = "center";
    ctx.textBaseline = "middle";
    ctx.fillText("GAME OVER", W / 2, H / 2 - 20);
    ctx.font = "20px 'Courier New', monospace";
    ctx.fillStyle = "#CCC";
    ctx.fillText("Score: " + score, W / 2, H / 2 + 20);
    ctx.fillText("Press SPACE to restart", W / 2, H / 2 + 55);

    if (keys["Space"]) {
      buildLevel();
      player.x = 80;
      player.y = 200;
      player.vx = 0;
      player.vy = 0;
      player.dead = false;
      camera.x = 0;
    }
  }

  function drawWinScreen() {
    const t = player.winTimer;
    ctx.fillStyle = "rgba(0,0,0,0.5)";
    ctx.fillRect(0, 0, W, H);

    ctx.fillStyle = "#FFD700";
    ctx.font = "bold 52px 'Courier New', monospace";
    ctx.textAlign = "center";
    ctx.textBaseline = "middle";
    ctx.fillText("🎉 YOU WIN! 🎉", W / 2, H / 2 - 40);

    ctx.fillStyle = "#FFF";
    ctx.font = "bold 28px 'Courier New', monospace";
    ctx.fillText("Score: " + score, W / 2, H / 2 + 10);

    ctx.font = "20px 'Courier New', monospace";
    ctx.fillStyle = "#CCC";
    ctx.fillText("Press SPACE to play again", W / 2, H / 2 + 50);

    if (keys["Space"]) {
      buildLevel();
      player.x = 80;
      player.y = 200;
      player.vx = 0;
      player.vy = 0;
      player.won = false;
      player.winTimer = 0;
      camera.x = 0;
    }
  }

  function render() {
    const time = Date.now() / 1000;
    const camX = Math.round(camera.x);

    // Sky
    ctx.fillStyle = skyGrad;
    ctx.fillRect(0, 0, W, H);

    // ─── Background parallax layers ───
    // Far hills
    ctx.save();
    ctx.translate(-Math.floor(camX * 0.15) % W, 0);
    for (let i = -1; i < 3; i++) {
      drawHill(i * 350 + 100, H - 40, 300, 100);
      drawHill(i * 350 + 300, H - 40, 200, 70);
    }
    ctx.restore();

    // Near hills
    ctx.save();
    ctx.translate(-Math.floor(camX * 0.35) % W, 0);
    for (let i = -1; i < 4; i++) {
      drawHill(i * 280 + 50, H - 20, 220, 60);
    }
    ctx.restore();

    // Clouds
    ctx.save();
    ctx.translate(-Math.floor(camX * 0.1), 0);
    drawCloud(100, 60, 1.2);
    drawCloud(400, 40, 0.8);
    drawCloud(700, 70, 1.0);
    drawCloud(1050, 50, 1.3);
    drawCloud(1400, 65, 0.9);
    drawCloud(1750, 45, 1.1);
    drawCloud(2100, 55, 1.0);
    drawCloud(2500, 70, 1.2);
    drawCloud(2900, 40, 0.8);
    ctx.restore();

    // ─── World ───
    ctx.save();
    ctx.translate(-camX, 0);

    // Ground
    ctx.fillStyle = "#5DA84A";
    ctx.fillRect(0, (ROWS - 2) * TILE, LEVEL_W, TILE * 2);
    // Ground detail
    ctx.fillStyle = "#4A8A3A";
    ctx.fillRect(0, (ROWS - 2) * TILE, LEVEL_W, 6);
    // Dirt layers
    ctx.fillStyle = "#8B6914";
    for (let r = ROWS - 1; r < ROWS; r++) {
      for (let c = 0; c < COLS; c++) {
        const bx = c * TILE;
        const by = r * TILE;
        // Random dirt texture
        if ((c + r * 7) % 5 === 0) {
          ctx.fillStyle = "#7A5A10";
          ctx.fillRect(bx + 4, by + 4, 8, 6);
        }
        if ((c * 3 + r * 11) % 7 === 0) {
          ctx.fillStyle = "#9A7A20";
          ctx.fillRect(bx + 18, by + 16, 10, 8);
        }
      }
    }

    // Blocks
    for (const b of blocks) {
      const bx = b.x, by = b.y;
      // Only draw if on screen
      if (bx + TILE < camX - 32 || bx > camX + W + 32) continue;

      if (b.type === "brick") drawBrickBlock(bx, by);
      else if (b.type === "question") drawQuestionBlock(bx, by, b.hit);
      else if (b.type === "pipe") drawPipe(bx, by);
      else if (b.type === "pipeBody") drawPipe(bx, by);
      else if (b.type === "platform") drawPlatform(bx, by);
      else if (b.type === "stone") drawStone(bx, by);
      else if (b.type === "flagpole") drawFlagpole(bx, by);
      else if (b.type === "flag") drawFlag(bx, by);
    }

    // Coins
    for (const c of coins) {
      if (c.collected) continue;
      if (c.x + c.w < camX - 32 || c.x > camX + W + 32) continue;
      drawCoin(c.x, c.y, c.bobOffset, time);
    }

    // Enemies
    for (const e of enemies) {
      if (e.x + e.w < camX - 64 || e.x > camX + W + 64) continue;
      if (e.alive) {
        drawGoomba(e.x, e.y);
      } else if (e.squishTimer > 0) {
        drawSquishedGoomba(e.x, e.y);
      }
    }

    // Particles
    for (const p of particles) {
      const alpha = p.life / p.maxLife;
      if (p.text) {
        ctx.fillStyle = p.color;
        ctx.globalAlpha = alpha;
        ctx.font = "bold 14px 'Courier New', monospace";
        ctx.textAlign = "center";
        ctx.fillText(p.text, p.x, p.y);
        ctx.globalAlpha = 1;
      } else {
        ctx.fillStyle = p.color;
        ctx.globalAlpha = alpha;
        ctx.fillRect(p.x - p.size / 2, p.y - p.size / 2, p.size, p.size);
        ctx.globalAlpha = 1;
      }
    }

    // Player
    if (!player.dead) {
      drawPlayer(player.x, player.y);
    } else {
      // Dead player flying up
      ctx.globalAlpha = 0.7;
      drawPlayer(player.x, player.y);
      ctx.globalAlpha = 1;
    }

    ctx.restore();

    // ─── HUD ───
    drawHUD();

    // ─── Overlays ───
    if (player.dead && player.deathTimer > 89) {
      drawGameOver();
    }
    if (player.won) {
      drawWinScreen();
    }
  }

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

  // ─── Init ──────────────────────────────────────────────────────
  buildLevel();
  // Place player on ground
  player.y = (ROWS - 3) * TILE;
  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 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;
      width: 800px; height: 450px;
      border: 2px solid #333;
    }
  </style>
</head>
<body>
  <canvas id="game" width="800" height="450"></canvas>
  <script src="game.js"></script>
</body>
</html>