[object Object]

← back to Model Arena

yolo idea D: head-to-head record matrix — pairwise W-L (row vs col) from crowned battles, color-coded, under the leaderboard

6fea94d0611bc494784572f14f31cb6030f1ba1c · 2026-07-23 00:27:55 -0700 · Steve

Files touched

Diff

commit 6fea94d0611bc494784572f14f31cb6030f1ba1c
Author: Steve <steve@designerwallcoverings.com>
Date:   Thu Jul 23 00:27:55 2026 -0700

    yolo idea D: head-to-head record matrix — pairwise W-L (row vs col) from crowned battles, color-coded, under the leaderboard
---
 public/index.html | 24 ++++++++++++++++++++++++
 server.js         |  5 ++++-
 yolo/IDEAS.md     |  1 +
 3 files changed, 29 insertions(+), 1 deletion(-)

diff --git a/public/index.html b/public/index.html
index 36e280a..d831ae1 100644
--- a/public/index.html
+++ b/public/index.html
@@ -190,6 +190,9 @@ tr.clk{cursor:pointer}tr.clk:hover td{background:rgba(0,229,255,.06)}
       <thead><tr><th>#</th><th>Model</th><th>Kind</th><th>ELO</th><th>Battles</th><th>Wins</th><th>Win %</th><th>🤖 avg</th><th>$/win</th><th>Errors</th><th>Avg time</th><th>Spend</th></tr></thead>
       <tbody id="b-rows"></tbody>
     </table>
+    <h2 style="margin-top:26px">Head-to-Head</h2>
+    <div class="note">Each cell = row model's win–loss record vs the column model, from crowned battles they both rendered in.</div>
+    <div style="overflow:auto"><table id="h2h-tbl"></table></div>
   </section>
 </main>
 <script>
@@ -427,12 +430,14 @@ async function loadBoard(){
     const tr = document.createElement('tr');
     tr.className='clk'+(i===0 && s.wins>0?' first':'');
     tr.onclick=()=>openProfile(s.id);
+    // (h2h rendered after the loop)
     tr.innerHTML = `<td>${i+1}</td><td>${s.label}</td><td>${s.kind}</td><td><b>${s.elo}</b></td><td>${s.battles}</td><td>${s.wins}</td>
       <td>${s.winRate==null?'—':s.winRate+'%'}</td><td>${s.avgAiScore==null?'—':s.avgAiScore}</td>
       <td>${s.costPerWin==null?'—':(s.costPerWin?'$'+s.costPerWin.toFixed(3):'$0')}</td>
       <td>${s.errors}</td><td>${s.avgSeconds==null?'—':s.avgSeconds+'s'}</td><td>${s.spend?'$'+s.spend.toFixed(2):'$0'}</td>`;
     tb.appendChild(tr);
   });
+  renderH2H(d);
 }
 
 // ---- model profile ----
@@ -456,6 +461,25 @@ async function openProfile(mid){
     g.appendChild(card);
   }
 }
+function renderH2H(d){
+  const tbl=$('#h2h-tbl'); if(!tbl) return;
+  // only models that fought at least one crowned battle
+  const active=d.models.filter(s=>s.battles>0);
+  if (active.length<2){ tbl.innerHTML='<tr><td class="note" style="border:0">Not enough crowned battles yet.</td></tr>'; return; }
+  const h=d.h2h||{};
+  let html='<thead><tr><th>vs →</th>'+active.map(s=>`<th title="${s.label}">${s.label.split(' ')[0]}</th>`).join('')+'</tr></thead><tbody>';
+  for (const r of active){
+    html+=`<tr><th style="text-align:left">${r.label}</th>`;
+    for (const c of active){
+      if (r.id===c.id){ html+='<td style="color:#3a3f5c">—</td>'; continue; }
+      const rec=(h[r.id]&&h[r.id][c.id])||{w:0,l:0};
+      const cls = rec.w>rec.l?'color:#2eff8f':(rec.l>rec.w?'color:#ff5d5d':'color:#7d84ad');
+      html+=`<td style="${cls}">${rec.w}–${rec.l}</td>`;
+    }
+    html+='</tr>';
+  }
+  tbl.innerHTML=html+'</tbody>';
+}
 
 loadModels().then(loadList);
 </script>
diff --git a/server.js b/server.js
index e6a76e0..5fca6cf 100644
--- a/server.js
+++ b/server.js
@@ -334,6 +334,8 @@ function buildLedger(categoryFilter) {
   const stats = {};
   for (const m of MODELS) stats[m.id] = { id: m.id, label: m.label, kind: m.kind, battles: 0, wins: 0, errors: 0, totalSeconds: 0, doneRuns: 0, spend: 0, elo: 1000, aiScoreSum: 0, aiScoreN: 0 };
   let aiAgree = 0, aiJudged = 0;
+  const h2h = {}; // h2h[winner][loser] = {w,l} from the winner's perspective (pairwise)
+  const bump = (a, b, win) => { (h2h[a] = h2h[a] || {}); (h2h[a][b] = h2h[a][b] || { w: 0, l: 0 }); h2h[a][b][win ? 'w' : 'l']++; };
   const pool = categoryFilter ? challenges.filter(c => (c.category || 'Custom') === categoryFilter) : challenges;
   // chronological pass (array is creation order) so ELO updates in battle order
   for (const c of pool) {
@@ -350,6 +352,7 @@ function buildLedger(categoryFilter) {
         const w = stats[c.winner], lo = stats[l.model];
         const ew = 1 / (1 + Math.pow(10, (lo.elo - w.elo) / 400));
         w.elo += 24 * (1 - ew); lo.elo += 24 * (0 - (1 - ew));
+        bump(c.winner, l.model, true); bump(l.model, c.winner, false);
       }
     }
     // AI-vs-human agreement on judged+crowned battles
@@ -364,7 +367,7 @@ function buildLedger(categoryFilter) {
     costPerWin: s.wins ? +(s.spend / s.wins).toFixed(4) : null,
     spend: +s.spend.toFixed(4),
   })).sort((a, b) => b.elo - a.elo || (b.winRate || 0) - (a.winRate || 0) || b.wins - a.wins);
-  return { models, aiAgreePct: aiJudged ? Math.round(100 * aiAgree / aiJudged) : null, aiJudged };
+  return { models, aiAgreePct: aiJudged ? Math.round(100 * aiAgree / aiJudged) : null, aiJudged, h2h };
 }
 
 // ---------- http ----------
diff --git a/yolo/IDEAS.md b/yolo/IDEAS.md
index d95247f..229d829 100644
--- a/yolo/IDEAS.md
+++ b/yolo/IDEAS.md
@@ -35,3 +35,4 @@ Answering the /contrarian critique ("one builder vote = theater", "toy demos not
 - [DONE] #A Per-category leaderboards — challenges auto-tagged Games/Real Work/Custom (inferCategory + backfill of existing); ledger accepts ?category, leaderboard has a category filter. Answers 'which model is best at REAL WORK vs games'. Games: Grok #1, 100% AI/human agreement.
 - [DONE] #B Export/share — GET /export/:id returns a self-contained standalone HTML of a battle (all artifacts inlined via sandboxed iframe srcdoc, scoreboard, winner/AI-pick), downloadable; verified it renders offline from file://. '⬇ export' button on each battle.
 - [DONE] #C Model profile page — click any leaderboard row → that model's full body of work (every artifact thumbnail across battles, won/AI-pick badges, per-battle stats) + aggregate stats. /api/models/:id/profile.
+- [DONE] #D Head-to-head matrix — pairwise win-loss record (row vs column) from crowned battles, green/red-coded, below the leaderboard. Grok dominant (3-0 vs Kimi/Hermes/Gemma).

← 8264d56 yolo idea C: model profile page — clickable leaderboard rows  ·  back to Model Arena  ·  yolo: regression note (documents the b-cat hidden-view click 0c31fff →