OverviewCode › R104
Code · blind round

R104 · Abelian sandpile toppling

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.

Show task
Create ONE self-contained HTML file (STRICT LIMIT: max 7000 characters in total — this is a budget on characters, not on lines, so removing line breaks does not help) that simulates an abelian sandpile on a grid drawn to a <canvas>: drop grains onto the centre cell, and whenever a cell holds four or more grains it topples, giving one grain to each of its four orthogonal neighbours. Colour cells by how many grains they hold and keep dropping grains so the characteristic fractal pattern grows outwards. Show the number of grains dropped and the number of topplings. 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)
mistral-small:24bOKFORMAT
Show code
```html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Abelian Sandpile</title>
<style>
body { margin: 0; overflow: hidden; }
canvas { display: block; }
#stats { position: absolute; top: 10px; left: 10px; color: white; font-family: Arial, sans-serif; background: rgba(0, 0, 0, 0.5); padding: 5px; border-radius: 5px; }
</style>
</head>
<body>
<canvas id="sandpileCanvas"></canvas>
<div id="stats">Grains Dropped: 0<br>Topplings: 0</div>
<script>
const canvas = document.getElementById('sandpileCanvas');
const ctx = canvas.getContext('2d');
const stats = document.getElementById('stats');

let width = window.innerWidth;
let height = window.innerHeight;
canvas.width = width;
canvas.height = height;

const cellSize = 10;
const gridWidth = Math.floor(width / cellSize);
const gridHeight = Math.floor(height / cellSize);

let grains = Array.from({ length: gridHeight }, () => Array(gridWidth).fill(0));
let topplings = 0;
let totalDropped = 0;

function drawGrid() {
  for (let y = 0; y < gridHeight; y++) {
    for (let x = 0; x < gridWidth; x++) {
      const value = grains[y][x];
      ctx.fillStyle = `rgb(${value * 32}, ${128 - value * 32}, ${64 + value * 32})`;
      ctx.fillRect(x * cellSize, y * cellSize, cellSize, cellSize);
    }
  }
}

function updateStats() {
  stats.innerHTML = `Grains Dropped: ${totalDropped}<br>Topplings: ${topplings}`;
}

function dropGrain(cx, cy) {
  grains[cy][cx]++;
  totalDropped++;

  if (grains[cy][cx] >= 4) {
    topple(cx, cy);
  }
}

function topple(x, y) {
  topplings++;
  grains[y][x] -= 4;

  if (y > 0 && ++grains[y - 1][x] >= 4) topple(x, y - 1);
  if (y < gridHeight - 1 && ++grains[y + 1][x] >= 4) topple(x, y + 1);
  if (x > 0 && ++grains[y][x - 1] >= 4) topple(x - 1, y);
  if (x < gridWidth - 1 && ++grains[y][x + 1] >= 4) topple(x + 1, y);
}

function simulate() {
  const cx = Math.floor(gridWidth / 2);
  const cy = Math.floor(gridHeight / 2);

  dropGrain(cx, cy);
  drawGrid();
  updateStats();

  if (totalDropped < 5000) requestAnimationFrame(simulate);
}

simulate();
</script>
</body>
</html>
```
qwen3.5:9bOKFORMAT
Show code
```html
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Abelian Sandpile</title>
    <style>body{background:#111;color:white;font-family:monospace;display:flex;flex-direction:column;align-items:center;margin:0;}#canvas{border:1px solid #333;}</style>
</head>
<body>
    <canvas id="canvas" width="600" height="600"></canvas>
    <div>Grains Dropped: <span id="grains_dropped">0</span> | Topplings: <span id="topplings">0</span></div>

    <script>
        const canvas = document.getElementById('canvas');
        const ctx = canvas.getContext('2d');
        let GRID_SIZE = 50; // 50x50 grid, cell size 12px (600/50)
        const CELL_SIZE = canvas.width / GRID_SIZE;

        let grid = [];
        let droppedGrainsCount = 0;
        let topplingCount = 0;
        let animationFrameId;

        function initializeGrid() {
            grid = Array(GRID_SIZE).fill(0).map(() => Array(GRID_SIZE).fill(0));
            droppedGrainsCount = 0;
            topplingCount = 0;
            document.getElementById('grains_dropped').textContent = '0';
            document.getElementById('topplings').textContent = '0';
        }

        function draw() {
            ctx.clearRect(0, 0, canvas.width, canvas.height);
            for (let y = 0; y < GRID_SIZE; y++) {
                for (let x = 0; x < GRID_SIZE; x++) {
                    const count = grid[y][x];
                    if (count > 0) {
                        // Color mapping based on grain count (using a simple gradient/palette)
                        ctx.fillStyle = `hsl(${count * 15 % 360}, 80%, ${Math.min(90, 40 + count * 2)}%)`;
                        ctx.fillRect(x * CELL_SIZE, y * CELL_SIZE, CELL_SIZE - 1, CELL_SIZE - 1);
                    } else {
                         ctx.fillStyle = '#111'; // Background color fill for empty cells
                         ctx.fillRect(x * CELL_SIZE, y * CELL_SIZE, CELL_SIZE - 1, CELL_SIZE - 1);
                    }
                }
            }
        }

        function topple(y, x) {
            let g = grid[y][x];
            if (g >= 4) {
                grid[y][x] -= 4;
                topplingCount++;
                
                const neighbors = [[0, 1], [0, -1], [1, 0], [-1, 0]];
                for (const [dy, dx] of neighbors) {
                    const ny = y + dy;
                    const nx = x + dx;
                    if (ny >= 0 && ny < GRID_SIZE && nx >= 0 && nx < GRID_SIZE) {
                        grid[ny][nx]++;
                    }
                }
                return true; // Toppled successfully
            }
            return false;
        }

        function processTopples() {
            let toppledThisCycle = 0;
            let stable = false;
            const maxIterations = GRID_SIZE * 2 + 1; // Safety break

            for (let i = 0; i < maxIterations && !stable; i++) {
                let newTopples = 0;
                // Iterate through the grid, prioritizing checking stability
                for (let y = 0; y < GRID_SIZE; y++) {
                    for (let x = 0; x < GRID_SIZE; x++) {
                        if (grid[y][x] >= 4) {
                            // Use a depth-first approach or simply check and topple immediately.
                            // Since the sandpile is abelian, order doesn't matter for the final state.
                            topple(y, x);
                            newTopples++;
                        }
                    }
                }

                if (newTopples === 0) {
                    stable = true;
                } else {
                    toppledThisCycle++
                }
            }
        }

        function dropGrain() {
            const cy = Math.floor(GRID_SIZE / 2);
            const cx = Math.floor(GRID_SIZE / 2);
            grid[cy][cx]++;
            droppedGrainsCount++;
        }

        function simulateStep() {
            // 1. Drop a grain (Controlled rate)
            dropGrain();

            // 2. Process topplings until stable
            processTopples();

            // 3. Update display and redraw
            document.getElementById('grains_dropped').textContent = droppedGrainsCount;
            document.getElementById('topplings').textContent = topplingCount;
            draw();
        }

        function animate() {
            simulateStep();
            animationFrameId = requestAnimationFrame(animate);
        }

        // --- Start Sequence ---
        initializeGrid();
        animate();
    </script>
</body>
</html>
```
qwen3-coder:30bOK
Show code
<!DOCTYPE html>
<html><head><title>Abelian Sandpile</title></head><style>body{background:#111;margin:0;display:flex;justify-content:center;align-items:center;min-height:100vh;color:white;font-family:monospace;}canvas{border:2px solid #555;}</style><body><canvas id="sandpileCanvas"></canvas><script>
const canvas = document.getElementById('sandpileCanvas');
const ctx = canvas.getContext('2d');

let GRID_SIZE = 80; // Virtual grid size (e.g., 80x80)
let CELL_PIXELS = 15;
canvas.width = GRID_SIZE * CELL_PIXELS;
canvas.height = GRID_SIZE * CELL_PIXELS;

// Grid state: stores grain counts
let grid = [];
let droppedCount = 0;
let toppleCount = 0;

const NEIGHBORS = [
    [0, 1], [0, -1], [1, 0], [-1, 0] // Up, Down, Right, Left
];
const THRESHOLD = 4; // Toppling threshold

function initializeGrid() {
    grid = Array(GRID_SIZE).fill(0).map(() => Array(GRID_SIZE).fill(0));
    droppedCount = 0;
    toppleCount = 0;
}

// Draws the grid based on current grain counts
function drawGrid() {
    ctx.clearRect(0, 0, canvas.width, canvas.height);
    for (let y = 0; y < GRID_SIZE; y++) {
        for (let x = 0; x < GRID_SIZE; x++) {
            const grains = grid[y][x];
            let color;
            if (grains === 0) color = '#333'; // Empty
            else if (grains === 1) color = '#FFD700'; // Gold/Yellow
            else if (grains === 2) color = '#FFA500'; // Orange
            else if (grains === 3) color = '#CC0000'; // Red
            else color = '#8B0000'; // Dark Red/Black for high stacks

            ctx.fillStyle = color;
            ctx.fillRect(x * CELL_PIXELS, y * CELL_PIXELS, CELL_PIXELS - 1, CELL_PIXELS - 1);
        }
    }
}

// Processes a single toppling event at (x, y)
function processTopple(x, y) {
    if (grid[y][x] >= THRESHOLD) {
        const excess = grid[y][x] - THRESHOLD;
        grid[y][x] -= THRESHOLD; 

        for (const [dx, dy] of NEIGHBORS) {
            const nx = x + dx;
            const ny = y + dy;
            if (nx >= 0 && nx < GRID_SIZE && ny >= 0 && ny < GRID_SIZE) {
                grid[ny][nx]++;
            }
        }
        toppleCount++;
        return true; // Toppled successfully
    }
    return false;
}

// Iteratively processes all topplings until the system stabilizes
function settle() {
    let toppled = true;
    while (toppled) {
        toppled = false;
        for (let y = 0; y < GRID_SIZE; y++) {
            for (let x = 0; x < GRID_SIZE; x++) {
                if (grid[y][x] >= THRESHOLD) {
                    // Toppling structure ensures the change is immediately processed and checked.
                    if (processTopple(x, y)) {
                        toppled = true;
                    }
                }
            }
        }
    }
}

// Main simulation loop step
function simulateStep() {
    const dropX = Math.floor(GRID_SIZE / 2);
    const dropY = Math.floor(GRID_SIZE / 2);

    // 1. Drop a grain at the center
    grid[dropY][dropX]++;
    droppedCount++;

    // 2. Settle the system (toppling until stable)
    settle();

    // 3. Redraw canvas and schedule next frame
    drawGrid();
    document.getElementById('info').innerHTML = `Dropped: ${droppedCount}<br>Topples: ${toppleCount}`;
}


// Setup information display element
const infoDiv = document.createElement('div');
infoDiv.id = 'info';
infoDiv.style.paddingLeft = '20px';
infoDiv.innerHTML = `Dropped: 0<br>Topples: 0`;
document.body.appendChild(infoDiv);


// Animation loop
function animate() {
    simulateStep();
    requestAnimationFrame(animate);
}

// Initial setup and start
initializeGrid();
drawGrid(); // Draw empty initial state
animate();
</script></body></html>
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.
deepseek-r1:14bERRBLANKFORMAT
Show code
```html
<!DOCTYPE html>
<html><body style="background:#000;margin:0;display:flex;justify-content:center;align-items:center;height:100vh">
<canvas id=c></canvas>
<script>
const c=document.getElementById('c'),ctx=c.getContext('2d');
const w=50,h=w*2,cellSize=h/100,c.size=cellSize;
c.width=w*h/100*c.size|0;c.height=h*h/100*c.size|0;
let grainsDropped=topples=0,g=4,i=0,j=0,grid=[],active=true;

class S{constructor(w,h){this.w=w;this.h=h;this.g=[];for(let y=0;y<h;++y)this.g[y]=new Array(w).fill(0)}
drop(x,y,n=1){while(n>0&&y>=0&&y<this.h&&x>=0&&x<this.w){this.g[y][x]+=n;n=0;if(this.g[y][x]>=g){topples++;for(let d=[-1,0],[1,0],[0,1],[0,-1];d.length;d.splice(1,1)) {let ny=y+d[0],nx=x+d[1];if(ny>=0&&ny<this.h&&nx>=0&&nx<this.w)this.g[ny][nx]++;}this.g[y][x]-=4;}}}
}

function draw(){c.width=c.width;c.height=c.height;const w=c.width,h=c.height,sw=w/(W-1),sh=h/(H-1);for(let y=0;y<H;++y) for(let x=0;x<W;++x){let s=grid[y][x];let c;if(s==0)c='#000';else if(s==1)c='#f00';else if(s==2)c='#0f0';else if(s==3)c='#00f';else c=hsl(0,0,Math.min(s*.1,.9));ctx.fillStyle=c;ctx.fillRect(x*sw,y*sh,sw-1,sh-1);}}
let W=51,H=51;
grid=new S(W,H);

function update(){if(!active)return;grid.drop(Math.floor(W/2),Math.floor(H/2));grainsDropped++;if(grid.g[Math.floor(H/2)][Math.floor(W/2)]>=g)update();else{requestAnimationFrame(update);draw();}
}

window.onload=()=>{
for(let y=0;y<H;++y) for(let x=0;x<W;++x){grid.g[y][x]=0;}grainsDropped=topples=0;document.title=`Abelian Sandpile (${grainsDropped} grains, ${topples} topplings)`;requestAnimationFrame(update);};
</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.
llama3.1:8bERRBLANKFORMAT
Show code
```html
<!DOCTYPE html>
<html>
<head>
    <title>Abelian Sandpiles</title>
    <style>
        canvas {
            border: 1px solid black;
        }
    </style>
</head>
<body>
<script>
const WIDTH = 400;
const HEIGHT = 400;
let grains = Array.from({length: HEIGHT}, () => new Array(WIDTH).fill(0));
let topplings = 0;
let drops = 0;
let animationRunning = true;

function draw() {
    const ctx = canvas.getContext('2d');
    for (let y = 0; y < HEIGHT; y++) {
        for (let x = 0; x < WIDTH; x++) {
            if (grains[y][x] === 4) {
                topple(x, y);
            }
            const color = grains[y][x] % 10;
            ctx.fillStyle = `rgb(${color * 32}, ${color * 64}, ${color * 128})`;
            ctx.fillRect(x, y, 1, 1);
        }
    }
    canvas.height = HEIGHT;
}

function topple(x, y) {
    topplings++;
    if (x > 0 && grains[y][x - 1] < 3) grains[y][x - 1]++;
    if (y > 0 && grains[y - 1][x] < 3) grains[y - 1][x]++;
    if (x < WIDTH - 1 && grains[y][x + 1] < 3) grains[y][x + 1]++;
    if (y < HEIGHT - 1 && grains[y + 1][x] < 3) grains[y + 1][x]++;
    grains[y][x] = 0;
}

function dropGrains() {
    drops++;
    const centerX = Math.floor(WIDTH / 2);
    const centerY = Math.floor(HEIGHT / 2);
    if (animationRunning || grains[centerY][centerX] === 0) {
        grains[centerY][centerX]++;
        requestAnimationFrame(draw);
        setTimeout(dropGrains, 16); // 60 FPS
    }
}

function init() {
    const canvas = document.createElement('canvas');
    canvas.width = WIDTH;
    canvas.height = HEIGHT;
    document.body.appendChild(canvas);

    dropGrains();
}

init();

</script>
</body>
</html>
```
qwen3:14bERRBLANKMINIFIED
Show code
<!DOCTYPE html>
<html><head><title>Abelian Sandpile Simulation</title><style>body{background:#111;display:flex;justify-content:center;align-items:center;margin:0;height:100vh}canvas{border:2px solid #333;}</style></head><body><canvas id="sandpileCanvas"></canvas><script>let C, G, W = 60, H = 60;let grid=Array(W).fill(0).map(()=>Array(H).fill(0));let droppedCount=0;let topplingCount=0;const canvas=document.getElementById('sandpileCanvas');canvas.width=480;canvas.height=480;Cgonstructor=function(){this.ctx=canvas.getContext('2d')};new Cgonstructor();
c=()=>{let x=Math.floor(W/2);let y=Math.floor(H/2);grid[x][y]++}
p=(x,y)=>{if(x<0||x>=W||y<0||y>=H)return false;return true}
r=(x,y)=>Math.max(0, Math.min(4, grid[x][y]))
d=()=>{let x=Math.floor(W/2);let y=Math.floor(H/2);droppedCount++;grid[x][y]++;}
t=()=>{let q=[];for(let i=0;i<W;++i)for(let j=0;j<H;++j){if(grid[i][j]>=4){q.push({x:i,y:j})}}while(q.length>0){const p={x:q.shift().x,y:q.shift().y};if(grid[p.x][p.y]<4)continue;let n=grid[p.x][p.y];n-=4;for(const dir of[[1,0],[ -1,0],[0,1],[0,-1]]){let nx=p.x+dir[0],ny=p.y+dir[1];if(p(nx,ny)){grid[nx][ny]++;q.push({x:nx,y:ny})}}grid[p.x][p.y]=n;topplingCount++;}}
f=(cx)=>{C.fillStyle='#333';C.fillRect(0,0,canvas.width,canvas.height);for(let i=0;i<W;++i){for(let j=0;j<H;++j){const val=grid[i][j];let color='#000';if(val==0)color='#1a1a1a';else if(val==1)color='#555';else if(val==2)color='#888';else if(val==3)color='#aaa';else if(val>=4)color='red';C.fillStyle=color;C.fillRect(i*4,j*4,4,4)}}
rfa=()=>{t();d();f();requestAnimationFrame(rfa);};rfa()</script></body></html>
qwq:32bERROKLIMFORMAT
Show code
<think>

Okay, I need to create a self-contained HTML file that simulates an abelian sandpile on a canvas. The user specified some key requirements: the sandpile should start at the center cell, topple when a cell has 4 or more grains, distribute grains to neighbors, color cells based on their grain count, track the number of grains dropped and topplings, use requestAnimationFrame for smooth animation, and restart with a new random instance when finished. Also, no external libraries and keep it under 7000 characters.

First, I'll structure the HTML with a canvas element. The main part will be the JavaScript code. Let me outline the steps:

1. **Initialize the grid**: The grid should be a 2D array. Since the simulation starts at the center, the grid's size needs to be odd so there's a clear center. Let's pick a size like 201x201 (so the center is at [100][100]). But this might be too big. Wait, 201x201 is 40,401 elements, which might be memory intensive. Maybe a smaller grid, like 101x101? Or even 51x51? Let me think. The user probably expects a decent fractal pattern, so maybe 101x101 is manageable.

2. **Setting up variables**: Need variables for the grid, current grain count, toppling count, and the canvas elements like context, width, height. The canvas should have a size that scales the grid appropriately. Each cell should be a pixel or a few pixels. Let's say each cell is 1x1 pixel to save memory. So the canvas width and height would be grid size in pixels. For a 101x101 grid, the canvas is 101x101px.

Wait, but if each cell is a pixel, then the canvas will be small. Maybe better to have each cell be, say, 2x2 pixels to make it visible. Let me think again. The user wants a fractal pattern, so scaling might be important. Alternatively, just use 1 pixel per cell and let it be. Or maybe 3x3 or 4x4? Hmm.

Alternatively, using the canvas's width and height to be the grid size multiplied by a scale factor. Let's choose 101x101 grid and a scale of 2, so 202x202 canvas. That's manageable.

3. **The sandpile rules**: When a cell has >=4 grains, it topples. Each topple sends one grain to each of the four neighbors. The grains in that cell decrease by 4 (since each of the four neighbors gets one). The toppled cell's count minus 4, and each neighbor gets +1. However, this might cause neighbors to also topple. This is an iterative process, so I need to manage active cells and process them in order.

Wait, the standard approach for abelian sandpiles is to use a queue for cells that need to topple. Since it's abelian, the order doesn't matter. So when a cell topples, it adds its neighbors to the queue if they exceed the threshold. But we have to manage this in a loop until no more cells need to topple.

But in JavaScript, since we're using requestAnimationFrame, which is asynchronous, we can't process all topplings in one frame. Need to handle this incrementally to keep it smooth. So each frame, we process some topplings, then schedule the next frame. This way, the animation isn't blocked by long-running computations.

4. **Grain visualization**: Colors based on the number of grains. Maybe use a color gradient. For example: 0 grains is white, 1 is green, 2 is yellow, 3 is red, 4+ maybe another color. But since the simulation may have cells above 4 temporarily during toppling, but they should stabilize. Wait, actually, after all topplings, all cells should have less than 4. So colors can represent 0-3. Maybe a simple color scheme.

5. **Tracking grains and topplings**: Global variables for total grains dropped and total topplings (each toppling is a cell that toppled once, so each occurrence counts).

6. **Animation loop**: Using requestAnimationFrame. Each frame, first add a grain to the center, then process the topplings. Since adding a single grain each frame may be too slow, but maybe better to drop multiple grains per frame? Or perhaps each frame drops a grain and processes the toppling steps.

Wait, the problem says "keep dropping grains" so perhaps each frame adds a grain and processes the resulting topplings. But to make it smooth, maybe each frame can add a grain and process some topples, but not all, so that the animation doesn't freeze. Alternatively, accumulate grains over time?

Alternatively, each frame, add one grain, then process all possible topples until none left, but that might take too long for large grids. Need to throttle the processing.

Hmm, the problem states that the simulation should "keep dropping grains so the characteristic fractal pattern grows outwards". That suggests that we drop a grain each time, and process all topplings immediately. But doing that in a single frame might be too slow for large grids. So maybe process topplings in chunks.

Another approach: Each frame, drop a grain, then process a certain number of toppling steps. This way, it can run smoothly but takes longer to simulate.

Alternatively, maybe the grid isn't that large. Let's see. Let me think of the code structure.

First, create a grid array. Let's say grid is a 2D array of 101x101, initialized to 0 except the center.

Wait, no, initially all cells except center are 0. Then, each frame, we add 1 to the center (or another position?), then check if it exceeds 4. But the center is where we drop grains each time.

Wait the problem says "drop grains onto the centre cell". So each step adds a grain to the center cell. Then, whenever a cell has 4 or more grains, it topples.

So the process is:

- Add a grain to the center cell (increase its count by 1).

- Check if it's >=4. If so, add it to the topple queue.

- Then, process the queue: for each cell in the queue, if it has >=4, topple it (send 1 to each neighbor, subtract 4), and add any neighbors that now have >=4 to the queue.

But this has to be done until the queue is empty.

But to handle this in requestAnimationFrame without blocking, we need to process a limited number of topplings per frame.

Alternatively, each frame adds a grain and does all possible topplings. If the grid is small enough, this could be manageable. Let's see.

Suppose the grid is 101x101. Each time we add a grain, the center cell might overflow and start a chain reaction. The number of topplings could be proportional to the radius squared, which for a 101x101 grid might be manageable.

Alternatively, maybe using a grid size of 51x51 to keep it smaller.

Let me proceed step by step.

First, in the HTML, we need a canvas and some divs to display the grain count and toppling count.

The JavaScript setup:

- Get the canvas element, set its size.

- Initialize grid as a 2D array. Maybe using a flat array for efficiency? Or a 2D array. Let's see.

Wait, in JavaScript, 2D arrays can be made with grid = Array(rows).fill().map(() => Array(cols).fill(0)); but for speed, maybe a flat array with row*cols + col as index? Not sure. For a grid of 101x101, a 2D array is manageable.

Let me proceed with 101x101 grid.

Canvas size: let's say each cell is 2x2 pixels, so the canvas is 202x202 pixels. That way, the fractal is visible.

The code steps:

Initialize variables:

const width = 202; // or whatever the scaling is.

Wait, maybe better to set the canvas width and height based on grid size. Let me think:

Let grid_size = 101; (odd, center at 50)

canvas width and height = grid_size * scale. Let's pick scale=2 for visibility. So 202x202.

Then, the grid is grid = Array(grid_size).fill().map(() => Array(grid_size).fill(0));

Initialize center at grid[50][50]?

Wait indexes start at 0, so center is at (grid_size-1)/2 =50 for grid_size=101.

Now, the main loop steps:

- Each frame, first add a grain to the center cell (grid[50][50] +=1; grains++).

- Then, check if the center cell is >=4. If so, start the toppling process.

The toppling process uses a queue. The process is:

While the queue is not empty:

- Dequeue a cell (i,j).

- If grid[i][j] >=4:

- Let's calculate how many grains to send. The amount to remove is (grid[i][j] - (grid[i][j] %4))? No, actually, each topple reduces the cell by 4, and each neighbor gets +1.

Wait the standard rule: when a cell has >=4 grains, it gives 1 to each neighbor and loses 4. So:

grid[i][j] -=4;

for each direction (up, down, left, right):

if cell is within grid, add 1 to neighbor.

Then, each of those neighbors might now be >=4, so they need to be added to the queue.

But to prevent processing the same cell multiple times, perhaps each cell that gets a grain is checked and added to the queue if it exceeds 4.

Wait the standard approach is:

When a cell topples, it gives 1 to each of its four neighbors. These neighbors may now have >=4 and need to topple themselves. The process continues until no cells have >=4.

This is done via a queue. So the algorithm is:

Initialize queue with any cell that has >=4 (starting with center after adding the new grain).

Then, while queue not empty:

- Dequeue a cell (i,j).

- If grid[i][j] <4, skip (since maybe it was added to queue before a previous toppling reduced it)

- Else, topple it:

  temp = grid[i][j] -4;

  (the cell now has temp grains left)

  for each direction:

   if neighbor exists (within grid):

    grid[neighbor] +=1

    if neighbor now is >=4, add it to the queue (but check if it's already there?)

  topplings +=1;

Wait but to optimize, we can just add it to the queue again, even if it's already there, but since the queue is processed in order, it's okay as the next time it's dequeued, it will check if it's still over 4.

Hmm, but this could lead to processing the same cell multiple times, but it's okay. It's an abelian process, so the order doesn't matter.

However, in practice, using a queue and processing each cell once per excess grains might be tricky. Alternatively, use a queue and ensure that each cell is only added once, but that's more complex.

Maybe the basic approach is sufficient here.

Now, the problem is implementing this in JavaScript with requestAnimationFrame. Since processing all topplings in one frame might take too long (if the fractal is large and many cells need to topple), we need to process in chunks to keep the animation smooth.

Alternatively, if the grid is small enough (like 101x101), maybe each frame can process all topplings quickly enough.

But let's see: each topple of a cell sends grains to neighbors, which may cause their own topples, etc. The number of topples can be large. For example, when the center reaches 4 and starts spreading out, each layer might require around O(r^2) topples for radius r. So for a grid of 100, that's 10^4 operations, which might be too much per frame.

Thus, to keep it smooth, we should process a limited number of topples each frame. Let's say, every frame, after adding the grain, process, say, 100 topples, then schedule the next frame.

Alternatively, use a flag to indicate whether we're still processing topples, and only add a new grain when the queue is empty.

Wait the problem says "keep dropping grains", so perhaps the grains are added continuously, but each grain addition can cause a chain reaction that must be processed.

Hmm, this is getting a bit complex.

Alternatively, perhaps the code will be structured with a main loop that runs on requestAnimationFrame. Each frame does:

1. If there are grains to add (wait, how often do we add grains? The problem says "keep dropping grains", so every frame? Or every frame, add a grain and process the topples?)

The problem says: "keep dropping grains so the characteristic fractal pattern grows outwards." So perhaps every frame, we drop a grain, then process all topples until none left. But if that takes too long, it will freeze the animation.

Hmm, perhaps the solution is to process a small number of steps each frame. Let's outline the code steps:

Variables:

- grid as a 2D array

- grainsDropped: counter for total grains dropped

- topplingsCount: how many topples have happened.

- queue: array for cells to process.

- isProcessing: flag to indicate if we're in the middle of processing topples.

The main loop (in requestAnimationFrame callback):

if (isProcessing) {

  process a few topples (e.g., 100 steps)

  if queue is empty, then isProcessing = false;

  else continue processing next frame

} else {

  // add a grain to center

  grid[center][center] +=1;

  grainsDropped++;

  // check if center now exceeds 4, add to queue if so.

  if (grid[center][center] >=4) {

    queue = [ [center,center] ]

    isProcessing = true;

  }

  // then process some topples (maybe even process a few here?)

}

Wait but this requires managing the queue and processing in chunks.

Alternatively, the following approach:

Each frame, first add a grain to the center, then process all topples, but if processing takes too long, the animation skips frames. But maybe the grid is small enough.

Alternatively, let me try to code the basic idea, assuming that each frame can process all the topples, and see if it fits the character limit.

Wait the user has a strict limit of 7000 characters. Let's think about code size.

The HTML structure is minimal:

<html><body><canvas id="c"></canvas><div id="stats"></div><script>...</script></body></html>

The script will need to handle the grid, animation, and drawing.

Let me structure the code:

First, canvas setup:

function main() {

   const c = document.getElementById('c');

   const ctx = c.getContext('2d');

   let grid_size = 101;

   const scale = 2; // each cell is 2x2 pixels

   c.width = grid_size * scale;

   c.height = grid_size * scale;

   const center = Math.floor(grid_size / 2);

   let grid = Array(grid_size).fill(0).map(() => Array(grid_size).fill(0));

   let grainsDropped = 0;

   let topplingsCount = 0;

   let queue = [];

   let isProcessing = false;

   // drawing function

   function draw() {

      ctx.clearRect(0,0,c.width,c.height);

      for (let i=0; i<grid_size; i++) {

         for (let j=0; j<grid_size; j++) {

            const cell = grid[i][j];

            let color;

            switch(cell) {

               case 0: color = '#fff'; break;

               case 1: color = '#0f0'; break;

               case 2: color = '#ff0'; break;

               case 3: color = '#f00'; break;

               default: color = '#00f'; // shouldn't happen in stable state

            }

            ctx.fillStyle = color;

            ctx.fillRect(j*scale,i*scale, scale, scale);

         }

      }

      document.getElementById('stats').innerHTML = `Grains: ${grainsDropped}, Topplings: ${topplingsCount}`;

   }

   function step() {

      // Add a grain to center

      grid[center][center]++;

      grainsDropped++;

      // Check if center needs to topple

      const initialCell = [center, center];

      if (grid[center][center] >=4) {

         queue = [ initialCell ];

         processQueue();

      }

      draw();

      requestAnimationFrame(step);

   }

   function processQueue() {

      let q = queue.splice(0); // process queue in current state

      let processed = 0;

      while (q.length >0 && processed < 100) { // limit to 100 steps per frame

         const [i,j] = q.shift();

         if (grid[i][j] <4) continue;

         // topple

         const excess = grid[i][j] -4;

         grid[i][j] -=4;

         topplingsCount++;

         // add to neighbors

         for (let di=-1; di<=1; di+=2) {

            const ni = i + di;

            if (ni >=0 && ni < grid_size) {

               grid[ni][j]++;

               if (grid[ni][j] >=4) {

                  q.push([ni,j]);

               }

            }

         }

         for (let dj=-1; dj<=1; dj+=2) {

            const nj = j + dj;

            if (nj >=0 && nj < grid_size) {

               grid[i][nj]++;

               if (grid[i][nj] >=4) {

                  q.push([i,nj]);

               }

            }

         }

         processed++;

      }

      // if more to process, schedule again

      if (q.length >0) {

         queue = q;

         requestAnimationFrame(processQueue);

      } else {

         queue = [];

      }

   }

   // start

   step();

}

main();

Wait but this code might have errors:

First, in processQueue, the way the queue is handled. Also, the direction loops for neighbors may not be correctly implemented. Also, maybe the queue should be processed in a way that any neighbor that reaches >=4 is added to the queue.

Wait in the code above, when adding neighbor cells, we check if their new value is >=4, and if so, push to q. But since q is a local copy of the queue (spliced from the main queue), this should work, but need to manage the remaining queue.

Hmm, perhaps this approach is getting too complicated. Alternatively, the step function can be written more straightforwardly.

Alternatively, perhaps using a while loop with a limited number of iterations each frame.

But let me see the character count.

The entire code:

The HTML part is small. The JS code needs to be concise.

Perhaps using a grid of size 21x21 instead of 101x101 to reduce the size? Since 101 is 100 cells, but maybe a smaller grid is possible.

Wait the problem doesn't require a specific grid size, just to create the animation. Maybe 21x21 is enough for demonstration.

Alternatively, use a 15x15 grid.

Also, perhaps the code can be minified a bit.

Alternatively, here's a draft:

<script>

const c=document.getElementById`c`,ctx=c.getContext`2d`

let g=[],gs=21,s=2,cn=10

c.width=c.height=gs*s

function r(x,y){return x<0||x>=gs||y<0||y>=gs?0:g[x][y]}

function init(){

g=Array(gs).fill(0).map(_=>Array(gs).fill(0))

document.getElementById`stats`.innerHTML=''

}

function draw(){

ctx.clearRect(0,0,c.width,c.height)

for(let i=0,j,k;i<gs;i++)for(j=0;j<gs;j++){

let v=g[i][j],c='#fff'

switch(v){case 1:c='#0f0';break;case 2:c='#ff0';break;case 3:c='#f00'}

ctx.fillStyle=c;ctx.fillRect(j*s,i*s,s,s)}

document.getElementById('stats').innerHTML=`G:${gsDropped} T:${topplings}`

}

function step(){

gsDropped++

g[cn][cn]++;

let q=[[cn,cn]],t=0

while(q.length&&t++<1e3){

let [x,y]=q.shift()

if(g[x][y]<4)continue

let ov=g[x][y]

g[x][y]-=4

topplings++

for(let d of[-1,1]){

let nx=x+d,ny=y+d

if(nx>=0&&nx<gs)q.push([nx,y]),g[nx][y]++

if(ny>=0&&ny<gs)q.push([x,ny]),g[x][ny]++

}

}

draw()

requestAnimationFrame(step)

}

init()

gsDropped=0;topplings=0

requestAnimationFrame(step)

</script>

Wait this is a very compressed version. It might have errors but gives an idea.

But I need to make sure variables are declared properly and the logic is correct.

Alternatively, perhaps the following:

The problem requires the code to work within 7000 characters. Let's try to write it step by step concisely.

Final code:

```html
<html><body><canvas id=c></canvas><div id=stats></div><script>const c=document.getElementById('c'),ctx=c.getContext('2d');let g=[],gs=31,s=2,cn=15;let grains=0,topps=0;c.width=c.height=gs*s;function draw(){ctx.clearRect(0,0,gs*s,gs*s);for(let i=0;i<gs;i++)for(let j=0;j<gs;j++){let v=g[i][j],c='#fff';if(v==1)c='#0f0';if(v==2)c='#ff0';if(v==3)c='#f00';ctx.fillStyle=c;ctx.fillRect(j*s,i*s,s,s);};document.getElementById('stats').innerHTML=`G:${grains} T:${topps}`}function step(){grains++;g[cn][cn]++;let q=[[cn,cn]],t=0;while(q.length&&t++<1e3){let[x,y]=q.shift();if(g[x][y]<4)continue;topps++;let ov=g[x][y];g[x][y]-=4;for(const d of[-1,1]){let nx=x+d;ny=y;if(nx>=0&&nx<gs){g[nx][y]++;if(g[nx][y]>=4)q.push([nx,y]);}let ny=y+d;if(ny>=0&&ny<gs){g[x][ny]++;if(g[x][ny]>=4)q.push([x,ny]);}}}draw();requestAnimationFrame(step)}function init(){g=Array(gs).fill(0).map(_=>Array(gs).fill(0));step()}init()</script></body></html>
```

But this may have syntax errors. Let me check:

Looking at the direction loops:

for(const d of[-1,1]){ // directions for x and y

nx=x+d; // x direction

ny=y; // unchanged y?

Wait no, perhaps for x direction:

nx = x +d, and y stays, and for y direction:

ny = y +d, and x stays.

Wait the code as written for the loops:

for directions d=-1 and 1:

nx = x+d, and then check if that is in grid. Then increment that neighbor's cell.

Similarly for ny = y+d?

Wait the code has:

for(const d of[-1,1]){ // for each direction difference

    let nx=x+d;ny=y; // move in x direction

    if(nx>=0&&nx<gs){

        g[nx][y]++;

        if(g[nx][y]>=4) q.push([nx,y]);

    }

    let ny=y+d;if(ny>=0&&ny<gs){

        g[x][ny]++;

        if(g[x][ny]>=4) q.push([x,ny]);

    }

Wait the variable 'ny' in the first part is not used, but in the next lines, the variable is redeclared.

Wait the code inside the loop for directions:

for(const d of [-1,1]){

   let nx =x+d, ny =y; // first part: x direction.

   if(nx >=0 && ...) then add to neighbor.

   Then, redeclare ny as y+d: let ny = y+d;

   Then check ny is in bounds and add to y direction.

}

Wait the first 'ny = y' is only in the first line of the loop, but then the next line redefines ny.

This way, for the x-direction:

nx = x +d, y stays.

For y-direction:

ny = y +d, x stays.

Thus, the code is correct for moving in x and y directions.

But in code, the variable names may conflict, but using let inside the loop should localize them.

Hmm, but in JavaScript, the variables are block-scoped. Since the for loop's body is a block, the variables nx and ny in the first part are within that block, but then redeclaring ny again in same block?

Wait no:

The code:

for(...) {

   let nx=x+d, ny=y;

   ... 

   let ny = y+d;

}

This is an error because 'ny' is already declared in this block.

Hence, this would throw an error.

Ah, that's a problem.

Alternative: separate the x and y directions:

for(const d of[-1,1]){

   let nx = x+d;

   if(nx >=0 && nx <gs){

      g[nx][y]++;

      if(g[nx][y] >=4) q.push([nx,y]);

   }

}

for(const d of[-1,1]){

   let ny = y+d;

   if(ny >=0 && ny <gs){

      g[x][ny]++;

      if(g[x][ny]>=4) q.push([x,ny]);

   }

}

But that would loop twice over the directions, which might be redundant.

Alternatively, do them in a single loop but with unique variable names:

for(const d of [-1,1]){

    let nx =x +d;

    if(nx >=0 && nx <gs){

        g[nx][y]++;

        if(g[nx][y]>=4) q.push([nx,y]);

    }

    let ny = y +d;

    if(ny >=0 && ny <gs){

        g[x][ny]++;

        if(g[x][ny]>=4) q.push([x,ny]);

    }

}

This way, nx and ny are separate variables in each iteration of d, so no conflict.

But in the code above, since it's in the same loop, for each d, both x and y directions are handled.

But the variable 'ny' is redeclared in the same block as nx and ny (the first ny was assigned y). But actually, in the first part:

let nx =x+d, ny=y;

Then, after, let ny = y+d.

This is a syntax error.

Thus the code is incorrect.

To fix, the variables should be declared separately:

for(const d of [-1,1]){

    let nx =x +d;

    if(nx >=0 && nx <gs){

        g[nx][y]++;

        if(g[nx][y]>=4) q.push([nx,y]);

    }

}

for(const d of [-1,1]){

    let ny=y+d;

    if(ny >=0 && ny <gs){

        g[x][ny]++;

        if(g[x][ny]>=4) q.push([x,ny]);

    }

}

But this way, it loops through directions twice.

Alternatively, loop over directions for x and y:

for (let dir of [ 'x', 'y' ]) {

   for (const d of [-1,1]) {

      if (dir === 'x') {

          let nx = x+d;

          if (nx >=0 && nx <gs) {

              g[nx][y]++;

              if (g[nx][y] >=4) q.push([nx,y]);

          }

      } else {

          let ny = y+d;

          if (ny >=0 && ny <gs) {

              g[x][ny]++;

              if (g[x][ny] >=4) q.push([x,ny]);

          }

      }

   }

}

But this adds more code.

Alternatively, just separate the x and y:

for (const d of [-1,1]) {

    let nx = x + d;

    if (nx >= 0 && nx < gs) {

        g[nx][y]++;

        if (g[nx][y] >=4) q.push([nx,y]);

    }

}

for (const d of [-1,1]) {

    let ny = y + d;

    if (ny >=0 && ny < gs) {

        g[x][ny]++;

        if (g[x][ny] >=4) q.push([x,ny]);

    }

}

This way, no variable conflicts.

Hence, in the original code, the x and y directions are handled in separate loops.

Thus, in the compressed code, this would be:

for(const d of[-1,1]){let nx=x+d;if(nx>=0&&nx<gs){g[nx][y]++;if(g[nx][y]>=4)q.push([nx,y])}}for(d of[-1,1]){let ny=y+d;if(ny>=0&&ny<gs){g[x][ny]++;if(g[x][ny]>=4)q.push([x,ny])}}

But in the code, the second loop uses 'd' without 'const'?

Wait in the first loop, it's 'const d of', but in the second loop, 'for(d of' would cause d to be a global variable, which is not good.

Hence, to fix:

Second loop should also use 'const d'.

Thus:

for(const d of[-1,1]){ ... }

then again:

for(const d of[-1,1]){ ... }

Thus, in code:

for(const d of[-1,1]){let nx=x+d;if(nx>=0&&nx<gs){g[nx][y]++;if(g[nx][y]>=4)q.push([nx,y])}}for(const d of[-1,1]){let ny=y+d;if(ny>=0&&ny<gs){g[x][ny]++;if(g[x][ny]>=4)q.push([x,ny])}}

Thus, this should work.

Revising the previous code:

The problematic part in step function:

The code inside the while loop:

topps++; // increment topps (topps is the topplings count)

let ov=g[x][y];g[x][y]-=4;

Then for directions:

for(const d of[-1,1]){

let nx=x+d;if(nx>=0&&nx<gs){

g[nx][y]++;

if(g[nx][y]>=4) q.push([nx,y]);

}

}

for(const d of[-1,1]){

let ny=y+d;if(ny>=0&&ny<gs){

g[x][ny]++;

if(g[x][ny]>=4) q.push([x,ny]);

}

}

Hence, the code is correct.

Now, returning to the compressed code:

The initial code had:

for(const d of[-1,1]){let nx=x+d;ny=y;if(nx>=0&&nx<gs)g[nx][y]++,g[nx][y]>=4&&q.push([nx,y]);let ny=y+d;if(ny>=0&&ny<gs)g[x][ny]++,g[x][ny]>=4&&q.push([x,ny]);}

This is not correct because of variable redeclaration.

But if written as two separate loops:

for(const d of[-1,1]){let nx=x+d;if(nx>=0&&nx<gs){g[nx][y]++;if(g[nx][y]>=4)q.push([nx,y])}}for(const d of[-1,1]){let ny=y+d;if(ny>=0&&ny<gs){g[x][ny]++;if(g[x][ny]>=4)q.push([x,ny])}}

This is okay.

So the corrected step function:

function step(){

grains++;g[cn][cn]++;

let q=[[cn,cn]],t=0;

while(q.length && t++<1e3){

let[x,y]=q.shift();

if(g[x][y]<4)continue;

topps++;

let ov=g[x][y];

g[x][y]-=4;

for(const d of[-1,1]){

    let nx=x+d;

    if(nx>=0&&nx<gs){

        g[nx][y]++;

        if(g[nx][y]>=4) q.push([nx,y]);

    }

}

for(const d of[-1,1]){

    let ny=y+d;

    if(ny>=0&&ny<gs){

        g[x][ny]++;

        if(g[x][ny]>=4) q.push([x,ny]);

    }

}

}

draw();

requestAnimationFrame(step);

}

Thus, this should work.

Putting it all together into a minified version:

```html
<html><body><canvas id=c></canvas><div id=stats></div><script>const c=document.getElementById('c'),ctx=c.getContext('2d');let g=[],gs=21,s=2,cn=10;let grains=0,topps=0;c.width=c.height=gs*s;function draw(){ctx.clearRect(0,0,gs*s,gs*s);for(let i=0;i<gs;i++)for(let j=0;j<gs;j++){let v=g[i][j],c='#fff';if(v==1)c='#0f0';if(v==2)c='#ff0';if(v==3)c='#f00';ctx.fillStyle=c;ctx.fillRect(j*s,i*s,s,s)}document.getElementById('stats').innerHTML=`G:${grains} T:${topps}`}function step(){grains++;g[cn][cn]++;let q=[[cn,cn]],t=0;while(q.length&&t++<1e3){let[x,y]=q.shift();if(g[x][y]<4)continue;topps++;let o=g[x][y],ov=o;g[x][y]=ov-4;for(const d of[-1,1]){let nx=x+d;if(nx>=0&&nx<gs){g[nx][y]++;if(g[nx][y]>=4)q.push([nx,y])}}for(const d of[-1,1]){let ny=y+d;if(ny>=0&&ny<gs){g[x][ny]++;if(g[x][ny]>=4)q.push([x,ny])}}}draw();requestAnimationFrame(step)}function init(){g=Array(gs).fill(0).map(_=>Array(gs).fill(0));step()}init()</script></body></html>
```

This might still have issues but is a compressed version. The grid size is 21x21, center at 10,10. The colors are basic.

This should solve the problem.

**Final Answer**
Here's the HTML with embedded JavaScript to create the sandpile animation:

\boxed{
\begin{verbatim}
<!DOCTYPE html>
<html>
<body>
<canvas id="c"></canvas>
<div id="stats"></div>
<script>
const c = document.getElementById('c'), ctx = c.getContext('2d');
let g = [], gs = 21, s = 2, cn = 10;
let grains = 0, topps = 0;
c.width = c.height = gs * s;

function draw() {
    ctx.clearRect(0, 0, gs * s, gs * s);
    for (let i = 0; i < gs; i++) {
        for (let j = 0; j < gs; j++) {
            let v = g[i][j], c = '#fff';
            if (v == 1) c = '#0f0';
            else if (v == 2) c = '#ff0';
            else if (v == 3) c = '#f00';
            ctx.fillStyle = c;
            ctx.fillRect(j * s, i * s, s, s);
        }
    }
    document.getElementById('stats').innerHTML = `Grains: ${grains}, Topplings: ${topps}`;
}

function step() {
    grains++;
    g[cn][cn]++;
    let q = [[cn, cn]], t = 0;
    while (q.length && t++ < 1e3) {
        let [x, y] = q.shift();
        if (g[x][y] < 4) continue;
        topps++;
        let ov = g[x][y];
        g[x][y] = ov - 4;
        for (const d of [-1, 1]) {
            let nx = x + d;
            if (nx >= 0 && nx < gs) {
                g[nx][y]++;
                if (g[nx][y] >= 4) q.push([nx, y]);
            }
        }
        for (const d of [-1, 1]) {
            let ny = y + d;
            if (ny >= 0 && ny < gs) {
                g[x][ny]++;
                if (g[x][ny] >= 4) q.push([x, ny]);
            }
        }
    }
    draw();
    requestAnimationFrame(step);
}

function init() {
    g = Array(gs).fill(0).map(() => Array(gs).fill(0));
    step();
}
init();
</script>
</body>
</html>
\end{verbatim}
}
deepseek-r1:32bBLANKMINIFIEDFORMAT
Show code
```html
<!DOCTYPE html>
<html><body style="background:#000;margin:0;display:flex;flex-direction:column;align-items:center"><canvas id=c width=601 height=601></canvas><div id=s style="color:white"></div><script>let c=document.getElementById('c'),ctx=c.getContext('2d'),w=c.width,h=w,g=Array(w).fill().map(()=>Array(h).fill(0)),x0=w/2|0,y0=h/2|0,drops=0,topples=0;g[x0][y0]=1e3;function draw(){ctx.fillStyle='#000';ctx.fillRect(0,0,w,h);for(let i=0;i<w;i++)for(let j=0;j<h;j++){let v=g[i][j]%4;if(v>0)ctx.fillStyle=`hsl(${v*90},100%,50%)`;else ctx.fillStyle='#000';ctx.fillRect(i,j,1,1)}s.textContent=`Dropped: ${drops}\nToppled: ${topples}`}function step(){let q=[];for(let i=0;i<w;i++)for(let j=0;j<h;j++)if(g[i][j]>=4)q.push([i,j]);if(!q.length){g[x0][y0]++;drops++;let n=g[x0][y0];if(n>=4){topples++;g[x0][y0]=n-4;for(let d of[[-1,0],[1,0],[0,-1],[0,1]]){let x=x0+d[0],y=y0+d[1];if(x>=0&&x<w&&y>=0&&y<h)g[x][y]++}}else return setTimeout(step,1)}else for(let[i,j]of q){topples++;let n=g[i][j]-4;g[i][j]=n;for(let d of[[-1,0],[1,0],[0,-1],[0,1]]){let x=i+d[0],y=j+d[1];if(x>=0&&x<w&&y>=0&&y<h)g[x][y]++}}requestAnimationFrame(draw);requestAnimationFrame(step)}step();</script></body></html>
```
command-r:35bERROKFORMAT
Show code
```html
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Abelian Sandpile</title>
    <style>
        body { margin: 0; }
        canvas { display: block; image-rendering: -moz-crisp-edges; image-rendering: -webkit-crisp-edges; image-rendering: pixelated; image-rendering: crisp-edges; }
    </style>
</head>
<body>
    <canvas></canvas>
    <script>
        let drops = 0, topples = 0;
        const colors = ["#4158D0", "#A43EEB", "#E22CBF", "#FF6161", "#FFA801"];

        function randomInt(min, max) {
            return Math.floor(Math.random() * (max - min + 1)) + min;
        }

        class Grid {
            constructor(size = 400, cellSize = 15) {
                this.grid = new Array(size).fill(null).map(() => new Array(size).fill({ grains: 0 }));
                this.canvas = document.querySelector("canvas");
                this.ctx = this.canvas.getContext("2d");

                this.center = { x: Math.floor(size / 2), y: Math.floor(size / 2) };
                this.cellSize = cellSize;
            }

            draw() {
                this.canvas.width = this.grid.length * this.cellSize;
                this.canvas.height = this.grid[0].length * this.cellSize;

                for (let y=0; y<this.grid[0].length; ++y) {
                    for (let x=0; x<this.grid.length; ++x) {
                        const cell = this.grid[x][y];
                        if (cell.grains > 0) {
                            this.ctx.fillStyle = colors[cell.grains - 1] || "black";
                            this.ctx.fillRect(x * this.cellSize, y * this.cellSize, this.cellSize, this.cellSize);
                        }
                    }
                }
            }

            topple() {
                for (let x=0; x<this.grid.length; ++x) {
                    for (let y=0; y<this.grid[0].length; ++y) {
                        const cell = this.grid[x][y];

                        if (cell.grains >= 4 && !cell.toppling) {
                            --cell.grains;
                            topples++;

                            [
                                [x-1, y], [x+1, y],
                                [x,   y-1], [x,   y+1]
                            ]
                            .filter(([cx, cy]) => cx >= 0 && cy >= 0 && this.grid[cx]?.[cy] !== null) // in bounds check
                            .forEach(([cx, cy]) => {
                                const c = this.grid[cx][cy];

                                if (c.grains < 4) {
                                    ++c.grains;

                                    if (!c.toppling && !cell.topplingFinished) {
                                        cell.topplingFinished = true; // no more than one topple per cycle
                                        requestAnimationFrame(() => this.topple());
                                    }
                                }

                                c.toppling = true;
                            });

                            cell.toppling = false;
                        }
                    }
                }
            }

            drop() {
                ++drops;

                if (this.grid[this.center.x][this.center.y].grains < 4) {
                    this.grid[this.center.x][this.center.y].grains++;

                    requestAnimationFrame(() => {
                        this.topple();
                        this.draw();

                        if (drops > 1000 && topples >= drops - 500) {
                            // restart
                            window.location.reload();
                        } else {
                            this.drop();
                        }
                    });
                }
            }

            start() {
                this.drop();
                setInterval(() => requestAnimationFrame( ()=>this.draw()), Math.random() * 30 + 1);
            }
        }

        new Grid().start();
    </script>
</body>
</html>
```
Outside the Brain cluster — same task, same character budget, collected through their own tooling (response times not comparable)
GPT-5.5 (Codex)OKMINIFIED
Show code
<!doctype html><html><head><meta charset=utf-8><meta name=viewport content="width=device-width,initial-scale=1"><title>Sandpile</title><style>html,body{margin:0;height:100%;overflow:hidden;background:#07080a;color:#dfe7ee;font:14px system-ui,sans-serif}canvas{display:block;width:100vw;height:100vh}#h{position:fixed;left:14px;top:12px;background:#0b1018cc;border:1px solid #263142;border-radius:8px;padding:10px 12px;line-height:1.45;backdrop-filter:blur(8px);box-shadow:0 8px 30px #0008}.n{font-variant-numeric:tabular-nums;color:#fff}</style></head><body><canvas id=c></canvas><div id=h>Grains <span id=g class=n>0</span><br>Topplings <span id=t class=n>0</span></div><script>
let c=document.getElementById("c"),x=c.getContext("2d"),G=document.getElementById("g"),T=document.getElementById("t"),w,h,n,a,b,q,qi,drops,tops,done,scale,pal;
function init(){w=innerWidth;h=innerHeight;devicePixelRatio>1?(c.width=w*2,c.height=h*2,x.setTransform(2,0,0,2,0,0)):(c.width=w,c.height=h,x.setTransform(1,0,0,1,0,0));n=Math.max(81,Math.min(321,Math.floor(Math.min(w,h)/3)|1));scale=Math.max(1,Math.ceil(Math.max(w,h)/n));n=(Math.ceil(Math.max(w,h)/scale)+3)|1;if(!(n&1))n++;a=new Uint8Array(n*n);b=new Uint8Array(n*n);q=new Int32Array(n*n*4);qi=0;drops=0;tops=0;done=0;pal=["#061016","#1e5f7a","#f2c94c","#f05a28"];x.fillStyle="#07080a";x.fillRect(0,0,w,h)}
function push(i){if(!b[i])b[i]=1,q[qi++]=i}
function step(){let m=(n>>1)*n+(n>>1),batch=4+Math.min(180,1+drops/220|0);for(let k=0;k<batch;k++){a[m]++;drops++;if(a[m]>3)push(m)}let lim=26000;while(qi&&lim--){let i=q[--qi];b[i]=0;if(a[i]<4)continue;let v=a[i]>>2;a[i]&=3;tops+=v;let r=i/n|0,col=i-r*n;if(r<2||col<2||r>n-3||col>n-3){done=1;continue}let u=i-n,d=i+n,l=i-1,rr=i+1;a[u]+=v;a[d]+=v;a[l]+=v;a[rr]+=v;if(a[u]>3)push(u);if(a[d]>3)push(d);if(a[l]>3)push(l);if(a[rr]>3)push(rr)}if(done)init()}
function draw(){let ox=(w-n*scale)/2,oy=(h-n*scale)/2;x.fillStyle="#07080a";x.fillRect(0,0,w,h);for(let r=0,i=0;r<n;r++)for(let col=0;col<n;col++,i++){let v=a[i];if(v){x.fillStyle=pal[v&3];x.fillRect(ox+col*scale,oy+r*scale,scale,scale)}}G.textContent=drops.toLocaleString();T.textContent=tops.toLocaleString()}
function loop(){for(let i=0;i<3;i++)step();draw();requestAnimationFrame(loop)}addEventListener("resize",init);init();loop();
</script></body></html>
Show code
<!DOCTYPE html><html><head><meta charset=UTF-8><title>Sandpile</title>
<style>html,body{margin:0;background:#0b0b0f;overflow:hidden;height:100%}
canvas{display:block;margin:0 auto;image-rendering:pixelated}
#i{position:fixed;top:10px;left:10px;color:#ddd;font:14px monospace;text-shadow:0 0 4px #000}</style>
</head><body>
<div id=i></div><canvas id=c></canvas>
<script>
var cv=document.getElementById('c'),ctx=cv.getContext('2d'),info=document.getElementById('i');
var N,cs,g,cx,cy,drops,topples,queue,maxDrops;
var colors=['#0b0b0f','#2ecc71','#3aa0ff','#ffd23a','#ff3a3a'];
function idx(r,c){return r*N+c}
function init(){
 N=61+2*(20+Math.floor(Math.random()*40));
 cs=Math.max(2,Math.floor(720/N));
 cv.width=cv.height=cs*N;
 g=new Int32Array(N*N);
 cx=(N-1)/2|0;cy=cx;
 drops=0;topples=0;queue=[];
 maxDrops=N*N*3;
}
init();
function topple(r,c){
 var i=idx(r,c);g[i]-=4;topples++;
 if(r>0)bump(r-1,c);
 if(r<N-1)bump(r+1,c);
 if(c>0)bump(r,c-1);
 if(c<N-1)bump(r,c+1);
}
function bump(r,c){
 var i=idx(r,c);g[i]++;
 if(g[i]>=4)queue.push(i);
}
function addGrain(){
 var i=idx(cy,cx);g[i]++;drops++;
 if(g[i]>=4)queue.push(i);
}
function processQueue(limit){
 var n=0;
 while(queue.length&&n<limit){
  var i=queue.pop();
  if(g[i]>=4){topple((i/N)|0,i%N);n++;}
 }
}
function draw(){
 ctx.fillStyle='#0b0b0f';
 ctx.fillRect(0,0,cv.width,cv.height);
 for(var r=0;r<N;r++){
  for(var c=0;c<N;c++){
   var v=g[idx(r,c)];
   if(v<=0)continue;
   ctx.fillStyle=colors[v>4?4:v];
   ctx.fillRect(c*cs,r*cs,cs-1,cs-1);
  }
 }
 info.textContent='Grains dropped: '+drops+'   Topplings: '+topples+'   Grid: '+N+'x'+N;
}
function frame(){
 if(drops<maxDrops){
  for(var k=0;k<8;k++)addGrain();
 }
 processQueue(8000);
 if(drops>=maxDrops&&queue.length===0){
  init();
 }
 draw();
 requestAnimationFrame(frame);
}
requestAnimationFrame(frame);
</script>
</body></html>
Show code
<!DOCTYPE html>
<html>
<head><meta charset="utf-8"><meta name="viewport" content="width=device-width"><style>body{margin:0;background:#000;display:flex;justify-content:center;align-items:center;height:100vh}canvas{border:1px solid #333}#i{position:absolute;top:10px;left:10px;color:#fff;font-family:monospace;font-size:12px}</style></head>
<body>
<canvas id="c"></canvas>
<div id="i">Grains: <span id="g">0</span> | Topplings: <span id="t">0</span></div>
<script>
const c=document.getElementById('c'),ctx=c.getContext('2d');
c.width=500;c.height=500;
const sz=2;const w=250,h=250;let grid=new Uint8Array(w*h),grains=0,topplings=0;
function f(){grains++;grid[(h>>1)*w+(w>>1)]+=4;let changed=1;while(changed){changed=0;for(let y=0;y<h;y++){for(let x=0;x<w;x++){const idx=y*w+x;if(grid[idx]>=4){topplings++;const g=grid[idx]>>2;grid[idx]&=3;if(y>0)grid[(y-1)*w+x]+=g;if(y<h-1)grid[(y+1)*w+x]+=g;if(x>0)grid[y*w+(x-1)]+=g;if(x<w-1)grid[y*w+(x+1)]+=g;changed=1;}}}}
ctx.fillStyle='#000';ctx.fillRect(0,0,c.width,c.height);for(let y=0;y<h;y++){for(let x=0;x<w;x++){const g=grid[y*w+x];if(g>0){const hue=[240,180,60,0][Math.min(g,3)];ctx.fillStyle=`hsl(${hue},80%,50%)`;ctx.fillRect(x*sz,y*sz,sz,sz);}}}
document.getElementById('g').textContent=grains;document.getElementById('t').textContent=topplings;requestAnimationFrame(f);}
f();
</script>
</body>
</html>
Results at a glance— every model in this round with score, response time and status.

Who wrote what

ModelBlind scoreLatencyStatus
mistral-small:24b 157.5 sOK
qwen3.5:9b 38.7 sOK
qwen3-coder:30b 23.1 sOK
gemma4:26b harness
deepseek-r1:14b 65.2 sOK
qwen3:8b harness
llama3.1:8b 130.7 sOK
qwen3:14b 32.3 sOK
qwq:32b 288.6 sOK
deepseek-r1:32b 106.8 sOK
command-r:35b 29.3 sOK