Flawless run and the most considerate code: spawn-overlap avoidance, exponential damping, floor-jitter friction.
Overview › Physics Lab › R3
Physics Lab · archive round
R3 · Bouncing-ball physics: gravity, friction, elastic collisions
Archive round — this round predates the blind-first layout, so answers appear with model names attached. Newer rounds keep the models anonymous until you reveal them.
Scores on this page are the original editorial 0–10 ratings from the 2026-07-17 write-up (writing / art-direction / runtime quality) — not blind rubric scores against a documented gold answer, which is how R31 onward are scored. The underlying answers are unchanged from that write-up.
Show task
A self-contained HTML physics sim, NO user input: balls with gravity, air friction, elastic collisions (walls + ball-to-ball). 8 balls at start, auto-spawn to 40, live counter + FPS.
Untouched model code, executed headless for the verdicts; the stills are the posters.
The answers
RUNS ✓spawn ✓ 11/40 @6sairborne physics ✓
RUNS ✓spawn ✓tightest file — 4.6 KB
Compact and correct: DPR-aware, framerate-independent, glowing render.
RUNS ✓trails + reactive glow
Works, with style — but shipped a dead FPS block, and the "spatial grid" secretly calls indexOf per neighbour. Flair, minus rigour. Disclosed.
RUNS — BUTall balls grounded @6s
Executes cleanly, but the energy budget is wrong: within seconds, a still life. "Elastic" was the brief.
RUNS — BUTballs nearly invisiblespawn stuck at 8
The R1+R2 leader stumbles: dark balls on a dark background, dead spawner. This is why we runtime-test.
SyntaxError: 'lastSpawnTime' has already been declared
One duplicate declaration, total loss — the script never parses.
renderer timed out twice (>45 s)
Locks the rendering thread. Worse than crashing: it takes bystanders with it.
Frontier reference — outside the field
Answered the identical prompt outside the field, deliberately
unscored. Collected through a context-free agent rather than the harness the other answers used, so response times are
not comparable and are not shown.
Claude Opus 5frontier reference
Show code
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Bouncing Balls Physics Sim</title>
<style>
:root { color-scheme: dark; }
* { margin: 0; padding: 0; box-sizing: border-box; }
html, body { width: 100%; height: 100%; overflow: hidden; background: #0b0f1a; }
canvas { display: block; width: 100vw; height: 100vh; }
#hud {
position: fixed; top: 14px; left: 14px;
font: 600 13px/1.5 ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
color: #e8ecf6; background: rgba(14, 20, 34, .62);
border: 1px solid rgba(255,255,255,.12); border-radius: 10px;
padding: 10px 14px; letter-spacing: .04em;
backdrop-filter: blur(8px); pointer-events: none; user-select: none;
}
#hud span { color: #7dd3fc; }
#hud .fps { color: #86efac; }
</style>
</head>
<body>
<canvas id="c"></canvas>
<div id="hud">
BALLS <span id="count">0</span> / 40<br>
FPS <span class="fps" id="fps">--</span>
</div>
<script>
(() => {
"use strict";
const canvas = document.getElementById("c");
const ctx = canvas.getContext("2d", { alpha: false });
const countEl = document.getElementById("count");
const fpsEl = document.getElementById("fps");
const START_BALLS = 8;
const MAX_BALLS = 40;
const SPAWN_EVERY = 0.55; // seconds between auto-spawns
const GRAVITY = 900; // px/s^2
const AIR_DRAG = 0.16; // linear drag coefficient (1/s)
const RESTITUTION = 1.0; // fully elastic
const SUBSTEPS = 2;
let W = 0, H = 0, DPR = 1;
function resize() {
DPR = Math.min(window.devicePixelRatio || 1, 2);
W = window.innerWidth;
H = window.innerHeight;
canvas.width = Math.round(W * DPR);
canvas.height = Math.round(H * DPR);
ctx.setTransform(DPR, 0, 0, DPR, 0, 0);
// keep balls inside after a resize
for (const b of balls) {
b.x = Math.min(Math.max(b.x, b.r), Math.max(b.r, W - b.r));
b.y = Math.min(Math.max(b.y, b.r), Math.max(b.r, H - b.r));
}
}
window.addEventListener("resize", resize);
const rand = (a, b) => a + Math.random() * (b - a);
const balls = [];
function makeBall() {
const r = rand(12, 30);
const hue = Math.floor(rand(0, 360));
return {
x: rand(r, Math.max(r + 1, W - r)),
y: rand(r, Math.min(H * 0.45, Math.max(r + 1, H - r))),
vx: rand(-320, 320),
vy: rand(-160, 160),
r,
m: r * r, // mass ~ area
hue,
fill: `hsl(${hue} 85% 58%)`,
edge: `hsl(${hue} 90% 76%)`
};
}
function spawn() {
if (balls.length >= MAX_BALLS) return;
// try a few times to find a non-overlapping spot
for (let attempt = 0; attempt < 24; attempt++) {
const b = makeBall();
let clear = true;
for (const o of balls) {
const dx = o.x - b.x, dy = o.y - b.y;
if (dx * dx + dy * dy < (o.r + b.r) * (o.r + b.r)) { clear = false; break; }
}
if (clear) { balls.push(b); return; }
}
balls.push(makeBall()); // give up on placement, overlap resolver handles it
}
function step(dt) {
// integrate + air friction (exponential decay = stable at any dt)
const damp = Math.exp(-AIR_DRAG * dt);
for (const b of balls) {
b.vy += GRAVITY * dt;
b.vx *= damp;
b.vy *= damp;
b.x += b.vx * dt;
b.y += b.vy * dt;
}
// ball <-> ball elastic collisions
for (let i = 0; i < balls.length; i++) {
const a = balls[i];
for (let j = i + 1; j < balls.length; j++) {
const b = balls[j];
let dx = b.x - a.x, dy = b.y - a.y;
const minD = a.r + b.r;
let d2 = dx * dx + dy * dy;
if (d2 >= minD * minD || d2 === 0) {
if (d2 !== 0) continue;
dx = 0.01; dy = 0; d2 = 0.0001;
}
const d = Math.sqrt(d2);
const nx = dx / d, ny = dy / d;
// positional correction (split by inverse mass)
const overlap = minD - d;
const invA = 1 / a.m, invB = 1 / b.m, invSum = invA + invB;
a.x -= nx * overlap * (invA / invSum);
a.y -= ny * overlap * (invA / invSum);
b.x += nx * overlap * (invB / invSum);
b.y += ny * overlap * (invB / invSum);
// impulse along the normal
const rvn = (b.vx - a.vx) * nx + (b.vy - a.vy) * ny;
if (rvn > 0) continue; // already separating
const jimp = -(1 + RESTITUTION) * rvn / invSum;
a.vx -= jimp * invA * nx;
a.vy -= jimp * invA * ny;
b.vx += jimp * invB * nx;
b.vy += jimp * invB * ny;
}
}
// walls (elastic)
for (const b of balls) {
if (b.x - b.r < 0) { b.x = b.r; b.vx = Math.abs(b.vx) * RESTITUTION; }
else if (b.x + b.r > W) { b.x = W - b.r; b.vx = -Math.abs(b.vx) * RESTITUTION; }
if (b.y - b.r < 0) { b.y = b.r; b.vy = Math.abs(b.vy) * RESTITUTION; }
else if (b.y + b.r > H) { b.y = H - b.r; b.vy = -Math.abs(b.vy) * RESTITUTION; }
}
}
function draw() {
ctx.fillStyle = "#0b0f1a";
ctx.fillRect(0, 0, W, H);
// subtle floor glow
const g = ctx.createLinearGradient(0, H - 140, 0, H);
g.addColorStop(0, "rgba(125,211,252,0)");
g.addColorStop(1, "rgba(125,211,252,0.07)");
ctx.fillStyle = g;
ctx.fillRect(0, H - 140, W, 140);
for (const b of balls) {
const grad = ctx.createRadialGradient(
b.x - b.r * 0.35, b.y - b.r * 0.4, b.r * 0.1,
b.x, b.y, b.r
);
grad.addColorStop(0, `hsl(${b.hue} 95% 78%)`);
grad.addColorStop(1, b.fill);
ctx.beginPath();
ctx.arc(b.x, b.y, b.r, 0, Math.PI * 2);
ctx.fillStyle = grad;
ctx.fill();
ctx.lineWidth = 1.5;
ctx.strokeStyle = b.edge;
ctx.stroke();
}
}
// --- main loop ---
let last = performance.now();
let spawnTimer = 0;
let fpsAccum = 0, fpsFrames = 0;
function frame(now) {
let dt = (now - last) / 1000;
last = now;
if (dt > 0.05) dt = 0.05; // clamp after tab switches / stalls
// auto-spawn
if (balls.length < MAX_BALLS) {
spawnTimer += dt;
while (spawnTimer >= SPAWN_EVERY && balls.length < MAX_BALLS) {
spawnTimer -= SPAWN_EVERY;
spawn();
}
}
const sub = dt / SUBSTEPS;
for (let s = 0; s < SUBSTEPS; s++) step(sub);
draw();
// HUD
fpsAccum += dt; fpsFrames++;
if (fpsAccum >= 0.35) {
fpsEl.textContent = (fpsFrames / fpsAccum).toFixed(0);
fpsAccum = 0; fpsFrames = 0;
}
countEl.textContent = balls.length;
requestAnimationFrame(frame);
}
resize();
for (let i = 0; i < START_BALLS; i++) spawn();
countEl.textContent = balls.length;
requestAnimationFrame(frame);
})();
</script>
</body>
</html>