GPT-6.1 Terra
by OpenAIReasoningClean
Running live in a sandboxed frame. Click inside it before using the keyboard.
How it went
- Total time
- 2 min 28 s
- Cost
- $0.067
- Output tokens
- 13,357
- Spent thinking
- 3,389 tokens
- Speed
- 93 tok/s
- Console errors
- 0
- First token after
- 4.5 s
- First visible text after
- 1 min 3 s
- Prompt tokens
- 508
- Code size
- 28.3 KB
- Finish reason
- stop
- Served by
- OpenAI
- Model id
openai/gpt-6.1-terra- Reasoning sent
{"effort":"high"}- Max output
- 128,000 tokens
- Generated
- Nov 12, 2026
What it was asked
The prompt
Build a polished Breakout (brick breaker) game in a single HTML file using vanilla JavaScript and a <canvas>. Requirements: - A paddle controlled with the mouse and with the left/right arrow keys. - The bounce angle off the paddle depends on where the ball hits it (center = straight up, edges = sharp angle). - One level with at least 6 rows of bricks, mixing normal bricks and tougher bricks that need 2 hits and visibly crack after the first one. - Some bricks visibly carry a power-up icon; when broken they drop it, and catching it with the paddle gives either a wider paddle (10 seconds) or multi-ball (3 balls). - The ball must never pass through bricks or the paddle. - Particles when a brick breaks. - 3 lives, score displayed. Game over and victory screens with a restart option.
System prompt, sent with every prompt
You are generating a self-contained web demo. It will be rendered automatically in a sandboxed iframe and compared side by side with the output of other AI models given the exact same task. Output rules: - Reply with exactly ONE fenced code block containing the complete file: ```html (or ```svg when the task asks for an SVG). No text before or after the code block. - Everything must be inline: CSS in <style>, JavaScript in <script>. No external files, images, fonts or network requests, except the CDN URLs explicitly allowed by the task. - Do not use localStorage, sessionStorage, cookies, IndexedDB, alert(), confirm(), prompt() or window.open(): they are blocked in the sandbox and will throw. - The page must look right in a 1280×800 viewport and adapt gracefully down to 375px wide. No horizontal scrollbar. - Something meaningful must be visible on first paint: a screenshot is taken automatically about 3 seconds after load, without any interaction. - Everything that makes the demo interesting must be visible within the first seconds: nothing hidden behind a high score, a later level or a long play session. - Games must start in a self-playing demo mode (a simple AI plays) behind a "Click or press a key to start" message, so the game can be judged in action without playing. - The iframe only receives keyboard events after the user clicks it: if the demo uses the keyboard, display a clear "Click or press a key to start" message. - The code must run with zero errors in the console of the latest Chrome.
Code run-1.html · 579 lines
<!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 {
color-scheme: dark;
--bg: #090d1b;
--panel: #11182b;
--line: #27314a;
--muted: #8792aa;
--text: #f4f6ff;
--cyan: #69e6e2;
}
* { box-sizing: border-box; }
body {
margin: 0;
min-height: 100vh;
color: var(--text);
font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
background:
radial-gradient(ellipse at 52% -25%, #202b52 0, transparent 50%),
radial-gradient(ellipse at 90% 85%, #101a32 0, transparent 38%),
#090d1b;
}
.shell { width: min(1136px, calc(100% - 48px)); margin: 0 auto; }
.masthead {
height: 99px;
display: flex;
justify-content: space-between;
align-items: center;
}
.brand { display: flex; align-items: center; gap: 13px; }
.brand-mark {
width: 39px; height: 39px; border-radius: 11px;
background: linear-gradient(145deg, #67f1df, #6a91ff);
box-shadow: 0 4px 22px #5cbfd536;
display: grid; place-items: center;
}
.brand-mark svg { width: 25px; height: 25px; }
.brand-name { font-size: 15px; font-weight: 850; letter-spacing: .13em; line-height: 1.1; }
.brand-sub { color: #78859f; font-size: 9px; font-weight: 700; letter-spacing: .2em; margin-top: 5px; }
.header-right { display: flex; align-items: center; gap: 11px; font-size: 10px; color: #8e9ab2; letter-spacing: .13em; font-weight: 800; }
.header-right .dot, .live-dot { width: 7px; height: 7px; border-radius: 50%; background: #6de6bd; box-shadow: 0 0 10px #6de6bd; }
.header-right .separator { width: 1px; height: 16px; background: #303a50; margin: 0 3px; }
.layout { display: grid; grid-template-columns: minmax(0, 1fr) 257px; gap: 20px; align-items: start; }
.game-card, .side-card { border: 1px solid #29344d; background: #11182a; border-radius: 16px; overflow: hidden; box-shadow: 0 20px 55px #0004; }
.game-top { min-height: 66px; padding: 0 20px; display: flex; align-items: center; justify-content: space-between; gap: 15px; }
.level-label { display: flex; align-items: center; gap: 11px; min-width: 0; }
.level-number {
width: 34px; height: 34px; border-radius: 9px; display: grid; place-items: center;
background: #222d46; color: #a5b5d2; font-size: 11px; font-weight: 800;
}
.eyebrow { color: #7f8da7; font-size: 9px; font-weight: 800; letter-spacing: .15em; }
.level-title { font-size: 13px; font-weight: 750; margin-top: 3px; white-space: nowrap; }
.stats { display: flex; gap: 25px; align-items: center; }
.stat { min-width: 59px; }
.stat-value { font-size: 16px; font-weight: 800; letter-spacing: .04em; margin-top: 2px; font-variant-numeric: tabular-nums; }
.hearts { color: #ff788d; letter-spacing: 2px; font-size: 15px; white-space: nowrap; }
.hearts .lost { color: #384157; }
.playfield { position: relative; overflow: hidden; background: #090f20; border-top: 1px solid #29344d; }
canvas { display: block; width: 100%; height: auto; aspect-ratio: 900 / 570; touch-action: none; cursor: crosshair; }
.demo-banner {
position: absolute; top: 68%; left: 50%; transform: translate(-50%, -50%);
text-align: center; pointer-events: none; white-space: nowrap;
border: 1px solid #53617b; border-radius: 11px;
padding: 11px 19px 10px;
background: #111a2ceF; box-shadow: 0 8px 32px #0008, inset 0 1px #ffffff0a;
}
.demo-banner strong { display: block; font-size: 12px; letter-spacing: .01em; }
.demo-banner span { display: block; color: #9caac1; font-size: 10px; margin-top: 4px; }
.demo-banner i { display: inline-block; width: 6px; height: 6px; margin: 0 6px 1px 0; border-radius: 50%; background: #72e8d3; box-shadow: 0 0 8px #72e8d3; }
.end-screen {
position: absolute; inset: 0; display: none; flex-direction: column; justify-content: center; align-items: center;
background: #080e1ce8; text-align: center; padding: 20px;
}
.end-screen.show { display: flex; }
.end-kicker { color: #6ce2d1; font-size: 10px; font-weight: 800; letter-spacing: .2em; }
.end-screen h2 { margin: 10px 0 6px; font-size: clamp(24px, 4vw, 40px); letter-spacing: -.04em; }
.end-screen p { color: #a5b1c7; margin: 0 0 20px; font-size: 13px; }
button {
border: 0; border-radius: 9px; padding: 11px 20px; color: #10182a;
background: #77ebdb; font: inherit; font-size: 12px; font-weight: 800; cursor: pointer;
}
button:hover { background: #a3fff0; }
button:focus-visible { outline: 2px solid white; outline-offset: 3px; }
.game-bottom { display: flex; justify-content: space-between; align-items: center; padding: 12px 3px 0; color: #7f8aa1; font-size: 10px; font-weight: 650; }
.game-bottom span { display: flex; align-items: center; gap: 7px; }
.game-bottom .live-dot { width: 6px; height: 6px; }
.side-card { padding: 22px 19px 18px; }
.side-kicker { color: #77dfd8; font-size: 9px; letter-spacing: .17em; font-weight: 850; }
.side-card h1 { font-size: 21px; line-height: 1.13; letter-spacing: -.035em; margin: 11px 0 8px; }
.side-card p { color: #929fb6; line-height: 1.55; font-size: 12px; margin: 0; }
.divider { height: 1px; background: #2a344a; margin: 19px 0; }
.section-label { color: #8794ab; font-size: 9px; font-weight: 850; letter-spacing: .15em; margin-bottom: 14px; }
.control-row { display: flex; align-items: center; justify-content: space-between; margin: 11px 0; color: #c3ccda; font-size: 11px; }
.keys { display: flex; gap: 4px; }
.key {
min-width: 27px; height: 25px; padding: 0 5px; display: grid; place-items: center;
border: 1px solid #3a465e; border-bottom-width: 2px; border-radius: 5px;
background: #202a3e; color: #d5deed; font-size: 12px; line-height: 1;
}
.mouse-icon { width: 19px; height: 25px; border: 1.5px solid #77859d; border-radius: 10px; position: relative; margin-right: 4px; }
.mouse-icon:after { content: ""; position: absolute; width: 2px; height: 6px; border-radius: 2px; background: #96a6bc; left: 7px; top: 4px; }
.power-row { display: flex; align-items: center; gap: 10px; margin-top: 13px; }
.power-icon {
flex: 0 0 33px; height: 33px; border-radius: 9px; display: grid; place-items: center;
color: #6fe9d5; background: #183b40; border: 1px solid #2b5d5c;
}
.power-icon.purple { color: #c1a1ff; background: #302748; border-color: #51416b; }
.power-icon svg { width: 21px; height: 21px; }
.power-name { font-size: 11px; font-weight: 750; }
.power-description { color: #8592a9; font-size: 10px; margin-top: 3px; }
.tip { margin-top: 19px; border: 1px solid #32435a; background: #192337; border-radius: 9px; padding: 11px; color: #9eacc2; font-size: 10px; line-height: 1.5; }
.tip b { color: #d5dfed; }
@media (max-width: 800px) {
.shell { width: min(680px, calc(100% - 28px)); }
.masthead { height: 78px; }
.layout { grid-template-columns: 1fr; gap: 18px; }
.side-card { display: grid; grid-template-columns: 1fr 1fr; column-gap: 24px; }
.side-intro { grid-column: 1 / -1; }
.side-card .first-divider { grid-column: 1 / -1; }
.side-card .middle-divider { display: none; }
.tip { grid-column: 1 / -1; }
.game-bottom { padding-bottom: 2px; }
.layout { padding-bottom: 24px; }
}
@media (max-width: 480px) {
.shell { width: calc(100% - 24px); }
.masthead { height: 70px; }
.header-right { font-size: 9px; gap: 7px; }
.header-right .edition { display: none; }
.header-right .separator { display: none; }
.brand-mark { width: 34px; height: 34px; }
.brand-name { font-size: 13px; }
.game-top { padding: 0 11px; min-height: 60px; gap: 6px; }
.level-label { gap: 7px; }
.level-number { width: 29px; height: 29px; }
.level-title { font-size: 11px; }
.eyebrow { font-size: 8px; }
.stats { gap: 10px; }
.stat { min-width: 45px; }
.stat-value { font-size: 13px; }
.hearts { font-size: 12px; letter-spacing: 1px; }
.demo-banner { padding: 7px 11px; top: 69%; }
.demo-banner strong { font-size: 10px; }
.demo-banner span { font-size: 9px; margin-top: 2px; }
.side-card { padding: 18px; }
}
</style>
</head>
<body>
<div class="shell">
<header class="masthead">
<div class="brand">
<div class="brand-mark" aria-hidden="true">
<svg viewBox="0 0 28 28" fill="none">
<rect x="4" y="5" width="8" height="5" rx="1.5" fill="#122339"/>
<rect x="15" y="5" width="9" height="5" rx="1.5" fill="#122339"/>
<rect x="8" y="12" width="8" height="5" rx="1.5" fill="#122339"/>
<circle cx="20" cy="15" r="2.5" fill="#122339"/>
<rect x="6" y="21" width="16" height="3" rx="1.5" fill="#122339"/>
</svg>
</div>
<div><div class="brand-name">BREAKOUT</div><div class="brand-sub">THE ARCADE COLLECTION</div></div>
</div>
<div class="header-right"><span class="dot"></span><span>READY TO PLAY</span><span class="separator"></span><span class="edition">VOL. 01</span></div>
</header>
<main class="layout">
<section>
<div class="game-card">
<div class="game-top">
<div class="level-label">
<div class="level-number">01</div>
<div><div class="eyebrow">THE NEON GRID</div><div class="level-title">Break them all</div></div>
</div>
<div class="stats">
<div class="stat"><div class="eyebrow">SCORE</div><div class="stat-value" id="score">00000</div></div>
<div class="stat"><div class="eyebrow">LIVES</div><div class="stat-value hearts" id="lives">♥♥♥</div></div>
</div>
</div>
<div class="playfield" id="playfield">
<canvas id="game" width="900" height="570" aria-label="Breakout game. Move your paddle with the mouse or arrow keys."></canvas>
<div class="demo-banner" id="demoBanner"><strong>Click or press a key to start</strong><span><i></i>Demo mode is playing</span></div>
<div class="end-screen" id="endScreen">
<div class="end-kicker" id="endKicker">GAME OVER</div>
<h2 id="endTitle">One more round?</h2>
<p id="endText">Final score: 0</p>
<button id="restart" type="button">Play again ↗</button>
</div>
</div>
</div>
<div class="game-bottom">
<span><i class="live-dot"></i><span id="modeLabel">AUTOPILOT ON</span></span>
<span>BUILT FOR A LITTLE BREAK ✦</span>
</div>
</section>
<aside class="side-card">
<div class="side-intro">
<div class="side-kicker">YOUR MISSION</div>
<h1>Clear the grid.</h1>
<p>Keep the ball in play and break every brick. You have three lives to make it count.</p>
</div>
<div class="divider first-divider"></div>
<div>
<div class="section-label">CONTROLS</div>
<div class="control-row"><span>Move paddle</span><span class="keys"><span class="key">←</span><span class="key">→</span></span></div>
<div class="control-row"><span>Or use your mouse</span><span class="mouse-icon"></span></div>
</div>
<div class="divider middle-divider"></div>
<div>
<div class="section-label">POWER-UPS</div>
<div class="power-row">
<div class="power-icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M3 12h18M6 9l-3 3 3 3M18 9l3 3-3 3"/><path d="M8 17h8"/></svg></div>
<div><div class="power-name">Wide paddle</div><div class="power-description">More room for 10 seconds</div></div>
</div>
<div class="power-row">
<div class="power-icon purple"><svg viewBox="0 0 24 24" fill="currentColor"><circle cx="12" cy="7" r="2.5"/><circle cx="7" cy="16" r="2.5"/><circle cx="17" cy="16" r="2.5"/></svg></div>
<div><div class="power-name">Multi-ball</div><div class="power-description">Three balls, triple the fun</div></div>
</div>
</div>
<div class="tip"><b>Pro tip</b> Hit the ball with the edge of your paddle to send it off at an angle.</div>
</aside>
</main>
</div>
<script>
(() => {
'use strict';
const canvas = document.getElementById('game');
const ctx = canvas.getContext('2d');
const W = 900, H = 570, R = 7;
const scoreEl = document.getElementById('score');
const livesEl = document.getElementById('lives');
const banner = document.getElementById('demoBanner');
const endScreen = document.getElementById('endScreen');
const modeLabel = document.getElementById('modeLabel');
const field = document.getElementById('playfield');
const paddle = { x: W / 2, y: 524, w: 122, h: 14, target: W / 2 };
const keys = { left: false, right: false };
const colors = ['#ff7385', '#ff9b72', '#ffc970', '#83dfb2', '#68d8df', '#79b5fc', '#ae99f5'];
const powerMap = {
'0,2':'wide', '0,8':'multi', '1,5':'multi', '2,1':'wide', '2,9':'multi',
'3,4':'wide', '3,8':'multi', '4,2':'multi', '4,7':'wide',
'5,1':'wide', '5,9':'multi', '6,3':'multi', '6,6':'wide', '6,10':'multi'
};
let bricks = [], balls = [], drops = [], particles = [];
let score = 0, lives = 3, remaining = 0, wideTime = 0;
let demo = true, ended = false, demoReset = 0, lastTime = 0;
function clamp(v, a, b) { return Math.max(a, Math.min(b, v)); }
function rr(x, y, w, h, r) {
ctx.beginPath();
ctx.roundRect(x, y, w, h, r);
}
function makeBall(x, y, vx, vy) { return { x, y, vx, vy, trail: [] }; }
function createBricks() {
const list = [], cols = 11, gap = 8, left = 44, bw = (W - left * 2 - gap * (cols - 1)) / cols;
for (let row = 0; row < 7; row++) {
for (let col = 0; col < cols; col++) {
const power = powerMap[row + ',' + col] || null;
const tough = ((row * 7 + col * 3) % 8 < 2 || (row === 0 && col % 3 === 0)) && !(row === 6 && col === 6);
list.push({ x: left + col * (bw + gap), y: 73 + row * 31, w: bw, h: 23, row, color: colors[row], hp: tough ? 2 : 1, maxHp: tough ? 2 : 1, power });
}
}
return list;
}
function updateHUD() {
scoreEl.textContent = String(score).padStart(5, '0');
livesEl.innerHTML = '♥'.repeat(lives) + '<span class="lost">' + '♥'.repeat(3 - lives) + '</span>';
modeLabel.textContent = demo ? 'AUTOPILOT ON' : (ended ? 'RUN COMPLETE' : 'YOUR RUN');
}
function reset(isDemo) {
demo = isDemo; ended = false; demoReset = 0;
score = 0; lives = 3; wideTime = 0; particles = []; drops = [];
bricks = createBricks(); remaining = bricks.length;
paddle.x = W / 2; paddle.target = W / 2; paddle.w = 122;
balls = [makeBall(W / 2, paddle.y - R - 3, 132, -320)];
banner.style.display = demo ? 'block' : 'none';
endScreen.classList.remove('show');
updateHUD();
}
function startGame() {
if (demo || ended) reset(false);
}
function finish(win) {
ended = true;
updateHUD();
if (demo) { demoReset = 1.8; return; }
document.getElementById('endKicker').textContent = win ? 'GRID CLEARED' : 'GAME OVER';
document.getElementById('endTitle').textContent = win ? 'Brilliantly done.' : 'One more round?';
document.getElementById('endText').textContent = (win ? 'You cleared every brick. ' : 'Nice run. ') + 'Final score: ' + score.toLocaleString();
endScreen.classList.add('show');
}
function burst(x, y, color, count) {
for (let i = 0; i < count; i++) {
const a = Math.random() * Math.PI * 2, speed = 35 + Math.random() * 150;
particles.push({ x, y, vx: Math.cos(a) * speed, vy: Math.sin(a) * speed, life: .35 + Math.random() * .45, max: .8, color, size: 2 + Math.random() * 3 });
}
}
function hitBrick(b) {
b.hp--;
if (b.hp === 0) {
remaining--;
score += b.maxHp === 2 ? 150 : 100;
burst(b.x + b.w / 2, b.y + b.h / 2, b.color, 11);
if (b.power) drops.push({ x: b.x + b.w / 2, y: b.y + b.h / 2, type: b.power, vy: 125 });
if (remaining === 0) finish(true);
} else {
score += 25;
burst(b.x + b.w / 2, b.y + b.h / 2, b.color, 4);
}
updateHUD();
}
// Swept circle against a rectangle: straight edges and rounded corners.
// This tests the entire frame's path rather than only its final position.
function sweepRect(ball, rect, limit) {
const { x, y, vx, vy } = ball, l = rect.x, r = rect.x + rect.w, t = rect.y, b = rect.y + rect.h;
let best = null;
function offer(time, nx, ny) {
if (time >= -1e-8 && time <= limit && vx * nx + vy * ny < -1e-7 && (!best || time < best.t)) {
best = { t: Math.max(0, time), nx, ny };
}
}
if (vx > 0) { const q = (l - R - x) / vx, yy = y + vy * q; if (yy >= t && yy <= b) offer(q, -1, 0); }
if (vx < 0) { const q = (r + R - x) / vx, yy = y + vy * q; if (yy >= t && yy <= b) offer(q, 1, 0); }
if (vy > 0) { const q = (t - R - y) / vy, xx = x + vx * q; if (xx >= l && xx <= r) offer(q, 0, -1); }
if (vy < 0) { const q = (b + R - y) / vy, xx = x + vx * q; if (xx >= l && xx <= r) offer(q, 0, 1); }
const speed2 = vx * vx + vy * vy;
if (speed2 > 0) {
for (const [cx, cy, sx, sy] of [[l,t,-1,-1], [r,t,1,-1], [l,b,-1,1], [r,b,1,1]]) {
const dx = x - cx, dy = y - cy;
const B = 2 * (dx * vx + dy * vy), C = dx * dx + dy * dy - R * R;
const disc = B * B - 4 * speed2 * C;
if (disc < 0) continue;
const q = (-B - Math.sqrt(disc)) / (2 * speed2);
if (q < -1e-8 || q > limit) continue;
const px = x + vx * q - cx, py = y + vy * q - cy;
if (px * sx >= -1e-7 && py * sy >= -1e-7) {
const len = Math.hypot(px, py) || 1;
offer(q, px / len, py / len);
}
}
}
return best;
}
function moveBall(ball, dt) {
let left = dt, iterations = 0;
while (left > 0.000001 && iterations++ < 12 && !ended) {
let best = null;
function candidate(t, nx, ny, type, object) {
if (t >= -1e-8 && t <= left && (!best || t < best.t)) best = { t: Math.max(0, t), nx, ny, type, object };
}
if (ball.vx < 0) candidate((20 + R - ball.x) / ball.vx, 1, 0, 'wall');
if (ball.vx > 0) candidate((W - 20 - R - ball.x) / ball.vx, -1, 0, 'wall');
if (ball.vy < 0) candidate((17 + R - ball.y) / ball.vy, 0, 1, 'wall');
if (ball.vy > 0) candidate((H + R - ball.y) / ball.vy, 0, -1, 'bottom');
if (ball.vy > 0 && ball.y < paddle.y + paddle.h) {
const hit = sweepRect(ball, { x: paddle.x - paddle.w / 2, y: paddle.y, w: paddle.w, h: paddle.h }, left);
if (hit) candidate(hit.t, hit.nx, hit.ny, 'paddle');
}
for (const brick of bricks) {
if (brick.hp <= 0) continue;
const hit = sweepRect(ball, brick, left);
if (hit) candidate(hit.t, hit.nx, hit.ny, 'brick', brick);
}
if (!best) { ball.x += ball.vx * left; ball.y += ball.vy * left; break; }
ball.x += ball.vx * best.t; ball.y += ball.vy * best.t;
left -= best.t;
if (best.type === 'bottom') return false;
if (best.type === 'paddle') {
const offset = clamp((ball.x - paddle.x) / (paddle.w / 2), -1, 1);
const angle = offset * 1.08;
const speed = Math.max(345, Math.hypot(ball.vx, ball.vy));
ball.vx = Math.sin(angle) * speed;
ball.vy = -Math.cos(angle) * speed;
ball.y = Math.min(ball.y, paddle.y - R - .05);
burst(ball.x, paddle.y, '#a4f9ef', 4);
} else {
const dot = ball.vx * best.nx + ball.vy * best.ny;
ball.vx -= 2 * dot * best.nx;
ball.vy -= 2 * dot * best.ny;
ball.x += best.nx * .03; ball.y += best.ny * .03;
if (best.type === 'brick') hitBrick(best.object);
}
}
return true;
}
function activate(type) {
if (type === 'wide') {
wideTime = 10;
paddle.w = 184;
} else if (balls.length) {
const source = balls[0];
const base = Math.atan2(source.vy, source.vx);
const speed = Math.hypot(source.vx, source.vy);
let n = 0;
while (balls.length < 3) {
const angle = base + (n++ === 0 ? .48 : -.48);
balls.push(makeBall(source.x, source.y, Math.cos(angle) * speed, Math.sin(angle) * speed));
}
}
}
function update(dt) {
if (ended) {
if (demo && (demoReset -= dt) <= 0) reset(true);
return;
}
if (wideTime > 0) {
wideTime -= dt;
if (wideTime <= 0) { wideTime = 0; paddle.w = 122; }
}
if (demo && balls.length) {
const targetBall = balls.reduce((a, b) => b.y > a.y ? b : a);
let target = targetBall.x;
if (targetBall.vy > 0) {
const travel = (paddle.y - R - targetBall.y) / targetBall.vy;
let x = targetBall.x + targetBall.vx * Math.max(0, travel);
const min = 20 + R, span = W - 40 - 2 * R;
let folded = ((x - min) % (2 * span) + 2 * span) % (2 * span);
target = min + (folded > span ? 2 * span - folded : folded);
target += Math.sin(performance.now() / 1600) * 15;
}
paddle.target = target;
} else if (!demo && (keys.left || keys.right)) {
paddle.target = paddle.x + ((keys.right ? 1 : 0) - (keys.left ? 1 : 0)) * 690 * dt;
}
paddle.target = clamp(paddle.target, 20 + paddle.w / 2, W - 20 - paddle.w / 2);
const move = clamp(paddle.target - paddle.x, -850 * dt, 850 * dt);
paddle.x = clamp(paddle.x + move, 20 + paddle.w / 2, W - 20 - paddle.w / 2);
for (const ball of balls) {
ball.trail.unshift({ x: ball.x, y: ball.y });
if (ball.trail.length > 8) ball.trail.pop();
}
balls = balls.filter(ball => moveBall(ball, dt));
if (!ended && balls.length === 0) {
lives--;
updateHUD();
if (lives <= 0) finish(false);
else balls.push(makeBall(paddle.x, paddle.y - R - 3, (Math.random() > .5 ? 1 : -1) * 132, -320));
}
for (let i = drops.length - 1; i >= 0; i--) {
const d = drops[i]; d.y += d.vy * dt;
if (d.y + 12 >= paddle.y && d.y - 12 <= paddle.y + paddle.h && Math.abs(d.x - paddle.x) <= paddle.w / 2 + 12) {
activate(d.type); burst(d.x, d.y, d.type === 'wide' ? '#71ead9' : '#c6a6ff', 14); drops.splice(i, 1);
} else if (d.y > H + 20) drops.splice(i, 1);
}
for (let i = particles.length - 1; i >= 0; i--) {
const p = particles[i]; p.x += p.vx * dt; p.y += p.vy * dt; p.vy += 100 * dt; p.life -= dt;
if (p.life <= 0) particles.splice(i, 1);
}
}
function drawPowerIcon(type, x, y, size, color) {
ctx.save(); ctx.translate(x, y); ctx.strokeStyle = color; ctx.fillStyle = color; ctx.lineWidth = Math.max(1.2, size * .13); ctx.lineCap = 'round'; ctx.lineJoin = 'round';
if (type === 'wide') {
ctx.beginPath(); ctx.moveTo(-size * .56, 0); ctx.lineTo(size * .56, 0);
ctx.moveTo(-size * .3, -size * .26); ctx.lineTo(-size * .56, 0); ctx.lineTo(-size * .3, size * .26);
ctx.moveTo(size * .3, -size * .26); ctx.lineTo(size * .56, 0); ctx.lineTo(size * .3, size * .26); ctx.stroke();
} else {
for (const [dx, dy] of [[0,-.35],[-.35,.3],[.35,.3]]) { ctx.beginPath(); ctx.arc(dx * size, dy * size, size * .19, 0, Math.PI * 2); ctx.fill(); }
}
ctx.restore();
}
function draw() {
ctx.clearRect(0, 0, W, H);
const bg = ctx.createLinearGradient(0, 0, 0, H);
bg.addColorStop(0, '#101a30'); bg.addColorStop(.65, '#0b1428'); bg.addColorStop(1, '#101a2f');
ctx.fillStyle = bg; ctx.fillRect(0, 0, W, H);
const glow = ctx.createRadialGradient(450, 100, 30, 450, 100, 550);
glow.addColorStop(0, '#26314c55'); glow.addColorStop(1, '#10182900');
ctx.fillStyle = glow; ctx.fillRect(0, 0, W, H);
ctx.fillStyle = '#a5c4e30d';
for (let y = 24; y < H; y += 24) for (let x = 24; x < W; x += 24) { ctx.beginPath(); ctx.arc(x, y, 1, 0, Math.PI * 2); ctx.fill(); }
ctx.strokeStyle = '#52627a35'; ctx.lineWidth = 1;
ctx.beginPath(); ctx.moveTo(20, 17); ctx.lineTo(20, H - 18); ctx.moveTo(W - 20, 17); ctx.lineTo(W - 20, H - 18); ctx.stroke();
ctx.font = 'bold 10px system-ui, sans-serif'; ctx.letterSpacing = '1.4px';
ctx.fillStyle = '#788aa3'; ctx.textAlign = 'left'; ctx.fillText('LEVEL 01 / THE NEON GRID', 44, 42);
ctx.textAlign = 'right'; ctx.fillText(remaining + ' BRICKS LEFT', W - 44, 42); ctx.letterSpacing = '0px';
for (const b of bricks) {
if (b.hp <= 0) continue;
ctx.save();
ctx.shadowColor = b.color + '55'; ctx.shadowBlur = 10;
ctx.fillStyle = b.color; rr(b.x, b.y, b.w, b.h, 4); ctx.fill();
ctx.shadowBlur = 0;
ctx.fillStyle = '#ffffff35'; rr(b.x + 2, b.y + 2, b.w - 4, 3, 2); ctx.fill();
ctx.fillStyle = '#12203926'; rr(b.x + 2, b.y + b.h - 4, b.w - 4, 2, 1); ctx.fill();
if (b.maxHp === 2) {
ctx.strokeStyle = '#ffffffaa'; ctx.lineWidth = 1; rr(b.x + 1, b.y + 1, b.w - 2, b.h - 2, 3); ctx.stroke();
ctx.fillStyle = '#22314b99';
ctx.fillRect(b.x + b.w - 12, b.y + 6, 2, 4); ctx.fillRect(b.x + b.w - 8, b.y + 6, 2, 4);
}
if (b.power) {
ctx.fillStyle = '#14243c66'; ctx.beginPath(); ctx.arc(b.x + b.w / 2, b.y + b.h / 2 + 1, 8, 0, Math.PI * 2); ctx.fill();
drawPowerIcon(b.power, b.x + b.w / 2, b.y + b.h / 2 + 1, 8, '#ffffff');
}
if (b.maxHp === 2 && b.hp === 1) {
ctx.strokeStyle = '#273348d9'; ctx.lineWidth = 1.7; ctx.lineJoin = 'round';
ctx.beginPath(); ctx.moveTo(b.x + b.w * .38, b.y + 1); ctx.lineTo(b.x + b.w * .46, b.y + 7); ctx.lineTo(b.x + b.w * .40, b.y + 12); ctx.lineTo(b.x + b.w * .53, b.y + 16); ctx.lineTo(b.x + b.w * .49, b.y + 22);
ctx.moveTo(b.x + b.w * .4, b.y + 12); ctx.lineTo(b.x + b.w * .29, b.y + 17); ctx.stroke();
}
ctx.restore();
}
for (const d of drops) {
const color = d.type === 'wide' ? '#71ead9' : '#c4a2ff';
ctx.save(); ctx.shadowColor = color; ctx.shadowBlur = 16;
ctx.fillStyle = '#19243a'; ctx.strokeStyle = color; ctx.lineWidth = 1.5;
rr(d.x - 13, d.y - 13, 26, 26, 8); ctx.fill(); ctx.stroke(); ctx.shadowBlur = 0;
drawPowerIcon(d.type, d.x, d.y, 12, color); ctx.restore();
}
for (const p of particles) {
ctx.globalAlpha = clamp(p.life / p.max, 0, 1); ctx.fillStyle = p.color;
rr(p.x, p.y, p.size, p.size, 1); ctx.fill();
}
ctx.globalAlpha = 1;
for (const ball of balls) {
for (let i = ball.trail.length - 1; i >= 0; i--) {
const pos = ball.trail[i];
ctx.fillStyle = `rgba(138, 238, 241, ${(1 - i / ball.trail.length) * .14})`;
ctx.beginPath(); ctx.arc(pos.x, pos.y, R * (1 - i / (ball.trail.length * 1.3)), 0, Math.PI * 2); ctx.fill();
}
ctx.save(); ctx.shadowColor = '#9dfaf2'; ctx.shadowBlur = 20;
ctx.fillStyle = '#f2fffe'; ctx.beginPath(); ctx.arc(ball.x, ball.y, R, 0, Math.PI * 2); ctx.fill(); ctx.restore();
}
ctx.save(); ctx.shadowColor = '#6cebd7'; ctx.shadowBlur = 20;
const pg = ctx.createLinearGradient(0, paddle.y, 0, paddle.y + paddle.h);
pg.addColorStop(0, '#b7fff2'); pg.addColorStop(1, '#55d9c8');
ctx.fillStyle = pg; rr(paddle.x - paddle.w / 2, paddle.y, paddle.w, paddle.h, 7); ctx.fill(); ctx.restore();
ctx.fillStyle = '#ffffff88'; rr(paddle.x - paddle.w / 2 + 9, paddle.y + 3, paddle.w - 18, 2, 1); ctx.fill();
if (wideTime > 0) {
ctx.fillStyle = '#9be9dc'; ctx.textAlign = 'center'; ctx.font = 'bold 9px system-ui, sans-serif';
ctx.fillText('WIDE ' + Math.ceil(wideTime) + 's', paddle.x, paddle.y + 30);
}
}
function frame(time) {
if (!lastTime) lastTime = time;
const dt = Math.min((time - lastTime) / 1000, .033);
lastTime = time;
update(dt); draw();
requestAnimationFrame(frame);
}
function setPointer(e) {
const rect = canvas.getBoundingClientRect();
paddle.target = clamp((e.clientX - rect.left) / rect.width * W, 0, W);
}
field.addEventListener('pointerdown', e => {
if (e.target.closest('button')) return;
startGame(); setPointer(e);
if (e.pointerType === 'touch') canvas.setPointerCapture(e.pointerId);
});
canvas.addEventListener('pointermove', e => {
if (!demo && !ended && (e.pointerType !== 'touch' || e.buttons)) setPointer(e);
});
document.addEventListener('keydown', e => {
if (['ArrowLeft', 'ArrowRight', ' ', 'Enter'].includes(e.key)) e.preventDefault();
if (demo) startGame();
else if (ended && (e.key === 'Enter' || e.key === ' ')) reset(false);
if (e.key === 'ArrowLeft') keys.left = true;
if (e.key === 'ArrowRight') keys.right = true;
});
document.addEventListener('keyup', e => {
if (e.key === 'ArrowLeft') keys.left = false;
if (e.key === 'ArrowRight') keys.right = false;
});
window.addEventListener('blur', () => { keys.left = false; keys.right = false; });
document.getElementById('restart').addEventListener('click', e => { e.stopPropagation(); reset(false); });
reset(true);
draw();
requestAnimationFrame(frame);
})();
</script>
</body>
</html>Reasoning trace (3,389 tokens)
**Designing collision detection** I need to deliver a polished HTML layout showcasing the game's action. It's crucial to ensure the ball doesn't pass through bricks or the paddle through continuous collision detection. I'll use methods like swept circles against rectangles for reliability. I should account for multiple balls, possibly three total, and set a frame rate limit. I'll also handle overlaps between adjacent bricks carefully to avoid false positives and ensure there are no mismatched collisions.**Calculating collision detection** I'm working on collision detection for the game where brick spacing is about 8 units and the ball radius is 6. I need to account for expanded rectangle overlap while using precise circle sweeps for resolution. I'll calculate collision candidates using swept circles against rectangles, checking for crossings along boundaries and resolving corner collisions with quadratics. I’ll implement a helper function for circle and rectangle collisions, and manage the impacts by reflecting the velocity correctly. The loop will handle up to 10 collisions to avoid tunneling issues.**Implementing paddle collision** I'm focusing on paddle collisions where the paddle is solid from the downward direction. I need to ensure that the ball can't pass through the sides when near the edges. I’ll create a function to handle full collisions with both sides and corners. If the ball is moving downward and is above the paddle, there should be a special bounce effect. The paddle's angle will be determined by the ball's position, and I'll keep the ball's speed constant at around 350 px/s within a canvas of 960x620.**Designing the game UI** I’m considering the side panel layout, which should include “HOW TO PLAY,” instructions, power-up icons, hints, and status. The screen could show a meaningful ball action with a message saying “Click or press a key to start” positioned at the lower canvas or perhaps floating at the top as a mini-banner. I want the demo mode clearly labeled and programmed to autoplay, while ensuring the game starts immediately upon user input.**Designing demo level requirements** I need to ensure that the demo doesn't complete a level in less than 3 seconds. For the layout, I might go with at least 6 rows or ideally 8 rows in total. If I set up 7 rows with 11 bricks each, that gives me 77 total. The canvas dimensions would be 900x580. I’m considering brick positions with specific x and y coordinates and a color gradient for the rows, reflecting their durability. There should be a noticeable crack after the first hit!**Calculating dimensions for canvas** I’m figuring out some dimensions for a canvas. The formula is 858 multiplied by 600, divided by 900, which gives me 572. That seems fine, so I’m noting down that I can use a height of 570 internally instead of 600 for the body. I'll also adjust the paddle position to y 530 accordingly. It feels good to have these measurements sorted!**Optimizing game layout** I’m figuring out the layout for a responsive mobile game. The header's flexible, and I want a single-column grid for the main area, with the game canvas possibly below the sidebar. For a 375 viewport width, the canvas needs adjusting to fit the aspect ratio, ensuring the bricks are still visible despite their size. The game logic involves setting the demo and running states, with UI overlays where users can click to start or interact. I'll handle key presses effectively without causing scrolling issues.**Refining game mechanics** I'm working on the mouse interactions for the canvas. The pointer activates only when the game is active. Upon clicking, the paddle should move to the mouse's X position. I want to ensure that the ball is visible, especially as the AI predicts its path based on its reflection off walls. The initial ball starts centered with a specific velocity. Additionally, I’m implementing a multi-ball feature that spawns clones with varying angles and speeds while tracking lives and scores through a dynamic UI.**Implementing catching mechanics** I'm refining the catching mechanic to allow for a wider catch for 10 seconds or enable multi-ball with exactly three balls. When multi-ball is activated, clones should spawn from the active ball but with rotated vectors to avoid heading downward. These clones will maintain a speed of 330 and occupy the same location without colliding. For scoring, a normal increment of 100 applies, and I plan to add an extra 25 on tough first hits, aiming for a break point at 150.**Exploring game mechanics** I'm considering how to handle the game mechanics. It seems like balls should only decrease when all are lost. In demo mode, I could reset the ball at the paddle without losing lives. If all are eliminated, there’s a graceful serve. For new ball launches, they could start just above the paddle. Upon game over, an overlay should show. There’s a thought to show a victory overlay and reset in demo mode with a short timeout. Power-ups will drop when bricks are destroyed, and I can draw some glowing icons!**Refining game parameters** I'm exploring how to calculate the candidate for collision detection when the velocity is negative. For the bottom of the play area, I need to check if y exceeds the height plus the radius. I plan to label the game canvas with "BRICKS REMAINING / 77" at the top. The game setup involves positioning the bricks correctly and maintaining visual clarity for the paddle. I want to add a glow effect to the ball and include particle trails for some fun aesthetics!**Adjusting paddle mechanics** I'm working on the paddle mechanics to avoid issues with overlapping the ball. If the paddle teleports using the mouse, that could cause problems, but I think it's okay if the ball never passes through the paddle from above. I plan to implement some smoothing while moving the paddle, with a target movement speed of maybe 1000 px/s for mouse control. Arrow keys will offer direct control, adjusting the paddle's x position at 620 px/s when pressed, and I'll store the paddle's center for accurate collision detection. Making the paddle shape rounded should help too!**Reviewing game design elements** I'm planning some features for a game. I want to ensure the ball's interactions with bricks are seamless, and I need to track the score accurately. It's important to use high-resolution rendering with precise canvas dimensions. I’ll add animations with requestAnimationFrame and utilize the latest features of the Canvas API. For visual elements, I'll use recognizable power icons and create a distinct design for stronger bricks. Oh, and the banner should be positioned carefully to avoid overlap.**Planning HTML and game mechanics** I'm figuring out how to set up the HTML and canvas for the game. I want the ball's starting position to be centered at a specific height and have an engaging banner with a pulsing effect. The game needs clear instructions for the user, like clicking or pressing a key to start. After the game ends, I’ll add a dark overlay with a restart button. I also need to ensure that the canvas is accessible and handle collisions effectively with proper velocity management.**Refining collision mechanics** I'm working on collision mechanics for bricks in the game. When a brick's health is greater than zero, I'll need to scan all possible impacts and apply directional restrictions. If the ball starts inside a corner, I have to calculate the exact collision to avoid loops. When the brick is hit, I'll destroy it and show some crack effects while updating the score. If multiple bricks are hit at once, I need to ensure they are positioned correctly to avoid overlapping. It’s all about getting that physics right!**Calculating paddle dynamics** I’m thinking through a scenario where something falls 220 px at 125 px/sec. If the paddle follows, it might catch it near 2.5 seconds. I wonder if the AI can catch the ball without relying solely on power. There's a chance that the pickup could miss. It’s interesting to visualize this dynamic. Okay, let’s go ahead and output the results.