OverviewCode › R100
Code · blind round

R100 · Sankey flow with travelling particles

blind round 4/8 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 8 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 renders a Sankey-style flow diagram on a <canvas>: three source nodes feed four target nodes through bands whose thickness is proportional to the flow, drawn as smooth curves. Send small particles travelling along each band at a rate matching its flow, so the busiest connections visibly carry more traffic. Let the flow values drift over time and show the total throughput. 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)
qwen3:14bOKLIMFORMAT
Show code
```html
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Dynamic Sankey Flow Simulation</title>
    <style>
        body { margin: 0; overflow: hidden; background-color: #111; }
        canvas { display: block; }
    </style>
</head>
<body>
    <canvas id="sankeyCanvas"></canvas>
    <script>
        const canvas = document.getElementById('sankeyCanvas');
        const ctx = canvas.getContext('2d');

        let W, H;
        let nodesS = [100, 300, 500]; // Source X coordinates
        let nodesT = [700, 600, 400, 200]; // Target X coordinates
        const numSources = nodesS.length;
        const numTargets = nodesT.length;

        // Initialize flow matrix: flows[s][t]
        let flows = [];
        for (let s = 0; s < numSources; s++) {
            flows[s] = new Array(numTargets).fill(Math.random() * 5 + 1); // Initial random flow
        }

        // Particle tracking data structure: particles[s][t] = [{x, y, speed}]
        let particles = [];
        for (let s = 0; s < numSources; s++) {
            particles[s] = [];
            for (let t = 0; t < numTargets; t++) {
                particles[s][t] = [];
            }
        }

        // Constants for visualization
        const NODE_Y_POS = 150; // Y position for all nodes
        const BAND_MAX_WIDTH = 40;
        const PARTICLE_COUNT = 60;

        function resizeCanvas() {
            W = window.innerWidth;
            H = window.innerHeight;
            canvas.width = W;
            canvas.height = H * 0.85;
        }
        window.addEventListener('resize', () => {
            resizeCanvas();
            // Re-initialize positions if needed, but for simplicity, we rely on scale
        });

        function initializeParticles() {
            for (let s = 0; s < numSources; s++) {
                for (let t = 0; t < numTargets; t++) {
                    const flowRate = flows[s][t];
                    // Initialize particles spread across the band's path
                    particles[s][t] = [];
                    for (let i = 0; i < PARTICLE_COUNT; i++) {
                        particles[s][t].push({
                            x: nodesS[s] + Math.random() * 10 - 5, 
                            y: NODE_Y_POS + Math.random() * 20 - 10, 
                            flowRate: flowRate, // Store the base rate
                            progress: Math.random(), // Used for tracking along the path
                        });
                    }
                }
            }
        }

        function updateFlows(deltaTime) {
            for (let s = 0; s < numSources; s++) {
                for (let t = 0; t < numTargets; t++) {
                    // Drift the flow: slow random walk
                    const fluctuation = (Math.random() - 0.5) * 2 * deltaTime * 0.1;
                    let newFlow = flows[s][t] + fluctuation;

                    // Clamp flow to reasonable positive values
                    flows[s][t] = Math.max(0.1, Math.min(10, newFlow));
                }
            }
        }

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

            for (let s = 0; s < numSources; s++) {
                for (let t = 0; t < numTargets; t++) {
                    const flow = flows[s][t];
                    if (flow < 0.5) continue; // Don't draw tiny connections

                    // Calculate width proportional to flow, clamped for visualization
                    const thickness = Math.min(BAND_MAX_WIDTH, flow * 3);
                    ctx.fillStyle = `rgba(${Math.floor(255 - (s*40 + t*30))}, ${Math.floor(100 + s*30)}, ${Math.floor(50 + t*40)}, 0.6)`;

                    // Simplified curved drawing: uses a trapezoidal approximation
                    ctx.beginPath();
                    let startX = nodesS[s];
                    let endX = nodesT[t];
                    let yBase = NODE_Y_POS;

                    // Start at source (left)
                    ctx.moveTo(startX, yBase - thickness / 2);
                    ctx.lineTo(startX + 50, yBase - thickness / 2); // Small offset for curve start
                    
                    // Draw a smooth path/curve approximation towards the target
                    // This is highly simplified to meet character limits while conveying flow volume change
                    const cp1x = Math.min(W * 0.6, startX + (endX - startX) * 0.5);
                    const cp1y = yBase;

                    ctx.bezierCurveTo(cp1x, yBase - thickness / 2, 
                                      Math.max(startX, endX) - 50, yBase - thickness / 2, 
                                      endX, yBase - thickness / 2); // Top edge to target

                    ctx.lineTo(endX, yBase + thickness / 2); // Target vertical drop
                    ctx.bezierCurveTo(Math.max(startX, endX) - 50, yBase + thickness / 2, cp1x, yBase + thickness / 2, startX + 50, yBase + thickness / 2); // Bottom edge curve back

                    ctx.closePath();
                    ctx.fill();
                }
            }
        }

        function updateParticles(deltaTime) {
            for (let s = 0; s < numSources; s++) {
                for (let t = 0; t < numTargets; t++) {
                    const flowRate = flows[s][t];
                    if (flowRate < 0.5) continue;

                    // The speed of particles is proportional to the current flow rate
                    const speedFactor = Math.max(0.1, flowRate * 0.3); 

                    for (let i = 0; i < particles[s][t].length; i++) {
                        const p = particles[s][t][i];
                        // Update progress based on time and proportional speed
                        p.progress += speedFactor * deltaTime * 0.05;

                        if (p.progress > 1) {
                            // Reset particle if it passed the target area
                            p.progress = 0; 
                            p.x = nodesS[s] + Math.random() * 10 - 5;
                            p.y = NODE_Y_POS + Math.random() * 20 - 10;
                        } else {
                            // Simple linear interpolation for movement along the x-axis, scaled by progress
                            const startX = nodesS[s];
                            const endX = nodesT[t];
                            p.x = startX + (endX - startX) * p.progress;

                            // Keep Y slightly jittery for visual appeal
                            p.y = NODE_Y_POS + Math.sin(p.progress * 10 + i) * 5;
                        }
                    }
                }
            }
        }

        function drawParticles() {
             ctx.fillStyle = '#ADD8E6'; // Light blue particles
             for (let s = 0; s < numSources; s++) {
                for (let t = 0; t < numTargets; t++) {
                    const pList = particles[s][t];
                    if (!pList) continue;

                    for (const p of pList) {
                        ctx.beginPath();
                        ctx.arc(p.x, p.y, 2, 0, Math.PI * 2);
                        ctx.fill();
                    }
                }
            }
        }

        let lastTime = 0;
        function animate(timestamp) {
            const deltaTime = timestamp - lastTime || 16;
            lastTime = timestamp;

            updateFlows(deltaTime / 16); // Normalize time delta (assuming 60fps base)
            drawBands();
            updateParticles(deltaTime / 16);
            drawParticles();

            requestAnimationFrame(animate);
        }

        // Start the simulation
        resizeCanvas();
        initializeParticles();
        requestAnimationFrame(animate);
    </script>
</body>
</html>
```
qwen3-coder:30bERRBLANKFORMAT
Show code
```html
<!DOCTYPE html>
<html>
<head><title></title></head>
<body><canvas id="sankey"></canvas><script>
const c=document.getElementById('sankey');
const ctx=c.getContext('2d');
let W=800,H=500;
c.width=W;c.height=H;

// Node definitions (fixed positions for simplicity)
const SOURCES={x:[100, 300, 500],y:H*0.2};
const TARGETS={x:[100, 600, 700, 900],y:H*0.8};

// Connections (Source Index -> Target Index)
let connections=[[0,0],[0,1],[1,1],[1,2],[2,2],[2,3]];

// Flow state and particle tracking
let flows={};
let particles=[];
const MAX_PARTICLES=50;
const FLOW_DAMPING=0.98;
const FLOW_BOOST=0.05;
let totalFlow=0;

function init() {
    flows={};
    particles=[];
    totalFlow=0;
    connections.forEach(([si, ti]) => {
        let key=`${si}-${ti}`;
        // Initialize flow to a random positive value (1-5 units)
        flows[key]=Math.random() * 4 + 1; 
    });
}

function updateFlows() {
    let newTotal=0;
    for(const key in flows){
        let currentF=flows[key];
        // Simulate flow drift: random change applied, then damped
        let delta=Math.random() - 0.5;
        flows[key]=currentF*FLOW_DAMPING + (delta * 0.1);
        if(Math.abs(flows[key]) < 0.1) flows[key] = Math.max(0.1, Math.sign(flows[key])*0.1);

        // Add a small boost proportional to the current flow magnitude
        flows[key]+=Math.random() * FLOW_BOOST;
        if(flows[key]>8){flows[key]=8};
        newTotal+=flows[key];
    }
    totalFlow=Math.min(10, Math.max(2, newTotal/5));
}

function getBandWidth(si, ti) {
    const flow=flows[`${si}-${ti}`]||0;
    // Width is proportional to the square root of the flow for visual effect
    return 4 + (Math.pow(flow,1.2)*0.5); 
}

function drawBands() {
    ctx.clearRect(0,0,W,H);
    ctx.fillStyle="#333";
    
    connections.forEach(([si, ti])=>{
        const key=`${si}-${ti}`;
        let width=getBandWidth(si, ti);
        let x1=SOURCES.x[si]; let y1=SOURCES.y;
        let x2=TARGETS.x[ti]; let y2=TARGETS.y;

        // Using a simple Bezier curve approximation for the band shape
        ctx.beginPath();
        ctx.moveTo(x1, y1);
        ctx.quadraticCurveTo((x1+x2)/2, (y1+y2)/2 - 50, x2, y2);
        ctx.lineTo(x2 + width*0.8, y2); // Simple rectangular approximation for the flow volume
        ctx.quadraticCurveTo((x1+x2)/2, (y1+y2)/2 - 50, x1 - width*0.8, y1);
        ctx.closePath();
        
        let alpha=Math.min(1.0, Math.max(0.3, flow/5)); // Color intensity based on flow
        ctx.fillStyle=`rgba(60, 140, 250, ${alpha})`;
        ctx.fill();

        // Store band geometry info for particle tracking (simplified)
        this.bandData = {x1: x1, y1: y1, x2: x2, y2: y2, w: width, flow: flow};
    });
}

function spawnParticle(si, ti){
    if(particles.length >= MAX_PARTICLES) return;
    
    const bandData={x1: SOURCES.x[si], y1: SOURCES.y, x2: TARGETS.x[ti], y2: TARGETS.y, w: getBandWidth(si, ti), flow: flows[`${si}-${ti}`]};

    if (bandData.flow < 0.5) return; // Don't spawn if flow is too low
    
    particles.push({
        x: bandData.x1, y: bandData.y1, 
        targetX: bandData.x2, targetY: bandData.y2, 
        width: bandData.w * 0.8, // Particle width slightly smaller than band
        speedFactor: Math.min(1.5, bandData.flow/3), // Speed proportional to flow
        progress: 0,
        startFlow: bandData.flow
    });
}

function updateParticles() {
    for(let i=particles.length-1; i>=0; i--){
        let p=particles[i];
        p.progress += p.speedFactor * 0.02; // Advance progress
        
        // Simple linear interpolation for movement
        let t = Math.min(1, p.progress);
        p.x = p.x + (p.targetX - p.x) * (1-t)*0.1 + (p.targetX - p.x) * 0.1*t; // Smoother movement curve approximation
        p.y = p.y + (p.targetY - p.y) * (1-t)*0.1 + (p.targetY - p.y) * 0.1*t;

        if(t > 1.05){ // Finished crossing or overshot
            particles.splice(i, 1);
            continue;
        }
    }
}

function drawParticles() {
    ctx.fillStyle="#FFD700";
    const p_spacing = 20 / Math.max(1, totalFlow*0.5 + 1); // More particles on higher flow
    
    particles.forEach((p, i)=>{
        // Use particle index/flow to slightly vary size/opacity
        let alpha=Math.min(1.0, p.startFlow / 4);
        ctx.globalAlpha=alpha;
        ctx.beginPath();
        ctx.arc(p.x, p.y, Math.max(2, Math.sin((i*0.1)+Date.now()*0.0005)*3)*0.5 + 1, 0, Math.PI * 2);
        ctx.fill();
    });
}

function loop(){
    updateFlows();
    drawBands();
    
    // Particle spawning: Spawn new particles proportional to flow volume
    const spawnRate=Math.floor(totalFlow*10 + 5);
    for(let i=0;i<spawnRate;++i){
        // Randomly pick a connection and try to spawn
        const [si, ti] = connections[Math.floor(Math.random()*connections.length)];
        spawnParticle(si, ti);
    }

    updateParticles();
    drawParticles();

    requestAnimationFrame(loop);
}

init();
loop();
</script></body></html>
```
No usable answer reached the harness (TIMEOUT). Counted as a did-not-finish.
gemma4:26bharness
No answer reached us (TIMEOUT) — 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.
No usable answer reached the harness (TIMEOUT). Counted as a did-not-finish.
deepseek-r1:14bERRBLANKFORMAT
Show code
```html
<!DOCTYPE html>
<html><body style="margin:0;background:#1a1a1a">
<canvas id="c"></canvas>
<script>
const canvas = document.getElementById('c');
const ctx = canvas.getContext('2d');
let width=800,height=600;
canvas.width=width;canvas.height=height;

const sources = [{x:150,y:100},{x:150,y:300},{x:150,y:500}];
const targets = [{x:600,y:125},{x:600,y:325},{x:600,y:475},{x:600,y:625}];
const connections=[];

class Connection {
  constructor(s,t,f) {
    this.s=s;this.t=t;
    this.flow=f;
    this.points=[];this.width=flowMax? f/flowMax*10 : 0;
    for(let i=0;i<8;i++) {
      let p = {x:0,y:0};
      this.points.push(p);
    }
    this.particle = {x:s.x,y:s.y,dirX:t.x-s.x,dirY:t.y-s.y,
                     speed:(Math.random()+1)*flowSpeed,faded:false};
  }

  draw() {
    ctx.beginPath();
    for(let i=0;i<this.points.length-1;i++) {
      let p1 = this.points[i];
      let p2 = this.points[i+1];
      if(i==0) {
        ctx.moveTo(p1.x,p1.y);
      } else {
        ctx.bezierCurveTo(
          this.points[i-1].x,this.points[i-1].y,
          p1.x,p1.y,
          p2.x,p2.y
        );
      }
    }
    ctx.strokeStyle = '#4CAF50';ctx.lineWidth=this.width;
    if(!this.faded) {ctx.stroke();} //Fade after first frame to allow full width drawing

    //Draw particles
    const scale=3;let dx=scale*(t.x-s.x),dy=scale*(t.y-s.y);
    for(let p of this.particles) {
      let dist = Math.sqrt((p.x-t.x)*(p.x-t.x)+(p.y-t.y)*(p.y-t.y));
      if(dist <50) { //Arrived
        connections.find(c => c.t==t).particles.push(p);
      } else {
        p.x += (dx)*p.speed;
        p.y += (dy)*p.speed;
      }
      ctx.fillStyle='#F44336';ctx.beginPath();ctx.arc(p.x,p.y,1,0,Math.PI*2);ctx.fill();
    }

    //Update points
    for(let i=0;i<this.points.length-1;i++) {
      let p = this.points[i];
      if(i==0) {p.x=s.x;p.y=s.y;} 
      else {p.x += (targets.find(t => t.y == this.points[i+1].y).x - s.x)/60;
            p.y += (targets.find(t => t.y == this.points[i+1].y).y - s.y)/60;}
    }
  }
}

let flowMax=0,flowSpeed=2;
const connections = sources.flatMap(s =>
  targets.map(t => {
    const f = Math.random()*300;
    if(f>flowMax) flowMax=f;
    return new Connection(s,t,f);
  })
);

function init() {
  ctx.fillStyle='#1a1a1a';ctx.fillRect(0,0,width,height);
  connections.forEach(c => c.points[0] = {x:c.s.x,y:c.s.y});
}

let totalFlow=connections.flatMap(c=>c.flow).reduce((a,b)=>a+b,0);

function animate() {
  ctx.fillStyle='rgba(26,26,26,0.05)';ctx.fillRect(0,0,width,height);
  
  connections.forEach(c => {
    c.width = c.flow/flowMax*10;
    c.points.forEach(p => {
      if(p!=c.points[0]) {
        p.x += (targets.find(t => t.y == c.t.y).x - c.s.x)/60;
        p.y += (targets.find(t => t.y == c.t.y).y - c.s.y)/60;
      }
    });
  });

  connections.forEach(c => {
    if(!c.faded) {
      ctx.beginPath();
      for(let i=0;i<c.points.length-1;i++) {
        if(i==0) {
          ctx.moveTo(c.points[i].x,c.points[i].y);
        } else {
          ctx.bezierCurveTo(
            c.points[i-1].x,c.points[i-1].y,
            c.points[i].x,c.points[i].y,
            c.points[i+1].x,c.points[i+1].y
          );
        }
      }
    }
  });

  connections.forEach(c => {
    ctx.strokeStyle = '#4CAF50';ctx.lineWidth=c.width;
    if(!c.faded) {ctx.stroke();}
  });

  totalFlow=connections.flatMap(c=>c.flow).reduce((a,b)=>a+b,0);
  
  ctx.fillStyle='white';ctx.font='20px Arial';
  ctx.fillText(`Total Flow: ${totalFlow.toFixed(1)}`, 50,50);

  flowMax *= Math.pow(0.998,1); //Gradually lower max to allow new connections
  if(Math.random()>0.99999) {
    init();
  }

  requestAnimationFrame(animate);
}

init();animate();

//Particles
connections.forEach(c => {
  c.particles = [];
});
</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.5:9bOKFORMAT
Show code
```html
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Flow Dynamics</title>
    <style>body {margin: 0;overflow: hidden;background: #111}canvas {display: block;}</style>
</head>
<body>
<canvas id="sankeyCanvas"></canvas>
<script>
const canvas = document.getElementById('sankeyCanvas');
const ctx = canvas.getContext('2d');

let W, H;
let SourceNodes = [
    { name: "A", x: 0.1, y: 0.33 },
    { name: "B", x: 0.5, y: 0.16 },
    { name: "C", x: 0.9, y: 0.33 }
];
let TargetNodes = [
    { name: "D", x: 0.1, y: 0.75 },
    { name: "E", x: 0.3, y: 0.82 },
    { name: "F", x: 0.6, y: 0.9 },
    { name: "G", x: 0.9, y: 0.75 }
];

// Connections: [SourceIndex][TargetIndex] -> FlowValue (A->D, A->E, ..., C->G)
let Flows = [];
const NUM_SOURCES = SourceNodes.length;
const NUM_TARGETS = TargetNodes.length;

function init() {
    W = canvas.width = window.innerWidth;
    H = canvas.height = window.innerHeight;

    Flows = Array(NUM_SOURCES).fill(0).map(() => Array(NUM_TARGETS).fill(Math.random()));
}

window.addEventListener('resize', () => {
    W = canvas.width = window.innerWidth;
    H = canvas.height = window.innerHeight;
});

function updateFlows() {
    for (let i = 0; i < NUM_SOURCES; i++) {
        for (let j = 0; j < NUM_TARGETS; j++) {
            // Small random drift, ensuring positive flow and keeping total somewhat stable
            let change = (Math.random() - 0.5) * 0.1;
            let currentFlow = Flows[i][j];
            let newFlow = Math.max(0.01, currentFlow + change);
            // Clamp to reasonable bounds
            newFlow = Math.min(2.5, newFlow);
            Flows[i][j] = newFlow;
        }
    }
}

class Particle {
    constructor(flowIndex) {
        this.flowIndex = flowIndex; // [srcIdx, tgtIdx]
        this.path = this.generatePath();
        this.progress = Math.random() * 2 * Math.PI; // Start randomized
        this.speedFactor = Flows[Math.floor(this.flowIndex[0])][Math.floor(this.flowIndex[1])] / 1.5;
    }

    generatePath() {
        const [src, tgt] = this.flowIndex;
        const s = SourceNodes[src];
        const t = TargetNodes[tgt];
        // Use a simple Bezier curve approximation for the path coordinates
        return [
            { x: s.x * W, y: s.y * H }, // P0 (Source)
            { x: s.x * W + 0.5 * W, y: s.y * H - 0.2 * H }, // C1 (Control 1)
            { x: t.x * W + 0.5 * W, y: t.y * H + 0.2 * H }, // C2 (Control 2)
            { x: t.x * W, y: t.y * H }  // P3 (Target)
        ];
    }

    getPoint() {
        const p = this.path;
        let t = Math.cos(this.progress); // Use cosine for smooth movement along a conceptual path length

        // Simple interpolation based on time/progress, simulating flow progression
        // We use the distance from P0 to calculate progress factor (t)
        let ptX = p[0].x + (p[3].x - p[0].x) * t;
        let ptY = p[0].y + (p[3].y - p[0].y) * t;

        return { x: Math.min(W, Math.max(0, ptX)), y: Math.min(H, Math.max(0, ptY)) };
    }

    update() {
        // Update progress based on flow rate and time step
        this.progress += (Math.sin(Date.now() / 1000) * 0.005 + this.speedFactor * 0.005);
    }
}

let ParticlesList = [];
const MAX_PARTICLES = 80;

function initializeParticles() {
    ParticlesList = [];
    for (let i = 0; i < NUM_SOURCES; i++) {
        for (let j = 0; j < NUM_TARGETS; j++) {
            // Create particles proportional to flow. Max particles per link is 3.
            const count = Math.floor(Flows[i][j] * 2);
            for (let k = 0; k < Math.min(count, 3); k++) {
                ParticlesList.push(new Particle([i, j]));
            }
        }
    }
    // Ensure we don't exceed the cap
    if (ParticlesList.length > MAX_PARTICLES) {
         ParticlesList = ParticlesList.slice(0, MAX_PARTICLES);
    }
}

function drawBands() {
    ctx.fillStyle = 'rgba(255, 255, 255, 0.1)';
    for (let i = 0; i < NUM_SOURCES; i++) {
        for (let j = 0; j < NUM_TARGETS; j++) {
            const flow = Flows[i][j];
            if (flow === 0) continue;

            // Calculate thickness: proportional to flow, clamped
            let widthRatio = Math.min(1, flow / 2);
            let width = 5 + widthRatio * 30;
            let heightFactor = 0.8; // Scaling factor for visualization area

            ctx.beginPath();
            // Path drawing approximation (simple rectangle/curve based on start/end points)
            const s = SourceNodes[i];
            const t = TargetNodes[j];

            // Start point: (s.x*W, s.y*H)
            ctx.moveTo(s.x * W, s.y * H);
            // End point: (t.x*W, t.y*H)
            ctx.lineTo(t.x * W, t.y * H);

            // Use a simplified Bezier path approximation for the visual band
            ctx.bezierCurveTo(
                s.x * W + 50 * widthRatio, s.y * H - 20, // Control point 1 (offset)
                t.x * W - 50 * widthRatio, t.y * H + 20, // Control point 2 (offset)
                t.x * W, t.y * H
            );

            // Fill the area with opacity proportional to flow
            ctx.lineTo(t.x * W + width, t.y * H + width / 4); // Fake closure for fill visualization
            ctx.closePath();
            ctx.globalAlpha = Math.min(0.8, flow * 0.15);
            ctx.fillStyle = `rgba(0, 120, 255, ${Math.min(0.8, flow * 0.15)})`;
            ctx.fill();
        }
    }
    ctx.globalAlpha = 1; // Reset alpha for particles
}

function drawParticles() {
    for (const particle of ParticlesList) {
        particle.update();
        const pos = particle.getPoint();
        
        // Draw the particle
        ctx.fillStyle = `rgba(255, 255, 200, ${Math.random() * 0.8 + 0.2})`;
        ctx.beginPath();
        ctx.arc(pos.x, pos.y, Math.random() * 1 + 1, 0, Math.PI * 2);
        ctx.fill();
    }
}

function drawTotalThroughput() {
    let total = 0;
    for (let i = 0; i < NUM_SOURCES; i++) {
        for (let j = 0; j < NUM_TARGETS; j++) {
            total += Flows[i][j];
        }
    }
    ctx.fillStyle = '#DDD';
    ctx.font = '18px sans-serif';
    ctx.textAlign = 'left';
    ctx.fillText(`Total Throughput: ${total.toFixed(2)} / Tick`, 20, H - 20);
}

function animate() {
    // Clear canvas with slight fade effect (trail)
    ctx.fillStyle = 'rgba(17, 17, 34, 0.2)';
    ctx.fillRect(0, 0, W, H);

    drawBands();
    drawParticles();
    drawTotalThroughput();

    requestAnimationFrame(animate);
}

function loop() {
    init(); // Resize/reset dimensions if needed
    updateFlows();
    initializeParticles();
    animate();
}

loop();
</script>
</body>
</html>
```
Results at a glance— every model in this round with score, response time and status.

Who wrote what

ModelBlind scoreLatencyStatus
qwen3:14b 56.4 sOK
qwen3-coder:30b 27.0 sOK
llama3.1:8b DNF
gemma4:26b harness
mistral-small:24b DNF
deepseek-r1:14b 65.2 sOK
qwen3:8b harness
qwen3.5:9b 65.4 sOK