R59 · Billiard break with spin and friction
Every answer below carries the name of the model that wrote it. Blind refers to the scoring: the review was done without knowing which model produced which answer — the published result names them.
2 of the 11 local models delivered nothing through our test harness in this round — for reasons that lie with our setup, not with the models. Those cards are marked DNF like any other, but they are not evidence about those models — the reason is noted on the card itself.
A frontier reference answered the same task (Claude Opus 5) — outside the field and deliberately unscored, further down the page.
Show task
Create ONE self-contained HTML file (STRICT LIMIT: max 120 lines total) that shows a billiard break with rolling friction on a <canvas>. A cue ball strikes a rack of exactly 10 numbered balls on a table with cushions. Model ball-to-ball elastic collisions, cushion bounces and rolling friction so the balls slow down and come to rest. When all balls have stopped, re-rack and break again automatically. Smooth requestAnimationFrame animation, NO user input, starts immediately and keeps running. No external libraries, no comments needed. Output ONLY the HTML file content, nothing else.
The answers
Show code
```html
<!DOCTYPE html>
<html>
<head><title>Billiard Break</title></head>
<body style="margin: 0; background-color: #2c3e50;"><canvas id="billiardsCanvas" width="800" height="450" style="background-color: #1abc9c; border: 10px solid #2ecc71;"></canvas><script>
const canvas = document.getElementById('billiardsCanvas');
const ctx = canvas.getContext('2d');
let balls = [];
let isBreaking = false;
let frameCount = 0;
const R = 8; // Ball radius
const FRICTION = 0.995;
const STOP_THRESHOLD = 0.1;
const TABLE_WIDTH = canvas.width - 20;
const TABLE_HEIGHT = canvas.height - 20;
function initRack() {
balls = [];
// Cue Ball (Ball 0)
balls.push({ x: 50, y: 225, vx: 10, vy: 0, radius: R, id: 0, color: 'white', isCue: true });
// Rack of 10 balls (Numbered 1 to 10)
for (let i = 0; i < 3; i++) { // Rows (i=0 is top)
for (let j = 0; j < 4 - Math.min(i, 2); j++) { // Columns
const index = 1 + i * 4 + j;
if (index <= 10) {
balls.push({ x: 50 + 30 + j * 30, y: 225 - i * 30, vx: 0, vy: 0, radius: R, id: index, color: ['#e74c3c', '#f1c40f', '#9b59b6', '#3498db'][index % 4], isCue: false });
}
}
}
// Adjust cue ball starting position slightly for better break angle
balls[0].x = 50; balls[0].y = 225;
}
function update() {
if (!isBreaking) return;
for (let i = 0; i < balls.length; i++) {
const ball = balls[i];
// Apply friction
ball.vx *= FRICTION;
ball.vy *= FRICTION;
// Check if stationary
if (Math.hypot(ball.vx, ball.vy) < STOP_THRESHOLD && Math.abs(ball.x - balls[0].x) > 10) {
// Only mark as stopped if not the cue ball and moving slowly
if (!ball.isCue && Math.random() < 0.95) { // Simple random check to prevent immediate re-break
ball.vx = 0;
ball.vy = 0;
}
}
// Update position
ball.x += ball.vx;
ball.y += ball.vy;
}
let collisionsOccurred = false;
// Cushion Collision
for (const ball of balls) {
if (ball.x - R < 0 || ball.x + R > canvas.width) {
ball.vx *= -1;
ball.x = Math.max(R, Math.min(canvas.width - R, ball.x));
collisionsOccurred = true;
}
if (ball.y - R < 0 || ball.y + R > canvas.height) {
ball.vy *= -1;
ball.y = Math.max(R, Math.min(canvas.height - R, ball.y));
collisionsOccurred = true;
}
}
// Ball-to-Ball Collision (N^2 check)
for (let i = 0; i < balls.length; i++) {
const b1 = balls[i];
for (let j = i + 1; j < balls.length; j++) {
const b2 = balls[j];
const dx = b2.x - b1.x;
const dy = b2.y - b1.y;
const distSq = dx * dx + dy * dy;
const minDist = b1.radius + b2.radius;
if (distSq < minDist * minDist) {
collisionsOccurred = true;
// Collision resolution: Overlap correction
const distance = Math.sqrt(distSq);
const correctionX = ((minDist / distance) - 1) * dx * 0.5;
const correctionY = ((minDist / distance) - 1) * dy * 0.5;
b1.x -= correctionX; b1.y -= correctionY;
b2.x += correctionX; b2.y += correctionY;
// Calculate relative velocity and normal vector
const dvx = b2.vx - b1.vx;
const dvy = b2.vy - b1.vy;
const dotProd = dvx * dx + dvy * dy;
if (dotProd > 0) continue; // Balls moving away
// Impulse calculation (Elastic collision, masses assumed equal to 1)
const impulse = -(1 + 1) / (1/1 + 1/1) * dotProd;
b1.vx -= dvx * impulse * dx / (dx * dx + dy * dy);
b1.vy -= dvy * impulse * dy / (dx * dx + dy * dy);
b2.vx += dvx * impulse * dx / (dx * dx + dy * dy);
b2.vy += dvy * impulse * dy / (dx * dx + dy * dy);
}
}
}
// Check if all balls have stopped
const activeBalls = balls.filter(b => Math.hypot(b.vx, b.vy) >= STOP_THRESHOLD);
if (activeBalls.length === 0 && collisionsOccurred === false && frameCount > 60) {
isBreaking = false;
setTimeout(() => {
initRack();
// Give the cue ball a strong initial hit vector after waiting 1 second
balls[0].vx = Math.cos(Math.PI / 4) * 15;
balls[0].vy = -Math.sin(Math.PI / 4) * 15;
isBreaking = true;
}, 1200);
}
}
function draw() {
ctx.clearRect(0, 0, canvas.width, canvas.height);
for (const ball of balls) {
ctx.beginPath();
ctx.arc(ball.x, ball.y, ball.radius, 0, Math.PI * 2);
ctx.fillStyle = ball.color;
if (ball.isCue) ctx.strokeStyle = 'white'; else ctx.strokeStyle = '#333';
ctx.lineWidth = 1;
ctx.fill();
ctx.stroke();
// Draw number on balls 1-9
if (!ball.isCue && ball.id >= 1 && ball.id <= 9) {
ctx.fillStyle = 'black';
ctx.font = 'bold 14px Arial';
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.fillText(ball.id, ball.x, ball.y);
}
}
}
function gameLoop() {
update();
draw();
requestAnimationFrame(gameLoop);
}
initRack(); // Initial setup
gameLoop(); // Start the animation loop
</script></body></html>
```Show code
```html
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Billiard Break</title>
<style>
body { margin: 0; overflow: hidden; }
canvas { display: block; }
</style>
</head>
<body>
<canvas id="billiardCanvas"></canvas>
<script>
const canvas = document.getElementById('billiardCanvas');
const ctx = canvas.getContext('2d');
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
const balls = [];
const rackPos = { x: 400, y: 350 };
const frictionCoeff = 0.01;
class Ball {
constructor(x, y, radius, color, mass) {
this.x = x; this.y = y;
this.vx = 0; this.vy = 0;
this.radius = radius;
this.color = color;
this.mass = mass;
}
draw() {
ctx.beginPath();
ctx.arc(this.x, this.y, this.radius, 0, Math.PI * 2);
ctx.fillStyle = this.color;
ctx.fill();
ctx.closePath();
}
update() {
if (this.vx > 0) this.vx -= frictionCoeff; else if (this.vx < 0) this.vx += frictionCoeff;
if (this.vy > 0) this.vy -= frictionCoeff; else if (this.vy < 0) this.vy += frictionCoeff;
this.x += this.vx; this.y += this.vy;
if (this.x - this.radius < 0 || this.x + this.radius > canvas.width) {
this.vx = -this.vx;
}
if (this.y - this.radius < 0 || this.y + this.radius > canvas.height) {
this.vy = -this.vy;
}
for (let other of balls) {
if (other !== this && Math.hypot(this.x - other.x, this.y - other.y) < this.radius + other.radius) {
const dx = other.x - this.x; const dy = other.y - this.y;
const dist = Math.sqrt(dx * dx + dy * dy);
const angle = Math.atan2(dy, dx);
const speed1 = this.vx * Math.cos(angle) + this.vy * Math.sin(angle);
const speed2 = other.vx * Math.cos(angle + Math.PI) + other.vy * Math.sin(angle + Math.PI);
const finalSpeed1 = ((this.mass - other.mass) * speed1 + 2 * other.mass * speed2) / (this.mass + other.mass);
const finalSpeed2 = ((other.mass - this.mass) * speed2 + 2 * this.mass * speed1) / (this.mass + other.mass);
this.vx = Math.cos(angle) * finalSpeed1;
this.vy = Math.sin(angle) * finalSpeed1;
other.vx = Math.cos(angle + Math.PI) * finalSpeed2;
other.vy = Math.sin(angle + Math.PI) * finalSpeed2;
}
}
if (Math.abs(this.vx) < 0.1 && Math.abs(this.vy) < 0.1) {
this.vx = 0; this.vy = 0;
}
}
}
function createBalls() {
balls.splice(0, balls.length);
for (let i = 0; i < 15; i++) {
const angle = i * Math.PI / 7.5 + Math.PI / 2;
const x = rackPos.x + 40 * Math.cos(angle);
const y = rackPos.y + 40 * Math.sin(angle);
balls.push(new Ball(x, y, 10, i === 0 ? 'white' : `hsl(${i*24}, 100%, 50%)`, 1));
}
}
function animate() {
ctx.clearRect(0, 0, canvas.width, canvas.height);
let allStopped = true;
balls.forEach(b => { b.update(); b.draw(); if (b.vx !== 0 || b.vy !== 0) allStopped = false; });
if (allStopped) createBalls();
requestAnimationFrame(animate);
}
createBalls();
requestAnimationFrame(animate);
</script>
</body>
</html>
```Show code
```html
<!DOCTYPE html>
<html>
<head>
<canvas id="c" width=800 height=600 style="background:#4CAF50"></canvas>
<style>canvas{position:absolute; left:0; top:-30px}</style>
</head>
<body>
<script>
const c=document.getElementById('c'), ctx=c.getContext('2d');
class Ball {
constructor(x,y) {this.x=x,this.y=y,this.vx=0,this.vy=0;
this.r=15;this.friction=.98;this restitution=.7}
draw() {
ctx.fillStyle=randomColor();
ctx.beginPath();ctx.arc(this.x,this.y,this.r,0,Math.PI*2);
ctx.fill()
}
update() {
if(Math.abs(this.vx)<.1 && Math.abs(this.vy)<.1) return;
this.vx *= this.friction;this.vy *= this.friction;
this.x += this.vx;this.y += this.vy
}
}
let balls=[...Array(10)].map((_,i)=>new Ball(400+Math.random()*200-100,300));
balls[0].vx=5;balls[0].vy=-5;
const cue=new Ball(100,300);
let stopped=true;
function randomColor(){return`hsl(${Math.random()*360},70%,80%)`}
function animate(){
ctx.clearRect(0,0,c.width,c.height);
balls.forEach(b=>{b.update();b.draw()});
cue.draw();
if(!stopped && balls.every(b=>{
return Math.abs(b.vx)<.1 && Math.abs(b.vy)<.1
})){
stopped=true;balls.splice(1);balls.push(new Ball(400+Math.random()*200-100,300));
cue.x=100;cue.y=300;
}
requestAnimationFrame(animate)
}
function Collision(b1,b2){
if(BBoxCollision(b1,b2)){
let dx=b2.x-b1.x, dy=b2.y-b1.y;
let dvx=b2.vx - b1.vx, dvy=b2.vy - b1.vy;
let dot=(dx*dx + dy*dy);
if(dot>0){
let temp = (b1.vx*b2.vx + b1.vy*b2.vy) / dot * 0.5;
b1.vx -= (dvx * temp)
b1.vy -= (dvy * temp)
b2.vx += (dvx * temp)
b2.vy += (dvy * temp)
}
}
}
function BBoxCollision(b1,b2){
return Math.abs(b1.x - b2.x) < b1.r + b2.r &&
Math.abs(b1.y - b2.y) < b1.r + b2.r
}
animate()
</script>
</body>
</html>
```Show code
```html
<!DOCTYPE html>
<html>
<head>
<title>Billiard Break Simulation</title>
<style>body { margin: 0; display: flex; justify-content: center; align-items: center; height: 100vh; background-color: #222; }</style>
</head>
<body>
<canvas id="billiardCanvas"></canvas>
<script>
const canvas = document.getElementById('billiardCanvas');
const ctx = canvas.getContext('2d');
let ws, balls = [];
const TABLE_WIDTH = 800;
const TABLE_HEIGHT = 400;
const BALL_RADIUS = 10;
const FRICTION = 0.995;
const DAMPING = 0.999;
const CUE_BALL_START_POS = { x: TABLE_WIDTH / 2, y: TABLE_HEIGHT - 3 * BALL_RADIUS };
canvas.width = TABLE_WIDTH;
canvas.height = TABLE_HEIGHT;
function createBalls(racked) {
balls = [];
const ballCount = racked ? 10 : 11;
for (let i = 0; i < ballCount; i++) balls.push({ x: Math.random() * TABLE_WIDTH, y: Math.random() * TABLE_HEIGHT, vx: 0, vy: 0, r: BALL_RADIUS, id: i });
// Set up the rack structure
const startX = TABLE_WIDTH / 2 - (ballCount > 1 ? 5 * BALL_RADIUS : 0);
const startY = TABLE_HEIGHT / 2 - 3 * BALL_RADIUS;
for (let i = 0; i < ballCount; i++) {
balls[i].x = startX + (i % 4) * (BALL_RADIUS * 2.5);
balls[i].y = startY + Math.floor(i / 4) * (BALL_RADIUS * 2.5);
}
// Cue ball setup (last ball added is the cue ball)
const cueBall = { x: CUE_BALL_START_POS.x, y: CUE_BALL_START_POS.y, vx: -10, vy: 0, r: BALL_RADIUS, id: 'cue' };
balls[ballCount - 1] = cueBall;
}
function checkCollision(b1, b2) {
const dx = b2.x - b1.x;
const dy = b2.y - b1.y;
const distSq = dx * dx + dy * dy;
const minDistance = b1.r + b2.r;
if (distSq < minDistance * minDistance) {
let angle = Math.atan2(dy, dx);
let overlap = minDistance - Math.sqrt(distSq);
// Resolve overlap by pushing them apart along the collision normal
b1.x -= (overlap / 2) * Math.cos(angle);
b1.y -= (overlap / 2) * Math.sin(angle);
b2.x += (overlap / 2) * Math.cos(angle);
b2.y += (overlap / 2) * Math.sin(angle);
// Calculate impulse for elastic collision
let nx = dx / Math.sqrt(distSq);
let ny = dy / Math.sqrt(distSq);
let v1n = b1.vx * nx + b1.vy * ny;
let v2n = b2.vx * nx + b2.vy * ny;
let impulseMagnitude = 2 * (v2n - v1n) / (1/1 + 1/1); // Mass=1 for all balls
b1.vx -= impulseMagnitude * nx;
b1.vy -= impulseMagnitude * ny;
b2.vx += impulseMagnitude * nx;
b2.vy += impulseMagnitude * ny;
return true;
}
return false;
}
function updatePhysics() {
for (let i = 0; i < balls.length; i++) {
const b = balls[i];
// Apply friction and damping
b.vx *= FRICTION;
b.vy *= FRICTION;
b.vx *= DAMPING;
b.vy *= DAMPING;
// Update position
b.x += b.vx;
b.y += b.vy;
// Wall collisions (Cushions)
if (b.x - b.r < 0 || b.x + b.r > TABLE_WIDTH) {
b.vx *= -1;
b.x = Math.max(b.r, Math.min(b.x, TABLE_WIDTH - b.r));
}
if (b.y - b.r < 0 || b.y + b.r > TABLE_HEIGHT) {
b.vy *= -1;
b.y = Math.max(b.r, Math.min(b.y, TABLE_HEIGHT - b.r));
}
// Collisions with other balls
for (let j = i + 1; j < balls.length; j++) {
if (checkCollision(b, balls[j])) {}
}
}
}
function draw() {
ctx.fillStyle = '#004c00';
ctx.fillRect(0, 0, TABLE_WIDTH, TABLE_HEIGHT);
balls.forEach(b => {
ctx.beginPath();
ctx.arc(b.x, b.y, b.r, 0, Math.PI * 2);
ctx.fillStyle = (b.id === 'cue') ? '#333' : (parseInt(b.id) % 2 == 0 ? '#ccc' : '#eee');
ctx.fill();
ctx.strokeStyle = '#000';
ctx.lineWidth = 1;
ctx.stroke();
});
}
function animate() {
updatePhysics();
draw();
let activeBalls = balls.filter(b => Math.abs(b.vx) > 0.1 || Math.abs(b.vy) > 0.1);
if (activeBalls.length > 0) {
requestAnimationFrame(animate);
} else {
setTimeout(() => {
createBalls(true); // Re-rack and re-break
requestAnimationFrame(animate);
}, 1000);
}
}
createBalls(false);
animate();
</script>
</body>
</html>
```Show code
```html
<!DOCTYPE html>
<html>
<head>
<title>Billiard Break</title>
<style>body{margin:0;overflow:hidden;background:#333}canvas{display:block}</style>
</head>
<body>
<canvas id="billiardsCanvas"></canvas>
<script>
const canvas = document.getElementById('billiardsCanvas');
const ctx = canvas.getContext('2d');
let W, H;
function setup() {
W = canvas.width = 800;
H = canvas.height = 500;
}
setup();
const R = 8;
const DAMPING = 0.995; // Rolling friction factor
const WALL_DAMPING = 0.85; // Energy loss on cushion bounce
let balls = [];
let frameCount = 0;
function createRack(startX, startY, spacing) {
balls = [];
for (let r = 0; r < 3; r++) {
for (let c = 0; c < 4; c++) {
// Creates a semi-rack pattern
if ((r + c) % 2 === 0) {
balls.push({ x: startX + c * spacing, y: startY - r * spacing, vx: (c === 3 ? 1 : 0), vy: (r === 0 ? 0.5 : 0), id: (r * 4 + c) });
} else {
balls.push({ x: startX + c * spacing, y: startY - r * spacing, vx: (c === 3 ? 1 : 0), vy: (r === 0 ? 0.5 : 0), id: (r * 4 + c) });
}
}
}
// Cue ball initialization (index 0)
balls[0] = { x: startX - spacing, y: startY, vx: 12, vy: 0 };
}
function initGame() {
createRack(W * 0.4, H * 0.75, R * 3);
frameCount = 0;
}
function checkCollisions(b1, b2) {
const dx = b2.x - b1.x;
const dy = b2.y - b1.y;
let distSq = dx * dx + dy * dy;
let minDistSq = 4 * R * R;
if (distSq < minDistSq) {
let nx = dx / Math.sqrt(distSq);
let ny = dy / Math.sqrt(distSq);
let overlap = Math.sqrt(minDistSq) - Math.sqrt(distSq);
// Separate balls slightly to prevent sticking
b1.x -= nx * overlap * 0.5;
b1.y -= ny * overlap * 0.5;
b2.x += nx * overlap * 0.5;
b2.y += ny * overlap * 0.5;
// Elastic collision response (simplified)
let v_dot = b1.vx * nx + b1.vy * ny;
let v_other_dot = b2.vx * nx + b2.vy * ny;
b1.vx -= 2 * v_other_dot * nx;
b1.vy -= 2 * v_other_dot * ny;
b2.vx += 2 * v_dot * nx;
b2.vy += 2 * v_dot * ny;
}
}
function updatePhysics() {
let allStopped = true;
for (let i = 0; i < balls.length; i++) {
const b = balls[i];
if (Math.abs(b.vx) > 0.1 || Math.abs(b.vy) > 0.1) allStopped = false;
// Apply friction damping
b.vx *= DAMPING;
b.vy *= DAMPING;
// Update position
b.x += b.vx;
b.y += b.vy;
// Wall collisions (Horizontal)
if (Math.abs(b.x - R) < 0 || Math.abs(b.x - W - R) < 0) {
b.vx *= -WALL_DAMPING;
b.x = Math.max(R, Math.min(W - R, b.x));
}
// Wall collisions (Vertical)
if (Math.abs(b.y - R) < 0 || Math.abs(b.y - H - R) < 0) {
b.vy *= -WALL_DAMPING;
b.y = Math.max(R, Math.min(H - R, b.y));
}
// Check collisions with other balls
for (let j = i + 1; j < balls.length; j++) {
checkCollisions(b, balls[j]);
}
}
return allStopped;
}
function draw() {
ctx.fillStyle = '#0B283D'; // Table color
ctx.fillRect(0, 0, W, H);
balls.forEach((b, i) => {
let color = (i === 0) ? 'red' : (i % 2 === 0 ? 'white' : '#CC6633'); // Cue ball is red
ctx.beginPath();
ctx.arc(b.x, b.y, R, 0, Math.PI * 2);
ctx.fillStyle = color;
ctx.fill();
});
}
function gameLoop() {
const stopped = updatePhysics();
draw();
if (stopped && frameCount % 60 === 0) {
setTimeout(initGame, 1000); // Re-rack after a delay
} else if (!stopped) {
frameCount++;
requestAnimationFrame(gameLoop);
}
}
window.onload = () => {
initGame();
requestAnimationFrame(gameLoop);
};
</script>
</body>
</html>
```Show code
```html
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Billiard Break</title>
<style>
body { margin: 0; }
canvas { border: 1px solid black; }
</style>
</head>
<body>
<script>
let canvas = document.createElement('canvas');
document.body.appendChild(canvas);
let ctx = canvas.getContext('2d');
let balls = [];
for (let i=0; i<10; i++) {
let x = Math.random() * (canvas.width - 20);
let y = Math.random() * (canvas.height - 20);
balls.push({
x, y,
vx: (Math.random()-0.5)*2,
vy: (Math.random()-0.5)*2,
r: 10
});
}
let cueBall = {x: canvas.width/2, y: canvas.height - 50, vx: 0, vy: 0};
function draw() {
ctx.clearRect(0,0,canvas.width,canvas.height);
for (let b of balls) {
let r = b.r;
ctx.beginPath();
ctx.arc(b.x, b.y, r, 0, Math.PI*2);
ctx.fillStyle = 'black';
ctx.fill();
ctx.strokeStyle = 'black';
ctx.lineWidth = 1;
ctx.stroke();
ctx.font = '12px Arial';
ctx.textAlign = "center";
ctx.textBaseline = "middle";
ctx.fillStyle = 'black';
ctx.fillText(b.x, b.x + r/2, b.y);
let dx = (cueBall.x-b.x)/Math.hypot(cueBall.x-b.x,cueBall.y-b.y);
let dy = (cueBall.y-b.y)/Math.hypot(cueBall.x-b.x,cueBall.y-b.y);
ctx.beginPath();
ctx.moveTo(b.x, b.y);
ctx.lineTo(b.x + dx*r, b.y + dy*r);
ctx.strokeStyle = 'black';
ctx.stroke();
}
let cueX = cueBall.x;
if (cueBall.vy > 0) {
let y = canvas.height - Math.floor(cueBall.vy * 100 / 3);
ctx.beginPath();
ctx.moveTo(cueX, y);
ctx.lineTo(cueX + 50, y);
ctx.strokeStyle = 'black';
ctx.stroke();
}
}
function update() {
for (let b of balls) {
if (b.x < 10 || b.x > canvas.width-10) b.vx *= -1;
if (b.y < 10 || b.y > canvas.height-10) b.vy *= -1;
if (Math.hypot(b.x-cueBall.x, b.y-cueBall.y) < cueBall.r + b.r) {
let v1 = Math.sqrt(cueBall.vx**2+cueBall.vy**2);
let v2 = Math.sqrt(b.vx**2+b.vy**2);
let theta = Math.atan2(b.vy, b.vx) - Math.atan2(cueBall.vy, cueBall.vx);
if (v1 > v2) {
let temp = v2;
v2 = v1 + (temp-v1)/4;
v1 = temp;
}
b.vx = v2 * Math.cos(theta) * 0.5;
b.vy = v2 * Math.sin(theta) * 0.5;
}
let dx = cueBall.x-b.x;
let dy = cueBall.y-b.y;
if (Math.hypot(dx,dy) < cueBall.r+b.r) {
let angle = Math.atan2(dy,dx);
b.vx += -dx/100;
b.vy += -dy/100;
dx = Math.cos(angle)*cueBall.r + Math.sin(angle)*cueBall.r;
dy = -Math.sin(angle)*cueBall.r + Math.cos(angle)*cueBall.r;
}
if (dx < 0) b.x += cueBall.vx * 0.01;
else b.x -= cueBall.vx * 0.01;
if (dy < 0) b.y += cueBall.vy * 0.01;
else b.y -= cueBall.vy * 0.01;
}
let alive = balls.filter(b => Math.hypot(b.vx, b.vy) > 1);
if (alive.length === 0) {
canvas.width = canvas.width;
for (let i=0; i<10; i++) {
let x = Math.random() * (canvas.width - 20);
let y = Math.random() * (canvas.height - 20);
balls.push({
x, y,
vx: (Math.random()-0.5)*2,
vy: (Math.random()-0.5)*2,
r: 10
});
}
} else {
cueBall.x = alive[0].x + Math.cos(Math.atan2(alive[0].vy,alive[0].vx))*20;
cueBall.y = alive[0].y + Math.sin(Math.atan2(alive[0].vy,alive[0].vx))*20;
}
}
function animate() {
requestAnimationFrame(animate);
update();
draw();
}
animate();
</script>
</body>
</html>
```Show code
```html
<!DOCTYPE html>
<html><body style="background:#333;margin:0;display:flex;justify-content:center;align-items:center;height:100vh">
<canvas id=c width=640 height=480></canvas>
<script>
b=[];t=null;speed=.2;tgt={x:320,y:295};size=20;friction=.99
class Ball{constructor(x,y,vx,vy,r,n){this.x=x;this.y=y;this.vx=vx;this.vy=vy;this.r=r||size;this.n=n}
draw(){c.fillStyle='white';c.beginPath();c.arc(this.x,this.y,this.r,0,2*Math.PI);c.fill();}
move(){this.x+=this.vx*=friction;this.y+=this.vy*=friction;if(Math.abs(this.vx)<.1&&Math.abs(this.vy)<.1)this.stop()}
stop(){this.vx=0;this.vy=0}
collide(b,a){let dx=b.x - a.x, dy=b.y -a.y,d=Math.sqrt(dx*dx+dy*dy);if(d < size*2){let nx=dx/d,ny=dy/d,vx1=b.vx,nx1=a.vx,vx2=a.vx,vy1=b.vy,vy2=a.vy
b.vx=vx1- (nx*(vx1 - vx2)+ ny*(vy1 - vy2))*nx*2
b.vy=vy1- (ny*(vy1 - vy2)+ nx*(vy1 - vy2))*ny*2
a.vx=nx1 + (nx*(vx1 - vx2)+ ny*(vy1 - vy2))*nx*2
a.vy=vy2 + (ny*(vy1 - vy2)+ nx*(vy1 - vy2))*ny*2}
bounce(b){if(b.x - size < 0||b.x + size >640)b.vx *= -1;if(b.y -size<0||b.y+size>480)b.vy *=-1}
setup(r=5,i=0,x=tgt.x,y=tgt.y- r*2+ (Math.floor(i/3))*r*2){for(let j=0;j<=i%3*2+1;j++){let a=new Ball(x +j*r -r*(Math.floor(i/3)), y +3*i, 0,0, size, i*j+j+i);b.push(a)}}
init(){b=[];this.setup(5,0);b[9]=new Ball(tgt.x-2*size,tgt.y,0,0,size,10);b.unshift(new Ball(160,240,-3*speed,0))}
animate(){requestAnimationFrame(this.animate.bind(this));c.clearRect(0,0,640,480);c.fillStyle='lime';c.fillRect(0,0,640,480);for(let a of b){a.move();a.bounce(a);a.draw()}b[0].collide(b[0],b[1]);let stop=true;for(let a of b)if(a.vx!=0||a.vy!=0)stop=false;if(stop)this.init()}
</script>
<script>
t=new Ball(0,0,0,0);t.init();t.animate()
</script></body></html>
```Show code
```html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Billiard Break Animation</title>
<style>
body {
margin: 0;
display: flex;
justify-content: center;
align-items: center;
height: 100vh;
background-color: #222;
}
canvas {
border: 3px solid #4CAF50;
box-shadow: 0 0 10px rgba(0, 0, 0, 0.5);
}
</style>
</head>
<body>
<canvas id="billiardCanvas" width="600" height="600"></canvas>
<script>
const canvas = document.getElementById("billiardCanvas");
const ctx = canvas.getContext("2d");
// Ball properties
const ballRadius = 25;
const ballsPerRow = 4;
const gap = ballRadius * 2;
let balls = [];
function createBalls() {
for (let i = 0; i < 10; i++) {
const x = (i % ballsPerRow) * gap + ballRadius + gap / 2;
const y = Math.floor(i / ballsPerRow) * gap + ballRadius + gap / 2;
balls.push({
x,
y,
dx: (Math.random() - 0.5) * 4,
dy: (Math.random() - 0.5) * 4,
color: `hsl(${i * 36}, 100%, 50%)`
});
}
// Add cue ball at the start of each break
balls.unshift({
x: canvas.width / 2,
y: canvas.height - ballRadius * 4,
dx: (Math.random() - 0.5) * 16,
dy: -(Math.random() + 0.5) * 8,
color: "#FFF"
});
}
function drawBall(ball) {
ctx.beginPath();
ctx.arc(ball.x, ball.y, ballRadius, 0, Math.PI * 2);
ctx.fillStyle = ball.color;
ctx.fill();
ctx.closePath();
}
function collideBalls() {
for (let i = 0; i < balls.length - 1; i++) {
const ballA = balls[i];
for (let j = i + 1; j < balls.length; j++) {
const ballB = balls[j];
// Calculate distance between centers of balls
const dx = ballA.x - ballB.x;
const dy = ballA.y - ballB.y;
const distance = Math.sqrt(dx * dx + dy * dy);
if (distance < 2 * ballRadius) {
// Handle elastic collision
const angleRad = Math.atan2(dy, dx);
const normalX = Math.cos(angleRad);
const normalY = Math.sin(angleRad);
let [va, vb] = [(ballA.dx + ballB.dx) / 4, (ballA.dy + ballB.dy) / 4];
let [ua, ub] = [[ballA.dx - va, ballA.dy - va], [ballB.dx - vb, ballB.dy - vb]];
const dotProduct = ua[0]*ub[1]-ub[0]*ua[1];
if(dotProduct > 0){
continue;
}
ua[0] *= -1;
ub[0] *= -1;
va += ua[0]/2 + ub[0]/2;
vb += ua[0]/2 + ub[0]/2;
ballA.dx = va + va;
ballB.dx = vb + vb;
}
}
}
}
function applyRollingFriction() {
for (let i = 1; i < balls.length; i++) {
const ball = balls[i];
// Calculate friction force based on velocity magnitude
let speedSq = ball.dx ** 2 + ball.dy ** 2;
if (speedSq <= 0) continue;
let speedMag = Math.sqrt(speedSq);
let frictionFactor = 0.95 - speedMag * 0.01;
// Apply friction to velocity
ball.dx *= frictionFactor;
ball.dy *= frictionFactor;
}
}
function bounceOffCushions() {
for (let i = 0; i < balls.length; i++) {
const ball = balls[i];
if ((ball.x - ballRadius) <= ballRadius ||
(canvas.width - ballRadius - ball.x) <= ballRadius) {
// Reverse horizontal velocity when hitting a cushion
ball.dx *= -1;
}
if ((ball.y - ballRadius) <= ballRadius ||
(canvas.height - ballRadius - ball.y) <= ballRadius){
// Reverse vertical velocity when hitting a cushion
ball.dy *= -1;
}
}
}
function resetBalls() {
balls = [];
createBalls();
}
function animate() {
ctx.clearRect(0, 0, canvas.width, canvas.height);
for (let i = 0; i < balls.length; i++) {
const ball = balls[i];
// Update ball positions
ball.x += ball.dx;
ball.y += ball.dy;
drawBall(ball);
}
collideBalls();
applyRollingFriction();
bounceOffCushions();
requestAnimationFrame(animate);
}
createBalls();
animate();
</script>
</body>
</html>
```Show code
<think> Okay, I need to create a self-contained HTML file that simulates a billiard break with rolling friction. The user wants it to be self-contained, under 120 lines, with no external libraries. Let me break down the requirements: First, the HTML structure. I'll need a canvas element and some JavaScript inside script tags. The canvas will be the main element where everything is drawn. The balls: there's a cue ball and 10 numbered balls. So total 11 balls. The cue ball is usually white, but maybe I'll just represent it as a separate object. The numbered balls are arranged in a rack. Wait, a typical billiard rack has 15 balls, but the user specified exactly 10. Hmm, maybe a triangle with 10? The exact arrangement might not matter as much as getting them in a tight group initially. Physics aspects: elastic collisions between balls, cushion bounces (so collisions with the table edges and cushions?), and rolling friction causing the balls to slow down over time. Then, when all balls have stopped, re-rack and start again automatically. Animation loop using requestAnimationFrame. No user input, so everything happens automatically from start. I need to model each ball's position, velocity, acceleration due to friction, etc. For collisions between balls, I'll need to check pairs of balls and handle elastic collisions when their distance is less than the sum of their radii. Since all balls are the same size, radius is a constant. The cushions: the table edges. When a ball hits the edge, it should reflect, but with some energy loss? Or is it just perfectly elastic? The user says cushion bounces, so maybe perfect reflection? But rolling friction applies to slow them down over time regardless. Rolling friction: Each frame, the velocity of the ball is reduced by a certain factor. Like velocity *= 0.99 or something, but maybe a constant deceleration. Wait, rolling friction would cause a continuous deceleration. Since in game physics, maybe each step we apply a damping factor. So velocity *= frictionFactor per frame. FrictionFactor less than 1, say 0.99 per frame. But need to choose a value so that after a reasonable time, the balls stop. The steps I need to handle each frame: 1. Update all balls' positions based on velocity, applying friction to velocity each step. 2. Check for collisions between each pair of balls. 3. Check for collisions with the table edges (cushions) and reflect the velocity accordingly. 4. Check if all balls have stopped (velocity below a threshold). When true, reset their positions and velocities for a new break. Collision handling between balls: When two balls overlap, we need to calculate the collision response. Since it's elastic, the velocities change direction based on the normal vector. The formula for elastic collisions between two moving objects can be used here. To simplify, since all masses are the same (assuming all balls have same mass), the velocity exchange is straightforward. The relative velocity in the direction of the normal (the line connecting centers) is reversed between the two balls. Wait, perhaps the algorithm is: For each pair of balls (a, b): - Calculate distance between centers: d = sqrt((x2 - x1)^2 + (y2 - y1)^2) - If distance < 2 * radius (since radius is same for all balls), they are colliding. - Compute the vector pointing from a to b: dx = bx - ax; dy = by - ay - The normal direction is (dx, dy). Need to normalize this vector. - Compute the velocity difference along the normal: v_rel = (va.x - vb.x)*dx + (va.y - vb.y)*dy / (distance) - The impulse should be applied so that the velocities along the normal are reversed. Since masses are equal, the impulse can be calculated as (2 * v_rel) / (distance) * (dx, dy) ? Alternatively, a simpler way when masses are equal is to swap the velocities along the normal. But I might need to look up the exact formula for elastic collisions between equal mass objects. The formula for equal masses is that the velocities along the line of centers are swapped. Wait, actually, the relative velocity towards each other is reversed. The normal component of the velocity between the two balls is reversed. So, perhaps: Let’s have the normal vector (dx, dy) normalized. The velocity components along the normal for each ball can be computed, then swapped or adjusted. Alternatively, here's a method: Let’s denote: vec_n = (dx, dy) normalized. The velocity of a in the direction of vec_n is va.dot(vec_n) Similarly for vb. Then, the total along that direction is va_n + vb_n = (va.dot(vec_n) + vb.dot(vec_n)) After collision, to have elastic (conserving momentum and kinetic energy), the velocities in the normal direction would be swapped. Wait, no, actually for equal masses, the velocities along the normal direction are exchanged? Or perhaps the relative velocity is reversed. Alternatively: After collision, the velocities along the normal direction: va_new = va - (va - vb) * (vec_n • (va - vb)) * vec_n * (2 / (mass_a + mass_b)) ) Since masses are equal (mass_a = mass_b = m), this simplifies to: va_new = va - (va - vb) * ( (va - vb) • vec_n ) * vec_n * (2/(2m)) ) But since m cancels, assuming unit mass, this becomes: va_new = vb + ( (vb - va) • vec_n ) * vec_n Wait, maybe it's better to use the standard formula for elastic collisions between two objects: Let’s define the normal vector as the unit vector pointing from a to b (vec_n). The velocities of a and b are v_a and v_b. The change in velocity for a is delta_v_a = ( (v_b - v_a) • vec_n ) * vec_n * (2 * mass_b) / (mass_a + mass_b) Similarly for b, delta_v_b = same as delta_v_a but with mass_a and mass_b swapped. Since masses are equal, mass_a = mass_b = m: delta_v_a = ( (v_b - v_a) • vec_n ) * vec_n * (2m)/(2m) ) = ( (v_b - v_a) • vec_n ) * vec_n So delta_v_a is added to v_a, and delta_v_b is added to v_b. Wait, actually the formula is: After collision, the velocities are: v_a' = v_a - ( ( (v_a - v_b) • vec_n ) * (vec_n) ) * (2 * mass_b) / (mass_a + mass_b) ) Similarly for v_b: v_b' = v_b + ( ( (v_a - v_b) • vec_n ) * (vec_n) ) * (2 * mass_a) / (mass_a + mass_b) ) So when masses are equal, this becomes: v_a' = v_a - ( (v_a - v_b) • vec_n ) * vec_n * (2m)/(2m) = v_a - (v_a - v_b) • vec_n * vec_n Similarly, v_b' = v_b + (v_a - v_b) • vec_n * vec_n Alternatively, maybe it's easier to code this as: Let’s compute the normal vector between the two centers (dx, dy). Then compute the vector from a to b: (dx,
Show code
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<title>Billiard Break</title>
<style>
html,body{margin:0;height:100%;background:#111;overflow:hidden}
canvas{display:block;width:100vw;height:100vh;background:#153d2a}
</style>
</head>
<body>
<canvas id="c"></canvas>
<script>
const c=document.getElementById("c"),x=c.getContext("2d");
let W,H,S,balls=[],last=0,rest=0;
const R=10,M=.995,C=.985,BC=.992,cols=["#fff","#f4d13d","#2864d8","#d93434","#7b3fa0","#e87d22","#219653","#7a2c24","#111","#f4d13d","#2864d8"];
function fit(){W=c.width=innerWidth*devicePixelRatio;H=c.height=innerHeight*devicePixelRatio;S=Math.min(W/900,H/520)}
addEventListener("resize",()=>{fit();rack()});fit();
function ball(n,x,y,vx=0,vy=0){balls.push({n,x,y,vx,vy,r:12*S,m:1})}
function rack(){
balls=[];let cx=W*.63,cy=H*.5,g=21*S,k=1;
for(let row=0;row<4;row++)for(let i=0;i<=row;i++)ball(k++,cx+row*g,cy+(i-row/2)*g*1.15);
ball(0,W*.23,cy,700*S,0);rest=0;
}
rack();
function step(dt){
let l=38*S,t=38*S,r=W-l,b=H-t;
for(let p of balls){
p.x+=p.vx*dt;p.y+=p.vy*dt;
p.vx*=Math.pow(M,dt*60);p.vy*=Math.pow(M,dt*60);
let s=Math.hypot(p.vx,p.vy);if(s<5*S)p.vx=p.vy=0;
if(p.x-p.r<l){p.x=l+p.r;p.vx=Math.abs(p.vx)*C}
if(p.x+p.r>r){p.x=r-p.r;p.vx=-Math.abs(p.vx)*C}
if(p.y-p.r<t){p.y=t+p.r;p.vy=Math.abs(p.vy)*C}
if(p.y+p.r>b){p.y=b-p.r;p.vy=-Math.abs(p.vy)*C}
}
for(let i=0;i<balls.length;i++)for(let j=i+1;j<balls.length;j++){
let a=balls[i],b=balls[j],dx=b.x-a.x,dy=b.y-a.y,d=Math.hypot(dx,dy)||1,min=a.r+b.r;
if(d<min){
let nx=dx/d,ny=dy/d,over=(min-d)/2;
a.x-=nx*over;a.y-=ny*over;b.x+=nx*over;b.y+=ny*over;
let tx=-ny,ty=nx,va=a.vx*nx+a.vy*ny,vb=b.vx*nx+b.vy*ny,ta=a.vx*tx+a.vy*ty,tb=b.vx*tx+b.vy*ty;
a.vx=vb*BC*nx+ta*tx;a.vy=vb*BC*ny+ta*ty;b.vx=va*BC*nx+tb*tx;b.vy=va*BC*ny+tb*ty;
}
}
if(balls.every(p=>!p.vx&&!p.vy)){rest+=dt;if(rest>1.4)rack()}else rest=0;
}
function draw(){
x.clearRect(0,0,W,H);
x.fillStyle="#0d2f20";x.fillRect(28*S,28*S,W-56*S,H-56*S);
x.lineWidth=20*S;x.strokeStyle="#5b341e";x.strokeRect(28*S,28*S,W-56*S,H-56*S);
x.lineWidth=8*S;x.strokeStyle="#c49b59";x.strokeRect(39*S,39*S,W-78*S,H-78*S);
for(let px of [44*S,W/2,W-44*S])for(let py of [44*S,H-44*S]){x.beginPath();x.fillStyle="#080808";x.arc(px,py,17*S,0,7);x.fill()}
for(let p of balls){
let grd=x.createRadialGradient(p.x-p.r*.35,p.y-p.r*.45,p.r*.1,p.x,p.y,p.r);
grd.addColorStop(0,"#fff");grd.addColorStop(.25,cols[p.n]);grd.addColorStop(1,"#111");
x.beginPath();x.fillStyle=grd;x.arc(p.x,p.y,p.r,0,7);x.fill();
if(p.n){x.beginPath();x.fillStyle="#fff";x.arc(p.x,p.y,p.r*.46,0,7);x.fill();x.fillStyle="#111";x.font=`${12*S}px Arial`;x.textAlign="center";x.textBaseline="middle";x.fillText(p.n,p.x,p.y)}
}
}
function loop(ts){let dt=Math.min((ts-last)/1000,.033)||.016;last=ts;step(dt);draw();requestAnimationFrame(loop)}
requestAnimationFrame(loop);
</script>
</body>
</html>Show code
<!DOCTYPE html>
<html><head><meta charset="utf-8"><title>Billiard Break</title>
<style>html,body{margin:0;background:#0b2f0b;overflow:hidden}canvas{display:block;margin:20px auto;background:#0a5a24}</style>
</head><body>
<canvas id="c" width="800" height="450"></canvas>
<script>
const cv=document.getElementById('c'),ctx=cv.getContext('2d');
const W=800,H=450,R=11,M=28,FR=0.992,STOP=0.03;
const colors=['#fff','#e6c200','#1e5ce6','#e63b1e','#5a2ea6','#e67a1e','#1e8f4a','#b5162e','#2c2c2c','#e6c200','#3a7de6'];
let balls=[],resetting=false;
function rack(){
balls=[{x:M+140,y:H/2,vx:0,vy:0,num:0}];
const apexX=W-M-190,dx=R*2,dy=R*2.2;
let n=1;
for(let row=0;row<4;row++){
for(let j=0;j<=row;j++){
balls.push({x:apexX+row*dx,y:H/2+(j-row/2)*dy,vx:0,vy:0,num:n++});
}
}
const ang=(Math.random()-0.5)*0.05;
balls[0].vx=Math.cos(ang)*10;
balls[0].vy=Math.sin(ang)*10;
}
function step(){
for(const b of balls){
b.x+=b.vx;b.y+=b.vy;
const sp=Math.hypot(b.vx,b.vy);
if(sp>0){
const f=Math.max(0,sp-0.025)/sp*FR;
b.vx*=f;b.vy*=f;
}
if(b.x-R<M){b.x=M+R;b.vx=-b.vx*0.9;}
if(b.x+R>W-M){b.x=W-M-R;b.vx=-b.vx*0.9;}
if(b.y-R<M){b.y=M+R;b.vy=-b.vy*0.9;}
if(b.y+R>H-M){b.y=H-M-R;b.vy=-b.vy*0.9;}
}
for(let i=0;i<balls.length;i++){
for(let j=i+1;j<balls.length;j++){
const a=balls[i],b=balls[j];
const dx=b.x-a.x,dy=b.y-a.y,d=Math.hypot(dx,dy);
if(d<2*R&&d>0){
const nx=dx/d,ny=dy/d,ov=2*R-d;
a.x-=nx*ov/2;a.y-=ny*ov/2;
b.x+=nx*ov/2;b.y+=ny*ov/2;
const rvx=b.vx-a.vx,rvy=b.vy-a.vy,rel=rvx*nx+rvy*ny;
if(rel<0){
a.vx+=rel*nx;a.vy+=rel*ny;
b.vx-=rel*nx;b.vy-=rel*ny;
}
}
}
}
}
function allStopped(){
return balls.every(b=>Math.hypot(b.vx,b.vy)<STOP);
}
function draw(){
ctx.clearRect(0,0,W,H);
ctx.fillStyle='#0a5a24';ctx.fillRect(M,M,W-2*M,H-2*M);
ctx.strokeStyle='#3a2412';ctx.lineWidth=M;
ctx.strokeRect(M/2,M/2,W-M,H-M);
for(const b of balls){
ctx.beginPath();ctx.arc(b.x,b.y,R,0,7);
ctx.fillStyle=colors[b.num];ctx.fill();
ctx.strokeStyle='#000';ctx.lineWidth=1;ctx.stroke();
if(b.num>0){
ctx.fillStyle=b.num===9?'#000':'#fff';ctx.font='9px sans-serif';ctx.textAlign='center';
ctx.fillText(b.num,b.x,b.y+3);
}
}
}
function loop(){
if(!resetting){
if(allStopped()){
resetting=true;
setTimeout(()=>{rack();resetting=false;},1000);
}else{
step();
}
}
draw();
requestAnimationFrame(loop);
}
rack();
loop();
</script>
</body></html>Show code
<!DOCTYPE html>
<html><head><meta charset="utf-8"><title>Billiard Break</title>
<style>html,body{margin:0;background:#14100c;overflow:hidden}canvas{display:block;margin:0 auto}</style></head>
<body><canvas id="c" width="940" height="520"></canvas>
<script>
const cv=document.getElementById('c'),ctx=cv.getContext('2d');
const W=940,H=520,M=36,R=12;
const COLS=["#ffd400","#1c54d8","#e02020","#7a1fa2","#ff7f1f","#1a7f3c","#8b1a1a","#222","#c9a800","#3a6fe0"];
let balls=[],state="play",wait=0;
function rack(){
balls=[];
const rx=650,ry=H/2,d=R*2+0.6;
let n=0;
for(let row=0;row<4;row++)
for(let k=0;k<=row;k++){
if(n>=10)break;
balls.push({x:rx+row*d*0.87+(Math.random()-0.5)*0.4,y:ry+(k-row/2)*d,vx:0,vy:0,n:++n});
}
const ang=(Math.random()-0.5)*0.05;
balls.push({x:210,y:ry+(Math.random()-0.5)*30,vx:15*Math.cos(ang),vy:15*Math.sin(ang),n:0});
state="play";
}
function physics(){
for(let s=0;s<3;s++){
for(const b of balls){b.x+=b.vx/3;b.y+=b.vy/3;}
for(let i=0;i<balls.length;i++)for(let j=i+1;j<balls.length;j++){
const a=balls[i],b=balls[j];
let dx=b.x-a.x,dy=b.y-a.y,d=Math.hypot(dx,dy);
if(d<2*R&&d>1e-6){
dx/=d;dy/=d;
const ov=(2*R-d)/2;
a.x-=dx*ov;a.y-=dy*ov;b.x+=dx*ov;b.y+=dy*ov;
const p=(a.vx-b.vx)*dx+(a.vy-b.vy)*dy;
if(p>0){a.vx-=p*dx;a.vy-=p*dy;b.vx+=p*dx;b.vy+=p*dy;}
}
}
for(const b of balls){
if(b.x<M+R){b.x=M+R;b.vx=-b.vx*0.92;}
if(b.x>W-M-R){b.x=W-M-R;b.vx=-b.vx*0.92;}
if(b.y<M+R){b.y=M+R;b.vy=-b.vy*0.92;}
if(b.y>H-M-R){b.y=H-M-R;b.vy=-b.vy*0.92;}
}
}
let moving=false;
for(const b of balls){
b.vx*=0.988;b.vy*=0.988;
const sp=Math.hypot(b.vx,b.vy);
if(sp<0.04){b.vx=0;b.vy=0;}else moving=true;
}
if(!moving&&state==="play"){state="rest";wait=90;}
}
function draw(){
ctx.fillStyle="#5a3c1e";ctx.fillRect(0,0,W,H);
ctx.fillStyle="#0f6e3a";ctx.fillRect(M,M,W-2*M,H-2*M);
for(const b of balls){
ctx.beginPath();ctx.arc(b.x,b.y,R,0,7);
ctx.fillStyle=b.n===0?"#f4f0e6":COLS[b.n-1];ctx.fill();
ctx.strokeStyle="rgba(0,0,0,0.35)";ctx.stroke();
if(b.n>0){
ctx.fillStyle="#fff";ctx.beginPath();ctx.arc(b.x,b.y,5.5,0,7);ctx.fill();
ctx.fillStyle="#111";ctx.font="bold 8px sans-serif";ctx.textAlign="center";
ctx.fillText(b.n,b.x,b.y+3);
}
}
ctx.fillStyle="#e8e0d0";ctx.font="14px monospace";ctx.textAlign="left";
ctx.fillText(state==="play"?"break in progress...":"all balls at rest — re-racking",M,24);
}
function loop(){
if(state==="rest"){if(--wait<=0)rack();}
else physics();
draw();
requestAnimationFrame(loop);
}
rack();
requestAnimationFrame(loop);
</script>
</body></html>Show code
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Billiard Simulation</title>
<style>
body { margin: 0; padding: 10px; font-family: monospace; background: #000; color: #0f0; }
canvas { display: block; margin: 10px auto; background: #1a3a1a; border: 1px solid #0f0; }
</style>
</head>
<body>
<canvas id="c" width="800" height="600"></canvas>
<script>
const canvas = document.getElementById('c');
const ctx = canvas.getContext('2d');
const w = canvas.width, h = canvas.height;
const r = 12;
const friction = 0.98;
const cushionRestitution = 0.8;
const ballRestitution = 0.9;
let balls = [];
let time = 0;
function breakRack() {
balls = [{ x: 150, y: h / 2, vx: 20, vy: 0 }];
const pyramid = 0.5;
for (let row = 0; row < 4; row++) {
for (let col = 0; col <= row; col++) {
balls.push({
x: w - 200 + row * 25,
y: h / 2 - row * 12 + col * 25,
vx: 0, vy: 0
});
}
}
}
breakRack();
function simulate() {
let allStopped = true;
for (let b of balls) {
b.x += b.vx;
b.y += b.vy;
b.vx *= friction;
b.vy *= friction;
if (Math.abs(b.vx) > 0.1 || Math.abs(b.vy) > 0.1) allStopped = false;
if (b.x - r < 10) { b.x = r + 10; b.vx *= -cushionRestitution; }
if (b.x + r > w - 10) { b.x = w - 10 - r; b.vx *= -cushionRestitution; }
if (b.y - r < 10) { b.y = r + 10; b.vy *= -cushionRestitution; }
if (b.y + r > h - 10) { b.y = h - 10 - r; b.vy *= -cushionRestitution; }
}
for (let i = 0; i < balls.length; i++) {
for (let j = i + 1; j < balls.length; j++) {
const dx = balls[j].x - balls[i].x;
const dy = balls[j].y - balls[i].y;
const d = Math.sqrt(dx * dx + dy * dy);
if (d < 2 * r) {
const nx = dx / d, ny = dy / d;
const dvx = balls[j].vx - balls[i].vx;
const dvy = balls[j].vy - balls[i].vy;
const dot = dvx * nx + dvy * ny;
if (dot > 0) continue;
const imp = dot / 2;
balls[i].vx += imp * nx * ballRestitution;
balls[i].vy += imp * ny * ballRestitution;
balls[j].vx -= imp * nx * ballRestitution;
balls[j].vy -= imp * ny * ballRestitution;
const overlap = 2 * r - d;
balls[i].x -= overlap * nx / 2;
balls[i].y -= overlap * ny / 2;
balls[j].x += overlap * nx / 2;
balls[j].y += overlap * ny / 2;
}
}
}
if (allStopped && time > 300) {
breakRack();
time = 0;
}
ctx.fillStyle = '#1a3a1a';
ctx.fillRect(0, 0, w, h);
ctx.strokeStyle = '#0f0';
ctx.lineWidth = 2;
ctx.strokeRect(10, 10, w - 20, h - 20);
for (let i = 0; i < balls.length; i++) {
ctx.fillStyle = i === 0 ? '#fff' : ['#ff0', '#f0f', '#0ff', '#f00', '#0f0', '#00f'][i % 6];
ctx.beginPath();
ctx.arc(balls[i].x, balls[i].y, r, 0, 2 * Math.PI);
ctx.fill();
}
time++;
requestAnimationFrame(simulate);
}
simulate();
</script>
</body>
</html>Results at a glance— every model in this round with score, response time and status.
Who wrote what
| Model | Blind score | Latency | Status |
|---|---|---|---|
| qwen3:8b | — | — | harness |
| gemma4:26b | — | — | harness |
| qwen3:14b | — | 45.9 s | OK |
| mistral-small:24b | — | 181.2 s | OK |
| deepseek-r1:14b | — | 81.8 s | OK |
| qwen3-coder:30b | — | 26.2 s | OK |
| qwen3.5:9b | — | 49.5 s | OK |
| llama3.1:8b | — | 125.6 s | OK |
| deepseek-r1:32b | — | 127.3 s | OK |
| command-r:35b | — | 140.9 s | OK |
| qwq:32b | — | 127.3 s | OK |
Frontier reference — outside the blind field
These models answered the identical prompt outside the blind field. They are deliberately not scored and not part of the blind comparison — they are there so you can see where a frontier model lands on the same task.
Show code
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>Billiard Break</title>
<style>
html,body{margin:0;height:100%;background:#12100e;display:flex;align-items:center;justify-content:center;font-family:system-ui,sans-serif}
canvas{max-width:96vw;max-height:92vh;border-radius:14px;box-shadow:0 24px 60px #000a}
</style>
</head>
<body>
<canvas id="c" width="900" height="480"></canvas>
<script>
const cv=document.getElementById('c'),g=cv.getContext('2d');
const W=900,H=480,R=11,PAD=42,L=PAD,T=PAD,Rt=W-PAD,B=H-PAD;
const COL={1:'#e8c22a',2:'#1f5fd0',3:'#d02b25',4:'#7a2bb5',5:'#e07a1c',6:'#1d8a4a',7:'#8a2a22',8:'#141414',9:'#e8c22a',10:'#1f5fd0'};
let balls=[],rest=0;
function rack(){
balls=[];let n=1;const ax=Rt-190,ay=H/2,d=R*2+0.6;
for(let row=0;row<4;row++)for(let k=0;k<=row;k++)
balls.push({x:ax+row*d*0.866,y:ay+(k-row/2)*d,vx:0,vy:0,n:n,c:COL[n++]});
balls.push({x:L+150,y:H/2+(Math.random()-.5)*10,vx:12+Math.random()*5,vy:(Math.random()-.5)*1.4,n:0,c:'#f6f3ea'});
rest=0;
}
function step(dt){
for(const b of balls){b.x+=b.vx*dt;b.y+=b.vy*dt;}
for(const b of balls){
if(b.x-R<L){b.x=L+(L-(b.x-R))-R;b.vx=-b.vx*0.92;}
if(b.x+R>Rt){b.x=Rt-((b.x+R)-Rt)-R;b.vx=-b.vx*0.92;}
if(b.y-R<T){b.y=T+(T-(b.y-R))-R;b.vy=-b.vy*0.92;}
if(b.y+R>B){b.y=B-((b.y+R)-B)-R;b.vy=-b.vy*0.92;}
}
for(let i=0;i<balls.length;i++)for(let j=i+1;j<balls.length;j++){
const a=balls[i],b=balls[j];let dx=b.x-a.x,dy=b.y-a.y,d=Math.hypot(dx,dy);
if(d===0){dx=0.01;d=0.01;}
if(d<R*2){
const nx=dx/d,ny=dy/d,ov=(R*2-d)/2;
a.x-=nx*ov;a.y-=ny*ov;b.x+=nx*ov;b.y+=ny*ov;
const p=(a.vx-b.vx)*nx+(a.vy-b.vy)*ny;
if(p>0){const e=p*0.98;a.vx-=e*nx;a.vy-=e*ny;b.vx+=e*nx;b.vy+=e*ny;}
}
}
}
function friction(){
let moving=false;
for(const b of balls){
const s=Math.hypot(b.vx,b.vy);
if(s<0.06){b.vx=0;b.vy=0;continue;}
const f=Math.max(0,1-0.012-0.02/s);
b.vx*=f;b.vy*=f;moving=true;
}
return moving;
}
function felt(){
g.fillStyle='#3a2113';g.fillRect(0,0,W,H);
g.fillStyle='#4d2c19';g.fillRect(8,8,W-16,H-16);
const grd=g.createRadialGradient(W/2,H/2,40,W/2,H/2,W*0.62);
grd.addColorStop(0,'#1f8a55');grd.addColorStop(1,'#0e5231');
g.fillStyle=grd;g.fillRect(L-14,T-14,Rt-L+28,B-T+28);
g.strokeStyle='#0a3a24';g.lineWidth=3;g.strokeRect(L-14,T-14,Rt-L+28,B-T+28);
g.strokeStyle='rgba(255,255,255,.12)';g.lineWidth=1;
g.beginPath();g.moveTo(L+150,T-14);g.lineTo(L+150,B+14);g.stroke();
}
function draw(){
felt();
for(const b of balls){
g.beginPath();g.ellipse(b.x+4,b.y+7,R*1.05,R*0.7,0,0,7);g.fillStyle='rgba(0,0,0,.32)';g.fill();
}
for(const b of balls){
g.beginPath();g.arc(b.x,b.y,R,0,7);g.fillStyle=b.c;g.fill();
if(b.n>8){g.save();g.beginPath();g.arc(b.x,b.y,R,0,7);g.clip();
g.fillStyle='#f6f3ea';g.fillRect(b.x-R,b.y-R,R*2,R*0.62);g.fillRect(b.x-R,b.y+R*0.38,R*2,R*0.62);g.restore();}
const sh=g.createRadialGradient(b.x-R*0.4,b.y-R*0.45,1,b.x,b.y,R*1.3);
sh.addColorStop(0,'rgba(255,255,255,.55)');sh.addColorStop(0.5,'rgba(255,255,255,0)');sh.addColorStop(1,'rgba(0,0,0,.4)');
g.fillStyle=sh;g.beginPath();g.arc(b.x,b.y,R,0,7);g.fill();
if(b.n>0){
g.beginPath();g.arc(b.x,b.y,R*0.52,0,7);g.fillStyle='#f6f3ea';g.fill();
g.fillStyle='#111';g.font='bold 10px system-ui';g.textAlign='center';g.textBaseline='middle';
g.fillText(b.n,b.x,b.y+0.5);
}
g.strokeStyle='rgba(0,0,0,.35)';g.lineWidth=1;g.beginPath();g.arc(b.x,b.y,R-0.5,0,7);g.stroke();
}
}
function loop(){
for(let s=0;s<5;s++)step(0.2);
const moving=friction();
if(!moving){if(++rest>70)rack();}else rest=0;
draw();
requestAnimationFrame(loop);
}
rack();loop();
</script>
</body>
</html>