Overview › Physics Lab › R59
Physics Lab · blind round

R59 · Billiard break with spin and friction

blind round 9/11 local models answered

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

Local models (Brain cluster) — identical prompt, shuffled order (the sequence is not a ranking)
qwen3:8bharness
No answer reached us (ERR) — and the reason lies with our setup, not with the model: reasoning consumed the entire token budget and the answer stayed empty; the gateway rejects budgets above 3000 tokens Counted as a did-not-finish, but it is not evidence about this model.
gemma4:26bharness
No answer reached us (THIN) — and the reason lies with our setup, not with the model: reasoning consumed the entire token budget (3 min 28 s, zero characters of answer); larger budgets are refused by the gateway Counted as a did-not-finish, but it is not evidence about this model.
qwen3:14bSTATLIMFORMAT
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>
```
mistral-small:24bSTATFORMAT
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>
```
deepseek-r1:14bERRBLANKFORMAT
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>
```
qwen3-coder:30bOKLIMFORMAT
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>
```
qwen3.5:9bSTATLIMFORMAT
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>
```
llama3.1:8bSTATLIMFORMAT
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>
```
deepseek-r1:32bERRBLANKFORMAT
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>
```
command-r:35bOKLIMFORMAT
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>
```
qwq:32bSTAT
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,
Outside the Brain cluster — same task, same character budget, collected through their own tooling (response times not comparable)
GPT-5.5 (Codex)frontier referenceOK
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>
Claude Sonnet 5frontier referenceOK
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>
Claude Fable 5frontier referenceOK
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>
Claude Haiku 4.5frontier referenceOK
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

ModelBlind scoreLatencyStatus
qwen3:8b ——harness
gemma4:26b ——harness
qwen3:14b —45.9 sOK
mistral-small:24b —181.2 sOK
deepseek-r1:14b —81.8 sOK
qwen3-coder:30b —26.2 sOK
qwen3.5:9b —49.5 sOK
llama3.1:8b —125.6 sOK
deepseek-r1:32b —127.3 sOK
command-r:35b —140.9 sOK
qwq:32b —127.3 sOK

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.

Collected differently from the blind field: the local models run through a plain API harness, the frontier references through a context-free agent, one shot, no retries. Response times are therefore not comparable and are not shown. On two English tasks the reference answer came back in German — an artefact of that harness, flagged on the affected rounds.
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>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>