← 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'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
→ 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'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'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'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"
]
}
}