hypeorcode
Network offOpen alone (new tab)

Run details

Model
openai/gpt-6.1-sol
Route
OpenRouter API, one user message
Reasoning
provider default, 516 reasoning tokens
Limits
60 min · 60,000 output tokens
Date
21 Oct 2026
Time
2 min 27 s
Size
23 kB
Tokens
83 in, 8,193 out
List price
$0.082 ($2 in / $10 out per M)
Paid
$0.082

Raw files: output · meta.json

Exact prompt

Make a Breakout game I can play right now with the keyboard or the mouse. Give it several levels, a score and some juice.

It must be one complete, self-contained HTML file: inline all CSS and JavaScript, and make no network requests of any kind (no CDNs, web fonts or remote images). Reply with the file in a single ```html code block.

Source

23 kB 606 lines Raw

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
<title>PRISM — Breakout</title>
<style>
  :root { color-scheme: dark; --ink:#edf5ff; --muted:#899bb8; --cyan:#65f4df; }
  * { box-sizing:border-box; }
  body {
    margin:0; min-height:100vh; min-height:100svh;
    background:radial-gradient(ellipse at 50% 0%,#182443 0%,#090e1b 58%,#060a12 100%);
    color:var(--ink); font-family:system-ui,-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif;
    display:flex; align-items:center; justify-content:center; padding:24px;
  }
  main { width:min(100%,1000px); }
  header { display:flex; align-items:center; justify-content:space-between; gap:20px; margin:0 2px 18px; }
  .brand { font-size:27px; font-weight:900; letter-spacing:7px; }
  .brand span { color:var(--cyan); }
  .tagline { color:var(--muted); font-size:11px; letter-spacing:2px; margin-top:5px; }
  .tools { display:flex; gap:8px; }
  button {
    font:inherit; cursor:pointer; color:var(--ink); border:1px solid #ffffff20;
    border-radius:10px; background:#ffffff08; padding:10px 13px;
    transition:background .15s,transform .15s,border-color .15s;
  }
  button:hover { background:#ffffff14; border-color:#ffffff50; }
  button:active { transform:translateY(1px); }
  button:focus-visible { outline:2px solid var(--cyan); outline-offset:4px; }
  .hud {
    display:grid; grid-template-columns:1.1fr 1fr 1fr 1fr;
    padding:17px 23px; border:1px solid #ffffff12; border-bottom:0;
    background:#111a2c; border-radius:17px 17px 0 0;
  }
  .label { font-size:10px; font-weight:700; letter-spacing:2px; color:var(--muted); margin-bottom:4px; }
  .value { font-size:21px; font-weight:800; font-variant-numeric:tabular-nums; }
  #score { color:var(--cyan); }
  .hud > :last-child { text-align:right; }
  #lives { letter-spacing:5px; color:#ff829a; }
  #arena {
    position:relative; overflow:hidden; background:#090f1e;
    border:1px solid #ffffff18; border-radius:0 0 17px 17px;
    box-shadow:0 30px 90px #0007; touch-action:none;
  }
  canvas { display:block; width:100%; aspect-ratio:3/2; cursor:none; }
  #overlay {
    position:absolute; inset:0; display:flex; align-items:center; justify-content:center;
    background:#070d1a9c; backdrop-filter:blur(5px); padding:20px;
  }
  #overlay.hidden { display:none; }
  .card { max-width:450px; text-align:center; }
  #eyebrow { color:var(--cyan); font-size:11px; font-weight:800; letter-spacing:4px; text-transform:uppercase; }
  h1 { font-size:clamp(28px,5vw,54px); letter-spacing:-2px; line-height:1.05; margin:16px 0; }
  #message { margin:0 auto 25px; color:#b3c1d8; font-size:15px; line-height:1.7; max-width:350px; }
  .primary { background:var(--cyan); color:#09221f; font-weight:850; border:0; padding:14px 29px; box-shadow:0 0 30px #65f4df20; }
  .primary:hover { background:#a3fff0; }
  #shortcut { margin-top:15px; color:#7e91b1; font-size:12px; }
  footer { display:flex; justify-content:space-between; gap:12px; margin:15px 3px 0; color:var(--muted); font-size:12px; line-height:1.7; }
  kbd { font:inherit; color:#d6e1f4; padding:2px 5px; background:#ffffff09; border:1px solid #ffffff12; border-radius:4px; }
  .dot { color:var(--cyan); margin-right:5px; }
  @media(max-width:600px) {
    body { padding:12px; }
    header { margin-bottom:12px; gap:10px; }
    .brand { font-size:22px; letter-spacing:4px; }
    .tagline { font-size:8px; letter-spacing:1.4px; }
    .tools { gap:5px; }
    .tools button { font-size:11px; padding:9px 8px; }
    .hud { padding:12px 13px; }
    .label { font-size:8px; letter-spacing:1px; }
    .value { font-size:17px; }
    #lives { letter-spacing:2px; }
    footer { font-size:10px; flex-wrap:wrap; }
    #message { font-size:12px; margin-bottom:15px; }
    h1 { margin:10px 0; }
    .primary { padding:11px 22px; }
    #shortcut { margin-top:10px; font-size:10px; }
  }
  @media(prefers-reduced-motion:reduce) { button { transition:none; } }
</style>
</head>
<body>
<main>
  <header>
    <div>
      <div class="brand">PRI<span>▰</span>SM</div>
      <div class="tagline">A LITTLE CHAOS. A LOT OF COLOR.</div>
    </div>
    <div class="tools">
      <button id="sound" aria-label="Enable sound" aria-pressed="false">Sound off</button>
      <button id="pause" aria-label="Pause game">Pause</button>
      <button id="restart" aria-label="Restart game">Restart</button>
    </div>
  </header>

  <div class="hud" aria-label="Game statistics">
    <div><div class="label">SCORE</div><div class="value" id="score">00000</div></div>
    <div><div class="label">BEST</div><div class="value" id="best">00000</div></div>
    <div><div class="label">LEVEL</div><div class="value" id="level">01 / 05</div></div>
    <div><div class="label">LIVES</div><div class="value" id="lives">●●●</div></div>
  </div>

  <div id="arena">
    <canvas id="game" width="900" height="600" aria-label="Breakout game. Move with the mouse or arrow keys. Space to launch, P to pause." role="img"></canvas>
    <div id="overlay">
      <div class="card">
        <div id="eyebrow">THE ARCADE, RECHARGED</div>
        <h1 id="title">Find your angle.</h1>
        <p id="message">Five colorful levels. Three lives. Break every brick, and keep the ball in play.</p>
        <button class="primary" id="action">Let's play →</button>
        <div id="shortcut">Move with your mouse or ← → · Space to start</div>
      </div>
    </div>
  </div>

  <footer>
    <div><kbd>←</kbd> <kbd>→</kbd> / <kbd>A</kbd> <kbd>D</kbd> or mouse / touch to move · <kbd>Space</kbd> launch · <kbd>P</kbd> pause</div>
    <div><span class="dot">✦</span>Hit with the paddle's edge to steer.</div>
  </footer>
</main>

<script>
(() => {
  'use strict';

  const canvas = document.getElementById('game');
  const ctx = canvas.getContext('2d');
  const arena = document.getElementById('arena');
  const overlay = document.getElementById('overlay');
  const ui = Object.fromEntries(
    ['score','best','level','lives','title','message','eyebrow','action','shortcut','pause','sound']
      .map(id => [id, document.getElementById(id)])
  );
  const W = 900, H = 600;
  const reducedMotion = matchMedia('(prefers-reduced-motion: reduce)').matches;
  const colors = ['#67f2d9','#67c9ff','#9294ff','#d18aff','#ff85ac','#ffc778'];
  const names = ['First Light', 'Split Spectrum', 'Diamond Rush', 'Hard Candy', 'The Final Prism'];

  let state = 'intro', level = 0, score = 0, best = 0, lives = 3;
  let bricks = [], particles = [], texts = [], rings = [];
  let shake = 0, flash = 0, time = 0, combo = 0;
  let targetX = W/2, inputMode = 'pointer';
  let soundOn = false, audio = null, lastTime = 0;
  const keys = new Set();
  const paddle = { x:W/2, y:552, w:124, h:14, vx:0, glow:0 };
  const ball = { x:W/2, y:538, vx:0, vy:0, r:7, speed:400, trail:[] };

  try { best = Number(localStorage.getItem('prism-breakout-best')) || 0; } catch (_) {}

  function resize() {
    const dpr = Math.min(window.devicePixelRatio || 1, 2);
    canvas.width = Math.round(canvas.clientWidth * dpr);
    canvas.height = Math.round(canvas.clientHeight * dpr);
  }
  window.addEventListener('resize', resize);
  resize();

  const clamp = (n, a, b) => Math.max(a, Math.min(b, n));
  const random = (a, b) => a + Math.random() * (b-a);

  function roundRect(x,y,w,h,r) {
    ctx.beginPath();
    ctx.roundRect(x,y,w,h,r);
  }

  function updateHUD() {
    ui.score.textContent = String(score).padStart(5,'0');
    ui.best.textContent = String(best).padStart(5,'0');
    ui.level.textContent = `${String(level+1).padStart(2,'0')} / 05`;
    ui.lives.textContent = '●'.repeat(lives) + '○'.repeat(3-lives);
  }

  function addScore(n) {
    score += n;
    if (score > best) {
      best = score;
      try { localStorage.setItem('prism-breakout-best', String(best)); } catch (_) {}
    }
    updateHUD();
  }

  function tone(freq, duration=.08, type='sine', gain=.045, slide=1) {
    if (!soundOn || !audio) return;
    const t = audio.currentTime;
    const osc = audio.createOscillator(), amp = audio.createGain();
    osc.type = type;
    osc.frequency.setValueAtTime(freq,t);
    osc.frequency.exponentialRampToValueAtTime(Math.max(30,freq*slide),t+duration);
    amp.gain.setValueAtTime(gain,t);
    amp.gain.exponentialRampToValueAtTime(.0001,t+duration);
    osc.connect(amp); amp.connect(audio.destination);
    osc.start(t); osc.stop(t+duration+.01);
  }

  function awakenAudio() {
    if (audio && audio.state === 'suspended') audio.resume().catch(() => {});
  }

  function showOverlay(eyebrow, title, message, button, shortcut='Press Space to continue') {
    ui.eyebrow.textContent = eyebrow;
    ui.title.textContent = title;
    ui.message.textContent = message;
    ui.action.textContent = button;
    ui.shortcut.textContent = shortcut;
    overlay.classList.remove('hidden');
    ui.pause.textContent = 'Pause';
  }

  function buildLevel() {
    bricks = [];
    const cols=12, bw=62, bh=23, gap=7, left=(W-(cols*bw+(cols-1)*gap))/2;
    const rows = level === 0 ? 5 : 7;
    for (let r=0;r<rows;r++) for (let c=0;c<cols;c++) {
      let include=true;
      if (level===1) include = c<5 || c>6 || r%3===0;
      if (level===2) include = Math.abs(c-5.5)/1.6 + Math.abs(r-3) <= 4.5;
      if (level===3) include = (r+c)%4 !== 0;
      if (level===4) include = r===0 || r===6 || c===0 || c===11 || (r+c)%2===0;
      if (!include) continue;
      const hp = (level===2 && r===3) || (level===3 && r%2===0) ||
                 (level===4 && (r===0 || r===6 || c===0 || c===11)) ? 2 : 1;
      bricks.push({
        x:left+c*(bw+gap), y:83+r*(bh+gap), w:bw, h:bh,
        hp, maxHp:hp, color:colors[(r+level)%colors.length], hit:0
      });
    }
    paddle.w = 124 - level*5;
    paddle.x = W/2; targetX=W/2;
    combo=0; particles=[]; texts=[]; rings=[];
    resetBall();
    updateHUD();
  }

  function resetBall() {
    ball.speed = 395 + level*35;
    ball.x = paddle.x; ball.y = paddle.y-ball.r-2;
    ball.vx=0; ball.vy=0; ball.trail=[];
  }

  function launch() {
    awakenAudio();
    overlay.classList.add('hidden');
    state='playing';
    const angle=random(-.34,.34);
    ball.vx=Math.sin(angle)*ball.speed;
    ball.vy=-Math.cos(angle)*ball.speed;
    tone(440,.12,'sine',.05,1.8);
  }

  function newGame() {
    score=0; lives=3; level=0; shake=0; flash=0;
    keys.clear();
    buildLevel();
    launch();
  }

  function primaryAction() {
    awakenAudio();
    if (state==='intro' || state==='over' || state==='win') newGame();
    else if (state==='serve') launch();
    else if (state==='clear') { level++; buildLevel(); launch(); }
    else if (state==='paused') togglePause();
  }

  function togglePause() {
    if (state==='playing') {
      state='paused'; keys.clear();
      showOverlay('TAKE A BREATHER','On pause.','Your next great ricochet can wait.','Resume →','P or Space to resume');
      ui.pause.textContent='Resume';
    } else if (state==='paused') {
      awakenAudio();
      state='playing'; overlay.classList.add('hidden');
      ui.pause.textContent='Pause';
    }
  }

  function burst(x,y,color,count=18,force=1) {
    for (let i=0;i<count;i++) {
      const a=random(0,Math.PI*2), s=random(40,200)*force;
      const life=random(.25,.65);
      particles.push({x,y,vx:Math.cos(a)*s,vy:Math.sin(a)*s,life,max:life,size:random(2,5),color});
    }
    if (particles.length>650) particles.splice(0,particles.length-650);
  }

  function ring(x,y,color) { rings.push({x,y,color,life:.4}); }

  function loseLife() {
    burst(ball.x,Math.min(ball.y,H-12),'#ff7e9c',36,1.5);
    shake = reducedMotion ? 0 : 10;
    flash=.12; lives--; combo=0; updateHUD();
    tone(170,.35,'sawtooth',.035,.28);
    resetBall();
    if (lives===0) {
      state='over';
      showOverlay('ONE MORE ROUND?','Out of orbit.',`You scored ${score.toLocaleString()} points and reached level ${level+1}. Your next run could be the one.`,'Try again →');
    } else {
      state='serve';
      showOverlay(`${lives} ${lives===1?'LIFE':'LIVES'} REMAINING`,'Stay in the game.', 'Reset your angle. The bricks are still waiting.','Launch ball →','Space or click Launch to continue');
    }
  }

  function completeLevel() {
    addScore(500*(level+1));
    state = level===4 ? 'win' : 'clear';
    shake = reducedMotion ? 0 : 8;
    for (let i=0;i<6;i++) burst(random(100,800),random(120,380),colors[i],25,1.5);
    tone(523,.2,'sine',.06,2);
    setTimeout(() => { if(state==='clear'||state==='win') tone(784,.3,'sine',.04,1.33); },120);
    if (state==='win') {
      addScore(lives*1000);
      showOverlay('SPECTRUM COMPLETE','Pure brilliance.',`All five levels cleared! Final score: ${score.toLocaleString()}, including ${lives*1000} points for your remaining lives.`,'Play again →');
    } else {
      showOverlay(`LEVEL ${level+1} COMPLETE · +${500*(level+1)} BONUS`,'Beautifully broken.',`Up next: ${names[level+1]}. A new pattern, a faster ball, and a little less room for error.`,'Next level →');
    }
  }

  function hitBrick(b) {
    b.hp--; b.hit=.15;
    ball.speed=Math.min(690,ball.speed+2.2);
    const speed=Math.hypot(ball.vx,ball.vy);
    ball.vx=ball.vx/speed*ball.speed;
    ball.vy=ball.vy/speed*ball.speed;
    if (b.hp<=0) {
      combo++;
      const multiplier=Math.min(5,1+Math.floor((combo-1)/5));
      const points=50*b.maxHp*multiplier;
      addScore(points);
      burst(ball.x,ball.y,b.color,16);
      texts.push({x:b.x+b.w/2,y:b.y,text:multiplier>1?`+${points} ×${multiplier}`:`+${points}`,life:.8,color:b.color});
      shake=Math.max(shake,reducedMotion?0:Math.min(5,1.5+combo*.1));
      tone(330+Math.min(combo,24)*27,.075,'triangle',.05,1.45);
    } else {
      addScore(20); burst(ball.x,ball.y,b.color,8);
      tone(220,.08,'square',.025,1.5);
    }
  }

  function step(dt) {
    const oldX = paddle.x;
    let direction=0;
    if (keys.has('arrowleft')||keys.has('a')) direction--;
    if (keys.has('arrowright')||keys.has('d')) direction++;
    if (inputMode==='keyboard') paddle.x+=direction*690*dt;
    else paddle.x+=(targetX-paddle.x)*Math.min(1,25*dt);
    paddle.x=clamp(paddle.x,paddle.w/2+9,W-paddle.w/2-9);
    paddle.vx=(paddle.x-oldX)/dt;
    paddle.glow=Math.max(0,paddle.glow-dt*3);

    const prevX=ball.x, prevY=ball.y;
    ball.x+=ball.vx*dt; ball.y+=ball.vy*dt;

    if (ball.x<ball.r+8) {
      ball.x=ball.r+8; ball.vx=Math.abs(ball.vx); tone(180,.035,'sine',.025);
    }
    if (ball.x>W-ball.r-8) {
      ball.x=W-ball.r-8; ball.vx=-Math.abs(ball.vx); tone(180,.035,'sine',.025);
    }
    if (ball.y<ball.r+8) {
      ball.y=ball.r+8; ball.vy=Math.abs(ball.vy); tone(220,.035,'sine',.025);
    }

    if (ball.vy>0 && prevY+ball.r<=paddle.y+5 &&
        ball.y+ball.r>=paddle.y && ball.y-ball.r<=paddle.y+paddle.h &&
        ball.x+ball.r>=paddle.x-paddle.w/2 && ball.x-ball.r<=paddle.x+paddle.w/2) {
      const offset=clamp((ball.x-paddle.x)/(paddle.w/2),-1,1);
      let vx=Math.sin(offset*1.1)*ball.speed + clamp(paddle.vx*.12,-70,70);
      vx=clamp(vx,-ball.speed*.92,ball.speed*.92);
      // Keep a little horizontal motion so vertical loops never trap the game.
      if (Math.abs(vx)<38) vx=(vx<0?-1:1)*38;
      ball.vx=vx;
      ball.vy=-Math.sqrt(ball.speed*ball.speed-vx*vx);
      ball.y=paddle.y-ball.r-.2;
      paddle.glow=1; combo=0;
      burst(ball.x,paddle.y,'#9affef',9,.65);
      ring(ball.x,paddle.y,'#65f4df');
      tone(260+Math.abs(offset)*180,.09,'sine',.055,1.6);
    }

    for (const b of bricks) {
      if (b.hp<=0) continue;
      const cx=clamp(ball.x,b.x,b.x+b.w), cy=clamp(ball.y,b.y,b.y+b.h);
      const dx=ball.x-cx, dy=ball.y-cy;
      if (dx*dx+dy*dy>ball.r*ball.r) continue;
      let nx=0,ny=0;
      if (prevY+ball.r<=b.y) ny=-1;
      else if (prevY-ball.r>=b.y+b.h) ny=1;
      else if (prevX+ball.r<=b.x) nx=-1;
      else if (prevX-ball.r>=b.x+b.w) nx=1;
      else if (Math.abs(dx)+Math.abs(dy)>0) {
        const length=Math.hypot(dx,dy); nx=dx/length; ny=dy/length;
      } else ny=ball.vy>0?-1:1;
      const dot=ball.vx*nx+ball.vy*ny;
      if (dot>=0) continue;
      ball.vx-=2*dot*nx; ball.vy-=2*dot*ny;
      if (nx===-1) ball.x=b.x-ball.r-.1;
      else if(nx===1) ball.x=b.x+b.w+ball.r+.1;
      if (ny===-1) ball.y=b.y-ball.r-.1;
      else if(ny===1) ball.y=b.y+b.h+ball.r+.1;
      hitBrick(b);
      if (bricks.every(brick=>brick.hp<=0)) completeLevel();
      break;
    }
    if (state==='playing' && ball.y>H+20) loseLife();
  }

  function effects(dt) {
    shake=Math.max(0,shake-dt*24);
    flash=Math.max(0,flash-dt);
    for (const b of bricks) b.hit=Math.max(0,b.hit-dt);
    for (const p of particles) {
      p.life-=dt; p.x+=p.vx*dt; p.y+=p.vy*dt;
      p.vy+=220*dt; p.vx*=Math.exp(-2*dt);
    }
    particles=particles.filter(p=>p.life>0);
    for (const t of texts) { t.life-=dt; t.y-=32*dt; }
    texts=texts.filter(t=>t.life>0);
    for (const r of rings) r.life-=dt;
    rings=rings.filter(r=>r.life>0);
  }

  function draw() {
    ctx.setTransform(canvas.width/W,0,0,canvas.height/H,0,0);
    ctx.fillStyle='#0a1120'; ctx.fillRect(0,0,W,H);
    ctx.save();
    if (shake && !reducedMotion) ctx.translate(random(-shake,shake),random(-shake,shake));

    const bg=ctx.createRadialGradient(W/2,180,30,W/2,230,570);
    bg.addColorStop(0,'#172340'); bg.addColorStop(1,'#090f1c');
    ctx.fillStyle=bg; ctx.fillRect(0,0,W,H);
    ctx.fillStyle='#a8caff12';
    for (let x=24;x<W;x+=30) for(let y=24;y<H;y+=30) ctx.fillRect(x,y,1,1);

    ctx.textAlign='left'; ctx.font='700 10px system-ui';
    ctx.fillStyle='#7185a8'; ctx.fillText(`0${level+1} / ${names[level].toUpperCase()}`,40,42);
    ctx.textAlign='right';
    const remaining=bricks.filter(b=>b.hp>0).length;
    ctx.fillText(`${remaining} BRICKS LEFT`,W-40,42);
    ctx.fillStyle='#ffffff06'; ctx.fillRect(40,56,W-80,1);

    for (const b of bricks) {
      if (b.hp<=0) continue;
      ctx.shadowColor=b.color; ctx.shadowBlur=b.hit>0?22:7;
      ctx.fillStyle=b.color; ctx.globalAlpha=b.hp===2?.92:.82;
      roundRect(b.x,b.y,b.w,b.h,4); ctx.fill();
      ctx.shadowBlur=0; ctx.globalAlpha=1;
      const shine=ctx.createLinearGradient(0,b.y,0,b.y+b.h);
      shine.addColorStop(0,'#ffffff40'); shine.addColorStop(.5,'#ffffff00'); shine.addColorStop(1,'#00000020');
      ctx.fillStyle=shine; roundRect(b.x,b.y,b.w,b.h,4); ctx.fill();
      ctx.fillStyle='#ffffff65'; roundRect(b.x+3,b.y+2,b.w-6,2,1); ctx.fill();
      if (b.hp===2) {
        ctx.strokeStyle='#ffffff80'; ctx.lineWidth=1;
        roundRect(b.x+4,b.y+4,b.w-8,b.h-8,2); ctx.stroke();
        ctx.fillStyle='#ffffffbb'; ctx.fillRect(b.x+b.w/2-2,b.y+b.h/2-2,4,4);
      }
      if (b.maxHp===2 && b.hp===1) {
        ctx.strokeStyle='#14273d88'; ctx.lineWidth=1.5;
        ctx.beginPath(); ctx.moveTo(b.x+b.w*.55,b.y);
        ctx.lineTo(b.x+b.w*.43,b.y+10); ctx.lineTo(b.x+b.w*.62,b.y+15);
        ctx.lineTo(b.x+b.w*.5,b.y+b.h); ctx.stroke();
      }
      if (b.hit>0) {
        ctx.globalAlpha=b.hit/.15; ctx.fillStyle='#fff';
        roundRect(b.x,b.y,b.w,b.h,4); ctx.fill(); ctx.globalAlpha=1;
      }
    }

    ctx.strokeStyle='#ff6e8c22'; ctx.setLineDash([4,9]);
    ctx.beginPath(); ctx.moveTo(15,H-14); ctx.lineTo(W-15,H-14); ctx.stroke();
    ctx.setLineDash([]);

    if (!reducedMotion) {
      for (let i=0;i<ball.trail.length;i++) {
        const p=ball.trail[i], f=i/ball.trail.length;
        ctx.globalAlpha=f*.24; ctx.fillStyle='#9af9ff';
        ctx.beginPath(); ctx.arc(p.x,p.y,ball.r*f,0,Math.PI*2); ctx.fill();
      }
      ctx.globalAlpha=1;
    }

    for (const r of rings) {
      const f=1-r.life/.4;
      ctx.globalAlpha=(1-f)*.6; ctx.strokeStyle=r.color; ctx.lineWidth=2;
      ctx.beginPath(); ctx.arc(r.x,r.y,8+f*35,0,Math.PI*2); ctx.stroke();
    }
    ctx.globalAlpha=1;

    ctx.shadowBlur=18+paddle.glow*15; ctx.shadowColor='#65f4df';
    const pg=ctx.createLinearGradient(0,paddle.y,0,paddle.y+paddle.h);
    pg.addColorStop(0,'#d7fff8'); pg.addColorStop(.4,'#7bffe4'); pg.addColorStop(1,'#31bda7');
    ctx.fillStyle=pg;
    roundRect(paddle.x-paddle.w/2,paddle.y,paddle.w,paddle.h,7); ctx.fill();
    ctx.shadowBlur=0;
    ctx.fillStyle='#ffffff99';
    roundRect(paddle.x-paddle.w/2+10,paddle.y+2,paddle.w-20,2,1); ctx.fill();
    ctx.fillStyle='#0e6b6c77';
    for(let i=-1;i<=1;i++) ctx.fillRect(paddle.x+i*5-1,paddle.y+6,2,4);

    if (state!=='over' && state!=='win' && state!=='clear') {
      ctx.shadowColor='#9af9ff'; ctx.shadowBlur=22;
      ctx.fillStyle='#fff';
      ctx.beginPath(); ctx.arc(ball.x,ball.y,ball.r,0,Math.PI*2); ctx.fill();
      ctx.shadowBlur=0;
    }

    for (const p of particles) {
      ctx.globalAlpha=clamp(p.life/p.max,0,1);
      ctx.fillStyle=p.color;
      ctx.fillRect(p.x-p.size/2,p.y-p.size/2,p.size,p.size);
    }
    ctx.globalAlpha=1;
    ctx.textAlign='center'; ctx.font='800 13px system-ui';
    for (const t of texts) {
      ctx.globalAlpha=Math.min(1,t.life*2);
      ctx.fillStyle=t.color; ctx.fillText(t.text,t.x,t.y);
    }
    ctx.globalAlpha=1;
    if (flash>0) {
      ctx.fillStyle=`rgba(255,100,130,${flash*.7})`; ctx.fillRect(0,0,W,H);
    }
    ctx.restore();
  }

  function frame(now) {
    const dt=Math.min((now-lastTime)/1000 || 0, .035);
    lastTime=now; time+=dt;
    if (state==='playing') {
      let left=dt;
      while(left>0 && state==='playing') {
        const slice=Math.min(left,1/180);
        step(slice); left-=slice;
      }
      if (state==='playing') {
        ball.trail.push({x:ball.x,y:ball.y});
        if (ball.trail.length>13) ball.trail.shift();
      }
    } else if (state==='serve'||state==='intro') {
      ball.x=paddle.x; ball.y=paddle.y-ball.r-2;
    }
    if (state!=='paused') effects(dt);
    draw();
    requestAnimationFrame(frame);
  }

  arena.addEventListener('pointermove', e => {
    if (state==='paused') return;
    const bounds=canvas.getBoundingClientRect();
    targetX=clamp((e.clientX-bounds.left)/bounds.width*W,0,W);
    inputMode='pointer';
  });
  canvas.addEventListener('pointerdown', e => {
    e.preventDefault(); awakenAudio();
    if (state==='intro'||state==='serve') primaryAction();
    if (e.pointerType!=='mouse') {
      const bounds=canvas.getBoundingClientRect();
      targetX=(e.clientX-bounds.left)/bounds.width*W;
      inputMode='pointer';
    }
  });

  window.addEventListener('keydown', e => {
    const k=e.key.toLowerCase();
    if (['arrowleft','arrowright','a','d',' ','p','escape'].includes(k)) {
      // Preserve normal keyboard activation of focused buttons.
      if (k===' ' && document.activeElement?.tagName==='BUTTON') return;
      e.preventDefault();
    }
    if (['arrowleft','arrowright','a','d'].includes(k)) {
      keys.add(k); inputMode='keyboard';
    }
    if (e.repeat) return;
    if (k===' ') {
      if (state==='playing') togglePause();
      else primaryAction();
    }
    if (k==='p'||k==='escape') togglePause();
  });
  window.addEventListener('keyup', e=>keys.delete(e.key.toLowerCase()));
  window.addEventListener('blur', () => {
    keys.clear();
    if(state==='playing') togglePause();
  });
  document.addEventListener('visibilitychange', () => {
    if(document.hidden && state==='playing') togglePause();
  });

  ui.action.addEventListener('click',primaryAction);
  ui.pause.addEventListener('click',togglePause);
  document.getElementById('restart').addEventListener('click',newGame);
  ui.sound.addEventListener('click', () => {
    if (!soundOn) {
      try {
        audio=audio || new (window.AudioContext || window.webkitAudioContext)();
        soundOn=true; awakenAudio(); tone(660,.1,'sine',.04,1.5);
      } catch (_) { soundOn=false; }
    } else soundOn=false;
    ui.sound.textContent=soundOn?'Sound on':'Sound off';
    ui.sound.setAttribute('aria-pressed',String(soundOn));
    ui.sound.setAttribute('aria-label',soundOn?'Disable sound':'Enable sound');
  });

  buildLevel();
  requestAnimationFrame(frame);
})();
</script>
</body>
</html>

Compare with

GPT-6.1 Sol: other attempts