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

qwen3.6-27b — web-platformer

rep 1 MANUAL partial/5

pi 170.4s · verify 4.4s · 48422 tok · responded: qwen3.6-27b · transcript

smoke 3/3

screenshot

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

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

// ── Constants ──
const GRAVITY = 0.55;
const PLAYER_SPEED = 3.5;
const JUMP_FORCE = -10.5;
const WORLD_W = 4800;
const GROUND_Y = H - 60;
const TILE = 30;

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

// ── Level Data ──
// Platforms: {x, y, w, h}
const platforms = [];
// Ground segments (gaps create pits)
const groundSegs = [
  { x: 0, w: 1200 },
  { x: 1320, w: 900 },
  { x: 2340, w: 2460 },
];

// Raised platforms
const raisedPlatforms = [
  { x: 200, y: 300, w: 120, h: 16 },
  { x: 400, y: 240, w: 120, h: 16 },
  { x: 600, y: 200, w: 90, h: 16 },
  { x: 750, y: 260, w: 120, h: 16 },
  { x: 900, y: 310, w: 90, h: 16 },
  { x: 1050, y: 250, w: 120, h: 16 },
  // After first pit
  { x: 1400, y: 300, w: 90, h: 16 },
  { x: 1550, y: 230, w: 120, h: 16 },
  { x: 1750, y: 180, w: 90, h: 16 },
  { x: 1900, y: 260, w: 150, h: 16 },
  { x: 2100, y: 300, w: 120, h: 16 },
  // After second pit
  { x: 2450, y: 290, w: 120, h: 16 },
  { x: 2650, y: 220, w: 90, h: 16 },
  { x: 2800, y: 170, w: 120, h: 16 },
  { x: 3000, y: 250, w: 150, h: 16 },
  { x: 3200, y: 200, w: 90, h: 16 },
  { x: 3350, y: 280, w: 120, h: 16 },
  // Staircase to flag
  { x: 3600, y: 310, w: 60, h: 16 },
  { x: 3660, y: 270, w: 60, h: 16 },
  { x: 3720, y: 230, w: 60, h: 16 },
  { x: 3780, y: 190, w: 60, h: 16 },
];

// Coins: {x, y, collected}
const coins = [
  { x: 240, y: 270 }, { x: 280, y: 270 },
  { x: 440, y: 210 }, { x: 480, y: 210 },
  { x: 630, y: 170 },
  { x: 790, y: 230 }, { x: 830, y: 230 },
  { x: 930, y: 280 },
  { x: 1090, y: 220 }, { x: 1130, y: 220 },
  // Above ground before pit
  { x: 300, y: GROUND_Y - 40 }, { x: 340, y: GROUND_Y - 40 },
  { x: 500, y: GROUND_Y - 40 }, { x: 540, y: GROUND_Y - 40 },
  { x: 700, y: GROUND_Y - 40 }, { x: 740, y: GROUND_Y - 40 },
  // After first pit
  { x: 1440, y: 270 },
  { x: 1590, y: 200 }, { x: 1630, y: 200 },
  { x: 1780, y: 150 },
  { x: 1940, y: 230 }, { x: 1980, y: 230 },
  { x: 2140, y: 270 },
  // Ground coins
  { x: 1500, y: GROUND_Y - 40 }, { x: 1540, y: GROUND_Y - 40 },
  { x: 1700, y: GROUND_Y - 40 }, { x: 1740, y: GROUND_Y - 40 },
  // After second pit
  { x: 2490, y: 260 }, { x: 2530, y: 260 },
  { x: 2680, y: 190 },
  { x: 2840, y: 140 }, { x: 2880, y: 140 },
  { x: 3040, y: 220 }, { x: 3080, y: 220 },
  { x: 3230, y: 170 },
  { x: 3390, y: 250 }, { x: 3430, y: 250 },
  // Ground coins
  { x: 2500, y: GROUND_Y - 40 }, { x: 2540, y: GROUND_Y - 40 },
  { x: 2700, y: GROUND_Y - 40 }, { x: 2740, y: GROUND_Y - 40 },
  { x: 2900, y: GROUND_Y - 40 }, { x: 2940, y: GROUND_Y - 40 },
  { x: 3100, y: GROUND_Y - 40 }, { x: 3140, y: GROUND_Y - 40 },
  // Staircase coins
  { x: 3620, y: 275 }, { x: 3680, y: 235 }, { x: 3740, y: 195 },
];

// Enemies: {x, y, w, h, vx, alive, type}
const enemies = [
  { x: 500, y: GROUND_Y - 24, w: 24, h: 24, vx: -1.2, alive: true, type: 'goomba' },
  { x: 850, y: GROUND_Y - 24, w: 24, h: 24, vx: -1.0, alive: true, type: 'goomba' },
  { x: 1100, y: GROUND_Y - 24, w: 24, h: 24, vx: 1.5, alive: true, type: 'goomba' },
  { x: 1600, y: GROUND_Y - 24, w: 24, h: 24, vx: -1.3, alive: true, type: 'goomba' },
  { x: 1950, y: GROUND_Y - 24, w: 24, h: 24, vx: -1.0, alive: true, type: 'goomba' },
  { x: 2600, y: GROUND_Y - 24, w: 24, h: 24, vx: 1.2, alive: true, type: 'goomba' },
  { x: 2850, y: GROUND_Y - 24, w: 24, h: 24, vx: -1.5, alive: true, type: 'goomba' },
  { x: 3100, y: GROUND_Y - 24, w: 24, h: 24, vx: -1.0, alive: true, type: 'goomba' },
  { x: 3400, y: GROUND_Y - 24, w: 24, h: 24, vx: 1.3, alive: true, type: 'goomba' },
  // Platform enemies
  { x: 440, y: 216, w: 24, h: 24, vx: -0.8, alive: true, type: 'goomba' },
  { x: 1590, y: 206, w: 24, h: 24, vx: -0.8, alive: true, type: 'goomba' },
  { x: 3040, y: 226, w: 24, h: 24, vx: 0.8, alive: true, type: 'goomba' },
];

// Flag goal
const flag = { x: 4000, y: GROUND_Y - 120, w: 16, h: 120 };

// Particles
let particles = [];

// ── Player ──
let player, camera, score, lives, gameState, winTimer, deathTimer;

function initGame() {
  player = {
    x: 80, y: GROUND_Y - 36, w: 20, h: 32,
    vx: 0, vy: 0,
    onGround: false,
    facing: 1, // 1 = right, -1 = left
    animFrame: 0,
    animTimer: 0,
    invincible: 0,
  };
  camera = { x: 0 };
  score = 0;
  lives = 3;
  gameState = 'playing'; // 'playing', 'win', 'dead', 'gameover'
  winTimer = 0;
  deathTimer = 0;
  particles = [];
  coins.forEach(c => c.collected = false);
  enemies.forEach(e => { e.alive = true; e.deadTimer = 0; });
}

// ── Collision helpers ──
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 getGroundYAt(x) {
  for (const seg of groundSegs) {
    if (x >= seg.x && x < seg.x + seg.w) return GROUND_Y;
  }
  return null; // no ground (pit)
}

function getAllSolids() {
  const solids = [];
  // Ground segments
  for (const seg of groundSegs) {
    solids.push({ x: seg.x, y: GROUND_Y, w: seg.w, h: 60, type: 'ground' });
  }
  // Raised platforms
  for (const p of raisedPlatforms) {
    solids.push({ x: p.x, y: p.y, w: p.w, h: p.h, type: 'platform' });
  }
  return solids;
}

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

// ── Update ──
function update() {
  if (gameState === 'win') {
    winTimer++;
    updateParticles();
    return;
  }
  if (gameState === 'dead') {
    deathTimer++;
    player.vy += GRAVITY;
    player.y += player.vy;
    updateParticles();
    if (deathTimer > 90) {
      if (lives <= 0) {
        gameState = 'gameover';
      } else {
        initGame();
      }
    }
    return;
  }
  if (gameState === 'gameover') {
    if (keys['KeyR']) initGame();
    return;
  }

  // ── Player movement ──
  const moveLeft = keys['ArrowLeft'] || keys['KeyA'];
  const moveRight = keys['ArrowRight'] || keys['KeyD'];
  const jumpKey = keys['Space'] || keys['ArrowUp'] || keys['KeyW'];

  if (moveLeft) { player.vx = -PLAYER_SPEED; player.facing = -1; }
  else if (moveRight) { player.vx = PLAYER_SPEED; player.facing = 1; }
  else { player.vx *= 0.75; if (Math.abs(player.vx) < 0.1) player.vx = 0; }

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

  // Variable jump height
  if (!jumpKey && player.vy < -3) {
    player.vy = -3;
  }

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

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

  // Invincibility timer
  if (player.invincible > 0) player.invincible--;

  // ── Horizontal movement & collision ──
  player.x += player.vx;
  const solids = getAllSolids();
  for (const s of solids) {
    if (rectOverlap(player, s)) {
      if (player.vx > 0) player.x = s.x - player.w;
      else if (player.vx < 0) player.x = s.x + s.w;
      player.vx = 0;
    }
  }

  // ── Vertical movement & collision ──
  player.y += player.vy;
  player.onGround = false;
  for (const s of solids) {
    if (rectOverlap(player, s)) {
      if (player.vy > 0) {
        player.y = s.y - player.h;
        player.vy = 0;
        player.onGround = true;
      } else if (player.vy < 0) {
        player.y = s.y + s.h;
        player.vy = 0;
      }
    }
  }

  // Clamp to world
  if (player.x < 0) player.x = 0;
  if (player.x + player.w > WORLD_W) player.x = WORLD_W - player.w;

  // Fall into pit
  if (player.y > H + 50) {
    loseLife();
    return;
  }

  // ── Coin collection ──
  for (const c of coins) {
    if (c.collected) continue;
    const coinRect = { x: c.x - 8, y: c.y - 8, w: 16, h: 16 };
    if (rectOverlap(player, coinRect)) {
      c.collected = true;
      score += 100;
      spawnParticles(c.x, c.y, '#FFD700', 8);
    }
  }

  // ── Enemy update ──
  for (const e of enemies) {
    if (!e.alive) {
      if (e.deadTimer !== undefined) e.deadTimer++;
      continue;
    }
    e.x += e.vx;
    // Simple patrol: reverse at edges or walls
    const groundBelow = getGroundYAt(e.x);
    if (groundBelow === null && e.y >= GROUND_Y - 30) {
      e.vx *= -1; // reverse at pit edge
    }
    // Reverse at solid walls
    for (const s of solids) {
      if (rectOverlap(e, s)) {
        e.vx *= -1;
        e.x += e.vx * 2;
      }
    }
    // Player-enemy collision
    if (player.invincible <= 0 && rectOverlap(player, e)) {
      // Stomp from above
      if (player.vy > 0 && player.y + player.h - 8 < e.y + e.h / 2) {
        e.alive = false;
        e.deadTimer = 30;
        player.vy = -7;
        score += 200;
        spawnParticles(e.x + e.w/2, e.y + e.h/2, '#8B4513', 6);
      } else {
        // Hit by enemy
        loseLife();
        return;
      }
    }
  }

  // ── Flag (win) ──
  const flagRect = { x: flag.x - 10, y: flag.y, w: flag.w + 20, h: flag.h };
  if (rectOverlap(player, flagRect)) {
    gameState = 'win';
    score += 1000;
    spawnParticles(flag.x, flag.y + 20, '#FFD700', 20);
    spawnParticles(flag.x, flag.y + 20, '#FF4500', 15);
  }

  // ── Camera ──
  const targetCam = player.x - W / 3;
  camera.x += (targetCam - camera.x) * 0.1;
  if (camera.x < 0) camera.x = 0;
  if (camera.x > WORLD_W - W) camera.x = WORLD_W - W;

  updateParticles();
}

function loseLife() {
  lives--;
  gameState = 'dead';
  deathTimer = 0;
  player.vy = -8;
  spawnParticles(player.x + player.w/2, player.y + player.h/2, '#FF0000', 10);
}

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

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

  // Background clouds (parallax)
  drawClouds();

  // Background hills (parallax)
  drawHills();

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

  // Draw ground
  drawGround();

  // Draw raised platforms
  drawPlatforms();

  // Draw coins
  drawCoins();

  // Draw enemies
  drawEnemies();

  // Draw flag
  drawFlag();

  // Draw particles
  drawParticles();

  // Draw player
  if (gameState !== 'dead') {
    drawPlayer();
  } else if (deathTimer < 60) {
    drawPlayer();
  }

  ctx.restore();

  // HUD
  drawHUD();

  // Win/Game Over overlays
  if (gameState === 'win') drawWinScreen();
  if (gameState === 'gameover') drawGameOverScreen();
}

function drawClouds() {
  const cloudOffset = camera.x * 0.2;
  ctx.fillStyle = 'rgba(255,255,255,0.8)';
  const cloudPositions = [
    { x: 100, y: 50, s: 1.2 }, { x: 400, y: 80, s: 0.8 },
    { x: 700, y: 40, s: 1.0 }, { x: 1100, y: 65, s: 1.3 },
    { x: 1500, y: 55, s: 0.9 }, { x: 2000, y: 75, s: 1.1 },
    { x: 2500, y: 45, s: 1.0 }, { x: 3000, y: 70, s: 0.8 },
    { x: 3500, y: 50, s: 1.2 }, { x: 4200, y: 60, s: 0.9 },
  ];
  for (const c of cloudPositions) {
    const cx = c.x - cloudOffset % 5000;
    // Wrap
    let wx = cx;
    if (wx < -100) wx += 5000;
    if (wx > W + 100) wx -= 5000;
    drawCloud(wx, c.y, c.s);
  }
}

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

function drawHills() {
  const hillOffset = camera.x * 0.35;
  ctx.fillStyle = '#4AA52E';
  const hillPositions = [
    { x: 50, r: 80 }, { x: 350, r: 50 }, { x: 650, r: 90 },
    { x: 1000, r: 60 }, { x: 1400, r: 75 }, { x: 1800, r: 55 },
    { x: 2200, r: 85 }, { x: 2700, r: 65 }, { x: 3200, r: 70 },
    { x: 3700, r: 80 }, { x: 4300, r: 55 },
  ];
  for (const h of hillPositions) {
    let hx = h.x - hillOffset % 5000;
    if (hx < -200) hx += 5000;
    if (hx > W + 200) hx -= 5000;
    ctx.beginPath();
    ctx.arc(hx, GROUND_Y, h.r, Math.PI, 0);
    ctx.fill();
  }
}

function drawGround() {
  for (const seg of groundSegs) {
    // Top dirt layer
    ctx.fillStyle = '#8B5E3C';
    ctx.fillRect(seg.x, GROUND_Y, seg.w, 60);
    // Grass top
    ctx.fillStyle = '#4CAF50';
    ctx.fillRect(seg.x, GROUND_Y, seg.w, 8);
    ctx.fillStyle = '#66BB6A';
    ctx.fillRect(seg.x, GROUND_Y, seg.w, 4);
    // Dirt pattern
    ctx.fillStyle = '#7A5230';
    for (let gx = seg.x; gx < seg.x + seg.w; gx += 20) {
      for (let gy = GROUND_Y + 12; gy < GROUND_Y + 56; gy += 14) {
        ctx.fillRect(gx + ((gy % 28 === 0) ? 10 : 0), gy, 10, 6);
      }
    }
  }
}

function drawPlatforms() {
  for (const p of raisedPlatforms) {
    // Shadow
    ctx.fillStyle = 'rgba(0,0,0,0.2)';
    ctx.fillRect(p.x + 2, p.y + 2, p.w, p.h);
    // Block body
    ctx.fillStyle = '#C8703A';
    ctx.fillRect(p.x, p.y, p.w, p.h);
    // Top highlight
    ctx.fillStyle = '#E8A060';
    ctx.fillRect(p.x, p.y, p.w, 4);
    // Brick lines
    ctx.strokeStyle = '#8B5E3C';
    ctx.lineWidth = 1;
    for (let bx = p.x + 15; bx < p.x + p.w; bx += 15) {
      ctx.beginPath();
      ctx.moveTo(bx, p.y);
      ctx.lineTo(bx, p.y + p.h);
      ctx.stroke();
    }
    // Border
    ctx.strokeStyle = '#6B3A1F';
    ctx.lineWidth = 1;
    ctx.strokeRect(p.x, p.y, p.w, p.h);
  }
}

function drawCoins() {
  const t = Date.now() / 300;
  for (const c of coins) {
    if (c.collected) continue;
    const bob = Math.sin(t + c.x * 0.01) * 3;
    const cy = c.y + bob;
    // Glow
    ctx.fillStyle = 'rgba(255,215,0,0.3)';
    ctx.beginPath();
    ctx.arc(c.x, cy, 12, 0, Math.PI * 2);
    ctx.fill();
    // Coin body
    ctx.fillStyle = '#FFD700';
    ctx.beginPath();
    ctx.arc(c.x, cy, 7, 0, Math.PI * 2);
    ctx.fill();
    ctx.strokeStyle = '#DAA520';
    ctx.lineWidth = 1.5;
    ctx.stroke();
    // Shine
    ctx.fillStyle = '#FFF8DC';
    ctx.beginPath();
    ctx.arc(c.x - 2, cy - 2, 2.5, 0, Math.PI * 2);
    ctx.fill();
  }
}

function drawEnemies() {
  for (const e of enemies) {
    if (!e.alive && (e.deadTimer === undefined || e.deadTimer > 30)) continue;

    ctx.save();
    if (!e.alive) {
      // Squished
      ctx.translate(e.x + e.w/2, e.y + e.h);
      ctx.scale(1, 0.3);
      ctx.translate(-(e.x + e.w/2), -(e.y + e.h));
      ctx.globalAlpha = 1 - e.deadTimer / 30;
    }

    // Body
    ctx.fillStyle = '#8B4513';
    ctx.beginPath();
    ctx.ellipse(e.x + e.w/2, e.y + e.h/2 + 2, e.w/2, e.h/2 - 2, 0, 0, Math.PI * 2);
    ctx.fill();

    // Feet
    ctx.fillStyle = '#3E1F0D';
    const footWiggle = Math.sin(Date.now() / 150) * 2;
    ctx.fillRect(e.x + 2, e.y + e.h - 6 + footWiggle, 7, 6);
    ctx.fillRect(e.x + e.w - 9, e.y + e.h - 6 - footWiggle, 7, 6);

    // Eyes
    ctx.fillStyle = '#FFF';
    ctx.beginPath();
    ctx.arc(e.x + 7, e.y + 10, 4, 0, Math.PI * 2);
    ctx.arc(e.x + e.w - 7, e.y + 10, 4, 0, Math.PI * 2);
    ctx.fill();
    ctx.fillStyle = '#000';
    ctx.beginPath();
    ctx.arc(e.x + 8, e.y + 10, 2, 0, Math.PI * 2);
    ctx.arc(e.x + e.w - 6, e.y + 10, 2, 0, Math.PI * 2);
    ctx.fill();

    // Eyebrows (angry)
    ctx.strokeStyle = '#000';
    ctx.lineWidth = 1.5;
    ctx.beginPath();
    ctx.moveTo(e.x + 4, e.y + 6);
    ctx.lineTo(e.x + 10, e.y + 8);
    ctx.stroke();
    ctx.beginPath();
    ctx.moveTo(e.x + e.w - 4, e.y + 6);
    ctx.lineTo(e.x + e.w - 10, e.y + 8);
    ctx.stroke();

    ctx.restore();
  }
}

function drawFlag() {
  // Pole
  ctx.fillStyle = '#888';
  ctx.fillRect(flag.x + 6, flag.y, 4, flag.h);
  // Ball on top
  ctx.fillStyle = '#FFD700';
  ctx.beginPath();
  ctx.arc(flag.x + 8, flag.y, 6, 0, Math.PI * 2);
  ctx.fill();
  // Flag
  const wave = Math.sin(Date.now() / 200) * 3;
  ctx.fillStyle = '#FF2200';
  ctx.beginPath();
  ctx.moveTo(flag.x + 10, flag.y + 5);
  ctx.lineTo(flag.x + 40 + wave, flag.y + 15);
  ctx.lineTo(flag.x + 10, flag.y + 30);
  ctx.closePath();
  ctx.fill();
  // Star on flag
  ctx.fillStyle = '#FFD700';
  ctx.font = '12px monospace';
  ctx.fillText('★', flag.x + 18, flag.y + 23);
  // Base
  ctx.fillStyle = '#666';
  ctx.fillRect(flag.x, flag.y + flag.h - 8, 16, 8);
}

function drawPlayer() {
  if (player.invincible > 0 && Math.floor(player.invincible / 3) % 2 === 0) return;

  const px = player.x;
  const py = 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);

  // Hat
  ctx.fillStyle = '#E01010';
  ctx.fillRect(2, 0, 16, 8);
  ctx.fillRect(0, 4, 20, 5);

  // Face
  ctx.fillStyle = '#FFB366';
  ctx.fillRect(2, 9, 16, 10);

  // Eye
  ctx.fillStyle = '#000';
  ctx.fillRect(12, 11, 3, 3);

  // Mustache
  ctx.fillStyle = '#4A2800';
  ctx.fillRect(8, 15, 10, 2);

  // Body (overalls)
  ctx.fillStyle = '#0050D0';
  ctx.fillRect(2, 19, 16, 8);

  // Shirt
  ctx.fillStyle = '#E01010';
  ctx.fillRect(0, 19, 4, 6);
  ctx.fillRect(16, 19, 4, 6);

  // Legs
  const legOffset = player.onGround ? Math.sin(player.animFrame * Math.PI / 2) * 3 : 0;
  ctx.fillStyle = '#0050D0';
  ctx.fillRect(2, 27, 7, 5 + legOffset);
  ctx.fillRect(11, 27, 7, 5 - legOffset);

  // Shoes
  ctx.fillStyle = '#6B3A1F';
  ctx.fillRect(0, 30 + legOffset, 9, 3);
  ctx.fillRect(11, 30 - legOffset, 9, 3);

  ctx.restore();
}

function drawParticles() {
  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;
}

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

  // Score
  ctx.fillStyle = '#FFD700';
  ctx.font = 'bold 16px monospace';
  ctx.fillText('SCORE: ' + score.toString().padStart(6, '0'), 20, 28);

  // Lives
  ctx.fillStyle = 'rgba(0,0,0,0.5)';
  ctx.fillRect(W - 100, 8, 90, 32);
  ctx.strokeStyle = '#FFF';
  ctx.strokeRect(W - 100, 8, 90, 32);
  ctx.fillStyle = '#FF4444';
  ctx.font = 'bold 16px monospace';
  ctx.fillText('LIVES: ' + lives, W - 90, 28);

  // Coins collected
  const collected = coins.filter(c => c.collected).length;
  ctx.fillStyle = 'rgba(0,0,0,0.5)';
  ctx.fillRect(W/2 - 60, 8, 120, 32);
  ctx.strokeStyle = '#FFF';
  ctx.strokeRect(W/2 - 60, 8, 120, 32);
  ctx.fillStyle = '#FFD700';
  ctx.font = 'bold 16px monospace';
  ctx.fillText('COINS: ' + collected + '/' + coins.length, W/2 - 50, 28);
}

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

  ctx.fillStyle = '#FFF';
  ctx.font = '24px monospace';
  ctx.fillText('Score: ' + score, W/2, H/2 + 20);

  const collected = coins.filter(c => c.collected).length;
  ctx.fillText('Coins: ' + collected + '/' + coins.length, W/2, H/2 + 55);

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

  if (keys['KeyR']) initGame();
}

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

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

  ctx.fillStyle = '#FFF';
  ctx.font = '24px monospace';
  ctx.fillText('Final Score: ' + score, W/2, H/2 + 25);

  ctx.fillStyle = '#AAA';
  ctx.font = '16px monospace';
  ctx.fillText('Press R to try again', W/2, H/2 + 65);
  ctx.textAlign = 'left';

  if (keys['KeyR']) initGame();
}

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

initGame();
gameLoop();

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

rep 2 MANUAL partial/5

pi 200.0s · verify 4.2s · 53752 tok · responded: qwen3.6-27b · 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 Canvas Bros</title>
<style>
  * { margin: 0; padding: 0; box-sizing: border-box; }
  body {
    background: #1a1a2e;
    display: flex;
    justify-content: center;
    align-items: center;
    min-height: 100vh;
    font-family: monospace;
    overflow: hidden;
  }
  #game {
    border: 3px solid #16213e;
    border-radius: 4px;
    image-rendering: pixelated;
  }
</style>
</head>
<body>
<canvas id="game" width="800" height="450"></canvas>
<script>
(function() {
  "use strict";

  const canvas = document.getElementById("game");
  const ctx = canvas.getContext("2d");
  const W = 800, H = 450;

  // ─── Constants ───────────────────────────────────────────────
  const GRAVITY = 0.55;
  const PLAYER_SPEED = 3.5;
  const PLAYER_RUN_SPEED = 5.0;
  const JUMP_FORCE = -11.5;
  const ENEMY_SPEED = 1.2;
  const WORLD_WIDTH = 4800;
  const GROUND_Y = H - 60;

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

  // ─── Game State ──────────────────────────────────────────────
  let state = "playing"; // "playing", "won", "dead", "title"
  let score = 0;
  let coins = 0;
  let lives = 3;
  let camera = { x: 0 };
  let frameCount = 0;
  let deathTimer = 0;
  let winTimer = 0;
  let particles = [];
  let floatingTexts = [];

  // ─── Level Data ──────────────────────────────────────────────
  // Platforms: {x, y, w, h, type}
  // type: "ground", "brick", "question", "pipe", "cloud"
  let platforms = [];
  let coinItems = [];
  let enemies = [];
  let flagPole = null;

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

    // Ground segments with gaps
    addGround(0, 800);
    addGround(900, 1600);
    addGround(1750, 3000);
    addGround(3150, WORLD_WIDTH);

    // ── Section 1: Intro platforms ──
    addPlatform(250, GROUND_Y - 80, 48, 48, "question");
    addPlatform(350, GROUND_Y - 80, 48, 48, "brick");
    addPlatform(400, GROUND_Y - 80, 48, 48, "question");
    addPlatform(450, GROUND_Y - 80, 48, 48, "brick");

    addPlatform(350, GROUND_Y - 170, 48, 48, "question");

    addPlatform(600, GROUND_Y - 60, 64, 64, "pipe");

    // Coins in the air
    addCoin(270, GROUND_Y - 140);
    addCoin(420, GROUND_Y - 140);
    addCoin(370, GROUND_Y - 220);

    // Enemies
    addEnemy(500, GROUND_Y - 24, "goomba");
    addEnemy(700, GROUND_Y - 24, "goomba");

    // ── Section 2: After first gap ──
    addPlatform(950, GROUND_Y - 70, 48, 48, "brick");
    addPlatform(1000, GROUND_Y - 70, 48, 48, "question");
    addPlatform(1050, GROUND_Y - 70, 48, 48, "brick");

    addPlatform(1150, GROUND_Y - 130, 48, 48, "brick");
    addPlatform(1200, GROUND_Y - 130, 48, 48, "question");
    addPlatform(1250, GROUND_Y - 130, 48, 48, "brick");

    addPlatform(1150, GROUND_Y - 210, 48, 48, "question");

    addPlatform(1350, GROUND_Y - 60, 64, 64, "pipe");

    addCoin(1020, GROUND_Y - 130);
    addCoin(1220, GROUND_Y - 190);
    addCoin(1170, GROUND_Y - 270);

    addEnemy(1050, GROUND_Y - 24, "goomba");
    addEnemy(1400, GROUND_Y - 24, "goomba");

    // ── Section 3: Staircase ──
    for (let i = 0; i < 5; i++) {
      addPlatform(1800 + i * 48, GROUND_Y - (i + 1) * 48, 48, 48, "brick");
    }
    for (let i = 0; i < 4; i++) {
      addPlatform(2000 + i * 48, GROUND_Y - (5 - i) * 48, 48, 48, "brick");
    }

    addCoin(1850, GROUND_Y - 130);
    addCoin(1900, GROUND_Y - 180);
    addCoin(1950, GROUND_Y - 230);
    addCoin(2000, GROUND_Y - 260);
    addCoin(2050, GROUND_Y - 210);

    addEnemy(1900, GROUND_Y - 24, "goomba");
    addEnemy(2200, GROUND_Y - 24, "goomba");

    // ── Section 4: Floating platforms over gap ──
    addPlatform(2300, GROUND_Y - 60, 80, 20, "ground");
    addPlatform(2450, GROUND_Y - 110, 80, 20, "ground");
    addPlatform(2600, GROUND_Y - 160, 80, 20, "ground");

    addCoin(2340, GROUND_Y - 110);
    addCoin(2490, GROUND_Y - 170);
    addCoin(2640, GROUND_Y - 220);

    // ── Section 5: Pipe gauntlet ──
    addPlatform(2800, GROUND_Y - 60, 64, 64, "pipe");
    addPlatform(2950, GROUND_Y - 80, 64, 80, "pipe");
    addPlatform(3100, GROUND_Y - 60, 64, 64, "pipe");

    addCoin(2832, GROUND_Y - 110);
    addCoin(2982, GROUND_Y - 140);
    addCoin(3132, GROUND_Y - 110);

    addEnemy(2700, GROUND_Y - 24, "goomba");
    addEnemy(2850, GROUND_Y - 24, "goomba");
    addEnemy(3000, GROUND_Y - 24, "goomba");

    // ── Section 6: Final staircase to flag ──
    for (let i = 0; i < 8; i++) {
      addPlatform(3400 + i * 48, GROUND_Y - (i + 1) * 48, 48, 48, "brick");
    }

    addCoin(3450, GROUND_Y - 130);
    addCoin(3500, GROUND_Y - 180);
    addCoin(3550, GROUND_Y - 230);
    addCoin(3600, GROUND_Y - 280);
    addCoin(3650, GROUND_Y - 330);
    addCoin(3700, GROUND_Y - 380);

    addEnemy(3400, GROUND_Y - 24, "goomba");
    addEnemy(3550, GROUND_Y - 24, "goomba");

    // Flag pole at end
    flagPole = { x: 4000, y: GROUND_Y - 320, w: 8, h: 320 };
  }

  function addGround(x, x2) {
    platforms.push({ x, y: GROUND_Y, w: x2 - x, h: H - GROUND_Y, type: "ground" });
  }

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

  function addCoin(x, y) {
    coinItems.push({ x, y, w: 16, h: 16, collected: false, bobOffset: Math.random() * Math.PI * 2 });
  }

  function addEnemy(x, y, type) {
    enemies.push({
      x, y, w: 28, h: 24, type,
      vx: -ENEMY_SPEED,
      alive: true,
      squishTimer: 0,
      startX: x,
      patrolDist: 120
    });
  }

  // ─── Player ──────────────────────────────────────────────────
  let player = {};

  function resetPlayer() {
    player = {
      x: 80, y: GROUND_Y - 40,
      w: 24, h: 32,
      vx: 0, vy: 0,
      onGround: false,
      facing: 1,
      animFrame: 0,
      animTimer: 0,
      invincible: 0
    };
  }

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

  function resolveCollisions() {
    player.onGround = false;

    for (const p of platforms) {
      if (!aabb(player, p)) continue;

      const overlapLeft = (player.x + player.w) - p.x;
      const overlapRight = (p.x + p.w) - player.x;
      const overlapTop = (player.y + player.h) - p.y;
      const overlapBottom = (p.y + p.h) - player.y;

      const minOverlapX = Math.min(overlapLeft, overlapRight);
      const minOverlapY = Math.min(overlapTop, overlapBottom);

      if (minOverlapY < minOverlapX) {
        if (overlapTop < overlapBottom) {
          // Landing on top
          player.y = p.y - player.h;
          player.vy = 0;
          player.onGround = true;
        } else {
          // Hitting from below
          player.y = p.y + p.h;
          player.vy = 0.5;
          if (p.type === "question" && !p.hit) {
            p.hit = true;
            score += 100;
            spawnParticles(p.x + p.w/2, p.y, "#FFD700", 6);
            addFloatingText("+100", p.x, p.y - 20, "#FFD700");
          }
          if (p.type === "brick" && !p.hit) {
            p.hit = true;
            score += 50;
            spawnParticles(p.x + p.w/2, p.y + p.h/2, "#C84C09", 8);
            addFloatingText("+50", p.x, p.y - 20, "#FFD700");
          }
        }
      } else {
        if (overlapLeft < overlapRight) {
          player.x = p.x - player.w;
        } else {
          player.x = p.x + p.w;
        }
        player.vx = 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() * 5 - 2,
        life: 30 + Math.random() * 20,
        maxLife: 50,
        color,
        size: 2 + Math.random() * 3
      });
    }
  }

  function addFloatingText(text, x, y, color) {
    floatingTexts.push({ text, x, y, life: 45, color });
  }

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

    if (state === "title") {
      if (keys["Space"] || keys["Enter"]) {
        state = "playing";
        keys["Space"] = false;
        keys["Enter"] = false;
      }
      return;
    }

    if (state === "dead") {
      deathTimer--;
      if (deathTimer <= 0) {
        if (lives <= 0) {
          state = "title";
          score = 0;
          coins = 0;
          lives = 3;
        } else {
          resetPlayer();
          camera.x = 0;
          state = "playing";
        }
      }
      return;
    }

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

    // ── Player movement ──
    const moveLeft = keys["ArrowLeft"] || keys["KeyA"];
    const moveRight = keys["ArrowRight"] || keys["KeyD"];
    const jumpKey = keys["Space"] || keys["ArrowUp"] || keys["KeyW"];
    const running = keys["ShiftLeft"] || keys["ShiftRight"];

    const speed = running ? PLAYER_RUN_SPEED : PLAYER_SPEED;

    if (moveLeft) { player.vx = -speed; player.facing = -1; }
    else if (moveRight) { player.vx = speed; player.facing = 1; }
    else { player.vx *= 0.7; if (Math.abs(player.vx) < 0.1) player.vx = 0; }

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

    // Variable jump height
    if (!jumpKey && player.vy < -3) {
      player.vy *= 0.85;
    }

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

    // Move X
    player.x += player.vx;
    resolveCollisions();

    // Move Y
    player.y += player.vy;
    resolveCollisions();

    // Clamp player
    if (player.x < 0) player.x = 0;
    if (player.x > WORLD_WIDTH - player.w) player.x = WORLD_WIDTH - player.w;

    // Fall death
    if (player.y > H + 50) {
      playerDie();
      return;
    }

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

    // Invincibility timer
    if (player.invincible > 0) player.invincible--;

    // ── Camera ──
    const targetCam = player.x - W * 0.35;
    camera.x += (targetCam - camera.x) * 0.1;
    if (camera.x < 0) camera.x = 0;
    if (camera.x > WORLD_WIDTH - W) camera.x = WORLD_WIDTH - W;

    // ── Coins ──
    for (const c of coinItems) {
      if (c.collected) continue;
      const coinBox = { x: c.x - 8, y: c.y - 8 + Math.sin(frameCount * 0.06 + c.bobOffset) * 4, w: 16, h: 16 };
      if (aabb(player, coinBox)) {
        c.collected = true;
        coins++;
        score += 50;
        spawnParticles(c.x, c.y, "#FFD700", 8);
        addFloatingText("+50", c.x - 10, c.y - 20, "#FFD700");
      }
    }

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

      e.x += e.vx;

      // Patrol
      if (e.x < e.startX - e.patrolDist || e.x > e.startX + e.patrolDist) {
        e.vx *= -1;
      }

      // Platform check for enemies
      let onPlatform = false;
      for (const p of platforms) {
        if (e.x + e.w > p.x && e.x < p.x + p.w &&
            e.y + e.h >= p.y && e.y + e.h <= p.y + 8) {
          onPlatform = true;
          break;
        }
      }
      if (!onPlatform && e.y > GROUND_Y - 10) {
        e.vx *= -1;
      }

      // Player collision
      if (player.invincible <= 0 && aabb(player, e)) {
        // Stomp from above
        if (player.vy > 0 && player.y + player.h - 8 < e.y + e.h / 2) {
          e.alive = false;
          e.squishTimer = 30;
          player.vy = -8;
          score += 200;
          spawnParticles(e.x + e.w/2, e.y, "#8B4513", 6);
          addFloatingText("+200", e.x, e.y - 20, "#FFD700");
        } else {
          playerHit();
        }
      }
    }

    // ── Flag pole ──
    if (flagPole && aabb(player, { x: flagPole.x - 10, y: flagPole.y, w: flagPole.w + 20, h: flagPole.h })) {
      state = "won";
      winTimer = 0;
      score += 1000;
      spawnParticles(flagPole.x, flagPole.y, "#FFD700", 20);
      spawnParticles(flagPole.x, flagPole.y + 40, "#FF6347", 15);
    }

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

    // ── Update floating texts ──
    for (let i = floatingTexts.length - 1; i >= 0; i--) {
      const ft = floatingTexts[i];
      ft.y -= 1;
      ft.life--;
      if (ft.life <= 0) floatingTexts.splice(i, 1);
    }
  }

  function playerHit() {
    if (player.invincible > 0) return;
    lives--;
    player.invincible = 90;
    player.vy = -8;
    spawnParticles(player.x + player.w/2, player.y + player.h/2, "#FF0000", 10);
    if (lives <= 0) {
      playerDie();
    }
  }

  function playerDie() {
    state = "dead";
    deathTimer = 60;
    spawnParticles(player.x + player.w/2, player.y + player.h/2, "#FF0000", 15);
  }

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

    // Clouds (parallax)
    drawClouds();

    // Hills (parallax)
    drawHills();

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

    // Platforms
    for (const p of platforms) {
      if (p.x + p.w < camera.x - 50 || p.x > camera.x + W + 50) continue;
      drawPlatform(p);
    }

    // Coins
    for (const c of coinItems) {
      if (c.collected) continue;
      if (c.x < camera.x - 50 || c.x > camera.x + W + 50) continue;
      drawCoin(c);
    }

    // Enemies
    for (const e of enemies) {
      if (e.x < camera.x - 100 || e.x > camera.x + W + 100) continue;
      drawEnemy(e);
    }

    // Flag pole
    if (flagPole) {
      drawFlagPole(flagPole);
    }

    // Player
    if (state === "playing" || state === "won") {
      if (player.invincible <= 0 || Math.floor(player.invincible / 4) % 2 === 0) {
        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;

    // Floating texts
    for (const ft of floatingTexts) {
      const alpha = ft.life / 45;
      ctx.globalAlpha = alpha;
      ctx.fillStyle = ft.color;
      ctx.font = "bold 14px monospace";
      ctx.textAlign = "center";
      ctx.fillText(ft.text, ft.x, ft.y);
    }
    ctx.globalAlpha = 1;

    ctx.restore();

    // HUD
    drawHUD();

    // Overlays
    if (state === "title") drawTitleScreen();
    if (state === "won") drawWinScreen();
    if (state === "dead") drawDeathScreen();
  }

  function drawClouds() {
    ctx.fillStyle = "rgba(255,255,255,0.8)";
    const cloudPositions = [
      { x: 100, y: 60, s: 1.2 },
      { x: 400, y: 40, s: 0.9 },
      { x: 700, y: 70, s: 1.0 },
      { x: 1100, y: 50, s: 1.3 },
      { x: 1500, y: 35, s: 0.8 },
      { x: 2000, y: 65, s: 1.1 },
      { x: 2500, y: 45, s: 1.0 },
      { x: 3000, y: 55, s: 1.4 },
      { x: 3500, y: 40, s: 0.9 },
      { x: 4000, y: 60, s: 1.2 },
    ];
    for (const c of cloudPositions) {
      const cx = c.x - camera.x * 0.3;
      // Wrap clouds
      const wrappedX = ((cx % (W + 200)) + W + 200) % (W + 200) - 100;
      const s = c.s;
      ctx.beginPath();
      ctx.ellipse(wrappedX, c.y, 40 * s, 18 * s, 0, 0, Math.PI * 2);
      ctx.fill();
      ctx.beginPath();
      ctx.ellipse(wrappedX - 25 * s, c.y + 5 * s, 28 * s, 14 * s, 0, 0, Math.PI * 2);
      ctx.fill();
      ctx.beginPath();
      ctx.ellipse(wrappedX + 25 * s, c.y + 3 * s, 30 * s, 15 * s, 0, 0, Math.PI * 2);
      ctx.fill();
    }
  }

  function drawHills() {
    ctx.fillStyle = "#4a8c3f";
    const hillPositions = [
      { x: 200, w: 200, h: 80 },
      { x: 800, w: 250, h: 100 },
      { x: 1600, w: 180, h: 70 },
      { x: 2400, w: 300, h: 110 },
      { x: 3200, w: 220, h: 90 },
      { x: 4000, w: 260, h: 95 },
    ];
    for (const h2 of hillPositions) {
      const hx = h2.x - camera.x * 0.5;
      const wrappedX = ((hx % (W + 400)) + W + 400) % (W + 400) - 200;
      ctx.beginPath();
      ctx.ellipse(wrappedX, GROUND_Y, h2.w / 2, h2.h, 0, Math.PI, 0);
      ctx.fill();
    }
  }

  function drawPlatform(p) {
    if (p.type === "ground") {
      // Ground with grass top
      ctx.fillStyle = "#8B6914";
      ctx.fillRect(p.x, p.y, p.w, p.h);
      // Grass
      ctx.fillStyle = "#4CAF50";
      ctx.fillRect(p.x, p.y, p.w, 8);
      ctx.fillStyle = "#66BB6A";
      ctx.fillRect(p.x, p.y, p.w, 4);
      // Dirt texture
      ctx.fillStyle = "#7A5B10";
      for (let dx = p.x; dx < p.x + p.w; dx += 24) {
        ctx.fillRect(dx + 4, p.y + 12, 12, 2);
        ctx.fillRect(dx + 16, p.y + 24, 8, 2);
      }
    } else if (p.type === "brick") {
      if (p.hit) {
        ctx.fillStyle = "#8B6914";
        ctx.fillRect(p.x, p.y, p.w, p.h);
        ctx.strokeStyle = "#5C4010";
        ctx.lineWidth = 1;
        ctx.strokeRect(p.x, p.y, p.w, p.h);
        return;
      }
      ctx.fillStyle = "#C84C09";
      ctx.fillRect(p.x, p.y, p.w, p.h);
      ctx.strokeStyle = "#8B3000";
      ctx.lineWidth = 2;
      ctx.strokeRect(p.x, p.y, p.w, p.h);
      // Brick lines
      ctx.strokeStyle = "#A0400A";
      ctx.lineWidth = 1;
      ctx.beginPath();
      ctx.moveTo(p.x, p.y + p.h / 2);
      ctx.lineTo(p.x + p.w, p.y + p.h / 2);
      ctx.moveTo(p.x + p.w / 2, p.y);
      ctx.lineTo(p.x + p.w / 2, p.y + p.h / 2);
      ctx.moveTo(p.x + p.w / 4, p.y + p.h / 2);
      ctx.lineTo(p.x + p.w / 4, p.y + p.h);
      ctx.moveTo(p.x + p.w * 3 / 4, p.y + p.h / 2);
      ctx.lineTo(p.x + p.w * 3 / 4, p.y + p.h);
      ctx.stroke();
    } else if (p.type === "question") {
      const flash = p.hit ? "#8B6914" : (Math.floor(frameCount / 8) % 2 === 0 ? "#FFB800" : "#FFD700");
      ctx.fillStyle = flash;
      ctx.fillRect(p.x, p.y, p.w, p.h);
      ctx.strokeStyle = p.hit ? "#5C4010" : "#C89600";
      ctx.lineWidth = 2;
      ctx.strokeRect(p.x, p.y, p.w, p.h);
      if (!p.hit) {
        ctx.fillStyle = "#8B6914";
        ctx.font = "bold 22px monospace";
        ctx.textAlign = "center";
        ctx.fillText("?", p.x + p.w / 2, p.y + p.h / 2 + 7);
      }
    } else if (p.type === "pipe") {
      // Pipe body
      ctx.fillStyle = "#338A33";
      ctx.fillRect(p.x + 4, p.y + 20, p.w - 8, p.h - 20);
      // Pipe top
      ctx.fillStyle = "#44AA44";
      ctx.fillRect(p.x, p.y, p.w, 24);
      // Pipe highlight
      ctx.fillStyle = "#55CC55";
      ctx.fillRect(p.x + 4, p.y + 2, 6, 20);
      ctx.fillRect(p.x + 8, p.y + 24, 4, p.h - 24);
      // Pipe shadow
      ctx.fillStyle = "#226622";
      ctx.fillRect(p.x + p.w - 10, p.y + 2, 4, 20);
      ctx.fillRect(p.x + p.w - 12, p.y + 24, 4, p.h - 24);
      // Pipe rim
      ctx.strokeStyle = "#1A551A";
      ctx.lineWidth = 2;
      ctx.strokeRect(p.x, p.y, p.w, 24);
    }
  }

  function drawCoin(c) {
    const bobY = Math.sin(frameCount * 0.06 + c.bobOffset) * 4;
    const cx = c.x;
    const cy = c.y + bobY;
    const stretch = Math.abs(Math.cos(frameCount * 0.08 + c.bobOffset));

    ctx.save();
    ctx.translate(cx, cy);
    ctx.scale(stretch, 1);

    // Coin body
    ctx.fillStyle = "#FFD700";
    ctx.beginPath();
    ctx.arc(0, 0, 8, 0, Math.PI * 2);
    ctx.fill();
    ctx.strokeStyle = "#DAA520";
    ctx.lineWidth = 2;
    ctx.stroke();

    // Coin shine
    ctx.fillStyle = "#FFF8DC";
    ctx.beginPath();
    ctx.arc(-2, -2, 3, 0, Math.PI * 2);
    ctx.fill();

    ctx.restore();
  }

  function drawEnemy(e) {
    if (!e.alive && e.squishTimer <= 0) return;

    const ex = e.x;
    const ey = e.y;

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

    // Goomba body
    ctx.fillStyle = "#8B4513";
    ctx.beginPath();
    ctx.ellipse(ex + e.w / 2, ey + e.h / 2 + 2, e.w / 2, e.h / 2 - 2, 0, 0, Math.PI * 2);
    ctx.fill();

    // Goomba head
    ctx.fillStyle = "#A0522D";
    ctx.beginPath();
    ctx.ellipse(ex + e.w / 2, ey + 6, e.w / 2 + 2, 10, 0, 0, Math.PI * 2);
    ctx.fill();

    // Eyes
    const eyeDir = e.vx > 0 ? 2 : -2;
    ctx.fillStyle = "#FFF";
    ctx.beginPath();
    ctx.ellipse(ex + 8 + eyeDir, ey + 6, 4, 5, 0, 0, Math.PI * 2);
    ctx.fill();
    ctx.beginPath();
    ctx.ellipse(ex + 20 + eyeDir, ey + 6, 4, 5, 0, 0, Math.PI * 2);
    ctx.fill();

    // Pupils
    ctx.fillStyle = "#000";
    ctx.beginPath();
    ctx.arc(ex + 9 + eyeDir, ey + 7, 2, 0, Math.PI * 2);
    ctx.fill();
    ctx.beginPath();
    ctx.arc(ex + 21 + eyeDir, ey + 7, 2, 0, Math.PI * 2);
    ctx.fill();

    // Feet
    const footAnim = Math.sin(frameCount * 0.15) * 3;
    ctx.fillStyle = "#000";
    ctx.fillRect(ex + 2, ey + e.h - 6 + footAnim, 8, 6);
    ctx.fillRect(ex + e.w - 10, ey + e.h - 6 - footAnim, 8, 6);
  }

  function drawFlagPole(fp) {
    // Pole
    ctx.fillStyle = "#888";
    ctx.fillRect(fp.x, fp.y, fp.w, fp.h);

    // Ball on top
    ctx.fillStyle = "#FFD700";
    ctx.beginPath();
    ctx.arc(fp.x + fp.w / 2, fp.y, 8, 0, Math.PI * 2);
    ctx.fill();

    // Flag
    const flagWave = Math.sin(frameCount * 0.08) * 3;
    ctx.fillStyle = "#FF4444";
    ctx.beginPath();
    ctx.moveTo(fp.x + fp.w, fp.y + 5);
    ctx.lineTo(fp.x + fp.w + 35 + flagWave, fp.y + 15);
    ctx.lineTo(fp.x + fp.w, fp.y + 30);
    ctx.closePath();
    ctx.fill();

    // Flag star
    ctx.fillStyle = "#FFD700";
    ctx.font = "12px monospace";
    ctx.textAlign = "center";
    ctx.fillText("★", fp.x + fp.w + 16 + flagWave / 2, fp.y + 22);
  }

  function drawPlayer() {
    const px = player.x;
    const py = 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);

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

    // Face
    ctx.fillStyle = "#FFB68E";
    ctx.fillRect(4, 8, 18, 12);

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

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

    // Body / shirt
    ctx.fillStyle = "#E00000";
    ctx.fillRect(2, 20, 20, 6);

    // Overalls
    ctx.fillStyle = "#0000CC";
    ctx.fillRect(2, 22, 20, 6);

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

    // Legs
    if (!player.onGround) {
      // Jumping pose
      ctx.fillStyle = "#0000CC";
      ctx.fillRect(2, 28, 8, 4);
      ctx.fillRect(14, 26, 8, 4);
      // Shoes
      ctx.fillStyle = "#8B4513";
      ctx.fillRect(0, 28, 8, 4);
      ctx.fillRect(16, 26, 8, 4);
    } else if (Math.abs(player.vx) > 0.5) {
      // Walking animation
      const legOffset = player.animFrame === 1 ? 3 : 0;
      ctx.fillStyle = "#0000CC";
      ctx.fillRect(2, 28, 8, 4 + legOffset);
      ctx.fillRect(14, 28, 8, 4 - legOffset);
      // Shoes
      ctx.fillStyle = "#8B4513";
      ctx.fillRect(0, 28 + legOffset, 10, 4);
      ctx.fillRect(14, 28 - legOffset, 10, 4);
    } else {
      // Standing
      ctx.fillStyle = "#0000CC";
      ctx.fillRect(2, 28, 8, 4);
      ctx.fillRect(14, 28, 8, 4);
      // Shoes
      ctx.fillStyle = "#8B4513";
      ctx.fillRect(0, 28, 10, 4);
      ctx.fillRect(14, 28, 10, 4);
    }

    ctx.restore();
  }

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

    ctx.fillStyle = "#FFF";
    ctx.font = "bold 16px monospace";
    ctx.textAlign = "left";

    // Score
    ctx.fillText("SCORE", 16, 14);
    ctx.fillText(String(score).padStart(6, "0"), 16, 30);

    // Coins
    ctx.fillStyle = "#FFD700";
    ctx.fillText("🪙 × " + String(coins).padStart(2, "0"), 160, 24);

    // Lives
    ctx.fillStyle = "#FF6B6B";
    ctx.fillText("♥ × " + lives, 300, 24);

    // World
    ctx.fillStyle = "#FFF";
    ctx.textAlign = "right";
    ctx.fillText("WORLD 1-1", W - 16, 24);
  }

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

    // Title
    ctx.fillStyle = "#FFD700";
    ctx.font = "bold 48px monospace";
    ctx.textAlign = "center";
    ctx.fillText("SUPER CANVAS BROS", W / 2, H / 2 - 60);

    ctx.fillStyle = "#FFF";
    ctx.font = "20px monospace";
    ctx.fillText("Arrow Keys / WASD to move", W / 2, H / 2);
    ctx.fillText("Space / Up to jump", W / 2, H / 2 + 30);
    ctx.fillText("Shift to run", W / 2, H / 2 + 60);

    // Blinking prompt
    if (Math.floor(frameCount / 30) % 2 === 0) {
      ctx.fillStyle = "#FFD700";
      ctx.font = "bold 22px monospace";
      ctx.fillText("Press SPACE or ENTER to start", W / 2, H / 2 + 120);
    }

    ctx.fillStyle = "#AAA";
    ctx.font = "12px monospace";
    ctx.fillText("Collect coins, stomp enemies, reach the flag!", W / 2, H / 2 + 160);
  }

  function drawWinScreen() {
    const alpha = Math.min(winTimer / 60, 0.7);
    ctx.fillStyle = "rgba(0,0,0," + alpha + ")";
    ctx.fillRect(0, 0, W, H);

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

    ctx.fillStyle = "#FFF";
    ctx.font = "22px monospace";
    ctx.fillText("Score: " + String(score).padStart(6, "0"), W / 2, H / 2 + 10);
    ctx.fillText("Coins: " + coins, W / 2, H / 2 + 40);

    if (winTimer > 60) {
      if (Math.floor(frameCount / 30) % 2 === 0) {
        ctx.fillStyle = "#FFD700";
        ctx.font = "bold 18px monospace";
        ctx.fillText("Press SPACE to play again", W / 2, H / 2 + 90);
      }
    }

    if (winTimer > 60 && keys["Space"]) {
      state = "title";
      score = 0;
      coins = 0;
      lives = 3;
      keys["Space"] = false;
    }
  }

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

    ctx.fillStyle = "#FF4444";
    ctx.font = "bold 36px monospace";
    ctx.textAlign = "center";
    ctx.fillText("LIFE LOST", W / 2, H / 2);

    if (lives > 0) {
      ctx.fillStyle = "#FFF";
      ctx.font = "18px monospace";
      ctx.fillText("Lives remaining: " + lives, W / 2, H / 2 + 35);
    }
  }

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

  // ─── Init ────────────────────────────────────────────────────
  buildLevel();
  resetPlayer();
  state = "title";
  gameLoop();

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

rep 3 MANUAL partial/5

pi 212.4s · verify 4.1s · 69135 tok · responded: qwen3.6-27b · 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 Pixel Adventure</title>
<style>
  * { margin: 0; padding: 0; box-sizing: border-box; }
  html, body {
    background: #1a1a2e;
    display: flex;
    justify-content: center;
    align-items: center;
    height: 100vh;
    overflow: hidden;
    font-family: monospace;
  }
  #game {
    border: 3px solid #e94560;
    border-radius: 4px;
    image-rendering: pixelated;
    cursor: none;
  }
</style>
</head>
<body>
<canvas id="game" width="800" height="450"></canvas>
<script>
(function() {
  "use strict";

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

  // ── Constants ──────────────────────────────────────────────
  const GRAVITY = 0.55;
  const FRICTION = 0.82;
  const MAX_FALL = 12;
  const WORLD_W = 4000;
  const GROUND_Y = 390;

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

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

  // ── Player ─────────────────────────────────────────────────
  var player = {
    x: 60, y: GROUND_Y - 36,
    w: 24, h: 36,
    vx: 0, vy: 0,
    speed: 0.55, maxSpeed: 4.5,
    jumpForce: -10.5,
    grounded: false,
    facing: 1,
    frame: 0, frameTimer: 0,
    alive: true,
    win: false,
    invincible: 0,
    jumpHeld: false
  };

  // ── Level Data ─────────────────────────────────────────────
  // Platforms: {x, y, w, h, type}
  // type: 1=ground-block, 2=brick, 3=question-block, 4=pipe
  var platforms = [];
  var coins = [];
  var enemies = [];
  var particles = [];
  var floatingTexts = [];
  var flag = null;

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

    // Ground segments (with gaps)
    var groundSegs = [
      {x:0, w:600}, {x:680, w:500}, {x:1250, w:800},
      {x:2150, w:600}, {x:2850, w:1150}
    ];
    for (var i = 0; i < groundSegs.length; i++) {
      platforms.push({x: groundSegs[i].x, y: GROUND_Y, w: groundSegs[i].w, h: 60, type: 1});
    }

    // Raised platforms
    var raised = [
      {x: 200, y: 300, w: 96, h: 24, type: 2},
      {x: 340, y: 250, w: 48, h: 24, type: 3},
      {x: 420, y: 250, w: 48, h: 24, type: 2},
      {x: 500, y: 200, w: 48, h: 24, type: 3},

      {x: 750, y: 310, w: 48, h: 24, type: 2},
      {x: 820, y: 260, w: 48, h: 24, type: 2},
      {x: 890, y: 210, w: 96, h: 24, type: 2},

      {x: 1050, y: 280, w: 144, h: 24, type: 2},
      {x: 1080, y: 200, w: 48, h: 24, type: 3},

      {x: 1350, y: 300, w: 48, h: 24, type: 3},
      {x: 1450, y: 250, w: 48, h: 24, type: 2},
      {x: 1550, y: 200, w: 96, h: 24, type: 2},
      {x: 1680, y: 280, w: 48, h: 24, type: 3},

      {x: 1850, y: 310, w: 144, h: 24, type: 2},
      {x: 1950, y: 240, w: 48, h: 24, type: 2},
      {x: 2050, y: 180, w: 48, h: 24, type: 3},

      {x: 2250, y: 300, w: 96, h: 24, type: 2},
      {x: 2400, y: 250, w: 48, h: 24, type: 2},
      {x: 2500, y: 200, w: 96, h: 24, type: 2},

      {x: 2700, y: 310, w: 48, h: 24, type: 3},
      {x: 2800, y: 260, w: 48, h: 24, type: 2},
      {x: 2900, y: 210, w: 96, h: 24, type: 2},
      {x: 3050, y: 260, w: 48, h: 24, type: 3},
      {x: 3150, y: 200, w: 48, h: 24, type: 2},

      // Staircase near end
      {x: 3300, y: 340, w: 48, h: 24, type: 2},
      {x: 3350, y: 300, w: 48, h: 24, type: 2},
      {x: 3400, y: 260, w: 48, h: 24, type: 2},
      {x: 3450, y: 220, w: 48, h: 24, type: 2},
    ];
    for (var j = 0; j < raised.length; j++) {
      platforms.push(raised[j]);
    }

    // Pipes
    var pipes = [
      {x: 560, y: GROUND_Y - 48, w: 48, h: 48, type: 4},
      {x: 1200, y: GROUND_Y - 64, w: 48, h: 64, type: 4},
      {x: 2100, y: GROUND_Y - 56, w: 48, h: 56, type: 4},
      {x: 2750, y: GROUND_Y - 48, w: 48, h: 48, type: 4},
    ];
    for (var k = 0; k < pipes.length; k++) {
      platforms.push(pipes[k]);
    }

    // Coins
    var coinPositions = [
      // On/above platforms
      {x:220,y:270},{x:260,y:270},{x:300,y:270},
      {x:360,y:220},{x:440,y:220},{x:520,y:170},
      {x:770,y:280},{x:840,y:230},{x:910,y:180},{x:950,y:180},
      {x:1070,y:250},{x:1110,y:250},{x:1150,y:250},
      {x:1090,y:170},
      {x:1370,y:270},{x:1470,y:220},{x:1570,y:170},{x:1610,y:170},
      {x:1700,y:250},
      {x:1870,y:280},{x:1910,y:280},{x:1970,y:210},{x:2070,y:150},
      {x:2270,y:270},{x:2310,y:270},{x:2420,y:220},{x:2520,y:170},{x:2560,y:170},
      {x:2720,y:280},{x:2820,y:230},{x:2920,y:180},{x:2960,y:180},
      {x:3070,y:230},{x:3170,y:170},
      {x:3320,y:310},{x:3370,y:270},{x:3420,y:230},{x:3470,y:190},
      // Ground level coins
      {x:630,y:360},{x:660,y:360},
      {x:1220,y:360},
      {x:2120,y:360},{x:2140,y:360},
      // Arc of coins
      {x:1700,y:340},{x:1730,y:310},{x:1760,y:290},{x:1790,y:310},{x:1820,y:340},
      {x:2600,y:340},{x:2630,y:310},{x:2660,y:290},{x:2690,y:310},{x:2720,y:340},
    ];
    for (var c = 0; c < coinPositions.length; c++) {
      coins.push({x: coinPositions[c].x, y: coinPositions[c].y, w: 16, h: 16, collected: false, bobTimer: Math.random()*Math.PI*2});
    }

    // Enemies (goombas)
    var enemyPositions = [
      {x: 350, y: GROUND_Y - 24, minX: 200, maxX: 550},
      {x: 800, y: GROUND_Y - 24, minX: 700, maxX: 1150},
      {x: 1350, y: GROUND_Y - 24, minX: 1300, maxX: 1900},
      {x: 1600, y: GROUND_Y - 24, minX: 1300, maxX: 1900},
      {x: 2300, y: GROUND_Y - 24, minX: 2150, maxX: 2700},
      {x: 2500, y: GROUND_Y - 24, minX: 2150, maxX: 2700},
      {x: 3000, y: GROUND_Y - 24, minX: 2850, maxX: 3400},
      {x: 3200, y: GROUND_Y - 24, minX: 2850, maxX: 3400},
    ];
    for (var e = 0; e < enemyPositions.length; e++) {
      var ep = enemyPositions[e];
      enemies.push({
        x: ep.x, y: ep.y, w: 24, h: 24,
        vx: 1.2, alive: true,
        minX: ep.minX, maxX: ep.maxX,
        frame: 0, frameTimer: 0,
        squishTimer: 0
      });
    }

    // Flag at the end
    flag = {x: 3750, y: GROUND_Y - 160, w: 8, h: 160, reached: false};
  }

  // ── Score ──────────────────────────────────────────────────
  var score = 0;
  var coinCount = 0;
  var totalCoins = 0;
  var gameState = "playing"; // "playing", "won", "dead"
  var deathTimer = 0;
  var winTimer = 0;

  // ── Collision helpers ──────────────────────────────────────
  function rectsOverlap(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(entity) {
    entity.grounded = false;
    for (var i = 0; i < platforms.length; i++) {
      var p = platforms[i];
      if (!rectsOverlap(entity, p)) continue;

      // Determine overlap on each axis
      var overlapLeft = (entity.x + entity.w) - p.x;
      var overlapRight = (p.x + p.w) - entity.x;
      var overlapTop = (entity.y + entity.h) - p.y;
      var overlapBottom = (p.y + p.h) - entity.y;

      var minOverlap = Math.min(overlapLeft, overlapRight, overlapTop, overlapBottom);

      if (minOverlap === overlapTop && entity.vy >= 0) {
        entity.y = p.y - entity.h;
        entity.vy = 0;
        entity.grounded = true;
      } else if (minOverlap === overlapBottom && entity.vy < 0) {
        entity.y = p.y + p.h;
        entity.vy = 1;
      } else if (minOverlap === overlapLeft) {
        entity.x = p.x - entity.w;
        entity.vx = 0;
      } else if (minOverlap === overlapRight) {
        entity.x = p.x + p.w;
        entity.vx = 0;
      }
    }
  }

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

  function spawnFloatingText(x, y, text, color) {
    floatingTexts.push({x: x, y: y, text: text, color: color || "#FFD700", life: 40, vy: -1.5});
  }

  // ── Update ─────────────────────────────────────────────────
  function update() {
    if (gameState === "won") {
      winTimer++;
      updateParticles();
      updateFloatingTexts();
      return;
    }

    if (gameState === "dead") {
      deathTimer++;
      updateParticles();
      if (deathTimer > 90) {
        resetGame();
      }
      return;
    }

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

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

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

    // Jump
    if (jumpKey && player.grounded && !player.jumpHeld) {
      player.vy = player.jumpForce;
      player.grounded = false;
      player.jumpHeld = true;
    }
    if (!jumpKey) {
      player.jumpHeld = false;
      // Variable jump height
      if (player.vy < -4) player.vy = -4;
    }

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

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

    // Move player
    player.x += player.vx;
    player.y += player.vy;

    // Resolve collisions
    resolveCollisions(player);

    // Clamp to world bounds
    if (player.x < 0) player.x = 0;
    if (player.x > WORLD_W - player.w) player.x = WORLD_W - player.w;

    // Fall death
    if (player.y > H + 50) {
      gameState = "dead";
      deathTimer = 0;
      spawnParticles(player.x, H - 20, "#ff6b6b", 15);
    }

    // Animation
    player.frameTimer++;
    if (Math.abs(player.vx) > 0.5 && player.grounded) {
      if (player.frameTimer % 6 === 0) player.frame = (player.frame + 1) % 4;
    } else if (!player.grounded) {
      player.frame = 5; // jump frame
    } else {
      player.frame = 0;
    }

    // Invincibility timer
    if (player.invincible > 0) player.invincible--;

    // Coins
    for (var ci = 0; ci < coins.length; ci++) {
      var coin = coins[ci];
      if (coin.collected) continue;
      coin.bobTimer += 0.05;
      var coinRect = {x: coin.x, y: coin.y + Math.sin(coin.bobTimer) * 3, w: coin.w, h: coin.h};
      if (rectsOverlap(player, coinRect)) {
        coin.collected = true;
        coinCount++;
        score += 100;
        spawnParticles(coin.x + 8, coin.y, "#FFD700", 8);
        spawnFloatingText(coin.x, coin.y - 10, "+100");
      }
    }

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

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

      // Gravity for enemies
      var enemyGrounded = false;
      enemy.y += 2;
      for (var pi = 0; pi < platforms.length; pi++) {
        var p = platforms[pi];
        if (enemy.x < p.x + p.w && enemy.x + enemy.w > p.x &&
            enemy.y + enemy.h > p.y && enemy.y + enemy.h < p.y + 10) {
          enemy.y = p.y - enemy.h;
          enemyGrounded = true;
        }
      }
      if (!enemyGrounded) {
        enemy.y -= 2;
      }

      // Animation
      enemy.frameTimer++;
      if (enemy.frameTimer % 10 === 0) enemy.frame = (enemy.frame + 1) % 2;

      // Player collision
      if (player.invincible <= 0 && rectsOverlap(player, enemy)) {
        // Stomp from above
        if (player.vy > 0 && player.y + player.h - 8 < enemy.y + enemy.h / 2) {
          enemy.alive = false;
          enemy.squishTimer = 30;
          player.vy = -7;
          score += 200;
          spawnParticles(enemy.x + 12, enemy.y + 12, "#8B4513", 10);
          spawnFloatingText(enemy.x, enemy.y - 10, "+200", "#ff6b6b");
        } else {
          // Hurt player
          gameState = "dead";
          deathTimer = 0;
          spawnParticles(player.x + 12, player.y + 18, "#ff6b6b", 15);
        }
      }
    }

    // Flag check
    if (flag && !flag.reached) {
      var flagRect = {x: flag.x - 10, y: flag.y, w: 30, h: flag.h};
      if (rectsOverlap(player, flagRect)) {
        flag.reached = true;
        gameState = "won";
        winTimer = 0;
        score += 1000;
        spawnParticles(flag.x, flag.y, "#FFD700", 30);
        spawnParticles(flag.x, flag.y + 40, "#ff6b6b", 20);
        spawnParticles(flag.x, flag.y + 80, "#4ecdc4", 20);
      }
    }

    // Camera
    var targetX = player.x - W / 2 + player.w / 2;
    camera.x += (targetX - camera.x) * 0.1;
    if (camera.x < 0) camera.x = 0;
    if (camera.x > WORLD_W - W) camera.x = WORLD_W - W;

    updateParticles();
    updateFloatingTexts();
  }

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

  function updateFloatingTexts() {
    for (var i = floatingTexts.length - 1; i >= 0; i--) {
      var ft = floatingTexts[i];
      ft.y += ft.vy;
      ft.life--;
      if (ft.life <= 0) floatingTexts.splice(i, 1);
    }
  }

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

    // Clouds (parallax)
    ctx.fillStyle = "rgba(255,255,255,0.8)";
    var cloudPositions = [
      {x: 100, y: 50, s: 1.2}, {x: 400, y: 80, s: 0.8},
      {x: 700, y: 40, s: 1.0}, {x: 1100, y: 60, s: 1.3},
      {x: 1500, y: 30, s: 0.9}, {x: 2000, y: 70, s: 1.1},
      {x: 2500, y: 45, s: 0.7}, {x: 3000, y: 55, s: 1.0},
      {x: 3500, y: 35, s: 1.2},
    ];
    for (var i = 0; i < cloudPositions.length; i++) {
      var c = cloudPositions[i];
      var cx = c.x - camera.x * 0.3;
      // Wrap clouds
      cx = ((cx % (W + 200)) + (W + 200)) % (W + 200) - 100;
      drawCloud(cx, c.y, c.s);
    }

    // Hills (parallax)
    ctx.fillStyle = "#5a9e5a";
    var hillPositions = [
      {x: 0, w: 300, h: 80}, {x: 400, w: 250, h: 60},
      {x: 800, w: 350, h: 90}, {x: 1300, w: 280, h: 70},
      {x: 1800, w: 320, h: 85}, {x: 2300, w: 260, h: 65},
      {x: 2800, w: 300, h: 75}, {x: 3300, w: 350, h: 80},
    ];
    for (var hi = 0; hi < hillPositions.length; hi++) {
      var h = hillPositions[hi];
      var hx = h.x - camera.x * 0.5;
      drawHill(hx, GROUND_Y, h.w, h.h);
    }
  }

  function drawCloud(x, y, scale) {
    ctx.save();
    ctx.translate(x, y);
    ctx.scale(scale, scale);
    ctx.beginPath();
    ctx.arc(0, 0, 20, 0, Math.PI * 2);
    ctx.arc(25, -5, 18, 0, Math.PI * 2);
    ctx.arc(-22, 2, 16, 0, Math.PI * 2);
    ctx.arc(10, -15, 15, 0, Math.PI * 2);
    ctx.arc(-8, -12, 14, 0, Math.PI * 2);
    ctx.fill();
    ctx.restore();
  }

  function drawHill(x, baseY, w, h) {
    ctx.beginPath();
    ctx.moveTo(x, baseY);
    ctx.quadraticCurveTo(x + w / 2, baseY - h, x + w, baseY);
    ctx.fill();
  }

  function drawPlatforms() {
    for (var i = 0; i < platforms.length; i++) {
      var p = platforms[i];
      var px = p.x - camera.x;
      var py = p.y;

      // Skip if off screen
      if (px + p.w < -10 || px > W + 10) continue;

      if (p.type === 1) {
        // Ground block
        ctx.fillStyle = "#8B6914";
        ctx.fillRect(px, py, p.w, p.h);
        // Grass top
        ctx.fillStyle = "#4CAF50";
        ctx.fillRect(px, py, p.w, 8);
        ctx.fillStyle = "#66BB6A";
        ctx.fillRect(px, py, p.w, 4);
        // Dirt texture
        ctx.fillStyle = "#7A5B10";
        for (var dx = 0; dx < p.w; dx += 20) {
          ctx.fillRect(px + dx + 5, py + 15, 8, 3);
          ctx.fillRect(px + dx + 12, py + 25, 6, 3);
        }
      } else if (p.type === 2) {
        // Brick
        ctx.fillStyle = "#C67B30";
        ctx.fillRect(px, py, p.w, p.h);
        ctx.strokeStyle = "#8B5E14";
        ctx.lineWidth = 1;
        ctx.strokeRect(px, py, p.w, p.h);
        // Brick pattern
        ctx.fillStyle = "#D4943A";
        ctx.fillRect(px + 2, py + 2, p.w / 2 - 2, p.h / 2 - 2);
        ctx.fillRect(px + p.w / 2 + 1, py + 2, p.w / 2 - 3, p.h / 2 - 2);
        ctx.fillRect(px + 2, py + p.h / 2 + 1, p.w - 4, p.h / 2 - 3);
      } else if (p.type === 3) {
        // Question block
        ctx.fillStyle = "#FFB800";
        ctx.fillRect(px, py, p.w, p.h);
        ctx.strokeStyle = "#CC8800";
        ctx.lineWidth = 2;
        ctx.strokeRect(px + 1, py + 1, p.w - 2, p.h - 2);
        // Question mark
        ctx.fillStyle = "#FFF";
        ctx.font = "bold 16px monospace";
        ctx.textAlign = "center";
        ctx.textBaseline = "middle";
        ctx.fillText("?", px + p.w / 2, py + p.h / 2 + 1);
        // Shine
        ctx.fillStyle = "rgba(255,255,255,0.3)";
        ctx.fillRect(px + 3, py + 3, 6, 6);
      } else if (p.type === 4) {
        // Pipe
        // Pipe body
        ctx.fillStyle = "#2ECC40";
        ctx.fillRect(px + 4, py + 16, p.w - 8, p.h - 16);
        // Pipe top
        ctx.fillStyle = "#3DCC4A";
        ctx.fillRect(px, py, p.w, 16);
        // Pipe highlights
        ctx.fillStyle = "#5DEE6A";
        ctx.fillRect(px + 6, py + 16, 6, p.h - 16);
        ctx.fillRect(px + 2, py + 2, p.w - 4, 4);
        // Pipe shadow
        ctx.fillStyle = "#26A835";
        ctx.fillRect(px + p.w - 10, py + 16, 6, p.h - 16);
        ctx.fillRect(px + p.w - 4, py + 2, 4, 14);
      }
    }
  }

  function drawCoins() {
    for (var i = 0; i < coins.length; i++) {
      var coin = coins[i];
      if (coin.collected) continue;
      var cx = coin.x - camera.x;
      var cy = coin.y + Math.sin(coin.bobTimer) * 3;
      if (cx < -20 || cx > W + 20) continue;

      // Coin body
      var stretch = Math.abs(Math.cos(coin.bobTimer * 1.5));
      ctx.save();
      ctx.translate(cx + 8, cy + 8);
      ctx.scale(Math.max(0.2, stretch), 1);

      ctx.fillStyle = "#FFD700";
      ctx.beginPath();
      ctx.arc(0, 0, 8, 0, Math.PI * 2);
      ctx.fill();

      ctx.fillStyle = "#FFA500";
      ctx.beginPath();
      ctx.arc(0, 0, 5, 0, Math.PI * 2);
      ctx.fill();

      ctx.fillStyle = "#FFD700";
      ctx.font = "bold 10px monospace";
      ctx.textAlign = "center";
      ctx.textBaseline = "middle";
      ctx.fillText("$", 0, 0);

      ctx.restore();
    }
  }

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

    var px = player.x - camera.x;
    var py = player.y;

    // Blinking when invincible
    if (player.invincible > 0 && Math.floor(player.invincible / 3) % 2 === 0) return;

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

    // Hat
    ctx.fillStyle = "#E63946";
    ctx.fillRect(-12, -18, 20, 8);
    ctx.fillRect(-10, -18, 16, 4);

    // Face
    ctx.fillStyle = "#FFD4A0";
    ctx.fillRect(-10, -10, 18, 14);

    // Eyes
    ctx.fillStyle = "#000";
    ctx.fillRect(2, -8, 3, 4);

    // Mustache
    ctx.fillStyle = "#5C3317";
    ctx.fillRect(0, -2, 10, 3);

    // Body / shirt
    ctx.fillStyle = "#E63946";
    ctx.fillRect(-10, 4, 18, 10);

    // Overalls
    ctx.fillStyle = "#1D3557";
    ctx.fillRect(-10, 8, 18, 12);

    // Overall straps
    ctx.fillStyle = "#1D3557";
    ctx.fillRect(-8, 4, 4, 6);
    ctx.fillRect(4, 4, 4, 6);

    // Buttons
    ctx.fillStyle = "#FFD700";
    ctx.fillRect(-7, 8, 2, 2);
    ctx.fillRect(5, 8, 2, 2);

    // Legs animation
    if (player.frame === 5) {
      // Jumping pose
      ctx.fillStyle = "#1D3557";
      ctx.fillRect(-10, 18, 7, 4);
      ctx.fillRect(3, 16, 7, 4);
      // Shoes
      ctx.fillStyle = "#5C3317";
      ctx.fillRect(-11, 20, 8, 4);
      ctx.fillRect(3, 18, 8, 4);
    } else {
      var legOffset = 0;
      if (player.frame === 1) legOffset = 3;
      if (player.frame === 3) legOffset = -3;

      ctx.fillStyle = "#1D3557";
      ctx.fillRect(-10, 18, 7, 4 + legOffset);
      ctx.fillRect(3, 18, 7, 4 - legOffset);

      // Shoes
      ctx.fillStyle = "#5C3317";
      ctx.fillRect(-11, 20 + Math.max(0, legOffset), 9, 4);
      ctx.fillRect(2, 20 + Math.max(0, -legOffset), 9, 4);
    }

    ctx.restore();
  }

  function drawEnemies() {
    for (var i = 0; i < enemies.length; i++) {
      var e = enemies[i];
      var ex = e.x - camera.x;
      var ey = e.y;
      if (ex < -30 || ex > W + 30) continue;

      if (!e.alive && e.squishTimer > 0) {
        // Squished
        var alpha = e.squishTimer / 30;
        ctx.globalAlpha = alpha;
        ctx.fillStyle = "#8B4513";
        ctx.fillRect(ex, ey + e.h - 6, e.w, 6);
        ctx.globalAlpha = 1;
        continue;
      }
      if (!e.alive) continue;

      // Body
      ctx.fillStyle = "#8B4513";
      ctx.fillRect(ex + 2, ey + 4, e.w - 4, e.h - 4);

      // Head
      ctx.fillStyle = "#A0522D";
      ctx.beginPath();
      ctx.arc(ex + e.w / 2, ey + 6, 12, Math.PI, 0);
      ctx.fill();

      // Eyes
      ctx.fillStyle = "#FFF";
      ctx.fillRect(ex + 5, ey + 4, 5, 5);
      ctx.fillRect(ex + 14, ey + 4, 5, 5);
      ctx.fillStyle = "#000";
      ctx.fillRect(ex + 7, ey + 5, 3, 3);
      ctx.fillRect(ex + 16, ey + 5, 3, 3);

      // Angry eyebrows
      ctx.fillStyle = "#000";
      ctx.fillRect(ex + 5, ey + 2, 5, 2);
      ctx.fillRect(ex + 14, ey + 2, 5, 2);

      // Feet
      var footFrame = e.frame;
      ctx.fillStyle = "#000";
      if (footFrame === 0) {
        ctx.fillRect(ex, ey + e.h - 4, 8, 4);
        ctx.fillRect(ex + e.w - 8, ey + e.h - 4, 8, 4);
      } else {
        ctx.fillRect(ex + 2, ey + e.h - 4, 8, 4);
        ctx.fillRect(ex + e.w - 10, ey + e.h - 4, 8, 4);
      }
    }
  }

  function drawFlag() {
    if (!flag) return;
    var fx = flag.x - camera.x;
    var fy = flag.y;
    if (fx < -50 || fx > W + 50) return;

    // Pole
    ctx.fillStyle = "#888";
    ctx.fillRect(fx, fy, 6, flag.h);

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

    // Flag
    var flagWave = Math.sin(Date.now() / 300) * 3;
    ctx.fillStyle = flag.reached ? "#FFD700" : "#E63946";
    ctx.beginPath();
    ctx.moveTo(fx + 6, fy + 5);
    ctx.lineTo(fx + 40 + flagWave, fy + 15);
    ctx.lineTo(fx + 6, fy + 35);
    ctx.fill();

    // Star on flag
    ctx.fillStyle = "#FFF";
    ctx.font = "12px monospace";
    ctx.textAlign = "center";
    ctx.fillText("★", fx + 20 + flagWave / 2, fy + 24);

    // Base
    ctx.fillStyle = "#666";
    ctx.fillRect(fx - 8, fy + flag.h - 8, 22, 8);
  }

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

  function drawFloatingTexts() {
    for (var i = 0; i < floatingTexts.length; i++) {
      var ft = floatingTexts[i];
      var alpha = ft.life / 40;
      ctx.globalAlpha = alpha;
      ctx.fillStyle = ft.color;
      ctx.font = "bold 14px monospace";
      ctx.textAlign = "center";
      ctx.fillText(ft.text, ft.x - camera.x, ft.y);
    }
    ctx.globalAlpha = 1;
  }

  function drawHUD() {
    // Score background
    ctx.fillStyle = "rgba(0,0,0,0.5)";
    ctx.fillRect(0, 0, W, 40);

    // Score
    ctx.fillStyle = "#FFF";
    ctx.font = "bold 18px monospace";
    ctx.textAlign = "left";
    ctx.textBaseline = "middle";
    ctx.fillText("SCORE: " + score, 15, 20);

    // Coins
    ctx.fillStyle = "#FFD700";
    ctx.fillText("🪙 " + coinCount + "/" + totalCoins, 200, 20);

    // Controls hint
    ctx.fillStyle = "rgba(255,255,255,0.5)";
    ctx.font = "11px monospace";
    ctx.textAlign = "right";
    ctx.fillText("← → or A/D to move | SPACE or ↑ to jump", W - 15, 20);
  }

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

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

    ctx.fillStyle = "#FFF";
    ctx.font = "18px monospace";
    ctx.fillText("Restarting...", W / 2, H / 2 + 20);
  }

  function drawWinScreen() {
    var alpha = Math.min(1, winTimer / 60);
    ctx.fillStyle = "rgba(0,0,0," + (alpha * 0.5) + ")";
    ctx.fillRect(0, 0, W, H);

    ctx.save();
    ctx.globalAlpha = alpha;

    // Stars
    var starCount = 5;
    for (var i = 0; i < starCount; i++) {
      var sx = W / 2 + Math.cos(winTimer * 0.03 + i * 1.25) * 150;
      var sy = H / 2 - 60 + Math.sin(winTimer * 0.04 + i * 1.25) * 20;
      ctx.fillStyle = "#FFD700";
      ctx.font = "24px monospace";
      ctx.textAlign = "center";
      ctx.fillText("★", sx, sy);
    }

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

    ctx.fillStyle = "#FFF";
    ctx.font = "20px monospace";
    ctx.fillText("Score: " + score, W / 2, H / 2 + 30);
    ctx.fillText("Coins: " + coinCount + "/" + totalCoins, W / 2, H / 2 + 55);

    ctx.restore();
  }

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

    drawBackground();
    drawPlatforms();
    drawCoins();
    drawFlag();
    drawEnemies();
    drawPlayer();
    drawParticles();
    drawFloatingTexts();
    drawHUD();

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

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

  // ── Reset ──────────────────────────────────────────────────
  function resetGame() {
    player.x = 60;
    player.y = GROUND_Y - 36;
    player.vx = 0;
    player.vy = 0;
    player.grounded = false;
    player.facing = 1;
    player.frame = 0;
    player.invincible = 0;
    player.jumpHeld = false;
    score = 0;
    coinCount = 0;
    particles = [];
    floatingTexts = [];
    gameState = "playing";
    deathTimer = 0;
    winTimer = 0;
    camera.x = 0;
    buildLevel();
    totalCoins = coins.length;
  }

  // ── Init ───────────────────────────────────────────────────
  buildLevel();
  totalCoins = coins.length;
  requestAnimationFrame(gameLoop);

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

rep 4 MANUAL partial/5

pi 208.1s · verify 4.1s · 53022 tok · responded: qwen3.6-27b · 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 Canvas Bros</title>
<style>
  * { margin: 0; padding: 0; box-sizing: border-box; }
  body {
    background: #1a1a2e;
    display: flex;
    justify-content: center;
    align-items: center;
    min-height: 100vh;
    font-family: 'Courier New', monospace;
    overflow: hidden;
  }
  #game {
    border: 3px solid #e94560;
    border-radius: 4px;
    image-rendering: pixelated;
    cursor: default;
  }
</style>
</head>
<body>
<canvas id="game" width="800" height="450"></canvas>
<script>
(function() {
  "use strict";

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

  // ─── Constants ───
  var GRAVITY = 0.55;
  var FRICTION = 0.82;
  var MAX_FALL = 12;
  var TILE = 32;

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

  function clearJustPressed() {
    keyJustPressed = {};
  }

  // ─── Level Data ───
  // Ground tiles: 0=air, 1=ground, 2=brick, 3=question block, 4=pipe body, 5=pipe top, 6=flag pole, 7=flag top
  var LEVEL_W = 120; // tiles wide
  var LEVEL_H = 14;  // tiles high (450/32 ≈ 14)

  function buildLevel() {
    var map = [];
    for (var y = 0; y < LEVEL_H; y++) {
      map[y] = [];
      for (var x = 0; x < LEVEL_W; x++) {
        // Ground: bottom 2 rows
        if (y >= LEVEL_H - 2) {
          map[y][x] = 1;
        } else {
          map[y][x] = 0;
        }
      }
    }

    // Gaps in ground
    var gaps = [[18, 20], [45, 47], [78, 80]];
    for (var g = 0; g < gaps.length; g++) {
      for (var gx = gaps[g][0]; gx <= gaps[g][1]; gx++) {
        map[LEVEL_H - 2][gx] = 0;
        map[LEVEL_H - 1][gx] = 0;
      }
    }

    // Platforms (floating rows of bricks)
    var platforms = [
      { x: 8, y: 9, w: 5, type: 2 },
      { x: 15, y: 7, w: 3, type: 2 },
      { x: 22, y: 10, w: 4, type: 2 },
      { x: 30, y: 8, w: 6, type: 2 },
      { x: 38, y: 6, w: 3, type: 3 },
      { x: 42, y: 10, w: 3, type: 2 },
      { x: 50, y: 9, w: 4, type: 2 },
      { x: 56, y: 7, w: 5, type: 2 },
      { x: 62, y: 10, w: 3, type: 2 },
      { x: 68, y: 8, w: 4, type: 3 },
      { x: 75, y: 6, w: 3, type: 2 },
      { x: 82, y: 9, w: 5, type: 2 },
      { x: 88, y: 7, w: 4, type: 2 },
      { x: 95, y: 10, w: 3, type: 2 },
      { x: 100, y: 8, w: 6, type: 2 },
    ];

    for (var p = 0; p < platforms.length; p++) {
      var pl = platforms[p];
      for (var px = pl.x; px < pl.x + pl.w; px++) {
        map[pl.y][px] = pl.type;
      }
    }

    // Pipes
    var pipes = [
      { x: 12, h: 2 },
      { x: 35, h: 3 },
      { x: 55, h: 2 },
      { x: 72, h: 4 },
      { x: 92, h: 3 },
    ];
    for (var pi = 0; pi < pipes.length; pi++) {
      var pipe = pipes[pi];
      var baseY = LEVEL_H - 2;
      map[baseY - 1][pipe.x] = 5; // pipe top
      for (var py = baseY; py < baseY + pipe.h - 1; py++) {
        if (py < LEVEL_H) map[py][pipe.x] = 4;
      }
      // Pipe right side
      map[baseY - 1][pipe.x + 1] = 5;
      for (var py2 = baseY; py2 < baseY + pipe.h - 1; py2++) {
        if (py2 < LEVEL_H) map[py2][pipe.x + 1] = 4;
      }
    }

    // Staircase near end
    for (var sx = 106; sx <= 114; sx++) {
      var stairH = sx - 106 + 1;
      for (var sy = LEVEL_H - 2; sy > LEVEL_H - 2 - stairH && sy >= 0; sy--) {
        map[sy][sx] = 1;
      }
    }

    // Flag pole
    var flagX = 116;
    for (var fy = 3; fy < LEVEL_H - 2; fy++) {
      map[fy][flagX] = 6;
    }
    map[3][flagX] = 7; // flag top

    return map;
  }

  var levelMap = buildLevel();

  // ─── Coins ───
  function buildCoins() {
    var coins = [];
    var coinPositions = [
      [9, 8], [10, 8], [11, 8],
      [16, 6], [17, 6],
      [23, 9], [24, 9],
      [31, 7], [32, 7], [33, 7], [34, 7],
      [39, 5],
      [43, 9],
      [51, 8], [52, 8],
      [57, 6], [58, 6], [59, 6],
      [63, 9],
      [69, 7], [70, 7],
      [76, 5],
      [83, 8], [84, 8], [85, 8],
      [89, 6], [90, 6],
      [96, 9],
      [101, 7], [102, 7], [103, 7], [104, 7],
      // Arc over gap
      [19, 10],
      [46, 10],
      [79, 10],
    ];
    for (var i = 0; i < coinPositions.length; i++) {
      coins.push({
        x: coinPositions[i][0] * TILE + 8,
        y: coinPositions[i][1] * TILE + 4,
        collected: false,
        animFrame: 0
      });
    }
    return coins;
  }

  var coins = buildCoins();

  // ─── Enemies ───
  function buildEnemies() {
    var enemies = [];
    var enemyPositions = [
      [10, LEVEL_H - 3],
      [25, LEVEL_H - 3],
      [33, LEVEL_H - 3],
      [48, LEVEL_H - 3],
      [60, LEVEL_H - 3],
      [70, LEVEL_H - 3],
      [85, LEVEL_H - 3],
      [97, LEVEL_H - 3],
    ];
    for (var i = 0; i < enemyPositions.length; i++) {
      enemies.push({
        x: enemyPositions[i][0] * TILE,
        y: enemyPositions[i][1] * TILE,
        w: TILE - 4,
        h: TILE - 4,
        vx: -1.2,
        alive: true,
        squishTimer: 0,
        animFrame: 0
      });
    }
    return enemies;
  }

  var enemies = buildEnemies();

  // ─── Clouds & Decorations ───
  var clouds = [];
  for (var i = 0; i < 25; i++) {
    clouds.push({
      x: i * 250 + Math.random() * 100,
      y: 20 + Math.random() * 60,
      w: 60 + Math.random() * 50,
      h: 25 + Math.random() * 15
    });
  }

  var bushes = [];
  for (var i = 0; i < 30; i++) {
    bushes.push({
      x: i * 200 + Math.random() * 80,
      w: 50 + Math.random() * 40,
      h: 15 + Math.random() * 10
    });
  }

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

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

  // ─── Player ───
  var player = {
    x: 3 * TILE,
    y: (LEVEL_H - 4) * TILE,
    w: 24,
    h: 30,
    vx: 0,
    vy: 0,
    onGround: false,
    facing: 1,
    walkFrame: 0,
    walkTimer: 0,
    dead: false,
    deathTimer: 0,
    invincible: 0,
    winTimer: 0,
    won: false
  };

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

  // ─── Score ───
  var score = 0;
  var coinCount = 0;
  var totalCoins = coins.length;
  var lives = 3;
  var gameTime = 0;
  var gameState = "play"; // play, dead, win, gameover

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

  function isSolid(tx, ty) {
    var t = getTile(tx, ty);
    return t === 1 || t === 2 || t === 3 || t === 4 || t === 5;
  }

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

  // ─── Hit block from below ───
  function hitBlock(tx, ty) {
    var t = getTile(tx, ty);
    if (t === 3) { // Question block
      levelMap[ty][tx] = 2; // become brick
      score += 100;
      coinCount++;
      spawnParticles(tx * TILE + TILE / 2, ty * TILE, "#FFD700", 8);
    }
  }

  // ─── Update Player ───
  function updatePlayer() {
    if (gameState !== "play") return;

    var accel = 0.45;
    var maxSpeed = 4.5;
    var jumpForce = -10.5;

    // Horizontal movement
    if (keys["ArrowLeft"] || keys["KeyA"]) {
      player.vx -= accel;
      player.facing = -1;
    }
    if (keys["ArrowRight"] || keys["KeyD"]) {
      player.vx += accel;
      player.facing = 1;
    }

    // Friction
    if (!keys["ArrowLeft"] && !keys["ArrowRight"] && !keys["KeyA"] && !keys["KeyD"]) {
      player.vx *= FRICTION;
      if (Math.abs(player.vx) < 0.1) player.vx = 0;
    }

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

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

    // Variable jump height (release early = shorter jump)
    if ((keys["Space"] || keys["ArrowUp"] || keys["KeyW"]) && player.vy < 0) {
      // keep going up
    } else {
      // cut upward momentum on release (handled naturally by gravity)
    }

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

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

    // ─── Horizontal Collision ───
    player.x += player.vx;
    var left = Math.floor(player.x / TILE);
    var right = Math.floor((player.x + player.w - 1) / TILE);
    var top = Math.floor(player.y / TILE);
    var bottom = Math.floor((player.y + player.h - 1) / TILE);

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

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

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

    for (var ty2 = top; ty2 <= bottom; ty2++) {
      for (var tx2 = left; tx2 <= right; tx2++) {
        if (isSolid(tx2, ty2)) {
          if (player.vy > 0) {
            player.y = ty2 * TILE - player.h;
            player.vy = 0;
            player.onGround = true;
          } else if (player.vy < 0) {
            player.y = (ty2 + 1) * TILE;
            player.vy = 0;
            hitBlock(tx2, ty2);
          }
        }
      }
    }

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

    // Fall death
    if (player.y > LEVEL_H * TILE + 50) {
      playerDie();
    }

    // Invincibility timer
    if (player.invincible > 0) player.invincible--;

    // Win check - reached flag
    var playerTileX = Math.floor((player.x + player.w / 2) / TILE);
    var playerTileY = Math.floor((player.y + player.h / 2) / TILE);
    if (getTile(playerTileX, playerTileY) === 6 || getTile(playerTileX, playerTileY) === 7) {
      if (!player.won) {
        player.won = true;
        gameState = "win";
        player.winTimer = 120;
        score += 1000;
        spawnParticles(player.x + player.w / 2, player.y, "#FFD700", 20);
      }
    }
  }

  function playerDie() {
    if (player.dead) return;
    lives--;
    if (lives <= 0) {
      gameState = "gameover";
    } else {
      gameState = "dead";
      player.deathTimer = 90;
      player.vy = -8;
    }
  }

  // ─── Update Enemies ───
  function updateEnemies() {
    if (gameState !== "play") return;

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

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

      e.x += e.vx;
      e.animFrame += 0.05;

      // Gravity for enemies
      var eBottom = Math.floor((e.y + e.h) / TILE);
      var eLeft = Math.floor(e.x / TILE);
      var eRight = Math.floor((e.x + e.w) / TILE);

      // Check if on ground
      if (!isSolid(eLeft, eBottom) && !isSolid(eRight, eBottom)) {
        e.y += 3;
      } else {
        // Snap to tile
        e.y = eBottom * TILE - e.h;
      }

      // Wall collision / edge detection
      var wallLeft = Math.floor(e.x / TILE);
      var wallRight = Math.floor((e.x + e.w + 1) / TILE);
      var wallY = Math.floor((e.y + e.h / 2) / TILE);

      if (isSolid(wallLeft, wallY)) {
        e.vx = Math.abs(e.vx);
      }
      if (isSolid(wallRight, wallY)) {
        e.vx = -Math.abs(e.vx);
      }

      // Edge detection - turn around at edges
      var edgeCheck = Math.floor((e.x + (e.vx > 0 ? e.w + 2 : -2)) / TILE);
      var belowEdge = Math.floor((e.y + e.h + 2) / TILE);
      if (!isSolid(edgeCheck, belowEdge)) {
        e.vx = -e.vx;
      }

      // Player collision
      if (!player.dead && player.invincible <= 0) {
        var playerRect = { x: player.x, y: player.y, w: player.w, h: player.h };
        var enemyRect = { x: e.x, y: e.y, w: e.w, h: e.h };

        if (rectCollide(playerRect, enemyRect)) {
          // Check if player is stomping
          if (player.vy > 0 && player.y + player.h - 8 < e.y + e.h / 2) {
            e.alive = false;
            e.squishTimer = 30;
            player.vy = -7; // bounce
            score += 200;
            spawnParticles(e.x + e.w / 2, e.y + e.h / 2, "#8B4513", 6);
          } else {
            playerDie();
          }
        }
      }
    }
  }

  // ─── Update Coins ───
  function updateCoins() {
    if (gameState !== "play") return;

    for (var i = 0; i < coins.length; i++) {
      var c = coins[i];
      if (c.collected) continue;

      c.animFrame += 0.08;

      var playerRect = { x: player.x, y: player.y, w: player.w, h: player.h };
      var coinRect = { x: c.x, y: c.y, w: 16, h: 16 };

      if (rectCollide(playerRect, coinRect)) {
        c.collected = true;
        coinCount++;
        score += 50;
        spawnParticles(c.x + 8, c.y + 8, "#FFD700", 5);
      }
    }
  }

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

  // ─── Update Camera ───
  function updateCamera() {
    var targetX = player.x - W / 3;
    camera.x += (targetX - camera.x) * 0.1;
    if (camera.x < 0) camera.x = 0;
    if (camera.x > LEVEL_W * TILE - W) camera.x = LEVEL_W * TILE - W;
  }

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

    // Clouds (parallax)
    ctx.fillStyle = "rgba(255,255,255,0.9)";
    for (var i = 0; i < clouds.length; i++) {
      var c = clouds[i];
      var cx = c.x - camera.x * 0.3;
      // Wrap
      while (cx < -100) cx += 250 * clouds.length * 0.3;
      while (cx > W + 100) cx -= 250 * clouds.length * 0.3;
      if (cx > -100 && cx < W + 100) {
        drawCloud(cx, c.y, c.w, c.h);
      }
    }

    // Bushes (parallax)
    for (var i = 0; i < bushes.length; i++) {
      var b = bushes[i];
      var bx = b.x - camera.x * 0.6;
      if (bx > -100 && bx < W + 100) {
        drawBush(bx, H - 64, b.w, b.h);
      }
    }
  }

  function drawCloud(x, y, w, h) {
    ctx.beginPath();
    ctx.arc(x + w * 0.3, y + h * 0.5, h * 0.5, 0, Math.PI * 2);
    ctx.arc(x + w * 0.6, y + h * 0.3, h * 0.6, 0, Math.PI * 2);
    ctx.arc(x + w * 0.85, y + h * 0.5, h * 0.45, 0, Math.PI * 2);
    ctx.fill();
  }

  function drawBush(x, y, w, h) {
    ctx.fillStyle = "#228B22";
    ctx.beginPath();
    ctx.arc(x + w * 0.3, y, h * 0.7, 0, Math.PI * 2);
    ctx.arc(x + w * 0.65, y - h * 0.1, h * 0.8, 0, Math.PI * 2);
    ctx.arc(x + w * 0.9, y, h * 0.6, 0, Math.PI * 2);
    ctx.fill();
  }

  function drawTile(tx, ty, type) {
    var x = tx * TILE - camera.x;
    var y = ty * TILE;
    if (x < -TILE || x > W + TILE) return;

    switch (type) {
      case 1: // Ground
        if (ty === LEVEL_H - 2) {
          // Top ground row (grass)
          ctx.fillStyle = "#8B4513";
          ctx.fillRect(x, y + 4, TILE, TILE - 4);
          ctx.fillStyle = "#228B22";
          ctx.fillRect(x, y, TILE, 6);
          // Grass detail
          ctx.fillStyle = "#32CD32";
          for (var gx = 0; gx < 4; gx++) {
            ctx.fillRect(x + gx * 8 + 2, y - 2, 2, 4);
          }
        } else {
          ctx.fillStyle = "#8B4513";
          ctx.fillRect(x, y, TILE, TILE);
          // Dirt pattern
          ctx.fillStyle = "#A0522D";
          ctx.fillRect(x + 4, y + 4, 8, 8);
          ctx.fillRect(x + 20, y + 16, 8, 8);
        }
        break;

      case 2: // Brick
        ctx.fillStyle = "#C84C09";
        ctx.fillRect(x, y, TILE, TILE);
        ctx.strokeStyle = "#8B3000";
        ctx.lineWidth = 1;
        ctx.strokeRect(x + 0.5, y + 0.5, TILE - 1, TILE - 1);
        // Brick lines
        ctx.fillStyle = "#8B3000";
        ctx.fillRect(x, y + TILE / 2 - 1, TILE, 2);
        ctx.fillRect(x + TILE / 2 - 1, y, 2, TILE / 2);
        ctx.fillRect(x + TILE / 4 - 1, y + TILE / 2, 2, TILE / 2);
        ctx.fillRect(x + TILE * 3 / 4 - 1, y + TILE / 2, 2, TILE / 2);
        break;

      case 3: // Question block
        ctx.fillStyle = "#FFB800";
        ctx.fillRect(x, y, TILE, TILE);
        ctx.strokeStyle = "#CC8800";
        ctx.lineWidth = 2;
        ctx.strokeRect(x + 1, y + 1, TILE - 2, TILE - 2);
        // Question mark
        ctx.fillStyle = "#8B4513";
        ctx.font = "bold 18px Courier New";
        ctx.textAlign = "center";
        ctx.textBaseline = "middle";
        ctx.fillText("?", x + TILE / 2, y + TILE / 2 + 1);
        break;

      case 4: // Pipe body
        ctx.fillStyle = "#00A800";
        ctx.fillRect(x + 2, y, TILE - 4, TILE);
        ctx.fillStyle = "#00D800";
        ctx.fillRect(x + 4, y, 8, TILE);
        ctx.fillStyle = "#006800";
        ctx.fillRect(x + TILE - 6, y, 4, TILE);
        break;

      case 5: // Pipe top
        ctx.fillStyle = "#00A800";
        ctx.fillRect(x - 2, y, TILE + 4, TILE);
        ctx.fillStyle = "#00D800";
        ctx.fillRect(x, y + 2, 10, TILE - 4);
        ctx.fillStyle = "#006800";
        ctx.fillRect(x + TILE - 8, y + 2, 6, TILE - 4);
        ctx.strokeStyle = "#005800";
        ctx.lineWidth = 2;
        ctx.strokeRect(x - 2, y, TILE + 4, TILE);
        break;

      case 6: // Flag pole
        ctx.fillStyle = "#888";
        ctx.fillRect(x + TILE / 2 - 2, y, 4, TILE);
        break;

      case 7: // Flag top
        ctx.fillStyle = "#888";
        ctx.fillRect(x + TILE / 2 - 2, y, 4, TILE);
        // Flag
        ctx.fillStyle = "#E03030";
        ctx.beginPath();
        ctx.moveTo(x + TILE / 2 + 2, y);
        ctx.lineTo(x + TILE / 2 + 22, y + 8);
        ctx.lineTo(x + TILE / 2 + 2, y + 16);
        ctx.fill();
        // Ball on top
        ctx.fillStyle = "#FFD700";
        ctx.beginPath();
        ctx.arc(x + TILE / 2, y, 4, 0, Math.PI * 2);
        ctx.fill();
        break;
    }
  }

  function drawPlayer() {
    if (player.dead && gameState === "dead") {
      // Death animation - rising
      ctx.save();
      ctx.translate(player.x - camera.x + player.w / 2, player.y + player.h / 2);
      ctx.rotate(player.deathTimer * 0.05);
      drawPlayerSprite(-player.w / 2, -player.h / 2);
      ctx.restore();
      return;
    }

    if (gameState === "gameover") return;

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

    var px = player.x - camera.x;
    var py = player.y;

    ctx.save();
    ctx.translate(px + player.w / 2, py + player.h / 2);
    if (player.facing === -1) ctx.scale(-1, 1);
    drawPlayerSprite(-player.w / 2, -player.h / 2);
    ctx.restore();
  }

  function drawPlayerSprite(x, y) {
    // Hat
    ctx.fillStyle = "#E03030";
    ctx.fillRect(x + 2, y, 20, 8);
    ctx.fillRect(x, y + 2, 24, 4);

    // Face
    ctx.fillStyle = "#FFCC99";
    ctx.fillRect(x + 4, y + 8, 16, 10);

    // Eye
    ctx.fillStyle = "#000";
    ctx.fillRect(x + 14, y + 10, 3, 3);

    // Mustache
    ctx.fillStyle = "#4A2800";
    ctx.fillRect(x + 10, y + 15, 10, 2);

    // Body / overalls
    ctx.fillStyle = "#E03030";
    ctx.fillRect(x + 2, y + 18, 20, 4);

    ctx.fillStyle = "#0050D0";
    ctx.fillRect(x + 2, y + 20, 20, 6);

    // Buttons
    ctx.fillStyle = "#FFD700";
    ctx.fillRect(x + 6, y + 21, 2, 2);
    ctx.fillRect(x + 16, y + 21, 2, 2);

    // Legs
    var legOffset = 0;
    if (!player.onGround) {
      legOffset = 2; // tucked legs when jumping
    } else if (Math.abs(player.vx) > 0.5) {
      legOffset = Math.sin(player.walkFrame * Math.PI / 2) * 3;
    }

    ctx.fillStyle = "#0050D0";
    ctx.fillRect(x + 2, y + 26 - legOffset, 8, 4 + legOffset);
    ctx.fillRect(x + 14, y + 26 + legOffset, 8, 4 - legOffset);

    // Shoes
    ctx.fillStyle = "#8B4513";
    ctx.fillRect(x + 1, y + 28, 9, 2);
    ctx.fillRect(x + 14, y + 28, 9, 2);
  }

  function drawCoin(c) {
    var x = c.x - camera.x;
    var y = c.y;
    if (x < -20 || x > W + 20) return;

    var scaleX = Math.cos(c.animFrame * 3);
    ctx.save();
    ctx.translate(x + 8, y + 8);
    ctx.scale(scaleX, 1);

    // Coin body
    ctx.fillStyle = "#FFD700";
    ctx.beginPath();
    ctx.arc(0, 0, 7, 0, Math.PI * 2);
    ctx.fill();

    ctx.fillStyle = "#FFA500";
    ctx.beginPath();
    ctx.arc(0, 0, 5, 0, Math.PI * 2);
    ctx.fill();

    ctx.fillStyle = "#FFD700";
    ctx.beginPath();
    ctx.arc(0, 0, 3, 0, Math.PI * 2);
    ctx.fill();

    ctx.restore();
  }

  function drawEnemy(e) {
    var x = e.x - camera.x;
    var y = e.y;
    if (x < -TILE || x > W + TILE) return;

    if (!e.alive) {
      if (e.squishTimer > 0) {
        // Squished
        ctx.fillStyle = "#8B4513";
        ctx.fillRect(x + 2, y + e.h - 8, e.w - 4, 8);
      }
      return;
    }

    // Body
    ctx.fillStyle = "#8B4513";
    ctx.fillRect(x + 2, y + 4, e.w - 4, e.h - 4);

    // Head
    ctx.fillStyle = "#A0522D";
    ctx.beginPath();
    ctx.arc(x + e.w / 2, y + 4, e.w / 2 - 2, Math.PI, 0);
    ctx.fill();

    // Eyes
    var eyeBounce = Math.sin(e.animFrame * 4) * 1;
    ctx.fillStyle = "#FFF";
    ctx.fillRect(x + 6, y + 6 + eyeBounce, 6, 6);
    ctx.fillRect(x + e.w - 12, y + 6 + eyeBounce, 6, 6);

    ctx.fillStyle = "#000";
    ctx.fillRect(x + 8, y + 8 + eyeBounce, 3, 3);
    ctx.fillRect(x + e.w - 10, y + 8 + eyeBounce, 3, 3);

    // Feet
    var footAnim = Math.sin(e.animFrame * 6) * 2;
    ctx.fillStyle = "#000";
    ctx.fillRect(x + 2, y + e.h - 4 + footAnim, 8, 4);
    ctx.fillRect(x + e.w - 10, y + e.h - 4 - footAnim, 8, 4);
  }

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

  function drawHUD() {
    // Score
    ctx.fillStyle = "#FFF";
    ctx.font = "bold 16px Courier New";
    ctx.textAlign = "left";
    ctx.textBaseline = "top";
    ctx.fillText("SCORE: " + score, 10, 10);

    // Coins
    ctx.fillStyle = "#FFD700";
    ctx.fillText("COINS: " + coinCount + " / " + totalCoins, 10, 30);

    // Lives
    ctx.fillStyle = "#E03030";
    ctx.fillText("LIVES: " + lives, 10, 50);

    // Controls hint
    ctx.fillStyle = "rgba(255,255,255,0.5)";
    ctx.font = "11px Courier New";
    ctx.textAlign = "right";
    ctx.fillText("Arrow Keys / WASD: Move  |  Space / Up: Jump", W - 10, 10);
  }

  function drawWinScreen() {
    player.winTimer--;

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

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

    ctx.fillStyle = "#FFF";
    ctx.font = "20px Courier New";
    ctx.fillText("Score: " + score, W / 2, H / 2 + 20);
    ctx.fillText("Coins: " + coinCount + " / " + totalCoins, W / 2, H / 2 + 50);

    if (player.winTimer <= 0) {
      ctx.fillStyle = "#AAA";
      ctx.font = "14px Courier New";
      ctx.fillText("Press R to restart", W / 2, H / 2 + 90);
    }
  }

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

    ctx.fillStyle = "#E03030";
    ctx.font = "bold 48px Courier New";
    ctx.textAlign = "center";
    ctx.textBaseline = "middle";
    ctx.fillText("GAME OVER", W / 2, H / 2 - 20);

    ctx.fillStyle = "#FFF";
    ctx.font = "20px Courier New";
    ctx.fillText("Final Score: " + score, W / 2, H / 2 + 20);

    ctx.fillStyle = "#AAA";
    ctx.font = "14px Courier New";
    ctx.fillText("Press R to restart", W / 2, H / 2 + 60);
  }

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

    ctx.fillStyle = "#FFF";
    ctx.font = "bold 24px Courier New";
    ctx.textAlign = "center";
    ctx.textBaseline = "middle";
    ctx.fillText("Lives remaining: " + lives, W / 2, H / 2);
  }

  // ─── Restart ───
  function resetGame() {
    levelMap = buildLevel();
    coins = buildCoins();
    enemies = buildEnemies();
    particles = [];
    player = {
      x: 3 * TILE,
      y: (LEVEL_H - 4) * TILE,
      w: 24,
      h: 30,
      vx: 0,
      vy: 0,
      onGround: false,
      facing: 1,
      walkFrame: 0,
      walkTimer: 0,
      dead: false,
      deathTimer: 0,
      invincible: 0,
      winTimer: 0,
      won: false
    };
    camera = { x: 0, y: 0 };
    score = 0;
    coinCount = 0;
    lives = 3;
    gameState = "play";
  }

  // ─── Main Loop ───
  function update() {
    gameTime++;

    if (gameState === "play") {
      updatePlayer();
      updateEnemies();
      updateCoins();
      updateCamera();
    } else if (gameState === "dead") {
      player.vy += GRAVITY;
      player.y += player.vy;
      player.deathTimer--;
      if (player.deathTimer <= 0) {
        gameState = "play";
        player.x = 3 * TILE;
        player.y = (LEVEL_H - 4) * TILE;
        player.vx = 0;
        player.vy = 0;
        player.dead = false;
        player.invincible = 60;
        camera.x = 0;
      }
    } else if (gameState === "win") {
      updateParticles();
    }

    updateParticles();

    // Restart
    if (keyJustPressed["KeyR"]) {
      resetGame();
    }

    clearJustPressed();
  }

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

    drawBackground();

    // Draw tiles in view
    var startTX = Math.max(0, Math.floor(camera.x / TILE) - 1);
    var endTX = Math.min(LEVEL_W, Math.ceil((camera.x + W) / TILE) + 1);

    for (var ty = 0; ty < LEVEL_H; ty++) {
      for (var tx = startTX; tx < endTX; tx++) {
        var tile = levelMap[ty][tx];
        if (tile > 0) {
          drawTile(tx, ty, tile);
        }
      }
    }

    // Draw coins
    for (var i = 0; i < coins.length; i++) {
      if (!coins[i].collected) drawCoin(coins[i]);
    }

    // Draw enemies
    for (var i = 0; i < enemies.length; i++) {
      drawEnemy(enemies[i]);
    }

    // Draw player
    drawPlayer();

    // Draw particles
    drawParticles();

    // HUD
    drawHUD();

    // Overlays
    if (gameState === "win") drawWinScreen();
    if (gameState === "gameover") drawGameOverScreen();
    if (gameState === "dead") drawDeathScreen();
  }

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

  // ─── Start ───
  gameLoop();

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

rep 5 MANUAL fail/5

pi 205.0s · verify 4.1s · 49201 tok · responded: qwen3.6-27b · 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 Canvas Bros</title>
<style>
  * { margin: 0; padding: 0; box-sizing: border-box; }
  html, body {
    background: #111;
    display: flex;
    justify-content: center;
    align-items: center;
    height: 100%;
    overflow: hidden;
    font-family: monospace;
  }
  #game {
    border: 3px solid #333;
    border-radius: 4px;
    image-rendering: pixelated;
    cursor: default;
  }
</style>
</head>
<body>
<canvas id="game" width="800" height="450"></canvas>
<script>
"use strict";

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

// ── Constants ──────────────────────────────────────────────
const GRAVITY = 0.55;
const FRICTION = 0.82;
const MOVE_ACCEL = 0.65;
const MAX_SPEED = 4.5;
const JUMP_FORCE = -10.5;
const BOUNCE_FORCE = -7;
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; });

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

// ── Level Data ─────────────────────────────────────────────
// Legend: 1=ground, 2=brick, 3=question block, 4=pipe body, 5=pipe top, 6=hard block
// Platforms are defined separately for clarity
const LEVEL_WIDTH = 120; // tiles
const LEVEL_HEIGHT = 14; // tiles (450/32 ≈ 14)

// Build a 2D tile map (0 = air)
const tiles = [];
for (let y = 0; y < LEVEL_HEIGHT; y++) {
  tiles[y] = [];
  for (let x = 0; x < LEVEL_WIDTH; x++) {
    tiles[y][x] = 0;
  }
}

// Ground (rows 12-13 = bottom two rows)
for (let x = 0; x < LEVEL_WIDTH; x++) {
  tiles[13][x] = 1;
  tiles[12][x] = 1;
}

// Gaps in ground
const gaps = [[18,20],[42,44],[72,75]];
gaps.forEach(([a,b]) => {
  for (let x = a; x <= b; x++) {
    tiles[12][x] = 0;
    tiles[13][x] = 0;
  }
});

// Raised platforms (brick blocks)
const platforms = [
  // [startX, y, length]
  [8, 9, 5],
  [15, 7, 3],
  [22, 9, 4],
  [28, 6, 6],
  [35, 9, 3],
  [38, 7, 5],
  [46, 9, 4],
  [50, 6, 3],
  [55, 9, 6],
  [62, 7, 4],
  [66, 5, 3],
  [76, 9, 5],
  [82, 7, 3],
  [86, 9, 4],
  [92, 6, 5],
  [98, 9, 3],
  [104, 7, 4],
];

platforms.forEach(([sx, y, len]) => {
  for (let i = 0; i < len; i++) {
    tiles[y][sx + i] = 2;
  }
});

// Question blocks (hold coins)
const questionBlocks = [
  [10, 9], [24, 9], [30, 6], [48, 9], [52, 6], [58, 9],
  [64, 7], [78, 9], [84, 7], [94, 6], [106, 7]
];
questionBlocks.forEach(([x, y]) => { tiles[y][x] = 3; });

// Pipes
const pipes = [
  [13, 10, 2],   // x, topRow, height
  [33, 11, 1],
  [60, 10, 2],
  [88, 11, 1],
  [100, 10, 2],
];
pipes.forEach(([x, topRow, h]) => {
  tiles[topRow][x] = 5; // pipe top
  for (let dy = 1; dy < h; dy++) {
    if (topRow + dy < LEVEL_HEIGHT) tiles[topRow + dy][x] = 4; // pipe body
  }
});

// Staircase near the end
for (let step = 0; step < 6; step++) {
  for (let dy = 0; dy <= step; dy++) {
    tiles[11 - dy][110 + step] = 6;
  }
}

// ── Coins ──────────────────────────────────────────────────
const coins = [];
// Coins on platforms
platforms.forEach(([sx, y, len]) => {
  for (let i = 0; i < len; i += 2) {
    coins.push({ x: (sx + i) * TILE + TILE/2, y: (y - 1) * TILE + TILE/2, collected: false });
  }
});
// Question block coins (tracked separately)
const qCoins = questionBlocks.map(([x, y]) => ({
  x: x * TILE + TILE/2, y: (y - 1) * TILE + TILE/2, collected: false
}));
// Floating coins in arcs
const arcCoins = [
  [20, 10], [21, 9], [22, 10],
  [44, 10], [45, 9], [46, 10],
  [74, 10], [75, 9], [76, 10],
  [80, 8], [81, 7], [82, 8],
  [96, 8], [97, 7], [98, 8],
];
arcCoins.forEach(([x, y]) => {
  coins.push({ x: x * TILE + TILE/2, y: y * TILE + TILE/2, collected: false });
});

// ── Enemies ────────────────────────────────────────────────
const enemies = [];
function spawnEnemy(x, y) {
  enemies.push({
    x: x * TILE, y: y * TILE,
    w: TILE - 4, h: TILE - 4,
    vx: -1.2, alive: true, squishTimer: 0,
    startX: x * TILE, patrolDist: 80
  });
}
spawnEnemy(12, 11);
spawnEnemy(25, 11);
spawnEnemy(36, 8);
spawnEnemy(47, 11);
spawnEnemy(56, 8);
spawnEnemy(63, 11);
spawnEnemy(77, 8);
spawnEnemy(83, 11);
spawnEnemy(93, 5);
spawnEnemy(105, 11);

// ── Flag / Goal ────────────────────────────────────────────
const flagX = 116 * TILE;
const flagTop = 3 * TILE;

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

// ── Player ─────────────────────────────────────────────────
const player = {
  x: 3 * TILE, y: 10 * TILE,
  w: 22, h: 28,
  vx: 0, vy: 0,
  onGround: false,
  facing: 1,
  walkFrame: 0,
  walkTimer: 0,
  jumpHeld: false,
  coyoteTime: 0,
  jumpBuffered: false,
  invincible: 0,
  dead: false,
  winTimer: 0,
};

let score = 0;
let coinCount = 0;
let gameState = "playing"; // playing, won, dead
let deathTimer = 0;

// ── Collision helpers ──────────────────────────────────────
function isSolid(tx, ty) {
  if (tx < 0 || tx >= LEVEL_WIDTH || ty < 0 || ty >= LEVEL_HEIGHT) return ty >= LEVEL_HEIGHT;
  const t = tiles[ty][tx];
  return t === 1 || t === 2 || t === 3 || t === 4 || t === 5 || t === 6;
}

function resolveX(entity) {
  const left = Math.floor(entity.x / TILE);
  const right = Math.floor((entity.x + entity.w - 1) / TILE);
  const top = Math.floor(entity.y / TILE);
  const bot = Math.floor((entity.y + entity.h - 1) / TILE);
  for (let ty = top; ty <= bot; ty++) {
    for (let tx = left; tx <= right; tx++) {
      if (isSolid(tx, ty)) {
        if (entity.vx > 0) {
          entity.x = tx * TILE - entity.w;
          entity.vx = 0;
        } else if (entity.vx < 0) {
          entity.x = (tx + 1) * TILE;
          entity.vx = 0;
        }
        return;
      }
    }
  }
}

function resolveY(entity) {
  const left = Math.floor(entity.x / TILE);
  const right = Math.floor((entity.x + entity.w - 1) / TILE);
  const top = Math.floor(entity.y / TILE);
  const bot = Math.floor((entity.y + entity.h - 1) / TILE);
  entity.onGround = false;
  for (let ty = top; ty <= bot; ty++) {
    for (let tx = left; tx <= right; tx++) {
      if (isSolid(tx, ty)) {
        if (entity.vy > 0) {
          entity.y = ty * TILE - entity.h;
          entity.vy = 0;
          entity.onGround = true;
        } else if (entity.vy < 0) {
          entity.y = (ty + 1) * TILE;
          entity.vy = 0;
          // Hit question block from below
          if (tiles[ty][tx] === 3) {
            tiles[ty][tx] = 6; // become hard block
            coinCount++;
            score += 100;
            spawnParticles(tx * TILE + TILE/2, ty * TILE, "#FFD700", 8);
            // Find matching qCoin
            for (const qc of qCoins) {
              const qtx = Math.floor(qc.x / TILE);
              const qty = Math.floor((qc.y + TILE/2) / TILE);
              if (qtx === tx && qty === ty + 1 && !qc.collected) {
                qc.collected = true;
                break;
              }
            }
          }
        }
        return;
      }
    }
  }
}

// ── Drawing helpers ────────────────────────────────────────
function drawSky() {
  // Gradient sky
  const grad = ctx.createLinearGradient(0, 0, 0, H);
  grad.addColorStop(0, "#5C94FC");
  grad.addColorStop(0.6, "#87CEEB");
  grad.addColorStop(1, "#B0E0FF");
  ctx.fillStyle = grad;
  ctx.fillRect(0, 0, W, H);

  // Clouds (parallax)
  ctx.fillStyle = "rgba(255,255,255,0.9)";
  const clouds = [
    [100, 50, 60], [350, 35, 80], [600, 65, 50], [900, 45, 70],
    [1200, 55, 55], [1500, 30, 75], [1800, 60, 65], [2100, 40, 80],
    [2400, 50, 50], [2700, 35, 70], [3000, 60, 55], [3300, 45, 65],
    [3600, 55, 75], [3900, 40, 60], [4200, 50, 70],
  ];
  clouds.forEach(([cx, cy, cw]) => {
    const px = cx - cam.x * 0.3;
    if (px > -100 && px < W + 100) {
      ctx.beginPath();
      ctx.ellipse(px, cy, cw/2, 14, 0, 0, Math.PI*2);
      ctx.fill();
      ctx.beginPath();
      ctx.ellipse(px - cw*0.25, cy + 5, cw*0.3, 10, 0, 0, Math.PI*2);
      ctx.fill();
      ctx.beginPath();
      ctx.ellipse(px + cw*0.25, cy + 3, cw*0.25, 11, 0, 0, Math.PI*2);
      ctx.fill();
    }
  });

  // Hills (parallax)
  ctx.fillStyle = "#4AA52E";
  const hills = [
    [80, 140, 100], [400, 90, 60], [700, 120, 80], [1000, 110, 90],
    [1400, 130, 70], [1700, 85, 55], [2000, 115, 95], [2400, 100, 75],
    [2800, 125, 85], [3200, 95, 65], [3600, 110, 80], [4000, 105, 70],
  ];
  hills.forEach(([hx, hw, hh]) => {
    const px = hx - cam.x * 0.5;
    if (px > -200 && px < W + 200) {
      ctx.beginPath();
      ctx.ellipse(px, H - 64, hw, hh, 0, Math.PI, 0);
      ctx.fill();
    }
  });

  // Bushes (parallax)
  ctx.fillStyle = "#2D8B2D";
  const bushes = [
    [150, 40], [500, 30], [850, 45], [1100, 35], [1500, 40],
    [1900, 30], [2200, 45], [2600, 35], [3000, 40], [3400, 30],
    [3800, 45], [4100, 35],
  ];
  bushes.forEach(([bx, bw]) => {
    const px = bx - cam.x * 0.6;
    if (px > -80 && px < W + 80) {
      ctx.beginPath();
      ctx.ellipse(px, H - 66, bw, 12, 0, Math.PI, 0);
      ctx.fill();
    }
  });
}

function drawTile(tx, ty) {
  const t = tiles[ty][tx];
  if (t === 0) return;
  const x = tx * TILE - cam.x;
  const y = ty * TILE - cam.y;
  if (x < -TILE || x > W + TILE || y < -TILE || y > H + TILE) return;

  switch (t) {
    case 1: // Ground
      ctx.fillStyle = "#C84C09";
      ctx.fillRect(x, y, TILE, TILE);
      ctx.fillStyle = "#E07020";
      ctx.fillRect(x + 2, y + 2, TILE - 4, TILE - 4);
      // Grass top
      if (ty === 0 || tiles[ty-1][tx] === 0) {
        ctx.fillStyle = "#4AA52E";
        ctx.fillRect(x, y, TILE, 6);
        ctx.fillStyle = "#5CC03A";
        for (let i = 0; i < 4; i++) {
          ctx.fillRect(x + i*8 + 1, y - 2, 4, 4);
        }
      }
      break;
    case 2: // Brick
      ctx.fillStyle = "#C84C09";
      ctx.fillRect(x, y, TILE, TILE);
      ctx.fillStyle = "#E07020";
      ctx.fillRect(x + 1, y + 1, 14, 14);
      ctx.fillRect(x + 17, y + 1, 14, 14);
      ctx.fillRect(x + 1, y + 17, 14, 14);
      ctx.fillRect(x + 17, y + 17, 14, 14);
      break;
    case 3: // Question block
      ctx.fillStyle = "#E8A000";
      ctx.fillRect(x, y, TILE, TILE);
      ctx.fillStyle = "#FFD700";
      ctx.fillRect(x + 2, y + 2, TILE - 4, TILE - 4);
      ctx.fillStyle = "#E8A000";
      ctx.font = "bold 18px monospace";
      ctx.textAlign = "center";
      ctx.fillText("?", x + TILE/2, y + TILE/2 + 6);
      break;
    case 4: // Pipe body
      ctx.fillStyle = "#20A020";
      ctx.fillRect(x, y, TILE, TILE);
      ctx.fillStyle = "#30C030";
      ctx.fillRect(x + 4, y, 12, TILE);
      ctx.fillStyle = "#188018";
      ctx.fillRect(x + TILE - 6, y, 4, TILE);
      break;
    case 5: // Pipe top
      ctx.fillStyle = "#20A020";
      ctx.fillRect(x - 2, y, TILE + 4, TILE);
      ctx.fillStyle = "#30C030";
      ctx.fillRect(x + 2, y + 2, 14, TILE - 2);
      ctx.fillStyle = "#188018";
      ctx.fillRect(x + TILE - 4, y + 2, 4, TILE - 2);
      break;
    case 6: // Hard block
      ctx.fillStyle = "#888";
      ctx.fillRect(x, y, TILE, TILE);
      ctx.fillStyle = "#999";
      ctx.fillRect(x + 2, y + 2, TILE - 4, TILE - 4);
      ctx.fillStyle = "#777";
      ctx.fillRect(x + 1, y + 1, TILE - 2, 2);
      ctx.fillRect(x + 1, y + 1, 2, TILE - 2);
      break;
  }
}

function drawPlayer() {
  if (player.dead) return;
  const px = player.x - cam.x;
  const py = player.y - cam.y;

  // Invincibility flash
  if (player.invincible > 0 && Math.floor(player.invincible / 3) % 2 === 0) return;

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

  // Body
  ctx.fillStyle = "#E02020"; // Red overalls
  ctx.fillRect(2, 10, player.w - 4, player.h - 10);

  // Head
  ctx.fillStyle = "#FDBA74"; // Skin
  ctx.fillRect(3, 0, player.w - 6, 12);

  // Hat
  ctx.fillStyle = "#E02020";
  ctx.fillRect(1, -2, player.w - 2, 7);
  ctx.fillRect(4, -4, player.w - 8, 4);

  // Eyes
  ctx.fillStyle = "#000";
  ctx.fillRect(player.w - 10, 3, 3, 4);

  // Legs (animated)
  const legOffset = player.onGround && Math.abs(player.vx) > 0.5
    ? Math.sin(player.walkTimer * 0.3) * 3 : 0;
  ctx.fillStyle = "#E02020";
  ctx.fillRect(2, player.h - 6, 8, 6);
  ctx.fillRect(player.w - 10, player.h - 6, 8, 6);
  // Shoes
  ctx.fillStyle = "#8B4513";
  ctx.fillRect(1, player.h - 3 + legOffset, 9, 3);
  ctx.fillRect(player.w - 10, player.h - 3 - legOffset, 9, 3);

  ctx.restore();
}

function drawEnemy(e) {
  if (!e.alive) {
    if (e.squishTimer > 0) {
      const ex = e.x - cam.x;
      const ey = e.y - cam.y;
      ctx.fillStyle = "#8B4513";
      ctx.fillRect(ex, ey + e.h - 8, e.w, 8);
    }
    return;
  }
  const ex = e.x - cam.x;
  const ey = e.y - cam.y;
  if (ex < -TILE || ex > W + TILE) return;

  // Goomba-like body
  ctx.fillStyle = "#8B4513";
  ctx.beginPath();
  ctx.ellipse(ex + e.w/2, ey + e.h/2 + 2, e.w/2, e.h/2 - 2, 0, 0, Math.PI*2);
  ctx.fill();

  // Feet
  const footAnim = Math.sin(Date.now() * 0.008) * 3;
  ctx.fillStyle = "#222";
  ctx.fillRect(ex + 2, ey + e.h - 4 + footAnim, 8, 4);
  ctx.fillRect(ex + e.w - 10, ey + e.h - 4 - footAnim, 8, 4);

  // Eyes
  ctx.fillStyle = "#FFF";
  ctx.fillRect(ex + 5, ey + 6, 6, 7);
  ctx.fillRect(ex + e.w - 11, ey + 6, 6, 7);
  ctx.fillStyle = "#000";
  ctx.fillRect(ex + 7, ey + 8, 3, 5);
  ctx.fillRect(ex + e.w - 9, ey + 8, 3, 5);

  // Angry eyebrows
  ctx.fillStyle = "#000";
  ctx.fillRect(ex + 4, ey + 4, 7, 2);
  ctx.fillRect(ex + e.w - 11, ey + 4, 7, 2);
}

function drawCoin(cx, cy) {
  const bob = Math.sin(Date.now() * 0.005 + cx * 0.1) * 3;
  const stretch = Math.abs(Math.cos(Date.now() * 0.003 + cx * 0.05));

  ctx.save();
  ctx.translate(cx, cy + bob);
  ctx.scale(stretch * 0.5 + 0.5, 1);

  ctx.fillStyle = "#FFD700";
  ctx.beginPath();
  ctx.ellipse(0, 0, 8, 10, 0, 0, Math.PI*2);
  ctx.fill();

  ctx.fillStyle = "#FFA500";
  ctx.beginPath();
  ctx.ellipse(0, 0, 5, 7, 0, 0, Math.PI*2);
  ctx.fill();

  ctx.fillStyle = "#FFD700";
  ctx.font = "bold 10px monospace";
  ctx.textAlign = "center";
  ctx.textBaseline = "middle";
  ctx.fillText("$", 0, 1);

  ctx.restore();
}

function drawFlag() {
  const fx = flagX - cam.x;
  const fy = flagTop - cam.y;
  if (fx < -50 || fx > W + 50) return;

  // Pole
  ctx.fillStyle = "#888";
  ctx.fillRect(fx + 14, fy, 4, 12 * TILE - flagTop);

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

  // Flag
  const wave = Math.sin(Date.now() * 0.004) * 3;
  ctx.fillStyle = "#E02020";
  ctx.beginPath();
  ctx.moveTo(fx + 18, fy + 5);
  ctx.lineTo(fx + 50 + wave, fy + 18);
  ctx.lineTo(fx + 18, fy + 35);
  ctx.closePath();
  ctx.fill();

  // Star on flag
  ctx.fillStyle = "#FFD700";
  ctx.font = "14px monospace";
  ctx.textAlign = "center";
  ctx.fillText("★", fx + 33 + wave/2, fy + 24);
}

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

  ctx.fillStyle = "#FFF";
  ctx.font = "bold 16px monospace";
  ctx.textAlign = "left";
  ctx.fillText("SCORE", 16, 16);
  ctx.fillStyle = "#FFD700";
  ctx.fillText(String(score).padStart(6, "0"), 16, 30);

  ctx.fillStyle = "#FFF";
  ctx.textAlign = "center";
  ctx.fillText("COINS", W/2 - 60, 16);
  ctx.fillStyle = "#FFD700";
  ctx.fillText("× " + coinCount, W/2 - 60, 30);

  ctx.fillStyle = "#FFF";
  ctx.textAlign = "right";
  ctx.fillText("WORLD 1-1", W - 16, 24);
}

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

// ── Game Logic ─────────────────────────────────────────────
function resetPlayer() {
  player.x = 3 * TILE;
  player.y = 10 * TILE;
  player.vx = 0;
  player.vy = 0;
  player.dead = false;
  player.invincible = 0;
  player.winTimer = 0;
  cam.x = 0;
  cam.y = 0;
  gameState = "playing";
}

function update() {
  if (gameState === "dead") {
    deathTimer--;
    if (deathTimer <= 0) resetPlayer();
    return;
  }

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

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

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

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

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

  // Coyote time (brief grace period after leaving edge)
  if (player.onGround) {
    player.coyoteTime = 6;
  } else {
    player.coyoteTime--;
  }

  // Jump
  if (jumpKey && !player.jumpHeld) {
    player.jumpBuffered = true;
  }
  if (!jumpKey) {
    player.jumpBuffered = false;
  }
  player.jumpHeld = jumpKey;

  if (player.jumpBuffered && player.coyoteTime > 0) {
    player.vy = JUMP_FORCE;
    player.coyoteTime = 0;
    player.jumpBuffered = false;
  }

  // Variable jump height (release to fall faster)
  if (!jumpKey && player.vy < -2) {
    player.vy *= 0.85;
  }

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

  // Walk animation
  if (player.onGround && Math.abs(player.vx) > 0.5) {
    player.walkTimer++;
  }

  // Move player X
  player.x += player.vx;
  resolveX(player);

  // Move player Y
  player.y += player.vy;
  resolveY(player);

  // Prevent going left of level start
  if (player.x < 0) player.x = 0;

  // Fall into pit
  if (player.y > H + 50) {
    playerDie();
  }

  // Invincibility timer
  if (player.invincible > 0) player.invincible--;

  // ── Coin collection ──
  coins.forEach(c => {
    if (c.collected) return;
    const dx = (player.x + player.w/2) - c.x;
    const dy = (player.y + player.h/2) - c.y;
    if (Math.abs(dx) < 18 && Math.abs(dy) < 18) {
      c.collected = true;
      coinCount++;
      score += 50;
      spawnParticles(c.x, c.y, "#FFD700", 6);
    }
  });

  // Question block coins
  qCoins.forEach(c => {
    if (c.collected) return;
    const dx = (player.x + player.w/2) - c.x;
    const dy = (player.y + player.h/2) - c.y;
    if (Math.abs(dx) < 18 && Math.abs(dy) < 18) {
      c.collected = true;
      coinCount++;
      score += 50;
      spawnParticles(c.x, c.y, "#FFD700", 6);
    }
  });

  // ── Enemy logic ──
  enemies.forEach(e => {
    if (!e.alive) {
      if (e.squishTimer > 0) e.squishTimer--;
      return;
    }

    // Patrol
    e.x += e.vx;
    if (Math.abs(e.x - e.startX) > e.patrolDist) {
      e.vx *= -1;
    }

    // Gravity for enemies
    const eBot = Math.floor((e.y + e.h) / TILE);
    const eMid = Math.floor((e.x + e.w/2) / TILE);
    if (!isSolid(eMid, eBot)) {
      e.y += 3;
    }

    // Player collision
    if (player.invincible <= 0) {
      const overlapX = Math.min(player.x + player.w, e.x + e.w) - Math.max(player.x, e.x);
      const overlapY = Math.min(player.y + player.h, e.y + e.h) - Math.max(player.y, e.y);
      if (overlapX > 4 && overlapY > 4) {
        // Check if player is stomping
        if (player.vy > 0 && player.y + player.h - 10 < e.y + e.h/2) {
          e.alive = false;
          e.squishTimer = 30;
          player.vy = BOUNCE_FORCE;
          score += 200;
          spawnParticles(e.x + e.w/2, e.y, "#8B4513", 8);
        } else {
          playerDie();
        }
      }
    }
  });

  // ── Flag / Win condition ──
  if (player.x + player.w > flagX && player.x < flagX + 32) {
    gameState = "won";
    score += 1000;
    spawnParticles(flagX + 16, flagTop + 20, "#FFD700", 20);
    spawnParticles(flagX + 16, flagTop + 20, "#E02020", 15);
  }

  // ── Camera ──
  const targetX = player.x - W / 3;
  const targetY = Math.min(player.y - H / 2, 0);
  cam.x += (targetX - cam.x) * 0.1;
  cam.y += (targetY - cam.y) * 0.1;
  if (cam.x < 0) cam.x = 0;
  if (cam.x > (LEVEL_WIDTH * TILE - W)) cam.x = LEVEL_WIDTH * TILE - W;
  if (cam.y > 0) cam.y = 0;

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

function playerDie() {
  if (player.dead) return;
  player.dead = true;
  gameState = "dead";
  deathTimer = 90;
  player.vy = -8;
  spawnParticles(player.x + player.w/2, player.y + player.h/2, "#E02020", 12);
}

// ── Render ─────────────────────────────────────────────────
function render() {
  ctx.clearRect(0, 0, W, H);

  drawSky();

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

  for (let ty = startTY; ty <= endTY; ty++) {
    for (let tx = startTX; tx <= endTX; tx++) {
      drawTile(tx, ty);
    }
  }

  // Draw coins
  coins.forEach(c => {
    if (!c.collected) drawCoin(c.x - cam.x, c.y - cam.y);
  });
  qCoins.forEach(c => {
    if (!c.collected) drawCoin(c.x - cam.x, c.y - cam.y);
  });

  // Draw flag
  drawFlag();

  // Draw enemies
  enemies.forEach(e => drawEnemy(e));

  // Draw player
  drawPlayer();

  // Draw particles
  drawParticles();

  // Draw HUD
  drawHUD();

  // Game states
  if (gameState === "dead") {
    ctx.fillStyle = "rgba(0,0,0,0.6)";
    ctx.fillRect(0, 0, W, H);
    ctx.fillStyle = "#E02020";
    ctx.font = "bold 36px monospace";
    ctx.textAlign = "center";
    ctx.fillText("GAME OVER", W/2, H/2 - 10);
    ctx.fillStyle = "#FFF";
    ctx.font = "16px monospace";
    ctx.fillText("Restarting...", W/2, H/2 + 25);
  }

  if (gameState === "won") {
    const alpha = Math.min(1, player.winTimer / 60);
    ctx.fillStyle = `rgba(0,0,0,${alpha * 0.5})`;
    ctx.fillRect(0, 0, W, H);
    ctx.fillStyle = "#FFD700";
    ctx.font = "bold 40px monospace";
    ctx.textAlign = "center";
    ctx.fillText("★ YOU WIN! ★", W/2, H/2 - 20);
    ctx.fillStyle = "#FFF";
    ctx.font = "20px monospace";
    ctx.fillText("Score: " + score, W/2, H/2 + 20);
    ctx.fillText("Coins: " + coinCount, W/2, H/2 + 48);
    if (player.winTimer > 120) {
      ctx.fillStyle = "#AAA";
      ctx.font = "14px monospace";
      ctx.fillText("Press R to restart", W/2, H/2 + 80);
      if (keys["KeyR"]) {
        score = 0;
        coinCount = 0;
        // Reset coins
        coins.forEach(c => c.collected = false);
        qCoins.forEach(c => c.collected = false);
        // Reset enemies
        enemies.length = 0;
        spawnEnemy(12, 11);
        spawnEnemy(25, 11);
        spawnEnemy(36, 8);
        spawnEnemy(47, 11);
        spawnEnemy(56, 8);
        spawnEnemy(63, 11);
        spawnEnemy(77, 8);
        spawnEnemy(83, 11);
        spawnEnemy(93, 5);
        spawnEnemy(105, 11);
        // Reset question blocks
        questionBlocks.forEach(([x, y]) => { tiles[y][x] = 3; });
        resetPlayer();
      }
    }
  }
}

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

// Start!
gameLoop();
</script>
</body>
</html>