Overview › Code › R76
Code · blind round

R76 · Falling sand as free particles

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 simulates falling sand as 1500 FREE-MOVING PARTICLES with continuous positions on a <canvas>. Each grain has a floating-point position and velocity, falls under gravity, loses energy on impact and comes to rest on the growing pile, so a realistic angle of repose forms. No grid, no cells — continuous coordinates only. Show the number of grains at rest and the pile height. Smooth requestAnimationFrame animation, NO user input, starts immediately and keeps running (restart with a fresh random instance when finished). 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)
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.
llama3.1:8bBLANKFORMAT
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>Free-falling Sand Simulation</title>
<style>
body {
  margin: 0;
  background-color: #f2f2f2;
}
canvas {
  border: 1px solid black;
}
</style>
</head>
<body>
<canvas id="c" width="800" height="600"></canvas>
<script>
function rand(x) { return Math.random() * x; }
var c = document.getElementById('c');
var ctx = c.getContext('2d');
ctx.fillStyle = 'black';
ctx.fillRect(0, 0, 800, 600);
var numParticles = 1500;
var particles = [];
for (var i = 0; i < numParticles; i++) {
  particles.push({
    x: rand(800),
    y: rand(500),
    vx: Math.random() * 2 - 1,
    vy: Math.random() * 2.5
  });
}
var time = 0;
function draw() {
  ctx.clearRect(0, 0, 800, 600);
  for (var i = 0; i < numParticles; i++) {
    var p = particles[i];
    if (p.vy > -0.1) { // particle still falling
      p.x += p.vx;
      p.y += p.vy;
      p.vy -= 0.2; // gravity
      ctx.fillStyle = 'rgba(128,0,0,' + Math.max(p.vy * 10, 0.05) + ')';
      ctx.fillRect(Math.floor(p.x), Math.floor(p.y - 5), 1, 6);
    }
  }
  var restingParticles = numParticles;
  for (var i = 0; i < numParticles; i++) {
    if (particles[i].vy < -0.1) { // particle at rest
      restingParticles--;
    }
  }
  document.getElementById('count').textContent = 'At rest: ' + restingParticles;
  var pileHeight = 0;
  for (var i = 0; i < numParticles; i++) {
    if (particles[i].vy < -0.1) { // particle at rest
      pileHeight = Math.max(pileHeight, particles[i].y);
    }
  }
  document.getElementById('height').textContent = 'Pile height: ' + Math.floor(pileHeight);
}
setInterval(draw, 16); // 60 FPS
</script>
<div id="count"></div>
<div id="height"></div>
</body>
</html>
```
qwen3.5:9bERRBLANKLIMFORMAT
Show code
```html
<!DOCTYPE html>
<html>
<body><canvas id="sandCanvas"></canvas><script>
const canvas = document.getElementById('sandCanvas');
const ctx = canvas.getContext('2d');
let W, H;

// Simulation Constants
const N_PARTICLES = 1500;
const GRAVITY = 0.3;
const DAMPING = 0.85; // Energy loss on impact
const PILE_LIMIT = 5; // Collision check radius (squared for performance)

let particles = [];
let isRunning = true;
let settledCount = 0;
let maxPileHeight = 0;

// Particle Structure: [x, y, vx, vy, active]
class Particle {
    constructor(x, y) {
        this.p = [x, y]; // Position
        this.v = [Math.random() * 2 - 1, Math.random() * -5]; // Velocity (some initial upward/lateral kick for variety)
        this.active = true;
    }

    update(dt) {
        if (!this.active) return;

        // Apply Gravity
        this.v[1] += GRAVITY * dt;

        // Update Position
        this.p[0] += this.v[0] * dt;
        this.p[1] += this.v[1] * dt;

        if (Math.abs(this.v[1]) > 0) {
            checkCollision();
        }
    }

    draw() {
        ctx.fillStyle = 'rgba(255, 165, 0', this.active ? ', 0.8)' : ', 1)';
        ctx.fillRect(this.p[0], this.p[1], 2, 2);
    }

    // Collision detection and settling logic (Simplified N-body interaction)
    checkCollision() {
        const x = this.p[0];
        const y = this.p[1];

        if (y > H - 5) { // Ground collision stop
            this.active = false;
            return true;
        }

        // Check against all other particles for collision
        for (let i = 0; i < particles.length; i++) {
            const p2 = particles[i];
            if (p2 === this || !p2.active) continue;

            const dx = x - p2.p[0];
            const dy = y - p2.p[1];
            const distSq = dx * dx + dy * dy;

            if (distSq < PILE_LIMIT && Math.abs(this.v[1]) > 0) {
                // Collision detected: Stop movement and settle
                this.active = false;
                p2.v[1] += GRAVITY * 0.1; // Give slight bounce to others
                this.v[1] = 0;
                this.v[0] *= DAMPING;

                // Adjust position slightly to rest on the surface
                const overlapDepth = PILE_LIMIT - Math.sqrt(distSq);
                this.p[1] += overlapDepth * 0.5; 
            }
        }
        return false;
    }
}

function initialize() {
    W = canvas.width = window.innerWidth * 0.95;
    H = canvas.height = window.innerHeight * 0.75;
    particles = [];
    settledCount = 0;
    maxPileHeight = 0;
    isRunning = true;

    for (let i = 0; i < N_PARTICLES; i++) {
        const startX = Math.random() * W * 0.8 + W * 0.1;
        const startY = H - 50 - Math.random() * 20; // Drop from above the canvas view
        particles[i] = new Particle(startX, startY);
    }
}

function updatePhysics(dt) {
    let settledThisFrame = 0;
    for (const p of particles) {
        if (p.active) {
            p.update(dt);
            // Check if the particle has fallen sufficiently far or stopped due to collision
            if (!p.checkCollision()) {
                // Simple check for settling: If vertical velocity is near zero AND it's not on the ground
                if (Math.abs(p.v[1]) < 0.5 && p.p[1] < H - 2) {
                    p.active = false;
                    settledThisFrame++;
                }
            }
        } else if (!p.active) {
             // Keep settled particles in place (they are part of the pile now)
        }
    }

    if (settledCount + settledThisFrame >= N_PARTICLES - 10) { // Check if almost all have settled
        isRunning = false;
        setTimeout(initialize, 2000); // Restart after a delay
    } else {
        settledCount += settledThisFrame;
    }

    // Update pile height metric
    maxPileHeight = Math.max(maxPileHeight, ...particles.map(p => p.p[1]));
}

function draw() {
    ctx.clearRect(0, 0, W, H);

    for (const p of particles) {
        if (p.active) {
            p.draw();
        } else {
            // Draw settled grains darker
            ctx.fillStyle = 'rgba(184, 134, 11', ', 1)';
            ctx.fillRect(p.p[0], p.p[1], 2, 2);
        }
    }

    // UI Overlay (Lines are kept simple for line count)
    ctx.fillStyle = 'white';
    ctx.font = '14px Arial';
    let yOffset = 20;
    ctx.fillText(`Grains At Rest: ${Math.floor(settledCount)} / ${N_PARTICLES}`, 10, yOffset);
    yOffset += 20;
    ctx.fillText(`Pile Height (Y): ${Math.round(H - maxPileHeight)}`, 10, yOffset);
}

function animate() {
    if (isRunning) {
        const dt = 1; // Time delta approximation
        updatePhysics(dt);
    }
    draw();
    requestAnimationFrame(animate);
}

window.addEventListener('resize', initialize);
initialize();
animate();
</script></body></html>
```
qwen3-coder:30bERRBLANKLIMFORMAT
Show code
```html
<!DOCTYPE html>
<html>
<head>
    <title>Continuous Sand Simulation</title>
    <style>body { margin: 0; background: #111; }</style>
</head>
<body>
<canvas id="sandCanvas"></canvas>
<script>
const canvas = document.getElementById('sandCanvas');
const ctx = canvas.getContext('2d');
let W, H;

function init() {
    W = window.innerWidth * 0.95;
    H = window.innerHeight * 0.8;
    canvas.width = W;
    canvas.height = H;
}
window.addEventListener('resize', init);
init();

const PARTICLE_COUNT = 1500;
let particles = [];
const GRAVITY = 9.8 / 60; // Adjusted for frame rate
const DAMPING = 0.85;     // Energy loss on impact
const MAX_VELOCITY = 20;

class Particle {
    constructor() {
        this.reset();
    }
    reset() {
        this.x = Math.random() * W * 0.3 + W * 0.1; // Start in a confined area
        this.y = -50;
        this.vx = (Math.random() - 0.5) * 5;
        this.vy = 0;
        this.r = 2;
    }

    update(pileHeight, pileMap) {
        if (this.isDead) return;

        // Apply gravity
        this.vy += GRAVITY;
        this.vx *= 0.98; // Air resistance
        this.x += this.vx;
        this.y += this.vy;

        let collisionY = pileHeight[Math.floor(this.x) % W];
        let groundCollision = false;

        // Collision detection with the "pile" structure or floor
        if (this.y >= collisionY - this.r * 2 && Math.abs(this.x - W / 2) < W/2) {
            this.vy *= -DAMPING; // Bounce and lose energy
            this.vx *= DAMPING;

            // If vertical velocity is small enough, settle down
            if (Math.abs(this.vy) < 0.5 && Math.abs(this.vx) < 0.1) {
                this.isDead = true; // Particle settles and becomes part of the pile
                return;
            }

            // Adjust position to sit exactly on the support plane
            this.y = collisionY[Math.floor(this.x)] - this.r * 2;
        } else if (this.y >= H - this.r) {
             this.isDead = true; // Hit floor
        }

        // Clamp position to canvas bounds
        this.x = Math.max(0, Math.min(W, this.x));
        if (this.y < 0) this.y = 0;
    }

    draw() {
        ctx.fillStyle = `rgba(139, 69, 19, ${Math.random() * 0.5 + 0.5})`;
        ctx.beginPath();
        ctx.arc(this.x, this.y, this.r, 0, Math.PI * 2);
        ctx.fill();
    }

    isDead = false; // Flag for settled particles
}

// Global map simulating the pile structure (stores Y coordinate at X)
let pileHeight = Array(Math.floor(W / 1)); 
let particleCountAtRest = 0;
let totalParticles = PARTICLE_COUNT;

function updateSimulation() {
    particles.forEach(p => p.update(pileHeight, null));

    // --- Pile Update Logic (Simplified support plane calculation) ---
    // On every frame, the pile height map should be updated by checking all settled particles
    const newPileHeight = Array(Math.floor(W / 1)).fill(H); // Initialize to floor level
    for (let i = 0; i < totalParticles; i++) {
        if (particles[i].isDead) {
            int x_idx = Math.min(Math.max(0, Math.floor(particles[i].x)), W - 1);
            newPileHeight[x_idx] = Math.min(newPileHeight[x_idx], particles[i].y + particles[i].r * 2);
        }
    }
    pileHeight = newPileHeight;

    // --- Particle Management & Reset ---
    let currentRestCount = 0;
    particles.forEach((p, i) => {
        if (p.isDead && !p.recycled) {
            currentRestCount++;
            p.recycled = true; // Mark as counted rest particle
        } else if (Math.random() < 0.01 || p.y > H + 50) { // Occasionally respawn or move out of bounds
             // If it settled, don't reset it unless needed for mass control
        }
    });

    particleCountAtRest = Math.min(totalParticles, currentRestCount);


    // --- Drawing & Looping ---
    ctx.clearRect(0, 0, W, H);

    particles.forEach((p, i) => {
        if (p.isDead && !p.recycled) {
            // If settled, keep drawing it slightly opaque but stationary
             ctx.fillStyle = 'rgba(139, 69, 19, 0.8)';
             ctx.beginPath();
             ctx.arc(p.x, p.y, p.r, 0, Math.PI * 2);
             ctx.fill();

        } else {
            p.draw();
        }
    });


    // Info Display
    ctx.fillStyle = '#FFF';
    ctx.font = '16px monospace';
    ctx.fillText(`Resting Grains: ${particleCountAtRest}/${totalParticles}`, 10, 20);

    requestAnimationFrame(updateSimulation);
}

particles = [];
for (let i = 0; i < PARTICLE_COUNT; i++) {
    particles.push(new Particle());
}

// Start the simulation loop
updateSimulation();
</script>
</body>
</html>
```
deepseek-r1:14bERRBLANKFORMAT
Show code
```html
<!DOCTYPE html>
<html>
<body style="background:#000;margin:0;display:flex;justify-content:center;height:100vh">
<canvas id=c width=800 height=600></canvas>
<script>
const c=document.getElementById('c'),ctx=c.getContext('2d');
let particles=[],groundHeight=0,countResting=0;
function init(){countResting=0;particles=[];for(let i=0;i<1500;i++) {
    let r=Math.random()*10+5, x = Math.random()*(c.width-2*r)+r, y=r;
    particles.push({x,y,vx:0,vy:Math.random()-0.5*3, radius:r,active:true});
}
const g=0.001; // gravity
function animate(){
    ctx.fillStyle='black';ctx.fillRect(0,0,c.width,c.height);
    ctx.beginPath();ctx.moveTo(0,c.height);ctx.lineTo(c.width,c.height);ctx.strokeStyle='white';ctx.lineWidth=2;
    ctx.stroke();
    let activeCount=0;
    for(let p of particles){
        if(!p.active) continue;
        p.vy+=g; // apply gravity
        let dx=p.x-Math.floor(p.x),dx=Math.abs(dx*2-1); //friction
        p.vx-=p.vx*dx*0.6; p.vy *= 0.995;
        p.x += p.vx; p.y += p.vy;
        
        // check collisions with ground and other particles
        if(p.y > c.height - p.radius){
            p.active=false;countResting++; continue;
        }
        for(let q of particles){
            if(!q.active || p===q) continue;
            let dx=p.x-q.x, dy=p.y-q.y,d=Math.sqrt(dx*dx+dy*dy);
            if(d < 0.5*p.radius && d>0){
                const angle = Math.atan2(dy,dx);
                const force= (0.5/d)*Math.min(1e3/(d*d),1e3);
                p.vx-=Math.cos(angle)*force/2;
                p.vy-=Math.sin(angle)*force/2;
                q.vx+=Math.cos(angle)*force/2;
                q.vy+=Math.sin(angle)*force/2;
            }
        }
        activeCount++;
    }
    
    // update count and height
    ctx.fillStyle='white';ctx.font='16px Arial';
    ctx.fillText(`Grains at rest: ${countResting}`,5,20);
    let maxY=particles.reduce((max,p)=>Math.max(max,p.y),0); 
    ctx.fillText(`Height: ${(c.height - maxY).toFixed(0)}px`,5,40);

    if(activeCount) requestAnimationFrame(animate);
}
init();requestAnimationFrame(animate);
</script>
</body>
</html>
```
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.
qwen3:14bBLANKLIM
Show code
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Continuous Sand Simulation</title>
    <style>body {margin:0;overflow:hidden;background:#111;}canvas {display:block;border:1px solid #333;}</style>
</head>
<body>
<canvas id="sandCanvas"></canvas>
<script>
const canvas = document.getElementById('sandCanvas');
const ctx = canvas.getContext('2d');
let W, H;
let particles = [];
let restingParticles = [];
let frameCount = 0;

function setup() {
    W = window.innerWidth * 0.95;
    H = window.innerHeight * 0.8;
    canvas.width = W;
    canvas.height = H;
    particles = [];
    restingParticles = [];
}

function init() {
    setup();
    for (let i = 0; i < 1500; i++) {
        particles.push({ x: Math.random() * W, y: -Math.random() * H, vx: 0, vy: 0 });
    }
    frameCount++;
}

function updatePhysics(p) {
    const G = 9.8 / 60; // Gravity scaled for frame rate
    const DAMPING = 0.7;
    let hit = false;

    // Apply gravity and movement
    p.vy += G;
    p.x += p.vx;
    p.y += p.vy;

    // Simple collision detection with the ground/pile structure
    for (const r of restingParticles) {
        if (Math.abs(r.x - p.x) < 10 && Math.abs(r.y - p.y) < 10) {
            hit = true;
            break;
        }
    }

    // If hitting the 'ground' or an existing particle, settle it
    if (p.y >= H * 0.9 || hit) {
        p.vy *= -DAMPING; // Bounce/loss of energy
        p.vx *= DAMPING;

        // Check for near-rest state
        if (Math.abs(p.vy) < 0.1 && Math.abs(p.vx) < 0.1 && p.y >= H * 0.9 - 5) {
            p.isResting = true;
        }
    }

    // If particle settles, transfer it to resting state and remove from active pool
    if (p.isResting) {
        restingParticles.push(Object.assign({}, p)); // Deep copy
        return false; // Indicates removal from active array
    }
    return true;
}

function update() {
    ctx.clearRect(0, 0, W, H);

    const activeParticles = [];
    let settledCount = 0;

    for (let i = 0; i < particles.length; i++) {
        const p = particles[i];
        if (!p.isResting) {
            // Apply slight lateral force for spreading
            p.vx += Math.sin(frameCount * 0.01 + i / 50) * 0.005;

            let keepActive = updatePhysics(p);
            if (keepActive) {
                activeParticles.push(p);
            } else {
                settledCount++;
            }
        }
    }
    particles = activeParticles;


    // Draw particles and rest pile
    ctx.fillStyle = '#555';
    for (const p of particles) {
        ctx.fillRect(p.x, p.y, 1, 1);
    }

    ctx.fillStyle = '#8a4d0e'; // Sand color
    for (const r of restingParticles) {
        ctx.beginPath();
        ctx.arc(r.x, r.y, 0.5, 0, Math.PI * 2);
        ctx.fill();
    }

    // Stats Display
    const restHeight = restingParticles.length > 0 ? Math.min(...restingParticles.map(p => p.y)) : H;
    document.getElementById('stats').innerHTML = `Resting Grains: ${restingParticles.length}<br>Pile Height: ${(H - restHeight).toFixed(1)}px`;

    requestAnimationFrame(update);
}

// Initialization and Runtime Management
window.onload = () => {
    const statsDiv = document.createElement('div');
    statsDiv.id = 'stats';
    statsDiv.style.color = '#ccc';
    document.body.appendChild(statsDiv);
    init();
};

</script>
<!-- The physics simulation logic implicitly handles the angle of repose by having particles settle only when they collide with an existing structure, reducing their energy until velocity approaches zero. -->
<div id="stats" style="position:absolute;top:10px;left:10px;color:#ccc;">Loading...</div>

</body>
</html>
mistral-small:24bOKFORMAT
Show code
```html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Falling Sand Simulation</title>
<style>
body { margin: 0; overflow: hidden; }
canvas { display: block; }
#info { position: absolute; top: 10px; left: 10px; color: white; font-size: 20px; z-index: 10; }
</style>
</head>
<body>
<canvas id="sandCanvas"></canvas>
<div id="info">Grains at Rest: 0, Pile Height: 0</div>

<script>
const canvas = document.getElementById('sandCanvas');
const ctx = canvas.getContext('2d');
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;

const grains = [];
const gravity = 0.1;
const friction = 0.95;
let restingGrains = 0;
let maxY = 0;

function createGrain() {
    return { x: Math.random() * canvas.width, y: 0, vx: (Math.random() - 0.5) * 2, vy: 0 };
}

for (let i = 0; i < 1500; i++) grains.push(createGrain());

function updateGravity(grains) {
    for (const grain of grains) {
        if (!grain.resting) {
            grain.vy += gravity;
        }
    }
}

function moveGrains(grains) {
    for (const grain of grains) {
        if (!grain.resting) {
            grain.x += grain.vx;
            grain.y += grain.vy;

            if (grain.y > canvas.height - 5 || grain.x < 0 || grain.x > canvas.width)
                grain.resting = true;
            grain.vx *= friction;
            grain.vy *= friction;
        }

        if(grain.y>maxY)grain.y=maxY,grain.resting=true,maxY=grain.y
    }
}

function checkRestingGrains(grains) {
    restingGrains = grains.filter(g => g.resting).length;
    document.getElementById('info').innerHTML = `Grains at Rest: ${restingGrains}, Pile Height: ${maxY}`;
    if (restingGrains === 1500) restartSimulation();
}

function drawGrains() {
    ctx.clearRect(0, 0, canvas.width, canvas.height);
    for (const grain of grains) {
        ctx.fillStyle = 'yellow';
        ctx.beginPath();
        ctx.arc(grain.x, grain.y, 2, 0, Math.PI * 2);
        ctx.fill();
    }
}

function animate() {
    updateGravity(grains);
    moveGrains(grains);
    checkRestingGrains(grains);
    drawGrains();
    requestAnimationFrame(animate);
}

function restartSimulation() {
    grains.length=0;
    maxY = 0;
    restingGrains = 0;
    for (let i = 0; i < 1500; i++) grains.push(createGrain());
}

animate();
</script>
</body>
</html>
```
deepseek-r1:32bOKFORMAT
Show code
```html
<!DOCTYPE html><html><head>
<style>body{margin:0;display:flex;flex-direction:column;align-items:center;background:#1a1a1a}
canvas{border:1px solid #333;margin-top:20px}.stats{color:white;padding:10px}</style>
</head><body>
<canvas id=c></canvas><div class=stats id=s>Particles active: 1500 | Height: 0px</div>
<script>
const canvas = document.getElementById('c'), cxt = canvas.getContext('2d');
let particles = [], w, h, stats = document.getElementById('s'), max_height = 0;

function resizeCanvas() {
    w = window.innerWidth * 0.8;
    h = window.innerHeight * 0.6;
    canvas.width = w;
    canvas.height = h;
}

window.addEventListener('resize', resizeCanvas);
resizeCanvas();

class Particle {
    constructor(x, y) {
        this.x = x || Math.random() * w;
        this.y = y || -Math.random() * 100;
        this.vx = (Math.random() - 0.5) * 2;
        this.vy = 0.1;
        this.mass = 1;
        this.radius = 8;
        this.rest = false;
    }
}

function init() {
    particles.length = 0;
    max_height = 0;
    for (let i = 0; i < 1500; i++) {
        particles.push(new Particle());
    }
    stats.textContent = `Particles active: ${particles.filter(p => !p.rest).length} | Height: ${Math.floor(max_height)}px`;
}

function draw() {
    cxt.fillStyle = 'rgba(0,0,0,0.1)';
    cxt.fillRect(0, 0, w, h);
    
    particles.forEach((p, i) => {
        if (p.rest) return;
        
        p.vy += 0.25;
        p.x += p.vx;
        p.y += p.vy;
        
        const r = p.radius;
        let otherParticles = particles.filter((_, j) => j > i);
        
        otherParticles.forEach(op => {
            if (op.rest) return;
            
            const dx = op.x - p.x, dy = op.y - p.y;
            const dist = Math.sqrt(dx * dx + dy * dy);
            const minDist = r + r;
            if (dist < minDist) {
                const angle = Math.atan2(dy, dx);
                const ax = Math.cos(angle), ay = Math.sin(angle);
                const delta = minDist - dist;
                
                p.x -= ax * delta * 0.5;
                p.y -= ay * delta * 0.5;
                op.x += ax * delta * 0.5;
                op.y += ay * delta * 0.5;
                
                const overlap = delta / minDist;
                const force = 1500 * overlap;
                p.vx -= force * Math.cos(angle) * 0.1;
                p.vy -= force * Math.sin(angle) * 0.1;
                op.vx += force * Math.cos(angle) * 0.1;
                op.vy += force * Math.sin(angle) * 0.1;
            }
        });
        
        if (p.y + r > h - 2 || p.x + r > w - 2 || p.x - r < 2) {
            p.vx *= -0.5;
            p.vy *= -0.5;
            
            if (p.y + r > h - 2 && Math.abs(p.vy) < 0.1) {
                p.rest = true;
                max_height = Math.max(max_height, p.y);
            }
        }
        
        cxt.beginPath();
        cxt.arc(p.x, p.y, r, 0, Math.PI * 2);
        const color = `hsl(${(p.y / h * 360) % 360},70%,50%)`;
        cxt.fillStyle = color;
        cxt.fill();
    });
    
    stats.textContent = `Particles active: ${particles.filter(p => !p.rest).length} | Height: ${Math.floor(max_height)}px`;
}

function runSimulation() {
    init();
    const start = Date.now();
    
    function animate() {
        draw();
        if (Date.now() - start > 60000) {
            requestAnimationFrame(animate);
        } else {
            requestAnimationFrame(animate);
        }
    }
    
    requestAnimationFrame(animate);
}

runSimulation();
</script></body></html>
```
command-r:35bERRBLANKFORMAT
Show code
```html
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Falling Sand Simulation</title>
    <style>
        body { margin: 0; }
        canvas { display: block; }
    </style>
</head>
<body>
    <canvas></canvas>

    <script>
        const canvas = document.querySelector('canvas');
        const ctx = canvas.getContext('2d');

        const W = window.innerWidth, H = window.innerHeight;
        canvas.width = W; canvas.height = H;

        const PI2 = Math.PI * 2, GRAVITY = 0.15, FRICTION = 0.98;
        
        class Particle {
            constructor() {
                this.x = random(W);
                this.y = -3 + random(H/5);
                this.vx = (random(0.7) > 0.5 ? 1 : -1) * random(GRAVITY*2);
                this.vy = GRAVITY;
            }

            update() {
                if (!this.collides()) {
                    this.x += this.vx;
                    this.y += this.vy;

                    const hyp = Math.hypot(this.vx, this.vy),
                          ang = Math.atan2(-this.vy, this.vx) + random() * PI2 / 8 - PI2/16;
                    this.vx = Math.cos(ang);
                    this.vy = Math.sin(ang);

                    if (hyp > GRAVITY*4) {
                        this.vx *= FRICTION;
                        this.vy *= FRICTION;
                    }
                } else {
                    this.resting = true;
                }
            }

            collides() {
                return (this.x < 0 || this.x >= W) || (this.y > H);
            }

            draw() {
                ctx.beginPath();
                const r = restings.length / 1500, g = Mathings.length / 1500;
                ctx.fillStyle = `rgba(${r*256},${g*256},0.3)`;
                ctx.arc(this.x|0, this.y|0, 1 + restings.length/3e3, 0, PI2);
                ctx.closePath();
                ctx.fill();
            }
        }

        const particles = Array(1500).fill().map(_ => new Particle());

        let time, restingCount = 0, matteringCount = 0;

        function animate() {
            ctx.clearRect(0, 0, W, H);

            if (!time) {
                time = Date.now();
            } else {
                const now = Date.now(), dt = (now - time)/1000;

                particles.forEach(p => p.update());
                
                restingCount -= matteringCount;
                matteringCount = 0;

                ctx.fillStyle = 'rgba(256,256,'+restingCount/4e2+'';
                ctx.fillText('Particles at rest: '+restingCount, W-190, H-3);
                
                particles.forEach(p => {
                    if (p.resting) restingCount++;
                    p.draw();
                    if (!p.collides()) matteringCount++;
                });

                requestAnimationFrame(animate);
            }

            time = now;
        }

        animate();
    </script>
</body>
</html>
```
qwq:32bSTATMINIFIED
Show code
<think>

Okay, the user wants me to create a self-contained HTML file that simulates falling sand with 1500 particles. Let me start by breaking down the requirements.

First, the HTML needs to use a canvas element. Since it's strict, I'll make sure to include the doctype and necessary tags. The simulation has to be continuous, using floating-point positions and velocities for each particle. Gravity will act on them, and when they hit the ground or another particle, they should lose energy, eventually coming to rest. The angle of repose should form naturally as the pile grows. 

I need to track the number of resting grains and the pile's height. Also, the animation must use requestAnimationFrame for smoothness. It should start automatically and restart with a new instance when finished. No comments or external libraries are allowed.

Hmm, let's think about the structure. The canvas will be the main element. The JavaScript will handle the particles. Each particle should have x and y coordinates, velocity in both directions. Gravity will affect the y-velocity. When a particle hits the ground (the bottom of the canvas) or another particle, it should lose some velocity, like 20%, and maybe check if it's come to rest (velocity under a threshold).

Wait, but how do particles interact? Since it's continuous, I can't use a grid. Maybe for each particle, when it hits the floor, check if there's another particle beneath it? Or perhaps when a particle comes to rest, it becomes part of the static pile, and other particles can land on it. Maybe track resting particles in an array, and when a moving particle's next position would be below a resting particle, it stops.

Alternatively, every frame, particles are updated: apply gravity, move, then check for collisions. For collisions with the ground: if the new y is beyond the bottom, set y to the bottom, reduce velocity, and check if it's now stationary. For collisions with other particles, it's more complex. Maybe check all other particles (but 1500 particles would make that O(n^2), which might be slow). Maybe a better way is needed, but perhaps for simplicity, it can be approximated with a threshold distance.

Wait, the user said "free-moving particles" so maybe particles can pass through each other unless they hit the ground or the pile. The pile's height is tracked, so maybe once a particle comes to rest, it's part of the pile, and others can land on it. So each moving particle checks if their y is above the pile's height at their x position? That might simplify things.

Alternatively, when a particle hits the ground (y = canvas height), it checks if there's a particle already resting at that x, and if so, maybe place it a little higher. Hmm, but I need to track the height at each x. Maybe an array that keeps the maximum y at each x coordinate? But how to represent that? Since the x is continuous, perhaps using an array with buckets. But the user said no grids, continuous coordinates only. So perhaps just track all resting particles and for a moving particle, check the closest ones?

Hmm, this is getting complicated. Let me think of a simpler approach first. Maybe particles can only interact with the ground first. When they hit the ground, they stop, and form the pile. The height of the pile is the highest y-coordinate among all resting particles. But then, particles falling on the pile would need to find a place to rest on top of the existing pile. But how to compute where they would land?

Alternatively, when a particle comes to rest (velocity below threshold), it's added to the resting array. Then, subsequent particles, when moving, can check if their current position is below any resting particle. But checking all resting particles each frame might be too slow for 1500 particles.

Maybe a better way is to compute the height at their current x. To do this, since resting particles are stored in an array, for a given x, find the maximum y of any resting particle within a certain radius. But that could be time-consuming. Alternatively, use a grid for the resting particles to speed up lookups, but the user said no grid. So perhaps I have to proceed with a less efficient method but manageable.

Alternatively, let me think of the following steps:

Each particle has:

- x, y (position)
- vx, vy (velocity)
- resting: a boolean

Initialize all particles at the top, starting to fall.

Each frame:

Loop through all particles:

If not resting:

- Apply gravity (vy += 0.1 or something)
- Apply drag or velocity loss (maybe multiply velocity by 0.99 each frame?)
- Move the particle: x += vx, y += vy
- Check for collisions:
   - If y exceeds canvas height (the floor), set y to the floor, then reduce vy by 80% (so it loses energy). Then check if vy is below a threshold (like 1), then set resting to true.
   - Also, check if the particle is below another resting particle. But how?

Wait, maybe the resting particles form the pile, so when a moving particle's y position is below the highest resting particle's y at that x, it can land on them. To compute the maximum y at the current x of the moving particle, I need to find among all resting particles the maximum y where the x is close enough. Since particles can spread, perhaps the maximum y within some horizontal distance.

Alternatively, for a moving particle, after moving, check if there are any resting particles below it (i.e., with x near and y less than current position). But that might be computationally intensive. Maybe for simplicity, just check the floor first. The problem is that without any collision between particles, they'd just pile up straight down, which would not form an angle of repose. So the angle comes from particles landing on top of each other, but that requires some method of detecting when they're on top of others.

Hmm, perhaps the problem is expecting a simplified approach where particles only interact with the floor and lose velocity on impact, and once they stop moving, they form a pile. The angle of repose would emerge naturally as particles land on the pile. Let me think of this way: each time a particle hits the floor, it bounces back up with reduced velocity (the 20% remaining). If it bounces up, then it might land again on another particle, but since all particles are moving, maybe the algorithm just needs to check the ground first and then see if the velocity drops to zero.

Alternatively, maybe the angle arises simply from the particles' initial positions. Wait, but how to get them to spread out?

Alternatively, perhaps when a particle hits the ground, it stops, and subsequent particles will land either on the ground or on top of a resting particle. To check if a moving particle is over a resting one, maybe the particle's y position is below the pile's height at that x. To track pile height: for each x, the max y of resting particles.

But tracking that could be done by maintaining an array of resting particles and for a given x, find the highest resting particle near that x. Since it's continuous, maybe use an array for the resting particles and for each moving particle, when it hits the floor, we check all resting particles to see if any are directly below, but that's O(n). For 1500 particles, that's 1500 checks each time. For each frame, that might be slow, but perhaps manageable since it's only when a particle is in motion.

Alternatively, maybe when a particle is resting, it's added to
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>Continuous Falling Sand</title>
<style>
html,body{margin:0;height:100%;overflow:hidden;background:#111;color:#eee;font:14px system-ui}
canvas{display:block;width:100vw;height:100vh}
#hud{position:fixed;left:14px;top:12px;background:#0008;padding:8px 10px;border-radius:6px}
</style>
</head>
<body>
<canvas id="c"></canvas><div id="hud"></div>
<script>
const c=document.getElementById("c"),x=c.getContext("2d"),hud=document.getElementById("hud");
let W,H,D,grains,active,done,last=0,spawn=0;
const N=1500,R=2.2,G=900,DT=1/120,REST=.55,FRIC=.78;
function reset(){
 D=Math.min(devicePixelRatio||1,2);W=c.width=innerWidth*D;H=c.height=innerHeight*D;c.style.width=innerWidth+"px";c.style.height=innerHeight+"px";x.setTransform(1,0,0,1,0,0);
 grains=[];active=[];done=false;spawn=0;last=0;
}
onresize=reset;reset();
function add(){
 let cx=W*.5+(Math.random()-.5)*W*.08;
 let g={x:cx,y:-10,vx:(Math.random()-.5)*80*D,vy:0,rest:false,a:Math.random()*6.28,c:`hsl(${38+Math.random()*14} 80% ${52+Math.random()*18}%)`};
 grains.push(g);active.push(g);spawn++;
}
function collide(g,o){
 let dx=g.x-o.x,dy=g.y-o.y,d2=dx*dx+dy*dy,min=R*D*2;
 if(d2>0&&d2<min*min){
  let d=Math.sqrt(d2),nx=dx/d,ny=dy/d,p=min-d;
  g.x+=nx*p;g.y+=ny*p;
  let vn=g.vx*nx+g.vy*ny;
  if(vn<0){g.vx-=nx*vn*(1+REST);g.vy-=ny*vn*(1+REST);g.vx*=FRIC;g.vy*=FRIC}
  if(Math.hypot(g.vx,g.vy)<18*D&&ny<-.28&&g.y>H*.18){g.vx=0;g.vy=0;g.rest=true}
 }
}
function step(){
 if(spawn<N)for(let i=0;i<3&&spawn<N;i++)add();
 let rest=grains.filter(g=>g.rest);
 for(let g of active){
  if(g.rest)continue;
  g.vy+=G*D*DT;g.x+=g.vx*DT;g.y+=g.vy*DT;
  if(g.x<R*D){g.x=R*D;g.vx*=-.35}
  if(g.x>W-R*D){g.x=W-R*D;g.vx*=-.35}
  if(g.y>H-R*D){g.y=H-R*D;g.vy*=-.22;g.vx*=FRIC;if(Math.abs(g.vy)<35*D){g.vy=0;g.rest=true}}
  for(let i=rest.length-1;i>=0;i--){let o=rest[i];if(Math.abs(g.x-o.x)<R*D*3&&Math.abs(g.y-o.y)<R*D*3)collide(g,o)}
 }
 active=active.filter(g=>!g.rest);
 if(spawn>=N&&!active.length){done=true;setTimeout(reset,1200)}
}
function draw(){
 x.fillStyle="#111";x.fillRect(0,0,W,H);
 x.fillStyle="#242424";x.fillRect(0,H-R*D,W,R*D);
 for(let g of grains){x.fillStyle=g.c;x.beginPath();x.arc(g.x,g.y,R*D,0,7);x.fill()}
 let rested=grains.reduce((n,g)=>n+(g.rest?1:0),0),top=H;
 for(let g of grains)if(g.rest&&g.y<top)top=g.y;
 hud.textContent="at rest: "+rested+" / "+N+"   pile height: "+Math.max(0,Math.round((H-top)/D))+" px";
}
function loop(t){
 if(!last)last=t;
 let n=Math.min(5,Math.floor((t-last)/1000/DT));last+=n*DT*1000;
 while(n--)step();
 draw();
 requestAnimationFrame(loop);
}
requestAnimationFrame(loop);
</script>
</body>
</html>
Claude Sonnet 5frontier referenceOK
Show code
<!doctype html>
<html><head><meta charset="utf-8"><title>Falling Sand Particles</title>
<style>html,body{margin:0;background:#101014;overflow:hidden;height:100%}canvas{display:block}</style>
</head><body>
<canvas id="c"></canvas>
<script>
const cv=document.getElementById('c'),ctx=cv.getContext('2d');
function resize(){cv.width=innerWidth;cv.height=innerHeight;}
resize();addEventListener('resize',resize);
const WX=40,WY=20,WW=600,WH=460,FLOOR=WY+WH,TOTAL=1500,MAXACTIVE=22,CELL=8,G=0.28;
let active,resting,grid,spawned,emitX,minRestY,waitTimer;
function rnd(a,b){return a+Math.random()*(b-a)}
function key(cx,cy){return cx+'_'+cy}
function cellOf(x,y){return [Math.floor((x-WX)/CELL),Math.floor((y-WY)/CELL)]}
function addToGrid(p,idx){
  const[cx,cy]=cellOf(p.x,p.y);
  const k=key(cx,cy);
  if(!grid.has(k))grid.set(k,[]);
  grid.get(k).push(idx);
}
function init(){
  active=[];resting=[];grid=new Map();
  spawned=0;emitX=rnd(WX+WW*0.3,WX+WW*0.7);
  minRestY=FLOOR;waitTimer=0;
}
init();
function tryCollide(p){
  const[cx,cy]=cellOf(p.x,p.y);
  let touched=false;
  for(let dx=-1;dx<=1;dx++)for(let dy=-1;dy<=1;dy++){
    const k=key(cx+dx,cy+dy);
    const list=grid.get(k);
    if(!list)continue;
    for(const idx of list){
      const o=resting[idx];
      const ddx=p.x-o.x,ddy=p.y-o.y;
      const d=Math.hypot(ddx,ddy),minD=p.r+o.r;
      if(d<minD&&d>0.0001){
        const nx=ddx/d,ny=ddy/d;
        p.x=o.x+nx*minD;p.y=o.y+ny*minD;
        p.vx+=-nx*Math.abs(p.vy)*0.4+rnd(-0.05,0.05);
        p.vy*=0.15;
        touched=true;
      }
    }
  }
  return touched;
}
function step(){
  if(spawned<TOTAL&&active.length<MAXACTIVE&&Math.random()<0.9){
    active.push({x:emitX+rnd(-4,4),y:WY+2,vx:rnd(-0.2,0.2),vy:0,r:rnd(1.6,2.4)});
    spawned++;
  }
  for(let i=active.length-1;i>=0;i--){
    const p=active[i];
    p.vy=Math.min(p.vy+G,7);
    p.x+=p.vx;p.y+=p.vy;
    p.vx*=0.985;
    if(p.x-p.r<WX){p.x=WX+p.r;p.vx*=-0.3;}
    if(p.x+p.r>WX+WW){p.x=WX+WW-p.r;p.vx*=-0.3;}
    let onFloor=false;
    if(p.y+p.r>FLOOR){p.y=FLOOR-p.r;p.vy*=0.1;onFloor=true;}
    const touched=tryCollide(p);
    const speed=Math.hypot(p.vx,p.vy);
    if((onFloor||touched)&&speed<0.35){
      resting.push(p);addToGrid(p,resting.length-1);
      minRestY=Math.min(minRestY,p.y-p.r);
      active.splice(i,1);
    }
  }
  if(spawned>=TOTAL&&active.length===0){
    waitTimer++;
    if(waitTimer>90)init();
  }
}
function draw(){
  ctx.fillStyle='#101014';ctx.fillRect(0,0,cv.width,cv.height);
  ctx.strokeStyle='#456';ctx.strokeRect(WX,WY,WW,WH);
  ctx.fillStyle='#d9b25c';
  for(const p of resting){ctx.beginPath();ctx.arc(p.x,p.y,p.r,0,7);ctx.fill();}
  ctx.fillStyle='#ffe08a';
  for(const p of active){ctx.beginPath();ctx.arc(p.x,p.y,p.r,0,7);ctx.fill();}
  ctx.fillStyle='#eef';ctx.font='16px monospace';ctx.textAlign='left';
  ctx.fillText('Falling Sand - Free Particles (continuous)',WX,FLOOR+30);
  ctx.font='13px monospace';ctx.fillStyle='#9cf';
  ctx.fillText('Grains at rest: '+resting.length+' / '+TOTAL,WX,FLOOR+52);
  ctx.fillText('Pile height: '+Math.round(FLOOR-minRestY)+'px',WX,FLOOR+72);
}
function loop(){
  step();draw();
  requestAnimationFrame(loop);
}
requestAnimationFrame(loop);
</script>
</body></html>
Claude Haiku 4.5frontier referenceOK
Show code
<!DOCTYPE html><html><head><title>r76</title></head><body><canvas id="c"></canvas><script>
let c=document.getElementById('c'),x=c.getContext('2d');c.width=800;c.height=600;
let g=9.8,p=[],rest=0,maxh=0;
for(let i=0;i<1500;i++){p.push({x:Math.random()*780+10,y:-30,vx:0,vy:0,r:2});}
function draw(){
  x.fillStyle="#fff";x.fillRect(0,0,800,600);
  // Update
  let moving=0;
  p.forEach(pa=>{
    pa.vy+=g*0.016;pa.y+=pa.vy*0.016;pa.x+=pa.vx*0.016;
    if(pa.y>560){pa.y=560;pa.vy*=-0.3;if(Math.abs(pa.vy)<1)pa.vy=0;}
    if(pa.x<10)pa.x=10;if(pa.x>790)pa.x=790;
    if(Math.abs(pa.vy)<0.1&&pa.y>555)rest++;else moving++;
    maxh=Math.max(maxh,600-pa.y);
  });
  // Draw
  x.fillStyle="#d4a574";
  p.forEach(pa=>{x.beginPath();x.arc(pa.x,pa.y,pa.r,0,Math.PI*2);x.fill();});
  // Gravity indicator
  x.strokeStyle="#999";x.lineWidth=1;
  for(let i=0;i<800;i+=50){x.beginPath();x.moveTo(i,565);x.lineTo(i+10,570);x.stroke();}
  x.fillStyle="#000";x.font="14px mono";x.fillText("At Rest:"+rest+" Moving:"+moving+" Height:"+Math.floor(maxh),100,50);
  if(rest>1490)setTimeout(()=>{p=[];rest=0;maxh=0;for(let i=0;i<1500;i++)p.push({x:Math.random()*780+10,y:-30,vx:0,vy:0,r:2});},500);
  requestAnimationFrame(draw);
}draw();
</script></body></html>
Results at a glance— every model in this round with score, response time and status.

Who wrote what

ModelBlind scoreLatencyStatus
gemma4:26b ——harness
llama3.1:8b —100.8 sOK
qwen3.5:9b —43.5 sOK
qwen3-coder:30b —22.9 sOK
deepseek-r1:14b —103.2 sOK
qwen3:8b ——harness
qwen3:14b —27.5 sOK
mistral-small:24b —106.2 sOK
deepseek-r1:32b —139.2 sOK
command-r:35b —138.4 sOK
qwq:32b —139.2 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>Falling Sand — 1500 Free Particles</title>
<style>
html,body{margin:0;height:100%;overflow:hidden;background:#080a0f;color:#e8e0cd;
font:12px/1.6 ui-monospace,SFMono-Regular,Menlo,Consolas,monospace}
#wrap{position:fixed;inset:0;display:flex;align-items:center;justify-content:center}
canvas{background:radial-gradient(120% 90% at 50% 0%,#161b26,#0a0d13);border-radius:12px;
box-shadow:0 24px 70px #000c,inset 0 0 0 1px #ffffff10}
#hud{position:fixed;top:18px;left:50%;transform:translateX(-50%);display:flex;gap:22px;
align-items:center;padding:8px 16px;border-radius:999px;background:#0f131caa;
backdrop-filter:blur(6px);box-shadow:0 0 0 1px #ffffff14;letter-spacing:.06em;white-space:nowrap}
#hud b{color:#f0d79a;letter-spacing:.16em;font-weight:600}
#hud i{font-style:normal;color:#fff;font-variant-numeric:tabular-nums}
#hud span{color:#9aa3b4;text-transform:uppercase;font-size:11px}
</style>
</head>
<body>
<div id="wrap"><canvas id="c"></canvas></div>
<div id="hud"><b>FALLING SAND</b><span>grains <i id="tot">0</i>/1500</span>
<span>at rest <i id="rst">0</i></span><span>pile height <i id="hgt">0</i>px</span></div>
<script>
const cv=document.getElementById('c'),ctx=cv.getContext('2d');
const eT=document.getElementById('tot'),eR=document.getElementById('rst'),eH=document.getElementById('hgt');
const N=1500,R=2.6,D=5.2,G=1150,SUB=3,IT=2,VMAX=340,K=100003;
const SH=['#f6e7bb','#ecd497','#ddbb75','#cba55f','#b78c4b'];
let W=0,H=0,P=[],spawned=0,emitX=0,emitJ=0,doneT=0,age=0;
function fit(){const d=Math.min(devicePixelRatio||1,2);
W=Math.max(320,Math.min(innerWidth-30,960));H=Math.max(300,Math.min(innerHeight-30,660));
cv.style.width=W+'px';cv.style.height=H+'px';cv.width=W*d|0;cv.height=H*d|0;ctx.setTransform(d,0,0,d,0,0);}
function reset(){P=[];spawned=0;doneT=0;age=0;emitX=W*(0.34+Math.random()*0.32);emitJ=6+Math.random()*32;}
fit();reset();
addEventListener('resize',()=>{fit();reset();});
function collide(){
const m=new Map(),n=P.length;
for(let i=0;i<n;i++){const p=P[i],k=Math.floor(p.x/D)*K+Math.floor(p.y/D);
let a=m.get(k);if(!a)m.set(k,a=[]);a.push(i);}
for(let it=0;it<IT;it++)for(let i=0;i<n;i++){
const a=P[i],cx=Math.floor(a.x/D),cy=Math.floor(a.y/D);
for(let ox=-1;ox<2;ox++)for(let oy=-1;oy<2;oy++){
const bucket=m.get((cx+ox)*K+cy+oy);if(!bucket)continue;
for(let q=0;q<bucket.length;q++){const j=bucket[q];if(j<=i)continue;const b=P[j];
const dx=b.x-a.x,dy=b.y-a.y,d2=dx*dx+dy*dy;if(d2>=D*D||d2<1e-9)continue;
const d=Math.sqrt(d2),nx=dx/d,ny=dy/d,ov=D-d;a.sup=1;b.sup=1;
if(a.s&&b.s){if(ov>R*0.5){a.s=0;b.s=0;}continue;}
if(a.s&&ov>R*0.6)a.s=0;if(b.s&&ov>R*0.6)b.s=0;
const wa=a.s?0:1,wb=b.s?0:1,tw=wa+wb;if(!tw)continue;
const c=ov*0.75/tw;a.x-=nx*c*wa;a.y-=ny*c*wa;b.x+=nx*c*wb;b.y+=ny*c*wb;
const vn=(b.vx-a.vx)*nx+(b.vy-a.vy)*ny;
if(vn<0){const im=-1.08*vn/tw;a.vx-=nx*im*wa;a.vy-=ny*im*wa;b.vx+=nx*im*wb;b.vy+=ny*im*wb;}
const tx=-ny,ty=nx,ft=((b.vx-a.vx)*tx+(b.vy-a.vy)*ty)*0.5/tw;
a.vx+=tx*ft*wa;a.vy+=ty*ft*wa;b.vx-=tx*ft*wb;b.vy-=ty*ft*wb;}}}}
function physics(){
if(spawned<N)for(let i=0;i<10&&spawned<N;i++){spawned++;
P.push({x:emitX+(Math.random()-0.5)*emitJ,y:-R-Math.random()*70,vx:(Math.random()-0.5)*26,
vy:60+Math.random()*45,s:0,t:0,sup:0,g:Math.random()*5|0});}
const dt=1/(60*SUB);
for(let k=0;k<SUB;k++){
for(const p of P){if(p.s)continue;p.sup=0;p.vy+=G*dt;
const sp=Math.hypot(p.vx,p.vy);if(sp>VMAX){p.vx*=VMAX/sp;p.vy*=VMAX/sp;}
p.x+=p.vx*dt;p.y+=p.vy*dt;}
collide();
for(const p of P){if(p.s)continue;
if(p.y>H-R){p.y=H-R;if(p.vy>0)p.vy*=-0.1;p.vx*=0.55;p.sup=1;}
if(p.x<R){p.x=R;p.vx=Math.abs(p.vx)*0.25;}else if(p.x>W-R){p.x=W-R;p.vx=-Math.abs(p.vx)*0.25;}
if(p.sup&&p.vx*p.vx+p.vy*p.vy<6){if(++p.t>20){p.s=1;p.vx=0;p.vy=0;}}else p.t=0;}}
age++;}
function draw(){
ctx.clearRect(0,0,W,H);
let rest=0,top=H;
for(const p of P)if(p.s){rest++;if(p.y<top)top=p.y;}
const surf=rest?top-R:H,ph=rest?Math.round(H-surf):0;
for(let g=0;g<5;g++){ctx.fillStyle=SH[g];ctx.beginPath();
for(const p of P){if(p.g!==g)continue;ctx.moveTo(p.x+R,p.y);ctx.arc(p.x,p.y,R,0,6.28319);}ctx.fill();}
if(rest){ctx.strokeStyle='rgba(120,190,255,.32)';ctx.setLineDash([7,7]);ctx.lineWidth=1;
ctx.beginPath();ctx.moveTo(0,surf+.5);ctx.lineTo(W,surf+.5);ctx.stroke();ctx.setLineDash([]);
ctx.fillStyle='rgba(150,205,255,.65)';ctx.fillText(ph+' px',10,surf-6);}
ctx.strokeStyle='rgba(255,255,255,.14)';ctx.beginPath();
ctx.moveTo(0,H-.5);ctx.lineTo(W,H-.5);ctx.stroke();
eT.textContent=P.length;eR.textContent=rest;eH.textContent=ph;
if(spawned>=N&&(P.length-rest<=4||age>2600)){if(++doneT>80)reset();}else doneT=0;}
function loop(){physics();draw();requestAnimationFrame(loop);}
requestAnimationFrame(loop);
</script>
</body>
</html>