• Updated 2 Nov, 13:50 UTC
  • Next run 3 Nov, 05:17 UTCNext 3 Nov 05:17
  • Last 7 days: 3 new models · 113 runs7 d: 3 models · 113 runs
Playable Breakout

Ternary Bonsai 2 9BPrismML

Network offOpen alone (new tab)

Run details

Model
prism-ml/ternary-bonsai-2-9b
Route
OpenRouter API, one user message
Served by
Darkbloom
Effort
the model's default
Limits
60 min · 65,536 output tokens (the model's limit)
Date
1 Nov 2026
Time
1 min 44 s
Size
25 kB
Tokens
87 in, 7,702 out (810 of them thinking)
List price
$0.001 at today's price ($0.08 in / $0.13 out per M)
Paid
$0.001

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

25 kB 892 lines Raw

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Neon Breakout</title>
    <style>
        :root {
            --bg-color: #1a1a2e;
            --grid-color: #16213e;
            --primary: #0f3460;
            --accent: #e94560;
            --text-color: #ffffff;
        }

        body {
            margin: 0;
            background-color: var(--bg-color);
            color: var(--text-color);
            font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
            overflow: hidden;
            display: flex;
            flex-direction: column;
            align-items: center;
            justify-content: center;
            height: 100vh;
        }

        #game-container {
            position: relative;
            box-shadow: 0 0 20px rgba(233, 69, 96, 0.2);
            border: 2px solid var(--accent);
        }

        canvas {
            background-color: var(--grid-color);
            display: block;
            border-radius: 8px;
        }

        #ui-layer {
            position: absolute;
            top: 0;
            left: 0;
            width: 100%;
            height: 100%;
            pointer-events: none;
            display: flex;
            flex-direction: column;
            justify-content: space-between;
            padding: 10px;
            box-sizing: border-box;
        }

        .score-board {
            font-size: 2rem;
            font-weight: bold;
            text-shadow: 0 0 10px var(--accent);
        }

        .level-indicator {
            font-size: 1.2rem;
            color: #0f3460;
            margin-top: 5px;
            font-weight: bold;
        }

        .controls-hint {
            position: absolute;
            bottom: 10px;
            font-size: 0.8rem;
            color: #aaa;
            pointer-events: none;
        }

        #overlay {
            position: absolute;
            top: 0;
            left: 0;
            width: 100%;
            height: 100%;
            background: rgba(0,0,0,0.85);
            display: flex;
            flex-direction: column;
            align-items: center;
            justify-content: center;
            font-size: 1.5rem;
            z-index: 10;
            transition: opacity 0.3s;
            pointer-events: none;
        }

        #overlay.hidden {
            opacity: 0;
            pointer-events: auto;
        }

        #overlay h1 {
            margin-bottom: 10px;
            color: #fff;
            text-shadow: 0 0 15px var(--accent);
        }

        button {
            padding: 10px 20px;
            font-size: 1rem;
            background: var(--accent);
            color: white;
            border: none;
            border-radius: 5px;
            cursor: pointer;
            pointer-events: auto;
            box-shadow: 0 0 10px var(--accent);
            transition: transform 0.1s, box-shadow 0.1s;
        }

        button:active {
            transform: scale(0.95);
        }

        button:disabled {
            opacity: 0.5;
            cursor: not-allowed;
        }

        .hidden {
            display: none !important;
        }
    </style>
</head>
<body>

    <div id="game-container">
        <canvas id="gameCanvas"></canvas>
        
        <div id="ui-layer">
            <div class="score-board" id="scoreDisplay">Score: 0</div>
            <div class="level-indicator" id="levelDisplay">Level: 1</div>
        </div>

        <div id="controls-hint">MOUSE to move paddle | ARROW KEYS to shoot ball</div>

        <div id="overlay">
            <h1>NEON BREAKOUT</h1>
            <p>Destroy the bricks!</p>
            <button id="startBtn">START GAME</button>
        </div>
    </div>

<script>
/**
 * NEON BREAKOUT
 * A self-contained arcade game featuring particles, physics, and juice.
 */

const canvas = document.getElementById('gameCanvas');
const ctx = canvas.getContext('2d');

// Resize canvas
function resize() {
    canvas.width = window.innerWidth;
    canvas.height = window.innerHeight - 80; // Padding for UI
    ctx.fillStyle = '#16213e';
    ctx.fillRect(0, 0, canvas.width, canvas.height);
    updateUi();
}
window.addEventListener('resize', resize);
resize();

// --- Game State ---
const state = {
    running: false,
    paused: false,
    score: 0,
    level: 1,
    ballsInPlay: [],
    bricks: [],
    particles: [],
    gameOver: false
};

// --- Input ---
const paddle = {
    x: canvas.width / 2,
    y: canvas.height - 60,
    width: 100,
    height: 10,
    speed: 8,
    color: '#fff'
};

const inputs = {
    mouse: { x: canvas.width / 2, y: canvas.height },
    keys: {}
};

document.addEventListener('mousedown', (e) => {
    inputs.mouse.x = e.offsetX + canvas.getBoundingClientRect().left;
    inputs.mouse.y = e.offsetY;
});
document.addEventListener('mousemove', (e) => {
    inputs.mouse.x = e.offsetX + canvas.getBoundingClientRect().left;
    inputs.mouse.y = e.offsetY;
});
document.addEventListener('mouseup', () => {
    inputs.mouse.x = null;
    inputs.mouse.y = null;
});
window.addEventListener('keydown', (e) => inputs.keys[e.code] = true);
window.addEventListener('keyup', (e) => inputs.keys[e.code] = false);

// --- Game Objects ---

class Ball {
    constructor() {
        this.size = 10;
        this.x = canvas.width / 2;
        this.y = 50;
        this.speed = 6;
        this.dx = Math.random() > 0.5 ? this.speed : -this.speed;
        this.dy = Math.abs(Math.random() * this.speed);
        this.color = '#e94560';
        this.mass = 1;
        this.glow = 10; // Screen shake intensity when hit
    }

    update() {
        this.x += this.dx;
        this.y += this.dy;
    }

    draw() {
        // Outer Glow
        ctx.beginPath();
        ctx.arc(this.x, this.y, this.size + this.glow, 0, Math.PI * 2);
        ctx.fillStyle = 'rgba(233, 69, 96, 0.3)';
        ctx.fill();
        
        // Core
        ctx.beginPath();
        ctx.arc(this.x, this.y, this.size, 0, Math.PI * 2);
        ctx.fillStyle = this.color;
        ctx.shadowBlur = 10;
        ctx.shadowColor = this.color;
        ctx.fill();
        ctx.shadowBlur = 0;
    }

    hitBrick() {
        this.glow = 15;
    }
}

class Brick {
    constructor(x, y) {
        this.x = x;
        this.y = y;
        this.width = 40;
        this.height = 20;
        // Different colors per level (randomized for variety)
        const colors = ['#ff6b6b', '#4ecdc4', '#45b7d1', '#96ceb4', '#ffeaa7'];
        this.color = colors[Math.floor(Math.random() * colors.length)];
        this.hp = 1; // Number of hits to break
    }

    update() { }

    draw() {
        ctx.save();
        ctx.shadowBlur = 15;
        ctx.shadowColor = this.color;
        ctx.fillStyle = this.color;
        // Round corners
        ctx.beginPath();
        ctx.roundRect(this.x, this.y, this.width, this.height, 4);
        ctx.fill();
        ctx.shadowBlur = 0;
        
        // HP Bars
        if (this.hp > 1) {
            ctx.fillStyle = 'rgba(0,0,0,0.5)';
            ctx.fillRect(this.x, this.y, this.width, 4);
            ctx.fillStyle = '#333';
            for(let i=0; i<this.hp; i++) {
                ctx.fillRect(this.x + (i * (this.width / this.hp)) - 2, this.y + 4, 4, this.height - 8);
            }
        }
        ctx.restore();
    }

    isHit(x, y) {
        return x < this.x + this.width &&
               x > this.x &&
               y < this.y + this.height &&
               y > this.y;
    }
}

class Particle {
    constructor(x, y, color) {
        this.x = x;
        this.y = y;
        this.vx = (Math.random() - 0.5) * 8;
        this.vy = (Math.random() - 0.5) * 8;
        this.life = 1;
        this.decay = 0.015;
        this.color = color;
        this.size = Math.random() * 3 + 1;
    }

    update() {
        this.x += this.vx;
        this.y += this.vy;
        this.life -= this.decay;
        this.size *= 0.9;
    }

    draw() {
        ctx.globalAlpha = this.life;
        ctx.fillStyle = this.color;
        ctx.beginPath();
        ctx.arc(this.x, this.y, this.size, 0, Math.PI * 2);
        ctx.fill();
        ctx.globalAlpha = 1;
    }
}

class ScreenShake {
    constructor(limit) {
        this.limit = limit;
        this.x = 0;
        this.y = 0;
    }

    add(x, y) {
        this.x += x;
        this.y += y;
        if (this.x > this.limit || this.x < -this.limit) this.x = -this.x;
        if (this.y > this.limit || this.y < -this.limit) this.y = -this.y;
    }
}

// --- Game Loop ---

function initGame() {
    state.score = 0;
    state.level = 1;
    state.gameOver = false;
    state.ballsInPlay = [];
    state.particles = [];
    
    paddle.x = (canvas.width - paddle.width) / 2;
    paddle.y = canvas.height - 60;

    createBricks(1);
    
    document.getElementById('startBtn').style.display = 'none';
    document.getElementById('overlay').classList.add('hidden');
    
    state.running = true;
    gameLoop();
}

function createBricks(level) {
    // Clear old bricks
    state.bricks = [];
    // Generate new bricks based on level
    // Simple 4x5 grid, expanding slightly per level
    let rows = 5;
    if (level > 4) rows = 6;
    let cols = 4;
    if (level > 7) cols = 5;

    for (let y = 0; y < rows; y++) {
        for (let x = 0; x < cols; x++) {
            // Add spacing for better control
            let gap = 20;
            let brickX = canvas.width / 2 - (cols * 40) / 2 + (x * 40) + (gap * x);
            let brickY = 100 + (y * 20);
            // Ensure it fits within canvas
            if(brickX > 0 && brickX + 40 < canvas.width) {
                state.bricks.push(new Brick(brickX, brickY));
            }
        }
    }
}

function checkBrickHits() {
    state.ballsInPlay.forEach(ball => {
        // Simple ABB algorithm for brick collision
        // We check each brick
        
        // Optimization: simple bounding box first
        let hit = false;
        for (let brick of state.bricks) {
            if (ball.x < brick.x + brick.width &&
                ball.x > brick.x &&
                ball.y < brick.y + brick.height &&
                ball.y > brick.y) {
                
                // Snapping logic (if we wanted complex physics, we'd use normal vectors here)
                // For now, we ensure the ball bounces back towards paddle logic
                
                // Calculate bounce angle (simplified)
                // Normal vector of wall
                // If we are hitting side/top/bottom, we flip the axis we are heading towards
                // Actually, simpler for a top-down feel: 
                // If ball moves faster than brick width ratio, it's a top/bottom hit.
                
                ball.y = brick.y + brick.height / 2; // Snap to center
                ball.dy = -Math.abs(ball.dy); // Snap vertical speed up

                // Check if hitting side (left/right) of the paddle logic usually, but here for bricks:
                // If we are moving sideways mostly
                if (Math.abs(ball.dx) > Math.abs(ball.dy)) {
                    ball.dx = -ball.dx; // Bounce sideways
                    hit = true;
                }
                
                // Create explosion juice
                if (ball.dx !== 0 && ball.dy !== 0) { // Only explode if it actually bounced
                     // Just explode a little to avoid constant explosion
                     // Actually, let's explode every hit on brick
                     createParticles(ball.x, ball.y, brick.color);
                }
                
                break; 
            }
        }
        
        if (!hit) {
             // Ball fell through bricks
             if (ball.y > canvas.height - 50) {
                 gameOver();
             }
             if (ball.y < 50) {
                 // Go to paddle
                 state.ballsInPlay = [];
                 resetBall();
             }
        }
    });
}

function resetBall() {
    state.ballsInPlay.push(new Ball());
}

function createParticles(x, y, color) {
    for(let i=0; i<6; i++) {
        state.particles.push(new Particle(x, y, color));
    }
}

function update() {
    // Paddle Logic
    if (inputs.mouse.y < canvas.height) {
        paddle.y = Math.max(0, Math.min(canvas.height - paddle.height, inputs.mouse.y - 60));
    }

    // Ball Logic
    state.ballsInPlay.forEach(ball => {
        ball.update();
        
        // Wall Bouncing
        if (ball.y < 0 || ball.y > canvas.height - ball.size - 10) {
            ball.dy = -ball.dy;
            ball.hitBrick();
        }
        if (ball.x < 0 || ball.x > canvas.width) {
            ball.dx = -ball.dx;
            ball.hitBrick();
        }

        // Paddle Hitting (Side walls only logic usually, but let's say it hits if it's in bounds)
        // Since paddle follows mouse, it stays in range.
        if (ball.y > paddle.y && ball.y < paddle.y + paddle.height &&
            ball.x > paddle.x && ball.x < paddle.x + paddle.width) {
            ball.dx = -ball.dx;
            ball.dy = ball.dy || Math.random() > 0.5 ? -5 : 5; // Add chaos on hit
            ball.hitBrick();
            
            // Add Juice to paddle
            paddle.glow = 10;
            paddle.color = '#ffeb3b'; 
            setTimeout(() => {
                paddle.color = '#fff';
                paddle.glow = 0;
            }, 200);
        }
    });

    // Particle Logic
    state.particles.forEach(p => p.update());
}

function draw() {
    // Shake Effect
    if (state.shakeLimit) {
        ctx.save();
        ctx.translate(state.shakeLimit.x, state.shakeLimit.y);
        state.shakeLimit = new ScreenShake(0); // Clear shake
        // Reapply shake logic here
    }
    
    // Draw Paddle
    ctx.shadowBlur = 20;
    ctx.shadowColor = '#fff';
    ctx.fillStyle = paddle.color;
    ctx.fillRect(paddle.x, paddle.y, paddle.width, paddle.height);
    ctx.shadowBlur = 0;

    // Draw Bricks
    state.bricks.forEach(brick => brick.draw());

    // Draw Balls
    state.ballsInPlay.forEach(ball => ball.draw());

    // Draw Particles
    state.particles.forEach(p => p.draw());

    // Grid Background lines for retro feel
    ctx.strokeStyle = 'rgba(255, 255, 255, 0.05)';
    ctx.lineWidth = 1;
    ctx.beginPath();
    for(let i=0; i<canvas.width; i+=40) {
        ctx.moveTo(i, 0); ctx.lineTo(i, canvas.height);
    }
    for(let i=0; i<canvas.height; i+=40) {
        ctx.moveTo(0, i); ctx.lineTo(canvas.width, i);
    }
    ctx.stroke();
    
    // UI Overlay
    drawUI();
}

function drawUI() {
    // We drew canvas, now overlay logic via canvas if we wanted, but CSS is cleaner for numbers.
}

function gameLoop() {
    if (!state.running) return;

    update();
    draw();
    
    // Update Particles (clear dead ones)
    state.particles = state.particles.filter(p => p.life > 0);
    
    requestAnimationFrame(gameLoop);
}

function updateScore() {
    document.getElementById('scoreDisplay').innerText = `Score: ${state.score}`;
    document.getElementById('levelDisplay').innerText = `Level: ${state.level}`;
}

function gameOver() {
    state.running = false;
    state.scoreDisplay.innerText = `Final Score: ${state.score}`;
    state.scoreDisplay.style.opacity = "1";
    document.getElementById('overlay').classList.remove('hidden');
    document.getElementById('overlay').innerHTML = `
        <h1>GAME OVER</h1>
        <p>Score: ${state.score}</p>
        <p>Level: ${state.level}</p>
        <button id="restartBtn">TRY AGAIN</button>
    `;
    document.getElementById('restartBtn').addEventListener('click', startGame);
}

function restartGame() {
    document.getElementById('overlay').classList.add('hidden');
    document.getElementById('startBtn').style.display = 'block';
}

// --- Init ---
document.getElementById('startBtn').addEventListener('click', initGame);
document.getElementById('restartBtn').addEventListener('click', restartGame);

// Start logic
let animationId;
if (state.running) {
    animationId = requestAnimationFrame(gameLoop);
}

// Simple Shake Implementation
let shakeLimit = new ScreenShake(15);
function hitObject(x, y) {
    shakeLimit.add(x - canvas.width/2, y - canvas.height/2);
    createParticles(x, y, '#fff');
}

// Expose functions to avoid closing scope issues if needed later
// gameLoop calls update, update calls hitObject on brick/paddle

// Wait, I missed the collision callback integration.
// Fixing in gameLoop inside the loop or just calling checkBrickHits manually in update.

// Let's refine the Ball update loop:
// Ball moves. It checks walls. It checks paddle. It checks bricks.
// When it hits anything, create particles and shake.

// Refactored update for clarity
function mainLoop() {
    if (state.running) {
        // 1. Physics
        state.ballsInPlay.forEach(ball => {
            ball.update();

            // Horizontal Wall
            if (ball.y < 0 || ball.y > canvas.height - ball.size - 20) {
                ball.dy = -ball.dy;
                hitObject(ball.x, ball.y);
            }

            // Vertical Wall
            if (ball.x < 0 || ball.x > canvas.width) {
                ball.dx = -ball.dx;
                hitObject(ball.x, ball.y);
            }

            // Paddle Hit
            if (ball.y > paddle.y && ball.y < paddle.y + paddle.height &&
                ball.x > paddle.x && ball.x < paddle.x + paddle.width) {
                ball.dx = -ball.dx;
                ball.dy = Math.abs(ball.dy) + 0.2; // Slight increase in speed
                hitObject(ball.x, ball.y);
            }

            // Brick Hit
            let hitBrick = false;
            state.bricks.forEach(brick => {
                if (ball.x < brick.x + brick.width &&
                    ball.x > brick.x &&
                    ball.y < brick.y + brick.height &&
                    ball.y > brick.y) {
                    
                    // Simple angle correction logic
                    if (Math.abs(ball.dx) > Math.abs(ball.dy)) {
                        ball.dx = -ball.dx;
                        hitBrick = true;
                    }
                }
            });

            // Ball fell past paddle (Game Over logic in ball loop)
            if (ball.y > canvas.height - 50) {
                state.gameOver = true;
                state.running = false;
            }

            // Ball returned to top
            if (ball.y < 50) {
                resetBall();
            }

            // Juice for bricks
            if (hitBrick) {
                 // Only create particles if hitting a brick
                 createParticles(ball.x, ball.y, brick.color); // We need scope access, let's fix this
            }
        });

        // 2. Cleanup and Shake
        state.particles = state.particles.filter(p => p.life > 0);
        // Apply shake to canvas
        ctx.save();
        ctx.translate(shakeLimit.x, shakeLimit.y);
        shakeLimit.add(0, 0); // Reset shake accumulator
        
        // 3. Draw
        // We need to redraw bricks properly too
        // But wait, I separated the logic.
        // Let's stick to the structure: update -> draw.
        // The hitObject needs to be called.
        
        // Re-implementing draw with shake applied
        ctx.save();
        ctx.translate(shakeLimit.x, shakeLimit.y);
        
        // Draw Grid
        ctx.strokeStyle = 'rgba(255, 255, 255, 0.05)';
        ctx.lineWidth = 1;
        ctx.beginPath();
        for(let i=0; i<canvas.width; i+=40) { ctx.moveTo(i, 0); ctx.lineTo(i, canvas.height); }
        for(let i=0; i<canvas.height; i+=40) { ctx.moveTo(0, i); ctx.lineTo(canvas.width, i); }
        ctx.stroke();

        // Draw Paddle
        ctx.fillStyle = '#fff';
        ctx.shadowBlur = 20;
        ctx.shadowColor = '#ffeb3b';
        ctx.fillRect(paddle.x, paddle.y, paddle.width, paddle.height);
        ctx.shadowBlur = 0;

        // Draw Bricks
        state.bricks.forEach(brick => brick.draw());

        // Draw Balls
        state.ballsInPlay.forEach(ball => ball.draw());

        // Draw Particles
        state.particles.forEach(p => p.draw());

        ctx.restore();
    } else {
        // Pause loop until resume
        if (state.gameOver || state.paused) {
             // Do nothing, just wait for restart
        }
    }

    // If running, loop next frame
    if (state.running) {
        animationId = requestAnimationFrame(gameLoop);
    }
}
    
function gameLoop() {
    if (state.running) {
        mainLoop();
    }
}

// Fix the particle creation inside the loop properly
// Moving particle generation out to a centralized event listener or method

// Helper for shaking the screen
let currentShakeOffset = {x:0, y:0};

function shakeScreen(x, y, amount) {
    currentShakeOffset.x += amount * (Math.random() - 0.5);
    currentShakeOffset.y += amount * (Math.random() - 0.5);
}

function handleHit(x, y, amount) {
    shakeScreen(x, y, amount);
    createParticles(x, y, '#fff');
}

// Corrected Game Loop
function run() {
    if (state.running) {
        // Update logic
        state.ballsInPlay.forEach(ball => {
            ball.update();

            // Wall Bounce
            if (ball.y < 0 || ball.y > canvas.height - ball.size) {
                ball.dy = -ball.dy;
                handleHit(ball.x, ball.y, 2);
            }
            if (ball.x < 0 || ball.x > canvas.width) {
                ball.dx = -ball.dx;
                handleHit(ball.x, ball.y, 2);
            }

            // Paddle
            if (ball.y > paddle.y && ball.y < paddle.y + paddle.height &&
                ball.x > paddle.x && ball.x < paddle.x + paddle.width) {
                ball.dy = Math.abs(ball.dy) + 0.2;
                ball.dx = -ball.dx;
                handleHit(ball.x, ball.y, 3);
            }

            // Bricks
            let hit = false;
            // Iterate backwards to allow removal
            for (let i = state.bricks.length - 1; i >= 0; i--) {
                let b = state.bricks[i];
                if (ball.x > b.x && ball.x < b.x + b.width &&
                    ball.y > b.y && ball.y < b.y + b.height) {
                    
                    ball.dy = -Math.abs(ball.dy);
                    ball.dx = -ball.dx;
                    hit = true;
                    handleHit(ball.x, ball.y, 5);
                    
                    b.hp--;
                    if(b.hp <= 0) {
                        // Remove brick
                        state.bricks.splice(i, 1);
                        updateScore();
                        levelUp();
                    }
                    break;
                }
            }

            // Check Win/Loss
            if (!hit && ball.y > canvas.height - 50) {
                state.running = false;
                state.gameOver = true;
            }
            if (!hit && ball.y < 50) {
                resetBall();
            }
        });

        // Update particles
        state.particles = state.particles.filter(p => p.life > 0);

        // Draw
        drawScene();
        
        // Check Level Win
        if (state.bricks.length === 0 && state.ballsInPlay.length > 0) {
            state.running = false;
            state.level++;
            levelUp();
            resetLevel();
        }
    }
}

function resetLevel() {
    createBricks(state.level);
    resetBall();
}

function levelUp() {
    state.score += 1000;
    updateScore();
    
    // Visual shake for level up
    shakeScreen(canvas.width/2, canvas.height/2, 10);
}

function drawScene() {
    ctx.clearRect(0, 0, canvas.width, canvas.height);
    
    // Draw Shake
    ctx.save();
    ctx.translate(currentShakeOffset.x, currentShakeOffset.y);
    ctx.translate(0, 0); // Reset
    
    // Grid
    ctx.strokeStyle = 'rgba(255, 255, 255, 0.05)';
    ctx.lineWidth = 1;
    ctx.beginPath();
    for(let i=0; i<canvas.width; i+=40) {
        ctx.moveTo(i, 0); ctx.lineTo(i, canvas.height);
    }
    for(let i=0; i<canvas.height; i+=40) {
        ctx.moveTo(0, i); ctx.lineTo(canvas.width, i);
    }
    ctx.stroke();

    // Paddle
    ctx.fillStyle = '#fff';
    ctx.shadowBlur = 20;
    ctx.shadowColor = '#ffeb3b';
    ctx.fillRect(paddle.x, paddle.y, paddle.width, paddle.height);
    ctx.shadowBlur = 0;

    // Bricks
    state.bricks.forEach(brick => brick.draw());

    // Balls
    state.ballsInPlay.forEach(ball => ball.draw());

    // Particles
    state.particles.forEach(p => p.draw());

    ctx.restore();
}

// Init logic
document.getElementById('startBtn').addEventListener('click', () => {
    state.running = true;
    state.score = 0;
    state.level = 1;
    state.ballsInPlay = [];
    state.gameOver = false;
    state.particles = [];
    currentShakeOffset = {x:0, y:0};
    
    paddle.x = (canvas.width - paddle.width) / 2;
    paddle.y = canvas.height - 60;

    document.getElementById('startBtn').style.display = 'none';
    document.getElementById('overlay').classList.add('hidden');
    updateScore();
    
    run();
});

</script>
</body>
</html>

Compare with

Ternary Bonsai 2 9B: other attempts