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

ornith-1.0-35b — web-platformer

rep 1 MANUAL fail/5

pi 75.8s · verify 4.4s · 48030 tok · responded: ornith-1.0-35b · 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">
<title>Super Platformer</title>
<style>
  * { margin: 0; padding: 0; box-sizing: border-box; }
  body {
    background: #1a1a2e;
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    height: 100vh;
    overflow: hidden;
    font-family: monospace;
  }
  canvas {
    border: 3px solid #e94560;
    border-radius: 4px;
    display: block;
    image-rendering: pixelated;
  }
</style>
</head>
<body>
<canvas id="game" width="800" height="450"></canvas>
<script>
(() => {
  const canvas = document.getElementById('game');
  const ctx = canvas.getContext('2d');
  const W = 800, H = 450;

  // ── 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 ─────────────────────────────────────────
  const TILE = 32;
  const LEVEL_W = 200;          // tiles wide
  const LEVEL_H = 14;           // tiles tall (450/32 ≈ 14)

  // tile types: 0=air, 1=ground, 2=brick, 3=coin, 4=goal_flag, 5=pipe, 6=question
  const levelData = [];
  for (let y = 0; y < LEVEL_H; y++) {
    levelData[y] = new Array(LEVEL_W).fill(0);
  }

  // ground (rows 12-13)
  for (let x = 0; x < LEVEL_W; x++) {
    // gaps
    if ((x >= 25 && x <= 27) || (x >= 55 && x <= 58) || (x >= 90 && x <= 93) || (x >= 130 && x <= 133)) continue;
    levelData[12][x] = 1;
    levelData[13][x] = 1;
  }

  // raised platforms (row 9)
  for (let x = 8; x <= 13; x++) levelData[9][x] = 2;
  for (let x = 20; x <= 26; x++) levelData[9][x] = 2;
  for (let x = 35; x <= 42; x++) levelData[9][x] = 2;
  for (let x = 48; x <= 54; x++) levelData[9][x] = 2;
  for (let x = 65; x <= 72; x++) levelData[9][x] = 2;
  for (let x = 78; x <= 85; x++) levelData[9][x] = 2;
  for (let x = 98; x <= 105; x++) levelData[9][x] = 2;
  for (let x = 110; x <= 116; x++) levelData[9][x] = 2;
  for (let x = 120; x <= 128; x++) levelData[9][x] = 2;
  for (let x = 140; x <= 148; x++) levelData[9][x] = 2;

  // higher platforms (row 6)
  for (let x = 15; x <= 18; x++) levelData[6][x] = 2;
  for (let x = 40; x <= 44; x++) levelData[6][x] = 2;
  for (let x = 70; x <= 74; x++) levelData[6][x] = 2;
  for (let x = 100; x <= 104; x++) levelData[6][x] = 2;
  for (let x = 135; x <= 139; x++) levelData[6][x] = 2;

  // coins on platforms
  const coinPositions = [];
  function placeCoins(row, startX, count) {
    for (let i = 0; i < count; i++) {
      const x = startX + i * 2;
      if (x < LEVEL_W) { levelData[row][x] = 3; coinPositions.push({x, y: row, collected: false}); }
    }
  }
  placeCoins(8, 9, 3);
  placeCoins(8, 21, 4);
  placeCoins(8, 36, 3);
  placeCoins(8, 49, 5);
  placeCoins(8, 66, 4);
  placeCoins(8, 79, 5);
  placeCoins(8, 99, 3);
  placeCoins(8, 111, 4);
  placeCoins(8, 121, 5);
  placeCoins(8, 141, 3);
  placeCoins(5, 16, 2);
  placeCoins(5, 41, 3);
  placeCoins(5, 71, 2);
  placeCoins(5, 101, 3);
  placeCoins(5, 136, 2);

  // extra coins on ground
  placeCoins(11, 5, 3);
  placeCoins(11, 30, 4);
  placeCoins(11, 60, 3);
  placeCoins(11, 80, 5);
  placeCoins(11, 100, 3);
  placeCoins(11, 120, 4);
  placeCoins(11, 150, 3);

  // pipes (obstacles)
  function placePipe(x, height) {
    for (let dy = 0; dy < height; dy++) {
      const row = 12 - height + dy;
      if (row >= 0 && row < LEVEL_H) {
        levelData[row][x] = 5;
        levelData[row][x+1] = 5;
      }
    }
  }
  placePipe(18, 2);
  placePipe(45, 3);
  placePipe(75, 2);
  placePipe(108, 3);
  placePipe(145, 2);

  // question blocks
  levelData[8][11] = 6;
  levelData[8][50] = 6;
  levelData[8][81] = 6;
  levelData[8][113] = 6;

  // goal flag
  levelData[4][160] = 4;
  levelData[5][160] = 4;
  levelData[6][160] = 4;
  levelData[7][160] = 4;
  levelData[8][160] = 4;
  levelData[9][160] = 4;
  levelData[10][160] = 4;
  levelData[11][160] = 4;
  levelData[12][160] = 1; // base
  levelData[13][160] = 1;

  // ── Player ─────────────────────────────────────────────
  const player = {
    x: 3 * TILE,
    y: 10 * TILE,
    w: 24,
    h: 30,
    vx: 0,
    vy: 0,
    onGround: false,
    facing: 1,
    score: 0,
    lives: 3,
    dead: false,
    win: false,
    animFrame: 0,
    animTimer: 0,
    jumpHeld: false,
    coyoteTime: 0,
    jumpBuffer: 0
  };

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

  // ── 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) * 6,
        vy: (Math.random() - 1) * 5,
        life: 30 + Math.random() * 20,
        color,
        size: 2 + Math.random() * 3
      });
    }
  }

  // ── Floating score texts ───────────────────────────────
  const floatingTexts = [];
  function spawnFloatingText(x, y, text, color) {
    floatingTexts.push({ x, y, text, color, life: 40 });
  }

  // ── Physics constants ──────────────────────────────────
  const GRAVITY = 0.55;
  const MAX_FALL = 12;
  const MOVE_ACCEL = 0.6;
  const MOVE_FRICTION = 0.78;
  const MAX_SPEED = 5.5;
  const JUMP_FORCE = -10.5;
  const JUMP_CUTOFF = -3; // when releasing jump early
  const COYOTE_FRAMES = 6;
  const JUMP_BUFFER_FRAMES = 8;

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

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

  function collides(px, py, pw, ph) {
    const left = Math.floor(px / TILE);
    const right = Math.floor((px + pw - 1) / TILE);
    const top = Math.floor(py / TILE);
    const bottom = Math.floor((py + ph - 1) / TILE);
    for (let ty = top; ty <= bottom; ty++) {
      for (let tx = left; tx <= right; tx++) {
        if (isSolid(getTile(tx, ty))) return true;
      }
    }
    return false;
  }

  // ── Update ─────────────────────────────────────────────
  function update() {
    if (player.dead || player.win) return;

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

    // Horizontal movement
    if (left) { player.vx -= MOVE_ACCEL; player.facing = -1; }
    if (right) { player.vx += MOVE_ACCEL; player.facing = 1; }
    if (!left && !right) player.vx *= MOVE_FRICTION;
    player.vx = Math.max(-MAX_SPEED, Math.min(MAX_SPEED, player.vx));
    if (Math.abs(player.vx) < 0.1) player.vx = 0;

    // Coyote time & jump buffer
    if (player.onGround) player.coyoteTime = COYOTE_FRAMES;
    else player.coyoteTime = Math.max(0, player.coyoteTime - 1);

    if (jumpKey && !player.jumpHeld) player.jumpBuffer = JUMP_BUFFER_FRAMES;
    else player.jumpBuffer = Math.max(0, player.jumpBuffer - 1);
    player.jumpHeld = jumpKey;

    // Jump
    if (player.jumpBuffer > 0 && player.coyoteTime > 0) {
      player.vy = JUMP_FORCE;
      player.onGround = false;
      player.coyoteTime = 0;
      player.jumpBuffer = 0;
      spawnParticles(player.x + player.w/2, player.y + player.h, '#fff', 5);
    }

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

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

    // Move X
    player.x += player.vx;
    if (collides(player.x, player.y, player.w, player.h)) {
      // bounce back
      if (player.vx > 0) player.x = Math.floor((player.x + player.w) / TILE) * TILE - player.w;
      else if (player.vx < 0) player.x = Math.floor(player.x / TILE) * TILE + TILE;
      player.vx = 0;
    }

    // Move Y
    player.y += player.vy;
    player.onGround = false;
    if (collides(player.x, player.y, player.w, player.h)) {
      if (player.vy > 0) {
        // landed
        player.y = Math.floor((player.y + player.h) / TILE) * TILE - player.h;
        player.vy = 0;
        player.onGround = true;
      } else if (player.vy < 0) {
        // hit head
        player.y = Math.floor(player.y / TILE) * TILE + TILE;
        player.vy = 0;
      }
    }

    // Collect coins
    const ptx = Math.floor((player.x + player.w/2) / TILE);
    const pty = Math.floor((player.y + player.h/2) / TILE);
    for (let dy = -1; dy <= 1; dy++) {
      for (let dx = -1; dx <= 1; dx++) {
        const tx = ptx + dx, ty = pty + dy;
        const tile = getTile(tx, ty);
        if (tile === 3) {
          levelData[ty][tx] = 0;
          player.score += 100;
          spawnParticles(tx * TILE + TILE/2, ty * TILE + TILE/2, '#FFD700', 8);
          spawnFloatingText(tx * TILE, ty * TILE - 10, '+100', '#FFD700');
        }
        if (tile === 6) {
          levelData[ty][tx] = 0;
          player.score += 200;
          spawnParticles(tx * TILE + TILE/2, ty * TILE + TILE/2, '#FF6B6B', 12);
          spawnFloatingText(tx * TILE, ty * TILE - 10, '+200', '#FF6B6B');
        }
      }
    }

    // Goal check
    const goalTile = getTile(160, 8);
    if (goalTile === 4 || (player.x + player.w > 160 * TILE && player.x < 160 * TILE + TILE && player.y < 10 * TILE)) {
      player.win = true;
      player.score += 1000;
      spawnParticles(160 * TILE + TILE/2, 6 * TILE, '#00FF00', 30);
    }

    // Fall death
    if (player.y > LEVEL_H * TILE + 50) {
      player.lives--;
      if (player.lives <= 0) {
        player.dead = true;
      } else {
        respawn();
      }
    }

    // Left boundary
    if (player.x < 0) player.x = 0;

    // 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;
      player.animTimer = 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);
    }

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

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

  function respawn() {
    player.x = 3 * TILE;
    player.y = 10 * TILE;
    player.vx = 0;
    player.vy = 0;
    player.dead = false;
    player.win = false;
  }

  // ── Drawing ────────────────────────────────────────────
  function drawSky() {
    // gradient sky
    const grad = ctx.createLinearGradient(0, 0, 0, H);
    grad.addColorStop(0, '#4DC9F6');
    grad.addColorStop(0.7, '#87CEEB');
    grad.addColorStop(1, '#B0E0E6');
    ctx.fillStyle = grad;
    ctx.fillRect(0, 0, W, H);
  }

  function drawClouds() {
    ctx.fillStyle = 'rgba(255,255,255,0.8)';
    const cloudPositions = [
      [100, 60, 60], [350, 40, 80], [600, 70, 50], [900, 50, 70],
      [1200, 65, 55], [1500, 45, 75], [1800, 55, 60], [2100, 70, 50],
      [2500, 40, 65], [2800, 60, 55], [3200, 50, 70], [3600, 65, 50],
      [4000, 45, 60], [4400, 55, 65], [4800, 40, 55]
    ];
    for (const [cx, cy, size] of cloudPositions) {
      const sx = cx - camera.x * 0.3;
      if (sx > -150 && sx < W + 150) {
        ctx.beginPath();
        ctx.arc(sx, cy, size * 0.4, 0, Math.PI * 2);
        ctx.arc(sx + size * 0.3, cy - size * 0.1, size * 0.35, 0, Math.PI * 2);
        ctx.arc(sx + size * 0.6, cy, size * 0.3, 0, Math.PI * 2);
        ctx.fill();
      }
    }
  }

  function drawHills() {
    ctx.fillStyle = '#5D8A3C';
    const hillData = [
      [50, 120], [400, 90], [800, 130], [1300, 100], [1800, 140],
      [2300, 95], [2800, 125], [3300, 105], [3800, 135], [4300, 90],
      [4800, 120]
    ];
    for (const [hx, hsize] of hillData) {
      const sx = hx - camera.x * 0.5;
      if (sx > -200 && sx < W + 200) {
        ctx.beginPath();
        ctx.moveTo(sx - hsize, H - 80);
        ctx.quadraticCurveTo(sx, H - 80 - hsize, sx + hsize, H - 80);
        ctx.fill();
      }
    }

    // darker hills behind
    ctx.fillStyle = '#4A7232';
    const hillData2 = [
      [200, 70], [600, 85], [1100, 65], [1600, 80], [2100, 75],
      [2600, 90], [3100, 70], [3600, 85], [4100, 65], [4600, 80]
    ];
    for (const [hx, hsize] of hillData2) {
      const sx = hx - camera.x * 0.4;
      if (sx > -200 && sx < W + 200) {
        ctx.beginPath();
        ctx.moveTo(sx - hsize, H - 60);
        ctx.quadraticCurveTo(sx, H - 60 - hsize, sx + hsize, H - 60);
        ctx.fill();
      }
    }
  }

  function drawTile(tx, ty, tile) {
    const sx = tx * TILE - camera.x;
    const sy = ty * TILE;
    if (sx < -TILE || sx > W + TILE) return;

    switch (tile) {
      case 1: // ground
        ctx.fillStyle = '#8B4513';
        ctx.fillRect(sx, sy, TILE, TILE);
        ctx.fillStyle = '#228B22';
        ctx.fillRect(sx, sy, TILE, 6);
        // dirt pattern
        ctx.fillStyle = '#A0522D';
        ctx.fillRect(sx + 4, sy + 10, 8, 6);
        ctx.fillRect(sx + 18, sy + 18, 10, 6);
        ctx.fillRect(sx + 8, sy + 24, 6, 6);
        break;
      case 2: // brick
        ctx.fillStyle = '#B85C38';
        ctx.fillRect(sx, sy, TILE, TILE);
        ctx.fillStyle = '#8B4513';
        ctx.fillRect(sx, sy, TILE, 1);
        ctx.fillRect(sx, sy + TILE/2, TILE, 1);
        ctx.fillRect(sx + TILE/2, sy, 1, TILE);
        ctx.fillRect(sx + TILE/4, sy + TILE/2, 1, TILE/2);
        ctx.fillRect(sx + TILE*3/4, sy + TILE/2, 1, TILE/2);
        break;
      case 3: // coin
        if (sx < -TILE || sx > W + TILE) return;
        const coinBob = Math.sin(Date.now() / 200 + tx) * 3;
        ctx.fillStyle = '#FFD700';
        ctx.beginPath();
        ctx.ellipse(sx + TILE/2, sy + TILE/2 + coinBob, 8, 10, 0, 0, Math.PI * 2);
        ctx.fill();
        ctx.fillStyle = '#FFA500';
        ctx.beginPath();
        ctx.ellipse(sx + TILE/2, sy + TILE/2 + coinBob, 5, 7, 0, 0, Math.PI * 2);
        ctx.fill();
        ctx.fillStyle = '#FFD700';
        ctx.font = 'bold 10px monospace';
        ctx.textAlign = 'center';
        ctx.fillText('$', sx + TILE/2, sy + TILE/2 + 4 + coinBob);
        break;
      case 4: // goal flag
        if (tx === 160 && ty === 8) {
          // flag top
          ctx.fillStyle = '#FF0000';
          ctx.beginPath();
          ctx.moveTo(sx + TILE, sy);
          ctx.lineTo(sx + TILE + 20, sy + 10);
          ctx.lineTo(sx + TILE, sy + 20);
          ctx.fill();
          // pole
          ctx.fillStyle = '#654321';
          ctx.fillRect(sx + TILE/2 - 2, sy - 40, 4, TILE + 40);
          // ball on top
          ctx.fillStyle = '#FFD700';
          ctx.beginPath();
          ctx.arc(sx + TILE/2, sy - 42, 5, 0, Math.PI * 2);
          ctx.fill();
        } else if (tx === 160 && ty > 8 && ty < 12) {
          // pole segments
          ctx.fillStyle = '#654321';
          ctx.fillRect(sx + TILE/2 - 2, sy, 4, TILE);
        }
        break;
      case 5: // pipe
        ctx.fillStyle = '#228B22';
        ctx.fillRect(sx, sy, TILE, TILE);
        ctx.fillStyle = '#006400';
        ctx.fillRect(sx, sy, 4, TILE);
        ctx.fillRect(sx + TILE - 4, sy, 4, TILE);
        ctx.fillStyle = '#32CD32';
        ctx.fillRect(sx + 4, sy, TILE - 8, 4);
        // pipe top
        if (ty === 12 || (getTile(tx, ty-1) !== 5)) {
          ctx.fillStyle = '#228B22';
          ctx.fillRect(sx - 4, sy, TILE + 8, 8);
          ctx.fillStyle = '#32CD32';
          ctx.fillRect(sx - 4, sy, TILE + 8, 3);
          ctx.fillStyle = '#006400';
          ctx.fillRect(sx - 4, sy + TILE - 3, TILE + 8, 3);
        }
        break;
      case 6: // question block
        const qBob = Math.sin(Date.now() / 300 + tx * 2) * 2;
        ctx.fillStyle = '#FFB347';
        ctx.fillRect(sx, sy + qBob, TILE, TILE);
        ctx.fillStyle = '#FF8C00';
        ctx.fillRect(sx, sy + qBob, TILE, 2);
        ctx.fillRect(sx, sy + TILE - 2 + qBob, TILE, 2);
        ctx.fillRect(sx, sy + qBob, 2, TILE);
        ctx.fillRect(sx + TILE - 2, sy + qBob, 2, TILE);
        ctx.fillStyle = '#FFF';
        ctx.font = 'bold 18px monospace';
        ctx.textAlign = 'center';
        ctx.fillText('?', sx + TILE/2, sy + TILE/2 + 6 + qBob);
        break;
    }
  }

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

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

    // Body
    ctx.fillStyle = '#E94560';
    ctx.fillRect(-10, -12, 20, 22);

    // Head
    ctx.fillStyle = '#FDBCB4';
    ctx.fillRect(-8, -18, 16, 10);

    // Hat
    ctx.fillStyle = '#E94560';
    ctx.fillRect(-9, -20, 18, 5);
    ctx.fillRect(-7, -22, 14, 3);

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

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

    // Legs (animated)
    ctx.fillStyle = '#4169E1';
    const legOffset = player.onGround ? Math.sin(player.animFrame * Math.PI / 2) * 4 : 2;
    ctx.fillRect(-8, 10, 7, 6 + legOffset);
    ctx.fillRect(2, 10, 7, 6 - legOffset);

    // Shoes
    ctx.fillStyle = '#8B4513';
    ctx.fillRect(-9, 15 + legOffset, 8, 4);
    ctx.fillRect(2, 15 - legOffset, 8, 4);

    ctx.restore();
  }

  function drawHUD() {
    // Score
    ctx.fillStyle = 'rgba(0,0,0,0.5)';
    ctx.fillRect(10, 10, 200, 35);
    ctx.fillStyle = '#FFD700';
    ctx.font = 'bold 18px monospace';
    ctx.textAlign = 'left';
    ctx.fillText('🪙 COINS: ' + player.score, 20, 33);

    // Lives
    ctx.fillStyle = 'rgba(0,0,0,0.5)';
    ctx.fillRect(W - 150, 10, 140, 35);
    ctx.fillStyle = '#FF6B6B';
    ctx.font = 'bold 18px monospace';
    ctx.textAlign = 'right';
    ctx.fillText('❤ LIVES: ' + player.lives, W - 20, 33);

    // Controls hint
    ctx.fillStyle = 'rgba(255,255,255,0.4)';
    ctx.font = '12px monospace';
    ctx.textAlign = 'center';
    ctx.fillText('Arrow Keys / WASD to move  •  Space / Up to jump', W/2, H - 10);
  }

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

  function drawFloatingTexts() {
    for (const ft of floatingTexts) {
      ctx.globalAlpha = ft.life / 40;
      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 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: ' + player.score, W/2, H/2 + 20);
    ctx.fillStyle = '#AAA';
    ctx.font = '16px monospace';
    ctx.fillText('Press R to restart', W/2, H/2 + 60);
  }

  function drawGameOver() {
    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 = '#AAA';
    ctx.font = '18px monospace';
    ctx.fillText('Press R to restart', W/2, H/2 + 30);
  }

  function draw() {
    drawSky();
    drawClouds();
    drawHills();

    // Draw tiles
    const startTx = Math.max(0, Math.floor(camera.x / TILE) - 1);
    const endTx = Math.min(LEVEL_W, Math.ceil((camera.x + W) / TILE) + 1);
    for (let ty = 0; ty < LEVEL_H; ty++) {
      for (let tx = startTx; tx < endTx; tx++) {
        const tile = levelData[ty][tx];
        if (tile !== 0) drawTile(tx, ty, tile);
      }
    }

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

    if (player.win) drawWinScreen();
    if (player.dead) drawGameOver();
  }

  // ── Game loop ──────────────────────────────────────────
  function gameLoop() {
    if (keys['KeyR'] && (player.dead || player.win)) {
      // restart
      player.x = 3 * TILE;
      player.y = 10 * TILE;
      player.vx = 0;
      player.vy = 0;
      player.score = 0;
      player.lives = 3;
      player.dead = false;
      player.win = false;
      player.onGround = false;
      player.coyoteTime = 0;
      player.jumpBuffer = 0;
      camera.x = 0;
      particles.length = 0;
      floatingTexts.length = 0;
      // reset coins
      for (const c of coinPositions) c.collected = false;
      for (let y = 0; y < LEVEL_H; y++)
        for (let x = 0; x < LEVEL_W; x++) {
          // we need to re-read the original level — simplest: reload from coinPositions
        }
      // Rebuild level from scratch for restart
      for (let y = 0; y < LEVEL_H; y++)
        for (let x = 0; x < LEVEL_W; x++)
          levelData[y][x] = 0;
      for (let x = 0; x < LEVEL_W; x++) {
        if ((x >= 25 && x <= 27) || (x >= 55 && x <= 58) || (x >= 90 && x <= 93) || (x >= 130 && x <= 133)) continue;
        levelData[12][x] = 1;
        levelData[13][x] = 1;
      }
      const platRows = [
        [9, 8, 13], [9, 20, 26], [9, 35, 42], [9, 48, 54], [9, 65, 72],
        [9, 78, 85], [9, 98, 105], [9, 110, 116], [9, 120, 128], [9, 140, 148],
        [6, 15, 18], [6, 40, 44], [6, 70, 74], [6, 100, 104], [6, 135, 139]
      ];
      for (const [, s, e] of platRows)
        for (let x = s; x <= e; x++) levelData[9][x] = 2;
      for (const [, s, e] of [[6,15,18],[6,40,44],[6,70,74],[6,100,104],[6,135,139]])
        for (let x = s; x <= e; x++) levelData[6][x] = 2;

      // coins
      function rc(row, startX, count) {
        for (let i = 0; i < count; i++) {
          const x = startX + i * 2;
          if (x < LEVEL_W) { levelData[row][x] = 3; coinPositions.push({x, y: row, collected: false}); }
        }
      }
      rc(8,9,3); rc(8,21,4); rc(8,36,3); rc(8,49,5); rc(8,66,4); rc(8,79,5); rc(8,99,3); rc(8,111,4); rc(8,121,5); rc(8,141,3);
      rc(5,16,2); rc(5,41,3); rc(5,71,2); rc(5,101,3); rc(5,136,2);
      rc(11,5,3); rc(11,30,4); rc(11,60,3); rc(11,80,5); rc(11,100,3); rc(11,120,4); rc(11,150,3);

      // pipes
      function rp(x, h) {
        for (let dy = 0; dy < h; dy++) {
          const row = 12 - h + dy;
          if (row >= 0 && row < LEVEL_H) { levelData[row][x] = 5; levelData[row][x+1] = 5; }
        }
      }
      rp(18,2); rp(45,3); rp(75,2); rp(108,3); rp(145,2);

      levelData[8][11] = 6; levelData[8][50] = 6; levelData[8][81] = 6; levelData[8][113] = 6;
      levelData[4][160] = 4; levelData[5][160] = 4; levelData[6][160] = 4; levelData[7][160] = 4;
      levelData[8][160] = 4; levelData[9][160] = 4; levelData[10][160] = 4; levelData[11][160] = 4;
      levelData[12][160] = 1; levelData[13][160] = 1;
    }

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

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

rep 2 MANUAL fail/5

pi 64.2s · verify 4.2s · 43560 tok · responded: ornith-1.0-35b · transcript

smoke 3/3

screenshot

verify output
CHECK PASS: canvas #game exists and is sized
CHECK PASS: no uncaught JS errors on load
CHECK PASS: keyboard input does not throw
score: 3/3 (100%)
produced code
game.js
// ============================================================
// Super Platformer — a small Mario-style side-scroller
// ============================================================
(() => {
  "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.6;
  const MAX_SPEED = 5.5;
  const JUMP_FORCE = -11.5;
  const COYOTE_FRAMES = 6;
  const JUMP_BUFFER_FRAMES = 6;
  const TILE = 40;

  // ---- level data (tile map) ----
  // 0=air, 1=ground, 2=brick, 3=coin, 4=flag pole, 5=goal block
  const LEVEL_W = 120;
  const LEVEL_H = 14;

  const map = [];
  for (let y = 0; y < LEVEL_H; y++) {
    map[y] = [];
    for (let x = 0; x < LEVEL_W; x++) {
      map[y][x] = 0;
    }
  }

  // ground (rows 12-13)
  for (let x = 0; x < LEVEL_W; x++) {
    // gaps
    if ((x >= 28 && x <= 30) || (x >= 55 && x <= 57) || (x >= 85 && x <= 87)) continue;
    map[12][x] = 1;
    map[13][x] = 1;
  }

  // platforms
  function placePlatform(y, xStart, xEnd, type) {
    for (let x = xStart; x <= xEnd; x++) {
      if (y >= 0 && y < LEVEL_H && x >= 0 && x < LEVEL_W) {
        map[y][x] = type;
      }
    }
  }

  // floating platforms
  placePlatform(9, 8, 13, 2);
  placePlatform(7, 18, 24, 2);
  placePlatform(5, 32, 38, 2);
  placePlatform(9, 42, 48, 2);
  placePlatform(6, 50, 56, 2);
  placePlatform(8, 60, 66, 2);
  placePlatform(5, 70, 76, 2);
  placePlatform(7, 80, 86, 2);
  placePlatform(4, 92, 98, 2);
  placePlatform(6, 102, 108, 2);
  placePlatform(3, 110, 115, 2);

  // stairs near end
  for (let i = 0; i < 5; i++) {
    placePlatform(11 - i, 110 + i, 110 + i, 2);
  }

  // coins
  function placeCoin(y, x) {
    if (y >= 0 && y < LEVEL_H && x >= 0 && x < LEVEL_W && map[y][x] === 0) {
      map[y][x] = 3;
    }
  }

  // coin rows on platforms
  for (let x = 9; x <= 12; x++) placeCoin(8, x);
  for (let x = 19; x <= 23; x++) placeCoin(6, x);
  for (let x = 33; x <= 37; x++) placeCoin(4, x);
  for (let x = 43; x <= 47; x++) placeCoin(8, x);
  for (let x = 51; x <= 55; x++) placeCoin(5, x);
  for (let x = 61; x <= 65; x++) placeCoin(7, x);
  for (let x = 71; x <= 75; x++) placeCoin(4, x);

  // scattered coins in the air
  placeCoin(10, 15);
  placeCoin(10, 16);
  placeCoin(10, 17);
  placeCoin(3, 50);
  placeCoin(3, 51);
  placeCoin(3, 52);
  placeCoin(10, 70);
  placeCoin(10, 71);
  placeCoin(2, 95);
  placeCoin(2, 96);
  placeCoin(2, 97);
  placeCoin(2, 98);

  // flag pole
  map[3][117] = 4;
  map[4][117] = 4;
  map[5][117] = 4;
  map[6][117] = 4;
  map[7][117] = 4;
  map[8][117] = 4;
  map[9][117] = 4;
  map[10][117] = 4;
  map[11][117] = 4;
  // goal block at base
  map[11][118] = 5;
  map[12][118] = 1;
  map[13][118] = 1;

  // ---- game state ----
  let player, coins, totalCoins, score, won, gameOver, camX, gameTick;
  let particles = [];
  let starField = [];
  let clouds = [];

  function init() {
    player = {
      x: 3 * TILE,
      y: 10 * TILE,
      w: 28,
      h: 36,
      vx: 0,
      vy: 0,
      onGround: false,
      facing: 1,
      frame: 0,
      frameTimer: 0,
      coyoteTimer: 0,
      jumpBufferTimer: 0,
      dead: false,
      deathTimer: 0,
    };
    coins = [];
    totalCoins = 0;
    score = 0;
    won = false;
    gameOver = false;
    camX = 0;
    gameTick = 0;
    particles = [];

    // collect coin positions from map
    for (let y = 0; y < LEVEL_H; y++) {
      for (let x = 0; x < LEVEL_W; x++) {
        if (map[y][x] === 3) {
          coins.push({ x: x * TILE + 10, y: y * TILE + 10, collected: false, bobOffset: Math.random() * Math.PI * 2 });
          totalCoins++;
        }
      }
    }

    // clouds
    clouds = [];
    for (let i = 0; i < 20; i++) {
      clouds.push({
        x: Math.random() * LEVEL_W * TILE,
        y: 30 + Math.random() * 120,
        w: 60 + Math.random() * 80,
        speed: 0.2 + Math.random() * 0.3,
      });
    }

    // stars
    starField = [];
    for (let i = 0; i < 50; i++) {
      starField.push({
        x: Math.random() * W,
        y: Math.random() * H * 0.6,
        size: 1 + Math.random() * 2,
        twinkle: Math.random() * Math.PI * 2,
      });
    }
  }

  // ---- input ----
  const keys = {};
  window.addEventListener("keydown", (e) => {
    keys[e.code] = true;
    if (["Space", "ArrowUp", "ArrowDown", "ArrowLeft", "ArrowRight"].includes(e.code)) {
      e.preventDefault();
    }
    if (won || gameOver) {
      if (e.code === "KeyR" || e.code === "Space") {
        init();
      }
    }
  });
  window.addEventListener("keyup", (e) => {
    keys[e.code] = false;
  });

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

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

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

  // ---- update ----
  function update() {
    if (won || gameOver) return;
    gameTick++;

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

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

    // jump buffer
    if (isJump()) {
      player.jumpBufferTimer = JUMP_BUFFER_FRAMES;
    }
    if (player.jumpBufferTimer > 0) player.jumpBufferTimer--;

    // coyote time
    if (player.onGround) {
      player.coyoteTimer = COYOTE_FRAMES;
    }
    if (player.coyoteTimer > 0) player.coyoteTimer--;

    // jump
    if (player.jumpBufferTimer > 0 && player.coyoteTimer > 0) {
      player.vy = JUMP_FORCE;
      player.coyoteTimer = 0;
      player.jumpBufferTimer = 0;
      player.onGround = false;
      spawnJumpParticles();
    }

    // variable jump height
    if (!isJump() && player.vy < -3) {
      player.vy *= 0.7;
    }

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

    // horizontal movement + collision
    player.x += player.vx;
    resolveCollisionX();

    // vertical movement + collision
    player.y += player.vy;
    player.onGround = false;
    resolveCollisionY();

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

    // coin collection
    for (const c of coins) {
      if (c.collected) continue;
      const dx = (player.x + player.w / 2) - (c.x + 10);
      const dy = (player.y + player.h / 2) - (c.y + 10);
      if (Math.sqrt(dx * dx + dy * dy) < 24) {
        c.collected = true;
        score++;
        spawnCoinParticles(c.x + 10, c.y + 10);
      }
    }

    // flag / goal check
    const flagTile = getTile(player.x + player.w / 2, player.y + player.h / 2);
    if (flagTile === 4 || flagTile === 5) {
      won = true;
      spawnWinParticles();
    }

    // fall death
    if (player.y > LEVEL_H * TILE + 50) {
      player.dead = true;
      player.deathTimer = 60;
    }

    if (player.dead) {
      player.deathTimer--;
      if (player.deathTimer <= 0) {
        init();
      }
    }

    // camera
    const targetCam = player.x - W * 0.35;
    camX += (targetCam - camX) * 0.1;
    if (camX < 0) camX = 0;
    if (camX > LEVEL_W * TILE - W) camX = LEVEL_W * TILE - W;

    // particles
    updateParticles();
  }

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

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

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

    for (let ty = top; ty <= bottom; ty++) {
      for (let tx = left; tx <= right; tx++) {
        const tile = getTile(tx * TILE, ty * TILE);
        if (isSolid(tile)) {
          if (p.vy > 0) {
            p.y = ty * TILE - p.h;
            p.vy = 0;
            p.onGround = true;
          } else if (p.vy < 0) {
            p.y = (ty + 1) * TILE;
            p.vy = 0;
          }
        }
        // flag pole collision
        if (tile === 4 || tile === 5) {
          // just pass through — goal check handles it
        }
      }
    }
  }

  // ---- particles ----
  function spawnJumpParticles() {
    for (let i = 0; i < 5; i++) {
      particles.push({
        x: player.x + player.w / 2 + (Math.random() - 0.5) * 20,
        y: player.y + player.h,
        vx: (Math.random() - 0.5) * 3,
        vy: Math.random() * 2,
        life: 20 + Math.random() * 10,
        color: "#c8a87a",
        size: 2 + Math.random() * 3,
      });
    }
  }

  function spawnCoinParticles(x, y) {
    for (let i = 0; i < 8; i++) {
      const angle = (Math.PI * 2 / 8) * i;
      particles.push({
        x, y,
        vx: Math.cos(angle) * 3,
        vy: Math.sin(angle) * 3,
        life: 25,
        color: "#ffd700",
        size: 3 + Math.random() * 2,
      });
    }
  }

  function spawnWinParticles() {
    for (let i = 0; i < 30; i++) {
      particles.push({
        x: player.x + player.w / 2,
        y: player.y + player.h / 2,
        vx: (Math.random() - 0.5) * 8,
        vy: (Math.random() - 0.5) * 8,
        life: 40 + Math.random() * 20,
        color: ["#ff0", "#f0f", "#0ff", "#f00", "#0f0"][Math.floor(Math.random() * 5)],
        size: 3 + Math.random() * 4,
      });
    }
  }

  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 drawSky() {
    // gradient sky
    const grad = ctx.createLinearGradient(0, 0, 0, H);
    grad.addColorStop(0, "#4a90d9");
    grad.addColorStop(0.6, "#87ceeb");
    grad.addColorStop(1, "#b8e4f0");
    ctx.fillStyle = grad;
    ctx.fillRect(0, 0, W, H);
  }

  function drawClouds() {
    ctx.fillStyle = "rgba(255,255,255,0.8)";
    for (const c of clouds) {
      const sx = c.x - camX * 0.3;
      const sy = c.y;
      if (sx + c.w < -100 || sx > W + 100) continue;
      // simple cloud shape
      ctx.beginPath();
      ctx.arc(sx, sy, 20, 0, Math.PI * 2);
      ctx.arc(sx + 25, sy - 10, 25, 0, Math.PI * 2);
      ctx.arc(sx + 50, sy, 20, 0, Math.PI * 2);
      ctx.arc(sx + 25, sy + 5, 22, 0, Math.PI * 2);
      ctx.fill();
    }
  }

  function drawHills() {
    // background hills (parallax)
    ctx.fillStyle = "#5a9e4e";
    for (let i = 0; i < 15; i++) {
      const hx = i * 200 - (camX * 0.4) % 200;
      const hy = H - 80;
      ctx.beginPath();
      ctx.arc(hx, hy, 80 + Math.sin(i) * 30, Math.PI, 0);
      ctx.fill();
    }
    ctx.fillStyle = "#6abf5e";
    for (let i = 0; i < 15; i++) {
      const hx = i * 200 + 100 - (camX * 0.4) % 200;
      const hy = H - 60;
      ctx.beginPath();
      ctx.arc(hx, hy, 60 + Math.cos(i) * 20, Math.PI, 0);
      ctx.fill();
    }
  }

  function drawTiles() {
    const startCol = Math.max(0, Math.floor(camX / TILE) - 1);
    const endCol = Math.min(LEVEL_W, startCol + Math.ceil(W / TILE) + 3);

    for (let y = 0; y < LEVEL_H; y++) {
      for (let x = startCol; x < endCol; x++) {
        const tile = map[y][x];
        if (tile === 0) continue;
        const sx = x * TILE - camX;
        const sy = y * TILE;

        if (tile === 1) {
          // ground
          ctx.fillStyle = "#8B4513";
          ctx.fillRect(sx, sy, TILE, TILE);
          ctx.fillStyle = "#6B3410";
          ctx.fillRect(sx, sy, TILE, 4);
          // grass on top
          if (y > 0 && map[y - 1][x] === 0) {
            ctx.fillStyle = "#4CAF50";
            ctx.fillRect(sx, sy, TILE, 6);
            ctx.fillStyle = "#388E3C";
            ctx.fillRect(sx, sy, TILE, 3);
          }
          // texture
          ctx.fillStyle = "rgba(0,0,0,0.1)";
          ctx.fillRect(sx + 5, sy + 10, 12, 8);
          ctx.fillRect(sx + 22, sy + 20, 10, 8);
        } else if (tile === 2) {
          // brick
          ctx.fillStyle = "#c0392b";
          ctx.fillRect(sx, sy, TILE, TILE);
          ctx.fillStyle = "#e74c3c";
          ctx.fillRect(sx + 1, sy + 1, TILE - 2, TILE / 2 - 2);
          ctx.fillStyle = "#a93226";
          ctx.fillRect(sx + 1, sy + TILE / 2, TILE - 2, TILE / 2 - 1);
          // mortar lines
          ctx.strokeStyle = "#7b241c";
          ctx.lineWidth = 1;
          ctx.strokeRect(sx, sy, TILE, TILE);
          ctx.beginPath();
          ctx.moveTo(sx, sy + TILE / 2);
          ctx.lineTo(sx + TILE, sy + TILE / 2);
          ctx.stroke();
          ctx.beginPath();
          ctx.moveTo(sx + TILE / 2, sy);
          ctx.lineTo(sx + TILE / 2, sy + TILE / 2);
          ctx.stroke();
        } else if (tile === 3) {
          // coin (handled separately)
        } else if (tile === 4) {
          // flag pole
          ctx.fillStyle = "#888";
          ctx.fillRect(sx + 18, sy, 4, TILE);
          if (y === 3) {
            // flag
            ctx.fillStyle = "#e74c3c";
            ctx.beginPath();
            ctx.moveTo(sx + 22, sy + 2);
            ctx.lineTo(sx + 52, sy + 14);
            ctx.lineTo(sx + 22, sy + 26);
            ctx.fill();
          }
        } else if (tile === 5) {
          // goal block
          ctx.fillStyle = "#f1c40f";
          ctx.fillRect(sx, sy, TILE, TILE);
          ctx.fillStyle = "#f39c12";
          ctx.fillRect(sx + 3, sy + 3, TILE - 6, TILE - 6);
          ctx.fillStyle = "#fff";
          ctx.font = "bold 20px monospace";
          ctx.textAlign = "center";
          ctx.fillText("★", sx + TILE / 2, sy + TILE / 2 + 7);
        }
      }
    }
  }

  function drawCoins() {
    for (const c of coins) {
      if (c.collected) continue;
      const sx = c.x - camX;
      const sy = c.y + Math.sin(gameTick * 0.06 + c.bobOffset) * 4;
      if (sx < -20 || sx > W + 20) continue;

      // glow
      ctx.fillStyle = "rgba(255,215,0,0.3)";
      ctx.beginPath();
      ctx.arc(sx + 10, sy + 10, 14, 0, Math.PI * 2);
      ctx.fill();

      // coin body
      const stretch = Math.abs(Math.sin(gameTick * 0.05 + c.bobOffset));
      ctx.fillStyle = "#ffd700";
      ctx.beginPath();
      ctx.ellipse(sx + 10, sy + 10, 8 * stretch + 2, 8, 0, 0, Math.PI * 2);
      ctx.fill();
      ctx.fillStyle = "#ffed4a";
      ctx.beginPath();
      ctx.ellipse(sx + 10, sy + 10, 5 * stretch + 1, 5, 0, 0, Math.PI * 2);
      ctx.fill();
    }
  }

  function drawPlayer() {
    if (player.dead) return;
    const p = player;
    const sx = p.x - camX;
    const sy = p.y;

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

    // body
    ctx.fillStyle = "#e74c3c"; // red shirt
    ctx.fillRect(-10, -8, 20, 18);

    // overalls
    ctx.fillStyle = "#2980b9";
    ctx.fillRect(-10, 4, 20, 12);
    // straps
    ctx.fillStyle = "#2980b9";
    ctx.fillRect(-8, -2, 4, 8);
    ctx.fillRect(4, -2, 4, 8);

    // head
    ctx.fillStyle = "#fdebd0";
    ctx.fillRect(-8, -18, 16, 12);

    // hat
    ctx.fillStyle = "#e74c3c";
    ctx.fillRect(-10, -22, 22, 6);
    ctx.fillRect(-6, -26, 18, 6);

    // eyes
    ctx.fillStyle = "#000";
    ctx.fillRect(1, -14, 3, 3);

    // mustache
    ctx.fillStyle = "#5d4037";
    ctx.fillRect(-2, -9, 12, 3);

    // shoes
    ctx.fillStyle = "#5d4037";
    if (p.onGround && Math.abs(p.vx) > 0.5) {
      // walking animation
      const legOffset = Math.sin(p.frame * Math.PI / 2) * 3;
      ctx.fillRect(-10, 14, 10, 4 + legOffset);
      ctx.fillRect(0, 14, 10, 4 - legOffset);
    } else {
      ctx.fillRect(-10, 14, 10, 4);
      ctx.fillRect(0, 14, 10, 4);
    }

    ctx.restore();
  }

  function drawParticles() {
    for (const p of particles) {
      const alpha = p.life / 40;
      ctx.globalAlpha = alpha;
      ctx.fillStyle = p.color;
      ctx.fillRect(p.x - camX - 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, 10, 180, 36);
    ctx.strokeStyle = "#ffd700";
    ctx.lineWidth = 2;
    ctx.strokeRect(10, 10, 180, 36);

    // coin icon
    ctx.fillStyle = "#ffd700";
    ctx.beginPath();
    ctx.arc(30, 28, 10, 0, Math.PI * 2);
    ctx.fill();
    ctx.fillStyle = "#ffed4a";
    ctx.beginPath();
    ctx.arc(30, 28, 6, 0, Math.PI * 2);
    ctx.fill();

    // score text
    ctx.fillStyle = "#fff";
    ctx.font = "bold 20px monospace";
    ctx.textAlign = "left";
    ctx.fillText(`${score} / ${totalCoins}`, 48, 33);

    // progress bar
    const progress = totalCoins > 0 ? score / totalCoins : 0;
    ctx.fillStyle = "rgba(0,0,0,0.3)";
    ctx.fillRect(W - 170, 14, 150, 12);
    ctx.fillStyle = progress >= 1 ? "#2ecc71" : "#f39c12";
    ctx.fillRect(W - 170, 14, 150 * progress, 12);
    ctx.strokeStyle = "#fff";
    ctx.lineWidth = 1;
    ctx.strokeRect(W - 170, 14, 150, 12);
  }

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

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

    ctx.fillStyle = "#fff";
    ctx.font = "24px monospace";
    ctx.fillText(`Coins: ${score} / ${totalCoins}`, W / 2, H / 2 + 10);

    ctx.font = "18px monospace";
    ctx.fillStyle = "#aaa";
    ctx.fillText("Press Space or R to play again", W / 2, H / 2 + 50);
  }

  function drawGameOver() {
    ctx.fillStyle = "rgba(0,0,0,0.5)";
    ctx.fillRect(0, 0, W, H);
    ctx.fillStyle = "#e74c3c";
    ctx.font = "bold 40px monospace";
    ctx.textAlign = "center";
    ctx.fillText("Game Over", W / 2, H / 2);
    ctx.fillStyle = "#fff";
    ctx.font = "18px monospace";
    ctx.fillText("Press Space or R to retry", W / 2, H / 2 + 40);
  }

  function draw() {
    drawSky();
    drawClouds();
    drawHills();
    drawTiles();
    drawCoins();
    drawPlayer();
    drawParticles();
    drawHUD();

    if (won) drawWinScreen();
    if (gameOver) drawGameOver();
  }

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

  init();
  loop();
})();
index.html
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>Super Platformer</title>
  <style>
    * { margin: 0; padding: 0; box-sizing: border-box; }
    body {
      background: #1a1a2e;
      display: flex;
      flex-direction: column;
      align-items: center;
      justify-content: center;
      height: 100vh;
      font-family: monospace;
      overflow: hidden;
    }
    canvas {
      border: 3px solid #e94560;
      border-radius: 4px;
      display: block;
      background: #87ceeb;
    }
    #info {
      color: #eee;
      margin-top: 12px;
      font-size: 14px;
      text-align: center;
    }
  </style>
</head>
<body>
  <canvas id="game" width="800" height="450"></canvas>
  <div id="info">Arrow Keys / WASD to move &bull; Space / Up to jump &bull; Collect all coins!</div>
  <script src="game.js"></script>
</body>
</html>

rep 3 MANUAL pass/5

pi 62.7s · verify 4.2s · 40618 tok · responded: ornith-1.0-35b · transcript

smoke 3/3

screenshot

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

const canvas = document.getElementById("game");
const ctx = canvas.getContext("2d");

canvas.width = 800;
canvas.height = 450;

// --- 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 constants ---
const GRAVITY = 0.55;
const FRICTION = 0.82;
const MOVE_SPEED = 1.0;
const MAX_SPEED = 5.5;
const JUMP_FORCE = -12;
const TERMINAL_VELOCITY = 14;
const WORLD_WIDTH = 4800;
const TILE = 48;

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

// --- Player ---
let player = {
  x: 100,
  y: 200,
  w: 32,
  h: 40,
  vx: 0,
  vy: 0,
  onGround: false,
  facing: 1,
  walkFrame: 0,
  walkTimer: 0,
  dead: false,
  won: false,
  wins: 0
};

// --- Level data ---
// Platforms: { x, y, w, h }
const platforms = [];
// Coins: { x, y, collected }
const coins = [];
// Pits / hazards
const pits = [];
// Flag / goal
const flag = { x: 4500, y: 0, reached: false };

function buildLevel() {
  platforms.length = 0;
  coins.length = 0;
  pits.length = 0;

  // Ground segments
  const ground = [
    [0, 800], [900, 1800], [2000, 3200], [3400, WORLD_WIDTH]
  ];
  for (const [gx, gx2] of ground) {
    platforms.push({ x: gx, y: 400, w: gx2 - gx, h: 50 });
    pits.push({ x: gx, y: 450, w: gx2 - gx, h: 100 }); // visual pit below
  }

  // Raised platforms
  const platData = [
    [300,  310, 144],
    [550,  250, 96],
    [800,  310, 144],
    [1050, 220, 120],
    [1300, 300, 96],
    [1500, 240, 144],
    [1800, 200, 96],
    [2100, 310, 144],
    [2400, 250, 120],
    [2700, 190, 96],
    [3000, 310, 144],
    [3300, 250, 120],
    [3600, 200, 96],
    [3900, 280, 144],
    [4200, 220, 120],
    [4500, 160, 144],
  ];
  for (const [px, py, pw] of platData) {
    platforms.push({ x: px, y: py, w: pw, h: 20 });
  }

  // Floating blocks (decorative / extra platforms)
  const blocks = [
    [600, 170, 48], [1100, 140, 48], [1900, 130, 48],
    [2500, 150, 48], [3100, 160, 48], [3700, 120, 48],
    [4100, 140, 48],
  ];
  for (const [bx, by, bw] of blocks) {
    platforms.push({ x: bx, y: by, w: bw, h: 20 });
  }

  // Coins
  const coinPositions = [
    // Ground coins
    [200, 360], [250, 360], [300, 360],
    [450, 360], [500, 360],
    // Platform coins
    [350, 270], [400, 270], [450, 270],
    [580, 210], [620, 210],
    [850, 270], [900, 270], [950, 270],
    [1080, 180], [1120, 180],
    [1330, 260],
    [1550, 200], [1600, 200], [1650, 200],
    [1830, 160],
    [2150, 270], [2200, 270], [2250, 270],
    [2440, 210], [2480, 210],
    [2730, 150],
    [3050, 270], [3100, 270], [3150, 270],
    [3340, 210], [3380, 210],
    [3630, 160],
    [3950, 240], [4000, 240], [4050, 240],
    [4240, 180], [4280, 180],
    // High coins
    [624, 130], [1124, 100], [1924, 90],
    [2524, 110], [3124, 120], [3724, 80], [4124, 100],
  ];
  for (const [cx, cy] of coinPositions) {
    coins.push({ x: cx, y: cy, collected: false, bobOffset: Math.random() * Math.PI * 2 });
  }

  // Flag
  flag.x = 4580;
  flag.y = 160;
  flag.reached = false;
}

function resetPlayer() {
  player.x = 100;
  player.y = 200;
  player.vx = 0;
  player.vy = 0;
  player.onGround = false;
  player.dead = false;
  player.won = false;
  player.wins = 0;
  buildLevel();
}

let score = 0;
let totalCoins = 0;
let gameTime = 0;
let deathTimer = 0;
let particles = [];
let screenShake = 0;

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

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

function update() {
  if (player.won) {
    gameTime++;
    return;
  }
  if (player.dead) {
    deathTimer--;
    if (deathTimer <= 0) resetPlayer();
    return;
  }

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

  if (moveX !== 0) {
    player.vx += moveX * MOVE_SPEED;
    player.facing = moveX;
  } else {
    player.vx *= FRICTION;
    if (Math.abs(player.vx) < 0.1) player.vx = 0;
  }

  // Jump
  if ((keys["Space"] || keys["ArrowUp"] || keys["KeyW"]) && player.onGround) {
    player.vy = JUMP_FORCE;
    player.onGround = false;
    spawnParticles(player.x + player.w / 2, player.y + player.h, "#fff", 5);
  }

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

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

  // Move X
  player.x += player.vx;
  if (player.x < 0) player.x = 0;
  if (player.x + player.w > WORLD_WIDTH) player.x = WORLD_WIDTH - player.w;

  // Collision X
  for (const p of platforms) {
    if (rectCollide(player, p)) {
      if (player.vx > 0) {
        player.x = p.x - player.w;
      } else if (player.vx < 0) {
        player.x = p.x + p.w;
      }
      player.vx = 0;
    }
  }

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

  // Collision Y
  for (const p of platforms) {
    if (rectCollide(player, p)) {
      if (player.vy > 0) {
        // Landing on top
        player.y = p.y - player.h;
        player.vy = 0;
        player.onGround = true;
      } else if (player.vy < 0) {
        // Hitting head
        player.y = p.y + p.h;
        player.vy = 0;
      }
    }
  }

  // Fall into pit
  if (player.y > 500) {
    player.dead = true;
    deathTimer = 45;
    screenShake = 10;
    spawnParticles(player.x + player.w / 2, 450, "#f44", 15);
  }

  // Coins
  for (const c of coins) {
    if (!c.collected) {
      const cx = c.x, cy = c.y;
      if (player.x + player.w > cx - 10 && player.x < cx + 10 &&
          player.y + player.h > cy - 10 && player.y < cy + 10) {
        c.collected = true;
        score += 10;
        spawnParticles(cx, cy, "#FFD700", 8);
      }
    }
  }

  // Flag / goal
  if (!flag.reached && !player.won) {
    if (player.x + player.w > flag.x && player.x < flag.x + 30) {
      player.won = true;
      player.wins++;
      score += 500;
      spawnParticles(flag.x, flag.y, "#0f0", 30);
      spawnParticles(flag.x, flag.y, "#FFD700", 20);
    }
  }

  // Walk animation
  if (player.onGround && Math.abs(player.vx) > 0.5) {
    player.walkTimer++;
    if (player.walkTimer > 6) {
      player.walkTimer = 0;
      player.walkFrame = (player.walkFrame + 1) % 4;
    }
  } else {
    player.walkFrame = 0;
    player.walkTimer = 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);
  }

  // Screen shake decay
  if (screenShake > 0) screenShake *= 0.85;
  if (screenShake < 0.5) screenShake = 0;

  // Camera
  const targetX = player.x - canvas.width / 2 + player.w / 2;
  camera.x += (targetX - camera.x) * 0.1;
  if (camera.x < 0) camera.x = 0;
  if (camera.x > WORLD_WIDTH - canvas.width) camera.x = WORLD_WIDTH - canvas.width;
}

// --- Drawing ---
function drawSky() {
  const grad = ctx.createLinearGradient(0, 0, 0, canvas.height);
  grad.addColorStop(0, "#5C94FC");
  grad.addColorStop(0.7, "#87CEEB");
  grad.addColorStop(1, "#B0E0E6");
  ctx.fillStyle = grad;
  ctx.fillRect(0, 0, canvas.width, canvas.height);

  // Clouds
  ctx.fillStyle = "rgba(255,255,255,0.8)";
  const cloudData = [
    [200, 60, 80], [700, 40, 100], [1200, 70, 70], [1800, 50, 90],
    [2500, 65, 80], [3200, 45, 110], [3800, 55, 75], [4400, 35, 90],
  ];
  for (const [cx, cy, cw] of cloudData) {
    const sx = cx - camera.x * 0.3;
    if (sx > -cw && sx < canvas.width + cw) {
      drawCloud(sx, cy, cw);
    }
  }

  // Hills (parallax)
  ctx.fillStyle = "#4CAF50";
  const hillData = [
    [100, 120], [600, 90], [1100, 130], [1700, 100], [2300, 120],
    [2900, 95], [3500, 110], [4100, 100],
  ];
  for (const [hx, hh] of hillData) {
    const sx = hx - camera.x * 0.4;
    drawHill(sx, 420, hh);
  }

  // Bushes
  ctx.fillStyle = "#2E7D32";
  const bushData = [
    [300, 30], [900, 25], [1500, 35], [2200, 28], [3000, 32], [3600, 26], [4200, 30],
  ];
  for (const [bx, bw] of bushData) {
    const sx = bx - camera.x * 0.6;
    drawBush(sx, 410, bw);
  }
}

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

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

function drawBush(x, baseY, w) {
  const r = w * 0.4;
  ctx.beginPath();
  ctx.arc(x, baseY - r * 0.5, r, Math.PI, 0);
  ctx.arc(x + w * 0.5, baseY - r * 0.3, r * 0.8, Math.PI, 0);
  ctx.fill();
}

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

  if (sx + p.w < -50 || sx > canvas.width + 50) return;

  if (p.h > 30) {
    // Ground
    // Top grass
    ctx.fillStyle = "#4CAF50";
    ctx.fillRect(sx, sy, p.w, 8);
    // Dirt
    ctx.fillStyle = "#8B4513";
    ctx.fillRect(sx, sy + 8, p.w, p.h - 8);
    // Dirt detail
    ctx.fillStyle = "#A0522D";
    for (let i = 0; i < p.w; i += 24) {
      ctx.fillRect(sx + i + 4, sy + 16, 8, 6);
      ctx.fillRect(sx + i + 14, sy + 28, 8, 6);
    }
  } else {
    // Floating platform
    // Top
    ctx.fillStyle = "#8B4513";
    ctx.fillRect(sx, sy, p.w, 6);
    // Body
    ctx.fillStyle = "#A0522D";
    ctx.fillRect(sx, sy + 6, p.w, p.h - 6);
    // Highlight
    ctx.fillStyle = "rgba(255,255,255,0.15)";
    ctx.fillRect(sx, sy, p.w, 3);
    // Shadow
    ctx.fillStyle = "rgba(0,0,0,0.15)";
    ctx.fillRect(sx, sy + p.h - 3, p.w, 3);
    // Brick pattern
    ctx.strokeStyle = "rgba(0,0,0,0.2)";
    ctx.lineWidth = 1;
    for (let i = 0; i < p.w; i += 24) {
      ctx.strokeRect(sx + i, sy, 24, p.h);
    }
  }
}

function drawCoin(c, time) {
  if (c.collected) return;
  const sx = c.x - camera.x;
  const sy = c.y + Math.sin(time * 0.05 + c.bobOffset) * 5;

  if (sx < -20 || sx > canvas.width + 20) return;

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

  // Coin body
  const scaleX = Math.abs(Math.cos(time * 0.06 + c.bobOffset));
  ctx.save();
  ctx.translate(sx, sy);
  ctx.scale(scaleX, 1);
  ctx.fillStyle = "#FFD700";
  ctx.beginPath();
  ctx.arc(0, 0, 9, 0, Math.PI * 2);
  ctx.fill();
  ctx.fillStyle = "#FFA500";
  ctx.beginPath();
  ctx.arc(0, 0, 6, 0, Math.PI * 2);
  ctx.fill();
  ctx.fillStyle = "#FFD700";
  ctx.font = "bold 10px sans-serif";
  ctx.textAlign = "center";
  ctx.textBaseline = "middle";
  ctx.fillText("$", 0, 1);
  ctx.restore();
}

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

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

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

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

  // Shirt
  ctx.fillStyle = "#D32F2F";
  ctx.fillRect(-12, -2, 24, 8);

  // Head
  ctx.fillStyle = "#FFCC80";
  ctx.fillRect(-9, -14, 18, 16);

  // Hat
  ctx.fillStyle = "#D32F2F";
  ctx.fillRect(-10, -16, 20, 6);
  ctx.fillRect(-6, -20, 16, 6);
  // Hat brim
  ctx.fillStyle = "#B71C1C";
  ctx.fillRect(-12, -12, 24, 3);

  // Eyes
  ctx.fillStyle = "#333";
  ctx.fillRect(2, -10, 4, 4);

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

  // Legs animation
  const legOffset = player.onGround ? Math.sin(player.walkFrame * Math.PI / 2) * 4 : 3;
  ctx.fillStyle = "#1565C0";
  ctx.fillRect(-9, 18, 8, 6 + legOffset);
  ctx.fillRect(1, 18, 8, 6 - legOffset);

  // Shoes
  ctx.fillStyle = "#5D4037";
  ctx.fillRect(-10, 22 + legOffset, 9, 4);
  ctx.fillRect(1, 22 - legOffset, 9, 4);

  ctx.restore();
}

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

  if (sx < -50 || sx > canvas.width + 50) return;

  // Pole
  ctx.fillStyle = "#666";
  ctx.fillRect(sx, sy, 6, 240);

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

  // Flag
  const wave = Math.sin(time * 0.08) * 5;
  ctx.fillStyle = player.won ? "#4CAF50" : "#D32F2F";
  ctx.beginPath();
  ctx.moveTo(sx + 6, sy + 5);
  ctx.lineTo(sx + 50 + wave, sy + 20);
  ctx.lineTo(sx + 6, sy + 40);
  ctx.closePath();
  ctx.fill();

  // Star on flag
  ctx.fillStyle = "#FFD700";
  ctx.font = "16px sans-serif";
  ctx.textAlign = "center";
  ctx.fillText("★", sx + 25 + wave * 0.5, sy + 28);

  // Base
  ctx.fillStyle = "#8B4513";
  ctx.fillRect(sx - 8, sy + 235, 22, 15);
}

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

function drawHUD() {
  // Score background
  ctx.fillStyle = "rgba(0,0,0,0.5)";
  roundRect(ctx, 10, 10, 180, 40, 8);
  ctx.fill();

  // Coin icon
  ctx.fillStyle = "#FFD700";
  ctx.font = "bold 18px sans-serif";
  ctx.textAlign = "left";
  ctx.textBaseline = "middle";
  ctx.fillText("🪙 " + score, 22, 31);

  // Coins collected
  const collected = coins.filter(c => c.collected).length;
  ctx.fillStyle = "rgba(0,0,0,0.5)";
  roundRect(ctx, canvas.width - 170, 10, 160, 40, 8);
  ctx.fill();

  ctx.fillStyle = "#fff";
  ctx.textAlign = "right";
  ctx.fillText("Coins: " + collected + "/" + coins.length, canvas.width - 22, 31);

  // Progress bar
  const progress = player.x / (WORLD_WIDTH - canvas.width);
  ctx.fillStyle = "rgba(0,0,0,0.3)";
  roundRect(ctx, canvas.width / 2 - 100, 14, 200, 8, 4);
  ctx.fill();
  ctx.fillStyle = "#4CAF50";
  roundRect(ctx, canvas.width / 2 - 100, 14, Math.max(8, 200 * Math.min(1, progress)), 8, 4);
  ctx.fill();

  // Win message
  if (player.won) {
    ctx.fillStyle = "rgba(0,0,0,0.6)";
    ctx.fillRect(0, 0, canvas.width, canvas.height);

    ctx.fillStyle = "#FFD700";
    ctx.font = "bold 48px sans-serif";
    ctx.textAlign = "center";
    ctx.textBaseline = "middle";
    ctx.fillText("🎉 YOU WIN! 🎉", canvas.width / 2, canvas.height / 2 - 40);

    ctx.fillStyle = "#fff";
    ctx.font = "24px sans-serif";
    ctx.fillText("Score: " + score + "  |  Coins: " + coins.filter(c => c.collected).length + "/" + coins.length, canvas.width / 2, canvas.height / 2 + 15);

    ctx.fillStyle = "#aaa";
    ctx.font = "16px sans-serif";
    ctx.fillText("Press R to play again", canvas.width / 2, canvas.height / 2 + 55);
  }
}

function roundRect(ctx, x, y, w, h, r) {
  ctx.beginPath();
  ctx.moveTo(x + r, y);
  ctx.lineTo(x + w - r, y);
  ctx.quadraticCurveTo(x + w, y, x + w, y + r);
  ctx.lineTo(x + w, y + h - r);
  ctx.quadraticCurveTo(x + w, y + h, x + w - r, y + h);
  ctx.lineTo(x + r, y + h);
  ctx.quadraticCurveTo(x, y + h, x, y + h - r);
  ctx.lineTo(x, y + r);
  ctx.quadraticCurveTo(x, y, x + r, y);
  ctx.closePath();
}

function drawDeathOverlay() {
  if (!player.dead) return;
  ctx.fillStyle = "rgba(200,0,0,0.3)";
  ctx.fillRect(0, 0, canvas.width, canvas.height);
  ctx.fillStyle = "#fff";
  ctx.font = "bold 32px sans-serif";
  ctx.textAlign = "center";
  ctx.textBaseline = "middle";
  ctx.fillText("OOPS!", canvas.width / 2, canvas.height / 2);
}

function draw() {
  const time = gameTime + (player.won ? 0 : Date.now() / 16);

  // Screen shake
  ctx.save();
  if (screenShake > 0) {
    ctx.translate(
      (Math.random() - 0.5) * screenShake * 2,
      (Math.random() - 0.5) * screenShake * 2
    );
  }

  drawSky();

  // Platforms
  for (const p of platforms) drawPlatform(p);

  // Coins
  for (const c of coins) drawCoin(c, time);

  // Flag
  drawFlag(time);

  // Player
  drawPlayer(time);

  // Particles
  drawParticles();

  ctx.restore();

  // HUD (not affected by screen shake)
  drawHUD();
  drawDeathOverlay();
}

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

// --- Init ---
resetPlayer();

// Restart on R
window.addEventListener("keydown", e => {
  if (e.code === "KeyR") resetPlayer();
});

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

    body {
      background: #1a1a2e;
      display: flex;
      flex-direction: column;
      align-items: center;
      justify-content: center;
      min-height: 100vh;
      font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
      overflow: hidden;
    }

    h1 {
      color: #FFD700;
      font-size: 28px;
      margin-bottom: 12px;
      text-shadow: 2px 2px 4px rgba(0,0,0,0.5);
      letter-spacing: 2px;
    }

    #game {
      border: 3px solid #333;
      border-radius: 6px;
      box-shadow: 0 0 30px rgba(92, 148, 252, 0.3);
      display: block;
      background: #000;
    }

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

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

rep 4 MANUAL partial/5

pi 53.4s · verify 4.1s · 18525 tok · responded: ornith-1.0-35b · 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">
<title>Super Platformer</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
html, body {
  width: 100%; height: 100%;
  background: #1a1a2e;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  overflow: hidden;
  font-family: monospace;
}
canvas {
  border: 3px solid #444;
  border-radius: 4px;
  display: block;
  background: #87CEEB;
}
#ui-overlay {
  position: absolute;
  top: 0; left: 0;
  width: 100%; height: 100%;
  pointer-events: none;
  display: flex;
  align-items: center;
  justify-content: center;
}
#message {
  display: none;
  background: rgba(0,0,0,0.7);
  color: #fff;
  padding: 30px 50px;
  border-radius: 12px;
  font-size: 28px;
  text-align: center;
  pointer-events: auto;
}
#message h2 { margin-bottom: 10px; font-size: 32px; }
#message p { font-size: 16px; color: #ccc; }
</style>
</head>
<body>
<canvas id="game" width="800" height="450"></canvas>
<div id="ui-overlay">
  <div id="message">
    <h2 id="msg-title">🏆 You Win!</h2>
    <p id="msg-body">Coins collected: 0</p>
    <p style="margin-top:12px;font-size:14px;color:#aaa;">Press Enter or Space to play again</p>
  </div>
</div>
<script>
"use strict";

// ─── Constants ────────────────────────────────────────────────────
const W = 800, H = 450;
const GRAVITY = 0.55;
const FRICTION = 0.82;
const MAX_SPEED = 5.5;
const JUMP_FORCE = -12;
const TILE = 40;

// ─── Canvas ───────────────────────────────────────────────────────
const canvas = document.getElementById("game");
const ctx = canvas.getContext("2d");
const msgDiv = document.getElementById("message");
const msgTitle = document.getElementById("msg-title");
const msgBody = document.getElementById("msg-body");

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

// ─── Level Design ─────────────────────────────────────────────────
// 0 = air, 1 = ground block, 2 = brick platform, 3 = coin, 4 = flag pole base, 5 = flag pole
// Level is 50 tiles wide (2000px), 11.25 tiles tall (450px)
const LEVEL_W = 50;
const LEVEL_H = 12;

function buildLevel() {
  const tiles = [];
  for (let y = 0; y < LEVEL_H; y++) {
    tiles[y] = [];
    for (let x = 0; x < LEVEL_W; x++) {
      tiles[y][x] = 0;
    }
  }

  // Ground (rows 10-11, with gaps)
  const groundSegments = [
    [0, 14],  // 0-13
    [17, 30], // 17-29
    [33, 49], // 33-48
  ];
  for (const [s, e] of groundSegments) {
    for (let x = s; x <= e; x++) {
      tiles[10][x] = 1;
      tiles[11][x] = 1;
    }
  }

  // Platforms (raised)
  const platforms = [
    [6, 5, 3],   // x=6, y=5, w=3  → tiles 6,7,8 at row 5
    [12, 7, 2],  // x=12, y=7, w=2
    [20, 6, 4],  // x=20, y=6, w=4
    [26, 4, 3],  // x=26, y=4, w=3
    [35, 7, 3],  // x=35, y=7, w=3
    [40, 5, 2],  // x=40, y=5, w=2
    [43, 3, 4],  // x=43, y=3, w=4
  ];
  for (const [sx, sy, w] of platforms) {
    for (let x = sx; x < sx + w; x++) {
      tiles[sy][x] = 2;
    }
  }

  // Staircase near the end
  for (let i = 0; i < 5; i++) {
    for (let y = 9 - i; y <= 9; y++) {
      tiles[y][45 + i] = 1;
    }
  }

  // Coins
  const coins = [
    [3, 8], [4, 8], [5, 8],
    [7, 4], [8, 4], [9, 4],
    [13, 6], [14, 6],
    [21, 5], [22, 5], [23, 5],
    [27, 3], [28, 3],
    [36, 6], [37, 6], [38, 6],
    [41, 4], [42, 4],
    [44, 2], [45, 2], [46, 2], [47, 2],
    [10, 9], [15, 9],
  ];

  // Flag pole at the end
  const flagX = 48;
  tiles[3][flagX] = 5;
  tiles[4][flagX] = 5;
  tiles[5][flagX] = 5;
  tiles[6][flagX] = 5;
  tiles[7][flagX] = 5;
  tiles[8][flagX] = 5;
  tiles[9][flagX] = 4; // base

  return { tiles, coins, flagX };
}

// ─── Game State ───────────────────────────────────────────────────
let level, coins, score, gameState, camX, player, particles;

function resetGame() {
  const lv = buildLevel();
  level = lv.tiles;
  coins = lv.coins.map(c => ({ x: c[0] * TILE + 8, y: c[1] * TILE + 8, w: 24, h: 24, collected: false, bobPhase: Math.random() * Math.PI * 2 }));
  score = 0;
  gameState = "playing";
  camX = 0;
  player = {
    x: 2 * TILE,
    y: 7 * TILE,
    w: 28,
    h: 36,
    vx: 0,
    vy: 0,
    onGround: false,
    facing: 1,
    walkFrame: 0,
    walkTimer: 0,
  };
  particles = [];
  msgDiv.style.display = "none";
}

resetGame();

// ─── Collision Helpers ────────────────────────────────────────────
function getTile(px, py) {
  const tx = Math.floor(px / TILE);
  const ty = Math.floor(py / TILE);
  if (tx < 0 || tx >= LEVEL_W || ty < 0 || ty >= LEVEL_H) return 0;
  return level[ty][tx];
}

function isSolid(tx, ty) {
  if (tx < 0 || tx >= LEVEL_W || ty < 0 || ty >= LEVEL_H) return false;
  return level[ty][tx] === 1 || level[ty][tx] === 2;
}

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

// ─── Update ───────────────────────────────────────────────────────
function update() {
  if (gameState !== "playing") return;

  const p = player;

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

  if (moveX !== 0) {
    p.vx += moveX * 1.2;
    if (p.vx > MAX_SPEED) p.vx = MAX_SPEED;
    if (p.vx < -MAX_SPEED) p.vx = -MAX_SPEED;
    p.facing = moveX;
  } else {
    p.vx *= FRICTION;
    if (Math.abs(p.vx) < 0.1) p.vx = 0;
  }

  // Jump
  if ((keys["Space"] || keys["ArrowUp"] || keys["KeyW"]) && p.onGround) {
    p.vy = JUMP_FORCE;
    p.onGround = false;
    spawnParticles(p.x + p.w / 2, p.y + p.h, "#fff", 5);
  }

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

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

  // Horizontal collision
  p.x += p.vx;
  resolveCollisionsX(p);

  // Vertical collision
  p.y += p.vy;
  p.onGround = false;
  resolveCollisionsY(p);

  // Keep in bounds
  if (p.x < 0) p.x = 0;
  if (p.y > LEVEL_H * TILE + 50) {
    // Fell in pit — respawn
    p.x = 2 * TILE;
    p.y = 7 * TILE;
    p.vx = 0;
    p.vy = 0;
  }

  // Coin collection
  for (const coin of coins) {
    if (coin.collected) continue;
    coin.bobPhase += 0.06;
    const bobY = Math.sin(coin.bobPhase) * 4;
    if (rectsOverlap(
      p.x, p.y, p.w, p.h,
      coin.x, coin.y + bobY, coin.w, coin.h
    )) {
      coin.collected = true;
      score++;
      spawnParticles(coin.x + coin.w / 2, coin.y + coin.h / 2, "#FFD700", 8);
    }
  }

  // Flag pole check (win)
  if (p.x + p.w > level[0] && level[0]) {
    // Just check if player reaches flag X area
    const flagTile = level[Math.floor(p.y / TILE)]?.[p.x + p.w / 2];
    if (flagTile === 4 || flagTile === 5) {
      gameState = "won";
      msgTitle.textContent = "🏆 You Win!";
      msgBody.textContent = `Coins collected: ${score} / ${coins.length}`;
      msgDiv.style.display = "block";
      spawnParticles(p.x + p.w / 2, p.y, "#FFD700", 20);
      return;
    }
  }

  // Camera
  const targetCam = p.x - W / 3;
  camX += (targetCam - camX) * 0.1;
  if (camX < 0) camX = 0;
  const maxCam = LEVEL_W * TILE - W;
  if (camX > maxCam) camX = maxCam;

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

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

function resolveCollisionsY(p) {
  const left = Math.floor(p.x / TILE);
  const right = Math.floor((p.x + p.w - 1) / TILE);
  for (let tx = left; tx <= right; tx++) {
    if (p.vy > 0) {
      const ty = Math.floor((p.y + p.h) / TILE);
      if (isSolid(tx, ty)) {
        p.y = ty * TILE - p.h;
        p.vy = 0;
        p.onGround = true;
      }
    } else if (p.vy < 0) {
      const ty = Math.floor(p.y / TILE);
      if (isSolid(tx, ty)) {
        p.y = (ty + 1) * TILE;
        p.vy = 0;
      }
    }
  }
}

function rectsOverlap(x1, y1, w1, h1, x2, y2, w2, h2) {
  return x1 < x2 + w2 && x1 + w1 > x2 && y1 < y2 + h2 && y1 + h1 > y2;
}

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

  // Clouds (parallax)
  ctx.fillStyle = "rgba(255,255,255,0.8)";
  const cloudData = [
    [100, 60, 60], [350, 40, 45], [600, 70, 50], [900, 50, 55],
    [1200, 65, 40], [1600, 45, 50], [2000, 55, 45], [2500, 40, 60],
    [3000, 60, 40], [3500, 50, 50], [4000, 35, 55], [4500, 65, 45],
    [5000, 45, 50], [5500, 55, 40], [6000, 40, 55],
  ];
  for (const [cx, cy, cr] of cloudData) {
    const sx = cx - camX * 0.3;
    const wrap = ((sx % (W + 200)) + W + 200) % (W + 200) - 100;
    drawCloud(wrap, cy, cr);
  }

  // Hills (parallax)
  ctx.fillStyle = "#66BB6A";
  const hillData = [
    [50, 380, 120], [300, 370, 80], [600, 385, 100], [900, 375, 90],
    [1200, 380, 110], [1500, 370, 85], [1800, 385, 95], [2200, 375, 105],
    [2600, 380, 80], [3000, 370, 100], [3400, 385, 90], [3800, 375, 110],
    [4200, 380, 85], [4600, 370, 95], [5000, 385, 100], [5400, 375, 90],
    [5800, 380, 105], [6200, 370, 80],
  ];
  for (const [hx, hy, hr] of hillData) {
    const sx = hx - camX * 0.5;
    ctx.beginPath();
    ctx.arc(sx, hy, hr, Math.PI, 0);
    ctx.fill();
  }
}

function drawCloud(x, y, r) {
  ctx.beginPath();
  ctx.arc(x, y, r * 0.6, 0, Math.PI * 2);
  ctx.arc(x + r * 0.5, y - r * 0.2, r * 0.5, 0, Math.PI * 2);
  ctx.arc(x + r, y, r * 0.55, 0, Math.PI * 2);
  ctx.arc(x + r * 0.5, y + r * 0.1, r * 0.45, 0, Math.PI * 2);
  ctx.fill();
}

function drawTiles() {
  const startCol = Math.floor(camX / TILE);
  const endCol = startCol + Math.ceil(W / TILE) + 1;

  for (let ty = 0; ty < LEVEL_H; ty++) {
    for (let tx = startCol; tx <= endCol && tx < LEVEL_W; tx++) {
      const tile = level[ty][tx];
      if (tile === 0) continue;

      const sx = tx * TILE - camX;
      const sy = ty * TILE;

      if (tile === 1) {
        // Ground block
        ctx.fillStyle = "#5D4037";
        ctx.fillRect(sx, sy, TILE, TILE);
        // Grass top
        if (ty === 0 || level[ty - 1][tx] === 0) {
          ctx.fillStyle = "#4CAF50";
          ctx.fillRect(sx, sy, TILE, 8);
          // Grass blades
          ctx.fillStyle = "#388E3C";
          for (let i = 0; i < 4; i++) {
            const gx = sx + i * 10 + 2;
            ctx.fillRect(gx, sy - 3, 3, 5);
          }
        }
        // Texture
        ctx.fillStyle = "rgba(0,0,0,0.1)";
        ctx.fillRect(sx + 2, sy + 2, TILE - 4, TILE - 4);
        ctx.fillStyle = "rgba(255,255,255,0.05)";
        ctx.fillRect(sx + 2, sy + 2, TILE - 4, 4);
      } else if (tile === 2) {
        // Brick platform
        ctx.fillStyle = "#BF360C";
        ctx.fillRect(sx, sy, TILE, TILE);
        ctx.fillStyle = "#D84315";
        ctx.fillRect(sx + 1, sy + 1, TILE / 2 - 2, TILE / 2 - 2);
        ctx.fillRect(sx + TILE / 2 + 1, sy + TILE / 2 + 1, TILE / 2 - 2, TILE / 2 - 2);
        ctx.strokeStyle = "#8B2500";
        ctx.lineWidth = 1;
        ctx.strokeRect(sx, sy, TILE, TILE);
      } else if (tile === 4) {
        // Flag pole base
        ctx.fillStyle = "#4E342E";
        ctx.fillRect(sx + 10, sy + 10, 20, 30);
        ctx.fillStyle = "#333";
        ctx.fillRect(sx + 12, sy + 12, 16, 26);
      } else if (tile === 5) {
        // Flag pole
        ctx.fillStyle = "#795548";
        ctx.fillRect(sx + 17, sy, 6, TILE);
      }
    }
  }
}

function drawFlag() {
  const flagTile = 3; // top of pole
  const fx = 48 * TILE + 20 - camX;
  // Flag triangle
  ctx.fillStyle = "#F44336";
  ctx.beginPath();
  ctx.moveTo(fx, flagTile * TILE + 5);
  ctx.lineTo(fx + 30, flagTile * TILE + 20);
  ctx.lineTo(fx, flagTile * TILE + 35);
  ctx.closePath();
  ctx.fill();
  // Star on flag
  ctx.fillStyle = "#FFEB3B";
  ctx.font = "14px monospace";
  ctx.fillText("★", fx + 8, flagTile * TILE + 25);
}

function drawCoins() {
  for (const coin of coins) {
    if (coin.collected) continue;
    const bobY = Math.sin(coin.bobPhase) * 4;
    const sx = coin.x - camX;
    const sy = coin.y + bobY;

    // Glow
    ctx.fillStyle = "rgba(255,215,0,0.3)";
    ctx.beginPath();
    ctx.arc(sx + coin.w / 2, sy + coin.h / 2, 16, 0, Math.PI * 2);
    ctx.fill();

    // Coin body
    const stretch = Math.abs(Math.sin(coin.bobPhase * 2));
    ctx.fillStyle = "#FFD700";
    ctx.beginPath();
    ctx.ellipse(sx + coin.w / 2, sy + coin.h / 2, 10 - stretch * 3, 10, 0, 0, Math.PI * 2);
    ctx.fill();
    ctx.strokeStyle = "#FFA000";
    ctx.lineWidth = 2;
    ctx.stroke();

    // $ symbol
    ctx.fillStyle = "#FFA000";
    ctx.font = "bold 12px monospace";
    ctx.textAlign = "center";
    ctx.fillText("$", sx + coin.w / 2, sy + coin.h / 2 + 5);
    ctx.textAlign = "left";
  }
}

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

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

  // Body
  ctx.fillStyle = "#E53935";
  ctx.fillRect(-10, -14, 20, 22);

  // Head
  ctx.fillStyle = "#FFCCBC";
  ctx.fillRect(-8, -22, 16, 10);

  // Hat
  ctx.fillStyle = "#E53935";
  ctx.fillRect(-10, -26, 22, 6);
  ctx.fillRect(-6, -28, 18, 4);

  // Eye
  ctx.fillStyle = "#000";
  ctx.fillRect(2, -19, 4, 4);

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

  // Legs (animated)
  ctx.fillStyle = "#1565C0";
  const legOffset = p.onGround ? Math.sin(p.walkTimer) * 4 : 0;
  ctx.fillRect(-8, 8, 7, 10 + legOffset);
  ctx.fillRect(2, 8, 7, 10 - legOffset);

  // Shoes
  ctx.fillStyle = "#5D4037";
  ctx.fillRect(-9, 17 + legOffset, 9, 4);
  ctx.fillRect(1, 17 - legOffset, 9, 4);

  ctx.restore();
}

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

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

  // Coin icon
  ctx.fillStyle = "#FFD700";
  ctx.beginPath();
  ctx.arc(32, 28, 10, 0, Math.PI * 2);
  ctx.fill();
  ctx.fillStyle = "#FFA000";
  ctx.font = "bold 12px monospace";
  ctx.textAlign = "center";
  ctx.fillText("$", 32, 32);

  // Score text
  ctx.fillStyle = "#fff";
  ctx.font = "bold 18px monospace";
  ctx.textAlign = "left";
  ctx.fillText(`${score} / ${coins.length}`, 48, 32);

  // Controls hint (fades out)
  if (score === 0 && player.x < TILE * 4) {
    ctx.fillStyle = "rgba(0,0,0,0.6)";
    ctx.fillRect(W / 2 - 160, H - 50, 320, 40);
    ctx.fillStyle = "#fff";
    ctx.font = "14px monospace";
    ctx.textAlign = "center";
    ctx.fillText("Arrow Keys / WASD to move  •  Space / Up to jump", W / 2, H - 25);
  }
}

function draw() {
  ctx.clearRect(0, 0, W, H);
  drawSky();
  drawTiles();
  drawFlag();
  drawCoins();
  drawPlayer();
  drawParticles();
  drawHUD();
}

// ─── Game Loop ────────────────────────────────────────────────────
function gameLoop() {
  // Handle restart
  if (gameState === "won" && (keys["Enter"] || keys["Space"])) {
    resetGame();
  }

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

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

rep 5 MANUAL fail/5

pi 61.8s · verify 4.2s · 40721 tok · responded: ornith-1.0-35b · 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">
<title>Super Platformer</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
  background: #1a1a2e;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  height: 100vh;
  overflow: hidden;
  font-family: monospace;
}
canvas {
  border: 3px solid #444;
  border-radius: 4px;
  display: block;
  image-rendering: pixelated;
}
#info {
  color: #aaa;
  margin-top: 10px;
  font-size: 14px;
  text-align: center;
}
</style>
</head>
<body>
<canvas id="game" width="800" height="450"></canvas>
<div id="info">Arrow Keys / WASD to move — Space / Up to jump — Collect coins &amp; reach the flag!</div>
<script>
// ============================================================
//  SUPER PLATFORMER — single-file, zero dependencies
// ============================================================

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

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

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

// ---- World geometry ----
const GRAVITY = 0.55;
const FRICTION = 0.82;
const MOVE_ACCEL = 0.6;
const MAX_SPEED = 5.5;
const JUMP_FORCE = -11.5;
const WORLD_WIDTH = 4200;

// Platforms: { x, y, w, h }
const platforms = [
  // ground segments
  { x: 0,     y: 400, w: 600,  h: 50 },
  { x: 700,   y: 400, w: 400,  h: 50 },
  { x: 1200,  y: 400, w: 800,  h: 50 },
  { x: 2200,  y: 400, w: 300,  h: 50 },
  { x: 2700,  y: 400, w: 1500, h: 50 },

  // raised platforms
  { x: 250,  y: 310, w: 150, h: 20 },
  { x: 500,  y: 250, w: 120, h: 20 },
  { x: 750,  y: 280, w: 180, h: 20 },
  { x: 1000, y: 220, w: 140, h: 20 },
  { x: 1300, y: 300, w: 200, h: 20 },
  { x: 1550, y: 230, w: 120, h: 20 },
  { x: 1750, y: 170, w: 160, h: 20 },
  { x: 2000, y: 300, w: 150, h: 20 },
  { x: 2400, y: 250, w: 180, h: 20 },
  { x: 2650, y: 190, w: 130, h: 20 },
  { x: 2900, y: 270, w: 200, h: 20 },
  { x: 3200, y: 200, w: 150, h: 20 },
  { x: 3500, y: 150, w: 180, h: 20 },
];

// Coins
const coins = [];
function spawnCoins() {
  coins.length = 0;
  // on ground
  for (let x = 100; x < 4000; x += 120) {
    coins.push({ x: x + 15, y: 370, r: 10, collected: false, bobPhase: Math.random() * Math.PI * 2 });
  }
  // on platforms
  platforms.forEach((p, i) => {
    if (i === 0 || i === 1) return; // skip first two ground segments
    const cx = p.x + p.w / 2 - 10;
    const cy = p.y - 30;
    coins.push({ x: cx, y: cy, r: 10, collected: false, bobPhase: Math.random() * Math.PI * 2 });
  });
  // extra floating coins
  coins.push({ x: 600, y: 200, r: 10, collected: false, bobPhase: 0 });
  coins.push({ x: 900, y: 170, r: 10, collected: false, bobPhase: 0 });
  coins.push({ x: 1800, y: 130, r: 10, collected: false, bobPhase: 0 });
  coins.push({ x: 3100, y: 160, r: 10, collected: false, bobPhase: 0 });
  coins.push({ x: 3400, y: 110, r: 10, collected: false, bobPhase: 0 });
  totalCoins = coins.length;
}
spawnCoins();

// Goal flag
const goal = { x: 4050, y: 150, w: 10, h: 250 };

// ---- Player ----
const player = {
  x: 80, y: 300, w: 28, h: 36,
  vx: 0, vy: 0,
  onGround: false,
  facing: 1, // 1 right, -1 left
  walkFrame: 0,
  walkTimer: 0,
  squash: 1,
  stretch: 1,
};

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

// ---- 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) * 6,
      vy: (Math.random() - 1.5) * 5,
      life: 30 + Math.random() * 20,
      maxLife: 50,
      r: 2 + Math.random() * 3,
      color,
    });
  }
}

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

// ---- Hills (decorative background) ----
const hills = [];
for (let i = 0; i < 15; i++) {
  hills.push({
    x: i * 350 + Math.random() * 100,
    r: 60 + Math.random() * 80,
    color: `hsl(${120 + Math.random() * 30}, ${40 + Math.random() * 20}%, ${25 + Math.random() * 15}%)`,
  });
}

// ============================================================
//  UPDATE
// ============================================================
function update() {
  if (gameState === 'won') {
    winTimer++;
    updateParticles();
    return;
  }

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

  // Horizontal movement
  if (moveRight) {
    player.vx += MOVE_ACCEL;
    player.facing = 1;
  } else if (moveLeft) {
    player.vx -= MOVE_ACCEL;
    player.facing = -1;
  } else {
    player.vx *= FRICTION;
    if (Math.abs(player.vx) < 0.1) player.vx = 0;
  }

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

  // Jump
  if (jumpKey && player.onGround) {
    player.vy = JUMP_FORCE;
    player.onGround = false;
    player.squash = 0.7;
    player.stretch = 1.3;
    spawnParticles(player.x + player.w / 2, player.y + player.h, '#fff', 5);
  }

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

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

  // Platform collision X
  for (const p of platforms) {
    if (rectCollide(player, p)) {
      if (player.vx > 0) {
        player.x = p.x - player.w;
      } else if (player.vx < 0) {
        player.x = p.x + p.w;
      }
      player.vx = 0;
    }
  }

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

  // Platform collision Y
  for (const p of platforms) {
    if (rectCollide(player, p)) {
      if (player.vy > 0) {
        // Landing on top
        player.y = p.y - player.h;
        player.vy = 0;
        if (!player.onGround) {
          player.squash = 1.3;
          player.stretch = 0.7;
          spawnParticles(player.x + player.w / 2, player.y + player.h, '#ccc', 3);
        }
        player.onGround = true;
      } else if (player.vy < 0) {
        // Hit head
        player.y = p.y + p.h;
        player.vy = 0;
      }
    }
  }

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

  // Fall death
  if (player.y > H + 100) {
    // Respawn
    player.x = 80;
    player.y = 300;
    player.vx = 0;
    player.vy = 0;
    score = Math.max(0, score - 5);
  }

  // Coin collection
  const px = player.x + player.w / 2;
  const py = player.y + player.h / 2;
  for (const c of coins) {
    if (c.collected) continue;
    const dx = px - c.x;
    const dy = py - (c.y + Math.sin(c.bobPhase) * 5);
    if (Math.sqrt(dx * dx + dy * dy) < c.r + 18) {
      c.collected = true;
      score += 10;
      spawnParticles(c.x, c.y, '#FFD700', 8);
    }
  }

  // Goal check
  if (player.x + player.w > goal.x && player.x < goal.x + goal.w &&
      player.y + player.h > goal.y && player.y < goal.y + goal.h) {
    gameState = 'won';
    winTimer = 0;
    spawnParticles(goal.x, goal.y + 30, '#FFD700', 30);
    spawnParticles(goal.x, goal.y + 30, '#FF6B6B', 20);
    spawnParticles(goal.x, goal.y + 30, '#4ECDC4', 20);
  }

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

  // Squash & stretch recovery
  player.squash += (1 - player.squash) * 0.15;
  player.stretch += (1 - player.stretch) * 0.15;

  // Coin bob
  for (const c of coins) {
    c.bobPhase += 0.06;
  }

  // Cloud drift
  for (const c of clouds) {
    c.x += c.speed;
    if (c.x > WORLD_WIDTH + 100) c.x = -c.w - 50;
  }

  updateParticles();

  // Camera
  const targetX = player.x - W / 2 + player.w / 2;
  camera.x += (targetX - camera.x) * 0.08;
  camera.x = Math.max(0, Math.min(WORLD_WIDTH - W, camera.x));
}

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

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

// ============================================================
//  DRAW
// ============================================================
function draw() {
  // Sky gradient
  const skyGrad = ctx.createLinearGradient(0, 0, 0, H);
  skyGrad.addColorStop(0, '#4FC3F7');
  skyGrad.addColorStop(0.6, '#81D4FA');
  skyGrad.addColorStop(1, '#B3E5FC');
  ctx.fillStyle = skyGrad;
  ctx.fillRect(0, 0, W, H);

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

  // Hills (parallax-ish)
  for (const h of hills) {
    ctx.fillStyle = h.color;
    ctx.beginPath();
    ctx.arc(h.x, 420, h.r, Math.PI, 0);
    ctx.fill();
  }

  // Clouds
  for (const c of clouds) {
    ctx.fillStyle = 'rgba(255,255,255,0.8)';
    drawCloud(c.x, c.y, c.w, c.h);
  }

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

  // Coins
  for (const c of coins) {
    if (c.collected) continue;
    drawCoin(c);
  }

  // Goal flag
  drawGoal();

  // Player
  drawPlayer();

  // Particles
  for (const p of particles) {
    const alpha = p.life / p.maxLife;
    ctx.globalAlpha = alpha;
    ctx.fillStyle = p.color;
    ctx.beginPath();
    ctx.arc(p.x, p.y, p.r * alpha, 0, Math.PI * 2);
    ctx.fill();
  }
  ctx.globalAlpha = 1;

  ctx.restore();

  // HUD
  drawHUD();

  // Win screen
  if (gameState === 'won') {
    drawWinScreen();
  }

  // Respawn hint
  if (player.y > H) {
    ctx.fillStyle = 'rgba(0,0,0,0.5)';
    ctx.fillRect(0, 0, W, H);
    ctx.fillStyle = '#fff';
    ctx.font = '24px monospace';
    ctx.textAlign = 'center';
    ctx.fillText('Oops! You fell. Respawn...', W / 2, H / 2);
  }
}

function drawCloud(x, y, w, h) {
  ctx.beginPath();
  ctx.ellipse(x + w / 2, y + h / 2, w / 2, h / 2, 0, 0, Math.PI * 2);
  ctx.fill();
  ctx.beginPath();
  ctx.ellipse(x + w * 0.25, y + h * 0.3, w * 0.3, h * 0.4, 0, 0, Math.PI * 2);
  ctx.fill();
  ctx.beginPath();
  ctx.ellipse(x + w * 0.7, y + h * 0.35, w * 0.25, h * 0.35, 0, 0, Math.PI * 2);
  ctx.fill();
}

function drawPlatform(p) {
  // Main block
  const grad = ctx.createLinearGradient(p.x, p.y, p.x, p.y + p.h);
  if (p.h > 30) {
    // Ground
    grad.addColorStop(0, '#4CAF50');
    grad.addColorStop(0.15, '#388E3C');
    grad.addColorStop(0.2, '#8D6E63');
    grad.addColorStop(1, '#5D4037');
  } else {
    // Floating platform
    grad.addColorStop(0, '#FF8A65');
    grad.addColorStop(1, '#D84315');
  }
  ctx.fillStyle = grad;
  ctx.fillRect(p.x, p.y, p.w, p.h);

  // Top highlight
  ctx.fillStyle = p.h > 30 ? 'rgba(76,175,80,0.6)' : 'rgba(255,138,101,0.5)';
  ctx.fillRect(p.x, p.y, p.w, 4);

  // Border
  ctx.strokeStyle = 'rgba(0,0,0,0.2)';
  ctx.lineWidth = 1;
  ctx.strokeRect(p.x, p.y, p.w, p.h);

  // Grass tufts on ground
  if (p.h > 30) {
    ctx.fillStyle = '#66BB6A';
    for (let gx = p.x + 5; gx < p.x + p.w - 5; gx += 15) {
      ctx.beginPath();
      ctx.moveTo(gx, p.y);
      ctx.quadraticCurveTo(gx + 3, p.y - 6, gx + 6, p.y);
      ctx.fill();
    }
  }
}

function drawCoin(c) {
  const bob = Math.sin(c.bobPhase) * 5;
  const cy = c.y + bob;
  const stretch = Math.abs(Math.cos(c.bobPhase * 0.5));

  ctx.save();
  ctx.translate(c.x, cy);
  ctx.scale(Math.max(0.2, stretch), 1);

  // Glow
  ctx.fillStyle = 'rgba(255,215,0,0.3)';
  ctx.beginPath();
  ctx.arc(0, 0, c.r + 4, 0, Math.PI * 2);
  ctx.fill();

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

  // Inner circle
  ctx.fillStyle = '#FFC107';
  ctx.beginPath();
  ctx.arc(0, 0, c.r * 0.6, 0, Math.PI * 2);
  ctx.fill();

  // $ symbol
  ctx.fillStyle = '#FFF8E1';
  ctx.font = 'bold 11px monospace';
  ctx.textAlign = 'center';
  ctx.textBaseline = 'middle';
  ctx.fillText('$', 0, 1);

  ctx.restore();
}

function drawGoal() {
  // Pole
  ctx.fillStyle = '#795548';
  ctx.fillRect(goal.x, goal.y, goal.w, goal.h);

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

  // Flag
  const wave = Math.sin(Date.now() / 300) * 5;
  ctx.fillStyle = '#F44336';
  ctx.beginPath();
  ctx.moveTo(goal.x + goal.w, goal.y + 10);
  ctx.lineTo(goal.x + goal.w + 40 + wave, goal.y + 25);
  ctx.lineTo(goal.x + goal.w, goal.y + 45);
  ctx.closePath();
  ctx.fill();

  // Star on flag
  ctx.fillStyle = '#FFD700';
  ctx.font = '14px monospace';
  ctx.textAlign = 'center';
  ctx.fillText('★', goal.x + goal.w + 18 + wave / 2, goal.y + 30);
}

function drawPlayer() {
  ctx.save();
  const cx = player.x + player.w / 2;
  const cy = player.y + player.h / 2;
  ctx.translate(cx, cy);
  ctx.scale(player.facing, 1);
  ctx.scale(player.squash, player.stretch);

  const hw = player.w / 2;
  const hh = player.h / 2;

  // Shadow
  ctx.fillStyle = 'rgba(0,0,0,0.15)';
  ctx.beginPath();
  ctx.ellipse(2, hh + 2, hw * 0.8, 4, 0, 0, Math.PI * 2);
  ctx.fill();

  // Body (blue overalls)
  ctx.fillStyle = '#1565C0';
  roundRect(-hw + 2, -hh + 14, player.w - 4, hh - 2, 3);

  // Shirt (red)
  ctx.fillStyle = '#E53935';
  roundRect(-hw + 2, -hh + 8, player.w - 4, 10, 3);

  // Head
  ctx.fillStyle = '#FFCC80';
  roundRect(-hw + 4, -hh + 2, player.w - 8, 12, 4);

  // Hat
  ctx.fillStyle = '#E53935';
  roundRect(-hw + 2, -hh, player.w - 4, 7, 3);
  // Hat brim
  ctx.fillStyle = '#C62828';
  roundRect(-hw + 1, -hh + 4, player.w - 2, 4, 2);

  // Eyes
  ctx.fillStyle = '#333';
  ctx.beginPath();
  ctx.arc(4, -hh + 8, 2.5, 0, Math.PI * 2);
  ctx.fill();

  // Mustache
  ctx.fillStyle = '#5D4037';
  roundRect(1, -hh + 11, 12, 3, 1);

  // Legs animation
  const legOffset = player.onGround ? Math.sin(player.walkFrame * Math.PI / 2) * 4 : 3;
  ctx.fillStyle = '#1565C0';
  roundRect(-hw + 4, hh - 8, 8, 8, 2);
  roundRect(hw - 12, hh - 8, 8, 8, 2);

  // Shoes
  ctx.fillStyle = '#5D4037';
  roundRect(-hw + 3, hh - 4, 10, 4, 2);
  roundRect(hw - 13, hh - 4, 10, 4, 2);

  ctx.restore();
}

function roundRect(x, y, w, h, r) {
  ctx.beginPath();
  ctx.moveTo(x + r, y);
  ctx.lineTo(x + w - r, y);
  ctx.quadraticCurveTo(x + w, y, x + w, y + r);
  ctx.lineTo(x + w, y + h - r);
  ctx.quadraticCurveTo(x + w, y + h, x + w - r, y + h);
  ctx.lineTo(x + r, y + h);
  ctx.quadraticCurveTo(x, y + h, x, y + h - r);
  ctx.lineTo(x, y + r);
  ctx.quadraticCurveTo(x, y, x + r, y);
  ctx.fill();
}

function drawHUD() {
  // Score background
  ctx.fillStyle = 'rgba(0,0,0,0.4)';
  roundRectHUD(10, 10, 200, 40, 8);

  // Coin icon
  ctx.fillStyle = '#FFD700';
  ctx.beginPath();
  ctx.arc(35, 30, 10, 0, Math.PI * 2);
  ctx.fill();
  ctx.fillStyle = '#FFF8E1';
  ctx.font = 'bold 12px monospace';
  ctx.textAlign = 'center';
  ctx.fillText('$', 35, 33);

  // Score text
  ctx.fillStyle = '#fff';
  ctx.font = 'bold 20px monospace';
  ctx.textAlign = 'left';
  ctx.fillText(`${score}`, 52, 35);

  // Coin counter
  const collected = coins.filter(c => c.collected).length;
  ctx.fillStyle = 'rgba(0,0,0,0.4)';
  roundRectHUD(220, 10, 180, 40, 8);
  ctx.fillStyle = '#FFD700';
  ctx.font = 'bold 16px monospace';
  ctx.textAlign = 'left';
  ctx.fillText(`Coins: ${collected} / ${totalCoins}`, 232, 35);

  // Distance marker
  const dist = Math.floor(player.x / 10);
  ctx.fillStyle = 'rgba(0,0,0,0.4)';
  roundRectHUD(W - 130, 10, 120, 30, 8);
  ctx.fillStyle = '#fff';
  ctx.font = '14px monospace';
  ctx.textAlign = 'right';
  ctx.fillText(`${dist}m`, W - 20, 31);
}

function roundRectHUD(x, y, w, h, r) {
  ctx.beginPath();
  ctx.moveTo(x + r, y);
  ctx.lineTo(x + w - r, y);
  ctx.quadraticCurveTo(x + w, y, x + w, y + r);
  ctx.lineTo(x + w, y + h - r);
  ctx.quadraticCurveTo(x + w, y + h, x + w - r, y + h);
  ctx.lineTo(x + r, y + h);
  ctx.quadraticCurveTo(x, y + h, x, y + h - r);
  ctx.lineTo(x, y + r);
  ctx.quadraticCurveTo(x, y, x + r, y);
  ctx.fill();
}

function drawWinScreen() {
  const alpha = Math.min(1, winTimer / 60);
  ctx.fillStyle = `rgba(0,0,0,${alpha * 0.6})`;
  ctx.fillRect(0, 0, W, H);

  if (winTimer > 30) {
    ctx.fillStyle = `rgba(255,255,255,${alpha})`;
    ctx.font = 'bold 48px monospace';
    ctx.textAlign = 'center';
    ctx.fillText('🎉 YOU WIN! 🎉', W / 2, H / 2 - 40);
  }

  if (winTimer > 60) {
    ctx.fillStyle = `rgba(255,255,255,${alpha})`;
    ctx.font = '24px monospace';
    ctx.textAlign = 'center';
    ctx.fillText(`Score: ${score}  |  Coins: ${coins.filter(c => c.collected).length} / ${totalCoins}`, W / 2, H / 2 + 20);
  }

  if (winTimer > 90) {
    const pulse = 0.8 + Math.sin(Date.now() / 200) * 0.2;
    ctx.fillStyle = `rgba(255,255,255,${alpha * pulse})`;
    ctx.font = '18px monospace';
    ctx.textAlign = 'center';
    ctx.fillText('Press R to restart', W / 2, H / 2 + 60);
  }

  // Restart
  if (winTimer > 90 && (keys['KeyR'] || keys['Enter'])) {
    restartGame();
  }
}

function restartGame() {
  score = 0;
  gameState = 'playing';
  winTimer = 0;
  player.x = 80;
  player.y = 300;
  player.vx = 0;
  player.vy = 0;
  player.onGround = false;
  spawnCoins();
  particles.length = 0;
}

// ============================================================
//  GAME LOOP
// ============================================================
function gameLoop() {
  update();
  draw();
  requestAnimationFrame(gameLoop);
}

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