From 06807801d091cf029200286c9b8168ea8da57fe0 Mon Sep 17 00:00:00 2001 From: Jose Luis Date: Fri, 20 Mar 2026 18:07:14 +0100 Subject: [PATCH] feat: animated execution log with truth table + waveform viewer When the player executes wiring, the panel transitions to a full execution log showing: - Animated truth table with rows revealing one by one (flash effect) - Digital waveform viewer with square wave signals for each port - Scanning cursor animation during evaluation - Pulsing glow verdict (pass/fail) after all test cases complete - Color-coded columns: orange for inputs, blue for outputs Co-Authored-By: Claude Opus 4.6 --- js/world/wiringPanel.js | 592 ++++++++++++++++++++++++++++------------ 1 file changed, 412 insertions(+), 180 deletions(-) diff --git a/js/world/wiringPanel.js b/js/world/wiringPanel.js index 4be2541..204b936 100644 --- a/js/world/wiringPanel.js +++ b/js/world/wiringPanel.js @@ -1,6 +1,7 @@ // wiringPanel.js — Wiring panel for connecting gadget ports to module ports // The player wires a gadget's I/O to a module door's ports, then executes // to verify the circuit satisfies the module's logic. +// After execution, shows an animated truth table + digital waveform viewer. import { worldState, solvePuzzle, isPuzzleSolved, startDialog } from './worldState.js'; import { showNotification } from './inventory.js'; @@ -17,15 +18,14 @@ let selectedGadget = null; let result = null; // { message, color } or null let resultTimer = 0; +// Execution log state +let execLog = null; // { rows, portNames, passed, startTime, revealedRows } +const ROW_REVEAL_MS = 300; // ms between each row appearing + // ==================== Public API ==================== export function isWiringOpen() { return panelOpen; } -/** - * Open the wiring panel for a module interaction with a chosen gadget. - * @param {Object} inter - The module interaction from the map - * @param {Object} gad - The gadget from inventory - */ export function openWiringPanel(inter, gad) { panelOpen = true; moduleInter = inter; @@ -35,6 +35,7 @@ export function openWiringPanel(inter, gad) { selectedModule = null; selectedGadget = null; result = null; + execLog = null; worldState.mode = 'wiring'; console.log(`[wiring] opened for module "${inter.label}" with gadget "${gad.name}"`); } @@ -44,12 +45,12 @@ export function closeWiringPanel() { moduleInter = null; gadget = null; wires = []; + execLog = null; worldState.mode = 'world'; } // ==================== Gadget port helpers ==================== -/** Build a flat list of gadget ports (inputs first, then outputs) */ function getGadgetPorts() { if (!gadget) return []; const ports = []; @@ -69,46 +70,47 @@ function getGadgetPorts() { export function handleWiringInput(key) { if (!panelOpen) return false; + // If execution log is showing, ESC or Enter dismisses it + if (execLog) { + if (key === 'Escape' || key === 'Enter') { + if (execLog.passed) { + closeWiringPanel(); + showNotification('Module unlocked!', '⚡', '#00ff88'); + startDialog([ + `⚡ "${gadget.name}" passed the verification!`, + 'The module hums to life and the door unlocks.' + ], 'System'); + } else { + execLog = null; // dismiss log, back to wiring + } + } + return true; + } + const mPorts = moduleInter.ports || []; const gPorts = getGadgetPorts(); switch (key) { - case 'ArrowUp': - case 'w': - case 'W': + case 'ArrowUp': case 'w': case 'W': cursor.index = Math.max(0, cursor.index - 1); break; - case 'ArrowDown': - case 's': - case 'S': { + case 'ArrowDown': case 's': case 'S': { const max = cursor.side === 'module' ? mPorts.length - 1 : gPorts.length - 1; cursor.index = Math.min(Math.max(max, 0), cursor.index + 1); break; } - case 'ArrowLeft': - case 'ArrowRight': - case 'a': - case 'A': - case 'd': - case 'D': + case 'ArrowLeft': case 'ArrowRight': case 'a': case 'A': case 'd': case 'D': cursor.side = cursor.side === 'module' ? 'gadget' : 'module'; - { - const max = cursor.side === 'module' ? mPorts.length - 1 : gPorts.length - 1; - cursor.index = Math.min(cursor.index, Math.max(max, 0)); - } + { const max = cursor.side === 'module' ? mPorts.length - 1 : gPorts.length - 1; + cursor.index = Math.min(cursor.index, Math.max(max, 0)); } break; - case 'e': - case 'E': - case ' ': + case 'e': case 'E': case ' ': selectPort(); break; case 'Enter': executeWiring(); break; - case 'Backspace': - case 'Delete': - case 'x': - case 'X': + case 'Backspace': case 'Delete': case 'x': case 'X': removeWireAtCursor(); break; case 'Escape': @@ -144,7 +146,6 @@ function tryCreateWire() { return; } - // Validation: module-out → gadget-in, OR gadget-out → module-in const valid = (mPort.dir === 'out' && gPort.dir === 'in') || (mPort.dir === 'in' && gPort.dir === 'out'); @@ -157,13 +158,10 @@ function tryCreateWire() { return; } - // Remove any existing wire on either port wires = wires.filter(w => w.moduleIdx !== selectedModule && w.gadgetIdx !== selectedGadget); - wires.push({ moduleIdx: selectedModule, gadgetIdx: selectedGadget }); result = { message: `✓ Wired ${mPort.name} ↔ ${gPort.name}`, color: '#00e599' }; resultTimer = Date.now(); - selectedModule = null; selectedGadget = null; } @@ -180,28 +178,17 @@ function removeWireAtCursor() { // ==================== Circuit evaluation ==================== -/** - * Mini circuit evaluator — runs a cloned gadget circuit with given input values. - * Returns an object mapping OUTPUT gate IDs → computed values. - */ function evaluateGadgetCircuit(gates, connections, inputValues) { - // inputValues = { gateId: 0|1 } const evalGates = JSON.parse(JSON.stringify(gates)); - - // Set input gate values for (const g of evalGates) { if (g.type === 'INPUT' && inputValues[g.id] !== undefined) { g.value = inputValues[g.id]; } } - - // Fixed-point iteration (max 20 passes) for (let iter = 0; iter < 20; iter++) { let changed = false; for (const g of evalGates) { if (g.type === 'INPUT' || g.type === 'CLOCK') continue; - - // Gather inputs from connections const inCount = (g.type === 'NOT' || g.type === 'OUTPUT') ? 1 : 2; const ins = []; for (let p = 0; p < inCount; p++) { @@ -209,11 +196,8 @@ function evaluateGadgetCircuit(gates, connections, inputValues) { if (conn) { const src = evalGates.find(s => s.id === conn.from); ins.push(src ? (src.value || 0) : 0); - } else { - ins.push(0); - } + } else { ins.push(0); } } - let val = 0; switch (g.type) { case 'AND': val = (ins[0] && ins[1]) ? 1 : 0; break; @@ -225,43 +209,39 @@ function evaluateGadgetCircuit(gates, connections, inputValues) { case 'OUTPUT': val = ins[0] || 0; break; default: val = g.value || 0; } - if (val !== g.value) { g.value = val; changed = true; } } if (!changed) break; } - return evalGates; } /** - * Execute the wiring — build a test() function from the wires, - * then run the module's verify function to check if the gadget passes. + * Build the test function + run all combos to produce the truth table, + * then pass it to the verify function. Store results for the execution log. */ function executeWiring() { const mPorts = moduleInter.ports || []; const gPorts = getGadgetPorts(); - // Build the test function that the verify code will call. - // test(moduleOutputValues) → moduleInputValues - function test(moduleOutputs) { - // moduleOutputs = { A: 0, B: 1, ... } — values for module's "out" ports + // Identify module out ports (inputs to the gadget) and in ports (outputs from gadget) + const outPorts = mPorts.filter(p => p.dir === 'out'); + const inPorts = mPorts.filter(p => p.dir === 'in'); + const n = outPorts.length; - // Map module output values → gadget input gates via wires + // Collect truth table rows: for each input combo, run the circuit + const rows = []; + + function testOnce(moduleOutputs) { const inputValues = {}; for (const wire of wires) { const mp = mPorts[wire.moduleIdx]; const gp = gPorts[wire.gadgetIdx]; if (mp.dir === 'out' && gp.dir === 'in') { - // Module provides value → gadget receives it inputValues[gp.gateId] = moduleOutputs[mp.name] || 0; } } - - // Evaluate the gadget circuit const evaluated = evaluateGadgetCircuit(gadget.gates, gadget.connections, inputValues); - - // Read gadget output gates → map back to module input ports via wires const moduleInputs = {}; for (const wire of wires) { const mp = mPorts[wire.moduleIdx]; @@ -271,37 +251,66 @@ function executeWiring() { moduleInputs[mp.name] = outGate ? (outGate.value || 0) : 0; } } - return moduleInputs; } - // Run the verify function + // Generate all input combos and record results + for (let combo = 0; combo < (1 << n); combo++) { + const inputs = {}; + for (let i = 0; i < n; i++) { + inputs[outPorts[i].name] = (combo >> i) & 1; + } + const outputs = testOnce(inputs); + rows.push({ inputs: { ...inputs }, outputs: { ...outputs } }); + } + + // Build test function that the verify code calls (using our precomputed results) + function test(moduleOutputs) { + const inputValues = {}; + for (const wire of wires) { + const mp = mPorts[wire.moduleIdx]; + const gp = gPorts[wire.gadgetIdx]; + if (mp.dir === 'out' && gp.dir === 'in') { + inputValues[gp.gateId] = moduleOutputs[mp.name] || 0; + } + } + const evaluated = evaluateGadgetCircuit(gadget.gates, gadget.connections, inputValues); + const moduleInputs = {}; + for (const wire of wires) { + const mp = mPorts[wire.moduleIdx]; + const gp = gPorts[wire.gadgetIdx]; + if (mp.dir === 'in' && gp.dir === 'out') { + const outGate = evaluated.find(g => g.id === gp.gateId); + moduleInputs[mp.name] = outGate ? (outGate.value || 0) : 0; + } + } + return moduleInputs; + } + + // Run verify try { const verifyFn = new Function('return ' + moduleInter.verify)(); const passed = verifyFn(test); - if (passed) { - result = { message: '⚡ VERIFIED! Module unlocked!', color: '#00ff88' }; - resultTimer = Date.now(); + // Build port name lists for display + const portNames = { + inputs: outPorts.map(p => p.name), + outputs: inPorts.map(p => p.name) + }; - // Mark as solved if it has an ID - if (moduleInter.moduleId) { - solvePuzzle(moduleInter.moduleId); - } + // Set up execution log with animated reveal + execLog = { + rows, + portNames, + passed, + startTime: Date.now(), + totalRows: rows.length + }; - // Close after a brief delay and show dialog - setTimeout(() => { - closeWiringPanel(); - showNotification('Module unlocked!', '⚡', '#00ff88'); - startDialog([ - `⚡ "${gadget.name}" passed the verification!`, - 'The module hums to life and the door unlocks.' - ], 'System'); - }, 800); - } else { - result = { message: '✗ Verification failed — wrong logic', color: '#ff4444' }; - resultTimer = Date.now(); + if (passed && moduleInter.moduleId) { + solvePuzzle(moduleInter.moduleId); } + } catch (e) { result = { message: `Error: ${e.message}`, color: '#ff4444' }; resultTimer = Date.now(); @@ -319,9 +328,25 @@ const WIRE_COLOR = '#ffdd44'; const SELECTED_COLOR = '#ffffff'; const CURSOR_COLOR = '#00ffcc'; +// Waveform/log colors +const WAVE_HIGH = '#00e599'; +const WAVE_LOW = '#334'; +const WAVE_GRID = '#1a1d2e'; +const TABLE_HEADER_BG = '#141828'; +const TABLE_ROW_BG = '#0d1018'; +const TABLE_ROW_ALT = '#111520'; +const PASS_COLOR = '#00ff88'; +const FAIL_COLOR = '#ff4444'; + export function drawWiringPanel(ctx, canvasW, canvasH) { if (!panelOpen || !moduleInter || !gadget) return; + // If execution log is active, draw that instead + if (execLog) { + drawExecutionLog(ctx, canvasW, canvasH); + return; + } + const mPorts = moduleInter.ports || []; const gPorts = getGadgetPorts(); @@ -364,15 +389,11 @@ export function drawWiringPanel(ctx, canvasW, canvasH) { ctx.font = 'bold 13px "Segoe UI", system-ui, sans-serif'; ctx.textAlign = 'center'; - - // Module ports header ctx.fillStyle = '#aaa'; ctx.fillText('MODULE PORTS', leftX + 80, colY); - - // Gadget ports header ctx.fillText('GADGET PORTS', rightX - 80, colY); - // Draw column separator + // Column separator ctx.strokeStyle = '#333'; ctx.lineWidth = 1; ctx.beginPath(); @@ -380,142 +401,90 @@ export function drawWiringPanel(ctx, canvasW, canvasH) { ctx.lineTo(px + pw / 2, py + ph - 50); ctx.stroke(); - // Port positions for wire drawing const modulePortPositions = []; const gadgetPortPositions = []; // Draw module ports - ctx.font = '13px "Segoe UI", system-ui, sans-serif'; - ctx.textAlign = 'left'; for (let i = 0; i < mPorts.length; i++) { const port = mPorts[i]; const yPos = portStartY + i * portSpacing; const isOut = port.dir === 'out'; const dotX = leftX; - const dotRadius = 8; - - // Port dot position (for wires) const wireX = leftX + 170; modulePortPositions.push({ x: wireX, y: yPos + 6 }); - - // Highlight if cursor is here const isCursor = cursor.side === 'module' && cursor.index === i; const isSelected = selectedModule === i; - // Background highlight if (isCursor) { ctx.fillStyle = 'rgba(0, 255, 200, 0.1)'; ctx.fillRect(leftX - 10, yPos - 6, 190, portSpacing - 8); } - - // Port dot ctx.beginPath(); - ctx.arc(dotX, yPos + 6, dotRadius, 0, Math.PI * 2); + ctx.arc(dotX, yPos + 6, 8, 0, Math.PI * 2); ctx.fillStyle = isSelected ? SELECTED_COLOR : (isOut ? PORT_OUT_COLOR : PORT_IN_COLOR); ctx.fill(); - if (isCursor) { - ctx.strokeStyle = CURSOR_COLOR; - ctx.lineWidth = 2; - ctx.stroke(); - } + if (isCursor) { ctx.strokeStyle = CURSOR_COLOR; ctx.lineWidth = 2; ctx.stroke(); } - // Direction arrow inside dot - ctx.fillStyle = '#000'; - ctx.font = 'bold 10px monospace'; - ctx.textAlign = 'center'; - ctx.textBaseline = 'middle'; + ctx.fillStyle = '#000'; ctx.font = 'bold 10px monospace'; + ctx.textAlign = 'center'; ctx.textBaseline = 'middle'; ctx.fillText(isOut ? '→' : '←', dotX, yPos + 6); - // Port label - ctx.textAlign = 'left'; - ctx.textBaseline = 'top'; + ctx.textAlign = 'left'; ctx.textBaseline = 'top'; ctx.font = '13px "Segoe UI", system-ui, sans-serif'; ctx.fillStyle = isCursor ? CURSOR_COLOR : '#ddd'; - ctx.fillText(`${port.name}`, dotX + 14, yPos); - - // Direction tag + ctx.fillText(port.name, dotX + 14, yPos); ctx.font = '10px monospace'; ctx.fillStyle = isOut ? PORT_OUT_COLOR : PORT_IN_COLOR; ctx.fillText(isOut ? 'OUT' : 'IN', dotX + 14, yPos + 16); - - // Wire connected indicator - const wire = wires.find(w => w.moduleIdx === i); - if (wire) { - ctx.fillStyle = WIRE_COLOR; - ctx.fillText('⚡', dotX + 50, yPos + 16); + if (wires.find(w => w.moduleIdx === i)) { + ctx.fillStyle = WIRE_COLOR; ctx.fillText('⚡', dotX + 50, yPos + 16); } } // Draw gadget ports - ctx.textAlign = 'right'; for (let i = 0; i < gPorts.length; i++) { const port = gPorts[i]; const yPos = portStartY + i * portSpacing; const isOut = port.dir === 'out'; const dotX = rightX; - const dotRadius = 8; - const wireX = rightX - 170; gadgetPortPositions.push({ x: wireX, y: yPos + 6 }); - const isCursor = cursor.side === 'gadget' && cursor.index === i; const isSelected = selectedGadget === i; - // Background highlight if (isCursor) { ctx.fillStyle = 'rgba(0, 255, 200, 0.1)'; ctx.fillRect(rightX - 180, yPos - 6, 190, portSpacing - 8); } - - // Port dot ctx.beginPath(); - ctx.arc(dotX, yPos + 6, dotRadius, 0, Math.PI * 2); + ctx.arc(dotX, yPos + 6, 8, 0, Math.PI * 2); ctx.fillStyle = isSelected ? SELECTED_COLOR : (isOut ? PORT_OUT_COLOR : PORT_IN_COLOR); ctx.fill(); - if (isCursor) { - ctx.strokeStyle = CURSOR_COLOR; - ctx.lineWidth = 2; - ctx.stroke(); - } + if (isCursor) { ctx.strokeStyle = CURSOR_COLOR; ctx.lineWidth = 2; ctx.stroke(); } - // Direction arrow - ctx.fillStyle = '#000'; - ctx.font = 'bold 10px monospace'; - ctx.textAlign = 'center'; - ctx.textBaseline = 'middle'; + ctx.fillStyle = '#000'; ctx.font = 'bold 10px monospace'; + ctx.textAlign = 'center'; ctx.textBaseline = 'middle'; ctx.fillText(isOut ? '→' : '←', dotX, yPos + 6); - // Port label - ctx.textAlign = 'right'; - ctx.textBaseline = 'top'; + ctx.textAlign = 'right'; ctx.textBaseline = 'top'; ctx.font = '13px "Segoe UI", system-ui, sans-serif'; ctx.fillStyle = isCursor ? CURSOR_COLOR : '#ddd'; - ctx.fillText(`${port.name}`, dotX - 14, yPos); - - // Direction tag + ctx.fillText(port.name, dotX - 14, yPos); ctx.font = '10px monospace'; ctx.fillStyle = isOut ? PORT_OUT_COLOR : PORT_IN_COLOR; ctx.fillText(isOut ? 'OUT' : 'IN', dotX - 14, yPos + 16); - - // Wire connected indicator - const wire = wires.find(w => w.gadgetIdx === i); - if (wire) { - ctx.fillStyle = WIRE_COLOR; - ctx.fillText('⚡', dotX - 60, yPos + 16); + if (wires.find(w => w.gadgetIdx === i)) { + ctx.fillStyle = WIRE_COLOR; ctx.fillText('⚡', dotX - 60, yPos + 16); } } - // Draw wires - ctx.strokeStyle = WIRE_COLOR; - ctx.lineWidth = 2; - ctx.setLineDash([6, 4]); + // Draw wires (bezier) + ctx.strokeStyle = WIRE_COLOR; ctx.lineWidth = 2; ctx.setLineDash([6, 4]); for (const wire of wires) { const mp = modulePortPositions[wire.moduleIdx]; const gp = gadgetPortPositions[wire.gadgetIdx]; if (mp && gp) { - ctx.beginPath(); - ctx.moveTo(mp.x, mp.y); - // Bezier curve for nice look + ctx.beginPath(); ctx.moveTo(mp.x, mp.y); const midX = (mp.x + gp.x) / 2; ctx.bezierCurveTo(midX, mp.y, midX, gp.y, gp.x, gp.y); ctx.stroke(); @@ -523,41 +492,304 @@ export function drawWiringPanel(ctx, canvasW, canvasH) { } ctx.setLineDash([]); - // Pending wire (half-selected) + // Pending wire indicator if (selectedModule !== null && selectedGadget === null) { const mp = modulePortPositions[selectedModule]; - if (mp) { - ctx.beginPath(); - ctx.arc(mp.x, mp.y, 5, 0, Math.PI * 2); - ctx.fillStyle = SELECTED_COLOR; - ctx.fill(); - } + if (mp) { ctx.beginPath(); ctx.arc(mp.x, mp.y, 5, 0, Math.PI * 2); ctx.fillStyle = SELECTED_COLOR; ctx.fill(); } } if (selectedGadget !== null && selectedModule === null) { const gp = gadgetPortPositions[selectedGadget]; - if (gp) { - ctx.beginPath(); - ctx.arc(gp.x, gp.y, 5, 0, Math.PI * 2); - ctx.fillStyle = SELECTED_COLOR; - ctx.fill(); - } + if (gp) { ctx.beginPath(); ctx.arc(gp.x, gp.y, 5, 0, Math.PI * 2); ctx.fillStyle = SELECTED_COLOR; ctx.fill(); } } - // Result message + // Result message (transient) if (result && (Date.now() - resultTimer < 3000)) { ctx.fillStyle = result.color; ctx.font = 'bold 14px "Segoe UI", system-ui, sans-serif'; - ctx.textAlign = 'center'; - ctx.textBaseline = 'bottom'; + ctx.textAlign = 'center'; ctx.textBaseline = 'bottom'; ctx.fillText(result.message, px + pw / 2, py + ph - 35); } // Controls hint - ctx.fillStyle = '#555'; - ctx.font = '11px "Segoe UI", system-ui, sans-serif'; - ctx.textAlign = 'center'; - ctx.textBaseline = 'bottom'; - ctx.fillText('↑↓←→: Navigate | E: Select/Wire | X: Remove | Enter: Execute | ESC: Close', px + pw / 2, py + ph - 10); + ctx.fillStyle = '#555'; ctx.font = '11px "Segoe UI", system-ui, sans-serif'; + ctx.textAlign = 'center'; ctx.textBaseline = 'bottom'; + ctx.fillText('↑↓←→: Navigate | E: Wire | X: Remove | Enter: Execute | ESC: Close', px + pw / 2, py + ph - 10); +} + +// ==================== Execution Log ==================== + +function drawExecutionLog(ctx, canvasW, canvasH) { + const { rows, portNames, passed, startTime, totalRows } = execLog; + const elapsed = Date.now() - startTime; + const revealedRows = Math.min(totalRows, Math.floor(elapsed / ROW_REVEAL_MS)); + const allRevealed = revealedRows >= totalRows; + // Show verdict after all rows + a small delay + const showVerdict = allRevealed && (elapsed > totalRows * ROW_REVEAL_MS + 400); + + const allPortNames = [...portNames.inputs, ...portNames.outputs]; + const numInputs = portNames.inputs.length; + const numOutputs = portNames.outputs.length; + const numCols = allPortNames.length; + + // Panel sizing — full-width execution log + const pw = Math.min(720, canvasW - 40); + const tableH = 28 * (totalRows + 1) + 8; // header + rows + const waveH = 40 * numCols + 20; // waveform area + const verdictH = showVerdict ? 60 : 0; + const ph = Math.min(canvasH - 40, 70 + tableH + 20 + waveH + verdictH + 50); + const px = (canvasW - pw) / 2; + const py = (canvasH - ph) / 2; + + // Dim background + ctx.fillStyle = 'rgba(0, 0, 0, 0.8)'; + ctx.fillRect(0, 0, canvasW, canvasH); + + // Panel + ctx.fillStyle = PANEL_BG; + ctx.strokeStyle = passed && showVerdict ? PASS_COLOR : PANEL_BORDER; + ctx.lineWidth = 2; + roundRect(ctx, px, py, pw, ph, 8); + ctx.fill(); + ctx.stroke(); + + // Glow effect if passed + if (passed && showVerdict) { + const glowIntensity = 0.15 + 0.1 * Math.sin(Date.now() / 300); + ctx.shadowColor = PASS_COLOR; + ctx.shadowBlur = 20; + ctx.strokeStyle = PASS_COLOR; + ctx.lineWidth = 2; + roundRect(ctx, px, py, pw, ph, 8); + ctx.stroke(); + ctx.shadowBlur = 0; + } + + // Title + ctx.fillStyle = '#00e599'; + ctx.font = 'bold 15px "Segoe UI", system-ui, sans-serif'; + ctx.textAlign = 'center'; ctx.textBaseline = 'top'; + ctx.fillText('⚡ EXECUTION LOG', px + pw / 2, py + 12); + + // Subtitle — scanning animation + const dots = '.'.repeat((Math.floor(elapsed / 400) % 4)); + const subtitle = allRevealed ? `All ${totalRows} test cases evaluated` : `Running test cases${dots}`; + ctx.fillStyle = '#888'; ctx.font = '11px "Segoe UI", system-ui, sans-serif'; + ctx.fillText(subtitle, px + pw / 2, py + 32); + + let curY = py + 52; + + // ==================== Truth Table ==================== + const colW = Math.min(70, (pw - 60) / numCols); + const tableX = px + (pw - colW * numCols) / 2; + const rowH = 28; + + // Header background + ctx.fillStyle = TABLE_HEADER_BG; + roundRect(ctx, tableX - 8, curY, colW * numCols + 16, rowH, 4); + ctx.fill(); + + // Header labels + ctx.font = 'bold 12px "Cascadia Code", "Fira Code", monospace'; + ctx.textBaseline = 'middle'; + for (let c = 0; c < numCols; c++) { + const cx = tableX + c * colW + colW / 2; + const isInput = c < numInputs; + ctx.fillStyle = isInput ? PORT_OUT_COLOR : PORT_IN_COLOR; + ctx.textAlign = 'center'; + ctx.fillText(allPortNames[c], cx, curY + rowH / 2); + } + + // Separator line under header + curY += rowH; + ctx.strokeStyle = '#333'; ctx.lineWidth = 1; + ctx.beginPath(); + ctx.moveTo(tableX - 4, curY); + ctx.lineTo(tableX + colW * numCols + 4, curY); + ctx.stroke(); + + // Vertical separator between inputs and outputs + if (numInputs > 0 && numOutputs > 0) { + const sepX = tableX + numInputs * colW; + ctx.strokeStyle = '#444'; ctx.lineWidth = 1; + ctx.setLineDash([3, 3]); + ctx.beginPath(); + ctx.moveTo(sepX, py + 52); + ctx.lineTo(sepX, curY + rowH * Math.min(revealedRows, totalRows) + 4); + ctx.stroke(); + ctx.setLineDash([]); + } + + // Data rows (animated reveal) + for (let r = 0; r < revealedRows; r++) { + const row = rows[r]; + const rowY = curY + r * rowH; + const rowAge = elapsed - r * ROW_REVEAL_MS; + const alpha = Math.min(1, rowAge / 200); // fade in + + // Row background + ctx.fillStyle = r % 2 === 0 ? TABLE_ROW_BG : TABLE_ROW_ALT; + ctx.globalAlpha = alpha; + ctx.fillRect(tableX - 8, rowY, colW * numCols + 16, rowH); + + // Flash effect on new row + if (rowAge < 250) { + const flash = 1 - rowAge / 250; + ctx.fillStyle = `rgba(0, 229, 153, ${flash * 0.15})`; + ctx.fillRect(tableX - 8, rowY, colW * numCols + 16, rowH); + } + + // Values + ctx.font = '13px "Cascadia Code", "Fira Code", monospace'; + ctx.textBaseline = 'middle'; + for (let c = 0; c < numCols; c++) { + const cx = tableX + c * colW + colW / 2; + const isInput = c < numInputs; + const portName = allPortNames[c]; + const val = isInput ? row.inputs[portName] : row.outputs[portName]; + + // Color: high = bright, low = dim + ctx.fillStyle = val ? WAVE_HIGH : '#555'; + ctx.textAlign = 'center'; + ctx.fillText(val !== undefined ? String(val) : '?', cx, rowY + rowH / 2); + } + ctx.globalAlpha = 1; + } + + curY += totalRows * rowH + 12; + + // ==================== Waveform Viewer ==================== + if (revealedRows > 0) { + const waveX = tableX; + const waveW = colW * numCols + 16; + const sigH = 28; + const sigSpacing = 38; + const labelW = 36; + const sigAreaW = waveW - labelW - 8; + + // Section header + ctx.fillStyle = '#666'; ctx.font = 'bold 10px "Segoe UI", system-ui, sans-serif'; + ctx.textAlign = 'left'; ctx.textBaseline = 'top'; + ctx.fillText('SIGNAL VIEW', waveX, curY); + curY += 16; + + // Background + ctx.fillStyle = '#0a0c14'; + roundRect(ctx, waveX - 8, curY - 4, waveW, sigSpacing * numCols + 12, 4); + ctx.fill(); + + // Grid lines (vertical, one per test case) + ctx.strokeStyle = WAVE_GRID; ctx.lineWidth = 1; + const stepW = sigAreaW / Math.max(totalRows, 1); + for (let i = 0; i <= totalRows; i++) { + const gx = waveX + labelW + i * stepW; + ctx.beginPath(); + ctx.moveTo(gx, curY); + ctx.lineTo(gx, curY + sigSpacing * numCols); + ctx.stroke(); + } + + // Draw each signal + for (let s = 0; s < numCols; s++) { + const sigY = curY + s * sigSpacing; + const isInput = s < numInputs; + const portName = allPortNames[s]; + const color = isInput ? PORT_OUT_COLOR : PORT_IN_COLOR; + + // Label + ctx.fillStyle = color; + ctx.font = 'bold 11px "Cascadia Code", "Fira Code", monospace'; + ctx.textAlign = 'left'; ctx.textBaseline = 'middle'; + ctx.fillText(portName, waveX, sigY + sigH / 2); + + // Draw square wave + ctx.strokeStyle = color; + ctx.lineWidth = 2; + ctx.beginPath(); + + let prevVal = null; + for (let r = 0; r < revealedRows; r++) { + const row = rows[r]; + const val = isInput ? (row.inputs[portName] || 0) : (row.outputs[portName] || 0); + const x1 = waveX + labelW + r * stepW; + const x2 = x1 + stepW; + const yHigh = sigY + 3; + const yLow = sigY + sigH - 3; + const yVal = val ? yHigh : yLow; + + if (r === 0) { + ctx.moveTo(x1, yVal); + } else if (prevVal !== val) { + // Transition — vertical edge + ctx.lineTo(x1, yVal); + } + ctx.lineTo(x2, yVal); + + // Fill area under high signals + if (val) { + ctx.save(); + ctx.fillStyle = color.replace(')', ', 0.08)').replace('rgb', 'rgba'); + if (color.startsWith('#')) { + const r2 = parseInt(color.slice(1,3), 16); + const g = parseInt(color.slice(3,5), 16); + const b = parseInt(color.slice(5,7), 16); + ctx.fillStyle = `rgba(${r2},${g},${b},0.1)`; + } + ctx.fillRect(x1, yHigh, stepW, yLow - yHigh); + ctx.restore(); + } + + prevVal = val; + } + ctx.stroke(); + + // Scanline animation (moving cursor) + if (!allRevealed) { + const scanX = waveX + labelW + revealedRows * stepW; + ctx.strokeStyle = '#00e599'; + ctx.lineWidth = 1; + ctx.setLineDash([2, 2]); + ctx.beginPath(); + ctx.moveTo(scanX, sigY); + ctx.lineTo(scanX, sigY + sigH); + ctx.stroke(); + ctx.setLineDash([]); + } + } + + curY += sigSpacing * numCols + 16; + } + + // ==================== Verdict ==================== + if (showVerdict) { + const verdictY = curY; + const pulse = 0.8 + 0.2 * Math.sin(Date.now() / 200); + + if (passed) { + ctx.fillStyle = PASS_COLOR; + ctx.font = `bold ${18 * pulse}px "Segoe UI", system-ui, sans-serif`; + ctx.textAlign = 'center'; ctx.textBaseline = 'top'; + ctx.fillText('⚡ VERIFICATION PASSED ⚡', px + pw / 2, verdictY); + + ctx.fillStyle = '#aaa'; ctx.font = '12px "Segoe UI", system-ui, sans-serif'; + ctx.fillText('Press Enter to unlock', px + pw / 2, verdictY + 26); + } else { + ctx.fillStyle = FAIL_COLOR; + ctx.font = 'bold 18px "Segoe UI", system-ui, sans-serif'; + ctx.textAlign = 'center'; ctx.textBaseline = 'top'; + ctx.fillText('✗ VERIFICATION FAILED', px + pw / 2, verdictY); + + ctx.fillStyle = '#888'; ctx.font = '12px "Segoe UI", system-ui, sans-serif'; + ctx.fillText('Press Esc to go back and adjust your wiring', px + pw / 2, verdictY + 26); + } + } + + // Footer hint + ctx.fillStyle = '#444'; ctx.font = '10px "Segoe UI", system-ui, sans-serif'; + ctx.textAlign = 'center'; ctx.textBaseline = 'bottom'; + const hint = showVerdict + ? (passed ? 'Enter: Continue | Esc: Close' : 'Esc: Back to wiring') + : 'Evaluating...'; + ctx.fillText(hint, px + pw / 2, py + ph - 8); } // ==================== Helpers ====================