OverviewCode › R73
Code · blind round

R73 · KMP string search with prefix table

blind round 3/8 local models answered

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

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. Which ones, and why, is spelled out in the reveal.

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 the Knuth-Morris-Pratt string search on a <canvas>. First build the prefix table for a short pattern and draw it as a labelled row, then slide the pattern along a longer text: highlight the compared characters, and when a mismatch occurs show the pattern JUMPING by the prefix-table amount instead of shifting by one. Mark every match found and show the number of character comparisons. 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:9bSTATLIMFORMAT
Show code
```html
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>KMP Animation</title>
    <style>body { margin: 0; background: #111; display: flex; justify-content: center; align-items: center; height: 100vh; } canvas { border: 2px solid #333; background: #000; }</style>
</head>
<body>
    <canvas id="kmpCanvas"></canvas>
    <script>
        const canvas = document.getElementById('kmpCanvas');
        const ctx = canvas.getContext('2d');

        // Constants for drawing
        const P_LEN = 5; // Pattern length
        const T_LEN = 100; // Text length
        canvas.width = 900;
        canvas.height = 100;

        let text = "ABABDABACADABABCABAB";
        let pattern = "ABABA";
        let lps = [];

        // State variables
        let i = 0, j = 0; // Pointers for T and P
        let comparisonCount = 0;
        let matchFound = false;
        let animationState = 'PREP'; // PREP -> LPS_DISPLAY -> SEARCH
        let lastShiftAmount = 0;

        // --- Utility Functions ---

        function calculateLPS(pat) {
            const m = pat.length;
            lps = Array(m).fill(0);
            let length = 0;
            let k = 1;
            for (let i = 1; i < m; i++) {
                while (k > 0 && pat[i] !== pat[k]) {
                    k = lps[k - 1];
                }
                if (pat[i] === pat[k]) {
                    lps[i] = k + 1;
                    k++;
                } else {
                    lps[i] = k;
                }
                length = Math.max(length, lps[i]);
            }
        }

        function draw() {
            ctx.clearRect(0, 0, canvas.width, canvas.height);
            const charWidth = 25;
            const offset = (canvas.width - (T_LEN * charWidth)) / 2;

            // Draw Text T
            let textOutput = "";
            for (let k = 0; k < T_LEN; k++) {
                textOutput += `<span style="color: white; margin-right: 4px;">${text[k]}</span>`;
            }
            ctx.font = "24px monospace";
            ctx.fillStyle = "#AAA";
            ctx.fillText(`Text: ${text.substring(0, T_LEN)}`, 10, 35);

            // Draw Pattern P
            let patternOutput = "";
            for (let k = 0; k < pattern.length; k++) {
                patternOutput += `<span style="color: white; margin-right: 4px;">${pattern[k]}</span>`;
            }
            ctx.fillText(`Pattern: ${pattern}`, 10, 75);

            // Draw LPS Table (Labels)
            let lpsStr = "LPS Array: ";
            for (let k = 0; k < pattern.length; k++) {
                lpsStr += `${lps[k] === 0 ? '0' : lps[k].toString().padStart(2, ' ') || ''} `;
            }
            ctx.font = "14px monospace";
            ctx.fillStyle = "#888";
            ctx.fillText(lpsStr, 250, 35);

            // Draw comparison highlights and match markers
            for (let k = 0; k < T_LEN; k++) {
                const x = offset + k * charWidth;
                // Highlight current text character being compared
                if (k === i && j > 0 || (i >= 0 && j === 0)) {
                    ctx.fillStyle = '#FFD700'; // Gold highlight for comparison focus
                    ctx.fillRect(x, 25, charWidth - 4, 25);
                } else {
                    ctx.fillStyle = 'transparent';
                    ctx.fillRect(x, 25, charWidth - 4, 25);
                }
            }

            // Draw Match Marker (if found)
            if (matchFound) {
                 ctx.font = "30px monospace";
                 ctx.fillStyle = '#00FF7F'; // Green for match
                 ctx.fillText("MATCH FOUND!", 50, 120);

                // Draw index tracking
                ctx.fillStyle = 'red';
                ctx.font = "24px monospace";
                ctx.fillText(`Matches: ${Math.floor(comparisonCount / 5)} | Comparisons: ${comparisonCount}`, 350, 120);
            } else {
                 // Draw index tracking
                ctx.fillStyle = '#888';
                ctx.font = "24px monospace";
                ctx.fillText(`Matches: 0 | Comparisons: ${comparisonCount}`, 350, 120);
            }

        }


        function kmpStep() {
            if (animationState === 'PREP') {
                calculateLPS(pattern);
                animationState = 'LPS_DISPLAY';
                draw();
                return requestAnimationFrame(kmpStep);
            }

            // --- KMP Search Logic (The core animation step) ---
            let comparisonMade = false; 

            if (j < pattern.length && i < text.length) {
                comparisonMade = true;
                const match = text[i] === pattern[j];
                comparisonCount++;

                // Highlight the current matching position on P
                ctx.fillStyle = '#FF4500'; 
                ctx.fillRect((canvas.width - (T_LEN * 25)) / 2 + i * 25, 25, 25, 25);

                if (match) {
                    i++;
                    j++;
                    if (j === pattern.length) {
                        // SUCCESS: Full match found
                        animationState = 'MATCH';
                        matchFound = true;
                        lastShiftAmount = j - pattern.length + 1; // Record start index
                    } else if (i < text.length) {
                         // Continue animation on next frame
                    }
                } else {
                    // MISMATCH: Shift required
                    comparisonMade = true;
                    if (j !== 0) {
                        // Calculate jump using LPS array
                        let newJ = lps[j - 1];
                        lastShiftAmount = j - newJ; // The shift amount conceptually

                        // Reset pointers for the next attempt
                        j = newJ;
                        i += lastShiftAmount;

                    } else {
                        // First character mismatch, just move text pointer
                        j = 0;
                        i++;
                    }
                }
            }

            draw();

            if (i >= text.length) {
                animationState = 'FINISHED';
                return;
            }

            // Throttle the animation for visibility
            setTimeout(() => {
                requestAnimationFrame(kmpStep);
            }, 150); // Animation speed delay
        }

        // Initial call to start the process
        kmpStep();
    </script>
</body>
</html>
```
MODEL Bqwen3-coder:30bSTATLIMFORMAT
Show code
```html
<!DOCTYPE html>
<html>
<head>
<title>KMP Animation</title>
<style>body{background:#111;color:white;font-family:monospace;margin:0;display:flex;justify-content:center;align-items:center;min-height:100vh;}canvas{border:2px solid #444;}</style>
</head>
<body>
<canvas id="kmpCanvas" width="800" height="150"></canvas>
<script>
const canvas = document.getElementById('kmpCanvas');
const ctx = canvas.getContext('2d');

let text, pattern;
let lps;
let state = { 
    t_idx: 0, p_idx: 0, matchFound: false, comparisons: 0, totalTime: 0, finished: false
};

// --- Helper Functions ---

function computeLPSArray(P) {
    const m = P.length;
    const lpsA = new Array(m).fill(0);
    let length = 0; // length of the previous longest prefix suffix
    let i = 1;
    while (i < m) {
        if (P[i] === P[length]) {
            length++;
            lpsA[i] = length;
            i++;
        } else {
            if (length !== 0) {
                length = lpsA[length - 1];
            } else {
                lpsA[i] = 0;
                i++;
            }
        }
    }
    return lpsA;
}

function initialize(newText, newPattern) {
    text = newText || "ABABDABACDABABCABAB";
    pattern = newPattern || "ABABCABAB";
    lps = computeLPSArray(pattern);
    state.t_idx = 0;
    state.p_idx = 0;
    state.matchFound = false;
    state.comparisons = 0;
    state.totalTime = 0;
    state.finished = false;
}

// --- Drawing and Animation Logic ---

function drawScene() {
    ctx.clearRect(0, 0, canvas.width, canvas.height);
    ctx.font = "24px monospace";
    const lineHeight = 35;

    // Draw Text (Background)
    let textY = 20;
    ctx.fillStyle = '#333';
    ctx.fillText(text, 10, textY);
    
    // Draw Pattern (Search Indicator)
    let patternX = 10 + ((state.t_idx - state.p_idx + 1) * 30);
    if (patternX < 10) patternX = 10;
    ctx.fillStyle = '#555';
    ctx.fillText(pattern, patternX, textY);

    // Highlight current comparison point
    let highlightX = 10 + (state.t_idx * 30);
    if (highlightX >= canvas.width - 5) highlightX = canvas.width - 6; // Clamp for end of line
    ctx.fillStyle = 'rgba(255, 255, 0, 0.4)';
    ctx.fillRect(Math.max(0, highlightX), 10, 30, 35);

    // Draw Status Info
    let statusY = textY + lineHeight + 10;
    ctx.fillStyle = 'white';
    ctx.fillText(`Comparisons: ${state.comparisons} | Matches Found: ${state.matchFound ? '!!!' : ''}`, 10, statusY);

    if (state.finished) {
        ctx.font = "30px monospace";
        ctx.fillStyle = '#99ff99';
        ctx.fillText("SEARCH COMPLETE", 10, statusY + 40);
    }
}


function kmpStep() {
    if (state.finished) return false;

    let t_char, p_char;
    let newPIdx = state.p_idx;
    let jumpDistance = 0;

    // Check for end of text
    if (state.t_idx >= text.length) {
        state.finished = true;
        return false;
    }

    t_char = text[state.t_idx];
    p_char = pattern[state.p_idx];
    
    // The actual comparison logic
    if (t_char === p_char) {
        state.comparisons++;
        state.t_idx++;
        newPIdx++;

        if (newPIdx === pattern.length) {
            // Match found!
            state.matchFound = true;
            console.log(`Match at index ${state.t_idx - 1}`);
            
            // Reset for next search (or keep searching if we want overlapping matches)
            let matchStartIdx = state.t_idx - pattern.length;
            if (matchStartIdx > 0) { // Avoid infinite loop on empty text/pattern
                state.p_idx = lps[pattern.length - 1]; 
                state.t_idx += (lps[pattern.length - 1] - state.p_idx);
            } else {
                 state.p_idx = 0;
                 state.t_idx++; // Simple advance
            }

        } else if (newPIdx > 0) {
             // Successful match, just move pointers
        }

    } else {
        // Mismatch handling
        state.comparisons++;
        let prevPIdx = state.p_idx;
        
        if (prevPIdx !== 0) {
            // Jump using LPS table
            newPIdx = lps[prevPIdx - 1];
        } else {
            // No jump possible, just advance text pointer
            newPIdx = 0;
        }

        state.p_idx = newPIdx;
        state.t_idx++; // Always advance the text pointer on mismatch
    }

    state.p_idx = Math.min(state.p_idx, pattern.length);
    state.t_idx = Math.min(state.t_idx, text.length);

    return true;
}


function animate() {
    drawScene();
    if (kmpStep()) {
        requestAnimationFrame(animate);
    } else if (!state.finished) {
         // Handle case where kmpStep might stop prematurely
    }
}

// --- Startup ---
initialize(); 
animate();
</script>
</body>
</html>
```
MODEL Cgemma4:26bDNF
No usable answer reached the harness (TIMEOUT). Counted as a did-not-finish.
MODEL Dqwen3:14bOKLIMFORMAT
Show code
```html
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>KMP Algorithm Visualization</title>
    <style>body {background: #1a1a2e; color: white; font-family: monospace;} canvas {border: 1px solid #4c3b79; display: block; margin: 20px auto; background: #2a2a4a;}</style>
</head>
<body>
<canvas id="kmpCanvas"></canvas>

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

    let T, P; // Text and Pattern
    let lT, lP;
    let pi;   // Failure function (prefix table)

    let k = 0; // Current text index
    let j = 0; // Current pattern index
    let comparisons = 0;
    let matches = 0;
    let animationRunning = true;

    const CANVAS_WIDTH = 900;
    const CANVAS_HEIGHT = 150;
    canvas.width = CANVAS_WIDTH;
    canvas.height = CANVAS_HEIGHT;

    function generateRandom(min, max) { return Math.floor(Math.random() * (max - min + 1)) + min; }
    function generateString(length) { let chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"; let result = ""; for (let i = 0; i < length; i++) result += chars.charAt(Math.floor(generateRandom(0, 25))); return result; }

    // KMP Prefix Function Generation
    function computePrefixTable(pattern) {
        const m = pattern.length;
        pi = new Array(m).fill(0);
        let length = 0; // len of the previous longest prefix suffix
        for (let i = 1; i < m; i++) {
            while (length > 0 && pattern[i] !== pattern[length]) {
                length = pi[length - 1];
            }
            if (pattern[i] === pattern[length]) {
                length++;
            }
            pi[i] = length;
        }
    }

    // Initialization function
    function initialize() {
        T = generateString(Math.min(70, Math.floor(CANVAS_WIDTH / 15))); // Text length based on canvas width
        P = generateString(Math.max(3, Math.min(8, T.length - 2))); // Pattern length
        lT = T.length;
        lP = P.length;

        computePrefixTable(P);
        k = 0; j = 0; comparisons = 0; matches = 0;
        animationRunning = true;
    }

    // Drawing function
    function draw() {
        ctx.clearRect(0, 0, CANVAS_WIDTH, CANVAS_HEIGHT);

        const textCharSize = CANVAS_WIDTH / lT;
        const patternCharSize = (CANVAS_WIDTH / lP) * 1.2; // Make pattern slightly bigger for visibility

        // Draw Text
        ctx.font = 'bold 30px monospace';
        ctx.fillStyle = '#e6e6fa';
        let textStr = '';
        for(let i=0; i<lT; i++) {
            textStr += T[i] + ' ';
        }
        ctx.fillText(textStr, 10, CANVAS_HEIGHT / 2 + 10);

        // Draw Pattern (Above Text)
        ctx.font = 'bold 35px monospace';
        let patternDisplay = '';
        for(let i=0; i<lP; i++) {
            patternDisplay += P[i] + ' ';
        }
        ctx.fillText('Pattern:', 10, CANVAS_HEIGHT / 2 - 40);
        ctx.fillText(patternDisplay, 150, CANVAS_HEIGHT / 2 - 40);

        // Highlight current comparison characters
        if (k > 0 && j < lP) {
            // Text highlight at k
             ctx.fillStyle = '#ffeb3b'; // Yellow
             ctx.fillRect(k * textCharSize + (k > 0 ? textCharSize / 2 : 10), CANVAS_HEIGHT/2 - 45, textCharSize*0.8, 40);

            // Pattern highlight at j
            ctx.fillStyle = '#ffeb3b';
            let patternX = 150 + (j * (CANVAS_WIDTH / lP) * 1.2) - ((CANVAS_WIDTH/lP) * 0.6); // Approximate spacing
            if (patternX > 0) {
                ctx.fillRect(patternX, CANVAS_HEIGHT/2 - 45, (CANVAS_WIDTH / lP) * 1.2 * 0.8, 40);
            }

        } else if (matches > 0 && k < lT) {
             // Draw match indicator below text
             ctx.fillStyle = 'rgba(34, 157, 34, 0.8)'; // Green overlay
             ctx.fillRect(k * textCharSize + 5, CANVAS_HEIGHT / 2 - 10, (lT - k) * textCharSize, 20);
        }

         // Draw Prefix Table Label
        ctx.font = '30px monospace';
        ctx.fillStyle = '#9d86ff';
        let prefixLabel = "Prefix Table (pi): ";
        for(let i=0; i<lP; i++) {
            prefixLabel += pi[i] + ' ';
        }
        ctx.fillText(prefixLabel, 150, CANVAS_HEIGHT / 2 - 40);

    }

    // KMP Core Logic Step
    function kmpStep() {
        if (!animationRunning) return;

        draw(); // Redraw state before computation

        comparisons++;

        const charT = T[k];
        const charP = P[j];

        let next_j, next_k;

        if (charT === charP) {
            // Match found
            next_j = j + 1;
            next_k = k + 1;
        } else {
            // Mismatch: Use failure function jump
            next_j = pi[j - 1] !== undefined ? pi[j - 1] : 0;
            next_k = k + 1; // Text index always advances unless text is exhausted

            if (j === 0) {
                // If mismatch at the start of the pattern, just move to next text character
                next_j = 0;
                next_k = k + 1;
            }
        }

        // Update state variables
        if (next_j === lP) {
            // Full match!
            matches++;
            console.log(`Match found at index ${k - lP + 1}`);
            // After a full match, we jump using the failure function again
            j = pi[lP - 1];
            next_j = j; // The pattern effectively shifts by this amount

        } else {
             j = next_j;
        }

        k++;

        if (k > lT) {
            // Text exhausted, restart simulation
            initialize();
        }
    }

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

        kmpStep();
        requestAnimationFrame(animate);
    }

    // Start the process
    initialize();
    animate();
</script>
</body>
</html>
```
MODEL Ellama3.1:8bDNF
No usable answer reached the harness (TIMEOUT). Counted as a did-not-finish.
MODEL Fqwen3:8bDNF
No usable answer reached the harness (ERR). Counted as a did-not-finish.
MODEL Gdeepseek-r1:14bDNF
No usable answer reached the harness (TIMEOUT). Counted as a did-not-finish.
No usable answer reached the harness (TIMEOUT). 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 51.2 sOK
MODEL B qwen3-coder:30b 28.2 sOK
MODEL C gemma4:26b harness
MODEL D qwen3:14b 52.7 sOK
MODEL E llama3.1:8b DNF
MODEL F qwen3:8b harness
MODEL G deepseek-r1:14b DNF
MODEL H mistral-small:24b 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>
<meta charset="utf-8">
<title>Knuth-Morris-Pratt string search</title>
<style>
html,body{margin:0;height:100%;background:#070a12;display:grid;place-items:center;font-family:system-ui,sans-serif}
canvas{background:#0b1020;border-radius:16px;box-shadow:0 24px 70px #000a}
</style>
<canvas id=c width=960 height=540></canvas>
<script>
const c=document.getElementById('c'),x=c.getContext('2d'),W=960,H=540;
const R=a=>a[Math.floor(Math.random()*a.length)];let S;
function init(){
 const A=Math.random()<.5?['A','B']:['A','B','C'];
 const m=3+Math.floor(Math.random()*3),n=25+Math.floor(Math.random()*6);
 let p='',t='';
 for(let k=0;k<m;k++)p+=R(A);
 for(let k=0;k<n;k++)t+=R(A);
 for(let k=0;k<2;k++){const a=Math.floor(Math.random()*(n-m+1));t=t.slice(0,a)+p+t.slice(a+m)}
 S={p,t,m,n,lps:Array(m).fill(0),ph:0,tk:1,tl:0,tcmp:null,tok:1,i:0,j:0,cmp:null,ok:1,
    comps:0,mat:[],off:0,doff:0,tmr:0,fl:0,msg:'Phase 1 - building the prefix table (LPS)',col:'#9fb4d6'};
}
function step(){
 if(S.ph==0){
  if(S.tk>=S.m){S.ph=1;S.tcmp=null;S.msg='Phase 2 - sliding the pattern along the text';S.col='#9fb4d6';return}
  S.tcmp=[S.tk,S.tl];S.comps++;
  if(S.p[S.tk]==S.p[S.tl]){S.tok=1;S.tl++;S.lps[S.tk]=S.tl;S.msg='p['+S.tk+'] = p['+S.tl+"-1] -> lps["+S.tk+'] = '+S.tl;S.tk++;S.col='#3ddc84'}
  else if(S.tl>0){S.tok=0;S.msg='mismatch -> fall back to lps['+(S.tl-1)+'] = '+S.lps[S.tl-1];S.tl=S.lps[S.tl-1];S.col='#ff6b7a'}
  else{S.tok=0;S.lps[S.tk]=0;S.msg='no border -> lps['+S.tk+'] = 0';S.tk++;S.col='#ff6b7a'}
  return;
 }
 if(S.ph==2){init();return}
 if(S.i>=S.n){S.ph=2;S.cmp=null;S.msg='Scan complete - '+S.mat.length+' match(es) in '+S.comps+' comparisons. Restarting...';S.col='#f5c542';return}
 S.cmp=[S.i,S.j];S.comps++;
 if(S.t[S.i]==S.p[S.j]){
  S.ok=1;S.i++;S.j++;S.col='#3ddc84';S.msg='t['+S.cmp[0]+'] = p['+S.cmp[1]+"] = '"+S.p[S.cmp[1]]+"' -> advance both";
  if(S.j==S.m){S.mat.push(S.i-S.m);S.fl=34;S.msg='MATCH at index '+(S.i-S.m)+' -> continue with lps = '+S.lps[S.m-1];S.j=S.lps[S.m-1];S.col='#f5c542'}
 }else{
  S.ok=0;S.col='#ff6b7a';
  if(S.j>0){const b=S.lps[S.j-1];S.msg="mismatch '"+S.t[S.i]+"' vs '"+S.p[S.j]+"' -> jump "+(S.j-b)+' (lps['+(S.j-1)+'] = '+b+'), no backtrack in text';S.j=b}
  else{S.msg="mismatch '"+S.t[S.i]+"' vs '"+S.p[0]+"' -> shift by 1";S.i++}
 }
 S.off=S.i-S.j;
}
function row(s,x0,y,cw,f,h){
 for(let k=0;k<s.length;k++){const cx=x0+k*cw,st=f(k);
  x.fillStyle=st[0];x.fillRect(cx+1.5,y,cw-3,h);
  x.strokeStyle=st[1];x.lineWidth=2;x.strokeRect(cx+1.5,y,cw-3,h);
  x.fillStyle=st[2];x.font='bold 19px ui-monospace,monospace';x.textAlign='center';
  x.fillText(s[k],cx+cw/2,y+h/2+1)}
}
function box(bx,by,lab,val,col){
 x.fillStyle='#101830';x.fillRect(bx,by,272,60);x.strokeStyle='#22314f';x.lineWidth=1;x.strokeRect(bx+.5,by+.5,271,59);
 x.textAlign='left';x.fillStyle='#6d7f9e';x.font='12px system-ui';x.fillText(lab,bx+16,by+19);
 x.fillStyle=col;x.font='bold 24px ui-monospace,monospace';x.fillText(val,bx+16,by+41);
}
function draw(){
 x.fillStyle='#0b1020';x.fillRect(0,0,W,H);x.textBaseline='middle';x.textAlign='left';
 x.fillStyle='#eaf1ff';x.font='bold 25px system-ui';x.fillText('Knuth-Morris-Pratt string search',40,34);
 x.fillStyle='#6d7f9e';x.font='13px ui-monospace,monospace';x.fillText('pattern "'+S.p+'"   ·   text of '+S.n+' chars   ·   alphabet-driven random instance',40,60);
 const pw=46,px=110,py=100;
 x.fillStyle='#8aa2c8';x.font='12px system-ui';x.textAlign='right';x.fillText('PREFIX',100,py+17);x.fillText('TABLE',100,py+50);
 row(S.p,px,py,pw,k=>{
  if(S.ph==0&&S.tcmp&&k==S.tcmp[0])return[S.tok?'#153a28':'#3b141d',S.tok?'#3ddc84':'#ff6b7a','#fff'];
  if(S.ph==0&&S.tcmp&&k==S.tcmp[1])return['#152a4c','#5aa9ff','#dbe9ff'];
  return['#141d33','#25324f','#cfe0ff']},34);
 for(let k=0;k<S.m;k++){const d=S.ph>0||k<S.tk,cx=px+k*pw;
  x.fillStyle='#0e1526';x.fillRect(cx+1.5,py+36,pw-3,28);x.strokeStyle='#25324f';x.lineWidth=1;x.strokeRect(cx+1.5,py+36,pw-3,28);
  x.textAlign='center';x.font='bold 16px ui-monospace,monospace';x.fillStyle=d?'#f5c542':'#2c3a58';x.fillText(d?S.lps[k]:'?',cx+pw/2,py+51)}
 x.strokeStyle='#1a2440';x.lineWidth=1;x.beginPath();x.moveTo(40,196);x.lineTo(920,196);x.stroke();
 const cw=Math.min(30,(W-190)/S.n),tx=110,ty=232,py2=300;
 x.textAlign='right';x.fillStyle='#8aa2c8';x.font='12px system-ui';x.fillText('TEXT',100,ty+17);x.fillText('PATTERN',100,py2+17);
 row(S.t,tx,ty,cw,k=>{
  if(S.ph==1&&S.cmp&&k==S.cmp[0])return[S.ok?'#153a28':'#3b141d',S.ok?'#3ddc84':'#ff6b7a','#fff'];
  if(S.mat.some(a=>k>=a&&k<a+S.m))return['#0f2c1d','#2c7a4b','#a9efc6'];
  return['#141d33','#25324f','#cfe0ff']},34);
 S.mat.forEach(a=>{x.fillStyle=S.fl>0&&a==S.mat[S.mat.length-1]?'#f5c542':'#3ddc84';x.fillRect(tx+a*cw+2,ty+38,S.m*cw-4,4)});
 const ox=tx+S.doff*cw;
 row(S.p,ox,py2,cw,k=>{
  if(S.ph==1&&S.cmp&&k==S.cmp[1])return[S.ok?'#153a28':'#3b141d',S.ok?'#3ddc84':'#ff6b7a','#fff'];
  if(S.ph==1&&k<S.j)return['#152a4c','#5aa9ff','#dbe9ff'];
  return['#161f38','#2b3a5c','#9fb4d6']},34);
 if(S.ph==1&&S.cmp){const a=tx+S.cmp[0]*cw+cw/2,b=ox+S.cmp[1]*cw+cw/2;
  x.strokeStyle=S.ok?'#3ddc84':'#ff6b7a';x.setLineDash([4,4]);x.lineWidth=2;x.beginPath();
  x.moveTo(a,ty+36);x.lineTo(a,ty+48);x.lineTo(b,py2-12);x.lineTo(b,py2-2);x.stroke();x.setLineDash([])}
 x.textAlign='left';x.fillStyle=S.col;x.font='bold 16px ui-monospace,monospace';x.fillText(S.msg,40,378);
 box(40,410,'CHARACTER COMPARISONS',''+S.comps,'#eaf1ff');
 box(344,410,'MATCHES FOUND',S.mat.length+(S.mat.length?'  @ '+S.mat.join(', '):''),'#3ddc84');
 box(648,410,'POINTERS','i = '+Math.min(S.i,S.n)+'   j = '+S.j,'#5aa9ff');
 x.fillStyle='#4a5a78';x.font='12px system-ui';x.fillText('the text pointer i never moves backwards - on a mismatch the pattern jumps forward by j - lps[j-1]',40,502);
}
function loop(){
 S.doff+=(S.off-S.doff)*.22;if(Math.abs(S.off-S.doff)<.012)S.doff=S.off;
 if(S.fl>0)S.fl--;
 if(++S.tmr>=(S.ph==0?20:S.ph==1?27:110)&&S.doff==S.off){S.tmr=0;step()}
 draw();requestAnimationFrame(loop);
}
init();loop();
</script>