[object Object]

← back to Agent Cabinet

Game Mode: /game route — 6 dept HQs + 134 cards w/ avatars, stats, synergies

eca07f17b1f5b7331798b3bdd00eef225afbb2ca · 2026-05-06 12:29:16 -0700 · Steve

Files touched

Diff

commit eca07f17b1f5b7331798b3bdd00eef225afbb2ca
Author: Steve <steve@designerwallcoverings.com>
Date:   Wed May 6 12:29:16 2026 -0700

    Game Mode: /game route — 6 dept HQs + 134 cards w/ avatars, stats, synergies
---
 server.js | 342 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
 1 file changed, 342 insertions(+)

diff --git a/server.js b/server.js
index bcce6bd..b5def9c 100644
--- a/server.js
+++ b/server.js
@@ -135,6 +135,337 @@ ${mm}
 </body></html>`;
 }
 
+// ---------- /game route — gamified roster with avatars, stats, synergies ----------
+
+// Deterministic emoji avatar pool. Pick by name hash so each director has stable avatar.
+const ANIMAL_AVATARS = ['🦊','🦅','🐺','🐉','🦁','🐯','🐼','🦉','🦄','🦋','🐙','🐢','🦏','🦎','🐝','🦂','🦇','🐠','🦜','🐧','🦩','🦚','🦔','🐿️','🦦','🐍','🦖','🐊','🐎','🐕','🦌','🦃','🦘','🐫','🦬','🦭','🐬','🦣','🪿','🐈‍⬛'];
+const TOOL_AVATARS  = ['⚙️','🔧','🛠️','🔨','⚒️','🪛','🪚','📏','📐','🧰','🧲','🔬','🔭','🧪','🧫','💎','🪞','🪪','🗝️','🪝','🧭','📡','💾','💿','📀','💽','🖥️','🖨️','⌨️','🖱️','🛡️','⚔️','🏹','🗡️','🪙','💰','💼','🎯','🎲','🎮'];
+
+function avatarFor(kind, name) {
+  // Deterministic FNV-1a hash
+  let h = 2166136261;
+  for (let i = 0; i < name.length; i++) { h ^= name.charCodeAt(i); h = (h * 16777619) >>> 0; }
+  const pool = kind === 'subagent' ? ANIMAL_AVATARS : TOOL_AVATARS;
+  return pool[h % pool.length];
+}
+
+// Stats derived from name length + kind for variety (nothing scientific — game flavor).
+function statsFor(kind, name) {
+  let h = 5381;
+  for (let i = 0; i < name.length; i++) h = ((h << 5) + h + name.charCodeAt(i)) >>> 0;
+  const r = (n, mod) => ((h >>> n) % mod) + 1;
+  return {
+    POWER: kind === 'subagent' ? 60 + r(0, 40) : 40 + r(2, 50),  // subagents punch harder
+    SPEED: kind === 'skill'    ? 60 + r(4, 40) : 40 + r(6, 50),  // skills are faster
+    DEPTH: 30 + r(8, 70),
+    REACH: 20 + r(10, 80),
+  };
+}
+
+// Specialty: one-liner derived from name (heuristic mapping)
+const SPECIALTIES = {
+  // operations
+  'secrets': 'API token routing master',
+  'domain-setup': 'Bare domain → live HTTPS site',
+  'cloudflare-manager': 'Edge-network conjurer',
+  'open-firewalls': 'Port-opening locksmith',
+  'process-hawk': 'Watches every pm2 like a hawk',
+  'pm2-crash-watcher': 'Last-hour crash reporter',
+  'deployment-engineer': 'CI/CD pipeline weaver',
+  'devops-engineer': 'Cloud ops generalist',
+  'domain-name-agent': 'WHOIS + DNS keeper',
+  'onboard-domain-agent': 'New-domain summoner',
+  'secrets-manager': 'Master vault keeper',
+  'analytics-agent': 'GA4 fleet commander',
+  // dw-commerce
+  'dw-site-build': 'Universal DW storefront blueprint',
+  'dw-shopify-theme-optimizer': 'Liquid speed alchemist',
+  'dw-marketing-copy': 'Conversion-tuned wordsmith',
+  'dw-seo-optimizer': 'Meta + structured-data tuner',
+  'dw-purchasing-vendor': 'Vendor email diplomat',
+  'dw-sku-analyst': 'SKU dedup detective',
+  'dw-legal-compliance': 'Settlement-clause guardian',
+  'room-setting-generator': 'Photoreal room conjurer',
+  'commercial-room-setting-generator': 'Hospitality-render specialist',
+  // research-content
+  'exa-agent': 'Grounded web search oracle',
+  'website-analysis': 'Synthesis-grade audit polymath',
+  'four-horsemen': 'Figma + Paper + Canva + 21st orchestrator',
+  'reviewed-demo-video': 'Pre+post 4-horsemen review video',
+  'session-debrief': 'VC-pitch debrief MP4 maker',
+  'clone-voice': 'Voice-clone enrollment master',
+  'mockups': 'N hand-built homepage variants',
+  'site-audit': 'Synthesis report + 90-day roadmap',
+  'peer-survey': 'Side-by-side competitor screenshots',
+  'competitors': 'Differentiation-vector hunter',
+  'tools-pack': 'Browser-side widget pack builder',
+  'extension': 'MV3 Chrome extension scaffolder',
+  'info-hub': 'Authoritative-source curator',
+  'debate-team-fast': '4-LLM consensus engine',
+  // engineering
+  'code-reviewer': 'PR audit specialist',
+  'security-auditor': 'OWASP & auth-flow guardian',
+  'performance-engineer': 'Profiler & bottleneck hunter',
+  'database-optimizer': 'N+1 slayer',
+  'backend-architect': 'API & microservice designer',
+  'frontend-developer': 'React UI craftsman',
+  'ai-engineer': 'LLM + RAG integration',
+  // compliance
+  'comms-compliance': 'CAN-SPAM / TCPA / CCPA gatekeeper',
+  'best-practices-reviewer': 'MEMORY.md pre-flight checker',
+  'best-practices': 'Standing-rules reviewer',
+};
+
+// Synergy graph: which directors pair naturally (for "Synergy" display).
+// Curated minimal map — keeps the game readable.
+const SYNERGIES = {
+  'dw-sku-analyst': ['shopify-vendor-bulk-updater', 'fullproduct'],
+  'shopify-vendor-bulk-updater': ['dw-sku-analyst', 'enrich-ai-tags'],
+  'secrets': ['domain-setup', 'cloudflare-manager'],
+  'domain-setup': ['cloudflare-manager', 'open-firewalls', 'secrets'],
+  'reviewed-demo-video': ['app-demo-video', 'four-horsemen', 'clone-voice'],
+  'session-debrief': ['app-demo-video', 'clone-voice', 'use-voice'],
+  'comms-compliance': ['best-practices-reviewer', 'dw-legal-compliance'],
+  'best-practices-reviewer': ['comms-compliance'],
+  'lawyer-build-agent': ['comms-compliance', 'la-research-agent'],
+  'doctor-agent': ['comms-compliance'],
+  'la-research-agent': ['lawyer-build-agent', 'exa-agent'],
+  'exa-agent': ['search-specialist', 'technical-researcher', 'la-research-agent'],
+  'website-analysis': ['site-audit', 'peer-survey', 'competitors', 'mockups', 'tools-pack', 'info-hub'],
+  'site-audit': ['website-analysis'],
+  'peer-survey': ['website-analysis', 'competitors'],
+  'mockups': ['four-horsemen', 'stampede', 'website-analysis'],
+  'four-horsemen': ['mockups', 'stampede', 'reviewed-demo-video'],
+  'stampede': ['four-horsemen', 'mockups'],
+  'pm2-crash-watcher': ['process-hawk', 'devops-engineer'],
+  'process-hawk': ['pm2-crash-watcher'],
+  'analytics-agent': ['dw-seo-optimizer', 'website-analysis'],
+  'code-reviewer': ['architect-review', 'security-auditor'],
+  'security-auditor': ['code-reviewer', 'best-practices-reviewer'],
+  'ai-engineer': ['prompt-engineer', 'mcp-expert', 'claude-api'],
+  'prompt-engineer': ['ai-engineer', 'prompt-cache-optimization'],
+  'debate-team-fast': ['claude-codex'],
+  'enrich-ai-tags': ['shopify-vendor-bulk-updater'],
+};
+
+const VP_THEME = {
+  'vp-operations':         { color: '#1d4ed8', boss: '🛡️', bossName: 'Operator', tagline: 'KEEPS THE LIGHTS ON' },
+  'vp-dw-commerce':        { color: '#16a34a', boss: '💰', bossName: 'Quartermaster', tagline: 'CATALOG, REVENUE, RETAIL' },
+  'vp-directories':        { color: '#ea580c', boss: '📜', bossName: 'Cartographer', tagline: 'VERTICAL DIRECTORIES' },
+  'vp-engineering':        { color: '#7c3aed', boss: '⚒️', bossName: 'Architect', tagline: 'CODE QUALITY & SHIP' },
+  'vp-research-content':   { color: '#0891b2', boss: '🔭', bossName: 'Scout', tagline: 'KNOW + CREATE' },
+  'vp-compliance-policy':  { color: '#dc2626', boss: '⚖️', bossName: 'Sentinel', tagline: 'GUARDS THE GATES' },
+};
+
+function renderGame(cabinet) {
+  // Need full director list (including vendor scrapers).
+  // Re-extract from cabinet.yaml directly — handles the multi-line array case.
+  const text = fs.readFileSync(CABINET, 'utf8');
+  const allLines = text.split('\n');
+  const fullCabinet = [];
+  let cur = null;
+  for (let i = 0; i < allLines.length; i++) {
+    const line = allLines[i];
+    if (line.match(/^  - vp:\s*(.+)$/)) {
+      cur = { vp: line.split(':')[1].trim(), domain: '', directors: [] };
+      fullCabinet.push(cur);
+    } else if (cur) {
+      const dm = line.match(/^    domain:\s*(.+)$/);
+      if (dm) cur.domain = dm[1].trim();
+      let m;
+      if ((m = line.match(/^\s+- skill:\s*([\w./-]+)/)))    cur.directors.push({ kind: 'skill',    name: m[1] });
+      else if ((m = line.match(/^\s+- subagent:\s*([\w./-]+)/))) cur.directors.push({ kind: 'subagent', name: m[1] });
+      else if (line.includes('alphabetical_skill_set:')) {
+        let buf = line, j = i;
+        while (!buf.includes(']') && j < allLines.length - 1) { j++; buf += ' ' + allLines[j]; }
+        const inner = (buf.match(/\[([^\]]+)\]/) || ['',''])[1];
+        for (const raw of inner.split(',')) {
+          const nm = raw.replace(/["']/g, '').trim();
+          if (/^[a-zA-Z][\w-]+$/.test(nm)) cur.directors.push({ kind: 'skill', name: nm });
+        }
+      }
+    }
+  }
+  // Dedup
+  for (const v of fullCabinet) {
+    const seen = new Set();
+    v.directors = v.directors.filter(d => { const k = d.kind+':'+d.name; if (seen.has(k)) return false; seen.add(k); return true; });
+  }
+  const totalDirectors = fullCabinet.reduce((a, v) => a + v.directors.length, 0);
+
+  function card(d) {
+    const av = avatarFor(d.kind, d.name);
+    const s = statsFor(d.kind, d.name);
+    const spec = SPECIALTIES[d.name] || (d.kind === 'subagent' ? 'Domain specialist' : 'Tool · skill bundle');
+    const synergies = (SYNERGIES[d.name] || []).slice(0, 3);
+    const sigil = d.kind === 'subagent' ? '@' : '/';
+    return `
+    <div class="card ${d.kind}" data-name="${d.name}" data-kind="${d.kind}">
+      <div class="avatar">${av}</div>
+      <div class="kind-tag">${d.kind === 'subagent' ? 'SUBAGENT' : 'SKILL'}</div>
+      <div class="name">${sigil}${d.name}</div>
+      <div class="spec">${spec}</div>
+      <div class="stats">
+        <div><span>POW</span><b>${s.POWER}</b></div>
+        <div><span>SPD</span><b>${s.SPEED}</b></div>
+        <div><span>DEP</span><b>${s.DEPTH}</b></div>
+        <div><span>RCH</span><b>${s.REACH}</b></div>
+      </div>
+      ${synergies.length ? `<div class="synergy">↔ ${synergies.map(x => `<span class="syn-pill" data-target="${x}">${x}</span>`).join(' ')}</div>` : ''}
+    </div>`;
+  }
+
+  const departmentBlocks = fullCabinet.map(v => {
+    const t = VP_THEME[v.vp] || { color: '#666', boss: '👑', bossName: 'Boss', tagline: '' };
+    const cards = v.directors.map(card).join('');
+    return `
+    <section class="dept" style="--theme:${t.color}">
+      <header class="dept-head">
+        <div class="boss-avatar">${t.boss}</div>
+        <div>
+          <div class="boss-title">${t.bossName.toUpperCase()}</div>
+          <h2>${v.vp}</h2>
+          <div class="boss-tag">${t.tagline} · ${v.directors.length} on team</div>
+        </div>
+      </header>
+      <div class="grid">${cards}</div>
+    </section>`;
+  }).join('');
+
+  return `<!doctype html>
+<html><head>
+<meta charset="utf-8">
+<title>The Cabinet — Steve's Agent Roster (Game Mode)</title>
+<style>
+  :root { --ink:#000; --paper:#fafaf6; }
+  * { box-sizing: border-box; }
+  body { font-family: -apple-system, BlinkMacSystemFont, sans-serif; margin:0; background: var(--paper); color: var(--ink); }
+  header.global {
+    padding: 36px 40px 24px; border-bottom: 4px solid #000; background:
+      linear-gradient(135deg, #fef3c7 0%, #fde68a 50%, #fcd34d 100%);
+  }
+  h1 { font-size: 38px; margin: 0 0 6px; letter-spacing: -.03em; font-weight: 900; font-family: 'Archivo Black', -apple-system, sans-serif; }
+  .stats-line { font-size: 13px; color: #444; margin-top: 8px; }
+  .stats-line b { color: #000; }
+  main { padding: 24px 32px 60px; max-width: 1840px; margin: 0 auto; }
+
+  .dept {
+    margin: 32px 0;
+    background: #fff;
+    border: 4px solid #000;
+    box-shadow: 8px 8px 0 #000;
+  }
+  .dept-head {
+    display: flex; align-items: center; gap: 22px;
+    padding: 22px 28px;
+    background: var(--theme);
+    color: #fff;
+    border-bottom: 4px solid #000;
+  }
+  .boss-avatar { font-size: 56px; line-height: 1; }
+  .dept-head h2 { margin: 2px 0; font-size: 26px; font-weight: 900; letter-spacing: -.02em; }
+  .boss-title { font-size: 11px; letter-spacing: .15em; font-weight: 800; opacity: .85; }
+  .boss-tag { font-size: 12px; opacity: .85; margin-top: 2px; letter-spacing: .04em; font-weight: 600; }
+  .grid {
+    display: grid; gap: 14px; padding: 22px;
+    grid-template-columns: repeat(auto-fill, minmax(218px, 1fr));
+  }
+  .card {
+    background: #fff;
+    border: 2px solid #000;
+    border-radius: 0;
+    padding: 14px;
+    position: relative;
+    transition: transform .15s ease, box-shadow .15s ease;
+    cursor: pointer;
+  }
+  .card:hover {
+    transform: translate(-3px, -3px);
+    box-shadow: 4px 4px 0 var(--theme);
+    z-index: 10;
+  }
+  .card.subagent { background: #ede9fe; }
+  .card.skill { background: #dcfce7; }
+  .card .avatar { font-size: 32px; line-height: 1; margin-bottom: 6px; }
+  .card .kind-tag {
+    position: absolute; top: 8px; right: 8px;
+    font-size: 9px; font-weight: 800; letter-spacing: .12em;
+    background: #000; color: #fff;
+    padding: 2px 6px;
+  }
+  .card .name { font-weight: 800; font-size: 13px; word-break: break-word; margin-bottom: 4px; }
+  .card .spec { font-size: 11px; color: #444; min-height: 30px; line-height: 1.35; margin-bottom: 8px; font-style: italic; }
+  .card .stats { display: grid; grid-template-columns: repeat(4, 1fr); gap: 4px; margin: 8px 0; }
+  .card .stats div { background: rgba(0,0,0,.05); padding: 4px 0; text-align: center; font-size: 9px; border: 1px solid rgba(0,0,0,.08); }
+  .card .stats span { display: block; font-weight: 700; opacity: .65; letter-spacing: .08em; font-size: 8px; }
+  .card .stats b { display: block; font-size: 13px; color: #000; }
+  .card .synergy {
+    margin-top: 6px; font-size: 10px; line-height: 1.5;
+    border-top: 1px dashed rgba(0,0,0,.2); padding-top: 6px;
+  }
+  .syn-pill {
+    background: #000; color: #fff; padding: 1px 6px;
+    margin-right: 2px; cursor: pointer;
+    font-family: ui-monospace, monospace;
+  }
+  .syn-pill:hover { background: var(--theme); }
+
+  /* Highlight when synergy clicked */
+  .card.glow {
+    box-shadow: 0 0 0 4px gold, 6px 6px 0 #000;
+    animation: pulse 1s ease infinite;
+  }
+  @keyframes pulse { 0%,100% { box-shadow: 0 0 0 4px gold, 6px 6px 0 #000; } 50% { box-shadow: 0 0 0 8px gold, 6px 6px 0 #000; } }
+
+  .legend {
+    display: inline-flex; gap: 10px; margin-top: 14px; flex-wrap: wrap;
+  }
+  .legend span { background: #000; color: #fff; padding: 4px 10px; font-size: 11px; font-weight: 700; }
+  footer { text-align: center; color: #666; padding: 30px; font-size: 11px; }
+</style>
+</head><body>
+<header class="global">
+  <h1>🏛 The Cabinet — Game Mode</h1>
+  <div class="stats-line"><b>Steve</b> (President) · <b>${fullCabinet.length}</b> Departments · <b>${totalDirectors}</b> directors on the roster</div>
+  <div class="legend">
+    <span style="background:#1d4ed8">🛡️ Operations</span>
+    <span style="background:#16a34a">💰 DW Commerce</span>
+    <span style="background:#ea580c">📜 Directories</span>
+    <span style="background:#7c3aed">⚒️ Engineering</span>
+    <span style="background:#0891b2">🔭 Research/Content</span>
+    <span style="background:#dc2626">⚖️ Compliance</span>
+  </div>
+</header>
+<main>${departmentBlocks}</main>
+<footer>
+  Click any synergy pill to highlight the partner agent. Cards auto-glow gold for 2 seconds when summoned.<br>
+  Edit roster: <code>~/Projects/agent-cabinet/cabinet.yaml</code> · Last rendered ${new Date().toLocaleString()}
+</footer>
+<script>
+  document.querySelectorAll('.syn-pill').forEach(p => {
+    p.addEventListener('click', e => {
+      e.stopPropagation();
+      const target = p.dataset.target;
+      const card = document.querySelector('.card[data-name="' + target + '"]');
+      if (!card) { alert('Partner ' + target + ' not on the roster — add via agent-architect.'); return; }
+      card.scrollIntoView({ behavior: 'smooth', block: 'center' });
+      card.classList.add('glow');
+      setTimeout(() => card.classList.remove('glow'), 2200);
+    });
+  });
+  document.querySelectorAll('.card').forEach(c => {
+    c.addEventListener('click', () => {
+      const n = c.dataset.name, k = c.dataset.kind;
+      const trigger = k === 'subagent' ? '@' + n : '/' + n;
+      navigator.clipboard?.writeText(trigger);
+      c.classList.add('glow');
+      setTimeout(() => c.classList.remove('glow'), 1200);
+    });
+  });
+</script>
+</body></html>`;
+}
+
 const server = http.createServer((req, res) => {
   if (req.url === '/health') { res.writeHead(200); res.end('ok'); return; }
   if (req.url === '/cabinet.yaml') {
@@ -142,6 +473,17 @@ const server = http.createServer((req, res) => {
     res.end(fs.readFileSync(CABINET, 'utf8'));
     return;
   }
+  if (req.url === '/game' || req.url === '/game/') {
+    try {
+      const html = renderGame();
+      res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
+      res.end(html);
+    } catch (e) {
+      res.writeHead(500);
+      res.end(`Game render error: ${e.message}\n${e.stack}`);
+    }
+    return;
+  }
   try {
     const cab = parseYaml(fs.readFileSync(CABINET, 'utf8'));
     const html = render(cab);

← 157848f Viewer: switch from Mermaid mindmap to flowchart TD with cla  ·  back to Agent Cabinet  ·  tighten .gitignore: add missing standing-rule patterns (dist 00a0d8a →