← Qiki · all runs · 📊 leaderboard

r-1784543685543-7tepp
buld me a small 2d snake game
model w8fajwih8isx2rl94rb8ggc1knmnf8x9-Qwen3.6-35B-A3B-MTP-GGUF upstream http://127.0.0.1:8080 harness 127.0.0.1 score 10 / 10
2026-07-20 10:34:45 UTC · total 45s · 1 step
code-app · done · 45s
2D Snake Game
10 / 10
model w8fajwih8isx2rl94rb8ggc1knmnf8x9-Qwen3.6-35B-A3B-MTP-GGUF harness 127.0.0.1:8080
validity 2/2 fidelity 2/2 structure 2/2 depth 2/2 cleanliness 2/2
game · 356 lines
Click the preview to focus for keyboard input. Open in new tab for full-screen play.
view source (19085 chars)
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Build a classic, polished 2D Snake game </title>
<style>
:root {
  --bg: #0a1a3d;
  --ground: #3d6e26;
  --primary: #ff6b6b;
  --secondary: #22d3ee;
  --danger: #ffd166;
  --text: #f2f4fa;
}
* { box-sizing: border-box; margin: 0; padding: 0; }
html, body { height: 100%; overflow: hidden; background: var(--bg); color: var(--text); font-family: 'Trebuchet MS', -apple-system, sans-serif; }
#stage { position: relative; width: 100vw; height: 100vh; display: flex; align-items: center; justify-content: center; }
canvas { max-width: 100%; max-height: 100%; background: var(--bg); border: 4px solid rgba(255,255,255,0.08); border-radius: 12px; box-shadow: 0 20px 60px rgba(0,0,0,0.45); touch-action: none; display: block; }
.hud { position: absolute; top: 24px; left: 24px; right: 24px; display: flex; justify-content: space-between; font-size: 22px; font-weight: bold; color: var(--text); text-shadow: 2px 2px 0 rgba(0,0,0,0.5); pointer-events: none; z-index: 10; }
.hud .score { color: var(--danger); }
.hud .level { color: var(--secondary); }
.overlay { position: absolute; inset: 0; background: rgba(0,0,0,0.72); display: none; align-items: center; justify-content: center; flex-direction: column; z-index: 20; color: var(--text); text-align: center; padding: 20px; }
.overlay.show { display: flex; }
.overlay h1 { font-size: clamp(2rem, 5vw, 3.6rem); margin-bottom: 12px; color: var(--primary); text-shadow: 3px 3px 0 rgba(0,0,0,0.5); }
.overlay p { font-size: 1.25rem; margin-bottom: 24px; max-width: 640px; opacity: 0.9; }
.btn { background: var(--primary); color: var(--text); border: 4px solid rgba(255,255,255,0.9); border-radius: 12px; padding: 14px 36px; font-size: 1.4rem; font-weight: bold; cursor: pointer; transition: transform 120ms; }
.btn:hover { transform: scale(1.06); }
.btn:active { transform: scale(0.94); }
.credit { position: absolute; bottom: 12px; right: 16px; font-size: 12px; opacity: 0.4; }
</style>
</head>
<body>
<div id="stage">
  <canvas id="game"></canvas>
  <div class="hud">
    <span class="score">Score: <span id="score-val">0</span></span>
    <span class="level">Level: <span id="level-val">1</span></span>
    <span class="lives">Lives: <span id="lives-val">3</span></span>
  </div>
  <div class="overlay show" id="overlay-start">
    <h1>Build a classic, polished 2D Snake game </h1>
    <p>Click enemies to score. Don&#39;t miss.</p>
    <button class="btn" id="btn-start">PLAY</button>
  </div>
  <div class="overlay" id="overlay-over">
    <h1>Game Over</h1>
    <p>Final score: <span id="final-score">0</span></p>
    <button class="btn" id="btn-restart">PLAY AGAIN</button>
  </div>
  <div class="overlay" id="overlay-clear">
    <h1>Level Complete!</h1>
    <p>Score: <span id="clear-score">0</span></p>
    <button class="btn" id="btn-next">NEXT LEVEL</button>
  </div>
  <span class="credit">Built on Own1 · Qiki</span>
</div>
<script>
const SPEC = {"title":"Build a classic, polished 2D Snake game ","tagline":"Click enemies to score. Don't miss.","palette":{"bg":"#0a1a3d","ground":"#3d6e26","primary":"#ff6b6b","secondary":"#22d3ee","danger":"#ffd166","text":"#f2f4fa"},"world":{"stageW":960,"stageH":640,"gravity":0,"friction":0.985,"airResistance":0.995,"groundY":560},"input":"click","sprites":{"target":{"svg":"<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 100 100\"><circle cx=\"50\" cy=\"50\" r=\"40\" fill=\"#ff6b6b\" stroke=\"#8b1a1a\" stroke-width=\"4\"/><circle cx=\"50\" cy=\"50\" r=\"24\" fill=\"#f2f4fa\"/><circle cx=\"50\" cy=\"50\" r=\"10\" fill=\"#ff6b6b\"/></svg>","w":60,"h":60},"hazard":{"svg":"<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 100 100\"><polygon points=\"50,10 90,90 10,90\" fill=\"#ffd166\" stroke=\"#8b6600\" stroke-width=\"4\"/><text x=\"50\" y=\"70\" font-size=\"42\" text-anchor=\"middle\" fill=\"#111\" font-weight=\"bold\">!</text></svg>","w":60,"h":60},"crosshair":{"svg":"<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 100 100\"><circle cx=\"50\" cy=\"50\" r=\"30\" fill=\"none\" stroke=\"#22d3ee\" stroke-width=\"4\"/><line x1=\"50\" y1=\"10\" x2=\"50\" y2=\"30\" stroke=\"#22d3ee\" stroke-width=\"4\"/><line x1=\"50\" y1=\"70\" x2=\"50\" y2=\"90\" stroke=\"#22d3ee\" stroke-width=\"4\"/><line x1=\"10\" y1=\"50\" x2=\"30\" y2=\"50\" stroke=\"#22d3ee\" stroke-width=\"4\"/><line x1=\"70\" y1=\"50\" x2=\"90\" y2=\"50\" stroke=\"#22d3ee\" stroke-width=\"4\"/></svg>","w":40,"h":40}},"entities":[{"name":"crosshair","sprite":"crosshair","behavior":"topdown","hp":999,"scoreOnKill":0,"isPlayer":true,"speed":0},{"name":"target","sprite":"target","behavior":"patrol","hp":1,"scoreOnKill":100,"speed":120},{"name":"hazard","sprite":"hazard","behavior":"patrol","hp":1,"scoreOnKill":-50,"speed":180}],"levels":[{"n":1,"goal":"score","goalTarget":300,"spawns":[{"entity":"crosshair","x":480,"y":320},{"entity":"target","x":200,"y":200,"vx":120},{"entity":"target","x":600,"y":350,"vx":-120},{"entity":"target","x":780,"y":150,"vx":90}]},{"n":2,"goal":"score","goalTarget":600,"spawns":[{"entity":"crosshair","x":480,"y":320},{"entity":"target","x":150,"y":180,"vx":150},{"entity":"target","x":500,"y":300,"vx":-150},{"entity":"target","x":800,"y":220,"vx":120},{"entity":"hazard","x":400,"y":500,"vx":180}]},{"n":3,"goal":"score","goalTarget":1000,"spawns":[{"entity":"crosshair","x":480,"y":320},{"entity":"target","x":100,"y":150,"vx":200},{"entity":"target","x":500,"y":250,"vx":-200},{"entity":"target","x":800,"y":350,"vx":160},{"entity":"target","x":300,"y":450,"vx":-160},{"entity":"hazard","x":300,"y":500,"vx":220},{"entity":"hazard","x":700,"y":100,"vx":-220}]}],"audio":{"on_action":[660,0.06,"square"],"on_hit":[220,0.08,"sawtooth"],"on_kill":[880,0.12,"triangle"],"on_level_clear":[[523,0.12,"sine"],[659,0.12,"sine"],[784,0.24,"sine"]],"on_gameover":[110,0.4,"sawtooth"]}};

'use strict';
const canvas = document.getElementById('game');
const ctx = canvas.getContext('2d');
const W = SPEC.world.stageW, H = SPEC.world.stageH;
canvas.width = W; canvas.height = H;

// LLM-spec normalization. Small models often forget groundY when the input
// mode is slingshot/platformer — with gravity > 0 and no ground, the player
// falls off-screen and the canvas looks empty. Auto-derive a floor.
if ((SPEC.world.groundY === null || SPEC.world.groundY === undefined) && SPEC.world.gravity > 0) {
  SPEC.world.groundY = Math.floor(H * 0.85);
}
const scoreEl = document.getElementById('score-val');
const levelEl = document.getElementById('level-val');
const livesEl = document.getElementById('lives-val');
const overStart = document.getElementById('overlay-start');
const overOver  = document.getElementById('overlay-over');
const overClear = document.getElementById('overlay-clear');
const finalScoreEl = document.getElementById('final-score');
const clearScoreEl = document.getElementById('clear-score');

// Preload sprites — decode inline SVG to <img> for fast canvas draws.
// LLM output normalization: our schema wants {svg, w, h} objects but smaller
// models often emit plain SVG strings and skip the sprite dimensions. Accept
// both shapes here so the game renders no matter which way the model composed
// the spec.
const SPRITE_IMGS = {}, SPRITE_META = {};
for (const [name, raw] of Object.entries(SPEC.sprites)) {
  const isString = typeof raw === 'string';
  const svg = isString ? raw : (raw && raw.svg) || '';
  const w = isString ? 40 : Number(raw && raw.w) || 40;
  const h = isString ? 40 : Number(raw && raw.h) || 40;
  if (!svg) continue;
  const img = new Image();
  img.src = 'data:image/svg+xml;utf8,' + encodeURIComponent(svg);
  SPRITE_IMGS[name] = img;
  SPRITE_META[name] = { w, h };
  // Backfill the spec so downstream reads via SPEC.sprites[<name>].w/h still work.
  SPEC.sprites[name] = { svg, w, h };
}

// AudioContext + tiny synth
let AC = null;
function sfx(tone) {
  if (!tone) return;
  if (!AC) AC = new (window.AudioContext || window.webkitAudioContext)();
  const [freq, dur, wave] = tone;
  const osc = AC.createOscillator(); const gain = AC.createGain();
  osc.type = wave || 'sine'; osc.frequency.value = freq;
  osc.connect(gain); gain.connect(AC.destination);
  gain.gain.setValueAtTime(0.18, AC.currentTime);
  gain.gain.exponentialRampToValueAtTime(0.001, AC.currentTime + dur);
  osc.start(); osc.stop(AC.currentTime + dur);
}
function melody(tones) { if (!tones) return; let t = 0; for (const tone of tones) { setTimeout(() => sfx(tone), t*1000); t += tone[1]; } }

// Game state
const S = { state: 'MENU', score: 0, level: 0, lives: 3, tPrev: 0, tSurv: 0, alive: [], playerRef: null };

function findEntitySpec(name) { return SPEC.entities.find(e => e.name === name) || null; }

function spawn(sp) {
  const espec = findEntitySpec(sp.entity);
  if (!espec) return null;
  const sprite = SPEC.sprites[espec.sprite] || Object.values(SPEC.sprites)[0];
  const w = sprite ? sprite.w : 40, h = sprite ? sprite.h : 40;
  const obj = {
    name: espec.name, sprite: espec.sprite, behavior: espec.behavior,
    hp: espec.hp, scoreOnKill: espec.scoreOnKill, isPlayer: !!espec.isPlayer,
    speed: espec.speed || 0,
    x: sp.x, y: sp.y, vx: sp.vx || 0, vy: sp.vy || 0,
    w, h, dead: false, patrolLeft: sp.x - 120, patrolRight: sp.x + 120,
    patrolDir: (sp.vx || espec.speed || 100) > 0 ? 1 : -1,
    orbitCX: sp.x, orbitCY: sp.y, orbitR: 80, orbitTheta: 0,
  };
  if (obj.isPlayer) S.playerRef = obj;
  return obj;
}

function loadLevel(idx) {
  const lvl = SPEC.levels[idx];
  if (!lvl) return null;
  S.alive = lvl.spawns.map(spawn).filter(Boolean);
  S.tSurv = 0;
  S.playerRef = S.alive.find(e => e.isPlayer) || null;
  levelEl.textContent = String(lvl.n);
  return lvl;
}

// Input state
const IN = { keys: {}, mouse: {x: W/2, y: H/2, down: false, downX: 0, downY: 0} };
window.addEventListener('keydown', e => { IN.keys[e.key.toLowerCase()] = true; if (S.state === 'PLAY' && SPEC.input === 'space' && (e.key === ' ' || e.key === 'ArrowUp')) { flapImpulse(); sfx(SPEC.audio.on_action); e.preventDefault(); } });
window.addEventListener('keyup',   e => { IN.keys[e.key.toLowerCase()] = false; });
function ptFromEvent(e) {
  const r = canvas.getBoundingClientRect();
  const t = e.touches ? e.touches[0] : (e.changedTouches ? e.changedTouches[0] : e);
  return { x: (t.clientX - r.left) * (W / r.width), y: (t.clientY - r.top) * (H / r.height) };
}
function onDown(e) {
  const p = ptFromEvent(e); IN.mouse.x = p.x; IN.mouse.y = p.y; IN.mouse.down = true; IN.mouse.downX = p.x; IN.mouse.downY = p.y;
  if (S.state !== 'PLAY') return;
  if (SPEC.input === 'click') { fireClick(p.x, p.y); sfx(SPEC.audio.on_action); }
  if (SPEC.input === 'space') { flapImpulse(); sfx(SPEC.audio.on_action); }
  e.preventDefault();
}
function onMove(e) { const p = ptFromEvent(e); IN.mouse.x = p.x; IN.mouse.y = p.y; e.preventDefault(); }
function onUp(e) {
  const p = ptFromEvent(e);
  if (S.state === 'PLAY' && IN.mouse.down) {
    if (SPEC.input === 'slingshot') fireSlingshot(p.x, p.y);
    if (SPEC.input === 'swipe')     fireSwipe(p.x, p.y);
  }
  IN.mouse.down = false; e.preventDefault();
}
canvas.addEventListener('mousedown', onDown);
canvas.addEventListener('mousemove', onMove);
canvas.addEventListener('mouseup', onUp);
canvas.addEventListener('touchstart', onDown, {passive: false});
canvas.addEventListener('touchmove', onMove, {passive: false});
canvas.addEventListener('touchend', onUp, {passive: false});

// Input actions
function fireClick(x, y) {
  for (const e of S.alive) {
    if (e.isPlayer) continue;
    if (Math.abs(e.x - x) < e.w/2 && Math.abs(e.y - y) < e.h/2) {
      hit(e); return;
    }
  }
}
function fireSlingshot(rx, ry) {
  const player = S.playerRef; if (!player) return;
  const dx = player.x - rx, dy = player.y - ry;
  const projSpec = SPEC.entities.find(e => e.behavior === 'projectile') || SPEC.entities.find(e => !e.isPlayer);
  if (!projSpec) return;
  const proj = spawn({entity: projSpec.name, x: player.x, y: player.y - 40, vx: dx * 3, vy: dy * 3});
  proj.behavior = 'projectile'; proj.isProjectile = true;
  S.alive.push(proj);
}
function fireSwipe(x, y) {
  const player = S.playerRef; if (!player) return;
  const dx = x - IN.mouse.downX, dy = y - IN.mouse.downY;
  const mag = Math.hypot(dx, dy) || 1;
  player.vx = (dx / mag) * (player.speed || 200);
  player.vy = (dy / mag) * (player.speed || 200);
}
function flapImpulse() {
  const player = S.playerRef; if (!player) return;
  player.vy = -420;
}

// Physics + update
function update(dt) {
  const gr = SPEC.world.gravity, gy = SPEC.world.groundY;
  for (const e of S.alive) {
    if (e.dead) continue;
    // player-driven arrows/wasd movement
    if (e.isPlayer && (SPEC.input === 'arrows' || SPEC.input === 'wasd')) {
      const leftKey = SPEC.input === 'wasd' ? 'a' : 'arrowleft';
      const rightKey = SPEC.input === 'wasd' ? 'd' : 'arrowright';
      const upKey    = SPEC.input === 'wasd' ? 'w' : 'arrowup';
      const dnKey    = SPEC.input === 'wasd' ? 's' : 'arrowdown';
      if (IN.keys[leftKey])  e.vx = -(e.speed || 240);
      else if (IN.keys[rightKey]) e.vx = (e.speed || 240);
      else e.vx *= Math.pow(SPEC.world.friction, dt);
      if (SPEC.world.gravity > 0) {
        if (IN.keys[upKey] && gy !== null && e.y + e.h/2 >= gy - 2) e.vy = -520;
      } else {
        if (IN.keys[upKey])  e.vy = -(e.speed || 240);
        else if (IN.keys[dnKey]) e.vy = (e.speed || 240);
        else e.vy *= Math.pow(SPEC.world.friction, dt);
      }
    }
    // behavior-specific update
    switch (e.behavior) {
      case 'gravity':    e.vy += gr * dt; break;
      case 'projectile': e.vy += gr * dt; e.vx *= Math.pow(SPEC.world.airResistance, dt); e.vy *= Math.pow(SPEC.world.airResistance, dt); break;
      case 'flap':       e.vy += gr * dt; break;
      case 'topdown':    e.vx *= Math.pow(SPEC.world.friction, dt); e.vy *= Math.pow(SPEC.world.friction, dt); break;
      case 'patrol': {
        e.vx = e.patrolDir * (e.speed || 120);
        if (e.x < e.patrolLeft)  e.patrolDir = 1;
        if (e.x > e.patrolRight) e.patrolDir = -1;
        break;
      }
      case 'homing': {
        const p = S.playerRef; if (p) {
          const dx = p.x - e.x, dy = p.y - e.y; const m = Math.hypot(dx, dy) || 1;
          e.vx = (dx / m) * (e.speed || 100); e.vy = (dy / m) * (e.speed || 100);
        }
        break;
      }
      case 'orbit': {
        e.orbitTheta += (e.speed || 60) / e.orbitR * dt;
        e.x = e.orbitCX + Math.cos(e.orbitTheta) * e.orbitR;
        e.y = e.orbitCY + Math.sin(e.orbitTheta) * e.orbitR;
        continue;
      }
      case 'static': continue;
    }
    e.x += e.vx * dt; e.y += e.vy * dt;
    // ground collision
    if (gy !== null && e.y + e.h/2 > gy) {
      e.y = gy - e.h/2; if (e.behavior === 'projectile' && Math.abs(e.vy) < 60) { e.dead = true; }
      e.vy = -e.vy * 0.5;
    }
    // wall wrap for topdown/patrol; kill projectile on edge
    if (e.behavior === 'projectile' && (e.x < -50 || e.x > W + 50 || e.y > H + 50)) e.dead = true;
    if (e.behavior === 'topdown' || e.behavior === 'flap') {
      e.x = Math.max(e.w/2, Math.min(W - e.w/2, e.x));
      e.y = Math.max(e.h/2, Math.min(H - e.h/2, e.y));
    }
  }
  // collision: projectile-vs-anyone, player-vs-nonPlayer for topdown/flap
  for (const a of S.alive) {
    if (a.dead) continue;
    if (a.isProjectile) {
      for (const b of S.alive) {
        if (b === a || b.dead || b.isPlayer) continue;
        if (aabb(a, b)) { hit(b); a.dead = true; break; }
      }
    }
    if (a.isPlayer && (SPEC.input === 'arrows' || SPEC.input === 'wasd' || SPEC.input === 'space' || SPEC.input === 'swipe')) {
      for (const b of S.alive) {
        if (b === a || b.dead || b.isPlayer) continue;
        if (aabb(a, b)) {
          if (b.scoreOnKill > 0) { hit(b); }
          else { S.lives--; b.dead = true; livesEl.textContent = String(S.lives); sfx(SPEC.audio.on_hit); if (S.lives <= 0) return gameOver(); }
        }
      }
    }
  }
  S.alive = S.alive.filter(e => !e.dead);
  // goal check
  const lvl = SPEC.levels[S.level]; if (!lvl) return;
  S.tSurv += dt;
  let done = false;
  if (lvl.goal === 'kill_all')      done = S.alive.filter(e => !e.isPlayer).length === 0;
  else if (lvl.goal === 'reach_x')  done = S.playerRef && S.playerRef.x >= (lvl.goalTarget || W - 60);
  else if (lvl.goal === 'reach_y')  done = S.playerRef && S.playerRef.y <= (lvl.goalTarget || 40);
  else if (lvl.goal === 'survive')  done = S.tSurv >= (lvl.goalTarget || 30);
  else if (lvl.goal === 'score')    done = S.score >= (lvl.goalTarget || 500);
  if (done) return levelClear();
}

function aabb(a, b) { return Math.abs(a.x - b.x) < (a.w + b.w) / 2 && Math.abs(a.y - b.y) < (a.h + b.h) / 2; }
function hit(e) {
  e.hp--;
  if (e.hp <= 0) { e.dead = true; S.score += e.scoreOnKill || 0; scoreEl.textContent = String(S.score); sfx(SPEC.audio.on_kill); }
  else { sfx(SPEC.audio.on_hit); }
}

// Render
function render() {
  ctx.fillStyle = SPEC.palette.bg; ctx.fillRect(0, 0, W, H);
  if (SPEC.world.groundY !== null) {
    ctx.fillStyle = SPEC.palette.ground || '#3d6e26';
    ctx.fillRect(0, SPEC.world.groundY, W, H - SPEC.world.groundY);
  }
  if (S.state === 'PLAY' && SPEC.input === 'slingshot' && IN.mouse.down && S.playerRef) {
    ctx.strokeStyle = SPEC.palette.primary; ctx.lineWidth = 3;
    ctx.beginPath(); ctx.moveTo(S.playerRef.x, S.playerRef.y - 40); ctx.lineTo(IN.mouse.x, IN.mouse.y); ctx.stroke();
  }
  for (const e of S.alive) {
    const img = SPRITE_IMGS[e.sprite];
    if (img && img.complete) ctx.drawImage(img, e.x - e.w/2, e.y - e.h/2, e.w, e.h);
    else { ctx.fillStyle = SPEC.palette.primary; ctx.fillRect(e.x - e.w/2, e.y - e.h/2, e.w, e.h); }
  }
}

// Loop
function frame(t) {
  const dt = Math.min(0.05, (t - S.tPrev) / 1000); S.tPrev = t;
  if (S.state === 'PLAY') update(dt);
  render();
  requestAnimationFrame(frame);
}

// State transitions
function showOverlay(el) { document.querySelectorAll('.overlay').forEach(o => o.classList.remove('show')); if (el) el.classList.add('show'); }
function startGame() { S.state = 'PLAY'; S.score = 0; S.level = 0; S.lives = 3; scoreEl.textContent = '0'; livesEl.textContent = '3'; loadLevel(0); showOverlay(null); }
function nextLevel() { S.level++; if (S.level >= SPEC.levels.length) { S.state = 'MENU'; showOverlay(overStart); return; } loadLevel(S.level); S.state = 'PLAY'; showOverlay(null); }
function levelClear() { S.state = 'CLEAR'; clearScoreEl.textContent = String(S.score); melody(SPEC.audio.on_level_clear); showOverlay(overClear); }
function gameOver() { S.state = 'OVER'; finalScoreEl.textContent = String(S.score); sfx(SPEC.audio.on_gameover); showOverlay(overOver); }

document.getElementById('btn-start').addEventListener('click', startGame);
document.getElementById('btn-restart').addEventListener('click', startGame);
document.getElementById('btn-next').addEventListener('click', nextLevel);

// Boot
showOverlay(overStart);
requestAnimationFrame(frame);

</script>
</body>
</html>
1 LLM call on this step
fail · 45336 ms · prompt 3302c → resp 2444c · t=0.7 max=3000 ·
→ w8fajwih8isx2rl94rb8ggc1knmnf8x9-Qwen3.6-35B-A3B-MTP-GGUF @ http://127.0.0.1:8080
LLM returned non-JSON when jsonMode=true: { "title": "Neon Serpent", "tagline", "palette": { "bg": "#050510", "ground": "#1a1a2e", "primary": "#00ff9d", "secondary": "#ff00ff",
raw JSON
{
  "id": "r-1784543685543-7tepp",
  "prompt": "buld me a small 2d snake game",
  "project": {
    "name": "Snake",
    "tagline": "The classic 2D arcade snake game",
    "theme": "minimalist retro arcade, clean geometric shapes, high contrast on dark background",
    "brandColors": [
      "#22c55e",
      "#111827",
      "#ffffff"
    ],
    "audience": "casual gamers + nostalgia seekers",
    "vibe": "simple, addictive, retro, clean"
  },
  "steps": [
    {
      "id": "s1-bbbav",
      "kind": "code-app",
      "params": {
        "brief": "Build a classic, polished 2D Snake game using HTML5 Canvas and JavaScript. Features: grid-based movement, snake grows when eating food, game over on collision with walls or self, score counter, high score tracking (local storage), start/restart button, and smooth controls (arrow keys or WASD). Style: Retro neon aesthetic on a dark background.",
        "kind": "game",
        "project": {
          "name": "Snake",
          "tagline": "The classic 2D arcade snake game",
          "theme": "minimalist retro arcade, clean geometric shapes, high contrast on dark background",
          "brandColors": [
            "#22c55e",
            "#111827",
            "#ffffff"
          ],
          "audience": "casual gamers + nostalgia seekers",
          "vibe": "simple, addictive, retro, clean"
        }
      },
      "label": "2D Snake Game"
    }
  ],
  "results": {
    "s1-bbbav": {
      "status": "done",
      "startedAt": 1784543685548,
      "output": {
        "html": "<!doctype html>\n<html lang=\"en\">\n<head>\n<meta charset=\"utf-8\">\n<meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\n<title>Build a classic, polished 2D Snake game </title>\n<style>\n:root {\n  --bg: #0a1a3d;\n  --ground: #3d6e26;\n  --primary: #ff6b6b;\n  --secondary: #22d3ee;\n  --danger: #ffd166;\n  --text: #f2f4fa;\n}\n* { box-sizing: border-box; margin: 0; padding: 0; }\nhtml, body { height: 100%; overflow: hidden; background: var(--bg); color: var(--text); font-family: 'Trebuchet MS', -apple-system, sans-serif; }\n#stage { position: relative; width: 100vw; height: 100vh; display: flex; align-items: center; justify-content: center; }\ncanvas { max-width: 100%; max-height: 100%; background: var(--bg); border: 4px solid rgba(255,255,255,0.08); border-radius: 12px; box-shadow: 0 20px 60px rgba(0,0,0,0.45); touch-action: none; display: block; }\n.hud { position: absolute; top: 24px; left: 24px; right: 24px; display: flex; justify-content: space-between; font-size: 22px; font-weight: bold; color: var(--text); text-shadow: 2px 2px 0 rgba(0,0,0,0.5); pointer-events: none; z-index: 10; }\n.hud .score { color: var(--danger); }\n.hud .level { color: var(--secondary); }\n.overlay { position: absolute; inset: 0; background: rgba(0,0,0,0.72); display: none; align-items: center; justify-content: center; flex-direction: column; z-index: 20; color: var(--text); text-align: center; padding: 20px; }\n.overlay.show { display: flex; }\n.overlay h1 { font-size: clamp(2rem, 5vw, 3.6rem); margin-bottom: 12px; color: var(--primary); text-shadow: 3px 3px 0 rgba(0,0,0,0.5); }\n.overlay p { font-size: 1.25rem; margin-bottom: 24px; max-width: 640px; opacity: 0.9; }\n.btn { background: var(--primary); color: var(--text); border: 4px solid rgba(255,255,255,0.9); border-radius: 12px; padding: 14px 36px; font-size: 1.4rem; font-weight: bold; cursor: pointer; transition: transform 120ms; }\n.btn:hover { transform: scale(1.06); }\n.btn:active { transform: scale(0.94); }\n.credit { position: absolute; bottom: 12px; right: 16px; font-size: 12px; opacity: 0.4; }\n</style>\n</head>\n<body>\n<div id=\"stage\">\n  <canvas id=\"game\"></canvas>\n  <div class=\"hud\">\n    <span class=\"score\">Score: <span id=\"score-val\">0</span></span>\n    <span class=\"level\">Level: <span id=\"level-val\">1</span></span>\n    <span class=\"lives\">Lives: <span id=\"lives-val\">3</span></span>\n  </div>\n  <div class=\"overlay show\" id=\"overlay-start\">\n    <h1>Build a classic, polished 2D Snake game </h1>\n    <p>Click enemies to score. Don&#39;t miss.</p>\n    <button class=\"btn\" id=\"btn-start\">PLAY</button>\n  </div>\n  <div class=\"overlay\" id=\"overlay-over\">\n    <h1>Game Over</h1>\n    <p>Final score: <span id=\"final-score\">0</span></p>\n    <button class=\"btn\" id=\"btn-restart\">PLAY AGAIN</button>\n  </div>\n  <div class=\"overlay\" id=\"overlay-clear\">\n    <h1>Level Complete!</h1>\n    <p>Score: <span id=\"clear-score\">0</span></p>\n    <button class=\"btn\" id=\"btn-next\">NEXT LEVEL</button>\n  </div>\n  <span class=\"credit\">Built on Own1 · Qiki</span>\n</div>\n<script>\nconst SPEC = {\"title\":\"Build a classic, polished 2D Snake game \",\"tagline\":\"Click enemies to score. Don't miss.\",\"palette\":{\"bg\":\"#0a1a3d\",\"ground\":\"#3d6e26\",\"primary\":\"#ff6b6b\",\"secondary\":\"#22d3ee\",\"danger\":\"#ffd166\",\"text\":\"#f2f4fa\"},\"world\":{\"stageW\":960,\"stageH\":640,\"gravity\":0,\"friction\":0.985,\"airResistance\":0.995,\"groundY\":560},\"input\":\"click\",\"sprites\":{\"target\":{\"svg\":\"<svg xmlns=\\\"http://www.w3.org/2000/svg\\\" viewBox=\\\"0 0 100 100\\\"><circle cx=\\\"50\\\" cy=\\\"50\\\" r=\\\"40\\\" fill=\\\"#ff6b6b\\\" stroke=\\\"#8b1a1a\\\" stroke-width=\\\"4\\\"/><circle cx=\\\"50\\\" cy=\\\"50\\\" r=\\\"24\\\" fill=\\\"#f2f4fa\\\"/><circle cx=\\\"50\\\" cy=\\\"50\\\" r=\\\"10\\\" fill=\\\"#ff6b6b\\\"/></svg>\",\"w\":60,\"h\":60},\"hazard\":{\"svg\":\"<svg xmlns=\\\"http://www.w3.org/2000/svg\\\" viewBox=\\\"0 0 100 100\\\"><polygon points=\\\"50,10 90,90 10,90\\\" fill=\\\"#ffd166\\\" stroke=\\\"#8b6600\\\" stroke-width=\\\"4\\\"/><text x=\\\"50\\\" y=\\\"70\\\" font-size=\\\"42\\\" text-anchor=\\\"middle\\\" fill=\\\"#111\\\" font-weight=\\\"bold\\\">!</text></svg>\",\"w\":60,\"h\":60},\"crosshair\":{\"svg\":\"<svg xmlns=\\\"http://www.w3.org/2000/svg\\\" viewBox=\\\"0 0 100 100\\\"><circle cx=\\\"50\\\" cy=\\\"50\\\" r=\\\"30\\\" fill=\\\"none\\\" stroke=\\\"#22d3ee\\\" stroke-width=\\\"4\\\"/><line x1=\\\"50\\\" y1=\\\"10\\\" x2=\\\"50\\\" y2=\\\"30\\\" stroke=\\\"#22d3ee\\\" stroke-width=\\\"4\\\"/><line x1=\\\"50\\\" y1=\\\"70\\\" x2=\\\"50\\\" y2=\\\"90\\\" stroke=\\\"#22d3ee\\\" stroke-width=\\\"4\\\"/><line x1=\\\"10\\\" y1=\\\"50\\\" x2=\\\"30\\\" y2=\\\"50\\\" stroke=\\\"#22d3ee\\\" stroke-width=\\\"4\\\"/><line x1=\\\"70\\\" y1=\\\"50\\\" x2=\\\"90\\\" y2=\\\"50\\\" stroke=\\\"#22d3ee\\\" stroke-width=\\\"4\\\"/></svg>\",\"w\":40,\"h\":40}},\"entities\":[{\"name\":\"crosshair\",\"sprite\":\"crosshair\",\"behavior\":\"topdown\",\"hp\":999,\"scoreOnKill\":0,\"isPlayer\":true,\"speed\":0},{\"name\":\"target\",\"sprite\":\"target\",\"behavior\":\"patrol\",\"hp\":1,\"scoreOnKill\":100,\"speed\":120},{\"name\":\"hazard\",\"sprite\":\"hazard\",\"behavior\":\"patrol\",\"hp\":1,\"scoreOnKill\":-50,\"speed\":180}],\"levels\":[{\"n\":1,\"goal\":\"score\",\"goalTarget\":300,\"spawns\":[{\"entity\":\"crosshair\",\"x\":480,\"y\":320},{\"entity\":\"target\",\"x\":200,\"y\":200,\"vx\":120},{\"entity\":\"target\",\"x\":600,\"y\":350,\"vx\":-120},{\"entity\":\"target\",\"x\":780,\"y\":150,\"vx\":90}]},{\"n\":2,\"goal\":\"score\",\"goalTarget\":600,\"spawns\":[{\"entity\":\"crosshair\",\"x\":480,\"y\":320},{\"entity\":\"target\",\"x\":150,\"y\":180,\"vx\":150},{\"entity\":\"target\",\"x\":500,\"y\":300,\"vx\":-150},{\"entity\":\"target\",\"x\":800,\"y\":220,\"vx\":120},{\"entity\":\"hazard\",\"x\":400,\"y\":500,\"vx\":180}]},{\"n\":3,\"goal\":\"score\",\"goalTarget\":1000,\"spawns\":[{\"entity\":\"crosshair\",\"x\":480,\"y\":320},{\"entity\":\"target\",\"x\":100,\"y\":150,\"vx\":200},{\"entity\":\"target\",\"x\":500,\"y\":250,\"vx\":-200},{\"entity\":\"target\",\"x\":800,\"y\":350,\"vx\":160},{\"entity\":\"target\",\"x\":300,\"y\":450,\"vx\":-160},{\"entity\":\"hazard\",\"x\":300,\"y\":500,\"vx\":220},{\"entity\":\"hazard\",\"x\":700,\"y\":100,\"vx\":-220}]}],\"audio\":{\"on_action\":[660,0.06,\"square\"],\"on_hit\":[220,0.08,\"sawtooth\"],\"on_kill\":[880,0.12,\"triangle\"],\"on_level_clear\":[[523,0.12,\"sine\"],[659,0.12,\"sine\"],[784,0.24,\"sine\"]],\"on_gameover\":[110,0.4,\"sawtooth\"]}};\n\n'use strict';\nconst canvas = document.getElementById('game');\nconst ctx = canvas.getContext('2d');\nconst W = SPEC.world.stageW, H = SPEC.world.stageH;\ncanvas.width = W; canvas.height = H;\n\n// LLM-spec normalization. Small models often forget groundY when the input\n// mode is slingshot/platformer — with gravity > 0 and no ground, the player\n// falls off-screen and the canvas looks empty. Auto-derive a floor.\nif ((SPEC.world.groundY === null || SPEC.world.groundY === undefined) && SPEC.world.gravity > 0) {\n  SPEC.world.groundY = Math.floor(H * 0.85);\n}\nconst scoreEl = document.getElementById('score-val');\nconst levelEl = document.getElementById('level-val');\nconst livesEl = document.getElementById('lives-val');\nconst overStart = document.getElementById('overlay-start');\nconst overOver  = document.getElementById('overlay-over');\nconst overClear = document.getElementById('overlay-clear');\nconst finalScoreEl = document.getElementById('final-score');\nconst clearScoreEl = document.getElementById('clear-score');\n\n// Preload sprites — decode inline SVG to <img> for fast canvas draws.\n// LLM output normalization: our schema wants {svg, w, h} objects but smaller\n// models often emit plain SVG strings and skip the sprite dimensions. Accept\n// both shapes here so the game renders no matter which way the model composed\n// the spec.\nconst SPRITE_IMGS = {}, SPRITE_META = {};\nfor (const [name, raw] of Object.entries(SPEC.sprites)) {\n  const isString = typeof raw === 'string';\n  const svg = isString ? raw : (raw && raw.svg) || '';\n  const w = isString ? 40 : Number(raw && raw.w) || 40;\n  const h = isString ? 40 : Number(raw && raw.h) || 40;\n  if (!svg) continue;\n  const img = new Image();\n  img.src = 'data:image/svg+xml;utf8,' + encodeURIComponent(svg);\n  SPRITE_IMGS[name] = img;\n  SPRITE_META[name] = { w, h };\n  // Backfill the spec so downstream reads via SPEC.sprites[<name>].w/h still work.\n  SPEC.sprites[name] = { svg, w, h };\n}\n\n// AudioContext + tiny synth\nlet AC = null;\nfunction sfx(tone) {\n  if (!tone) return;\n  if (!AC) AC = new (window.AudioContext || window.webkitAudioContext)();\n  const [freq, dur, wave] = tone;\n  const osc = AC.createOscillator(); const gain = AC.createGain();\n  osc.type = wave || 'sine'; osc.frequency.value = freq;\n  osc.connect(gain); gain.connect(AC.destination);\n  gain.gain.setValueAtTime(0.18, AC.currentTime);\n  gain.gain.exponentialRampToValueAtTime(0.001, AC.currentTime + dur);\n  osc.start(); osc.stop(AC.currentTime + dur);\n}\nfunction melody(tones) { if (!tones) return; let t = 0; for (const tone of tones) { setTimeout(() => sfx(tone), t*1000); t += tone[1]; } }\n\n// Game state\nconst S = { state: 'MENU', score: 0, level: 0, lives: 3, tPrev: 0, tSurv: 0, alive: [], playerRef: null };\n\nfunction findEntitySpec(name) { return SPEC.entities.find(e => e.name === name) || null; }\n\nfunction spawn(sp) {\n  const espec = findEntitySpec(sp.entity);\n  if (!espec) return null;\n  const sprite = SPEC.sprites[espec.sprite] || Object.values(SPEC.sprites)[0];\n  const w = sprite ? sprite.w : 40, h = sprite ? sprite.h : 40;\n  const obj = {\n    name: espec.name, sprite: espec.sprite, behavior: espec.behavior,\n    hp: espec.hp, scoreOnKill: espec.scoreOnKill, isPlayer: !!espec.isPlayer,\n    speed: espec.speed || 0,\n    x: sp.x, y: sp.y, vx: sp.vx || 0, vy: sp.vy || 0,\n    w, h, dead: false, patrolLeft: sp.x - 120, patrolRight: sp.x + 120,\n    patrolDir: (sp.vx || espec.speed || 100) > 0 ? 1 : -1,\n    orbitCX: sp.x, orbitCY: sp.y, orbitR: 80, orbitTheta: 0,\n  };\n  if (obj.isPlayer) S.playerRef = obj;\n  return obj;\n}\n\nfunction loadLevel(idx) {\n  const lvl = SPEC.levels[idx];\n  if (!lvl) return null;\n  S.alive = lvl.spawns.map(spawn).filter(Boolean);\n  S.tSurv = 0;\n  S.playerRef = S.alive.find(e => e.isPlayer) || null;\n  levelEl.textContent = String(lvl.n);\n  return lvl;\n}\n\n// Input state\nconst IN = { keys: {}, mouse: {x: W/2, y: H/2, down: false, downX: 0, downY: 0} };\nwindow.addEventListener('keydown', e => { IN.keys[e.key.toLowerCase()] = true; if (S.state === 'PLAY' && SPEC.input === 'space' && (e.key === ' ' || e.key === 'ArrowUp')) { flapImpulse(); sfx(SPEC.audio.on_action); e.preventDefault(); } });\nwindow.addEventListener('keyup',   e => { IN.keys[e.key.toLowerCase()] = false; });\nfunction ptFromEvent(e) {\n  const r = canvas.getBoundingClientRect();\n  const t = e.touches ? e.touches[0] : (e.changedTouches ? e.changedTouches[0] : e);\n  return { x: (t.clientX - r.left) * (W / r.width), y: (t.clientY - r.top) * (H / r.height) };\n}\nfunction onDown(e) {\n  const p = ptFromEvent(e); IN.mouse.x = p.x; IN.mouse.y = p.y; IN.mouse.down = true; IN.mouse.downX = p.x; IN.mouse.downY = p.y;\n  if (S.state !== 'PLAY') return;\n  if (SPEC.input === 'click') { fireClick(p.x, p.y); sfx(SPEC.audio.on_action); }\n  if (SPEC.input === 'space') { flapImpulse(); sfx(SPEC.audio.on_action); }\n  e.preventDefault();\n}\nfunction onMove(e) { const p = ptFromEvent(e); IN.mouse.x = p.x; IN.mouse.y = p.y; e.preventDefault(); }\nfunction onUp(e) {\n  const p = ptFromEvent(e);\n  if (S.state === 'PLAY' && IN.mouse.down) {\n    if (SPEC.input === 'slingshot') fireSlingshot(p.x, p.y);\n    if (SPEC.input === 'swipe')     fireSwipe(p.x, p.y);\n  }\n  IN.mouse.down = false; e.preventDefault();\n}\ncanvas.addEventListener('mousedown', onDown);\ncanvas.addEventListener('mousemove', onMove);\ncanvas.addEventListener('mouseup', onUp);\ncanvas.addEventListener('touchstart', onDown, {passive: false});\ncanvas.addEventListener('touchmove', onMove, {passive: false});\ncanvas.addEventListener('touchend', onUp, {passive: false});\n\n// Input actions\nfunction fireClick(x, y) {\n  for (const e of S.alive) {\n    if (e.isPlayer) continue;\n    if (Math.abs(e.x - x) < e.w/2 && Math.abs(e.y - y) < e.h/2) {\n      hit(e); return;\n    }\n  }\n}\nfunction fireSlingshot(rx, ry) {\n  const player = S.playerRef; if (!player) return;\n  const dx = player.x - rx, dy = player.y - ry;\n  const projSpec = SPEC.entities.find(e => e.behavior === 'projectile') || SPEC.entities.find(e => !e.isPlayer);\n  if (!projSpec) return;\n  const proj = spawn({entity: projSpec.name, x: player.x, y: player.y - 40, vx: dx * 3, vy: dy * 3});\n  proj.behavior = 'projectile'; proj.isProjectile = true;\n  S.alive.push(proj);\n}\nfunction fireSwipe(x, y) {\n  const player = S.playerRef; if (!player) return;\n  const dx = x - IN.mouse.downX, dy = y - IN.mouse.downY;\n  const mag = Math.hypot(dx, dy) || 1;\n  player.vx = (dx / mag) * (player.speed || 200);\n  player.vy = (dy / mag) * (player.speed || 200);\n}\nfunction flapImpulse() {\n  const player = S.playerRef; if (!player) return;\n  player.vy = -420;\n}\n\n// Physics + update\nfunction update(dt) {\n  const gr = SPEC.world.gravity, gy = SPEC.world.groundY;\n  for (const e of S.alive) {\n    if (e.dead) continue;\n    // player-driven arrows/wasd movement\n    if (e.isPlayer && (SPEC.input === 'arrows' || SPEC.input === 'wasd')) {\n      const leftKey = SPEC.input === 'wasd' ? 'a' : 'arrowleft';\n      const rightKey = SPEC.input === 'wasd' ? 'd' : 'arrowright';\n      const upKey    = SPEC.input === 'wasd' ? 'w' : 'arrowup';\n      const dnKey    = SPEC.input === 'wasd' ? 's' : 'arrowdown';\n      if (IN.keys[leftKey])  e.vx = -(e.speed || 240);\n      else if (IN.keys[rightKey]) e.vx = (e.speed || 240);\n      else e.vx *= Math.pow(SPEC.world.friction, dt);\n      if (SPEC.world.gravity > 0) {\n        if (IN.keys[upKey] && gy !== null && e.y + e.h/2 >= gy - 2) e.vy = -520;\n      } else {\n        if (IN.keys[upKey])  e.vy = -(e.speed || 240);\n        else if (IN.keys[dnKey]) e.vy = (e.speed || 240);\n        else e.vy *= Math.pow(SPEC.world.friction, dt);\n      }\n    }\n    // behavior-specific update\n    switch (e.behavior) {\n      case 'gravity':    e.vy += gr * dt; break;\n      case 'projectile': e.vy += gr * dt; e.vx *= Math.pow(SPEC.world.airResistance, dt); e.vy *= Math.pow(SPEC.world.airResistance, dt); break;\n      case 'flap':       e.vy += gr * dt; break;\n      case 'topdown':    e.vx *= Math.pow(SPEC.world.friction, dt); e.vy *= Math.pow(SPEC.world.friction, dt); break;\n      case 'patrol': {\n        e.vx = e.patrolDir * (e.speed || 120);\n        if (e.x < e.patrolLeft)  e.patrolDir = 1;\n        if (e.x > e.patrolRight) e.patrolDir = -1;\n        break;\n      }\n      case 'homing': {\n        const p = S.playerRef; if (p) {\n          const dx = p.x - e.x, dy = p.y - e.y; const m = Math.hypot(dx, dy) || 1;\n          e.vx = (dx / m) * (e.speed || 100); e.vy = (dy / m) * (e.speed || 100);\n        }\n        break;\n      }\n      case 'orbit': {\n        e.orbitTheta += (e.speed || 60) / e.orbitR * dt;\n        e.x = e.orbitCX + Math.cos(e.orbitTheta) * e.orbitR;\n        e.y = e.orbitCY + Math.sin(e.orbitTheta) * e.orbitR;\n        continue;\n      }\n      case 'static': continue;\n    }\n    e.x += e.vx * dt; e.y += e.vy * dt;\n    // ground collision\n    if (gy !== null && e.y + e.h/2 > gy) {\n      e.y = gy - e.h/2; if (e.behavior === 'projectile' && Math.abs(e.vy) < 60) { e.dead = true; }\n      e.vy = -e.vy * 0.5;\n    }\n    // wall wrap for topdown/patrol; kill projectile on edge\n    if (e.behavior === 'projectile' && (e.x < -50 || e.x > W + 50 || e.y > H + 50)) e.dead = true;\n    if (e.behavior === 'topdown' || e.behavior === 'flap') {\n      e.x = Math.max(e.w/2, Math.min(W - e.w/2, e.x));\n      e.y = Math.max(e.h/2, Math.min(H - e.h/2, e.y));\n    }\n  }\n  // collision: projectile-vs-anyone, player-vs-nonPlayer for topdown/flap\n  for (const a of S.alive) {\n    if (a.dead) continue;\n    if (a.isProjectile) {\n      for (const b of S.alive) {\n        if (b === a || b.dead || b.isPlayer) continue;\n        if (aabb(a, b)) { hit(b); a.dead = true; break; }\n      }\n    }\n    if (a.isPlayer && (SPEC.input === 'arrows' || SPEC.input === 'wasd' || SPEC.input === 'space' || SPEC.input === 'swipe')) {\n      for (const b of S.alive) {\n        if (b === a || b.dead || b.isPlayer) continue;\n        if (aabb(a, b)) {\n          if (b.scoreOnKill > 0) { hit(b); }\n          else { S.lives--; b.dead = true; livesEl.textContent = String(S.lives); sfx(SPEC.audio.on_hit); if (S.lives <= 0) return gameOver(); }\n        }\n      }\n    }\n  }\n  S.alive = S.alive.filter(e => !e.dead);\n  // goal check\n  const lvl = SPEC.levels[S.level]; if (!lvl) return;\n  S.tSurv += dt;\n  let done = false;\n  if (lvl.goal === 'kill_all')      done = S.alive.filter(e => !e.isPlayer).length === 0;\n  else if (lvl.goal === 'reach_x')  done = S.playerRef && S.playerRef.x >= (lvl.goalTarget || W - 60);\n  else if (lvl.goal === 'reach_y')  done = S.playerRef && S.playerRef.y <= (lvl.goalTarget || 40);\n  else if (lvl.goal === 'survive')  done = S.tSurv >= (lvl.goalTarget || 30);\n  else if (lvl.goal === 'score')    done = S.score >= (lvl.goalTarget || 500);\n  if (done) return levelClear();\n}\n\nfunction aabb(a, b) { return Math.abs(a.x - b.x) < (a.w + b.w) / 2 && Math.abs(a.y - b.y) < (a.h + b.h) / 2; }\nfunction hit(e) {\n  e.hp--;\n  if (e.hp <= 0) { e.dead = true; S.score += e.scoreOnKill || 0; scoreEl.textContent = String(S.score); sfx(SPEC.audio.on_kill); }\n  else { sfx(SPEC.audio.on_hit); }\n}\n\n// Render\nfunction render() {\n  ctx.fillStyle = SPEC.palette.bg; ctx.fillRect(0, 0, W, H);\n  if (SPEC.world.groundY !== null) {\n    ctx.fillStyle = SPEC.palette.ground || '#3d6e26';\n    ctx.fillRect(0, SPEC.world.groundY, W, H - SPEC.world.groundY);\n  }\n  if (S.state === 'PLAY' && SPEC.input === 'slingshot' && IN.mouse.down && S.playerRef) {\n    ctx.strokeStyle = SPEC.palette.primary; ctx.lineWidth = 3;\n    ctx.beginPath(); ctx.moveTo(S.playerRef.x, S.playerRef.y - 40); ctx.lineTo(IN.mouse.x, IN.mouse.y); ctx.stroke();\n  }\n  for (const e of S.alive) {\n    const img = SPRITE_IMGS[e.sprite];\n    if (img && img.complete) ctx.drawImage(img, e.x - e.w/2, e.y - e.h/2, e.w, e.h);\n    else { ctx.fillStyle = SPEC.palette.primary; ctx.fillRect(e.x - e.w/2, e.y - e.h/2, e.w, e.h); }\n  }\n}\n\n// Loop\nfunction frame(t) {\n  const dt = Math.min(0.05, (t - S.tPrev) / 1000); S.tPrev = t;\n  if (S.state === 'PLAY') update(dt);\n  render();\n  requestAnimationFrame(frame);\n}\n\n// State transitions\nfunction showOverlay(el) { document.querySelectorAll('.overlay').forEach(o => o.classList.remove('show')); if (el) el.classList.add('show'); }\nfunction startGame() { S.state = 'PLAY'; S.score = 0; S.level = 0; S.lives = 3; scoreEl.textContent = '0'; livesEl.textContent = '3'; loadLevel(0); showOverlay(null); }\nfunction nextLevel() { S.level++; if (S.level >= SPEC.levels.length) { S.state = 'MENU'; showOverlay(overStart); return; } loadLevel(S.level); S.state = 'PLAY'; showOverlay(null); }\nfunction levelClear() { S.state = 'CLEAR'; clearScoreEl.textContent = String(S.score); melody(SPEC.audio.on_level_clear); showOverlay(overClear); }\nfunction gameOver() { S.state = 'OVER'; finalScoreEl.textContent = String(S.score); sfx(SPEC.audio.on_gameover); showOverlay(overOver); }\n\ndocument.getElementById('btn-start').addEventListener('click', startGame);\ndocument.getElementById('btn-restart').addEventListener('click', startGame);\ndocument.getElementById('btn-next').addEventListener('click', nextLevel);\n\n// Boot\nshowOverlay(overStart);\nrequestAnimationFrame(frame);\n\n</script>\n</body>\n</html>",
        "kind": "game",
        "lineCount": 356,
        "charCount": 19085,
        "llmAuthored": false,
        "fromFallback": true,
        "specTitle": "Build a classic, polished 2D Snake game ",
        "levelCount": 3,
        "entityCount": 3,
        "spriteCount": 3,
        "inputMode": "click"
      },
      "produces": {
        "appHtml": "<!doctype html>\n<html lang=\"en\">\n<head>\n<meta charset=\"utf-8\">\n<meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\n<title>Build a classic, polished 2D Snake game </title>\n<style>\n:root {\n  --bg: #0a1a3d;\n  --ground: #3d6e26;\n  --primary: #ff6b6b;\n  --secondary: #22d3ee;\n  --danger: #ffd166;\n  --text: #f2f4fa;\n}\n* { box-sizing: border-box; margin: 0; padding: 0; }\nhtml, body { height: 100%; overflow: hidden; background: var(--bg); color: var(--text); font-family: 'Trebuchet MS', -apple-system, sans-serif; }\n#stage { position: relative; width: 100vw; height: 100vh; display: flex; align-items: center; justify-content: center; }\ncanvas { max-width: 100%; max-height: 100%; background: var(--bg); border: 4px solid rgba(255,255,255,0.08); border-radius: 12px; box-shadow: 0 20px 60px rgba(0,0,0,0.45); touch-action: none; display: block; }\n.hud { position: absolute; top: 24px; left: 24px; right: 24px; display: flex; justify-content: space-between; font-size: 22px; font-weight: bold; color: var(--text); text-shadow: 2px 2px 0 rgba(0,0,0,0.5); pointer-events: none; z-index: 10; }\n.hud .score { color: var(--danger); }\n.hud .level { color: var(--secondary); }\n.overlay { position: absolute; inset: 0; background: rgba(0,0,0,0.72); display: none; align-items: center; justify-content: center; flex-direction: column; z-index: 20; color: var(--text); text-align: center; padding: 20px; }\n.overlay.show { display: flex; }\n.overlay h1 { font-size: clamp(2rem, 5vw, 3.6rem); margin-bottom: 12px; color: var(--primary); text-shadow: 3px 3px 0 rgba(0,0,0,0.5); }\n.overlay p { font-size: 1.25rem; margin-bottom: 24px; max-width: 640px; opacity: 0.9; }\n.btn { background: var(--primary); color: var(--text); border: 4px solid rgba(255,255,255,0.9); border-radius: 12px; padding: 14px 36px; font-size: 1.4rem; font-weight: bold; cursor: pointer; transition: transform 120ms; }\n.btn:hover { transform: scale(1.06); }\n.btn:active { transform: scale(0.94); }\n.credit { position: absolute; bottom: 12px; right: 16px; font-size: 12px; opacity: 0.4; }\n</style>\n</head>\n<body>\n<div id=\"stage\">\n  <canvas id=\"game\"></canvas>\n  <div class=\"hud\">\n    <span class=\"score\">Score: <span id=\"score-val\">0</span></span>\n    <span class=\"level\">Level: <span id=\"level-val\">1</span></span>\n    <span class=\"lives\">Lives: <span id=\"lives-val\">3</span></span>\n  </div>\n  <div class=\"overlay show\" id=\"overlay-start\">\n    <h1>Build a classic, polished 2D Snake game </h1>\n    <p>Click enemies to score. Don&#39;t miss.</p>\n    <button class=\"btn\" id=\"btn-start\">PLAY</button>\n  </div>\n  <div class=\"overlay\" id=\"overlay-over\">\n    <h1>Game Over</h1>\n    <p>Final score: <span id=\"final-score\">0</span></p>\n    <button class=\"btn\" id=\"btn-restart\">PLAY AGAIN</button>\n  </div>\n  <div class=\"overlay\" id=\"overlay-clear\">\n    <h1>Level Complete!</h1>\n    <p>Score: <span id=\"clear-score\">0</span></p>\n    <button class=\"btn\" id=\"btn-next\">NEXT LEVEL</button>\n  </div>\n  <span class=\"credit\">Built on Own1 · Qiki</span>\n</div>\n<script>\nconst SPEC = {\"title\":\"Build a classic, polished 2D Snake game \",\"tagline\":\"Click enemies to score. Don't miss.\",\"palette\":{\"bg\":\"#0a1a3d\",\"ground\":\"#3d6e26\",\"primary\":\"#ff6b6b\",\"secondary\":\"#22d3ee\",\"danger\":\"#ffd166\",\"text\":\"#f2f4fa\"},\"world\":{\"stageW\":960,\"stageH\":640,\"gravity\":0,\"friction\":0.985,\"airResistance\":0.995,\"groundY\":560},\"input\":\"click\",\"sprites\":{\"target\":{\"svg\":\"<svg xmlns=\\\"http://www.w3.org/2000/svg\\\" viewBox=\\\"0 0 100 100\\\"><circle cx=\\\"50\\\" cy=\\\"50\\\" r=\\\"40\\\" fill=\\\"#ff6b6b\\\" stroke=\\\"#8b1a1a\\\" stroke-width=\\\"4\\\"/><circle cx=\\\"50\\\" cy=\\\"50\\\" r=\\\"24\\\" fill=\\\"#f2f4fa\\\"/><circle cx=\\\"50\\\" cy=\\\"50\\\" r=\\\"10\\\" fill=\\\"#ff6b6b\\\"/></svg>\",\"w\":60,\"h\":60},\"hazard\":{\"svg\":\"<svg xmlns=\\\"http://www.w3.org/2000/svg\\\" viewBox=\\\"0 0 100 100\\\"><polygon points=\\\"50,10 90,90 10,90\\\" fill=\\\"#ffd166\\\" stroke=\\\"#8b6600\\\" stroke-width=\\\"4\\\"/><text x=\\\"50\\\" y=\\\"70\\\" font-size=\\\"42\\\" text-anchor=\\\"middle\\\" fill=\\\"#111\\\" font-weight=\\\"bold\\\">!</text></svg>\",\"w\":60,\"h\":60},\"crosshair\":{\"svg\":\"<svg xmlns=\\\"http://www.w3.org/2000/svg\\\" viewBox=\\\"0 0 100 100\\\"><circle cx=\\\"50\\\" cy=\\\"50\\\" r=\\\"30\\\" fill=\\\"none\\\" stroke=\\\"#22d3ee\\\" stroke-width=\\\"4\\\"/><line x1=\\\"50\\\" y1=\\\"10\\\" x2=\\\"50\\\" y2=\\\"30\\\" stroke=\\\"#22d3ee\\\" stroke-width=\\\"4\\\"/><line x1=\\\"50\\\" y1=\\\"70\\\" x2=\\\"50\\\" y2=\\\"90\\\" stroke=\\\"#22d3ee\\\" stroke-width=\\\"4\\\"/><line x1=\\\"10\\\" y1=\\\"50\\\" x2=\\\"30\\\" y2=\\\"50\\\" stroke=\\\"#22d3ee\\\" stroke-width=\\\"4\\\"/><line x1=\\\"70\\\" y1=\\\"50\\\" x2=\\\"90\\\" y2=\\\"50\\\" stroke=\\\"#22d3ee\\\" stroke-width=\\\"4\\\"/></svg>\",\"w\":40,\"h\":40}},\"entities\":[{\"name\":\"crosshair\",\"sprite\":\"crosshair\",\"behavior\":\"topdown\",\"hp\":999,\"scoreOnKill\":0,\"isPlayer\":true,\"speed\":0},{\"name\":\"target\",\"sprite\":\"target\",\"behavior\":\"patrol\",\"hp\":1,\"scoreOnKill\":100,\"speed\":120},{\"name\":\"hazard\",\"sprite\":\"hazard\",\"behavior\":\"patrol\",\"hp\":1,\"scoreOnKill\":-50,\"speed\":180}],\"levels\":[{\"n\":1,\"goal\":\"score\",\"goalTarget\":300,\"spawns\":[{\"entity\":\"crosshair\",\"x\":480,\"y\":320},{\"entity\":\"target\",\"x\":200,\"y\":200,\"vx\":120},{\"entity\":\"target\",\"x\":600,\"y\":350,\"vx\":-120},{\"entity\":\"target\",\"x\":780,\"y\":150,\"vx\":90}]},{\"n\":2,\"goal\":\"score\",\"goalTarget\":600,\"spawns\":[{\"entity\":\"crosshair\",\"x\":480,\"y\":320},{\"entity\":\"target\",\"x\":150,\"y\":180,\"vx\":150},{\"entity\":\"target\",\"x\":500,\"y\":300,\"vx\":-150},{\"entity\":\"target\",\"x\":800,\"y\":220,\"vx\":120},{\"entity\":\"hazard\",\"x\":400,\"y\":500,\"vx\":180}]},{\"n\":3,\"goal\":\"score\",\"goalTarget\":1000,\"spawns\":[{\"entity\":\"crosshair\",\"x\":480,\"y\":320},{\"entity\":\"target\",\"x\":100,\"y\":150,\"vx\":200},{\"entity\":\"target\",\"x\":500,\"y\":250,\"vx\":-200},{\"entity\":\"target\",\"x\":800,\"y\":350,\"vx\":160},{\"entity\":\"target\",\"x\":300,\"y\":450,\"vx\":-160},{\"entity\":\"hazard\",\"x\":300,\"y\":500,\"vx\":220},{\"entity\":\"hazard\",\"x\":700,\"y\":100,\"vx\":-220}]}],\"audio\":{\"on_action\":[660,0.06,\"square\"],\"on_hit\":[220,0.08,\"sawtooth\"],\"on_kill\":[880,0.12,\"triangle\"],\"on_level_clear\":[[523,0.12,\"sine\"],[659,0.12,\"sine\"],[784,0.24,\"sine\"]],\"on_gameover\":[110,0.4,\"sawtooth\"]}};\n\n'use strict';\nconst canvas = document.getElementById('game');\nconst ctx = canvas.getContext('2d');\nconst W = SPEC.world.stageW, H = SPEC.world.stageH;\ncanvas.width = W; canvas.height = H;\n\n// LLM-spec normalization. Small models often forget groundY when the input\n// mode is slingshot/platformer — with gravity > 0 and no ground, the player\n// falls off-screen and the canvas looks empty. Auto-derive a floor.\nif ((SPEC.world.groundY === null || SPEC.world.groundY === undefined) && SPEC.world.gravity > 0) {\n  SPEC.world.groundY = Math.floor(H * 0.85);\n}\nconst scoreEl = document.getElementById('score-val');\nconst levelEl = document.getElementById('level-val');\nconst livesEl = document.getElementById('lives-val');\nconst overStart = document.getElementById('overlay-start');\nconst overOver  = document.getElementById('overlay-over');\nconst overClear = document.getElementById('overlay-clear');\nconst finalScoreEl = document.getElementById('final-score');\nconst clearScoreEl = document.getElementById('clear-score');\n\n// Preload sprites — decode inline SVG to <img> for fast canvas draws.\n// LLM output normalization: our schema wants {svg, w, h} objects but smaller\n// models often emit plain SVG strings and skip the sprite dimensions. Accept\n// both shapes here so the game renders no matter which way the model composed\n// the spec.\nconst SPRITE_IMGS = {}, SPRITE_META = {};\nfor (const [name, raw] of Object.entries(SPEC.sprites)) {\n  const isString = typeof raw === 'string';\n  const svg = isString ? raw : (raw && raw.svg) || '';\n  const w = isString ? 40 : Number(raw && raw.w) || 40;\n  const h = isString ? 40 : Number(raw && raw.h) || 40;\n  if (!svg) continue;\n  const img = new Image();\n  img.src = 'data:image/svg+xml;utf8,' + encodeURIComponent(svg);\n  SPRITE_IMGS[name] = img;\n  SPRITE_META[name] = { w, h };\n  // Backfill the spec so downstream reads via SPEC.sprites[<name>].w/h still work.\n  SPEC.sprites[name] = { svg, w, h };\n}\n\n// AudioContext + tiny synth\nlet AC = null;\nfunction sfx(tone) {\n  if (!tone) return;\n  if (!AC) AC = new (window.AudioContext || window.webkitAudioContext)();\n  const [freq, dur, wave] = tone;\n  const osc = AC.createOscillator(); const gain = AC.createGain();\n  osc.type = wave || 'sine'; osc.frequency.value = freq;\n  osc.connect(gain); gain.connect(AC.destination);\n  gain.gain.setValueAtTime(0.18, AC.currentTime);\n  gain.gain.exponentialRampToValueAtTime(0.001, AC.currentTime + dur);\n  osc.start(); osc.stop(AC.currentTime + dur);\n}\nfunction melody(tones) { if (!tones) return; let t = 0; for (const tone of tones) { setTimeout(() => sfx(tone), t*1000); t += tone[1]; } }\n\n// Game state\nconst S = { state: 'MENU', score: 0, level: 0, lives: 3, tPrev: 0, tSurv: 0, alive: [], playerRef: null };\n\nfunction findEntitySpec(name) { return SPEC.entities.find(e => e.name === name) || null; }\n\nfunction spawn(sp) {\n  const espec = findEntitySpec(sp.entity);\n  if (!espec) return null;\n  const sprite = SPEC.sprites[espec.sprite] || Object.values(SPEC.sprites)[0];\n  const w = sprite ? sprite.w : 40, h = sprite ? sprite.h : 40;\n  const obj = {\n    name: espec.name, sprite: espec.sprite, behavior: espec.behavior,\n    hp: espec.hp, scoreOnKill: espec.scoreOnKill, isPlayer: !!espec.isPlayer,\n    speed: espec.speed || 0,\n    x: sp.x, y: sp.y, vx: sp.vx || 0, vy: sp.vy || 0,\n    w, h, dead: false, patrolLeft: sp.x - 120, patrolRight: sp.x + 120,\n    patrolDir: (sp.vx || espec.speed || 100) > 0 ? 1 : -1,\n    orbitCX: sp.x, orbitCY: sp.y, orbitR: 80, orbitTheta: 0,\n  };\n  if (obj.isPlayer) S.playerRef = obj;\n  return obj;\n}\n\nfunction loadLevel(idx) {\n  const lvl = SPEC.levels[idx];\n  if (!lvl) return null;\n  S.alive = lvl.spawns.map(spawn).filter(Boolean);\n  S.tSurv = 0;\n  S.playerRef = S.alive.find(e => e.isPlayer) || null;\n  levelEl.textContent = String(lvl.n);\n  return lvl;\n}\n\n// Input state\nconst IN = { keys: {}, mouse: {x: W/2, y: H/2, down: false, downX: 0, downY: 0} };\nwindow.addEventListener('keydown', e => { IN.keys[e.key.toLowerCase()] = true; if (S.state === 'PLAY' && SPEC.input === 'space' && (e.key === ' ' || e.key === 'ArrowUp')) { flapImpulse(); sfx(SPEC.audio.on_action); e.preventDefault(); } });\nwindow.addEventListener('keyup',   e => { IN.keys[e.key.toLowerCase()] = false; });\nfunction ptFromEvent(e) {\n  const r = canvas.getBoundingClientRect();\n  const t = e.touches ? e.touches[0] : (e.changedTouches ? e.changedTouches[0] : e);\n  return { x: (t.clientX - r.left) * (W / r.width), y: (t.clientY - r.top) * (H / r.height) };\n}\nfunction onDown(e) {\n  const p = ptFromEvent(e); IN.mouse.x = p.x; IN.mouse.y = p.y; IN.mouse.down = true; IN.mouse.downX = p.x; IN.mouse.downY = p.y;\n  if (S.state !== 'PLAY') return;\n  if (SPEC.input === 'click') { fireClick(p.x, p.y); sfx(SPEC.audio.on_action); }\n  if (SPEC.input === 'space') { flapImpulse(); sfx(SPEC.audio.on_action); }\n  e.preventDefault();\n}\nfunction onMove(e) { const p = ptFromEvent(e); IN.mouse.x = p.x; IN.mouse.y = p.y; e.preventDefault(); }\nfunction onUp(e) {\n  const p = ptFromEvent(e);\n  if (S.state === 'PLAY' && IN.mouse.down) {\n    if (SPEC.input === 'slingshot') fireSlingshot(p.x, p.y);\n    if (SPEC.input === 'swipe')     fireSwipe(p.x, p.y);\n  }\n  IN.mouse.down = false; e.preventDefault();\n}\ncanvas.addEventListener('mousedown', onDown);\ncanvas.addEventListener('mousemove', onMove);\ncanvas.addEventListener('mouseup', onUp);\ncanvas.addEventListener('touchstart', onDown, {passive: false});\ncanvas.addEventListener('touchmove', onMove, {passive: false});\ncanvas.addEventListener('touchend', onUp, {passive: false});\n\n// Input actions\nfunction fireClick(x, y) {\n  for (const e of S.alive) {\n    if (e.isPlayer) continue;\n    if (Math.abs(e.x - x) < e.w/2 && Math.abs(e.y - y) < e.h/2) {\n      hit(e); return;\n    }\n  }\n}\nfunction fireSlingshot(rx, ry) {\n  const player = S.playerRef; if (!player) return;\n  const dx = player.x - rx, dy = player.y - ry;\n  const projSpec = SPEC.entities.find(e => e.behavior === 'projectile') || SPEC.entities.find(e => !e.isPlayer);\n  if (!projSpec) return;\n  const proj = spawn({entity: projSpec.name, x: player.x, y: player.y - 40, vx: dx * 3, vy: dy * 3});\n  proj.behavior = 'projectile'; proj.isProjectile = true;\n  S.alive.push(proj);\n}\nfunction fireSwipe(x, y) {\n  const player = S.playerRef; if (!player) return;\n  const dx = x - IN.mouse.downX, dy = y - IN.mouse.downY;\n  const mag = Math.hypot(dx, dy) || 1;\n  player.vx = (dx / mag) * (player.speed || 200);\n  player.vy = (dy / mag) * (player.speed || 200);\n}\nfunction flapImpulse() {\n  const player = S.playerRef; if (!player) return;\n  player.vy = -420;\n}\n\n// Physics + update\nfunction update(dt) {\n  const gr = SPEC.world.gravity, gy = SPEC.world.groundY;\n  for (const e of S.alive) {\n    if (e.dead) continue;\n    // player-driven arrows/wasd movement\n    if (e.isPlayer && (SPEC.input === 'arrows' || SPEC.input === 'wasd')) {\n      const leftKey = SPEC.input === 'wasd' ? 'a' : 'arrowleft';\n      const rightKey = SPEC.input === 'wasd' ? 'd' : 'arrowright';\n      const upKey    = SPEC.input === 'wasd' ? 'w' : 'arrowup';\n      const dnKey    = SPEC.input === 'wasd' ? 's' : 'arrowdown';\n      if (IN.keys[leftKey])  e.vx = -(e.speed || 240);\n      else if (IN.keys[rightKey]) e.vx = (e.speed || 240);\n      else e.vx *= Math.pow(SPEC.world.friction, dt);\n      if (SPEC.world.gravity > 0) {\n        if (IN.keys[upKey] && gy !== null && e.y + e.h/2 >= gy - 2) e.vy = -520;\n      } else {\n        if (IN.keys[upKey])  e.vy = -(e.speed || 240);\n        else if (IN.keys[dnKey]) e.vy = (e.speed || 240);\n        else e.vy *= Math.pow(SPEC.world.friction, dt);\n      }\n    }\n    // behavior-specific update\n    switch (e.behavior) {\n      case 'gravity':    e.vy += gr * dt; break;\n      case 'projectile': e.vy += gr * dt; e.vx *= Math.pow(SPEC.world.airResistance, dt); e.vy *= Math.pow(SPEC.world.airResistance, dt); break;\n      case 'flap':       e.vy += gr * dt; break;\n      case 'topdown':    e.vx *= Math.pow(SPEC.world.friction, dt); e.vy *= Math.pow(SPEC.world.friction, dt); break;\n      case 'patrol': {\n        e.vx = e.patrolDir * (e.speed || 120);\n        if (e.x < e.patrolLeft)  e.patrolDir = 1;\n        if (e.x > e.patrolRight) e.patrolDir = -1;\n        break;\n      }\n      case 'homing': {\n        const p = S.playerRef; if (p) {\n          const dx = p.x - e.x, dy = p.y - e.y; const m = Math.hypot(dx, dy) || 1;\n          e.vx = (dx / m) * (e.speed || 100); e.vy = (dy / m) * (e.speed || 100);\n        }\n        break;\n      }\n      case 'orbit': {\n        e.orbitTheta += (e.speed || 60) / e.orbitR * dt;\n        e.x = e.orbitCX + Math.cos(e.orbitTheta) * e.orbitR;\n        e.y = e.orbitCY + Math.sin(e.orbitTheta) * e.orbitR;\n        continue;\n      }\n      case 'static': continue;\n    }\n    e.x += e.vx * dt; e.y += e.vy * dt;\n    // ground collision\n    if (gy !== null && e.y + e.h/2 > gy) {\n      e.y = gy - e.h/2; if (e.behavior === 'projectile' && Math.abs(e.vy) < 60) { e.dead = true; }\n      e.vy = -e.vy * 0.5;\n    }\n    // wall wrap for topdown/patrol; kill projectile on edge\n    if (e.behavior === 'projectile' && (e.x < -50 || e.x > W + 50 || e.y > H + 50)) e.dead = true;\n    if (e.behavior === 'topdown' || e.behavior === 'flap') {\n      e.x = Math.max(e.w/2, Math.min(W - e.w/2, e.x));\n      e.y = Math.max(e.h/2, Math.min(H - e.h/2, e.y));\n    }\n  }\n  // collision: projectile-vs-anyone, player-vs-nonPlayer for topdown/flap\n  for (const a of S.alive) {\n    if (a.dead) continue;\n    if (a.isProjectile) {\n      for (const b of S.alive) {\n        if (b === a || b.dead || b.isPlayer) continue;\n        if (aabb(a, b)) { hit(b); a.dead = true; break; }\n      }\n    }\n    if (a.isPlayer && (SPEC.input === 'arrows' || SPEC.input === 'wasd' || SPEC.input === 'space' || SPEC.input === 'swipe')) {\n      for (const b of S.alive) {\n        if (b === a || b.dead || b.isPlayer) continue;\n        if (aabb(a, b)) {\n          if (b.scoreOnKill > 0) { hit(b); }\n          else { S.lives--; b.dead = true; livesEl.textContent = String(S.lives); sfx(SPEC.audio.on_hit); if (S.lives <= 0) return gameOver(); }\n        }\n      }\n    }\n  }\n  S.alive = S.alive.filter(e => !e.dead);\n  // goal check\n  const lvl = SPEC.levels[S.level]; if (!lvl) return;\n  S.tSurv += dt;\n  let done = false;\n  if (lvl.goal === 'kill_all')      done = S.alive.filter(e => !e.isPlayer).length === 0;\n  else if (lvl.goal === 'reach_x')  done = S.playerRef && S.playerRef.x >= (lvl.goalTarget || W - 60);\n  else if (lvl.goal === 'reach_y')  done = S.playerRef && S.playerRef.y <= (lvl.goalTarget || 40);\n  else if (lvl.goal === 'survive')  done = S.tSurv >= (lvl.goalTarget || 30);\n  else if (lvl.goal === 'score')    done = S.score >= (lvl.goalTarget || 500);\n  if (done) return levelClear();\n}\n\nfunction aabb(a, b) { return Math.abs(a.x - b.x) < (a.w + b.w) / 2 && Math.abs(a.y - b.y) < (a.h + b.h) / 2; }\nfunction hit(e) {\n  e.hp--;\n  if (e.hp <= 0) { e.dead = true; S.score += e.scoreOnKill || 0; scoreEl.textContent = String(S.score); sfx(SPEC.audio.on_kill); }\n  else { sfx(SPEC.audio.on_hit); }\n}\n\n// Render\nfunction render() {\n  ctx.fillStyle = SPEC.palette.bg; ctx.fillRect(0, 0, W, H);\n  if (SPEC.world.groundY !== null) {\n    ctx.fillStyle = SPEC.palette.ground || '#3d6e26';\n    ctx.fillRect(0, SPEC.world.groundY, W, H - SPEC.world.groundY);\n  }\n  if (S.state === 'PLAY' && SPEC.input === 'slingshot' && IN.mouse.down && S.playerRef) {\n    ctx.strokeStyle = SPEC.palette.primary; ctx.lineWidth = 3;\n    ctx.beginPath(); ctx.moveTo(S.playerRef.x, S.playerRef.y - 40); ctx.lineTo(IN.mouse.x, IN.mouse.y); ctx.stroke();\n  }\n  for (const e of S.alive) {\n    const img = SPRITE_IMGS[e.sprite];\n    if (img && img.complete) ctx.drawImage(img, e.x - e.w/2, e.y - e.h/2, e.w, e.h);\n    else { ctx.fillStyle = SPEC.palette.primary; ctx.fillRect(e.x - e.w/2, e.y - e.h/2, e.w, e.h); }\n  }\n}\n\n// Loop\nfunction frame(t) {\n  const dt = Math.min(0.05, (t - S.tPrev) / 1000); S.tPrev = t;\n  if (S.state === 'PLAY') update(dt);\n  render();\n  requestAnimationFrame(frame);\n}\n\n// State transitions\nfunction showOverlay(el) { document.querySelectorAll('.overlay').forEach(o => o.classList.remove('show')); if (el) el.classList.add('show'); }\nfunction startGame() { S.state = 'PLAY'; S.score = 0; S.level = 0; S.lives = 3; scoreEl.textContent = '0'; livesEl.textContent = '3'; loadLevel(0); showOverlay(null); }\nfunction nextLevel() { S.level++; if (S.level >= SPEC.levels.length) { S.state = 'MENU'; showOverlay(overStart); return; } loadLevel(S.level); S.state = 'PLAY'; showOverlay(null); }\nfunction levelClear() { S.state = 'CLEAR'; clearScoreEl.textContent = String(S.score); melody(SPEC.audio.on_level_clear); showOverlay(overClear); }\nfunction gameOver() { S.state = 'OVER'; finalScoreEl.textContent = String(S.score); sfx(SPEC.audio.on_gameover); showOverlay(overOver); }\n\ndocument.getElementById('btn-start').addEventListener('click', startGame);\ndocument.getElementById('btn-restart').addEventListener('click', startGame);\ndocument.getElementById('btn-next').addEventListener('click', nextLevel);\n\n// Boot\nshowOverlay(overStart);\nrequestAnimationFrame(frame);\n\n</script>\n</body>\n</html>",
        "appKind": "game",
        "siteHtml": "<!doctype html>\n<html lang=\"en\">\n<head>\n<meta charset=\"utf-8\">\n<meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\n<title>Build a classic, polished 2D Snake game </title>\n<style>\n:root {\n  --bg: #0a1a3d;\n  --ground: #3d6e26;\n  --primary: #ff6b6b;\n  --secondary: #22d3ee;\n  --danger: #ffd166;\n  --text: #f2f4fa;\n}\n* { box-sizing: border-box; margin: 0; padding: 0; }\nhtml, body { height: 100%; overflow: hidden; background: var(--bg); color: var(--text); font-family: 'Trebuchet MS', -apple-system, sans-serif; }\n#stage { position: relative; width: 100vw; height: 100vh; display: flex; align-items: center; justify-content: center; }\ncanvas { max-width: 100%; max-height: 100%; background: var(--bg); border: 4px solid rgba(255,255,255,0.08); border-radius: 12px; box-shadow: 0 20px 60px rgba(0,0,0,0.45); touch-action: none; display: block; }\n.hud { position: absolute; top: 24px; left: 24px; right: 24px; display: flex; justify-content: space-between; font-size: 22px; font-weight: bold; color: var(--text); text-shadow: 2px 2px 0 rgba(0,0,0,0.5); pointer-events: none; z-index: 10; }\n.hud .score { color: var(--danger); }\n.hud .level { color: var(--secondary); }\n.overlay { position: absolute; inset: 0; background: rgba(0,0,0,0.72); display: none; align-items: center; justify-content: center; flex-direction: column; z-index: 20; color: var(--text); text-align: center; padding: 20px; }\n.overlay.show { display: flex; }\n.overlay h1 { font-size: clamp(2rem, 5vw, 3.6rem); margin-bottom: 12px; color: var(--primary); text-shadow: 3px 3px 0 rgba(0,0,0,0.5); }\n.overlay p { font-size: 1.25rem; margin-bottom: 24px; max-width: 640px; opacity: 0.9; }\n.btn { background: var(--primary); color: var(--text); border: 4px solid rgba(255,255,255,0.9); border-radius: 12px; padding: 14px 36px; font-size: 1.4rem; font-weight: bold; cursor: pointer; transition: transform 120ms; }\n.btn:hover { transform: scale(1.06); }\n.btn:active { transform: scale(0.94); }\n.credit { position: absolute; bottom: 12px; right: 16px; font-size: 12px; opacity: 0.4; }\n</style>\n</head>\n<body>\n<div id=\"stage\">\n  <canvas id=\"game\"></canvas>\n  <div class=\"hud\">\n    <span class=\"score\">Score: <span id=\"score-val\">0</span></span>\n    <span class=\"level\">Level: <span id=\"level-val\">1</span></span>\n    <span class=\"lives\">Lives: <span id=\"lives-val\">3</span></span>\n  </div>\n  <div class=\"overlay show\" id=\"overlay-start\">\n    <h1>Build a classic, polished 2D Snake game </h1>\n    <p>Click enemies to score. Don&#39;t miss.</p>\n    <button class=\"btn\" id=\"btn-start\">PLAY</button>\n  </div>\n  <div class=\"overlay\" id=\"overlay-over\">\n    <h1>Game Over</h1>\n    <p>Final score: <span id=\"final-score\">0</span></p>\n    <button class=\"btn\" id=\"btn-restart\">PLAY AGAIN</button>\n  </div>\n  <div class=\"overlay\" id=\"overlay-clear\">\n    <h1>Level Complete!</h1>\n    <p>Score: <span id=\"clear-score\">0</span></p>\n    <button class=\"btn\" id=\"btn-next\">NEXT LEVEL</button>\n  </div>\n  <span class=\"credit\">Built on Own1 · Qiki</span>\n</div>\n<script>\nconst SPEC = {\"title\":\"Build a classic, polished 2D Snake game \",\"tagline\":\"Click enemies to score. Don't miss.\",\"palette\":{\"bg\":\"#0a1a3d\",\"ground\":\"#3d6e26\",\"primary\":\"#ff6b6b\",\"secondary\":\"#22d3ee\",\"danger\":\"#ffd166\",\"text\":\"#f2f4fa\"},\"world\":{\"stageW\":960,\"stageH\":640,\"gravity\":0,\"friction\":0.985,\"airResistance\":0.995,\"groundY\":560},\"input\":\"click\",\"sprites\":{\"target\":{\"svg\":\"<svg xmlns=\\\"http://www.w3.org/2000/svg\\\" viewBox=\\\"0 0 100 100\\\"><circle cx=\\\"50\\\" cy=\\\"50\\\" r=\\\"40\\\" fill=\\\"#ff6b6b\\\" stroke=\\\"#8b1a1a\\\" stroke-width=\\\"4\\\"/><circle cx=\\\"50\\\" cy=\\\"50\\\" r=\\\"24\\\" fill=\\\"#f2f4fa\\\"/><circle cx=\\\"50\\\" cy=\\\"50\\\" r=\\\"10\\\" fill=\\\"#ff6b6b\\\"/></svg>\",\"w\":60,\"h\":60},\"hazard\":{\"svg\":\"<svg xmlns=\\\"http://www.w3.org/2000/svg\\\" viewBox=\\\"0 0 100 100\\\"><polygon points=\\\"50,10 90,90 10,90\\\" fill=\\\"#ffd166\\\" stroke=\\\"#8b6600\\\" stroke-width=\\\"4\\\"/><text x=\\\"50\\\" y=\\\"70\\\" font-size=\\\"42\\\" text-anchor=\\\"middle\\\" fill=\\\"#111\\\" font-weight=\\\"bold\\\">!</text></svg>\",\"w\":60,\"h\":60},\"crosshair\":{\"svg\":\"<svg xmlns=\\\"http://www.w3.org/2000/svg\\\" viewBox=\\\"0 0 100 100\\\"><circle cx=\\\"50\\\" cy=\\\"50\\\" r=\\\"30\\\" fill=\\\"none\\\" stroke=\\\"#22d3ee\\\" stroke-width=\\\"4\\\"/><line x1=\\\"50\\\" y1=\\\"10\\\" x2=\\\"50\\\" y2=\\\"30\\\" stroke=\\\"#22d3ee\\\" stroke-width=\\\"4\\\"/><line x1=\\\"50\\\" y1=\\\"70\\\" x2=\\\"50\\\" y2=\\\"90\\\" stroke=\\\"#22d3ee\\\" stroke-width=\\\"4\\\"/><line x1=\\\"10\\\" y1=\\\"50\\\" x2=\\\"30\\\" y2=\\\"50\\\" stroke=\\\"#22d3ee\\\" stroke-width=\\\"4\\\"/><line x1=\\\"70\\\" y1=\\\"50\\\" x2=\\\"90\\\" y2=\\\"50\\\" stroke=\\\"#22d3ee\\\" stroke-width=\\\"4\\\"/></svg>\",\"w\":40,\"h\":40}},\"entities\":[{\"name\":\"crosshair\",\"sprite\":\"crosshair\",\"behavior\":\"topdown\",\"hp\":999,\"scoreOnKill\":0,\"isPlayer\":true,\"speed\":0},{\"name\":\"target\",\"sprite\":\"target\",\"behavior\":\"patrol\",\"hp\":1,\"scoreOnKill\":100,\"speed\":120},{\"name\":\"hazard\",\"sprite\":\"hazard\",\"behavior\":\"patrol\",\"hp\":1,\"scoreOnKill\":-50,\"speed\":180}],\"levels\":[{\"n\":1,\"goal\":\"score\",\"goalTarget\":300,\"spawns\":[{\"entity\":\"crosshair\",\"x\":480,\"y\":320},{\"entity\":\"target\",\"x\":200,\"y\":200,\"vx\":120},{\"entity\":\"target\",\"x\":600,\"y\":350,\"vx\":-120},{\"entity\":\"target\",\"x\":780,\"y\":150,\"vx\":90}]},{\"n\":2,\"goal\":\"score\",\"goalTarget\":600,\"spawns\":[{\"entity\":\"crosshair\",\"x\":480,\"y\":320},{\"entity\":\"target\",\"x\":150,\"y\":180,\"vx\":150},{\"entity\":\"target\",\"x\":500,\"y\":300,\"vx\":-150},{\"entity\":\"target\",\"x\":800,\"y\":220,\"vx\":120},{\"entity\":\"hazard\",\"x\":400,\"y\":500,\"vx\":180}]},{\"n\":3,\"goal\":\"score\",\"goalTarget\":1000,\"spawns\":[{\"entity\":\"crosshair\",\"x\":480,\"y\":320},{\"entity\":\"target\",\"x\":100,\"y\":150,\"vx\":200},{\"entity\":\"target\",\"x\":500,\"y\":250,\"vx\":-200},{\"entity\":\"target\",\"x\":800,\"y\":350,\"vx\":160},{\"entity\":\"target\",\"x\":300,\"y\":450,\"vx\":-160},{\"entity\":\"hazard\",\"x\":300,\"y\":500,\"vx\":220},{\"entity\":\"hazard\",\"x\":700,\"y\":100,\"vx\":-220}]}],\"audio\":{\"on_action\":[660,0.06,\"square\"],\"on_hit\":[220,0.08,\"sawtooth\"],\"on_kill\":[880,0.12,\"triangle\"],\"on_level_clear\":[[523,0.12,\"sine\"],[659,0.12,\"sine\"],[784,0.24,\"sine\"]],\"on_gameover\":[110,0.4,\"sawtooth\"]}};\n\n'use strict';\nconst canvas = document.getElementById('game');\nconst ctx = canvas.getContext('2d');\nconst W = SPEC.world.stageW, H = SPEC.world.stageH;\ncanvas.width = W; canvas.height = H;\n\n// LLM-spec normalization. Small models often forget groundY when the input\n// mode is slingshot/platformer — with gravity > 0 and no ground, the player\n// falls off-screen and the canvas looks empty. Auto-derive a floor.\nif ((SPEC.world.groundY === null || SPEC.world.groundY === undefined) && SPEC.world.gravity > 0) {\n  SPEC.world.groundY = Math.floor(H * 0.85);\n}\nconst scoreEl = document.getElementById('score-val');\nconst levelEl = document.getElementById('level-val');\nconst livesEl = document.getElementById('lives-val');\nconst overStart = document.getElementById('overlay-start');\nconst overOver  = document.getElementById('overlay-over');\nconst overClear = document.getElementById('overlay-clear');\nconst finalScoreEl = document.getElementById('final-score');\nconst clearScoreEl = document.getElementById('clear-score');\n\n// Preload sprites — decode inline SVG to <img> for fast canvas draws.\n// LLM output normalization: our schema wants {svg, w, h} objects but smaller\n// models often emit plain SVG strings and skip the sprite dimensions. Accept\n// both shapes here so the game renders no matter which way the model composed\n// the spec.\nconst SPRITE_IMGS = {}, SPRITE_META = {};\nfor (const [name, raw] of Object.entries(SPEC.sprites)) {\n  const isString = typeof raw === 'string';\n  const svg = isString ? raw : (raw && raw.svg) || '';\n  const w = isString ? 40 : Number(raw && raw.w) || 40;\n  const h = isString ? 40 : Number(raw && raw.h) || 40;\n  if (!svg) continue;\n  const img = new Image();\n  img.src = 'data:image/svg+xml;utf8,' + encodeURIComponent(svg);\n  SPRITE_IMGS[name] = img;\n  SPRITE_META[name] = { w, h };\n  // Backfill the spec so downstream reads via SPEC.sprites[<name>].w/h still work.\n  SPEC.sprites[name] = { svg, w, h };\n}\n\n// AudioContext + tiny synth\nlet AC = null;\nfunction sfx(tone) {\n  if (!tone) return;\n  if (!AC) AC = new (window.AudioContext || window.webkitAudioContext)();\n  const [freq, dur, wave] = tone;\n  const osc = AC.createOscillator(); const gain = AC.createGain();\n  osc.type = wave || 'sine'; osc.frequency.value = freq;\n  osc.connect(gain); gain.connect(AC.destination);\n  gain.gain.setValueAtTime(0.18, AC.currentTime);\n  gain.gain.exponentialRampToValueAtTime(0.001, AC.currentTime + dur);\n  osc.start(); osc.stop(AC.currentTime + dur);\n}\nfunction melody(tones) { if (!tones) return; let t = 0; for (const tone of tones) { setTimeout(() => sfx(tone), t*1000); t += tone[1]; } }\n\n// Game state\nconst S = { state: 'MENU', score: 0, level: 0, lives: 3, tPrev: 0, tSurv: 0, alive: [], playerRef: null };\n\nfunction findEntitySpec(name) { return SPEC.entities.find(e => e.name === name) || null; }\n\nfunction spawn(sp) {\n  const espec = findEntitySpec(sp.entity);\n  if (!espec) return null;\n  const sprite = SPEC.sprites[espec.sprite] || Object.values(SPEC.sprites)[0];\n  const w = sprite ? sprite.w : 40, h = sprite ? sprite.h : 40;\n  const obj = {\n    name: espec.name, sprite: espec.sprite, behavior: espec.behavior,\n    hp: espec.hp, scoreOnKill: espec.scoreOnKill, isPlayer: !!espec.isPlayer,\n    speed: espec.speed || 0,\n    x: sp.x, y: sp.y, vx: sp.vx || 0, vy: sp.vy || 0,\n    w, h, dead: false, patrolLeft: sp.x - 120, patrolRight: sp.x + 120,\n    patrolDir: (sp.vx || espec.speed || 100) > 0 ? 1 : -1,\n    orbitCX: sp.x, orbitCY: sp.y, orbitR: 80, orbitTheta: 0,\n  };\n  if (obj.isPlayer) S.playerRef = obj;\n  return obj;\n}\n\nfunction loadLevel(idx) {\n  const lvl = SPEC.levels[idx];\n  if (!lvl) return null;\n  S.alive = lvl.spawns.map(spawn).filter(Boolean);\n  S.tSurv = 0;\n  S.playerRef = S.alive.find(e => e.isPlayer) || null;\n  levelEl.textContent = String(lvl.n);\n  return lvl;\n}\n\n// Input state\nconst IN = { keys: {}, mouse: {x: W/2, y: H/2, down: false, downX: 0, downY: 0} };\nwindow.addEventListener('keydown', e => { IN.keys[e.key.toLowerCase()] = true; if (S.state === 'PLAY' && SPEC.input === 'space' && (e.key === ' ' || e.key === 'ArrowUp')) { flapImpulse(); sfx(SPEC.audio.on_action); e.preventDefault(); } });\nwindow.addEventListener('keyup',   e => { IN.keys[e.key.toLowerCase()] = false; });\nfunction ptFromEvent(e) {\n  const r = canvas.getBoundingClientRect();\n  const t = e.touches ? e.touches[0] : (e.changedTouches ? e.changedTouches[0] : e);\n  return { x: (t.clientX - r.left) * (W / r.width), y: (t.clientY - r.top) * (H / r.height) };\n}\nfunction onDown(e) {\n  const p = ptFromEvent(e); IN.mouse.x = p.x; IN.mouse.y = p.y; IN.mouse.down = true; IN.mouse.downX = p.x; IN.mouse.downY = p.y;\n  if (S.state !== 'PLAY') return;\n  if (SPEC.input === 'click') { fireClick(p.x, p.y); sfx(SPEC.audio.on_action); }\n  if (SPEC.input === 'space') { flapImpulse(); sfx(SPEC.audio.on_action); }\n  e.preventDefault();\n}\nfunction onMove(e) { const p = ptFromEvent(e); IN.mouse.x = p.x; IN.mouse.y = p.y; e.preventDefault(); }\nfunction onUp(e) {\n  const p = ptFromEvent(e);\n  if (S.state === 'PLAY' && IN.mouse.down) {\n    if (SPEC.input === 'slingshot') fireSlingshot(p.x, p.y);\n    if (SPEC.input === 'swipe')     fireSwipe(p.x, p.y);\n  }\n  IN.mouse.down = false; e.preventDefault();\n}\ncanvas.addEventListener('mousedown', onDown);\ncanvas.addEventListener('mousemove', onMove);\ncanvas.addEventListener('mouseup', onUp);\ncanvas.addEventListener('touchstart', onDown, {passive: false});\ncanvas.addEventListener('touchmove', onMove, {passive: false});\ncanvas.addEventListener('touchend', onUp, {passive: false});\n\n// Input actions\nfunction fireClick(x, y) {\n  for (const e of S.alive) {\n    if (e.isPlayer) continue;\n    if (Math.abs(e.x - x) < e.w/2 && Math.abs(e.y - y) < e.h/2) {\n      hit(e); return;\n    }\n  }\n}\nfunction fireSlingshot(rx, ry) {\n  const player = S.playerRef; if (!player) return;\n  const dx = player.x - rx, dy = player.y - ry;\n  const projSpec = SPEC.entities.find(e => e.behavior === 'projectile') || SPEC.entities.find(e => !e.isPlayer);\n  if (!projSpec) return;\n  const proj = spawn({entity: projSpec.name, x: player.x, y: player.y - 40, vx: dx * 3, vy: dy * 3});\n  proj.behavior = 'projectile'; proj.isProjectile = true;\n  S.alive.push(proj);\n}\nfunction fireSwipe(x, y) {\n  const player = S.playerRef; if (!player) return;\n  const dx = x - IN.mouse.downX, dy = y - IN.mouse.downY;\n  const mag = Math.hypot(dx, dy) || 1;\n  player.vx = (dx / mag) * (player.speed || 200);\n  player.vy = (dy / mag) * (player.speed || 200);\n}\nfunction flapImpulse() {\n  const player = S.playerRef; if (!player) return;\n  player.vy = -420;\n}\n\n// Physics + update\nfunction update(dt) {\n  const gr = SPEC.world.gravity, gy = SPEC.world.groundY;\n  for (const e of S.alive) {\n    if (e.dead) continue;\n    // player-driven arrows/wasd movement\n    if (e.isPlayer && (SPEC.input === 'arrows' || SPEC.input === 'wasd')) {\n      const leftKey = SPEC.input === 'wasd' ? 'a' : 'arrowleft';\n      const rightKey = SPEC.input === 'wasd' ? 'd' : 'arrowright';\n      const upKey    = SPEC.input === 'wasd' ? 'w' : 'arrowup';\n      const dnKey    = SPEC.input === 'wasd' ? 's' : 'arrowdown';\n      if (IN.keys[leftKey])  e.vx = -(e.speed || 240);\n      else if (IN.keys[rightKey]) e.vx = (e.speed || 240);\n      else e.vx *= Math.pow(SPEC.world.friction, dt);\n      if (SPEC.world.gravity > 0) {\n        if (IN.keys[upKey] && gy !== null && e.y + e.h/2 >= gy - 2) e.vy = -520;\n      } else {\n        if (IN.keys[upKey])  e.vy = -(e.speed || 240);\n        else if (IN.keys[dnKey]) e.vy = (e.speed || 240);\n        else e.vy *= Math.pow(SPEC.world.friction, dt);\n      }\n    }\n    // behavior-specific update\n    switch (e.behavior) {\n      case 'gravity':    e.vy += gr * dt; break;\n      case 'projectile': e.vy += gr * dt; e.vx *= Math.pow(SPEC.world.airResistance, dt); e.vy *= Math.pow(SPEC.world.airResistance, dt); break;\n      case 'flap':       e.vy += gr * dt; break;\n      case 'topdown':    e.vx *= Math.pow(SPEC.world.friction, dt); e.vy *= Math.pow(SPEC.world.friction, dt); break;\n      case 'patrol': {\n        e.vx = e.patrolDir * (e.speed || 120);\n        if (e.x < e.patrolLeft)  e.patrolDir = 1;\n        if (e.x > e.patrolRight) e.patrolDir = -1;\n        break;\n      }\n      case 'homing': {\n        const p = S.playerRef; if (p) {\n          const dx = p.x - e.x, dy = p.y - e.y; const m = Math.hypot(dx, dy) || 1;\n          e.vx = (dx / m) * (e.speed || 100); e.vy = (dy / m) * (e.speed || 100);\n        }\n        break;\n      }\n      case 'orbit': {\n        e.orbitTheta += (e.speed || 60) / e.orbitR * dt;\n        e.x = e.orbitCX + Math.cos(e.orbitTheta) * e.orbitR;\n        e.y = e.orbitCY + Math.sin(e.orbitTheta) * e.orbitR;\n        continue;\n      }\n      case 'static': continue;\n    }\n    e.x += e.vx * dt; e.y += e.vy * dt;\n    // ground collision\n    if (gy !== null && e.y + e.h/2 > gy) {\n      e.y = gy - e.h/2; if (e.behavior === 'projectile' && Math.abs(e.vy) < 60) { e.dead = true; }\n      e.vy = -e.vy * 0.5;\n    }\n    // wall wrap for topdown/patrol; kill projectile on edge\n    if (e.behavior === 'projectile' && (e.x < -50 || e.x > W + 50 || e.y > H + 50)) e.dead = true;\n    if (e.behavior === 'topdown' || e.behavior === 'flap') {\n      e.x = Math.max(e.w/2, Math.min(W - e.w/2, e.x));\n      e.y = Math.max(e.h/2, Math.min(H - e.h/2, e.y));\n    }\n  }\n  // collision: projectile-vs-anyone, player-vs-nonPlayer for topdown/flap\n  for (const a of S.alive) {\n    if (a.dead) continue;\n    if (a.isProjectile) {\n      for (const b of S.alive) {\n        if (b === a || b.dead || b.isPlayer) continue;\n        if (aabb(a, b)) { hit(b); a.dead = true; break; }\n      }\n    }\n    if (a.isPlayer && (SPEC.input === 'arrows' || SPEC.input === 'wasd' || SPEC.input === 'space' || SPEC.input === 'swipe')) {\n      for (const b of S.alive) {\n        if (b === a || b.dead || b.isPlayer) continue;\n        if (aabb(a, b)) {\n          if (b.scoreOnKill > 0) { hit(b); }\n          else { S.lives--; b.dead = true; livesEl.textContent = String(S.lives); sfx(SPEC.audio.on_hit); if (S.lives <= 0) return gameOver(); }\n        }\n      }\n    }\n  }\n  S.alive = S.alive.filter(e => !e.dead);\n  // goal check\n  const lvl = SPEC.levels[S.level]; if (!lvl) return;\n  S.tSurv += dt;\n  let done = false;\n  if (lvl.goal === 'kill_all')      done = S.alive.filter(e => !e.isPlayer).length === 0;\n  else if (lvl.goal === 'reach_x')  done = S.playerRef && S.playerRef.x >= (lvl.goalTarget || W - 60);\n  else if (lvl.goal === 'reach_y')  done = S.playerRef && S.playerRef.y <= (lvl.goalTarget || 40);\n  else if (lvl.goal === 'survive')  done = S.tSurv >= (lvl.goalTarget || 30);\n  else if (lvl.goal === 'score')    done = S.score >= (lvl.goalTarget || 500);\n  if (done) return levelClear();\n}\n\nfunction aabb(a, b) { return Math.abs(a.x - b.x) < (a.w + b.w) / 2 && Math.abs(a.y - b.y) < (a.h + b.h) / 2; }\nfunction hit(e) {\n  e.hp--;\n  if (e.hp <= 0) { e.dead = true; S.score += e.scoreOnKill || 0; scoreEl.textContent = String(S.score); sfx(SPEC.audio.on_kill); }\n  else { sfx(SPEC.audio.on_hit); }\n}\n\n// Render\nfunction render() {\n  ctx.fillStyle = SPEC.palette.bg; ctx.fillRect(0, 0, W, H);\n  if (SPEC.world.groundY !== null) {\n    ctx.fillStyle = SPEC.palette.ground || '#3d6e26';\n    ctx.fillRect(0, SPEC.world.groundY, W, H - SPEC.world.groundY);\n  }\n  if (S.state === 'PLAY' && SPEC.input === 'slingshot' && IN.mouse.down && S.playerRef) {\n    ctx.strokeStyle = SPEC.palette.primary; ctx.lineWidth = 3;\n    ctx.beginPath(); ctx.moveTo(S.playerRef.x, S.playerRef.y - 40); ctx.lineTo(IN.mouse.x, IN.mouse.y); ctx.stroke();\n  }\n  for (const e of S.alive) {\n    const img = SPRITE_IMGS[e.sprite];\n    if (img && img.complete) ctx.drawImage(img, e.x - e.w/2, e.y - e.h/2, e.w, e.h);\n    else { ctx.fillStyle = SPEC.palette.primary; ctx.fillRect(e.x - e.w/2, e.y - e.h/2, e.w, e.h); }\n  }\n}\n\n// Loop\nfunction frame(t) {\n  const dt = Math.min(0.05, (t - S.tPrev) / 1000); S.tPrev = t;\n  if (S.state === 'PLAY') update(dt);\n  render();\n  requestAnimationFrame(frame);\n}\n\n// State transitions\nfunction showOverlay(el) { document.querySelectorAll('.overlay').forEach(o => o.classList.remove('show')); if (el) el.classList.add('show'); }\nfunction startGame() { S.state = 'PLAY'; S.score = 0; S.level = 0; S.lives = 3; scoreEl.textContent = '0'; livesEl.textContent = '3'; loadLevel(0); showOverlay(null); }\nfunction nextLevel() { S.level++; if (S.level >= SPEC.levels.length) { S.state = 'MENU'; showOverlay(overStart); return; } loadLevel(S.level); S.state = 'PLAY'; showOverlay(null); }\nfunction levelClear() { S.state = 'CLEAR'; clearScoreEl.textContent = String(S.score); melody(SPEC.audio.on_level_clear); showOverlay(overClear); }\nfunction gameOver() { S.state = 'OVER'; finalScoreEl.textContent = String(S.score); sfx(SPEC.audio.on_gameover); showOverlay(overOver); }\n\ndocument.getElementById('btn-start').addEventListener('click', startGame);\ndocument.getElementById('btn-restart').addEventListener('click', startGame);\ndocument.getElementById('btn-next').addEventListener('click', nextLevel);\n\n// Boot\nshowOverlay(overStart);\nrequestAnimationFrame(frame);\n\n</script>\n</body>\n</html>"
      },
      "finishedAt": 1784543730886,
      "llmCalls": [
        {
          "upstream": "http://127.0.0.1:8080",
          "modelRequested": "txt2txt",
          "temperature": 0.7,
          "maxTokens": 3000,
          "timeoutMs": 600000,
          "startedAt": 1784543685550,
          "finishedAt": 1784543730886,
          "ok": false,
          "promptChars": 3302,
          "responseChars": 2444,
          "modelServed": "w8fajwih8isx2rl94rb8ggc1knmnf8x9-Qwen3.6-35B-A3B-MTP-GGUF",
          "error": "LLM returned non-JSON when jsonMode=true: {\n  \"title\": \"Neon Serpent\",\n  \"tagline\",\n  \"palette\": {\n    \"bg\": \"#050510\",\n    \"ground\": \"#1a1a2e\",\n    \"primary\": \"#00ff9d\",\n    \"secondary\": \"#ff00ff\",\n    \"danger\": \"#ff3333\",\n    \"text\": \"#ffffff\"\n  },\n  \"world\": {\n    \"stageW\": 600,\n    \"stageH\": 600,\n    \"gravity\": 0,\n    \"friction\": 1.0,\n    \"airResistance\": 1.0,\n    \"groundY\": null\n  },\n  \"input\": \"arrows\",\n  \"sprites\": {\n    \"head\": \"<"
        }
      ]
    }
  },
  "startedAt": 1784543685543,
  "fingerprint": {
    "ownAiUrl": "http://127.0.0.1:8080",
    "harnessHost": "127.0.0.1",
    "modelServed": "qwen-image",
    "nodeVersion": "v24.15.0",
    "capturedAt": 1784543685543
  },
  "finishedAt": 1784543730889,
  "scores": {
    "perStep": {
      "s1-bbbav": {
        "validity": 2,
        "fidelity": 2,
        "structure": 2,
        "depth": 2,
        "cleanliness": 2,
        "total": 10,
        "notes": []
      }
    },
    "total": 10,
    "perKind": {
      "code-app": 10
    },
    "axes": [
      "validity",
      "fidelity",
      "structure",
      "depth",
      "cleanliness"
    ]
  }
}