// Level system — puzzle definitions, verification, and progression import { state } from './state.js'; import { evaluate } from './gates.js'; // Level definitions export const LEVELS = [ { id: 'buffer', category: 'Logic Basics', title: 'Buffer', description: 'Connect input to output directly. Pass the value through.', availableGates: ['INPUT', 'OUTPUT'], testCases: [ { inputs: { 0: 0 }, outputs: { 0: 0 } }, { inputs: { 0: 1 }, outputs: { 0: 1 } } ], hints: ['Use only one INPUT and one OUTPUT gate.'], difficulty: 1 }, { id: 'not_gate', category: 'Logic Basics', title: 'NOT Gate', description: 'Build a NOT gate using only NAND gates. Invert the input signal.', availableGates: ['INPUT', 'OUTPUT', 'NAND'], testCases: [ { inputs: { 0: 0 }, outputs: { 0: 1 } }, { inputs: { 0: 1 }, outputs: { 0: 0 } } ], hints: ['Connect both inputs of a NAND gate to the same signal.'], difficulty: 1 }, { id: 'and_gate', category: 'Logic Basics', title: 'AND Gate', description: 'Build an AND gate using only NAND gates. AND = NAND with inverted output.', availableGates: ['INPUT', 'OUTPUT', 'NAND'], testCases: [ { inputs: { 0: 0, 1: 0 }, outputs: { 0: 0 } }, { inputs: { 0: 0, 1: 1 }, outputs: { 0: 0 } }, { inputs: { 0: 1, 1: 0 }, outputs: { 0: 0 } }, { inputs: { 0: 1, 1: 1 }, outputs: { 0: 1 } } ], hints: ['NOT(NAND) = AND. You need 2 NAND gates.', 'Use a NAND gate as a NOT on the output.'], difficulty: 2 }, { id: 'or_gate', category: 'Logic Basics', title: 'OR Gate', description: 'Build an OR gate using NAND gates. OR = NAND(NOT A, NOT B).', availableGates: ['INPUT', 'OUTPUT', 'NAND'], testCases: [ { inputs: { 0: 0, 1: 0 }, outputs: { 0: 0 } }, { inputs: { 0: 0, 1: 1 }, outputs: { 0: 1 } }, { inputs: { 0: 1, 1: 0 }, outputs: { 0: 1 } }, { inputs: { 0: 1, 1: 1 }, outputs: { 0: 1 } } ], hints: ['Invert both inputs first, then NAND them.', 'You need 3 NAND gates total.'], difficulty: 2 }, { id: 'xor_gate', category: 'Logic Basics', title: 'XOR Gate', description: 'Build an XOR gate using NAND gates. True when inputs differ.', availableGates: ['INPUT', 'OUTPUT', 'NAND'], testCases: [ { inputs: { 0: 0, 1: 0 }, outputs: { 0: 0 } }, { inputs: { 0: 0, 1: 1 }, outputs: { 0: 1 } }, { inputs: { 0: 1, 1: 0 }, outputs: { 0: 1 } }, { inputs: { 0: 1, 1: 1 }, outputs: { 0: 0 } } ], hints: ['XOR = (A NAND B) NAND (NOT(A NAND NOT B) NAND NOT(NOT A NAND B))', 'This requires 4-5 NAND gates.'], difficulty: 3 }, { id: 'xnor_gate', category: 'Logic Basics', title: 'XNOR Gate', description: 'Build an XNOR gate. True when both inputs are equal.', availableGates: ['INPUT', 'OUTPUT', 'NAND'], testCases: [ { inputs: { 0: 0, 1: 0 }, outputs: { 0: 1 } }, { inputs: { 0: 0, 1: 1 }, outputs: { 0: 0 } }, { inputs: { 0: 1, 1: 0 }, outputs: { 0: 0 } }, { inputs: { 0: 1, 1: 1 }, outputs: { 0: 1 } } ], hints: ['XNOR is the inverse of XOR.', 'Build XOR first, then invert the output with another NAND-as-NOT.'], difficulty: 3 }, // ============ Combinational Logic ============ { id: 'mux_2to1', category: 'Combinational Logic', title: '2:1 Multiplexer', description: 'Build a MUX. When SEL=0, output A. When SEL=1, output B. Three inputs: A (0), B (1), SEL (2).', availableGates: ['INPUT', 'OUTPUT', 'AND', 'OR', 'NOT'], testCases: [ { inputs: { 0: 0, 1: 0, 2: 0 }, outputs: { 0: 0 } }, // SEL=0 → A=0 { inputs: { 0: 1, 1: 0, 2: 0 }, outputs: { 0: 1 } }, // SEL=0 → A=1 { inputs: { 0: 0, 1: 1, 2: 0 }, outputs: { 0: 0 } }, // SEL=0 → A=0 { inputs: { 0: 1, 1: 1, 2: 0 }, outputs: { 0: 1 } }, // SEL=0 → A=1 { inputs: { 0: 0, 1: 0, 2: 1 }, outputs: { 0: 0 } }, // SEL=1 → B=0 { inputs: { 0: 0, 1: 1, 2: 1 }, outputs: { 0: 1 } }, // SEL=1 → B=1 { inputs: { 0: 1, 1: 0, 2: 1 }, outputs: { 0: 0 } }, // SEL=1 → B=0 { inputs: { 0: 1, 1: 1, 2: 1 }, outputs: { 0: 1 } } // SEL=1 → B=1 ], hints: ['MUX = (A AND NOT SEL) OR (B AND SEL).', 'You need 1 NOT, 2 AND, and 1 OR gate.'], difficulty: 2 }, { id: 'demux_1to2', category: 'Combinational Logic', title: '1:2 Demultiplexer', description: 'Route input to one of two outputs based on SEL. Inputs: DATA (0), SEL (1). Outputs: Q0, Q1.', availableGates: ['INPUT', 'OUTPUT', 'AND', 'NOT'], testCases: [ { inputs: { 0: 0, 1: 0 }, outputs: { 0: 0, 1: 0 } }, // DATA=0, SEL=0 → Q0=0, Q1=0 { inputs: { 0: 1, 1: 0 }, outputs: { 0: 1, 1: 0 } }, // DATA=1, SEL=0 → Q0=1, Q1=0 { inputs: { 0: 0, 1: 1 }, outputs: { 0: 0, 1: 0 } }, // DATA=0, SEL=1 → Q0=0, Q1=0 { inputs: { 0: 1, 1: 1 }, outputs: { 0: 0, 1: 1 } } // DATA=1, SEL=1 → Q0=0, Q1=1 ], hints: ['Q0 = DATA AND NOT SEL. Q1 = DATA AND SEL.', 'You need 1 NOT and 2 AND gates.'], difficulty: 2 }, { id: 'and3', category: 'Combinational Logic', title: '3-input AND', description: 'Build a 3-input AND gate. Output is 1 only when all three inputs are 1.', availableGates: ['INPUT', 'OUTPUT', 'AND'], testCases: [ { inputs: { 0: 0, 1: 0, 2: 0 }, outputs: { 0: 0 } }, { inputs: { 0: 1, 1: 0, 2: 0 }, outputs: { 0: 0 } }, { inputs: { 0: 1, 1: 1, 2: 0 }, outputs: { 0: 0 } }, { inputs: { 0: 0, 1: 1, 2: 1 }, outputs: { 0: 0 } }, { inputs: { 0: 1, 1: 1, 2: 1 }, outputs: { 0: 1 } } ], hints: ['Chain two AND gates: (A AND B) AND C.'], difficulty: 1 }, { id: 'majority', category: 'Combinational Logic', title: 'Majority Gate', description: 'Output 1 if at least two of the three inputs are 1.', availableGates: ['INPUT', 'OUTPUT', 'AND', 'OR'], testCases: [ { inputs: { 0: 0, 1: 0, 2: 0 }, outputs: { 0: 0 } }, { inputs: { 0: 1, 1: 0, 2: 0 }, outputs: { 0: 0 } }, { inputs: { 0: 0, 1: 1, 2: 0 }, outputs: { 0: 0 } }, { inputs: { 0: 0, 1: 0, 2: 1 }, outputs: { 0: 0 } }, { inputs: { 0: 1, 1: 1, 2: 0 }, outputs: { 0: 1 } }, { inputs: { 0: 1, 1: 0, 2: 1 }, outputs: { 0: 1 } }, { inputs: { 0: 0, 1: 1, 2: 1 }, outputs: { 0: 1 } }, { inputs: { 0: 1, 1: 1, 2: 1 }, outputs: { 0: 1 } } ], hints: ['Majority = (A AND B) OR (A AND C) OR (B AND C).', 'You need 3 AND gates and 2 OR gates.'], difficulty: 2 }, { id: 'parity', category: 'Combinational Logic', title: 'Even Parity', description: 'Output 1 if an even number of inputs are 1 (0 counts as even). Three inputs.', availableGates: ['INPUT', 'OUTPUT', 'XOR', 'NOT'], testCases: [ { inputs: { 0: 0, 1: 0, 2: 0 }, outputs: { 0: 1 } }, // 0 ones = even { inputs: { 0: 1, 1: 0, 2: 0 }, outputs: { 0: 0 } }, // 1 one = odd { inputs: { 0: 0, 1: 1, 2: 0 }, outputs: { 0: 0 } }, { inputs: { 0: 0, 1: 0, 2: 1 }, outputs: { 0: 0 } }, { inputs: { 0: 1, 1: 1, 2: 0 }, outputs: { 0: 1 } }, // 2 ones = even { inputs: { 0: 1, 1: 0, 2: 1 }, outputs: { 0: 1 } }, { inputs: { 0: 0, 1: 1, 2: 1 }, outputs: { 0: 1 } }, { inputs: { 0: 1, 1: 1, 2: 1 }, outputs: { 0: 0 } } // 3 ones = odd ], hints: ['XOR gives odd parity. Invert it for even parity.', 'Chain: NOT(A XOR B XOR C).'], difficulty: 2 }, // ============ Arithmetic ============ { id: 'half_adder', category: 'Arithmetic', title: 'Half Adder', description: 'Build a half adder. Two outputs: Sum (A XOR B) and Carry (A AND B).', availableGates: ['INPUT', 'OUTPUT', 'NAND'], testCases: [ { inputs: { 0: 0, 1: 0 }, outputs: { 0: 0, 1: 0 } }, { inputs: { 0: 0, 1: 1 }, outputs: { 0: 1, 1: 0 } }, { inputs: { 0: 1, 1: 0 }, outputs: { 0: 1, 1: 0 } }, { inputs: { 0: 1, 1: 1 }, outputs: { 0: 0, 1: 1 } } ], hints: ['Two independent functions: Sum=XOR, Carry=AND.', 'You need 2 OUTPUT gates.'], difficulty: 3 }, { id: 'full_adder', category: 'Arithmetic', title: 'Full Adder', description: 'Build a full adder with carry-in. Outputs: Sum and Carry-out.', availableGates: ['INPUT', 'OUTPUT', 'NAND'], testCases: [ { inputs: { 0: 0, 1: 0, 2: 0 }, outputs: { 0: 0, 1: 0 } }, { inputs: { 0: 0, 1: 0, 2: 1 }, outputs: { 0: 1, 1: 0 } }, { inputs: { 0: 0, 1: 1, 2: 0 }, outputs: { 0: 1, 1: 0 } }, { inputs: { 0: 0, 1: 1, 2: 1 }, outputs: { 0: 0, 1: 1 } }, { inputs: { 0: 1, 1: 0, 2: 0 }, outputs: { 0: 1, 1: 0 } }, { inputs: { 0: 1, 1: 0, 2: 1 }, outputs: { 0: 0, 1: 1 } }, { inputs: { 0: 1, 1: 1, 2: 0 }, outputs: { 0: 0, 1: 1 } }, { inputs: { 0: 1, 1: 1, 2: 1 }, outputs: { 0: 1, 1: 1 } } ], hints: ['A full adder = two half adders + OR for the carry.', 'Sum = A XOR B XOR Cin. Cout = (A AND B) OR (Cin AND (A XOR B)).'], difficulty: 4 }, { id: 'half_subtractor', category: 'Arithmetic', title: 'Half Subtractor', description: 'Build A minus B. Outputs: Difference (A XOR B) and Borrow (NOT A AND B).', availableGates: ['INPUT', 'OUTPUT', 'AND', 'XOR', 'NOT'], testCases: [ { inputs: { 0: 0, 1: 0 }, outputs: { 0: 0, 1: 0 } }, // 0-0: diff=0, borrow=0 { inputs: { 0: 0, 1: 1 }, outputs: { 0: 1, 1: 1 } }, // 0-1: diff=1, borrow=1 { inputs: { 0: 1, 1: 0 }, outputs: { 0: 1, 1: 0 } }, // 1-0: diff=1, borrow=0 { inputs: { 0: 1, 1: 1 }, outputs: { 0: 0, 1: 0 } } // 1-1: diff=0, borrow=0 ], hints: ['Difference = A XOR B (same as addition!).', 'Borrow = (NOT A) AND B.'], difficulty: 2 }, { id: 'comparator', category: 'Arithmetic', title: '1-bit Comparator', description: 'Compare A and B. Three outputs: A>B, A=B, A0: GT=1, EQ=0, LT=0 { inputs: { 0: 1, 1: 1 }, outputs: { 0: 0, 1: 1, 2: 0 } } // 1=1: GT=0, EQ=1, LT=0 ], hints: ['A>B = A AND NOT B.', 'A=B = NOT(A XOR B) = XNOR.', 'A l.id === id); } /** * Check if a level is unlocked */ export function isLevelUnlocked(levelId) { return progress.unlockedLevels.includes(levelId); } /** * Check if a level is completed */ export function isLevelCompleted(levelId) { return progress.completedLevels.includes(levelId); } /** * Verify if the current circuit passes all test cases for a level */ export function verifyLevel(levelId) { const level = getLevel(levelId); if (!level) return { passed: false, results: [], message: 'Level not found' }; const results = []; let allPassed = true; for (const testCase of level.testCases) { const result = runTestCase(level, testCase); results.push(result); if (!result.passed) allPassed = false; } return { passed: allPassed, results, message: allPassed ? 'All tests passed! ✓' : `${results.filter(r => r.passed).length}/${results.length} tests passed` }; } /** * Run a single test case */ function runTestCase(level, testCase) { // Find INPUT gates const inputGates = state.gates.filter(g => g.type === 'INPUT'); // Find OUTPUT gates const outputGates = state.gates.filter(g => g.type === 'OUTPUT'); // Check if we have the right number of inputs and outputs const inputIds = Object.keys(testCase.inputs).map(Number).sort((a, b) => a - b); const outputIds = Object.keys(testCase.outputs).map(Number).sort((a, b) => a - b); if (inputGates.length !== inputIds.length || outputGates.length !== outputIds.length) { return { passed: false, inputs: testCase.inputs, expectedOutputs: testCase.outputs, actualOutputs: {}, error: `Expected ${inputIds.length} inputs and ${outputIds.length} outputs, got ${inputGates.length} and ${outputGates.length}` }; } // Set input values for (let i = 0; i < inputIds.length; i++) { if (inputGates[i]) { inputGates[i].value = testCase.inputs[i]; } } // Evaluate circuit state.gates.forEach(g => { if (g.type !== 'INPUT' && g.type !== 'CLOCK') g.value = 0; }); state.gates.forEach(g => evaluate(g)); // Check outputs const expected = testCase.outputs; let passed = true; const actualOutputs = {}; for (let i = 0; i < outputIds.length; i++) { const actualValue = outputGates[i]?.value || 0; actualOutputs[i] = actualValue; if (actualValue !== expected[i]) { passed = false; } } return { passed, inputs: testCase.inputs, expectedOutputs: expected, actualOutputs }; } /** * Complete a level and unlock the next one */ export function completeLevel(levelId) { const levelIndex = LEVELS.findIndex(l => l.id === levelId); if (levelIndex >= 0 && !progress.completedLevels.includes(levelId)) { progress.completedLevels.push(levelId); } // Unlock next level if (levelIndex < LEVELS.length - 1) { const nextId = LEVELS[levelIndex + 1].id; if (!progress.unlockedLevels.includes(nextId)) { progress.unlockedLevels.push(nextId); } } saveProgress(); } /** * Register a custom component (saved circuit) */ export function registerComponent(name, gateTypes, connections) { progress.customComponents[name] = { name, gateTypes, connections }; saveProgress(); } /** * Get a custom component */ export function getComponent(name) { return progress.customComponents[name]; } /** * Get all custom components */ export function getAllComponents() { return progress.customComponents; } /** * Reset progress (dev/testing) */ export function resetProgress() { progress.unlockedLevels = ['buffer']; progress.completedLevels = []; progress.currentLevel = null; progress.customComponents = {}; saveProgress(); }