hypeorcode
Network offOpen alone (new tab)

Run details

Model
opencode/nemotron-3-ultra-free
Route
opencode CLI
Reasoning
provider default
Limits
60 min · 128,000 output tokens
Date
2 Oct 2026
Time
2 min 31 s
Size
21 kB
Tokens
39,891 in, 7,848 out
List price
$0.043 ($0.6 in / $2.4 out per M)
Paid
Not billed per run (CLI plan)

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). Save it as index.html in the current directory, then stop. Do not ask questions.

Source

21 kB 610 lines Raw

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Breakout</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
    background: #1a1a2e;
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    min-height: 100vh;
    font-family: 'Courier New', monospace;
    color: #eee;
    overflow: hidden;
}
#game-container { position: relative; }
canvas { display: block; border: 3px solid #0f3460; border-radius: 8px; box-shadow: 0 0 30px rgba(0,255,200,0.3), inset 0 0 30px rgba(0,0,0,0.5); background: #0d0d1a; }
#ui {
    position: absolute;
    top: -60px;
    left: 0;
    right: 0;
    display: flex;
    justify-content: space-between;
    padding: 0 10px;
    font-size: 18px;
    font-weight: bold;
    text-shadow: 0 0 10px #00ffc8, 0 0 20px #00ffc8;
}
#ui span { color: #00ffc8; }
#start-screen, #game-over-screen, #level-complete-screen, #victory-screen {
    position: absolute;
    top: 50%;
    left: 50%;
    transform: translate(-50%, -50%);
    text-align: center;
    z-index: 10;
    background: rgba(13,13,26,0.95);
    padding: 40px 60px;
    border-radius: 16px;
    border: 3px solid #00ffc8;
    box-shadow: 0 0 40px rgba(0,255,200,0.4), inset 0 0 40px rgba(0,255,200,0.1);
}
.hidden { display: none !important; }
h1 { font-size: 48px; margin-bottom: 10px; background: linear-gradient(90deg, #00ffc8, #00aaff, #ff00aa); -webkit-background-clip: text; -webkit-text-fill-color: transparent; text-shadow: none; animation: glow 2s ease-in-out infinite; }
@keyframes glow { 0%,100% { filter: brightness(1); } 50% { filter: brightness(1.3); } }
p { margin: 15px 0; color: #aaa; font-size: 16px; }
button {
    background: linear-gradient(135deg, #00ffc8, #00aaff);
    border: none;
    padding: 14px 40px;
    font-size: 18px;
    font-weight: bold;
    color: #0d0d1a;
    border-radius: 8px;
    cursor: pointer;
    transition: all 0.2s;
    text-transform: uppercase;
    letter-spacing: 2px;
    box-shadow: 0 4px 20px rgba(0,255,200,0.4);
}
button:hover { transform: scale(1.05); box-shadow: 0 6px 30px rgba(0,255,200,0.6); }
button:active { transform: scale(0.98); }
#level-display { font-size: 24px; color: #ffaa00; margin: 20px 0; text-shadow: 0 0 15px #ffaa00; }
#final-score { font-size: 32px; color: #00ffc8; margin: 20px 0; text-shadow: 0 0 20px #00ffc8; }
.controls-hint { font-size: 13px; color: #666; margin-top: 20px; line-height: 1.8; }
.controls-hint kbd {
    background: #2a2a4a;
    border: 1px solid #0f3460;
    padding: 2px 8px;
    border-radius: 4px;
    color: #00ffc8;
    font-family: inherit;
}
</style>
</head>
<body>
<div id="game-container">
    <canvas id="canvas" width="800" height="600"></canvas>
    <div id="ui">
        <div>SCORE: <span id="score">0</span></div>
        <div>LEVEL: <span id="level">1</span></div>
        <div>LIVES: <span id="lives">3</span></div>
    </div>
    <div id="start-screen">
        <h1>BREAKOUT</h1>
        <p>Destroy all bricks to advance</p>
        <button id="start-btn">START GAME</button>
        <div class="controls-hint">
            <kbd>←</kbd> <kbd>→</kbd> or <kbd>Mouse</kbd> to move paddle<br>
            <kbd>Space</kbd> or <kbd>Click</kbd> to launch ball<br>
            <kbd>P</kbd> to pause
        </div>
    </div>
    <div id="level-complete-screen" class="hidden">
        <h1>LEVEL COMPLETE!</h1>
        <div id="level-bonus"></div>
        <button id="next-level-btn">NEXT LEVEL</button>
    </div>
    <div id="game-over-screen" class="hidden">
        <h1>GAME OVER</h1>
        <p>Final Score: <span id="final-score">0</span></p>
        <button id="restart-btn">PLAY AGAIN</button>
    </div>
    <div id="victory-screen" class="hidden">
        <h1>YOU WIN!</h1>
        <p>Congratulations! You cleared all levels!</p>
        <p>Final Score: <span id="victory-score">0</span></p>
        <button id="victory-restart-btn">PLAY AGAIN</button>
    </div>
</div>
<script>
const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d');
const W = canvas.width, H = canvas.height;

const PADDLE_W = 100, PADDLE_H = 16, PADDLE_Y = H - 40, PADDLE_SPEED = 9;
const BALL_R = 9, BALL_BASE_SPEED = 5.5;
const BRICK_ROWS = 5, BRICK_COLS = 12, BRICK_W = 60, BRICK_H = 22, BRICK_PAD = 4, BRICK_TOP = 60;

const COLORS = [
    '#ff3366', '#ff9933', '#ffff33', '#33ff66', '#3399ff', '#9933ff', '#ff33cc', '#00ffcc'
];
const BRICK_COLORS = [
    ['#ff3366','#ff6688'], ['#ff9933','#ffbb66'], ['#ffff33','#ffff66'],
    ['#33ff66','#66ff88'], ['#3399ff','#66bbff']
];

let paddle = { x: W/2 - PADDLE_W/2, w: PADDLE_W };
let balls = [];
let bricks = [];
let particles = [];
let score = 0, level = 1, lives = 3;
let gameState = 'start'; // start, playing, paused, levelComplete, gameOver, victory
let mouseX = W/2;
let paddleTargetX = W/2 - PADDLE_W/2;
let screenShake = 0;
let combo = 0, lastHitTime = 0;

const KEYS = { left: false, right: false, space: false };
const POWERUP_TYPES = ['expand', 'multi', 'slow', 'life', 'speed'];
let powerups = [];

function rand(min, max) { return Math.random() * (max - min) + min; }
function randInt(min, max) { return Math.floor(Math.random() * (max - min + 1)) + min; }

function createBricks() {
    bricks = [];
    const layouts = [
        null,
        Array(BRICK_ROWS).fill().map((_,r) => Array(BRICK_COLS).fill().map((_,c) => ({r,c, hp: 1}))),
        Array(BRICK_ROWS).fill().map((_,r) => Array(BRICK_COLS).fill().map((_,c) => ({r,c, hp: r < 2 ? 2 : 1}))),
        Array(BRICK_ROWS).fill().map((_,r) => Array(BRICK_COLS).fill().map((_,c) => ({r,c, hp: c % 3 === 0 ? 2 : 1}))),
        Array(BRICK_ROWS).fill().map((_,r) => Array(BRICK_COLS).fill().map((_,c) => ({r,c, hp: (r+c)%2===0 ? 2 : 1}))),
        Array(6).fill().map((_,r) => Array(14).fill().map((_,c) => ({r,c, hp: r < 3 ? 3 : 2})))
    ];
    const layout = layouts[Math.min(level, layouts.length-1)] || layouts[1];
    const rows = layout.length, cols = layout[0].length;
    const startX = (W - (cols * (BRICK_W + BRICK_PAD) - BRICK_PAD)) / 2;
    for (let r = 0; r < rows; r++) {
        for (let c = 0; c < cols; c++) {
            const b = layout[r][c];
            if (b) {
                const colors = BRICK_COLORS[Math.min(r, BRICK_COLORS.length-1)];
                bricks.push({
                    x: startX + c * (BRICK_W + BRICK_PAD),
                    y: BRICK_TOP + r * (BRICK_H + BRICK_PAD),
                    w: BRICK_W, h: BRICK_H,
                    hp: b.hp, maxHp: b.hp,
                    color: colors[0], colorLight: colors[1],
                    points: 10 * b.hp,
                    alive: true
                });
            }
        }
    }
}

function spawnBall(x, y, vx, vy) {
    balls.push({ x, y, r: BALL_R, vx, vy, trail: [], speedMult: 1, glow: 0 });
}

function launchBall() {
    if (balls.length === 0) {
        spawnBall(paddle.x + paddle.w/2, PADDLE_Y - BALL_R, rand(-0.5, 0.5) * BALL_BASE_SPEED, -BALL_BASE_SPEED);
    } else {
        balls.forEach(b => { if (b.vy === 0) b.vy = -BALL_BASE_SPEED; });
    }
}

function addParticles(x, y, color, count, speed, size, life) {
    for (let i = 0; i < count; i++) {
        const angle = rand(0, Math.PI * 2);
        const spd = rand(speed * 0.5, speed);
        particles.push({
            x, y, vx: Math.cos(angle) * spd, vy: Math.sin(angle) * spd,
            color, size: rand(size * 0.5, size), life: rand(life * 0.5, life), maxLife: life
        });
    }
}

function addScreenShake(intensity) { screenShake = Math.max(screenShake, intensity); }

function spawnPowerup(x, y) {
    if (Math.random() < 0.15) {
        powerups.push({
            x, y, w: 24, h: 24, vy: 2,
            type: POWERUP_TYPES[randInt(0, POWERUP_TYPES.length-1)],
            life: 1, rotation: 0
        });
    }
}

function applyPowerup(type) {
    switch(type) {
        case 'expand': paddle.w = Math.min(paddle.w + 30, 200); break;
        case 'multi': {
            const currentBalls = balls.length;
            for (let i = 0; i < currentBalls; i++) {
                const b = balls[i];
                spawnBall(b.x, b.y, b.vx + rand(-2,2), b.vy + rand(-2,2));
                spawnBall(b.x, b.y, b.vx + rand(-2,2), b.vy + rand(-2,2));
            }
            break;
        }
        case 'slow': balls.forEach(b => b.speedMult = 0.6); setTimeout(() => balls.forEach(b => b.speedMult = 1), 8000); break;
        case 'life': lives = Math.min(lives + 1, 5); updateUI(); break;
        case 'speed': balls.forEach(b => b.speedMult = 1.5); setTimeout(() => balls.forEach(b => b.speedMult = 1), 5000); break;
    }
    addParticles(paddle.x + paddle.w/2, PADDLE_Y, '#00ffc8', 30, 8, 6, 40);
}

function updateUI() {
    document.getElementById('score').textContent = score;
    document.getElementById('level').textContent = level;
    document.getElementById('lives').textContent = lives;
}

function resetGame() {
    score = 0; level = 1; lives = 3;
    paddle = { x: W/2 - PADDLE_W/2, w: PADDLE_W };
    balls = []; particles = []; powerups = [];
    combo = 0;
    createBricks();
    updateUI();
}

function nextLevel() {
    level++;
    paddle = { x: W/2 - PADDLE_W/2, w: PADDLE_W };
    balls = []; particles = []; powerups = [];
    createBricks();
    updateUI();
    gameState = 'playing';
}

function drawPaddle() {
    const g = ctx.createLinearGradient(paddle.x, PADDLE_Y, paddle.x, PADDLE_Y + PADDLE_H);
    g.addColorStop(0, '#00ffc8'); g.addColorStop(1, '#0088aa');
    ctx.fillStyle = g;
    ctx.shadowColor = '#00ffc8'; ctx.shadowBlur = 15;
    roundRect(ctx, paddle.x, PADDLE_Y, paddle.w, PADDLE_H, 8);
    ctx.fill(); ctx.shadowBlur = 0;
    
    ctx.strokeStyle = 'rgba(255,255,255,0.3)';
    ctx.lineWidth = 2;
    roundRect(ctx, paddle.x, PADDLE_Y, paddle.w, PADDLE_H, 8);
    ctx.stroke();
}

function drawBall(b) {
    b.trail.unshift({ x: b.x, y: b.y });
    if (b.trail.length > 8) b.trail.pop();
    
    b.trail.forEach((p, i) => {
        const alpha = (i / b.trail.length) * 0.4;
        ctx.beginPath();
        ctx.arc(p.x, p.y, b.r * (i / b.trail.length), 0, Math.PI * 2);
        ctx.fillStyle = `rgba(0, 255, 200, ${alpha})`;
        ctx.fill();
    });
    
    const g = ctx.createRadialGradient(b.x, b.y, 0, b.x, b.y, b.r);
    g.addColorStop(0, '#ffffff');
    g.addColorStop(0.5, '#00ffc8');
    g.addColorStop(1, '#0088aa');
    ctx.fillStyle = g;
    ctx.shadowColor = '#00ffc8';
    ctx.shadowBlur = b.glow > 0 ? 25 : 15;
    ctx.beginPath();
    ctx.arc(b.x, b.y, b.r, 0, Math.PI * 2);
    ctx.fill();
    ctx.shadowBlur = 0;
    
    if (b.glow > 0) b.glow--;
}

function drawBrick(b) {
    if (!b.alive) return;
    const hpRatio = b.hp / b.maxHp;
    ctx.fillStyle = b.color;
    ctx.shadowColor = b.color; ctx.shadowBlur = 8;
    roundRect(ctx, b.x, b.y, b.w, b.h, 4);
    ctx.fill();
    ctx.shadowBlur = 0;
    
    ctx.strokeStyle = b.colorLight;
    ctx.lineWidth = 2;
    roundRect(ctx, b.x + 1, b.y + 1, b.w - 2, b.h - 2, 3);
    ctx.stroke();
    
    if (b.maxHp > 1) {
        ctx.fillStyle = 'rgba(255,255,255,0.9)';
        ctx.font = 'bold 12px monospace';
        ctx.textAlign = 'center';
        ctx.fillText(b.hp, b.x + b.w/2, b.y + b.h/2 + 4);
    }
}

function drawPowerup(p) {
    p.rotation += 0.03;
    p.life -= 0.005;
    const colors = { expand: '#00ff66', multi: '#ff33cc', slow: '#33aaff', life: '#ff3366', speed: '#ffaa00' };
    ctx.save();
    ctx.translate(p.x + p.w/2, p.y + p.h/2);
    ctx.rotate(p.rotation);
    ctx.fillStyle = colors[p.type];
    ctx.shadowColor = colors[p.type]; ctx.shadowBlur = 15;
    ctx.beginPath();
    for (let i = 0; i < 5; i++) {
        const a = (i * 4 * Math.PI / 5) - Math.PI/2;
        const r = i % 2 === 0 ? p.w/2 : p.w/4;
        ctx.lineTo(Math.cos(a) * r, Math.sin(a) * r);
    }
    ctx.closePath(); ctx.fill(); ctx.shadowBlur = 0;
    ctx.restore();
}

function drawParticles() {
    particles.forEach(p => {
        const alpha = p.life / p.maxLife;
        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;
}

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

function rectCircleCollide(rx, ry, rw, rh, cx, cy, cr) {
    const nearestX = Math.max(rx, Math.min(cx, rx + rw));
    const nearestY = Math.max(ry, Math.min(cy, ry + rh));
    const dx = cx - nearestX, dy = cy - nearestY;
    return dx*dx + dy*dy < cr*cr;
}

function getCollisionSide(rx, ry, rw, rh, cx, cy, cr, vx, vy) {
    const halfW = rw/2, halfH = rh/2;
    const centerX = rx + halfW, centerY = ry + halfH;
    const dx = cx - centerX, dy = cy - centerY;
    const overlapX = halfW + cr - Math.abs(dx);
    const overlapY = halfH + cr - Math.abs(dy);
    if (overlapX < overlapY) return dx > 0 ? 'left' : 'right';
    return dy > 0 ? 'top' : 'bottom';
}

function update(dt) {
    if (gameState !== 'playing') return;
    
    if (KEYS.left) paddleTargetX -= PADDLE_SPEED * dt * 60;
    if (KEYS.right) paddleTargetX += PADDLE_SPEED * dt * 60;
    
    paddleTargetX = Math.max(0, Math.min(W - paddle.w, paddleTargetX));
    paddle.x += (paddleTargetX - paddle.x) * 0.25;
    
    balls.forEach((b, bi) => {
        b.vy = Math.max(-15, Math.min(15, b.vy));
        b.vx = Math.max(-12, Math.min(12, b.vx));
        
        const speed = BALL_BASE_SPEED * b.speedMult;
        const len = Math.sqrt(b.vx*b.vx + b.vy*b.vy);
        if (len > 0) { b.vx = (b.vx/len) * speed; b.vy = (b.vy/len) * speed; }
        
        b.x += b.vx * dt * 60;
        b.y += b.vy * dt * 60;
        
        if (b.x - b.r < 0) { b.x = b.r; b.vx = Math.abs(b.vx); addScreenShake(3); }
        if (b.x + b.r > W) { b.x = W - b.r; b.vx = -Math.abs(b.vx); addScreenShake(3); }
        if (b.y - b.r < 0) { b.y = b.r; b.vy = Math.abs(b.vy); addScreenShake(3); }
        
        if (b.y + b.r > PADDLE_Y && b.y - b.r < PADDLE_Y + PADDLE_H && b.vy > 0) {
            if (b.x > paddle.x && b.x < paddle.x + paddle.w) {
                b.y = PADDLE_Y - b.r;
                const hitPos = (b.x - paddle.x) / paddle.w - 0.5;
                b.vx = hitPos * 10;
                b.vy = -Math.abs(b.vy);
                b.glow = 10;
                addParticles(b.x, b.y, '#00ffc8', 8, 5, 4, 20);
                combo = 0;
            }
        }
        
        if (b.y > H + 50) {
            balls.splice(bi, 1);
            if (balls.length === 0) {
                lives--;
                updateUI();
                addScreenShake(15);
                addParticles(paddle.x + paddle.w/2, PADDLE_Y, '#ff3366', 40, 10, 8, 60);
                if (lives <= 0) {
                    gameState = 'gameOver';
                    document.getElementById('final-score').textContent = score;
                    document.getElementById('game-over-screen').classList.remove('hidden');
                } else {
                    setTimeout(() => launchBall(), 1000);
                }
            }
        }
        
        bricks.forEach(brick => {
            if (!brick.alive) return;
            if (rectCircleCollide(brick.x, brick.y, brick.w, brick.h, b.x, b.y, b.r)) {
                const side = getCollisionSide(brick.x, brick.y, brick.w, brick.h, b.x, b.y, b.r, b.vx, b.vy);
                if (side === 'left' || side === 'right') b.vx = -b.vx;
                else b.vy = -b.vy;
                
                brick.hp--;
                const now = Date.now();
                if (now - lastHitTime < 500) combo++; else combo = 1;
                lastHitTime = now;
                
                const points = brick.points + combo * 50;
                score += points;
                updateUI();
                
                addParticles(b.x, b.y, brick.color, 20, 8, 6, 40);
                addScreenShake(5);
                b.glow = 15;
                
                if (brick.hp <= 0) {
                    brick.alive = false;
                    addParticles(brick.x + brick.w/2, brick.y + brick.h/2, brick.color, 40, 12, 8, 60);
                    spawnPowerup(brick.x + brick.w/2, brick.y + brick.h/2);
                }
            }
        });
    });
    
    powerups.forEach((p, i) => {
        p.y += p.vy * dt * 60;
        if (p.y > H) powerups.splice(i, 1);
        else if (p.y + p.h > PADDLE_Y && p.y < PADDLE_Y + PADDLE_H && p.x > paddle.x && p.x < paddle.x + paddle.w) {
            applyPowerup(p.type);
            powerups.splice(i, 1);
        }
    });
    
    particles.forEach((p, i) => {
        p.x += p.vx * dt * 60;
        p.y += p.vy * dt * 60;
        p.vy += 0.15 * dt * 60;
        p.life -= dt * 60;
        if (p.life <= 0) particles.splice(i, 1);
    });
    
    const aliveBricks = bricks.filter(b => b.alive).length;
    if (aliveBricks === 0) {
        if (level >= 5) {
            gameState = 'victory';
            document.getElementById('victory-score').textContent = score;
            document.getElementById('victory-screen').classList.remove('hidden');
        } else {
            gameState = 'levelComplete';
            document.getElementById('level-bonus').innerHTML = `Bonus: <span style="color:#00ffc8">${level * 1000}</span>`;
            score += level * 1000;
            updateUI();
            document.getElementById('level-complete-screen').classList.remove('hidden');
        }
    }
}

function draw() {
    ctx.save();
    if (screenShake > 0) {
        ctx.translate(rand(-screenShake, screenShake), rand(-screenShake, screenShake));
        screenShake *= 0.85;
        if (screenShake < 0.5) screenShake = 0;
    }
    
    ctx.fillStyle = '#0d0d1a';
    ctx.fillRect(0, 0, W, H);
    
    const grad = ctx.createLinearGradient(0, 0, 0, H);
    grad.addColorStop(0, 'rgba(0,255,200,0.03)');
    grad.addColorStop(1, 'rgba(0,170,255,0.02)');
    ctx.fillStyle = grad;
    ctx.fillRect(0, 0, W, H);
    
    for (let i = 0; i < 50; i++) {
        const x = (i * 173 + Date.now() * 0.01) % W;
        const y = (i * 239 + Date.now() * 0.005) % H;
        ctx.fillStyle = `rgba(0,255,200,${0.05 + Math.sin(Date.now()*0.001 + i)*0.05})`;
        ctx.beginPath(); ctx.arc(x, y, 1, 0, Math.PI*2); ctx.fill();
    }
    
    bricks.forEach(drawBrick);
    powerups.forEach(drawPowerup);
    particles.forEach(drawParticles);
    balls.forEach(drawBall);
    drawPaddle();
    
    ctx.restore();
}

function gameLoop(ts) {
    const dt = Math.min((ts - (gameLoop.last || ts)) / 1000, 0.05);
    gameLoop.last = ts;
    update(dt);
    draw();
    requestAnimationFrame(gameLoop);
}

function showScreen(id) {
    ['start-screen','level-complete-screen','game-over-screen','victory-screen'].forEach(s => {
        document.getElementById(s).classList.add('hidden');
    });
    document.getElementById(id).classList.remove('hidden');
}

document.getElementById('start-btn').onclick = () => {
    showScreen('start-screen');
    resetGame();
    launchBall();
    gameState = 'playing';
};

document.getElementById('next-level-btn').onclick = () => {
    nextLevel();
};

document.getElementById('restart-btn').onclick = () => {
    showScreen('start-screen');
    resetGame();
    launchBall();
    gameState = 'playing';
};

document.getElementById('victory-restart-btn').onclick = () => {
    showScreen('start-screen');
    resetGame();
    launchBall();
    gameState = 'playing';
};

canvas.addEventListener('mousemove', e => {
    const rect = canvas.getBoundingClientRect();
    mouseX = e.clientX - rect.left;
    paddleTargetX = mouseX - paddle.w/2;
});

canvas.addEventListener('click', () => { if (gameState === 'playing') launchBall(); });

window.addEventListener('keydown', e => {
    if (e.code === 'ArrowLeft' || e.code === 'KeyA') KEYS.left = true;
    if (e.code === 'ArrowRight' || e.code === 'KeyD') KEYS.right = true;
    if (e.code === 'Space') { KEYS.space = true; if (gameState === 'playing') launchBall(); }
    if (e.code === 'KeyP') gameState = gameState === 'playing' ? 'paused' : 'playing';
});

window.addEventListener('keyup', e => {
    if (e.code === 'ArrowLeft' || e.code === 'KeyA') KEYS.left = false;
    if (e.code === 'ArrowRight' || e.code === 'KeyD') KEYS.right = false;
    if (e.code === 'Space') KEYS.space = false;
});

canvas.addEventListener('touchmove', e => {
    e.preventDefault();
    const rect = canvas.getBoundingClientRect();
    mouseX = e.touches[0].clientX - rect.left;
    paddleTargetX = mouseX - paddle.w/2;
}, { passive: false });

canvas.addEventListener('touchstart', e => {
    e.preventDefault();
    if (gameState === 'playing') launchBall();
}, { passive: false });

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

Compare with

Nemotron 3 Ultra: other attempts