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
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>

