← Race the Sun results

Race the Sun

thinkingcap qwen3.6 27b · composer

Initial view of Race the Sun

Browser check: Artifact runtime error. This is a technical check, not a quality review.

Reviews & generation details

Reviews

No human review yet. Unreviewed does not mean unsuccessful.

Generation context

Imported historical artifact. Its exact original instructions and lineage are not known.

Generation prompt · exact text unavailable

See the current prompt for reference. It may differ from the instructions used to generate this artifact.

Inspect original source 40,211 bytes · SHA-256 5375fc17e1fe
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Race the Sun — Solar Runner</title>
<style>
/* ── css: layout ── */
* { margin: 0; padding: 0; box-sizing: border-box; }
html, body { width: 100%; height: 100%; overflow: hidden; background: #0a0a12; font-family: 'Segoe UI', system-ui, sans-serif; }
#game { display: block; width: 100vw; height: 100vh; }

/* HUD */
#hud { position: fixed; top: 0; left: 0; right: 0; pointer-events: none; z-index: 10; padding: 16px 24px; display: flex; align-items: center; gap: 20px; }
#score-line { color: #fff; font-size: 18px; font-weight: 700; text-shadow: 0 0 8px rgba(255,160,50,.6); letter-spacing: .5px; white-space: nowrap; }
#multiplier { color: #ffd43b; font-size: 22px; font-weight: 900; text-shadow: 0 0 12px rgba(255,212,59,.7); min-width: 50px; }
#energy-bar-wrap { flex: 1; max-width: 260px; height: 14px; background: rgba(0,0,0,.5); border-radius: 7px; overflow: hidden; border: 1px solid rgba(255,255,255,.15); }
#energy-bar { width: 100%; height: 100%; background: linear-gradient(90deg, #ff6b35, #ffd43b, #7bed9f); border-radius: 7px; transition: width .15s ease-out; }
#speed-display { color: rgba(255,255,255,.7); font-size: 14px; font-weight: 600; min-width: 70px; text-align: right; white-space: nowrap; }

/* Title & Game Over screens */
#title-screen, #gameover-screen { position: fixed; inset: 0; display: flex; flex-direction: column; align-items: center; justify-content: center; z-index: 20; background: radial-gradient(ellipse at 50% 60%, rgba(30,15,40,.85), rgba(5,5,15,.97)); }
#title-screen h1 { font-size: clamp(36px, 7vw, 72px); color: #ffd43b; text-shadow: 0 0 40px rgba(255,180,50,.5), 0 0 80px rgba(255,100,20,.3); letter-spacing: 6px; font-weight: 900; animation: titlePulse 3s ease-in-out infinite; }
@keyframes titlePulse { 0%,100% { text-shadow: 0 0 40px rgba(255,180,50,.5), 0 0 80px rgba(255,100,20,.3); } 50% { text-shadow: 0 0 60px rgba(255,180,50,.7), 0 0 100px rgba(255,100,20,.5); } }
.subtitle { color: #ff9f43; font-size: clamp(16px, 3vw, 28px); margin-top: 4px; letter-spacing: 4px; text-transform: uppercase; }
.controls { color: rgba(255,255,255,.5); font-size: 14px; margin-top: 24px; letter-spacing: 1px; }
#high-score-line { color: #ffd43b; font-size: 16px; margin-top: 12px; opacity: .8; }

button { pointer-events: all; margin-top: 32px; padding: 14px 48px; font-size: 18px; font-weight: 700; letter-spacing: 3px; border: none; border-radius: 6px; cursor: pointer; background: linear-gradient(135deg, #ff6b35, #ee5a24); color: #fff; box-shadow: 0 4px 24px rgba(255,107,53,.4); transition: transform .1s, box-shadow .1s; }
button:hover { transform: scale(1.05); box-shadow: 0 6px 32px rgba(255,107,53,.6); }
button:active { transform: scale(.97); }

#gameover-screen h1 { font-size: clamp(32px, 6vw, 64px); color: #ff4757; text-shadow: 0 0 40px rgba(255,71,87,.5); letter-spacing: 4px; }
#gameover-screen p { color: rgba(255,255,255,.8); font-size: 20px; margin-top: 8px; }

/* Sun indicator on HUD */
#sun-indicator { width: 16px; height: 16px; border-radius: 50%; background: #ffd43b; box-shadow: 0 0 8px rgba(255,212,59,.6); flex-shrink: 0; }
</style>
</head>
<body>
<canvas id="game"></canvas>
<div id="hud"><div id="sun-indicator"></div>
  <div id="score-line"><span id="dist">0m</span> · <span id="score">0</span></div>
  <div id="multiplier">×1.0</div>
  <div id="energy-bar-wrap"><div id="energy-bar"></div></div>
  <div id="speed-display">0 km/h</div>
</div>
<div id="title-screen">
  <h1>SOLAR RUNNER</h1>
  <p class="subtitle">Race the Sun</p>
  <p class="controls">WASD / Arrows — Move · Space — Boost · R — Restart</p>
  <div id="high-score-line"></div>
  <button id="start-btn">LAUNCH</button>
</div>
<div id="gameover-screen" style="display:none">
  <h1>SOLAR FLARE</h1>
  <p id="go-dist">Distance: 0m</p>
  <p id="go-score">Score: 0</p>
  <p id="go-highscore"></p>
  <button id="restart-btn">RELAUNCH (R)</button>
</div>
<script>
'use strict';

// ── const: CFG ──
// === CONSTANTS ===
const CFG = {
  // Player
  PLAYER_W: 28, PLAYER_H: 16, PLAYER_DEPTH: 40,
  MOVE_SPEED: 20, MOVE_EASE: 0.18, BANK_ANGLE: 0.35, PITCH_LEAN: 0.2,
  BOOST_SPEED_MULT: 1.6, BOOST_DRAIN: 0.7, BOOST_COOLDOWN: 0.4,
  
  // World
  WORLD_WIDTH: 800, LANE_COUNT: 5,
  SEGMENT_LENGTH: 80, DRAW_DISTANCE: 60,
  BASE_SPEED: 120, MAX_SPEED: 400, SPEED_RAMP: 0.35,
  
  // Sun & Energy
  SUN_START_Y: 0.35, SUN_SET_TIME: 100,
  ENERGY_MAX: 100, ENERGY_DRAIN: 0.6, SHADOW_DRAIN_MULT: 2.5,
  PICKUP_ENERGY: 18, SUN_RESCUE_AMOUNT: 0.03,
  
  // Obstacles
  OBSTACLE_SPAWN_RATE: 0.35, OBSTACLE_MIN_GAP: 3,
  NEAR_MISS_DIST: 42,
  
  // Scoring
  BASE_SCORE_PER_METER: 1, NEAR_MISS_BONUS: 50,
  
  // Visuals
  PARTICLE_MAX: 200, SHAKE_DECAY: 0.85,
};

// ── function: seededRandom ──
// === UTILITIES ===
function seededRandom(seed) {
  let s = seed;
  return function() {
    s = (s * 16807 + 0) % 2147483647;
    return (s - 1) / 2147483646;
  };
}

// ── function: lerp ──
function lerp(a, b, t) { return a + (b - a) * t; }

// ── function: clamp ──
function clamp(v, lo, hi) { return Math.max(lo, Math.min(hi, v)); }

// ── function: randRange ──
function randRange(lo, hi) { return lo + Math.random() * (hi - lo); }

// ── let: _rng ──
// Seeded RNG for world generation
let _rng = null;

// ── function: getRNG ──
function getRNG() { return _rng || (_rng = seededRandom(Date.now() & 0x7fffffff)); }

// ── let: _audioCtx ──
// === AUDIO SYSTEM ===
let _audioCtx = null;

// ── let: _droneOsc ──
let _droneOsc = null;

// ── let: _droneGain ──
let _droneGain = null;

// ── function: initAudio ──
function initAudio() {
  if (_audioCtx) return;
  try {
    _audioCtx = new (window.AudioContext || window.webkitAudioContext)();
    _droneGain = _audioCtx.createGain();
    _droneGain.gain.value = 0.03;
    _droneGain.connect(_audioCtx.destination);
    _droneOsc = _audioCtx.createOscillator();
    _droneOsc.type = 'sawtooth';
    _droneOsc.frequency.value = 55;
    const droneFilter = _audioCtx.createBiquadFilter();
    droneFilter.type = 'lowpass';
    droneFilter.frequency.value = 120;
    _droneOsc.connect(droneFilter);
    droneFilter.connect(_droneGain);
    _droneOsc.start();
  } catch(e) {}
}

// ── function: updateDrone ──
function updateDrone(speedRatio) {
  if (!_droneOsc || !_audioCtx) return;
  const t = _audioCtx.currentTime;
  _droneOsc.frequency.setTargetAtTime(50 + speedRatio * 40, t, 0.1);
  _droneGain.gain.setTargetAtTime(0.02 + speedRatio * 0.03, t, 0.1);
}

// ── function: playPickup ──
function playPickup(freq) {
  freq = freq || 880;
  if (!_audioCtx) return;
  const t = _audioCtx.currentTime;
  const o = _audioCtx.createOscillator();
  const g = _audioCtx.createGain();
  o.type = 'sine';
  o.frequency.setValueAtTime(freq, t);
  o.frequency.exponentialRampToValueAtTime(freq * 1.5, t + 0.08);
  g.gain.setValueAtTime(0.12, t);
  g.gain.exponentialRampToValueAtTime(0.001, t + 0.15);
  o.connect(g).connect(_audioCtx.destination);
  o.start(t);
  o.stop(t + 0.15);
}

// ── function: playCrash ──
function playCrash() {
  if (!_audioCtx) return;
  const t = _audioCtx.currentTime;
  for (let i = 0; i < 4; i++) {
    const o = _audioCtx.createOscillator();
    const g = _audioCtx.createGain();
    o.type = 'square';
    o.frequency.value = 80 + Math.random() * 200;
    g.gain.setValueAtTime(0.15, t);
    g.gain.exponentialRampToValueAtTime(0.001, t + 0.3);
    o.connect(g).connect(_audioCtx.destination);
    o.start(t);
    o.stop(t + 0.3);
  }
}

// ── function: playNearMiss ──
function playNearMiss() {
  if (!_audioCtx) return;
  const t = _audioCtx.currentTime;
  const o = _audioCtx.createOscillator();
  const g = _audioCtx.createGain();
  o.type = 'triangle';
  o.frequency.setValueAtTime(1200, t);
  o.frequency.exponentialRampToValueAtTime(600, t + 0.1);
  g.gain.setValueAtTime(0.08, t);
  g.gain.exponentialRampToValueAtTime(0.001, t + 0.12);
  o.connect(g).connect(_audioCtx.destination);
  o.start(t);
  o.stop(t + 0.12);
}

// ── function: playBoost ──
function playBoost() {
  if (!_audioCtx) return;
  const t = _audioCtx.currentTime;
  const o = _audioCtx.createOscillator();
  const g = _audioCtx.createGain();
  o.type = 'sawtooth';
  o.frequency.setValueAtTime(200, t);
  o.frequency.exponentialRampToValueAtTime(800, t + 0.15);
  g.gain.setValueAtTime(0.06, t);
  g.gain.exponentialRampToValueAtTime(0.001, t + 0.2);
  o.connect(g).connect(_audioCtx.destination);
  o.start(t);
  o.stop(t + 0.2);
}

// ── let: particles ──
// === PARTICLE SYSTEM ===
let particles = [];

// ── function: spawnParticle ──
function spawnParticle(x, y, vx, vy, life, color, size) {
  if (particles.length >= CFG.PARTICLE_MAX) return;
  particles.push({ x, y, vx, vy, life, maxLife: life, color, size });
}

// ── function: emitThrust ──
function emitThrust(cx, cy, angle) {
  for (let i = 0; i < 2; i++) {
    const spread = randRange(-0.3, 0.3);
    spawnParticle(
      cx + randRange(-4, 4), cy + 8,
      Math.sin(angle + spread) * -150 + randRange(-20, 20),
      Math.cos(angle + spread) * -150 + randRange(-20, 20),
      randRange(0.3, 0.6),
      i % 2 === 0 ? '#ff9f43' : '#ffd43b',
      randRange(2, 5)
    );
  }
}

// ── function: emitPickupBurst ──
function emitPickupBurst(x, y, color) {
  for (let i = 0; i < 16; i++) {
    const a = Math.random() * Math.PI * 2;
    const sp = randRange(40, 150);
    spawnParticle(x, y, Math.cos(a) * sp, Math.sin(a) * sp, randRange(0.3, 0.7), color, randRange(2, 6));
  }
}

// ── function: emitCrashBurst ──
function emitCrashBurst(x, y) {
  for (let i = 0; i < 40; i++) {
    const a = Math.random() * Math.PI * 2;
    const sp = randRange(50, 300);
    spawnParticle(x, y, Math.cos(a) * sp, Math.sin(a) * sp, randRange(0.4, 1.2),
      ['#ff4757', '#ff6b81', '#ffa502', '#fff'][Math.floor(Math.random() * 4)],
      randRange(3, 8));
  }
}

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

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

// ── let: player ──
// === PLAYER ===
let player = null;

// ── function: createPlayer ──
function createPlayer() {
  return {
    x: 0, y: 0,           // world position (x=strafe, y=pitch)
    targetX: 0, targetY: 0,
    bankAngle: 0, pitchLean: 0,
    energy: CFG.ENERGY_MAX,
    alive: true,
    invincibleTimer: 0,
  };
}

// ── function: updatePlayer ──
function updatePlayer(p, dt) {
  // Smooth movement with easing
  const ease = 1 - Math.pow(1 - CFG.MOVE_EASE, dt * 60);
  p.x += (p.targetX - p.x) * ease;
  p.y += (p.targetY - p.y) * ease;
  
  // Bank angle follows horizontal velocity
  const dx = p.targetX - p.x;
  const targetBank = clamp(dx * 0.015, -CFG.BANK_ANGLE, CFG.BANK_ANGLE);
  p.bankAngle += (targetBank - p.bankAngle) * ease * 2;
  
  // Pitch lean follows vertical velocity
  const dy = p.targetY - p.y;
  const targetPitch = clamp(dy * 0.01, -CFG.PITCH_LEAN, CFG.PITCH_LEAN);
  p.pitchLean += (targetPitch - p.pitchLean) * ease * 2;
  
  // Clamp position to world bounds
  const halfW = CFG.WORLD_WIDTH / 2 - 30;
  p.targetX = clamp(p.targetX, -halfW, halfW);
  p.targetY = clamp(p.targetY, -150, 100);
  
  // Invincibility timer
  if (p.invincibleTimer > 0) p.invincibleTimer -= dt;
}

// ── function: drawPlayer ──
function drawPlayer(ctx, cx, cy, bankAngle, pitchLean, flashAlpha) {
  ctx.save();
  ctx.translate(cx, cy);
  ctx.rotate(bankAngle);
  
  const w = CFG.PLAYER_W;
  const d = CFG.PLAYER_DEPTH;
  
  // Engine trail glow
  const engineGlow = 0.4 + Math.sin(Date.now() * 0.015) * 0.2;
  ctx.fillStyle = `rgba(255,180,50,${engineGlow})`;
  ctx.beginPath();
  ctx.arc(-w/3, d/2 + 4, 4, 0, Math.PI * 2);
  ctx.fill();
  ctx.beginPath();
  ctx.arc(w/3, d/2 + 4, 4, 0, Math.PI * 2);
  ctx.fill();
  
  // Shadow beneath craft
  ctx.fillStyle = 'rgba(0,0,0,0.25)';
  ctx.beginPath();
  ctx.ellipse(2, 16, w * 0.7, 5, 0, 0, Math.PI * 2);
  ctx.fill();
  
  // Main body - sleek fighter shape
  const grad = ctx.createLinearGradient(-w/2, -d/2, w/2, d/2);
  grad.addColorStop(0, '#e84393');
  grad.addColorStop(0.5, '#fd79a8');
  grad.addColorStop(1, '#ff6b35');
  
  ctx.fillStyle = grad;
  ctx.beginPath();
  // Nose point
  ctx.moveTo(0, -d/2);
  // Right wing tip (swept back)
  ctx.lineTo(w/2 + 6, d/4);
  // Right engine housing
  ctx.lineTo(w/3, d/2);
  // Tail notch
  ctx.lineTo(-w/8, d/2 + 3);
  // Left engine housing
  ctx.lineTo(-w/3, d/2);
  // Left wing tip (swept back)
  ctx.lineTo(-w/2 - 6, d/4);
  ctx.closePath();
  ctx.fill();
  
  // Body outline for definition
  ctx.strokeStyle = 'rgba(255,200,180,0.3)';
  ctx.lineWidth = 1;
  ctx.stroke();
  
  // Cockpit highlight
  const cockpitGrad = ctx.createLinearGradient(0, -d/2 + 4, 0, d/6);
  cockpitGrad.addColorStop(0, 'rgba(255,255,255,0.5)');
  cockpitGrad.addColorStop(1, 'rgba(255,255,255,0)');
  
  ctx.fillStyle = cockpitGrad;
  ctx.beginPath();
  ctx.moveTo(0, -d/2 + 6);
  ctx.lineTo(w/4, d/6);
  ctx.lineTo(-w/4, d/6);
  ctx.closePath();
  ctx.fill();
  
  // Wing accents
  ctx.strokeStyle = 'rgba(255,180,100,0.4)';
  ctx.lineWidth = 1;
  ctx.beginPath();
  ctx.moveTo(-w/3, d/6);
  ctx.lineTo(-w/2 - 4, d/4 + 2);
  ctx.stroke();
  ctx.beginPath();
  ctx.moveTo(w/3, d/6);
  ctx.lineTo(w/2 + 4, d/4 + 2);
  ctx.stroke();
  
  // Damage flash overlay
  if (flashAlpha > 0) {
    ctx.fillStyle = `rgba(255,100,100,${flashAlpha})`;
    ctx.fill();
  }
  
  ctx.restore();
}

// ── let: worldSegments ──
// === WORLD & OBSTACLES ===
let worldSegments = [];

// ── let: pickups ──
let pickups = [];

// ── let: distanceTraveled ──
let distanceTraveled = 0;

// ── let: gameTime ──
let gameTime = 0;

// ── const: OBS_TYPES ──
const OBS_TYPES = ['block', 'tall', 'wide', 'low'];

// ── const: PICKUP_TYPES ──
const PICKUP_TYPES = ['energy', 'score', 'multiplier'];

// ── function: resetWorld ──
function resetWorld() {
  worldSegments = [];
  pickups = [];
  distanceTraveled = 0;
  gameTime = 0;
  _rng = seededRandom(Date.now() & 0x7fffffff);
}

// ── function: spawnSegment ──
function spawnSegment(z) {
  const rng = getRNG();
  const seg = { z, obstacles: [], hasShadow: false };
  
  // Difficulty scales with time - more obstacles as game progresses
  const difficulty = clamp(gameTime / 120, 0, 1);
  const spawnChance = CFG.OBSTACLE_SPAWN_RATE + difficulty * 0.3;
  
  if (rng() < spawnChance) {
    // Ensure minimum gap between obstacle groups
    const lastSeg = worldSegments[worldSegments.length - 1];
    if (lastSeg && lastSeg.obstacles.length > 0 && 
        z - lastSeg.z < CFG.OBSTACLE_MIN_GAP * CFG.SEGMENT_LENGTH) {
      return; // Skip this segment to maintain gaps
    }
    
    const count = Math.floor(rng() * (2 + difficulty * 2)) + 1;
    for (let i = 0; i < count; i++) {
      const typeRoll = rng();
      let type, ow, oh;
      
      if (typeRoll < 0.35) {
        // Standard block
        type = 'block';
        ow = randRange(30, 60);
        oh = randRange(40, 80);
      } else if (typeRoll < 0.55) {
        // Tall pillar - creates shadow zones
        type = 'tall';
        ow = randRange(25, 45);
        oh = randRange(100, 160);
      } else if (typeRoll < 0.75) {
        // Wide barrier - harder to dodge
        type = 'wide';
        ow = randRange(80, 140);
        oh = randRange(30, 50);
      } else {
        // Low obstacle - must fly over
        type = 'low';
        ow = randRange(40, 70);
        oh = randRange(20, 35);
      }
      
      const ox = (rng() - 0.5) * CFG.WORLD_WIDTH * 0.8;
      const oy = -oh / 2 + randRange(-10, 20);
      
      seg.obstacles.push({ x: ox, y: oy, w: ow, h: oh, type });
      
      if (type === 'tall') seg.hasShadow = true;
    }
  }
  
  worldSegments.push(seg);
}

// ── function: spawnPickup ──
function spawnPickup(z) {
  const rng = getRNG();
  if (rng() > 0.3) return; // 30% chance per segment
  
  const typeRoll = rng();
  let type;
  if (typeRoll < 0.45) type = 'energy';
  else if (typeRoll < 0.8) type = 'score';
  else type = 'multiplier';
  
  const px = (rng() - 0.5) * CFG.WORLD_WIDTH * 0.7;
  const py = randRange(-80, 30);
  
  pickups.push({ x: px, y: py, z, type, collected: false, bobPhase: rng() * Math.PI * 2 });
}

// ── function: updateWorld ──
function updateWorld(dt, speed) {
  distanceTraveled += speed * dt;
  gameTime += dt;
  
  // Move segments toward player (z decreases)
  for (const seg of worldSegments) {
    seg.z -= speed * dt;
  }
  
  // Move pickups
  for (const p of pickups) {
    p.z -= speed * dt;
    p.bobPhase += dt * 3;
  }
  
  // Remove passed segments/pickups
  worldSegments = worldSegments.filter(s => s.z > -CFG.SEGMENT_LENGTH);
  pickups = pickups.filter(p => p.z > -50 && !p.collected);
  
  // Spawn new segments ahead
  const maxZ = worldSegments.length ? Math.max(...worldSegments.map(s => s.z)) : 0;
  const spawnAhead = CFG.DRAW_DISTANCE * CFG.SEGMENT_LENGTH + CFG.SEGMENT_LENGTH * 5;
  
  if (maxZ < spawnAhead) {
    for (let z = maxZ + CFG.SEGMENT_LENGTH; z < spawnAhead; z += CFG.SEGMENT_LENGTH) {
      spawnSegment(z);
      spawnPickup(z);
    }
  }
}

// ── function: checkCollisions ──
function checkCollisions(p, segs) {
  let nearMiss = false;
  
  for (const seg of segs) {
    if (seg.z < -10 || seg.z > CFG.SEGMENT_LENGTH * 2) continue;
    
    for (const obs of seg.obstacles) {
      // Simple AABB in world space
      const px = p.x, py = p.y;
      const halfW = CFG.PLAYER_W / 2 + 4;
      const halfH = CFG.PLAYER_H / 2 + 4;
      
      if (px + halfW > obs.x - obs.w/2 && px - halfW < obs.x + obs.w/2 &&
          py + halfH > obs.y && py - halfH < obs.y + obs.h) {
        // Collision!
        return { hit: true, nearMiss: false };
      }
      
      // Near miss check
      const margin = CFG.NEAR_MISS_DIST;
      if (px + halfW + margin > obs.x - obs.w/2 && px - halfW - margin < obs.x + obs.w/2 &&
          py + halfH + margin > obs.y && py - halfH - margin < obs.y + obs.h) {
        nearMiss = true;
      }
    }
  }
  
  return { hit: false, nearMiss };
}

// ── function: checkPickupCollisions ──
function checkPickupCollisions(p) {
  for (const pk of pickups) {
    if (pk.collected || pk.z < -10 || pk.z > CFG.SEGMENT_LENGTH * 2) continue;
    
    const dx = p.x - pk.x;
    const dy = p.y - pk.y;
    const dist = Math.sqrt(dx*dx + dy*dy);
    
    if (dist < 35) {
      pk.collected = true;
      return pk.type;
    }
  }
  return null;
}

// ── function: isInShadow ──
function isInShadow(p, segs) {
  for (const seg of segs) {
    if (!seg.hasShadow || seg.z < -10 || seg.z > CFG.SEGMENT_LENGTH * 3) continue;
    // If player is behind a tall obstacle relative to sun position
    return true;
  }
  return false;
}

// ── let: canvas ──
// === RENDERING STATE ===
let canvas = null;

// ── let: ctx ──
let ctx = null;

// ── let: W ──
let W = 0;

// ── let: H ──
let H = 0;

// ── function: resizeCanvas ──
function resizeCanvas() {
  if (!canvas) return;
  W = canvas.width = window.innerWidth;
  H = canvas.height = window.innerHeight;
}

// ── function: getSunY ──
function getSunY() {
  const progress = clamp(gameTime / CFG.SUN_SET_TIME, 0, 1);
  const baseY = lerp(CFG.SUN_START_Y, 1.15, Math.pow(progress, 0.7));
  // Sun reserve pushes sun back up slightly
  return clamp(baseY - sunReserve * 0.15, CFG.SUN_START_Y - 0.05, 1.15);
}

// ── function: lerpColor ──
function lerpColor(c1, c2, t) {
  const r1 = parseInt(c1.slice(1,3),16), g1 = parseInt(c1.slice(3,5),16), b1 = parseInt(c1.slice(5,7),16);
  const r2 = parseInt(c2.slice(1,3),16), g2 = parseInt(c2.slice(3,5),16), b2 = parseInt(c2.slice(5,7),16);
  const r = Math.round(lerp(r1,r2,t)), g = Math.round(lerp(g1,g2,t)), b = Math.round(lerp(b1,b2,t));
  return `rgb(${r},${g},${b})`;
}

// ── function: drawSky ──
function drawSky() {
  const sunY = getSunY();
  const grad = ctx.createLinearGradient(0, 0, 0, H);
  const darkness = clamp(sunY - 0.35, 0, 1) * 0.6;
  
  grad.addColorStop(0, lerpColor('#1a0533', '#050208', darkness));
  grad.addColorStop(0.3, lerpColor('#4a1942', '#0a0510', darkness));
  grad.addColorStop(0.5, lerpColor('#c0392b', '#1a0a0a', darkness));
  grad.addColorStop(0.65, lerpColor('#e67e22', '#2d1810', darkness));
  grad.addColorStop(0.75, lerpColor('#f39c12', '#3d2510', darkness));
  grad.addColorStop(1, lerpColor('#ffeaa7', '#4a3020', darkness));
  
  ctx.fillStyle = grad;
  ctx.fillRect(0, 0, W, H);
}

// ── function: drawSun ──
function drawSun() {
  const sunY = getSunY();
  const sx = W * 0.5;
  const sy = H * sunY;
  const radius = Math.min(W, H) * 0.12;
  
  for (let i = 4; i >= 0; i--) {
    const r = radius + i * radius * 0.6;
    const alpha = 0.08 - i * 0.012;
    ctx.fillStyle = `rgba(255,200,80,${alpha})`;
    ctx.beginPath();
    ctx.arc(sx, sy, r, 0, Math.PI * 2);
    ctx.fill();
  }
  
  const sunGrad = ctx.createRadialGradient(sx, sy, 0, sx, sy, radius);
  sunGrad.addColorStop(0, '#fff8e1');
  sunGrad.addColorStop(0.3, '#ffd43b');
  sunGrad.addColorStop(0.7, '#ff9f43');
  sunGrad.addColorStop(1, 'rgba(255,100,20,0)');
  
  ctx.fillStyle = sunGrad;
  ctx.beginPath();
  ctx.arc(sx, sy, radius, 0, Math.PI * 2);
  ctx.fill();
}

// ── function: drawGround ──
function drawGround() {
  const horizonY = H * getSunY() + 30;
  
  const groundGrad = ctx.createLinearGradient(0, horizonY, 0, H);
  groundGrad.addColorStop(0, '#1a0a2e');
  groundGrad.addColorStop(0.5, '#0d0618');
  groundGrad.addColorStop(1, '#050310');
  
  ctx.fillStyle = groundGrad;
  ctx.fillRect(0, horizonY, W, H - horizonY);
  
  const vanishX = W / 2;
  const numLines = 15;
  
  ctx.strokeStyle = 'rgba(255,160,80,0.08)';
  ctx.lineWidth = 1;
  
  for (let i = 0; i < numLines; i++) {
    const t = Math.pow(i / numLines, 1.5);
    const y = lerp(horizonY, H, t);
    ctx.beginPath();
    ctx.moveTo(0, y);
    ctx.lineTo(W, y);
    ctx.stroke();
  }
  
  for (let i = -8; i <= 8; i++) {
    const bottomX = vanishX + i * W / 12;
    ctx.beginPath();
    ctx.moveTo(vanishX, horizonY);
    ctx.lineTo(bottomX, H);
    ctx.stroke();
  }
}

// ── function: projectPoint ──
function projectPoint(worldZ, worldX, worldY) {
  const camDist = 200;
  const fov = 500;
  const z = worldZ + camDist;
  if (z <= 1) return null;
  
  const scale = fov / z;
  const screenX = W / 2 + worldX * scale;
  const horizonY = H * getSunY() + 30;
  const groundOffset = lerp(0, H - horizonY, clamp(worldZ / (CFG.DRAW_DISTANCE * CFG.SEGMENT_LENGTH), 0, 1));
  const screenY = horizonY + groundOffset - worldY * scale;
  
  return { x: screenX, y: screenY, scale };
}

// ── function: drawObstacles ──
function drawObstacles() {
  const sortedSegs = [...worldSegments].sort((a, b) => b.z - a.z);
  
  for (const seg of sortedSegs) {
    if (seg.z < 0 || seg.z > CFG.DRAW_DISTANCE * CFG.SEGMENT_LENGTH) continue;
    
    // Distance-based alpha fade for far obstacles
    const distFade = clamp(seg.z / (CFG.DRAW_DISTANCE * CFG.SEGMENT_LENGTH), 0, 1);
    const alpha = lerp(0.3, 1, 1 - distFade);
    
    for (const obs of seg.obstacles) {
      const proj = projectPoint(seg.z, obs.x, obs.y + obs.h / 2);
      if (!proj) continue;
      
      const sw = obs.w * proj.scale;
      const sh = obs.h * proj.scale;
      
      ctx.globalAlpha = alpha;
      
      // Shadow beneath obstacle
      ctx.fillStyle = 'rgba(0,0,0,0.35)';
      ctx.fillRect(proj.x - sw/2 + 5, proj.y - sh/2 + 5, sw, sh);
      
      // Obstacle body with sunset-themed gradient
      const obsGrad = ctx.createLinearGradient(proj.x - sw/2, proj.y - sh/2, proj.x + sw/2, proj.y + sh/2);
      obsGrad.addColorStop(0, '#8e44ad');
      obsGrad.addColorStop(0.5, '#6c3483');
      obsGrad.addColorStop(1, '#4a235a');
      
      ctx.fillStyle = obsGrad;
      ctx.fillRect(proj.x - sw/2, proj.y - sh/2, sw, sh);
      
      // Edge highlight (sunlit side)
      ctx.strokeStyle = 'rgba(255,180,120,0.4)';
      ctx.lineWidth = 1;
      ctx.strokeRect(proj.x - sw/2, proj.y - sh/2, sw, sh);
      
      // Top face (pseudo-3D effect)
      ctx.fillStyle = 'rgba(255,200,160,0.2)';
      ctx.fillRect(proj.x - sw/2, proj.y - sh/2, sw, Math.max(2, sh * 0.08));
      
      // Side face for depth
      ctx.fillStyle = 'rgba(30,15,40,0.5)';
      ctx.fillRect(proj.x + sw/2 - 3, proj.y - sh/2, 3, sh);
      
      ctx.globalAlpha = 1;
    }
  }
}

// ── function: drawPickups ──
function drawPickups() {
  for (const pk of pickups) {
    if (pk.collected || pk.z < 5 || pk.z > CFG.DRAW_DISTANCE * CFG.SEGMENT_LENGTH) continue;
    
    const proj = projectPoint(pk.z, pk.x, pk.y);
    if (!proj) continue;
    
    const size = 12 * proj.scale;
    const bob = Math.sin(pk.bobPhase) * 4 * proj.scale;
    const py = proj.y + bob;
    
    ctx.save();
    ctx.translate(proj.x, py);
    
    let color, glowColor;
    switch (pk.type) {
      case 'energy': color = '#7bed9f'; glowColor = 'rgba(123,237,159,'; break;
      case 'score': color = '#ffd43b'; glowColor = 'rgba(255,212,59,'; break;
      case 'multiplier': color = '#ff6b81'; glowColor = 'rgba(255,107,129,'; break;
    }
    
    ctx.fillStyle = glowColor + '0.3)';
    ctx.beginPath();
    ctx.arc(0, 0, size * 1.8, 0, Math.PI * 2);
    ctx.fill();
    
    // Diamond shape
    ctx.fillStyle = color;
    ctx.beginPath();
    ctx.moveTo(0, -size);
    ctx.lineTo(size * 0.7, 0);
    ctx.lineTo(0, size);
    ctx.lineTo(-size * 0.7, 0);
    ctx.closePath();
    ctx.fill();
    
    // Inner highlight
    ctx.fillStyle = 'rgba(255,255,255,0.4)';
    ctx.beginPath();
    ctx.moveTo(0, -size * 0.5);
    ctx.lineTo(size * 0.3, 0);
    ctx.lineTo(0, size * 0.3);
    ctx.lineTo(-size * 0.3, 0);
    ctx.closePath();
    ctx.fill();
    
    ctx.restore();
  }
}

// ── function: drawSpeedLines ──
function drawSpeedLines(speedRatio) {
  if (speedRatio < 0.5) return;
  
  const intensity = (speedRatio - 0.5) * 2;
  const numLines = Math.floor(intensity * 30);
  
  ctx.strokeStyle = `rgba(255,255,255,${intensity * 0.15})`;
  ctx.lineWidth = 1;
  
  for (let i = 0; i < numLines; i++) {
    const x = Math.random() * W;
    const y = H * getSunY() + Math.random() * (H - H * getSunY());
    const len = randRange(20, 80) * intensity;
    
    ctx.beginPath();
    ctx.moveTo(x, y);
    ctx.lineTo(x + (x - W/2) * 0.1, y + len);
    ctx.stroke();
  }
}

// ── function: drawChromaticAberration ──
function drawChromaticAberration(speedRatio) {
  if (speedRatio < 0.7) return;
  const intensity = (speedRatio - 0.7) / 0.3 * 0.05;
  
  ctx.globalCompositeOperation = 'screen';
  ctx.fillStyle = `rgba(255,0,0,${intensity})`;
  ctx.fillRect(2, 0, W, H);
  ctx.fillStyle = `rgba(0,0,255,${intensity})`;
  ctx.fillRect(-2, 0, W, H);
  ctx.globalCompositeOperation = 'source-over';
}

// ── let: gameState ──
// === GAME STATE & INPUT ===
let gameState = 'title';

// ── let: currentSpeed ──
let currentSpeed = CFG.BASE_SPEED;

// ── let: score ──
let score = 0;

// ── let: multiplier ──
let multiplier = 1;

// ── let: screenShakeX ──
let screenShakeX = 0;

// ── let: screenShakeY ──
let screenShakeY = 0;

// ── let: shakeIntensity ──
let shakeIntensity = 0;

// ── let: flashAlpha ──
let flashAlpha = 0;

// ── let: boostEnergy ──
let boostEnergy = 100;

// ── let: isBoosting ──
let isBoosting = false;

// ── let: nearMissTimer ──
let nearMissTimer = 0;

// ── let: lastTime ──
let lastTime = null;

// ── const: keys ──
const keys = {};

// ── function: resetGameState ──
function resetGameState() {
  player = createPlayer();
  currentSpeed = CFG.BASE_SPEED;
  score = 0;
  multiplier = 1;
  screenShakeX = 0;
  screenShakeY = 0;
  shakeIntensity = 0;
  flashAlpha = 0;
  boostEnergy = 100;
  isBoosting = false;
  nearMissTimer = 0;
  sunReserve = 0;
  particles = [];
  resetWorld();
}

// ── function: triggerShake ──
function triggerShake(intensity) {
  shakeIntensity = Math.max(shakeIntensity, intensity);
}

// ── function: handleInput ──
function handleInput(dt) {
  if (gameState !== 'playing') return;
  
  const moveAmt = CFG.MOVE_SPEED * dt * 60;
  
  if (keys['ArrowLeft'] || keys['KeyA']) player.targetX -= moveAmt;
  if (keys['ArrowRight'] || keys['KeyD']) player.targetX += moveAmt;
  if (keys['ArrowUp'] || keys['KeyW']) player.targetY -= moveAmt;
  if (keys['ArrowDown'] || keys['KeyS']) player.targetY += moveAmt;
  
  if (keys['Space'] && boostEnergy > 0 && !isBoosting) {
    isBoosting = true;
    playBoost();
  }
}

// ── function: updateHUD ──
function updateHUD() {
  document.getElementById('dist').textContent = `${Math.floor(distanceTraveled)}m`;
  document.getElementById('score').textContent = Math.floor(score);
  document.getElementById('multiplier').textContent = `×${multiplier.toFixed(1)}`;
  document.getElementById('energy-bar').style.width = `${player.energy}%`;
  
  const speedKmh = Math.floor(currentSpeed * 2.5);
  document.getElementById('speed-display').textContent = `${speedKmh} km/h`;
  
  // Energy bar color based on level
  const eb = document.getElementById('energy-bar');
  if (player.energy < 20) {
    eb.style.background = '#ff4757';
  } else if (player.energy < 40) {
    eb.style.background = 'linear-gradient(90deg, #ff6b35, #ffa502)';
  } else {
    eb.style.background = 'linear-gradient(90deg, #ff6b35, #ffd43b, #7bed9f)';
  }
  
  // Sun indicator - changes color and size based on sun position
  const si = document.getElementById('sun-indicator');
  if (si) {
    const sunY = getSunY();
    const sunProgress = clamp((sunY - CFG.SUN_START_Y) / 0.8, 0, 1);
    const size = lerp(16, 8, sunProgress);
    si.style.width = `${size}px`;
    si.style.height = `${size}px`;
    
    if (sunProgress > 0.7) {
      si.style.background = '#ff4757';
      si.style.boxShadow = '0 0 12px rgba(255,71,87,.8)';
    } else if (sunProgress > 0.4) {
      si.style.background = '#ffa502';
      si.style.boxShadow = '0 0 10px rgba(255,165,2,.6)';
    } else {
      si.style.background = '#ffd43b';
      si.style.boxShadow = '0 0 8px rgba(255,212,59,.6)';
    }
  }
}

// ── function: gameOver ──
function gameOver() {
  gameState = 'gameover';
  playCrash();
  triggerShake(15);
  flashAlpha = 0.8;
  
  const proj = projectPoint(15, player.x, player.y);
  if (proj) emitCrashBurst(proj.x, proj.y);
  
  const hs = parseInt(localStorage.getItem('solarRunnerHighScore') || '0');
  const finalScore = Math.floor(score);
  if (finalScore > hs) {
    localStorage.setItem('solarRunnerHighScore', finalScore.toString());
  }
  
  document.getElementById('go-dist').textContent = `Distance: ${Math.floor(distanceTraveled)}m`;
  document.getElementById('go-score').textContent = `Score: ${finalScore}`;
  document.getElementById('go-highscore').textContent = `Best: ${Math.max(hs, finalScore)}`;
  document.getElementById('gameover-screen').style.display = 'flex';
}

// ── function: updateGame ──
function updateGame(dt) {
  if (gameState !== 'playing') return;
  
  const speedMult = isBoosting ? CFG.BOOST_SPEED_MULT : 1;
  currentSpeed += CFG.SPEED_RAMP * dt;
  currentSpeed = Math.min(currentSpeed, CFG.MAX_SPEED);
  const effectiveSpeed = currentSpeed * speedMult;
  
  // Boost energy management
  if (isBoosting) {
    boostEnergy -= CFG.BOOST_DRAIN * dt * 60;
    if (boostEnergy <= 0) {
      boostEnergy = 0;
      isBoosting = false;
    }
  } else {
    boostEnergy = Math.min(100, boostEnergy + 5 * dt);
  }
  
  // Sun reserve slowly decays
  sunReserve *= Math.pow(0.98, dt * 60);
  
  // Update player
  updatePlayer(player, dt);
  
  // Update world
  updateWorld(dt, effectiveSpeed);
  
  // Energy drain
  const shadowMult = isInShadow(player, worldSegments) ? CFG.SHADOW_DRAIN_MULT : 1;
  player.energy -= CFG.ENERGY_DRAIN * shadowMult * dt;
  
  if (player.energy <= 0) {
    gameOver();
    return;
  }
  
  // Collision detection
  const collision = checkCollisions(player, worldSegments);
  if (collision.hit && player.invincibleTimer <= 0) {
    gameOver();
    return;
  }
  
  if (collision.nearMiss) {
    score += CFG.NEAR_MISS_BONUS * multiplier;
    triggerShake(3);
    nearMissTimer = 0.2;
    playNearMiss();
  }
  
  // Pickup collection
  const pickupType = checkPickupCollisions(player);
  if (pickupType) {
    const proj = projectPoint(15, player.x, player.y);
    if (proj) emitPickupBurst(proj.x, proj.y, '#ffd43b');
    
    switch(pickupType) {
      case 'energy':
        player.energy = Math.min(CFG.ENERGY_MAX, player.energy + CFG.PICKUP_ENERGY);
        sunReserve += 1; // Each energy pickup also helps the sun
        playPickup(660);
        break;
      case 'score':
        score += 100 * multiplier;
        playPickup(880);
        break;
      case 'multiplier':
        multiplier = Math.min(multiplier + 0.5, 10);
        playPickup(1100);
        break;
    }
  }
  
  // Distance score
  score += effectiveSpeed * CFG.BASE_SCORE_PER_METER * dt * multiplier * 0.01;
  
  // Screen shake decay
  if (shakeIntensity > 0) {
    screenShakeX = (Math.random() - 0.5) * shakeIntensity * 2;
    screenShakeY = (Math.random() - 0.5) * shakeIntensity * 2;
    shakeIntensity *= Math.pow(CFG.SHAKE_DECAY, dt * 60);
    if (shakeIntensity < 0.1) {
      shakeIntensity = 0;
      screenShakeX = 0;
      screenShakeY = 0;
    }
  }
  
  // Flash decay
  flashAlpha *= Math.pow(0.9, dt * 60);
  
  // Near miss timer
  if (nearMissTimer > 0) nearMissTimer -= dt;
  
  // Update particles
  updateParticles(dt);
  
  // Thrust particles
  emitThrust(W/2 + screenShakeX, H * 0.65 + screenShakeY, player.pitchLean);
  
  // Audio drone
  const speedRatio = (currentSpeed - CFG.BASE_SPEED) / (CFG.MAX_SPEED - CFG.BASE_SPEED);
  updateDrone(speedRatio);
  
  // Update HUD
  updateHUD();
}

// ── function: render ──
function render() {
  ctx.save();
  ctx.translate(screenShakeX, screenShakeY);
  
  drawSky();
  drawStars(ctx);
  drawSun();
  drawGround();
  drawObstacles();
  drawPickups();
  
  // Draw player craft at fixed screen position
  const px = W / 2;
  const py = H * 0.65;
  
  // Boost glow effect around player
  if (isBoosting) {
    ctx.fillStyle = `rgba(100,200,255,${0.1 + Math.sin(Date.now() * 0.01) * 0.05})`;
    ctx.beginPath();
    ctx.arc(px, py, 40, 0, Math.PI * 2);
    ctx.fill();
    
    // Boost trail lines
    for (let i = 0; i < 3; i++) {
      const offset = randRange(-15, 15);
      ctx.strokeStyle = `rgba(100,200,255,${randRange(0.1, 0.3)})`;
      ctx.lineWidth = 1;
      ctx.beginPath();
      ctx.moveTo(px + offset, py + 20);
      ctx.lineTo(px + offset * 1.5, py + randRange(40, 80));
      ctx.stroke();
    }
  }
  
  drawPlayer(ctx, px, py, player.bankAngle, player.pitchLean, flashAlpha);
  
  // Particles on top
  drawParticles(ctx);
  
  // Speed effects
  const speedRatio = (currentSpeed - CFG.BASE_SPEED) / (CFG.MAX_SPEED - CFG.BASE_SPEED);
  drawSpeedLines(speedRatio);
  drawChromaticAberration(speedRatio);
  
  // Damage flash overlay
  if (flashAlpha > 0.01) {
    ctx.fillStyle = `rgba(255,80,60,${flashAlpha * 0.3})`;
    ctx.fillRect(-10, -10, W + 20, H + 20);
  }
  
  // Near miss indicator
  if (nearMissTimer > 0) {
    ctx.fillStyle = `rgba(255,212,59,${nearMissTimer * 3})`;
    ctx.font = 'bold 24px system-ui';
    ctx.textAlign = 'center';
    ctx.fillText('NEAR MISS!', W/2, H * 0.5);
  }
  
  // Boost indicator text
  if (isBoosting) {
    ctx.fillStyle = `rgba(100,200,255,${0.3 + Math.sin(Date.now() * 0.01) * 0.1})`;
    ctx.font = 'bold 18px system-ui';
    ctx.textAlign = 'center';
    ctx.fillText('⚡ BOOST ⚡', W/2, H * 0.75);
  }
  
  // Sun warning when low
  const sunY = getSunY();
  if (sunY > 0.8) {
    const warnAlpha = Math.sin(Date.now() * 0.005) * 0.15 + 0.15;
    ctx.fillStyle = `rgba(255,50,30,${warnAlpha})`;
    ctx.font = 'bold 16px system-ui';
    ctx.textAlign = 'center';
    ctx.fillText('☀ SUN SETTING — GRAB ENERGY!', W/2, H * 0.4);
  }
  
  // Vignette effect
  const vigGrad = ctx.createRadialGradient(W/2, H/2, W*0.3, W/2, H/2, W*0.8);
  vigGrad.addColorStop(0, 'rgba(0,0,0,0)');
  vigGrad.addColorStop(1, 'rgba(0,0,0,0.4)');
  ctx.fillStyle = vigGrad;
  ctx.fillRect(-10, -10, W + 20, H + 20);
  
  ctx.restore();
}

// ── function: gameLoop ──
function gameLoop(timestamp) {
  if (!lastTime) lastTime = timestamp;
  let dt = (timestamp - lastTime) / 1000;
  lastTime = timestamp;
  
  dt = Math.min(dt, 0.05);
  
  handleInput(dt);
  updateGame(dt);
  
  if (gameState === 'playing') {
    render();
  } else if (gameState === 'title' || gameState === 'gameover') {
    drawSky();
    drawStars(ctx);
    drawSun();
    drawGround();
  }
  
  requestAnimationFrame(gameLoop);
}

// ── function: startGame ──
function startGame() {
  initAudio();
  resetGameState();
  gameState = 'playing';
  document.getElementById('title-screen').style.display = 'none';
  document.getElementById('gameover-screen').style.display = 'none';
  document.getElementById('hud').style.display = 'flex';
}

// ── function: showHighScore ──
function showHighScore() {
  const hs = localStorage.getItem('solarRunnerHighScore');
  if (hs) {
    document.getElementById('high-score-line').textContent = `Best: ${hs}`;
  }
}

// ── let: sunReserve ──
let sunReserve = 0;

// ── let: stars ──
let stars = [];

// ── function: initStars ──
function initStars() {
  stars = [];
  for (let i = 0; i < 80; i++) {
    stars.push({
      x: Math.random(),
      y: Math.random() * 0.6,
      size: randRange(0.5, 2),
      twinkleSpeed: randRange(1, 3),
      twinklePhase: Math.random() * Math.PI * 2,
    });
  }
}

// ── function: drawStars ──
function drawStars(ctx) {
  const t = Date.now() * 0.001;
  for (const s of stars) {
    const alpha = 0.3 + Math.sin(t * s.twinkleSpeed + s.twinklePhase) * 0.25;
    ctx.fillStyle = `rgba(255,240,220,${alpha})`;
    ctx.beginPath();
    ctx.arc(s.x * W, s.y * H, s.size, 0, Math.PI * 2);
    ctx.fill();
  }
}

// ── main ──
// === MAIN ENTRY POINT ===
canvas = document.getElementById('game');
ctx = canvas.getContext('2d');

resizeCanvas();
initStars();
window.addEventListener('resize', function() {
  resizeCanvas();
  initStars();
});

// Input handling
document.addEventListener('keydown', function(e) {
  keys[e.code] = true;
  
  // Restart on R key during game over
  if (e.code === 'KeyR' && gameState === 'gameover') {
    startGame();
  }
  
  // Prevent scrolling with arrow keys/space
  if (['ArrowUp','ArrowDown','ArrowLeft','ArrowRight','Space'].includes(e.code)) {
    e.preventDefault();
  }
});

document.addEventListener('keyup', function(e) {
  keys[e.code] = false;
  if (e.code === 'Space') isBoosting = false;
});

// Button handlers
document.getElementById('start-btn').addEventListener('click', startGame);
document.getElementById('restart-btn').addEventListener('click', startGame);

// Show high score on title screen
showHighScore();

// Start the game loop
requestAnimationFrame(gameLoop);
</script>
</body>
</html>
<!-- agent-meta {"model":"thinkingcap-qwen3.6-27b","provider":"lmstudio","persona":"composer","sessionId":"78f6f33e-ac4a-49ca-8763-341d8241dbf9","tokensIn":2021081,"tokensOut":32637,"tokensTotal":2053718,"turns":67,"toolCalls":66,"failedToolCalls":8,"timestamp":"2026-07-18T15:38:14.138Z"} -->