OverviewCode › R63
Code · blind round

R63 · Kruskal's MST with union-find

blind round 4/8 local models answered

Answers below are anonymized and shuffled. Read, judge, pick a favourite — then scroll down and reveal the models.

The reveal also holds a frontier reference on the same task (Claude Opus 5) — kept out of sight here so it cannot steer your judgement.

Show task
Create ONE self-contained HTML file (STRICT LIMIT: max 120 lines total) that animates Kruskal's minimum spanning tree on a random graph of 25 nodes drawn on a <canvas>. Sort the edges by weight and add them one at a time: colour an edge green when union-find accepts it and red when it would close a cycle, and tint the nodes by their current component so the components visibly merge. Show the number of accepted edges and the total weight. 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 — blind

Local models (Brain cluster) — identical prompt, anonymized order
MODEL Aqwen3.5:9bBLANKLIMFORMAT
Show code
```html
<!DOCTYPE html>
<html>
<head><title>Kruskal's MST</title></head>
<body>
<canvas id="mstCanvas" width="800" height="600" style="display:block;"></canvas>
<script>
const canvas = document.getElementById('mstCanvas');
const ctx = canvas.getContext('2d');

let N = 25;
let nodes = [];
let edges = [];
let parent, rank;
let edgesToProcess = [];
let currentEdgeIndex = 0;
let acceptedEdgesCount = 0;
let totalWeight = 0;
let animationRunning = true;

function initGraph() {
    nodes = Array.from({ length: N }, (_, i) => ({ x: Math.random() * canvas.width, y: Math.random() * canvas.height }));
    edges = [];
    for (let i = 0; i < N; i++) {
        for (let j = i + 1; j < N; j++) {
            const dx = nodes[i].x - nodes[j].x;
            const dy = nodes[i].y - nodes[j].y;
            const weight = Math.sqrt(dx * dx + dy * dy);
            edges.push({ u: i, v: j, w: weight });
        }
    }
    edges.sort((a, b) => a.w - b.w);

    parent = Array.from({ length: N }, (_, i) => i);
    rank = Array(N).fill(0);
    currentEdgeIndex = 0;
    acceptedEdgesCount = 0;
    totalWeight = 0;
}

function find(i) {
    if (parent[i] === i) return i;
    return parent[i] = find(parent[i]);
}

function union(i, j) {
    let rootI = find(i);
    let rootJ = find(j);
    if (rootI !== rootJ) {
        if (rank[rootI] < rank[rootJ]) {
            parent[rootI] = rootJ;
        } else if (rank[rootI] > rank[rootJ]) {
            parent[rootJ] = rootI;
        } else {
            parent[rootJ] = rootI;
            rank[rootI]++;
        }
        return true;
    }
    return false;
}

function draw() {
    ctx.clearRect(0, 0, canvas.width, canvas.height);
    const componentColors = {};
    for (let i = 0; i < N; i++) {
        const root = find(i);
        if (!componentColors[root]) componentColors[root] = 'rgba(50, 150, 255, 0.3)';
        nodes[i].color = componentColors[root];
    }

    ctx.lineWidth = 2;
    for (let i = 0; i < N; i++) {
        ctx.beginPath();
        ctx.arc(nodes[i].x, nodes[i].y, 10, 0, Math.PI * 2);
        ctx.fillStyle = nodes[i].color || 'rgba(50, 150, 255, 0.3)';
        ctx.fill();
    }

    for (let i = 0; i < edges.length; i++) {
        const e = edges[i];
        let color = '#999';
        if (e.accepted) color = 'green';
        if (e.rejected) color = 'red';

        ctx.beginPath();
        ctx.moveTo(nodes[e.u].x, nodes[e.u].y);
        ctx.lineTo(nodes[e.v].x, nodes[e.v].y);
        ctx.strokeStyle = color;
        ctx.lineWidth = e.accepted ? 3 : (e.rejected ? 2 : 1);
        ctx.stroke();
    }
}

function processEdge() {
    if (!animationRunning) return;

    if (currentEdgeIndex >= edges.length) {
        console.log("MST Complete.");
        return; // Stop animation if finished
    }

    const e = edges[currentEdgeIndex];
    let accepted = false;
    let rejected = false;

    accepted = union(e.u, e.v);

    if (accepted) {
        e.accepted = true;
        e.rejected = false;
        acceptedEdgesCount++;
        totalWeight += e.w;
    } else {
        e.accepted = false;
        e.rejected = true;
    }

    currentEdgeIndex++;
    draw();
    document.getElementById('stats').innerHTML = `Accepted Edges: ${acceptedEdgesCount}<br>Total Weight: ${totalWeight.toFixed(2)}`;
}

function animate() {
    if (animationRunning) {
        processEdge();
        requestAnimationFrame(animate);
    }
}

// Setup HTML requirements
document.body.innerHTML = `<canvas id="mstCanvas" width="800" height="600" style="display:block;"><div id="stats" style="position:absolute; top:10px; left:10px; font-family:monospace;">Accepted Edges: 0<br>Total Weight: 0.00</div></canvas>`;

initGraph();
animate();
</script>
</body>
</html>
```
MODEL Bllama3.1:8bERRSTATFORMAT
Show code
```html
<!DOCTYPE html>
<html>
<head>
  <title>Kruskal</title>
  <style>
    canvas {
      border: 1px solid black;
    }
  </style>
</head>
<body>
  <canvas id="c" width="800" height="600"></canvas>
  <script>
    const c = document.getElementById('c');
    const ctx = c.getContext('2d');
    const N = 25;

    let nodes = [], edges = [];
    for (let i = 0; i < N; i++) {
      nodes.push([Math.random() * c.width, Math.random() * c.height]);
    }

    for (let i = 0; i < N - 1; i++) {
      for (let j = i + 1; j < N; j++) {
        edges.push({a: i, b: j, w: Math.random()});
      }
    }

    function sortEdges() {
      edges.sort((a, b) => a.w - b.w);
    }

    let idx = 0;
    let component = new Array(N).fill(0);
    let unioned = new Array(N).fill(false);

    function draw() {
      ctx.clearRect(0, 0, c.width, c.height);
      for (let i = 0; i < N; i++) {
        if (!unioned[i]) {
          const r = Math.floor(Math.random() * 256), g = Math.floor(Math.random() * 256), b = Math.floor(Math.random() * 256);
          ctx.fillStyle = `rgb(${r}, ${g}, ${b})`;
          ctx.beginPath();
          ctx.arc(nodes[i][0], nodes[i][1], 10, 0, 2 * Math.PI);
          ctx.fill();
        }
      }

      for (let i = idx - 1; i < edges.length; i++) {
        if (!unioned[edges[i].a] || !unioned[edges[i].b]) continue;

        const r = (unioned[component[edges[i].a]] && unioned[component[edges[i].b]]) ? 255 : 0;
        ctx.strokeStyle = `rgb(${r}, ${255 - r}, 0)`;
        ctx.beginPath();
        ctx.moveTo(nodes[edges[i].a][0], nodes[edges[i].a][1]);
        ctx.lineTo(nodes[edges[i].b][0], nodes[edges[i].b][1]);
        ctx.stroke();

        if (unionFind(component, edges[i].a, edges[i].b)) {
          ctx.strokeStyle = 'green';
          idx++;
        } else {
          ctx.strokeStyle = 'red';
        }
      }

      const acceptedEdges = idx;
      const totalWeight = edges.slice(0, idx).reduce((s, e) => s + (unioned[e.a] && unioned[e.b] ? 0 : e.w), 0);
      ctx.font = '24px Arial';
      ctx.fillStyle = 'black';
      ctx.textAlign = 'left';
      ctx.textBaseline = 'top';
      ctx.fillText(`Accepted edges: ${acceptedEdges}`, 10, 10);
      ctx.textAlign = 'right';
      ctx.fillText(`Total weight: ${totalWeight.toFixed(2)}`, c.width - 10, 10);

      requestAnimationFrame(draw);
    }

    function unionFind(component, a, b) {
      if (component[a] == component[b]) return false;
      const rootA = findRoot(component, a), rootB = findRoot(component, b);
      if (rootA != rootB) {
        component[rootA] = rootB;
        return true;
      } else {
        return false;
      }
    }

    function findRoot(component, x) {
      let y = x;
      while (!unioned[y]) y = component[y];
      while (y != component[x]) {
        const temp = component[x];
        component[x] = y;
        x = temp;
      }
      return y;
    }

    function init() {
      sortEdges();
      idx = 0;
      unioned.fill(false);
      draw();
    }

    init();
  </script>
</body>
</html>
```
No usable answer reached the harness (TIMEOUT). Counted as a did-not-finish.
MODEL Ddeepseek-r1:14bDNF
No usable answer reached the harness (TIMEOUT). Counted as a did-not-finish.
MODEL Eqwen3-coder:30bERRBLANKLIMFORMAT
Show code
```html
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Kruskal's MST Animation</title>
    <style>body {margin: 0; display: flex; justify-content: center; align-items: center; height: 100vh; background-color: #222; color: white; font-family: sans-serif;}#canvasContainer {display: flex; gap: 20px;}canvas {border: 1px solid #444; background-color: #111;}</style>
</head>
<body>

<div id="canvasContainer">
    <canvas id="mstCanvas" width="800" height="600"></canvas>
</div>
<div style="display: flex; flex-direction: column; gap: 10px;">
    <div>Accepted Edges: <span id="acceptedCount">0</span> | Total Weight: <span id="totalWeight">0.00</span></div>
    <button onclick="initializeGraph()">Restart</button>
</div>

<script>
let canvas, ctx;
const N = 25;
let nodes = [];
let edges = [];
let parent, rank;
let acceptedEdges = new Set();
let totalWeight = 0;
let edgeIndex = 0;

function initializeGraph() {
    canvas = document.getElementById('mstCanvas');
    ctx = canvas.getContext('2d');
    nodes = [];
    edges = [];
    acceptedEdges.clear();
    totalWeight = 0;
    edgeIndex = 0;

    for (let i = 0; i < N; i++) {
        nodes.push({ x: Math.random() * (canvas.width - 100) + 50, y: Math.random() * (canvas.height - 100) + 50 });
    }

    for (let i = 0; i < N; i++) {
        for (let j = i + 1; j < N; j++) {
            let dist = Math.sqrt(Math.pow(nodes[i].x - nodes[j].x, 2) + Math.pow(nodes[i].y - nodes[j].y, 2));
            edges.push({ u: i, v: j, weight: dist });
        }
    }

    edges.sort((a, b) => a.weight - b.weight);

    parent = Array.from({ length: N }, (_, i) => i);
    rank = new Array(N).fill(0);

    function find(i) {
        if (parent[i] === i) return i;
        return parent[i] = find(parent[i]);
    }

    function union(i, j) {
        let rootI = find(i);
        let rootJ = find(j);
        if (rootI !== rootJ) {
            if (rank[rootI] < rank[rootJ]) {
                parent[rootI] = rootJ;
            } else if (rank[rootI] > rank[rootJ]) {
                parent[rootJ] = rootI;
            } else {
                parent[rootJ] = rootI;
                rank[rootI]++;
            }
            return true;
        }
        return false;
    }

    requestAnimationFrame(animate);
}

function drawGraph() {
    ctx.clearRect(0, 0, canvas.width, canvas.height);

    // Draw Nodes and Components
    for (let i = 0; i < N; i++) {
        const root = find(i);
        let componentColor = `hsl(${root * 360 / N}, 70%, ${50 + Math.floor(Math.random() * 10)}%)`;
        ctx.fillStyle = componentColor;
        ctx.beginPath();
        ctx.arc(nodes[i].x, nodes[i].y, 8, 0, Math.PI * 2);
        ctx.fill();
        ctx.strokeStyle = '#FFF';
        ctx.lineWidth = 1;
        ctx.stroke();
    }

    // Draw Edges (All edges)
    for (let i = 0; i < N; i++) {
        for (let j = i + 1; j < N; j++) {
            const edge = edges[i * N / 2 + Math.floor(j - i) / N]; // Simplified indexing for drawing all pairs, not necessary here since we iterate over `edges` array directly later
        }
    }

    // Draw Current State Edges (The ones processed so far)
    for (let i = 0; i < edgeIndex; i++) {
        const e = edges[i];
        ctx.beginPath();
        ctx.moveTo(nodes[e.u].x, nodes[e.u].y);
        ctx.lineTo(nodes[e.v].x, nodes[e.v].y);

        if (acceptedEdges.has(`${e.u}-${e.v}`)) {
            ctx.strokeStyle = '#4CAF50'; // Green
            ctx.lineWidth = 2;
        } else if (acceptedEdges.has(`${e.v}-${e.u}`)) {
             ctx.strokeStyle = '#4CAF50'; // Should not happen in set key
             ctx.lineWidth = 2;
        } else if (i < edgeIndex) {
            // Red if it was considered but rejected
             ctx.strokeStyle = '#F44336'; // Red
             ctx.lineWidth = 1;
        } else {
            continue;
        }

        ctx.stroke();
    }
}

function animate() {
    if (edgeIndex < edges.length && acceptedEdges.size < N - 1) {
        const e = edges[edgeIndex];
        let accepted = union(e.u, e.v);

        // Update Visualization and State
        if (accepted) {
            acceptedEdges.add(`${Math.min(e.u, e.v)}-${Math.max(e.u, e.v)}`);
            totalWeight += e.weight;
        } else {
             // Mark rejected edges briefly (handled by drawing logic or persistent red glow)
        }

        document.getElementById('acceptedCount').textContent = acceptedEdges.size;
        document.getElementById('totalWeight').textContent = totalWeight.toFixed(2);
        drawGraph();
        edgeIndex++;

        requestAnimationFrame(animate);
    } else {
        // Animation finished or loop condition met (The prompt implies running until complete)
        if (acceptedEdges.size === N - 1 && edgeIndex >= edges.length / 2 + 50) { // Heuristic check for completion
             setTimeout(() => {
                initializeGraph();
            }, 2000);
        }
    }
}

window.onload = initializeGraph;
</script>

</body>
</html>
```
MODEL Fqwen3:14bERRBLANKLIMFORMAT
Show code
```html
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Kruskal's MST</title>
    <style>body {margin:0;overflow:hidden;background:#111;}canvas {display:block;background:#222;}</style>
</head>
<body>
<canvas id="mstCanvas"></canvas>
<script>
const canvas = document.getElementById('mstCanvas');
const ctx = canvas.getContext('2d');

let N = 25;
let nodes, edges, parent, rank;
let acceptedCount = 0;
let totalWeight = 0;
let activeEdges = [];
let animationFrameId = null;

canvas.width = window.innerWidth;
canvas.height = window.innerHeight - 30;

function generateGraph() {
    nodes = Array(N).fill(0).map((_, i) => ({ x: Math.random() * canvas.width, y: Math.random() * canvas.height }));
    let edgeSet = new Set();
    for (let i = 0; i < N; i++) {
        for (let j = i + 1; j < N; j++) {
            if (Math.random() < 0.7) { // Adjust density
                const key = [i, j].sort().join('-');
                if (!edgeSet.has(key)) {
                    let weight = Math.floor(Math.random() * 10) + 1;
                    edges.push({ u: i, v: j, weight: weight });
                    edgeSet.add(key);
                }
            }
        }
    }
    edges.sort((a, b) => a.weight - b.weight);
    activeEdges = [...edges];
}

function makeSets() {
    parent = Array(N).fill(0).map((_, i) => i);
    rank = Array(N).fill(0);
}

function findSet(i) {
    if (parent[i] === i) return i;
    return parent[i] = findSet(parent[i]);
}

function unionSets(i, j) {
    let rootI = findSet(i);
    let rootJ = findSet(j);

    if (rootI !== rootJ) {
        if (rank[rootI] < rank[rootJ]) {
            parent[rootI] = rootJ;
        } else if (rank[rootI] > rank[rootJ]) {
            parent[rootJ] = rootI;
        } else {
            parent[rootJ] = rootI;
            rank[rootI]++;
        }
        return true; // Successful union
    }
    return false; // Cycle detected
}

function drawGraph(edge, color) {
    ctx.beginPath();
    ctx.moveTo(nodes[edge.u].x, nodes[edge.u].y);
    ctx.lineTo(nodes[edge.v].x, nodes[edge.v].y);
    ctx.strokeStyle = color;
    ctx.lineWidth = 2;
    ctx.stroke();

    // Draw weight text
    const midX = (nodes[edge.u].x + nodes[edge.v].x) / 2;
    const midY = (nodes[edge.u].y + nodes[edge.v].y) / 2 - 10;
    ctx.fillStyle = '#fff';
    ctx.font = '14px Arial';
    ctx.fillText(edge.weight, midX - 5, midY);
}

function drawComponents() {
    // Reset background for component drawing
    ctx.clearRect(0, 0, canvas.width, canvas.height);
    nodes.forEach((node, i) => {
        let root = findSet(i);
        const components = {};
        for (let j = 0; j < N; j++) {
            const r = findSet(j);
            components[r] = components[r] || [];
            components[r].push(j);
        }

        // Simple coloring based on component root index for visualization
        const componentColor = `hsl(${root * 360 / N}, 70%, ${50 + (i % 4) * 10}%)`;

        ctx.fillStyle = componentColor;
        ctx.beginPath();
        ctx.arc(node.x, node.y, 8, 0, Math.PI * 2);
        ctx.fill();

        // Draw nodes and labels on top of color fill
        ctx.strokeStyle = '#fff';
        ctx.lineWidth = 1;
        ctx.stroke();

        ctx.fillStyle = '#fff';
        ctx.textAlign = 'center';
        ctx.textBaseline = 'middle';
        ctx.fillText(i, node.x, node.y);
    });
}

let currentEdgeIndex = 0;

function stepSimulation() {
    if (!activeEdges || activeEdges.length === 0) return null;

    const edge = activeEdges[currentEdgeIndex];
    if (!edge) {
        return false; // Finished processing all edges
    }

    let accepted = unionSets(edge.u, edge.v);
    let color = accepted ? 'lime' : 'red';

    // Draw the processed edge for this step (temporarily over component coloring)
    drawGraph(edge, color);

    if (accepted) {
        acceptedCount++;
        totalWeight += edge.weight;
    }

    currentEdgeIndex++;
    return true; // Still running
}

function animate() {
    // 1. Draw Components first (shows merging state based on UFS array)
    drawComponents();
    
    // 2. Calculate and draw the current edge being processed
    let finished = stepSimulation();

    if (!finished) {
        // If all edges are processed, restart the simulation
        generateGraph();
        acceptedCount = 0;
        totalWeight = 0;
        currentEdgeIndex = 0;
        makeSets();
        document.getElementById('stats').innerHTML = `Finished! MST Weight: ${totalWeight}. Click to Restart.`;
        return;
    }

    // Update stats display
    let statusText = `Accepted Edges: ${acceptedCount} / ${Math.min(N - 1, activeEdges.length)} | Total Weight: ${totalWeight}`;
    if (currentEdgeIndex < activeEdges.length) {
        statusText += `<br>Processing Edge (${activeEdges[currentEdgeIndex].u}-${activeEdges[currentEdgeIndex].v}) W:${activeEdges[currentEdgeIndex].weight} ...`;
    } else {
         statusText = `Finished! MST Weight: ${totalWeight}. Restarting...`;
    }

    document.getElementById('stats').innerHTML = statusText;


    animationFrameId = requestAnimationFrame(animate);
}

// Setup UI and initial run
document.body.insertAdjacentHTML('beforeend', '<div id="stats" style="position: fixed; bottom: 10px; left: 10px; color:#fff; background:rgba(0,0,0,0.7); padding:10px; font-family:monospace;">Initializing Graph...</div>');

function startSimulation() {
    if (animationFrameId) cancelAnimationFrame(animationFrameId);
    generateGraph();
    acceptedCount = 0;
    totalWeight = 0;
    currentEdgeIndex = 0;
    makeSets();
    animate();
}

startSimulation();
</script>
</body>
</html>
```
MODEL Ggemma4:26bDNF
No usable answer reached the harness (TIMEOUT). Counted as a did-not-finish.
MODEL Hqwen3:8bDNF
No usable answer reached the harness (ERR). Counted as a did-not-finish.
Reveal the models— compare the answers first, then open. Names, blind scores and latencies are hidden until you do.

Who wrote what

Blind labelModelBlind scoreLatencyStatus
MODEL A qwen3.5:9b 21.1 sOK
MODEL B llama3.1:8b 8.9 sOK
MODEL C mistral-small:24b DNF
MODEL D deepseek-r1:14b DNF
MODEL E qwen3-coder:30b 26.9 sOK
MODEL F qwen3:14b 25.7 sOK
MODEL G gemma4:26b DNF
MODEL H qwen3:8b DNF

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>Kruskal's MST</title>
<style>
html,body{margin:0;height:100%;background:#080b11;overflow:hidden}
canvas{display:block}
</style>
</head>
<body>
<canvas id="c"></canvas>
<script>
const cv=document.getElementById('c'),g=cv.getContext('2d'),N=25,D=115;
let w,h,dpr,nodes,edges,par,siz,hue,idx,acc,tot,t0,done;
const ease=p=>p*p*(3-2*p),la=(a,b,k)=>a+((((b-a)%360)+540)%360-180)*k;
function resize(){dpr=Math.min(devicePixelRatio||1,2);w=innerWidth;h=innerHeight;cv.width=w*dpr;cv.height=h*dpr;g.setTransform(dpr,0,0,dpr,0,0);}
function find(i){return par[i]===i?i:par[i]=find(par[i]);}
function gen(){
  const m=68,ty=Math.min(120,h*0.22);nodes=[];
  for(let i=0;i<N;i++){let p,k=0;
    do{p={x:m+Math.random()*Math.max(20,w-2*m),y:ty+Math.random()*Math.max(20,h-ty-m)};k++;}
    while(k<300&&nodes.some(q=>Math.hypot(q.x-p.x,q.y-p.y)<Math.min(w,h)*0.105));
    p.hu=i*360/N;p.th=p.hu;nodes.push(p);}
  const all=[];
  for(let i=0;i<N;i++)for(let j=i+1;j<N;j++)all.push({a:i,b:j,w:Math.round(Math.hypot(nodes[i].x-nodes[j].x,nodes[i].y-nodes[j].y)/6),s:0});
  all.sort((p,q)=>p.w-q.w||p.a-q.a);
  const up=[...Array(N).keys()],f=i=>up[i]===i?i:up[i]=f(up[i]),keep=new Set();
  for(const e of all){const ra=f(e.a),rb=f(e.b);if(ra!==rb){up[ra]=rb;keep.add(e.a+'-'+e.b);}}
  const lim=all[Math.min(all.length,70)-1].w;
  edges=all.filter(e=>e.w<=lim||keep.has(e.a+'-'+e.b));
  par=[...Array(N).keys()];siz=Array(N).fill(1);hue=nodes.map(n=>n.hu);
  idx=0;acc=0;tot=0;done=0;
}
function commit(){
  const e=edges[idx++],ra=find(e.a),rb=find(e.b);
  if(ra===rb){e.s=2;return;}
  let A=ra,B=rb;if(siz[A]<siz[B]){A=rb;B=ra;}
  par[B]=A;siz[A]+=siz[B];e.s=1;acc++;tot+=e.w;
}
function draw(p){
  g.fillStyle='#080b11';g.fillRect(0,0,w,h);
  g.lineCap='round';
  for(const e of edges){
    if(e.s===0)continue;
    const A=nodes[e.a],B=nodes[e.b];
    g.strokeStyle=e.s===1?'rgba(64,230,140,.9)':'rgba(255,72,96,.32)';
    g.lineWidth=e.s===1?2.6:1.2;
    if(e.s===1){g.shadowColor='rgba(64,230,140,.75)';g.shadowBlur=10;}
    g.beginPath();g.moveTo(A.x,A.y);g.lineTo(B.x,B.y);g.stroke();g.shadowBlur=0;
  }
  g.strokeStyle='rgba(150,180,220,.09)';g.lineWidth=1;
  for(let i=idx;i<edges.length;i++){const e=edges[i],A=nodes[e.a],B=nodes[e.b];
    g.beginPath();g.moveTo(A.x,A.y);g.lineTo(B.x,B.y);g.stroke();}
  if(idx<edges.length&&!done){
    const e=edges[idx],A=nodes[e.a],B=nodes[e.b],k=ease(p),cyc=find(e.a)===find(e.b);
    g.strokeStyle=cyc?'rgba(255,120,140,.95)':'rgba(255,255,255,.95)';g.lineWidth=3.4;
    g.shadowColor=cyc?'rgba(255,80,110,.8)':'rgba(255,255,255,.6)';g.shadowBlur=16;
    g.beginPath();g.moveTo(A.x,A.y);g.lineTo(A.x+(B.x-A.x)*k,A.y+(B.y-A.y)*k);g.stroke();g.shadowBlur=0;
    g.fillStyle='#fff';g.beginPath();g.arc(A.x+(B.x-A.x)*k,A.y+(B.y-A.y)*k,3.2,0,7);g.fill();
    g.font='12px ui-monospace,monospace';g.fillStyle='rgba(230,240,255,.85)';g.textAlign='center';
    g.fillText(e.w,(A.x+B.x)/2,(A.y+B.y)/2-8);
  }
  for(let i=0;i<N;i++){
    const n=nodes[i];n.th=hue[find(i)];n.hu=la(n.hu,n.th,.12);
    g.fillStyle='hsl('+n.hu.toFixed(0)+',72%,58%)';
    g.shadowColor='hsl('+n.hu.toFixed(0)+',80%,50%)';g.shadowBlur=14;
    g.beginPath();g.arc(n.x,n.y,9,0,7);g.fill();g.shadowBlur=0;
    g.strokeStyle='rgba(8,11,17,.9)';g.lineWidth=2;g.stroke();
  }
  const comps=new Set();for(let i=0;i<N;i++)comps.add(find(i));
  g.textAlign='left';
  g.font='600 20px ui-sans-serif,system-ui,sans-serif';g.fillStyle='#eaf2ff';
  g.fillText("Kruskal's Minimum Spanning Tree", 28, 40);
  g.font='13px ui-monospace,monospace';g.fillStyle='rgba(190,210,235,.85)';
  g.fillText('edge '+Math.min(idx+1,edges.length)+' / '+edges.length+'   nodes 25   components '+comps.size, 28, 62);
  g.fillStyle='rgba(64,230,140,.95)';g.fillText('accepted  '+acc+' / '+(N-1), 28, 84);
  g.fillStyle='rgba(255,200,120,.95)';g.fillText('total weight  '+tot, 190, 84);
  if(done){g.fillStyle='rgba(255,255,255,.75)';g.fillText('MST complete — restarting…', 400, 84);}
  const bw=Math.min(360,w-56);
  g.fillStyle='rgba(255,255,255,.08)';g.fillRect(28,96,bw,4);
  g.fillStyle='rgba(64,230,140,.8)';g.fillRect(28,96,bw*(idx/edges.length),4);
}
function loop(now){
  if(t0===undefined)t0=now;
  if(done){if(now-done>1700){gen();t0=now;}}
  else{
    while(now-t0>=D&&idx<edges.length){commit();t0+=D;}
    if(idx>=edges.length)done=now;
  }
  draw(idx<edges.length&&!done?Math.min(1,(now-t0)/D):0);
  requestAnimationFrame(loop);
}
addEventListener('resize',()=>{resize();gen();t0=undefined;});
resize();gen();requestAnimationFrame(loop);
</script>
</body>
</html>